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
@@ -1,53 +0,0 @@
1
- import { Project } from 'hikkaku'
2
- import {
3
- changeXBy,
4
- changeYBy,
5
- clear,
6
- gotoXY,
7
- penDown,
8
- penUp,
9
- repeat,
10
- setY,
11
- whenFlagClicked,
12
- } from 'hikkaku/blocks'
13
- import { bench, run } from 'mitata'
14
- import { createHeadlessVM, createProgramModuleFromProject } from '../js'
15
-
16
- const emptyProject = new Project()
17
- const emptyCompiled = createProgramModuleFromProject({
18
- projectJson: emptyProject.toScratch(),
19
- })
20
- bench('render empty/moonscratch', () => {
21
- const emptyVM = createHeadlessVM({ program: emptyCompiled })
22
- emptyVM.renderFrame()
23
- })
24
-
25
- const filledProject = new Project()
26
- filledProject.createSprite('mysprite').run(() => {
27
- whenFlagClicked(() => {
28
- clear()
29
- gotoXY(-240, -180)
30
- repeat(420, () => {
31
- changeXBy(1)
32
- setY(-180)
33
-
34
- penDown()
35
- repeat(360, () => {
36
- changeYBy(1)
37
- })
38
- penUp()
39
- })
40
- })
41
- })
42
-
43
- const filledCompiled = createProgramModuleFromProject({
44
- projectJson: filledProject.toScratch(),
45
- })
46
- bench('render filled/moonscratch', () => {
47
- const filledVM = createHeadlessVM({ program: filledCompiled })
48
- filledVM.renderFrame()
49
- })
50
-
51
- if (import.meta.main) {
52
- await run()
53
- }
@@ -1,8 +0,0 @@
1
- import { run } from 'mitata'
2
-
3
- import './calc.bench.ts'
4
- import './draw.bench.ts'
5
- import './load.bench.ts'
6
- import './render.bench.ts'
7
-
8
- await run()
@@ -1,15 +0,0 @@
1
- /// <reference types="@turbowarp/types" />
2
-
3
- declare module '@scratch/scratch-vm' {
4
- import type VM from 'scratch-vm'
5
- export { VM }
6
- const VMClass: typeof VM
7
- export default VMClass
8
- }
9
-
10
- declare module '@scratch/scratch-render' {
11
- import type Render from 'scratch-render'
12
- export { Render }
13
- const RenderClass: typeof Render
14
- export default RenderClass
15
- }
@@ -1,103 +0,0 @@
1
- # Scratch VM Event Loop Spec
2
-
3
- This document records how `scratch-vm` controls frame stepping, thread ticks, and time,
4
- and how `moonscratch` mirrors that behavior.
5
-
6
- ## Terminology
7
-
8
- - `frame`
9
- - One call to `Runtime._step()`.
10
- - In browser mode, called by `setInterval` from `Runtime.start()`.
11
- - `tick`
12
- - One inner pass of `Sequencer.stepThreads()` while-loop.
13
- - A tick attempts to run each runnable thread once.
14
- - `ms`
15
- - Millisecond time base used by timer/wait behavior.
16
- - `redrawRequested`
17
- - Runtime flag set by visible operations, used to decide if the current frame
18
- should continue ticking.
19
- - `yield`
20
- - Pause current thread until the next scheduler turn.
21
- - `yieldTick`
22
- - Pause current thread until the next sequencer tick.
23
-
24
- ## Observed Scratch VM Behavior
25
-
26
- ## Frame loop
27
-
28
- - `Runtime.start()` sets `currentStepTime` then schedules `_step()`:
29
- - `THREAD_STEP_INTERVAL = 1000 / 60`
30
- - `THREAD_STEP_INTERVAL_COMPATIBILITY = 1000 / 30`
31
- - Compatibility mode changes the interval; turbo mode does not.
32
-
33
- ## What happens in one frame (`Runtime._step`)
34
-
35
- 1. Clean killed threads.
36
- 2. Start edge-activated hats.
37
- 3. Reset `redrawRequested = false`.
38
- 4. Call `sequencer.stepThreads()`.
39
- 5. Update glows/monitor state.
40
- 6. Draw renderer frame once.
41
-
42
- ## Tick loop (`Sequencer.stepThreads`)
43
-
44
- - `WORK_TIME = 0.75 * runtime.currentStepTime`.
45
- - Continue ticking while:
46
- 1. there are threads,
47
- 2. at least one active thread exists,
48
- 3. elapsed work time is below `WORK_TIME`,
49
- 4. `runtime.turboMode || !runtime.redrawRequested`.
50
- - Each tick:
51
- - Iterate through thread list.
52
- - Skip done threads.
53
- - Run runnable threads (`stepThread`).
54
- - Clean up done threads.
55
-
56
- ## `yieldTick` semantics
57
-
58
- - `util.yieldTick()` sets `thread.status = STATUS_YIELD_TICK`.
59
- - At the first tick of the next `stepThreads` call, this status is cleared and thread resumes.
60
- - This is "next tick", not "after 1 ms".
61
-
62
- ## Relationship: 1 tick, 1 frame, 1 ms
63
-
64
- - `1 frame` is a scheduler/render boundary (`_step`).
65
- - `1 frame` contains `N` ticks, where `N` is dynamic.
66
- - `1 ms` is timer/wait time axis and does not map 1:1 to ticks.
67
- - Therefore:
68
- - frame count controls pacing,
69
- - tick count controls execution throughput,
70
- - ms controls timer predicates.
71
-
72
- ## Turbo Mode (important)
73
-
74
- - Turbo mode does not change frame interval directly.
75
- - Turbo mode changes the tick continuation condition:
76
- - without turbo: redraw request can stop additional ticks in the same frame,
77
- - with turbo: redraw request does not stop same-frame ticking.
78
- - Practical effect: same FPS, more work per frame.
79
-
80
- ## Moonscratch Design Rules
81
-
82
- ## Time model
83
-
84
- - Runtime clock is virtual: `nowMs`.
85
- - Time is updated only via explicit `setTime(nowMs)`.
86
- - `stepFrame` does not advance clock time.
87
-
88
- ## Frame API policy
89
-
90
- - Canonical API:
91
- - `setTime(nowMs)`
92
- - `stepFrame(frameCount = 1)`
93
- - `runFrames(...)`
94
- - `runUntilIdle(...)`
95
- - Recommended viewer loop:
96
- 1. call `setTime(...)` once per RAF/tick,
97
- 2. run one or more `stepFrame(...)` calls within a work budget,
98
- 3. render once after stepping.
99
-
100
- ## Compatibility note
101
-
102
- - `scratch-vm` uses one draw per `Runtime._step()` and uses `redrawRequested` to decide whether to continue stepping in the same frame.
103
- - `moonscratch` should preserve this shape: avoid exposing intermediate script progress by rendering between partial step chunks.
@@ -1,50 +0,0 @@
1
- # Moonscratch Time Separation Notes
2
-
3
- This document summarizes the investigation around `examples/3d-cube` rendering behavior
4
- and the design choice to separate VM time updates from stepping.
5
-
6
- ## Problem observed
7
-
8
- - In `scratch-gui` / `scratch-vm`, the cube appears to render as a single visual update
9
- (or in a gap users do not perceive).
10
- - In the old `moonscratch` viewer loop, users could see pen drawing progress line-by-line.
11
- - This was not primarily a raw performance issue. It was caused by scheduling boundaries
12
- becoming visible.
13
-
14
- ## Key findings
15
-
16
- - Both runtimes are incremental internally.
17
- - The visible difference came from how stepping and rendering were orchestrated:
18
- - `scratch-vm`:
19
- - one `Runtime._step()` per interval,
20
- - `Sequencer.stepThreads()` runs until work budget / redraw constraints,
21
- - renderer draws once at end of `_step`.
22
- - old `moonscratch` viewer:
23
- - stepped in small chunks tied to RAF pacing,
24
- - intermediate VM progress was rendered more directly.
25
-
26
- ## Design change
27
-
28
- - `stepFrame` now executes scheduler work only.
29
- - `setTime(nowMs)` is the only API that moves VM clock time.
30
- - This makes time ownership explicit and lets callers control:
31
- - when timer/wait predicates advance,
32
- - how much execution is done before rendering.
33
-
34
- ## Viewer policy
35
-
36
- Recommended loop:
37
-
38
- 1. `setTime(now)` once per display tick.
39
- 2. Run `stepFrame(1)` repeatedly within a bounded work window.
40
- 3. Render exactly once after stepping.
41
-
42
- This preserves responsiveness while reducing visible intermediate pen progress.
43
-
44
- ## Compatibility and migration notes
45
-
46
- - Breaking API changes:
47
- - removed: `stepFrame(frameCount, frameMs)`
48
- - added/renamed: `setTime(nowMs)` (formerly `setNowMs`)
49
- - removed from `FrameReport`: `nowMs` and frame-duration-derived fields
50
- - Time-dependent scripts (`wait`, `timer`) require caller-driven `setTime` updates.
package/index.html DELETED
@@ -1,91 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>MoonScratch Viewer</title>
7
- <style>
8
- :root {
9
- color-scheme: dark;
10
- font-family: 'Trebuchet MS', 'Segoe UI', 'Hiragino Kaku Gothic ProN',
11
- 'Yu Gothic', 'Meiryo', sans-serif;
12
- }
13
-
14
- body {
15
- margin: 0;
16
- min-height: 100vh;
17
- background:
18
- radial-gradient(circle at 10% 20%, #24344d 0%, transparent 35%),
19
- radial-gradient(circle at 90% 20%, #3d2549 0%, transparent 35%),
20
- linear-gradient(180deg, #111827, #0d1117);
21
- color: #f8fafc;
22
- display: flex;
23
- justify-content: center;
24
- align-items: flex-start;
25
- padding: 20px;
26
- }
27
-
28
- main {
29
- width: min(920px, 100%);
30
- background: rgba(15, 23, 42, 0.75);
31
- border: 1px solid rgba(148, 163, 184, 0.35);
32
- border-radius: 12px;
33
- padding: 20px;
34
- box-shadow:
35
- 0 20px 45px rgba(0, 0, 0, 0.35),
36
- inset 0 1px 0 rgba(255, 255, 255, 0.05);
37
- backdrop-filter: blur(3px);
38
- }
39
-
40
- h1 {
41
- margin: 0 0 12px;
42
- font-size: 1.45rem;
43
- letter-spacing: 0.02em;
44
- }
45
-
46
- .controls {
47
- display: flex;
48
- gap: 12px;
49
- align-items: center;
50
- flex-wrap: wrap;
51
- margin-bottom: 16px;
52
- }
53
-
54
- label {
55
- font-weight: 700;
56
- color: #e2e8f0;
57
- }
58
-
59
- select {
60
- border: 1px solid #334155;
61
- background: #0f172a;
62
- color: #f8fafc;
63
- padding: 8px 10px;
64
- border-radius: 8px;
65
- min-width: 260px;
66
- }
67
-
68
- .status {
69
- margin: 8px 0 14px;
70
- color: #94a3b8;
71
- min-height: 1.4em;
72
- }
73
-
74
- canvas {
75
- width: 100%;
76
- max-width: 100%;
77
- aspect-ratio: 4 / 3;
78
- background: #0f172a;
79
- border: 1px solid rgba(148, 163, 184, 0.45);
80
- border-radius: 8px;
81
- display: block;
82
- image-rendering: pixelated;
83
- margin-top: 12px;
84
- }
85
- </style>
86
- </head>
87
- <body>
88
- <main id="app"></main>
89
- <script src="./viewer/index.ts" type="module"></script>
90
- </body>
91
- </html>
package/js/AGENTS.md DELETED
@@ -1,5 +0,0 @@
1
- # js
2
-
3
- - Public APIs and modules for Bun/TypeScript.
4
- - Prioritize running `bun typecheck` and `bun run test` when making changes.
5
- - Keep boundary types and conversion logic explicit to stay aligned with MoonBit.
package/js/a.ts DELETED
@@ -1,52 +0,0 @@
1
- import {
2
- callProcedure,
3
- defineProcedure,
4
- procedureLabel,
5
- repeat,
6
- say,
7
- whenFlagClicked,
8
- } from '../../hikkaku/src/blocks'
9
- import { Project } from '../../hikkaku/src/index'
10
- import { createHeadlessVM, createProgramModuleFromProject } from '.'
11
-
12
- const project = new Project()
13
- project.stage.run(() => {
14
- const a = defineProcedure(
15
- [procedureLabel('a')],
16
- () => {
17
- repeat(100000, () => {
18
- say('Hello, World!')
19
- })
20
- },
21
- true,
22
- )
23
- whenFlagClicked(() => {
24
- repeat(10, () => {
25
- callProcedure(a, [])
26
- })
27
- })
28
- })
29
-
30
- const program = createProgramModuleFromProject({
31
- projectJson: await Bun.file(
32
- new URL('../../../examples/tesseract/dist/project.json', import.meta.url),
33
- ).json(), //project.toScratch(),
34
- })
35
-
36
- const vm = createHeadlessVM({
37
- program,
38
- options: {
39
- stepTimeoutTicks: 100,
40
- },
41
- })
42
-
43
- vm.greenFlag()
44
-
45
- while (true) {
46
- const report = vm.stepFrame()
47
- console.log('Stopped:', report.stopReason)
48
- if (report.stopReason === 'finished' || report.stopReason === 'warp-exit') {
49
- console.log(report.stopReason)
50
- break
51
- }
52
- }
@@ -1,5 +0,0 @@
1
- # js/assets
2
-
3
- - Manages asset generation, validation, and conversion logic.
4
- - Add corresponding `*.test.ts` files with new features to fix input variation coverage.
5
- - For breaking changes, verify compatibility through the public surface in `index.ts`.
@@ -1,14 +0,0 @@
1
- import { describe, expect, test } from 'vite-plus/test'
2
-
3
- import { encodeBase64 } from './base64.ts'
4
-
5
- describe('moonscratch/js/assets/base64.ts', () => {
6
- test('encodes rgba bytes as base64', () => {
7
- const encoded = encodeBase64(new Uint8Array([0, 255, 0, 255]))
8
- expect(encoded).toBe('AP8A/w==')
9
- })
10
-
11
- test('encodes empty bytes', () => {
12
- expect(encodeBase64(new Uint8Array([]))).toBe('')
13
- })
14
- })
@@ -1,21 +0,0 @@
1
- interface BufferLike {
2
- from(input: Uint8Array): { toString(encoding: string): string }
3
- }
4
-
5
- export const encodeBase64 = (bytes: Uint8Array): string => {
6
- const maybeBuffer = (globalThis as { Buffer?: BufferLike }).Buffer
7
- if (maybeBuffer) {
8
- return maybeBuffer.from(bytes).toString('base64')
9
- }
10
-
11
- const maybeBtoa = (globalThis as { btoa?: (binary: string) => string }).btoa
12
- if (typeof maybeBtoa === 'function') {
13
- let binary = ''
14
- for (let i = 0; i < bytes.length; i += 1) {
15
- binary += String.fromCharCode(bytes[i] ?? 0)
16
- }
17
- return maybeBtoa(binary)
18
- }
19
-
20
- throw new Error('No base64 encoder found in this runtime')
21
- }
@@ -1,26 +0,0 @@
1
- import { describe, expect, test } from 'vite-plus/test'
2
-
3
- import { buildAsset } from './build-asset.ts'
4
-
5
- describe('moonscratch/js/assets/build-asset.ts', () => {
6
- test('builds rgba asset from flat bytes', () => {
7
- const asset = buildAsset(2, 1, [0, 255, 0, 255, 255, 0, 0, 255])
8
- expect(asset).toEqual({
9
- width: 2,
10
- height: 1,
11
- rgbaBase64: 'AP8A//8AAP8=',
12
- })
13
- })
14
-
15
- test('throws when rgba length is shorter than expected', () => {
16
- expect(() => buildAsset(2, 1, [255, 0, 0, 255])).toThrow(
17
- 'rgba length must be at least 8',
18
- )
19
- })
20
-
21
- test('throws when rgba contains invalid byte values', () => {
22
- expect(() => buildAsset(1, 1, [300, 0, 0, 255])).toThrow(
23
- 'rgba[0] must be between 0 and 255',
24
- )
25
- })
26
- })
@@ -1,28 +0,0 @@
1
- import { encodeBase64 } from './base64.ts'
2
- import type { RgbaAsset } from './types.ts'
3
- import { toByte, toPositiveInt } from './validation.ts'
4
-
5
- export const buildAsset = (
6
- width: number,
7
- height: number,
8
- rgba: ArrayLike<number>,
9
- ): RgbaAsset => {
10
- const safeWidth = toPositiveInt(width, 'width')
11
- const safeHeight = toPositiveInt(height, 'height')
12
- const expectedLength = safeWidth * safeHeight * 4
13
-
14
- if (rgba.length < expectedLength) {
15
- throw new Error(`rgba length must be at least ${expectedLength}`)
16
- }
17
-
18
- const bytes = new Uint8Array(expectedLength)
19
- for (let i = 0; i < expectedLength; i += 1) {
20
- bytes[i] = toByte(rgba[i], `rgba[${i}]`)
21
- }
22
-
23
- return {
24
- width: safeWidth,
25
- height: safeHeight,
26
- rgbaBase64: encodeBase64(bytes),
27
- }
28
- }
@@ -1,142 +0,0 @@
1
- import { describe, expect, test, vi } from 'vite-plus/test'
2
-
3
- import {
4
- fromCanvas,
5
- fromImageBytes,
6
- fromImageData,
7
- fromImageFile,
8
- fromRgbaBytes,
9
- fromRgbaMatrix,
10
- } from './create.ts'
11
-
12
- const sharpCalls: unknown[] = []
13
-
14
- vi.mock('sharp', () => ({
15
- default: vi.fn((input?: unknown) => {
16
- sharpCalls.push(input)
17
-
18
- return {
19
- ensureAlpha() {
20
- return this
21
- },
22
- raw() {
23
- return this
24
- },
25
- async toBuffer() {
26
- return {
27
- data: new Uint8Array([0, 255, 0, 255]),
28
- info: {
29
- width: 1,
30
- height: 1,
31
- },
32
- }
33
- },
34
- }
35
- }),
36
- }))
37
-
38
- describe('moonscratch/js/assets/create.ts', () => {
39
- test('builds an asset from image data', () => {
40
- const asset = fromImageData({
41
- width: 1,
42
- height: 1,
43
- data: new Uint8Array([0, 255, 0, 255]),
44
- })
45
-
46
- expect(asset).toEqual({
47
- width: 1,
48
- height: 1,
49
- rgbaBase64: 'AP8A/w==',
50
- })
51
- })
52
-
53
- test('builds an asset from flat rgba bytes', () => {
54
- const asset = fromRgbaBytes(
55
- 2,
56
- 1,
57
- new Uint8Array([0, 255, 0, 255, 255, 0, 0, 255]),
58
- )
59
-
60
- expect(asset).toEqual({
61
- width: 2,
62
- height: 1,
63
- rgbaBase64: 'AP8A//8AAP8=',
64
- })
65
- })
66
-
67
- test('builds an asset from H x W x 4 matrix', () => {
68
- const asset = fromRgbaMatrix([
69
- [
70
- [0, 255, 0, 255],
71
- [255, 0, 0, 255],
72
- ],
73
- [
74
- [0, 0, 255, 255],
75
- [255, 255, 255, 255],
76
- ],
77
- ])
78
-
79
- expect(asset).toEqual({
80
- width: 2,
81
- height: 2,
82
- rgbaBase64: 'AP8A//8AAP8AAP///////w==',
83
- })
84
- })
85
-
86
- test('builds an asset from canvas API', () => {
87
- const getImageData = vi.fn(() => ({
88
- width: 1,
89
- height: 1,
90
- data: new Uint8Array([255, 0, 0, 255]),
91
- }))
92
-
93
- const getContext = vi.fn(() => ({ getImageData }))
94
-
95
- const asset = fromCanvas({
96
- width: 1,
97
- height: 1,
98
- getContext,
99
- })
100
-
101
- expect(getContext).toHaveBeenCalledWith('2d')
102
- expect(getImageData).toHaveBeenCalledWith(0, 0, 1, 1)
103
- expect(asset.rgbaBase64).toBe('/wAA/w==')
104
- })
105
-
106
- test('throws when matrix row widths are inconsistent', () => {
107
- expect(() =>
108
- fromRgbaMatrix([
109
- [
110
- [0, 0, 0, 255],
111
- [255, 255, 255, 255],
112
- ],
113
- [[0, 0, 0, 255]],
114
- ]),
115
- ).toThrow('pixels[1] must contain exactly 2 columns')
116
- })
117
-
118
- test('loads an image asset from bytes via sharp', async () => {
119
- const asset = await fromImageBytes(new Uint8Array([1, 2, 3]))
120
-
121
- expect(sharpCalls).toEqual([new Uint8Array([1, 2, 3])])
122
- expect(asset).toEqual({
123
- width: 1,
124
- height: 1,
125
- rgbaBase64: 'AP8A/w==',
126
- })
127
- })
128
-
129
- test('loads an image asset from file via sharp', async () => {
130
- const asset = await fromImageFile('fixtures/example.png')
131
-
132
- expect(sharpCalls).toEqual([
133
- new Uint8Array([1, 2, 3]),
134
- 'fixtures/example.png',
135
- ])
136
- expect(asset).toEqual({
137
- width: 1,
138
- height: 1,
139
- rgbaBase64: 'AP8A/w==',
140
- })
141
- })
142
- })