131 lines
2.9 KiB
TypeScript
131 lines
2.9 KiB
TypeScript
|
window.onload = function () {
|
||
|
class Particle {
|
||
|
public x: number;
|
||
|
public y: number;
|
||
|
public radius: number;
|
||
|
public color: string;
|
||
|
public vx: number = 0;
|
||
|
public vy: number = 0;
|
||
|
|
||
|
public static DEFAULT_RADIUS = 2.5;
|
||
|
|
||
|
public constructor(x: number, y: number, color: string, radius: number) {
|
||
|
this.x = x;
|
||
|
this.y = y;
|
||
|
this.color = color;
|
||
|
this.radius = radius;
|
||
|
}
|
||
|
|
||
|
public draw() {
|
||
|
ctx.beginPath();
|
||
|
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
||
|
ctx.fillStyle = this.color;
|
||
|
ctx.fill();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const canvas = document.querySelector("canvas") as HTMLCanvasElement;
|
||
|
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
|
||
|
|
||
|
canvas.width = window.innerWidth;
|
||
|
canvas.height = window.innerHeight;
|
||
|
|
||
|
window.addEventListener("resize", () => {
|
||
|
canvas.width = window.innerWidth;
|
||
|
canvas.height = window.innerHeight;
|
||
|
});
|
||
|
|
||
|
const particles: Particle[] = [];
|
||
|
|
||
|
function randomInt(min: number, max: number) {
|
||
|
return Math.floor(Math.random() * (max - min + 1) + min);
|
||
|
}
|
||
|
|
||
|
function makeGroup(
|
||
|
number: number,
|
||
|
color: string,
|
||
|
options: { radius?: number }
|
||
|
) {
|
||
|
const group: Particle[] = [];
|
||
|
|
||
|
for (let i = 0; i < number; i++) {
|
||
|
const particle = new Particle(
|
||
|
randomInt(0, canvas.width),
|
||
|
randomInt(0, canvas.height),
|
||
|
color,
|
||
|
options.radius ?? Particle.DEFAULT_RADIUS
|
||
|
);
|
||
|
|
||
|
group.push(particle);
|
||
|
particles.push(particle);
|
||
|
}
|
||
|
|
||
|
return group;
|
||
|
}
|
||
|
|
||
|
type Options = {
|
||
|
minDistance?: number;
|
||
|
maxDistance?: number;
|
||
|
g?: number;
|
||
|
};
|
||
|
|
||
|
function interaction(
|
||
|
group1: Particle[],
|
||
|
group2: Particle[],
|
||
|
options: Options = {}
|
||
|
) {
|
||
|
options.g = options.g ?? 0.1;
|
||
|
options.maxDistance = options.maxDistance ?? 100;
|
||
|
options.minDistance = options.minDistance ?? 0;
|
||
|
|
||
|
for (let i = 0; i < group1.length; i++) {
|
||
|
let fx = 0;
|
||
|
let fy = 0;
|
||
|
|
||
|
for (let j = 0; j < group2.length; j++) {
|
||
|
let a = group1[i];
|
||
|
let b = group2[j];
|
||
|
|
||
|
const dx = a.x - b.x;
|
||
|
const dy = a.y - b.y;
|
||
|
const d = Math.sqrt(dx * dx + dy * dy);
|
||
|
|
||
|
if (d > 0 && d < options.maxDistance && d > options.minDistance) {
|
||
|
const F = options.g / d;
|
||
|
fx += F * dx;
|
||
|
fy += F * dy;
|
||
|
}
|
||
|
|
||
|
a.vx = (a.vx + fx) * 0.5;
|
||
|
a.vy = (a.vy + fy) * 0.5;
|
||
|
a.x += a.vx * 0.005;
|
||
|
a.y += a.vy * 0.005;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const blue = makeGroup(5000, "#0000ff", { radius: 1 });
|
||
|
const red = makeGroup(1000, "#ff0000", { radius: 2.5 });
|
||
|
const green = makeGroup(50, "#00ff00", { radius: 4 });
|
||
|
|
||
|
function animate() {
|
||
|
interaction(red, red, { g: -0.05, maxDistance: 250 });
|
||
|
interaction(green, red, { g: -0.1, maxDistance: 500 });
|
||
|
interaction(red, green, { g: -0.05, maxDistance: 250 });
|
||
|
interaction(blue, red, { g: -0.4, maxDistance: 250 });
|
||
|
|
||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
|
|
||
|
ctx.fillStyle = "#000000";
|
||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||
|
|
||
|
particles.forEach((particle) => {
|
||
|
particle.draw();
|
||
|
});
|
||
|
|
||
|
requestAnimationFrame(animate);
|
||
|
}
|
||
|
|
||
|
animate();
|
||
|
};
|