Skip to content

Instantly share code, notes, and snippets.

@Sorebit
Last active November 21, 2022 15:21
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 Sorebit/8c7c7a5191c1d8c4963f0adf9906db9d to your computer and use it in GitHub Desktop.
Save Sorebit/8c7c7a5191c1d8c4963f0adf9906db9d to your computer and use it in GitHub Desktop.
💧 This is a small water pond filled with digital deterministic fish.
<!DOCTYPE html>
<meta charset="utf-8"/>
<title>💧</title>
<style type="text/css">
* {margin: 0; padding: 0;}
canvas {position: absolute; left: 0; top: 0;}
body {overflow: hidden;}
#main {background: #235d80; z-index: 1;}
</style>
<canvas id="main"></canvas>
<script src="main.js" defer></script>
// This is a small water pond filled with digital deterministic fish.
// Their movement and colors are based on start parameters.
class Game {
constructor(particleCount, fullyDeveloped) {
this.tickNum = 0;
this.can = document.getElementById('main');
this.ctx = this.can.getContext('2d');
// Whether the fish should have their tails grown or grow them publicly
fullyDeveloped = fullyDeveloped || false;
this.particles = this.createParticles(particleCount, fullyDeveloped);
}
createParticles(N, fullyDeveloped) {
// Returns N new particles
let pts = [];
for (let i = 0; i < N; i++) {
pts.push(
new Particle(
Math.random() * Math.PI, // random spin
120 + (Math.random() - 0.5) * 30, // [90, 150] frames
fullyDeveloped,
)
);
}
return pts;
}
updateParticles() {
for (let i in this.particles) {
this.particles[i].update(this.tickNum)
}
}
drawParticle(pt) {
this.ctx.lineWidth = 7;
this.ctx.lineCap = 'round';
for (let i = 0; i < pt.frames.length(); i++) {
const frame = pt.frames.at(i);
// Position on canvas
const cx = this.can.width * (frame.x);
const cy = this.can.height * (frame.y);
//
const l = (pt.frames.length() - i) / (pt.frames.length() + 1);
// Position of the segment
const spin = frame.spin;
const x = l * (pt.length / 2) * Math.cos(spin);
const y = l * (pt.length / 2) * Math.sin(spin);
const c = frame.color;
this.ctx.strokeStyle = `rgba(${c.r},${c.g},${c.b},0.2)`;
this.ctx.beginPath();
// A line with it's middle on the particle spine
this.ctx.moveTo(cx + x, cy - y);
this.ctx.lineTo(cx - x, cy + y);
this.ctx.stroke();
this.ctx.closePath();
}
}
draw() {
// Clear screen
this.ctx.clearRect(0, 0, this.can.width, this.can.height);
// Draw every particle
for (let i in this.particles) {
this.drawParticle(this.particles[i]);
}
}
tick() {
this.updateParticles();
this.draw();
this.tickNum += 1;
requestAnimationFrame(this.tick.bind(this));
}
};
class Particle {
// So the particle is made up of segments like so
// 0: S
// 1: -S-
// 2: --S--
// The S represents the spine, the hyphens represent the sides of the segment
constructor(spin, maxFrames, startFullyDeveloped) {
// maxFrames - how many segments the tail has
// START PARAMS
this.startSpin = spin;
const spinDir = Math.random() >= 0.5 ? 1 : -1;
this.spinConst = 0.005 + Math.random() * 0.05 * spinDir;
this.spinMod = 0.5 + Math.random() * 0.5;
this.waveOff = new Vec2(
Math.random() * Math.PI * 2,
Math.random() * Math.PI * 2
)
this.baseColor = {
r: Math.min(240, 190 + Math.floor(Math.random() * 30 - 10)),
g: Math.min(240, 200 + Math.floor(Math.random() * 30 - 10)),
b: Math.min(240, 220 + Math.floor(Math.random() * 30 - 10)),
}
// The tail is remembered within frames.
this.length = 20 + Math.random() * 40; // How many pixels long is the tail
this.frames = new CappedList(maxFrames);
startFullyDeveloped = startFullyDeveloped || false;
if (startFullyDeveloped) {
// Start up by inserting frames
for (let i = 0; i < maxFrames; i++) {
this.update(maxFrames - i);
}
}
}
posAt(tickNum) {
const center = new Vec2(0.5, 0.5);
// Move in what direction exactly???
const x = center.x + Math.sin(this.waveOff.x + tickNum * 0.002) * (0.25) *
(1 + (Math.sin(tickNum * 0.02) * 0.1));
const y = center.y + Math.sin(this.waveOff.y + tickNum * 0.002) * 0.35;
return new Vec2(x, y);
}
spinAt(tickNum) {
return (this.startSpin + this.spinConst * tickNum) * this.spinMod;
}
colorAt(tickNum) {
const colorOff = Math.sin(this.spinAt(tickNum)) * 20;
// console.log(colorOff);
const r = Math.min(255, this.baseColor.r + colorOff);
const g = Math.min(255, this.baseColor.g + colorOff);
const b = Math.min(255, this.baseColor.b + colorOff);
return {r: r, g: g, b: b};
}
frameAt(tickNum) {
// Update spin direction?
const spin = this.startSpin + this.spinConst * tickNum;
const pos = this.posAt(tickNum);
const color = this.colorAt(tickNum);
return {
spin: spin,
x: pos.x, y: pos.y,
color: color
// TODO sin length
};
}
update(tickNum) {
const f = this.frameAt(tickNum)
// Remember frames
this.frames.add(f);
}
};
// --- Utilities ---------------------------------------------------------------
class Vec2 {
constructor(x, y) { this.x = x; this.y = y; }
}
class CappedList {
constructor(cap) {
this.items = [];
this._cap = cap;
}
at(index) {
return this.items[index];
}
add(item) {
this.items.push(item);
// Keep at most N latest frames
if (this.items.length > this._cap) {
this.items.shift();
}
}
length() {
return this.items.length;
}
}
// --- Main --------------------------------------------------------------------
function main() {
const game = new Game(8, false);
function handleWindowResize() {
game.can.width = window.innerWidth;
game.can.height = window.innerHeight;
}
window.onresize = handleWindowResize;
handleWindowResize();
game.tick();
}
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment