Kinetic Art: How to Create Moving Sculptures and Motion Art With Code
Kinetic art is art that moves. Coined by Naum Gabo and Antoine Pevsner in their 1920 Realistic Manifesto, the term describes sculptures, installations, and compositions where movement is an essential part of the work — not a side effect but the medium itself. Alexander Calder's mobiles, Jean Tinguely's mechanical contraptions, and Theo Jansen's wind-powered Strandbeest are all kinetic art. The art isn't in any single position — it's in the motion, the rhythm, the constantly shifting relationships between parts.
Code is a natural medium for kinetic art because physics simulation is trivial on a computer. Gravity, pendulums, springs, wind, friction, linkages — each is a few lines of math that produce endlessly varied motion. And unlike physical kinetic sculptures, digital ones can break the laws of physics whenever the aesthetic demands it. You can have frictionless pendulums, impossible linkages, and forces that exist nowhere in nature.
This guide builds 8 kinetic art pieces from scratch in JavaScript and Canvas. Each explores a different type of motion: periodic, chaotic, mechanical, organic, wave-based, and interactive. No libraries — just physics and pixels.
The physics of beautiful motion
Kinetic art relies on a few core motion types:
- Periodic motion: pendulums, oscillators, and harmonic systems that repeat. The beauty comes from phase relationships between multiple oscillators — slightly different frequencies create evolving patterns that never quite repeat
- Mechanical motion: linkages, gears, and cams that convert one type of motion into another. A rotating crank becomes linear reciprocation; a four-bar linkage traces graceful curves
- Wave motion: disturbances that propagate through a medium. Each element follows simple rules but the emergent pattern flows across space
- Chaotic motion: deterministic systems that are exquisitely sensitive to initial conditions. A double pendulum looks random but is entirely predictable given exact starting values
- Interactive motion: systems that respond to viewer input — mouse position, clicks, scroll — making the viewer a participant in the kinetic sculpture
Example 1: Pendulum wave
A row of pendulums with slightly different lengths. Each swings at its own frequency. Together they create mesmerizing wave patterns that cycle between alignment and chaos. This is one of the most famous kinetic art demonstrations — a real version hangs in science museums worldwide.
var c = document.createElement('canvas');
c.width = 600; c.height = 400;
document.body.appendChild(c);
var ctx = c.getContext('2d');
var n = 24;
var baseFreq = 1.4;
var freqStep = 0.04;
var t = 0;
function draw() {
ctx.fillStyle = 'rgba(10, 10, 25, 0.15)';
ctx.fillRect(0, 0, c.width, c.height);
var spacing = c.width / (n + 1);
for (var i = 0; i < n; i++) {
var freq = baseFreq + i * freqStep;
var angle = Math.sin(t * freq) * 1.2;
var length = 100 + i * 6;
var px = spacing * (i + 1);
var py = 30;
var bx = px + Math.sin(angle) * length;
var by = py + Math.cos(angle) * length;
ctx.strokeStyle = 'rgba(120, 180, 255, 0.3)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(bx, by);
ctx.stroke();
var hue = 200 + i * 6;
var radius = 8 - i * 0.15;
ctx.beginPath();
ctx.arc(bx, by, Math.max(3, radius), 0, Math.PI * 2);
ctx.fillStyle = 'hsl(' + hue + ', 80%, 65%)';
ctx.fill();
ctx.strokeStyle = 'hsl(' + hue + ', 90%, 80%)';
ctx.lineWidth = 1.5;
ctx.stroke();
}
ctx.fillStyle = '#334';
ctx.fillRect(0, 0, c.width, 32);
ctx.fillStyle = '#556';
ctx.fillRect(0, 30, c.width, 4);
t += 0.025;
requestAnimationFrame(draw);
}
draw();
Each pendulum's frequency is baseFreq + i * freqStep. When all phases align, the pendulums form a straight line. As they drift apart, they create traveling waves, standing waves, and eventually apparent chaos — before slowly converging back to alignment. The cycle period is 2π / freqStep, so the full pattern repeats every ~157 seconds with these values. The trailing effect (semi-transparent background fill) creates motion blur that makes the wave pattern visible.
Example 2: Calder-style mobile
Alexander Calder invented the mobile in the 1930s — balanced structures of arms and weights that rotate freely in air currents. This simulation creates a recursive mobile where each arm can hold either a weight or another sub-mobile. The result is a tree of gently swaying, interconnected parts.
var c = document.createElement('canvas');
c.width = 600; c.height = 500;
document.body.appendChild(c);
var ctx = c.getContext('2d');
function makeMobile(depth, maxDepth) {
if (depth >= maxDepth || Math.random() < 0.25) {
var hue = Math.floor(Math.random() * 360);
return { type: 'weight', radius: 8 + Math.random() * 14, hue: hue, mass: 1 };
}
var left = makeMobile(depth + 1, maxDepth);
var right = makeMobile(depth + 1, maxDepth);
var lm = left.type === 'weight' ? left.mass : left.leftMass + left.rightMass;
var rm = right.type === 'weight' ? right.mass : right.leftMass + right.rightMass;
var total = lm + rm;
var armLen = 50 + Math.random() * 60;
return {
type: 'arm', left: left, right: right,
leftLen: armLen * rm / total, rightLen: armLen * lm / total,
leftMass: lm, rightMass: rm,
angle: Math.random() * 0.5 - 0.25,
velocity: 0, damping: 0.98, spring: 0.0004 + Math.random() * 0.0003
};
}
var mobile = makeMobile(0, 4);
var wind = 0, windTarget = 0, t = 0;
function updateMobile(node, dt) {
if (node.type === 'weight') return;
var torque = wind * 0.12 * (node.leftMass - node.rightMass);
torque -= node.spring * node.angle * 800;
node.velocity += torque * dt;
node.velocity *= node.damping;
node.angle += node.velocity * dt;
node.angle = Math.max(-0.5, Math.min(0.5, node.angle));
updateMobile(node.left, dt);
updateMobile(node.right, dt);
}
function drawMobile(node, x, y) {
if (node.type === 'weight') {
ctx.beginPath();
ctx.arc(x, y, node.radius, 0, Math.PI * 2);
ctx.fillStyle = 'hsl(' + node.hue + ', 70%, 55%)';
ctx.fill();
ctx.strokeStyle = 'hsl(' + node.hue + ', 80%, 70%)';
ctx.lineWidth = 1.5;
ctx.stroke();
return;
}
var cos = Math.cos(node.angle), sin = Math.sin(node.angle);
var lx = x - node.leftLen * cos, ly = y + Math.abs(node.leftLen * sin) + 30;
var rx = x + node.rightLen * cos, ry = y + Math.abs(node.rightLen * sin) + 30;
ctx.strokeStyle = 'rgba(180, 190, 200, 0.8)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(lx, y); ctx.lineTo(rx, y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(lx, y); ctx.lineTo(lx, ly);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(rx, y); ctx.lineTo(rx, ry);
ctx.stroke();
drawMobile(node.left, lx, ly);
drawMobile(node.right, rx, ry);
}
function draw() {
ctx.fillStyle = '#0a0a19';
ctx.fillRect(0, 0, c.width, c.height);
t++;
if (t % 120 === 0) windTarget = (Math.random() - 0.5) * 2;
wind += (windTarget - wind) * 0.01;
updateMobile(mobile, 1);
ctx.strokeStyle = 'rgba(180, 190, 200, 0.5)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(c.width / 2, 0);
ctx.lineTo(c.width / 2, 40);
ctx.stroke();
drawMobile(mobile, c.width / 2, 40);
requestAnimationFrame(draw);
}
draw();
The mobile is generated recursively: each node is either a colored weight or an arm holding two children. Arms are balanced using the lever principle — heavier children get shorter arm segments. A slowly drifting wind variable applies torque to each arm proportional to its mass imbalance. Spring forces restore each arm toward horizontal, creating gentle oscillation. The recursive structure means motion at the top cascades down through every child, creating complex correlated movement from simple per-node physics.
Example 3: Wind sculpture field
A field of spinning elements that respond to a flowing wind field. Each element is a little pinwheel whose rotation speed depends on the local wind strength. The wind itself is a time-varying noise field, so the sculpture field ripples and shifts like a field of grass in a breeze.
var c = document.createElement('canvas');
c.width = 600; c.height = 400;
document.body.appendChild(c);
var ctx = c.getContext('2d');
var cols = 20, rows = 13;
var spins = [];
for (var i = 0; i < cols * rows; i++) spins[i] = Math.random() * Math.PI * 2;
function noise2d(x, y) {
var n = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
return n - Math.floor(n);
}
function smoothNoise(x, y) {
var ix = Math.floor(x), iy = Math.floor(y);
var fx = x - ix, fy = y - iy;
fx = fx * fx * (3 - 2 * fx);
fy = fy * fy * (3 - 2 * fy);
var a = noise2d(ix, iy), b = noise2d(ix + 1, iy);
var cc = noise2d(ix, iy + 1), d = noise2d(ix + 1, iy + 1);
return a + (b - a) * fx + (cc - a) * fy + (a - b - cc + d) * fx * fy;
}
var t = 0;
function draw() {
ctx.fillStyle = '#0d1117';
ctx.fillRect(0, 0, c.width, c.height);
var sx = c.width / cols, sy = c.height / rows;
for (var row = 0; row < rows; row++) {
for (var col = 0; col < cols; col++) {
var idx = row * cols + col;
var x = (col + 0.5) * sx, y = (row + 0.5) * sy;
var wind = smoothNoise(col * 0.15 + t * 0.3, row * 0.15 + t * 0.1);
var speed = (wind - 0.3) * 0.25;
spins[idx] += speed;
var armLen = sx * 0.35;
var arms = 4;
for (var a = 0; a < arms; a++) {
var angle = spins[idx] + (a / arms) * Math.PI * 2;
var ex = x + Math.cos(angle) * armLen;
var ey = y + Math.sin(angle) * armLen;
var brightness = 40 + wind * 60;
var hue = 190 + wind * 40;
ctx.strokeStyle = 'hsl(' + hue + ', 50%, ' + brightness + '%)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(ex, ey);
ctx.stroke();
ctx.beginPath();
ctx.arc(ex, ey, 2, 0, Math.PI * 2);
ctx.fillStyle = 'hsl(' + hue + ', 70%, ' + (brightness + 15) + '%)';
ctx.fill();
}
ctx.beginPath();
ctx.arc(x, y, 2.5, 0, Math.PI * 2);
ctx.fillStyle = '#445';
ctx.fill();
}
}
t += 0.008;
requestAnimationFrame(draw);
}
draw();
Each pinwheel accumulates rotation based on local wind strength from a smooth 2D noise function. The noise scrolls over time, creating the appearance of wind gusting across the field. Color and brightness also respond to wind strength — faster-spinning elements glow brighter and shift toward cyan. The effect resembles Daan Roosegaarde's "Windvogel" or Anthony Howe's large-scale wind sculptures, where hundreds of synchronized elements create emergent waves of motion.
Example 4: Harmonograph — the drawing machine
A harmonograph is a physical device where pendulums move a pen across paper. Two or three pendulums operating on different axes create intricate Lissajous-like patterns. Real harmonographs were popular parlor toys in the 19th century. Every drawing is unique because the pendulums slowly decay and drift.
var c = document.createElement('canvas');
c.width = 500; c.height = 500;
document.body.appendChild(c);
var ctx = c.getContext('2d');
ctx.fillStyle = '#0a0a19';
ctx.fillRect(0, 0, c.width, c.height);
var p1 = { f: 2.01, phase: 0, amp: 180, decay: 0.0003 };
var p2 = { f: 3.0, phase: Math.PI / 4, amp: 180, decay: 0.0004 };
var p3 = { f: 2.99, phase: Math.PI / 7, amp: 30, decay: 0.0002 };
var t = 0, step = 0;
function draw() {
for (var i = 0; i < 80; i++) {
var d1 = p1.amp * Math.exp(-p1.decay * t);
var d2 = p2.amp * Math.exp(-p2.decay * t);
var d3 = p3.amp * Math.exp(-p3.decay * t);
var x = c.width / 2 + d1 * Math.sin(t * p1.f + p1.phase) + d3 * Math.sin(t * p3.f + p3.phase);
var y = c.height / 2 + d2 * Math.sin(t * p2.f + p2.phase);
var alpha = Math.max(0.02, Math.exp(-p1.decay * t * 0.8));
var hue = (t * 8) % 360;
ctx.beginPath();
ctx.arc(x, y, 0.8, 0, Math.PI * 2);
ctx.fillStyle = 'hsla(' + hue + ', 70%, 60%, ' + alpha + ')';
ctx.fill();
t += 0.012;
}
step++;
if (step < 600) {
requestAnimationFrame(draw);
}
}
draw();
Three damped sinusoidal pendulums control X, Y, and a perturbation axis. The near-integer frequency ratios (2.01:3.0:2.99) mean the pattern almost repeats but never quite does — each loop traces slightly differently from the last. Exponential decay reduces amplitude over time, causing the drawing to spiral inward toward the center as a real harmonograph would. The hue shifts with time so you can see the drawing's temporal evolution — early strokes in blue, later strokes in green, final strokes in red as the pendulums settle.
Example 5: Kinetic typography
Text as kinetic sculpture. Each letter is an independent physical object with position, velocity, and response to forces. The letters spell a word but they're alive — bouncing, breathing, responding to your mouse like a school of fish.
var c = document.createElement('canvas');
c.width = 600; c.height = 300;
document.body.appendChild(c);
var ctx = c.getContext('2d');
var word = 'KINETIC';
var letters = [];
var mx = c.width / 2, my = c.height / 2;
for (var i = 0; i < word.length; i++) {
var homeX = 100 + i * 65;
letters.push({
ch: word[i], homeX: homeX, homeY: 150,
x: homeX, y: 150, vx: 0, vy: 0,
angle: 0, va: 0, scale: 1, vs: 0
});
}
c.addEventListener('mousemove', function(e) {
var r = c.getBoundingClientRect();
mx = e.clientX - r.left; my = e.clientY - r.top;
});
function draw() {
ctx.fillStyle = 'rgba(10, 10, 25, 0.12)';
ctx.fillRect(0, 0, c.width, c.height);
for (var i = 0; i < letters.length; i++) {
var L = letters[i];
var dx = mx - L.x, dy = my - L.y;
var dist = Math.sqrt(dx * dx + dy * dy) + 1;
if (dist < 120) {
var force = (120 - dist) / 120 * 8;
L.vx -= (dx / dist) * force;
L.vy -= (dy / dist) * force;
L.va += (dx > 0 ? -1 : 1) * force * 0.01;
L.vs += 0.05;
}
L.vx += (L.homeX - L.x) * 0.03;
L.vy += (L.homeY - L.y) * 0.03;
L.va -= L.angle * 0.05;
L.vs -= (L.scale - 1) * 0.08;
L.vx *= 0.92; L.vy *= 0.92;
L.va *= 0.92; L.vs *= 0.9;
L.x += L.vx; L.y += L.vy;
L.angle += L.va;
L.scale += L.vs;
L.scale = Math.max(0.5, Math.min(2, L.scale));
ctx.save();
ctx.translate(L.x, L.y);
ctx.rotate(L.angle);
ctx.scale(L.scale, L.scale);
var hue = 200 + i * 22;
ctx.fillStyle = 'hsl(' + hue + ', 75%, 65%)';
ctx.font = 'bold 52px Arial, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(L.ch, 0, 0);
ctx.restore();
}
requestAnimationFrame(draw);
}
draw();
Each letter has home coordinates where it wants to be (spring force), plus repulsion from the mouse cursor. When the mouse approaches, letters scatter with rotation and scale changes. Spring forces pull them back when the mouse moves away. Damping on all velocity channels ensures smooth settling. The result feels tactile — as if you're reaching into the screen and disturbing physical letter-blocks. This technique is used in kinetic typography for music videos, title sequences, and interactive web headers.
Example 6: Magnetic field lines
Iron filings near a magnet arrange into beautiful field line patterns. This simulates that effect: small particles align along the field produced by multiple magnetic dipoles. Move your mouse to reposition the primary magnet and watch the field restructure in real time.
var c = document.createElement('canvas');
c.width = 500; c.height = 500;
document.body.appendChild(c);
var ctx = c.getContext('2d');
var magnets = [
{ x: 250, y: 250, strength: 500 },
{ x: 150, y: 350, strength: -300 },
{ x: 380, y: 180, strength: -300 }
];
var filings = [];
for (var i = 0; i < 3000; i++) {
filings.push({ x: Math.random() * c.width, y: Math.random() * c.height });
}
c.addEventListener('mousemove', function(e) {
var r = c.getBoundingClientRect();
magnets[0].x = e.clientX - r.left;
magnets[0].y = e.clientY - r.top;
});
function draw() {
ctx.fillStyle = '#0a0a19';
ctx.fillRect(0, 0, c.width, c.height);
for (var i = 0; i < filings.length; i++) {
var f = filings[i];
var fx = 0, fy = 0;
for (var m = 0; m < magnets.length; m++) {
var mg = magnets[m];
var dx = f.x - mg.x, dy = f.y - mg.y;
var dist = Math.sqrt(dx * dx + dy * dy) + 5;
var force = mg.strength / (dist * dist);
fx += (dx / dist) * force;
fy += (dy / dist) * force;
}
var fLen = Math.sqrt(fx * fx + fy * fy) + 0.001;
var nx = fx / fLen, ny = fy / fLen;
var len = Math.min(6, fLen * 3 + 1);
var brightness = Math.min(80, 20 + fLen * 40);
ctx.strokeStyle = 'hsl(45, 50%, ' + brightness + '%)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(f.x - nx * len, f.y - ny * len);
ctx.lineTo(f.x + nx * len, f.y + ny * len);
ctx.stroke();
}
for (var m = 0; m < magnets.length; m++) {
var mg = magnets[m];
ctx.beginPath();
ctx.arc(mg.x, mg.y, 6, 0, Math.PI * 2);
ctx.fillStyle = mg.strength > 0 ? '#f55' : '#55f';
ctx.fill();
}
requestAnimationFrame(draw);
}
draw();
Each filing is a short line segment aligned with the local magnetic field vector — the superposition of all dipole fields. The field from each magnet falls off with the inverse square of distance, matching real magnetic behavior. Positive (red) and negative (blue) magnets create the characteristic pattern of field lines flowing from north to south. Brightness scales with field strength, so filings near magnets glow brightest. Moving the primary magnet restructures the entire field instantly, creating a visceral sense of invisible forces made visible — which is exactly what kinetic art at its best achieves.
Example 7: Mechanical linkage — Chebyshev's lambda mechanism
Mechanical linkages convert rotary motion into complex curves. Chebyshev's lambda mechanism, invented in 1850, produces an approximately straight-line motion from a rotating crank — a fundamental building block of steam engines and walking robots. This simulation shows the linkage in motion, tracing the beautiful path of its coupler point.
var c = document.createElement('canvas');
c.width = 500; c.height = 500;
document.body.appendChild(c);
var ctx = c.getContext('2d');
var trail = [];
var t = 0;
var ox = 250, oy = 220;
var fixedLen = 100;
var crankLen = 50;
var linkLen = 120;
var extLen = 120;
function draw() {
ctx.fillStyle = 'rgba(10, 10, 25, 0.04)';
ctx.fillRect(0, 0, c.width, c.height);
var crankX = ox - fixedLen / 2;
var crankY = oy;
var cx = crankX + Math.cos(t) * crankLen;
var cy = crankY + Math.sin(t) * crankLen;
var pivotX = ox + fixedLen / 2;
var pivotY = oy;
var dx = cx - pivotX, dy = cy - pivotY;
var d = Math.sqrt(dx * dx + dy * dy);
if (d > linkLen) d = linkLen;
var a = Math.atan2(dy, dx);
var cosA = (d * d + linkLen * linkLen - linkLen * linkLen) / (2 * d * linkLen);
cosA = Math.max(-1, Math.min(1, cosA));
var offset = Math.acos(cosA);
var jx = pivotX + Math.cos(a - offset) * linkLen;
var jy = pivotY + Math.sin(a - offset) * linkLen;
var ratio = (linkLen + extLen) / linkLen;
var ex = pivotX + (jx - pivotX) * ratio;
var ey = pivotY + (jy - pivotY) * ratio;
trail.push({ x: ex, y: ey });
if (trail.length > 800) trail.shift();
if (trail.length > 1) {
for (var i = 1; i < trail.length; i++) {
var alpha = i / trail.length;
var hue = (i * 0.5) % 360;
ctx.strokeStyle = 'hsla(' + hue + ', 70%, 60%, ' + (alpha * 0.8) + ')';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(trail[i - 1].x, trail[i - 1].y);
ctx.lineTo(trail[i].x, trail[i].y);
ctx.stroke();
}
}
ctx.strokeStyle = '#667';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(crankX, crankY);
ctx.lineTo(cx, cy);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(jx, jy);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(pivotX, pivotY);
ctx.lineTo(ex, ey);
ctx.stroke();
var joints = [
{ x: crankX, y: crankY, r: 5, col: '#888' },
{ x: pivotX, y: pivotY, r: 5, col: '#888' },
{ x: cx, y: cy, r: 4, col: '#aaa' },
{ x: jx, y: jy, r: 4, col: '#aaa' },
{ x: ex, y: ey, r: 6, col: '#4af' }
];
for (var j = 0; j < joints.length; j++) {
ctx.beginPath();
ctx.arc(joints[j].x, joints[j].y, joints[j].r, 0, Math.PI * 2);
ctx.fillStyle = joints[j].col;
ctx.fill();
}
t += 0.02;
requestAnimationFrame(draw);
}
draw();
The linkage has two fixed pivots, a rotating crank, and a connecting link that extends beyond the joint. The coupler point (blue dot) at the end of the extension traces a complex closed curve. With these proportions, the top portion of the curve is nearly a straight line — Chebyshev's brilliant insight was that this approximate straight motion could replace a heavy crosshead guide in a steam engine. The trail shows the full coupler curve, colored by time. Different link ratios produce completely different curves — try changing extLen to see Watt's curve, Roberts' curve, or Peaucellier's exact straight-line linkage.
Example 8: Wave machine — coupled oscillators
A row of oscillators connected by springs. Disturb one end and watch the wave propagate, reflect off the far wall, interfere with itself, and gradually dissipate. This is the principle behind Newton's cradle, Slinky waves, and the kinetic wave sculptures seen in airports and hotel lobbies.
var c = document.createElement('canvas');
c.width = 600; c.height = 400;
document.body.appendChild(c);
var ctx = c.getContext('2d');
var n = 60;
var nodes = [];
for (var i = 0; i < n; i++) {
nodes.push({ y: 0, vy: 0, prevY: 0 });
}
var spring = 0.3, damping = 0.997;
var dragging = false, dragIdx = -1;
c.addEventListener('mousedown', function(e) {
var r = c.getBoundingClientRect();
var mx = e.clientX - r.left;
dragIdx = Math.round((mx / c.width) * (n - 1));
dragIdx = Math.max(0, Math.min(n - 1, dragIdx));
dragging = true;
});
c.addEventListener('mousemove', function(e) {
if (!dragging) return;
var r = c.getBoundingClientRect();
var my = e.clientY - r.top;
nodes[dragIdx].y = (my - c.height / 2) * 0.8;
nodes[dragIdx].vy = 0;
});
c.addEventListener('mouseup', function() { dragging = false; });
var t = 0;
function draw() {
ctx.fillStyle = '#0a0a19';
ctx.fillRect(0, 0, c.width, c.height);
if (t < 60 && !dragging) {
nodes[0].y = Math.sin(t * 0.2) * 120 * Math.exp(-t * 0.03);
}
for (var i = 0; i < n; i++) {
if (dragging && i === dragIdx) continue;
var left = i > 0 ? nodes[i - 1].y : 0;
var right = i < n - 1 ? nodes[i + 1].y : 0;
var force = spring * (left + right - 2 * nodes[i].y);
nodes[i].vy += force;
nodes[i].vy *= damping;
}
for (var i = 0; i < n; i++) {
if (dragging && i === dragIdx) continue;
nodes[i].prevY = nodes[i].y;
nodes[i].y += nodes[i].vy;
}
var spacing = c.width / (n + 1);
var cy = c.height / 2;
ctx.strokeStyle = 'rgba(100, 140, 200, 0.15)';
ctx.lineWidth = 1;
for (var i = 0; i < n; i++) {
var x = spacing * (i + 1);
ctx.beginPath();
ctx.moveTo(x, cy);
ctx.lineTo(x, cy + nodes[i].y);
ctx.stroke();
}
ctx.strokeStyle = 'rgba(100, 200, 255, 0.6)';
ctx.lineWidth = 2;
ctx.beginPath();
for (var i = 0; i < n; i++) {
var x = spacing * (i + 1);
if (i === 0) ctx.moveTo(x, cy + nodes[i].y);
else ctx.lineTo(x, cy + nodes[i].y);
}
ctx.stroke();
for (var i = 0; i < n; i++) {
var x = spacing * (i + 1);
var energy = Math.abs(nodes[i].vy) * 8;
var hue = 200 - energy * 3;
var bright = 50 + Math.min(30, energy * 10);
ctx.beginPath();
ctx.arc(x, cy + nodes[i].y, 4, 0, Math.PI * 2);
ctx.fillStyle = 'hsl(' + Math.max(0, hue) + ', 80%, ' + bright + '%)';
ctx.fill();
}
t++;
requestAnimationFrame(draw);
}
draw();
Each node is coupled to its neighbors by springs. The wave equation emerges naturally: acceleration = spring * (left + right - 2 * self). An initial impulse on the left generates a wave that travels right, reflects off the fixed boundary, and returns inverted. Click and drag any node to inject energy. High-energy nodes glow warm (shifting from blue toward red) while resting nodes stay cool blue. The slight damping means waves gradually lose energy, just as real kinetic wave sculptures eventually settle. Try dragging the middle node up and releasing — you'll see two waves propagate in opposite directions, reflect, and interfere constructively when they meet back in the middle.
From digital motion to physical sculpture
Kinetic art bridges the digital and physical worlds. Every simulation here has a physical analog: pendulum waves exist as museum exhibits, Calder mobiles hang in the MoMA, wind sculptures spin on rooftops, and harmonograph drawings were Victorian entertainment. Code lets you explore these systems instantly — tweak a frequency, add a force, break a physical law — and see the result in milliseconds instead of months of workshop time.
Directions to explore:
- Combine kinetic typography with SVG animation for resolution-independent motion art that works at any scale
- Apply gravitational physics to kinetic sculptures — orbital mobiles where elements swing around attractors instead of on rigid arms
- Use particle systems as kinetic elements — each particle becomes a tiny moving component in a larger sculpture
- Build a double pendulum — the simplest chaotic system, where two hinged arms create wildly unpredictable yet deterministic motion
- Combine wave machines with audio input — sound drives the wave, making music visible as kinetic motion
- Create Lissajous tables: a grid where each cell shows the harmonograph pattern for that row/column frequency combination
- Explore Theo Jansen linkages: complex multi-bar mechanisms that produce organic walking motion from simple rotation
The essence of kinetic art is that time is a material. A sculpture that doesn't move is frozen in one moment. A kinetic sculpture exists across all its moments simultaneously — it is the sum of every position, every transition, every phase relationship. On Lumitree, many micro-worlds embody this principle: they are never the same twice, their beauty lives in perpetual motion, and every visitor experiences a unique moment in an infinite choreography.