@mocanvas/wasm 1.0.0 → 4.0.0
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/ARCHITECTURE.md +421 -0
- package/BENCHMARK.md +519 -0
- package/CLEAN_ROOM.md +50 -0
- package/COMPAT.md +282 -0
- package/CUSTOM_SHAPES.md +880 -0
- package/LICENSE +110 -16
- package/MIGRATION.md +807 -0
- package/README.md +22 -3
- package/UI.md +256 -0
- package/dist/index.d.ts +110 -6
- package/dist/index.js +155 -6
- package/dist/index.js.map +1 -1
- package/package.json +11 -13
- package/pkg/mocanvas-wasm-base64.js +1 -1
- package/pkg/mocanvas.d.ts +17 -0
- package/pkg/mocanvas.js +21 -0
- package/pkg/mocanvas_bg.wasm +0 -0
- package/pkg/mocanvas_bg.wasm.d.ts +1 -0
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# @mocanvas/wasm
|
|
2
2
|
|
|
3
|
-
The Rust/WebAssembly engine behind
|
|
3
|
+
The Rust/WebAssembly engine behind mocanvas
|
|
4
4
|
and its TypeScript bridge. The engine owns the scene: it holds shape geometry,
|
|
5
5
|
maintains a spatial index, hit-tests, culls to the viewport, tessellates and
|
|
6
6
|
batches, and writes vertex/index/batch buffers into linear memory that the host
|
|
7
7
|
uploads to the GPU by pointer — no per-frame copying.
|
|
8
8
|
|
|
9
|
-
Most applications use
|
|
9
|
+
Most applications use `@mocanvas/mocanvas` or `@mocanvas/editor` instead of this package
|
|
10
10
|
directly. No React, no other dependencies.
|
|
11
11
|
|
|
12
12
|
## Install
|
|
@@ -99,4 +99,23 @@ ESM only.
|
|
|
99
99
|
|
|
100
100
|
## License
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
**Source-available, not open source.** Free to use for:
|
|
103
|
+
|
|
104
|
+
- personal, non-commercial projects;
|
|
105
|
+
- non-profit organisations;
|
|
106
|
+
- development, evaluation, testing and staging — including inside a for-profit
|
|
107
|
+
company, so you can try it and build against it before committing;
|
|
108
|
+
- teaching and academic research.
|
|
109
|
+
|
|
110
|
+
**Shipping it in a commercial product, service or website needs a written
|
|
111
|
+
agreement with us.** That includes anything sold, anything that earns revenue
|
|
112
|
+
directly or through advertising, and internal tools running a for-profit
|
|
113
|
+
business.
|
|
114
|
+
|
|
115
|
+
To arrange one, or if you are unsure which side of the line you are on, write to
|
|
116
|
+
**mocanvas@symbio.agency** — we would rather answer the question than have you
|
|
117
|
+
guess.
|
|
118
|
+
|
|
119
|
+
The full terms are in `LICENSE`, shipped in this package. Versions released
|
|
120
|
+
earlier under MIT stay available under MIT, on the terms they were released
|
|
121
|
+
with.
|
package/UI.md
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# The default UI
|
|
2
|
+
|
|
3
|
+
`<Mocanvas />` ships a small, opinionated interface: a toolbar, a zoom bar, a
|
|
4
|
+
style panel, an optional statistics chip, and the chrome the editor draws on the
|
|
5
|
+
canvas itself. All of it is optional and all of it is themeable from CSS.
|
|
6
|
+
|
|
7
|
+
Source lives in `packages/mocanvas/src/ui/`:
|
|
8
|
+
|
|
9
|
+
| File | What it holds |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `DefaultUi.tsx` | The toolbar, the zoom bar, the stats chip, and the `DefaultUi` wrapper |
|
|
12
|
+
| `StylePanel.tsx` | The style panel and `getStylePanelSections`, the rule for which rows appear |
|
|
13
|
+
| `icons.tsx` | The whole icon set, plus the geo icons generated from canvas geometry |
|
|
14
|
+
| `overlays.tsx` | The shared tooltip and popover layers, and `placeNear` |
|
|
15
|
+
| `ui.css` | Every design token and every rule |
|
|
16
|
+
| `useKeyboardShortcuts.ts` | The default key bindings |
|
|
17
|
+
|
|
18
|
+
## Design tokens
|
|
19
|
+
|
|
20
|
+
Tokens are declared on `.mocanvas` (the editor container), on `.mocanvas-panel`,
|
|
21
|
+
and on `.mocanvas-layer` (the floating tooltip and popover, which sit outside the
|
|
22
|
+
panels in the DOM). Override any of them on `.mocanvas`, on a wrapper, or on a
|
|
23
|
+
single panel.
|
|
24
|
+
|
|
25
|
+
### Surfaces and colour
|
|
26
|
+
|
|
27
|
+
| Token | Light | Dark | Used for |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| `--mocanvas-ui-panel` | `#ffffff` | `#1b1d22` | Panel, popover and picker background |
|
|
30
|
+
| `--mocanvas-ui-panel-border` | `#d6d9e0` | `#3c404a` | Panel hairline, dividers, group separators |
|
|
31
|
+
| `--mocanvas-ui-control` | `rgba(16,24,40,.06)` | `rgba(255,255,255,.08)` | Recessed background of a segmented button |
|
|
32
|
+
| `--mocanvas-ui-text` | `#16181d` | `#eceef3` | Icon and label colour |
|
|
33
|
+
| `--mocanvas-ui-muted` | `#5c6070` | `#a4a9b4` | Row labels, stats text |
|
|
34
|
+
| `--mocanvas-ui-accent` | `#2563eb` | `#6ea8fe` | Selected control, focus ring |
|
|
35
|
+
| `--mocanvas-ui-accent-fg` | `#ffffff` | `#0e1116` | Icon on an accent fill |
|
|
36
|
+
| `--mocanvas-ui-accent-soft` | 12% accent | 18% accent | Hover on a segmented button, open disclosure |
|
|
37
|
+
| `--mocanvas-ui-hover` | 6% ink | 9% white | Hover on a bare button |
|
|
38
|
+
| `--mocanvas-ui-active` | 13% ink | 18% white | Pointer-down on a bare button |
|
|
39
|
+
| `--mocanvas-ui-shadow` | — | — | Panel elevation |
|
|
40
|
+
| `--mocanvas-ui-tip-bg` / `-fg` / `-muted` | — | — | Tooltip pill, its label, its shortcut |
|
|
41
|
+
|
|
42
|
+
Every text-on-background pair is at or above 4.5:1 in both palettes. Measured on
|
|
43
|
+
the shipped values, the tightest are `accent` on a segmented control (4.58 light,
|
|
44
|
+
5.54 dark) and `muted` on the panel (6.25 light, 7.16 dark). Disabled controls
|
|
45
|
+
render at 35% opacity, which WCAG exempts.
|
|
46
|
+
|
|
47
|
+
### Metrics
|
|
48
|
+
|
|
49
|
+
| Token | Value | Meaning |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| `--mocanvas-ui-btn` | `40px` | Every hit target — buttons, swatches, popover cells |
|
|
52
|
+
| `--mocanvas-ui-icon` | `20px` | Icon size inside a button |
|
|
53
|
+
| `--mocanvas-ui-gap` | `2px` | Gap between adjacent controls |
|
|
54
|
+
| `--mocanvas-ui-pad` | `4px` | Padding of a bar |
|
|
55
|
+
| `--mocanvas-ui-pad-lg` | `10px` | Padding of the style panel, gap between its groups |
|
|
56
|
+
| `--mocanvas-ui-inset` | `12px` | Distance from a panel to the viewport edge |
|
|
57
|
+
| `--mocanvas-ui-radius` / `-sm` | `12px` / `8px` | Panel radius / control radius |
|
|
58
|
+
| `--mocanvas-ui-dock` | `300px` | Width the bottom-left and bottom-right docks reserve beside the centred toolbar |
|
|
59
|
+
| `--mocanvas-ui-bottom-dock` | `62px` (`124px` under 1290px) | Height the bottom edge occupies; the style panel stops above it |
|
|
60
|
+
| `--mocanvas-ui-font` / `-mono` | system stacks | Panel type / stats chip |
|
|
61
|
+
|
|
62
|
+
`--mocanvas-ui-dock` is the mechanism that keeps the three bottom panels apart:
|
|
63
|
+
the toolbar is centred on the viewport but may not grow into the reservation, so
|
|
64
|
+
it wraps rather than collide. Shrink it if your zoom bar is narrower than the
|
|
65
|
+
default one.
|
|
66
|
+
|
|
67
|
+
### Canvas chrome
|
|
68
|
+
|
|
69
|
+
These are read by the editor's indicator layer (`Canvas.tsx`), not by the panels,
|
|
70
|
+
so they apply even with `hideUi`. They are deliberately **not** redefined for the
|
|
71
|
+
dark UI: the canvas keeps its own background, which does not follow
|
|
72
|
+
`prefers-color-scheme`.
|
|
73
|
+
|
|
74
|
+
| Token | Value | Used for |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| `--mocanvas-selection` | `#2f6fe4` | Selection and hover outlines, handle strokes, brush border |
|
|
77
|
+
| `--mocanvas-selection-fg` | `#ffffff` | Fill behind a solid handle |
|
|
78
|
+
| `--mocanvas-brush-fill` | 12% selection | Brush rectangle interior |
|
|
79
|
+
| `--mocanvas-snap` | `#cf3fe0` | Snap lines and their end markers |
|
|
80
|
+
|
|
81
|
+
The two stroke colours clear 3:1 against a light canvas (`#f9fafb`: 4.45 and
|
|
82
|
+
3.69) and against a dark one (`#1b1d22`: 3.63 and 4.37). If you render a dark
|
|
83
|
+
canvas, swap `--mocanvas-selection-fg` for a dark value so handles stay filled
|
|
84
|
+
with the canvas colour rather than white.
|
|
85
|
+
|
|
86
|
+
Handles are drawn at 9px (corners), 6px (shape handles) and 5.5px (rotate) in
|
|
87
|
+
screen space, with a 1.5px stroke. Their hit radius is a separate editor
|
|
88
|
+
constant, `HANDLE_HIT_RADIUS` in `packages/editor/src/editor/selectionHandles.ts`.
|
|
89
|
+
|
|
90
|
+
## Icon grid rules
|
|
91
|
+
|
|
92
|
+
Icons are original artwork on a 24×24 viewBox, painted with `currentColor`.
|
|
93
|
+
|
|
94
|
+
- **Grid.** 24×24. Ink, stroke included, stays inside it.
|
|
95
|
+
- **Weight.** Stroke 1.75, round caps and joins. Only texture marks deviate and
|
|
96
|
+
say so at the call site: the fill hatching (1.25), the dotted rule (2.6 with a
|
|
97
|
+
zero-length dash, so the caps draw the dots), the mono rails (1.4).
|
|
98
|
+
- **Centring.** Ink is optically centred on (12, 12), within half a grid unit.
|
|
99
|
+
The only exceptions are semantic: `valign-top` and `valign-bottom` sit high and
|
|
100
|
+
low on purpose.
|
|
101
|
+
- **Extent.** The longest ink dimension lands between 16 and 18.25 units — about
|
|
102
|
+
17.75 for a full-bleed form — so no icon reads heavier than its neighbour. The
|
|
103
|
+
`size-*` ramp is exempt: its whole job is to differ in size.
|
|
104
|
+
- **Distinctness.** No two icons may draw the same artwork; `icons.test.tsx`
|
|
105
|
+
enforces this. It is why the handwriting font is a script `a` rather than a
|
|
106
|
+
fourth capital A, and why `oval` gets a wider box than `ellipse`.
|
|
107
|
+
- **Geo icons** are generated from `getGeoGeometry`, the same code the canvas
|
|
108
|
+
draws with, so a toolbar button always matches the shape it creates. Each kind
|
|
109
|
+
is fitted into the box `getGeoIconBox(kind)` returns: square at `GEO_BOX` (16)
|
|
110
|
+
by default, and flatter or narrower for kinds whose name implies a proportion
|
|
111
|
+
(`rectangle`, `oval`, the four arrows). The longest side is always `GEO_BOX`.
|
|
112
|
+
|
|
113
|
+
Render one with `<Icon name="select" size={20} />`. `size` sets the SVG's
|
|
114
|
+
attributes; buttons additionally take their icon size from `--mocanvas-ui-icon`,
|
|
115
|
+
so inline marks like the "mixed" badge keep the size they ask for.
|
|
116
|
+
|
|
117
|
+
## Overriding the UI
|
|
118
|
+
|
|
119
|
+
### Turn it off
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
<Mocanvas hideUi />
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
This drops the toolbar, zoom bar, style panel and stats chip. `ui.css` still
|
|
126
|
+
loads, so the canvas chrome tokens above keep working. Default keyboard
|
|
127
|
+
shortcuts are wired by `<Mocanvas />` itself and are unaffected; call
|
|
128
|
+
`useKeyboardShortcuts(editor)` yourself if you build on `<Canvas />` directly.
|
|
129
|
+
|
|
130
|
+
### Replace the canvas chrome
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
<Mocanvas components={{ Indicators: MyIndicators, Brush: MyBrush, Background: MyGrid }} />
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`components` is forwarded to `<Canvas />`. `Indicators` draws selection and hover
|
|
137
|
+
outlines and handles, `Brush` the marquee, `Background` a layer behind the canvas.
|
|
138
|
+
Each receives `{ editor }` and renders into the SVG overlay (`Background` into a
|
|
139
|
+
plain DOM layer). Omit one to keep the default.
|
|
140
|
+
|
|
141
|
+
### Rebuild the panels
|
|
142
|
+
|
|
143
|
+
Compose your own from the exported parts:
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
import { Mocanvas, Toolbar, ZoomBar, StylePanel, UiTooltip, Icon } from "mocanvas"
|
|
147
|
+
|
|
148
|
+
<Mocanvas hideUi>
|
|
149
|
+
<Toolbar />
|
|
150
|
+
<MyOwnInspector />
|
|
151
|
+
<UiTooltip />
|
|
152
|
+
</Mocanvas>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`Toolbar`, `ZoomBar`, `StylePanel`, `DebugStats`, `Popover`, `UiTooltip`, `Icon`
|
|
156
|
+
and `TOOLBAR_GROUPS` are all exported. Children of `<Mocanvas>` render above the
|
|
157
|
+
canvas inside the editor container, so the tokens apply to them too.
|
|
158
|
+
|
|
159
|
+
### Restyle it
|
|
160
|
+
|
|
161
|
+
```css
|
|
162
|
+
.mocanvas {
|
|
163
|
+
--mocanvas-ui-accent: #12b886;
|
|
164
|
+
--mocanvas-ui-radius: 6px;
|
|
165
|
+
--mocanvas-ui-btn: 44px;
|
|
166
|
+
--mocanvas-selection: #12b886;
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## The panels
|
|
171
|
+
|
|
172
|
+
### Toolbar — bottom centre
|
|
173
|
+
|
|
174
|
+
Tools in four groups separated by dividers: select and hand; draw and eraser; the
|
|
175
|
+
five common geo kinds plus a disclosure for the other fifteen; text, note, and
|
|
176
|
+
whichever of arrow, line and frame the app registered. Entries whose tool is not
|
|
177
|
+
registered disappear, so a cut-down `tools` prop yields a cut-down bar. The
|
|
178
|
+
active tool is `aria-pressed`; a geo button is pressed only when its own kind is
|
|
179
|
+
active. The disclosure opens a 5-column popover of the remaining kinds and is
|
|
180
|
+
itself marked pressed when one of them is active. The bar is centred on the
|
|
181
|
+
viewport, wraps to more rows rather than growing into the docks, and moves to a
|
|
182
|
+
row of its own below 1290px.
|
|
183
|
+
|
|
184
|
+
### Zoom bar — bottom left
|
|
185
|
+
|
|
186
|
+
Zoom out, the current percentage (a button that resets to 100%), zoom in, zoom to
|
|
187
|
+
fit, then a divider and undo/redo. Undo and redo are `disabled` when there is
|
|
188
|
+
nothing to undo or redo, at 35% opacity and without a tooltip.
|
|
189
|
+
|
|
190
|
+
### Style panel — top right
|
|
191
|
+
|
|
192
|
+
Appears when the selection carries at least one style, or when a drawing tool is
|
|
193
|
+
active with nothing selected. Rows are grouped, and separated by a hairline:
|
|
194
|
+
|
|
195
|
+
1. **Shape** — the geo kind, as one button showing the current shape that opens a
|
|
196
|
+
20-cell popover.
|
|
197
|
+
2. **Colour** — the stroke colour, and the label colour when the selection can
|
|
198
|
+
carry one; twelve swatches each, six to a row.
|
|
199
|
+
3. **Stroke and fill** — Fill, Dash and Size, four choices each.
|
|
200
|
+
4. **Text** — Font, then horizontal and vertical alignment sharing one row.
|
|
201
|
+
5. **Opacity** — a slider; only with a selection, since it edits shapes rather
|
|
202
|
+
than a style.
|
|
203
|
+
|
|
204
|
+
Which rows appear is decided by `getStylePanelSections(editor)`, which reads
|
|
205
|
+
`editor.getSharedStyles()` for a selection and
|
|
206
|
+
`editor.getStylePropsForType(toolId)` otherwise. A selection of two lines
|
|
207
|
+
therefore shows Colour, Dash and Size and nothing else. A row whose selected
|
|
208
|
+
shapes disagree shows a dashed "mixed" badge beside its label. The panel scrolls
|
|
209
|
+
when it is taller than the space above the bottom dock, with a fade and a shadow
|
|
210
|
+
at whichever edge has more content behind it. Below 560px it spans the width and
|
|
211
|
+
is capped at 42% of the height.
|
|
212
|
+
|
|
213
|
+
### Statistics chip — bottom right
|
|
214
|
+
|
|
215
|
+
Shape counts, drawn versus culled, and milliseconds per frame. Toggled with
|
|
216
|
+
`⌥D`, or with the `showStats` prop. It moves above the zoom bar below 560px.
|
|
217
|
+
|
|
218
|
+
### Tooltip and popover
|
|
219
|
+
|
|
220
|
+
Both are single, `position: fixed`, viewport-clamped layers (`overlays.tsx`).
|
|
221
|
+
A tooltip labels any element with `data-tooltip`, adding `data-shortcut` in a
|
|
222
|
+
muted weight; it appears after a 500ms rest, immediately on keyboard focus, sits
|
|
223
|
+
above its control so it never covers it, and flips below when there is no room.
|
|
224
|
+
`placeNear` does the arithmetic and is unit-tested. Popovers dismiss on outside
|
|
225
|
+
pointer-down and on Escape, which also returns focus to the button.
|
|
226
|
+
|
|
227
|
+
## States
|
|
228
|
+
|
|
229
|
+
| State | Bare button | Segmented button | Swatch |
|
|
230
|
+
| --- | --- | --- | --- |
|
|
231
|
+
| Rest | transparent | `--mocanvas-ui-control` | transparent |
|
|
232
|
+
| Hover | `--mocanvas-ui-hover` | `--mocanvas-ui-accent-soft` | `--mocanvas-ui-hover` |
|
|
233
|
+
| Pointer down | `--mocanvas-ui-active`, scaled 0.94 | as hover, scaled 0.94 | scaled 0.92 |
|
|
234
|
+
| Selected | accent fill, accent-fg icon | accent fill, accent-fg icon | double ring in accent |
|
|
235
|
+
| Selected + hover | accent fill plus an inset ring | same | same |
|
|
236
|
+
| Focus (keyboard) | 2px accent outline, 2px offset | same | same |
|
|
237
|
+
| Disabled | 35% opacity, no tooltip, default cursor | — | — |
|
|
238
|
+
|
|
239
|
+
The focus ring is offset by 2px so a ring of panel colour separates it from an
|
|
240
|
+
accent-filled button; without that gap, focus would be invisible on the active
|
|
241
|
+
tool. `prefers-reduced-motion` removes every transition.
|
|
242
|
+
|
|
243
|
+
## Pointer targets
|
|
244
|
+
|
|
245
|
+
Selection handles use a 24x24 px pointer target (`HANDLE_HIT_RADIUS = 12` in
|
|
246
|
+
`packages/editor/src/editor/selectionHandles.ts`), which is the WCAG 2.2
|
|
247
|
+
minimum. Two rules keep that from swallowing small shapes:
|
|
248
|
+
|
|
249
|
+
- Edge handles (top, right, bottom, left) only appear once that edge is at
|
|
250
|
+
least `4 * HANDLE_HIT_RADIUS` long on screen; below that the two corners
|
|
251
|
+
already cover the whole edge.
|
|
252
|
+
- On a selection smaller than six handles across, `getHandleHitRadius` scales
|
|
253
|
+
the target down (never below 4 px) so the shape's interior stays draggable.
|
|
254
|
+
|
|
255
|
+
Both measure the *screen-space* edge lengths of the transformed corners, so a
|
|
256
|
+
rotated selection behaves the same as an upright one.
|
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,34 @@ declare const OP: {
|
|
|
15
15
|
readonly SET_STYLE: 4;
|
|
16
16
|
readonly CLEAR: 5;
|
|
17
17
|
readonly SET_TEXTURE: 6;
|
|
18
|
+
readonly SET_GEO: 7;
|
|
19
|
+
readonly SET_SPLINE: 8;
|
|
20
|
+
readonly SET_POLY: 9;
|
|
21
|
+
readonly SET_DRAW: 10;
|
|
18
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Flag bits for the parametric geometry commands; must match
|
|
25
|
+
* `crates/mocanvas-wasm/src/lib.rs::geo_flag`.
|
|
26
|
+
*/
|
|
27
|
+
declare const GEO_FLAG: {
|
|
28
|
+
/** `setGeo`: mirror the silhouette left-to-right inside its own box. */
|
|
29
|
+
readonly FLIP_X: number;
|
|
30
|
+
/** `setGeo`: mirror the silhouette top-to-bottom inside its own box. */
|
|
31
|
+
readonly FLIP_Y: number;
|
|
32
|
+
/** `setSpline` / `setPoly` / `setDraw`: close the outline. */
|
|
33
|
+
readonly CLOSED: number;
|
|
34
|
+
/** `setDraw`, per segment: this run came from a pen, so smooth it. */
|
|
35
|
+
readonly FREEHAND: number;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The built-in geo silhouettes the engine can build from parameters, by name.
|
|
39
|
+
*
|
|
40
|
+
* The numbers are the indices of `GEO_SHAPE_KINDS`, which is part of the file
|
|
41
|
+
* format — a kind is only ever appended. A `geo` value that is not in here is a
|
|
42
|
+
* custom silhouette the engine has no generator for; upload its path with
|
|
43
|
+
* {@link CommandWriter.setGeometry} instead.
|
|
44
|
+
*/
|
|
45
|
+
declare const GEO_KIND: Readonly<Record<string, number>>;
|
|
19
46
|
/** Path opcodes; must match `mocanvas-geo::PathCmd`. */
|
|
20
47
|
declare const PATH_OP: {
|
|
21
48
|
readonly MOVE: 0;
|
|
@@ -51,7 +78,7 @@ interface CameraState {
|
|
|
51
78
|
/** Floats per vertex in `FrameBuffers.vertices`: `x y u v r g b a`. */
|
|
52
79
|
declare const VERTEX_FLOATS = 8;
|
|
53
80
|
/** `u32` words per record in `FrameBuffers.batches`. */
|
|
54
|
-
declare const BATCH_WORDS =
|
|
81
|
+
declare const BATCH_WORDS = 8;
|
|
55
82
|
/** `u32` words per record in `FrameBuffers.overlay`. */
|
|
56
83
|
declare const OVERLAY_WORDS = 10;
|
|
57
84
|
/** Page-space clip rectangle `[minX, minY, maxX, maxY]`. */
|
|
@@ -64,10 +91,11 @@ interface FrameBuffers {
|
|
|
64
91
|
vertices: Float32Array;
|
|
65
92
|
indices: Uint32Array;
|
|
66
93
|
/**
|
|
67
|
-
* `BATCH_WORDS` (
|
|
68
|
-
* clipMinY clipMaxX clipMaxY`. The clip words are f32 bits in page
|
|
69
|
-
* four zero means unclipped. Texture 0 = solid color. A new batch
|
|
70
|
-
* whenever the texture
|
|
94
|
+
* `BATCH_WORDS` (8) words per batch: `firstIndex indexCount texture clipMinX
|
|
95
|
+
* clipMinY clipMaxX clipMaxY isolate`. The clip words are f32 bits in page
|
|
96
|
+
* space; all four zero means unclipped. Texture 0 = solid color. A new batch
|
|
97
|
+
* starts whenever the texture, the clip rect or the isolation group changes —
|
|
98
|
+
* use `readBatch`.
|
|
71
99
|
*/
|
|
72
100
|
batches: Uint32Array;
|
|
73
101
|
/**
|
|
@@ -99,6 +127,16 @@ interface Batch {
|
|
|
99
127
|
texture: number;
|
|
100
128
|
/** Page-space clip rect, or undefined when unclipped. */
|
|
101
129
|
clip?: ClipRect;
|
|
130
|
+
/**
|
|
131
|
+
* Isolation group, or 0 for an ordinary batch.
|
|
132
|
+
*
|
|
133
|
+
* Non-zero means the batch is one translucent shape's whole mark, and the
|
|
134
|
+
* backend has to paint each pixel of it once: the engine bakes opacity into
|
|
135
|
+
* vertex alpha, so a stroke that crosses itself would otherwise blend twice
|
|
136
|
+
* and show the crossing as a dark knot. The number is unique per frame and
|
|
137
|
+
* rises in draw order, which is what lets a backend tell two of them apart.
|
|
138
|
+
*/
|
|
139
|
+
isolate: number;
|
|
102
140
|
}
|
|
103
141
|
interface OverlayEntry {
|
|
104
142
|
handle: Handle;
|
|
@@ -155,6 +193,49 @@ declare class CommandWriter {
|
|
|
155
193
|
remove(handle: Handle): void;
|
|
156
194
|
/** `pathWords` is the flat f32 path encoding (opcode, args...). */
|
|
157
195
|
setGeometry(handle: Handle, pathWords: ArrayLike<number>): void;
|
|
196
|
+
/**
|
|
197
|
+
* Build a built-in geo silhouette in the engine from its parameters.
|
|
198
|
+
*
|
|
199
|
+
* This is the fast path behind {@link CommandWriter.setGeometry} for the
|
|
200
|
+
* shapes the engine knows how to draw itself: five words go over the wire
|
|
201
|
+
* instead of a whole outline, and the trigonometry runs in WebAssembly. The
|
|
202
|
+
* resulting path is identical to the one the TypeScript generators produce.
|
|
203
|
+
*
|
|
204
|
+
* `kind` is an index from {@link GEO_KIND}. A silhouette that is not in that
|
|
205
|
+
* table has no generator here — send its path with `setGeometry`.
|
|
206
|
+
*/
|
|
207
|
+
/**
|
|
208
|
+
* A built-in geo outline, generated by the engine from `(kind, w, h)`.
|
|
209
|
+
*
|
|
210
|
+
* `strokeWidth` reaches only the open marks drawn *inside* an outline — the X
|
|
211
|
+
* of an x-box — whose ends sit on the outline and would otherwise spike
|
|
212
|
+
* through it by half a stroke. The outline itself does not move with it.
|
|
213
|
+
*/
|
|
214
|
+
setGeo(handle: Handle, kind: number, w: number, h: number, flags?: number, strokeWidth?: number): void;
|
|
215
|
+
/**
|
|
216
|
+
* A smooth cubic spline through `points` (interleaved `x, y`), built in the
|
|
217
|
+
* engine. `flags` takes {@link GEO_FLAG}.CLOSED.
|
|
218
|
+
*/
|
|
219
|
+
setSpline(handle: Handle, points: ArrayLike<number>, flags?: number): void;
|
|
220
|
+
/**
|
|
221
|
+
* A polyline through `points` (interleaved `x, y`), or a polygon when
|
|
222
|
+
* `flags` has {@link GEO_FLAG}.CLOSED.
|
|
223
|
+
*/
|
|
224
|
+
setPoly(handle: Handle, points: ArrayLike<number>, flags?: number): void;
|
|
225
|
+
private writePoints;
|
|
226
|
+
/**
|
|
227
|
+
* A freehand stroke, as the runs the pen actually made.
|
|
228
|
+
*
|
|
229
|
+
* Each segment is `{ points, freehand }` with `points` interleaved `x, y`; a
|
|
230
|
+
* `freehand` run is smoothed in the engine (one 1-2-1 pass, endpoints fixed)
|
|
231
|
+
* before the runs are concatenated into one outline. `flags` takes
|
|
232
|
+
* {@link GEO_FLAG}.CLOSED, which closes the stroke when it has more than two
|
|
233
|
+
* points.
|
|
234
|
+
*/
|
|
235
|
+
setDraw(handle: Handle, segments: readonly {
|
|
236
|
+
points: ArrayLike<number>;
|
|
237
|
+
freehand: boolean;
|
|
238
|
+
}[], flags?: number): void;
|
|
158
239
|
setStyle(handle: Handle, s: StyleWords): void;
|
|
159
240
|
/** Set the fill texture of a shape (0 = solid fill). Independent of `setStyle`. */
|
|
160
241
|
setTexture(handle: Handle, texture: number): void;
|
|
@@ -220,6 +301,20 @@ declare class EngineBridge {
|
|
|
220
301
|
bounds(handle: Handle): [number, number, number, number] | null;
|
|
221
302
|
/** Geometry page bounds `[minX, minY, maxX, maxY]` or null: `bounds` without the stroke pad. */
|
|
222
303
|
geometryBounds(handle: Handle): [number, number, number, number] | null;
|
|
304
|
+
/**
|
|
305
|
+
* The outline a `dash: "draw"` shape is really drawn with, as path words, or
|
|
306
|
+
* `null` when the shape is missing or drawn in another style.
|
|
307
|
+
*
|
|
308
|
+
* The hand-drawn style strokes a perturbed, corner-rounded sketch rather than
|
|
309
|
+
* the shape's own geometry, so this is the only way for the host to trace what
|
|
310
|
+
* is on screen instead of what the maths says. It comes from the same call the
|
|
311
|
+
* renderer makes, so the two agree exactly.
|
|
312
|
+
*
|
|
313
|
+
* Copied out rather than handed over as a view: the engine's buffer is reused
|
|
314
|
+
* by the next call that writes to it, and a caller holding a view would find
|
|
315
|
+
* its contents replaced underneath.
|
|
316
|
+
*/
|
|
317
|
+
sketchPath(handle: Handle): Float32Array | null;
|
|
223
318
|
unionBounds(handles: ArrayLike<number>): [number, number, number, number] | null;
|
|
224
319
|
/** Union of every shape's ink bounds, or null when the scene is empty. */
|
|
225
320
|
allBounds(): [number, number, number, number] | null;
|
|
@@ -248,6 +343,15 @@ declare class EngineBridge {
|
|
|
248
343
|
declare function loadEngine(input?: InitInput): Promise<EngineBridge>;
|
|
249
344
|
/** Synchronous variant for tests / Node: pass the compiled bytes. */
|
|
250
345
|
declare function loadEngineSync(bytes: SyncInitInput): EngineBridge;
|
|
346
|
+
/**
|
|
347
|
+
* The engine created by the most recent `loadEngine` / `loadEngineSync`, or
|
|
348
|
+
* `null` if the module has never been loaded in this realm.
|
|
349
|
+
*
|
|
350
|
+
* `new Editor({...})` falls back to this when no `engine` is passed, which is
|
|
351
|
+
* what lets an app construct an editor without threading the bridge through
|
|
352
|
+
* its own code.
|
|
353
|
+
*/
|
|
354
|
+
declare function getLoadedEngine(): EngineBridge | null;
|
|
251
355
|
declare function engineVersion(): string;
|
|
252
356
|
|
|
253
|
-
export { BATCH_WORDS, type Batch, type CameraState, type ClipRect, CommandWriter, EngineBridge, FLAG, type FrameBuffers, HIT_FILTER, type Handle, OP, OVERLAY_WORDS, type OverlayEntry, PATH_OP, type StyleWords, VERTEX_FLOATS, bitsf32, engineVersion, f32bits, loadEngine, loadEngineSync, readClip };
|
|
357
|
+
export { BATCH_WORDS, type Batch, type CameraState, type ClipRect, CommandWriter, EngineBridge, FLAG, type FrameBuffers, GEO_FLAG, GEO_KIND, HIT_FILTER, type Handle, OP, OVERLAY_WORDS, type OverlayEntry, PATH_OP, type StyleWords, VERTEX_FLOATS, bitsf32, engineVersion, f32bits, getLoadedEngine, loadEngine, loadEngineSync, readClip };
|
package/dist/index.js
CHANGED
|
@@ -8,8 +8,44 @@ var OP = {
|
|
|
8
8
|
SET_GEOMETRY: 3,
|
|
9
9
|
SET_STYLE: 4,
|
|
10
10
|
CLEAR: 5,
|
|
11
|
-
SET_TEXTURE: 6
|
|
11
|
+
SET_TEXTURE: 6,
|
|
12
|
+
SET_GEO: 7,
|
|
13
|
+
SET_SPLINE: 8,
|
|
14
|
+
SET_POLY: 9,
|
|
15
|
+
SET_DRAW: 10
|
|
12
16
|
};
|
|
17
|
+
var GEO_FLAG = {
|
|
18
|
+
/** `setGeo`: mirror the silhouette left-to-right inside its own box. */
|
|
19
|
+
FLIP_X: 1 << 0,
|
|
20
|
+
/** `setGeo`: mirror the silhouette top-to-bottom inside its own box. */
|
|
21
|
+
FLIP_Y: 1 << 1,
|
|
22
|
+
/** `setSpline` / `setPoly` / `setDraw`: close the outline. */
|
|
23
|
+
CLOSED: 1 << 0,
|
|
24
|
+
/** `setDraw`, per segment: this run came from a pen, so smooth it. */
|
|
25
|
+
FREEHAND: 1 << 0
|
|
26
|
+
};
|
|
27
|
+
var GEO_KIND = Object.freeze({
|
|
28
|
+
rectangle: 0,
|
|
29
|
+
ellipse: 1,
|
|
30
|
+
triangle: 2,
|
|
31
|
+
diamond: 3,
|
|
32
|
+
pentagon: 4,
|
|
33
|
+
hexagon: 5,
|
|
34
|
+
octagon: 6,
|
|
35
|
+
star: 7,
|
|
36
|
+
rhombus: 8,
|
|
37
|
+
"rhombus-2": 9,
|
|
38
|
+
oval: 10,
|
|
39
|
+
trapezoid: 11,
|
|
40
|
+
"arrow-right": 12,
|
|
41
|
+
"arrow-left": 13,
|
|
42
|
+
"arrow-up": 14,
|
|
43
|
+
"arrow-down": 15,
|
|
44
|
+
"x-box": 16,
|
|
45
|
+
"check-box": 17,
|
|
46
|
+
cloud: 18,
|
|
47
|
+
heart: 19
|
|
48
|
+
});
|
|
13
49
|
var PATH_OP = {
|
|
14
50
|
MOVE: 0,
|
|
15
51
|
LINE: 1,
|
|
@@ -33,7 +69,7 @@ var HIT_FILTER = {
|
|
|
33
69
|
HOLLOW_ONLY: 4
|
|
34
70
|
};
|
|
35
71
|
var VERTEX_FLOATS = 8;
|
|
36
|
-
var BATCH_WORDS =
|
|
72
|
+
var BATCH_WORDS = 8;
|
|
37
73
|
var OVERLAY_WORDS = 10;
|
|
38
74
|
var scratchF32 = new Float32Array(1);
|
|
39
75
|
var scratchU32 = new Uint32Array(scratchF32.buffer);
|
|
@@ -113,6 +149,90 @@ var CommandWriter = class {
|
|
|
113
149
|
for (let k = 0; k < n; k++) v[i++] = f32bits(pathWords[k]);
|
|
114
150
|
this.len = i;
|
|
115
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Build a built-in geo silhouette in the engine from its parameters.
|
|
154
|
+
*
|
|
155
|
+
* This is the fast path behind {@link CommandWriter.setGeometry} for the
|
|
156
|
+
* shapes the engine knows how to draw itself: five words go over the wire
|
|
157
|
+
* instead of a whole outline, and the trigonometry runs in WebAssembly. The
|
|
158
|
+
* resulting path is identical to the one the TypeScript generators produce.
|
|
159
|
+
*
|
|
160
|
+
* `kind` is an index from {@link GEO_KIND}. A silhouette that is not in that
|
|
161
|
+
* table has no generator here — send its path with `setGeometry`.
|
|
162
|
+
*/
|
|
163
|
+
/**
|
|
164
|
+
* A built-in geo outline, generated by the engine from `(kind, w, h)`.
|
|
165
|
+
*
|
|
166
|
+
* `strokeWidth` reaches only the open marks drawn *inside* an outline — the X
|
|
167
|
+
* of an x-box — whose ends sit on the outline and would otherwise spike
|
|
168
|
+
* through it by half a stroke. The outline itself does not move with it.
|
|
169
|
+
*/
|
|
170
|
+
setGeo(handle, kind, w, h, flags = 0, strokeWidth = 0) {
|
|
171
|
+
this.ensure(7);
|
|
172
|
+
const v = this.view;
|
|
173
|
+
let i = this.len;
|
|
174
|
+
v[i++] = OP.SET_GEO;
|
|
175
|
+
v[i++] = handle;
|
|
176
|
+
v[i++] = kind >>> 0;
|
|
177
|
+
v[i++] = flags >>> 0;
|
|
178
|
+
v[i++] = f32bits(w);
|
|
179
|
+
v[i++] = f32bits(h);
|
|
180
|
+
v[i++] = f32bits(strokeWidth);
|
|
181
|
+
this.len = i;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* A smooth cubic spline through `points` (interleaved `x, y`), built in the
|
|
185
|
+
* engine. `flags` takes {@link GEO_FLAG}.CLOSED.
|
|
186
|
+
*/
|
|
187
|
+
setSpline(handle, points, flags = 0) {
|
|
188
|
+
this.writePoints(OP.SET_SPLINE, handle, points, flags);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* A polyline through `points` (interleaved `x, y`), or a polygon when
|
|
192
|
+
* `flags` has {@link GEO_FLAG}.CLOSED.
|
|
193
|
+
*/
|
|
194
|
+
setPoly(handle, points, flags = 0) {
|
|
195
|
+
this.writePoints(OP.SET_POLY, handle, points, flags);
|
|
196
|
+
}
|
|
197
|
+
writePoints(op, handle, points, flags) {
|
|
198
|
+
const n = points.length >> 1;
|
|
199
|
+
this.ensure(4 + n * 2);
|
|
200
|
+
const v = this.view;
|
|
201
|
+
let i = this.len;
|
|
202
|
+
v[i++] = op;
|
|
203
|
+
v[i++] = handle;
|
|
204
|
+
v[i++] = flags >>> 0;
|
|
205
|
+
v[i++] = n;
|
|
206
|
+
for (let k = 0; k < n * 2; k++) v[i++] = f32bits(points[k]);
|
|
207
|
+
this.len = i;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* A freehand stroke, as the runs the pen actually made.
|
|
211
|
+
*
|
|
212
|
+
* Each segment is `{ points, freehand }` with `points` interleaved `x, y`; a
|
|
213
|
+
* `freehand` run is smoothed in the engine (one 1-2-1 pass, endpoints fixed)
|
|
214
|
+
* before the runs are concatenated into one outline. `flags` takes
|
|
215
|
+
* {@link GEO_FLAG}.CLOSED, which closes the stroke when it has more than two
|
|
216
|
+
* points.
|
|
217
|
+
*/
|
|
218
|
+
setDraw(handle, segments, flags = 0) {
|
|
219
|
+
let words = 4;
|
|
220
|
+
for (const seg of segments) words += 2 + (seg.points.length & -2);
|
|
221
|
+
this.ensure(words);
|
|
222
|
+
const v = this.view;
|
|
223
|
+
let i = this.len;
|
|
224
|
+
v[i++] = OP.SET_DRAW;
|
|
225
|
+
v[i++] = handle;
|
|
226
|
+
v[i++] = flags >>> 0;
|
|
227
|
+
v[i++] = segments.length;
|
|
228
|
+
for (const seg of segments) {
|
|
229
|
+
const n = seg.points.length >> 1;
|
|
230
|
+
v[i++] = seg.freehand ? GEO_FLAG.FREEHAND : 0;
|
|
231
|
+
v[i++] = n;
|
|
232
|
+
for (let k = 0; k < n * 2; k++) v[i++] = f32bits(seg.points[k]);
|
|
233
|
+
}
|
|
234
|
+
this.len = i;
|
|
235
|
+
}
|
|
116
236
|
setStyle(handle, s) {
|
|
117
237
|
this.ensure(8);
|
|
118
238
|
const v = this.view;
|
|
@@ -229,7 +349,12 @@ var EngineBridge = class _EngineBridge {
|
|
|
229
349
|
}
|
|
230
350
|
/** Decode one batch record starting at word `offset` (a multiple of `BATCH_WORDS`). */
|
|
231
351
|
static readBatch(batches, offset) {
|
|
232
|
-
const b = {
|
|
352
|
+
const b = {
|
|
353
|
+
firstIndex: batches[offset],
|
|
354
|
+
indexCount: batches[offset + 1],
|
|
355
|
+
texture: batches[offset + 2],
|
|
356
|
+
isolate: batches[offset + 7]
|
|
357
|
+
};
|
|
233
358
|
const clip = readClip(batches, offset + 3);
|
|
234
359
|
if (clip) b.clip = clip;
|
|
235
360
|
return b;
|
|
@@ -282,6 +407,24 @@ var EngineBridge = class _EngineBridge {
|
|
|
282
407
|
geometryBounds(handle) {
|
|
283
408
|
return this.engine.geometry_bounds(handle) ? this.readBox() : null;
|
|
284
409
|
}
|
|
410
|
+
/**
|
|
411
|
+
* The outline a `dash: "draw"` shape is really drawn with, as path words, or
|
|
412
|
+
* `null` when the shape is missing or drawn in another style.
|
|
413
|
+
*
|
|
414
|
+
* The hand-drawn style strokes a perturbed, corner-rounded sketch rather than
|
|
415
|
+
* the shape's own geometry, so this is the only way for the host to trace what
|
|
416
|
+
* is on screen instead of what the maths says. It comes from the same call the
|
|
417
|
+
* renderer makes, so the two agree exactly.
|
|
418
|
+
*
|
|
419
|
+
* Copied out rather than handed over as a view: the engine's buffer is reused
|
|
420
|
+
* by the next call that writes to it, and a caller holding a view would find
|
|
421
|
+
* its contents replaced underneath.
|
|
422
|
+
*/
|
|
423
|
+
sketchPath(handle) {
|
|
424
|
+
const n = this.engine.sketch_path(handle);
|
|
425
|
+
if (n === 0) return null;
|
|
426
|
+
return new Float32Array(this.memory.buffer, this.engine.f32_ptr(), n).slice();
|
|
427
|
+
}
|
|
285
428
|
unionBounds(handles) {
|
|
286
429
|
if (this.cmd.pending > 0) throw new Error("unionBounds called with unflushed commands");
|
|
287
430
|
const n = handles.length;
|
|
@@ -354,17 +497,23 @@ async function loadEngine(input) {
|
|
|
354
497
|
initPromise = input === void 0 ? loadDefault() : init({ module_or_path: input }).then((o) => o.memory);
|
|
355
498
|
}
|
|
356
499
|
const memory = await initPromise;
|
|
357
|
-
|
|
500
|
+
currentEngine = new EngineBridge(new Engine(), memory);
|
|
501
|
+
return currentEngine;
|
|
358
502
|
}
|
|
359
503
|
function loadEngineSync(bytes) {
|
|
360
504
|
const out = initSync({ module: bytes });
|
|
361
505
|
initPromise = Promise.resolve(out.memory);
|
|
362
|
-
|
|
506
|
+
currentEngine = new EngineBridge(new Engine(), out.memory);
|
|
507
|
+
return currentEngine;
|
|
508
|
+
}
|
|
509
|
+
var currentEngine = null;
|
|
510
|
+
function getLoadedEngine() {
|
|
511
|
+
return currentEngine;
|
|
363
512
|
}
|
|
364
513
|
function engineVersion() {
|
|
365
514
|
return version();
|
|
366
515
|
}
|
|
367
516
|
|
|
368
|
-
export { BATCH_WORDS, CommandWriter, EngineBridge, FLAG, HIT_FILTER, OP, OVERLAY_WORDS, PATH_OP, VERTEX_FLOATS, bitsf32, engineVersion, f32bits, loadEngine, loadEngineSync, readClip };
|
|
517
|
+
export { BATCH_WORDS, CommandWriter, EngineBridge, FLAG, GEO_FLAG, GEO_KIND, HIT_FILTER, OP, OVERLAY_WORDS, PATH_OP, VERTEX_FLOATS, bitsf32, engineVersion, f32bits, getLoadedEngine, loadEngine, loadEngineSync, readClip };
|
|
369
518
|
//# sourceMappingURL=index.js.map
|
|
370
519
|
//# sourceMappingURL=index.js.map
|