moonscratch 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/dist/chunk-DQk6qfdC.mjs +18 -0
  2. package/dist/index.d.mts +1173 -0
  3. package/dist/index.mjs +27135 -0
  4. package/package.json +6 -1
  5. package/.agents/skills/moonbit-agent-guide/LICENSE +0 -202
  6. package/.agents/skills/moonbit-agent-guide/SKILL.mbt.md +0 -1126
  7. package/.agents/skills/moonbit-agent-guide/SKILL.md +0 -1126
  8. package/.agents/skills/moonbit-agent-guide/ide.md +0 -116
  9. package/.agents/skills/moonbit-agent-guide/references/advanced-moonbit-build.md +0 -106
  10. package/.agents/skills/moonbit-agent-guide/references/moonbit-language-fundamentals.mbt.md +0 -422
  11. package/.agents/skills/moonbit-agent-guide/references/moonbit-language-fundamentals.md +0 -422
  12. package/.agents/skills/moonbit-practice/SKILL.md +0 -258
  13. package/.agents/skills/moonbit-practice/assets/ci.yaml +0 -25
  14. package/.agents/skills/moonbit-practice/reference/agents.md +0 -1469
  15. package/.agents/skills/moonbit-practice/reference/configuration.md +0 -228
  16. package/.agents/skills/moonbit-practice/reference/ffi.md +0 -229
  17. package/.agents/skills/moonbit-practice/reference/ide.md +0 -189
  18. package/.agents/skills/moonbit-practice/reference/performance.md +0 -217
  19. package/.agents/skills/moonbit-practice/reference/refactor.md +0 -154
  20. package/.agents/skills/moonbit-practice/reference/stdlib.md +0 -351
  21. package/.agents/skills/moonbit-practice/reference/testing.md +0 -228
  22. package/.agents/skills/moonbit-refactoring/LICENSE +0 -21
  23. package/.agents/skills/moonbit-refactoring/SKILL.md +0 -323
  24. package/.githooks/README.md +0 -23
  25. package/.githooks/pre-commit +0 -3
  26. package/.github/workflows/copilot-setup-steps.yml +0 -40
  27. package/.turbo/turbo-typecheck.log +0 -2
  28. package/AGENTS.md +0 -91
  29. package/PLAN.md +0 -64
  30. package/TODO.md +0 -120
  31. package/benchmarks/calc.bench.ts +0 -144
  32. package/benchmarks/draw.bench.ts +0 -215
  33. package/benchmarks/load.bench.ts +0 -28
  34. package/benchmarks/render.bench.ts +0 -53
  35. package/benchmarks/run.bench.ts +0 -8
  36. package/benchmarks/types.d.ts +0 -15
  37. package/docs/scratch-vm-specs/eventloop.md +0 -103
  38. package/docs/scratch-vm-specs/moonscratch-time-separation.md +0 -50
  39. package/index.html +0 -91
  40. package/js/AGENTS.md +0 -5
  41. package/js/a.ts +0 -52
  42. package/js/assets/AGENTS.md +0 -5
  43. package/js/assets/base64.test.ts +0 -14
  44. package/js/assets/base64.ts +0 -21
  45. package/js/assets/build-asset.test.ts +0 -26
  46. package/js/assets/build-asset.ts +0 -28
  47. package/js/assets/create.test.ts +0 -142
  48. package/js/assets/create.ts +0 -122
  49. package/js/assets/index.test.ts +0 -15
  50. package/js/assets/index.ts +0 -2
  51. package/js/assets/types.ts +0 -26
  52. package/js/assets/validation.test.ts +0 -34
  53. package/js/assets/validation.ts +0 -25
  54. package/js/assets.test.ts +0 -14
  55. package/js/assets.ts +0 -1
  56. package/js/index.test.ts +0 -26
  57. package/js/index.ts +0 -3
  58. package/js/render/index.test.ts +0 -65
  59. package/js/render/index.ts +0 -13
  60. package/js/render/sharp.ts +0 -87
  61. package/js/render/svg.ts +0 -68
  62. package/js/render/types.ts +0 -35
  63. package/js/render/utils.ts +0 -108
  64. package/js/render/webgl.ts +0 -274
  65. package/js/sharp-optional.d.ts +0 -16
  66. package/js/test/helpers.ts +0 -116
  67. package/js/test/hikkaku-sample.test.ts +0 -37
  68. package/js/test/rubik-components.input-motion.test.ts +0 -60
  69. package/js/test/rubik-components.lists.test.ts +0 -49
  70. package/js/test/rubik-components.operators.test.ts +0 -104
  71. package/js/test/rubik-components.pen.test.ts +0 -112
  72. package/js/test/rubik-components.procedures-loops.test.ts +0 -72
  73. package/js/test/rubik-components.variables-branches.test.ts +0 -57
  74. package/js/test/rubik-components.visibility-entry.test.ts +0 -31
  75. package/js/test/test-projects.ts +0 -598
  76. package/js/test/variable.ts +0 -200
  77. package/js/test/warp.test.ts +0 -59
  78. package/js/vm/AGENTS.md +0 -6
  79. package/js/vm/README.md +0 -183
  80. package/js/vm/bindings.test.ts +0 -13
  81. package/js/vm/bindings.ts +0 -5
  82. package/js/vm/compare-operators.test.ts +0 -145
  83. package/js/vm/constants.test.ts +0 -11
  84. package/js/vm/constants.ts +0 -4
  85. package/js/vm/effect-guards.test.ts +0 -68
  86. package/js/vm/effect-guards.ts +0 -44
  87. package/js/vm/factory.test.ts +0 -486
  88. package/js/vm/factory.ts +0 -615
  89. package/js/vm/headless-vm.test.ts +0 -131
  90. package/js/vm/headless-vm.ts +0 -342
  91. package/js/vm/index.test.ts +0 -28
  92. package/js/vm/index.ts +0 -5
  93. package/js/vm/internal-types.ts +0 -32
  94. package/js/vm/json.test.ts +0 -40
  95. package/js/vm/json.ts +0 -273
  96. package/js/vm/normalize.test.ts +0 -48
  97. package/js/vm/normalize.ts +0 -65
  98. package/js/vm/options.test.ts +0 -30
  99. package/js/vm/options.ts +0 -55
  100. package/js/vm/pen-transparency.test.ts +0 -115
  101. package/js/vm/program-wasm.ts +0 -322
  102. package/js/vm/scheduler-render.test.ts +0 -401
  103. package/js/vm/scratch-assets.test.ts +0 -136
  104. package/js/vm/scratch-assets.ts +0 -202
  105. package/js/vm/types.ts +0 -358
  106. package/js/vm/value-guards.test.ts +0 -25
  107. package/js/vm/value-guards.ts +0 -18
  108. package/moon.mod.json +0 -10
  109. package/scripts/preinstall.ts +0 -4
  110. package/src/AGENTS.md +0 -6
  111. package/src/api.mbt +0 -161
  112. package/src/api_aot_commands.mbt +0 -184
  113. package/src/api_effects_json.mbt +0 -72
  114. package/src/api_options.mbt +0 -60
  115. package/src/api_program_wasm.mbt +0 -1647
  116. package/src/api_program_wat.mbt +0 -2206
  117. package/src/api_snapshot_json.mbt +0 -44
  118. package/src/cmd/AGENTS.md +0 -5
  119. package/src/cmd/main/AGENTS.md +0 -5
  120. package/src/cmd/main/main.mbt +0 -29
  121. package/src/cmd/main/moon.pkg +0 -7
  122. package/src/cmd/main/pkg.generated.mbti +0 -13
  123. package/src/json_helpers.mbt +0 -176
  124. package/src/moon.pkg +0 -65
  125. package/src/moonscratch.mbt +0 -3
  126. package/src/moonscratch_wbtest.mbt +0 -40
  127. package/src/parser_sb3.mbt +0 -890
  128. package/src/pkg.generated.mbti +0 -479
  129. package/src/runtime_eval.mbt +0 -2844
  130. package/src/runtime_exec.mbt +0 -3850
  131. package/src/runtime_render.mbt +0 -2550
  132. package/src/runtime_state.mbt +0 -870
  133. package/src/test/AGENTS.md +0 -3
  134. package/src/test/projects/AGENTS.md +0 -6
  135. package/src/test/projects/moon.pkg +0 -4
  136. package/src/test/projects/moonscratch_compat_test.mbt +0 -642
  137. package/src/test/projects/moonscratch_core_test.mbt +0 -1332
  138. package/src/test/projects/moonscratch_runtime_test.mbt +0 -1087
  139. package/src/test/projects/pkg.generated.mbti +0 -13
  140. package/src/test/projects/test_support.mbt +0 -35
  141. package/src/types_effects.mbt +0 -20
  142. package/src/types_error.mbt +0 -4
  143. package/src/types_options.mbt +0 -31
  144. package/src/types_runtime_structs.mbt +0 -254
  145. package/src/types_vm.mbt +0 -109
  146. package/tsconfig.json +0 -29
  147. package/viewer/index.ts +0 -399
  148. package/viewer/vite.d.ts +0 -1
  149. package/viewer/worker.ts +0 -161
  150. package/vite.config.ts +0 -11
package/js/vm/json.ts DELETED
@@ -1,273 +0,0 @@
1
- import type { MoonResult } from './internal-types.ts'
2
- import type { JsonValue, ProjectJson } from './types.ts'
3
-
4
- const isRecord = (input: unknown): input is Record<string, unknown> => {
5
- return input !== null && typeof input === 'object' && !Array.isArray(input)
6
- }
7
-
8
- const hasAnyKey = (input: Record<string, unknown>): boolean => {
9
- for (const _key in input) {
10
- return true
11
- }
12
- return false
13
- }
14
-
15
- const compactCostumes = (input: unknown): unknown[] => {
16
- if (!Array.isArray(input)) {
17
- return []
18
- }
19
- const costumes: unknown[] = []
20
- for (const raw of input) {
21
- if (!isRecord(raw)) {
22
- costumes.push({})
23
- continue
24
- }
25
-
26
- const costume: Record<string, unknown> = {}
27
- if (typeof raw.name === 'string') {
28
- costume.name = raw.name
29
- }
30
- if (typeof raw.assetId === 'string' && raw.assetId.length > 0) {
31
- costume.assetId = raw.assetId
32
- }
33
- if (typeof raw.md5ext === 'string' && raw.md5ext.length > 0) {
34
- costume.md5ext = raw.md5ext
35
- }
36
- if (
37
- typeof raw.bitmapResolution === 'number' &&
38
- Number.isFinite(raw.bitmapResolution) &&
39
- raw.bitmapResolution !== 1
40
- ) {
41
- costume.bitmapResolution = raw.bitmapResolution
42
- }
43
- if (
44
- typeof raw.rotationCenterX === 'number' &&
45
- Number.isFinite(raw.rotationCenterX) &&
46
- raw.rotationCenterX !== 0
47
- ) {
48
- costume.rotationCenterX = raw.rotationCenterX
49
- }
50
- if (
51
- typeof raw.rotationCenterY === 'number' &&
52
- Number.isFinite(raw.rotationCenterY) &&
53
- raw.rotationCenterY !== 0
54
- ) {
55
- costume.rotationCenterY = raw.rotationCenterY
56
- }
57
- if (
58
- typeof raw.width === 'number' &&
59
- Number.isFinite(raw.width) &&
60
- raw.width > 0
61
- ) {
62
- costume.width = raw.width
63
- }
64
- if (
65
- typeof raw.height === 'number' &&
66
- Number.isFinite(raw.height) &&
67
- raw.height > 0
68
- ) {
69
- costume.height = raw.height
70
- }
71
- costumes.push(costume)
72
- }
73
- return costumes
74
- }
75
-
76
- const compactBlocks = (input: unknown): Record<string, unknown> => {
77
- if (!isRecord(input)) {
78
- return {}
79
- }
80
- const blocks: Record<string, unknown> = {}
81
- for (const id in input) {
82
- const raw = input[id]
83
- if (!isRecord(raw)) {
84
- continue
85
- }
86
-
87
- const block: Record<string, unknown> = {}
88
- if (typeof raw.opcode === 'string') {
89
- block.opcode = raw.opcode
90
- }
91
- if (typeof raw.next === 'string') {
92
- block.next = raw.next
93
- }
94
- if (typeof raw.parent === 'string') {
95
- block.parent = raw.parent
96
- }
97
- if (isRecord(raw.inputs) && hasAnyKey(raw.inputs)) {
98
- block.inputs = raw.inputs
99
- }
100
- if (isRecord(raw.fields) && hasAnyKey(raw.fields)) {
101
- block.fields = raw.fields
102
- }
103
- if (isRecord(raw.mutation) && hasAnyKey(raw.mutation)) {
104
- block.mutation = raw.mutation
105
- }
106
- if (raw.topLevel === true) {
107
- block.topLevel = true
108
- }
109
- blocks[id] = block
110
- }
111
- return blocks
112
- }
113
-
114
- const compactTargets = (input: unknown): unknown[] => {
115
- if (!Array.isArray(input)) {
116
- return []
117
- }
118
- const targets: unknown[] = []
119
- for (const raw of input) {
120
- if (!isRecord(raw)) {
121
- continue
122
- }
123
-
124
- const target: Record<string, unknown> = {}
125
- if (typeof raw.name === 'string') {
126
- target.name = raw.name
127
- }
128
- if (raw.isStage === true) {
129
- target.isStage = true
130
- }
131
- if (typeof raw.x === 'number' && Number.isFinite(raw.x) && raw.x !== 0) {
132
- target.x = raw.x
133
- }
134
- if (typeof raw.y === 'number' && Number.isFinite(raw.y) && raw.y !== 0) {
135
- target.y = raw.y
136
- }
137
- if (
138
- typeof raw.direction === 'number' &&
139
- Number.isFinite(raw.direction) &&
140
- raw.direction !== 90
141
- ) {
142
- target.direction = raw.direction
143
- }
144
- if (
145
- typeof raw.size === 'number' &&
146
- Number.isFinite(raw.size) &&
147
- raw.size !== 100
148
- ) {
149
- target.size = raw.size
150
- }
151
- if (
152
- typeof raw.volume === 'number' &&
153
- Number.isFinite(raw.volume) &&
154
- raw.volume !== 100
155
- ) {
156
- target.volume = raw.volume
157
- }
158
- if (
159
- typeof raw.musicInstrument === 'number' &&
160
- Number.isFinite(raw.musicInstrument) &&
161
- raw.musicInstrument !== 1
162
- ) {
163
- target.musicInstrument = raw.musicInstrument
164
- }
165
- if (
166
- typeof raw.textToSpeechVoice === 'string' &&
167
- raw.textToSpeechVoice.length > 0 &&
168
- raw.textToSpeechVoice !== 'ALTO'
169
- ) {
170
- target.textToSpeechVoice = raw.textToSpeechVoice
171
- }
172
- if (raw.visible === false) {
173
- target.visible = false
174
- }
175
- if (
176
- typeof raw.currentCostume === 'number' &&
177
- Number.isFinite(raw.currentCostume) &&
178
- raw.currentCostume !== 0
179
- ) {
180
- target.currentCostume = raw.currentCostume
181
- }
182
- target.variables = isRecord(raw.variables) ? raw.variables : {}
183
- target.lists = isRecord(raw.lists) ? raw.lists : {}
184
- target.blocks = compactBlocks(raw.blocks)
185
- target.costumes = compactCostumes(raw.costumes)
186
- targets.push(target)
187
- }
188
- return targets
189
- }
190
-
191
- export const toProjectJsonString = (input: string | ProjectJson): string => {
192
- if (typeof input === 'string') {
193
- if (input.trim().length === 0) {
194
- throw new Error('projectJson must be a non-empty JSON string or object')
195
- }
196
- return input
197
- }
198
-
199
- try {
200
- if (!isRecord(input) || !Array.isArray(input.targets)) {
201
- return JSON.stringify(input)
202
- }
203
- return JSON.stringify({
204
- targets: compactTargets(input.targets),
205
- })
206
- } catch (error) {
207
- const message = error instanceof Error ? error.message : String(error)
208
- throw new Error(`projectJson could not be serialized as JSON: ${message}`)
209
- }
210
- }
211
-
212
- export const toJsonString = (
213
- input: string | ProjectJson,
214
- inputName: string,
215
- requireNonEmpty: boolean,
216
- ): string => {
217
- if (typeof input === 'string') {
218
- if (requireNonEmpty && input.trim().length === 0) {
219
- throw new Error(`${inputName} must be a non-empty JSON string or object`)
220
- }
221
- return input
222
- }
223
-
224
- try {
225
- return JSON.stringify(input)
226
- } catch (error) {
227
- const message = error instanceof Error ? error.message : String(error)
228
- throw new Error(`${inputName} could not be serialized as JSON: ${message}`)
229
- }
230
- }
231
-
232
- export const toOptionalJsonString = (
233
- input: string | JsonValue | undefined,
234
- inputName: string,
235
- ): string | undefined => {
236
- if (input === undefined) {
237
- return undefined
238
- }
239
- return toJsonString(input, inputName, false)
240
- }
241
-
242
- const formatVmError = (error: unknown): string => {
243
- if (error && typeof error === 'object') {
244
- const candidate = (error as { _0?: unknown })._0
245
- if (typeof candidate === 'string' && candidate.trim().length > 0) {
246
- return candidate
247
- }
248
- }
249
- try {
250
- return JSON.stringify(error)
251
- } catch {
252
- return String(error)
253
- }
254
- }
255
-
256
- export const unwrapResult = <T>(
257
- result: MoonResult<T, unknown>,
258
- context: string,
259
- ): T => {
260
- if (result.$tag === 1) {
261
- return result._0
262
- }
263
- throw new Error(`${context}: ${formatVmError(result._0)}`)
264
- }
265
-
266
- export const parseJson = <T>(text: string, context: string): T => {
267
- try {
268
- return JSON.parse(text) as T
269
- } catch (error) {
270
- const message = error instanceof Error ? error.message : String(error)
271
- throw new Error(`${context}: failed to parse JSON (${message})`)
272
- }
273
- }
@@ -1,48 +0,0 @@
1
- import { describe, expect, test } from 'vite-plus/test'
2
-
3
- import {
4
- cloneTranslateCache,
5
- normalizeLanguage,
6
- normalizeMaxFrames,
7
- normalizeNowMs,
8
- toFrameReport,
9
- } from './normalize.ts'
10
-
11
- describe('moonscratch/js/vm/normalize.ts', () => {
12
- test('normalizes language code', () => {
13
- expect(normalizeLanguage(' JA ')).toBe('ja')
14
- expect(normalizeLanguage('')).toBe('en')
15
- })
16
-
17
- test('clones and normalizes translate cache', () => {
18
- const cache = cloneTranslateCache({ JA: { hello: 'こんにちは' } })
19
- expect(cache).toEqual({ ja: { hello: 'こんにちは' } })
20
- })
21
-
22
- test('normalizes frame inputs', () => {
23
- expect(normalizeNowMs(16.9)).toBe(16)
24
- expect(normalizeMaxFrames(10.7)).toBe(10)
25
- })
26
-
27
- test('maps raw frame report fields', () => {
28
- expect(
29
- toFrameReport({
30
- active_threads: 2,
31
- tick_count: 4,
32
- op_count: 100,
33
- emitted_effects: 3,
34
- stop_reason: 'timeout',
35
- should_render: true,
36
- is_in_warp: false,
37
- }),
38
- ).toEqual({
39
- activeThreads: 2,
40
- ticks: 4,
41
- ops: 100,
42
- emittedEffects: 3,
43
- stopReason: 'timeout',
44
- shouldRender: true,
45
- isInWarp: false,
46
- })
47
- })
48
- })
@@ -1,65 +0,0 @@
1
- import { DEFAULT_LANGUAGE } from './constants.ts'
2
- import type { RawFrameReport } from './internal-types.ts'
3
- import type { FrameReport, FrameStopReason, TranslateCache } from './types.ts'
4
-
5
- export const normalizeLanguage = (language: unknown): string =>
6
- String(language ?? '')
7
- .trim()
8
- .toLowerCase() || DEFAULT_LANGUAGE
9
-
10
- export const cloneTranslateCache = (
11
- cache: TranslateCache | undefined,
12
- ): TranslateCache => {
13
- const out: TranslateCache = {}
14
- for (const [language, bucket] of Object.entries(cache ?? {})) {
15
- if (!bucket || typeof bucket !== 'object' || Array.isArray(bucket)) {
16
- continue
17
- }
18
- const normalizedLanguage = normalizeLanguage(language)
19
- out[normalizedLanguage] = {}
20
- for (const [words, translated] of Object.entries(bucket)) {
21
- out[normalizedLanguage][String(words)] = String(translated)
22
- }
23
- }
24
- return out
25
- }
26
-
27
- export const normalizeNowMs = (nowMs: number): number => {
28
- if (!Number.isFinite(nowMs)) {
29
- throw new Error('nowMs must be a finite number')
30
- }
31
- return Math.trunc(nowMs)
32
- }
33
-
34
- export const normalizeMaxFrames = (maxFrames: number): number => {
35
- if (!Number.isFinite(maxFrames)) {
36
- throw new Error('maxFrames must be a finite number')
37
- }
38
- const out = Math.trunc(maxFrames)
39
- if (out <= 0) {
40
- throw new Error('maxFrames must be greater than 0')
41
- }
42
- return out
43
- }
44
-
45
- const toFrameStopReason = (reason: string): FrameStopReason => {
46
- if (
47
- reason === 'finished' ||
48
- reason === 'timeout' ||
49
- reason === 'rerender' ||
50
- reason === 'warp-exit'
51
- ) {
52
- return reason
53
- }
54
- return 'timeout'
55
- }
56
-
57
- export const toFrameReport = (report: RawFrameReport): FrameReport => ({
58
- activeThreads: report.active_threads,
59
- ticks: report.tick_count,
60
- ops: report.op_count,
61
- emittedEffects: report.emitted_effects,
62
- stopReason: toFrameStopReason(report.stop_reason),
63
- shouldRender: report.should_render,
64
- isInWarp: report.is_in_warp,
65
- })
@@ -1,30 +0,0 @@
1
- import { describe, expect, test } from 'vite-plus/test'
2
-
3
- import { toOptionsJson } from './options.ts'
4
-
5
- describe('moonscratch/js/vm/options.ts', () => {
6
- test('returns undefined when options are omitted', () => {
7
- expect(toOptionsJson(undefined)).toBeUndefined()
8
- })
9
-
10
- test('passes through raw options JSON strings', () => {
11
- expect(toOptionsJson('{"turbo":true}')).toBe('{"turbo":true}')
12
- })
13
-
14
- test('maps camelCase and snake_case fields to raw options', () => {
15
- const json = toOptionsJson({
16
- turbo: true,
17
- compatibility30tps: true,
18
- max_clones: 123,
19
- deterministic: true,
20
- seed: 42,
21
- penWidth: 480,
22
- pen_height: 360,
23
- stepTimeoutTicks: 2048,
24
- })
25
-
26
- expect(json).toBe(
27
- '{"turbo":true,"compatibility_30tps":true,"max_clones":123,"deterministic":true,"seed":42,"pen_width":480,"pen_height":360,"step_timeout_ticks":2048}',
28
- )
29
- })
30
- })
package/js/vm/options.ts DELETED
@@ -1,55 +0,0 @@
1
- import type { RawVMOptions } from './internal-types.ts'
2
- import type { VMOptionsInput } from './types.ts'
3
-
4
- export const toOptionsJson = (
5
- options: string | VMOptionsInput | undefined,
6
- ): string | undefined => {
7
- if (options === undefined) {
8
- return undefined
9
- }
10
- if (typeof options === 'string') {
11
- return options
12
- }
13
-
14
- const raw: RawVMOptions = {}
15
- if (options.turbo !== undefined) {
16
- raw.turbo = options.turbo
17
- }
18
-
19
- const compatibility30tps =
20
- options.compatibility30tps ?? options.compatibility_30tps
21
- if (compatibility30tps !== undefined) {
22
- raw.compatibility_30tps = compatibility30tps
23
- }
24
-
25
- const maxClones = options.maxClones ?? options.max_clones
26
- if (maxClones !== undefined) {
27
- raw.max_clones = maxClones
28
- }
29
-
30
- if (options.deterministic !== undefined) {
31
- raw.deterministic = options.deterministic
32
- }
33
-
34
- if (options.seed !== undefined) {
35
- raw.seed = options.seed
36
- }
37
-
38
- const penWidth = options.penWidth ?? options.pen_width
39
- if (penWidth !== undefined) {
40
- raw.pen_width = penWidth
41
- }
42
-
43
- const penHeight = options.penHeight ?? options.pen_height
44
- if (penHeight !== undefined) {
45
- raw.pen_height = penHeight
46
- }
47
-
48
- const stepTimeoutTicks =
49
- options.stepTimeoutTicks ?? options.step_timeout_ticks
50
- if (stepTimeoutTicks !== undefined) {
51
- raw.step_timeout_ticks = stepTimeoutTicks
52
- }
53
-
54
- return JSON.stringify(raw)
55
- }
@@ -1,115 +0,0 @@
1
- import { Project } from 'hikkaku'
2
- import {
3
- clear,
4
- gotoXY,
5
- penDown,
6
- penUp,
7
- setPenColorParamTo,
8
- setPenColorToColor,
9
- setPenSizeTo,
10
- whenFlagClicked,
11
- } from 'hikkaku/blocks'
12
- import { describe, expect, test } from 'vite-plus/test'
13
- import { stepMany } from '../test/test-projects.ts'
14
- import { createHeadlessVM, createProgramModuleFromProject } from './factory.ts'
15
-
16
- describe('moonscratch/js/vm pen transparency', () => {
17
- test('does not overdraw alpha by repeatedly blending the same stroke area', () => {
18
- const project = new Project()
19
- const sprite = project.createSprite('pen-sprite')
20
-
21
- sprite.run(() => {
22
- whenFlagClicked(() => {
23
- clear()
24
- setPenSizeTo(6)
25
-
26
- setPenColorToColor('#ff0000')
27
- setPenColorParamTo('transparency', 0)
28
- penDown()
29
- gotoXY(-100, 0)
30
- gotoXY(100, 0)
31
- penUp()
32
-
33
- setPenColorToColor('#0000ff')
34
- setPenColorParamTo('transparency', 70)
35
- penDown()
36
- gotoXY(-100, 0)
37
- gotoXY(100, 0)
38
- penUp()
39
- })
40
- })
41
-
42
- const program = createProgramModuleFromProject({
43
- projectJson: project.toScratch(),
44
- })
45
- const vm = createHeadlessVM({
46
- program,
47
- initialNowMs: 0,
48
- })
49
- vm.greenFlag()
50
- stepMany(vm, 8)
51
-
52
- const frame = vm.renderFrame()
53
- const centerX = Math.floor(frame.width / 2)
54
- const centerY = Math.floor(frame.height / 2)
55
- const base = (centerY * frame.width + centerX) * 4
56
- const r = frame.pixels[base] ?? 0
57
- const g = frame.pixels[base + 1] ?? 0
58
- const b = frame.pixels[base + 2] ?? 0
59
- const a = frame.pixels[base + 3] ?? 0
60
-
61
- // Blue over red with transparency should stay mixed, not collapse to near-solid blue.
62
- expect(a).toBe(255)
63
- expect(g).toBeLessThan(16)
64
- expect(r).toBeGreaterThan(70)
65
- expect(b).toBeGreaterThan(70)
66
- expect(r).toBeLessThan(220)
67
- expect(b).toBeLessThan(220)
68
- expect(Math.abs(r - b)).toBeLessThan(90)
69
- })
70
-
71
- test('keeps subpixel coverage on diagonal size-1 pen lines', () => {
72
- const project = new Project()
73
- const sprite = project.createSprite('pen-sprite')
74
-
75
- sprite.run(() => {
76
- whenFlagClicked(() => {
77
- clear()
78
- setPenSizeTo(1)
79
- setPenColorToColor('#00ff00')
80
- setPenColorParamTo('transparency', 0)
81
- penDown()
82
- gotoXY(-120, -80)
83
- gotoXY(120, 80)
84
- penUp()
85
- })
86
- })
87
-
88
- const program = createProgramModuleFromProject({
89
- projectJson: project.toScratch(),
90
- })
91
- const vm = createHeadlessVM({
92
- program,
93
- initialNowMs: 0,
94
- })
95
- vm.greenFlag()
96
- stepMany(vm, 8)
97
-
98
- const frame = vm.renderFrame()
99
- let pureGreen = 0
100
- let blendedGreen = 0
101
- for (let index = 0; index < frame.pixels.length; index += 4) {
102
- const r = frame.pixels[index] ?? 0
103
- const g = frame.pixels[index + 1] ?? 0
104
- const b = frame.pixels[index + 2] ?? 0
105
- if (r === 0 && g === 255 && b === 0) {
106
- pureGreen += 1
107
- } else if (g === 255 && r === b && r > 0 && r < 255) {
108
- blendedGreen += 1
109
- }
110
- }
111
-
112
- expect(pureGreen).toBeGreaterThan(0)
113
- expect(blendedGreen).toBeGreaterThan(0)
114
- })
115
- })