lecodes-sdk 1.2.0 → 2.0.1

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 (260) hide show
  1. package/README.md +5 -2
  2. package/dist/editor.d.ts +12 -0
  3. package/dist/global.d.ts +4 -8
  4. package/dist/host.d.ts +2 -3
  5. package/dist/types/animate/tween/Animation.d.ts +0 -3
  6. package/dist/types/animate/tween/animateValue.d.ts +4 -2
  7. package/dist/types/animate/tween/easing.d.ts +8 -0
  8. package/dist/types/animate/tween/spec.d.ts +15 -7
  9. package/dist/types/audio/audio.d.ts +2 -1
  10. package/dist/types/canvas/Canvas.d.ts +40 -108
  11. package/dist/types/canvas/gen/cssColor.d.ts +17 -0
  12. package/dist/types/canvas/gen/recorder.d.ts +118 -0
  13. package/dist/types/canvas/gen/spec.d.ts +144 -0
  14. package/dist/types/core/color.d.ts +3 -1
  15. package/dist/types/core/pins.d.ts +18 -0
  16. package/dist/types/g2/Node2D.d.ts +5 -8
  17. package/dist/types/g2/Scene2D.d.ts +6 -2
  18. package/dist/types/gl/Foliage.d.ts +30 -7
  19. package/dist/types/gl/Light.d.ts +8 -0
  20. package/dist/types/gl/Lightmap.d.ts +13 -2
  21. package/dist/types/gl/Material.d.ts +17 -3
  22. package/dist/types/gl/Model.d.ts +6 -2
  23. package/dist/types/gl/Node.d.ts +3 -6
  24. package/dist/types/gl/Scene.d.ts +41 -13
  25. package/dist/types/gl/Texture.d.ts +1 -1
  26. package/dist/types/gl/animation/Locomotion.d.ts +8 -1
  27. package/dist/types/inject.d.ts +11 -11
  28. package/dist/types/inject.editor.d.ts +1 -0
  29. package/dist/types/net/core.d.ts +7 -0
  30. package/dist/types/plugin.d.ts +76 -0
  31. package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
  32. package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
  33. package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
  34. package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
  35. package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
  36. package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
  37. package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
  38. package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
  39. package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
  40. package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
  41. package/dist/types/runtime/app.d.ts +9 -2
  42. package/dist/types/runtime/fetch.d.ts +2 -0
  43. package/dist/types/runtime/input.d.ts +1 -1
  44. package/dist/types/runtime/media.d.ts +6 -10
  45. package/dist/types/runtime/misc.d.ts +4 -1
  46. package/dist/types/runtime/net.d.ts +3 -2
  47. package/dist/types/runtime/touch.d.ts +32 -0
  48. package/dist/types/scene/defineScene.d.ts +43 -2
  49. package/dist/types/scene/editor.d.ts +52 -0
  50. package/dist/types/scene/gizmos.d.ts +7 -4
  51. package/dist/types/ui/NativeView.d.ts +6 -4
  52. package/dist/types/ui/UI.d.ts +1 -1
  53. package/dist/types/ui/UIBottomSheet.d.ts +6 -12
  54. package/dist/types/ui/UIButton.d.ts +12 -14
  55. package/dist/types/ui/UIContainer.d.ts +0 -6
  56. package/dist/types/ui/UIImage.d.ts +1 -4
  57. package/dist/types/ui/UIInput.d.ts +8 -24
  58. package/dist/types/ui/UIModal.d.ts +0 -2
  59. package/dist/types/ui/UINode.d.ts +70 -56
  60. package/dist/types/ui/UIPager.d.ts +28 -27
  61. package/dist/types/ui/UIPopover.d.ts +0 -2
  62. package/dist/types/ui/UIScreen.d.ts +12 -17
  63. package/dist/types/ui/UIScrollable.d.ts +1 -4
  64. package/dist/types/ui/UIText.d.ts +0 -2
  65. package/dist/types/ui/UIVideo.d.ts +3 -5
  66. package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
  67. package/dist/types/ui/UIWidget.d.ts +10 -9
  68. package/dist/types/ui/colorKeys.gen.d.ts +9 -0
  69. package/dist/types/ui/presentable.d.ts +46 -32
  70. package/dist/types/ui/router.d.ts +18 -7
  71. package/dist/types/ui/styleColor.d.ts +1 -0
  72. package/dist/types/ui/transitions.d.ts +18 -0
  73. package/dist/types/ui/tree.d.ts +75 -0
  74. package/dist/types/version.d.ts +10 -0
  75. package/dist/types.json +1 -1
  76. package/package.json +12 -3
  77. package/prompts/2d.md +2 -6
  78. package/prompts/3d.md +1 -5
  79. package/prompts/README.md +1 -1
  80. package/prompts/canvas.md +9 -8
  81. package/prompts/compose.ts +1 -1
  82. package/prompts/core.md +3 -3
  83. package/prompts/design.md +1 -1
  84. package/prompts/dist/2d-game.md +473 -239
  85. package/prompts/dist/3d-app.md +553 -205
  86. package/prompts/dist/ar-app.md +435 -202
  87. package/prompts/dist/design.md +113 -95
  88. package/prompts/dist/ui-app.md +386 -170
  89. package/prompts/ui-design.md +2 -3
  90. package/prompts/ui.md +45 -37
  91. package/src/animate/tween/Animation.ts +34 -150
  92. package/src/animate/tween/Timeline.ts +175 -175
  93. package/src/animate/tween/animateValue.ts +6 -3
  94. package/src/animate/tween/easing.ts +10 -3
  95. package/src/animate/tween/spec.ts +41 -15
  96. package/src/audio/Sound.ts +3 -3
  97. package/src/audio/audio.ts +2 -1
  98. package/src/bridges/2d.d.ts +317 -0
  99. package/src/bridges/app.d.ts +91 -0
  100. package/src/bridges/audio.d.ts +97 -0
  101. package/src/bridges/canvas.d.ts +79 -0
  102. package/src/bridges/device.d.ts +72 -0
  103. package/src/bridges/fetch.d.ts +80 -0
  104. package/src/bridges/files.d.ts +70 -0
  105. package/src/bridges/gl.d.ts +1133 -0
  106. package/src/bridges/input.d.ts +72 -0
  107. package/src/bridges/media.d.ts +55 -0
  108. package/src/bridges/nav.d.ts +71 -0
  109. package/src/bridges/net.d.ts +52 -0
  110. package/src/bridges/service.d.ts +52 -0
  111. package/src/bridges/socket.d.ts +31 -0
  112. package/src/bridges/storage.d.ts +33 -0
  113. package/src/bridges/tree.d.ts +301 -0
  114. package/src/bridges/types.d.ts +49 -0
  115. package/src/canvas/Canvas.ts +114 -159
  116. package/src/canvas/gen/cssColor.ts +224 -0
  117. package/src/canvas/gen/recorder.ts +212 -0
  118. package/src/canvas/gen/spec.ts +201 -0
  119. package/src/chisel.ts +193 -0
  120. package/src/compile/assetMacro.ts +1 -1
  121. package/src/compile/bundler.ts +11 -2
  122. package/src/compile/compileProject.ts +43 -4
  123. package/src/compile/fontMacro.ts +3 -4
  124. package/src/compile/header.ts +26 -5
  125. package/src/compile/index.ts +3 -1
  126. package/src/compile/liteMaterial.ts +1 -1
  127. package/src/compile/sceneEditor.ts +11 -26
  128. package/src/core/color.ts +73 -30
  129. package/src/core/pins.ts +51 -0
  130. package/src/core/signals.ts +8 -1
  131. package/src/g2/CharacterController2D.ts +3 -3
  132. package/src/g2/Node2D.ts +57 -39
  133. package/src/g2/Physics2D.ts +2 -2
  134. package/src/g2/Scene2D.ts +35 -23
  135. package/src/g2/Texture2D.ts +1 -1
  136. package/src/g2/loop.ts +4 -4
  137. package/src/gl/CameraPlace.ts +52 -52
  138. package/src/gl/Foliage.ts +72 -17
  139. package/src/gl/Geometry.ts +1 -2
  140. package/src/gl/Light.ts +10 -0
  141. package/src/gl/Lightmap.ts +45 -29
  142. package/src/gl/Material.ts +95 -51
  143. package/src/gl/Mesh.ts +120 -120
  144. package/src/gl/Model.ts +21 -16
  145. package/src/gl/Node.ts +91 -24
  146. package/src/gl/Particles.ts +1 -1
  147. package/src/gl/Scene.ts +100 -44
  148. package/src/gl/Texture.ts +8 -7
  149. package/src/gl/animation/AnimationClip.ts +1 -1
  150. package/src/gl/animation/DynamicBone.ts +482 -482
  151. package/src/gl/animation/Locomotion.ts +8 -3
  152. package/src/gl/nav/NavMesh.ts +3 -4
  153. package/src/gl/physics/Physics.ts +2 -2
  154. package/src/gl/physics/physicsEvents.ts +3 -3
  155. package/src/gl/scenarios.ts +291 -291
  156. package/src/gl/terrain/Terrain.ts +4 -5
  157. package/src/gl/touch.ts +14 -15
  158. package/src/host.d.ts +2 -3
  159. package/src/inject.editor.ts +7 -0
  160. package/src/inject.ts +13 -16
  161. package/src/net/core.ts +6 -5
  162. package/src/net/index.ts +1 -1
  163. package/src/net/replication.ts +1 -1
  164. package/src/plugin.ts +191 -0
  165. package/src/plugins/gen/camera/contract.d.ts +27 -0
  166. package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
  167. package/src/plugins/gen/camera/sdk/camera.ts +57 -0
  168. package/src/plugins/gen/geolocation/contract.d.ts +50 -0
  169. package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
  170. package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
  171. package/src/plugins/gen/map/contract.d.ts +144 -0
  172. package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
  173. package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
  174. package/src/plugins/gen/push/contract.d.ts +61 -0
  175. package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
  176. package/src/plugins/gen/push/sdk/push.ts +105 -0
  177. package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
  178. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
  179. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
  180. package/src/plugins/permission.ts +5 -4
  181. package/src/runtime/app.ts +20 -9
  182. package/src/runtime/appEvents.ts +5 -4
  183. package/src/runtime/channel.ts +18 -15
  184. package/src/runtime/clipboard.ts +4 -3
  185. package/src/runtime/datetime.ts +2 -1
  186. package/src/runtime/device.ts +17 -15
  187. package/src/runtime/fetch.ts +30 -20
  188. package/src/runtime/files.ts +16 -15
  189. package/src/runtime/input.ts +12 -10
  190. package/src/runtime/media.ts +50 -46
  191. package/src/runtime/misc.ts +7 -3
  192. package/src/runtime/net.ts +8 -7
  193. package/src/runtime/rpc.ts +1 -3
  194. package/src/runtime/service.ts +19 -14
  195. package/src/runtime/share.ts +4 -3
  196. package/src/runtime/storage.ts +6 -4
  197. package/src/runtime/touch.ts +32 -0
  198. package/src/scene/defineScene.ts +61 -365
  199. package/src/scene/editor.ts +408 -0
  200. package/src/scene/editorPlugins.ts +3 -3
  201. package/src/scene/gizmos.ts +15 -9
  202. package/src/server/db/marci/query.ts +1 -1
  203. package/src/server/host.ts +1 -1
  204. package/src/server/runtime.ts +1 -1
  205. package/src/ui/NativeView.ts +72 -25
  206. package/src/ui/UI.ts +3 -3
  207. package/src/ui/UIBottomSheet.ts +16 -17
  208. package/src/ui/UIButton.ts +54 -16
  209. package/src/ui/UIContainer.ts +0 -6
  210. package/src/ui/UIImage.ts +34 -30
  211. package/src/ui/UIInput.ts +29 -37
  212. package/src/ui/UIModal.ts +1 -3
  213. package/src/ui/UINode.ts +347 -297
  214. package/src/ui/UIPager.ts +93 -78
  215. package/src/ui/UIPopover.ts +0 -2
  216. package/src/ui/UIScreen.ts +41 -36
  217. package/src/ui/UIScrollable.ts +19 -13
  218. package/src/ui/UISpacer.ts +1 -1
  219. package/src/ui/UITabs.ts +8 -6
  220. package/src/ui/UIText.ts +7 -17
  221. package/src/ui/UIVideo.ts +30 -27
  222. package/src/ui/UIVirtualizedList.ts +58 -59
  223. package/src/ui/UIWidget.ts +38 -18
  224. package/src/ui/colorKeys.gen.ts +37 -0
  225. package/src/ui/fonts.ts +2 -2
  226. package/src/ui/presentable.ts +59 -42
  227. package/src/ui/router.ts +52 -35
  228. package/src/ui/styleColor.ts +56 -0
  229. package/src/ui/theme.ts +7 -6
  230. package/src/ui/transitions.ts +249 -0
  231. package/src/ui/tree.ts +346 -0
  232. package/src/version.ts +24 -0
  233. package/tests/helpers/engineWorld.ts +11 -0
  234. package/tests/helpers/fakeTree.ts +353 -0
  235. package/tests/helpers/hostStubs.ts +31 -0
  236. package/tests/helpers/index.ts +14 -0
  237. package/tests/helpers/memoryMarci.ts +124 -0
  238. package/tests/helpers/phases.ts +23 -0
  239. package/tests/helpers/preload.ts +18 -0
  240. package/tests/helpers/stubApp.ts +2 -0
  241. package/tests/helpers/stubDevice.ts +2 -0
  242. package/tests/helpers/stubFetch.ts +2 -0
  243. package/tests/helpers/stubInput.ts +2 -0
  244. package/dist/inject.js +0 -4629
  245. package/dist/types/core/registry.d.ts +0 -7
  246. package/dist/types/plugins/camera.d.ts +0 -25
  247. package/dist/types/plugins/push.d.ts +0 -46
  248. package/src/bridges.d.ts +0 -1769
  249. package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
  250. package/src/compile/__tests__/assetMacro.test.ts +0 -100
  251. package/src/compile/__tests__/assetName.test.ts +0 -55
  252. package/src/compile/__tests__/compile.test.ts +0 -310
  253. package/src/compile/__tests__/detectEntry.test.ts +0 -151
  254. package/src/compile/__tests__/fontMacro.test.ts +0 -199
  255. package/src/compile/__tests__/serverSplit.test.ts +0 -27
  256. package/src/core/__tests__/stateMachine.test.ts +0 -132
  257. package/src/core/registry.ts +0 -23
  258. package/src/plugins/camera.ts +0 -81
  259. package/src/plugins/push.ts +0 -132
  260. package/src/plugins/qr.ts +0 -73
@@ -1,16 +1,19 @@
1
- // A core 2D drawing surface that bakes to a texture — see docs/canvas-contract.md. It is NOT part of
2
- // creator-2d/creator-gl: the texture it produces is consumed by 2D sprites, 3D materials and UI
3
- // images alike ("a canvas is just a texture"), so world/label text and arbitrary 2D graphics are
4
- // drawn once, per platform, on that platform's native 2D stack (web OffscreenCanvas, iOS CoreGraphics,
5
- // Android Canvas).
1
+ // A core 2D drawing surface that bakes to a texture. It is NOT part of creator-2d/creator-gl: the
2
+ // texture it produces is consumed by 2D sprites, 3D materials and UI images alike ("a canvas is just a
3
+ // texture"), so world/label text and arbitrary 2D graphics are drawn once, per platform, on that
4
+ // platform's native 2D stack.
6
5
  //
7
- // The API looks immediate but RECORDS commands into a compact buffer and flushes them to the backend
8
- // ONCE per bake (one bridge crossing, not one per lineTo). Colors and fonts are CSS strings, resolved
9
- // by the platform. Do all drawing AFTER `await registerFont(...)` — an unloaded font mis-measures.
6
+ // The drawing API is the AnyCanvas recorder (engines/canvas, vendored into ./gen by `bun run
7
+ // vendor:canvas`): a Canvas2D-shaped API that RECORDS an opcode stream. `_bakeSurface` flushes the
8
+ // stream to `_creatorCanvas` ONCE per bake (one bridge crossing, not one per lineTo); the runtime's
9
+ // AnyCanvas core turns it into a draw list the host's painter replays. Colors and fonts are CSS
10
+ // strings, resolved by the core. Do all drawing AFTER `await registerFont(...)` — an unloaded font
11
+ // mis-measures.
10
12
  //
11
13
  // State (fillStyle, font, textAlign, …) is get/set like the browser; drawing calls are methods (they
12
14
  // return `this`, so drawing still chains). `pixelRatio` is the device-pixel multiplier (browser-style
13
- // `scale(sx, sy)` is a transform).
15
+ // `scale(sx, sy)` is a transform). Gradients (`createLinearGradient` / `createRadialGradient`),
16
+ // `setLineDash`, `clip`, `arcTo`, `ellipse`, `roundRect`, `letterSpacing` all work everywhere.
14
17
  //
15
18
  // const c = new Canvas(160, 40, { pixelRatio: 2 })
16
19
  // c.font = 'bold 24px Inter'
@@ -19,39 +22,40 @@
19
22
  // c.fillStyle = '#000a'; c.roundRect(0, 0, w, 40, 12).fill()
20
23
  // c.fillStyle = '#fff'; c.textAlign = 'center'; c.fillText('Alice', w / 2, 27)
21
24
  // new Sprite({ texture: c }) // Canvas is a valid texture source everywhere
25
+ //
26
+ // A surface is an owned resource (docs/owned.md): a Canvas / Bitmap keeps the runtime's Handle as `_h`
27
+ // and puts `_h.id` on the wire. Once the wrapper is dropped AND no UIImage shows the surface, the
28
+ // runtime frees it; `destroy()` is the optional early free. No freed-channel registration
29
+ // (registerFreedKind): the runtime never reports a canvas free — a drop needs no report, destroy() is
30
+ // the wrapper's own call, and a world's teardown only frees surfaces whose wrappers die with it.
22
31
 
32
+ import { DEAD_HANDLE } from "../core/pins"
23
33
  import { Texture2D } from "../g2/Texture2D"
24
34
  import { File, type FetchResponse } from "../runtime/fetch"
25
35
  import type { SvgSourceValue } from "../runtime/misc"
26
-
27
- // Opcodes — MUST stay in sync with docs/canvas-contract.md.
28
- const OP = {
29
- SAVE: 0, RESTORE: 1, TRANSLATE: 2, SCALE: 3, ROTATE: 4, GLOBAL_ALPHA: 5,
30
- FILL_STYLE: 6, STROKE_STYLE: 7, LINE_WIDTH: 8, LINE_JOIN: 9, LINE_CAP: 10,
31
- FONT: 11, TEXT_ALIGN: 12, TEXT_BASELINE: 13,
32
- BEGIN_PATH: 14, CLOSE_PATH: 15, MOVE_TO: 16, LINE_TO: 17, QUADRATIC_TO: 18, BEZIER_TO: 19,
33
- ARC: 20, RECT: 21, ROUND_RECT: 22, FILL: 23, STROKE: 24,
34
- FILL_RECT: 25, STROKE_RECT: 26, CLEAR_RECT: 27, FILL_TEXT: 28, STROKE_TEXT: 29,
35
- DRAW_IMAGE: 30,
36
- } as const
36
+ import { Recorder, Gradient, type ImageRef, type TextAlignName, type TextBaselineName, type LineJoinName, type LineCapName, type FillRuleName } from "./gen/recorder"
37
+
38
+ export { Gradient }
39
+ export type TextAlign = TextAlignName
40
+ export type TextBaseline = TextBaselineName
41
+ export type LineJoin = LineJoinName
42
+ export type LineCap = LineCapName
43
+ export type FillRule = FillRuleName
44
+ export interface CanvasTextMetrics { width: number, ascent: number, descent: number }
37
45
 
38
46
  // Above this many buffered values, a buffer that is STILL growing between bakes is a leak, not a
39
47
  // drawing (a dense static picture is a few thousand; a 15 Hz graph without reset() passes this in
40
48
  // seconds and doubles its frame cost every minute).
41
49
  const CMD_GROWTH_WARN = 20000
42
50
 
43
- const ALIGN = { left: 0, center: 1, right: 2, start: 3, end: 4 } as const
44
- const BASELINE = { alphabetic: 0, top: 1, middle: 2, bottom: 3, hanging: 4, ideographic: 5 } as const
45
- const JOIN = { miter: 0, round: 1, bevel: 2 } as const
46
- const CAP = { butt: 0, round: 1, square: 2 } as const
47
-
48
- export type TextAlign = keyof typeof ALIGN
49
- export type TextBaseline = keyof typeof BASELINE
50
- export type LineJoin = keyof typeof JOIN
51
- export type LineCap = keyof typeof CAP
52
- export interface CanvasTextMetrics { width: number, ascent: number, descent: number }
51
+ /** The runtime's answer for a surface → the handle a wrapper keeps. A Handle from a runtime with the
52
+ * canvas kind; a plain id from one built before it (the committed wasm blobs, an installed phone lib,
53
+ * a stale server) or a test stub → `{ id }`, which nothing finalizes (freed by destroy() or the
54
+ * world's teardown, as before); 0 / null → DEAD_HANDLE. */
55
+ const surfaceHandle = (r: Handle | number | null | undefined): Handle =>
56
+ typeof r === "number" ? (r > 0 ? { id: r } : DEAD_HANDLE) : (r ?? DEAD_HANDLE)
53
57
 
54
- export class Canvas {
58
+ export class Canvas extends Recorder {
55
59
  /** Brand so `Sprite`/`UIImage`/`Material` can accept a Canvas anywhere a texture source is expected. */
56
60
  readonly __canvas = true as const
57
61
 
@@ -60,25 +64,18 @@ export class Canvas {
60
64
  /** Device-pixel multiplier (draw at 2 for crisp retina text). Applied by the backend. */
61
65
  pixelRatio: number
62
66
 
63
- private _cmd: number[] = []
64
- private _refs: string[] = []
65
- // Shadowed drawing state so the get/set accessors read back the current value (like the browser).
66
- private _globalAlpha = 1
67
- private _fillStyle = "#000000"
68
- private _strokeStyle = "#000000"
69
- private _lineWidth = 1
70
- private _lineJoin: LineJoin = "miter"
71
- private _lineCap: LineCap = "butt"
72
- private _font = "10px sans-serif"
73
- private _textAlign: TextAlign = "start"
74
- private _textBaseline: TextBaseline = "alphabetic"
75
- private _surfaceId = 0
67
+ /** @internal The surface's owned handle (`_h.id` = the surface id; DEAD_HANDLE until the first bake
68
+ * and after destroy()). */
69
+ _h: Handle = DEAD_HANDLE
70
+ // The Bitmaps a recorded DRAW_IMAGE names: every bake replays the recording, so each must outlive
71
+ // the op (a dropped Bitmap's surface would go on the drain). Cleared by reset(), which drops the ops.
72
+ private _drawn?: Set<Bitmap>
76
73
  private _tex2d?: Texture2D // cached 2D texture, re-uploaded on update()
77
74
  /** @internal — cached 3D (creator-gl) texture id, set by gl/Texture.fromCanvas; re-uploaded on
78
75
  * update(). Kept as a bare id (not a Texture import) so a 2D-only canvas never pulls the 3D engine. */
79
76
  _tex3dId = 0
80
77
  // Set once this canvas is baked as a UIImage source (see _bakeSurfaceForUI). When true, update()
81
- // also refreshes the on-screen image node(s) via _creatorUI — the UI consumer the 2D/3D texture
78
+ // also refreshes the on-screen image node(s) via _creatorTree — the UI consumer the 2D/3D texture
82
79
  // hooks below don't cover. Stays a bare flag so a non-UI canvas never references the UI bridge.
83
80
  private _uiBound = false
84
81
  // Growth watch: the command buffer is a RECORDING that every bake replays in full, so a canvas
@@ -88,127 +85,66 @@ export class Canvas {
88
85
  private _warnedGrowth = false
89
86
 
90
87
  constructor(width: number, height: number, opts: { pixelRatio?: number } = {}) {
88
+ super()
91
89
  this.width = width
92
90
  this.height = height
93
91
  this.pixelRatio = opts.pixelRatio ?? 1
94
92
  }
95
93
 
96
- private _ref(s: string): number {
97
- const i = this._refs.indexOf(s)
98
- if (i >= 0) return i
99
- this._refs.push(s)
100
- return this._refs.length - 1
101
- }
102
-
103
- /** Discard the recorded drawing to start a new one (for dynamic canvases). */
104
- reset(): this { this._cmd.length = 0; this._refs.length = 0; return this }
105
94
  /** Change the surface size; takes effect on the next bake. */
106
95
  resize(width: number, height: number): this { this.width = width; this.height = height; return this }
107
96
 
108
- // --- transforms & state stack (actions; chainable) ---
109
- save(): this { this._cmd.push(OP.SAVE); return this }
110
- restore(): this { this._cmd.push(OP.RESTORE); return this }
111
- translate(x: number, y: number): this { this._cmd.push(OP.TRANSLATE, x, y); return this }
112
- scale(sx: number, sy: number): this { this._cmd.push(OP.SCALE, sx, sy); return this }
113
- rotate(rad: number): this { this._cmd.push(OP.ROTATE, rad); return this }
114
-
115
- // --- drawing state (get/set like the browser; colors/fonts are CSS strings resolved by the platform).
116
- // The setter records the state op AND shadows the value so the getter reads it back.
117
- get globalAlpha(): number { return this._globalAlpha }
118
- set globalAlpha(a: number) { this._globalAlpha = a; this._cmd.push(OP.GLOBAL_ALPHA, a) }
119
- get fillStyle(): string { return this._fillStyle }
120
- set fillStyle(color: string) { this._fillStyle = color; this._cmd.push(OP.FILL_STYLE, this._ref(color)) }
121
- get strokeStyle(): string { return this._strokeStyle }
122
- set strokeStyle(color: string) { this._strokeStyle = color; this._cmd.push(OP.STROKE_STYLE, this._ref(color)) }
123
- get lineWidth(): number { return this._lineWidth }
124
- set lineWidth(w: number) { this._lineWidth = w; this._cmd.push(OP.LINE_WIDTH, w) }
125
- get lineJoin(): LineJoin { return this._lineJoin }
126
- set lineJoin(j: LineJoin) { this._lineJoin = j; this._cmd.push(OP.LINE_JOIN, JOIN[j]) }
127
- get lineCap(): LineCap { return this._lineCap }
128
- set lineCap(c: LineCap) { this._lineCap = c; this._cmd.push(OP.LINE_CAP, CAP[c]) }
129
- get font(): string { return this._font }
130
- set font(f: string) { this._font = f; this._cmd.push(OP.FONT, this._ref(f)) }
131
- get textAlign(): TextAlign { return this._textAlign }
132
- set textAlign(a: TextAlign) { this._textAlign = a; this._cmd.push(OP.TEXT_ALIGN, ALIGN[a]) }
133
- get textBaseline(): TextBaseline { return this._textBaseline }
134
- set textBaseline(b: TextBaseline) { this._textBaseline = b; this._cmd.push(OP.TEXT_BASELINE, BASELINE[b]) }
135
-
136
- // --- paths ---
137
- beginPath(): this { this._cmd.push(OP.BEGIN_PATH); return this }
138
- closePath(): this { this._cmd.push(OP.CLOSE_PATH); return this }
139
- moveTo(x: number, y: number): this { this._cmd.push(OP.MOVE_TO, x, y); return this }
140
- lineTo(x: number, y: number): this { this._cmd.push(OP.LINE_TO, x, y); return this }
141
- quadraticCurveTo(cx: number, cy: number, x: number, y: number): this { this._cmd.push(OP.QUADRATIC_TO, cx, cy, x, y); return this }
142
- bezierCurveTo(c1x: number, c1y: number, c2x: number, c2y: number, x: number, y: number): this {
143
- this._cmd.push(OP.BEZIER_TO, c1x, c1y, c2x, c2y, x, y); return this
144
- }
145
- arc(x: number, y: number, r: number, a0: number, a1: number, ccw = false): this {
146
- this._cmd.push(OP.ARC, x, y, r, a0, a1, ccw ? 1 : 0); return this
147
- }
148
- rect(x: number, y: number, w: number, h: number): this { this._cmd.push(OP.RECT, x, y, w, h); return this }
149
- roundRect(x: number, y: number, w: number, h: number, r: number): this { this._cmd.push(OP.ROUND_RECT, x, y, w, h, r); return this }
150
- fill(evenOdd = false): this { this._cmd.push(OP.FILL, evenOdd ? 1 : 0); return this }
151
- stroke(): this { this._cmd.push(OP.STROKE); return this }
152
-
153
- // --- shape/text sugar ---
154
- fillRect(x: number, y: number, w: number, h: number): this { this._cmd.push(OP.FILL_RECT, x, y, w, h); return this }
155
- strokeRect(x: number, y: number, w: number, h: number): this { this._cmd.push(OP.STROKE_RECT, x, y, w, h); return this }
156
- clearRect(x: number, y: number, w: number, h: number): this { this._cmd.push(OP.CLEAR_RECT, x, y, w, h); return this }
157
- fillText(text: string, x: number, y: number, maxWidth = 0): this { this._cmd.push(OP.FILL_TEXT, this._ref(text), x, y, maxWidth); return this }
158
- strokeText(text: string, x: number, y: number, maxWidth = 0): this { this._cmd.push(OP.STROKE_TEXT, this._ref(text), x, y, maxWidth); return this }
159
-
160
97
  // --- images ---
161
- /** Blit a `Bitmap` (a snapshot from `toBitmap()`) onto this canvas. All coordinates are LOGICAL —
162
- * including the source sub-rect in the 9-arg form (it's scaled by the bitmap's pixelRatio here).
163
- * Records a DRAW_IMAGE op, so the blit runs at bake time: keep the Bitmap alive until the next
164
- * `update()`/`toBitmap()`/`texture()`. Three browser-style forms:
98
+ /** Blit a `Bitmap` (a snapshot from `toBitmap()` or `loadImage()`) onto this canvas. All coordinates
99
+ * are LOGICAL — including the source sub-rect in the 9-arg form (it's scaled by the bitmap's
100
+ * pixelRatio here). Records a DRAW_IMAGE op, so the blit runs at bake time; the canvas keeps the
101
+ * Bitmap alive until `reset()` removes the op. Three browser-style forms:
165
102
  * drawImage(bmp, dx, dy) — whole bitmap at its natural size
166
103
  * drawImage(bmp, dx, dy, dw, dh) — whole bitmap, scaled into dw×dh
167
104
  * drawImage(bmp, sx, sy, sw, sh, dx, dy, dw, dh) — a sub-rect of the bitmap, scaled into dw×dh */
168
- drawImage(img: Bitmap, dx: number, dy: number): this
169
- drawImage(img: Bitmap, dx: number, dy: number, dw: number, dh: number): this
170
- drawImage(img: Bitmap, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): this
171
- drawImage(img: Bitmap, a: number, b: number, c?: number, d?: number, e?: number, f?: number, g?: number, h?: number): this {
172
- const pr = img.pixelRatio
173
- let sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number
174
- if (e === undefined) { // (dx, dy) or (dx, dy, dw, dh): the whole bitmap
175
- sx = 0; sy = 0; sw = img.pixelWidth; sh = img.pixelHeight
176
- dx = a; dy = b; dw = c ?? img.width; dh = d ?? img.height
177
- } else { // (sx, sy, sw, sh, dx, dy, dw, dh): a sub-rect (logical → device px)
178
- sx = a * pr; sy = b * pr; sw = c! * pr; sh = d! * pr
179
- dx = e; dy = f!; dw = g!; dh = h!
180
- }
181
- this._cmd.push(OP.DRAW_IMAGE, img._surfaceId, sx, sy, sw, sh, dx, dy, dw, dh)
182
- return this
105
+ drawImage(img: Bitmap | ImageRef, dx: number, dy: number): this
106
+ drawImage(img: Bitmap | ImageRef, dx: number, dy: number, dw: number, dh: number): this
107
+ drawImage(img: Bitmap | ImageRef, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): this
108
+ drawImage(img: Bitmap | ImageRef, a: number, b: number, c?: number, d?: number, e?: number, f?: number, g?: number, h?: number): this {
109
+ if (!isBitmap(img)) return super.drawImage(img, a, b, c!, d!, e!, f!, g!, h!)
110
+ ;(this._drawn ??= new Set()).add(img)
111
+ const ref: ImageRef = { surface: img._h.id, width: img.pixelWidth, height: img.pixelHeight }
112
+ if (e === undefined) return super.drawImage(ref, 0, 0, ref.width, ref.height, a, b, c ?? img.width, d ?? img.height)
113
+ const pr = img.pixelRatio // the sub-rect is logical → device px
114
+ return super.drawImage(ref, a * pr, b * pr, c! * pr, d! * pr, e, f!, g!, h!)
183
115
  }
184
116
 
185
117
  /** Decode an image source into a standalone `Bitmap` you can blit with `drawImage()` — either an
186
- * `SvgSource(...)` (rasterized) or a `FetchResponse` holding already-fetched image bytes (decoded).
187
- * This is async because turning SVG markup / encoded bytes into pixels needs a decode step (on the
188
- * web, `createImageBitmap` / an `<img>` load); `await` it ONCE, then `drawImage(bitmap, …)` is
189
- * synchronous and the Bitmap is reusable. The returned Bitmap owns a native surface at the image's
190
- * natural pixel size (`pixelRatio` 1) — `destroy()` it when done. Typical use:
118
+ * `SvgSource(...)` (drawn by the core, painted by the host) or a `FetchResponse` holding already-fetched
119
+ * image bytes (decoded; an SVG file is detected). This is async because the web host decodes with the
120
+ * browser; `await` it ONCE, then `drawImage(bitmap, …)` is synchronous and the Bitmap is reusable. The
121
+ * returned Bitmap owns a native surface at the image's natural pixel size (`pixelRatio` 1), freed
122
+ * once nothing references the Bitmap (`destroy()` frees it early). Typical use:
191
123
  * const logo = await canvas.loadImage(SvgSource(svg)) // or: await fetch(url) → the response
192
124
  * canvas.drawImage(logo, 0, 0) // sync, blit as often as you like */
193
125
  loadImage(source: SvgSourceValue | FetchResponse): Promise<Bitmap> {
194
126
  // Discriminate without touching FetchResponse internals: only an SvgSource carries `.svg`.
195
127
  const isSvg = typeof (source as SvgSourceValue).svg === "string"
196
128
  const svg = isSvg ? (source as SvgSourceValue).svg : ""
197
- const bufferId = isSvg ? -1 : (source as any)._id // <0 = the SVG path; else the host buffer id
129
+ const bufferId = isSvg ? -1 : (source as any)._h.id // <0 = the SVG path; else the host buffer id
198
130
  return new Promise<Bitmap>((res, rej) => {
199
- _creatorCanvas.loadImage(svg, bufferId, (surfaceId, w, h) => res(new Bitmap(surfaceId, w, h, 1)), rej)
131
+ _creatorCanvas.loadImage(svg, bufferId, (surface, w, h) => res(new Bitmap(surface, w, h, 1)), rej)
200
132
  })
201
133
  }
202
134
 
203
- /** Measure `text` in the current font on THIS platform. The one synchronous round-trip. */
135
+ /** Measure `text` in the current font and letterSpacing on THIS platform. The one synchronous
136
+ * round-trip. */
204
137
  measureText(text: string): CanvasTextMetrics {
205
- const o = _creatorCanvas.measureText(text, this._font)
206
- return { width: o[0], ascent: o[1], descent: o[2] }
138
+ const o = _creatorCanvas.measureText(text, this.font)
139
+ // Like the browser: letterSpacing is added after every character (spec/ops.h; the painters draw it so).
140
+ const ls = this.letterSpacing
141
+ const extra = ls && Number.isFinite(ls) ? ls * [...text].length : 0
142
+ return { width: o[0] + extra, ascent: o[1], descent: o[2] }
207
143
  }
208
144
 
209
145
  /** @internal — rasterize the recorded commands into the surface; returns the surface id. */
210
146
  _bakeSurface(): number {
211
- const len = this._cmd.length
147
+ const len = this.length
212
148
  if (!this._warnedGrowth && len > CMD_GROWTH_WARN && this._lastBakeLen > 0 && len > this._lastBakeLen) {
213
149
  this._warnedGrowth = true
214
150
  console.warn(
@@ -219,9 +155,19 @@ export class Canvas {
219
155
  )
220
156
  }
221
157
  this._lastBakeLen = len
222
- const buf = new Float32Array(this._cmd)
223
- this._surfaceId = _creatorCanvas.rasterize(this._surfaceId, this.width, this.height, this.pixelRatio, buf, buf.length, this._refs)
224
- return this._surfaceId
158
+ const { cmd, refs } = this.stream()
159
+ const r = _creatorCanvas.rasterize(this._h.id, this.width, this.height, this.pixelRatio, cmd, cmd.length, refs)
160
+ // A Handle = a surface made now. A number = a re-bake (or an older runtime / a stub): the same id
161
+ // keeps the Handle OBJECT — replacing it would drop the real handle and free the surface under us.
162
+ if (typeof r === "object" && r !== null) this._h = r
163
+ else if (r !== this._h.id) this._h = surfaceHandle(r)
164
+ return this._h.id
165
+ }
166
+
167
+ /** Discard the recording (and the Bitmaps its drawImage ops kept alive). */
168
+ reset(): this {
169
+ this._drawn = undefined
170
+ return super.reset()
225
171
  }
226
172
 
227
173
  /** @internal — bake for a `UIImage(canvas)` source: same as `_bakeSurface()` but also flags this
@@ -245,18 +191,19 @@ export class Canvas {
245
191
  if (this._tex2d) _creator2d.updateTextureFromCanvas(this._tex2d.id, surfaceId)
246
192
  if (this._tex3dId) _creator.updateTextureFromCanvas(this._tex3dId, surfaceId)
247
193
  // UIImage(canvas): repaint any live image node showing this surface with the re-baked pixels —
248
- // draw-only, no relayout (the surface keeps its id and size). Feature-detected so an older host
249
- // without the hook just no-ops (fall back to re-assigning UIImage.src). See docs/canvas-contract.md.
250
- if (this._uiBound) _creatorUI.refreshCanvasSurface?.(surfaceId)
194
+ // draw-only, no relayout (the surface keeps its id and size). Feature-detected so a host without
195
+ // the hook just no-ops (fall back to re-assigning UIImage.src).
196
+ if (this._uiBound) _creatorTree.refreshCanvasSurface?.(surfaceId)
251
197
  return this
252
198
  }
253
199
 
254
200
  /** Copy the current pixels into a standalone, immutable `Bitmap` — independent of this canvas, so
255
201
  * later drawing/`reset()` never touches it. Draw it back with `drawImage()`, export it with
256
- * `toDataURL()`, or use it to "flatten" an ever-growing drawing so re-bakes stay O(1):
257
- * const snap = canvas.toBitmap()
258
- * canvas.reset(); canvas.drawImage(snap, 0, 0) // one op replaces the whole history
259
- * // …keep drawing on top; snap.destroy() once it's been drawn in. */
202
+ * `toFile()`, or use it to "flatten" an ever-growing drawing so re-bakes stay O(1):
203
+ * const next = canvas.toBitmap() // bakes with the previous snapshot still alive
204
+ * canvas.reset(); canvas.drawImage(next, 0, 0) // one op replaces the whole history
205
+ * The canvas keeps a drawn Bitmap alive while its drawImage is recorded, and the old snapshot goes
206
+ * on its own once reset() dropped it — no destroy() needed. */
260
207
  toBitmap(): Bitmap {
261
208
  return new Bitmap(_creatorCanvas.snapshot(this._bakeSurface()), this.width, this.height, this.pixelRatio)
262
209
  }
@@ -271,12 +218,14 @@ export class Canvas {
271
218
  })
272
219
  }
273
220
 
221
+ /** Free the surface now instead of when the canvas is dropped (an image still showing it goes blank).
222
+ * The recording stays: a later bake makes a new surface. */
274
223
  destroy(): void {
275
- if (this._surfaceId) _creatorCanvas.destroySurface(this._surfaceId)
224
+ if (this._h.id) _creatorCanvas.destroySurface(this._h.id)
225
+ this._h = DEAD_HANDLE
276
226
  this._tex2d?.destroy()
277
227
  // The 3D texture (if any) is owned by creator-gl's engine and freed at its teardown — no per-texture
278
228
  // destroy in the 3D API — so we just drop our cached id here.
279
- this._surfaceId = 0
280
229
  this._tex2d = undefined
281
230
  this._tex3dId = 0
282
231
  }
@@ -286,21 +235,23 @@ export class Canvas {
286
235
  export const isCanvas = (v: unknown): v is Canvas =>
287
236
  typeof v === "object" && v !== null && (v as { __canvas?: unknown }).__canvas === true
288
237
 
289
- /** An immutable RGBA8 snapshot of a canvas, produced by `Canvas.toBitmap()`. It owns its own baked
290
- * surface (a copy), so the source canvas can keep drawing or `reset()` without affecting it. Use it
291
- * to stamp/blit back (`canvas.drawImage(bitmap, …)`), to export (`bitmap.toDataURL()`), or as a
292
- * cheap "layer" you composite. Call `destroy()` when done — it holds a native surface. */
238
+ /** An immutable RGBA8 snapshot of a canvas, produced by `Canvas.toBitmap()` / `loadImage()`. It owns its
239
+ * own baked surface (a copy), so the source canvas can keep drawing or `reset()` without affecting it.
240
+ * Use it to stamp/blit back (`canvas.drawImage(bitmap, …)`), to export (`bitmap.toFile()`), or as a
241
+ * cheap "layer" you composite. Its surface is freed once nothing references the Bitmap (a canvas that
242
+ * drew it keeps it until `reset()`); `destroy()` frees it early. */
293
243
  export class Bitmap {
294
244
  readonly __bitmap = true as const
295
- /** @internal — a standalone `_creatorCanvas` surface (RGBA8), independent of any Canvas. */
296
- readonly _surfaceId: number
245
+ /** @internal — the owned handle of a standalone `_creatorCanvas` surface (RGBA8), independent of any
246
+ * Canvas (`_h.id` = the surface id; DEAD_HANDLE after destroy()). */
247
+ _h: Handle
297
248
  /** Logical width, matching the Canvas it was captured from (pixelRatio is crispness, not size). */
298
249
  readonly width: number
299
250
  readonly height: number
300
251
  readonly pixelRatio: number
301
252
 
302
- constructor(surfaceId: number, width: number, height: number, pixelRatio: number) {
303
- this._surfaceId = surfaceId
253
+ constructor(surface: Handle | number | null, width: number, height: number, pixelRatio: number) {
254
+ this._h = surfaceHandle(surface)
304
255
  this.width = width
305
256
  this.height = height
306
257
  this.pixelRatio = pixelRatio
@@ -313,12 +264,16 @@ export class Bitmap {
313
264
  /** Encode to an image `File` — 'image/png' (default) or 'image/jpeg'. See `Canvas.toFile`. */
314
265
  toFile(name = "canvas.png", type = "image/png"): Promise<File> {
315
266
  return new Promise<File>((res, rej) => {
316
- _creatorCanvas.toFile(this._surfaceId, type, (bufferId, size) => res(new File(bufferId, name, size)), rej)
267
+ _creatorCanvas.toFile(this._h.id, type, (bufferId, size) => res(new File(bufferId, name, size)), rej)
317
268
  })
318
269
  }
319
270
 
320
- /** Free the native surface. The bitmap must not be drawn or encoded afterwards. */
321
- destroy(): void { _creatorCanvas.destroySurface(this._surfaceId) }
271
+ /** Free the native surface now instead of when the bitmap is dropped. It must not be drawn or encoded
272
+ * afterwards (a canvas whose recording still draws it blits nothing). */
273
+ destroy(): void {
274
+ if (this._h.id) _creatorCanvas.destroySurface(this._h.id)
275
+ this._h = DEAD_HANDLE
276
+ }
322
277
  }
323
278
 
324
279
  /** Duck-type guard for a Bitmap (see `isCanvas`). */
@@ -0,0 +1,224 @@
1
+ // GENERATED by scripts/vendor-canvas.ts from engines/canvas/recorders/ts/src/cssColor.ts @ ae6bc30b952b — do not edit here.
2
+ // Bump the submodule and run `bun run vendor:canvas`; `bun run check:canvas-ops` fails when this copy is stale.
3
+ // The CSS color parser — the TypeScript twin of core/include/anycanvas/css_color.h: the same grammar,
4
+ // the same name table, float32 emulated op for op (Math.fround, in the header's order), so both give
5
+ // the same bits; tests/golden/colors holds them equal (tests/ts/cssColor.test.ts). Standalone: no
6
+ // imports, no DOM, nothing at the top level but const declarations (the SDK vendors this file, and
7
+ // its bundler drops top-level side effects).
8
+ //
9
+ // Grammar (the header has the full text): #rgb #rgba #rrggbb #rrggbbaa · rgb() / rgba() · hsl() /
10
+ // hsla() · the 148 CSS Color 4 names · transparent · clear (the ONE non-CSS alias, = transparent).
11
+ // Case-insensitive, surrounding ASCII whitespace ignored; a string stops at an embedded NUL, like the
12
+ // C string the header reads. Numbers are CSS number tokens (nan / inf / hex reject), units only
13
+ // deg / rad / grad / turn, out-of-range values clamp. Anything else is null.
14
+
15
+ /** A color: straight float32 RGBA, 0..1. */
16
+ export interface CssRgba { r: number, g: number, b: number, a: number }
17
+
18
+ /** The named colors as 0xRRGGBBAA: the 148 CSS Color 4 names, transparent and clear (= transparent). */
19
+ export const CSS_COLOR_NAMES: Readonly<Record<string, number>> = {
20
+ aliceblue: 0xf0f8ffff, antiquewhite: 0xfaebd7ff, aqua: 0x00ffffff, aquamarine: 0x7fffd4ff, azure: 0xf0ffffff, beige: 0xf5f5dcff,
21
+ bisque: 0xffe4c4ff, black: 0x000000ff, blanchedalmond: 0xffebcdff, blue: 0x0000ffff, blueviolet: 0x8a2be2ff, brown: 0xa52a2aff,
22
+ burlywood: 0xdeb887ff, cadetblue: 0x5f9ea0ff, chartreuse: 0x7fff00ff, chocolate: 0xd2691eff, coral: 0xff7f50ff, cornflowerblue: 0x6495edff,
23
+ cornsilk: 0xfff8dcff, crimson: 0xdc143cff, cyan: 0x00ffffff, darkblue: 0x00008bff, darkcyan: 0x008b8bff, darkgoldenrod: 0xb8860bff,
24
+ darkgray: 0xa9a9a9ff, darkgreen: 0x006400ff, darkgrey: 0xa9a9a9ff, darkkhaki: 0xbdb76bff, darkmagenta: 0x8b008bff, darkolivegreen: 0x556b2fff,
25
+ darkorange: 0xff8c00ff, darkorchid: 0x9932ccff, darkred: 0x8b0000ff, darksalmon: 0xe9967aff, darkseagreen: 0x8fbc8fff, darkslateblue: 0x483d8bff,
26
+ darkslategray: 0x2f4f4fff, darkslategrey: 0x2f4f4fff, darkturquoise: 0x00ced1ff, darkviolet: 0x9400d3ff, deeppink: 0xff1493ff, deepskyblue: 0x00bfffff,
27
+ dimgray: 0x696969ff, dimgrey: 0x696969ff, dodgerblue: 0x1e90ffff, firebrick: 0xb22222ff, floralwhite: 0xfffaf0ff, forestgreen: 0x228b22ff,
28
+ fuchsia: 0xff00ffff, gainsboro: 0xdcdcdcff, ghostwhite: 0xf8f8ffff, gold: 0xffd700ff, goldenrod: 0xdaa520ff, gray: 0x808080ff,
29
+ green: 0x008000ff, greenyellow: 0xadff2fff, grey: 0x808080ff, honeydew: 0xf0fff0ff, hotpink: 0xff69b4ff, indianred: 0xcd5c5cff,
30
+ indigo: 0x4b0082ff, ivory: 0xfffff0ff, khaki: 0xf0e68cff, lavender: 0xe6e6faff, lavenderblush: 0xfff0f5ff, lawngreen: 0x7cfc00ff,
31
+ lemonchiffon: 0xfffacdff, lightblue: 0xadd8e6ff, lightcoral: 0xf08080ff, lightcyan: 0xe0ffffff, lightgoldenrodyellow: 0xfafad2ff, lightgray: 0xd3d3d3ff,
32
+ lightgreen: 0x90ee90ff, lightgrey: 0xd3d3d3ff, lightpink: 0xffb6c1ff, lightsalmon: 0xffa07aff, lightseagreen: 0x20b2aaff, lightskyblue: 0x87cefaff,
33
+ lightslategray: 0x778899ff, lightslategrey: 0x778899ff, lightsteelblue: 0xb0c4deff, lightyellow: 0xffffe0ff, lime: 0x00ff00ff, limegreen: 0x32cd32ff,
34
+ linen: 0xfaf0e6ff, magenta: 0xff00ffff, maroon: 0x800000ff, mediumaquamarine: 0x66cdaaff, mediumblue: 0x0000cdff, mediumorchid: 0xba55d3ff,
35
+ mediumpurple: 0x9370dbff, mediumseagreen: 0x3cb371ff, mediumslateblue: 0x7b68eeff, mediumspringgreen: 0x00fa9aff, mediumturquoise: 0x48d1ccff, mediumvioletred: 0xc71585ff,
36
+ midnightblue: 0x191970ff, mintcream: 0xf5fffaff, mistyrose: 0xffe4e1ff, moccasin: 0xffe4b5ff, navajowhite: 0xffdeadff, navy: 0x000080ff,
37
+ oldlace: 0xfdf5e6ff, olive: 0x808000ff, olivedrab: 0x6b8e23ff, orange: 0xffa500ff, orangered: 0xff4500ff, orchid: 0xda70d6ff,
38
+ palegoldenrod: 0xeee8aaff, palegreen: 0x98fb98ff, paleturquoise: 0xafeeeeff, palevioletred: 0xdb7093ff, papayawhip: 0xffefd5ff, peachpuff: 0xffdab9ff,
39
+ peru: 0xcd853fff, pink: 0xffc0cbff, plum: 0xdda0ddff, powderblue: 0xb0e0e6ff, purple: 0x800080ff, rebeccapurple: 0x663399ff,
40
+ red: 0xff0000ff, rosybrown: 0xbc8f8fff, royalblue: 0x4169e1ff, saddlebrown: 0x8b4513ff, salmon: 0xfa8072ff, sandybrown: 0xf4a460ff,
41
+ seagreen: 0x2e8b57ff, seashell: 0xfff5eeff, sienna: 0xa0522dff, silver: 0xc0c0c0ff, skyblue: 0x87ceebff, slateblue: 0x6a5acdff,
42
+ slategray: 0x708090ff, slategrey: 0x708090ff, snow: 0xfffafaff, springgreen: 0x00ff7fff, steelblue: 0x4682b4ff, tan: 0xd2b48cff,
43
+ teal: 0x008080ff, thistle: 0xd8bfd8ff, tomato: 0xff6347ff, turquoise: 0x40e0d0ff, violet: 0xee82eeff, wheat: 0xf5deb3ff,
44
+ white: 0xffffffff, whitesmoke: 0xf5f5f5ff, yellow: 0xffff00ff, yellowgreen: 0x9acd32ff,
45
+ transparent: 0x00000000, clear: 0x00000000,
46
+ }
47
+
48
+ // float32 arithmetic: every operation of the header, rounded as it rounds (a double op on two float32
49
+ // operands, rounded once to float32, is the float32 op).
50
+ const f = Math.fround
51
+ const T13 = f(1 / 3), T16 = f(1 / 6), T23 = f(2 / 3), PI = f(3.14159265), GRAD = f(0.9)
52
+
53
+ // ASCII only, by char code (no toLowerCase / trim: they know Unicode, the header does not).
54
+ const isSpace = (c: number): boolean => c === 32 || (c >= 9 && c <= 13)
55
+ const isDigit = (c: number): boolean => c >= 48 && c <= 57
56
+ const lower = (c: number): number => c >= 65 && c <= 90 ? c + 32 : c
57
+ const isLetter = (c: number): boolean => { const l = lower(c); return l >= 97 && l <= 122 }
58
+ const hexNibble = (c: number): number => {
59
+ const l = lower(c)
60
+ return l >= 48 && l <= 57 ? l - 48 : l >= 97 && l <= 102 ? l - 87 : -1
61
+ }
62
+ const equalsLower = (s: string, at: number, n: number, word: string): boolean => {
63
+ if (n !== word.length) return false
64
+ for (let i = 0; i < n; i++) if (lower(s.charCodeAt(at + i)) !== word.charCodeAt(i)) return false
65
+ return true
66
+ }
67
+
68
+ // The CSS number token at s[at..end): [+-]? (digits (. digits*)? | . digits) ([eE] [+-]? digits)? → its
69
+ // length, 0 when there is none.
70
+ const numberLength = (s: string, at: number, end: number): number => {
71
+ let i = at
72
+ if (i < end && (s.charCodeAt(i) === 43 || s.charCodeAt(i) === 45)) i++
73
+ let intDigits = 0, fracDigits = 0
74
+ while (i < end && isDigit(s.charCodeAt(i))) { i++; intDigits++ }
75
+ if (i < end && s.charCodeAt(i) === 46) {
76
+ let j = i + 1
77
+ while (j < end && isDigit(s.charCodeAt(j))) { j++; fracDigits++ }
78
+ if (intDigits > 0 || fracDigits > 0) i = j
79
+ }
80
+ if (intDigits === 0 && fracDigits === 0) return 0
81
+ if (i < end && (s.charCodeAt(i) === 101 || s.charCodeAt(i) === 69)) {
82
+ let j = i + 1
83
+ if (j < end && (s.charCodeAt(j) === 43 || s.charCodeAt(j) === 45)) j++
84
+ if (j < end && isDigit(s.charCodeAt(j))) {
85
+ while (j < end && isDigit(s.charCodeAt(j))) j++
86
+ i = j
87
+ }
88
+ }
89
+ return i - at
90
+ }
91
+
92
+ interface Arg { v: number, percent: boolean }
93
+
94
+ // The arguments between the parentheses, s[at..end): 3 or 4 numbers with a % flag each, or null.
95
+ const parseArgs = (s: string, at: number, end: number): Arg[] | null => {
96
+ const out: Arg[] = []
97
+ let i = at
98
+ while (i < end) {
99
+ while (i < end && (isSpace(s.charCodeAt(i)) || s.charCodeAt(i) === 44 || s.charCodeAt(i) === 47)) i++
100
+ if (i >= end) break
101
+ const len = numberLength(s, i, end)
102
+ if (len === 0 || len >= 64) return null // the header's stack buffer
103
+ let v = f(parseFloat(s.slice(i, i + len)))
104
+ i += len
105
+ let percent = false
106
+ if (i < end && s.charCodeAt(i) === 37) {
107
+ percent = true
108
+ i++
109
+ } else if (i < end && isLetter(s.charCodeAt(i))) {
110
+ const u = i
111
+ while (i < end && isLetter(s.charCodeAt(i))) i++
112
+ const ul = i - u
113
+ if (equalsLower(s, u, ul, "deg")) {
114
+ // degrees: the number as it is
115
+ } else if (equalsLower(s, u, ul, "rad")) {
116
+ v = f(v * 180)
117
+ v = f(v / PI)
118
+ } else if (equalsLower(s, u, ul, "turn")) {
119
+ v = f(v * 360)
120
+ } else if (equalsLower(s, u, ul, "grad")) {
121
+ v = f(v * GRAD)
122
+ } else {
123
+ return null
124
+ }
125
+ }
126
+ if (!Number.isFinite(v)) return null
127
+ if (out.length === 4) return null
128
+ out.push({ v, percent })
129
+ }
130
+ return out.length >= 3 ? out : null
131
+ }
132
+
133
+ const clamp01 = (v: number): number => v < 0 ? 0 : v > 1 ? 1 : v
134
+
135
+ const hueToRgb = (p: number, q: number, t: number): number => {
136
+ if (t < 0) t = f(t + 1)
137
+ if (t > 1) t = f(t - 1)
138
+ const d = f(q - p)
139
+ if (t < T16) return f(p + f(f(d * 6) * t))
140
+ if (t < 0.5) return q
141
+ if (t < T23) return f(p + f(f(d * f(T23 - t)) * 6))
142
+ return p
143
+ }
144
+
145
+ const fromBytes = (r: number, g: number, b: number, a: number): CssRgba => ({ r: f(r / 255), g: f(g / 255), b: f(b / 255), a: f(a / 255) })
146
+
147
+ /** A CSS color → straight float32 RGBA, or null when `input` is not a color. */
148
+ export const parseCssColor = (input: string): CssRgba | null => {
149
+ if (typeof input !== "string") return null
150
+ const nul = input.indexOf("\0")
151
+ const s = nul < 0 ? input : input.slice(0, nul)
152
+ let b = 0, e = s.length
153
+ while (b < e && isSpace(s.charCodeAt(b))) b++
154
+ while (e > b && isSpace(s.charCodeAt(e - 1))) e--
155
+ const n = e - b
156
+ if (n === 0) return null
157
+
158
+ if (s.charCodeAt(b) === 35) { // #
159
+ const digits = n - 1
160
+ if (digits !== 3 && digits !== 4 && digits !== 6 && digits !== 8) return null
161
+ const v: number[] = []
162
+ for (let i = 0; i < digits; i++) {
163
+ const h = hexNibble(s.charCodeAt(b + 1 + i))
164
+ if (h < 0) return null
165
+ v.push(h)
166
+ }
167
+ return digits <= 4
168
+ ? fromBytes(v[0] * 17, v[1] * 17, v[2] * 17, digits === 4 ? v[3] * 17 : 255)
169
+ : fromBytes(v[0] * 16 + v[1], v[2] * 16 + v[3], v[4] * 16 + v[5], digits === 8 ? v[6] * 16 + v[7] : 255)
170
+ }
171
+
172
+ const paren = s.indexOf("(", b)
173
+ if (paren >= 0 && paren < e && s.charCodeAt(e - 1) === 41) {
174
+ const args = parseArgs(s, paren + 1, e - 1)
175
+ if (!args) return null
176
+ const alpha = args.length === 4 ? clamp01(args[3].percent ? f(args[3].v / 100) : args[3].v) : 1
177
+ const fnLen = paren - b
178
+ if (equalsLower(s, b, fnLen, "rgb") || equalsLower(s, b, fnLen, "rgba")) {
179
+ const channel = (x: Arg): number => clamp01(x.percent ? f(x.v / 100) : f(x.v / 255))
180
+ return { r: channel(args[0]), g: channel(args[1]), b: channel(args[2]), a: alpha }
181
+ }
182
+ if (equalsLower(s, b, fnLen, "hsl") || equalsLower(s, b, fnLen, "hsla")) {
183
+ let h = args[0].v % 360 // fmodf: exact, the sign of the dividend
184
+ if (h < 0) h = f(h + 360)
185
+ h = f(h / 360)
186
+ const sat = clamp01(f(args[1].v / 100))
187
+ const l = clamp01(f(args[2].v / 100))
188
+ if (sat <= 0) return { r: l, g: l, b: l, a: alpha }
189
+ const q = l < 0.5 ? f(l * f(1 + sat)) : f(f(l + sat) - f(l * sat))
190
+ const p = f(f(2 * l) - q)
191
+ return {
192
+ r: clamp01(hueToRgb(p, q, f(h + T13))),
193
+ g: clamp01(hueToRgb(p, q, h)),
194
+ b: clamp01(hueToRgb(p, q, f(h - T13))),
195
+ a: alpha,
196
+ }
197
+ }
198
+ return null
199
+ }
200
+
201
+ // A name: looked up only when it can be one ('lightgoldenrodyellow' is the longest, 20).
202
+ if (n > 20) return null
203
+ let name = ""
204
+ for (let i = b; i < e; i++) name += String.fromCharCode(lower(s.charCodeAt(i)))
205
+ if (!Object.prototype.hasOwnProperty.call(CSS_COLOR_NAMES, name)) return null
206
+ const v = CSS_COLOR_NAMES[name]
207
+ return fromBytes(v >>> 24, (v >>> 16) & 255, (v >>> 8) & 255, v & 255)
208
+ }
209
+
210
+ /** A float channel → a byte, as the header's toByte: float32, clamp (NaN → 0), round half up (0.5 → 128). */
211
+ export const cssToByte = (v: number): number => {
212
+ const x = f(v)
213
+ if (!(x > 0)) return 0
214
+ if (x >= 1) return 255
215
+ const s = f(x * 255)
216
+ return Math.floor(f(s + 0.5))
217
+ }
218
+
219
+ /** 0xRRGGBBAA (unsigned). */
220
+ export const cssRgba8 = (c: CssRgba): number =>
221
+ ((cssToByte(c.r) << 24) | (cssToByte(c.g) << 16) | (cssToByte(c.b) << 8) | cssToByte(c.a)) >>> 0
222
+
223
+ /** '#rrggbbaa', lower-case. */
224
+ export const cssHex8 = (c: CssRgba): string => "#" + cssRgba8(c).toString(16).padStart(8, "0")