UNPKG

lava-lamp-bubbles

Version:
305 lines (280 loc) 9.74 kB
export default class LavaLampBubbles { private lava0: LavaLamp; private ctx: CanvasRenderingContext2D; private screen: any; constructor(canvasID: string, speed: number, color1: string, color2: string) { // canvas this.screen = new Ge1doot(canvasID).init(true); this.ctx = this.screen.ctx; this.screen.resize(); window.addEventListener("resize", () => this.screen.resize()); // create LavaLamps this.lava0 = new LavaLamp(this.ctx, this.screen.width, this.screen.height, speed, 10, color1, color2); } public start() { requestAnimationFrame(() => this.start()); this.ctx.clearRect(0, 0, screen.width, screen.height); this.lava0.renderMetaballs(); } } class Ge1doot { private elem: HTMLCanvasElement; private ctx: CanvasRenderingContext2D; private width = 0; private height = 0; private left = 0; private top = 0; constructor(id: string) { this.elem = <HTMLCanvasElement>document.getElementById(id); if (this.elem === null) throw "Invalid Canvas id!"; this.ctx = <CanvasRenderingContext2D>this.elem.getContext("2d"); } init(initRes: boolean) { window.addEventListener("resize", () => this.resize()); this.elem.onselectstart = function () { return false; }; this.elem.ondrag = function () { return false; }; initRes && this.resize(); return this; } resize() { let o: any = this.elem; this.width = o.offsetWidth; this.height = o.offsetHeight; for (this.left = 0, this.top = 0; o != null; o = o.offsetParent) { this.left += o.offsetLeft; this.top += o.offsetTop; } if (this.ctx) { this.elem.width = this.width; this.elem.height = this.height; } } } class Point { public x: number; public y: number; public magnitude: number; public computed: number = 0; public force: number = 0; constructor(x: number, y: number) { this.x = x; this.y = y; this.magnitude = x * x + y * y; this.computed = 0; this.force = 0; } add(p: Point) { return new Point(this.x + p.x, this.y + p.y); } } class Ball { private vel: Point; public pos: Point; public size: number; private width: number; private height: number; constructor(par: LavaLamp) { let min = 0.05; let max = 0.075; this.vel = new Point( (Math.random() > 0.5 ? 1 : -1) * (0.2 + Math.random() * 0.25) * 0.25 * par.speed, (Math.random() > 0.5 ? 1 : -1) * (0.2 + Math.random()) * 0.25 * par.speed ); this.pos = new Point( par.width * 0.2 + Math.random() * par.width * 0.6, par.height * 0.2 + Math.random() * par.height * 0.6 ); this.size = par.wh / 15 + (Math.random() * (max - min) + min) * (par.wh / 15); this.width = par.width; this.height = par.height; } move() { // 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); } } class LavaLamp { private ctx: CanvasRenderingContext2D; private step = 5; public width: number; public height: number; public wh: number; private sx: number; private sy: number; public speed: number; private paint = false; private metaFill: any; private plx: number[]; private ply: number[]; private mscases: number[]; private ix: number[]; private grid: Point[]; private balls: Ball[]; private iter = 0; private sign = 1; constructor( ctx: CanvasRenderingContext2D, width: number, height: number, speed: number, numBalls: number, c0: string, c1: string ) { this.ctx = ctx; this.step = 5; this.width = width; this.height = height; this.wh = Math.min(width, height); this.sx = Math.floor(this.width / this.step); this.sy = Math.floor(this.height / this.step); this.speed = speed; this.paint = false; this.metaFill = createRadialGradient(this.ctx, 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 (let 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 (let k = 0; k < numBalls; k++) { this.balls[k] = new Ball(this); } } computeForce(x: number, y: number, idx: number) { let force: number; let id = idx || x + y * (this.sx + 2); if (x === 0 || y === 0 || x === this.sx || y === this.sy) { force = 0.6 * this.sign; } else { force = 0; let cell = this.grid[id]; let i = 0; let ball: 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; } marchingSquares(next: any) { let x = next[0]; let y = next[1]; let pdir = next[2]; let id = x + y * (this.sx + 2); if (this.grid[id].computed === this.iter) { return false; } let dir: any; let mscase = 0; // neighbors force for (let i = 0; i < 4; i++) { let idn = x + this.ix[i + 12] + (y + this.ix[i + 16]) * (this.sx + 2); let 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 let 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); this.ctx.lineTo( this.grid[x + this.plx[4 * dir] + (y + this.ply[4 * dir]) * (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]; } } renderMetaballs() { let i = 0; let ball: Ball; while ((ball = this.balls[i++])) ball.move(); // reset grid this.iter++; this.sign = -this.sign; this.paint = false; this.ctx.fillStyle = this.metaFill; this.ctx.beginPath(); // compute metaballs i = 0; while ((ball = this.balls[i++])) { // first cell let next: any = [Math.round(ball.pos.x / this.step), Math.round(ball.pos.y / this.step), false]; // marching squares while (next) { next = this.marchingSquares(next); } // fill and close path if (this.paint) { this.ctx.fill(); this.ctx.closePath(); this.ctx.beginPath(); this.paint = false; } } } } // gradients function createRadialGradient(ctx: CanvasRenderingContext2D, w: number, h: number, r: number, c0: string, c1: string) { let gradient = ctx.createRadialGradient(w / 1, h / 1, 0, w / 1, h / 1, r); gradient.addColorStop(0, c0); gradient.addColorStop(1, c1); return gradient; }