gpucanvas 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +181 -31
- package/dist/{chunk-JQQ44OTN.js → chunk-RBXGH44N.js} +1535 -62
- 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-JQQ44OTN.js.map +0 -1
- package/dist/types-BVYtrpUQ.d.ts +0 -95
|
@@ -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(
|
|
@@ -48,7 +57,7 @@ function bindCameraInput(element, getCamera, onChange) {
|
|
|
48
57
|
};
|
|
49
58
|
let drag = null;
|
|
50
59
|
const onPointerDown = (event) => {
|
|
51
|
-
if (event.button !== 0 && event.button !== 1) return;
|
|
60
|
+
if (event.button !== 0 && event.button !== 1 || event.shiftKey) return;
|
|
52
61
|
if (event.target !== element && !event.target.hasAttribute?.("data-gpucanvas-surface"))
|
|
53
62
|
return;
|
|
54
63
|
drag = { pointerId: event.pointerId, x: event.clientX, y: event.clientY };
|
|
@@ -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`;
|
|
@@ -105,6 +500,9 @@ function boundsOf(rects) {
|
|
|
105
500
|
|
|
106
501
|
// src/types.ts
|
|
107
502
|
var DEFAULT_CAMERA = { x: 0, y: 0, zoom: 1 };
|
|
503
|
+
var DEFAULT_SELECTION_COLOR = "#4f8cff";
|
|
504
|
+
var SELECTION_WIDTH = 2;
|
|
505
|
+
var HOVER_WIDTH = 1;
|
|
108
506
|
|
|
109
507
|
// src/dom-backend.ts
|
|
110
508
|
function createDomCanvas(host, options) {
|
|
@@ -127,14 +525,102 @@ function createDomCanvas(host, options) {
|
|
|
127
525
|
transformOrigin: "0 0",
|
|
128
526
|
willChange: "transform"
|
|
129
527
|
});
|
|
528
|
+
const overlay = document.createElement("div");
|
|
529
|
+
Object.assign(overlay.style, { position: "absolute", left: "0", top: "0", zIndex: "1" });
|
|
530
|
+
world.append(overlay);
|
|
130
531
|
surface.append(world);
|
|
131
532
|
host.append(surface);
|
|
132
533
|
const nodes = /* @__PURE__ */ new Map();
|
|
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
|
+
};
|
|
133
618
|
let camera = options.camera ?? DEFAULT_CAMERA;
|
|
134
619
|
let frameMs = 0;
|
|
135
620
|
const apply = () => {
|
|
136
621
|
const start = performance.now();
|
|
137
622
|
world.style.transform = `scale(${camera.zoom}) translate(${-camera.x}px, ${-camera.y}px)`;
|
|
623
|
+
world.style.setProperty("--gpucanvas-zoom", String(camera.zoom));
|
|
138
624
|
frameMs = performance.now() - start;
|
|
139
625
|
};
|
|
140
626
|
const setCamera = (next) => {
|
|
@@ -161,12 +647,39 @@ function createDomCanvas(host, options) {
|
|
|
161
647
|
applyNodeSize(element, rect);
|
|
162
648
|
},
|
|
163
649
|
removeNode(element) {
|
|
650
|
+
clearEffect(element);
|
|
164
651
|
nodes.delete(element);
|
|
652
|
+
outlinedNodes.delete(element);
|
|
653
|
+
element.style.outline = "";
|
|
165
654
|
element.remove();
|
|
166
655
|
},
|
|
167
656
|
getNodeRect: (element) => nodes.get(element) ?? null,
|
|
657
|
+
getNodesIn(rect) {
|
|
658
|
+
return [...nodes].filter(([, node]) => intersects(node, rect)).map(([element]) => element);
|
|
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
|
+
},
|
|
667
|
+
setSelection(elements) {
|
|
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);
|
|
678
|
+
},
|
|
168
679
|
getCamera: () => camera,
|
|
169
680
|
setCamera,
|
|
681
|
+
screenToWorld: (point) => screenToWorld(camera, point),
|
|
682
|
+
worldToScreen: (point) => worldToScreen(camera, point),
|
|
170
683
|
// The browser repaints DOM nodes itself.
|
|
171
684
|
invalidate() {
|
|
172
685
|
},
|
|
@@ -193,6 +706,7 @@ function createDomCanvas(host, options) {
|
|
|
193
706
|
};
|
|
194
707
|
},
|
|
195
708
|
destroy() {
|
|
709
|
+
cancelAnimationFrame(boxFrame);
|
|
196
710
|
unbindInput();
|
|
197
711
|
surface.remove();
|
|
198
712
|
}
|
|
@@ -350,7 +864,7 @@ var Atlas = class {
|
|
|
350
864
|
};
|
|
351
865
|
|
|
352
866
|
// src/composite-shader.ts
|
|
353
|
-
var
|
|
867
|
+
var prelude = (
|
|
354
868
|
/* wgsl */
|
|
355
869
|
`
|
|
356
870
|
struct View {
|
|
@@ -359,8 +873,10 @@ struct View {
|
|
|
359
873
|
zoom: f32,
|
|
360
874
|
dpr: f32,
|
|
361
875
|
atlasSize: f32,
|
|
362
|
-
|
|
876
|
+
time: f32, // seconds since the canvas started
|
|
363
877
|
placeholder: vec4f, // premultiplied color for nodes without a snapshot
|
|
878
|
+
pointer: vec4f, // pointer and last press in world units; far away when there is none
|
|
879
|
+
pressTime: f32, // seconds since the last press
|
|
364
880
|
}
|
|
365
881
|
|
|
366
882
|
@group(0) @binding(0) var<uniform> view: View;
|
|
@@ -369,8 +885,9 @@ struct View {
|
|
|
369
885
|
|
|
370
886
|
struct Instance {
|
|
371
887
|
@location(0) rect: vec4f, // world x, y, width, height
|
|
372
|
-
@location(1) uv: vec4f, // atlas texels x0, y0, x1, y1
|
|
373
|
-
@location(2) info: vec4f, // layer, placeholder
|
|
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
|
|
374
891
|
}
|
|
375
892
|
|
|
376
893
|
struct Varyings {
|
|
@@ -378,31 +895,63 @@ struct Varyings {
|
|
|
378
895
|
@location(0) texel: vec2f,
|
|
379
896
|
@location(1) @interpolate(flat) bounds: vec4f,
|
|
380
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
|
|
381
903
|
}
|
|
382
904
|
|
|
383
905
|
@vertex
|
|
384
906
|
fn vs(@builtin(vertex_index) vertex: u32, instance: Instance) -> Varyings {
|
|
385
907
|
let corner = vec2f(f32(vertex & 1u), f32(vertex >> 1u));
|
|
908
|
+
// A ring's quad reaches past its element by the ring's width, an effect's by its padding.
|
|
909
|
+
let ring = instance.info.y > 1.5;
|
|
910
|
+
let grow = select(instance.info.z, instance.info.z / view.zoom, ring);
|
|
911
|
+
let start = instance.rect.xy - grow;
|
|
912
|
+
let end = instance.rect.xy + instance.rect.zw + grow;
|
|
386
913
|
// Snap edges to device pixels so a snapshot drawn at its raster scale stays crisp.
|
|
387
914
|
let scale = view.zoom * view.dpr;
|
|
388
|
-
let topLeft = round((
|
|
389
|
-
let bottomRight = round((
|
|
915
|
+
let topLeft = round((start - view.origin) * scale) / view.dpr;
|
|
916
|
+
let bottomRight = round((end - view.origin) * scale) / view.dpr;
|
|
390
917
|
let point = mix(topLeft, bottomRight, corner);
|
|
391
918
|
var out: Varyings;
|
|
392
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);
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
920
|
+
if (ring) {
|
|
921
|
+
// World units from the element's top left, and the element's own edges.
|
|
922
|
+
out.texel = mix(vec2f(-grow), instance.rect.zw + grow, corner);
|
|
923
|
+
out.bounds = vec4f(0.0, 0.0, instance.rect.zw);
|
|
924
|
+
out.color = instance.uv;
|
|
925
|
+
} else {
|
|
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;
|
|
929
|
+
// Half a texel inside the node's rectangle: the outermost sample center that cannot blend
|
|
930
|
+
// with whatever sits next to it in the atlas.
|
|
931
|
+
out.bounds = vec4f(instance.uv.xy + 0.5, instance.uv.zw - 0.5);
|
|
932
|
+
}
|
|
397
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;
|
|
398
938
|
return out;
|
|
399
939
|
}
|
|
400
|
-
|
|
940
|
+
`
|
|
941
|
+
);
|
|
942
|
+
var compositeShader = (
|
|
943
|
+
/* wgsl */
|
|
944
|
+
`${prelude}
|
|
401
945
|
@fragment
|
|
402
946
|
fn fs(in: Varyings) -> @location(0) vec4f {
|
|
403
947
|
// Derivatives first: WGSL only allows them in uniform control flow.
|
|
404
948
|
let dx = dpdx(in.texel);
|
|
405
949
|
let dy = dpdy(in.texel);
|
|
950
|
+
if (in.info.y > 1.5) {
|
|
951
|
+
// The ring covers what lies outside the element; the element itself shows through.
|
|
952
|
+
let outside = max(in.bounds.xy - in.texel, in.texel - in.bounds.zw);
|
|
953
|
+
return select(vec4f(0.0), in.color, max(outside.x, outside.y) > 0.0);
|
|
954
|
+
}
|
|
406
955
|
if (in.info.y > 0.5) {
|
|
407
956
|
return view.placeholder;
|
|
408
957
|
}
|
|
@@ -422,9 +971,63 @@ fn fs(in: Varyings) -> @location(0) vec4f {
|
|
|
422
971
|
}
|
|
423
972
|
`
|
|
424
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
|
+
);
|
|
425
1028
|
|
|
426
1029
|
// src/gpu-renderer.ts
|
|
427
|
-
var FLOATS_PER_INSTANCE =
|
|
1030
|
+
var FLOATS_PER_INSTANCE = 16;
|
|
428
1031
|
var CompositeRenderer = class {
|
|
429
1032
|
constructor(device, context, format, atlas, background, placeholder) {
|
|
430
1033
|
this.device = device;
|
|
@@ -432,13 +1035,60 @@ var CompositeRenderer = class {
|
|
|
432
1035
|
this.atlas = atlas;
|
|
433
1036
|
this.background = background;
|
|
434
1037
|
this.placeholder = placeholder;
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
+
]
|
|
438
1048
|
});
|
|
439
|
-
this.
|
|
440
|
-
|
|
441
|
-
|
|
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
|
|
1057
|
+
});
|
|
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] }),
|
|
442
1092
|
vertex: {
|
|
443
1093
|
module,
|
|
444
1094
|
entryPoint: "vs",
|
|
@@ -449,7 +1099,8 @@ var CompositeRenderer = class {
|
|
|
449
1099
|
attributes: [
|
|
450
1100
|
{ shaderLocation: 0, offset: 0, format: "float32x4" },
|
|
451
1101
|
{ shaderLocation: 1, offset: 16, format: "float32x4" },
|
|
452
|
-
{ shaderLocation: 2, offset: 32, format: "float32x4" }
|
|
1102
|
+
{ shaderLocation: 2, offset: 32, format: "float32x4" },
|
|
1103
|
+
{ shaderLocation: 3, offset: 48, format: "float32x4" }
|
|
453
1104
|
]
|
|
454
1105
|
}
|
|
455
1106
|
]
|
|
@@ -469,35 +1120,24 @@ var CompositeRenderer = class {
|
|
|
469
1120
|
]
|
|
470
1121
|
},
|
|
471
1122
|
primitive: { topology: "triangle-strip" }
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
this.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
});
|
|
488
|
-
|
|
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;
|
|
489
1140
|
}
|
|
490
|
-
device;
|
|
491
|
-
context;
|
|
492
|
-
atlas;
|
|
493
|
-
background;
|
|
494
|
-
placeholder;
|
|
495
|
-
instances = new Float32Array(FLOATS_PER_INSTANCE * 4096);
|
|
496
|
-
instanceBuffer;
|
|
497
|
-
uniforms;
|
|
498
|
-
uniformData = new Float32Array(12);
|
|
499
|
-
pipeline;
|
|
500
|
-
bindGroup;
|
|
501
1141
|
/** Grows the CPU and GPU instance storage to hold at least `count` instances. */
|
|
502
1142
|
reserve(count) {
|
|
503
1143
|
const floats = count * FLOATS_PER_INSTANCE;
|
|
@@ -506,7 +1146,7 @@ var CompositeRenderer = class {
|
|
|
506
1146
|
this.instanceBuffer.destroy();
|
|
507
1147
|
this.instanceBuffer = this.createInstanceBuffer();
|
|
508
1148
|
}
|
|
509
|
-
draw(camera, cssWidth, cssHeight, dpr, count) {
|
|
1149
|
+
draw(camera, cssWidth, cssHeight, dpr, count, runs, effects) {
|
|
510
1150
|
const { device } = this;
|
|
511
1151
|
this.uniformData.set([
|
|
512
1152
|
cssWidth,
|
|
@@ -516,8 +1156,13 @@ var CompositeRenderer = class {
|
|
|
516
1156
|
camera.zoom,
|
|
517
1157
|
dpr,
|
|
518
1158
|
this.atlas.size,
|
|
519
|
-
|
|
520
|
-
...this.placeholder
|
|
1159
|
+
effects.time,
|
|
1160
|
+
...this.placeholder,
|
|
1161
|
+
effects.pointer.x,
|
|
1162
|
+
effects.pointer.y,
|
|
1163
|
+
effects.press.x,
|
|
1164
|
+
effects.press.y,
|
|
1165
|
+
effects.sincePress
|
|
521
1166
|
]);
|
|
522
1167
|
device.queue.writeBuffer(this.uniforms, 0, this.uniformData);
|
|
523
1168
|
if (count > 0) {
|
|
@@ -542,10 +1187,14 @@ var CompositeRenderer = class {
|
|
|
542
1187
|
]
|
|
543
1188
|
});
|
|
544
1189
|
if (count > 0) {
|
|
545
|
-
pass.setPipeline(this.pipeline);
|
|
546
1190
|
pass.setBindGroup(0, this.bindGroup);
|
|
547
1191
|
pass.setVertexBuffer(0, this.instanceBuffer);
|
|
548
|
-
|
|
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
|
+
}
|
|
549
1198
|
}
|
|
550
1199
|
pass.end();
|
|
551
1200
|
device.queue.submit([encoder.finish()]);
|
|
@@ -685,6 +1334,7 @@ function cssColorToRgba(color) {
|
|
|
685
1334
|
const [r, g, b, a] = context.getImageData(0, 0, 1, 1).data;
|
|
686
1335
|
return [r / 255, g / 255, b / 255, a / 255];
|
|
687
1336
|
}
|
|
1337
|
+
var premultiplied = ([r, g, b, a]) => [r * a, g * a, b * a, a];
|
|
688
1338
|
var distanceToCenter = (rect, view) => Math.hypot(
|
|
689
1339
|
rect.x + rect.width / 2 - (view.x + view.width / 2),
|
|
690
1340
|
rect.y + rect.height / 2 - (view.y + view.height / 2)
|
|
@@ -737,7 +1387,30 @@ async function createGpuCanvas(host, options) {
|
|
|
737
1387
|
atlas,
|
|
738
1388
|
...colorsFor(options.background ?? "#f4f4f5")
|
|
739
1389
|
);
|
|
1390
|
+
const selectionColor = premultiplied(
|
|
1391
|
+
cssColorToRgba(options.selectionColor ?? DEFAULT_SELECTION_COLOR)
|
|
1392
|
+
);
|
|
1393
|
+
const hoverColor = options.hoverColor ? premultiplied(cssColorToRgba(options.hoverColor)) : selectionColor;
|
|
740
1394
|
const nodes = /* @__PURE__ */ new Map();
|
|
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
|
+
};
|
|
741
1414
|
const pages = /* @__PURE__ */ new Map();
|
|
742
1415
|
atlas.keep = function* () {
|
|
743
1416
|
const onScreen = (tile) => intersects(tile.rect, frameView);
|
|
@@ -1134,10 +1807,21 @@ async function createGpuCanvas(host, options) {
|
|
|
1134
1807
|
const composite = () => {
|
|
1135
1808
|
let instances = visible.length;
|
|
1136
1809
|
for (const node of visible) instances += node.tiles.length + (node.preview ? 1 : 0);
|
|
1810
|
+
instances += selection.size + 1;
|
|
1137
1811
|
renderer.reserve(instances);
|
|
1138
1812
|
const data = renderer.instances;
|
|
1139
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
|
+
};
|
|
1140
1822
|
const push = (rect, slot) => {
|
|
1823
|
+
startRun(null);
|
|
1824
|
+
runs[runs.length - 1].count++;
|
|
1141
1825
|
const offset = count++ * FLOATS_PER_INSTANCE;
|
|
1142
1826
|
data[offset] = rect.x;
|
|
1143
1827
|
data[offset + 1] = rect.y;
|
|
@@ -1154,6 +1838,40 @@ async function createGpuCanvas(host, options) {
|
|
|
1154
1838
|
} else {
|
|
1155
1839
|
data[offset + 9] = 1;
|
|
1156
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);
|
|
1861
|
+
};
|
|
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++;
|
|
1867
|
+
const offset = count++ * FLOATS_PER_INSTANCE;
|
|
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);
|
|
1873
|
+
data[offset + 9] = 2;
|
|
1874
|
+
data[offset + 10] = width;
|
|
1157
1875
|
};
|
|
1158
1876
|
let onThumbnail = 0;
|
|
1159
1877
|
for (const node of visible) {
|
|
@@ -1167,6 +1885,15 @@ async function createGpuCanvas(host, options) {
|
|
|
1167
1885
|
if (before === 3 && shown === 2) stats.lostDetail++;
|
|
1168
1886
|
node.shown = shown;
|
|
1169
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
|
+
}
|
|
1170
1897
|
push(node.rect, base);
|
|
1171
1898
|
const current = (tile) => tile.scale === detailScale && !tile.stale;
|
|
1172
1899
|
for (const tile of node.tiles)
|
|
@@ -1176,9 +1903,17 @@ async function createGpuCanvas(host, options) {
|
|
|
1176
1903
|
if (current(tile) && atlas.isValid(tile.slot)) push(tile.rect, tile.slot);
|
|
1177
1904
|
}
|
|
1178
1905
|
stats.onThumbnail = onThumbnail;
|
|
1906
|
+
for (const element of selection) pushRing(element, selectionColor, SELECTION_WIDTH);
|
|
1907
|
+
if (hovered && !selection.has(hovered)) pushRing(hovered, hoverColor, HOVER_WIDTH);
|
|
1179
1908
|
const cssWidth = canvas.clientWidth;
|
|
1180
1909
|
const dpr = canvas.width / Math.max(1, cssWidth);
|
|
1181
|
-
|
|
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
|
+
});
|
|
1182
1917
|
};
|
|
1183
1918
|
const frame = (inPaint) => {
|
|
1184
1919
|
if (destroyed) return;
|
|
@@ -1222,7 +1957,7 @@ async function createGpuCanvas(host, options) {
|
|
|
1222
1957
|
stats.visible = visible.length;
|
|
1223
1958
|
stats.pending = pending;
|
|
1224
1959
|
stats.frameMs = performance.now() - start;
|
|
1225
|
-
if (pending > 0) schedule();
|
|
1960
|
+
if (pending > 0 || effectsMoving) schedule();
|
|
1226
1961
|
};
|
|
1227
1962
|
let awaitingPaint = false;
|
|
1228
1963
|
canvas.onpaint = (event) => {
|
|
@@ -1237,6 +1972,7 @@ async function createGpuCanvas(host, options) {
|
|
|
1237
1972
|
}
|
|
1238
1973
|
}
|
|
1239
1974
|
}
|
|
1975
|
+
if (changed) ringRects.clear();
|
|
1240
1976
|
if (!awaitingPaint && !changed) return;
|
|
1241
1977
|
awaitingPaint = false;
|
|
1242
1978
|
frame(true);
|
|
@@ -1250,6 +1986,7 @@ async function createGpuCanvas(host, options) {
|
|
|
1250
1986
|
return element ? nodes.get(element) : void 0;
|
|
1251
1987
|
};
|
|
1252
1988
|
const mutations = new MutationObserver((records) => {
|
|
1989
|
+
ringRects.clear();
|
|
1253
1990
|
let changed = false;
|
|
1254
1991
|
for (const record of records) {
|
|
1255
1992
|
const target = record.target;
|
|
@@ -1332,13 +2069,9 @@ async function createGpuCanvas(host, options) {
|
|
|
1332
2069
|
};
|
|
1333
2070
|
const pickLive = () => {
|
|
1334
2071
|
if (!pointer) return setLive(null);
|
|
1335
|
-
const
|
|
1336
|
-
const worldY = camera.y + pointer.y / camera.zoom;
|
|
2072
|
+
const point = screenToWorld(camera, pointer);
|
|
1337
2073
|
let hit = null;
|
|
1338
|
-
for (const node of nodes.values())
|
|
1339
|
-
const { x, y, width, height } = node.rect;
|
|
1340
|
-
if (worldX >= x && worldX < x + width && worldY >= y && worldY < y + height) hit = node;
|
|
1341
|
-
}
|
|
2074
|
+
for (const node of nodes.values()) if (containsPoint(node.rect, point)) hit = node;
|
|
1342
2075
|
setLive(hit);
|
|
1343
2076
|
};
|
|
1344
2077
|
const onPointerMove = (event) => {
|
|
@@ -1350,6 +2083,17 @@ async function createGpuCanvas(host, options) {
|
|
|
1350
2083
|
pointer = null;
|
|
1351
2084
|
setLive(null);
|
|
1352
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
|
+
};
|
|
1353
2097
|
const onFocusIn = (event) => {
|
|
1354
2098
|
const node = nodeOf(event.target) ?? null;
|
|
1355
2099
|
if (node === focused) return;
|
|
@@ -1388,6 +2132,7 @@ async function createGpuCanvas(host, options) {
|
|
|
1388
2132
|
document.fonts.addEventListener("loadingdone", onFontsLoaded);
|
|
1389
2133
|
surface.addEventListener("pointermove", onPointerMove);
|
|
1390
2134
|
surface.addEventListener("pointerleave", onPointerLeave);
|
|
2135
|
+
surface.addEventListener("pointerdown", onPointerDown, true);
|
|
1391
2136
|
canvas.addEventListener("focusin", onFocusIn);
|
|
1392
2137
|
canvas.addEventListener("focusout", onFocusOut);
|
|
1393
2138
|
const usedAt = /* @__PURE__ */ new WeakMap();
|
|
@@ -1549,6 +2294,7 @@ async function createGpuCanvas(host, options) {
|
|
|
1549
2294
|
if (!node) return;
|
|
1550
2295
|
const resized = node.rect.width !== rect.width || node.rect.height !== rect.height;
|
|
1551
2296
|
node.rect = rect;
|
|
2297
|
+
ringRects.clear();
|
|
1552
2298
|
if (resized) {
|
|
1553
2299
|
if (live === node) setLive(null);
|
|
1554
2300
|
releaseNode(node);
|
|
@@ -1563,6 +2309,8 @@ async function createGpuCanvas(host, options) {
|
|
|
1563
2309
|
if (lingering === node) lingering = null;
|
|
1564
2310
|
if (focused === node) focused = null;
|
|
1565
2311
|
transitioning.delete(node);
|
|
2312
|
+
effects.delete(element);
|
|
2313
|
+
selection.delete(element);
|
|
1566
2314
|
nodes.delete(element);
|
|
1567
2315
|
releaseNode(node);
|
|
1568
2316
|
element.remove();
|
|
@@ -1572,6 +2320,42 @@ async function createGpuCanvas(host, options) {
|
|
|
1572
2320
|
const node = nodes.get(element);
|
|
1573
2321
|
return node ? { ...node.rect } : null;
|
|
1574
2322
|
},
|
|
2323
|
+
getNodesIn(rect) {
|
|
2324
|
+
const hits = [];
|
|
2325
|
+
for (const node of nodes.values()) if (intersects(node.rect, rect)) hits.push(node.element);
|
|
2326
|
+
return hits;
|
|
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
|
+
},
|
|
2338
|
+
setSelection(elements) {
|
|
2339
|
+
selection = new Set(elements);
|
|
2340
|
+
ringRects.clear();
|
|
2341
|
+
schedule();
|
|
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),
|
|
1575
2359
|
getCamera: () => camera,
|
|
1576
2360
|
setCamera,
|
|
1577
2361
|
invalidate: onFontsLoaded,
|
|
@@ -1613,6 +2397,7 @@ async function createGpuCanvas(host, options) {
|
|
|
1613
2397
|
canvas.removeEventListener("load", onResourceLoad, true);
|
|
1614
2398
|
surface.removeEventListener("pointermove", onPointerMove);
|
|
1615
2399
|
surface.removeEventListener("pointerleave", onPointerLeave);
|
|
2400
|
+
surface.removeEventListener("pointerdown", onPointerDown, true);
|
|
1616
2401
|
canvas.removeEventListener("focusin", onFocusIn);
|
|
1617
2402
|
canvas.removeEventListener("focusout", onFocusOut);
|
|
1618
2403
|
canvas.removeEventListener("transitionrun", onTransitionRun);
|
|
@@ -1628,6 +2413,689 @@ async function createGpuCanvas(host, options) {
|
|
|
1628
2413
|
};
|
|
1629
2414
|
}
|
|
1630
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
|
+
|
|
1631
3099
|
// src/index.ts
|
|
1632
3100
|
async function createGpuCanvas2(host, options = {}) {
|
|
1633
3101
|
const mode = options.mode ?? "auto";
|
|
@@ -1643,9 +3111,14 @@ async function createGpuCanvas2(host, options = {}) {
|
|
|
1643
3111
|
}
|
|
1644
3112
|
|
|
1645
3113
|
export {
|
|
3114
|
+
screenToWorld,
|
|
3115
|
+
worldToScreen,
|
|
1646
3116
|
zoomAt,
|
|
1647
3117
|
fitCamera,
|
|
3118
|
+
defineEffect,
|
|
3119
|
+
EFFECTS,
|
|
1648
3120
|
supportsGpuCanvas,
|
|
3121
|
+
bindEditor,
|
|
1649
3122
|
createGpuCanvas2 as createGpuCanvas
|
|
1650
3123
|
};
|
|
1651
|
-
//# sourceMappingURL=chunk-
|
|
3124
|
+
//# sourceMappingURL=chunk-RBXGH44N.js.map
|