It appeared that the robots were dancing in perfect synchronous harmony. That is, until something unexplained happened and drove them to break apart.
A Pen by Gerard Ferrandez on CodePen.
<canvas></canvas><!-- | |
o | |
\_/\o | |
( Oo) \|/ | |
(_=-) .===O- ~~Z~A~P~~ -O- | |
/ \_/U' /|\ | |
|| |_/ | |
\\ | | |
{K || | |
| PP | |
| || | |
(__\\ | |
--> |
"use strict"; | |
///////////////// worker thread code /////////////////// | |
const theLastExperience = noWorkers => { | |
"use strict"; | |
// ---- robot structure ---- | |
const struct = { | |
points: [ | |
{ | |
x: 0, | |
y: -4, | |
f(s, d) { | |
this.y -= 0.01 * s * ts; | |
} | |
}, | |
{ | |
x: 0, | |
y: -16, | |
f(s, d) { | |
this.y -= 0.02 * s * d * ts; | |
} | |
}, | |
{ | |
x: 0, | |
y: 12, | |
f(s, d) { | |
this.y += 0.02 * s * d * ts; | |
} | |
}, | |
{ x: -12, y: 0 }, | |
{ x: 12, y: 0 }, | |
{ | |
x: -3, | |
y: 34, | |
f(s, d) { | |
if (d > 0) { | |
this.x += 0.01 * s * ts; | |
this.y -= 0.015 * s * ts; | |
} else { | |
this.y += 0.02 * s * ts; | |
} | |
} | |
}, | |
{ | |
x: 3, | |
y: 34, | |
f(s, d) { | |
if (d > 0) { | |
this.y += 0.02 * s * ts; | |
} else { | |
this.x -= 0.01 * s * ts; | |
this.y -= 0.015 * s * ts; | |
} | |
} | |
}, | |
{ | |
x: -28, | |
y: 0, | |
f(s, d) { | |
this.x += this.vx * 0.025 * ts; | |
this.y -= 0.001 * s * ts; | |
} | |
}, | |
{ | |
x: 28, | |
y: 0, | |
f(s, d) { | |
this.x += this.vx * 0.025 * ts; | |
this.y -= 0.001 * s * ts; | |
} | |
}, | |
{ | |
x: -3, | |
y: 64, | |
f(s, d) { | |
this.y += 0.015 * s * ts; | |
if (d > 0) { | |
this.y -= 0.01 * s * ts; | |
} else { | |
this.y += 0.05 * s * ts; | |
} | |
} | |
}, | |
{ | |
x: 3, | |
y: 64, | |
f(s, d) { | |
this.y += 0.015 * s * ts; | |
if (d > 0) { | |
this.y += 0.05 * s * ts; | |
} else { | |
this.y -= 0.01 * s * ts; | |
} | |
} | |
} | |
], | |
links: [ | |
{ p0: 3, p1: 7, size: 12, lum: 0.5 }, | |
{ p0: 1, p1: 3, size: 24, lum: 0.5 }, | |
{ p0: 1, p1: 0, size: 60, lum: 0.5, disk: 1 }, | |
{ p0: 5, p1: 9, size: 16, lum: 0.5 }, | |
{ p0: 2, p1: 5, size: 32, lum: 0.5 }, | |
{ p0: 1, p1: 2, size: 50, lum: 1 }, | |
{ p0: 6, p1: 10, size: 16, lum: 1.5 }, | |
{ p0: 2, p1: 6, size: 32, lum: 1.5 }, | |
{ p0: 4, p1: 8, size: 12, lum: 1.5 }, | |
{ p0: 1, p1: 4, size: 24, lum: 1.5 } | |
] | |
}; | |
class Robot { | |
constructor(color, light, size, x, y, struct) { | |
this.x = x; | |
this.points = []; | |
this.links = []; | |
this.frame = 0; | |
this.dir = 1; | |
this.size = size; | |
this.color = Math.round(color); | |
this.light = light; | |
// ---- create points ---- | |
for (const p of struct.points) { | |
this.points.push(new Robot.Point(size * p.x + x, size * p.y + y, p.f)); | |
} | |
// ---- create links ---- | |
for (const link of struct.links) { | |
const p0 = this.points[link.p0]; | |
const p1 = this.points[link.p1]; | |
const dx = p0.x - p1.x; | |
const dy = p0.y - p1.y; | |
this.links.push( | |
new Robot.Link( | |
this, | |
p0, | |
p1, | |
Math.sqrt(dx * dx + dy * dy), | |
link.size * size / 3, | |
link.lum, | |
link.force, | |
link.disk | |
) | |
); | |
} | |
} | |
update() { | |
if (++this.frame % Math.round(20 / ts) === 0) this.dir = -this.dir; | |
if (this === pointer.dancerDrag && this.size < 16 && this.frame > 600) { | |
pointer.dancerDrag = null; | |
dancers.push( | |
new Robot( | |
this.color + 90, | |
this.light * 1.25, | |
this.size * 2, | |
pointer.x, | |
pointer.y - 100 * this.size * 2, | |
struct | |
) | |
); | |
dancers.sort(function(d0, d1) { | |
return d0.size - d1.size; | |
}); | |
} | |
// ---- update links ---- | |
for (const link of this.links) link.update(); | |
// ---- update points ---- | |
for (const point of this.points) point.update(this); | |
// ---- ground ---- | |
for (const link of this.links) { | |
const p1 = link.p1; | |
if (p1.y > canvas.height * ground - link.size * 0.5) { | |
p1.y = canvas.height * ground - link.size * 0.5; | |
p1.x -= p1.vx; | |
p1.vx = 0; | |
p1.vy = 0; | |
} | |
} | |
// ---- center position ---- | |
this.points[3].x += (this.x - this.points[3].x) * 0.001; | |
} | |
draw() { | |
for (const link of this.links) { | |
if (link.size) { | |
const dx = link.p1.x - link.p0.x; | |
const dy = link.p1.y - link.p0.y; | |
const a = Math.atan2(dy, dx); | |
// ---- shadow ---- | |
ctx.save(); | |
ctx.translate(link.p0.x + link.size * 0.25, link.p0.y + link.size * 0.25); | |
ctx.rotate(a); | |
ctx.drawImage( | |
link.shadow, | |
-link.size * 0.5, | |
-link.size * 0.5 | |
); | |
ctx.restore(); | |
// ---- stroke ---- | |
ctx.save(); | |
ctx.translate(link.p0.x, link.p0.y); | |
ctx.rotate(a); | |
ctx.drawImage( | |
link.image, | |
-link.size * 0.5, | |
-link.size * 0.5 | |
); | |
ctx.restore(); | |
} | |
} | |
} | |
} | |
Robot.Link = class Link { | |
constructor(parent, p0, p1, dist, size, light, force, disk) { | |
this.p0 = p0; | |
this.p1 = p1; | |
this.distance = dist; | |
this.size = size; | |
this.light = light || 1.0; | |
this.force = force || 0.5; | |
this.image = this.stroke( | |
"hsl(" + parent.color + " ,30%, " + parent.light * this.light + "%)", | |
true, disk, dist, size | |
); | |
this.shadow = this.stroke("rgba(0,0,0,0.5)", false, disk, dist, size); | |
} | |
update() { | |
const p0 = this.p0; | |
const p1 = this.p1; | |
const dx = p1.x - p0.x; | |
const dy = p1.y - p0.y; | |
const dist = Math.sqrt(dx * dx + dy * dy); | |
if (dist > 0.0) { | |
const tw = p0.w + p1.w; | |
const r1 = p1.w / tw; | |
const r0 = p0.w / tw; | |
const dz = (this.distance - dist) * this.force; | |
const sx = dx / dist * dz; | |
const sy = dy / dist * dz; | |
p1.x += sx * r0; | |
p1.y += sy * r0; | |
p0.x -= sx * r1; | |
p0.y -= sy * r1; | |
} | |
} | |
stroke(color, axis, disk, dist, size) { | |
let image; | |
if (noWorkers) { | |
image = document.createElement("canvas"); | |
image.width = dist + size; | |
image.height = size; | |
} else { | |
image = new OffscreenCanvas(dist + size, size); | |
} | |
const ict = image.getContext("2d"); | |
ict.beginPath(); | |
ict.lineCap = "round"; | |
ict.lineWidth = size; | |
ict.strokeStyle = color; | |
if (disk) { | |
ict.arc(size * 0.5 + dist, size * 0.5, size * 0.5, 0, 2 * Math.PI); | |
ict.fillStyle = color; | |
ict.fill(); | |
} else { | |
ict.moveTo(size * 0.5, size * 0.5); | |
ict.lineTo(size * 0.5 + dist, size * 0.5); | |
ict.stroke(); | |
} | |
if (axis) { | |
const s = size / 10; | |
ict.fillStyle = "#000"; | |
ict.fillRect(size * 0.5 - s, size * 0.5 - s, s * 2, s * 2); | |
ict.fillRect(size * 0.5 - s + dist, size * 0.5 - s, s * 2, s * 2); | |
} | |
return image; | |
} | |
}; | |
Robot.Point = class Point { | |
constructor(x, y, fn, w) { | |
this.x = x; | |
this.y = y; | |
this.w = w || 0.5; | |
this.fn = fn || null; | |
this.px = x; | |
this.py = y; | |
this.vx = 0.0; | |
this.vy = 0.0; | |
} | |
update(robot) { | |
// ---- dragging ---- | |
if (robot === pointer.dancerDrag && this === pointer.pointDrag) { | |
this.x += (pointer.x - this.x) * 0.1; | |
this.y += (pointer.y - this.y) * 0.1; | |
} | |
// ---- dance ---- | |
if (robot !== pointer.dancerDrag) { | |
this.fn && this.fn(16 * Math.sqrt(robot.size), robot.dir); | |
} | |
// ---- verlet integration ---- | |
this.vx = this.x - this.px; | |
this.vy = this.y - this.py; | |
this.px = this.x; | |
this.py = this.y; | |
this.vx *= 0.995; | |
this.vy *= 0.995; | |
this.x += this.vx; | |
this.y += this.vy + 0.01 * ts; | |
} | |
}; | |
// ---- init ---- | |
const dancers = []; | |
let ground = 1.0; | |
let canvas = { width: 0, height: 0, resize: true }; | |
let ctx = null; | |
let pointer = { x: 0, y: 0, dancerDrag: null, pointDrag: null }; | |
let ts = 1; | |
let lastTime = 0; | |
// ---- messages from the main thread ---- | |
const message = e => { | |
switch (e.data.msg) { | |
case "start": | |
canvas.elem = e.data.elem; | |
canvas.width = canvas.elem.width; | |
canvas.height = canvas.elem.height; | |
ctx = canvas.elem.getContext("2d"); | |
initRobots(); | |
requestAnimationFrame(run); | |
break; | |
case "resize": | |
canvas.width = e.data.width; | |
canvas.height = e.data.height; | |
canvas.resize = true; | |
break; | |
case "pointerMove": | |
pointer.x = e.data.x; | |
pointer.y = e.data.y; | |
break; | |
case "pointerDown": | |
pointer.x = e.data.x; | |
pointer.y = e.data.y; | |
for (const dancer of dancers) { | |
for (const point of dancer.points) { | |
const dx = pointer.x - point.x; | |
const dy = pointer.y - point.y; | |
const d = Math.sqrt(dx * dx + dy * dy); | |
if (d < 60) { | |
pointer.dancerDrag = dancer; | |
pointer.pointDrag = point; | |
dancer.frame = 0; | |
} | |
} | |
} | |
break; | |
case "pointerUp": | |
pointer.dancerDrag = null; | |
break; | |
} | |
}; | |
// ---- resize screen ---- | |
const resize = () => { | |
canvas.elem.width = canvas.width; | |
canvas.elem.height = canvas.height; | |
canvas.resize = false; | |
ground = canvas.height > 500 ? 0.85 : 1.0; | |
for (let i = 0; i < dancers.length; i++) { | |
dancers[i].x = (i + 2) * canvas.width / 9; | |
} | |
} | |
// ---- main loop ---- | |
const run = (time) => { | |
requestAnimationFrame(run); | |
if (canvas.resize === true) resize(); | |
// ---- adjust speed to screen freq ---- | |
if (lastTime !== 0) { | |
const t = (time - lastTime) / 16; | |
ts += (t - ts) * 0.1; | |
if (ts > 1) ts = 1; | |
} | |
lastTime = time; | |
// ---- clear screen ---- | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
ctx.fillStyle = "#222"; | |
ctx.fillRect(0, 0, canvas.width, canvas.height * 0.15); | |
ctx.fillRect(0, canvas.height * 0.85, canvas.width, canvas.height * 0.15); | |
// ---- animate robots ---- | |
for (const dancer of dancers) { | |
dancer.update(); | |
dancer.draw(); | |
} | |
}; | |
const initRobots = () => { | |
// ---- instanciate robots ---- | |
ground = canvas.height > 500 ? 0.85 : 1.0; | |
for (let i = 0; i < 6; i++) { | |
dancers.push( | |
new Robot( | |
i * 360 / 7, | |
80, | |
Math.sqrt(Math.min(canvas.width, canvas.height)) / 6, | |
(i + 2) * canvas.width / 9, | |
canvas.height * 0.5 - 100, | |
struct | |
) | |
); | |
} | |
}; | |
// ---- main thread vs. worker | |
if (noWorkers) { | |
// ---- emulate postMessage interface ---- | |
return { | |
postMessage(data) { | |
message({ data: data }); | |
} | |
}; | |
} else { | |
// ---- worker messaging ---- | |
onmessage = message; | |
} | |
}; | |
///////////////// main thread code /////////////////// | |
let worker = null; | |
const createWorker = fn => { | |
const URL = window.URL || window.webkitURL; | |
return new Worker(URL.createObjectURL(new Blob(["(" + fn + ")()"]))); | |
}; | |
// ---- init canvas ---- | |
const canvas = document.querySelector("canvas"); | |
canvas.width = canvas.offsetWidth; | |
canvas.height = canvas.offsetHeight; | |
// ---- instanciate worker ---- | |
if (window.Worker && window.OffscreenCanvas) { | |
// instanciating background worker from a function | |
worker = createWorker(theLastExperience); | |
// cloning OffscreenCanvas | |
const offscreen = canvas.transferControlToOffscreen(); | |
// sending data to worker | |
worker.postMessage({ msg: "start", elem: offscreen }, [offscreen]); | |
} else { | |
// falling back execution to the main thread | |
worker = theLastExperience(true); | |
worker.postMessage({ msg: "start", elem: canvas }); | |
} | |
// ---- resize event ---- | |
window.addEventListener( | |
"resize", | |
() => { | |
worker.postMessage({ | |
msg: "resize", | |
width: canvas.offsetWidth, | |
height: canvas.offsetHeight | |
}); | |
}, | |
false | |
); | |
// ---- pointer events ---- | |
const pointer = { | |
x: 0, | |
y: 0, | |
down(e) { | |
this.move(e); | |
worker.postMessage({ | |
msg: "pointerDown", | |
x: this.x, | |
y: this.y | |
}); | |
}, | |
up(e) { | |
worker.postMessage({ | |
msg: "pointerUp" | |
}); | |
}, | |
move(e) { | |
if (e.targetTouches) { | |
e.preventDefault(); | |
this.x = e.targetTouches[0].clientX; | |
this.y = e.targetTouches[0].clientY; | |
} else { | |
this.x = e.clientX; | |
this.y = e.clientY; | |
} | |
worker.postMessage({ | |
msg: "pointerMove", | |
x: this.x, | |
y: this.y | |
}); | |
} | |
}; | |
window.addEventListener("mousemove", e => pointer.move(e), false); | |
canvas.addEventListener("touchmove", e => pointer.move(e), false); | |
window.addEventListener("mousedown", e => pointer.down(e), false); | |
window.addEventListener("touchstart", e => pointer.down(e), false); | |
window.addEventListener("mouseup", e => pointer.up(e), false); | |
window.addEventListener("touchend", e => pointer.up(e), false); |
body, html { | |
position: absolute; | |
margin: 0; | |
padding: 0; | |
width: 100%; | |
height: 100%; | |
overflow: hidden; | |
} | |
canvas { | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
background:#000; | |
cursor: pointer; | |
} |
It appeared that the robots were dancing in perfect synchronous harmony. That is, until something unexplained happened and drove them to break apart.
A Pen by Gerard Ferrandez on CodePen.