diff --git a/docs/animation-architecture.md b/docs/animation-architecture.md index 5daa2c0..df431e5 100644 --- a/docs/animation-architecture.md +++ b/docs/animation-architecture.md @@ -4,18 +4,33 @@ The section is statically rendered by Astro. The heading, description, actions and technology labels are present in the first HTML response. The animated canvas is decorative and does not contain SEO-critical content. +The graph is borderless and covers the full hero section. The initial desktop camera places the demon on the right, while the canvas and pointer coordinate system continue across the whole section so a dragged node can stretch the graph far beyond its resting silhouette. + The graph uses two Three.js draw calls: -- `THREE.Points` with a custom shader for every node; +- `THREE.Points` with a custom shader for every body, face and spark node; - `THREE.LineSegments` for every connection. No post-processing pass is used. Soft glow is calculated inside the point fragment shader, which avoids an additional framebuffer and reduces GPU cost on laptops and mobile devices. ## Shape generation -The flame body is generated deterministically from a seeded pseudo-random function. Points are accepted inside a union of a rounded body and five tapered flame tongues. Arms, eyes, pupils and the mouth are generated from explicit curves so the face remains readable at every quality level. +The silhouette is generated deterministically from a seeded pseudo-random function. The body combines a broad rounded base, shoulders and five tapered flame tongues. The tall central tongue and raised side arms define the reference pose. + +The face is generated as explicit graph structures: + +- two large luminous eyes with outer rings, inner rings and dense fill nodes; +- dark pupil rings that preserve clear black centers; +- a wide open mouth with separate upper and lower contours plus red interior particles. + +Body points are excluded from the eye and mouth voids so the expression remains readable at every quality level. Larger hub nodes are distributed through the body to reproduce the visual hierarchy of an Obsidian graph. -The graph connects flame points to nearby neighbours. Face rings and mouth curves have stronger internal springs. Each facial group is also attached to nearby flame nodes. +Ambient fire uses two spark systems: + +- radial starburst clusters with a larger hub and connected satellites; +- paired embers distributed around the silhouette. + +Every generated node has at least one connection. The main body uses local nearest-neighbour edges, facial structures use stronger internal springs and selected face nodes attach back to the flame body. ## Physics @@ -24,13 +39,18 @@ Every node stores its current position, velocity and immutable target position. 1. spring forces along graph edges; 2. a target-anchor force that preserves the silhouette; 3. velocity damping; -4. a very small role-aware idle drift. +4. role-aware idle convection; +5. rendering-time flame and spark drift. -While a node is dragged, its target anchor is weakened for connected nodes up to four graph steps away. Influence decays by graph distance. The selected node follows the pointer directly, while its neighbours follow through the spring network. After release, target anchors restore the original silhouette. +Flame and arm nodes move with layered slow and fast waves. Spark nodes use a wider, softer movement range. Eyes, pupils and mouth nodes stay comparatively stable so the expression does not dissolve. + +While a node is dragged, its target anchor is weakened for connected nodes up to five graph steps away. Influence decays by graph distance. The selected node follows the pointer directly, while its neighbours follow through the spring network. After release, target anchors restore the original silhouette. ## Torch reveal -The code wall is a DOM layer behind the canvas. It remains transparent until a node is dragged. Pointer position updates CSS custom properties that control a radial mask. Pointer speed controls opacity, producing a torch-like reveal without rendering code into WebGL. +Six columns of small code cover the entire hero background in two DOM layers. The ambient layer remains barely visible across the section. The lit layer sits behind the WebGL graph and uses a radial CSS mask controlled by custom properties. + +Dragging a node updates the mask position, radius and opacity. Pointer speed increases both brightness and illuminated area, producing a torch-like reveal without rendering code into WebGL. Releasing the node fades the lit code back into darkness. ## Loading and fallbacks @@ -39,7 +59,8 @@ The first render displays an inline SVG graph generated from the same determinis The implementation also: - caps device pixel ratio; -- reduces node count for coarse pointers and small viewports; +- reduces the source node count for coarse pointers and small viewports; +- uses a responsive off-centre camera on desktop and a lower camera composition on narrow screens; - pauses animation when the document is hidden; - disposes GPU resources on disconnect; - preserves the SVG fallback after WebGL context failure; diff --git a/src/components/CalciferGraph.astro b/src/components/CalciferGraph.astro index 87688df..9e4d4ed 100644 --- a/src/components/CalciferGraph.astro +++ b/src/components/CalciferGraph.astro @@ -1,12 +1,12 @@ --- import { createCalciferGraph } from '../scripts/graph/generateGraph' -const fallbackGraph = createCalciferGraph({ nodeCount: 60, seed: 20260710 }) +const fallbackGraph = createCalciferGraph({ nodeCount: 180, seed: 20260710 }) const toColor = (color: readonly [number, number, number]): string => `rgb(${Math.round(color[0] * 255)} ${Math.round(color[1] * 255)} ${Math.round(color[2] * 255)})` -const codeWall = ` -type AgentTask = { +const codeColumns = [ + `type AgentTask = { scope: "frontend" evidence: Verification[] approvalRequired: boolean @@ -25,21 +25,134 @@ export const guardrails = { unapprovedPackages: false, fakeVerification: false, } -` + +const evidence = await verifyRoute({ + viewport: 375, + command: "npm run check", +})`, + `interface ProjectContext { + framework: "astro" | "react" | "next" + styling: "css-modules" | "scoped-css" + packageManager: "npm" + constraints: string[] +} + +export const inspectProject = async () => { + const files = await readRelevantFiles() + const patterns = inferExistingPatterns(files) + const risks = identifyChangeRisks(patterns) + + return { + files, + patterns, + risks, + } +} + +const scope = selectMinimumFiles({ + requestedChange, + dependencies, + existingComponents, +})`, + `const implementation = defineWorkflow({ + classify: classifyPrompt, + inspect: inspectExistingCode, + plan: createMilestones, + execute: implementApprovedScope, + review: selfReviewChanges, + verify: collectEvidence, +}) + +const report = { + changed: changedFiles, + why: implementationReason, + verified: verificationEvidence, + risks: remainingRisks, +} + +if (requiresNewPackage(report)) { + await requestApproval() +} + +return formatCompactResult(report)`, + `export const visualVerification = async () => { + const desktop = await captureViewport(1440, 900) + const tablet = await captureViewport(1024, 768) + const mobile = await captureViewport(390, 844) + + return compareAgainstIntent({ + desktop, + tablet, + mobile, + checks: [ + "layout", + "overflow", + "contrast", + "interaction", + ], + }) +} + +const result = await runExistingChecks() +assertEvidence(result)`, + `type ChangeDecision = + | { kind: "reuse"; source: string } + | { kind: "extend"; source: string } + | { kind: "create"; reason: string } + +const decision = await chooseImplementation({ + existingCode, + approvedScope, + projectConventions, +}) + +const patch = await applyDecision(decision) +const review = await inspectDiff(patch) + +return { + patch, + review, + verified: review.isSafe, +}`, + `const release = await prepareRelease({ + branch: "feat/calcifer-firefield-refinement", + target: "main", + checks: [ + "types", + "tests", + "production-build", + ], +}) + +const pullRequest = await openPullRequest({ + title: release.title, + summary: release.summary, + evidence: release.evidence, + risks: release.risks, +}) + +export default pullRequest`, +] --- - + + + - {fallbackGraph.edges.slice(0, 260).map((edge) => { + {fallbackGraph.edges.slice(0, 1400).map((edge) => { const source = fallbackGraph.nodes[edge.source] const target = fallbackGraph.nodes[edge.target] @@ -54,8 +167,8 @@ export const guardrails = { x2={target.x} y2={target.y} stroke={toColor(source.color)} - stroke-opacity="0.22" - stroke-width="0.006" + stroke-opacity={source.role === 'pupil' ? '0.04' : '0.28'} + stroke-width="0.0048" /> ) })} @@ -63,9 +176,9 @@ export const guardrails = { ))} @@ -73,8 +186,8 @@ export const guardrails = { - The graph is decorative. Drag a visible node to pull its connected neighbours and reveal code in - the darkness behind it. + The graph is decorative. Drag a visible node to stretch the connected fire demon and reveal the + code hidden in the darkness behind it. @@ -103,9 +216,9 @@ export const guardrails = { } if (typeof idleWindow.requestIdleCallback === 'function') { - this.idleId = idleWindow.requestIdleCallback(() => void this.load(), { timeout: 1600 }) + this.idleId = idleWindow.requestIdleCallback(() => void this.load(), { timeout: 1400 }) } else { - this.timeoutId = globalThis.setTimeout(() => void this.load(), 900) + this.timeoutId = globalThis.setTimeout(() => void this.load(), 760) } } @@ -157,24 +270,17 @@ export const guardrails = {