gpucanvas 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -10
- package/dist/{chunk-VOFDBN5J.js → chunk-RBXGH44N.js} +1500 -91
- package/dist/chunk-RBXGH44N.js.map +1 -0
- package/dist/editor-BSRqjhN8.d.ts +267 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +11 -1
- package/dist/react.d.ts +37 -3
- package/dist/react.js +92 -2
- package/dist/react.js.map +1 -1
- package/package.json +6 -3
- package/dist/chunk-VOFDBN5J.js.map +0 -1
- package/dist/types-D8v10v6m.d.ts +0 -101
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
// src/camera.ts
|
|
2
|
+
var screenToWorld = (camera, point) => ({
|
|
3
|
+
x: camera.x + point.x / camera.zoom,
|
|
4
|
+
y: camera.y + point.y / camera.zoom
|
|
5
|
+
});
|
|
6
|
+
var worldToScreen = (camera, point) => ({
|
|
7
|
+
x: (point.x - camera.x) * camera.zoom,
|
|
8
|
+
y: (point.y - camera.y) * camera.zoom
|
|
9
|
+
});
|
|
2
10
|
var MIN_ZOOM = 0.02;
|
|
3
11
|
var MAX_ZOOM = 16;
|
|
4
12
|
var clampZoom = (zoom) => Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom));
|
|
@@ -16,6 +24,7 @@ function visibleWorldRect(camera, viewportWidth, viewportHeight) {
|
|
|
16
24
|
height: viewportHeight / camera.zoom
|
|
17
25
|
};
|
|
18
26
|
}
|
|
27
|
+
var containsPoint = (rect, point) => point.x >= rect.x && point.x < rect.x + rect.width && point.y >= rect.y && point.y < rect.y + rect.height;
|
|
19
28
|
var intersects = (a, b) => a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
|
|
20
29
|
function fitCamera(bounds, viewportWidth, viewportHeight, padding = 40) {
|
|
21
30
|
const zoom = clampZoom(
|
|
@@ -82,6 +91,392 @@ function bindCameraInput(element, getCamera, onChange) {
|
|
|
82
91
|
};
|
|
83
92
|
}
|
|
84
93
|
|
|
94
|
+
// src/effects.ts
|
|
95
|
+
var defineEffect = (definition) => Object.freeze({
|
|
96
|
+
animated: false,
|
|
97
|
+
params: [],
|
|
98
|
+
pad: 0,
|
|
99
|
+
...definition
|
|
100
|
+
});
|
|
101
|
+
var effectParams = (effect, params) => [0, 1, 2, 3].map((i) => params?.[i] ?? effect.params[i] ?? 0);
|
|
102
|
+
var effectPad = (effect, params) => typeof effect.pad === "function" ? effect.pad(params) : effect.pad;
|
|
103
|
+
var HUE_ROTATE = (
|
|
104
|
+
/* wgsl */
|
|
105
|
+
`
|
|
106
|
+
fn hueRotate(color: vec3f, degrees: f32) -> vec3f {
|
|
107
|
+
let c = cos(radians(degrees));
|
|
108
|
+
let s = sin(radians(degrees));
|
|
109
|
+
let m = mat3x3f(
|
|
110
|
+
vec3f(0.213 + 0.787 * c - 0.213 * s, 0.213 - 0.213 * c + 0.143 * s, 0.213 - 0.213 * c - 0.787 * s),
|
|
111
|
+
vec3f(0.715 - 0.715 * c - 0.715 * s, 0.715 + 0.285 * c + 0.140 * s, 0.715 - 0.715 * c + 0.715 * s),
|
|
112
|
+
vec3f(0.072 - 0.072 * c + 0.928 * s, 0.072 - 0.072 * c - 0.283 * s, 0.072 + 0.928 * c + 0.072 * s),
|
|
113
|
+
);
|
|
114
|
+
return m * color;
|
|
115
|
+
}
|
|
116
|
+
`
|
|
117
|
+
);
|
|
118
|
+
var hue = defineEffect({
|
|
119
|
+
name: "hue",
|
|
120
|
+
animated: true,
|
|
121
|
+
params: [90],
|
|
122
|
+
wgsl: (
|
|
123
|
+
/* wgsl */
|
|
124
|
+
`${HUE_ROTATE}
|
|
125
|
+
fn effect(input: EffectInput) -> vec4f {
|
|
126
|
+
let color = source(input.uv);
|
|
127
|
+
return vec4f(clamp(hueRotate(color.rgb, input.time * input.params.x), vec3f(0.0), vec3f(color.a)), color.a);
|
|
128
|
+
}`
|
|
129
|
+
),
|
|
130
|
+
css: ([speed], time) => `hue-rotate(${(time * speed % 360).toFixed(1)}deg)`
|
|
131
|
+
});
|
|
132
|
+
var grayscale = defineEffect({
|
|
133
|
+
name: "grayscale",
|
|
134
|
+
params: [1],
|
|
135
|
+
wgsl: (
|
|
136
|
+
/* wgsl */
|
|
137
|
+
`
|
|
138
|
+
fn effect(input: EffectInput) -> vec4f {
|
|
139
|
+
let color = source(input.uv);
|
|
140
|
+
let gray = dot(color.rgb, vec3f(0.2126, 0.7152, 0.0722));
|
|
141
|
+
return vec4f(mix(color.rgb, vec3f(gray), input.params.x), color.a);
|
|
142
|
+
}`
|
|
143
|
+
),
|
|
144
|
+
css: ([amount]) => `grayscale(${amount})`
|
|
145
|
+
});
|
|
146
|
+
var blur = defineEffect({
|
|
147
|
+
name: "blur",
|
|
148
|
+
params: [6],
|
|
149
|
+
pad: ([radius]) => radius,
|
|
150
|
+
wgsl: (
|
|
151
|
+
/* wgsl */
|
|
152
|
+
`
|
|
153
|
+
fn effect(input: EffectInput) -> vec4f {
|
|
154
|
+
let radius = input.params.x / input.size;
|
|
155
|
+
var color = vec4f(0.0);
|
|
156
|
+
var weight = 0.0;
|
|
157
|
+
// A Vogel spiral: even coverage of the disk with few taps.
|
|
158
|
+
for (var i = 0u; i < 32u; i++) {
|
|
159
|
+
let r = sqrt((f32(i) + 0.5) / 32.0);
|
|
160
|
+
let a = f32(i) * 2.39996;
|
|
161
|
+
let w = exp(-2.0 * r * r);
|
|
162
|
+
color += source(input.uv + vec2f(cos(a), sin(a)) * r * radius) * w;
|
|
163
|
+
weight += w;
|
|
164
|
+
}
|
|
165
|
+
return color / weight;
|
|
166
|
+
}`
|
|
167
|
+
),
|
|
168
|
+
css: ([radius]) => `blur(${radius / 2}px)`
|
|
169
|
+
});
|
|
170
|
+
var glow = defineEffect({
|
|
171
|
+
name: "glow",
|
|
172
|
+
animated: true,
|
|
173
|
+
params: [0.31, 0.55, 1, 24],
|
|
174
|
+
pad: (params) => params[3],
|
|
175
|
+
period: Math.PI,
|
|
176
|
+
wgsl: (
|
|
177
|
+
/* wgsl */
|
|
178
|
+
`
|
|
179
|
+
fn effect(input: EffectInput) -> vec4f {
|
|
180
|
+
let color = source(input.uv);
|
|
181
|
+
let reach = input.params.w * (0.65 + 0.35 * sin(input.time * 2.0));
|
|
182
|
+
// How much of the shape lies within reach, sampled on two rings.
|
|
183
|
+
var cover = 0.0;
|
|
184
|
+
for (var i = 0u; i < 24u; i++) {
|
|
185
|
+
let a = f32(i) * 0.2618;
|
|
186
|
+
let r = select(0.5, 1.0, i % 2u == 0u);
|
|
187
|
+
cover += source(input.uv + vec2f(cos(a), sin(a)) * r * reach / input.size).a;
|
|
188
|
+
}
|
|
189
|
+
let alpha = smoothstep(0.0, 1.0, cover / 24.0) * 0.9;
|
|
190
|
+
return color + vec4f(input.params.rgb * alpha, alpha) * (1.0 - color.a);
|
|
191
|
+
}`
|
|
192
|
+
),
|
|
193
|
+
css: ([r, g, b, reach], time) => {
|
|
194
|
+
const radius = reach * (0.65 + 0.35 * Math.sin(time * 2));
|
|
195
|
+
const rgb = [r, g, b].map((v) => Math.round(v * 255)).join(" ");
|
|
196
|
+
return `drop-shadow(0 0 ${(radius / 2).toFixed(1)}px rgb(${rgb} / 0.9))`;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
var ripple = defineEffect({
|
|
200
|
+
name: "ripple",
|
|
201
|
+
animated: true,
|
|
202
|
+
params: [6, 90],
|
|
203
|
+
pad: ([amplitude]) => amplitude,
|
|
204
|
+
wgsl: (
|
|
205
|
+
/* wgsl */
|
|
206
|
+
`
|
|
207
|
+
fn effect(input: EffectInput) -> vec4f {
|
|
208
|
+
let p = (input.uv - 0.5) * input.size;
|
|
209
|
+
let d = length(p);
|
|
210
|
+
let wave = sin(d / input.params.y * 6.2832 - input.time * 4.0);
|
|
211
|
+
let push = select(vec2f(0.0), p / d, d > 0.001) * wave * input.params.x;
|
|
212
|
+
let color = source(input.uv + push / input.size);
|
|
213
|
+
return vec4f(color.rgb * (1.0 + 0.06 * wave), color.a);
|
|
214
|
+
}`
|
|
215
|
+
)
|
|
216
|
+
});
|
|
217
|
+
var pixelate = defineEffect({
|
|
218
|
+
name: "pixelate",
|
|
219
|
+
params: [8],
|
|
220
|
+
wgsl: (
|
|
221
|
+
/* wgsl */
|
|
222
|
+
`
|
|
223
|
+
fn effect(input: EffectInput) -> vec4f {
|
|
224
|
+
let block = max(input.params.x, 1.0);
|
|
225
|
+
let cell = (floor(input.uv * input.size / block) + 0.5) * block;
|
|
226
|
+
return source(cell / input.size);
|
|
227
|
+
}`
|
|
228
|
+
)
|
|
229
|
+
});
|
|
230
|
+
var dissolve = defineEffect({
|
|
231
|
+
name: "dissolve",
|
|
232
|
+
params: [0.5],
|
|
233
|
+
wgsl: (
|
|
234
|
+
/* wgsl */
|
|
235
|
+
`
|
|
236
|
+
fn effect(input: EffectInput) -> vec4f {
|
|
237
|
+
let color = source(input.uv);
|
|
238
|
+
let p = input.uv * input.size / 24.0;
|
|
239
|
+
let n = noise(p) * 0.65 + noise(p * 2.7) * 0.35;
|
|
240
|
+
let edge = n - (input.params.x * 1.2 - 0.1);
|
|
241
|
+
let keep = smoothstep(0.0, 0.02, edge);
|
|
242
|
+
let burn = (1.0 - smoothstep(0.0, 0.08, edge)) * keep;
|
|
243
|
+
return color * keep + vec4f(1.0, 0.45, 0.1, 1.0) * burn * color.a;
|
|
244
|
+
}`
|
|
245
|
+
),
|
|
246
|
+
css: ([progress]) => `opacity(${1 - progress})`
|
|
247
|
+
});
|
|
248
|
+
var glass = defineEffect({
|
|
249
|
+
name: "glass",
|
|
250
|
+
animated: true,
|
|
251
|
+
params: [110, 1.8],
|
|
252
|
+
wgsl: (
|
|
253
|
+
/* wgsl */
|
|
254
|
+
`
|
|
255
|
+
fn effect(input: EffectInput) -> vec4f {
|
|
256
|
+
let d = (input.uv - input.pointer) * input.size;
|
|
257
|
+
let radius = input.params.x;
|
|
258
|
+
let t = length(d) / radius;
|
|
259
|
+
if (t >= 1.0) {
|
|
260
|
+
let shadow = (1.0 - smoothstep(1.0, 1.12, t)) * 0.18;
|
|
261
|
+
let under = source(input.uv);
|
|
262
|
+
return vec4f(under.rgb * (1.0 - shadow), under.a);
|
|
263
|
+
}
|
|
264
|
+
// A dome: strongest at the center, flattening to no change at the rim.
|
|
265
|
+
let scale = mix(1.0 / input.params.y, 1.0, t * t * t);
|
|
266
|
+
let bent = input.pointer + d * scale / input.size;
|
|
267
|
+
let fringe = d / radius * (t * t * 6.0) / input.size;
|
|
268
|
+
let middle = source(bent);
|
|
269
|
+
var color = vec4f(source(bent + fringe).r, middle.g, source(bent - fringe).b, middle.a);
|
|
270
|
+
// A bright rim and a highlight up and to the left.
|
|
271
|
+
let rim = smoothstep(0.86, 0.97, t) * (1.0 - smoothstep(0.985, 1.0, t));
|
|
272
|
+
let shine = pow(max(0.0, 1.0 - length(d / radius - vec2f(-0.38, -0.42)) / 0.42), 2.0);
|
|
273
|
+
// Light catches the upper left of the rim, and the lower right falls into shade.
|
|
274
|
+
let side = dot(normalize(d + vec2f(0.0001)), vec2f(-0.7071, -0.7071));
|
|
275
|
+
let light = rim * (0.25 + 0.35 * side) + shine * 0.3;
|
|
276
|
+
let shade = rim * max(0.0, -side) * 0.25;
|
|
277
|
+
color = vec4f(color.rgb * (1.0 - shade), color.a);
|
|
278
|
+
color = color + vec4f(light, light, light, light) * (1.0 - color.a * 0.4);
|
|
279
|
+
return min(color, vec4f(1.0));
|
|
280
|
+
}`
|
|
281
|
+
)
|
|
282
|
+
});
|
|
283
|
+
var shockwave = defineEffect({
|
|
284
|
+
name: "shockwave",
|
|
285
|
+
animated: true,
|
|
286
|
+
params: [900, 60, 18],
|
|
287
|
+
wgsl: (
|
|
288
|
+
/* wgsl */
|
|
289
|
+
`
|
|
290
|
+
fn effect(input: EffectInput) -> vec4f {
|
|
291
|
+
let fade = exp(-input.sincePress * 1.6);
|
|
292
|
+
if (fade < 0.01) {
|
|
293
|
+
return source(input.uv);
|
|
294
|
+
}
|
|
295
|
+
let d = (input.uv - input.press) * input.size;
|
|
296
|
+
let dist = length(d);
|
|
297
|
+
let x = (dist - input.sincePress * input.params.x) / input.params.y;
|
|
298
|
+
let ring = exp(-x * x);
|
|
299
|
+
let push = select(vec2f(0.0), d / dist, dist > 0.001) * sin(x * 2.5) * ring * input.params.z * fade;
|
|
300
|
+
let offset = push / input.size;
|
|
301
|
+
let middle = source(input.uv - offset);
|
|
302
|
+
let color = vec4f(source(input.uv - offset * 1.4).r, middle.g, source(input.uv - offset * 0.6).b, middle.a);
|
|
303
|
+
return color + vec4f(0.45, 0.65, 1.0, 1.0) * ring * fade * 0.25 * middle.a;
|
|
304
|
+
}`
|
|
305
|
+
)
|
|
306
|
+
});
|
|
307
|
+
var glitch = defineEffect({
|
|
308
|
+
name: "glitch",
|
|
309
|
+
animated: true,
|
|
310
|
+
wgsl: (
|
|
311
|
+
/* wgsl */
|
|
312
|
+
`
|
|
313
|
+
fn effect(input: EffectInput) -> vec4f {
|
|
314
|
+
// Every node glitches on its own schedule.
|
|
315
|
+
let own = hash(input.size);
|
|
316
|
+
let tick = floor(input.time * 12.0 + own * 50.0);
|
|
317
|
+
let burst = step(0.82, hash(vec2f(floor(tick / 3.0), own)));
|
|
318
|
+
if (burst == 0.0) {
|
|
319
|
+
return source(input.uv);
|
|
320
|
+
}
|
|
321
|
+
let row = floor(input.uv.y * mix(12.0, 40.0, hash(vec2f(tick, 1.0))));
|
|
322
|
+
let tear = step(0.6, hash(vec2f(row, tick))) * (hash(vec2f(row, tick + 7.0)) - 0.5) * 0.06;
|
|
323
|
+
let uv = vec2f(input.uv.x + tear, input.uv.y);
|
|
324
|
+
let split = vec2f(0.012 * hash(vec2f(tick, 3.0)), 0.0);
|
|
325
|
+
let middle = source(uv);
|
|
326
|
+
var color = vec4f(source(uv + split).r, middle.g, source(uv - split).b, middle.a);
|
|
327
|
+
// A few blocks show pixels from elsewhere, with their channels swapped.
|
|
328
|
+
let block = floor(input.uv * vec2f(14.0, 8.0));
|
|
329
|
+
if (hash(block + vec2f(tick)) > 0.975) {
|
|
330
|
+
let moved = source(fract(input.uv + vec2f(hash(block), hash(block + 9.0)) * 0.3));
|
|
331
|
+
color = vec4f(moved.brg, moved.a);
|
|
332
|
+
}
|
|
333
|
+
return color;
|
|
334
|
+
}`
|
|
335
|
+
)
|
|
336
|
+
});
|
|
337
|
+
var vhs = defineEffect({
|
|
338
|
+
name: "vhs",
|
|
339
|
+
animated: true,
|
|
340
|
+
wgsl: (
|
|
341
|
+
/* wgsl */
|
|
342
|
+
`
|
|
343
|
+
fn effect(input: EffectInput) -> vec4f {
|
|
344
|
+
let y = input.uv.y;
|
|
345
|
+
let t = input.time;
|
|
346
|
+
var wave = sin(y * 38.0 + t * 3.0) * 0.0012 + sin(y * 6.0 - t * 1.3) * 0.002;
|
|
347
|
+
// Head-switching noise along the bottom.
|
|
348
|
+
let bottom = smoothstep(0.93, 1.0, y);
|
|
349
|
+
wave += bottom * (hash(vec2f(floor(y * 180.0), floor(t * 30.0))) - 0.5) * 0.04;
|
|
350
|
+
let uv = vec2f(input.uv.x + wave, y);
|
|
351
|
+
let bleed = vec2f(3.0 / input.size.x, 0.0);
|
|
352
|
+
let middle = source(uv);
|
|
353
|
+
var color = vec4f(source(uv + bleed).r, middle.g, source(uv - bleed * 1.5).b, middle.a);
|
|
354
|
+
// Softer, warmer and a little washed out.
|
|
355
|
+
let gray = dot(color.rgb, vec3f(0.299, 0.587, 0.114));
|
|
356
|
+
color = vec4f(mix(vec3f(gray), color.rgb, 0.8) * vec3f(1.04, 1.0, 0.92), color.a);
|
|
357
|
+
let band = smoothstep(0.08, 0.0, abs(fract(y * 0.6 - t * 0.12) - 0.5));
|
|
358
|
+
let lines = 0.94 + 0.06 * sin(y * input.size.y * 3.1416);
|
|
359
|
+
let grain = (hash(input.uv * input.size + vec2f(t * 97.0, t * 61.0)) - 0.5) * 0.12;
|
|
360
|
+
return vec4f(clamp(color.rgb * lines + (band * 0.08 + grain) * color.a, vec3f(0.0), vec3f(color.a)), color.a);
|
|
361
|
+
}`
|
|
362
|
+
)
|
|
363
|
+
});
|
|
364
|
+
var frost = defineEffect({
|
|
365
|
+
name: "frost",
|
|
366
|
+
animated: true,
|
|
367
|
+
params: [140],
|
|
368
|
+
wgsl: (
|
|
369
|
+
/* wgsl */
|
|
370
|
+
`
|
|
371
|
+
fn effect(input: EffectInput) -> vec4f {
|
|
372
|
+
let p = input.uv * input.size;
|
|
373
|
+
let dist = length((input.uv - input.pointer) * input.size);
|
|
374
|
+
let edge = noise(p / 40.0) * 50.0;
|
|
375
|
+
let ice = smoothstep(input.params.x * 0.55, input.params.x * 1.2, dist + edge);
|
|
376
|
+
if (ice <= 0.0) {
|
|
377
|
+
return source(input.uv);
|
|
378
|
+
}
|
|
379
|
+
// Frosted glass: the node seen blurred and bent through uneven ice.
|
|
380
|
+
let thick = noise(p / 90.0) * 0.6 + noise(p / 23.0) * 0.4;
|
|
381
|
+
let bend = (vec2f(noise(p / 14.0), noise(p / 14.0 + 31.0)) - 0.5) * 6.0 * ice;
|
|
382
|
+
var color = vec4f(0.0);
|
|
383
|
+
for (var i = 0u; i < 8u; i++) {
|
|
384
|
+
let a = f32(i) * 0.7854 + 0.4;
|
|
385
|
+
let r = select(3.0, 6.0, i % 2u == 0u) * ice;
|
|
386
|
+
color += source((p + bend + vec2f(cos(a), sin(a)) * r) / input.size);
|
|
387
|
+
}
|
|
388
|
+
color /= 8.0;
|
|
389
|
+
let white = vec4f(0.9, 0.95, 1.0, 1.0) * color.a;
|
|
390
|
+
// Feathery crystals and a few glints.
|
|
391
|
+
let crystals = smoothstep(0.7, 0.95, noise(p / 12.0 + noise(p / 40.0) * 2.0)) * 0.1;
|
|
392
|
+
let glint = step(0.997, hash(floor(p / 1.5))) * 0.8;
|
|
393
|
+
let frosted = mix(color, white, (0.3 + thick * 0.25) * ice);
|
|
394
|
+
return min(frosted + vec4f((crystals + glint) * ice) * color.a, vec4f(color.a));
|
|
395
|
+
}`
|
|
396
|
+
)
|
|
397
|
+
});
|
|
398
|
+
var dither = defineEffect({
|
|
399
|
+
name: "dither",
|
|
400
|
+
animated: true,
|
|
401
|
+
params: [130, 4],
|
|
402
|
+
wgsl: (
|
|
403
|
+
/* wgsl */
|
|
404
|
+
`
|
|
405
|
+
fn effect(input: EffectInput) -> vec4f {
|
|
406
|
+
let p = input.uv * input.size;
|
|
407
|
+
let dist = length((input.uv - input.pointer) * input.size);
|
|
408
|
+
let radius = input.params.x;
|
|
409
|
+
if (dist > radius + 1.5) {
|
|
410
|
+
return source(input.uv);
|
|
411
|
+
}
|
|
412
|
+
let block = max(input.params.y, 1.0);
|
|
413
|
+
let cell = floor(p / block);
|
|
414
|
+
let base = source((cell + 0.5) * block / input.size);
|
|
415
|
+
var bayer = array<f32, 16>(0.0, 8.0, 2.0, 10.0, 12.0, 4.0, 14.0, 6.0, 3.0, 11.0, 1.0, 9.0, 15.0, 7.0, 13.0, 5.0);
|
|
416
|
+
let index = u32(cell.x) % 4u + (u32(cell.y) % 4u) * 4u;
|
|
417
|
+
let threshold = (bayer[index] + 0.5) / 16.0 - 0.5;
|
|
418
|
+
// Two levels per channel: eight colors, mixed by the pattern.
|
|
419
|
+
let straight = base.rgb / max(base.a, 0.0001);
|
|
420
|
+
let levels = floor(clamp(straight + threshold * 0.6, vec3f(0.0), vec3f(1.0)) + 0.5);
|
|
421
|
+
let dithered = vec4f(levels * base.a, base.a);
|
|
422
|
+
let ring = 1.0 - smoothstep(0.0, 1.5, abs(dist - radius));
|
|
423
|
+
return mix(dithered, vec4f(0.1, 0.1, 0.12, 1.0), ring * 0.8);
|
|
424
|
+
}`
|
|
425
|
+
)
|
|
426
|
+
});
|
|
427
|
+
var EFFECTS = {
|
|
428
|
+
glass,
|
|
429
|
+
shockwave,
|
|
430
|
+
glitch,
|
|
431
|
+
vhs,
|
|
432
|
+
frost,
|
|
433
|
+
dither,
|
|
434
|
+
glow,
|
|
435
|
+
hue,
|
|
436
|
+
ripple,
|
|
437
|
+
blur,
|
|
438
|
+
pixelate,
|
|
439
|
+
dissolve,
|
|
440
|
+
grayscale
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// src/elements.ts
|
|
444
|
+
function rectInNode(node, nodeRect, element) {
|
|
445
|
+
if (element === node) return { ...nodeRect };
|
|
446
|
+
const outer = node.getBoundingClientRect();
|
|
447
|
+
const inner = element.getBoundingClientRect();
|
|
448
|
+
const scale = outer.width > 0 ? nodeRect.width / outer.width : 1;
|
|
449
|
+
return {
|
|
450
|
+
x: nodeRect.x + (inner.left - outer.left) * scale,
|
|
451
|
+
y: nodeRect.y + (inner.top - outer.top) * scale,
|
|
452
|
+
width: inner.width * scale,
|
|
453
|
+
height: inner.height * scale
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
function elementInNode(node, nodeRect, point) {
|
|
457
|
+
const outer = node.getBoundingClientRect();
|
|
458
|
+
const scale = nodeRect.width > 0 ? outer.width / nodeRect.width : 1;
|
|
459
|
+
const x = outer.left + (point.x - nodeRect.x) * scale;
|
|
460
|
+
const y = outer.top + (point.y - nodeRect.y) * scale;
|
|
461
|
+
let hit = node;
|
|
462
|
+
descend: for (; ; ) {
|
|
463
|
+
for (let child = hit.lastElementChild; child; child = child.previousElementSibling) {
|
|
464
|
+
const box = child.getBoundingClientRect();
|
|
465
|
+
if (box.width > 0 && box.height > 0 && x >= box.left && x < box.right && y >= box.top && y < box.bottom) {
|
|
466
|
+
hit = child;
|
|
467
|
+
continue descend;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return hit;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function nodeOfElement(nodes, element) {
|
|
474
|
+
for (let at = element; at; at = at.parentElement) {
|
|
475
|
+
if (nodes.has(at)) return at;
|
|
476
|
+
}
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
|
|
85
480
|
// src/node-style.ts
|
|
86
481
|
function applyNodeSize(element, rect) {
|
|
87
482
|
element.style.width = `${rect.width}px`;
|
|
@@ -107,6 +502,7 @@ function boundsOf(rects) {
|
|
|
107
502
|
var DEFAULT_CAMERA = { x: 0, y: 0, zoom: 1 };
|
|
108
503
|
var DEFAULT_SELECTION_COLOR = "#4f8cff";
|
|
109
504
|
var SELECTION_WIDTH = 2;
|
|
505
|
+
var HOVER_WIDTH = 1;
|
|
110
506
|
|
|
111
507
|
// src/dom-backend.ts
|
|
112
508
|
function createDomCanvas(host, options) {
|
|
@@ -129,11 +525,96 @@ function createDomCanvas(host, options) {
|
|
|
129
525
|
transformOrigin: "0 0",
|
|
130
526
|
willChange: "transform"
|
|
131
527
|
});
|
|
528
|
+
const overlay = document.createElement("div");
|
|
529
|
+
Object.assign(overlay.style, { position: "absolute", left: "0", top: "0", zIndex: "1" });
|
|
530
|
+
world.append(overlay);
|
|
132
531
|
surface.append(world);
|
|
133
532
|
host.append(surface);
|
|
134
533
|
const nodes = /* @__PURE__ */ new Map();
|
|
135
|
-
|
|
136
|
-
const
|
|
534
|
+
const ring = (color, width) => `calc(${width}px / var(--gpucanvas-zoom)) solid ${color}`;
|
|
535
|
+
const selectionColor = options.selectionColor ?? DEFAULT_SELECTION_COLOR;
|
|
536
|
+
const selectionRing = ring(selectionColor, SELECTION_WIDTH);
|
|
537
|
+
const hoverRing = ring(options.hoverColor ?? selectionColor, HOVER_WIDTH);
|
|
538
|
+
let selection = /* @__PURE__ */ new Set();
|
|
539
|
+
let hovered = null;
|
|
540
|
+
let outlinedNodes = /* @__PURE__ */ new Set();
|
|
541
|
+
const boxes = /* @__PURE__ */ new Map();
|
|
542
|
+
const elementRect = (element) => {
|
|
543
|
+
const node = element.isConnected ? nodeOfElement(nodes, element) : null;
|
|
544
|
+
return node ? rectInNode(node, nodes.get(node), element) : null;
|
|
545
|
+
};
|
|
546
|
+
let boxFrame = 0;
|
|
547
|
+
const placeBoxes = () => {
|
|
548
|
+
boxFrame = boxes.size ? requestAnimationFrame(placeBoxes) : 0;
|
|
549
|
+
for (const [element, box] of boxes) {
|
|
550
|
+
const rect = elementRect(element);
|
|
551
|
+
box.style.display = rect ? "block" : "none";
|
|
552
|
+
if (rect) {
|
|
553
|
+
box.style.transform = `translate(${rect.x}px, ${rect.y}px)`;
|
|
554
|
+
box.style.width = `${rect.width}px`;
|
|
555
|
+
box.style.height = `${rect.height}px`;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
const drawOutlines = () => {
|
|
560
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
561
|
+
for (const element of selection) wanted.set(element, selectionRing);
|
|
562
|
+
if (hovered && !wanted.has(hovered)) wanted.set(hovered, hoverRing);
|
|
563
|
+
const nextNodes = /* @__PURE__ */ new Set();
|
|
564
|
+
for (const [element, outline] of wanted) {
|
|
565
|
+
if (!nodes.has(element)) continue;
|
|
566
|
+
element.style.outline = outline;
|
|
567
|
+
nextNodes.add(element);
|
|
568
|
+
}
|
|
569
|
+
for (const node of outlinedNodes) if (!nextNodes.has(node)) node.style.outline = "";
|
|
570
|
+
outlinedNodes = nextNodes;
|
|
571
|
+
for (const [element, box] of boxes) {
|
|
572
|
+
if (wanted.has(element)) continue;
|
|
573
|
+
box.remove();
|
|
574
|
+
boxes.delete(element);
|
|
575
|
+
}
|
|
576
|
+
for (const [element, outline] of wanted) {
|
|
577
|
+
if (nodes.has(element)) continue;
|
|
578
|
+
let box = boxes.get(element);
|
|
579
|
+
if (!box) {
|
|
580
|
+
box = document.createElement("div");
|
|
581
|
+
Object.assign(box.style, {
|
|
582
|
+
position: "absolute",
|
|
583
|
+
left: "0",
|
|
584
|
+
top: "0",
|
|
585
|
+
pointerEvents: "none"
|
|
586
|
+
});
|
|
587
|
+
overlay.append(box);
|
|
588
|
+
boxes.set(element, box);
|
|
589
|
+
}
|
|
590
|
+
box.style.outline = outline;
|
|
591
|
+
}
|
|
592
|
+
if (boxes.size && !boxFrame) placeBoxes();
|
|
593
|
+
};
|
|
594
|
+
const animations = /* @__PURE__ */ new Map();
|
|
595
|
+
const KEYFRAMES = 24;
|
|
596
|
+
const clearEffect = (element) => {
|
|
597
|
+
animations.get(element)?.cancel();
|
|
598
|
+
animations.delete(element);
|
|
599
|
+
element.style.filter = "";
|
|
600
|
+
};
|
|
601
|
+
const applyEffect = (element, effect, params) => {
|
|
602
|
+
clearEffect(element);
|
|
603
|
+
if (!effect?.css) return;
|
|
604
|
+
const values = effectParams(effect, params);
|
|
605
|
+
if (!effect.animated) {
|
|
606
|
+
element.style.filter = effect.css(values, 0);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const period = effect.period ?? 4;
|
|
610
|
+
const keyframes = Array.from({ length: KEYFRAMES + 1 }, (_, i) => ({
|
|
611
|
+
filter: effect.css(values, i / KEYFRAMES * period)
|
|
612
|
+
}));
|
|
613
|
+
animations.set(
|
|
614
|
+
element,
|
|
615
|
+
element.animate(keyframes, { duration: period * 1e3, iterations: Infinity })
|
|
616
|
+
);
|
|
617
|
+
};
|
|
137
618
|
let camera = options.camera ?? DEFAULT_CAMERA;
|
|
138
619
|
let frameMs = 0;
|
|
139
620
|
const apply = () => {
|
|
@@ -166,22 +647,39 @@ function createDomCanvas(host, options) {
|
|
|
166
647
|
applyNodeSize(element, rect);
|
|
167
648
|
},
|
|
168
649
|
removeNode(element) {
|
|
650
|
+
clearEffect(element);
|
|
169
651
|
nodes.delete(element);
|
|
170
|
-
|
|
652
|
+
outlinedNodes.delete(element);
|
|
653
|
+
element.style.outline = "";
|
|
171
654
|
element.remove();
|
|
172
655
|
},
|
|
173
656
|
getNodeRect: (element) => nodes.get(element) ?? null,
|
|
174
657
|
getNodesIn(rect) {
|
|
175
658
|
return [...nodes].filter(([, node]) => intersects(node, rect)).map(([element]) => element);
|
|
176
659
|
},
|
|
660
|
+
getNodeOf: (element) => nodeOfElement(nodes, element),
|
|
661
|
+
getElementRect: elementRect,
|
|
662
|
+
getElementAt(point) {
|
|
663
|
+
let top = null;
|
|
664
|
+
for (const [element, rect] of nodes) if (containsPoint(rect, point)) top = element;
|
|
665
|
+
return top ? elementInNode(top, nodes.get(top), point) : null;
|
|
666
|
+
},
|
|
177
667
|
setSelection(elements) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
668
|
+
selection = new Set(elements);
|
|
669
|
+
drawOutlines();
|
|
670
|
+
},
|
|
671
|
+
setHover(element) {
|
|
672
|
+
if (element === hovered) return;
|
|
673
|
+
hovered = element;
|
|
674
|
+
drawOutlines();
|
|
675
|
+
},
|
|
676
|
+
setEffect(element, effect, params) {
|
|
677
|
+
if (nodes.has(element)) applyEffect(element, effect, params);
|
|
182
678
|
},
|
|
183
679
|
getCamera: () => camera,
|
|
184
680
|
setCamera,
|
|
681
|
+
screenToWorld: (point) => screenToWorld(camera, point),
|
|
682
|
+
worldToScreen: (point) => worldToScreen(camera, point),
|
|
185
683
|
// The browser repaints DOM nodes itself.
|
|
186
684
|
invalidate() {
|
|
187
685
|
},
|
|
@@ -208,6 +706,7 @@ function createDomCanvas(host, options) {
|
|
|
208
706
|
};
|
|
209
707
|
},
|
|
210
708
|
destroy() {
|
|
709
|
+
cancelAnimationFrame(boxFrame);
|
|
211
710
|
unbindInput();
|
|
212
711
|
surface.remove();
|
|
213
712
|
}
|
|
@@ -365,7 +864,7 @@ var Atlas = class {
|
|
|
365
864
|
};
|
|
366
865
|
|
|
367
866
|
// src/composite-shader.ts
|
|
368
|
-
var
|
|
867
|
+
var prelude = (
|
|
369
868
|
/* wgsl */
|
|
370
869
|
`
|
|
371
870
|
struct View {
|
|
@@ -374,9 +873,10 @@ struct View {
|
|
|
374
873
|
zoom: f32,
|
|
375
874
|
dpr: f32,
|
|
376
875
|
atlasSize: f32,
|
|
377
|
-
|
|
876
|
+
time: f32, // seconds since the canvas started
|
|
378
877
|
placeholder: vec4f, // premultiplied color for nodes without a snapshot
|
|
379
|
-
|
|
878
|
+
pointer: vec4f, // pointer and last press in world units; far away when there is none
|
|
879
|
+
pressTime: f32, // seconds since the last press
|
|
380
880
|
}
|
|
381
881
|
|
|
382
882
|
@group(0) @binding(0) var<uniform> view: View;
|
|
@@ -385,8 +885,9 @@ struct View {
|
|
|
385
885
|
|
|
386
886
|
struct Instance {
|
|
387
887
|
@location(0) rect: vec4f, // world x, y, width, height
|
|
388
|
-
@location(1) uv: vec4f, // atlas texels x0, y0, x1, y1
|
|
389
|
-
@location(2) info: vec4f, // layer, kind
|
|
888
|
+
@location(1) uv: vec4f, // atlas texels x0, y0, x1, y1; for a ring, its premultiplied color
|
|
889
|
+
@location(2) info: vec4f, // layer, kind (0 snapshot, 1 placeholder, 2 ring), ring width or effect pad
|
|
890
|
+
@location(3) params: vec4f, // an effect's parameters
|
|
390
891
|
}
|
|
391
892
|
|
|
392
893
|
struct Varyings {
|
|
@@ -394,14 +895,19 @@ struct Varyings {
|
|
|
394
895
|
@location(0) texel: vec2f,
|
|
395
896
|
@location(1) @interpolate(flat) bounds: vec4f,
|
|
396
897
|
@location(2) @interpolate(flat) info: vec2f,
|
|
898
|
+
@location(3) @interpolate(flat) color: vec4f,
|
|
899
|
+
@location(4) uv: vec2f, // 0 to 1 across the node, past that in an effect's padding
|
|
900
|
+
@location(5) @interpolate(flat) size: vec2f,
|
|
901
|
+
@location(6) @interpolate(flat) params: vec4f,
|
|
902
|
+
@location(7) @interpolate(flat) corner: vec2f, // the node's top left in world units
|
|
397
903
|
}
|
|
398
904
|
|
|
399
905
|
@vertex
|
|
400
906
|
fn vs(@builtin(vertex_index) vertex: u32, instance: Instance) -> Varyings {
|
|
401
907
|
let corner = vec2f(f32(vertex & 1u), f32(vertex >> 1u));
|
|
402
|
-
// A
|
|
908
|
+
// A ring's quad reaches past its element by the ring's width, an effect's by its padding.
|
|
403
909
|
let ring = instance.info.y > 1.5;
|
|
404
|
-
let grow = select(
|
|
910
|
+
let grow = select(instance.info.z, instance.info.z / view.zoom, ring);
|
|
405
911
|
let start = instance.rect.xy - grow;
|
|
406
912
|
let end = instance.rect.xy + instance.rect.zw + grow;
|
|
407
913
|
// Snap edges to device pixels so a snapshot drawn at its raster scale stays crisp.
|
|
@@ -412,28 +918,39 @@ fn vs(@builtin(vertex_index) vertex: u32, instance: Instance) -> Varyings {
|
|
|
412
918
|
var out: Varyings;
|
|
413
919
|
out.position = vec4f(point.x / view.size.x * 2.0 - 1.0, 1.0 - point.y / view.size.y * 2.0, 0.0, 1.0);
|
|
414
920
|
if (ring) {
|
|
415
|
-
// World units from the
|
|
921
|
+
// World units from the element's top left, and the element's own edges.
|
|
416
922
|
out.texel = mix(vec2f(-grow), instance.rect.zw + grow, corner);
|
|
417
923
|
out.bounds = vec4f(0.0, 0.0, instance.rect.zw);
|
|
924
|
+
out.color = instance.uv;
|
|
418
925
|
} else {
|
|
419
|
-
|
|
926
|
+
let pad = grow * (instance.uv.zw - instance.uv.xy) / instance.rect.zw;
|
|
927
|
+
out.texel = mix(instance.uv.xy - pad, instance.uv.zw + pad, corner);
|
|
928
|
+
out.color = instance.uv;
|
|
420
929
|
// Half a texel inside the node's rectangle: the outermost sample center that cannot blend
|
|
421
930
|
// with whatever sits next to it in the atlas.
|
|
422
931
|
out.bounds = vec4f(instance.uv.xy + 0.5, instance.uv.zw - 0.5);
|
|
423
932
|
}
|
|
424
933
|
out.info = instance.info.xy;
|
|
934
|
+
out.uv = mix(-grow / instance.rect.zw, 1.0 + grow / instance.rect.zw, corner);
|
|
935
|
+
out.size = instance.rect.zw;
|
|
936
|
+
out.corner = instance.rect.xy;
|
|
937
|
+
out.params = instance.params;
|
|
425
938
|
return out;
|
|
426
939
|
}
|
|
427
|
-
|
|
940
|
+
`
|
|
941
|
+
);
|
|
942
|
+
var compositeShader = (
|
|
943
|
+
/* wgsl */
|
|
944
|
+
`${prelude}
|
|
428
945
|
@fragment
|
|
429
946
|
fn fs(in: Varyings) -> @location(0) vec4f {
|
|
430
947
|
// Derivatives first: WGSL only allows them in uniform control flow.
|
|
431
948
|
let dx = dpdx(in.texel);
|
|
432
949
|
let dy = dpdy(in.texel);
|
|
433
950
|
if (in.info.y > 1.5) {
|
|
434
|
-
// The ring covers what lies outside the
|
|
951
|
+
// The ring covers what lies outside the element; the element itself shows through.
|
|
435
952
|
let outside = max(in.bounds.xy - in.texel, in.texel - in.bounds.zw);
|
|
436
|
-
return select(vec4f(0.0),
|
|
953
|
+
return select(vec4f(0.0), in.color, max(outside.x, outside.y) > 0.0);
|
|
437
954
|
}
|
|
438
955
|
if (in.info.y > 0.5) {
|
|
439
956
|
return view.placeholder;
|
|
@@ -454,25 +971,124 @@ fn fs(in: Varyings) -> @location(0) vec4f {
|
|
|
454
971
|
}
|
|
455
972
|
`
|
|
456
973
|
);
|
|
974
|
+
var effectShader = (wgsl) => (
|
|
975
|
+
/* wgsl */
|
|
976
|
+
`${prelude}
|
|
977
|
+
struct EffectInput {
|
|
978
|
+
uv: vec2f,
|
|
979
|
+
size: vec2f,
|
|
980
|
+
time: f32,
|
|
981
|
+
zoom: f32,
|
|
982
|
+
params: vec4f,
|
|
983
|
+
pointer: vec2f,
|
|
984
|
+
press: vec2f,
|
|
985
|
+
sincePress: f32,
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
var<private> sourceRect: vec4f;
|
|
989
|
+
var<private> sourceLayer: i32;
|
|
990
|
+
|
|
991
|
+
fn source(uv: vec2f) -> vec4f {
|
|
992
|
+
if (any(uv < vec2f(0.0)) || any(uv > vec2f(1.0))) {
|
|
993
|
+
return vec4f(0.0);
|
|
994
|
+
}
|
|
995
|
+
let texel = clamp(mix(sourceRect.xy, sourceRect.zw, uv), sourceRect.xy + 0.5, sourceRect.zw - 0.5);
|
|
996
|
+
return textureSampleLevel(atlas, atlasSampler, texel / view.atlasSize, sourceLayer, 0.0);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
fn hash(p: vec2f) -> f32 {
|
|
1000
|
+
return fract(sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
fn noise(p: vec2f) -> f32 {
|
|
1004
|
+
let i = floor(p);
|
|
1005
|
+
let f = fract(p);
|
|
1006
|
+
let u = f * f * (3.0 - 2.0 * f);
|
|
1007
|
+
return mix(
|
|
1008
|
+
mix(hash(i), hash(i + vec2f(1.0, 0.0)), u.x),
|
|
1009
|
+
mix(hash(i + vec2f(0.0, 1.0)), hash(i + vec2f(1.0, 1.0)), u.x),
|
|
1010
|
+
u.y,
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
${wgsl}
|
|
1015
|
+
|
|
1016
|
+
@fragment
|
|
1017
|
+
fn fs(in: Varyings) -> @location(0) vec4f {
|
|
1018
|
+
sourceRect = in.color;
|
|
1019
|
+
sourceLayer = i32(in.info.x);
|
|
1020
|
+
let pointer = (view.pointer.xy - in.corner) / in.size;
|
|
1021
|
+
let press = (view.pointer.zw - in.corner) / in.size;
|
|
1022
|
+
return effect(
|
|
1023
|
+
EffectInput(in.uv, in.size, view.time, view.zoom, in.params, pointer, press, view.pressTime),
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
`
|
|
1027
|
+
);
|
|
457
1028
|
|
|
458
1029
|
// src/gpu-renderer.ts
|
|
459
|
-
var FLOATS_PER_INSTANCE =
|
|
1030
|
+
var FLOATS_PER_INSTANCE = 16;
|
|
460
1031
|
var CompositeRenderer = class {
|
|
461
|
-
constructor(device, context, format, atlas, background, placeholder
|
|
1032
|
+
constructor(device, context, format, atlas, background, placeholder) {
|
|
462
1033
|
this.device = device;
|
|
463
1034
|
this.context = context;
|
|
464
1035
|
this.atlas = atlas;
|
|
465
1036
|
this.background = background;
|
|
466
1037
|
this.placeholder = placeholder;
|
|
467
|
-
this.
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
1038
|
+
this.bindGroupLayout = device.createBindGroupLayout({
|
|
1039
|
+
entries: [
|
|
1040
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: {} },
|
|
1041
|
+
{
|
|
1042
|
+
binding: 1,
|
|
1043
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
1044
|
+
texture: { viewDimension: "2d-array" }
|
|
1045
|
+
},
|
|
1046
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: {} }
|
|
1047
|
+
]
|
|
1048
|
+
});
|
|
1049
|
+
this.sampler = device.createSampler({ magFilter: "linear", minFilter: "linear" });
|
|
1050
|
+
this.pipeline = device.createRenderPipeline(
|
|
1051
|
+
this.pipelineDescriptor("gpucanvas composite", compositeShader, format)
|
|
1052
|
+
);
|
|
1053
|
+
this.format = format;
|
|
1054
|
+
this.uniforms = device.createBuffer({
|
|
1055
|
+
size: this.uniformData.byteLength,
|
|
1056
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
472
1057
|
});
|
|
473
|
-
this.
|
|
474
|
-
|
|
475
|
-
|
|
1058
|
+
this.bindGroup = device.createBindGroup({
|
|
1059
|
+
layout: this.bindGroupLayout,
|
|
1060
|
+
entries: [
|
|
1061
|
+
{ binding: 0, resource: { buffer: this.uniforms } },
|
|
1062
|
+
{ binding: 1, resource: atlas.view },
|
|
1063
|
+
{ binding: 2, resource: this.sampler }
|
|
1064
|
+
]
|
|
1065
|
+
});
|
|
1066
|
+
this.instanceBuffer = this.createInstanceBuffer();
|
|
1067
|
+
}
|
|
1068
|
+
device;
|
|
1069
|
+
context;
|
|
1070
|
+
atlas;
|
|
1071
|
+
background;
|
|
1072
|
+
placeholder;
|
|
1073
|
+
instances = new Float32Array(FLOATS_PER_INSTANCE * 4096);
|
|
1074
|
+
instanceBuffer;
|
|
1075
|
+
uniforms;
|
|
1076
|
+
uniformData = new Float32Array(20);
|
|
1077
|
+
pipeline;
|
|
1078
|
+
bindGroup;
|
|
1079
|
+
bindGroupLayout;
|
|
1080
|
+
sampler;
|
|
1081
|
+
/** Effect pipelines, null while one compiles. Compiling never blocks a frame. */
|
|
1082
|
+
effects = /* @__PURE__ */ new Map();
|
|
1083
|
+
/** Called when an effect pipeline is ready, so the caller can draw it. */
|
|
1084
|
+
onEffectReady = () => {
|
|
1085
|
+
};
|
|
1086
|
+
format;
|
|
1087
|
+
pipelineDescriptor(label, code, format) {
|
|
1088
|
+
const module = this.device.createShaderModule({ label, code });
|
|
1089
|
+
return {
|
|
1090
|
+
label,
|
|
1091
|
+
layout: this.device.createPipelineLayout({ bindGroupLayouts: [this.bindGroupLayout] }),
|
|
476
1092
|
vertex: {
|
|
477
1093
|
module,
|
|
478
1094
|
entryPoint: "vs",
|
|
@@ -483,7 +1099,8 @@ var CompositeRenderer = class {
|
|
|
483
1099
|
attributes: [
|
|
484
1100
|
{ shaderLocation: 0, offset: 0, format: "float32x4" },
|
|
485
1101
|
{ shaderLocation: 1, offset: 16, format: "float32x4" },
|
|
486
|
-
{ shaderLocation: 2, offset: 32, format: "float32x4" }
|
|
1102
|
+
{ shaderLocation: 2, offset: 32, format: "float32x4" },
|
|
1103
|
+
{ shaderLocation: 3, offset: 48, format: "float32x4" }
|
|
487
1104
|
]
|
|
488
1105
|
}
|
|
489
1106
|
]
|
|
@@ -503,37 +1120,24 @@ var CompositeRenderer = class {
|
|
|
503
1120
|
]
|
|
504
1121
|
},
|
|
505
1122
|
primitive: { topology: "triangle-strip" }
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
this.
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
});
|
|
522
|
-
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
/** Whether `effect` can draw this frame. The first ask starts compiling it. */
|
|
1126
|
+
hasEffect(effect) {
|
|
1127
|
+
if (this.effects.has(effect)) return this.effects.get(effect) !== null;
|
|
1128
|
+
this.effects.set(effect, null);
|
|
1129
|
+
this.device.createRenderPipelineAsync(
|
|
1130
|
+
this.pipelineDescriptor(
|
|
1131
|
+
`gpucanvas effect ${effect.name}`,
|
|
1132
|
+
effectShader(effect.wgsl),
|
|
1133
|
+
this.format
|
|
1134
|
+
)
|
|
1135
|
+
).then((pipeline) => {
|
|
1136
|
+
this.effects.set(effect, pipeline);
|
|
1137
|
+
this.onEffectReady();
|
|
1138
|
+
}).catch((error) => console.error(`[gpucanvas] effect "${effect.name}" failed`, error));
|
|
1139
|
+
return false;
|
|
523
1140
|
}
|
|
524
|
-
device;
|
|
525
|
-
context;
|
|
526
|
-
atlas;
|
|
527
|
-
background;
|
|
528
|
-
placeholder;
|
|
529
|
-
selection;
|
|
530
|
-
outline;
|
|
531
|
-
instances = new Float32Array(FLOATS_PER_INSTANCE * 4096);
|
|
532
|
-
instanceBuffer;
|
|
533
|
-
uniforms;
|
|
534
|
-
uniformData = new Float32Array(16);
|
|
535
|
-
pipeline;
|
|
536
|
-
bindGroup;
|
|
537
1141
|
/** Grows the CPU and GPU instance storage to hold at least `count` instances. */
|
|
538
1142
|
reserve(count) {
|
|
539
1143
|
const floats = count * FLOATS_PER_INSTANCE;
|
|
@@ -542,7 +1146,7 @@ var CompositeRenderer = class {
|
|
|
542
1146
|
this.instanceBuffer.destroy();
|
|
543
1147
|
this.instanceBuffer = this.createInstanceBuffer();
|
|
544
1148
|
}
|
|
545
|
-
draw(camera, cssWidth, cssHeight, dpr, count) {
|
|
1149
|
+
draw(camera, cssWidth, cssHeight, dpr, count, runs, effects) {
|
|
546
1150
|
const { device } = this;
|
|
547
1151
|
this.uniformData.set([
|
|
548
1152
|
cssWidth,
|
|
@@ -552,9 +1156,13 @@ var CompositeRenderer = class {
|
|
|
552
1156
|
camera.zoom,
|
|
553
1157
|
dpr,
|
|
554
1158
|
this.atlas.size,
|
|
555
|
-
|
|
1159
|
+
effects.time,
|
|
556
1160
|
...this.placeholder,
|
|
557
|
-
|
|
1161
|
+
effects.pointer.x,
|
|
1162
|
+
effects.pointer.y,
|
|
1163
|
+
effects.press.x,
|
|
1164
|
+
effects.press.y,
|
|
1165
|
+
effects.sincePress
|
|
558
1166
|
]);
|
|
559
1167
|
device.queue.writeBuffer(this.uniforms, 0, this.uniformData);
|
|
560
1168
|
if (count > 0) {
|
|
@@ -579,10 +1187,14 @@ var CompositeRenderer = class {
|
|
|
579
1187
|
]
|
|
580
1188
|
});
|
|
581
1189
|
if (count > 0) {
|
|
582
|
-
pass.setPipeline(this.pipeline);
|
|
583
1190
|
pass.setBindGroup(0, this.bindGroup);
|
|
584
1191
|
pass.setVertexBuffer(0, this.instanceBuffer);
|
|
585
|
-
|
|
1192
|
+
let current = null;
|
|
1193
|
+
for (const run of runs.length ? runs : [{ effect: null, start: 0, count }]) {
|
|
1194
|
+
const pipeline = run.effect && this.effects.get(run.effect) || this.pipeline;
|
|
1195
|
+
if (pipeline !== current) pass.setPipeline(current = pipeline);
|
|
1196
|
+
pass.draw(4, run.count, 0, run.start);
|
|
1197
|
+
}
|
|
586
1198
|
}
|
|
587
1199
|
pass.end();
|
|
588
1200
|
device.queue.submit([encoder.finish()]);
|
|
@@ -773,12 +1385,32 @@ async function createGpuCanvas(host, options) {
|
|
|
773
1385
|
context,
|
|
774
1386
|
format,
|
|
775
1387
|
atlas,
|
|
776
|
-
...colorsFor(options.background ?? "#f4f4f5")
|
|
777
|
-
premultiplied(cssColorToRgba(options.selectionColor ?? DEFAULT_SELECTION_COLOR)),
|
|
778
|
-
SELECTION_WIDTH
|
|
1388
|
+
...colorsFor(options.background ?? "#f4f4f5")
|
|
779
1389
|
);
|
|
1390
|
+
const selectionColor = premultiplied(
|
|
1391
|
+
cssColorToRgba(options.selectionColor ?? DEFAULT_SELECTION_COLOR)
|
|
1392
|
+
);
|
|
1393
|
+
const hoverColor = options.hoverColor ? premultiplied(cssColorToRgba(options.hoverColor)) : selectionColor;
|
|
780
1394
|
const nodes = /* @__PURE__ */ new Map();
|
|
781
|
-
const
|
|
1395
|
+
const effects = /* @__PURE__ */ new Map();
|
|
1396
|
+
const startedAt = performance.now();
|
|
1397
|
+
const FAR_AWAY = { x: -1e7, y: -1e7 };
|
|
1398
|
+
renderer.onEffectReady = () => schedule();
|
|
1399
|
+
const runs = [];
|
|
1400
|
+
let effectsMoving = false;
|
|
1401
|
+
let selection = /* @__PURE__ */ new Set();
|
|
1402
|
+
let hovered = null;
|
|
1403
|
+
const ringRects = /* @__PURE__ */ new Map();
|
|
1404
|
+
const ringRect = (element) => {
|
|
1405
|
+
const own = nodes.get(element);
|
|
1406
|
+
if (own) return own.rect;
|
|
1407
|
+
const at = element.isConnected ? nodeOfElement(nodes, element) : null;
|
|
1408
|
+
if (!at) return null;
|
|
1409
|
+
const node = nodes.get(at);
|
|
1410
|
+
let rect = ringRects.get(element);
|
|
1411
|
+
if (!rect) ringRects.set(element, rect = rectInNode(at, node.rect, element));
|
|
1412
|
+
return rect;
|
|
1413
|
+
};
|
|
782
1414
|
const pages = /* @__PURE__ */ new Map();
|
|
783
1415
|
atlas.keep = function* () {
|
|
784
1416
|
const onScreen = (tile) => intersects(tile.rect, frameView);
|
|
@@ -1174,12 +1806,22 @@ async function createGpuCanvas(host, options) {
|
|
|
1174
1806
|
};
|
|
1175
1807
|
const composite = () => {
|
|
1176
1808
|
let instances = visible.length;
|
|
1177
|
-
for (const node of visible)
|
|
1178
|
-
|
|
1809
|
+
for (const node of visible) instances += node.tiles.length + (node.preview ? 1 : 0);
|
|
1810
|
+
instances += selection.size + 1;
|
|
1179
1811
|
renderer.reserve(instances);
|
|
1180
1812
|
const data = renderer.instances;
|
|
1181
1813
|
let count = 0;
|
|
1814
|
+
runs.length = 0;
|
|
1815
|
+
effectsMoving = false;
|
|
1816
|
+
let runEffect = null;
|
|
1817
|
+
const startRun = (effect) => {
|
|
1818
|
+
if (runs.length && runEffect === effect) return;
|
|
1819
|
+
runEffect = effect;
|
|
1820
|
+
runs.push({ effect, start: count, count: 0 });
|
|
1821
|
+
};
|
|
1182
1822
|
const push = (rect, slot) => {
|
|
1823
|
+
startRun(null);
|
|
1824
|
+
runs[runs.length - 1].count++;
|
|
1183
1825
|
const offset = count++ * FLOATS_PER_INSTANCE;
|
|
1184
1826
|
data[offset] = rect.x;
|
|
1185
1827
|
data[offset + 1] = rect.y;
|
|
@@ -1196,11 +1838,40 @@ async function createGpuCanvas(host, options) {
|
|
|
1196
1838
|
} else {
|
|
1197
1839
|
data[offset + 9] = 1;
|
|
1198
1840
|
}
|
|
1841
|
+
data[offset + 10] = 0;
|
|
1842
|
+
};
|
|
1843
|
+
const pushEffect = (node, slot, worn) => {
|
|
1844
|
+
startRun(worn.effect);
|
|
1845
|
+
runs[runs.length - 1].count++;
|
|
1846
|
+
const offset = count++ * FLOATS_PER_INSTANCE;
|
|
1847
|
+
const { rect } = node;
|
|
1848
|
+
data[offset] = rect.x;
|
|
1849
|
+
data[offset + 1] = rect.y;
|
|
1850
|
+
data[offset + 2] = rect.width;
|
|
1851
|
+
data[offset + 3] = rect.height;
|
|
1852
|
+
atlas.touch(slot.layer);
|
|
1853
|
+
data[offset + 4] = slot.x;
|
|
1854
|
+
data[offset + 5] = slot.y;
|
|
1855
|
+
data[offset + 6] = slot.x + slot.width;
|
|
1856
|
+
data[offset + 7] = slot.y + slot.height;
|
|
1857
|
+
data[offset + 8] = slot.layer;
|
|
1858
|
+
data[offset + 9] = 0;
|
|
1859
|
+
data[offset + 10] = worn.pad;
|
|
1860
|
+
data.set(worn.params, offset + 12);
|
|
1199
1861
|
};
|
|
1200
|
-
const pushRing = (
|
|
1862
|
+
const pushRing = (element, color, width) => {
|
|
1863
|
+
const rect = ringRect(element);
|
|
1864
|
+
if (!rect || !intersects(rect, frameView)) return;
|
|
1865
|
+
startRun(null);
|
|
1866
|
+
runs[runs.length - 1].count++;
|
|
1201
1867
|
const offset = count++ * FLOATS_PER_INSTANCE;
|
|
1202
|
-
data
|
|
1868
|
+
data[offset] = rect.x;
|
|
1869
|
+
data[offset + 1] = rect.y;
|
|
1870
|
+
data[offset + 2] = rect.width;
|
|
1871
|
+
data[offset + 3] = rect.height;
|
|
1872
|
+
data.set(color, offset + 4);
|
|
1203
1873
|
data[offset + 9] = 2;
|
|
1874
|
+
data[offset + 10] = width;
|
|
1204
1875
|
};
|
|
1205
1876
|
let onThumbnail = 0;
|
|
1206
1877
|
for (const node of visible) {
|
|
@@ -1214,6 +1885,15 @@ async function createGpuCanvas(host, options) {
|
|
|
1214
1885
|
if (before === 3 && shown === 2) stats.lostDetail++;
|
|
1215
1886
|
node.shown = shown;
|
|
1216
1887
|
node.shownFrame = stats.frames;
|
|
1888
|
+
const worn = effects.size ? effects.get(node.element) : void 0;
|
|
1889
|
+
if (worn) {
|
|
1890
|
+
if (worn.effect.animated) effectsMoving = true;
|
|
1891
|
+
if (base && renderer.hasEffect(worn.effect)) {
|
|
1892
|
+
pushEffect(node, base, worn);
|
|
1893
|
+
continue;
|
|
1894
|
+
}
|
|
1895
|
+
effectsMoving = true;
|
|
1896
|
+
}
|
|
1217
1897
|
push(node.rect, base);
|
|
1218
1898
|
const current = (tile) => tile.scale === detailScale && !tile.stale;
|
|
1219
1899
|
for (const tile of node.tiles)
|
|
@@ -1223,12 +1903,17 @@ async function createGpuCanvas(host, options) {
|
|
|
1223
1903
|
if (current(tile) && atlas.isValid(tile.slot)) push(tile.rect, tile.slot);
|
|
1224
1904
|
}
|
|
1225
1905
|
stats.onThumbnail = onThumbnail;
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
}
|
|
1906
|
+
for (const element of selection) pushRing(element, selectionColor, SELECTION_WIDTH);
|
|
1907
|
+
if (hovered && !selection.has(hovered)) pushRing(hovered, hoverColor, HOVER_WIDTH);
|
|
1229
1908
|
const cssWidth = canvas.clientWidth;
|
|
1230
1909
|
const dpr = canvas.width / Math.max(1, cssWidth);
|
|
1231
|
-
|
|
1910
|
+
const now = performance.now();
|
|
1911
|
+
renderer.draw(camera, cssWidth, canvas.clientHeight, dpr, count, runs, {
|
|
1912
|
+
time: (now - startedAt) / 1e3,
|
|
1913
|
+
pointer: pointer ? screenToWorld(camera, pointer) : FAR_AWAY,
|
|
1914
|
+
press,
|
|
1915
|
+
sincePress: Math.min((now - pressedAt) / 1e3, 1e4)
|
|
1916
|
+
});
|
|
1232
1917
|
};
|
|
1233
1918
|
const frame = (inPaint) => {
|
|
1234
1919
|
if (destroyed) return;
|
|
@@ -1272,7 +1957,7 @@ async function createGpuCanvas(host, options) {
|
|
|
1272
1957
|
stats.visible = visible.length;
|
|
1273
1958
|
stats.pending = pending;
|
|
1274
1959
|
stats.frameMs = performance.now() - start;
|
|
1275
|
-
if (pending > 0) schedule();
|
|
1960
|
+
if (pending > 0 || effectsMoving) schedule();
|
|
1276
1961
|
};
|
|
1277
1962
|
let awaitingPaint = false;
|
|
1278
1963
|
canvas.onpaint = (event) => {
|
|
@@ -1287,6 +1972,7 @@ async function createGpuCanvas(host, options) {
|
|
|
1287
1972
|
}
|
|
1288
1973
|
}
|
|
1289
1974
|
}
|
|
1975
|
+
if (changed) ringRects.clear();
|
|
1290
1976
|
if (!awaitingPaint && !changed) return;
|
|
1291
1977
|
awaitingPaint = false;
|
|
1292
1978
|
frame(true);
|
|
@@ -1300,6 +1986,7 @@ async function createGpuCanvas(host, options) {
|
|
|
1300
1986
|
return element ? nodes.get(element) : void 0;
|
|
1301
1987
|
};
|
|
1302
1988
|
const mutations = new MutationObserver((records) => {
|
|
1989
|
+
ringRects.clear();
|
|
1303
1990
|
let changed = false;
|
|
1304
1991
|
for (const record of records) {
|
|
1305
1992
|
const target = record.target;
|
|
@@ -1382,13 +2069,9 @@ async function createGpuCanvas(host, options) {
|
|
|
1382
2069
|
};
|
|
1383
2070
|
const pickLive = () => {
|
|
1384
2071
|
if (!pointer) return setLive(null);
|
|
1385
|
-
const
|
|
1386
|
-
const worldY = camera.y + pointer.y / camera.zoom;
|
|
2072
|
+
const point = screenToWorld(camera, pointer);
|
|
1387
2073
|
let hit = null;
|
|
1388
|
-
for (const node of nodes.values())
|
|
1389
|
-
const { x, y, width, height } = node.rect;
|
|
1390
|
-
if (worldX >= x && worldX < x + width && worldY >= y && worldY < y + height) hit = node;
|
|
1391
|
-
}
|
|
2074
|
+
for (const node of nodes.values()) if (containsPoint(node.rect, point)) hit = node;
|
|
1392
2075
|
setLive(hit);
|
|
1393
2076
|
};
|
|
1394
2077
|
const onPointerMove = (event) => {
|
|
@@ -1400,6 +2083,17 @@ async function createGpuCanvas(host, options) {
|
|
|
1400
2083
|
pointer = null;
|
|
1401
2084
|
setLive(null);
|
|
1402
2085
|
};
|
|
2086
|
+
let press = FAR_AWAY;
|
|
2087
|
+
let pressedAt = -Infinity;
|
|
2088
|
+
const onPointerDown = (event) => {
|
|
2089
|
+
const bounds = canvas.getBoundingClientRect();
|
|
2090
|
+
press = screenToWorld(camera, {
|
|
2091
|
+
x: event.clientX - bounds.left,
|
|
2092
|
+
y: event.clientY - bounds.top
|
|
2093
|
+
});
|
|
2094
|
+
pressedAt = performance.now();
|
|
2095
|
+
if (effects.size) schedule();
|
|
2096
|
+
};
|
|
1403
2097
|
const onFocusIn = (event) => {
|
|
1404
2098
|
const node = nodeOf(event.target) ?? null;
|
|
1405
2099
|
if (node === focused) return;
|
|
@@ -1438,6 +2132,7 @@ async function createGpuCanvas(host, options) {
|
|
|
1438
2132
|
document.fonts.addEventListener("loadingdone", onFontsLoaded);
|
|
1439
2133
|
surface.addEventListener("pointermove", onPointerMove);
|
|
1440
2134
|
surface.addEventListener("pointerleave", onPointerLeave);
|
|
2135
|
+
surface.addEventListener("pointerdown", onPointerDown, true);
|
|
1441
2136
|
canvas.addEventListener("focusin", onFocusIn);
|
|
1442
2137
|
canvas.addEventListener("focusout", onFocusOut);
|
|
1443
2138
|
const usedAt = /* @__PURE__ */ new WeakMap();
|
|
@@ -1599,6 +2294,7 @@ async function createGpuCanvas(host, options) {
|
|
|
1599
2294
|
if (!node) return;
|
|
1600
2295
|
const resized = node.rect.width !== rect.width || node.rect.height !== rect.height;
|
|
1601
2296
|
node.rect = rect;
|
|
2297
|
+
ringRects.clear();
|
|
1602
2298
|
if (resized) {
|
|
1603
2299
|
if (live === node) setLive(null);
|
|
1604
2300
|
releaseNode(node);
|
|
@@ -1613,7 +2309,8 @@ async function createGpuCanvas(host, options) {
|
|
|
1613
2309
|
if (lingering === node) lingering = null;
|
|
1614
2310
|
if (focused === node) focused = null;
|
|
1615
2311
|
transitioning.delete(node);
|
|
1616
|
-
|
|
2312
|
+
effects.delete(element);
|
|
2313
|
+
selection.delete(element);
|
|
1617
2314
|
nodes.delete(element);
|
|
1618
2315
|
releaseNode(node);
|
|
1619
2316
|
element.remove();
|
|
@@ -1628,14 +2325,37 @@ async function createGpuCanvas(host, options) {
|
|
|
1628
2325
|
for (const node of nodes.values()) if (intersects(node.rect, rect)) hits.push(node.element);
|
|
1629
2326
|
return hits;
|
|
1630
2327
|
},
|
|
2328
|
+
getNodeOf: (element) => nodeOfElement(nodes, element),
|
|
2329
|
+
getElementRect(element) {
|
|
2330
|
+
const rect = ringRect(element);
|
|
2331
|
+
return rect ? { ...rect } : null;
|
|
2332
|
+
},
|
|
2333
|
+
getElementAt(point) {
|
|
2334
|
+
let top = null;
|
|
2335
|
+
for (const node of nodes.values()) if (containsPoint(node.rect, point)) top = node;
|
|
2336
|
+
return top ? elementInNode(top.element, top.rect, point) : null;
|
|
2337
|
+
},
|
|
1631
2338
|
setSelection(elements) {
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
const node = nodes.get(element);
|
|
1635
|
-
if (node) selected.add(node);
|
|
1636
|
-
}
|
|
2339
|
+
selection = new Set(elements);
|
|
2340
|
+
ringRects.clear();
|
|
1637
2341
|
schedule();
|
|
1638
2342
|
},
|
|
2343
|
+
setHover(element) {
|
|
2344
|
+
if (element === hovered) return;
|
|
2345
|
+
hovered = element;
|
|
2346
|
+
ringRects.clear();
|
|
2347
|
+
schedule();
|
|
2348
|
+
},
|
|
2349
|
+
setEffect(element, effect, params) {
|
|
2350
|
+
if (!nodes.has(element)) return;
|
|
2351
|
+
if (effect) {
|
|
2352
|
+
const values = effectParams(effect, params);
|
|
2353
|
+
effects.set(element, { effect, params: values, pad: effectPad(effect, values) });
|
|
2354
|
+
} else effects.delete(element);
|
|
2355
|
+
schedule();
|
|
2356
|
+
},
|
|
2357
|
+
screenToWorld: (point) => screenToWorld(camera, point),
|
|
2358
|
+
worldToScreen: (point) => worldToScreen(camera, point),
|
|
1639
2359
|
getCamera: () => camera,
|
|
1640
2360
|
setCamera,
|
|
1641
2361
|
invalidate: onFontsLoaded,
|
|
@@ -1677,6 +2397,7 @@ async function createGpuCanvas(host, options) {
|
|
|
1677
2397
|
canvas.removeEventListener("load", onResourceLoad, true);
|
|
1678
2398
|
surface.removeEventListener("pointermove", onPointerMove);
|
|
1679
2399
|
surface.removeEventListener("pointerleave", onPointerLeave);
|
|
2400
|
+
surface.removeEventListener("pointerdown", onPointerDown, true);
|
|
1680
2401
|
canvas.removeEventListener("focusin", onFocusIn);
|
|
1681
2402
|
canvas.removeEventListener("focusout", onFocusOut);
|
|
1682
2403
|
canvas.removeEventListener("transitionrun", onTransitionRun);
|
|
@@ -1692,6 +2413,689 @@ async function createGpuCanvas(host, options) {
|
|
|
1692
2413
|
};
|
|
1693
2414
|
}
|
|
1694
2415
|
|
|
2416
|
+
// src/layers.ts
|
|
2417
|
+
var sameLayer = (a, b) => !!a && !!b && a[0] === b[0];
|
|
2418
|
+
var layerIn = (layer, list) => list.some((other) => sameLayer(layer, other));
|
|
2419
|
+
function layerRect(canvas, layer) {
|
|
2420
|
+
let union = null;
|
|
2421
|
+
for (const element of layer) {
|
|
2422
|
+
const rect = canvas.getElementRect(element);
|
|
2423
|
+
if (rect) union = union ? unionRect(union, rect) : rect;
|
|
2424
|
+
}
|
|
2425
|
+
return union;
|
|
2426
|
+
}
|
|
2427
|
+
function unionRect(a, b) {
|
|
2428
|
+
const x = Math.min(a.x, b.x);
|
|
2429
|
+
const y = Math.min(a.y, b.y);
|
|
2430
|
+
return {
|
|
2431
|
+
x,
|
|
2432
|
+
y,
|
|
2433
|
+
width: Math.max(a.x + a.width, b.x + b.width) - x,
|
|
2434
|
+
height: Math.max(a.y + a.height, b.y + b.height) - y
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
var sameRect = (a, b) => !!a && !!b && Math.abs(a.x - b.x) < 0.5 && Math.abs(a.y - b.y) < 0.5 && Math.abs(a.width - b.width) < 0.5 && Math.abs(a.height - b.height) < 0.5;
|
|
2438
|
+
function createLayers(canvas, pick) {
|
|
2439
|
+
const chainOf = (element) => {
|
|
2440
|
+
const node = canvas.getNodeOf(element);
|
|
2441
|
+
if (!node) return [];
|
|
2442
|
+
let inner;
|
|
2443
|
+
if (pick === "node") inner = [];
|
|
2444
|
+
else if (pick === "element") {
|
|
2445
|
+
inner = [];
|
|
2446
|
+
for (let at = element; at && at !== node; at = at.parentElement) {
|
|
2447
|
+
inner.unshift([at]);
|
|
2448
|
+
}
|
|
2449
|
+
} else {
|
|
2450
|
+
inner = pick(element, canvas).map((layer) => [layer].flat()).filter((layer) => layer.length > 0 && layer[0] !== node);
|
|
2451
|
+
}
|
|
2452
|
+
const chain = [[node]];
|
|
2453
|
+
let previous = canvas.getNodeRect(node);
|
|
2454
|
+
for (const layer of inner) {
|
|
2455
|
+
const rect = layerRect(canvas, layer);
|
|
2456
|
+
if (sameRect(rect, previous)) continue;
|
|
2457
|
+
chain.push(layer);
|
|
2458
|
+
previous = rect;
|
|
2459
|
+
}
|
|
2460
|
+
return chain;
|
|
2461
|
+
};
|
|
2462
|
+
const chainAt = (world) => {
|
|
2463
|
+
const element = canvas.getElementAt(world);
|
|
2464
|
+
return element ? chainOf(element) : [];
|
|
2465
|
+
};
|
|
2466
|
+
const childrenOf = (parent) => {
|
|
2467
|
+
if (pick === "node") return [];
|
|
2468
|
+
const children = [];
|
|
2469
|
+
if (pick === "element") {
|
|
2470
|
+
const visit = (element, outer) => {
|
|
2471
|
+
for (const child of element.children) {
|
|
2472
|
+
const rect = canvas.getElementRect(child);
|
|
2473
|
+
if (!rect || rect.width <= 0 || rect.height <= 0) continue;
|
|
2474
|
+
if (sameRect(rect, outer)) visit(child, outer);
|
|
2475
|
+
else children.push([child]);
|
|
2476
|
+
}
|
|
2477
|
+
};
|
|
2478
|
+
for (const root of parent) visit(root, layerRect(canvas, parent));
|
|
2479
|
+
return children;
|
|
2480
|
+
}
|
|
2481
|
+
for (const root of parent) {
|
|
2482
|
+
for (const element of root.querySelectorAll("*")) {
|
|
2483
|
+
if (children.some((child2) => child2.some((held) => held.contains(element)))) continue;
|
|
2484
|
+
const chain = chainOf(element);
|
|
2485
|
+
const index = chain.findIndex((layer) => sameLayer(layer, parent));
|
|
2486
|
+
const child = index >= 0 ? chain[index + 1] : void 0;
|
|
2487
|
+
if (child && !layerIn(child, children)) children.push(child);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
return children;
|
|
2491
|
+
};
|
|
2492
|
+
const layersIn = (scope, rect) => scope ? childrenOf(scope).filter((layer) => {
|
|
2493
|
+
const box = layerRect(canvas, layer);
|
|
2494
|
+
return !!box && intersects(box, rect);
|
|
2495
|
+
}) : canvas.getNodesIn(rect).map((node) => [node]);
|
|
2496
|
+
return { chainOf, chainAt, childrenOf, layersIn };
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
// src/overlay.ts
|
|
2500
|
+
var HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
|
|
2501
|
+
var HANDLE_PX = 8;
|
|
2502
|
+
var CURSORS = {
|
|
2503
|
+
nw: "nwse-resize",
|
|
2504
|
+
se: "nwse-resize",
|
|
2505
|
+
ne: "nesw-resize",
|
|
2506
|
+
sw: "nesw-resize",
|
|
2507
|
+
n: "ns-resize",
|
|
2508
|
+
s: "ns-resize",
|
|
2509
|
+
e: "ew-resize",
|
|
2510
|
+
w: "ew-resize"
|
|
2511
|
+
};
|
|
2512
|
+
var pill = (color) => ({
|
|
2513
|
+
position: "absolute",
|
|
2514
|
+
font: "500 11px/16px system-ui, sans-serif",
|
|
2515
|
+
padding: "0 5px",
|
|
2516
|
+
borderRadius: "3px",
|
|
2517
|
+
color: "#fff",
|
|
2518
|
+
background: color,
|
|
2519
|
+
whiteSpace: "nowrap",
|
|
2520
|
+
pointerEvents: "none"
|
|
2521
|
+
});
|
|
2522
|
+
function createOverlay(surface, color, hoverColor) {
|
|
2523
|
+
const root = document.createElement("div");
|
|
2524
|
+
Object.assign(root.style, {
|
|
2525
|
+
position: "absolute",
|
|
2526
|
+
inset: "0",
|
|
2527
|
+
overflow: "hidden",
|
|
2528
|
+
pointerEvents: "none",
|
|
2529
|
+
zIndex: "2"
|
|
2530
|
+
});
|
|
2531
|
+
const make = (style) => {
|
|
2532
|
+
const element = document.createElement("div");
|
|
2533
|
+
Object.assign(element.style, { left: "0", top: "0" }, style, { display: "none" });
|
|
2534
|
+
root.append(element);
|
|
2535
|
+
return element;
|
|
2536
|
+
};
|
|
2537
|
+
const hoverName = make(pill(hoverColor));
|
|
2538
|
+
const name = make(pill(color));
|
|
2539
|
+
const size = make(pill(color));
|
|
2540
|
+
const bounds = make({ position: "absolute", border: `1px solid ${color}` });
|
|
2541
|
+
const handles = HANDLES.map((handle) => {
|
|
2542
|
+
const element = make({
|
|
2543
|
+
position: "absolute",
|
|
2544
|
+
width: `${HANDLE_PX}px`,
|
|
2545
|
+
height: `${HANDLE_PX}px`,
|
|
2546
|
+
boxSizing: "border-box",
|
|
2547
|
+
background: "#fff",
|
|
2548
|
+
border: `1px solid ${color}`,
|
|
2549
|
+
pointerEvents: "auto",
|
|
2550
|
+
cursor: CURSORS[handle]
|
|
2551
|
+
});
|
|
2552
|
+
element.dataset.gpucanvasHandle = handle;
|
|
2553
|
+
return element;
|
|
2554
|
+
});
|
|
2555
|
+
const guides = [];
|
|
2556
|
+
surface.append(root);
|
|
2557
|
+
const show = (element, visible) => {
|
|
2558
|
+
element.style.display = visible ? "block" : "none";
|
|
2559
|
+
};
|
|
2560
|
+
const place = (element, x, y) => {
|
|
2561
|
+
element.style.transform = `translate(${Math.round(x)}px, ${Math.round(y)}px)`;
|
|
2562
|
+
};
|
|
2563
|
+
const text = (element, value) => {
|
|
2564
|
+
if (element.textContent !== value) element.textContent = value;
|
|
2565
|
+
};
|
|
2566
|
+
return {
|
|
2567
|
+
render(state) {
|
|
2568
|
+
const { hover, selection } = state;
|
|
2569
|
+
show(hoverName, !!hover?.name);
|
|
2570
|
+
if (hover?.name) {
|
|
2571
|
+
text(hoverName, hover.name);
|
|
2572
|
+
place(hoverName, hover.rect.x - 1, hover.rect.y - 19);
|
|
2573
|
+
}
|
|
2574
|
+
show(name, !!selection?.name);
|
|
2575
|
+
show(size, !!selection);
|
|
2576
|
+
show(bounds, !!selection && state.bounds);
|
|
2577
|
+
for (const handle of handles) show(handle, !!selection && state.handles);
|
|
2578
|
+
if (selection) {
|
|
2579
|
+
const { rect } = selection;
|
|
2580
|
+
if (selection.name) {
|
|
2581
|
+
text(name, selection.name);
|
|
2582
|
+
place(name, rect.x - 1, rect.y - 19);
|
|
2583
|
+
}
|
|
2584
|
+
const { width, height } = selection.size;
|
|
2585
|
+
const label = `${Math.round(width)} \xD7 ${Math.round(height)}`;
|
|
2586
|
+
text(size, selection.count > 1 ? `${selection.count} layers \xB7 ${label}` : label);
|
|
2587
|
+
place(size, rect.x + rect.width / 2 - size.offsetWidth / 2, rect.y + rect.height + 6);
|
|
2588
|
+
if (state.bounds) {
|
|
2589
|
+
place(bounds, rect.x, rect.y);
|
|
2590
|
+
bounds.style.width = `${rect.width}px`;
|
|
2591
|
+
bounds.style.height = `${rect.height}px`;
|
|
2592
|
+
}
|
|
2593
|
+
if (state.handles) {
|
|
2594
|
+
HANDLES.forEach((handle, i) => {
|
|
2595
|
+
const fx = handle.includes("w") ? 0 : handle.includes("e") ? 1 : 0.5;
|
|
2596
|
+
const fy = handle.includes("n") ? 0 : handle.includes("s") ? 1 : 0.5;
|
|
2597
|
+
place(
|
|
2598
|
+
handles[i],
|
|
2599
|
+
rect.x + rect.width * fx - HANDLE_PX / 2,
|
|
2600
|
+
rect.y + rect.height * fy - HANDLE_PX / 2
|
|
2601
|
+
);
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
while (guides.length < state.guides.length) {
|
|
2606
|
+
guides.push(make({ position: "absolute", background: "#f43f5e" }));
|
|
2607
|
+
}
|
|
2608
|
+
guides.forEach((line, i) => {
|
|
2609
|
+
const guide = state.guides[i];
|
|
2610
|
+
show(line, !!guide);
|
|
2611
|
+
if (!guide) return;
|
|
2612
|
+
place(line, guide.x, guide.y);
|
|
2613
|
+
line.style.width = `${Math.max(1, guide.width)}px`;
|
|
2614
|
+
line.style.height = `${Math.max(1, guide.height)}px`;
|
|
2615
|
+
});
|
|
2616
|
+
},
|
|
2617
|
+
remove() {
|
|
2618
|
+
root.remove();
|
|
2619
|
+
}
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
var guideRect = (guide, toScreen) => {
|
|
2623
|
+
const a = toScreen(guide.x1, guide.y1);
|
|
2624
|
+
const b = toScreen(guide.x2, guide.y2);
|
|
2625
|
+
return {
|
|
2626
|
+
x: Math.min(a.x, b.x),
|
|
2627
|
+
y: Math.min(a.y, b.y),
|
|
2628
|
+
width: Math.abs(b.x - a.x),
|
|
2629
|
+
height: Math.abs(b.y - a.y)
|
|
2630
|
+
};
|
|
2631
|
+
};
|
|
2632
|
+
|
|
2633
|
+
// src/snap.ts
|
|
2634
|
+
var ALL_ANCHORS = [0, 0.5, 1];
|
|
2635
|
+
var anchorsX = (rect) => ALL_ANCHORS.map((f) => rect.x + rect.width * f);
|
|
2636
|
+
var anchorsY = (rect) => ALL_ANCHORS.map((f) => rect.y + rect.height * f);
|
|
2637
|
+
function nearest(moving, lines, threshold) {
|
|
2638
|
+
let best = Infinity;
|
|
2639
|
+
for (const from of moving) {
|
|
2640
|
+
for (const to of lines) {
|
|
2641
|
+
const delta = to - from;
|
|
2642
|
+
if (Math.abs(delta) <= threshold && Math.abs(delta) < Math.abs(best)) best = delta;
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
return best === Infinity ? 0 : best;
|
|
2646
|
+
}
|
|
2647
|
+
function snapRect(rect, targets, threshold, edges = { x: ALL_ANCHORS, y: ALL_ANCHORS }) {
|
|
2648
|
+
const xs = targets.flatMap(anchorsX);
|
|
2649
|
+
const ys = targets.flatMap(anchorsY);
|
|
2650
|
+
const dx = nearest(
|
|
2651
|
+
edges.x.map((f) => rect.x + rect.width * f),
|
|
2652
|
+
xs,
|
|
2653
|
+
threshold
|
|
2654
|
+
);
|
|
2655
|
+
const dy = nearest(
|
|
2656
|
+
edges.y.map((f) => rect.y + rect.height * f),
|
|
2657
|
+
ys,
|
|
2658
|
+
threshold
|
|
2659
|
+
);
|
|
2660
|
+
const moved = { ...rect, x: rect.x + dx, y: rect.y + dy };
|
|
2661
|
+
return { dx, dy, guides: guidesFor(moved, targets, edges) };
|
|
2662
|
+
}
|
|
2663
|
+
function guidesFor(rect, targets, edges) {
|
|
2664
|
+
const guides = [];
|
|
2665
|
+
const close = (a, b) => Math.abs(a - b) < 0.01;
|
|
2666
|
+
for (const f of edges.x) {
|
|
2667
|
+
const x = rect.x + rect.width * f;
|
|
2668
|
+
const hits = targets.filter((target) => anchorsX(target).some((at) => close(at, x)));
|
|
2669
|
+
if (!hits.length) continue;
|
|
2670
|
+
const top = Math.min(rect.y, ...hits.map((t) => t.y));
|
|
2671
|
+
const bottom2 = Math.max(rect.y + rect.height, ...hits.map((t) => t.y + t.height));
|
|
2672
|
+
guides.push({ x1: x, y1: top, x2: x, y2: bottom2 });
|
|
2673
|
+
}
|
|
2674
|
+
for (const f of edges.y) {
|
|
2675
|
+
const y = rect.y + rect.height * f;
|
|
2676
|
+
const hits = targets.filter((target) => anchorsY(target).some((at) => close(at, y)));
|
|
2677
|
+
if (!hits.length) continue;
|
|
2678
|
+
const left = Math.min(rect.x, ...hits.map((t) => t.x));
|
|
2679
|
+
const right2 = Math.max(rect.x + rect.width, ...hits.map((t) => t.x + t.width));
|
|
2680
|
+
guides.push({ x1: left, y1: y, x2: right2, y2: y });
|
|
2681
|
+
}
|
|
2682
|
+
return guides;
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
// src/editor.ts
|
|
2686
|
+
function createMarquee(canvas, color = DEFAULT_SELECTION_COLOR, select = (world) => canvas.getNodesIn(world)) {
|
|
2687
|
+
const box = document.createElement("div");
|
|
2688
|
+
Object.assign(box.style, {
|
|
2689
|
+
position: "absolute",
|
|
2690
|
+
display: "none",
|
|
2691
|
+
pointerEvents: "none",
|
|
2692
|
+
border: `1px solid ${color}`,
|
|
2693
|
+
background: `color-mix(in srgb, ${color} 12%, transparent)`,
|
|
2694
|
+
zIndex: "1"
|
|
2695
|
+
});
|
|
2696
|
+
canvas.surface.append(box);
|
|
2697
|
+
return {
|
|
2698
|
+
/** Spans the box from `a` to `b` (surface pixels) and selects what it covers. */
|
|
2699
|
+
update(a, b) {
|
|
2700
|
+
const x = Math.min(a.x, b.x);
|
|
2701
|
+
const y = Math.min(a.y, b.y);
|
|
2702
|
+
const width = Math.abs(b.x - a.x);
|
|
2703
|
+
const height = Math.abs(b.y - a.y);
|
|
2704
|
+
Object.assign(box.style, {
|
|
2705
|
+
display: "block",
|
|
2706
|
+
left: `${x}px`,
|
|
2707
|
+
top: `${y}px`,
|
|
2708
|
+
width: `${width}px`,
|
|
2709
|
+
height: `${height}px`
|
|
2710
|
+
});
|
|
2711
|
+
const camera = canvas.getCamera();
|
|
2712
|
+
const hits = select({
|
|
2713
|
+
...screenToWorld(camera, { x, y }),
|
|
2714
|
+
width: width / camera.zoom,
|
|
2715
|
+
height: height / camera.zoom
|
|
2716
|
+
});
|
|
2717
|
+
canvas.setSelection(hits);
|
|
2718
|
+
return hits;
|
|
2719
|
+
},
|
|
2720
|
+
hide() {
|
|
2721
|
+
box.style.display = "none";
|
|
2722
|
+
},
|
|
2723
|
+
remove() {
|
|
2724
|
+
box.remove();
|
|
2725
|
+
}
|
|
2726
|
+
};
|
|
2727
|
+
}
|
|
2728
|
+
var CLICK_SLOP = 4;
|
|
2729
|
+
var SNAP_PX = 6;
|
|
2730
|
+
var MAX_NAMED = 1;
|
|
2731
|
+
var isEditable = (element) => !!element && (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement || element.isContentEditable);
|
|
2732
|
+
function bindEditor(canvas, options = {}) {
|
|
2733
|
+
const {
|
|
2734
|
+
pick = "node",
|
|
2735
|
+
trigger = "shift-click",
|
|
2736
|
+
hover = true,
|
|
2737
|
+
marquee = true,
|
|
2738
|
+
labels = true,
|
|
2739
|
+
snap = true,
|
|
2740
|
+
color = DEFAULT_SELECTION_COLOR,
|
|
2741
|
+
onChange,
|
|
2742
|
+
onTransform
|
|
2743
|
+
} = options;
|
|
2744
|
+
const label = options.label ?? ((layer) => canvas.getNodeOf(layer[0]) === layer[0] ? null : layer[0].localName);
|
|
2745
|
+
const { surface } = canvas;
|
|
2746
|
+
const layers = createLayers(canvas, pick);
|
|
2747
|
+
const transforms = !!onTransform && trigger === "click";
|
|
2748
|
+
const overlay = createOverlay(surface, color, color);
|
|
2749
|
+
let marqueeLayers = [];
|
|
2750
|
+
const box = marquee ? createMarquee(canvas, color, (world) => {
|
|
2751
|
+
marqueeLayers = layers.layersIn(scopes.at(-1) ?? null, world);
|
|
2752
|
+
return marqueeLayers.flat();
|
|
2753
|
+
}) : null;
|
|
2754
|
+
let selection = [];
|
|
2755
|
+
let scopes = [];
|
|
2756
|
+
let hovered = null;
|
|
2757
|
+
let gesture = null;
|
|
2758
|
+
let pointer = null;
|
|
2759
|
+
let shift = false;
|
|
2760
|
+
let swallowClick = false;
|
|
2761
|
+
const commit = (next, nextScopes = scopes) => {
|
|
2762
|
+
selection = next.filter((layer, i) => next.findIndex((other) => sameLayer(layer, other)) === i);
|
|
2763
|
+
scopes = selection.length ? nextScopes : [];
|
|
2764
|
+
canvas.setSelection(selection.flat());
|
|
2765
|
+
onChange?.(selection);
|
|
2766
|
+
draw();
|
|
2767
|
+
};
|
|
2768
|
+
const setHovered = (layer) => {
|
|
2769
|
+
if (sameLayer(layer, hovered) || layer === hovered) return;
|
|
2770
|
+
hovered = layer;
|
|
2771
|
+
canvas.setHover(layer?.[0] ?? null);
|
|
2772
|
+
draw();
|
|
2773
|
+
};
|
|
2774
|
+
const at = (event) => {
|
|
2775
|
+
const bounds = surface.getBoundingClientRect();
|
|
2776
|
+
return { x: event.clientX - bounds.left, y: event.clientY - bounds.top };
|
|
2777
|
+
};
|
|
2778
|
+
const chainAt = (point) => layers.chainAt(canvas.screenToWorld(point));
|
|
2779
|
+
const targetIn = (chain) => {
|
|
2780
|
+
if (!chain.length) return null;
|
|
2781
|
+
for (let i = scopes.length - 1; i >= 0; i--) {
|
|
2782
|
+
const k = chain.findIndex((layer) => sameLayer(layer, scopes[i]));
|
|
2783
|
+
if (k < 0) continue;
|
|
2784
|
+
return k + 1 < chain.length ? { layer: chain[k + 1], scopes: chain.slice(0, k + 1) } : { layer: chain[k], scopes: chain.slice(0, k) };
|
|
2785
|
+
}
|
|
2786
|
+
return { layer: chain[0], scopes: [] };
|
|
2787
|
+
};
|
|
2788
|
+
const selecting = (withShift) => trigger === "click" || withShift;
|
|
2789
|
+
const updateHover = () => {
|
|
2790
|
+
const show = hover && pointer && !gesture && selecting(shift);
|
|
2791
|
+
setHovered(show ? targetIn(chainAt(pointer))?.layer ?? null : null);
|
|
2792
|
+
};
|
|
2793
|
+
let frame = 0;
|
|
2794
|
+
const draw = () => {
|
|
2795
|
+
if (!frame) frame = requestAnimationFrame(render);
|
|
2796
|
+
};
|
|
2797
|
+
const toScreen = (rect) => {
|
|
2798
|
+
const { x, y } = canvas.worldToScreen(rect);
|
|
2799
|
+
const zoom = canvas.getCamera().zoom;
|
|
2800
|
+
return { x, y, width: rect.width * zoom, height: rect.height * zoom };
|
|
2801
|
+
};
|
|
2802
|
+
const unionOf = (rects) => rects.reduce(
|
|
2803
|
+
(union, rect) => !rect ? union : union ? unionRect(union, rect) : rect,
|
|
2804
|
+
null
|
|
2805
|
+
);
|
|
2806
|
+
const render = () => {
|
|
2807
|
+
frame = 0;
|
|
2808
|
+
const transforming = gesture?.kind === "move" || gesture?.kind === "resize" ? gesture : null;
|
|
2809
|
+
const world = transforming ? unionOf(transforming.to) : selection.length ? unionOf(selection.map((layer) => layerRect(canvas, layer))) : null;
|
|
2810
|
+
const hoverRect = hovered && !layerIn(hovered, selection) ? layerRect(canvas, hovered) : null;
|
|
2811
|
+
overlay.render({
|
|
2812
|
+
hover: labels && hoverRect ? { rect: toScreen(hoverRect), name: label(hovered) } : null,
|
|
2813
|
+
selection: labels && world ? {
|
|
2814
|
+
rect: toScreen(world),
|
|
2815
|
+
size: world,
|
|
2816
|
+
name: selection.length <= MAX_NAMED ? label(selection[0]) : null,
|
|
2817
|
+
count: selection.length
|
|
2818
|
+
} : null,
|
|
2819
|
+
bounds: selection.length > 1 || !!transforming,
|
|
2820
|
+
handles: transforms && selection.length > 0 && gesture?.kind !== "marquee",
|
|
2821
|
+
guides: transforming ? transforming.guides.map(
|
|
2822
|
+
(guide) => guideRect(guide, (x, y) => canvas.worldToScreen({ x, y }))
|
|
2823
|
+
) : []
|
|
2824
|
+
});
|
|
2825
|
+
if (selection.length || hovered || gesture) draw();
|
|
2826
|
+
};
|
|
2827
|
+
const emit = (phase) => {
|
|
2828
|
+
if (gesture?.kind !== "move" && gesture?.kind !== "resize") return;
|
|
2829
|
+
onTransform?.({
|
|
2830
|
+
phase,
|
|
2831
|
+
kind: gesture.kind,
|
|
2832
|
+
layers: gesture.layers,
|
|
2833
|
+
from: gesture.from,
|
|
2834
|
+
to: phase === "cancel" ? gesture.from : gesture.to
|
|
2835
|
+
});
|
|
2836
|
+
};
|
|
2837
|
+
const snapTargets = (moving) => {
|
|
2838
|
+
if (!snap) return [];
|
|
2839
|
+
const scope = scopes.at(-1);
|
|
2840
|
+
if (!scope) {
|
|
2841
|
+
const view = visibleWorldRect(canvas.getCamera(), surface.clientWidth, surface.clientHeight);
|
|
2842
|
+
return canvas.getNodesIn(view).filter((node) => !moving.some((layer) => layer[0] === node)).map((node) => canvas.getNodeRect(node));
|
|
2843
|
+
}
|
|
2844
|
+
const rects = layers.childrenOf(scope).filter((layer) => !layerIn(layer, moving)).map((layer) => layerRect(canvas, layer));
|
|
2845
|
+
return [layerRect(canvas, scope), ...rects].filter((rect) => !!rect);
|
|
2846
|
+
};
|
|
2847
|
+
const beginTransform = (start, handle) => {
|
|
2848
|
+
const from = selection.map((layer) => layerRect(canvas, layer));
|
|
2849
|
+
if (from.some((rect) => !rect)) return;
|
|
2850
|
+
const rects = from;
|
|
2851
|
+
gesture = {
|
|
2852
|
+
kind: handle ? "resize" : "move",
|
|
2853
|
+
start,
|
|
2854
|
+
handle,
|
|
2855
|
+
layers: selection,
|
|
2856
|
+
from: rects,
|
|
2857
|
+
bounds: unionOf(rects),
|
|
2858
|
+
to: rects,
|
|
2859
|
+
targets: snapTargets(selection),
|
|
2860
|
+
guides: []
|
|
2861
|
+
};
|
|
2862
|
+
setHovered(null);
|
|
2863
|
+
emit("start");
|
|
2864
|
+
};
|
|
2865
|
+
const updateTransform = (point, keepRatio) => {
|
|
2866
|
+
if (gesture?.kind !== "move" && gesture?.kind !== "resize") return;
|
|
2867
|
+
const zoom = canvas.getCamera().zoom;
|
|
2868
|
+
let dx = (point.x - gesture.start.x) / zoom;
|
|
2869
|
+
let dy = (point.y - gesture.start.y) / zoom;
|
|
2870
|
+
const { bounds, handle } = gesture;
|
|
2871
|
+
const threshold = SNAP_PX / zoom;
|
|
2872
|
+
let next;
|
|
2873
|
+
if (!handle) {
|
|
2874
|
+
if (keepRatio) Math.abs(dx) > Math.abs(dy) ? dy = 0 : dx = 0;
|
|
2875
|
+
next = { ...bounds, x: bounds.x + dx, y: bounds.y + dy };
|
|
2876
|
+
const snapped = snapRect(next, gesture.targets, threshold);
|
|
2877
|
+
next = { ...next, x: next.x + snapped.dx, y: next.y + snapped.dy };
|
|
2878
|
+
gesture.guides = snapped.guides;
|
|
2879
|
+
} else {
|
|
2880
|
+
let left = bounds.x;
|
|
2881
|
+
let top = bounds.y;
|
|
2882
|
+
let right2 = bounds.x + bounds.width;
|
|
2883
|
+
let bottom2 = bounds.y + bounds.height;
|
|
2884
|
+
if (handle.includes("w")) left = Math.min(left + dx, right2 - 1);
|
|
2885
|
+
if (handle.includes("e")) right2 = Math.max(right2 + dx, left + 1);
|
|
2886
|
+
if (handle.includes("n")) top = Math.min(top + dy, bottom2 - 1);
|
|
2887
|
+
if (handle.includes("s")) bottom2 = Math.max(bottom2 + dy, top + 1);
|
|
2888
|
+
next = { x: left, y: top, width: right2 - left, height: bottom2 - top };
|
|
2889
|
+
const corner = handle.length === 2;
|
|
2890
|
+
if (keepRatio && corner) {
|
|
2891
|
+
const scale = Math.max(next.width / bounds.width, next.height / bounds.height);
|
|
2892
|
+
const width = bounds.width * scale;
|
|
2893
|
+
const height = bounds.height * scale;
|
|
2894
|
+
next = {
|
|
2895
|
+
x: handle.includes("w") ? right2 - width : left,
|
|
2896
|
+
y: handle.includes("n") ? bottom2 - height : top,
|
|
2897
|
+
width,
|
|
2898
|
+
height
|
|
2899
|
+
};
|
|
2900
|
+
gesture.guides = [];
|
|
2901
|
+
} else {
|
|
2902
|
+
const edges = {
|
|
2903
|
+
x: handle.includes("w") ? [0] : handle.includes("e") ? [1] : [],
|
|
2904
|
+
y: handle.includes("n") ? [0] : handle.includes("s") ? [1] : []
|
|
2905
|
+
};
|
|
2906
|
+
const snapped = snapRect(next, gesture.targets, threshold, edges);
|
|
2907
|
+
if (edges.x[0] === 0)
|
|
2908
|
+
next = { ...next, x: next.x + snapped.dx, width: next.width - snapped.dx };
|
|
2909
|
+
if (edges.x[0] === 1) next = { ...next, width: next.width + snapped.dx };
|
|
2910
|
+
if (edges.y[0] === 0)
|
|
2911
|
+
next = { ...next, y: next.y + snapped.dy, height: next.height - snapped.dy };
|
|
2912
|
+
if (edges.y[0] === 1) next = { ...next, height: next.height + snapped.dy };
|
|
2913
|
+
gesture.guides = snapped.guides;
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
const sx = next.width / bounds.width;
|
|
2917
|
+
const sy = next.height / bounds.height;
|
|
2918
|
+
gesture.to = gesture.from.map((rect) => ({
|
|
2919
|
+
x: next.x + (rect.x - bounds.x) * sx,
|
|
2920
|
+
y: next.y + (rect.y - bounds.y) * sy,
|
|
2921
|
+
width: rect.width * sx,
|
|
2922
|
+
height: rect.height * sy
|
|
2923
|
+
}));
|
|
2924
|
+
emit("move");
|
|
2925
|
+
draw();
|
|
2926
|
+
};
|
|
2927
|
+
const onPointerDown = (event) => {
|
|
2928
|
+
shift = event.shiftKey;
|
|
2929
|
+
swallowClick = false;
|
|
2930
|
+
if (event.button !== 0) return;
|
|
2931
|
+
const point = at(event);
|
|
2932
|
+
const take = () => {
|
|
2933
|
+
event.stopPropagation();
|
|
2934
|
+
swallowClick = true;
|
|
2935
|
+
surface.setPointerCapture(event.pointerId);
|
|
2936
|
+
};
|
|
2937
|
+
const handle = event.target.closest?.("[data-gpucanvas-handle]");
|
|
2938
|
+
if (handle && transforms) {
|
|
2939
|
+
take();
|
|
2940
|
+
return beginTransform(point, handle.dataset.gpucanvasHandle);
|
|
2941
|
+
}
|
|
2942
|
+
const target = targetIn(chainAt(point));
|
|
2943
|
+
if (!target) {
|
|
2944
|
+
if (event.shiftKey && box) {
|
|
2945
|
+
take();
|
|
2946
|
+
gesture = { kind: "marquee", start: point };
|
|
2947
|
+
return;
|
|
2948
|
+
}
|
|
2949
|
+
if (selection.length) commit([], []);
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
if (!selecting(event.shiftKey)) return;
|
|
2953
|
+
take();
|
|
2954
|
+
const additive = trigger === "click" ? event.shiftKey : event.shiftKey && (event.metaKey || event.ctrlKey);
|
|
2955
|
+
const inside = !!box && sameLayer(target.layer, scopes.at(-1));
|
|
2956
|
+
if (!additive && !inside && !layerIn(target.layer, selection)) {
|
|
2957
|
+
commit([target.layer], target.scopes);
|
|
2958
|
+
}
|
|
2959
|
+
gesture = { kind: "press", start: point, additive, target, inside };
|
|
2960
|
+
setHovered(null);
|
|
2961
|
+
};
|
|
2962
|
+
const onPointerMove = (event) => {
|
|
2963
|
+
shift = event.shiftKey;
|
|
2964
|
+
pointer = at(event);
|
|
2965
|
+
if (!gesture) return updateHover();
|
|
2966
|
+
if (gesture.kind === "press") {
|
|
2967
|
+
if (Math.hypot(pointer.x - gesture.start.x, pointer.y - gesture.start.y) <= CLICK_SLOP)
|
|
2968
|
+
return;
|
|
2969
|
+
if (gesture.inside || trigger !== "click" && box) {
|
|
2970
|
+
gesture = { kind: "marquee", start: gesture.start };
|
|
2971
|
+
} else if (trigger === "click" && transforms) beginTransform(gesture.start, null);
|
|
2972
|
+
else return;
|
|
2973
|
+
}
|
|
2974
|
+
if (gesture?.kind === "marquee") {
|
|
2975
|
+
box.update(gesture.start, pointer);
|
|
2976
|
+
selection = marqueeLayers;
|
|
2977
|
+
onChange?.(selection);
|
|
2978
|
+
draw();
|
|
2979
|
+
} else updateTransform(pointer, event.shiftKey);
|
|
2980
|
+
};
|
|
2981
|
+
const onPointerUp = () => {
|
|
2982
|
+
const done = gesture;
|
|
2983
|
+
gesture = null;
|
|
2984
|
+
if (!done) return;
|
|
2985
|
+
if (done.kind === "marquee") {
|
|
2986
|
+
box.hide();
|
|
2987
|
+
commit(marqueeLayers);
|
|
2988
|
+
} else if (done.kind === "press") {
|
|
2989
|
+
const { layer, scopes: inside } = done.target;
|
|
2990
|
+
if (!done.additive) commit([layer], inside);
|
|
2991
|
+
else if (layerIn(layer, selection))
|
|
2992
|
+
commit(selection.filter((other) => !sameLayer(other, layer)));
|
|
2993
|
+
else commit([...selection, layer], inside);
|
|
2994
|
+
} else {
|
|
2995
|
+
gesture = done;
|
|
2996
|
+
emit("end");
|
|
2997
|
+
gesture = null;
|
|
2998
|
+
}
|
|
2999
|
+
updateHover();
|
|
3000
|
+
draw();
|
|
3001
|
+
};
|
|
3002
|
+
const onDoubleClick = (event) => {
|
|
3003
|
+
if (!selecting(event.shiftKey)) return;
|
|
3004
|
+
event.preventDefault();
|
|
3005
|
+
event.stopPropagation();
|
|
3006
|
+
const chain = chainAt(at(event));
|
|
3007
|
+
let depth = -1;
|
|
3008
|
+
chain.forEach((layer, i) => {
|
|
3009
|
+
if (layerIn(layer, selection)) depth = i;
|
|
3010
|
+
});
|
|
3011
|
+
if (depth >= 0 && chain[depth + 1]) commit([chain[depth + 1]], chain.slice(0, depth + 1));
|
|
3012
|
+
};
|
|
3013
|
+
const onPointerLeave = () => {
|
|
3014
|
+
pointer = null;
|
|
3015
|
+
setHovered(null);
|
|
3016
|
+
};
|
|
3017
|
+
const onMouseDown = (event) => {
|
|
3018
|
+
if (!swallowClick) return;
|
|
3019
|
+
event.preventDefault();
|
|
3020
|
+
event.stopPropagation();
|
|
3021
|
+
};
|
|
3022
|
+
const onClick = (event) => {
|
|
3023
|
+
if (!swallowClick) return;
|
|
3024
|
+
swallowClick = false;
|
|
3025
|
+
event.preventDefault();
|
|
3026
|
+
event.stopPropagation();
|
|
3027
|
+
};
|
|
3028
|
+
const up = () => {
|
|
3029
|
+
const parent = scopes.at(-1);
|
|
3030
|
+
if (parent) commit([parent], scopes.slice(0, -1));
|
|
3031
|
+
};
|
|
3032
|
+
const onKey = (event) => {
|
|
3033
|
+
if (event.key === "Shift") {
|
|
3034
|
+
shift = event.type === "keydown";
|
|
3035
|
+
return updateHover();
|
|
3036
|
+
}
|
|
3037
|
+
if (event.type !== "keydown" || isEditable(document.activeElement)) return;
|
|
3038
|
+
if (event.key === "Escape") {
|
|
3039
|
+
if (gesture?.kind === "move" || gesture?.kind === "resize") {
|
|
3040
|
+
emit("cancel");
|
|
3041
|
+
gesture = null;
|
|
3042
|
+
} else if (scopes.length) up();
|
|
3043
|
+
else if (selection.length) commit([], []);
|
|
3044
|
+
return;
|
|
3045
|
+
}
|
|
3046
|
+
if (!selection.length) return;
|
|
3047
|
+
if (event.key === "Enter") {
|
|
3048
|
+
event.preventDefault();
|
|
3049
|
+
if (event.shiftKey) return up();
|
|
3050
|
+
const first = layers.childrenOf(selection[0])[0];
|
|
3051
|
+
if (first) commit([first], [...scopes, selection[0]]);
|
|
3052
|
+
} else if (event.key === "Tab") {
|
|
3053
|
+
event.preventDefault();
|
|
3054
|
+
const scope = scopes.at(-1);
|
|
3055
|
+
const siblings = scope ? layers.childrenOf(scope) : canvas.getNodesIn({ x: -1e9, y: -1e9, width: 2e9, height: 2e9 }).map((node) => [node]);
|
|
3056
|
+
const index = siblings.findIndex((layer) => sameLayer(layer, selection[0]));
|
|
3057
|
+
const step = event.shiftKey ? -1 : 1;
|
|
3058
|
+
const next = siblings[(index + step + siblings.length) % siblings.length];
|
|
3059
|
+
if (next) commit([next]);
|
|
3060
|
+
}
|
|
3061
|
+
};
|
|
3062
|
+
surface.addEventListener("pointerdown", onPointerDown, true);
|
|
3063
|
+
surface.addEventListener("mousedown", onMouseDown, true);
|
|
3064
|
+
surface.addEventListener("click", onClick, true);
|
|
3065
|
+
surface.addEventListener("dblclick", onDoubleClick, true);
|
|
3066
|
+
surface.addEventListener("pointermove", onPointerMove);
|
|
3067
|
+
surface.addEventListener("pointerup", onPointerUp);
|
|
3068
|
+
surface.addEventListener("pointercancel", onPointerUp);
|
|
3069
|
+
surface.addEventListener("pointerleave", onPointerLeave);
|
|
3070
|
+
window.addEventListener("keydown", onKey);
|
|
3071
|
+
window.addEventListener("keyup", onKey);
|
|
3072
|
+
return {
|
|
3073
|
+
get: () => selection,
|
|
3074
|
+
set: (next) => commit(
|
|
3075
|
+
[...next].map((layer) => [layer].flat()),
|
|
3076
|
+
[]
|
|
3077
|
+
),
|
|
3078
|
+
clear: () => commit([], []),
|
|
3079
|
+
destroy() {
|
|
3080
|
+
surface.removeEventListener("pointerdown", onPointerDown, true);
|
|
3081
|
+
surface.removeEventListener("mousedown", onMouseDown, true);
|
|
3082
|
+
surface.removeEventListener("click", onClick, true);
|
|
3083
|
+
surface.removeEventListener("dblclick", onDoubleClick, true);
|
|
3084
|
+
surface.removeEventListener("pointermove", onPointerMove);
|
|
3085
|
+
surface.removeEventListener("pointerup", onPointerUp);
|
|
3086
|
+
surface.removeEventListener("pointercancel", onPointerUp);
|
|
3087
|
+
surface.removeEventListener("pointerleave", onPointerLeave);
|
|
3088
|
+
window.removeEventListener("keydown", onKey);
|
|
3089
|
+
window.removeEventListener("keyup", onKey);
|
|
3090
|
+
cancelAnimationFrame(frame);
|
|
3091
|
+
box?.remove();
|
|
3092
|
+
overlay.remove();
|
|
3093
|
+
canvas.setHover(null);
|
|
3094
|
+
canvas.setSelection([]);
|
|
3095
|
+
}
|
|
3096
|
+
};
|
|
3097
|
+
}
|
|
3098
|
+
|
|
1695
3099
|
// src/index.ts
|
|
1696
3100
|
async function createGpuCanvas2(host, options = {}) {
|
|
1697
3101
|
const mode = options.mode ?? "auto";
|
|
@@ -1707,9 +3111,14 @@ async function createGpuCanvas2(host, options = {}) {
|
|
|
1707
3111
|
}
|
|
1708
3112
|
|
|
1709
3113
|
export {
|
|
3114
|
+
screenToWorld,
|
|
3115
|
+
worldToScreen,
|
|
1710
3116
|
zoomAt,
|
|
1711
3117
|
fitCamera,
|
|
3118
|
+
defineEffect,
|
|
3119
|
+
EFFECTS,
|
|
1712
3120
|
supportsGpuCanvas,
|
|
3121
|
+
bindEditor,
|
|
1713
3122
|
createGpuCanvas2 as createGpuCanvas
|
|
1714
3123
|
};
|
|
1715
|
-
//# sourceMappingURL=chunk-
|
|
3124
|
+
//# sourceMappingURL=chunk-RBXGH44N.js.map
|