@andresclua/creative
v0.2.1
Published
Utility functions for creative coding: math, easing, mouse tracking, animation loops, color interpolation & DOM helpers.
Maintainers
Readme
@andresclua/creative
30+ utility functions for creative coding. Math, easing, mouse tracking, animation, color interpolation & DOM helpers — zero dependencies, ~1.8 kB gzipped.
Live Demo & Interactive Examples
Install
npm install @andresclua/creativeyarn add @andresclua/creativeCDN (UMD)
<script src="https://unpkg.com/@andresclua/creative"></script>
<script>
const { lerp, damp } = Creative;
</script>Why
- Zero dependencies — pure math, nothing to install.
- Tree-shakeable — import only what you use.
- Framework agnostic — works with Three.js, Pixi, GSAP, React, Vue, vanilla JS.
- Dual build — ES module + UMD, use it anywhere.
- Frame-rate independent —
dampreplaceslerpinside any loop and behaves consistently on 60fps and 144fps.
Quick Start
HTML — smooth cursor follower
<div id="follower"></div>
<script type="module">
import { lerp } from '@andresclua/creative';
const el = document.getElementById('follower');
let pos = { x: 0, y: 0 };
let target = { x: 0, y: 0 };
window.addEventListener('mousemove', (e) => {
target.x = e.clientX;
target.y = e.clientY;
});
function tick() {
pos.x = lerp(pos.x, target.x, 0.08);
pos.y = lerp(pos.y, target.y, 0.08);
el.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
requestAnimationFrame(tick);
}
tick();
</script>Canvas — particle field
import { distance, map, clamp, randomInRange } from '@andresclua/creative';
const particles = Array.from({ length: 300 }, () => ({
x: randomInRange(0, canvas.width),
y: randomInRange(0, canvas.height),
r: randomInRange(2, 5),
}));
function draw() {
for (const p of particles) {
const d = distance(p.x, p.y, mouse.x, mouse.y);
const factor = clamp(1 - d / 200, 0, 1);
const radius = map(factor, 0, 1, p.r, p.r * 4);
ctx.beginPath();
ctx.arc(p.x, p.y, radius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(108, 92, 231, ${map(factor, 0, 1, 0.15, 1)})`;
ctx.fill();
}
}Three.js / Pixi — works inside any loop
import { damp, randomInRange } from '@andresclua/creative';
// Three.js
function animate() {
const dt = clock.getDelta();
camera.position.x = damp(camera.position.x, target.x, 5, dt);
mesh.rotation.y += dt * 0.5;
}
// Pixi
app.ticker.add((delta) => {
const dt = delta.deltaMS / 1000;
sprite.x = damp(sprite.x, target.x, 5, dt);
});API
Math
import { lerp, map, clamp, smoothstep, randomInRange, distance, normalize, degToRad, radToDeg, wrap } from '@andresclua/creative';| Function | Signature | Description |
|:---------|:----------|:------------|
| lerp | (a, b, t) | Linear interpolation between a and b |
| map | (x, a, b, c, d) | Re-map x from range [a,b] to [c,d] |
| clamp | (value, min, max) | Constrain a value between min and max |
| smoothstep | (edge0, edge1, x) | Hermite smooth interpolation |
| randomInRange | (min, max) | Random integer in [min, max] |
| distance | (x1, y1, x2, y2) | Euclidean distance between two 2D points |
| normalize | (val, min, max) | Normalize a value to the 0–1 range |
| degToRad | (deg) | Degrees to radians |
| radToDeg | (rad) | Radians to degrees |
| wrap | (val, min, max) | Wrap a value within [min, max) range |
lerp(0, 100, 0.5) // 50
map(5, 0, 10, 0, 100) // 50
clamp(150, 0, 100) // 100
distance(0, 0, 3, 4) // 5
normalize(50, 0, 200) // 0.25
wrap(370, 0, 360) // 10Easing
import { easeOutCubic, spring, damp } from '@andresclua/creative';All standard easing functions take t (0–1) and return a number:
| Function | Curve |
|:---------|:------|
| easeInQuad | Quadratic ease-in |
| easeOutQuad | Quadratic ease-out |
| easeInOutQuad | Quadratic ease-in-out |
| easeInCubic | Cubic ease-in |
| easeOutCubic | Cubic ease-out |
| easeInOutCubic | Cubic ease-in-out |
| easeInQuart | Quartic ease-in |
| easeOutQuart | Quartic ease-out |
| easeInOutQuart | Quartic ease-in-out |
| easeInExpo | Exponential ease-in |
| easeOutExpo | Exponential ease-out |
Special easing:
| Function | Signature | Description |
|:---------|:----------|:------------|
| spring | (t, damping?, freq?) | Spring interpolation with overshoot. Default: damping=0.5, freq=15 |
| damp | (a, b, lambda, dt) | Frame-rate independent smooth interpolation. Drop-in replacement for lerp inside animation loops. |
// Animate a DOM element with easing
const start = performance.now();
function tick(now) {
const t = Math.min((now - start) / 1000, 1);
el.style.left = easeOutCubic(t) * 500 + 'px';
if (t < 1) requestAnimationFrame(tick);
}
// damp: use instead of lerp(a, b, 0.1) inside loops
// lerp(pos, target, 0.1) ← speed varies with frame rate
// damp(pos, target, 5, dt) ← same speed on 60fps and 144fpsMouse
import { getMousePos, calcWinsize, createMouseTracker, mouseDistFromElement } from '@andresclua/creative';| Function | Signature | Returns |
|:---------|:----------|:--------|
| getMousePos | (event) | { x, y } from a MouseEvent |
| calcWinsize | () | { width, height } of the window |
| createMouseTracker | () | Live tracker object (see below) |
| mouseDistFromElement | (event, el) | Distance from cursor to element center |
createMouseTracker() returns an object that updates automatically on mousemove:
const tracker = createMouseTracker();
// Read in any animation loop:
tracker.pos // { x, y }
tracker.velocity // { x, y } in px/s
tracker.speed // scalar px/s
tracker.angle // radians
// Clean up when done:
tracker.destroy();Magnetic buttons example:
area.addEventListener('mousemove', (e) => {
const d = mouseDistFromElement(e, btn);
if (d < 150) {
const rect = btn.getBoundingClientRect();
const pull = map(d, 0, 150, 0.4, 0);
btn.style.transform = `translate(
${(e.clientX - rect.left - rect.width/2) * pull}px,
${(e.clientY - rect.top - rect.height/2) * pull}px
)`;
}
});AnimationLoop
import { AnimationLoop } from '@andresclua/creative';A managed requestAnimationFrame loop with delta time, start/stop, and elapsed tracking.
const loop = new AnimationLoop((dt, elapsed) => {
// dt = seconds since last frame (~0.016 at 60fps)
// elapsed = total seconds since start
angle += dt * 2; // 2 rad/s, frame-rate independent
});
loop.start(); // begin
loop.stop(); // pause (cancels rAF)
loop.running; // boolean
loop.elapsed; // total secondsDOM
import { getOffset, isInViewport } from '@andresclua/creative';| Function | Signature | Description |
|:---------|:----------|:------------|
| getOffset | (el) | { top, left } — element position relative to the document |
| isInViewport | (el, threshold?) | boolean — is the element visible? threshold (0–1) = fraction that must be visible |
window.addEventListener('scroll', () => {
if (isInViewport(el, 0.3)) {
el.classList.add('visible');
}
}, { passive: true });
const { top, left } = getOffset(el);Color
import { hexToRgb, rgbToHex, lerpColor } from '@andresclua/creative';| Function | Signature | Description |
|:---------|:----------|:------------|
| hexToRgb | (hex) | { r, g, b } from a hex string |
| rgbToHex | (r, g, b) | Hex string from RGB values |
| lerpColor | (colorA, colorB, t) | Interpolate between two hex colors |
hexToRgb('#6c5ce7') // { r: 108, g: 92, b: 231 }
rgbToHex(108, 92, 231) // '#6c5ce7'
lerpColor('#ff0000', '#0000ff', 0.5) // '#800080'
// Scroll-based background color
window.addEventListener('scroll', () => {
const t = clamp(window.scrollY / 1000, 0, 1);
document.body.style.background = lerpColor('#0a0a0f', '#1a1a2e', t);
});Imports
ES Module (recommended)
// Import everything
import * as Creative from '@andresclua/creative';
// Import only what you need (tree-shakeable)
import { lerp, damp, easeOutCubic } from '@andresclua/creative';CommonJS
const { lerp, damp } = require('@andresclua/creative');UMD / Script tag
<script src="https://unpkg.com/@andresclua/creative"></script>
<script>
const { lerp, damp, lerpColor } = Creative;
</script>Examples
See all functions in action with interactive demos:
Browser Support
Works in all modern browsers (ES2020+). The UMD build supports older bundlers.
License
ISC -- Andres Clua
