Public
Edited
Mar 2, 2023
1 fork
6 stars
Insert cell
Insert cell
Insert cell
output = createCanvas(width, 512, draw, setup, {css: "background: black"})
Insert cell
Insert cell
// The hardware acceleration of p8g means we're mainly
// limited by how quickly we can iterate over the data.
// To speed that up we use typed arrays that share the same
// backing buffer for storing all data for our particles.
// Another trick is that we don't actually "create" new
// particles, we re-use the same fixed number of items
// in the typed array and recycle a few of the oldest
// each frame. That way we avoid spending a lot of time
// on garbage collection (since nothing is allocated).
function setup(p8g) {
p8g.background(255);

const {width, height} = p8g;
const {random, sin, cos, PI} = Math;
const TAU = 2 * PI;

// Fake a struct by using the same backing buffer
// for a Float32Array and Uint8Array
// - dx, dy, x, and y use float32 values,
// - R, G, and B use uint8 vallues.
// The only tricky part is calculating
// the offsets correctly when using these
// two arrays.
const pos = new Float32Array(totalParticles*5);
const rgb = new Uint8Array(pos.buffer);

// Initialize particles
for (let j = 0; j < totalParticles; j++) {
const idx = j*5;
const phi = TAU * random();
const r = 1 + random() * 10 + random() * 10;
// dx and dy
pos[idx] = r * sin(phi);
pos[idx+1] = r * cos(phi);
// x and y position
pos[idx+2] = random() * width;
pos[idx+3] = random() * height;
// RGB
const RGB_idx = idx * 4 + (4*4);
const t = j*256/totalParticles;
rgb[RGB_idx] = (1 + sin(t*TAU)) * 127.5 | 0// Math.sqrt(random())*255|0;
rgb[RGB_idx+1] = (1 + sin((t + 2/3)*TAU)) * 127.5 | 0// Math.sqrt(random())*255|0;
rgb[RGB_idx+2] = (1 + sin((t + 1/3)*TAU)) * 127.5 | 0// Math.sqrt(random())*255|0;
}

return [pos, rgb];
}
Insert cell
function draw (p8g, [pos, rgb]) {
// fade out
p8g.noStroke();
p8g.noSmooth()
p8g.fill(255, 8);
p8g.rect(0, 0, p8g.width, p8g.height);
updateParticles(p8g, pos, rgb);
drawText(p8g);
}
Insert cell
thrownParticles = Math.max(1, totalParticles/256 | 0);
Insert cell
// updateParticles is again a closure, so that state is maintained between draw calls.
updateParticles = {
let i = 0, pmx = 0, pmy = 0;
return (p8g, pos, rgb) => {
throwParticles(i, pos, p8g.mouseX, p8g.mouseY, pmx, pmy);
fallingParticles(p8g, i, pos, rgb)
pmx = p8g.mouseX;
pmy = p8g.mouseY;
i = (i + thrownParticles) % totalParticles;
}
}
Insert cell
function throwParticles(i, pos, mouseX, mouseY, pmx, pmy) {
// thickness of particles

// mouse delta, used for throwing
const dmx = mouseX - pmx;
const dmy = mouseY - pmy;

const TAU = 2 * Math.PI;
const {random, sin, cos} = Math;

for (let j = 0; j < thrownParticles; j++) {
const idx = ((j+i)%totalParticles)*5;
const phi = TAU * random();
const r = 1 + random() * 10 + random() * 10;
// throw more upwards/downwards, but don't accelerate too much sideways
pos[idx] = r * sin(phi) + dmx * 2;
pos[idx+1] = r * cos(phi) + dmy * 4;
// position at cursor
pos[idx+2] = mouseX;
pos[idx+3] = mouseY;
}
}
Insert cell
function fallingParticles({width, height, stroke, strokeWeight, line}, i, pos, rgb){
// set up thickness of particles
strokeWeight(3);
for (let j = thrownParticles; j < totalParticles; j++) {
const idx = ((j+i)%totalParticles)*5;
let dx = pos[idx] = pos[idx] * 31 / 32;
// damping + gravity
let dy = pos[idx+1] = pos[idx+1] * 31 / 32 + 0.5;
let x = pos[idx+2] + dx;
let y = pos[idx+3] + dy;

// clip to box
if (x < 0) {
dx = -dx;
x = -x;
} else if (x > width) {
dx = -dx;
x = 2*width - x;
}

if (y < 0) {
dy = -dy;
y = -y;
} else if (y > height) {
dy = -dy;
y = 2*height -y;
}

pos[idx] = dx;
pos[idx+1] = dy;
pos[idx+2] = x;
pos[idx+3] = y;
const RGB_idx = idx*4 + (4*4);
stroke(rgb[RGB_idx], rgb[RGB_idx + 1], rgb[RGB_idx + 2]);
line(x-dx, y-dy, x, y);
}
}
Insert cell
drawText = {
let t = 0;
return (p8g) => {
// change text color over time, use sinebow colormap
t = (t+1) & 0x7F;
// set the stroke the color to the opposite side of the sinebow
const ts = t + 0x40 & 0x7F;
const {sin} = Math;
const TAU = Math.PI * 2;
p8g.fill(
(1 + sin(t*0.0078125*TAU)) * 127.5 | 0,
(1 + sin((t*0.0078125 + 2/3)*TAU)) * 127.5 | 0,
(1 + sin((t*0.0078125 + 1/3)*TAU)) * 127.5 | 0
)
p8g.stroke(
(1 + sin(ts*0.0078125*TAU)) * 127.5 | 0,
(1 + sin((ts*0.0078125 + 2/3)*TAU)) * 127.5 | 0,
(1 + sin((ts*0.0078125 + 1/3)*TAU)) * 127.5 | 0
)
p8g.textSize(40);
p8g.strokeWeight(8);
p8g.smooth();
p8g.text(`${totalParticles} particles`, 100, 140);
};
}
Insert cell
Insert cell
// The normal p8g approach would be to assign a draw function to
// the p8g object, but because it's imported through skypack
// at runtime the p8g module is read-only. So instead we use
// this workaround.

createCanvas = {
const p8g = await import('https://cdn.skypack.dev/p8g.js@0.8.3?min');

let canvas = null;
let _draw = () => {};
let _setup = () => {};
let request = null;
let state = null;
return async function createCanvas(width, height, draw = _draw, setup = _setup, opts = {}) {

const {
css
} = opts
// cancel previous loop
if (request) cancelAnimationFrame(request);

if(typeof _setup !== "function") throw new Error("setup is not a function!");
if(typeof _draw !== "function") throw new Error("draw is not a function!")

_setup = setup;
_draw = draw;

if (!canvas) {
canvas = await html`${p8g.createCanvas(width, height)}`;
}
if (typeof css === "string") canvas.style = css;

state = setup(p8g);

// Draw Loop
request = requestAnimationFrame(function tick() {
draw(p8g, state);
request = requestAnimationFrame(tick);
});

invalidation.then(() => cancelAnimationFrame(request));

return canvas;
}
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more