@andresclua/creative

Utility functions for creative coding — each section shows an HTML example, a Canvas example, and a combined example.

Math & Lerp

lerp, map, smoothstep, clamp, randomInRange, distance, normalize, degToRad, radToDeg, wrap

full code
<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>
Creative ideas
Custom cursors that lag behind the real cursor. Tooltip positioning that feels organic. Parallax layers with different lerp factors per depth. Smooth scroll indicators.
HTML Pure DOM — one div + style.left/top. No canvas needed. Works with any element.
full code
<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>
Creative ideas
Interactive hero backgrounds. Night sky with flashlight effect. Data viz hover highlights.
canvas 300 particles at 60fps — canvas only. Each particle is one ctx.arc() call.
full code
<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>
Creative ideas
Generative art. Music visualizers. Loading screens. Game backgrounds. Uses wrap, degToRad, smoothstep, lerp together.
canvas 800 particles with per-frame trig — pure canvas territory.

Easing

10 standard curves + spring + damp

full code
<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>
Creative ideas
Page transitions. Modal entrances. Notification slides. Menu animations. Any time you move a DOM element with JS, apply an easing curve for natural motion.
HTML Just element.style.left + easing. No canvas, no framework.
full code
<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>
Creative ideas
Chart bars that animate on scroll. Skill bars. Dashboard loading states. clamp + stagger offset creates the cascade.
canvas 12 bars is fine in DOM too, but canvas gives you pixel-perfect roundRect + HSL in one draw call.
full code
<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>
Creative ideas
Drag & drop with overshoot. Pull-to-refresh. Notification toasts. Low damping + high freq = playful. High damping + low freq = elegant.
canvas or HTML Works on DOM too: el.style.transform = \`translate(\${x}px, \${y}px)\`.

Mouse Tracking

getMousePos, calcWinsize, createMouseTracker, mouseDistFromElement

x
0
y
0
speed
0
angle
0
velocity x
0
velocity y
0
full code
<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>
Creative ideas
Debug overlays. Gesture detection via speed thresholds. Idle detection — trigger screensaver when speed === 0.
HTML Just textContent updates — no canvas needed.
A
B
C
full code
<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>
Creative ideas
Magnetic navigation links. Magnetic CTAs. Icon grids. Real buttons stay accessible and clickable.
HTML Real DOM elements with transform: translate(). Canvas can't have clickable buttons.
full code
<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>
Creative ideas
Interactive hero backgrounds. 404 pages. Product reveals. Mouse velocity becomes a physical wind force.
canvas 150 particles + connection lines + physics = canvas only.

Toolkit Helpers

Utility functions you drop inside any render loop — Three.js, Pixi, GSAP, Canvas, whatever.

full code
<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>
Creative ideas
Procedural layouts. Random avatars. Generative UI patterns. randomInRange + map = controlled randomness for any DOM element.
HTML Pure DOM — create elements and set inline styles. Works in React, Vue, Svelte, anywhere.
full code
<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>
Creative ideas
Replace any lerp inside a loop with damp. Same feel, consistent on 60fps and 144fps. Camera follow in Three.js. Smooth scroll.
any framework damp is pure math — drop it into Three.js, Pixi, GSAP, or raw rAF.
full code
<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>
Creative ideas
Confetti on form submit. Fireworks. Dust particles in Three.js. Click to spawn bursts. damp = air resistance, randomInRange = spawn variation.
any framework Same pattern in Three.js: new THREE.Vector3(randomInRange(-2,2), ...).

Color

hexToRgb, rgbToHex, lerpColor

#3e93e8
full code
<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>
Creative ideas
Theme transitions. Scroll-based color shifts. Hover state interpolation.
HTML Apply result to style.backgroundColor, CSS variables, or SVG fills.
full code
<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>
Creative ideas
Heatmaps. Progress indicators. Audio visualizer color bars.
canvas Pixel-by-pixel gradients = canvas only.
full code
<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>
Creative ideas
Mesh gradients like Apple/Stripe. Ambient wallpapers. Album art generators. Render at 80x80, scale up smoothly.
canvas Pixel-level color = ImageData + low-res trick.

Distance

Euclidean distance between two points

0 px
full code
<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>
Creative ideas
Measurement tools. Snapping — snap when distance < threshold. Collision detection.
HTML Draggable divs + distance(). No canvas needed.
full code
<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>
Creative ideas
Dot matrix backgrounds. Light source simulation. Interactive data grids.
canvas 300+ dots at 60fps = canvas.
full code (simplified)
<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>
Creative ideas
Generative art. Territory maps. Stained glass effects. Mouse = extra seed.
canvas Pixel-level computation at low-res, scaled up.

DOM Utilities

isInViewport, getOffset

scroll down to see the box
outside viewport
offsetTop
0
offsetLeft
0
full code
<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>
Creative ideas
Scroll-triggered animations. Lazy loading. Analytics — track which sections users see. Simpler than IntersectionObserver for quick checks.
HTML Pure DOM utilities.
full code
<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>
Creative ideas
Skill bars. Timeline entries. Stats counters. CSS does the animation, JS just flips the switch at the right moment.
HTML 100% DOM. CSS transition + JS trigger. Clean, performant, accessible.