Public
Edited
Nov 28, 2023
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
mutable cursor = ({x: -500, y: -500})
Insert cell
{
canvas.addEventListener('mousemove', (event) => {
mutable cursor = {x: event.offsetX, y: event.offsetY}
})

canvas.addEventListener('mouseout', (event) => {
mutable cursor = {x: -1000, y: -1000}
})
}
Insert cell
class Snowfall {

constructor(width, height, particleSize=5, particleCount=4000) {
this.width = width;
this.height = height;
this.particleSize = particleSize;
this.particleCount = particleCount;
this.particles = []; // List of {x, y, r, dx, dy} particles

this.gravity = 0.2;
this.drag = 0.5;

this.noise = new noisejs.Noise(0);
this.noiseScale = 60;
this.noiseStrengthFactor = 0.4;
this.timer = 0;
}

makeParticle() {
return {
x: Math.random() * this.width,
y: Math.random () * this.height * -1,
z: Math.random() * this.height,
r: (Math.random() * this.particleSize / 2) + 0.1,
dx: (Math.random() - 0.5) * 10,
dy: 0
}
}

drift(particle, options) {

// Blown by wind
const windDirection = (this.noise.perlin3(
particle.x / options.noiseScale,
particle.y / options.noiseScale,
particle.z / options.noiseScale + this.timer * options.noiseSpeed, // Drift z-wards over time
) + Math.random() * 0.1) * Math.PI;
const windStrength = this.noise.perlin2(
particle.x / options.noiseScale,
particle.y / options.noiseScale
) * options.noiseStrength;

// Repel from curor
const cursorDistance = Math.sqrt(
Math.pow(mutable cursor.x - particle.x, 2) +
Math.pow(mutable cursor.y - particle.y , 2)
);
const cursorDirection = Math.atan2(
mutable cursor.x - particle.x,
mutable cursor.y - particle.y
)
// Make cursor similar in strength to gravity at a short distance
const cursorStrength = options.cursorStrength / Math.pow(1 + cursorDistance / 100, 2);
return {
z: particle.z,
x: particle.x + particle.dx,
// Calculate new dx
dx: (
// Current Speed
particle.dx
// Plus some change from noise field
+ Math.sin(windDirection) * windStrength
// Plus cursor repulsion
- Math.sin(cursorDirection) * cursorStrength
// Minus drag force propotional to v squared (preserve sign)
- (particle.dx ** 2 * options.drag * Math.sign(particle.dx) / particle.r)
)
,
y: particle.y + particle.dy,
dy: (
// current speed
particle.dy
// Plus noise field
+ Math.cos(windDirection) * windStrength
// Plus cursor repulsion
- Math.cos(cursorDirection) * cursorStrength
// Minus drag acceleration
- (particle.dy ** 2 * options.drag * Math.sign(particle.dy) / particle.r)
// Plus gravity
+ options.gravity
)
,
r: particle.r
}
}

tick(options) {
this.timer += 1;
const newParticles = this.particles.filter(
// Exit snowflakes that have fallen out of frame.
d => d.y < (this.height + this.particleSize) && d.x > -50 && d.x < width + 50
).map(
// Float downwards
d => this.drift(d, options)
);

while (newParticles.length < options.particleCount) {
newParticles.push(this.makeParticle())
}

this.particles = newParticles;
}

draw(context, options) {
context.save()
context.fillStyle = 'black'
context.globalAlpha = 0.5
context.fillRect(0, 0, this.width, this.height);

// Draw Cursor and cursor direction
if (options.showCursor) {
context.beginPath();
context.arc(mutable cursor.x, mutable cursor.y, options.cursorRadius, 0, TWOPI);
context.fillStyle = 'green'
context.fill()
}
for (let i = 0; i < this.particles.length; ++i) {
const p = this.particles[i];
const fadeOutFactor = (options.fadeOut ? 1 - Math.min(p.y / this.height, 1) : 1) ** 0.5;
context.beginPath();
context.fillStyle = 'white'
context.globalAlpha = 1 * fadeOutFactor;
context.arc(p.x, p.y, p.r * fadeOutFactor, 0, TWOPI);
context.fill();
}
context.restore()
}

}
Insert cell
TWOPI = Math.PI * 2
Insert cell
noisejs = import("https://cdn.skypack.dev/noisejs@2.1.0")
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