app
Circuit Trace Generators
Public Made by Adomby adom
Two dependency-free, seeded circuit-background generators: a seamless torus tile with ambient or progressive reveal, and the v9 continuous field with obstacles, scroll reveal, and five ambient systems.
master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
const SVG_NS = "http://www.w3.org/2000/svg";
function clamp(value, minimum = 0, maximum = 1) {
return Math.max(minimum, Math.min(maximum, value));
}
function smoothstep(value) {
const amount = clamp(value);
return amount * amount * (3 - 2 * amount);
}
function pathData(points) {
return points.map(([x, y], index) => `${index ? "L" : "M"}${x} ${y}`).join(" ");
}
function svgElement(name, attributes = {}) {
const element = document.createElementNS(SVG_NS, name);
for (const [key, value] of Object.entries(attributes)) element.setAttribute(key, String(value));
return element;
}
function brandColor(x, width) {
const position = clamp(x / Math.max(1, width));
if (position < 0.5) return `hsl(${176 + position * 58} 92% 63%)`;
return `hsl(${205 + (position - 0.5) * 86} 91% 70%)`;
}
function clearRevealStyle(visual) {
visual.path.setAttribute("d", visual.originalPathData);
visual.path.removeAttribute("stroke-dasharray");
visual.path.removeAttribute("stroke-dashoffset");
visual.path.style.removeProperty("opacity");
visual.path.style.removeProperty("will-change");
for (const via of [visual.startVia, visual.endVia]) {
via?.style.removeProperty("opacity");
via?.style.removeProperty("will-change");
}
}
export function createTraceFlowReveal(svg, field, config, random, { traceLayer = null, onComplete = null } = {}) {
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const staticLayer = traceLayer || svg.querySelector("[data-static-traces]");
const glowLayer = svgElement("g", {
"data-animation": "trace-growth",
"aria-hidden": "true",
});
glowLayer.style.mixBlendMode = "screen";
svg.append(glowLayer);
const traceGroups = [...staticLayer.querySelectorAll("[data-circuit-trace]")];
const visuals = field.traces.map((trace, index) => {
const group = traceGroups[index];
return {
index,
trace,
group,
path: group.querySelector("[data-trace-path]"),
startVia: group.querySelector('[data-trace-via="start"]'),
endVia: group.querySelector('[data-trace-via="end"]'),
sourceVia: null,
destinationVia: null,
overlay: null,
startedAt: 0,
duration: 0,
desiredStart: 0,
length: Math.max(1, trace.length),
originalPathData: group.querySelector("[data-trace-path]").getAttribute("d"),
revealed: true,
};
});
let ordered = [];
let active = new Map();
let nextIndex = 0;
let timelineStart = 0;
let frameRequest = 0;
let frameCount = 0;
let lastFrameTime = 0;
let running = false;
let completed = true;
const scale = Math.max(0.25, config.featureScale || 1);
const life = clamp(config.revealPower ?? 0.72, 0, 1.5);
const revealMotion = config.revealMotion !== false;
const speedFactor = clamp(0.72 + life * 0.36, 0.72, 1.26);
const glowStrength = clamp((config.revealGlow ?? config.growthGlow ?? 0.82) * (0.68 + life * 0.25), 0, 1.15);
const requestedMaxActive = Number(config.revealMaxActiveTraces);
const maxActive = Number.isFinite(requestedMaxActive)
? clamp(Math.round(requestedMaxActive), 1, 160)
: clamp(Math.round(16 + Math.sqrt(visuals.length) * 1.55), 22, 54);
const targetFrameRate = visuals.length > 1200 ? 30 : visuals.length > 600 ? 45 : 60;
const frameInterval = 1000 / targetFrameRate;
function removeOverlay(visual) {
visual.overlay?.group.remove();
visual.overlay = null;
}
function completeVisual(visual) {
if (visual.revealed) return;
clearRevealStyle(visual);
removeOverlay(visual);
visual.revealed = true;
}
function finish(notify = false) {
if (completed && !running && active.size === 0) return;
window.cancelAnimationFrame(frameRequest);
frameRequest = 0;
running = false;
completed = true;
for (const visual of visuals) completeVisual(visual);
glowLayer.replaceChildren();
active.clear();
nextIndex = ordered.length;
if (notify) onComplete?.();
}
function prepareTimeline() {
const requestedAngle = Number(config.spawnAngle ?? config.revealAngle ?? config.growthAngle);
const generatedAngle = Number(config.flowAngle);
const angleDegrees = Number.isFinite(requestedAngle)
? requestedAngle
: Number.isFinite(generatedAngle)
? generatedAngle
: 24;
const angle = (angleDegrees * Math.PI) / 180;
const direction = [Math.cos(angle), Math.sin(angle)];
const across = [-direction[1], direction[0]];
const span = Math.max(field.width, field.height);
const phase = random() * Math.PI * 2;
const requestedSwirl = Number(config.spawnSwirl);
const swirl = Number.isFinite(requestedSwirl)
? clamp(requestedSwirl, 0, 0.35)
: 0.065;
const requestedMode = String(config.spawnMode || "flow").toLowerCase();
const spawnMode = ["flow", "radial", "edges", "scatter"].includes(requestedMode)
? requestedMode
: "flow";
const radialOffset = Math.min(field.width, field.height) * 0.12;
const radialCenter = [
field.width * 0.5 - direction[0] * radialOffset,
field.height * 0.5 - direction[1] * radialOffset,
];
const flowPotential = (point) => {
const projection = point[0] * direction[0] + point[1] * direction[1];
const cross = point[0] * across[0] + point[1] * across[1];
return projection
+ Math.sin((cross / Math.max(1, span)) * Math.PI * 3.1 + phase) * span * swirl;
};
const modePotential = (point) => {
if (spawnMode === "radial") {
const offsetX = point[0] - radialCenter[0];
const offsetY = point[1] - radialCenter[1];
const distance = Math.hypot(offsetX, offsetY);
const bearing = Math.atan2(offsetY, offsetX) - angle;
return distance
+ Math.sin(bearing * 2.6 + (distance / Math.max(1, span)) * Math.PI * 2.2 + phase)
* span * swirl;
}
if (spawnMode === "edges") {
const edgeDepth = Math.min(
Math.max(0, point[0]),
Math.max(0, field.width - point[0]),
Math.max(0, point[1]),
Math.max(0, field.height - point[1]),
);
const cross = point[0] * across[0] + point[1] * across[1];
const directionalBias = flowPotential(point) * 0.055;
return edgeDepth + directionalBias
+ Math.sin((cross / Math.max(1, span)) * Math.PI * 3.1 + phase) * span * swirl;
}
return flowPotential(point);
};
const entries = visuals.map((visual) => {
const points = visual.trace.points;
const first = points[0];
const last = points.at(-1);
const midpoint = points[Math.floor(points.length / 2)];
const reverse = modePotential(first) > modePotential(last);
const orientedPoints = reverse ? [...points].reverse() : points;
visual.path.setAttribute("d", pathData(orientedPoints));
visual.sourceVia = reverse ? visual.endVia : visual.startVia;
visual.destinationVia = reverse ? visual.startVia : visual.endVia;
const localVariation = (random() - 0.5) * span * 0.045;
const key = spawnMode === "scatter"
? random() * span
: modePotential(midpoint) + localVariation;
return { visual, key };
});
entries.sort((first, second) => first.key - second.key || first.visual.index - second.visual.index);
const minimum = entries[0]?.key || 0;
const maximum = entries.at(-1)?.key || minimum + 1;
const duration = clamp(config.revealDuration ?? config.growthDuration ?? 7600, 2600, 18000) / speedFactor;
const startWindow = Math.max(900, duration - 1750);
const range = Math.max(1, maximum - minimum);
const baseDurations = entries.map(({ visual }) => (
clamp(260 + Math.pow(visual.length, 0.88) * 5.6, 420, 3200) / speedFactor
));
const averageDuration = baseDurations.reduce((sum, value) => sum + value, 0)
/ Math.max(1, baseDurations.length);
const targetAverage = maxActive * startWindow * 0.82 / Math.max(1, entries.length);
const durationScale = clamp(targetAverage / Math.max(1, averageDuration), 0.05, 1);
const minimumDuration = clamp(160 * Math.sqrt(360 / Math.max(1, entries.length)), 52, 180);
for (const [rank, entry] of entries.entries()) {
const spatial = clamp((entry.key - minimum) / range);
const orderedRank = entries.length > 1 ? rank / (entries.length - 1) : 0;
entry.visual.desiredStart = (spatial * 0.78 + orderedRank * 0.22) * startWindow;
entry.visual.duration = clamp(
baseDurations[rank] * durationScale,
minimumDuration,
3200 / speedFactor,
);
}
return entries.map((entry) => entry.visual);
}
function prepareVisuals() {
for (const visual of visuals) {
removeOverlay(visual);
visual.path.setAttribute("stroke-dasharray", `${visual.length} ${visual.length + 1}`);
visual.path.setAttribute("stroke-dashoffset", String(visual.length));
visual.path.style.opacity = "0";
visual.revealed = false;
for (const via of [visual.startVia, visual.endVia]) {
if (!via) continue;
via.style.opacity = "0";
}
}
}
function createOverlay(visual) {
const color = brandColor(
visual.trace.points[Math.floor(visual.trace.points.length / 2)][0],
field.width,
);
const headLength = clamp(visual.length * 0.16, 12 * scale, 42 * scale);
const pathAttributes = {
d: visual.path.getAttribute("d"),
fill: "none",
stroke: color,
"stroke-linecap": "round",
"stroke-linejoin": "round",
"stroke-dasharray": `${headLength} ${visual.length + headLength}`,
"stroke-dashoffset": headLength,
};
const group = svgElement("g", { "data-growth-active": visual.index });
const halo = svgElement("path", {
...pathAttributes,
"stroke-width": config.traceWidth * scale * 4.2,
opacity: 0,
});
const core = svgElement("path", {
...pathAttributes,
"stroke-width": config.traceWidth * scale * 1.35,
opacity: 0,
});
const source = visual.trace.points[0];
const destination = visual.trace.points.at(-1);
const sourcePoint = visual.sourceVia === visual.startVia ? source : destination;
const destinationPoint = visual.destinationVia === visual.endVia ? destination : source;
const sourceRing = svgElement("circle", {
cx: sourcePoint[0], cy: sourcePoint[1], r: config.viaOuterRadius * scale,
fill: "none", stroke: color, "stroke-width": Math.max(0.8, config.traceWidth * scale), opacity: 0,
});
const destinationRing = svgElement("circle", {
cx: destinationPoint[0], cy: destinationPoint[1], r: config.viaOuterRadius * scale,
fill: "none", stroke: color, "stroke-width": Math.max(0.8, config.traceWidth * scale), opacity: 0,
});
group.append(halo, core, sourceRing, destinationRing);
glowLayer.append(group);
return { group, halo, core, sourceRing, destinationRing, headLength };
}
function activate(visual, timestamp) {
visual.startedAt = timestamp;
visual.overlay = createOverlay(visual);
active.set(visual.index, visual);
}
function drawVisual(visual, timestamp) {
const progress = clamp((timestamp - visual.startedAt) / visual.duration);
const routed = smoothstep(progress);
const drawn = visual.length * routed;
const energy = Math.sin(Math.PI * progress) * glowStrength;
const sourceEnergy = 1 - smoothstep(progress / 0.28);
const destinationEnergy = smoothstep((progress - 0.76) / 0.24);
const overlay = visual.overlay;
visual.path.setAttribute("stroke-dashoffset", String(visual.length - drawn));
visual.path.style.opacity = String(clamp(0.46 + routed * 0.54));
if (visual.sourceVia) visual.sourceVia.style.opacity = String(smoothstep(progress / 0.12));
if (visual.destinationVia) visual.destinationVia.style.opacity = String(destinationEnergy);
const dashOffset = overlay.headLength - drawn;
overlay.halo.setAttribute("stroke-dashoffset", String(dashOffset));
overlay.core.setAttribute("stroke-dashoffset", String(dashOffset));
overlay.halo.setAttribute("opacity", String(energy * 0.16));
overlay.core.setAttribute("opacity", String(energy * 0.92));
overlay.sourceRing.setAttribute("r", String(config.viaOuterRadius * scale * (1 + sourceEnergy * 1.9)));
overlay.sourceRing.setAttribute("opacity", String(sourceEnergy * glowStrength * 0.72));
overlay.destinationRing.setAttribute("r", String(config.viaOuterRadius * scale * (1 + destinationEnergy * 1.35)));
overlay.destinationRing.setAttribute("opacity", String((1 - destinationEnergy) * destinationEnergy * 3.2 * glowStrength));
if (progress < 1) return false;
completeVisual(visual);
return true;
}
function renderFrame(timestamp) {
frameRequest = window.requestAnimationFrame(renderFrame);
const frameElapsed = lastFrameTime ? timestamp - lastFrameTime : frameInterval;
if (frameElapsed < frameInterval) return;
lastFrameTime = lastFrameTime
? timestamp - (frameElapsed % frameInterval)
: timestamp;
frameCount += 1;
const elapsed = timestamp - timelineStart;
while (
nextIndex < ordered.length
&& ordered[nextIndex].desiredStart <= elapsed
&& active.size < maxActive
) {
activate(ordered[nextIndex], timestamp);
nextIndex += 1;
}
for (const [index, visual] of active) {
if (!drawVisual(visual, timestamp)) continue;
active.delete(index);
}
if (nextIndex >= ordered.length && active.size === 0) finish(true);
}
function start() {
finish();
ordered = prepareTimeline();
nextIndex = 0;
frameCount = 0;
lastFrameTime = 0;
if (!revealMotion || reducedMotion || life <= 0.02 || ordered.length === 0) return false;
prepareVisuals();
running = true;
completed = false;
timelineStart = performance.now();
frameRequest = window.requestAnimationFrame(renderFrame);
return true;
}
function stop() {
finish();
}
function destroy() {
finish();
document.removeEventListener("visibilitychange", onVisibilityChange);
glowLayer.remove();
}
function onVisibilityChange() {
if (document.hidden && running) finish();
}
document.addEventListener("visibilitychange", onVisibilityChange);
return {
start,
stop,
trigger: start,
destroy,
get stats() {
return {
system: "trace-flow-sweep",
animationNodes: glowLayer.childElementCount,
activeTraces: active.size,
revealedTraces: nextIndex - active.size,
traceCount: visuals.length,
maximumActiveTraces: maxActive,
targetFrameRate,
frameCount,
running,
completed,
reducedMotion,
};
},
};
}