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,122 +0,0 @@
1
- import { buildAsset } from './build-asset.ts'
2
- import type {
3
- CanvasLike,
4
- ImageDataLike,
5
- RgbaAsset,
6
- RgbaMatrix,
7
- } from './types.ts'
8
- import { toByte, toPositiveInt } from './validation.ts'
9
-
10
- export const fromRgbaBytes = (
11
- width: number,
12
- height: number,
13
- rgba: ArrayLike<number>,
14
- ): RgbaAsset => buildAsset(width, height, rgba)
15
-
16
- export const fromImageData = (imageData: ImageDataLike): RgbaAsset =>
17
- buildAsset(imageData.width, imageData.height, imageData.data)
18
-
19
- export const fromCanvas = (canvas: CanvasLike): RgbaAsset => {
20
- const width = toPositiveInt(canvas.width, 'canvas.width')
21
- const height = toPositiveInt(canvas.height, 'canvas.height')
22
- const context = canvas.getContext('2d')
23
-
24
- if (!context) {
25
- throw new Error('canvas.getContext("2d") returned null')
26
- }
27
-
28
- return fromImageData(context.getImageData(0, 0, width, height))
29
- }
30
-
31
- export const fromRgbaMatrix = (pixels: RgbaMatrix): RgbaAsset => {
32
- const height = pixels.length
33
- if (height <= 0) {
34
- throw new Error('pixels must have at least one row')
35
- }
36
-
37
- const firstRow = pixels[0]
38
- const width = firstRow?.length ?? 0
39
- if (width <= 0) {
40
- throw new Error('pixels must have at least one column')
41
- }
42
-
43
- const flat = new Uint8Array(width * height * 4)
44
-
45
- for (let y = 0; y < height; y += 1) {
46
- const row = pixels[y]
47
- if (!row || row.length !== width) {
48
- throw new Error(`pixels[${y}] must contain exactly ${width} columns`)
49
- }
50
-
51
- for (let x = 0; x < width; x += 1) {
52
- const pixel = row[x]
53
- if (!pixel || pixel.length < 4) {
54
- throw new Error(`pixels[${y}][${x}] must contain 4 channels (RGBA)`)
55
- }
56
-
57
- const base = (y * width + x) * 4
58
- flat[base] = toByte(pixel[0], `pixels[${y}][${x}][0]`)
59
- flat[base + 1] = toByte(pixel[1], `pixels[${y}][${x}][1]`)
60
- flat[base + 2] = toByte(pixel[2], `pixels[${y}][${x}][2]`)
61
- flat[base + 3] = toByte(pixel[3], `pixels[${y}][${x}][3]`)
62
- }
63
- }
64
-
65
- return buildAsset(width, height, flat)
66
- }
67
-
68
- type SharpNamespace = {
69
- default?: (input?: unknown) => SharpPipeline
70
- }
71
-
72
- type SharpPipeline = {
73
- ensureAlpha(): SharpPipeline
74
- raw(): SharpPipeline
75
- toBuffer(options: { resolveWithObject: true }): Promise<{
76
- data: Uint8Array
77
- info: {
78
- width: number
79
- height: number
80
- }
81
- }>
82
- }
83
-
84
- const loadSharp = async (): Promise<(input?: unknown) => SharpPipeline> => {
85
- try {
86
- const sharp = (await import('sharp')) as SharpNamespace
87
- return (
88
- sharp.default ??
89
- ((sharp as unknown as { sharp?: (input?: unknown) => SharpPipeline })
90
- .sharp as ((input?: unknown) => SharpPipeline) | undefined) ??
91
- (() => {
92
- throw new Error('invalid sharp module shape')
93
- })
94
- )
95
- } catch (error) {
96
- const reason = error instanceof Error ? error.message : String(error)
97
- throw new Error(`sharp is required to load image assets: ${reason}`)
98
- }
99
- }
100
-
101
- const fromSharpPipeline = async (
102
- pipeline: SharpPipeline,
103
- ): Promise<RgbaAsset> => {
104
- const { data, info } = await pipeline
105
- .ensureAlpha()
106
- .raw()
107
- .toBuffer({ resolveWithObject: true })
108
- return fromRgbaBytes(info.width, info.height, data)
109
- }
110
-
111
- export const fromImageBytes = async (
112
- bytes: ArrayBuffer | Uint8Array,
113
- ): Promise<RgbaAsset> => {
114
- const sharp = await loadSharp()
115
- const normalized = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)
116
- return fromSharpPipeline(sharp(normalized))
117
- }
118
-
119
- export const fromImageFile = async (path: string): Promise<RgbaAsset> => {
120
- const sharp = await loadSharp()
121
- return fromSharpPipeline(sharp(path))
122
- }
@@ -1,15 +0,0 @@
1
- import { describe, expect, test } from 'vite-plus/test'
2
-
3
- import * as create from './create.ts'
4
- import * as index from './index.ts'
5
-
6
- describe('moonscratch/js/assets/index.ts', () => {
7
- test('re-exports create helpers', () => {
8
- expect(index.fromRgbaBytes).toBe(create.fromRgbaBytes)
9
- expect(index.fromImageData).toBe(create.fromImageData)
10
- expect(index.fromCanvas).toBe(create.fromCanvas)
11
- expect(index.fromRgbaMatrix).toBe(create.fromRgbaMatrix)
12
- expect(index.fromImageBytes).toBe(create.fromImageBytes)
13
- expect(index.fromImageFile).toBe(create.fromImageFile)
14
- })
15
- })
@@ -1,2 +0,0 @@
1
- export * from './create.ts'
2
- export * from './types.ts'
@@ -1,26 +0,0 @@
1
- export interface RgbaAsset {
2
- width: number
3
- height: number
4
- rgbaBase64: string
5
- }
6
-
7
- export type RgbaTuple = readonly [number, number, number, number]
8
- export type RgbaMatrix = ReadonlyArray<
9
- ReadonlyArray<RgbaTuple | ReadonlyArray<number>>
10
- >
11
-
12
- export interface ImageDataLike {
13
- width: number
14
- height: number
15
- data: ArrayLike<number>
16
- }
17
-
18
- export interface Canvas2DContextLike {
19
- getImageData(sx: number, sy: number, sw: number, sh: number): ImageDataLike
20
- }
21
-
22
- export interface CanvasLike {
23
- width: number
24
- height: number
25
- getContext(contextId: string, options?: unknown): Canvas2DContextLike | null
26
- }
@@ -1,34 +0,0 @@
1
- import { describe, expect, test } from 'vite-plus/test'
2
-
3
- import { toByte, toPositiveInt } from './validation.ts'
4
-
5
- describe('moonscratch/js/assets/validation.ts', () => {
6
- test('toPositiveInt accepts finite positive integers', () => {
7
- expect(toPositiveInt(1, 'width')).toBe(1)
8
- })
9
-
10
- test('toPositiveInt rejects invalid values', () => {
11
- expect(() => toPositiveInt(Number.NaN, 'width')).toThrow(
12
- 'width must be a finite number',
13
- )
14
- expect(() => toPositiveInt(1.5, 'height')).toThrow(
15
- 'height must be an integer',
16
- )
17
- expect(() => toPositiveInt(0, 'height')).toThrow(
18
- 'height must be greater than 0',
19
- )
20
- })
21
-
22
- test('toByte accepts byte values', () => {
23
- expect(toByte(255, 'rgba[0]')).toBe(255)
24
- })
25
-
26
- test('toByte rejects out-of-range values', () => {
27
- expect(() => toByte(-1, 'rgba[0]')).toThrow(
28
- 'rgba[0] must be between 0 and 255',
29
- )
30
- expect(() => toByte(256, 'rgba[1]')).toThrow(
31
- 'rgba[1] must be between 0 and 255',
32
- )
33
- })
34
- })
@@ -1,25 +0,0 @@
1
- export const toPositiveInt = (value: unknown, name: string): number => {
2
- if (typeof value !== 'number' || !Number.isFinite(value)) {
3
- throw new Error(`${name} must be a finite number`)
4
- }
5
- if (!Number.isInteger(value)) {
6
- throw new Error(`${name} must be an integer`)
7
- }
8
- if (value <= 0) {
9
- throw new Error(`${name} must be greater than 0`)
10
- }
11
- return value
12
- }
13
-
14
- export const toByte = (value: unknown, name: string): number => {
15
- if (typeof value !== 'number' || !Number.isFinite(value)) {
16
- throw new Error(`${name} must be a finite number`)
17
- }
18
- if (!Number.isInteger(value)) {
19
- throw new Error(`${name} must be an integer`)
20
- }
21
- if (value < 0 || value > 255) {
22
- throw new Error(`${name} must be between 0 and 255`)
23
- }
24
- return value
25
- }
package/js/assets.test.ts DELETED
@@ -1,14 +0,0 @@
1
- import { describe, expect, test } from 'vite-plus/test'
2
- import * as assetsIndex from './assets/index.ts'
3
- import * as assets from './assets.ts'
4
-
5
- describe('moonscratch/js/assets.ts', () => {
6
- test('re-exports public asset APIs', () => {
7
- expect(assets.fromRgbaBytes).toBe(assetsIndex.fromRgbaBytes)
8
- expect(assets.fromImageData).toBe(assetsIndex.fromImageData)
9
- expect(assets.fromCanvas).toBe(assetsIndex.fromCanvas)
10
- expect(assets.fromRgbaMatrix).toBe(assetsIndex.fromRgbaMatrix)
11
- expect(assets.fromImageBytes).toBe(assetsIndex.fromImageBytes)
12
- expect(assets.fromImageFile).toBe(assetsIndex.fromImageFile)
13
- })
14
- })
package/js/assets.ts DELETED
@@ -1 +0,0 @@
1
- export * from './assets/index.ts'
package/js/index.test.ts DELETED
@@ -1,26 +0,0 @@
1
- import { describe, expect, test } from 'vite-plus/test'
2
-
3
- import * as assetsIndex from './assets/index.ts'
4
- import * as index from './index.ts'
5
- import * as renderIndex from './render/index.ts'
6
- import * as vmIndex from './vm/index.ts'
7
-
8
- describe('moonscratch/js/index.ts', () => {
9
- test('re-exports vm and assets APIs', () => {
10
- expect(index.createHeadlessVM).toBe(vmIndex.createHeadlessVM)
11
- expect(index.createVM).toBe(vmIndex.createVM)
12
- expect(index.moonscratch).toBe(vmIndex.moonscratch)
13
-
14
- expect(index.fromRgbaBytes).toBe(assetsIndex.fromRgbaBytes)
15
- expect(index.fromImageData).toBe(assetsIndex.fromImageData)
16
- expect(index.fromCanvas).toBe(assetsIndex.fromCanvas)
17
- expect(index.fromRgbaMatrix).toBe(assetsIndex.fromRgbaMatrix)
18
- expect(index.fromImageBytes).toBe(assetsIndex.fromImageBytes)
19
- expect(index.fromImageFile).toBe(assetsIndex.fromImageFile)
20
-
21
- expect(index.renderWithSVG).toBe(renderIndex.renderWithSVG)
22
- expect(index.renderWithSharp).toBe(renderIndex.renderWithSharp)
23
- expect(index.renderWithWebGL).toBe(renderIndex.renderWithWebGL)
24
- expect(index.normalizeRenderFrame).toBe(renderIndex.normalizeRenderFrame)
25
- })
26
- })
package/js/index.ts DELETED
@@ -1,3 +0,0 @@
1
- export * from './assets/index.ts'
2
- export * from './render/index.ts'
3
- export * from './vm/index.ts'
@@ -1,65 +0,0 @@
1
- import { describe, expect, test, vi } from 'vite-plus/test'
2
-
3
- import { renderWithSharp, renderWithSVG, renderWithWebGL } from './index.ts'
4
-
5
- const sharpCalls: unknown[] = []
6
-
7
- vi.mock('sharp', () => ({
8
- default: vi.fn((input?: unknown, options?: unknown) => {
9
- sharpCalls.push([input, options])
10
- return {
11
- raw() {
12
- return this
13
- },
14
- png() {
15
- return this
16
- },
17
- jpeg() {
18
- return this
19
- },
20
- webp() {
21
- return this
22
- },
23
- async toBuffer() {
24
- return Buffer.from([1, 2, 3, 4])
25
- },
26
- }
27
- }),
28
- }))
29
-
30
- describe('moonscratch/js/render', () => {
31
- const frame = {
32
- width: 2,
33
- height: 2,
34
- pixels: new Uint8Array([
35
- 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255,
36
- ]),
37
- }
38
-
39
- test('renderWithSVG builds a compact svg payload', () => {
40
- const svg = renderWithSVG(frame)
41
- expect(svg).toContain('<svg')
42
- expect(svg).toContain('shape-rendering="crispEdges"')
43
- expect(svg).toContain('fill="rgb(255,0,0)"')
44
- expect(svg).toContain('</svg>')
45
- })
46
-
47
- test('renderWithSharp calls sharp and returns buffer', async () => {
48
- const buffer = await renderWithSharp(frame)
49
- expect(sharpCalls).toEqual([
50
- [frame.pixels, { raw: { width: 2, height: 2, channels: 4 } }],
51
- ])
52
- expect(buffer).toEqual(Buffer.from([1, 2, 3, 4]))
53
- })
54
-
55
- test('renderWithWebGL throws when canvas is not available', () => {
56
- expect(() =>
57
- renderWithWebGL({
58
- ...frame,
59
- width: 1,
60
- height: 1,
61
- pixels: new Uint8Array([0, 0, 0, 255]),
62
- }),
63
- ).toThrow('renderWithWebGL')
64
- })
65
- })
@@ -1,13 +0,0 @@
1
- export { renderWithSharp } from './sharp.ts'
2
- export { renderWithSVG } from './svg.ts'
3
- export type {
4
- RenderFrame,
5
- RenderFrameLike,
6
- RenderImageData,
7
- RenderWithSharpOptions,
8
- RenderWithWebGLOptions,
9
- RenderWithWebGLResult,
10
- WebGLRenderResult,
11
- } from './types.ts'
12
- export { normalizeRenderFrame, normalizeRenderFrameTrusted } from './utils.ts'
13
- export { renderWithWebGL } from './webgl.ts'
@@ -1,87 +0,0 @@
1
- import type { RenderFrame, RenderWithSharpOptions } from './types.ts'
2
- import { normalizeRenderFrame } from './utils.ts'
3
-
4
- type SharpNamespace = {
5
- default?: SharpFactory
6
- sharp?: SharpFactory
7
- }
8
-
9
- type SharpFactory = (input?: unknown, options?: unknown) => SharpPipeline
10
-
11
- type SharpPipeline = {
12
- raw?(): SharpPipeline
13
- png?(): SharpPipeline
14
- jpeg?(): SharpPipeline
15
- webp?(): SharpPipeline
16
- toBuffer(): Promise<unknown>
17
- }
18
-
19
- const loadSharp = async (): Promise<
20
- (input?: unknown, options?: unknown) => SharpPipeline
21
- > => {
22
- try {
23
- const sharp = (await import('sharp')) as unknown as SharpNamespace
24
- if (sharp.default) {
25
- return sharp.default
26
- }
27
- const direct = sharp.sharp
28
- if (typeof direct === 'function') {
29
- return direct
30
- }
31
- throw new Error('invalid sharp module shape')
32
- } catch (error) {
33
- const reason = error instanceof Error ? error.message : String(error)
34
- throw new Error(
35
- `sharp is required to render PNG/WebP/JPEG output: ${reason}`,
36
- )
37
- }
38
- }
39
-
40
- export const renderWithSharp = async (
41
- input: RenderFrame,
42
- options: RenderWithSharpOptions = {},
43
- ): Promise<Buffer> => {
44
- const frame = normalizeRenderFrame(input)
45
- if (frame.width <= 0 || frame.height <= 0) {
46
- return Buffer.from([])
47
- }
48
-
49
- const sharp = await loadSharp()
50
- const pipeline = sharp(frame.pixels, {
51
- raw: { width: frame.width, height: frame.height, channels: 4 },
52
- })
53
- const format = options.format ?? 'png'
54
- let current = pipeline
55
- switch (format) {
56
- case 'png':
57
- if (typeof current.png === 'function') {
58
- current = current.png()
59
- }
60
- break
61
- case 'jpeg':
62
- case 'jpg':
63
- if (typeof current.jpeg === 'function') {
64
- current = current.jpeg()
65
- }
66
- break
67
- case 'webp':
68
- if (typeof current.webp === 'function') {
69
- current = current.webp()
70
- }
71
- break
72
- default:
73
- throw new Error(`unsupported sharp output format: ${String(format)}`)
74
- }
75
-
76
- if (typeof current.toBuffer !== 'function') {
77
- throw new Error('sharp pipeline does not expose toBuffer')
78
- }
79
- const rawBuffer = await current.toBuffer()
80
- if (rawBuffer instanceof Buffer) {
81
- return rawBuffer
82
- }
83
- if (rawBuffer instanceof Uint8Array) {
84
- return Buffer.from(rawBuffer)
85
- }
86
- throw new Error('sharp returned unsupported buffer type')
87
- }
package/js/render/svg.ts DELETED
@@ -1,68 +0,0 @@
1
- import type { RenderFrame } from './types.ts'
2
- import { normalizeRenderFrame } from './utils.ts'
3
-
4
- const appendRect = (
5
- parts: string[],
6
- x: number,
7
- y: number,
8
- width: number,
9
- rgba: [number, number, number, number],
10
- ): void => {
11
- const [r, g, b, a] = rgba
12
- if (width <= 0 || a <= 0) {
13
- return
14
- }
15
- const opacity = a < 255 ? ` fill-opacity="${a / 255}"` : ''
16
- parts.push(
17
- `<rect x="${x}" y="${y}" width="${width}" height="1" fill="rgb(${r},${g},${b})"${opacity}/>`,
18
- )
19
- }
20
-
21
- const isPixelEqual = (
22
- pixels: Uint8Array,
23
- base: number,
24
- otherBase: number,
25
- ): boolean =>
26
- pixels[base] === pixels[otherBase] &&
27
- pixels[base + 1] === pixels[otherBase + 1] &&
28
- pixels[base + 2] === pixels[otherBase + 2] &&
29
- pixels[base + 3] === pixels[otherBase + 3]
30
-
31
- const pixelByte = (value: number | undefined): number => {
32
- if (typeof value !== 'number' || !Number.isFinite(value)) {
33
- return 0
34
- }
35
- return value
36
- }
37
-
38
- export const renderWithSVG = (frame: RenderFrame): string => {
39
- const normalized = normalizeRenderFrame(frame)
40
- const { width, height, pixels } = normalized
41
- const parts = [
42
- `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" shape-rendering="crispEdges">`,
43
- ]
44
-
45
- for (let y = 0; y < height; y += 1) {
46
- let x = 0
47
- while (x < width) {
48
- const base = (y * width + x) * 4
49
- const r = pixelByte(pixels[base])
50
- const g = pixelByte(pixels[base + 1])
51
- const b = pixelByte(pixels[base + 2])
52
- const a = pixelByte(pixels[base + 3])
53
- let runWidth = 1
54
- while (x + runWidth < width) {
55
- const nextBase = (y * width + x + runWidth) * 4
56
- if (!isPixelEqual(pixels, base, nextBase)) {
57
- break
58
- }
59
- runWidth += 1
60
- }
61
- appendRect(parts, x, y, runWidth, [r, g, b, a])
62
- x += runWidth
63
- }
64
- }
65
-
66
- parts.push('</svg>')
67
- return parts.join('')
68
- }
@@ -1,35 +0,0 @@
1
- import type { CanvasLike } from '../assets/types.ts'
2
-
3
- export interface RenderFrame {
4
- width: number
5
- height: number
6
- pixels: Uint8Array
7
- }
8
-
9
- export interface RenderFrameLike {
10
- width: number
11
- height: number
12
- pixels: ArrayLike<number>
13
- }
14
-
15
- export interface RenderImageData {
16
- width: number
17
- height: number
18
- data: Uint8ClampedArray
19
- }
20
-
21
- export interface RenderWithSharpOptions {
22
- format?: 'png' | 'jpeg' | 'jpg' | 'webp'
23
- }
24
-
25
- export interface RenderWithWebGLOptions {
26
- canvas?: CanvasLike
27
- }
28
-
29
- export interface RenderWithWebGLResult {
30
- canvas: unknown
31
- toImageData: () => RenderImageData
32
- toImageElement: () => unknown
33
- }
34
-
35
- export type WebGLRenderResult = RenderWithWebGLResult
@@ -1,108 +0,0 @@
1
- import type { RenderFrame, RenderFrameLike } from './types.ts'
2
-
3
- const clampByte = (value: unknown): number => {
4
- const n = Number(value)
5
- if (!Number.isFinite(n)) {
6
- return 0
7
- }
8
- const out = Math.trunc(n)
9
- if (out <= 0) {
10
- return 0
11
- }
12
- if (out >= 255) {
13
- return 255
14
- }
15
- return out
16
- }
17
-
18
- const toByteArray = (value: ArrayLike<number>): ArrayLike<number> => {
19
- return value
20
- }
21
-
22
- const normalizeRenderFrameMeta = (
23
- input: RenderFrameLike,
24
- ): {
25
- width: number
26
- height: number
27
- expectedLength: number
28
- rawPixels: ArrayLike<number>
29
- } => {
30
- if (
31
- typeof input !== 'object' ||
32
- input === null ||
33
- typeof input.width !== 'number' ||
34
- !Number.isFinite(input.width) ||
35
- typeof input.height !== 'number' ||
36
- !Number.isFinite(input.height)
37
- ) {
38
- throw new Error('render frame input must be a finite width/height object')
39
- }
40
- const width = input.width
41
- const height = input.height
42
- if (!Number.isFinite(width) || !Number.isFinite(height)) {
43
- throw new Error('render frame width/height must be finite numbers')
44
- }
45
- const normalizedWidth = Math.trunc(width)
46
- const normalizedHeight = Math.trunc(height)
47
- if (normalizedWidth < 0 || normalizedHeight < 0) {
48
- throw new Error('render frame dimensions must be non-negative')
49
- }
50
-
51
- const expectedLength = normalizedWidth * normalizedHeight * 4
52
- const rawPixels = toByteArray(input.pixels)
53
- return {
54
- width: normalizedWidth,
55
- height: normalizedHeight,
56
- expectedLength,
57
- rawPixels,
58
- }
59
- }
60
-
61
- export const normalizeRenderFrame = (input: RenderFrameLike): RenderFrame => {
62
- const { width, height, expectedLength, rawPixels } =
63
- normalizeRenderFrameMeta(input)
64
- const pixels = new Uint8Array(expectedLength)
65
- const sourceLength = Math.min(expectedLength, rawPixels.length)
66
- for (let i = 0; i < sourceLength; i += 1) {
67
- pixels[i] = clampByte(rawPixels[i])
68
- }
69
- return {
70
- width,
71
- height,
72
- pixels,
73
- }
74
- }
75
-
76
- export const normalizeRenderFrameTrusted = (
77
- input: RenderFrameLike,
78
- ): RenderFrame => {
79
- const { width, height, expectedLength, rawPixels } =
80
- normalizeRenderFrameMeta(input)
81
- if (rawPixels instanceof Uint8Array) {
82
- return {
83
- width,
84
- height,
85
- pixels:
86
- rawPixels.length === expectedLength
87
- ? rawPixels
88
- : rawPixels.subarray(0, expectedLength),
89
- }
90
- }
91
- if (Array.isArray(rawPixels) && rawPixels.length >= expectedLength) {
92
- return {
93
- width,
94
- height,
95
- pixels: rawPixels as unknown as Uint8Array,
96
- }
97
- }
98
- const pixels = new Uint8Array(expectedLength)
99
- const sourceLength = Math.min(expectedLength, rawPixels.length)
100
- for (let i = 0; i < sourceLength; i += 1) {
101
- pixels[i] = rawPixels[i] as number
102
- }
103
- return {
104
- width,
105
- height,
106
- pixels,
107
- }
108
- }