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.
- package/README.md +5 -2
- package/dist/editor.d.ts +12 -0
- package/dist/global.d.ts +4 -8
- package/dist/host.d.ts +2 -3
- package/dist/types/animate/tween/Animation.d.ts +0 -3
- package/dist/types/animate/tween/animateValue.d.ts +4 -2
- package/dist/types/animate/tween/easing.d.ts +8 -0
- package/dist/types/animate/tween/spec.d.ts +15 -7
- package/dist/types/audio/audio.d.ts +2 -1
- package/dist/types/canvas/Canvas.d.ts +40 -108
- package/dist/types/canvas/gen/cssColor.d.ts +17 -0
- package/dist/types/canvas/gen/recorder.d.ts +118 -0
- package/dist/types/canvas/gen/spec.d.ts +144 -0
- package/dist/types/core/color.d.ts +3 -1
- package/dist/types/core/pins.d.ts +18 -0
- package/dist/types/g2/Node2D.d.ts +5 -8
- package/dist/types/g2/Scene2D.d.ts +6 -2
- package/dist/types/gl/Foliage.d.ts +30 -7
- package/dist/types/gl/Light.d.ts +8 -0
- package/dist/types/gl/Lightmap.d.ts +13 -2
- package/dist/types/gl/Material.d.ts +17 -3
- package/dist/types/gl/Model.d.ts +6 -2
- package/dist/types/gl/Node.d.ts +3 -6
- package/dist/types/gl/Scene.d.ts +41 -13
- package/dist/types/gl/Texture.d.ts +1 -1
- package/dist/types/gl/animation/Locomotion.d.ts +8 -1
- package/dist/types/inject.d.ts +11 -11
- package/dist/types/inject.editor.d.ts +1 -0
- package/dist/types/net/core.d.ts +7 -0
- package/dist/types/plugin.d.ts +76 -0
- package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
- package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
- package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
- package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
- package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
- package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
- package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
- package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
- package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
- package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
- package/dist/types/runtime/app.d.ts +9 -2
- package/dist/types/runtime/fetch.d.ts +2 -0
- package/dist/types/runtime/input.d.ts +1 -1
- package/dist/types/runtime/media.d.ts +6 -10
- package/dist/types/runtime/misc.d.ts +4 -1
- package/dist/types/runtime/net.d.ts +3 -2
- package/dist/types/runtime/touch.d.ts +32 -0
- package/dist/types/scene/defineScene.d.ts +43 -2
- package/dist/types/scene/editor.d.ts +52 -0
- package/dist/types/scene/gizmos.d.ts +7 -4
- package/dist/types/ui/NativeView.d.ts +6 -4
- package/dist/types/ui/UI.d.ts +1 -1
- package/dist/types/ui/UIBottomSheet.d.ts +6 -12
- package/dist/types/ui/UIButton.d.ts +12 -14
- package/dist/types/ui/UIContainer.d.ts +0 -6
- package/dist/types/ui/UIImage.d.ts +1 -4
- package/dist/types/ui/UIInput.d.ts +8 -24
- package/dist/types/ui/UIModal.d.ts +0 -2
- package/dist/types/ui/UINode.d.ts +70 -56
- package/dist/types/ui/UIPager.d.ts +28 -27
- package/dist/types/ui/UIPopover.d.ts +0 -2
- package/dist/types/ui/UIScreen.d.ts +12 -17
- package/dist/types/ui/UIScrollable.d.ts +1 -4
- package/dist/types/ui/UIText.d.ts +0 -2
- package/dist/types/ui/UIVideo.d.ts +3 -5
- package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
- package/dist/types/ui/UIWidget.d.ts +10 -9
- package/dist/types/ui/colorKeys.gen.d.ts +9 -0
- package/dist/types/ui/presentable.d.ts +46 -32
- package/dist/types/ui/router.d.ts +18 -7
- package/dist/types/ui/styleColor.d.ts +1 -0
- package/dist/types/ui/transitions.d.ts +18 -0
- package/dist/types/ui/tree.d.ts +75 -0
- package/dist/types/version.d.ts +10 -0
- package/dist/types.json +1 -1
- package/package.json +12 -3
- package/prompts/2d.md +2 -6
- package/prompts/3d.md +1 -5
- package/prompts/README.md +1 -1
- package/prompts/canvas.md +9 -8
- package/prompts/compose.ts +1 -1
- package/prompts/core.md +3 -3
- package/prompts/design.md +1 -1
- package/prompts/dist/2d-game.md +473 -239
- package/prompts/dist/3d-app.md +553 -205
- package/prompts/dist/ar-app.md +435 -202
- package/prompts/dist/design.md +113 -95
- package/prompts/dist/ui-app.md +386 -170
- package/prompts/ui-design.md +2 -3
- package/prompts/ui.md +45 -37
- package/src/animate/tween/Animation.ts +34 -150
- package/src/animate/tween/Timeline.ts +175 -175
- package/src/animate/tween/animateValue.ts +6 -3
- package/src/animate/tween/easing.ts +10 -3
- package/src/animate/tween/spec.ts +41 -15
- package/src/audio/Sound.ts +3 -3
- package/src/audio/audio.ts +2 -1
- package/src/bridges/2d.d.ts +317 -0
- package/src/bridges/app.d.ts +91 -0
- package/src/bridges/audio.d.ts +97 -0
- package/src/bridges/canvas.d.ts +79 -0
- package/src/bridges/device.d.ts +72 -0
- package/src/bridges/fetch.d.ts +80 -0
- package/src/bridges/files.d.ts +70 -0
- package/src/bridges/gl.d.ts +1133 -0
- package/src/bridges/input.d.ts +72 -0
- package/src/bridges/media.d.ts +55 -0
- package/src/bridges/nav.d.ts +71 -0
- package/src/bridges/net.d.ts +52 -0
- package/src/bridges/service.d.ts +52 -0
- package/src/bridges/socket.d.ts +31 -0
- package/src/bridges/storage.d.ts +33 -0
- package/src/bridges/tree.d.ts +301 -0
- package/src/bridges/types.d.ts +49 -0
- package/src/canvas/Canvas.ts +114 -159
- package/src/canvas/gen/cssColor.ts +224 -0
- package/src/canvas/gen/recorder.ts +212 -0
- package/src/canvas/gen/spec.ts +201 -0
- package/src/chisel.ts +193 -0
- package/src/compile/assetMacro.ts +1 -1
- package/src/compile/bundler.ts +11 -2
- package/src/compile/compileProject.ts +43 -4
- package/src/compile/fontMacro.ts +3 -4
- package/src/compile/header.ts +26 -5
- package/src/compile/index.ts +3 -1
- package/src/compile/liteMaterial.ts +1 -1
- package/src/compile/sceneEditor.ts +11 -26
- package/src/core/color.ts +73 -30
- package/src/core/pins.ts +51 -0
- package/src/core/signals.ts +8 -1
- package/src/g2/CharacterController2D.ts +3 -3
- package/src/g2/Node2D.ts +57 -39
- package/src/g2/Physics2D.ts +2 -2
- package/src/g2/Scene2D.ts +35 -23
- package/src/g2/Texture2D.ts +1 -1
- package/src/g2/loop.ts +4 -4
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/Foliage.ts +72 -17
- package/src/gl/Geometry.ts +1 -2
- package/src/gl/Light.ts +10 -0
- package/src/gl/Lightmap.ts +45 -29
- package/src/gl/Material.ts +95 -51
- package/src/gl/Mesh.ts +120 -120
- package/src/gl/Model.ts +21 -16
- package/src/gl/Node.ts +91 -24
- package/src/gl/Particles.ts +1 -1
- package/src/gl/Scene.ts +100 -44
- package/src/gl/Texture.ts +8 -7
- package/src/gl/animation/AnimationClip.ts +1 -1
- package/src/gl/animation/DynamicBone.ts +482 -482
- package/src/gl/animation/Locomotion.ts +8 -3
- package/src/gl/nav/NavMesh.ts +3 -4
- package/src/gl/physics/Physics.ts +2 -2
- package/src/gl/physics/physicsEvents.ts +3 -3
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/terrain/Terrain.ts +4 -5
- package/src/gl/touch.ts +14 -15
- package/src/host.d.ts +2 -3
- package/src/inject.editor.ts +7 -0
- package/src/inject.ts +13 -16
- package/src/net/core.ts +6 -5
- package/src/net/index.ts +1 -1
- package/src/net/replication.ts +1 -1
- package/src/plugin.ts +191 -0
- package/src/plugins/gen/camera/contract.d.ts +27 -0
- package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
- package/src/plugins/gen/camera/sdk/camera.ts +57 -0
- package/src/plugins/gen/geolocation/contract.d.ts +50 -0
- package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
- package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
- package/src/plugins/gen/map/contract.d.ts +144 -0
- package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
- package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
- package/src/plugins/gen/push/contract.d.ts +61 -0
- package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
- package/src/plugins/gen/push/sdk/push.ts +105 -0
- package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
- package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
- package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
- package/src/plugins/permission.ts +5 -4
- package/src/runtime/app.ts +20 -9
- package/src/runtime/appEvents.ts +5 -4
- package/src/runtime/channel.ts +18 -15
- package/src/runtime/clipboard.ts +4 -3
- package/src/runtime/datetime.ts +2 -1
- package/src/runtime/device.ts +17 -15
- package/src/runtime/fetch.ts +30 -20
- package/src/runtime/files.ts +16 -15
- package/src/runtime/input.ts +12 -10
- package/src/runtime/media.ts +50 -46
- package/src/runtime/misc.ts +7 -3
- package/src/runtime/net.ts +8 -7
- package/src/runtime/rpc.ts +1 -3
- package/src/runtime/service.ts +19 -14
- package/src/runtime/share.ts +4 -3
- package/src/runtime/storage.ts +6 -4
- package/src/runtime/touch.ts +32 -0
- package/src/scene/defineScene.ts +61 -365
- package/src/scene/editor.ts +408 -0
- package/src/scene/editorPlugins.ts +3 -3
- package/src/scene/gizmos.ts +15 -9
- package/src/server/db/marci/query.ts +1 -1
- package/src/server/host.ts +1 -1
- package/src/server/runtime.ts +1 -1
- package/src/ui/NativeView.ts +72 -25
- package/src/ui/UI.ts +3 -3
- package/src/ui/UIBottomSheet.ts +16 -17
- package/src/ui/UIButton.ts +54 -16
- package/src/ui/UIContainer.ts +0 -6
- package/src/ui/UIImage.ts +34 -30
- package/src/ui/UIInput.ts +29 -37
- package/src/ui/UIModal.ts +1 -3
- package/src/ui/UINode.ts +347 -297
- package/src/ui/UIPager.ts +93 -78
- package/src/ui/UIPopover.ts +0 -2
- package/src/ui/UIScreen.ts +41 -36
- package/src/ui/UIScrollable.ts +19 -13
- package/src/ui/UISpacer.ts +1 -1
- package/src/ui/UITabs.ts +8 -6
- package/src/ui/UIText.ts +7 -17
- package/src/ui/UIVideo.ts +30 -27
- package/src/ui/UIVirtualizedList.ts +58 -59
- package/src/ui/UIWidget.ts +38 -18
- package/src/ui/colorKeys.gen.ts +37 -0
- package/src/ui/fonts.ts +2 -2
- package/src/ui/presentable.ts +59 -42
- package/src/ui/router.ts +52 -35
- package/src/ui/styleColor.ts +56 -0
- package/src/ui/theme.ts +7 -6
- package/src/ui/transitions.ts +249 -0
- package/src/ui/tree.ts +346 -0
- package/src/version.ts +24 -0
- package/tests/helpers/engineWorld.ts +11 -0
- package/tests/helpers/fakeTree.ts +353 -0
- package/tests/helpers/hostStubs.ts +31 -0
- package/tests/helpers/index.ts +14 -0
- package/tests/helpers/memoryMarci.ts +124 -0
- package/tests/helpers/phases.ts +23 -0
- package/tests/helpers/preload.ts +18 -0
- package/tests/helpers/stubApp.ts +2 -0
- package/tests/helpers/stubDevice.ts +2 -0
- package/tests/helpers/stubFetch.ts +2 -0
- package/tests/helpers/stubInput.ts +2 -0
- package/dist/inject.js +0 -4629
- package/dist/types/core/registry.d.ts +0 -7
- package/dist/types/plugins/camera.d.ts +0 -25
- package/dist/types/plugins/push.d.ts +0 -46
- package/src/bridges.d.ts +0 -1769
- package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
- package/src/compile/__tests__/assetMacro.test.ts +0 -100
- package/src/compile/__tests__/assetName.test.ts +0 -55
- package/src/compile/__tests__/compile.test.ts +0 -310
- package/src/compile/__tests__/detectEntry.test.ts +0 -151
- package/src/compile/__tests__/fontMacro.test.ts +0 -199
- package/src/compile/__tests__/serverSplit.test.ts +0 -27
- package/src/core/__tests__/stateMachine.test.ts +0 -132
- package/src/core/registry.ts +0 -23
- package/src/plugins/camera.ts +0 -81
- package/src/plugins/push.ts +0 -132
- package/src/plugins/qr.ts +0 -73
package/src/canvas/Canvas.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
// A core 2D drawing surface that bakes to a texture
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
private
|
|
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
|
|
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
|
|
162
|
-
* including the source sub-rect in the 9-arg form (it's scaled by the bitmap's
|
|
163
|
-
* Records a DRAW_IMAGE op, so the blit runs at bake time
|
|
164
|
-
* `
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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(...)` (
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
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).
|
|
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, (
|
|
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
|
|
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.
|
|
206
|
-
|
|
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.
|
|
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
|
|
223
|
-
|
|
224
|
-
|
|
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
|
|
249
|
-
//
|
|
250
|
-
if (this._uiBound)
|
|
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
|
-
* `
|
|
257
|
-
* const
|
|
258
|
-
* canvas.reset(); canvas.drawImage(
|
|
259
|
-
*
|
|
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.
|
|
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
|
|
290
|
-
* surface (a copy), so the source canvas can keep drawing or `reset()` without affecting it.
|
|
291
|
-
* to stamp/blit back (`canvas.drawImage(bitmap, …)`), to export (`bitmap.
|
|
292
|
-
* cheap "layer" you composite.
|
|
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
|
|
296
|
-
|
|
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(
|
|
303
|
-
this.
|
|
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.
|
|
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
|
|
321
|
-
|
|
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")
|