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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
import { createConductorAnimation } from "./circuit-animation-conductor.js?v=1";
import { createHandshakeAnimation } from "./circuit-animation-handshake.js?v=2";
import { createTraceGrowthAnimation } from "./circuit-animation-growth.js?v=1";
import {
createReflectionAnimation,
REFLECTION_DEFAULTS,
} from "./circuit-animation-reflection.js?v=1";
import {
createCircuitBackground as createLegacyCircuitBackground,
DEFAULTS as GEOMETRY_DEFAULTS,
ANIMATION_PRESETS as LEGACY_ANIMATION_PRESETS,
generateCircuitField,
pathData,
} from "./circuit-background-v4.js?geometry=1";
const SVG_NS = "http://www.w3.org/2000/svg";
const DEFAULTS = Object.freeze({
...GEOMETRY_DEFAULTS,
animationSystem: "none",
motion: false,
liveliness: 0,
atmosphereStrength: 0,
handshakeInterval: 1650,
handshakeSpeed: 230,
handshakeLength: 48,
maxHandshakes: 3,
growthDuration: 7600,
growthGlow: 0.82,
...REFLECTION_DEFAULTS,
});
const ANIMATION_PRESETS = Object.freeze({
none: LEGACY_ANIMATION_PRESETS.none,
conductor: LEGACY_ANIMATION_PRESETS.conductor,
routed: LEGACY_ANIMATION_PRESETS.adom,
handshake: Object.freeze({
animationSystem: "handshake",
motion: true,
liveliness: 0.72,
handshakeInterval: 1650,
handshakeSpeed: 230,
handshakeLength: 48,
maxHandshakes: 3,
atmosphereStrength: 0,
}),
growth: Object.freeze({
animationSystem: "growth",
motion: true,
liveliness: 0.72,
growthDuration: 7600,
growthGlow: 0.88,
atmosphereStrength: 0.16,
}),
reflection: Object.freeze({
animationSystem: "reflection",
motion: true,
liveliness: 0.68,
reflectionStrength: 0.62,
reflectionCycle: 14800,
reflectionWidth: 0.3,
reflectionAngle: 18,
reflectionDrift: 6,
reflectionRest: 0.12,
reflectionQuiet: 0.84,
reflectionPointer: true,
atmosphereStrength: 0.14,
}),
});
let instanceCount = 0;
function hashSeed(value) {
let hash = 2166136261;
for (const char of String(value)) {
hash ^= char.charCodeAt(0);
hash = Math.imul(hash, 16777619);
}
return hash >>> 0;
}
function mulberry32(seed) {
return function random() {
let value = (seed += 0x6d2b79f5);
value = Math.imul(value ^ (value >>> 15), value | 1);
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
};
}
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 createModernCircuitBackground(mount, options = {}) {
if (!(mount instanceof Element)) throw new TypeError("mount must be a DOM element");
const id = `circuit-field-${++instanceCount}`;
let config = { ...DEFAULTS, ...options };
let field;
let svg;
let conductor;
let handshake;
let growth;
let reflection;
let resizeObserver;
let resizeTimer;
let pointer = null;
let lastSize = "";
const interactionTarget = mount.parentElement || mount;
const readingZone = interactionTarget.querySelector?.("[data-circuit-reading-zone]") || null;
const motionPreference = window.matchMedia("(prefers-reduced-motion: reduce)");
let reducedMotion = motionPreference.matches;
const originalPosition = mount.style.position;
const originalOverflow = mount.style.overflow;
if (getComputedStyle(mount).position === "static") mount.style.position = "relative";
mount.style.overflow = "hidden";
function life() {
return Math.max(0, Math.min(1.5, config.liveliness));
}
function featureScale() {
return Math.max(0.25, config.featureScale);
}
function addVia(group, point, role) {
const scale = featureScale();
const circles = [
svgElement("circle", {
cx: point[0], cy: point[1], r: config.viaOuterRadius * scale, fill: config.traceColor,
}),
svgElement("circle", {
cx: point[0], cy: point[1], r: config.viaInnerRadius * scale,
fill: "var(--circuit-hole, #0d1117)",
}),
];
if (!role) {
group.append(...circles);
return circles;
}
const via = svgElement("g", { "data-trace-via": role });
via.append(...circles);
group.append(via);
return via;
}
function destroyAnimationSystems() {
conductor?.destroy();
handshake?.destroy();
growth?.destroy();
reflection?.destroy();
conductor = undefined;
handshake = undefined;
growth = undefined;
reflection = undefined;
}
function build(force = false) {
const pixelWidth = Math.max(1, mount.clientWidth);
const pixelHeight = Math.max(1, mount.clientHeight);
const worldWidth = pixelWidth / config.scale;
const worldHeight = pixelHeight / config.scale;
const pitch = config.pitch * featureScale();
const size = `${Math.floor(worldWidth / pitch)}x${Math.floor(worldHeight / pitch)}`;
if (!force && size === lastSize) return false;
lastSize = size;
destroyAnimationSystems();
svg?.remove();
field = generateCircuitField({ ...config, width: worldWidth, height: worldHeight });
const motionNamespace = config.animationSystem === "conductor"
? "signals"
: config.animationSystem === "handshake"
? "handshakes"
: config.animationSystem;
const motionRandom = mulberry32(hashSeed(`${config.seed}:${size}:${motionNamespace}`));
svg = svgElement("svg", {
id,
"aria-hidden": true,
focusable: false,
viewBox: `0 0 ${field.width} ${field.height}`,
preserveAspectRatio: "none",
});
Object.assign(svg.style, {
position: "absolute",
inset: "0",
width: "100%",
height: "100%",
display: "block",
pointerEvents: "none",
});
const traceLayer = svgElement("g", { "data-static-traces": "" });
const growthMarkup = config.animationSystem === "growth";
for (const [index, trace] of field.traces.entries()) {
const tracePath = svgElement("path", {
...(growthMarkup ? { "data-trace-path": "" } : {}),
d: pathData(trace.points),
fill: "none",
stroke: config.traceColor,
"stroke-width": config.traceWidth * config.featureScale,
"stroke-linecap": "butt",
"stroke-linejoin": "miter",
});
if (!growthMarkup) {
traceLayer.append(tracePath);
if (trace.viaStart) addVia(traceLayer, trace.points[0]);
if (trace.viaEnd) addVia(traceLayer, trace.points.at(-1));
continue;
}
const traceGroup = svgElement("g", { "data-circuit-trace": index });
traceGroup.append(tracePath);
if (trace.viaStart) addVia(traceGroup, trace.points[0], "start");
if (trace.viaEnd) addVia(traceGroup, trace.points.at(-1), "end");
traceLayer.append(traceGroup);
}
svg.append(traceLayer);
mount.prepend(svg);
if (config.animationSystem === "conductor") {
conductor = createConductorAnimation(svg, field, config, motionRandom);
} else if (
config.animationSystem === "handshake"
&& config.motion
&& !reducedMotion
&& life() > 0.02
) {
handshake = createHandshakeAnimation(mount, field, config, motionRandom, { readingZone });
} else if (config.animationSystem === "growth") {
growth = createTraceGrowthAnimation(svg, field, config, motionRandom, { traceLayer });
} else if (config.animationSystem === "reflection") {
reflection = createReflectionAnimation(mount, field, config, motionRandom, { readingZone });
}
return true;
}
function onPointerMove(event) {
if (!field) return;
const rect = mount.getBoundingClientRect();
pointer = {
x: (event.clientX - rect.left) / rect.width * field.width,
y: (event.clientY - rect.top) / rect.height * field.height,
at: performance.now(),
};
conductor?.pointerMoved(pointer);
handshake?.pointerMoved(pointer);
reflection?.pointerMoved(pointer);
}
function onPointerLeave() {
if (pointer) pointer.at = performance.now() - 3000;
handshake?.pointerLeft();
reflection?.pointerLeft();
}
function onClick(event) {
onPointerMove(event);
if (config.animationSystem === "conductor") conductor?.clicked(pointer);
else if (config.animationSystem === "handshake") handshake?.clicked(pointer);
else if (config.animationSystem === "reflection") reflection?.clicked(pointer);
}
function stopMotion() {
conductor?.stop();
handshake?.stop();
growth?.stop();
reflection?.stop();
}
function startMotion() {
stopMotion();
if (config.animationSystem === "none" || !config.motion || reducedMotion || life() <= 0.02) return;
if (config.animationSystem === "conductor") conductor?.start();
else if (config.animationSystem === "handshake") handshake?.start();
else if (config.animationSystem === "growth") growth?.start();
else if (config.animationSystem === "reflection") reflection?.start();
}
function onMotionPreferenceChange(event) {
reducedMotion = event.matches;
if (reducedMotion) {
stopMotion();
return;
}
lastSize = "";
build(true);
startMotion();
}
function regenerate(nextOptions = {}) {
stopMotion();
config = { ...config, ...nextOptions };
lastSize = "";
build(true);
startMotion();
return api;
}
function triggerAnimation(origin = null) {
if (config.animationSystem === "conductor") return conductor?.trigger(origin) || false;
if (config.animationSystem === "handshake") return handshake?.trigger(origin) || false;
if (config.animationSystem === "growth") return growth?.trigger() || false;
if (config.animationSystem === "reflection") return reflection?.trigger(origin) || false;
return false;
}
function exportSvg() {
const clone = svg.cloneNode(true);
clone.querySelectorAll("[data-animation]").forEach((layer) => layer.remove());
clone.querySelectorAll("[data-trace-path]").forEach((path) => {
path.removeAttribute("stroke-dasharray");
path.removeAttribute("stroke-dashoffset");
path.style.removeProperty("opacity");
path.style.removeProperty("will-change");
});
clone.querySelectorAll("[data-circuit-trace]").forEach((group, index) => {
const path = group.querySelector("[data-trace-path]");
const trace = field.traces[index];
if (path && trace) path.setAttribute("d", pathData(trace.points));
});
clone.querySelectorAll("[data-trace-via]").forEach((via) => {
via.style.removeProperty("opacity");
via.style.removeProperty("will-change");
});
clone.removeAttribute("style");
return `<?xml version="1.0" encoding="UTF-8"?>\n${clone.outerHTML}`;
}
function destroy() {
stopMotion();
destroyAnimationSystems();
window.clearTimeout(resizeTimer);
resizeObserver?.disconnect();
interactionTarget.removeEventListener("pointermove", onPointerMove);
interactionTarget.removeEventListener("pointerleave", onPointerLeave);
interactionTarget.removeEventListener("click", onClick);
if (motionPreference.removeEventListener) {
motionPreference.removeEventListener("change", onMotionPreferenceChange);
} else {
motionPreference.removeListener?.(onMotionPreferenceChange);
}
svg?.remove();
mount.style.position = originalPosition;
mount.style.overflow = originalOverflow;
}
const api = {
regenerate,
triggerAnimation,
triggerBloom: triggerAnimation,
exportSvg,
destroy,
get field() { return field; },
get animationStats() {
if (config.animationSystem === "none") {
return { system: "none", animationNodes: 0, frameCount: 0, reducedMotion };
}
if (config.animationSystem === "conductor") return conductor?.stats;
if (config.animationSystem === "handshake") {
return handshake?.stats || {
system: "adom-handshake",
animationNodes: 0,
activeTransactions: 0,
frameCount: 0,
reducedMotion,
};
}
if (config.animationSystem === "growth") {
return growth?.stats || {
system: "trace-growth",
animationNodes: 0,
activeTraces: 0,
frameCount: 0,
reducedMotion,
};
}
if (config.animationSystem === "reflection") {
return reflection?.stats || {
system: "adom-reflection",
animationNodes: 0,
activeReflections: 0,
frameCount: 0,
reducedMotion,
};
}
return { system: "unknown", animationNodes: 0, frameCount: 0, reducedMotion };
},
};
build(true);
resizeObserver = new ResizeObserver(() => {
window.clearTimeout(resizeTimer);
resizeTimer = window.setTimeout(() => {
if (build()) {
stopMotion();
startMotion();
}
}, 220);
});
resizeObserver.observe(mount);
if (config.interactive) {
interactionTarget.addEventListener("pointermove", onPointerMove, { passive: true });
interactionTarget.addEventListener("pointerleave", onPointerLeave, { passive: true });
interactionTarget.addEventListener("click", onClick);
}
if (motionPreference.addEventListener) {
motionPreference.addEventListener("change", onMotionPreferenceChange);
} else {
motionPreference.addListener?.(onMotionPreferenceChange);
}
startMotion();
return api;
}
export function createCircuitBackground(mount, options = {}) {
let config = { ...DEFAULTS, ...options };
let destroyed = false;
const usesRoutedField = (value) => value.animationSystem === "adom";
const createEngine = () => usesRoutedField(config)
? createLegacyCircuitBackground(mount, config)
: createModernCircuitBackground(mount, config);
let engine = createEngine();
const api = {
regenerate(nextOptions = {}) {
if (destroyed) throw new Error("Cannot regenerate a destroyed circuit background");
const wasRouted = usesRoutedField(config);
config = { ...config, ...nextOptions };
if (wasRouted !== usesRoutedField(config)) {
engine.destroy();
engine = createEngine();
} else {
engine.regenerate(nextOptions);
}
return api;
},
triggerAnimation(origin = null) {
return engine.triggerAnimation?.(origin) || false;
},
triggerBloom(origin = null) {
return engine.triggerBloom?.(origin) || engine.triggerAnimation?.(origin) || false;
},
exportSvg() {
return engine.exportSvg();
},
destroy() {
if (destroyed) return;
destroyed = true;
engine.destroy();
},
get field() { return engine.field; },
get animationStats() { return engine.animationStats; },
};
return api;
}
export { DEFAULTS, ANIMATION_PRESETS, generateCircuitField, pathData };