opencode-agent-flow 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,155 @@
1
+ import * as THREE from 'three';
2
+ import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
3
+ import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
4
+ import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
5
+ import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
6
+ import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
7
+
8
+ // Screen-space ribbon meshes retain their width on GPUs with one-pixel GL lines.
9
+ const STRANDS = 7, STEPS = 56, MAX_PARTICLES = 3000;
10
+ function sample(e, u, strand, time) {
11
+ const dx = e.b[0] - e.a[0], dy = e.b[1] - e.a[1], len = Math.hypot(dx, dy) || 1, v = 1 - u;
12
+ const spread = e.bend + (strand - 3) * 8 + Math.sin(time * .65 + strand * 1.7) * 10;
13
+ const wave = Math.sin(u * Math.PI * 3 + strand * 1.4 + time * .7) * Math.sin(u * Math.PI) * 5;
14
+ const offset = 3 * v * v * u * spread - 3 * v * u * u * spread * .65 + wave;
15
+ return [e.a[0] + dx * u - dy / len * offset, e.a[1] + dy * u + dx / len * offset];
16
+ }
17
+
18
+ export function createFlow(fallbackCanvas, onLost) {
19
+ const canvas = document.createElement('canvas');
20
+ canvas.setAttribute('aria-hidden', 'true'); canvas.style.pointerEvents = 'none';
21
+ let renderer, composer;
22
+ try {
23
+ const options = { alpha: true, premultipliedAlpha: true, antialias: false, powerPreference: 'low-power' };
24
+ const context = canvas.getContext('webgl2', options);
25
+ if (!context) throw new Error('WebGL2 unavailable; using Canvas 2D');
26
+ renderer = new THREE.WebGLRenderer({ canvas, context, ...options });
27
+ renderer.setClearColor(0x000000, 0);
28
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
29
+ renderer.toneMappingExposure = 1.15;
30
+ const scene = new THREE.Scene();
31
+ const camera = new THREE.OrthographicCamera(0, 1364, 0, 540, -10, 10);
32
+ camera.position.z = 1;
33
+ composer = new EffectComposer(renderer);
34
+ composer.addPass(new RenderPass(scene, camera));
35
+ const bloom = new UnrealBloomPass(new THREE.Vector2(1364, 540), 1.3, .65, .3);
36
+ // The stock truncated Gaussian still has ~60% weight at its edge, making
37
+ // isolated sparks bloom into boxes. Let each separable kernel decay first.
38
+ for (const material of bloom.separableBlurMaterials) {
39
+ const weights = material.uniforms.gaussianCoefficients.value;
40
+ const sigma = weights.length / 4;
41
+ for (let i = 0; i < weights.length; i++) weights[i] = Math.exp(-.5 * i * i / (sigma * sigma));
42
+ }
43
+ composer.addPass(bloom);
44
+ composer.addPass(new OutputPass());
45
+ // UnrealBloomPass writes opaque alpha. Rebuild coverage from the final,
46
+ // tone-mapped light, with premultiplied RGB for native page compositing.
47
+ // Black becomes fully transparent; even the faint bloom retains soft edges.
48
+ composer.addPass(new ShaderPass({
49
+ uniforms: { tDiffuse: { value: null } },
50
+ vertexShader: `varying vec2 vUv;
51
+ void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.); }`,
52
+ fragmentShader: `uniform sampler2D tDiffuse; varying vec2 vUv;
53
+ void main() { vec3 light = texture2D(tDiffuse, vUv).rgb;
54
+ float coverage = clamp(max(light.r, max(light.g, light.b)), 0., 1.);
55
+ gl_FragColor = vec4(light, coverage); }`
56
+ }));
57
+ fallbackCanvas.after(canvas);
58
+ const ribbons = new THREE.Group(); scene.add(ribbons);
59
+ const lineMaterial = new THREE.MeshBasicMaterial({ color: 0xff3d8b, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide, toneMapped: false });
60
+ // Explicit billboard UVs avoid driver-dependent point-sprite coordinates.
61
+ const particleGeometry = new THREE.InstancedBufferGeometry();
62
+ particleGeometry.setAttribute('position', new THREE.Float32BufferAttribute([-.5,-.5,0, .5,-.5,0, .5,.5,0, -.5,.5,0], 3));
63
+ particleGeometry.setIndex([0, 1, 2, 0, 2, 3]);
64
+ const positions = new Float32Array(MAX_PARTICLES * 3), sizes = new Float32Array(MAX_PARTICLES), intensities = new Float32Array(MAX_PARTICLES), whites = new Float32Array(MAX_PARTICLES);
65
+ for (const [name, array, size] of [['offset', positions, 3], ['size', sizes, 1], ['intensity', intensities, 1], ['white', whites, 1]]) particleGeometry.setAttribute(name, new THREE.InstancedBufferAttribute(array, size).setUsage(THREE.DynamicDrawUsage));
66
+ const particleMaterial = new THREE.ShaderMaterial({
67
+ transparent: true, depthWrite: false, blending: THREE.AdditiveBlending,
68
+ side: THREE.DoubleSide,
69
+ vertexShader: `attribute vec3 offset; attribute float size; attribute float intensity; attribute float white;
70
+ varying float power; varying float pale; varying vec2 spriteUv;
71
+ void main() { power = intensity; pale = white; spriteUv = position.xy * 2.;
72
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(offset + position * size, 1.); }`,
73
+ fragmentShader: `varying float power; varying float pale; varying vec2 spriteUv;
74
+ void main() { float r = length(spriteUv); if (r >= 1.) discard;
75
+ float halo = exp(-r*r*5.) * (1. - smoothstep(.55, 1., r));
76
+ float core = exp(-r*r*36.);
77
+ vec3 pink = mix(vec3(1., .045, .26), vec3(1., .55, .72), pale);
78
+ gl_FragColor = vec4((pink * halo * .65 + vec3(1.8) * core) * power, halo); }`
79
+ });
80
+ const particles = new THREE.Mesh(particleGeometry, particleMaterial); particles.frustumCulled = false; scene.add(particles);
81
+ let dpr = 1, count = 0, budget = 1800, slow = 0, dimensions = '';
82
+ function particle(x, y, size, intensity, white = 0) {
83
+ if (count >= MAX_PARTICLES) return;
84
+ positions[count * 3] = x; positions[count * 3 + 1] = y; sizes[count] = size; intensities[count] = intensity; whites[count] = white; count++;
85
+ }
86
+ function dispose() {
87
+ for (const mesh of [...ribbons.children]) { mesh.geometry.dispose(); mesh.material.dispose(); ribbons.remove(mesh); }
88
+ particleGeometry.dispose(); particleMaterial.dispose(); lineMaterial.dispose(); composer.dispose(); renderer.dispose(); canvas.remove();
89
+ }
90
+ canvas.addEventListener('webglcontextlost', event => { event.preventDefault(); dispose(); onLost(); }, { once: true });
91
+ return {
92
+ resize(width, height, ratio) {
93
+ const displayWidth = canvas.parentElement.getBoundingClientRect().width || width;
94
+ const key = `${width}:${height}:${ratio}:${displayWidth}`;
95
+ if (dimensions === key) return;
96
+ dimensions = key;
97
+ dpr = Math.min(2, ratio); renderer.setPixelRatio(dpr); renderer.setSize(width, height, false);
98
+ composer.setPixelRatio(dpr); composer.setSize(width, height);
99
+ camera.right = width; camera.bottom = height; camera.updateProjectionMatrix();
100
+ budget = displayWidth < 700 || navigator.hardwareConcurrency <= 4 ? 900 : 2400;
101
+ },
102
+ draw(edges, hub, visible, time) {
103
+ const start = performance.now(); count = 0;
104
+ const needed = edges.length * STRANDS;
105
+ while (ribbons.children.length > needed) { const mesh = ribbons.children.at(-1); ribbons.remove(mesh); mesh.geometry.dispose(); mesh.material.dispose(); }
106
+ while (ribbons.children.length < needed) {
107
+ const geometry = new THREE.BufferGeometry(), vertices = new Float32Array(STEPS * 6 * 3);
108
+ geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3).setUsage(THREE.DynamicDrawUsage));
109
+ const mesh = new THREE.Mesh(geometry, lineMaterial.clone()); mesh.frustumCulled = false; ribbons.add(mesh);
110
+ }
111
+ edges.forEach((e, index) => {
112
+ const waiting = e.status === 'waiting', active = e.active;
113
+ const power = active ? (waiting ? .85 + Math.sin(time * 2.4) * .2 : 1.1) : .19;
114
+ for (let j = 0; j < STRANDS; j++) {
115
+ const mesh = ribbons.children[index * STRANDS + j], array = mesh.geometry.attributes.position.array;
116
+ mesh.material.color.setRGB(power * 1.25, power * (waiting ? .8 : .07), power * (waiting ? 1 : .35));
117
+ const width = active ? 1.6 : .8;
118
+ let prev = sample(e, 0, j, time);
119
+ for (let k = 0; k < STEPS; k++) {
120
+ const next = sample(e, (k + 1) / STEPS, j, time), dx = next[0] - prev[0], dy = next[1] - prev[1], len = Math.hypot(dx, dy) || 1;
121
+ const nx = -dy / len * width / 2, ny = dx / len * width / 2;
122
+ array.set([prev[0]+nx,prev[1]+ny,0, prev[0]-nx,prev[1]-ny,0, next[0]+nx,next[1]+ny,0, next[0]+nx,next[1]+ny,0, prev[0]-nx,prev[1]-ny,0, next[0]-nx,next[1]-ny,0], k * 18);
123
+ prev = next;
124
+ }
125
+ mesh.geometry.attributes.position.needsUpdate = true;
126
+ const density = Math.max(1, Math.min(active ? 8 : 2, Math.floor((budget - 90) / Math.max(1, needed))));
127
+ for (let p = 0; p < density; p++) {
128
+ const phase = (time * (waiting ? .12 : active ? .22 : .035) + p / density + j * .137 + index * .173) % 1;
129
+ const xy = sample(e, e.returning ? 1 - phase : phase, j, time);
130
+ particle(...xy, p % 5 ? 8 : 13, active ? 1.65 : .5, waiting ? 1 : .15);
131
+ }
132
+ }
133
+ // Large soft dock flashes, with a hot white nucleus at the card border.
134
+ for (const xy of [e.a, e.b]) {
135
+ particle(...xy, active ? 54 : 25, power * .9, waiting ? 1 : 0);
136
+ particle(...xy, active ? 20 + Math.sin(time * 3 + index) * 3 : 9, power * 3.8, .5);
137
+ }
138
+ });
139
+ if (visible) {
140
+ particle(...hub, 380, .22, 0);
141
+ for (let i = 0; i < 70; i++) {
142
+ const a = i * 2.399 + time * .025, r = 117 + Math.sin(i * 7 + time * .4) * 4;
143
+ particle(hub[0] + Math.cos(a) * r, hub[1] + Math.sin(a) * r, i % 7 ? 9 : 18, i % 7 ? .65 : 1.2);
144
+ }
145
+ }
146
+ particleGeometry.instanceCount = count;
147
+ for (const attribute of Object.values(particleGeometry.attributes)) if (attribute.isInstancedBufferAttribute) attribute.needsUpdate = true;
148
+ composer.render();
149
+ // Reduce density on sustained slow frames, not on a single shader compilation.
150
+ if (performance.now() - start > 28) { if (++slow > 25) { budget = Math.max(350, budget * .8); slow = 0; } } else slow = Math.max(0, slow - 1);
151
+ },
152
+ dispose
153
+ };
154
+ } catch (error) { composer?.dispose(); renderer?.dispose(); canvas.remove(); throw error; }
155
+ }
@@ -0,0 +1,10 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>agent flow</title><link rel="stylesheet" href="/style.css"><script type="importmap">{"imports":{"three":"/vendor/three/three.module.min.js","three/addons/":"/vendor/three/"}}</script><script src="/app.js" defer></script></head>
4
+ <body>
5
+ <header><div class="heading"><h1>agent flow<span> / </span><span id="title">connecting…</span></h1><small id="subtitle">opencode · orchestration topology</small></div><div class="controls"><div class="view-switch" role="group" aria-label="View mode"><button id="view-all" type="button">all sessions</button><button id="view-session" type="button">session</button></div><input id="session-search" type="search" placeholder="search sessions…" aria-label="Search sessions" hidden><label for="root">session</label><select id="root" aria-label="Root session"></select><button id="motion" type="button" aria-pressed="false">pause motion</button><div class="status"><i></i><span id="connection">connecting</span><span id="counts"></span></div></div></header>
6
+ <section id="session-picker" hidden aria-label="Select session"><h2>// select a session</h2><p>One root. Every agent in its subtree. Live metrics, scoped to your session.</p><div id="session-cards"></div></section>
7
+ <main><section class="scene"><div class="scene-label">// execution graph <span id="graphMeta"></span></div><div class="viewport"><div class="network"><canvas id="wires" aria-label="Agent connections"></canvas><div id="nodes"></div><div class="hub" hidden><svg viewBox="0 0 236 236" aria-hidden="true"><circle cx="118" cy="118" r="114" fill="none" stroke="#c470983f" stroke-dasharray="2 7"/><circle cx="118" cy="118" r="101" fill="none" stroke="#573044" stroke-width="3"/><circle id="contextRing" cx="118" cy="118" r="101" fill="none" stroke="#ff5c9d" stroke-width="3" transform="rotate(-90 118 118)"/></svg><h2 id="lead"></h2><p id="context"></p><b id="model"></b><small id="leadTool"></small></div></div></div><div id="empty" hidden></div><div class="scene-foot"><span id="sceneSummary"></span><span id="transport">white → pink / token transport</span></div></section>
8
+ <section class="dash" aria-label="Metrics"><article class="panel"><h3>// event log</h3><div id="events" class="scroll"></div></article><article class="panel"><h3>// tokens / hour</h3><div id="hourly" class="scroll"></div></article><article class="panel"><h3>// cache hit</h3><div class="gauge"><svg viewBox="0 0 100 100" aria-hidden="true"><circle cx="50" cy="50" r="43" fill="none" stroke="#442032" stroke-width="5"/><circle id="cacheRing" cx="50" cy="50" r="43" fill="none" stroke="#ff6fae" stroke-width="5"/></svg><div class="value"><span id="cacheValue">—</span><small>hit / window</small></div></div></article><article class="panel"><h3>// activity heat</h3><div id="heat" class="scroll"></div><div class="legend">quiet ░▒▓█ active · 12 × 5 min</div></article><article class="panel"><h3>// tokens today</h3><div class="big" id="total">—</div><div class="estimate" id="estimate"></div><div id="today" class="breakdown scroll"></div></article><article class="panel"><h3>// ledger</h3><div id="ledger" class="ledger scroll"></div></article></section></main>
9
+ <footer><span id="health">transport / public HTTP + SSE · read-only</span><span id="clock"></span></footer>
10
+ </body></html>
@@ -0,0 +1,6 @@
1
+ :root{--pink:#ff3d8b;--soft:#ff6fae;--muted:#ac8d9d;--line:#4c2537}*{box-sizing:border-box}[hidden]{display:none!important}body{margin:0;background:#0d0609;color:#f5e5ec;font:11px 'JetBrains Mono','SFMono-Regular',Consolas,monospace;background-image:linear-gradient(#ff80b005 1px,transparent 1px),linear-gradient(90deg,#ff80b005 1px,transparent 1px);background-size:40px 40px}header{min-height:96px;margin:0 38px;border-bottom:1px solid #472033;display:flex;justify-content:space-between;align-items:center;gap:24px}.heading{min-width:0;flex:1}h1{font-size:17px;font-weight:500;letter-spacing:-.6px;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}h1>span:first-child{color:var(--soft)}header small{display:block;margin-top:9px;color:var(--muted);font-size:10px}.controls{display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:flex-end;max-width:520px}.controls label{color:var(--muted);font-size:9px}select,button{font:10px monospace;background:#1b0d16;color:#ddbdce;border:1px solid #654152;padding:7px;max-width:230px;cursor:pointer}select:focus-visible,button:focus-visible{outline:2px solid var(--soft);outline-offset:3px}button:hover{background:#392032}.status{color:var(--soft);font-size:10px;width:100%;text-align:right;display:flex;justify-content:flex-end;align-items:center;gap:10px}.status i{display:inline-block;width:5px;height:5px;background:var(--pink);box-shadow:0 0 12px var(--pink);border-radius:50%}.status.offline{color:#bba1cb}.status.offline i{background:#aaa;box-shadow:none}.scene{height:540px;position:relative;overflow:hidden;background:radial-gradient(ellipse at 50% 48%,#8c174e22,transparent 60%)}.viewport{position:absolute;inset:0;overflow:hidden}.network{position:absolute;width:1364px;height:540px;left:50%;transform:translateX(-50%);transform-origin:50% 0}canvas{position:absolute;inset:0;width:100%;height:100%}.node{position:absolute;width:200px;background:#160b12f5;border:1px solid #654152;z-index:2;box-shadow:0 8px 30px #0005;transition:opacity .25s,border-color .25s,transform .3s cubic-bezier(.2,0,0,1);animation:enter .3s ease-out}.cap{height:27px;background:#392b34;color:#e1d3db;padding:5px 9px;display:flex;justify-content:space-between;gap:8px;align-items:center;font-weight:600}.cap strong{font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tag{font-size:8px;font-weight:500;opacity:.8;flex-shrink:0}.rows{padding:8px 9px;line-height:21px;color:#b194a4;font-size:9px}.row{display:flex;gap:10px;justify-content:space-between}.row span:last-child{color:#f5d4e4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:132px}.node.working{border-color:#b63b70}.node.working .cap{background:#ff508f;color:#240d18}.node.waiting{border-color:#f5eaf0;box-shadow:0 0 26px #ff3d8b16}.node.waiting .cap{background:#f5eaf0;color:#25121c}.node.done{opacity:.67}.hub{position:absolute;left:564px;top:152px;width:236px;height:236px;z-index:3;text-align:center;display:flex;flex-direction:column;justify-content:center;align-items:center;border-radius:50%;background:radial-gradient(circle,#30101f 5%,#180c15 65%,transparent 66%)}.hub svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none}#contextRing{filter:drop-shadow(0 0 5px #ff3d8b);transition:stroke-dasharray .35s}.hub h2{font-size:35px;max-width:185px;overflow:hidden;text-overflow:ellipsis;letter-spacing:-2px;margin:0 0 9px;font-weight:800;text-shadow:0 0 32px #ff77ae66}.hub p{font-size:9px;color:#d3a9bb;margin:0}.hub b{font-weight:400;font-size:9px;color:var(--soft);margin-top:15px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hub small{font-size:8px;color:var(--muted);margin-top:10px;max-width:170px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.scene-label{position:absolute;left:38px;top:20px;color:#af8a9d;font-size:9px;letter-spacing:1px;z-index:4}.scene-foot{position:absolute;bottom:16px;left:38px;right:38px;display:flex;gap:20px;justify-content:space-between;color:#ba8fa2;font-size:9px;z-index:4}.dash{display:grid;grid-template-columns:1.35fr 1fr .86fr 1fr 1.05fr 1fr;gap:10px;margin:0 28px 25px}.panel{height:221px;min-width:0;border:1px solid #492335;background:#160b11d9;padding:16px 13px;overflow:hidden}.panel h3{color:#c09aab;font-size:10px;font-weight:400;margin:0 0 20px}.scroll{overflow:auto;max-height:158px;scrollbar-width:thin;scrollbar-color:#65314b transparent}.event{font-size:9px;margin-bottom:11px;white-space:nowrap;color:#c3a7b7;overflow:hidden;text-overflow:ellipsis}.event em{font-style:normal;color:var(--soft);display:inline-block;width:42px}.event time{font-size:8px;color:var(--muted);margin-right:7px}.bars{display:flex;align-items:center;margin-bottom:6px;gap:7px;color:#bd94a8;font-size:8px}.bars time{min-width:14px}.bars .track{flex:1}.bars i{display:block;height:5px;background:linear-gradient(90deg,#782549,#ff6ba7);box-shadow:0 0 10px #ff3d8b22;min-width:1px}.bars b{font-weight:400;width:37px;text-align:right}.gauge{width:111px;height:111px;margin:0 auto;position:relative}.gauge svg{width:100%;height:100%;transform:rotate(-90deg)}.gauge .value{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;font-size:25px}.gauge small{font-size:8px;color:#b28b9d;margin-top:4px}.heat-row{display:grid;grid-template-columns:43px repeat(12,1fr);gap:3px;margin-bottom:5px;align-items:center}.heat-row span{font-size:7px;overflow:hidden;text-overflow:ellipsis;color:#bc98ab}.heat-row i{height:10px;background:#ff3d8b}.legend{color:#b38ca0;font-size:8px;margin-top:12px}#heat{max-height:128px}.big{font-size:30px;color:#ffe7f1;letter-spacing:-2px;margin-top:-4px}.estimate{color:var(--soft);font-size:10px;margin:7px 0}.breakdown{font-size:8px;color:#c09aae;line-height:19px;max-height:90px}.breakdown div{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ledger{font-size:9px;color:#c4a3b5;line-height:25px;white-space:nowrap}.ledger b{font-weight:400;color:#ff6fae}.ledger .idle,.ledger .done{color:#bba1cb}.ledger .waiting{color:#fff}footer{margin:0 38px 22px;color:#a07c8f;display:flex;gap:20px;justify-content:space-between;font-size:9px}#empty{position:absolute;inset:0;display:grid;place-items:center;color:#c19aaf;font-size:13px}.muted{color:var(--muted);font-size:10px}.approval .cap{background:#f5eaf0;color:#25121c}@keyframes enter{from{opacity:0;translate:0 8px}to{opacity:1;translate:0 0}}@media(max-width:1100px){.dash{grid-template-columns:repeat(3,1fr)}header{margin:0 24px}.scene-label{left:24px}}@media(max-width:650px){header{padding:18px 0;align-items:flex-start;flex-direction:column;gap:16px}.controls{justify-content:flex-start;max-width:100%}.status{justify-content:flex-start;text-align:left}h1{font-size:14px;max-width:calc(100vw - 48px)}select{max-width:190px}.scene{height:460px}.viewport{overflow-x:auto}.network{left:0;transform:none}.dash{grid-template-columns:1fr;margin:0 18px 20px}.panel{height:215px}.scene-foot{left:24px;right:24px;flex-direction:column;gap:6px}footer{margin:0 24px 20px;flex-direction:column}.scene-label{font-size:8px}}@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}
2
+ @media(min-width:651px){header{min-height:76px}}
3
+ .bars{height:8px;line-height:8px;margin-bottom:5px}
4
+ .view-switch{display:flex;flex-shrink:0}.view-switch button{border-radius:0}.view-switch button+button{border-left:0}.view-switch button[aria-pressed=true]{color:#fff;background:#64243f;border-color:#d36b99}#session-search{font:10px monospace;width:145px;min-width:0;padding:7px;color:#f5e5ec;background:#1b0d16;border:1px solid #654152}#session-search:focus-visible{outline:2px solid var(--soft);outline-offset:3px}.controls{max-width:690px}body[data-view=session] header{min-height:102px}#session-picker{padding:35px 38px;min-height:70vh}#session-picker h2{font-size:15px;font-weight:500;color:var(--soft)}#session-picker>p{color:#c9abbc;line-height:1.6}#session-cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,360px),1fr));gap:16px;margin-top:24px}.session-card{display:flex;flex-direction:column;align-items:flex-start;text-align:left;gap:13px;max-width:none;width:100%;padding:20px;background:#160b12;line-height:1.5}.session-card strong{font-size:12px;font-weight:500;overflow-wrap:anywhere}.session-card span{color:#c9abbc;overflow-wrap:anywhere}.session-card .working{color:#ff85b9}@media(max-width:900px){header{flex-wrap:wrap;padding:16px 0;gap:14px}.controls{max-width:100%;justify-content:flex-start}.status{justify-content:flex-start;text-align:left}#session-picker{padding:24px}}
5
+ #group-sessions:popover-open{max-height:min(320px,calc(100vh - 24px))}
6
+ .node[role=button]{cursor:pointer}.node[role=button]:focus-visible{outline:2px solid var(--soft);outline-offset:4px}.cap strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cap strong small{font-size:10px;white-space:nowrap}.cap .tag{flex-shrink:0}#group-sessions{position:fixed;inset:auto;margin:0;width:min(410px,calc(100vw - 24px));max-height:min(420px,calc(100vh - 24px));padding:14px;border:1px solid #ac567c;background:#1b0d16;color:#f5e5ec;box-shadow:0 14px 60px #000c;font:11px monospace;overflow:auto}#group-sessions::backdrop{background:transparent}.popover-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:8px}.popover-heading button{padding:4px 10px}.session-item{padding:12px 0;border-top:1px solid var(--line)}.session-item>strong{display:block;overflow-wrap:anywhere;font-weight:500;line-height:1.5}.session-item>div{display:flex;gap:12px;flex-wrap:wrap;margin-top:7px;color:#c9abbc}.session-item .working{color:#ff85b9}.session-item .waiting{color:white}.session-item .done,.session-item .idle{color:#c9abbc}
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright © 2010-2025 three.js authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,363 @@
1
+ import {
2
+ Clock,
3
+ HalfFloatType,
4
+ NoBlending,
5
+ Vector2,
6
+ WebGLRenderTarget
7
+ } from 'three';
8
+ import { CopyShader } from '../shaders/CopyShader.js';
9
+ import { ShaderPass } from './ShaderPass.js';
10
+ import { ClearMaskPass, MaskPass } from './MaskPass.js';
11
+
12
+ /**
13
+ * Used to implement post-processing effects in three.js.
14
+ * The class manages a chain of post-processing passes to produce the final visual result.
15
+ * Post-processing passes are executed in order of their addition/insertion.
16
+ * The last pass is automatically rendered to screen.
17
+ *
18
+ * This module can only be used with {@link WebGLRenderer}.
19
+ *
20
+ * ```js
21
+ * const composer = new EffectComposer( renderer );
22
+ *
23
+ * // adding some passes
24
+ * const renderPass = new RenderPass( scene, camera );
25
+ * composer.addPass( renderPass );
26
+ *
27
+ * const glitchPass = new GlitchPass();
28
+ * composer.addPass( glitchPass );
29
+ *
30
+ * const outputPass = new OutputPass()
31
+ * composer.addPass( outputPass );
32
+ *
33
+ * function animate() {
34
+ *
35
+ * composer.render(); // instead of renderer.render()
36
+ *
37
+ * }
38
+ * ```
39
+ *
40
+ * @three_import import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
41
+ */
42
+ class EffectComposer {
43
+
44
+ /**
45
+ * Constructs a new effect composer.
46
+ *
47
+ * @param {WebGLRenderer} renderer - The renderer.
48
+ * @param {WebGLRenderTarget} [renderTarget] - This render target and a clone will
49
+ * be used as the internal read and write buffers. If not given, the composer creates
50
+ * the buffers automatically.
51
+ */
52
+ constructor( renderer, renderTarget ) {
53
+
54
+ /**
55
+ * The renderer.
56
+ *
57
+ * @type {WebGLRenderer}
58
+ */
59
+ this.renderer = renderer;
60
+
61
+ this._pixelRatio = renderer.getPixelRatio();
62
+
63
+ if ( renderTarget === undefined ) {
64
+
65
+ const size = renderer.getSize( new Vector2() );
66
+ this._width = size.width;
67
+ this._height = size.height;
68
+
69
+ renderTarget = new WebGLRenderTarget( this._width * this._pixelRatio, this._height * this._pixelRatio, { type: HalfFloatType } );
70
+ renderTarget.texture.name = 'EffectComposer.rt1';
71
+
72
+ } else {
73
+
74
+ this._width = renderTarget.width;
75
+ this._height = renderTarget.height;
76
+
77
+ }
78
+
79
+ this.renderTarget1 = renderTarget;
80
+ this.renderTarget2 = renderTarget.clone();
81
+ this.renderTarget2.texture.name = 'EffectComposer.rt2';
82
+
83
+ /**
84
+ * A reference to the internal write buffer. Passes usually write
85
+ * their result into this buffer.
86
+ *
87
+ * @type {WebGLRenderTarget}
88
+ */
89
+ this.writeBuffer = this.renderTarget1;
90
+
91
+ /**
92
+ * A reference to the internal read buffer. Passes usually read
93
+ * the previous render result from this buffer.
94
+ *
95
+ * @type {WebGLRenderTarget}
96
+ */
97
+ this.readBuffer = this.renderTarget2;
98
+
99
+ /**
100
+ * Whether the final pass is rendered to the screen (default framebuffer) or not.
101
+ *
102
+ * @type {boolean}
103
+ * @default true
104
+ */
105
+ this.renderToScreen = true;
106
+
107
+ /**
108
+ * An array representing the (ordered) chain of post-processing passes.
109
+ *
110
+ * @type {Array<Pass>}
111
+ */
112
+ this.passes = [];
113
+
114
+ /**
115
+ * A copy pass used for internal swap operations.
116
+ *
117
+ * @private
118
+ * @type {ShaderPass}
119
+ */
120
+ this.copyPass = new ShaderPass( CopyShader );
121
+ this.copyPass.material.blending = NoBlending;
122
+
123
+ /**
124
+ * The internal clock for managing time data.
125
+ *
126
+ * @private
127
+ * @type {Clock}
128
+ */
129
+ this.clock = new Clock();
130
+
131
+ }
132
+
133
+ /**
134
+ * Swaps the internal read/write buffers.
135
+ */
136
+ swapBuffers() {
137
+
138
+ const tmp = this.readBuffer;
139
+ this.readBuffer = this.writeBuffer;
140
+ this.writeBuffer = tmp;
141
+
142
+ }
143
+
144
+ /**
145
+ * Adds the given pass to the pass chain.
146
+ *
147
+ * @param {Pass} pass - The pass to add.
148
+ */
149
+ addPass( pass ) {
150
+
151
+ this.passes.push( pass );
152
+ pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
153
+
154
+ }
155
+
156
+ /**
157
+ * Inserts the given pass at a given index.
158
+ *
159
+ * @param {Pass} pass - The pass to insert.
160
+ * @param {number} index - The index into the pass chain.
161
+ */
162
+ insertPass( pass, index ) {
163
+
164
+ this.passes.splice( index, 0, pass );
165
+ pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
166
+
167
+ }
168
+
169
+ /**
170
+ * Removes the given pass from the pass chain.
171
+ *
172
+ * @param {Pass} pass - The pass to remove.
173
+ */
174
+ removePass( pass ) {
175
+
176
+ const index = this.passes.indexOf( pass );
177
+
178
+ if ( index !== - 1 ) {
179
+
180
+ this.passes.splice( index, 1 );
181
+
182
+ }
183
+
184
+ }
185
+
186
+ /**
187
+ * Returns `true` if the pass for the given index is the last enabled pass in the pass chain.
188
+ *
189
+ * @param {number} passIndex - The pass index.
190
+ * @return {boolean} Whether the pass for the given index is the last pass in the pass chain.
191
+ */
192
+ isLastEnabledPass( passIndex ) {
193
+
194
+ for ( let i = passIndex + 1; i < this.passes.length; i ++ ) {
195
+
196
+ if ( this.passes[ i ].enabled ) {
197
+
198
+ return false;
199
+
200
+ }
201
+
202
+ }
203
+
204
+ return true;
205
+
206
+ }
207
+
208
+ /**
209
+ * Executes all enabled post-processing passes in order to produce the final frame.
210
+ *
211
+ * @param {number} deltaTime - The delta time in seconds. If not given, the composer computes
212
+ * its own time delta value.
213
+ */
214
+ render( deltaTime ) {
215
+
216
+ // deltaTime value is in seconds
217
+
218
+ if ( deltaTime === undefined ) {
219
+
220
+ deltaTime = this.clock.getDelta();
221
+
222
+ }
223
+
224
+ const currentRenderTarget = this.renderer.getRenderTarget();
225
+
226
+ let maskActive = false;
227
+
228
+ for ( let i = 0, il = this.passes.length; i < il; i ++ ) {
229
+
230
+ const pass = this.passes[ i ];
231
+
232
+ if ( pass.enabled === false ) continue;
233
+
234
+ pass.renderToScreen = ( this.renderToScreen && this.isLastEnabledPass( i ) );
235
+ pass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime, maskActive );
236
+
237
+ if ( pass.needsSwap ) {
238
+
239
+ if ( maskActive ) {
240
+
241
+ const context = this.renderer.getContext();
242
+ const stencil = this.renderer.state.buffers.stencil;
243
+
244
+ //context.stencilFunc( context.NOTEQUAL, 1, 0xffffffff );
245
+ stencil.setFunc( context.NOTEQUAL, 1, 0xffffffff );
246
+
247
+ this.copyPass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime );
248
+
249
+ //context.stencilFunc( context.EQUAL, 1, 0xffffffff );
250
+ stencil.setFunc( context.EQUAL, 1, 0xffffffff );
251
+
252
+ }
253
+
254
+ this.swapBuffers();
255
+
256
+ }
257
+
258
+ if ( MaskPass !== undefined ) {
259
+
260
+ if ( pass instanceof MaskPass ) {
261
+
262
+ maskActive = true;
263
+
264
+ } else if ( pass instanceof ClearMaskPass ) {
265
+
266
+ maskActive = false;
267
+
268
+ }
269
+
270
+ }
271
+
272
+ }
273
+
274
+ this.renderer.setRenderTarget( currentRenderTarget );
275
+
276
+ }
277
+
278
+ /**
279
+ * Resets the internal state of the EffectComposer.
280
+ *
281
+ * @param {WebGLRenderTarget} [renderTarget] - This render target has the same purpose like
282
+ * the one from the constructor. If set, it is used to setup the read and write buffers.
283
+ */
284
+ reset( renderTarget ) {
285
+
286
+ if ( renderTarget === undefined ) {
287
+
288
+ const size = this.renderer.getSize( new Vector2() );
289
+ this._pixelRatio = this.renderer.getPixelRatio();
290
+ this._width = size.width;
291
+ this._height = size.height;
292
+
293
+ renderTarget = this.renderTarget1.clone();
294
+ renderTarget.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
295
+
296
+ }
297
+
298
+ this.renderTarget1.dispose();
299
+ this.renderTarget2.dispose();
300
+ this.renderTarget1 = renderTarget;
301
+ this.renderTarget2 = renderTarget.clone();
302
+
303
+ this.writeBuffer = this.renderTarget1;
304
+ this.readBuffer = this.renderTarget2;
305
+
306
+ }
307
+
308
+ /**
309
+ * Resizes the internal read and write buffers as well as all passes. Similar to {@link WebGLRenderer#setSize},
310
+ * this method honors the current pixel ration.
311
+ *
312
+ * @param {number} width - The width in logical pixels.
313
+ * @param {number} height - The height in logical pixels.
314
+ */
315
+ setSize( width, height ) {
316
+
317
+ this._width = width;
318
+ this._height = height;
319
+
320
+ const effectiveWidth = this._width * this._pixelRatio;
321
+ const effectiveHeight = this._height * this._pixelRatio;
322
+
323
+ this.renderTarget1.setSize( effectiveWidth, effectiveHeight );
324
+ this.renderTarget2.setSize( effectiveWidth, effectiveHeight );
325
+
326
+ for ( let i = 0; i < this.passes.length; i ++ ) {
327
+
328
+ this.passes[ i ].setSize( effectiveWidth, effectiveHeight );
329
+
330
+ }
331
+
332
+ }
333
+
334
+ /**
335
+ * Sets device pixel ratio. This is usually used for HiDPI device to prevent blurring output.
336
+ * Setting the pixel ratio will automatically resize the composer.
337
+ *
338
+ * @param {number} pixelRatio - The pixel ratio to set.
339
+ */
340
+ setPixelRatio( pixelRatio ) {
341
+
342
+ this._pixelRatio = pixelRatio;
343
+
344
+ this.setSize( this._width, this._height );
345
+
346
+ }
347
+
348
+ /**
349
+ * Frees the GPU-related resources allocated by this instance. Call this
350
+ * method whenever the composer is no longer used in your app.
351
+ */
352
+ dispose() {
353
+
354
+ this.renderTarget1.dispose();
355
+ this.renderTarget2.dispose();
356
+
357
+ this.copyPass.dispose();
358
+
359
+ }
360
+
361
+ }
362
+
363
+ export { EffectComposer };