moonscratch 0.1.1 → 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 (149) 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/AGENTS.md +0 -91
  28. package/PLAN.md +0 -64
  29. package/TODO.md +0 -120
  30. package/benchmarks/calc.bench.ts +0 -144
  31. package/benchmarks/draw.bench.ts +0 -215
  32. package/benchmarks/load.bench.ts +0 -28
  33. package/benchmarks/render.bench.ts +0 -53
  34. package/benchmarks/run.bench.ts +0 -8
  35. package/benchmarks/types.d.ts +0 -15
  36. package/docs/scratch-vm-specs/eventloop.md +0 -103
  37. package/docs/scratch-vm-specs/moonscratch-time-separation.md +0 -50
  38. package/index.html +0 -91
  39. package/js/AGENTS.md +0 -5
  40. package/js/a.ts +0 -52
  41. package/js/assets/AGENTS.md +0 -5
  42. package/js/assets/base64.test.ts +0 -14
  43. package/js/assets/base64.ts +0 -21
  44. package/js/assets/build-asset.test.ts +0 -26
  45. package/js/assets/build-asset.ts +0 -28
  46. package/js/assets/create.test.ts +0 -142
  47. package/js/assets/create.ts +0 -122
  48. package/js/assets/index.test.ts +0 -15
  49. package/js/assets/index.ts +0 -2
  50. package/js/assets/types.ts +0 -26
  51. package/js/assets/validation.test.ts +0 -34
  52. package/js/assets/validation.ts +0 -25
  53. package/js/assets.test.ts +0 -14
  54. package/js/assets.ts +0 -1
  55. package/js/index.test.ts +0 -26
  56. package/js/index.ts +0 -3
  57. package/js/render/index.test.ts +0 -65
  58. package/js/render/index.ts +0 -13
  59. package/js/render/sharp.ts +0 -87
  60. package/js/render/svg.ts +0 -68
  61. package/js/render/types.ts +0 -35
  62. package/js/render/utils.ts +0 -108
  63. package/js/render/webgl.ts +0 -274
  64. package/js/sharp-optional.d.ts +0 -16
  65. package/js/test/helpers.ts +0 -116
  66. package/js/test/hikkaku-sample.test.ts +0 -37
  67. package/js/test/rubik-components.input-motion.test.ts +0 -60
  68. package/js/test/rubik-components.lists.test.ts +0 -49
  69. package/js/test/rubik-components.operators.test.ts +0 -104
  70. package/js/test/rubik-components.pen.test.ts +0 -112
  71. package/js/test/rubik-components.procedures-loops.test.ts +0 -72
  72. package/js/test/rubik-components.variables-branches.test.ts +0 -57
  73. package/js/test/rubik-components.visibility-entry.test.ts +0 -31
  74. package/js/test/test-projects.ts +0 -598
  75. package/js/test/variable.ts +0 -200
  76. package/js/test/warp.test.ts +0 -59
  77. package/js/vm/AGENTS.md +0 -6
  78. package/js/vm/README.md +0 -183
  79. package/js/vm/bindings.test.ts +0 -13
  80. package/js/vm/bindings.ts +0 -5
  81. package/js/vm/compare-operators.test.ts +0 -145
  82. package/js/vm/constants.test.ts +0 -11
  83. package/js/vm/constants.ts +0 -4
  84. package/js/vm/effect-guards.test.ts +0 -68
  85. package/js/vm/effect-guards.ts +0 -44
  86. package/js/vm/factory.test.ts +0 -486
  87. package/js/vm/factory.ts +0 -615
  88. package/js/vm/headless-vm.test.ts +0 -131
  89. package/js/vm/headless-vm.ts +0 -342
  90. package/js/vm/index.test.ts +0 -28
  91. package/js/vm/index.ts +0 -5
  92. package/js/vm/internal-types.ts +0 -32
  93. package/js/vm/json.test.ts +0 -40
  94. package/js/vm/json.ts +0 -273
  95. package/js/vm/normalize.test.ts +0 -48
  96. package/js/vm/normalize.ts +0 -65
  97. package/js/vm/options.test.ts +0 -30
  98. package/js/vm/options.ts +0 -55
  99. package/js/vm/pen-transparency.test.ts +0 -115
  100. package/js/vm/program-wasm.ts +0 -322
  101. package/js/vm/scheduler-render.test.ts +0 -401
  102. package/js/vm/scratch-assets.test.ts +0 -136
  103. package/js/vm/scratch-assets.ts +0 -202
  104. package/js/vm/types.ts +0 -358
  105. package/js/vm/value-guards.test.ts +0 -25
  106. package/js/vm/value-guards.ts +0 -18
  107. package/moon.mod.json +0 -10
  108. package/scripts/preinstall.ts +0 -4
  109. package/src/AGENTS.md +0 -6
  110. package/src/api.mbt +0 -161
  111. package/src/api_aot_commands.mbt +0 -184
  112. package/src/api_effects_json.mbt +0 -72
  113. package/src/api_options.mbt +0 -60
  114. package/src/api_program_wasm.mbt +0 -1647
  115. package/src/api_program_wat.mbt +0 -2206
  116. package/src/api_snapshot_json.mbt +0 -44
  117. package/src/cmd/AGENTS.md +0 -5
  118. package/src/cmd/main/AGENTS.md +0 -5
  119. package/src/cmd/main/main.mbt +0 -29
  120. package/src/cmd/main/moon.pkg +0 -7
  121. package/src/cmd/main/pkg.generated.mbti +0 -13
  122. package/src/json_helpers.mbt +0 -176
  123. package/src/moon.pkg +0 -65
  124. package/src/moonscratch.mbt +0 -3
  125. package/src/moonscratch_wbtest.mbt +0 -40
  126. package/src/parser_sb3.mbt +0 -890
  127. package/src/pkg.generated.mbti +0 -479
  128. package/src/runtime_eval.mbt +0 -2844
  129. package/src/runtime_exec.mbt +0 -3850
  130. package/src/runtime_render.mbt +0 -2550
  131. package/src/runtime_state.mbt +0 -870
  132. package/src/test/AGENTS.md +0 -3
  133. package/src/test/projects/AGENTS.md +0 -6
  134. package/src/test/projects/moon.pkg +0 -4
  135. package/src/test/projects/moonscratch_compat_test.mbt +0 -642
  136. package/src/test/projects/moonscratch_core_test.mbt +0 -1332
  137. package/src/test/projects/moonscratch_runtime_test.mbt +0 -1087
  138. package/src/test/projects/pkg.generated.mbti +0 -13
  139. package/src/test/projects/test_support.mbt +0 -35
  140. package/src/types_effects.mbt +0 -20
  141. package/src/types_error.mbt +0 -4
  142. package/src/types_options.mbt +0 -31
  143. package/src/types_runtime_structs.mbt +0 -254
  144. package/src/types_vm.mbt +0 -109
  145. package/tsconfig.json +0 -29
  146. package/viewer/index.ts +0 -399
  147. package/viewer/vite.d.ts +0 -1
  148. package/viewer/worker.ts +0 -161
  149. package/vite.config.ts +0 -61
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
- })