Skip to content

Instantly share code, notes, and snippets.

@Garciat
Last active October 18, 2016 08:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Garciat/40699e763651c22754c0057f89151087 to your computer and use it in GitHub Desktop.
Save Garciat/40699e763651c22754c0057f89151087 to your computer and use it in GitHub Desktop.
<body style="margin:0"></body>
<script src="http://gbrlgrct.com/gists/07d74ba686ce04eccb11faa44f2ae229/functional.js"></script>
<script>
'use script';
// === SETUP
const SPACEW = document.body.clientWidth;
const SPACEH = document.body.clientHeight;
const canvas = document.createElement('canvas');
canvas.width = SPACEW;
canvas.height = SPACEH;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
// === MATHS
Math.TAU = 2 * Math.PI;
class Vec2 {
constructor(x, y) {
this.x = x || 0;
this.y = y || 0;
}
clone() {
return new Vec2(this.x, this.y);
}
distanceTo(v) {
return v.clone().sub_(this).length();
}
lengthSq() {
return this.x * this.x + this.y * this.y;
}
length() {
return Math.sqrt(this.lengthSq());
}
norm() {
return this.clone().sdiv_(this.length());
}
perpCW() {
return new Vec2(-this.y, this.x);
}
perpCCW() {
return new Vec2(this.y, -this.x);
}
add(v) {
return this.clone().add_(v);
}
add_(v) {
this.x += v.x;
this.y += v.y;
return this;
}
sub(v) {
return this.clone().sub_(v);
}
sub_(v) {
this.x -= v.x;
this.y -= v.y;
return this;
}
smul(k) {
return this.clone().smul_(k);
}
smul_(k) {
this.x *= k;
this.y *= k;
return this;
}
sdiv(k) {
return this.clone().sdiv_(k);
}
sdiv_(k) {
this.x /= k;
this.y /= k;
return this;
}
static zero() {
return Vec2.fromXY(0, 0);
}
static fromXY(x, y) {
return new Vec2(x, y);
}
static fromRads(r) {
return Vec2.fromXY(Math.cos(r), Math.sin(r));
}
static fromAngle(a) {
return Vec2.fromRads(a / 360 * Math.TAU);
}
}
function uniformI(a, b) {
return Math.floor(a + (b - a) * Math.random());
}
function randomHsla() {
return randomHslaWithHue(uniformI(0, 360));
}
function randomHslaWithHue(hue) {
return [hue, 100, uniformI(20, 80), 1];
}
function hslaToColor(hsla) {
return `hsla(${hsla[0]}, ${hsla[1]}%, ${hsla[2]}%, ${hsla[3]})`;
}
function randomDirectionVec2() {
return Vec2.fromRads(Math.random() * Math.TAU);
}
// === ENTITIES
function makeCircleDrawing(size, color) {
var canvas = document.createElement('canvas');
canvas.width = canvas.height = size * 2;
var ctx = canvas.getContext('2d');
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(size, size, size, 0, Math.TAU);
ctx.fill();
return canvas;
}
class Particle {
constructor(pos, spd, hsla, trace, size) {
this.deleted = false;
this.active = false;
this.trace = Boolean(trace);
this.pos = pos.clone();
this.spd = spd.clone();
this.size = size || uniformI(2, 6);
this.hsla = hsla;
this.color = hslaToColor(hsla);
this.path = [];
this.canvas = makeCircleDrawing(this.size, this.color);
}
}
class Force {
constructor(pos, value, color, pulsate) {
this.pos = pos.clone();
this.value = value;
this.color = color || (value < 0 ? 'blue' : 'white');
this.pulsate = pulsate || null;
this.pulsateState = -1.0 * Math.sign(value);
}
step() {
const pulsateLimit = 30;
this.value += this.pulsateState * 0.8;
if (this.value >= pulsateLimit || this.value <= -pulsateLimit) {
this.pulsateState *= -1;
}
this.color = hslaToColor([240, 100, 50 + 50 * Math.abs(pulsateLimit + this.value) / (pulsateLimit * 2), 1]);
}
}
class ParticleGenerator {
constructor(pos, value, color) {
this.pos = pos.clone();
this.value = value;
this.color = color || 'orange';
}
}
// === STATE
let particles = [];
let forces = [];
let mousePositions = [];
let particleGenerators = [];
let mousePosition = Vec2.zero();
let mouseSpeed = Vec2.zero();
let shouldProduce = false;
let shiftPressed = false;
let controlPressed = false;
let altPressed = false;
let clickDown = null;
let friction = false;
let showForceGens = true;
let currentHue = null;
const COMPOSITE_OPERATION_DEFAULT = 'source-over';
let compositeOperation = COMPOSITE_OPERATION_DEFAULT;
const SINK_RADIUS = 10;
let showParticleSpeedVector = false;
// === HELPERS
function produceParticlesAtPos(bag, pos, n) {
for (let i = 0; i < n; ++i) {
const spd = randomDirectionVec2().smul_(5 * Math.random() + 5);
bag.push(new Particle(pos, spd, randomHsla()));
}
}
function updateMouseSpeed(pos) {
mousePositions.push(pos);
if (mousePositions.length === 11) {
mousePositions.shift();
}
const n = mousePositions.length;
let spd = Vec2.zero();
for (var i = 1; i < n; ++i) {
spd.add_(mousePositions[i]);
spd.sub_(mousePositions[i - 1]);
}
spd.sdiv_(n);
mouseSpeed = spd;
}
function cleanOutOfBounds(bag) {
var nP = bag.length;
var ds = 0;
for (var iP = 0; iP < nP; ++iP) {
var p = bag[iP];
if (p.deleted) {
ds += 1;
continue;
}
if (!checkBounds(p)) {
p.deleted = true;
}
}
if (ds > 5000) {
return bag.filter(p => !p.deleted);
}
return null;
}
function checkBounds(subject) {
return subject.pos.x >= 0 &&
subject.pos.y >= 0 &&
subject.pos.x <= SPACEW &&
subject.pos.y <= SPACEH;
}
// === EVENTS
window.addEventListener('mousemove', function (ev) {
const pos = Vec2.fromXY(ev.clientX, ev.clientY);
if (clickDown === 1 && particles.length > 0) {
const lastParticle = particles[particles.length - 1];
if (pos.distanceTo(lastParticle.pos) > 25) {
let hsla = randomHslaWithHue(lastParticle.hsla[0]);
particles.push(new Particle(pos, Vec2.zero(), hsla, true));
}
} else if (clickDown === 2 && forces.length > 0) {
if (controlPressed) {
const axL = mouseSpeed.perpCW().norm();
const axR = mouseSpeed.perpCCW().norm();
const f = 20;
forces.push(new Force(pos.add(axL.smul(10)), -f));
forces.push(new Force(pos.add(axL.smul(5)), f));
forces.push(new Force(pos.add(axR.smul(5)), f));
forces.push(new Force(pos.add(axR.smul(10)), -f));
} else {
const lastForce = forces[forces.length - 1];
if (pos.distanceTo(lastForce.pos) > 25) {
forces.push(new Force(pos, lastForce.value));
}
}
}
mousePosition = pos;
});
window.addEventListener('mousedown', function (ev) {
const pos = Vec2.fromXY(ev.clientX, ev.clientY);
if (ev.button === 0) {
if (shiftPressed) {
particleGenerators.push(new ParticleGenerator(pos, 2));
} else {
shouldProduce = true;
}
} else if (ev.button === 1) {
particles.push(new Particle(pos, Vec2.zero(), randomHsla(), true));
} else if (ev.button === 2) {
let forceValue = 10;
let pulsate = null;
if (shiftPressed) {
forceValue *= -1;
}
if (altPressed) {
pulsate = 'linear';
}
forces.push(new Force(pos, forceValue, null, pulsate));
}
clickDown = ev.button;
});
window.addEventListener('mouseup', function (ev) {
if (ev.button === 0) {
shouldProduce = false;
}
particles.filter(p => p.trace).forEach(p => p.active = true);
clickDown = null;
});
window.addEventListener('keydown', function (ev) {
if (ev.keyCode === 16) { // shift
shiftPressed = true;
} else if (ev.keyCode === 17) { // control
controlPressed = true;
} else if (ev.keyCode === 18) { // alt
altPressed = true;
} else if (ev.keyCode === 32) { // space
particles = [];
} else if (ev.keyCode === 70) { // f
friction = !friction;
} else if (ev.keyCode === 68) { // d
forces = forces.filter(f => f.pos.distanceTo(mousePosition) > SINK_RADIUS);
particleGenerators = particleGenerators.filter(f => f.pos.distanceTo(mousePosition) > SINK_RADIUS);
} else if (ev.keyCode === 72) { // h
showForceGens = !showForceGens;
} else if (ev.keyCode === 67) { // c
compositeOperation = prompt('Composite operation', compositeOperation) || COMPOSITE_OPERATION_DEFAULT;
} else if (ev.keyCode === 90) { // z
forces = forces.slice(0, forces.length - 1);
} else if (ev.keyCode === 86) { // v
showParticleSpeedVector = !showParticleSpeedVector;
}
});
window.addEventListener('keyup', function (ev) {
if (ev.keyCode === 16) { // shift
shiftPressed = false;
} else if (ev.keyCode === 17) { // control
controlPressed = false;
} else if (ev.keyCode === 18) { // alt
altPressed = false;
}
});
canvas.addEventListener('contextmenu', function (ev) {
ev.preventDefault();
});
setInterval(function () {
updateMouseSpeed(mousePosition);
if (shouldProduce) {
produceParticlesAtPos(particles, mousePosition, uniformI(10, 20));
}
particleGenerators.forEach(gen => {
produceParticlesAtPos(particles, gen.pos, uniformI(1, gen.value));
});
}, 16);
// clean up out-of-bounds particles
setInterval(function () {
var newParticles = cleanOutOfBounds(particles);
if (newParticles) {
particles = newParticles;
}
}, 5000)
// === DRAWING
function applyGravity(sink, subject) {
const dpos = sink.pos.sub(subject.pos);
const dd = dpos.length();
if (dd <= SINK_RADIUS) {
return;
}
const ir2 = Math.pow(dd, 2);
const vel = dpos.sdiv(dd).smul(100 * sink.value / ir2);
subject.spd.add_(vel);
}
function applyFriction(subject) {
subject.spd.add_(subject.spd.smul(-0.05));
}
function drawVector(ctx, pos, vec, scale, color) {
const vecS = pos.add(vec.smul(scale));
ctx.beginPath();
ctx.moveTo(pos.x, pos.y);
ctx.lineTo(vecS.x, vecS.y);
ctx.strokeStyle = color;
ctx.lineWidth = 2
ctx.stroke();
}
function simulate() {
var nP = particles.length;
var nF = forces.length;
for (var iP = 0; iP < nP; ++iP) {
var particle = particles[iP];
if (particle.deleted) {
continue;
}
if (particle.trace && !particle.active) {
continue;
}
particle.pos.add_(particle.spd);
for (var iF = 0; iF < nF; ++iF) {
applyGravity(forces[iF], particle);
}
if (friction) {
applyFriction(particle);
}
if (particle.trace) {
particle.path.push(particle.pos.clone());
}
}
}
function clearScreen() {
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, SPACEW, SPACEH);
}
function drawPath(path, color) {
ctx.beginPath();
ctx.moveTo(path[0].x, path[0].y);
var nXY = path.length;
for (var iXY = 0; iXY < nXY; ++iXY) {
var p = path[iXY];
ctx.lineTo(p.x, p.y);
}
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.stroke();
}
function drawParticles() {
var nP = particles.length;
for (var iP = 0; iP < nP; ++iP) {
var particle = particles[iP];
if (particle.deleted) {
continue;
}
var sz = +particle.size;
ctx.drawImage(particle.canvas, particle.pos.x - sz, particle.pos.y - sz);
if (particle.trace && particle.active) {
drawPath(particle.path, particle.color);
}
if (showParticleSpeedVector) {
drawVector(ctx, particle.pos, particle.spd, 5, particle.color);
}
}
}
function drawForces() {
var nF = forces.length;
for (var iF = 0; iF < nF; ++iF) {
var force = forces[iF];
ctx.fillStyle = force.color;
ctx.beginPath();
ctx.arc(force.pos.x, force.pos.y, SINK_RADIUS, 0, Math.TAU);
ctx.fill();
}
}
function applyForcePulsation() {
var nF = forces.length;
for (var iF = 0; iF < nF; ++iF) {
var force = forces[iF];
if (force.pulsate) {
force.step();
}
}
}
function drawParticleGenerators() {
var nF = particleGenerators.length;
for (var iF = 0; iF < nF; ++iF) {
var gen = particleGenerators[iF];
ctx.fillStyle = gen.color;
ctx.beginPath();
ctx.arc(gen.pos.x, gen.pos.y, SINK_RADIUS, 0, Math.TAU);
ctx.fill();
}
}
function draw() {
clearScreen();
ctx.globalCompositeOperation = compositeOperation;
drawParticles();
if (showForceGens) {
drawForces();
drawParticleGenerators();
}
applyForcePulsation();
// drawVector(ctx, mousePosition, mouseSpeed, 5, 'green');
}
function loop() {
simulate();
draw();
requestAnimationFrame(loop);
}
// === GO !
loop();
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment