Skip to content

Commit 930ccab

Browse files
committed
Animate Orleans grain networks and routed request streams
1 parent b3933cf commit 930ccab

6 files changed

Lines changed: 100 additions & 28 deletions

File tree

‎docs/Features/BenchmarkComparisons.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1388,3 +1388,7 @@ site qualification and actual Website/Pages delivery evidence remain required.
13881388
REQ-BC-SCENE-001 / AC-BC-SCENE-001 / TASK-BC-SCENE-001 replace the model-card carousel with three illustrative silo/node groups, eighteen distinct grain activations and thirty intra/inter-silo links. Each node retains its own storage base; grain routing must not suggest migration of storage handles or live telemetry. Desktop and mobile SVG posters convey the same concepts. Native projected DOM labels and the canonical SVG mark remain sharp across viewport/DPR changes. Motion, pause, reduced motion, visibility, disposal and existing renderer budgets remain mandatory.
13891389

13901390
ADR-040 owns this presentation-only replacement. Files: scene-geometry.mjs owns topology and motion; scene-lifecycle.mjs/scene-observers.mjs project labels and report actual graph counts; index.html/scene.css own semantics and presentation; existing cluster-poster assets own fallback. SiteVectorAssetTests and SiteBrowserVectorAssetAssertions verify actual asset semantics, native graph counts, label containment and Retina poses through Aspire. Existing scene suites retain lifecycle/budget gates. Ordered delivery: specify, replace assets/graph, native browser qualification, scoped checkpoint, Website qualification and Pages verification. Rollback is a reviewed source revert; no alternative scene implementation is retained.
1391+
1392+
## Owner clarification: replace motion inside the existing hero, 2026-10-06
1393+
1394+
REQ/AC/TASK-BC-SCENE-002 supersedes the first graph's subtle breathing motion. Keep the screenshot-identified hero figure and surrounding landing sections; do not add another block. Replace its animation with an expressive native Three.js cluster choreography: grains rotate within three fixed transparent silos, curved graph links track those grains, moving request packets follow real links and node-local storage lights on the commit phase. The scene is conceptual rather than live telemetry. Preserve the 3/18/30 graph contract, canonical vector mark, crisp contained labels, reduced motion/pause/visibility/disposal and all renderer budgets. Existing native scene assertions verify running/paused frames, graph topology and responsive bounds; browser review must visibly confirm travel over successive frames. Bootstrap fragment navigation and instant test viewport setup avoid inspecting transitional offscreen frames. ADR-040 retains the same source ownership, staged verification and Pages delivery contract.

‎site/AGENTS.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,7 @@
6666
- The current four workflows are Build and Tests (`build-and-tests.yml`), Benchmarks (`benchmarks.yml`), Website (`website.yml`) and prepared manual Release (`release.yml`). Only Website builds, qualifies and publishes the site on trusted main push/manual; Benchmarks ends with a bounded Website dispatch and Website has no `workflow_run` executor subscription. These explicit owner corrections supersede every earlier three-workflow, filename, executor, placement and completion-subscription clause; every unrelated suite, permission and qualification rule remains mandatory.
6767
- The sole measured producer contract is the current 1,386-worker/2,530-input cohort in ADR-076. Its executed dependency manifest contains exactly 80 current source paths. Remove old-plan readers, schemas, event-file parsers and their exclusive fixtures or inventory entries under the root owner-only migration/legacy super rule. This supersedes earlier 270/277 and old-reader requirements only; retain bounded authenticated REST producer/artifact provenance, exact current source and input hashes, failed/null accounting, strict selected-evidence rejection, freshness and unchanged 80/70/90 coverage thresholds.
6868
- When no current authenticated producer is ready, qualify the complete content-only site without figures. Real native TUnit/Node/Chrome operations, no skips in each applicable suite, source/coverage inventories and needs-gated least-privilege Pages remain required. Local tests enter the same Aspire-owned AppHost and are development evidence; genuine exact-source Linux/provider proof closes delivery. Controlled rejection inputs cannot become published measurements.
69+
70+
## Owner correction: screenshot-identified animation, 2026-10-06
71+
72+
- When the owner identifies an existing visual block in a screenshot and asks for another animation, replace that block's animation while preserving its surrounding landing layout. Do not infer a separate new block. A requested cluster illustration must show meaningful moving requests/grains/graph interactions, rather than a mostly static arrangement of cards or shapes.

‎site/Features/BenchmarkComparisons/scene-geometry.mjs‎

Lines changed: 88 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export const SCENE = Object.freeze({
22
limits: Object.freeze({ maxDevicePixelRatio: 1.5, maxBufferPixels: 1_000_000, maxDrawCalls: 30, maxTriangles: 5_000,
33
settleMilliseconds: 500, settleRenderMilliseconds: 450, maxSpinStepMilliseconds: 50 }),
4-
world: Object.freeze({ coreSize: 1.05, coreY: 1.9, cameraFov: 24, cameraNear: 0.1, cameraFar: 60,
4+
world: Object.freeze({ coreSize: 0.8, coreY: 2.0, cameraFov: 24, cameraNear: 0.1, cameraFar: 60,
55
cameraX: 0, cameraY: 2.5, cameraZ: 11.2, cameraLookY: 0.15, cameraAspect: 1,
66
pointerYaw: 0.12, pointerPitch: 0.05, smoothingDivisor: 115, millisecondsPerSecond: 1000 }),
77
core: Object.freeze({ selector: '.cluster-core', perspectiveScaleIndex: 5, matrixPrefix: 'matrix3d(', matrixSuffix: ')',
@@ -53,28 +53,35 @@ export function createSceneGraph(THREE) {
5353
const root = new THREE.Group();
5454
const resources = new Set();
5555
const own = value => (resources.add(value), value);
56-
scene.add(root, new THREE.AmbientLight(0xffffff, 2));
56+
scene.add(root, new THREE.AmbientLight(0xffffff, 1.8));
5757
const light = new THREE.DirectionalLight(0xffffff, 3);
5858
light.position.set(-3, 6, 5);
5959
scene.add(light);
6060
const nodes = SILOS.map((position, index) => createSilo(THREE, own, root, position, COLORS[index]));
6161
const positions = SILOS.flatMap(silo => GRAINS.map(grain => new THREE.Vector3(...grain).add(new THREE.Vector3(...silo))));
6262
const grains = new THREE.InstancedMesh(own(new THREE.SphereGeometry(0.12, 12, 8)),
63-
own(new THREE.MeshStandardNodeMaterial({ roughness: 0.3, metalness: 0.12 })), positions.length);
63+
own(new THREE.MeshStandardNodeMaterial({ roughness: 0.22, metalness: 0.16 })), positions.length);
6464
own(grains);
6565
const matrix = new THREE.Matrix4();
66-
positions.forEach((position, index) => {
67-
grains.setMatrixAt(index, matrix.makeTranslation(position.x, position.y, position.z));
68-
grains.setColorAt(index, new THREE.Color(COLORS[Math.floor(index / GRAINS.length)]));
69-
});
66+
const color = new THREE.Color();
67+
const white = new THREE.Color(0xffffff);
7068
root.add(grains);
7169
const links = SILOS.flatMap((_, silo) => EDGES.map(([a, b]) => [silo * GRAINS.length + a, silo * GRAINS.length + b]));
7270
links.push([1, 13], [7, 16], [5, 11]);
73-
const vertices = links.flatMap(([a, b]) => [...positions[a].toArray(), ...positions[b].toArray()]);
7471
const geometry = own(new THREE.BufferGeometry());
75-
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
76-
const lineMaterial = own(new THREE.LineBasicNodeMaterial({ color: 0x777486, transparent: true, opacity: 0.48 }));
77-
root.add(new THREE.LineSegments(geometry, lineMaterial));
72+
const lines = new THREE.Float32BufferAttribute(new Float32Array((27 + 3 * 24) * 6), 3);
73+
geometry.setAttribute('position', lines);
74+
root.add(new THREE.LineSegments(geometry,
75+
own(new THREE.LineBasicNodeMaterial({ color: 0x8b8299, transparent: true, opacity: 0.5 }))));
76+
const packets = new THREE.InstancedMesh(own(new THREE.SphereGeometry(0.085, 6, 4)),
77+
own(new THREE.MeshBasicNodeMaterial({ color: 0xffffff })), 24);
78+
own(packets);
79+
for (let index = 0; index < packets.count; index++) {
80+
color.setHex(0x8f7baa).lerp(white, (index % 8) / 9);
81+
packets.setColorAt(index, color);
82+
}
83+
root.add(packets);
84+
const point = new THREE.Vector3();
7885
const projectLabels = createLabelProjection(THREE, root, camera);
7986
const projectCore = createCoreProjection(THREE, root, camera);
8087
const resize = aspect => {
@@ -83,37 +90,91 @@ export function createSceneGraph(THREE) {
8390
camera.lookAt(0, SCENE.world.cameraLookY, 0);
8491
camera.updateProjectionMatrix();
8592
};
93+
const animate = seconds => {
94+
animateGrains(seconds, positions, grains, matrix, color, white);
95+
updateLinks(links, positions, lines, point);
96+
animatePackets(seconds, links, positions, packets, matrix, point);
97+
nodes.forEach((node, index) => {
98+
node.emissiveIntensity = Math.pow(Math.max(0, Math.sin(seconds * 2.1 - index * 0.35)), 8) * 0.7;
99+
});
100+
};
86101
resize(1);
102+
animate(0);
87103
return {
88-
scene, camera, root, projectCore, projectLabels, resize,
104+
scene, camera, root, projectCore, projectLabels, resize, animate,
89105
counts: { silos: nodes.length, grains: positions.length, links: links.length },
90-
animate: seconds => {
91-
positions.forEach((position, index) => {
92-
const scale = 1 + Math.sin(seconds * 1.7 + index * 0.9) * 0.12;
93-
matrix.makeScale(scale, scale, scale).setPosition(position);
94-
grains.setMatrixAt(index, matrix);
95-
});
96-
grains.instanceMatrix.needsUpdate = true;
97-
lineMaterial.opacity = 0.42 + Math.sin(seconds * 0.7) * 0.06;
98-
},
99106
dispose: () => { for (const value of resources) value.dispose(); resources.clear(); },
100107
};
101108
}
102109

110+
function animateGrains(seconds, positions, grains, matrix, color, white) {
111+
positions.forEach((position, index) => {
112+
const silo = Math.floor(index / GRAINS.length);
113+
const local = GRAINS[index % GRAINS.length];
114+
const angle = seconds * (silo === 1 ? -0.28 : 0.23) + silo * 0.18;
115+
position.set(SILOS[silo][0] + local[0] * Math.cos(angle) - local[2] * Math.sin(angle),
116+
SILOS[silo][1] + local[1] + Math.sin(seconds * 1.2 + index) * 0.045,
117+
SILOS[silo][2] + local[0] * Math.sin(angle) + local[2] * Math.cos(angle));
118+
const activity = Math.pow(Math.max(0, Math.sin(seconds * 2.1 - index * 0.45)), 6);
119+
const scale = 1 + activity * 0.45;
120+
matrix.makeScale(scale, scale, scale).setPosition(position);
121+
grains.setMatrixAt(index, matrix);
122+
color.setHex(COLORS[silo]).lerp(white, activity * 0.35);
123+
grains.setColorAt(index, color);
124+
});
125+
grains.instanceMatrix.needsUpdate = true;
126+
grains.instanceColor.needsUpdate = true;
127+
}
128+
129+
function routePoint(link, positions, progress, index, point) {
130+
point.copy(positions[link[0]]).lerp(positions[link[1]], progress);
131+
if (index >= 27) point.y += Math.sin(progress * Math.PI) * 0.8;
132+
return point;
133+
}
134+
135+
function updateLinks(links, positions, attribute, point) {
136+
let vertex = 0;
137+
links.forEach((link, index) => {
138+
const segments = index < 27 ? 1 : 24;
139+
for (let segment = 0; segment < segments; segment++) {
140+
routePoint(link, positions, segment / segments, index, point);
141+
attribute.setXYZ(vertex++, point.x, point.y, point.z);
142+
routePoint(link, positions, (segment + 1) / segments, index, point);
143+
attribute.setXYZ(vertex++, point.x, point.y, point.z);
144+
}
145+
});
146+
attribute.needsUpdate = true;
147+
}
148+
149+
function animatePackets(seconds, links, positions, packets, matrix, point) {
150+
for (let index = 0; index < packets.count; index++) {
151+
const route = Math.floor(index / 8);
152+
const trail = index % 8;
153+
const progress = ((seconds * 0.34 + route / 3 - trail * 0.016) % 1 + 1) % 1;
154+
const linkIndex = 27 + route;
155+
routePoint(links[linkIndex], positions, progress, linkIndex, point);
156+
const scale = 1 - trail * 0.115;
157+
matrix.makeScale(scale, scale, scale).setPosition(point);
158+
packets.setMatrixAt(index, matrix);
159+
}
160+
packets.instanceMatrix.needsUpdate = true;
161+
}
162+
103163
function createSilo(THREE, own, root, position, color) {
104164
const group = new THREE.Group();
105165
group.position.set(...position);
106-
const glass = new THREE.Mesh(own(new THREE.BoxGeometry(1.7, 1.65, 0.95)),
166+
const glass = new THREE.Mesh(own(new THREE.CylinderGeometry(0.9, 0.9, 1.65, 24)),
107167
own(new THREE.MeshBasicNodeMaterial({ color, transparent: true, opacity: 0.13, depthWrite: false })));
108168
glass.position.y = 0.82;
109-
const outline = new THREE.LineSegments(own(new THREE.EdgesGeometry(glass.geometry)),
110-
own(new THREE.LineBasicNodeMaterial({ color, transparent: true, opacity: 0.55 })));
169+
const outline = new THREE.LineSegments(own(new THREE.EdgesGeometry(glass.geometry, 20)),
170+
own(new THREE.LineBasicNodeMaterial({ color, transparent: true, opacity: 0.65 })));
111171
outline.position.copy(glass.position);
112-
const base = new THREE.Mesh(own(new THREE.BoxGeometry(1.85, 0.12, 1.1)),
113-
own(new THREE.MeshStandardNodeMaterial({ color: 0x34343b, roughness: 0.6, metalness: 0.2 })));
172+
const material = own(new THREE.MeshStandardNodeMaterial({ color: 0x34343b, roughness: 0.5, metalness: 0.25,
173+
emissive: 0xa28ebc, emissiveIntensity: 0 }));
174+
const base = new THREE.Mesh(own(new THREE.CylinderGeometry(0.98, 0.98, 0.16, 24)), material);
114175
group.add(glass, outline, base);
115176
root.add(group);
116-
return group;
177+
return material;
117178
}
118179

119180
function createLabelProjection(THREE, root, camera) {

‎tests/KeyLoad.SiteTests/Features/BenchmarkComparisons/Assertions/SiteBrowserVectorAssetAssertions.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ internal static class SiteBrowserVectorAssetAssertions
66
{
77
public static async Task AssertReadyMarkAsync(SiteBrowserCdpClient cdp, CancellationToken cancellationToken)
88
{
9+
await Assert.That(await cdp.WaitForExpressionAsync(SiteBrowserSceneTokens.GraphReadyPredicate,
10+
cancellationToken)).IsTrue();
911
var state = await ReadMarkAsync(cdp, cancellationToken);
1012
await Assert.That(state.GetProperty(SiteVectorAssetTokens.CountField).GetInt32()).IsEqualTo(SiteTokens.One);
1113
await Assert.That(state.GetProperty(SiteVectorAssetTokens.LoadedField).GetBoolean()).IsTrue();

‎tests/KeyLoad.SiteTests/Features/BenchmarkComparisons/Contracts/SiteBrowserSceneTokens.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ internal static class SiteBrowserSceneTokens
1313
public const string VisibleField = "visible";
1414
public const string ContainedField = "contained";
1515
public const string LabelsHiddenField = "labelsHidden";
16+
public const string GraphReadyPredicate = "document.querySelector('#cluster-scene')?.dataset.sceneState === 'ready' && document.querySelectorAll('#cluster-scene [data-silo-label]').length === 3 && [...document.querySelectorAll('#cluster-scene [data-silo-label]')].every(label => {const style=getComputedStyle(label);return style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity)>0;})";
1617
public const string ClusterGraphScript = """
1718
(() => {
1819
const host = document.querySelector('#cluster-scene');

‎tests/KeyLoad.SiteTests/Features/BenchmarkComparisons/Contracts/SiteBrowserUiTokens.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ internal static class SiteBrowserUiTokens
5252
public const string TrianglesField = "triangles";
5353
public const string NoScriptSelector = "noscript";
5454
public const string OverflowScript = "document.documentElement.scrollWidth>window.innerWidth";
55-
public const string SceneScrollScript = "document.querySelector('#cluster-scene')?.scrollIntoView({block:'center'})";
55+
public const string SceneScrollScript = "document.querySelector('#cluster-scene')?.scrollIntoView({block:'center',behavior:'instant'})";
5656
public const string SceneSnapshotScript = "({backend:document.querySelector('#cluster-scene')?.getAttribute('data-graphics-backend'),status:document.querySelector('" + SelectorSceneStatus + "')?.textContent,frame:document.querySelector('#cluster-scene')?.getAttribute('data-frame-state'),canvasCount:document.querySelectorAll('#cluster-scene canvas').length,bufferPixels:Number(document.querySelector('#cluster-scene')?.getAttribute('data-buffer-pixels')??0),drawCalls:Number(document.querySelector('#cluster-scene')?.getAttribute('data-draw-calls')??0),triangles:Number(document.querySelector('#cluster-scene')?.getAttribute('data-triangles')??0)})";
5757
public const string MotionEnableScript = "(()=>{const e=document.querySelector('#scene-motion');if(!e||e.disabled)return false;if(e.getAttribute('aria-pressed')!=='true')e.click();return e.getAttribute('aria-pressed')==='true'})()";
5858
public const string MotionDisableScript = "(()=>{const e=document.querySelector('#scene-motion');if(!e||e.disabled)return false;if(e.getAttribute('aria-pressed')==='true')e.click();return e.getAttribute('aria-pressed')==='false'})()";

0 commit comments

Comments
 (0)