From e624c0315af3dc856c8fb1e069a8b90d38205017 Mon Sep 17 00:00:00 2001 From: Yevgeniy Tyan <102475316+ytvee@users.noreply.github.com> Date: Fri, 10 Jul 2026 18:12:48 +0300 Subject: [PATCH 1/9] refactor: extend graph roles for ambient sparks --- src/scripts/graph/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scripts/graph/types.ts b/src/scripts/graph/types.ts index f4252d8..b007399 100644 --- a/src/scripts/graph/types.ts +++ b/src/scripts/graph/types.ts @@ -1,4 +1,4 @@ -export type GraphNodeRole = 'flame' | 'arm' | 'eye' | 'pupil' | 'mouth' +export type GraphNodeRole = 'flame' | 'arm' | 'eye' | 'pupil' | 'mouth' | 'spark' export type Rgb = readonly [number, number, number] From 6f30f5bf64313f7a771fabe42d3b2ad39153aaf9 Mon Sep 17 00:00:00 2001 From: Yevgeniy Tyan <102475316+ytvee@users.noreply.github.com> Date: Fri, 10 Jul 2026 18:14:11 +0300 Subject: [PATCH 2/9] feat: rebuild demon silhouette from dense fire graph --- src/scripts/graph/generateGraph.ts | 545 +++++++++++++++++++++++------ 1 file changed, 443 insertions(+), 102 deletions(-) diff --git a/src/scripts/graph/generateGraph.ts b/src/scripts/graph/generateGraph.ts index 908b91c..e36d759 100644 --- a/src/scripts/graph/generateGraph.ts +++ b/src/scripts/graph/generateGraph.ts @@ -29,6 +29,16 @@ const mixColor = (from: Rgb, to: Rgb, amount: number): Rgb => [ mix(from[2], to[2], amount), ] +const pointInsideEllipse = ( + x: number, + y: number, + centerX: number, + centerY: number, + radiusX: number, + radiusY: number, +): boolean => + Math.pow((x - centerX) / radiusX, 2) + Math.pow((y - centerY) / radiusY, 2) <= 1 + const pointInsideTongue = ( x: number, y: number, @@ -45,42 +55,63 @@ const pointInsideTongue = ( } const center = centerX + lean * progress - const taper = Math.pow(1 - progress, 0.72) - const ripple = 0.86 + Math.sin(progress * Math.PI * 2.2 + centerX * 8) * 0.14 + const taper = Math.pow(1 - progress, 0.68) + const ripple = 0.86 + Math.sin(progress * Math.PI * 2.4 + centerX * 9) * 0.14 return Math.abs(x - center) <= width * taper * ripple } -const pointInsideFlame = (x: number, y: number): boolean => { - const body = Math.pow(x / 0.79, 2) + Math.pow((y + 0.14) / 0.82, 2) <= 1 - const lowerGlow = Math.pow(x / 0.65, 2) + Math.pow((y + 0.55) / 0.42, 2) <= 1 - const center = pointInsideTongue(x, y, 0.02, 0.2, 0.93, 0.34, 0.08) - const leftInner = pointInsideTongue(x, y, -0.34, 0.19, 0.66, 0.25, -0.08) - const rightInner = pointInsideTongue(x, y, 0.36, 0.18, 0.69, 0.23, 0.1) - const leftOuter = pointInsideTongue(x, y, -0.62, 0.02, 0.48, 0.18, -0.08) - const rightOuter = pointInsideTongue(x, y, 0.63, 0.04, 0.46, 0.17, 0.09) +const pointInsideSilhouette = (x: number, y: number): boolean => { + const body = pointInsideEllipse(x, y, 0, -0.16, 0.88, 0.72) + const base = pointInsideEllipse(x, y, 0, -0.62, 0.72, 0.32) + const leftShoulder = pointInsideEllipse(x, y, -0.68, -0.16, 0.29, 0.38) + const rightShoulder = pointInsideEllipse(x, y, 0.68, -0.16, 0.29, 0.38) + const center = pointInsideTongue(x, y, 0.02, 0.18, 1.12, 0.3, 0.08) + const leftInner = pointInsideTongue(x, y, -0.31, 0.18, 0.78, 0.24, -0.09) + const rightInner = pointInsideTongue(x, y, 0.34, 0.16, 0.76, 0.23, 0.12) + const leftOuter = pointInsideTongue(x, y, -0.58, 0.02, 0.58, 0.19, -0.13) + const rightOuter = pointInsideTongue(x, y, 0.61, 0.02, 0.6, 0.19, 0.12) + + return ( + body || + base || + leftShoulder || + rightShoulder || + center || + leftInner || + rightInner || + leftOuter || + rightOuter + ) +} + +const pointInsideFaceVoid = (x: number, y: number): boolean => { + const leftEye = pointInsideEllipse(x, y, -0.28, -0.17, 0.22, 0.2) + const rightEye = pointInsideEllipse(x, y, 0.28, -0.17, 0.22, 0.2) + const mouth = pointInsideEllipse(x, y, 0, -0.49, 0.39, 0.24) - return body || lowerGlow || center || leftInner || rightInner || leftOuter || rightOuter + return leftEye || rightEye || mouth } const getFlameColor = (x: number, y: number): Rgb => { - const centerDistance = clamp( - Math.sqrt(Math.pow(x / 0.86, 2) + Math.pow((y + 0.34) / 1.12, 2)), - 0, - 1, - ) - const heightHeat = clamp((y + 0.12) / 1.05, 0, 1) - const edgeHeat = clamp(Math.abs(x) / 0.82, 0, 1) - const heat = clamp(centerDistance * 0.45 + heightHeat * 0.42 + edgeHeat * 0.23, 0, 1) - const yellow: Rgb = [1, 0.82, 0.22] - const orange: Rgb = [1, 0.29, 0.035] - const red: Rgb = [0.72, 0.045, 0.018] - - if (heat < 0.58) { - return mixColor(yellow, orange, heat / 0.58) + const radial = clamp(Math.hypot(x / 0.98, (y + 0.25) / 1.32), 0, 1) + const height = clamp((y + 0.05) / 1.25, 0, 1) + const edge = clamp(Math.abs(x) / 0.94, 0, 1) + const heat = clamp(radial * 0.38 + height * 0.36 + edge * 0.28, 0, 1) + const cream: Rgb = [1, 0.86, 0.56] + const amber: Rgb = [1, 0.49, 0.08] + const orange: Rgb = [1, 0.19, 0.025] + const red: Rgb = [0.68, 0.025, 0.012] + + if (heat < 0.33) { + return mixColor(cream, amber, heat / 0.33) + } + + if (heat < 0.7) { + return mixColor(amber, orange, (heat - 0.33) / 0.37) } - return mixColor(orange, red, (heat - 0.58) / 0.42) + return mixColor(orange, red, (heat - 0.7) / 0.3) } const quadraticPoint = (start: Point, control: Point, end: Point, progress: number): Point => { @@ -133,7 +164,7 @@ const addEdge = ( target: number, stiffness: number, ): void => { - if (source === target) { + if (source === target || source < 0 || target < 0) { return } @@ -145,8 +176,8 @@ const addEdge = ( return } - const sourceNode = nodes[source] - const targetNode = nodes[target] + const sourceNode = nodes[first] + const targetNode = nodes[second] if (!sourceNode || !targetNode) { return @@ -156,7 +187,11 @@ const addEdge = ( edges.push({ source: first, target: second, - restLength: Math.hypot(sourceNode.x - targetNode.x, sourceNode.y - targetNode.y), + restLength: Math.hypot( + sourceNode.x - targetNode.x, + sourceNode.y - targetNode.y, + sourceNode.z - targetNode.z, + ), stiffness, }) } @@ -232,20 +267,23 @@ export const createCalciferGraph = ({ const edges: GraphEdgeSeed[] = [] const edgeKeys = new Set() const flameNodeIds: number[] = [] - const desiredFlameCount = Math.max(54, Math.floor(nodeCount * 0.72)) + const desiredFlameCount = Math.max(120, Math.floor(nodeCount * 0.78)) let attempts = 0 - while (flameNodeIds.length < desiredFlameCount && attempts < desiredFlameCount * 80) { + while (flameNodeIds.length < desiredFlameCount && attempts < desiredFlameCount * 120) { attempts += 1 - const x = mix(-0.92, 0.92, random()) - const y = mix(-0.96, 1.12, random()) + const x = mix(-0.99, 0.99, random()) + const y = mix(-0.92, 1.31, random()) - if (!pointInsideFlame(x, y)) { + if (!pointInsideSilhouette(x, y) || pointInsideFaceVoid(x, y)) { continue } - const coreBias = 1 - clamp(Math.hypot(x / 0.9, (y + 0.15) / 1.16), 0, 1) - const size = mix(3.2, 7.4, random() * 0.64 + coreBias * 0.36) + const centerBias = 1 - clamp(Math.hypot(x / 0.93, (y + 0.18) / 1.28), 0, 1) + const hub = random() < 0.105 + const size = hub + ? mix(7.2, 11.2, random()) + : mix(2.25, 5.15, random() * 0.72 + centerBias * 0.28) const id = addNode( nodes, random, @@ -253,123 +291,420 @@ export const createCalciferGraph = ({ { x, y }, size, getFlameColor(x, y), - mix(4.6, 7.8, 1 - coreBias), - mix(-0.03, 0.03, random()), + mix(5.2, 9.4, 1 - centerBias), + mix(-0.04, 0.05, random()), ) flameNodeIds.push(id) } const armNodeIds: number[] = [] - const armDefinitions = [ - { - start: { x: -0.66, y: -0.15 }, - control: { x: -0.98, y: -0.52 }, - end: { x: -1.19, y: -0.23 }, - }, - { - start: { x: 0.66, y: -0.15 }, - control: { x: 0.98, y: -0.52 }, - end: { x: 1.19, y: -0.23 }, - }, - ] - - for (const definition of armDefinitions) { - const arm: number[] = [] - - for (let index = 0; index < 11; index += 1) { - const progress = index / 10 - const point = quadraticPoint(definition.start, definition.control, definition.end, progress) + + for (const side of [-1, 1]) { + const mainArm: number[] = [] + const start = { x: side * 0.68, y: -0.08 } + const control = { x: side * 1.02, y: 0.04 } + const end = { x: side * 1.18, y: 0.55 } + + for (let index = 0; index < 16; index += 1) { + const progress = index / 15 + const point = quadraticPoint(start, control, end, progress) + point.x += Math.sin(progress * Math.PI * 2.2) * 0.025 * side + point.y += Math.sin(progress * Math.PI) * 0.035 const id = addNode( nodes, random, 'arm', point, - mix(4.4, 6.8, 1 - Math.abs(progress - 0.85)), - mixColor([1, 0.34, 0.045], [0.83, 0.07, 0.018], progress), - 6.8, - 0.015, + mix(4.2, 8.2, 0.35 + random() * 0.65), + mixColor([1, 0.42, 0.055], [0.78, 0.035, 0.014], progress), + 7.8, + 0.02, ) - arm.push(id) + mainArm.push(id) armNodeIds.push(id) if (index > 0) { - addEdge(edges, edgeKeys, nodes, arm[index - 1] ?? id, id, 16) + addEdge(edges, edgeKeys, nodes, mainArm[index - 1] ?? id, id, 17) } } - const root = arm[0] + for (let index = 0; index < 24; index += 1) { + const progress = random() + const center = quadraticPoint(start, control, end, progress) + const spread = mix(0.035, 0.12, Math.sin(progress * Math.PI)) + const point = { + x: center.x + mix(-spread, spread, random()), + y: center.y + mix(-spread, spread, random()), + } + const id = addNode( + nodes, + random, + 'arm', + point, + mix(2.4, 5.1, random()), + mixColor([1, 0.34, 0.035], [0.76, 0.025, 0.012], progress), + 7.2, + mix(-0.015, 0.04, random()), + ) + armNodeIds.push(id) + const mainTarget = mainArm[Math.min(mainArm.length - 1, Math.round(progress * 15))] + + if (mainTarget !== undefined) { + addEdge(edges, edgeKeys, nodes, id, mainTarget, 14) + } + } + + const root = mainArm[0] const rootNode = root === undefined ? undefined : nodes[root] if (root !== undefined && rootNode) { - const nearest = nearestNode(nodes, rootNode, new Set(['flame']), root) - addEdge(edges, edgeKeys, nodes, root, nearest, 17) + const target = nearestNode(nodes, rootNode, new Set(['flame']), root) + addEdge(edges, edgeKeys, nodes, root, target, 18) } } const eyeNodeIds: number[] = [] const pupilNodeIds: number[] = [] - for (const centerX of [-0.27, 0.27]) { - const ring: number[] = [] + for (const centerX of [-0.28, 0.28]) { + const centerY = -0.17 + const outerRing: number[] = [] + const innerRing: number[] = [] - for (let index = 0; index < 14; index += 1) { - const angle = (index / 14) * Math.PI * 2 + for (let index = 0; index < 30; index += 1) { + const angle = (index / 30) * Math.PI * 2 const point = { - x: centerX + Math.cos(angle) * 0.155, - y: -0.12 + Math.sin(angle) * 0.135, + x: centerX + Math.cos(angle) * 0.19, + y: centerY + Math.sin(angle) * 0.17, } - const id = addNode(nodes, random, 'eye', point, 5.9, [1, 0.95, 0.79], 12, 0.09) - ring.push(id) + const id = addNode( + nodes, + random, + 'eye', + point, + mix(4.1, 6.6, random()), + mixColor([1, 0.78, 0.36], [1, 0.98, 0.86], random() * 0.76), + 15, + 0.11, + ) + outerRing.push(id) + eyeNodeIds.push(id) + } + + for (let index = 0; index < 22; index += 1) { + const angle = (index / 22) * Math.PI * 2 + const point = { + x: centerX + Math.cos(angle) * 0.125, + y: centerY + Math.sin(angle) * 0.112, + } + const id = addNode( + nodes, + random, + 'eye', + point, + mix(3.4, 5.4, random()), + [1, 0.95, 0.79], + 15.5, + 0.12, + ) + innerRing.push(id) eyeNodeIds.push(id) } - for (let index = 0; index < ring.length; index += 1) { - const current = ring[index] - const next = ring[(index + 1) % ring.length] + for (let index = 0; index < outerRing.length; index += 1) { + const current = outerRing[index] + const next = outerRing[(index + 1) % outerRing.length] + const inner = innerRing[Math.round((index / outerRing.length) * innerRing.length) % innerRing.length] if (current !== undefined && next !== undefined) { - addEdge(edges, edgeKeys, nodes, current, next, 22) + addEdge(edges, edgeKeys, nodes, current, next, 24) + } + + if (current !== undefined && inner !== undefined && index % 2 === 0) { + addEdge(edges, edgeKeys, nodes, current, inner, 19) } } - for (let index = 0; index < 4; index += 1) { - const angle = (index / 4) * Math.PI * 2 + for (let index = 0; index < innerRing.length; index += 1) { + const current = innerRing[index] + const next = innerRing[(index + 1) % innerRing.length] + + if (current !== undefined && next !== undefined) { + addEdge(edges, edgeKeys, nodes, current, next, 23) + } + } + + for (let index = 0; index < 28; index += 1) { + const angle = random() * Math.PI * 2 + const radius = Math.sqrt(random()) const point = { - x: centerX + (centerX < 0 ? 0.025 : -0.025) + Math.cos(angle) * 0.035, - y: -0.135 + Math.sin(angle) * 0.035, + x: centerX + Math.cos(angle) * 0.16 * radius, + y: centerY + Math.sin(angle) * 0.142 * radius, + } + + if (Math.hypot((point.x - centerX) / 0.16, (point.y - centerY) / 0.142) < 0.34) { + continue } - const id = addNode(nodes, random, 'pupil', point, 6.8, [0.06, 0.045, 0.035], 16, 0.13) + + const id = addNode( + nodes, + random, + 'eye', + point, + mix(2.4, 4.5, random()), + mixColor([1, 0.76, 0.28], [1, 0.99, 0.9], random()), + 15.5, + 0.12, + ) + eyeNodeIds.push(id) + const target = nearestNode(nodes, point, new Set(['eye']), id) + addEdge(edges, edgeKeys, nodes, id, target, 17) + } + + const pupilCenterX = centerX + (centerX < 0 ? 0.015 : -0.015) + const pupilCenterY = centerY - 0.006 + const pupilRing: number[] = [] + + for (let index = 0; index < 12; index += 1) { + const angle = (index / 12) * Math.PI * 2 + const point = { + x: pupilCenterX + Math.cos(angle) * 0.055, + y: pupilCenterY + Math.sin(angle) * 0.052, + } + const id = addNode(nodes, random, 'pupil', point, 6.8, [0.012, 0.01, 0.009], 18, 0.16) + pupilRing.push(id) pupilNodeIds.push(id) + } - const ringTarget = ring[(index * 3 + 1) % ring.length] - if (ringTarget !== undefined) { - addEdge(edges, edgeKeys, nodes, id, ringTarget, 18) + for (let index = 0; index < pupilRing.length; index += 1) { + const current = pupilRing[index] + const next = pupilRing[(index + 1) % pupilRing.length] + + if (current !== undefined && next !== undefined) { + addEdge(edges, edgeKeys, nodes, current, next, 26) + } + } + + for (let index = 0; index < 4; index += 1) { + const pupil = pupilRing[index * 3] + const eye = innerRing[index * 5] + + if (pupil !== undefined && eye !== undefined) { + addEdge(edges, edgeKeys, nodes, pupil, eye, 16) + } + } + + for (const index of [1, 8, 16, 23]) { + const eye = outerRing[index] + + if (eye !== undefined) { + const eyeNode = nodes[eye] + const target = eyeNode + ? nearestNode(nodes, eyeNode, new Set(['flame']), eye) + : -1 + addEdge(edges, edgeKeys, nodes, eye, target, 10) } } } const mouthNodeIds: number[] = [] + const upperLip: number[] = [] + const lowerLip: number[] = [] + + for (let index = 0; index < 24; index += 1) { + const progress = index / 23 + const point = quadraticPoint( + { x: -0.34, y: -0.42 }, + { x: 0, y: -0.54 }, + { x: 0.34, y: -0.42 }, + progress, + ) + const id = addNode( + nodes, + random, + 'mouth', + point, + mix(3.5, 5.8, random()), + [0.92, 0.09, 0.022], + 16, + 0.14, + ) + upperLip.push(id) + mouthNodeIds.push(id) + } - for (let index = 0; index < 15; index += 1) { - const progress = index / 14 + for (let index = 0; index < 28; index += 1) { + const progress = index / 27 const point = quadraticPoint( - { x: -0.21, y: -0.39 }, - { x: 0, y: -0.56 }, - { x: 0.21, y: -0.39 }, + { x: -0.34, y: -0.43 }, + { x: 0, y: -0.75 }, + { x: 0.34, y: -0.43 }, progress, ) - const id = addNode(nodes, random, 'mouth', point, 5.2, [0.31, 0.025, 0.018], 15, 0.12) + const id = addNode( + nodes, + random, + 'mouth', + point, + mix(3.2, 5.4, random()), + mixColor([0.96, 0.12, 0.026], [0.58, 0.018, 0.012], Math.sin(progress * Math.PI)), + 16, + 0.14, + ) + lowerLip.push(id) mouthNodeIds.push(id) + } - if (index > 0) { - addEdge(edges, edgeKeys, nodes, mouthNodeIds[index - 1] ?? id, id, 22) + for (const contour of [upperLip, lowerLip]) { + for (let index = 1; index < contour.length; index += 1) { + addEdge(edges, edgeKeys, nodes, contour[index - 1] ?? -1, contour[index] ?? -1, 24) } } - connectNearestNeighbours(nodes, edges, edgeKeys, flameNodeIds, 3, 0.31, 10.5) - connectNearestNeighbours(nodes, edges, edgeKeys, armNodeIds, 2, 0.3, 14) + addEdge(edges, edgeKeys, nodes, upperLip[0] ?? -1, lowerLip[0] ?? -1, 25) + addEdge( + edges, + edgeKeys, + nodes, + upperLip[upperLip.length - 1] ?? -1, + lowerLip[lowerLip.length - 1] ?? -1, + 25, + ) + + for (let index = 0; index < 32; index += 1) { + const angle = random() * Math.PI * 2 + const radius = Math.sqrt(random()) + const point = { + x: Math.cos(angle) * 0.29 * radius, + y: -0.53 + Math.sin(angle) * 0.13 * radius, + } + const id = addNode( + nodes, + random, + 'mouth', + point, + mix(2.2, 4.3, random()), + mixColor([0.56, 0.012, 0.008], [1, 0.2, 0.035], random() * 0.72), + 16.5, + 0.145, + ) + mouthNodeIds.push(id) + const target = nearestNode(nodes, point, new Set(['mouth']), id) + addEdge(edges, edgeKeys, nodes, id, target, 18) + } + + for (const index of [0, 7, 15, 23]) { + const mouth = upperLip[index] + + if (mouth !== undefined) { + const mouthNode = nodes[mouth] + const target = mouthNode + ? nearestNode(nodes, mouthNode, new Set(['flame']), mouth) + : -1 + addEdge(edges, edgeKeys, nodes, mouth, target, 10) + } + } + + const sparkNodeIds: number[] = [] + const clusterCount = Math.max(12, Math.floor(nodeCount / 30)) + + for (let clusterIndex = 0; clusterIndex < clusterCount; clusterIndex += 1) { + const angle = mix(0.04 * Math.PI, 0.96 * Math.PI, random()) + const radiusX = mix(1.04, 1.46, random()) + const radiusY = mix(0.9, 1.32, random()) + const center = { + x: Math.cos(angle) * radiusX, + y: Math.sin(angle) * radiusY + mix(-0.08, 0.16, random()), + } + const hub = addNode( + nodes, + random, + 'spark', + center, + mix(5.4, 9.6, random()), + mixColor([1, 0.27, 0.025], [1, 0.82, 0.36], random()), + mix(4.4, 7.2, random()), + mix(-0.03, 0.08, random()), + ) + sparkNodeIds.push(hub) + const satelliteCount = 5 + Math.floor(random() * 7) + let previous = hub + + for (let satelliteIndex = 0; satelliteIndex < satelliteCount; satelliteIndex += 1) { + const satelliteAngle = random() * Math.PI * 2 + const distance = mix(0.035, 0.13, random()) + const point = { + x: center.x + Math.cos(satelliteAngle) * distance, + y: center.y + Math.sin(satelliteAngle) * distance, + } + const satellite = addNode( + nodes, + random, + 'spark', + point, + mix(1.8, 4.1, random()), + mixColor([1, 0.18, 0.018], [1, 0.9, 0.55], random()), + mix(3.8, 6.2, random()), + mix(-0.04, 0.1, random()), + ) + sparkNodeIds.push(satellite) + addEdge(edges, edgeKeys, nodes, hub, satellite, 8.5) + + if (satelliteIndex > 0 && random() > 0.42) { + addEdge(edges, edgeKeys, nodes, previous, satellite, 6.5) + } + + previous = satellite + } + } + + const emberPairCount = Math.max(18, Math.floor(nodeCount / 18)) + + for (let pairIndex = 0; pairIndex < emberPairCount; pairIndex += 1) { + let x = mix(-1.46, 1.46, random()) + let y = mix(-0.28, 1.4, random()) + let safety = 0 + + while (pointInsideSilhouette(x, y) && safety < 30) { + x = mix(-1.46, 1.46, random()) + y = mix(-0.28, 1.4, random()) + safety += 1 + } + + const first = addNode( + nodes, + random, + 'spark', + { x, y }, + mix(1.7, 3.6, random()), + mixColor([1, 0.16, 0.015], [1, 0.78, 0.32], random()), + mix(3.6, 5.5, random()), + mix(-0.04, 0.08, random()), + ) + const direction = random() * Math.PI * 2 + const distance = mix(0.025, 0.075, random()) + const second = addNode( + nodes, + random, + 'spark', + { + x: x + Math.cos(direction) * distance, + y: y + Math.sin(direction) * distance, + }, + mix(1.4, 2.8, random()), + [1, 0.52, 0.08], + mix(3.6, 5.5, random()), + mix(-0.04, 0.08, random()), + ) + sparkNodeIds.push(first, second) + addEdge(edges, edgeKeys, nodes, first, second, 5.5) + } + + connectNearestNeighbours(nodes, edges, edgeKeys, flameNodeIds, 4, 0.245, 10.5) + connectNearestNeighbours(nodes, edges, edgeKeys, armNodeIds, 3, 0.18, 13) const connectedNodeIds = new Set() + for (const edge of edges) { connectedNodeIds.add(edge.source) connectedNodeIds.add(edge.target) @@ -381,23 +716,29 @@ export const createCalciferGraph = ({ } const node = nodes[nodeId] + if (!node) { continue } - const nearest = nearestNode(nodes, node, new Set(['flame']), nodeId) - addEdge(edges, edgeKeys, nodes, nodeId, nearest, 10.5) + const target = nearestNode(nodes, node, new Set(['flame']), nodeId) + addEdge(edges, edgeKeys, nodes, nodeId, target, 10.5) } - for (const nodeId of [...eyeNodeIds, ...pupilNodeIds, ...mouthNodeIds]) { + for (const nodeId of [...eyeNodeIds, ...pupilNodeIds, ...mouthNodeIds, ...sparkNodeIds]) { + if (connectedNodeIds.has(nodeId)) { + continue + } + const node = nodes[nodeId] if (!node) { continue } - const nearest = nearestNode(nodes, node, new Set(['flame']), nodeId) - addEdge(edges, edgeKeys, nodes, nodeId, nearest, 8) + const roles = new Set([node.role]) + const target = nearestNode(nodes, node, roles, nodeId) + addEdge(edges, edgeKeys, nodes, nodeId, target, node.role === 'spark' ? 5.5 : 12) } return { nodes, edges } From 11ad0c4f91d0ad454adece52cd682aa578e00069 Mon Sep 17 00:00:00 2001 From: Yevgeniy Tyan <102475316+ytvee@users.noreply.github.com> Date: Fri, 10 Jul 2026 18:14:32 +0300 Subject: [PATCH 3/9] feat: add convection-like idle graph motion --- src/scripts/graph/physics.ts | 49 +++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/src/scripts/graph/physics.ts b/src/scripts/graph/physics.ts index 16ff6ec..85de55d 100644 --- a/src/scripts/graph/physics.ts +++ b/src/scripts/graph/physics.ts @@ -58,6 +58,22 @@ const applyEdgeSpring = ( accelerationZ[edge.target] = (accelerationZ[edge.target] ?? 0) - forceZ } +const getRoleMotion = (role: PhysicsState['nodes'][number]['role']): number => { + if (role === 'spark') { + return 2.15 + } + + if (role === 'arm') { + return 1.35 + } + + if (role === 'flame') { + return 1 + } + + return 0.12 +} + export const stepGraphPhysics = ( state: PhysicsState, edges: GraphEdgeSeed[], @@ -92,28 +108,33 @@ export const stepGraphPhysics = ( continue } - const faceScale = node.role === 'flame' || node.role === 'arm' ? 1 : 0.2 - const driftX = Math.sin(elapsedSeconds * 1.17 + node.phase) * 0.013 * faceScale * motionScale - const driftY = Math.cos(elapsedSeconds * 1.43 + node.phase * 1.31) * 0.015 * faceScale * motionScale + const roleMotion = getRoleMotion(node.role) + const slowWave = Math.sin(elapsedSeconds * 1.05 + node.phase) + const fastWave = Math.sin(elapsedSeconds * 3.6 + node.phase * 1.73) + const verticalWave = Math.cos(elapsedSeconds * 1.34 + node.phase * 1.21) + const driftX = + (slowWave * 0.014 + fastWave * 0.0055) * roleMotion * motionScale + const driftY = + (verticalWave * 0.017 + fastWave * 0.0045) * roleMotion * motionScale + + (node.role === 'spark' ? Math.sin(elapsedSeconds * 0.72 + node.phase) * 0.024 : 0) const influence = options.dragInfluence?.[index] ?? 0 - const anchorScale = 1 - influence * 0.72 + const anchorScale = 1 - influence * 0.78 const anchor = node.anchorStrength * anchorScale const targetX = node.targetX + driftX const targetY = node.targetY + driftY - const targetZ = node.targetZ + Math.sin(elapsedSeconds * 0.88 + node.phase) * 0.008 * faceScale + const targetZ = + node.targetZ + + Math.sin(elapsedSeconds * 0.92 + node.phase) * 0.009 * roleMotion * motionScale - accelerationX[index] = - (accelerationX[index] ?? 0) + (targetX - node.x) * anchor - accelerationY[index] = - (accelerationY[index] ?? 0) + (targetY - node.y) * anchor - accelerationZ[index] = - (accelerationZ[index] ?? 0) + (targetZ - node.z) * anchor + accelerationX[index] = (accelerationX[index] ?? 0) + (targetX - node.x) * anchor + accelerationY[index] = (accelerationY[index] ?? 0) + (targetY - node.y) * anchor + accelerationZ[index] = (accelerationZ[index] ?? 0) + (targetZ - node.z) * anchor node.vx += (accelerationX[index] ?? 0) * delta node.vy += (accelerationY[index] ?? 0) * delta node.vz += (accelerationZ[index] ?? 0) * delta - const damping = Math.exp(-5.8 * delta) + const damping = Math.exp(-(node.role === 'spark' ? 4.65 : 5.55) * delta) node.vx *= damping node.vy *= damping node.vz *= damping @@ -143,11 +164,11 @@ export const createDragInfluence = ( while (queue.length > 0) { const current = queue.shift() - if (!current || current.depth > 4) { + if (!current || current.depth > 5) { continue } - influence[current.index] = Math.max(influence[current.index] ?? 0, Math.pow(0.58, current.depth)) + influence[current.index] = Math.max(influence[current.index] ?? 0, Math.pow(0.62, current.depth)) for (const neighbour of adjacency[current.index] ?? []) { if (visited.has(neighbour)) { From d7259fb936c31e05c33196b72c6c53777976df9c Mon Sep 17 00:00:00 2001 From: Yevgeniy Tyan <102475316+ytvee@users.noreply.github.com> Date: Fri, 10 Jul 2026 18:15:27 +0300 Subject: [PATCH 4/9] feat: render full-section burning graph and torch reveal --- src/scripts/graph/mountCalciferGraph.ts | 166 +++++++++++++++++------- 1 file changed, 117 insertions(+), 49 deletions(-) diff --git a/src/scripts/graph/mountCalciferGraph.ts b/src/scripts/graph/mountCalciferGraph.ts index 1617a93..ec6b0dd 100644 --- a/src/scripts/graph/mountCalciferGraph.ts +++ b/src/scripts/graph/mountCalciferGraph.ts @@ -12,14 +12,17 @@ interface PointerSample { const vertexShader = ` attribute float aSize; attribute float aHeat; + attribute float aGlow; varying vec3 vColor; varying float vHeat; + varying float vGlow; uniform float uPixelRatio; uniform float uViewportScale; void main() { vColor = color; vHeat = aHeat; + vGlow = aGlow; vec4 viewPosition = modelViewMatrix * vec4(position, 1.0); gl_Position = projectionMatrix * viewPosition; gl_PointSize = aSize * uPixelRatio * uViewportScale; @@ -29,15 +32,18 @@ const vertexShader = ` const fragmentShader = ` varying vec3 vColor; varying float vHeat; + varying float vGlow; void main() { float distanceToCenter = distance(gl_PointCoord, vec2(0.5)); if (distanceToCenter > 0.5) discard; - float core = 1.0 - smoothstep(0.08, 0.34, distanceToCenter); - float halo = 1.0 - smoothstep(0.18, 0.5, distanceToCenter); - float alpha = clamp(core + halo * (0.36 + vHeat * 0.18), 0.0, 1.0); - vec3 glow = mix(vColor, vec3(1.0, 0.86, 0.52), halo * 0.18 + vHeat * 0.12); + float core = 1.0 - smoothstep(0.05, 0.28, distanceToCenter); + float halo = 1.0 - smoothstep(0.16, 0.5, distanceToCenter); + float haloStrength = halo * vGlow; + float alpha = clamp(core + haloStrength * (0.42 + vHeat * 0.26), 0.0, 1.0); + vec3 fireCore = vec3(1.0, 0.9, 0.62); + vec3 glow = mix(vColor, fireCore, (haloStrength * 0.22 + vHeat * 0.16) * vGlow); gl_FragColor = vec4(glow, alpha); } ` @@ -50,14 +56,14 @@ const getQualityNodeCount = (): number => { const width = window.innerWidth if (coarsePointer || width < 640) { - return 160 + return 230 } if (width < 1100) { - return 220 + return 330 } - return 310 + return 470 } const writeColor = (target: Float32Array, offset: number, color: Rgb, multiplier = 1): void => { @@ -71,6 +77,7 @@ const createPointGeometry = (graph: CalciferGraphData): THREE.BufferGeometry => const colors = new Float32Array(graph.nodes.length * 3) const sizes = new Float32Array(graph.nodes.length) const heat = new Float32Array(graph.nodes.length) + const glow = new Float32Array(graph.nodes.length) for (const node of graph.nodes) { const offset = node.id * 3 @@ -80,6 +87,7 @@ const createPointGeometry = (graph: CalciferGraphData): THREE.BufferGeometry => writeColor(colors, offset, node.color) sizes[node.id] = node.size heat[node.id] = 0 + glow[node.id] = node.role === 'pupil' ? 0 : 1 } const geometry = new THREE.BufferGeometry() @@ -87,6 +95,7 @@ const createPointGeometry = (graph: CalciferGraphData): THREE.BufferGeometry => geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)) geometry.setAttribute('aSize', new THREE.BufferAttribute(sizes, 1)) geometry.setAttribute('aHeat', new THREE.BufferAttribute(heat, 1)) + geometry.setAttribute('aGlow', new THREE.BufferAttribute(glow, 1)) return geometry } @@ -116,8 +125,8 @@ const createLineGeometry = (graph: CalciferGraphData): THREE.BufferGeometry => { positions[offset + 3] = target.x positions[offset + 4] = target.y positions[offset + 5] = target.z - writeColor(colors, offset, source.color, 0.58) - writeColor(colors, offset + 3, target.color, 0.58) + writeColor(colors, offset, source.color, 0.56) + writeColor(colors, offset + 3, target.color, 0.56) } const geometry = new THREE.BufferGeometry() @@ -178,14 +187,18 @@ export const mountCalciferGraph = ( ): (() => void) => { const graph = createCalciferGraph({ nodeCount: getQualityNodeCount() }) const state = createPhysicsState(graph) + const torchSurface = host.closest('.hero') ?? host const renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: !window.matchMedia('(pointer: coarse)').matches, powerPreference: 'high-performance', }) + renderer.outputColorSpace = THREE.SRGBColorSpace + renderer.setClearColor(0x000000, 0) + const scene = new THREE.Scene() - const camera = new THREE.OrthographicCamera(-1.45, 1.45, 1.35, -1.35, 0.1, 20) + const camera = new THREE.OrthographicCamera(-1.8, 1.8, 1.45, -1.45, 0.1, 20) camera.position.z = 5 const pointGeometry = createPointGeometry(graph) @@ -196,6 +209,7 @@ export const mountCalciferGraph = ( vertexColors: true, depthWrite: false, depthTest: false, + blending: THREE.NormalBlending, uniforms: { uPixelRatio: { value: 1 }, uViewportScale: { value: 1 }, @@ -203,18 +217,20 @@ export const mountCalciferGraph = ( }) const points = new THREE.Points(pointGeometry, pointMaterial) points.renderOrder = 2 + points.frustumCulled = false const lineGeometry = createLineGeometry(graph) const lineMaterial = new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, - opacity: 0.34, + opacity: 0.42, depthWrite: false, depthTest: false, blending: THREE.AdditiveBlending, }) const lines = new THREE.LineSegments(lineGeometry, lineMaterial) lines.renderOrder = 1 + lines.frustumCulled = false scene.add(lines, points) @@ -227,6 +243,7 @@ export const mountCalciferGraph = ( const sizeAttribute = pointGeometry.getAttribute('aSize') as THREE.BufferAttribute const linePositionAttribute = lineGeometry.getAttribute('position') as THREE.BufferAttribute const lineColorAttribute = lineGeometry.getAttribute('color') as THREE.BufferAttribute + const renderPositions = new Float32Array(graph.nodes.length * 3) let frameId = 0 let disposed = false let paused = document.hidden @@ -238,32 +255,34 @@ export const mountCalciferGraph = ( const resize = (): void => { const width = Math.max(1, host.clientWidth) const height = Math.max(1, host.clientHeight) - const pixelRatio = Math.min(window.devicePixelRatio, width < 700 ? 1.25 : 1.6) + const pixelRatio = Math.min(window.devicePixelRatio, width < 700 ? 1.2 : 1.55) const aspect = width / height - const vertical = 1.28 + const vertical = aspect < 0.8 ? 1.72 : aspect < 1.15 ? 1.52 : 1.42 + const horizontal = vertical * aspect + const centerX = aspect > 1.35 ? -horizontal * 0.31 : aspect > 1.05 ? -horizontal * 0.12 : 0 camera.top = vertical camera.bottom = -vertical - camera.left = -vertical * aspect - camera.right = vertical * aspect + camera.left = centerX - horizontal + camera.right = centerX + horizontal camera.updateProjectionMatrix() renderer.setPixelRatio(pixelRatio) renderer.setSize(width, height, false) - pointMaterial.uniforms.uPixelRatio = { value: pixelRatio } - pointMaterial.uniforms.uViewportScale = { - value: clamp(Math.min(width, height) / 620, 0.78, 1.18), - } + pointMaterial.uniforms.uPixelRatio.value = pixelRatio + pointMaterial.uniforms.uViewportScale.value = clamp(Math.min(width, height) / 720, 0.72, 1.18) } - const setTorch = (event: PointerEvent, intensity: number): void => { - const rect = host.getBoundingClientRect() + const setTorch = (event: PointerEvent, intensity: number, speed = 0): void => { + const rect = torchSurface.getBoundingClientRect() const x = clamp(event.clientX - rect.left, 0, rect.width) const y = clamp(event.clientY - rect.top, 0, rect.height) + const radius = clamp(190 + speed * 240, 190, 350) - host.style.setProperty('--torch-x', `${x}px`) - host.style.setProperty('--torch-y', `${y}px`) - host.style.setProperty('--torch-intensity', intensity.toFixed(3)) + torchSurface.style.setProperty('--torch-x', `${x}px`) + torchSurface.style.setProperty('--torch-y', `${y}px`) + torchSurface.style.setProperty('--torch-radius', `${radius}px`) + torchSurface.style.setProperty('--torch-intensity', intensity.toFixed(3)) } const updateGeometry = (elapsed: number): void => { @@ -281,18 +300,47 @@ export const mountCalciferGraph = ( const offset = index * 3 const influence = dragInfluence?.[index] ?? 0 - const flicker = - seed.role === 'flame' || seed.role === 'arm' - ? (Math.sin(elapsed * 4.3 + seed.phase) + Math.sin(elapsed * 7.1 + seed.phase * 1.7)) * - 0.055 + const slowFlicker = Math.sin(elapsed * 2.35 + seed.phase) + const quickFlicker = Math.sin(elapsed * 6.8 + seed.phase * 1.83) + const upwardBias = clamp((seed.y + 0.85) / 2.15, 0, 1) + const isFire = seed.role === 'flame' || seed.role === 'arm' + const isSpark = seed.role === 'spark' + const renderDriftX = isSpark + ? Math.sin(elapsed * 1.5 + seed.phase) * 0.025 + : isFire + ? quickFlicker * 0.0055 * (0.5 + upwardBias) : 0 - const activeHeat = clamp(influence * 0.9 + Math.max(0, flicker), 0, 1) - const brightness = 1 + activeHeat * 0.2 - - positionAttribute.setXYZ(index, node.x, node.y, node.z) + const renderDriftY = isSpark + ? Math.sin(elapsed * 1.08 + seed.phase * 1.27) * 0.035 + : isFire + ? Math.max(0, slowFlicker) * 0.009 * (0.45 + upwardBias) + : 0 + const renderX = node.x + renderDriftX + const renderY = node.y + renderDriftY + const renderZ = node.z + const idleHeat = isSpark + ? 0.34 + (slowFlicker + 1) * 0.19 + : isFire + ? 0.12 + Math.max(0, slowFlicker * 0.24 + quickFlicker * 0.12) + : seed.role === 'eye' + ? 0.22 + Math.max(0, slowFlicker) * 0.08 + : seed.role === 'mouth' + ? 0.14 + Math.max(0, quickFlicker) * 0.12 + : 0 + const activeHeat = seed.role === 'pupil' ? 0 : clamp(idleHeat + influence * 0.92, 0, 1) + const brightness = seed.role === 'pupil' ? 1 : 1 + activeHeat * 0.24 + const scalePulse = + seed.role === 'pupil' + ? 1 + : 1 + slowFlicker * (isSpark ? 0.12 : isFire ? 0.055 : 0.018) + influence * 0.2 + + renderPositions[offset] = renderX + renderPositions[offset + 1] = renderY + renderPositions[offset + 2] = renderZ + positionAttribute.setXYZ(index, renderX, renderY, renderZ) writeColor(colorArray, offset, seed.color, brightness) heatArray[index] = activeHeat - sizeArray[index] = seed.size * (1 + flicker * 0.6 + influence * 0.18) + sizeArray[index] = seed.size * scalePulse } for (let index = 0; index < graph.edges.length; index += 1) { @@ -302,23 +350,40 @@ export const mountCalciferGraph = ( continue } - const source = state.nodes[edge.source] - const target = state.nodes[edge.target] const sourceSeed = graph.nodes[edge.source] const targetSeed = graph.nodes[edge.target] - if (!source || !target || !sourceSeed || !targetSeed) { + if (!sourceSeed || !targetSeed) { continue } - const offset = index * 6 + const sourceOffset = edge.source * 3 + const targetOffset = edge.target * 3 + const lineOffset = index * 6 const edgeHeat = Math.max(dragInfluence?.[edge.source] ?? 0, dragInfluence?.[edge.target] ?? 0) - const multiplier = 0.56 + edgeHeat * 0.62 - - linePositionAttribute.setXYZ(index * 2, source.x, source.y, source.z) - linePositionAttribute.setXYZ(index * 2 + 1, target.x, target.y, target.z) - writeColor(lineColorAttribute.array as Float32Array, offset, sourceSeed.color, multiplier) - writeColor(lineColorAttribute.array as Float32Array, offset + 3, targetSeed.color, multiplier) + const idlePulse = + Math.max(0, Math.sin(elapsed * 2.1 + sourceSeed.phase + targetSeed.phase)) * 0.16 + const multiplier = 0.5 + idlePulse + edgeHeat * 0.72 + + linePositionAttribute.setXYZ( + index * 2, + renderPositions[sourceOffset] ?? 0, + renderPositions[sourceOffset + 1] ?? 0, + renderPositions[sourceOffset + 2] ?? 0, + ) + linePositionAttribute.setXYZ( + index * 2 + 1, + renderPositions[targetOffset] ?? 0, + renderPositions[targetOffset + 1] ?? 0, + renderPositions[targetOffset + 2] ?? 0, + ) + writeColor(lineColorAttribute.array as Float32Array, lineOffset, sourceSeed.color, multiplier) + writeColor( + lineColorAttribute.array as Float32Array, + lineOffset + 3, + targetSeed.color, + multiplier, + ) } positionAttribute.needsUpdate = true @@ -355,7 +420,7 @@ export const mountCalciferGraph = ( const onPointerDown = (event: PointerEvent): void => { const point = getPointerWorld(event, canvas, camera) - const threshold = event.pointerType === 'touch' ? 0.24 : 0.16 + const threshold = event.pointerType === 'touch' ? 0.25 : 0.17 const nearest = findNearestNode(graph, state, point, threshold) if (nearest < 0) { @@ -370,13 +435,13 @@ export const mountCalciferGraph = ( canvas.setPointerCapture(event.pointerId) canvas.style.cursor = 'grabbing' host.dataset.dragging = 'true' - setTorch(event, 0.72) + setTorch(event, 0.74) } const onPointerMove = (event: PointerEvent): void => { if (draggedIndex < 0) { const point = getPointerWorld(event, canvas, camera) - canvas.style.cursor = findNearestNode(graph, state, point, 0.16) >= 0 ? 'grab' : 'default' + canvas.style.cursor = findNearestNode(graph, state, point, 0.17) >= 0 ? 'grab' : 'default' return } @@ -390,10 +455,10 @@ export const mountCalciferGraph = ( event.clientY - (previous?.y ?? event.clientY), ) const speed = distance / elapsed - const intensity = clamp(0.58 + speed * 0.7, 0.58, 1) + const intensity = clamp(0.62 + speed * 0.72, 0.62, 1) pointerSample = { x: event.clientX, y: event.clientY, time: now } - setTorch(event, intensity) + setTorch(event, intensity, speed) } const releasePointer = (event: PointerEvent): void => { @@ -410,11 +475,12 @@ export const mountCalciferGraph = ( pointerSample = undefined canvas.style.cursor = 'default' host.dataset.dragging = 'false' - host.style.setProperty('--torch-intensity', '0') + torchSurface.style.setProperty('--torch-intensity', '0') } const onVisibilityChange = (): void => { paused = document.hidden + previousFrameTime = performance.now() } const onContextLost = (event: Event): void => { @@ -426,6 +492,8 @@ export const mountCalciferGraph = ( const onContextRestored = (): void => { host.dataset.failed = 'false' resize() + updateGeometry(elapsedSeconds) + renderer.render(scene, camera) } canvas.addEventListener('pointerdown', onPointerDown) From e424f2af6f1ea9373ceeab0df9b73d65f5f187f8 Mon Sep 17 00:00:00 2001 From: Yevgeniy Tyan <102475316+ytvee@users.noreply.github.com> Date: Fri, 10 Jul 2026 18:16:16 +0300 Subject: [PATCH 5/9] feat: expand graph into borderless full-section fire field --- src/components/CalciferGraph.astro | 277 ++++++++++++++++++++++------- 1 file changed, 217 insertions(+), 60 deletions(-) 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 = {