Utility functions for creative coding — each section shows an HTML example, a Canvas example, and a combined example.
lerp, map, smoothstep, clamp, randomInRange, distance, normalize, degToRad, radToDeg, wrap
<div id="area" style="position: relative; height: 250px">
<div id="follower" style="
position: absolute; width: 36px; height: 36px;
border-radius: 50%; background: #6c5ce7;
pointer-events: none;
transform: translate(-50%, -50%);
"></div>
</div>
<script type="module">
import { lerp } from '@andresclua/creative';
const area = document.getElementById('area');
const follower = document.getElementById('follower');
let pos = { x: 0, y: 0 };
let target = { x: 0, y: 0 };
const factor = 0.08;
area.addEventListener('mousemove', (e) => {
const rect = area.getBoundingClientRect();
target.x = e.clientX - rect.left;
target.y = e.clientY - rect.top;
});
function tick() {
pos.x = lerp(pos.x, target.x, factor);
pos.y = lerp(pos.y, target.y, factor);
follower.style.left = pos.x + 'px';
follower.style.top = pos.y + 'px';
requestAnimationFrame(tick);
}
tick();
</script>
div + style.left/top. No canvas needed. Works with any element.
<canvas id="canvas"></canvas>
<script type="module">
import { distance, map, clamp, randomInRange } from '@andresclua/creative';
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = canvas.parentElement.clientWidth;
canvas.height = 350;
let mouse = { x: -9999, y: -9999 };
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouse.x = e.clientX - rect.left;
mouse.y = e.clientY - rect.top;
});
// Seed 300 particles with random positions + sizes
const particles = [];
for (let i = 0; i < 300; i++) {
particles.push({
x: randomInRange(0, canvas.width),
y: randomInRange(0, canvas.height),
baseRadius: randomInRange(2, 5),
});
}
function tick() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const maxDist = 200;
for (const p of particles) {
const d = distance(p.x, p.y, mouse.x, mouse.y);
const factor = clamp(1 - d / maxDist, 0, 1);
const radius = map(factor, 0, 1, p.baseRadius, p.baseRadius * 4);
const alpha = map(factor, 0, 1, 0.15, 1);
ctx.beginPath();
ctx.arc(p.x, p.y, radius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(108, 92, 231, ${alpha})`;
ctx.fill();
}
requestAnimationFrame(tick);
}
tick();
</script>
ctx.arc() call.
<canvas id="canvas"></canvas>
<script type="module">
import { lerp, smoothstep, degToRad, wrap } from '@andresclua/creative';
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 800; canvas.height = 450;
let mouse = { x: canvas.width / 2, y: canvas.height / 2 };
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouse.x = e.clientX - rect.left;
mouse.y = e.clientY - rect.top;
});
const arms = 4, stars = [];
for (let i = 0; i < 800; i++) {
stars.push({
dist: Math.random() * 0.5,
angleOffset: Math.random() * Math.PI * 2,
size: 0.5 + Math.random() * 1.5,
brightness: 0.3 + Math.random() * 0.7,
});
}
let last = performance.now();
function tick(now) {
const elapsed = (now - last) / 1000;
const cx = lerp(canvas.width / 2, mouse.x, 0.15);
const cy = lerp(canvas.height / 2, mouse.y, 0.15);
const maxR = Math.min(canvas.width, canvas.height) * 0.45;
ctx.fillStyle = 'rgba(10, 10, 15, 0.15)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (const s of stars) {
const armAngle = degToRad((360 / arms) *
Math.floor(s.angleOffset / (Math.PI * 2 / arms) + 0.5));
const angle = wrap(s.dist * 6 + armAngle + elapsed, 0, Math.PI * 2);
const glow = smoothstep(0, 0.3, s.dist) * s.brightness;
ctx.beginPath();
ctx.arc(
cx + Math.cos(angle) * s.dist * maxR,
cy + Math.sin(angle) * s.dist * maxR,
s.size, 0, Math.PI * 2
);
ctx.fillStyle = `rgba(162, 155, 254, ${glow})`;
ctx.fill();
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
</script>
wrap, degToRad, smoothstep, lerp together.
10 standard curves + spring + damp
<div id="box" style="
position: absolute; left: 20px; top: 50%;
width: 50px; height: 50px; border-radius: 8px;
background: #6c5ce7; transform: translateY(-50%);
"></div>
<button id="play">Play</button>
<script type="module">
import { easeOutCubic } from '@andresclua/creative';
const box = document.getElementById('box');
const playBtn = document.getElementById('play');
const startX = 20;
const endX = 600; // adjust to container width
const duration = 1000; // ms
playBtn.addEventListener('click', () => {
const start = performance.now();
function tick(now) {
const elapsed = now - start;
const t = Math.min(elapsed / duration, 1);
const eased = easeOutCubic(t);
box.style.left = startX + (endX - startX) * eased + 'px';
if (t < 1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
});
</script>
element.style.left + easing. No canvas, no framework.
<canvas id="canvas"></canvas>
<button id="trigger">Trigger</button>
<script type="module">
import { easeOutCubic, clamp, map } from '@andresclua/creative';
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 800; canvas.height = 350;
const barCount = 12;
const stagger = 0.06; // seconds between each bar
const duration = 0.6; // seconds per bar
let startTime = -1;
document.getElementById('trigger').addEventListener('click', () => {
startTime = performance.now() / 1000;
});
function tick() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const now = performance.now() / 1000;
const barH = (canvas.height - 20) / barCount - 6;
for (let i = 0; i < barCount; i++) {
const localT = startTime < 0
? 0
: clamp((now - startTime - i * stagger) / duration, 0, 1);
const eased = easeOutCubic(localT);
const barW = eased * (canvas.width - 40);
const hue = map(i, 0, barCount - 1, 250, 170);
ctx.fillStyle = `hsla(${hue}, 70%, 65%, ${0.4 + eased * 0.6})`;
ctx.beginPath();
ctx.roundRect(20, 10 + i * (barH + 6), barW, barH, 4);
ctx.fill();
}
requestAnimationFrame(tick);
}
tick();
</script>
clamp + stagger offset creates the cascade.
roundRect + HSL in one draw call.
<canvas id="canvas"></canvas>
<script type="module">
import { spring } from '@andresclua/creative';
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 800; canvas.height = 350;
let t = -1, origin = {x:400,y:175}, dest = {x:400,y:175};
const trail = [];
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
origin = { ...dest };
dest = { x: e.clientX - rect.left, y: e.clientY - rect.top };
t = 0;
trail.length = 0;
});
function tick(now) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (t < 0) {
ctx.fillStyle = 'rgba(136,136,170,0.4)';
ctx.font = '14px monospace';
ctx.textAlign = 'center';
ctx.fillText('click anywhere', canvas.width / 2, canvas.height / 2);
ctx.textAlign = 'start';
} else {
t = Math.min(t + 0.02, 1);
const s = spring(t, 0.5, 15);
const x = origin.x + (dest.x - origin.x) * s;
const y = origin.y + (dest.y - origin.y) * s;
trail.push({ x, y });
if (trail.length > 120) trail.shift();
// Draw trail
ctx.beginPath();
ctx.strokeStyle = 'rgba(162,155,254,0.3)';
ctx.lineWidth = 2;
trail.forEach((p, i) =>
i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
ctx.stroke();
// Draw ball
ctx.beginPath();
ctx.arc(x, y, 12, 0, Math.PI * 2);
ctx.fillStyle = '#6c5ce7';
ctx.fill();
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
</script>
el.style.transform = \`translate(\${x}px, \${y}px)\`.
getMousePos, calcWinsize, createMouseTracker, mouseDistFromElement
<div id="stat-x">0</div>
<div id="stat-y">0</div>
<div id="stat-speed">0</div>
<div id="stat-angle">0</div>
<script type="module">
import { createMouseTracker } from '@andresclua/creative';
const tracker = createMouseTracker();
function tick() {
document.getElementById('stat-x').textContent = Math.round(tracker.pos.x);
document.getElementById('stat-y').textContent = Math.round(tracker.pos.y);
document.getElementById('stat-speed').textContent = Math.round(tracker.speed);
document.getElementById('stat-angle').textContent =
(tracker.angle * 180 / Math.PI).toFixed(1) + '\u00B0';
requestAnimationFrame(tick);
}
tick();
// When done: tracker.destroy();
</script>
textContent updates — no canvas needed.
<div id="area" style="position: relative">
<div class="magnetic-btn" data-strength="0.4">A</div>
<div class="magnetic-btn" data-strength="0.3">B</div>
</div>
<script type="module">
import { mouseDistFromElement, map } from '@andresclua/creative';
const area = document.getElementById('area');
const btns = area.querySelectorAll('.magnetic-btn');
area.addEventListener('mousemove', (e) => {
btns.forEach((btn) => {
const d = mouseDistFromElement(e, btn);
const threshold = 150;
if (d < threshold) {
const rect = btn.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const strength = parseFloat(btn.dataset.strength);
const pull = map(d, 0, threshold, strength, 0);
const tx = (e.clientX - cx) * pull;
const ty = (e.clientY - cy) * pull;
btn.style.transform = `translate(${tx}px, ${ty}px)`;
} else {
btn.style.transform = 'translate(0, 0)';
}
});
});
area.addEventListener('mouseleave', () => {
btns.forEach(btn => btn.style.transform = 'translate(0, 0)');
});
</script>
transform: translate(). Canvas can't have clickable buttons.
<canvas id="canvas"></canvas>
<script type="module">
import {
createMouseTracker, distance, map, clamp,
randomInRange, damp
} from '@andresclua/creative';
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 800; canvas.height = 450;
const tracker = createMouseTracker();
// Create 150 particles at random home positions
const particles = [];
for (let i = 0; i < 150; i++) {
const x = randomInRange(20, canvas.width - 20);
const y = randomInRange(20, canvas.height - 20);
particles.push({ x, y, homeX: x, homeY: y, vx: 0, vy: 0,
radius: 2 + Math.random() * 2 });
}
let lastTime = performance.now();
function tick(now) {
const dt = (now - lastTime) / 1000;
lastTime = now;
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const p of particles) {
// Mouse velocity pushes nearby particles
const d = distance(p.x, p.y, tracker.pos.x, tracker.pos.y);
if (d < 180 && d > 0) {
const force = map(d, 0, 180, 1, 0);
p.vx += tracker.velocity.x * 0.015 * force;
p.vy += tracker.velocity.y * 0.015 * force;
}
p.x += p.vx * dt;
p.y += p.vy * dt;
p.x = damp(p.x, p.homeX, 2, dt);
p.y = damp(p.y, p.homeY, 2, dt);
p.vx = damp(p.vx, 0, 5, dt);
p.vy = damp(p.vy, 0, 5, dt);
// Draw
const disp = distance(p.x, p.y, p.homeX, p.homeY);
const hue = map(clamp(disp, 0, 100), 0, 100, 250, 340);
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${hue}, 70%, 65%, ${0.5 + disp*0.005})`;
ctx.fill();
}
// Draw lines between nearby particles
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const d = distance(particles[i].x, particles[i].y,
particles[j].x, particles[j].y);
if (d < 80) {
ctx.strokeStyle = `rgba(108,92,231,${map(d,0,80,0.25,0)})`;
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.stroke();
}
}
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
</script>
Utility functions you drop inside any render loop — Three.js, Pixi, GSAP, Canvas, whatever.
<div id="container"></div>
<button id="regen">Regenerate</button>
<script type="module">
import { randomInRange, map, clamp } from '@andresclua/creative';
const container = document.getElementById('container');
function generate() {
container.innerHTML = '';
const count = randomInRange(12, 24);
for (let i = 0; i < count; i++) {
const size = randomInRange(30, 80);
const hue = randomInRange(230, 300);
const radius = randomInRange(4, size / 2);
const opacity = map(size, 30, 80, 0.4, 1);
const el = document.createElement('div');
el.style.cssText = `
width: ${size}px; height: ${size}px;
border-radius: ${radius}px;
background: hsla(${hue}, 70%, 65%, ${opacity});
`;
container.appendChild(el);
}
}
document.getElementById('regen').addEventListener('click', generate);
generate();
</script>
randomInRange + map = controlled randomness for any DOM element.
<canvas id="canvas"></canvas>
<script type="module">
import { lerp, damp } from '@andresclua/creative';
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 800; canvas.height = 350;
let mouse = { x: 0, y: 0 };
let lerpPos = { x: 0, y: 0 };
let dampPos = { x: 0, y: 0 };
const lambda = 5;
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouse.x = e.clientX - rect.left;
mouse.y = e.clientY - rect.top;
});
let lastTime = performance.now();
function tick(now) {
const dt = (now - lastTime) / 1000;
lastTime = now;
// BAD: frame-rate dependent
lerpPos.x = lerp(lerpPos.x, mouse.x, 0.1);
lerpPos.y = lerp(lerpPos.y, mouse.y, 0.1);
// GOOD: frame-rate independent
dampPos.x = damp(dampPos.x, mouse.x, lambda, dt);
dampPos.y = damp(dampPos.y, mouse.y, lambda, dt);
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw both
ctx.beginPath();
ctx.arc(lerpPos.x, canvas.height * 0.25, 14, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(253,121,168,0.8)';
ctx.fill();
ctx.beginPath();
ctx.arc(dampPos.x, canvas.height * 0.75, 14, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(108,92,231,0.8)';
ctx.fill();
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
// In Three.js: camera.position.x = damp(cam.x, target.x, 5, clock.getDelta())
// In Pixi: sprite.x = damp(sprite.x, target, 5, ticker.deltaMS / 1000)
</script>
damp is pure math — drop it into Three.js, Pixi, GSAP, or raw rAF.
<canvas id="canvas"></canvas>
<script type="module">
import { randomInRange, damp } from '@andresclua/creative';
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 800; canvas.height = 450;
const particles = [];
const gravity = 150;
function spawn(x, y, count) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = randomInRange(80, 330);
particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 150,
life: 1,
decay: 0.3 + Math.random() * 0.6,
size: randomInRange(2, 6),
hue: randomInRange(240, 290),
});
}
}
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
spawn(e.clientX - rect.left, e.clientY - rect.top, 50);
});
let lastTime = performance.now();
function tick(now) {
const dt = (now - lastTime) / 1000;
lastTime = now;
// Auto-spawn from bottom
spawn(canvas.width / 2 + (Math.random() - 0.5) * 40, canvas.height - 10, 3);
ctx.fillStyle = 'rgba(10, 10, 15, 0.15)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.vy += gravity * dt;
p.vx = damp(p.vx, 0, 1.5, dt); // air resistance
p.x += p.vx * dt;
p.y += p.vy * dt;
p.life -= p.decay * dt;
if (p.life <= 0) { particles.splice(i, 1); continue; }
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${p.hue}, 70%, 65%, ${p.life * 0.8})`;
ctx.fill();
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
</script>
damp = air resistance, randomInRange = spawn variation.
new THREE.Vector3(randomInRange(-2,2), ...).
hexToRgb, rgbToHex, lerpColor
<input type="color" id="colorA" value="#6c5ce7" />
<input type="color" id="colorB" value="#00cec9" />
<input type="range" id="slider" min="0" max="1" step="0.01" value="0.5" />
<div id="swatch"></div>
<span id="hex"></span>
<script type="module">
import { lerpColor } from '@andresclua/creative';
const colorA = document.getElementById('colorA');
const colorB = document.getElementById('colorB');
const slider = document.getElementById('slider');
const swatch = document.getElementById('swatch');
const hex = document.getElementById('hex');
function update() {
const t = parseFloat(slider.value);
const result = lerpColor(colorA.value, colorB.value, t);
swatch.style.background = result;
hex.textContent = result;
}
colorA.addEventListener('input', update);
colorB.addEventListener('input', update);
slider.addEventListener('input', update);
update();
</script>
style.backgroundColor, CSS variables, or SVG fills.
<canvas id="canvas"></canvas>
<script type="module">
import { lerpColor } from '@andresclua/creative';
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 800; canvas.height = 250;
const colorA = '#e74c3c', colorB = '#3498db', colorC = '#2ecc71';
let start = performance.now();
function tick(now) {
const elapsed = (now - start) / 1000;
const w = canvas.width, h = canvas.height;
for (let x = 0; x < w; x++) {
const t = x / w;
const color = t < 0.5
? lerpColor(colorA, colorB, t * 2)
: lerpColor(colorB, colorC, (t - 0.5) * 2);
ctx.fillStyle = color;
ctx.fillRect(x, 0, 1, h);
}
// Animated playhead
const playT = (Math.sin(elapsed * 0.8) + 1) / 2;
const px = playT * w;
const playColor = playT < 0.5
? lerpColor(colorA, colorB, playT * 2)
: lerpColor(colorB, colorC, (playT - 0.5) * 2);
ctx.beginPath();
ctx.arc(px, h / 2, 24, 0, Math.PI * 2);
ctx.fillStyle = playColor;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
</script>
<canvas id="canvas"></canvas>
<script type="module">
import { lerpColor, hexToRgb, clamp } from '@andresclua/creative';
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Low-res buffer for performance
const buf = document.createElement('canvas');
const bufCtx = buf.getContext('2d');
const RES = 80;
buf.width = RES; buf.height = RES;
const corners = ['#6c5ce7', '#fd79a8', '#00cec9', '#fdcb6e'];
let start = performance.now();
function tick(now) {
const elapsed = (now - start) / 1000;
canvas.width = canvas.parentElement.clientWidth;
canvas.height = 450;
const img = bufCtx.createImageData(RES, RES);
for (let y = 0; y < RES; y++) {
for (let x = 0; x < RES; x++) {
let u = x / (RES - 1);
let v = y / (RES - 1);
u += Math.sin(v * 4 + elapsed * 0.5) * 0.03;
v += Math.cos(u * 4 + elapsed * 0.7) * 0.03;
u = clamp(u, 0, 1); v = clamp(v, 0, 1);
const top = lerpColor(corners[0], corners[1], u);
const btm = lerpColor(corners[2], corners[3], u);
const rgb = hexToRgb(lerpColor(top, btm, v));
const i = (y * RES + x) * 4;
img.data[i]=rgb.r; img.data[i+1]=rgb.g;
img.data[i+2]=rgb.b; img.data[i+3]=255;
}
}
bufCtx.putImageData(img, 0, 0);
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(buf, 0, 0, canvas.width, canvas.height);
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
</script>
ImageData + low-res trick.
Euclidean distance between two points
<div id="area" style="position: relative; height: 250px">
<div id="pointA" style="
position: absolute; width: 20px; height: 20px;
border-radius: 50%; background: #6c5ce7;
cursor: grab; transform: translate(-50%, -50%);
left: 100px; top: 100px;
"></div>
<div id="pointB" style="
position: absolute; width: 20px; height: 20px;
border-radius: 50%; background: #fd79a8;
cursor: grab; transform: translate(-50%, -50%);
left: 300px; top: 180px;
"></div>
<span id="label">0 px</span>
</div>
<script type="module">
import { distance } from '@andresclua/creative';
const area = document.getElementById('area');
const a = document.getElementById('pointA');
const b = document.getElementById('pointB');
const label = document.getElementById('label');
let dragging = null;
function makeDraggable(el) {
el.addEventListener('mousedown', () => { dragging = el; });
}
makeDraggable(a);
makeDraggable(b);
area.addEventListener('mousemove', (e) => {
if (!dragging) return;
const rect = area.getBoundingClientRect();
dragging.style.left = (e.clientX - rect.left) + 'px';
dragging.style.top = (e.clientY - rect.top) + 'px';
updateDistance();
});
window.addEventListener('mouseup', () => { dragging = null; });
function updateDistance() {
const ax = parseInt(a.style.left);
const ay = parseInt(a.style.top);
const bx = parseInt(b.style.left);
const by = parseInt(b.style.top);
const d = distance(ax, ay, bx, by);
label.textContent = d.toFixed(1) + ' px';
label.style.left = (ax + bx) / 2 + 'px';
label.style.top = (ay + by) / 2 - 20 + 'px';
}
updateDistance();
</script>
distance(). No canvas needed.
<canvas id="canvas"></canvas>
<script type="module">
import { distance, clamp, normalize } from '@andresclua/creative';
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 800; canvas.height = 350;
let mouse = { x: -9999, y: -9999 };
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouse.x = e.clientX - rect.left;
mouse.y = e.clientY - rect.top;
});
function tick() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const spacing = 30, maxDist = 150;
for (let x = spacing; x < canvas.width; x += spacing) {
for (let y = spacing; y < canvas.height; y += spacing) {
const d = distance(x, y, mouse.x, mouse.y);
const factor = clamp(1 - normalize(d, 0, maxDist), 0, 1);
ctx.beginPath();
ctx.arc(x, y, 2 + factor * 6, 0, Math.PI * 2);
ctx.fillStyle = `rgba(108,92,231, ${0.1 + factor * 0.9})`;
ctx.fill();
}
}
requestAnimationFrame(tick);
}
tick();
</script>
<canvas id="canvas"></canvas>
<script type="module">
import { distance, clamp } from '@andresclua/creative';
// Low-res buffer for performance
const RES = 120;
const buf = document.createElement('canvas');
buf.width = RES; buf.height = RES;
const bufCtx = buf.getContext('2d');
// Seeds orbit slowly
const seeds = Array.from({ length: 8 }, () => ({
ax: Math.random() * 0.5 + 0.1,
ay: Math.random() * 0.3 + 0.1,
px: Math.random() * Math.PI * 2,
py: Math.random() * Math.PI * 2,
hue: Math.random() * 360,
}));
function tick(now) {
const elapsed = now / 1000;
const pts = seeds.map(s => ({
x: (Math.sin(elapsed * s.ax + s.px) + 1) / 2,
y: (Math.sin(elapsed * s.ay + s.py) + 1) / 2,
hue: s.hue,
}));
const img = bufCtx.createImageData(RES, RES);
for (let py = 0; py < RES; py++) {
for (let px = 0; px < RES; px++) {
const u = px / RES, v = py / RES;
let minD = Infinity, closest = 0;
for (let s = 0; s < pts.length; s++) {
const d = distance(u, v, pts[s].x, pts[s].y);
if (d < minD) { minD = d; closest = s; }
}
// Color by nearest seed, darken at borders
const edge = clamp(1 - minD * 6, 0.3, 1);
// ... set pixel to hsl(pts[closest].hue, 65%, 55% * edge)
}
}
bufCtx.putImageData(img, 0, 0);
ctx.drawImage(buf, 0, 0, canvas.width, canvas.height);
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
</script>
isInViewport, getOffset
<div id="box">outside viewport</div>
<script type="module">
import { isInViewport, getOffset } from '@andresclua/creative';
const box = document.getElementById('box');
window.addEventListener('scroll', () => {
const visible = isInViewport(box, 0.3);
box.classList.toggle('visible', visible);
box.textContent = visible ? 'in viewport' : 'outside viewport';
const off = getOffset(box);
console.log('top:', off.top, 'left:', off.left);
}, { passive: true });
</script>
IntersectionObserver for quick checks.
<div class="bar" style="
transform: scaleX(0); transform-origin: left;
transition: transform .6s cubic-bezier(.22,1,.36,1);
"></div>
<div class="bar" style="
transform: scaleX(0); transform-origin: left;
transition: transform .8s cubic-bezier(.22,1,.36,1);
"></div>
<script type="module">
import { isInViewport } from '@andresclua/creative';
const bars = document.querySelectorAll('.bar');
window.addEventListener('scroll', () => {
bars.forEach((bar) => {
if (isInViewport(bar, 0.3)) {
bar.style.transform = 'scaleX(1)';
}
});
}, { passive: true });
</script>
transition + JS trigger. Clean, performant, accessible.