@tongue-core/engine 0.1.0-rc.2 → 0.1.0-rc.3

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.
@@ -0,0 +1,233 @@
1
+ import { cos, PI, sin } from "@tongue-core/film/model";
2
+ /** Corners sharper than this (a ratio of miter length to width) are beveled, as in SVG. */
3
+ const MITER_LIMIT = 4;
4
+ const dist = (ax, ay, bx, by) => Math.sqrt((bx - ax) * (bx - ax) + (by - ay) * (by - ay));
5
+ /** Straight pieces per curve: enough that none strays more than `tolerance` from the curve. */
6
+ const pieces = (bend, tolerance) => Math.min(96, Math.max(1, Math.ceil(Math.sqrt(bend / tolerance))));
7
+ /** The path as polylines, one per subpath, with repeated points removed. */
8
+ export function flatten(verbs, tolerance) {
9
+ const lines = [];
10
+ let current = [];
11
+ let x = 0, y = 0;
12
+ const add = (px, py, smooth) => {
13
+ const last = current[current.length - 1];
14
+ if (!last || last.x !== px || last.y !== py)
15
+ current.push({ x: px, y: py, smooth });
16
+ x = px;
17
+ y = py;
18
+ };
19
+ const end = (closed) => {
20
+ if (current.length > 0)
21
+ lines.push({ points: current, closed: closed && current.length > 2 });
22
+ current = [];
23
+ };
24
+ for (const v of verbs) {
25
+ switch (v.kind) {
26
+ case "move":
27
+ end(false);
28
+ add(v.x, v.y, false);
29
+ break;
30
+ case "line":
31
+ add(v.x, v.y, false);
32
+ break;
33
+ case "quad": {
34
+ const x0 = x, y0 = y;
35
+ const n = pieces(0.25 * dist(x0 - v.cx, y0 - v.cy, v.cx - v.x, v.cy - v.y), tolerance);
36
+ for (let i = 1; i <= n; i++) {
37
+ const t = i / n, u = 1 - t;
38
+ add(u * u * x0 + 2 * u * t * v.cx + t * t * v.x, u * u * y0 + 2 * u * t * v.cy + t * t * v.y, i < n);
39
+ }
40
+ break;
41
+ }
42
+ case "cubic": {
43
+ const x0 = x, y0 = y;
44
+ const bend = 0.75 * Math.max(dist(x0 - v.c1x, y0 - v.c1y, v.c1x - v.c2x, v.c1y - v.c2y), dist(v.c1x - v.c2x, v.c1y - v.c2y, v.c2x - v.x, v.c2y - v.y));
45
+ const n = pieces(bend, tolerance);
46
+ for (let i = 1; i <= n; i++) {
47
+ const t = i / n, u = 1 - t;
48
+ const a = u * u * u, b = 3 * u * u * t, c = 3 * u * t * t, d = t * t * t;
49
+ add(a * x0 + b * v.c1x + c * v.c2x + d * v.x, a * y0 + b * v.c1y + c * v.c2y + d * v.y, i < n);
50
+ }
51
+ break;
52
+ }
53
+ case "close": {
54
+ const first = current[0];
55
+ if (first) {
56
+ // Back at the start, the closing line's end is that same point, not a second one.
57
+ const last = current[current.length - 1];
58
+ if (last && last.x === first.x && last.y === first.y && current.length > 1)
59
+ current.pop();
60
+ end(true);
61
+ x = first.x;
62
+ y = first.y;
63
+ add(x, y, false);
64
+ }
65
+ break;
66
+ }
67
+ }
68
+ }
69
+ // A lone point left by a close, or by a move that nothing followed.
70
+ if (current.length > 1)
71
+ end(false);
72
+ return lines;
73
+ }
74
+ const lengthOf = (line) => {
75
+ let total = 0;
76
+ const count = line.closed ? line.points.length : line.points.length - 1;
77
+ for (let i = 0; i < count; i++) {
78
+ const a = line.points[i], b = line.points[(i + 1) % line.points.length];
79
+ if (a && b)
80
+ total += dist(a.x, a.y, b.x, b.y);
81
+ }
82
+ return total;
83
+ };
84
+ /** The stretches of `lines` between two distances along their combined length. */
85
+ function trimmed(lines, from, to) {
86
+ const out = [];
87
+ let start = 0;
88
+ for (const line of lines) {
89
+ const length = lengthOf(line);
90
+ const a = Math.max(from - start, 0), b = Math.min(to - start, length);
91
+ start += length;
92
+ if (b <= a && !(length === 0 && a === 0 && b === 0))
93
+ continue;
94
+ if (a <= 0 && b >= length) {
95
+ out.push(line);
96
+ continue;
97
+ }
98
+ const points = [];
99
+ let walked = 0;
100
+ const count = line.closed ? line.points.length : line.points.length - 1;
101
+ for (let i = 0; i < count; i++) {
102
+ const p = line.points[i], q = line.points[(i + 1) % line.points.length];
103
+ if (!p || !q)
104
+ continue;
105
+ const seg = dist(p.x, p.y, q.x, q.y);
106
+ const lo = (a - walked) / seg, hi = (b - walked) / seg;
107
+ if (hi >= 0 && lo <= 1) {
108
+ const t0 = Math.max(lo, 0), t1 = Math.min(hi, 1);
109
+ if (points.length === 0)
110
+ points.push({ x: p.x + (q.x - p.x) * t0, y: p.y + (q.y - p.y) * t0, smooth: false });
111
+ if (t1 > t0)
112
+ points.push({ x: p.x + (q.x - p.x) * t1, y: p.y + (q.y - p.y) * t1, smooth: t1 < 1 ? false : q.smooth });
113
+ }
114
+ walked += seg;
115
+ }
116
+ if (points.length > 1)
117
+ out.push({ points, closed: false });
118
+ }
119
+ return out;
120
+ }
121
+ /** A polygon, wound so its signed area is positive, so overlaps add up and never cancel out. */
122
+ function polygon(out, xs, ys) {
123
+ const n = xs.length;
124
+ let area = 0;
125
+ for (let i = 0; i < n; i++) {
126
+ const j = (i + 1) % n;
127
+ area += (xs[i] ?? 0) * (ys[j] ?? 0) - (xs[j] ?? 0) * (ys[i] ?? 0);
128
+ }
129
+ if (area === 0)
130
+ return;
131
+ for (let k = 0; k < n; k++) {
132
+ const i = area > 0 ? k : n - 1 - k;
133
+ out.push({ kind: k === 0 ? "move" : "line", x: xs[i] ?? 0, y: ys[i] ?? 0 });
134
+ }
135
+ out.push({ kind: "close" });
136
+ }
137
+ function disk(out, x, y, r, sides) {
138
+ const xs = [], ys = [];
139
+ for (let i = 0; i < sides; i++) {
140
+ const a = (i / sides) * 2 * PI;
141
+ xs.push(x + r * cos(a));
142
+ ys.push(y + r * sin(a));
143
+ }
144
+ polygon(out, xs, ys);
145
+ }
146
+ function corner(out, p, ax, ay, bx, by, hw, join, sides) {
147
+ // (ax, ay) and (bx, by): unit directions arriving at and leaving p.
148
+ const turn = ax * by - ay * bx, along = ax * bx + ay * by;
149
+ if (turn === 0 && along >= 0)
150
+ return;
151
+ if (join === "round" && !p.smooth) {
152
+ disk(out, p.x, p.y, hw, sides);
153
+ return;
154
+ }
155
+ // The outer side of the turn: rotate the direction a quarter away from where it is turning.
156
+ const side = turn > 0 ? -1 : 1;
157
+ const nax = -ay * side, nay = ax * side, nbx = -by * side, nby = bx * side;
158
+ const a1 = p.x + nax * hw, a2 = p.y + nay * hw, b1 = p.x + nbx * hw, b2 = p.y + nby * hw;
159
+ if (join === "miter" && !p.smooth) {
160
+ // Miter ratio is 1 / sin(half the angle between the pieces).
161
+ const half = Math.sqrt(Math.max(0, (1 + along) / 2));
162
+ if (half > 0 && 1 / half <= MITER_LIMIT) {
163
+ const mx = nax + nbx, my = nay + nby;
164
+ const ml = Math.sqrt(mx * mx + my * my);
165
+ if (ml > 0) {
166
+ const reach = hw / half;
167
+ polygon(out, [p.x, a1, p.x + (mx / ml) * reach, b1], [p.y, a2, p.y + (my / ml) * reach, b2]);
168
+ return;
169
+ }
170
+ }
171
+ }
172
+ polygon(out, [p.x, a1, b1], [p.y, a2, b2]);
173
+ }
174
+ /** The outline of a stroked path as fillable verbs (nonzero rule). `scale` is how many surface
175
+ * pixels one path unit covers: it sets how finely curves and round ends are drawn. */
176
+ export function strokeOutline(verbs, stroke, scale) {
177
+ if (!(stroke.width > 0) || !(scale > 0))
178
+ return [];
179
+ const hw = stroke.width / 2;
180
+ const tolerance = 0.2 / scale;
181
+ const sides = Math.min(64, Math.max(8, Math.ceil(PI * Math.sqrt(hw * scale) * 2)));
182
+ const lines = flatten(verbs, tolerance);
183
+ const total = lines.reduce((sum, l) => sum + lengthOf(l), 0);
184
+ const from = Math.min(Math.max(stroke.trim[0], 0), 1) * total, to = Math.min(Math.max(stroke.trim[1], 0), 1) * total;
185
+ if (to <= from && total > 0)
186
+ return [];
187
+ const out = [];
188
+ for (const line of from <= 0 && to >= total ? lines : trimmed(lines, from, to)) {
189
+ const n = line.points.length;
190
+ const first = line.points[0];
191
+ if (!first)
192
+ continue;
193
+ if (n === 1) {
194
+ if (stroke.cap === "round")
195
+ disk(out, first.x, first.y, hw, sides);
196
+ continue;
197
+ }
198
+ const count = line.closed ? n : n - 1;
199
+ let px = 0, py = 0;
200
+ let startX = 0, startY = 0;
201
+ for (let i = 0; i < count; i++) {
202
+ const a = line.points[i], b = line.points[(i + 1) % n];
203
+ if (!a || !b)
204
+ continue;
205
+ const l = dist(a.x, a.y, b.x, b.y);
206
+ if (l === 0)
207
+ continue;
208
+ const dx = (b.x - a.x) / l, dy = (b.y - a.y) / l;
209
+ const nx = -dy * hw, ny = dx * hw;
210
+ if (i === 0) {
211
+ startX = dx;
212
+ startY = dy;
213
+ }
214
+ else
215
+ corner(out, a, px, py, dx, dy, hw, stroke.join, sides);
216
+ const openStart = !line.closed && i === 0 && stroke.cap === "square" ? hw : 0;
217
+ const openEnd = !line.closed && i === count - 1 && stroke.cap === "square" ? hw : 0;
218
+ const ax = a.x - dx * openStart, ay = a.y - dy * openStart, bx = b.x + dx * openEnd, by = b.y + dy * openEnd;
219
+ polygon(out, [ax + nx, bx + nx, bx - nx, ax - nx], [ay + ny, by + ny, by - ny, ay - ny]);
220
+ px = dx;
221
+ py = dy;
222
+ }
223
+ if (line.closed)
224
+ corner(out, first, px, py, startX, startY, hw, stroke.join, sides);
225
+ else if (stroke.cap === "round") {
226
+ const last = line.points[n - 1];
227
+ disk(out, first.x, first.y, hw, sides);
228
+ if (last)
229
+ disk(out, last.x, last.y, hw, sides);
230
+ }
231
+ }
232
+ return out;
233
+ }
@@ -26,6 +26,9 @@ export declare class Raster implements RasterBackend {
26
26
  /** Uploads an image as a QOI file. */
27
27
  loadImage(id: number, qoi: Uint8Array): void;
28
28
  hasImage(id: number): boolean;
29
+ /** The raster's WebAssembly memory. It grows to its high-water mark and stays there; what is freed
30
+ * inside it is used again. */
31
+ get memoryBytes(): number;
29
32
  /** Executes a display list into the frame. */
30
33
  render(list: Uint8Array): void;
31
34
  private copy;
@@ -118,6 +118,11 @@ export class Raster {
118
118
  hasImage(id) {
119
119
  return this.x.has_image(id) === 1;
120
120
  }
121
+ /** The raster's WebAssembly memory. It grows to its high-water mark and stays there; what is freed
122
+ * inside it is used again. */
123
+ get memoryBytes() {
124
+ return this.x.memory.buffer.byteLength;
125
+ }
121
126
  /** Executes a display list into the frame. */
122
127
  render(list) {
123
128
  const code = this.withBytes(list, (ptr, len) => this.x.render(ptr, len));