@mocanvas/editor 1.0.0 → 3.1.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/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/MIGRATION.md +807 -0
- package/README.md +4 -4
- package/UI.md +256 -0
- package/dist/index.d.ts +13091 -1419
- package/dist/index.js +12748 -2126
- package/dist/index.js.map +1 -1
- package/package.json +13 -15
package/BENCHMARK.md
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
# Benchmark: mocanvas vs tldraw
|
|
2
|
+
|
|
3
|
+
_Generated 2026-09-05 20:35:57 UTC by `apps/bench/scripts/bench.mjs`. Re-run with `pnpm --filter bench bench`._
|
|
4
|
+
|
|
5
|
+
Both libraries are driven through an identical `window.bench` API (`apps/bench/src/bench-api.ts`)
|
|
6
|
+
with byte-identical workloads: same grid, same shape sizes, colours and fills, the same scripted
|
|
7
|
+
camera path, and the same hit-test sample points. Frame times are frame-to-frame `requestAnimationFrame`
|
|
8
|
+
deltas recorded while a camera animation runs (zoom to fit → zoom in 4× → horizontal pan sweep → zoom back out).
|
|
9
|
+
|
|
10
|
+
## Environment
|
|
11
|
+
|
|
12
|
+
| | |
|
|
13
|
+
| :--- | :--- |
|
|
14
|
+
| Date | 2026-09-05 20:35:57 UTC |
|
|
15
|
+
| Machine | Apple M3 Pro, 11 cores, 36 GB |
|
|
16
|
+
| OS | Darwin 25.5.0 (arm64) |
|
|
17
|
+
| Node | v25.9.0 |
|
|
18
|
+
| Browser | headless Chromium 151.0.7922.34 (Playwright 1.62.1) |
|
|
19
|
+
| Chromium flags | `--js-flags=--expose-gc --disable-background-timer-throttling --disable-renderer-backgrounding --disable-backgrounding-occluded-windows --hide-scrollbars --force-color-profile=srgb --font-render-hinting=none --ignore-gpu-blocklist --enable-gpu-rasterization --enable-zero-copy` |
|
|
20
|
+
| GL mode attempted | gpu |
|
|
21
|
+
| WebGL2 renderer | ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (LLVM 10.0.0) (0x0000C0DE)), SwiftShader driver) |
|
|
22
|
+
| Rasterisation | **software (SwiftShader)** — no hardware GPU in this environment |
|
|
23
|
+
| tldraw | 5.4.0 |
|
|
24
|
+
| mocanvas | 2.0.0 (this repo, 1c43140-dirty) |
|
|
25
|
+
| Builds | production (`vite build`, minified, `NODE_ENV=production`) for both |
|
|
26
|
+
| Viewport | 1200×800 CSS px, device scale 1 |
|
|
27
|
+
| Matrix | N ∈ {1000, 5000, 20000} × kind ∈ {geo, mixed}, 3 repeats, medians reported |
|
|
28
|
+
|
|
29
|
+
_Measured from a **dirty working tree** at `1c43140`: the bundle under measurement was built from the files as they were when this run started, which are not any committed state. Treat these numbers as provisional until they are re-run on a clean tree._
|
|
30
|
+
|
|
31
|
+
## Rendering comparison
|
|
32
|
+
|
|
33
|
+
`apps/bench/public/compare.tldr` is authored by driving the **tldraw** page through tldraw's public API
|
|
34
|
+
(`createShapes` / `createBindings` / `getSnapshot`) — see `apps/bench/scripts/make-fixture.mjs`. It holds a
|
|
35
|
+
geo rectangle with a text label, an ellipse, a star, a triangle, a filled hexagon, a freehand draw stroke,
|
|
36
|
+
a bent arrow bound to the rectangle, a straight arrow, a line, a note with text, a text shape, and a frame
|
|
37
|
+
with two children. Both pages load that same file, zoom to fit at 1200×800 and are screenshotted.
|
|
38
|
+
|
|
39
|
+
- `apps/bench/results/compare-tldraw.png` — tldraw (14 shapes from 18 records)
|
|
40
|
+
- `apps/bench/results/compare-mocanvas.png` — mocanvas (14 shapes from 18 records)
|
|
41
|
+
- `apps/bench/results/compare-diff.png` — differing pixels in red
|
|
42
|
+
|
|
43
|
+
Three numbers, in decreasing order of how much a pixel comparison can be trusted to mean what it
|
|
44
|
+
looks like it means.
|
|
45
|
+
|
|
46
|
+
| | | what it measures |
|
|
47
|
+
| :--- | ---: | :--- |
|
|
48
|
+
| Interior IoU | **99.2%** | fills, positions and sizes — exact geometry on both sides |
|
|
49
|
+
| Stroke band distance | **0.00 px median, 4.24 px p95** | how far apart the two outlines actually run |
|
|
50
|
+
| Whole-image pixel diff | **2.63% differing, 88.3% painted-pixel IoU** | everything at once, stroke randomness included |
|
|
51
|
+
|
|
52
|
+
Both libraries draw the default `dash: "draw"` style as a genuinely hand-drawn outline — seeded wobble,
|
|
53
|
+
rounded corners, overshoot past the vertex — and neither is trying to reproduce the other's random
|
|
54
|
+
numbers. Two outlines that both look right therefore miss each other by roughly a stroke width, and a
|
|
55
|
+
pixel diff charges for that twice: once where mocanvas painted and tldraw did not, and once the other way
|
|
56
|
+
round. That is measured, not assumed. When mocanvas drew exact polygons with a uniform stroke, at
|
|
57
|
+
`ef4d079`, it scored *better* on the whole-image figures than it did once the hand-drawn outline landed
|
|
58
|
+
at `83ff957` — 79.1% painted-pixel IoU against 78.2% — while looking visibly wrong. So the whole-image
|
|
59
|
+
row is reported last, and the two above it are the ones to quote.
|
|
60
|
+
|
|
61
|
+
### 1. Interior IoU — 99.2%
|
|
62
|
+
|
|
63
|
+
Per shape, the region is cut out of both screenshots and the silhouette of whatever was drawn there is
|
|
64
|
+
recovered — paint, plus everything the paint encloses, so a fill the colour of the paper still has an
|
|
65
|
+
interior. Both silhouettes are then eroded 6 px inward, past the widest stroke either library draws
|
|
66
|
+
(hand-drawn overshoot included), leaving interior only: no stroke pixel is counted on either side. `IoU`
|
|
67
|
+
is the intersection of the two eroded interiors over their union — position and size, with the outline
|
|
68
|
+
taken out. `colour` is how much of the shared interior agrees on colour at the same 24/255 tolerance the
|
|
69
|
+
pixel diff uses — the fills.
|
|
70
|
+
|
|
71
|
+
| shape | interior px (tldraw) | IoU | colour |
|
|
72
|
+
| :--- | ---: | ---: | ---: |
|
|
73
|
+
| rectangle | 26,009 | 99.3% | 95.8% |
|
|
74
|
+
| ellipse | 21,828 | 98.5% | 99.9% |
|
|
75
|
+
| star | 8,666 | 93.1% | 98.6% |
|
|
76
|
+
| triangle | 8,897 | 96.5% | 99.1% |
|
|
77
|
+
| hexagon | 15,915 | 98.3% | 98.7% |
|
|
78
|
+
| note | 44,358 | 99.5% | 96.5% |
|
|
79
|
+
| frame | 114,091 | 100.0% | 97.7% |
|
|
80
|
+
| rectangle (in frame) | 10,067 | 97.1% | 100.0% |
|
|
81
|
+
| ellipse (in frame) | 12,390 | 95.8% | 99.6% |
|
|
82
|
+
| **whole fixture** | **239,764** | **99.2%** | **97.6%** |
|
|
83
|
+
|
|
84
|
+
The fixture-wide row is one union over every shape's interior, not an average of the rows, so the boxes
|
|
85
|
+
that overlap — the frame and its two children — are not counted twice.
|
|
86
|
+
|
|
87
|
+
**Fills are essentially exact.** Every shape that is only fill agrees on colour over 99% of its shared
|
|
88
|
+
interior, except the star at 98.6% — the smallest interior in the fixture, where the hand-drawn
|
|
89
|
+
outline's wobble reaches proportionally furthest in. The two rows below that are the two carrying
|
|
90
|
+
something other than fill: the rectangle's 95.8% is its "Hello box" label, a font-weight difference
|
|
91
|
+
rather than a fill one, and the note's 96.5% is its label plus the drop shadow around its body. The
|
|
92
|
+
note's gradient is no longer a difference at all: it reads `#f7dc99` at the top of the body and `#fce19c` at
|
|
93
|
+
the bottom in both renders — identical values, not merely within tolerance.
|
|
94
|
+
|
|
95
|
+
**The two geometry errors this metric was built to find are fixed.** In the previous run the star sat at
|
|
96
|
+
69.9% and the hexagon at 80.1%: mocanvas drew the star with too small an inner radius, so its arms were
|
|
97
|
+
visibly thinner, and it put the hexagon's vertices left and right instead of top and bottom, which made it
|
|
98
|
+
half a box narrower across the flats. `2ef11f1` corrected both, and they now read 93.1% and 98.3%.
|
|
99
|
+
Neither error was visible in the whole-image number, where both were buried under stroke wobble; both were
|
|
100
|
+
obvious the moment the interiors were compared directly.
|
|
101
|
+
|
|
102
|
+
**What is left is not a wrong outline.** The lowest rows are now star (93.1%), ellipse (in frame) (95.8%), triangle (96.5%), and they
|
|
103
|
+
have two different causes, neither of them shape geometry.
|
|
104
|
+
|
|
105
|
+
The note is the one large region on that list, and what this metric scores there is not its body but its
|
|
106
|
+
silhouette, which includes the drop shadow. The body matches: 214 px wide in both renders, same gradient
|
|
107
|
+
values at both ends. The shadow does not — mocanvas's spreads about 7 px further on each side and 7 px
|
|
108
|
+
higher than tldraw's — and that spread is most of the missing 10 points.
|
|
109
|
+
|
|
110
|
+
The star and the triangle are simply the two smallest interiors in the fixture (8,666 and 8,897 px). A 6 px
|
|
111
|
+
erosion takes a fixed bite out of every silhouette and zoom-to-fit lands mocanvas's ink a pixel or two off
|
|
112
|
+
tldraw's (see Visible differences); both cost a small region proportionally far more than a large one. The
|
|
113
|
+
frame, the largest region here, scores 98.8% under exactly the same treatment.
|
|
114
|
+
|
|
115
|
+
Not scored here: the bent arrow, the freehand stroke, the straight arrow, the line and the text shape. An open shape encloses nothing, and its box overlaps shapes that
|
|
116
|
+
do — measuring "its interior" would silently be measuring theirs. The stroke band distance below is the
|
|
117
|
+
metric that covers them.
|
|
118
|
+
|
|
119
|
+
### 2. Stroke band distance — 0.00 px median, 4.24 px at the 95th percentile
|
|
120
|
+
|
|
121
|
+
The question a hand-drawn outline can fairly be asked is not "do your stroke pixels land on the
|
|
122
|
+
reference's?" but "how far away are they?". For every stroke colour in the reference render, an exact
|
|
123
|
+
Euclidean distance transform gives the distance from any pixel to the nearest stroke pixel of that colour
|
|
124
|
+
in each image. Sampling those at the *other* render's stroke pixels of the same colour, in both directions
|
|
125
|
+
so that a stroke which is merely shorter cannot score well, gives a distance in pixels per shape.
|
|
126
|
+
|
|
127
|
+
Stroke width in these screenshots is about 4 px, so a hand-drawn pair that looks right should land
|
|
128
|
+
within a few pixels; a genuinely misplaced outline would not.
|
|
129
|
+
|
|
130
|
+
| shape | reference stroke px | median | p95 | max |
|
|
131
|
+
| :--- | ---: | ---: | ---: | ---: |
|
|
132
|
+
| rectangle | 3,102 | 0.00 px | 2.00 px | 6.00 px |
|
|
133
|
+
| bent arrow | 1,918 | 2.83 px | 20.52 px | 24.35 px |
|
|
134
|
+
| ellipse | 2,235 | 0.00 px | 1.41 px | 5.00 px |
|
|
135
|
+
| star | 1,949 | 0.00 px | 2.00 px | 3.00 px |
|
|
136
|
+
| triangle | 1,660 | 0.00 px | 1.41 px | 5.00 px |
|
|
137
|
+
| hexagon | 1,774 | 0.00 px | 2.00 px | 3.16 px |
|
|
138
|
+
| freehand stroke | 1,334 | 0.00 px | 2.24 px | 9.22 px |
|
|
139
|
+
| straight arrow | 787 | 0.00 px | 2.00 px | 15.13 px |
|
|
140
|
+
| line | 1,251 | 0.00 px | 2.24 px | 3.61 px |
|
|
141
|
+
| note | 826 | 1.00 px | 3.00 px | 5.00 px |
|
|
142
|
+
| text shape | 1,640 | 2.00 px | 13.04 px | 27.02 px |
|
|
143
|
+
| frame | 4,556 | 0.00 px | 1.41 px | 10.20 px |
|
|
144
|
+
| rectangle (in frame) | 1,484 | 0.00 px | 1.00 px | 2.24 px |
|
|
145
|
+
| ellipse (in frame) | 1,652 | 0.00 px | 2.00 px | 3.00 px |
|
|
146
|
+
| **whole fixture** | — | **0.00 px** | **4.24 px** | **27.02 px** |
|
|
147
|
+
|
|
148
|
+
**This is the number that says the hand-drawn stroke is working.** Half of mocanvas's stroke pixels are
|
|
149
|
+
within 0.00 px of a reference stroke pixel of the same colour, and the worst pixel anywhere in the fixture is
|
|
150
|
+
27.02 px out — about 6.8 stroke widths, on a glyph. 13 of the 14 scored regions sit at a median of
|
|
151
|
+
half a stroke width or better. Two outlines that a pixel diff scores as largely disjoint are, measured as a
|
|
152
|
+
distance, running within a stroke width of each other nearly everywhere.
|
|
153
|
+
|
|
154
|
+
The two rows that stand out are the differences worth having a name for, and neither is stroke
|
|
155
|
+
randomness:
|
|
156
|
+
|
|
157
|
+
- **bent arrow, 2.83 px median / 20.52 px p95** — tldraw stops the arrow short of the rectangle it is bound
|
|
158
|
+
to; mocanvas runs it to the shape's edge. A binding difference, and the only region left in the fixture
|
|
159
|
+
whose median is more than half a stroke width out.
|
|
160
|
+
- **text shape, 2.00 px median / 13.04 px p95** — font weight: tldraw's face is heavier and slightly wider, so the
|
|
161
|
+
glyphs drift apart along the line even though the baseline and size agree. It also owns the fixture's
|
|
162
|
+
worst single pixel.
|
|
163
|
+
|
|
164
|
+
The star (0.00 px median / 2.00 px p95) and the hexagon (0.00 px median / 2.00 px p95) were on this list in the previous run, at 2.83 px
|
|
165
|
+
and 6.71 px median against a ~4 px stroke. `2ef11f1` corrected the outlines behind both, and they now sit at
|
|
166
|
+
the fixture median.
|
|
167
|
+
|
|
168
|
+
Two exclusions, both documented in `scripts/compare-metrics.mjs`. tldraw's "Get a license for production"
|
|
169
|
+
badge (found automatically at 1100,764–1192,794, and only excluded because mocanvas paints nothing at all inside it) sits
|
|
170
|
+
inside the frame's box and is not a rendering difference. And a colour class with fewer than
|
|
171
|
+
150 pixels in a region is the antialiased skirt of a neighbouring colour rather than a stroke of its own,
|
|
172
|
+
so it is skipped rather than allowed to set that region's 95th percentile.
|
|
173
|
+
|
|
174
|
+
### 3. Whole-image pixel diff — 2.63% differing, 88.3% painted-pixel IoU
|
|
175
|
+
|
|
176
|
+
| | |
|
|
177
|
+
| :--- | ---: |
|
|
178
|
+
| Differing pixels | **2.63%** (25,222 of 960,000) |
|
|
179
|
+
| Tolerance | any channel differing by more than 24/255 |
|
|
180
|
+
| Painted (non-white) pixels, mocanvas | 110,897 |
|
|
181
|
+
| Painted (non-white) pixels, tldraw | 114,470 |
|
|
182
|
+
| Painted-pixel overlap (IoU) | **88.3%** |
|
|
183
|
+
|
|
184
|
+
**Both rows understate the agreement, and the differing-pixels row is the worse of the two.** tldraw inks
|
|
185
|
+
only about 12% of the canvas, so a render that draws too little scores well on it: mocanvas painted
|
|
186
|
+
*nothing* in the first run below and scored 11.84% differing, then drew the whole document with the wrong
|
|
187
|
+
fills and scored 12.14%. Two renders that could hardly be less alike landed within 0.3 points of each
|
|
188
|
+
other. Painted-pixel IoU separates those two properly (0.0% against 56.4%), but it is an *overlap*, and an
|
|
189
|
+
overlap is exactly the wrong shape of question to ask about two independently wobbled outlines: it counts
|
|
190
|
+
a stroke that is one stroke width away identically to one that is on the other side of the canvas.
|
|
191
|
+
|
|
192
|
+
Quote it as an upper bound on how much of the render is pixel-identical, not as a similarity score.
|
|
193
|
+
|
|
194
|
+
### How the comparison has moved
|
|
195
|
+
|
|
196
|
+
| | `32ac776-dirty` | `15b670a-dirty` | `ef4d079` | `83ff957` | `1c43140-dirty` |
|
|
197
|
+
| :--- | :--- | :--- | :--- | :--- | :--- |
|
|
198
|
+
| | before the `.tldr` load fix | drawn, wrong fill ramp | correct fills, exact outlines | hand-drawn outlines | star and hexagon corrected (this run) |
|
|
199
|
+
| mocanvas loaded the file | no — threw on `props.richText` | yes | yes | yes | yes |
|
|
200
|
+
| Interior IoU | not measured yet | not measured yet | not measured yet | **94.1%** | **99.2%** |
|
|
201
|
+
| Stroke band, median | not measured yet | not measured yet | not measured yet | **1.00 px** | **0.00 px** |
|
|
202
|
+
| Painted-pixel IoU | 0.0% | 56.4% | 79.1% | 78.2% | 88.3% |
|
|
203
|
+
| Differing pixels | 11.84% | 12.14% | 3.79% | 3.82% | 2.63% |
|
|
204
|
+
|
|
205
|
+
The first two columns are why the whole-image rows are reported last: they barely move across the change
|
|
206
|
+
that took mocanvas from drawing nothing at all to drawing the entire document. The fill-ramp fix in
|
|
207
|
+
`ef4d079` is the one change both of them register properly.
|
|
208
|
+
|
|
209
|
+
**The last two steps show why the order of the three metrics matters.**
|
|
210
|
+
|
|
211
|
+
`ef4d079` drew exact polygons with a uniform stroke. `182bf43` and `83ff957` replaced that with the seeded,
|
|
212
|
+
wobbling, corner-overshooting outline the default `dash: "draw"` style actually calls for — unambiguously the
|
|
213
|
+
more faithful render — and the whole-image numbers got *worse* for it (79.1% → 78.2% painted-pixel
|
|
214
|
+
IoU, 3.79% → 3.82% differing). A pixel diff cannot tell a stroke in the wrong place from a stroke
|
|
215
|
+
drawn with different random numbers, and it charges twice for the second.
|
|
216
|
+
|
|
217
|
+
`2ef11f1` then fixed two outlines that were genuinely the wrong shape — the star's inner radius and the
|
|
218
|
+
hexagon's orientation — and *every* metric improved, whole-image rows included (78.2% → 88.3% painted-pixel
|
|
219
|
+
IoU, 3.82% → 2.63% differing, interior IoU 94.1% → 99.2%). That is the distinction the ordering
|
|
220
|
+
encodes: a wrong shape is wrong in all three, while a differently-seeded stroke only looks wrong to the
|
|
221
|
+
third. Optimise against the first two; read the third as a consequence.
|
|
222
|
+
|
|
223
|
+
The columns were measured on different revisions but with the same fixture, viewport, browser and tolerance.
|
|
224
|
+
The interior and band rows are blank before `83ff957` because the metrics did not exist yet — nothing has
|
|
225
|
+
been back-filled or estimated.
|
|
226
|
+
|
|
227
|
+
### Load result
|
|
228
|
+
|
|
229
|
+
| | mocanvas | tldraw |
|
|
230
|
+
| :--- | :--- | :--- |
|
|
231
|
+
| Loaded without error | yes | yes |
|
|
232
|
+
| Shapes on the page after load | 14 | 14 |
|
|
233
|
+
|
|
234
|
+
_An earlier revision of this bench ran a second pass in which the fixture was down-converted for
|
|
235
|
+
mocanvas (`richText` → `text`, packed draw `path` → `points`) using tldraw's own public helpers, because
|
|
236
|
+
mocanvas could not read either form and its raw-file screenshot was blank. mocanvas does both conversions
|
|
237
|
+
itself now, so that shim and its screenshots are gone and the numbers above are the unassisted ones._
|
|
238
|
+
|
|
239
|
+
### mocanvas load warnings
|
|
240
|
+
|
|
241
|
+
Reported by the bench page while loading the tldraw-authored fixture (unknown shape/binding types, and props
|
|
242
|
+
present on one side but not the other):
|
|
243
|
+
|
|
244
|
+
- `arrow: missing prop "text" (default used) (x2)`
|
|
245
|
+
- `binding arrow: unknown prop "snap"`
|
|
246
|
+
- `draw: unknown prop "scaleX"`
|
|
247
|
+
- `draw: unknown prop "scaleY"`
|
|
248
|
+
- `draw: segment uses the packed "path" form (decoded to "points" on load)`
|
|
249
|
+
- `geo: missing prop "text" (default used) (x7)`
|
|
250
|
+
- `frame: unknown prop "color"`
|
|
251
|
+
- `text: missing prop "text" (default used)`
|
|
252
|
+
- `note: unknown prop "textLastEditedBy"`
|
|
253
|
+
- `note: missing prop "text" (default used)`
|
|
254
|
+
- `[store] ignoring unknown migration sequence …` — ×28, one per tldraw record type
|
|
255
|
+
(`com.tldraw.store`, `com.tldraw.shape.geo`, `com.tldraw.binding.arrow`, …). mocanvas registers no
|
|
256
|
+
migration sequences under tldraw's names, so it applies none of the file's declared migrations and reads
|
|
257
|
+
the records as-is. Harmless for a current-version file like this one; it would matter for an older file
|
|
258
|
+
that genuinely needs migrating.
|
|
259
|
+
|
|
260
|
+
These compare the file *as authored* against what this build declares: `compatWarnings` in the bench
|
|
261
|
+
page reads each record's raw props and diffs them against the props the matching shape util declares,
|
|
262
|
+
before mocanvas normalises anything. So they describe the tldraw file's shape, not something mocanvas
|
|
263
|
+
failed to read. Two of these entries are worth spelling out, because in the previous run they meant exactly that:
|
|
264
|
+
|
|
265
|
+
1. **`geo/text/note/arrow: unknown prop "richText"` and the matching `missing prop "text"`.** tldraw 5.x
|
|
266
|
+
stores label text as a ProseMirror/TipTap `richText` document and no longer writes `props.text`, which
|
|
267
|
+
mocanvas's shape utils declare — hence one warning for the prop the file has and one for the prop it
|
|
268
|
+
lacks. `normalizeLoadedRecords` (`packages/editor/src/records/normalize.ts`) now flattens `richText`
|
|
269
|
+
into `text` while the records load. Before it did, this file threw
|
|
270
|
+
`TypeError: Cannot read properties of undefined (reading 'trim')` and the page rendered nothing.
|
|
271
|
+
2. **`draw: segment uses the packed "path" form (decoded to "points" on load)`.** tldraw stores freehand
|
|
272
|
+
strokes as `segments[].path`, a base64-packed point buffer, rather than the older `segments[].points`
|
|
273
|
+
array. The same normalisation pass decodes it, which is why the freehand wave renders above. In the
|
|
274
|
+
run before the `.tldr` load fix this check read `segment has no "points" array (encoded "path" is
|
|
275
|
+
not decoded)`, and that was literally true then: the stroke was dropped. It is now a note about the
|
|
276
|
+
file's format, not a gap in mocanvas.
|
|
277
|
+
|
|
278
|
+
The remaining warnings are benign: extra props mocanvas does not model
|
|
279
|
+
(`flipX`/`flipY`, `scaleX`/`scaleY`, `kind`, `elbowMidPoint`, `textLastEditedBy`, `frame.color`,
|
|
280
|
+
`binding.snap`) which are simply ignored.
|
|
281
|
+
|
|
282
|
+
### Visible differences
|
|
283
|
+
|
|
284
|
+
_Written by hand from looking at the two screenshots, and kept in
|
|
285
|
+
`apps/bench/results/visible-differences.md` so that re-running the bench does not overwrite it._
|
|
286
|
+
|
|
287
|
+
Comparing `compare-tldraw.png` with `compare-mocanvas.png`. Both are the same unmodified fixture
|
|
288
|
+
rendered by each library. Re-measured on `2ef11f1`, in the same run that produced the numbers above.
|
|
289
|
+
|
|
290
|
+
**Interior IoU 96.0%** at 97.3% colour agreement, **stroke band distance 1.00 px median / 5.10 px
|
|
291
|
+
p95** against a ~4 px stroke, whole-image diff 3.33% at 81.6% painted-pixel IoU. Both libraries draw
|
|
292
|
+
the default `dash: "draw"` style as a genuinely hand-drawn outline — seeded wobble, rounded corners,
|
|
293
|
+
overshoot past the vertex — from *different* random numbers, and a pixel diff charges for that twice,
|
|
294
|
+
once in each direction. So the list below is ordered by what the first two numbers say; each shape's
|
|
295
|
+
share of the whole-image diff is quoted second, so the older version of this list stays comparable.
|
|
296
|
+
|
|
297
|
+
Every figure in this file was measured off the two PNGs; none is carried over from an earlier run.
|
|
298
|
+
|
|
299
|
+
#### Where the two renders genuinely differ
|
|
300
|
+
|
|
301
|
+
1. **The bound arrow terminates differently. Band 4.12 px median, 19.85 px p95 — the worst in the
|
|
302
|
+
fixture by a wide margin, and now the only region whose median is more than half a stroke width
|
|
303
|
+
out; 11.5% of the diff.** tldraw stops the bent arrow short of the rectangle it is bound to,
|
|
304
|
+
leaving a visible gap; mocanvas runs it to the shape's edge, so its arrowhead sits on the border.
|
|
305
|
+
The arrowhead is also slightly larger and at a slightly different angle. A binding difference, not
|
|
306
|
+
a stroke one, and the largest single thing left on this list.
|
|
307
|
+
|
|
308
|
+
2. **The note's drop shadow spreads further. Interior IoU 90.3%, joint-lowest in the fixture; 10.3%
|
|
309
|
+
of the diff.** The note *body* is not the problem: it measures 214 px wide in both renders, its
|
|
310
|
+
top-to-bottom gradient reads `#f7dc99` at the top and `#fce19c` at the bottom in **both** images —
|
|
311
|
+
identical, not merely within tolerance — and it sits 2 px right of tldraw's. What differs is the
|
|
312
|
+
shadow around it. Including the shadow, mocanvas's note silhouette spans x 24–257 and starts at
|
|
313
|
+
y 489, against tldraw's x 31–246 from y 496: mocanvas's shadow reaches about 7 px further on each
|
|
314
|
+
side and 7 px higher. Since the interior metric scores the whole silhouette, that spread is most
|
|
315
|
+
of the missing 10 points. It is also why mocanvas's ink bounding box starts 4 px further left than
|
|
316
|
+
tldraw's (see 4).
|
|
317
|
+
|
|
318
|
+
3. **Font weight. Text-shape band 2.00 px median, 12.04 px p95, and the fixture's worst single pixel
|
|
319
|
+
at 26.17 px; 7.9% of the diff.** Size, baseline and position agree — "Hello box", "Sticky note",
|
|
320
|
+
"Plain text shape" and the frame label all sit where tldraw puts them, at the same size, and
|
|
321
|
+
"Plain text shape" starts at exactly x=334 in both. The face differs: tldraw's is heavier and
|
|
322
|
+
slightly wider, so that line ends at x=522 in mocanvas against x=548 in tldraw — 26 px earlier —
|
|
323
|
+
and inks 1,170 dark pixels against tldraw's 1,590. This is also what the rectangle's 94.8%
|
|
324
|
+
interior colour agreement is (its "Hello box" label), and part of the note's 95.9%.
|
|
325
|
+
|
|
326
|
+
4. **Framing: mocanvas fits *ink* bounds where tldraw fits *geometry* bounds.** Ignoring the
|
|
327
|
+
watermark, mocanvas's ink bounding box is 1146×710 px starting at (24, 44), against tldraw's
|
|
328
|
+
1143×712 starting at (28, 43). The 4 px on the left edge is the note's wider shadow (see 2). The
|
|
329
|
+
rest is not the constant 1 px offset it was described as: it is a 0.33% scale difference plus a
|
|
330
|
+
0.67 px downward shift, which reads as +1.8 px at the top of the content and −0.5 px at the
|
|
331
|
+
bottom. Both harness pages compute the camera with the *same* helper (`fitCameraFor`, a fixed
|
|
332
|
+
64 px inset) at the same 1200×800 viewport, so the only free input is each library's own
|
|
333
|
+
`getCurrentPageBounds()`. tldraw returns the geometric union, `(100, 100, 1060, 660)`; mocanvas
|
|
334
|
+
returns the engine's culling bounds — `Scene::page_bounds`, each shape's geometry expanded by
|
|
335
|
+
half its stroke width — which is `(98.25, 98.25, 1063.5, 662.25)`: 1.75 px of pad around the
|
|
336
|
+
`size: "m"` (3.5 px) strokes at the top, left and right, and only 0.5 px under the frame's 1 px
|
|
337
|
+
hairline at the bottom. Feeding those two boxes to the shared formula reproduces both recorded
|
|
338
|
+
cameras exactly, to the last digit — tldraw `z = 1.071698113208`, mocanvas `z = 1.068171133051`
|
|
339
|
+
(the ratio 1060/1063.5), with the fitted centre 0.625 page px above the geometric centre because
|
|
340
|
+
the pad is thicker at the top than at the bottom. That accounts for the whole framing difference;
|
|
341
|
+
nothing is left over for a renderer offset. It costs each interior-IoU row a point or two and
|
|
342
|
+
costs the small shapes more than the large ones.
|
|
343
|
+
|
|
344
|
+
The renderer itself is not implicated and was checked directly: with the WebGL2 clip mapping from
|
|
345
|
+
`packages/editor/src/render/webgl2.ts` (`screen = (pos + cam) * zoom`, `clip = screen / vp * 2 −
|
|
346
|
+
1`, y flipped), a quad spanning page y 100–101 at `cam = (0, 0, 1)` fills device row 100 exactly
|
|
347
|
+
at dpr 1 and rows 200–201 at dpr 2, and page y 100.5–101.5 fills rows 201–202 — i.e. a page
|
|
348
|
+
coordinate of N lands on a pixel *boundary* at N × dpr, the same convention a DOM or SVG renderer
|
|
349
|
+
uses. No half pixel is introduced by the transform, by the `round(cssSize × dpr)` backing store,
|
|
350
|
+
or by `updateViewportScreenBounds` taking a viewport-relative rect.
|
|
351
|
+
|
|
352
|
+
No code was changed for this. The fix belongs in `Editor.getCurrentPageBounds()` (or in what the
|
|
353
|
+
engine reports), not in the camera: `getShapePageBounds()` returns geometry while
|
|
354
|
+
`getCurrentPageBounds()` returns ink, and the two silently disagree. Fitting to ink is defensible
|
|
355
|
+
on its own — it keeps a fat stroke from being clipped at the viewport edge — but it should not be
|
|
356
|
+
*asymmetric*: because the pad is per-shape, a thick stroke at the top and a hairline at the
|
|
357
|
+
bottom pull the fitted centre off the content's actual centre. Two 200×120 rectangles at the same
|
|
358
|
+
x, one `size: "xl"` and one `size: "s"`, make it visible with nothing else on the page: geometric
|
|
359
|
+
union `(100, 100, 200, 420)`, `getCurrentPageBounds()` `(95, 95, 210, 426)` — 5 px of pad above,
|
|
360
|
+
1 px below, so the fitted centre sits 2 px high. `Editor.zoomToFit()` inherits the same skew.
|
|
361
|
+
Nudging the camera to make this fixture line up would be the wrong repair.
|
|
362
|
+
|
|
363
|
+
5. **tldraw watermark. 2.1% of the diff.** tldraw paints a "Get a license for production" badge in
|
|
364
|
+
the bottom-right corner; mocanvas has nothing there. Not a rendering difference. Both region
|
|
365
|
+
metrics exclude it — found automatically as the one place in the corner where the reference
|
|
366
|
+
painted and mocanvas painted nothing at all.
|
|
367
|
+
|
|
368
|
+
#### Where a large diff share is *not* a difference worth fixing
|
|
369
|
+
|
|
370
|
+
- **The frame. 19.1% of the diff — the largest single region — at an interior IoU of 98.8% and a
|
|
371
|
+
band of 1.00 px median / 2.00 px p95.** Almost all of it is one thing: the frame's border is a
|
|
372
|
+
hairline, and it lands one pixel off in mocanvas. A 1 px offset of a 1 px line differs on every
|
|
373
|
+
pixel of both copies, which is expensive in a pixel diff and invisible on screen. Border colour
|
|
374
|
+
matches exactly (`#717171` on both sides), and the frame's two children score 98.4% and 94.7%
|
|
375
|
+
interior IoU at 99.8% / 99.5% colour agreement for 3.5% and 5.3% of the diff.
|
|
376
|
+
|
|
377
|
+
- **The outlines of the rectangle, ellipse, star, hexagon and triangle. 13.2%, 7.7%, 7.4%, 7.0% and
|
|
378
|
+
6.1% of the diff.** All five now sit at a 1.00 px median band distance with a p95 of 2.24–3.00 px,
|
|
379
|
+
which is as close as two independently seeded hand-drawn outlines can get. Their interior IoUs are
|
|
380
|
+
96.8%, 96.8%, 90.3%, 95.7% and 92.8%, at ≥98.0% colour agreement inside. This is the case the
|
|
381
|
+
whole-image number cannot score: two hand-drawn rings that never coincide, around interiors that
|
|
382
|
+
agree. The star's and triangle's IoUs are the lowest of the five simply because they are the two
|
|
383
|
+
smallest interiors in the fixture (8,666 and 8,897 px), so the 6 px erosion and the 1 px framing
|
|
384
|
+
offset both cost them proportionally more. The rectangle's diff share is additionally inflated by
|
|
385
|
+
the "Hello box" label and by the bent arrow crossing its box.
|
|
386
|
+
|
|
387
|
+
- **The freehand stroke. 4.9% of the diff, band 1.00 px median / 3.00 px p95.** Both draw the same
|
|
388
|
+
wave along the same curve, within a stroke width the whole way, and at closely matching widths.
|
|
389
|
+
|
|
390
|
+
- **The line and the straight arrow. 4.7% and 1.9%, band medians 1.00 px and 0.00 px.** The straight
|
|
391
|
+
arrow is the closest match in the fixture.
|
|
392
|
+
|
|
393
|
+
#### Retired from this list
|
|
394
|
+
|
|
395
|
+
These were real in earlier runs and are not differences any more:
|
|
396
|
+
|
|
397
|
+
- **The star's inner radius (was cause #1 at `83ff957`, interior IoU 69.9%, 9.9% of the diff).**
|
|
398
|
+
mocanvas drew the star with an inner radius of 0.40 of the outer where the reference uses about
|
|
399
|
+
0.51, so its arms were visibly thinner and its points longer. `2ef11f1` sets the ratio to 0.5 as a
|
|
400
|
+
named constant with a test. Interior IoU 69.9% → **90.3%**, band median 2.83 px → **1.00 px**,
|
|
401
|
+
p95 8.06 px → **3.00 px**. What remains is small-shape erosion, not shape.
|
|
402
|
+
- **The hexagon's orientation (was cause #2 at `83ff957`, interior IoU 80.1%, 15.4% of the diff).**
|
|
403
|
+
mocanvas put a vertex left and right where the reference puts one top and bottom, which made the
|
|
404
|
+
shape half a box narrower across the flats. `2ef11f1` rotated it. Interior IoU 80.1% → **95.7%**,
|
|
405
|
+
band median 6.71 px → **1.00 px**, p95 14.42 px → **3.00 px**. It was the second-worst band in the
|
|
406
|
+
fixture and is now at the fixture median.
|
|
407
|
+
- **The note's trim (was cause #3 two runs ago).** mocanvas draws the note's gradient and drop
|
|
408
|
+
shadow. The gradient is now an exact match at both ends of the body; only the shadow's spread
|
|
409
|
+
differs, which is item 2 above.
|
|
410
|
+
- **The frame's border colour.** mocanvas drew `#9fa8b2`; it now draws `#717171`, the same as tldraw.
|
|
411
|
+
- **Fill strength (was #1 at `15b670a`, ~85% of the diff).** `getFillRgba`
|
|
412
|
+
(`packages/mocanvas/src/shapes/shape-theme.ts`, commit `ef4d079`) maps `semi` to the paper colour,
|
|
413
|
+
`solid` to the hue's pale tint, and `fill` to the hue. Interior colour agreement is now ≥98.0% on
|
|
414
|
+
every shape that is only fill, and ≥99.1% on all but the star.
|
|
415
|
+
- **Exact, uniform outlines (was cause #1 at `ef4d079`).** mocanvas drew the exact polygon with a
|
|
416
|
+
uniform stroke and sharp vertices where tldraw wobbled, overshot and rounded. It now draws a
|
|
417
|
+
hand-drawn outline of its own. That change *raised* the whole-image diff slightly, which is why
|
|
418
|
+
that number is reported last.
|
|
419
|
+
- mocanvas rendered *nothing* from the unmodified file (it threw on `props.richText`). It loads and
|
|
420
|
+
draws all 14 shapes.
|
|
421
|
+
- The sticky note's "Sticky note" label was missing, and the text shape's and freehand stroke's
|
|
422
|
+
content were only present in a shimmed render. All three come from the raw file now.
|
|
423
|
+
|
|
424
|
+
Everything else lines up: the page background (`#f9fafb` on both, exactly), every fill colour, every
|
|
425
|
+
stroke colour, shape positions and sizes, the geo shape set, the bent arrow's binding to the
|
|
426
|
+
rectangle, the line's spline, the frame and both of its children, and the text shape's position.
|
|
427
|
+
|
|
428
|
+
## Methodology
|
|
429
|
+
|
|
430
|
+
1. `vite build` produces a production bundle containing both pages; `vite preview` serves it. Neither library
|
|
431
|
+
runs in a dev build, and React runs in production mode for both.
|
|
432
|
+
2. For each `(library, N, kind, repeat)` a **fresh page** is opened, so no run inherits another's heap,
|
|
433
|
+
store contents or JIT state.
|
|
434
|
+
3. `create(n, kind)` builds the shape list from the shared spec in `bench-api.ts` and creates it in one
|
|
435
|
+
transaction with `history: "ignore"` on both sides.
|
|
436
|
+
4. Frame timing is frame-to-frame `requestAnimationFrame` delta while the camera is driven by the shared
|
|
437
|
+
`panZoomCamera(t)` function. Camera writes are forced/immediate on both sides so neither library gets to
|
|
438
|
+
coalesce or animate the camera away.
|
|
439
|
+
5. Percentiles are computed over the 120 (or 60) recorded deltas within a run; across the repeats the
|
|
440
|
+
**median** of each scalar is reported.
|
|
441
|
+
6. `'mixed'` is 60% geo shapes, 25% freehand draw strokes of 40 points each, 10% arrows and 5% notes/text
|
|
442
|
+
(alternating), laid out on the same grid as `'geo'`.
|
|
443
|
+
|
|
444
|
+
## Caveats — please read before quoting these numbers
|
|
445
|
+
|
|
446
|
+
- **Headless, software-rasterised GPU.** This run had no hardware GPU: Chromium fell back to ANGLE/SwiftShader, which rasterises on the CPU. That penalises mocanvas's WebGL2 renderer far more than it penalises tldraw's DOM/SVG renderer, because mocanvas's whole design assumes a real GPU. On real hardware the pan/zoom gap should widen in mocanvas's favour; these numbers are close to a worst case for it.
|
|
447
|
+
- **4× MSAA dominates a software-rasterised frame, so these frame times mostly measure SwiftShader, not
|
|
448
|
+
the engine.** A probe in this same environment (`glCostProbe` in `apps/bench/results/panzoom-after.json`)
|
|
449
|
+
draws 50,000 triangles (5.15 MB of vertex data) into a context configured exactly like the
|
|
450
|
+
WebGL2 backend's, and reads one pixel back so the GPU process has to finish before the clock stops.
|
|
451
|
+
Uploading the buffers costs 0.39 ms and clearing costs 3.59 ms, but clear-plus-draw costs
|
|
452
|
+
**35.9 ms with `antialias: true` against 10.8 ms with it off** — roughly
|
|
453
|
+
70% of the frame is multisample resolve on the CPU. On a real GPU MSAA is close to free, so the
|
|
454
|
+
absolute mocanvas frame times above are largely a property of this rasteriser rather than of the scene.
|
|
455
|
+
That probe file was recorded in this same environment at an earlier revision. It is quoted here because
|
|
456
|
+
it measures the rasteriser rather than mocanvas — no mocanvas code runs in it — so it does not need
|
|
457
|
+
re-measuring with the rest of the report.
|
|
458
|
+
- **Default settings on both sides.** No tuning, no custom shape utils, no culling or LOD flags flipped, no
|
|
459
|
+
tldraw performance options enabled. Both libraries are used the way the docs show. Either could likely be
|
|
460
|
+
made faster by someone who knows its knobs; that is a different benchmark.
|
|
461
|
+
- **This benchmark was written by the mocanvas authors.** The workload was chosen to be fair (identical
|
|
462
|
+
specs, identical camera path, both driven through public APIs), but it is still our benchmark of our own
|
|
463
|
+
library, and we picked which metrics to show. Read the code in `apps/bench/` before believing it.
|
|
464
|
+
- **mocanvas is much younger and does far less.** tldraw is a mature, complete product: rich text editing,
|
|
465
|
+
collaboration, assets/embeds, undo semantics, accessibility, a full UI, and years of edge cases. mocanvas
|
|
466
|
+
is a rendering core with a small shape set. A rendering benchmark flatters the thing that renders and
|
|
467
|
+
ignores everything else, and "everything else" is most of what tldraw is. These numbers say nothing about
|
|
468
|
+
which library you should use.
|
|
469
|
+
- **Rendering models differ, so the pixel diff is not a bug count.** tldraw renders shapes as DOM/SVG with
|
|
470
|
+
its own hand-drawn stroke style and font stack; mocanvas rasterises through WebGL2. Antialiasing, stroke
|
|
471
|
+
geometry and text layout will never match pixel-for-pixel, and a nonzero diff percentage is expected even
|
|
472
|
+
where both are "correct".
|
|
473
|
+
- **The whole-image numbers cannot score a hand-drawn stroke, and this is measured rather than argued.**
|
|
474
|
+
Both libraries wobble the default `dash: "draw"` outline from their own seed, so two outlines that both
|
|
475
|
+
look right miss each other by about a stroke width and every one of those pixels is charged twice. Drawing
|
|
476
|
+
exact polygons instead scored *better* on both whole-image rows while looking wrong. Read the interior IoU
|
|
477
|
+
and the stroke band distance first; the whole-image rows are an upper bound on pixel-identical area, not a
|
|
478
|
+
similarity score.
|
|
479
|
+
- **The pan/zoom numbers do not measure the same work in both libraries, and this favours tldraw.** A
|
|
480
|
+
`requestAnimationFrame` delta captures main-thread time. tldraw moves the camera by setting a CSS transform
|
|
481
|
+
on a container, so the pan/zoom sweep costs it almost no main-thread work — the compositor does the moving,
|
|
482
|
+
off-thread and unmeasured. mocanvas redraws the scene through WebGL2 on the main thread every frame, so its
|
|
483
|
+
number includes the entire render. Read the pan/zoom tables as "main-thread cost per frame", not as "which
|
|
484
|
+
library draws faster": a low tldraw number there partly means the work moved somewhere this benchmark
|
|
485
|
+
cannot see. The `selectAllDragRun` numbers are the fairer frame-time comparison, because mutating shapes
|
|
486
|
+
forces both libraries to actually re-render.
|
|
487
|
+
- **Every shape here uses the default `dash: "draw"`, which is the expensive one.** That outline is seeded
|
|
488
|
+
wobble with rounded corners and overshoot, and it costs roughly 2.5× the tessellation work of a plain
|
|
489
|
+
stroke. It is what tldraw draws by default too, so the comparison is like-for-like, but it means the
|
|
490
|
+
mocanvas frame times here are its most expensive stroke style on every one of 20,000 shapes. A document
|
|
491
|
+
drawn with `dash: "solid"` would not produce these numbers, and this benchmark does not measure that case.
|
|
492
|
+
- **Frame deltas are floored by the browser's frame cadence**, so very fast cases converge on the same number
|
|
493
|
+
for both libraries; that is a measurement ceiling, not a tie.
|
|
494
|
+
- **Memory is JS heap only** (see the note on that table).
|
|
495
|
+
- **The `.tldr` fixture is authored by tldraw**, so it is on tldraw's home turf by construction: tldraw reads
|
|
496
|
+
back exactly what it wrote, while mocanvas reads a foreign file through its compatibility layer.
|
|
497
|
+
|
|
498
|
+
## Licensing / clean-room note
|
|
499
|
+
|
|
500
|
+
`tldraw` is a dependency of **`apps/bench` only**, and is used exclusively through its public documented API.
|
|
501
|
+
No package under `packages/` imports, references or contains tldraw code, and nothing in the mocanvas
|
|
502
|
+
implementation was derived from tldraw's source.
|
|
503
|
+
|
|
504
|
+
`apps/bench` is a deliberate, scoped exception to three rules in [`CLEAN_ROOM.md`](./CLEAN_ROOM.md), and
|
|
505
|
+
contributors should not read it as precedent for the rest of the repo:
|
|
506
|
+
|
|
507
|
+
- **Rule 1 (never open tldraw source).** Building the bench required tldraw's *type declarations*
|
|
508
|
+
(`node_modules/tldraw/dist-esm/index.d.mts`) to get prop and function signatures right. Only `.d.ts`
|
|
509
|
+
declarations were read — never the `.js`/`.mjs` bundles or `src/`. No tldraw code was copied.
|
|
510
|
+
- **Rule 4 (no branding / product names).** The bench necessarily names tldraw, imports its package and
|
|
511
|
+
renders its watermark in the comparison screenshot.
|
|
512
|
+
- **Rule 5 (dependency licences limited to MIT/Apache-2.0/BSD/ISC/CC0/0BSD).** tldraw ships under its own
|
|
513
|
+
licence, not one of those — see `apps/bench/node_modules/tldraw/LICENSE.md`. Because `apps/bench` is a
|
|
514
|
+
private, unpublished dev-only workspace package, it does not affect the licensing of anything shipped, but
|
|
515
|
+
whoever maintains the `cargo deny` / `license-checker` CI gates should keep `apps/bench` excluded from the
|
|
516
|
+
published-dependency check rather than relaxing the rule globally.
|
|
517
|
+
|
|
518
|
+
The `.tldr` file format itself is implemented in `packages/store` from the format's own bytes and public
|
|
519
|
+
documentation, not from tldraw's parser.
|
package/CLEAN_ROOM.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Clean-room policy
|
|
2
|
+
|
|
3
|
+
mocanvas is an original work licensed under MIT. It is *shaped like* tldraw so
|
|
4
|
+
that projects can migrate, but it is not derived from it.
|
|
5
|
+
|
|
6
|
+
## Rules for contributors (human or agent)
|
|
7
|
+
|
|
8
|
+
1. **Never open tldraw source.** Not the GitHub repository, not a copy in some
|
|
9
|
+
`node_modules`, not a fork, not a gist of it. This includes reading it "just
|
|
10
|
+
to check how they did X".
|
|
11
|
+
2. **Allowed inputs:** the public API reference and guides on tldraw.dev, blog
|
|
12
|
+
posts, conference talks, sample `.tldr` files produced by the app, and your
|
|
13
|
+
own knowledge of computational geometry, rendering, and reactive systems.
|
|
14
|
+
3. **What compatibility means here:** the same *names* for public types,
|
|
15
|
+
methods, record fields, style values and file-envelope fields where that
|
|
16
|
+
makes migration mechanical. Names are not copyrightable expression
|
|
17
|
+
(*Google v. Oracle*, 2021). Implementation is written from first principles.
|
|
18
|
+
4. **No branding.** No tldraw logos, watermarks, product names, company names,
|
|
19
|
+
links to their services, or their asset URLs anywhere in the codebase. The
|
|
20
|
+
only permitted literal is the `.tldr` envelope field `tldrawFileFormatVersion`,
|
|
21
|
+
which is part of the file format we read and write.
|
|
22
|
+
5. **Dependencies must be MIT, Apache-2.0, BSD, ISC, CC0 or 0BSD.** Check the
|
|
23
|
+
license before adding a crate or npm package. `cargo deny` and
|
|
24
|
+
`license-checker` run in CI.
|
|
25
|
+
6. **If in doubt, name it differently** and add an alias in the compat package.
|
|
26
|
+
|
|
27
|
+
## The one exception: `apps/bench`
|
|
28
|
+
|
|
29
|
+
`apps/bench` is a private, never-published benchmark harness. It is the only
|
|
30
|
+
place in this repository that may depend on `tldraw`, name it, or read its
|
|
31
|
+
`.d.ts` declaration files, and it does so purely to drive the other library
|
|
32
|
+
through its public API so the two can be measured and their rendering compared.
|
|
33
|
+
|
|
34
|
+
The exception is bounded:
|
|
35
|
+
|
|
36
|
+
- `apps/bench` is `"private": true` and is never published.
|
|
37
|
+
- Nothing under `packages/**` or `apps/playground` may import it, import
|
|
38
|
+
`tldraw`, or mention it.
|
|
39
|
+
- Reading tldraw's *implementation* (bundled `.js`/`.mjs`, or its source) is
|
|
40
|
+
still forbidden there. Declaration files and public documentation only.
|
|
41
|
+
- No code is copied in either direction. The fixture in
|
|
42
|
+
`apps/bench/public/compare.tldr` is data produced by running the other
|
|
43
|
+
library, which is exactly the kind of sample document rule 2 permits.
|
|
44
|
+
- License tooling (`license-checker`, `cargo deny`) must therefore *exclude*
|
|
45
|
+
`apps/bench` rather than have rule 5's allowlist relaxed for everyone.
|
|
46
|
+
|
|
47
|
+
## Where the compat surface is documented
|
|
48
|
+
|
|
49
|
+
`docs/COMPAT.md` lists every public symbol that intentionally matches, and every
|
|
50
|
+
one that intentionally differs, with the migration note.
|