Sources
Main html file: | dhtml/lavalamp.html |
ge1doot mini library: | /library/ge1doot.js |
CSS
html {
overflow: hidden;
-ms-touch-action: none;
-ms-content-zooming: none;
}
body {
position: absolute;
margin: 0px;
padding: 0px;
background: #000;
width: 100%;
height: 100%;
}
#screen {
position: absolute;
background: #000;
left: 50%;
width: 100%;
height: 100%;
cursor:pointer;
}
HTML
<canvas id="screen">CANVAS metaballs simulation</canvas>
JS
/* =======================================================
* ---- HTML5 CANVAS Lavalamp ----
* script: Gerard Ferrandez - 21 January 2013
* Released under the MIT license
* http://www.dhteumeuleu.com/LICENSE.html
* ======================================================= */
"use strict";
(function () {
var scr, ctx, pointer, lava, backFill;
// ==== Point constructor ====
var Point = function (x, y) {
this.x = x;
this.y = y;
this.magnitude = x * x + y * y;
this.computed = 0;
this.force = 0;
}
Point.prototype.add = function (p) {
return new Point (this.x + p.x, this.y + p.y);
}
// ==== Ball constructor ====
var Ball = function (parent) {
this.vel = new Point(
(Math.random() > 0.5 ? 1 : -1) * (0.2 + Math.random() * 0.25),
(Math.random() > 0.5 ? 1 : -1) * (0.2 + Math.random() * 1)
);
this.pos = new Point(
parent.width * 0.5,
parent.height * 0.5
);
this.size = (parent.height / 20) + Math.random() * (parent.height / 15);
}
// ==== move balls ====
Ball.prototype.move = function () {
// ---- interact with pointer ----
if (pointer.isDown) {
var dx = pointer.X - this.pos.x;
var dy = pointer.Y - this.pos.y;
var a = Math.atan2(dy, dx);
var v = -Math.min(
10,
200 / Math.sqrt(dx * dx + dy * dy)
);
this.pos = this.pos.add(
new Point(
Math.cos(a) * v,
Math.sin(a) * v
)
);
}
// ---- bounce borders ----
if (this.pos.x >= this.width - this.size) {
if (this.vel.x > 0) this.vel.x = -this.vel.x;
this.pos.x = this.width - this.size;
} else if (this.pos.x <= this.size) {
if (this.vel.x < 0) this.vel.x = -this.vel.x;
this.pos.x = this.size;
}
if (this.pos.y >= this.height - this.size) {
if (this.vel.y > 0) this.vel.y = -this.vel.y;
this.pos.y = this.height - this.size;
} else if (this.pos.y <= this.size) {
if (this.vel.y < 0) this.vel.y = -this.vel.y;
this.pos.y = this.size;
}
// ---- velocity ----
this.pos = this.pos.add(this.vel);
}
// ==== lavalamp constructor ====
var LavaLamp = function (width, height, numBalls, c0, c1) {
this.step = 10;
this.width = width;
this.height = height;
this.sx = Math.floor(this.width / this.step);
this.sy = Math.floor(this.height / this.step);
this.paint = false;
this.metaFill = createRadialGradient(width, height, width, c0, c1);
this.plx = [0,0,1,0,1,1,1,1,1,1,0,1,0,0,0,0];
this.ply = [0,0,0,0,0,0,1,0,0,1,1,1,0,1,0,1];
this.mscases = [0,3,0,3,1,3,0,3,2,2,0,2,1,1,0];
this.ix = [1,0,-1,0,0,1,0,-1,-1,0,1,0,0,1,1,0,0,0,1,1];
this.grid = [];
this.balls = [];
this.iter = 0;
this.sign = 1;
// ---- init grid ----
for (var i = 0; i < (this.sx + 2) * (this.sy + 2); i++) {
this.grid[i] = new Point(
(i % (this.sx + 2)) * this.step,
(Math.floor(i / (this.sx + 2))) * this.step
)
}
// ---- create metaballs ----
for( var i = 0; i < numBalls; i++) {
this.balls[i] = new Ball(this);
}
Ball.prototype.width = this.width;
Ball.prototype.height = this.height;
}
// ==== compute cell force ====
LavaLamp.prototype.computeForce = function (x, y, idx) {
var force;
var id = idx || x + y * (this.sx + 2);
if (x === 0 || y === 0 || x === this.sx || y === this.sy) {
var force = 0.6 * this.sign;
} else {
var cell = this.grid[id];
var force = 0;
var i = 0, ball;
while (ball = this.balls[i++]) {
force += ball.size * ball.size / (
- 2 * cell.x * ball.pos.x
- 2 * cell.y * ball.pos.y
+ ball.pos.magnitude + cell.magnitude
);
}
force *= this.sign
}
this.grid[id].force = force;
return force;
}
// ---- compute cell ----
LavaLamp.prototype.marchingSquares = function (next) {
var x = next[0];
var y = next[1];
var pdir = next[2];
var id = x + y * (this.sx + 2);
if (this.grid[id].computed === this.iter) return false;
var dir, mscase = 0;
// ---- neighbors force ----
for (var i = 0; i < 4; i++) {
var idn = (x + this.ix[i+12]) + (y + this.ix[i+16]) * (this.sx + 2);
var force = this.grid[idn].force;
if ((force > 0 && this.sign < 0) || (force < 0 && this.sign > 0) || !force) {
// ---- compute force if not in buffer ----
force = this.computeForce(
x + this.ix[i+12],
y + this.ix[i+16],
idn
);
}
if (Math.abs(force) > 1) mscase += Math.pow(2, i);
}
if (mscase === 15) {
// --- inside ---
return [x, y - 1, false];
} else {
// ---- ambiguous cases ----
if (mscase === 5) dir = (pdir === 2) ? 3 : 1;
else if (mscase === 10) dir = (pdir === 3) ? 0 : 2;
else {
// ---- lookup ----
dir = this.mscases[mscase];
this.grid[id].computed = this.iter;
}
// ---- draw line ----
var ix = this.step / (
Math.abs(Math.abs(this.grid[(x + this.plx[4*dir+2]) + (y + this.ply[4*dir+2]) * (this.sx + 2)].force) - 1) /
Math.abs(Math.abs(this.grid[(x + this.plx[4*dir+3]) + (y + this.ply[4*dir+3]) * (this.sx + 2)].force) - 1) + 1
);
ctx.lineTo(
this.grid[(x + this.plx[4*dir+0]) + (y + this.ply[4*dir+0]) * (this.sx + 2)].x + this.ix[dir] * ix,
this.grid[(x + this.plx[4*dir+1]) + (y + this.ply[4*dir+1]) * (this.sx + 2)].y + this.ix[dir+4] * ix
);
this.paint = true;
// ---- next ----
return [
x + this.ix[dir+4],
y + this.ix[dir+8],
dir
];
}
}
LavaLamp.prototype.renderMetaballs = function () {
var i = 0, ball;
while (ball = this.balls[i++]) ball.move();
// ---- reset grid ----
this.iter++;
this.sign = -this.sign;
this.paint = false;
ctx.fillStyle = this.metaFill;
ctx.beginPath();
// ---- compute metaballs ----
i = 0;
while (ball = this.balls[i++]){
// ---- first cell ----
var next = [
Math.round(ball.pos.x / this.step),
Math.round(ball.pos.y / this.step), false
];
// ---- marching squares ----
do {
next = this.marchingSquares(next);
} while (next);
// ---- fill and close path ----
if (this.paint) {
ctx.fill();
ctx.closePath();
ctx.beginPath();
this.paint = false;
}
}
}
// ==== compute gradients ====
var createRadialGradient = function (w, h, r, c0, c1) {
var gradient = ctx.createRadialGradient(
w / 2, h / 2, 0,
w / 2, h / 2, r
);
gradient.addColorStop(0, c0);
gradient.addColorStop(1, c1);
return gradient;
}
// ==== main loop ====
var run = function () {
// ---- clear screen ----
ctx.fillStyle = backFill;
ctx.fillRect(0, 0, scr.width, scr.height);
// ---- render lava ----
lava.renderMetaballs();
// ---- next frame ----
requestAnimFrame(run);
}
// ==== init script ====
var init = function (s) {
// ---- canvas ----
scr = new ge1doot.Screen({
container: "screen"
});
ctx = scr.ctx;
// ---- resize canvas ----
var w = Math.min(scr.width, 400);
scr.elem.style.width = w + "px";
scr.elem.style.marginLeft = Math.round(-w / 2) + "px";
scr.resize();
// ---- pointer ----
pointer = new ge1doot.Pointer({});
// ---- create LavaLamps ----
backFill = createRadialGradient(scr.width, scr.height, scr.width, "#000", "#222");
lava = new LavaLamp(w, scr.height, 8, "#ffc821", "#fa0000");
// ---- start engine ----
run();
}
return {
// ---- launch script -----
load : function (setup) {
window.addEventListener('load', function () {
init();
}, false);
}
}
})().load();