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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
const SVG_NS = "http://www.w3.org/2000/svg";
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 pathData(points) {
return points.map(([x, y], index) => `${index ? "L" : "M"}${x.toFixed(2)},${y.toFixed(2)}`).join("");
}
function integer(random, min, max) {
return Math.floor(random() * (max - min + 1)) + min;
}
function weighted(random, entries) {
let total = 0;
for (const [, weight] of entries) total += weight;
let roll = random() * total;
for (const [value, weight] of entries) {
roll -= weight;
if (roll <= 0) return value;
}
return entries.at(-1)?.[0];
}
function shuffle(random, values) {
for (let index = values.length - 1; index > 0; index -= 1) {
const swap = integer(random, 0, index);
[values[index], values[swap]] = [values[swap], values[index]];
}
return values;
}
function compareTraceDistance(a, b) {
return a.distance - b.distance || a.index - b.index;
}
function compareTraceKey(distance, index, ranked) {
return distance - ranked.distance || index - ranked.index;
}
function siftDownTraceDistance(heap, index) {
while (true) {
const left = index * 2 + 1;
if (left >= heap.length) return;
const right = left + 1;
let largest = left;
if (right < heap.length && compareTraceDistance(heap[left], heap[right]) < 0) largest = right;
if (compareTraceDistance(heap[index], heap[largest]) >= 0) return;
[heap[index], heap[largest]] = [heap[largest], heap[index]];
index = largest;
}
}
function nearestTraces(traces, origin, count) {
if (!(count > 0)) return [];
const nearest = [];
for (let index = 0; index < traces.length; index += 1) {
const trace = traces[index];
const midpoint = trace.points[Math.floor(trace.points.length / 2)];
const distance = Math.hypot(midpoint[0] - origin.x, midpoint[1] - origin.y);
if (nearest.length < count) {
nearest.push({ trace, distance, index });
let child = nearest.length - 1;
while (child > 0) {
const parent = Math.floor((child - 1) / 2);
if (compareTraceDistance(nearest[parent], nearest[child]) >= 0) break;
[nearest[parent], nearest[child]] = [nearest[child], nearest[parent]];
child = parent;
}
} else if (compareTraceKey(distance, index, nearest[0]) < 0) {
nearest[0] = { trace, distance, index };
siftDownTraceDistance(nearest, 0);
}
}
return nearest.sort(compareTraceDistance);
}
export function createConductorAnimation(svg, field, config, random) {
const ambientLayer = svgElement("g", {
"data-animation": "conductor-ambient", fill: "none", "pointer-events": "none",
});
const signalLayer = svgElement("g", {
"data-animation": "conductor-signals", fill: "none", "pointer-events": "none",
});
svg.append(ambientLayer, signalLayer);
let timer;
let waveTimer;
let activeSignals = 0;
let ambientCurrentCount = 0;
let ambientNodeCount = 0;
let wavesTriggered = 0;
let pointer = null;
let lastPointerWake = 0;
const scheduled = new Set();
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
function life() {
return Math.max(0, Math.min(1.5, config.liveliness));
}
function featureScale() {
return Math.max(0.25, config.featureScale);
}
function trackTimeout(callback, delay) {
const timeout = window.setTimeout(() => {
scheduled.delete(timeout);
callback();
}, Math.max(0, delay));
scheduled.add(timeout);
return timeout;
}
function clearScheduled() {
for (const timeout of scheduled) window.clearTimeout(timeout);
scheduled.clear();
}
function nearestTrace(origin = pointer) {
if (!origin || !field.traces.length) return null;
let result = null;
let best = Infinity;
const attempts = Math.min(72, field.traces.length);
for (let attempt = 0; attempt < attempts; attempt += 1) {
const trace = field.traces[integer(random, 0, field.traces.length - 1)];
const point = trace.points[Math.floor(trace.points.length / 2)];
const distance = Math.hypot(point[0] - origin.x, point[1] - origin.y);
if (distance < best) {
best = distance;
result = trace;
}
}
return result;
}
function flashNode(point, color, delay = 0, intensity = 1) {
if (delay > 0) {
trackTimeout(() => flashNode(point, color, 0, intensity), delay);
return;
}
if (!signalLayer.isConnected || reducedMotion || !config.motion || !config.nodeFlashes) return;
const scale = featureScale();
const radius = config.viaOuterRadius * scale;
const ring = svgElement("circle", {
cx: point[0], cy: point[1], r: radius * 0.7,
fill: "none", stroke: color,
"stroke-width": Math.max(0.8, config.traceWidth * scale * 0.85),
opacity: 0,
});
ring.style.filter = `drop-shadow(0 0 ${3.5 * scale}px ${color})`;
signalLayer.appendChild(ring);
const duration = 480 + intensity * 220;
ring.animate(
[
{ opacity: 0, transform: "scale(.45)" },
{ offset: 0.18, opacity: Math.min(1, 0.55 + intensity * 0.25), transform: "scale(.9)" },
{ opacity: 0, transform: `scale(${2.2 + intensity * 0.7})` },
],
{ duration, easing: "cubic-bezier(.16,.8,.3,1)", fill: "forwards" },
);
ring.style.transformBox = "fill-box";
ring.style.transformOrigin = "center";
trackTimeout(() => ring.remove(), duration + 40);
}
function spawnSignal({
nearPointer = false,
trace = null,
delay = 0,
reverse,
color,
intensity = 1,
lengthScale = 1,
speedScale = 1,
endpointFlash = false,
} = {}) {
if (delay > 0) {
trackTimeout(() => spawnSignal({
nearPointer, trace, reverse, color, intensity, lengthScale, speedScale, endpointFlash,
}), delay);
return true;
}
if (!config.motion || reducedMotion || activeSignals >= config.maxSignals || !field.traces.length) return false;
const selected = trace || (nearPointer && nearestTrace()) || field.traces[integer(random, 0, field.traces.length - 1)];
const reversed = reverse ?? random() < 0.5;
const points = reversed ? [...selected.points].reverse() : selected.points;
const selectedColor = color || weighted(random, config.signalColors);
const scale = featureScale();
const requestedLength = config.signalLength * scale * lengthScale;
const signalLength = Math.max(12 * scale, Math.min(requestedLength, selected.length * 0.72));
const signalSpeed = config.signalSpeed * scale * (0.78 + life() * 0.27) * speedScale;
const duration = Math.max(460, ((selected.length + signalLength) / signalSpeed) * 1000);
const dashGap = selected.length + signalLength;
const group = svgElement("g", { "data-signal": "" });
const halo = svgElement("path", {
d: pathData(points), stroke: selectedColor,
"stroke-width": (5.2 + intensity * 2.2) * scale,
"stroke-linecap": "round",
"stroke-dasharray": `${signalLength} ${dashGap}`,
"stroke-dashoffset": signalLength,
opacity: 0,
});
const core = svgElement("path", {
d: pathData(points), stroke: selectedColor,
"stroke-width": (1.25 + intensity * 1.25) * scale,
"stroke-linecap": "round",
"stroke-dasharray": `${signalLength} ${dashGap}`,
"stroke-dashoffset": signalLength,
opacity: 0,
});
halo.style.filter = `blur(${1.6 * scale}px) drop-shadow(0 0 ${4.5 * scale}px ${selectedColor})`;
core.style.filter = `drop-shadow(0 0 ${2.8 * scale}px ${selectedColor})`;
if (config.signalHalos) group.appendChild(halo);
group.appendChild(core);
signalLayer.appendChild(group);
activeSignals += 1;
const movement = [
{ strokeDashoffset: signalLength, opacity: 0 },
{ offset: 0.08, opacity: 0.92 },
{ offset: 0.84, opacity: 0.72 },
{ strokeDashoffset: -selected.length, opacity: 0 },
];
if (config.signalHalos) {
halo.animate(movement.map((frame) => ({ ...frame, opacity: (frame.opacity || 0) * 0.22 * intensity })), {
duration, easing: "linear", fill: "forwards",
});
}
core.animate(movement, { duration, easing: "linear", fill: "forwards" });
if (endpointFlash) {
flashNode(points[0], selectedColor, 0, intensity);
flashNode(points.at(-1), selectedColor, duration * 0.87, intensity * 0.8);
}
trackTimeout(() => {
group.remove();
activeSignals = Math.max(0, activeSignals - 1);
}, duration + 100);
return true;
}
function spawnConvoy({ nearPointer = false, trace = null } = {}) {
if (!config.motion || !config.signalConvoys || reducedMotion || !field.traces.length) return;
const selected = trace || (nearPointer && nearestTrace()) || field.traces[integer(random, 0, field.traces.length - 1)];
const count = Math.max(2, Math.min(4, Math.round(2 + life() + random())));
const selectedColor = weighted(random, config.signalColors);
const reverse = random() < 0.5;
for (let index = 0; index < count; index += 1) {
spawnSignal({
trace: selected,
reverse,
color: selectedColor,
delay: index * (78 + random() * 54),
intensity: 0.62 + index * 0.08,
lengthScale: 0.28 + random() * 0.22,
speedScale: 0.92 + random() * 0.18,
endpointFlash: index === 0,
});
}
}
function ripple(origin, color) {
if (!signalLayer.isConnected) return;
const scale = featureScale();
const startRadius = config.pitch * scale * 0.8;
const ring = svgElement("circle", {
cx: origin.x, cy: origin.y, r: startRadius,
fill: "none", stroke: color,
"stroke-width": Math.max(0.7, config.traceWidth * scale * 0.72),
opacity: 0.72,
});
ring.style.filter = `drop-shadow(0 0 ${4 * scale}px ${color})`;
signalLayer.appendChild(ring);
ring.animate(
[
{ opacity: 0.72, transform: "scale(.35)" },
{ offset: 0.3, opacity: 0.34 },
{ opacity: 0, transform: "scale(6)" },
],
{ duration: 980, easing: "cubic-bezier(.12,.72,.18,1)", fill: "forwards" },
);
ring.style.transformBox = "fill-box";
ring.style.transformOrigin = "center";
trackTimeout(() => ring.remove(), 1040);
}
function spawnWave(origin = null) {
if (!config.motion || !config.signalWaves || reducedMotion || !field.traces.length) return false;
const source = origin || {
x: field.width * (0.14 + random() * 0.72),
y: field.height * (0.14 + random() * 0.72),
};
const selectedColor = weighted(random, config.signalColors);
ripple(source, selectedColor);
const count = Math.min(field.traces.length, Math.max(6, Math.round(6 + life() * 8)));
const ranked = nearestTraces(field.traces, source, count);
const outerDistance = ranked[count - 1]?.distance || 1;
for (let index = 0; index < count; index += 1) {
const { trace, distance } = ranked[index];
const start = trace.points[0];
const end = trace.points.at(-1);
const reverse = Math.hypot(end[0] - source.x, end[1] - source.y)
< Math.hypot(start[0] - source.x, start[1] - source.y);
spawnSignal({
trace,
reverse,
color: selectedColor,
delay: 90 + distance / outerDistance * 560 + index * 14,
intensity: 0.64 + (1 - distance / outerDistance) * 0.34,
lengthScale: 0.44 + random() * 0.2,
speedScale: 1.02 + random() * 0.18,
endpointFlash: index % 4 === 0,
});
}
wavesTriggered += 1;
return true;
}
function seedAmbientLife() {
ambientCurrentCount = 0;
ambientNodeCount = 0;
if (!config.motion || reducedMotion || life() <= 0.02 || !field.traces.length) return;
const scale = featureScale();
const eligible = field.traces.filter((trace) => trace.length > 28 * scale);
const currents = config.ambientCurrents
? Math.min(eligible.length, Math.max(1, Math.round(config.ambientCurrentCount * life())))
: 0;
shuffle(random, eligible);
for (const trace of eligible.slice(0, currents)) {
const color = weighted(random, config.signalColors);
const dash = Math.max(12 * scale, trace.length * (0.07 + random() * 0.1));
const current = svgElement("path", {
d: pathData(random() < 0.5 ? [...trace.points].reverse() : trace.points),
stroke: color,
"stroke-width": config.traceWidth * scale * (0.9 + random() * 0.55),
"stroke-linecap": "round",
"stroke-dasharray": `${dash} ${trace.length + dash}`,
"stroke-dashoffset": dash,
opacity: 0,
});
current.style.filter = `drop-shadow(0 0 ${2.2 * scale}px ${color})`;
ambientLayer.appendChild(current);
const duration = (6400 + random() * 9000) / (0.72 + life() * 0.35);
current.animate(
[
{ strokeDashoffset: dash, opacity: 0 },
{ offset: 0.14, opacity: 0.08 + life() * 0.055 },
{ offset: 0.78, opacity: 0.05 + life() * 0.04 },
{ strokeDashoffset: -trace.length, opacity: 0 },
],
{ duration, delay: -random() * duration, iterations: Infinity, easing: "linear" },
);
ambientCurrentCount += 1;
}
const nodes = config.ambientNodes
? Math.min(field.traces.length, Math.max(1, Math.round(config.ambientNodeCount * life())))
: 0;
for (let index = 0; index < nodes; index += 1) {
const trace = field.traces[integer(random, 0, field.traces.length - 1)];
const point = random() < 0.5 ? trace.points[0] : trace.points.at(-1);
const color = weighted(random, config.signalColors);
const node = svgElement("circle", {
cx: point[0], cy: point[1],
r: config.viaOuterRadius * scale * (0.34 + random() * 0.18),
fill: color, opacity: 0,
});
node.style.filter = `drop-shadow(0 0 ${3.2 * scale}px ${color})`;
node.style.transformBox = "fill-box";
node.style.transformOrigin = "center";
ambientLayer.appendChild(node);
const duration = 2600 + random() * 4300;
node.animate(
[
{ opacity: 0, transform: "scale(.65)" },
{ offset: 0.48, opacity: 0.16 + life() * 0.11, transform: "scale(1.22)" },
{ opacity: 0, transform: "scale(.72)" },
],
{ duration, delay: -random() * duration, iterations: Infinity, easing: "ease-in-out" },
);
ambientNodeCount += 1;
}
}
function pointerMoved(origin) {
pointer = origin;
if (!pointer) return;
const wakeInterval = config.pointerWakeInterval / Math.max(0.55, 0.55 + life() * 0.5);
if (config.motion && config.pointerWake && !reducedMotion && pointer.at - lastPointerWake > wakeInterval) {
lastPointerWake = pointer.at;
spawnSignal({
nearPointer: true,
intensity: 0.5 + life() * 0.16,
lengthScale: 0.34,
speedScale: 1.3,
});
}
}
function clicked(origin) {
pointer = origin;
spawnWave(pointer);
if (config.signalConvoys) trackTimeout(() => spawnConvoy({ nearPointer: true }), 160);
}
function stop() {
window.clearInterval(timer);
window.clearInterval(waveTimer);
timer = undefined;
waveTimer = undefined;
clearScheduled();
}
function start() {
stop();
if (!config.motion || reducedMotion || life() <= 0.02) return;
seedAmbientLife();
const interval = Math.max(130, config.signalInterval / (0.55 + life() * 0.78));
timer = window.setInterval(() => {
if (document.hidden) return;
const nearPointer = pointer && performance.now() - pointer.at < 3200;
if (config.signalConvoys && random() < 0.12 + life() * 0.13) spawnConvoy({ nearPointer });
else spawnSignal({
nearPointer,
intensity: 0.62 + random() * 0.34,
lengthScale: 0.52 + random() * 0.58,
speedScale: 0.86 + random() * 0.34,
endpointFlash: random() < 0.12,
});
}, interval);
if (config.signalWaves) {
const intervalForWaves = Math.max(2800, config.waveInterval / (0.66 + life() * 0.48));
waveTimer = window.setInterval(() => {
if (!document.hidden) spawnWave();
}, intervalForWaves);
}
if (config.signalConvoys) trackTimeout(() => spawnConvoy(), 360);
if (config.signalWaves) trackTimeout(() => spawnWave(), 1450);
}
function destroy() {
stop();
ambientLayer.remove();
signalLayer.remove();
}
return {
start,
stop,
destroy,
pointerMoved,
clicked,
trigger: spawnWave,
get stats() {
return {
system: "original-conductor",
activeSignals,
ambientCurrentCount,
ambientNodeCount,
wavesTriggered,
reducedMotion,
signalHalos: config.signalHalos,
signalConvoys: config.signalConvoys,
signalWaves: config.signalWaves,
pointerWake: config.pointerWake,
};
},
};
}