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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
const TAU = Math.PI * 2;
const CANVAS_PIXEL_BUDGET = 1_500_000;
const CONTINUOUS_CANVAS_MAX_DIMENSION = 8192;
export const REFLECTION_DEFAULTS = Object.freeze({
reflectionStrength: 0.58,
reflectionCycle: 14800,
reflectionWidth: 0.3,
reflectionAngle: 18,
reflectionDrift: 6,
reflectionRest: 0.12,
reflectionQuiet: 0.84,
reflectionPointer: true,
reflectionColors: Object.freeze(["#00e6dc", "#49a9ff", "#a58cff"]),
});
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 colorWithAlpha(color, alpha) {
const amount = clamp(alpha);
const hexadecimal = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(String(color).trim());
if (hexadecimal) {
const value = hexadecimal[1].length === 3
? hexadecimal[1].split("").map((character) => `${character}${character}`).join("")
: hexadecimal[1];
const red = Number.parseInt(value.slice(0, 2), 16);
const green = Number.parseInt(value.slice(2, 4), 16);
const blue = Number.parseInt(value.slice(4, 6), 16);
return `rgba(${red}, ${green}, ${blue}, ${amount})`;
}
const rgb = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i.exec(String(color));
if (rgb) return `rgba(${rgb[1]}, ${rgb[2]}, ${rgb[3]}, ${amount})`;
return amount < 0.001 ? "rgba(0, 0, 0, 0)" : color;
}
function paletteFor(settings) {
const colors = Array.isArray(settings.reflectionColors)
? settings.reflectionColors.filter(Boolean)
: [];
return [
colors[0] || REFLECTION_DEFAULTS.reflectionColors[0],
colors[1] || colors[0] || REFLECTION_DEFAULTS.reflectionColors[1],
colors[2] || colors[1] || colors[0] || REFLECTION_DEFAULTS.reflectionColors[2],
];
}
function tracePath(context, points) {
if (!points?.length) return false;
context.beginPath();
context.moveTo(points[0][0], points[0][1]);
for (let index = 1; index < points.length; index += 1) {
context.lineTo(points[index][0], points[index][1]);
}
context.stroke();
return true;
}
function projectionRange(width, height, xAxis, yAxis) {
const projections = [
0,
width * xAxis,
height * yAxis,
width * xAxis + height * yAxis,
];
return {
minimum: Math.min(...projections),
maximum: Math.max(...projections),
};
}
export function createReflectionAnimation(
mount,
field,
config = {},
random = Math.random,
{ readingZone = null } = {},
) {
if (!mount?.appendChild) throw new TypeError("mount must be a DOM element");
if (!field?.traces || !Number.isFinite(field.width) || !Number.isFinite(field.height)) {
throw new TypeError("field must contain width, height, and traces");
}
let settings = { ...REFLECTION_DEFAULTS, ...config };
const randomValue = typeof random === "function" ? random : Math.random;
const motionPreference = window.matchMedia("(prefers-reduced-motion: reduce)");
let reducedMotion = motionPreference.matches;
let destroyed = false;
let requested = false;
let running = false;
let frameRequest;
let frameCount = 0;
let lastPaintTime = 0;
let targetFrameRate = 45;
let frameInterval = 1000 / targetFrameRate;
let pixelRatio = 1;
let pointer = null;
let sweepVisible = false;
const pulses = [];
const phaseOffset = randomValue() * Math.max(1000, settings.reflectionCycle);
const canvas = document.createElement("canvas");
canvas.setAttribute("aria-hidden", "true");
canvas.setAttribute("data-animation", "adom-reflection-canvas");
Object.assign(canvas.style, {
position: "absolute",
inset: "0",
width: "100%",
height: "100%",
display: "block",
pointerEvents: "none",
zIndex: "1",
contain: "strict",
mixBlendMode: config.reflectionBlendMode || "screen",
});
mount.appendChild(canvas);
const context = canvas.getContext("2d", { alpha: true, desynchronized: true })
|| canvas.getContext("2d");
const maskCanvas = document.createElement("canvas");
let maskContext = maskCanvas.getContext("2d", { alpha: true });
function life() {
return clamp(Number(settings.liveliness) || 0, 0, 1.5);
}
function featureScale() {
return Math.max(0.25, Number(settings.featureScale) || 1);
}
function shouldRun() {
return !destroyed
&& !reducedMotion
&& settings.motion !== false
&& life() > 0.02
&& Boolean(context);
}
function readingEllipse() {
const mountRect = mount.getBoundingClientRect();
const zoneRect = readingZone?.getBoundingClientRect?.();
if (
zoneRect
&& mountRect.width > 0
&& mountRect.height > 0
&& zoneRect.width > 0
&& zoneRect.height > 0
) {
const scaleX = field.width / mountRect.width;
const scaleY = field.height / mountRect.height;
const paddingX = Math.max(52, zoneRect.width * 0.14);
const paddingY = Math.max(38, zoneRect.height * 0.17);
return {
x: (zoneRect.left + zoneRect.width * 0.5 - mountRect.left) * scaleX,
y: (zoneRect.top + zoneRect.height * 0.5 - mountRect.top) * scaleY,
radiusX: (zoneRect.width * 0.5 + paddingX) * scaleX,
radiusY: (zoneRect.height * 0.5 + paddingY) * scaleY,
};
}
return {
x: field.width * 0.5,
y: field.height * 0.48,
radiusX: field.width * 0.34,
radiusY: field.height * 0.28,
};
}
function removeViaHoles() {
const innerRadius = Math.max(0, Number(settings.viaInnerRadius) || 0) * featureScale();
if (innerRadius <= 0) return;
maskContext.globalCompositeOperation = "destination-out";
for (const trace of field.traces) {
const endpoints = [];
if (trace.viaStart && trace.points?.length) endpoints.push(trace.points[0]);
if (trace.viaEnd && trace.points?.length) endpoints.push(trace.points.at(-1));
for (const point of endpoints) {
maskContext.beginPath();
maskContext.arc(point[0], point[1], innerRadius, 0, TAU);
maskContext.fill();
}
}
}
function softenReadingZone() {
const strength = clamp(Number(settings.reflectionQuiet), 0, 0.98);
if (strength <= 0) return;
const quiet = readingEllipse();
const layers = 10;
const layerAlpha = 1 - ((1 - strength) ** (1 / layers));
maskContext.globalCompositeOperation = "destination-out";
maskContext.fillStyle = "#000";
maskContext.globalAlpha = layerAlpha;
for (let index = 0; index < layers; index += 1) {
const amount = index / (layers - 1);
const size = 1 - amount * 0.58;
maskContext.beginPath();
maskContext.ellipse(
quiet.x,
quiet.y,
Math.max(1, quiet.radiusX * size),
Math.max(1, quiet.radiusY * size),
0,
0,
TAU,
);
maskContext.fill();
}
maskContext.globalAlpha = 1;
}
function buildMask() {
if (!maskContext || !maskCanvas.width || !maskCanvas.height) return;
maskContext.setTransform(1, 0, 0, 1, 0, 0);
maskContext.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
maskContext.setTransform(
maskCanvas.width / field.width,
0,
0,
maskCanvas.height / field.height,
0,
0,
);
maskContext.globalCompositeOperation = "source-over";
maskContext.globalAlpha = 1;
maskContext.strokeStyle = "#fff";
maskContext.fillStyle = "#fff";
maskContext.lineWidth = Math.max(0.2, Number(settings.traceWidth) || 1.58) * featureScale();
maskContext.lineCap = "butt";
maskContext.lineJoin = "miter";
for (const trace of field.traces) tracePath(maskContext, trace.points);
const outerRadius = Math.max(0, Number(settings.viaOuterRadius) || 0) * featureScale();
if (outerRadius > 0) {
for (const trace of field.traces) {
const endpoints = [];
if (trace.viaStart && trace.points?.length) endpoints.push(trace.points[0]);
if (trace.viaEnd && trace.points?.length) endpoints.push(trace.points.at(-1));
for (const point of endpoints) {
maskContext.beginPath();
maskContext.arc(point[0], point[1], outerRadius, 0, TAU);
maskContext.fill();
}
}
}
removeViaHoles();
softenReadingZone();
maskContext.setTransform(1, 0, 0, 1, 0, 0);
maskContext.globalCompositeOperation = "source-over";
maskContext.globalAlpha = 1;
}
function sizeCanvases() {
const width = Math.max(1, mount.clientWidth);
const height = Math.max(1, mount.clientHeight);
const pixelBudgetRatio = Math.sqrt(CANVAS_PIXEL_BUDGET / Math.max(1, width * height));
const legacyPixelRatio = clamp(
Math.min(window.devicePixelRatio || 1, 1.5, pixelBudgetRatio),
0.45,
1.5,
);
const continuousGeneration = settings.continuousGeneration === true;
pixelRatio = continuousGeneration
? Math.min(
window.devicePixelRatio || 1,
1.5,
pixelBudgetRatio,
CONTINUOUS_CANVAS_MAX_DIMENSION / width,
CONTINUOUS_CANVAS_MAX_DIMENSION / height,
)
: legacyPixelRatio;
const roundDimension = continuousGeneration ? Math.floor : Math.round;
const nextWidth = Math.max(1, roundDimension(width * pixelRatio));
const nextHeight = Math.max(1, roundDimension(height * pixelRatio));
if (canvas.width !== nextWidth) canvas.width = nextWidth;
if (canvas.height !== nextHeight) canvas.height = nextHeight;
if (maskCanvas.width !== nextWidth) maskCanvas.width = nextWidth;
if (maskCanvas.height !== nextHeight) maskCanvas.height = nextHeight;
if (context) {
context.setTransform(1, 0, 0, 1, 0, 0);
context.clearRect(0, 0, canvas.width, canvas.height);
}
const frameRatePixelCount = Math.max(1, Math.round(width * legacyPixelRatio))
* Math.max(1, Math.round(height * legacyPixelRatio));
targetFrameRate = frameRatePixelCount > 1_100_000 ? 30 : 45;
frameInterval = 1000 / targetFrameRate;
buildMask();
}
function reflectionStrength() {
const configured = clamp(Number(settings.reflectionStrength), 0, 1.4);
return configured * clamp(0.56 + life() * 0.34, 0.32, 1);
}
function drawSweep(timestamp) {
const cycle = Math.max(4800, Number(settings.reflectionCycle) || REFLECTION_DEFAULTS.reflectionCycle)
/ clamp(0.84 + life() * 0.12, 0.72, 1.1);
const rest = clamp(Number(settings.reflectionRest), 0, 0.42);
const travelPortion = 1 - rest;
const phase = ((timestamp + phaseOffset) % cycle) / cycle;
if (phase >= travelPortion) {
sweepVisible = false;
return;
}
sweepVisible = true;
const progress = phase / travelPortion;
const easedProgress = clamp(progress + Math.sin(progress * TAU) * 0.038);
const drift = (Number(settings.reflectionDrift) || 0)
* Math.sin(timestamp / 23000 * TAU + phaseOffset / cycle * TAU);
const angle = ((Number(settings.reflectionAngle) || 0) + drift) * Math.PI / 180;
const xAxis = Math.cos(angle);
const yAxis = Math.sin(angle);
const range = projectionRange(field.width, field.height, xAxis, yAxis);
const diagonal = Math.hypot(field.width, field.height);
const width = diagonal * clamp(Number(settings.reflectionWidth), 0.08, 0.72);
const center = range.minimum - width
+ easedProgress * (range.maximum - range.minimum + width * 2);
const palette = paletteFor(settings);
const strength = reflectionStrength();
const start = center - width * 0.5;
const end = center + width * 0.5;
const sheen = context.createLinearGradient(
xAxis * start,
yAxis * start,
xAxis * end,
yAxis * end,
);
sheen.addColorStop(0, colorWithAlpha(palette[0], 0));
sheen.addColorStop(0.14, colorWithAlpha(palette[0], 0.12));
sheen.addColorStop(0.31, colorWithAlpha(palette[0], 0.82));
sheen.addColorStop(0.5, colorWithAlpha(palette[1], 1));
sheen.addColorStop(0.7, colorWithAlpha(palette[2], 0.76));
sheen.addColorStop(0.88, colorWithAlpha(palette[2], 0.1));
sheen.addColorStop(1, colorWithAlpha(palette[2], 0));
context.globalAlpha = strength;
context.fillStyle = sheen;
context.fillRect(0, 0, field.width, field.height);
const edgeWidth = width * 0.1;
const edgeCenter = center - width * 0.035;
const edge = context.createLinearGradient(
xAxis * (edgeCenter - edgeWidth),
yAxis * (edgeCenter - edgeWidth),
xAxis * (edgeCenter + edgeWidth),
yAxis * (edgeCenter + edgeWidth),
);
edge.addColorStop(0, colorWithAlpha(palette[0], 0));
edge.addColorStop(0.44, colorWithAlpha(palette[1], 0.28));
edge.addColorStop(0.5, colorWithAlpha("#c9ffff", 0.88));
edge.addColorStop(0.57, colorWithAlpha(palette[2], 0.24));
edge.addColorStop(1, colorWithAlpha(palette[2], 0));
context.globalAlpha = strength * 0.24;
context.fillStyle = edge;
context.fillRect(0, 0, field.width, field.height);
}
function drawPointerReflection(timestamp) {
if (!settings.reflectionPointer || !pointer) return;
const age = timestamp - pointer.at;
if (age < 0 || age > 1800) return;
const fade = 1 - smoothstep(age / 1800);
const radius = Math.hypot(field.width, field.height) * 0.085;
const palette = paletteFor(settings);
const glow = context.createRadialGradient(
pointer.x,
pointer.y,
0,
pointer.x,
pointer.y,
radius,
);
glow.addColorStop(0, colorWithAlpha(palette[0], 0.7));
glow.addColorStop(0.42, colorWithAlpha(palette[1], 0.32));
glow.addColorStop(1, colorWithAlpha(palette[2], 0));
context.globalAlpha = reflectionStrength() * fade * 0.2;
context.fillStyle = glow;
context.fillRect(
pointer.x - radius,
pointer.y - radius,
radius * 2,
radius * 2,
);
}
function drawPulses(timestamp) {
const diagonal = Math.hypot(field.width, field.height);
const palette = paletteFor(settings);
for (let index = pulses.length - 1; index >= 0; index -= 1) {
const pulse = pulses[index];
const progress = (timestamp - pulse.startedAt) / pulse.duration;
if (progress >= 1) {
pulses.splice(index, 1);
continue;
}
const eased = smoothstep(progress);
const radius = diagonal * (0.025 + eased * 0.22);
const ring = context.createRadialGradient(
pulse.x,
pulse.y,
0,
pulse.x,
pulse.y,
radius,
);
ring.addColorStop(0, colorWithAlpha(pulse.color, 0));
ring.addColorStop(0.58, colorWithAlpha(pulse.color, 0));
ring.addColorStop(0.78, colorWithAlpha(pulse.color, 0.64));
ring.addColorStop(0.9, colorWithAlpha(palette[1], 0.34));
ring.addColorStop(1, colorWithAlpha(palette[2], 0));
context.globalAlpha = reflectionStrength() * (1 - smoothstep(progress)) * 0.5;
context.fillStyle = ring;
context.fillRect(
pulse.x - radius,
pulse.y - radius,
radius * 2,
radius * 2,
);
}
}
function paint(timestamp) {
context.setTransform(1, 0, 0, 1, 0, 0);
context.clearRect(0, 0, canvas.width, canvas.height);
context.setTransform(
canvas.width / field.width,
0,
0,
canvas.height / field.height,
0,
0,
);
context.globalCompositeOperation = "source-over";
drawSweep(timestamp);
drawPointerReflection(timestamp);
drawPulses(timestamp);
context.setTransform(1, 0, 0, 1, 0, 0);
context.globalAlpha = 1;
context.globalCompositeOperation = "destination-in";
context.drawImage(maskCanvas, 0, 0);
context.globalCompositeOperation = "source-over";
}
function renderFrame(timestamp) {
if (!running || destroyed || !canvas.isConnected) return;
frameRequest = window.requestAnimationFrame(renderFrame);
const elapsed = lastPaintTime ? timestamp - lastPaintTime : frameInterval;
if (elapsed < frameInterval) return;
lastPaintTime = lastPaintTime ? timestamp - (elapsed % frameInterval) : timestamp;
frameCount += 1;
paint(timestamp);
}
function halt(clear = true) {
running = false;
window.cancelAnimationFrame(frameRequest);
frameRequest = undefined;
lastPaintTime = 0;
if (clear && context) {
context.setTransform(1, 0, 0, 1, 0, 0);
context.clearRect(0, 0, canvas.width, canvas.height);
}
}
function begin() {
if (running || !requested || !shouldRun() || document.hidden) return;
running = true;
lastPaintTime = 0;
frameRequest = window.requestAnimationFrame(renderFrame);
}
function start() {
requested = true;
begin();
}
function stop() {
requested = false;
halt();
}
function pointerMoved(point) {
pointer = point ? { ...point, at: performance.now() } : null;
}
function pointerLeft() {
if (pointer) pointer.at = performance.now() - 1800;
}
function trigger(origin = null) {
if (!running || reducedMotion || settings.motion === false) return false;
const point = origin || {
x: field.width * (0.2 + randomValue() * 0.6),
y: field.height * (0.22 + randomValue() * 0.56),
};
const palette = paletteFor(settings);
const colorIndex = clamp(Math.floor(point.x / Math.max(1, field.width) * 3), 0, 2);
if (pulses.length >= 2) pulses.shift();
pulses.push({
x: clamp(point.x, 0, field.width),
y: clamp(point.y, 0, field.height),
color: palette[colorIndex],
startedAt: performance.now(),
duration: 1180,
});
return true;
}
function update(nextConfig = {}) {
if (destroyed) return api;
settings = { ...settings, ...nextConfig };
canvas.style.mixBlendMode = settings.reflectionBlendMode || "screen";
sizeCanvases();
if (!shouldRun()) halt();
else if (requested) begin();
return api;
}
function resize() {
if (!destroyed) sizeCanvases();
return api;
}
function onPreferenceChange(event) {
reducedMotion = event.matches;
if (reducedMotion) halt();
else if (requested) begin();
}
function onVisibilityChange() {
if (document.hidden) halt(false);
else if (requested) begin();
}
function destroy() {
if (destroyed) return;
destroyed = true;
stop();
pulses.length = 0;
pointer = null;
document.removeEventListener("visibilitychange", onVisibilityChange);
if (motionPreference.removeEventListener) {
motionPreference.removeEventListener("change", onPreferenceChange);
} else {
motionPreference.removeListener?.(onPreferenceChange);
}
canvas.remove();
canvas.width = 0;
canvas.height = 0;
maskCanvas.width = 0;
maskCanvas.height = 0;
maskContext = null;
}
const api = {
start,
stop,
destroy,
update,
resize,
pointerMoved,
pointerLeft,
clicked: trigger,
trigger,
get stats() {
return {
system: "adom-reflection",
renderer: "canvas-precomposed-trace-mask",
animationNodes: canvas.isConnected ? 1 : 0,
activeReflections: Number(sweepVisible) + pulses.length,
activePulses: pulses.length,
frameCount,
targetFrameRate,
rendererPixelRatio: pixelRatio,
maskTraceCount: field.traces.length,
perFrameTraceBudget: 0,
quietZoneProtected: true,
readingZoneTracked: Boolean(readingZone),
reducedMotion,
running,
};
},
};
sizeCanvases();
document.addEventListener("visibilitychange", onVisibilityChange);
if (motionPreference.addEventListener) {
motionPreference.addEventListener("change", onPreferenceChange);
} else {
motionPreference.addListener?.(onPreferenceChange);
}
return api;
}