@graview/render 0.0.0-stage → 0.1.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.
Files changed (47) hide show
  1. package/LICENSE +96 -0
  2. package/README.md +43 -2
  3. package/dist/gpu.d.ts +6 -0
  4. package/dist/gpu.d.ts.map +1 -0
  5. package/dist/gpu.js +23 -0
  6. package/dist/gpu.js.map +1 -0
  7. package/dist/index.d.ts +12 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +31 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/interaction/pointer-router.d.ts +94 -0
  12. package/dist/interaction/pointer-router.d.ts.map +1 -0
  13. package/dist/interaction/pointer-router.js +148 -0
  14. package/dist/interaction/pointer-router.js.map +1 -0
  15. package/dist/platform/html-in-canvas.d.ts +100 -0
  16. package/dist/platform/html-in-canvas.d.ts.map +1 -0
  17. package/dist/platform/html-in-canvas.js +172 -0
  18. package/dist/platform/html-in-canvas.js.map +1 -0
  19. package/dist/platform/matrix.d.ts +53 -0
  20. package/dist/platform/matrix.d.ts.map +1 -0
  21. package/dist/platform/matrix.js +61 -0
  22. package/dist/platform/matrix.js.map +1 -0
  23. package/dist/scene/compositor.d.ts +112 -0
  24. package/dist/scene/compositor.d.ts.map +1 -0
  25. package/dist/scene/compositor.js +340 -0
  26. package/dist/scene/compositor.js.map +1 -0
  27. package/dist/scene/compositor.wgsl.d.ts +15 -0
  28. package/dist/scene/compositor.wgsl.d.ts.map +1 -0
  29. package/dist/scene/compositor.wgsl.js +106 -0
  30. package/dist/scene/compositor.wgsl.js.map +1 -0
  31. package/dist/scene/connectors.d.ts +60 -0
  32. package/dist/scene/connectors.d.ts.map +1 -0
  33. package/dist/scene/connectors.js +95 -0
  34. package/dist/scene/connectors.js.map +1 -0
  35. package/dist/scene/frame-plan.d.ts +83 -0
  36. package/dist/scene/frame-plan.d.ts.map +1 -0
  37. package/dist/scene/frame-plan.js +119 -0
  38. package/dist/scene/frame-plan.js.map +1 -0
  39. package/dist/scene/from-layout.d.ts +14 -0
  40. package/dist/scene/from-layout.d.ts.map +1 -0
  41. package/dist/scene/from-layout.js +35 -0
  42. package/dist/scene/from-layout.js.map +1 -0
  43. package/dist/scene/plane.d.ts +68 -0
  44. package/dist/scene/plane.d.ts.map +1 -0
  45. package/dist/scene/plane.js +94 -0
  46. package/dist/scene/plane.js.map +1 -0
  47. package/package.json +64 -3
@@ -0,0 +1,340 @@
1
+ /// <reference types="@webgpu/types" />
2
+ //
3
+ // The WebGPU globals are part of this file's PUBLIC declarations, so the
4
+ // reference has to live HERE rather than on the package entry: TypeScript
5
+ // drops a type reference from a `.d.ts` that does not itself name one of its
6
+ // types, so a reference on `index.ts` was emitted nowhere and an installed
7
+ // consumer got "cannot find name GPUDevice" from a file they cannot change.
8
+ // Found by typechecking a scratch project against the packed tarballs.
9
+ //
10
+ // It also has to be the ONLY route in: listing `@webgpu/types` under
11
+ // `compilerOptions.types` makes the globals ambient for this build, at which
12
+ // point TypeScript considers the reference satisfied and drops it from the
13
+ // emit. The build sees no difference; a stranger does.
14
+ import { PointerRouter } from "../interaction/pointer-router.js";
15
+ import { captureElement, detectCapabilities, requestPaint, setLayoutSubtree, toDOMMatrix, updateElementGeometry, } from "../platform/html-in-canvas.js";
16
+ import { COMPOSITOR_WGSL } from "./compositor.wgsl.js";
17
+ import { planFrame, } from "./frame-plan.js";
18
+ const UNIFORM_BYTES = 64; // 4 x vec4f
19
+ /**
20
+ * Captures views into GPU textures and composites them as quads at plane
21
+ * depth, then reports each drawn position back to the browser.
22
+ *
23
+ * The frame is decided by `planFrame` — which fidelity captures, what order
24
+ * things draw in, where each view lands — and this class only submits it.
25
+ * Keeping the decisions out of here is what lets them be tested in CI with
26
+ * no GPU and no DOM.
27
+ */
28
+ export class Compositor {
29
+ canvas;
30
+ deps;
31
+ options;
32
+ views = new Map();
33
+ resources = new Map();
34
+ capturedAt = {};
35
+ router;
36
+ pipeline = null;
37
+ sampler = null;
38
+ frame = 0;
39
+ detachRouter = null;
40
+ connectors = [];
41
+ lastPlan = null;
42
+ lastPlacements = [];
43
+ capabilities;
44
+ constructor(canvas, deps, options = {}) {
45
+ this.canvas = canvas;
46
+ this.deps = deps;
47
+ this.options = options;
48
+ setLayoutSubtree(canvas, true);
49
+ this.capabilities = detectCapabilities(deps.gpu.device.gpu.queue, canvas);
50
+ this.router = new PointerRouter(canvas, {
51
+ ...(options.platformHandlesHitTesting === undefined
52
+ ? {}
53
+ : { platformHandlesHitTesting: options.platformHandlesHitTesting }),
54
+ });
55
+ }
56
+ attach() {
57
+ this.detachRouter = this.router.attach();
58
+ return () => this.detach();
59
+ }
60
+ detach() {
61
+ this.detachRouter?.();
62
+ this.detachRouter = null;
63
+ }
64
+ /**
65
+ * Brings the scene in line with a new set of views, keeping the GPU
66
+ * resources of everything that survived.
67
+ *
68
+ * Removing and re-adding every view on each edit destroys and reallocates
69
+ * every texture, which is both wasteful and — at this scene's size — enough
70
+ * to bring the GPU process down. Reconciling touches only what changed.
71
+ */
72
+ reconcile(views) {
73
+ const next = new Map(views.map((view) => [view.id, view]));
74
+ for (const id of [...this.views.keys()]) {
75
+ if (!next.has(id))
76
+ this.remove(id);
77
+ }
78
+ for (const view of views) {
79
+ const existing = this.views.get(view.id);
80
+ if (!existing) {
81
+ this.add(view);
82
+ continue;
83
+ }
84
+ // What actually changes the PIXELS: a different element, a different
85
+ // box, or a different opacity. Moving a view does not — and marking a
86
+ // move dirty put every plane-1 view back into the per-frame capture
87
+ // budget for the whole of every transition.
88
+ const resized = existing.width !== view.width || existing.height !== view.height;
89
+ const replaced = existing.element !== view.element;
90
+ const faded = (existing.opacity ?? 1) !== (view.opacity ?? 1);
91
+ const dirty = existing.dirty || resized || replaced || faded;
92
+ this.views.set(view.id, { ...view, dirty });
93
+ if (replaced || (dirty && !this.capturedAtIsFresh(view.id))) {
94
+ // Force a first-class recapture rather than relying on the fidelity
95
+ // rule, which for `glyph` never recaptures at all.
96
+ delete this.capturedAt[view.id];
97
+ }
98
+ }
99
+ }
100
+ /** Whether this view's cached texture still reflects what it looks like. */
101
+ capturedAtIsFresh(id) {
102
+ return this.capturedAt[id] !== undefined && this.views.get(id)?.dirty !== true;
103
+ }
104
+ add(view) {
105
+ if (view.element.parentElement !== this.canvas) {
106
+ // The platform's own message for this arrives from deep inside a
107
+ // capture call, a frame later, naming neither the view nor the fix.
108
+ throw new Error(`View "${view.id}" must be an immediate child of the scene canvas — ` +
109
+ "the capture API rejects deeper descendants. Nest inside a view, " +
110
+ "never between views.");
111
+ }
112
+ this.views.set(view.id, { ...view, dirty: true });
113
+ }
114
+ remove(id) {
115
+ const resource = this.resources.get(id);
116
+ resource?.texture.destroy();
117
+ resource?.uniform.destroy();
118
+ this.resources.delete(id);
119
+ this.views.delete(id);
120
+ delete this.capturedAt[id];
121
+ }
122
+ /** Replaces the connector set, e.g. after a layout change. */
123
+ setConnectors(connectors) {
124
+ this.connectors = connectors;
125
+ }
126
+ /** Marks a view's texture stale, so the next frame recaptures it. */
127
+ invalidate(id) {
128
+ const view = this.views.get(id);
129
+ if (view)
130
+ this.views.set(id, { ...view, dirty: true });
131
+ }
132
+ all() {
133
+ return [...this.views.values()];
134
+ }
135
+ /** Last frame's plan. Measurement harnesses and tests only. */
136
+ plan() {
137
+ return this.lastPlan;
138
+ }
139
+ /** Where each view was drawn last frame, with the element it came from. */
140
+ placements() {
141
+ return this.lastPlacements;
142
+ }
143
+ /** Raw device handle. Measurement harnesses only. */
144
+ deviceForProbe() {
145
+ return this.deps.gpu.device.gpu;
146
+ }
147
+ /** Asks the browser for a `paint` event — the only moment capture is legal. */
148
+ scheduleFrame() {
149
+ requestPaint(this.canvas);
150
+ }
151
+ /**
152
+ * One frame: capture what is stale, draw every view back to front, and tell
153
+ * the browser where each one landed.
154
+ *
155
+ * Call this from a `paint` listener. Outside one the browser has no paint
156
+ * record and capture throws.
157
+ */
158
+ render() {
159
+ const device = this.deps.gpu.device.gpu;
160
+ const queue = device.queue;
161
+ this.frame += 1;
162
+ // Read the canvas's CURRENT backing size every frame.
163
+ //
164
+ // This used to be a tuple captured when the compositor was built. Once
165
+ // the scene became responsive the canvas resized underneath it, the
166
+ // shader kept mapping canvas pixels to NDC with the old dimensions, and
167
+ // every quad landed somewhere it should not — barely at the origin,
168
+ // badly further down. The canvas is the surface; there is no second
169
+ // source of truth to go stale.
170
+ const canvasWidth = this.canvas.width;
171
+ const canvasHeight = this.canvas.height;
172
+ const plan = planFrame([...this.views.values()], this.connectors, {
173
+ canvasWidth,
174
+ canvasHeight,
175
+ capturedAt: this.capturedAt,
176
+ ...(this.options.connectorOverrides
177
+ ? { connectorOverrides: this.options.connectorOverrides }
178
+ : {}),
179
+ });
180
+ this.lastPlan = plan;
181
+ for (const capture of plan.captures) {
182
+ const view = this.views.get(capture.viewId);
183
+ if (!view)
184
+ continue;
185
+ const resource = this.ensureResources(device, view);
186
+ captureElement(queue, view.element, resource.texture);
187
+ this.capturedAt[view.id] = this.frame;
188
+ this.views.set(view.id, { ...view, dirty: false });
189
+ }
190
+ const encoder = device.createCommandEncoder({ label: "graview-compositor" });
191
+ const pass = encoder.beginRenderPass({
192
+ colorAttachments: [
193
+ {
194
+ view: this.deps.surface.context.getCurrentTexture().createView(),
195
+ clearValue: this.groundColor(),
196
+ loadOp: "clear",
197
+ storeOp: "store",
198
+ },
199
+ ],
200
+ });
201
+ pass.setPipeline(this.ensurePipeline(device));
202
+ const placements = [];
203
+ for (const draw of plan.draws) {
204
+ const view = this.views.get(draw.viewId);
205
+ const resource = this.resources.get(draw.viewId);
206
+ if (!view || !resource)
207
+ continue;
208
+ queue.writeBuffer(resource.uniform, 0, packUniform(draw, canvasWidth, canvasHeight, this.options.ground ?? [0.957, 0.949, 0.933]));
209
+ pass.setBindGroup(0, this.ensureBindGroup(device, resource));
210
+ pass.draw(6);
211
+ placements.push({
212
+ element: view.element,
213
+ transform: draw.transform,
214
+ width: view.width,
215
+ height: view.height,
216
+ depth: view.plane,
217
+ });
218
+ }
219
+ pass.end();
220
+ queue.submit([encoder.finish()]);
221
+ // Report the drawn positions even on builds that ignore them: the call is
222
+ // free, and it is what makes the scene correct the day it lands.
223
+ if (this.capabilities.geometrySync) {
224
+ for (const report of plan.geometry) {
225
+ const view = this.views.get(report.viewId);
226
+ if (!view)
227
+ continue;
228
+ updateElementGeometry(this.canvas, view.element, {
229
+ canvasTransform: toDOMMatrix(report.transform),
230
+ });
231
+ }
232
+ }
233
+ this.lastPlacements = placements;
234
+ this.router.setPlacements(placements);
235
+ return plan;
236
+ }
237
+ groundColor() {
238
+ const [r, g, b] = this.options.ground ?? [0.957, 0.949, 0.933];
239
+ return { r, g, b, a: 1 };
240
+ }
241
+ ensurePipeline(device) {
242
+ if (this.pipeline)
243
+ return this.pipeline;
244
+ const module = device.createShaderModule({
245
+ code: COMPOSITOR_WGSL,
246
+ label: "graview-compositor",
247
+ });
248
+ this.pipeline = device.createRenderPipeline({
249
+ layout: "auto",
250
+ vertex: { module, entryPoint: "vs_main" },
251
+ fragment: {
252
+ module,
253
+ entryPoint: "fs_main",
254
+ targets: [
255
+ {
256
+ format: navigator.gpu.getPreferredCanvasFormat(),
257
+ blend: {
258
+ color: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha" },
259
+ alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha" },
260
+ },
261
+ },
262
+ ],
263
+ },
264
+ primitive: { topology: "triangle-list" },
265
+ });
266
+ return this.pipeline;
267
+ }
268
+ ensureResources(device, view) {
269
+ // CEIL, not round. The browser rasterises an element of height 399.4 into
270
+ // 400 rows, and a 399-row texture makes the copy fail validation — which
271
+ // it does silently, leaving the view with whatever was in the texture
272
+ // before, or nothing at all. Allocate at least what can arrive.
273
+ const width = Math.max(1, Math.ceil(view.width));
274
+ const height = Math.max(1, Math.ceil(view.height));
275
+ const existing = this.resources.get(view.id);
276
+ if (existing && existing.width === width && existing.height === height) {
277
+ return existing;
278
+ }
279
+ existing?.texture.destroy();
280
+ const texture = device.createTexture({
281
+ size: [width, height],
282
+ format: "rgba8unorm",
283
+ usage: GPUTextureUsage.COPY_DST |
284
+ GPUTextureUsage.TEXTURE_BINDING |
285
+ GPUTextureUsage.RENDER_ATTACHMENT,
286
+ label: `graview-view-${view.id}`,
287
+ });
288
+ const resource = {
289
+ texture,
290
+ width,
291
+ height,
292
+ bindGroup: null,
293
+ uniform: existing?.uniform ??
294
+ device.createBuffer({
295
+ size: UNIFORM_BYTES,
296
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
297
+ label: `graview-plane-${view.id}`,
298
+ }),
299
+ };
300
+ this.resources.set(view.id, resource);
301
+ return resource;
302
+ }
303
+ ensureBindGroup(device, resource) {
304
+ if (resource.bindGroup)
305
+ return resource.bindGroup;
306
+ this.sampler ??= device.createSampler({
307
+ magFilter: "linear",
308
+ minFilter: "linear",
309
+ addressModeU: "clamp-to-edge",
310
+ addressModeV: "clamp-to-edge",
311
+ });
312
+ resource.bindGroup = device.createBindGroup({
313
+ layout: this.ensurePipeline(device).getBindGroupLayout(0),
314
+ entries: [
315
+ { binding: 0, resource: { buffer: resource.uniform } },
316
+ { binding: 1, resource: resource.texture.createView() },
317
+ { binding: 2, resource: this.sampler },
318
+ ],
319
+ });
320
+ return resource.bindGroup;
321
+ }
322
+ dispose() {
323
+ this.detach();
324
+ for (const id of [...this.resources.keys()])
325
+ this.remove(id);
326
+ }
327
+ }
328
+ /**
329
+ * The uniform layout the compositing shader reads, packed as data so a test
330
+ * can assert on it with no GPU present.
331
+ */
332
+ export function packUniform(draw, canvasWidth, canvasHeight, ground) {
333
+ return new Float32Array([
334
+ draw.transform[12], draw.transform[13], draw.width, draw.height,
335
+ draw.style.blur, draw.style.falloff, draw.style.shadow, draw.opacity,
336
+ canvasWidth, canvasHeight, 0, 0,
337
+ ground[0], ground[1], ground[2], 1,
338
+ ]);
339
+ }
340
+ //# sourceMappingURL=compositor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compositor.js","sourceRoot":"","sources":["../../src/scene/compositor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The plane compositing shader.
3
+ *
4
+ * One textured quad per drawn view. The vertex stage places it from a plane
5
+ * transform expressed in canvas pixels; the fragment stage applies the
6
+ * atmospheric-depth treatment: a separable-ish box blur, contrast falloff
7
+ * toward the ground colour, and a soft drop shadow that separates one plane
8
+ * from the one behind it.
9
+ *
10
+ * Blur and falloff live here rather than in CSS because per-plane blur is
11
+ * near-free on the GPU and expensive in the compositor — that difference is
12
+ * the whole justification for the GPU pipeline.
13
+ */
14
+ export declare const COMPOSITOR_WGSL = "\nstruct Plane {\n // x, y, width, height in canvas pixels, after the plane transform.\n rect: vec4f,\n // blur radius in texels, contrast falloff 0..1, shadow strength, and the\n // view's own opacity \u2014 which is how a node entering or leaving the scene\n // fades rather than popping.\n style: vec4f,\n // canvas size in pixels, and the ground colour's rgb packed as xyz.\n canvas: vec4f,\n ground: vec4f,\n}\n\n@group(0) @binding(0) var<uniform> plane: Plane;\n@group(0) @binding(1) var view_texture: texture_2d<f32>;\n@group(0) @binding(2) var view_sampler: sampler;\n\nstruct VertexOut {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n}\n\n@vertex\nfn vs_main(@builtin(vertex_index) index: u32) -> VertexOut {\n // Two triangles, no vertex buffer: the quad is entirely a function of the\n // plane rect, so nothing has to be uploaded per frame except the uniform.\n var corners = array<vec2f, 6>(\n vec2f(0.0, 0.0), vec2f(1.0, 0.0), vec2f(0.0, 1.0),\n vec2f(0.0, 1.0), vec2f(1.0, 0.0), vec2f(1.0, 1.0),\n );\n let corner = corners[index];\n let pixel = plane.rect.xy + corner * plane.rect.zw;\n // Canvas pixels to clip space, y down.\n let ndc = vec2f(\n pixel.x / plane.canvas.x * 2.0 - 1.0,\n 1.0 - pixel.y / plane.canvas.y * 2.0,\n );\n var out: VertexOut;\n out.position = vec4f(ndc, 0.0, 1.0);\n out.uv = corner;\n return out;\n}\n\nfn blurred(uv: vec2f, radius: f32) -> vec4f {\n if (radius <= 0.0) {\n return textureSample(view_texture, view_sampler, uv);\n }\n let size = vec2f(textureDimensions(view_texture, 0));\n let step = radius / size;\n var sum = vec4f(0.0);\n var weight = 0.0;\n // A 9-tap kernel: enough to read as depth-of-field at plane scale, cheap\n // enough to run on every capture every frame.\n for (var y = -1; y <= 1; y = y + 1) {\n for (var x = -1; x <= 1; x = x + 1) {\n let offset = vec2f(f32(x), f32(y)) * step;\n let w = select(1.0, 2.0, x == 0 && y == 0);\n sum = sum + textureSample(view_texture, view_sampler, uv + offset) * w;\n weight = weight + w;\n }\n }\n return sum / weight;\n}\n\n@fragment\nfn fs_main(in: VertexOut) -> @location(0) vec4f {\n let radius = plane.style.x;\n let falloff = plane.style.y;\n let shadow = plane.style.z;\n let opacity = plane.style.w;\n\n var color = blurred(in.uv, radius);\n\n // Recede toward the GROUND colour, not toward grey. On a dark ground that\n // reads as dimming and on a light one as haze \u2014 which is what distance\n // actually does to a surface, rather than a fixed \"faded\" look that only\n // works in one scheme.\n color = vec4f(mix(color.rgb, plane.ground.rgb, falloff), color.a);\n\n // Contact shadow around the quad's own border, drawn into its alpha so no\n // second pass is needed. It is what separates one plane from the one\n // behind it when both are dark.\n let edge = min(min(in.uv.x, in.uv.y), min(1.0 - in.uv.x, 1.0 - in.uv.y));\n let lift = 1.0 - smoothstep(0.0, 0.055, edge);\n color = vec4f(color.rgb * (1.0 - lift * shadow), color.a);\n\n // Premultiplied: the target blends with one-minus-src-alpha, so scaling\n // colour and alpha together is what keeps a half-faded view from darkening\n // instead of dissolving.\n return vec4f(color.rgb * opacity, color.a * opacity);\n}\n";
15
+ //# sourceMappingURL=compositor.wgsl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compositor.wgsl.d.ts","sourceRoot":"","sources":["../../src/scene/compositor.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,2vGA2F3B,CAAC"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The plane compositing shader.
3
+ *
4
+ * One textured quad per drawn view. The vertex stage places it from a plane
5
+ * transform expressed in canvas pixels; the fragment stage applies the
6
+ * atmospheric-depth treatment: a separable-ish box blur, contrast falloff
7
+ * toward the ground colour, and a soft drop shadow that separates one plane
8
+ * from the one behind it.
9
+ *
10
+ * Blur and falloff live here rather than in CSS because per-plane blur is
11
+ * near-free on the GPU and expensive in the compositor — that difference is
12
+ * the whole justification for the GPU pipeline.
13
+ */
14
+ export const COMPOSITOR_WGSL = /* wgsl */ `
15
+ struct Plane {
16
+ // x, y, width, height in canvas pixels, after the plane transform.
17
+ rect: vec4f,
18
+ // blur radius in texels, contrast falloff 0..1, shadow strength, and the
19
+ // view's own opacity — which is how a node entering or leaving the scene
20
+ // fades rather than popping.
21
+ style: vec4f,
22
+ // canvas size in pixels, and the ground colour's rgb packed as xyz.
23
+ canvas: vec4f,
24
+ ground: vec4f,
25
+ }
26
+
27
+ @group(0) @binding(0) var<uniform> plane: Plane;
28
+ @group(0) @binding(1) var view_texture: texture_2d<f32>;
29
+ @group(0) @binding(2) var view_sampler: sampler;
30
+
31
+ struct VertexOut {
32
+ @builtin(position) position: vec4f,
33
+ @location(0) uv: vec2f,
34
+ }
35
+
36
+ @vertex
37
+ fn vs_main(@builtin(vertex_index) index: u32) -> VertexOut {
38
+ // Two triangles, no vertex buffer: the quad is entirely a function of the
39
+ // plane rect, so nothing has to be uploaded per frame except the uniform.
40
+ var corners = array<vec2f, 6>(
41
+ vec2f(0.0, 0.0), vec2f(1.0, 0.0), vec2f(0.0, 1.0),
42
+ vec2f(0.0, 1.0), vec2f(1.0, 0.0), vec2f(1.0, 1.0),
43
+ );
44
+ let corner = corners[index];
45
+ let pixel = plane.rect.xy + corner * plane.rect.zw;
46
+ // Canvas pixels to clip space, y down.
47
+ let ndc = vec2f(
48
+ pixel.x / plane.canvas.x * 2.0 - 1.0,
49
+ 1.0 - pixel.y / plane.canvas.y * 2.0,
50
+ );
51
+ var out: VertexOut;
52
+ out.position = vec4f(ndc, 0.0, 1.0);
53
+ out.uv = corner;
54
+ return out;
55
+ }
56
+
57
+ fn blurred(uv: vec2f, radius: f32) -> vec4f {
58
+ if (radius <= 0.0) {
59
+ return textureSample(view_texture, view_sampler, uv);
60
+ }
61
+ let size = vec2f(textureDimensions(view_texture, 0));
62
+ let step = radius / size;
63
+ var sum = vec4f(0.0);
64
+ var weight = 0.0;
65
+ // A 9-tap kernel: enough to read as depth-of-field at plane scale, cheap
66
+ // enough to run on every capture every frame.
67
+ for (var y = -1; y <= 1; y = y + 1) {
68
+ for (var x = -1; x <= 1; x = x + 1) {
69
+ let offset = vec2f(f32(x), f32(y)) * step;
70
+ let w = select(1.0, 2.0, x == 0 && y == 0);
71
+ sum = sum + textureSample(view_texture, view_sampler, uv + offset) * w;
72
+ weight = weight + w;
73
+ }
74
+ }
75
+ return sum / weight;
76
+ }
77
+
78
+ @fragment
79
+ fn fs_main(in: VertexOut) -> @location(0) vec4f {
80
+ let radius = plane.style.x;
81
+ let falloff = plane.style.y;
82
+ let shadow = plane.style.z;
83
+ let opacity = plane.style.w;
84
+
85
+ var color = blurred(in.uv, radius);
86
+
87
+ // Recede toward the GROUND colour, not toward grey. On a dark ground that
88
+ // reads as dimming and on a light one as haze — which is what distance
89
+ // actually does to a surface, rather than a fixed "faded" look that only
90
+ // works in one scheme.
91
+ color = vec4f(mix(color.rgb, plane.ground.rgb, falloff), color.a);
92
+
93
+ // Contact shadow around the quad's own border, drawn into its alpha so no
94
+ // second pass is needed. It is what separates one plane from the one
95
+ // behind it when both are dark.
96
+ let edge = min(min(in.uv.x, in.uv.y), min(1.0 - in.uv.x, 1.0 - in.uv.y));
97
+ let lift = 1.0 - smoothstep(0.0, 0.055, edge);
98
+ color = vec4f(color.rgb * (1.0 - lift * shadow), color.a);
99
+
100
+ // Premultiplied: the target blends with one-minus-src-alpha, so scaling
101
+ // colour and alpha together is what keeps a half-faded view from darkening
102
+ // instead of dissolving.
103
+ return vec4f(color.rgb * opacity, color.a * opacity);
104
+ }
105
+ `;
106
+ //# sourceMappingURL=compositor.wgsl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compositor.wgsl.js","sourceRoot":"","sources":["../../src/scene/compositor.wgsl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2FzC,CAAC"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Connector styling. Edge kind determines stroke treatment, because the line
3
+ * between two nodes carries meaning: `protects` must never look like
4
+ * `assigned-to`, or the picture is lying about the graph.
5
+ */
6
+ export type StrokePattern = "solid" | "dashed" | "dotted" | "double" | "tapered";
7
+ export type EndCap = "none" | "arrow" | "dot" | "bar";
8
+ export interface ConnectorStyle {
9
+ readonly pattern: StrokePattern;
10
+ readonly width: number;
11
+ readonly cap: EndCap;
12
+ /** Degrees around the colour wheel; the renderer maps it into its own palette. */
13
+ readonly hue: number;
14
+ readonly opacity: number;
15
+ /** An explicit CSS colour from the brand's kit; absent, the hue paints it. */
16
+ readonly colour?: string;
17
+ }
18
+ /**
19
+ * A distinct treatment for an edge kind, derived rather than authored.
20
+ *
21
+ * A new edge kind gets a stroke nobody had to choose — which is the same
22
+ * promise the rest of the framework makes: declare the kind, and the visual
23
+ * language follows. Apps override anything they care about.
24
+ */
25
+ /**
26
+ * The hue thread lives in core, beside the brand that overrides it; this is
27
+ * the same function every surface reads, re-exported so nothing that reached
28
+ * it here has to move.
29
+ */
30
+ export { hueFor } from "@graview/core";
31
+ export declare function connectorStyle(kind: string, overrides?: Readonly<Record<string, Partial<ConnectorStyle>>>): ConnectorStyle;
32
+ /**
33
+ * Checks that a set of edge kinds are actually distinguishable from one
34
+ * another. Two relationships that render identically are a bug in the visual
35
+ * language, not a cosmetic issue — so this is worth asserting in a test
36
+ * rather than noticing in a screenshot.
37
+ */
38
+ export declare function distinguishable(kinds: readonly string[], overrides?: Readonly<Record<string, Partial<ConnectorStyle>>>): {
39
+ ok: boolean;
40
+ collisions: readonly [string, string][];
41
+ };
42
+ /**
43
+ * The dash array for a stroke pattern, in one place.
44
+ *
45
+ * Exported because the scene is not the only thing that draws a connector: a
46
+ * legend has to draw the SAME line, and a legend whose swatch is an
47
+ * approximation is a legend you cannot trust. Copying this map was the exact
48
+ * failure the legend's own docstring promised against.
49
+ */
50
+ export declare const CONNECTOR_DASH: Readonly<Record<StrokePattern, string | undefined>>;
51
+ /** The colour a connector of this kind is stroked in, on either surface. */
52
+ export declare function connectorStroke(style: ConnectorStyle): string;
53
+ /**
54
+ * How thick, given where it is drawn.
55
+ *
56
+ * Inside the scene a connector is an aside and stays thin; above the stack the
57
+ * lines are the content and earn their weight.
58
+ */
59
+ export declare function connectorWidth(style: ConnectorStyle, overview: boolean): number;
60
+ //# sourceMappingURL=connectors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connectors.d.ts","sourceRoot":"","sources":["../../src/scene/connectors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;AACjF,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,KAAK,CAAC;AAEtD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,kFAAkF;IAClF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAoBD;;;;;;GAMG;AACH;;;;GAIG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,SAAS,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC,CAAM,GAChE,cAAc,CAUhB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,GAC5D;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAA;CAAE,CAa1D;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC,CAM9E,CAAC;AAEF,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAE7D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CAE/E"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Connector styling. Edge kind determines stroke treatment, because the line
3
+ * between two nodes carries meaning: `protects` must never look like
4
+ * `assigned-to`, or the picture is lying about the graph.
5
+ */
6
+ /**
7
+ * The treatments a derived style may take. Deliberately distinguishable by
8
+ * shape as well as by hue: colour alone is not a way to tell two relationships
9
+ * apart.
10
+ */
11
+ const PATTERNS = ["solid", "dashed", "dotted", "double", "tapered"];
12
+ const CAPS = ["arrow", "none", "dot", "bar"];
13
+ /** Stable string hash — the same edge kind always gets the same treatment. */
14
+ function hash(value) {
15
+ let h = 2166136261;
16
+ for (let i = 0; i < value.length; i++) {
17
+ h ^= value.charCodeAt(i);
18
+ h = Math.imul(h, 16777619);
19
+ }
20
+ return h >>> 0;
21
+ }
22
+ /**
23
+ * A distinct treatment for an edge kind, derived rather than authored.
24
+ *
25
+ * A new edge kind gets a stroke nobody had to choose — which is the same
26
+ * promise the rest of the framework makes: declare the kind, and the visual
27
+ * language follows. Apps override anything they care about.
28
+ */
29
+ /**
30
+ * The hue thread lives in core, beside the brand that overrides it; this is
31
+ * the same function every surface reads, re-exported so nothing that reached
32
+ * it here has to move.
33
+ */
34
+ export { hueFor } from "@graview/core";
35
+ export function connectorStyle(kind, overrides = {}) {
36
+ const h = hash(kind);
37
+ const derived = {
38
+ pattern: PATTERNS[h % PATTERNS.length],
39
+ width: 1.5 + ((h >>> 3) % 3) * 0.75,
40
+ cap: CAPS[(h >>> 7) % CAPS.length],
41
+ hue: (h >>> 11) % 360,
42
+ opacity: 0.85,
43
+ };
44
+ return { ...derived, ...overrides[kind] };
45
+ }
46
+ /**
47
+ * Checks that a set of edge kinds are actually distinguishable from one
48
+ * another. Two relationships that render identically are a bug in the visual
49
+ * language, not a cosmetic issue — so this is worth asserting in a test
50
+ * rather than noticing in a screenshot.
51
+ */
52
+ export function distinguishable(kinds, overrides) {
53
+ const styles = kinds.map((kind) => [kind, connectorStyle(kind, overrides)]);
54
+ const collisions = [];
55
+ for (let i = 0; i < styles.length; i++) {
56
+ for (let j = i + 1; j < styles.length; j++) {
57
+ const [aKind, a] = styles[i];
58
+ const [bKind, b] = styles[j];
59
+ const sameShape = a.pattern === b.pattern && a.cap === b.cap && a.width === b.width;
60
+ const sameHue = Math.abs(a.hue - b.hue) < 0.04;
61
+ if (sameShape && sameHue)
62
+ collisions.push([aKind, bKind]);
63
+ }
64
+ }
65
+ return { ok: collisions.length === 0, collisions };
66
+ }
67
+ /**
68
+ * The dash array for a stroke pattern, in one place.
69
+ *
70
+ * Exported because the scene is not the only thing that draws a connector: a
71
+ * legend has to draw the SAME line, and a legend whose swatch is an
72
+ * approximation is a legend you cannot trust. Copying this map was the exact
73
+ * failure the legend's own docstring promised against.
74
+ */
75
+ export const CONNECTOR_DASH = {
76
+ solid: undefined,
77
+ dashed: "7 5",
78
+ dotted: "1 5",
79
+ double: "12 3",
80
+ tapered: "10 3 3 3",
81
+ };
82
+ /** The colour a connector of this kind is stroked in, on either surface. */
83
+ export function connectorStroke(style) {
84
+ return style.colour ?? `hsl(${Math.round(style.hue)} 55% 62%)`;
85
+ }
86
+ /**
87
+ * How thick, given where it is drawn.
88
+ *
89
+ * Inside the scene a connector is an aside and stays thin; above the stack the
90
+ * lines are the content and earn their weight.
91
+ */
92
+ export function connectorWidth(style, overview) {
93
+ return overview ? Math.max(1.6, style.width) : Math.min(1.4, style.width);
94
+ }
95
+ //# sourceMappingURL=connectors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connectors.js","sourceRoot":"","sources":["../../src/scene/connectors.ts"],"names":[],"mappings":"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"}