leumas-private-shared
Version:
Private React JSX Package For Leumas Shared Components, Headers, Footers, Asides, Login Pages, API Key Manager and much more. Styles and everything reusable to avoid DRY code across all of our subdomains
388 lines (328 loc) • 11.2 kB
JSX
import React, { useEffect, useRef } from 'react';
import { styled } from '@mui/system';
const SpipaWrapper = styled('div')`
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
background: radial-gradient(#555, #111);
`;
const UI = styled('div')`
display: none;
position: fixed;
z-index: 5;
bottom: 0;
left: 0;
width: 120px;
padding: 10px;
background: rgba(255, 255, 255, 0.7);
p {
font-size: 11px;
font-weight: 700;
&.zoom {
margin-bottom: 5px;
span {
margin-right: 5px;
border: solid 1px #777;
cursor: pointer;
border-radius: 2px;
&.zoomin {
padding: 2px 5px;
}
&.zoomout {
padding: 2px 8px;
}
&:hover {
background: black;
color: white;
}
}
}
}
`;
const SpipaBackground = ({ children }) => {
const canvasRef = useRef(null);
const uiRef = useRef(null);
useEffect(() => {
const App = {};
App.setup = function () {
const canvas = canvasRef.current;
this.filename = 'spipa';
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
this.canvas = canvas;
this.ctx = this.canvas.getContext('2d');
this.width = this.canvas.width;
this.height = this.canvas.height;
this.dataToImageRatio = 1;
this.ctx.imageSmoothingEnabled = false;
this.ctx.webkitImageSmoothingEnabled = false;
this.ctx.msImageSmoothingEnabled = false;
this.xC = this.width / 2;
this.yC = this.height / 2;
this.stepCount = 0;
this.particles = [];
this.lifespan = 1000;
this.popPerBirth = 1;
this.maxPop = 300;
this.birthFreq = 2;
// Build grid
this.gridSize = 8; // Motion coords
this.gridSteps = Math.floor(1000 / this.gridSize);
this.grid = [];
let i = 0;
for (let xx = -500; xx < 500; xx += this.gridSize) {
for (let yy = -500; yy < 500; yy += this.gridSize) {
// Radial field, triangular function of r with max around r0
const r = Math.sqrt(xx * xx + yy * yy);
const r0 = 100;
let field;
if (r < r0) field = (255 / r0) * r;
else if (r > r0) field = 255 - Math.min(255, (r - r0) / 2);
this.grid.push({
x: xx,
y: yy,
busyAge: 0,
spotIndex: i,
isEdge:
xx === -500
? 'left'
: xx === -500 + this.gridSize * (this.gridSteps - 1)
? 'right'
: yy === -500
? 'top'
: yy === -500 + this.gridSize * (this.gridSteps - 1)
? 'bottom'
: false,
field: field,
});
i++;
}
}
this.gridMaxIndex = i;
// Counters for UI
this.drawnInLastFrame = 0;
this.deathCount = 0;
this.initDraw();
};
App.evolve = function () {
const time1 = performance.now();
this.stepCount++;
// Increment all grid ages
this.grid.forEach((e) => {
if (e.busyAge > 0) e.busyAge++;
});
if (this.stepCount % this.birthFreq === 0 && this.particles.length + this.popPerBirth < this.maxPop) {
this.birth();
}
App.move();
App.draw();
const time2 = performance.now();
// Update UI
uiRef.current.querySelector('.dead').textContent = this.deathCount;
uiRef.current.querySelector('.alive').textContent = this.particles.length;
uiRef.current.querySelector('.fps').textContent = Math.floor(1000 / (time2 - time1));
uiRef.current.querySelector('.drawn').textContent = this.drawnInLastFrame;
};
App.birth = function () {
let x, y;
const gridSpotIndex = Math.floor(Math.random() * this.gridMaxIndex);
const gridSpot = this.grid[gridSpotIndex];
x = gridSpot.x;
y = gridSpot.y;
const particle = {
hue: 200,
sat: 95,
lum: 20 + Math.floor(40 * Math.random()),
x: x,
y: y,
xLast: x,
yLast: y,
xSpeed: 0,
ySpeed: 0,
age: 0,
ageSinceStuck: 0,
attractor: {
oldIndex: gridSpotIndex,
gridSpotIndex: gridSpotIndex,
},
name: 'seed-' + Math.ceil(10000000 * Math.random()),
};
this.particles.push(particle);
};
App.kill = function (particleName) {
const newArray = this.particles.filter((seed) => seed.name !== particleName);
this.particles = newArray;
};
App.move = function () {
for (let i = 0; i < this.particles.length; i++) {
// Get particle
const p = this.particles[i];
// Save last position
p.xLast = p.x;
p.yLast = p.y;
// Attractor and corresponding grid spot
const index = p.attractor.gridSpotIndex;
let gridSpot = this.grid[index];
// Maybe move attractor and with certain constraints
if (Math.random() < 0.5) {
// Move attractor
if (!gridSpot.isEdge) {
// Change particle's attractor grid spot and local move function's grid spot
const topIndex = index - 1;
const bottomIndex = index + 1;
const leftIndex = index - this.gridSteps;
const rightIndex = index + this.gridSteps;
const topSpot = this.grid[topIndex];
const bottomSpot = this.grid[bottomIndex];
const leftSpot = this.grid[leftIndex];
const rightSpot = this.grid[rightIndex];
// Choose neighbour with highest field value (with some disobedience...)
const chaos = 30;
const maxFieldSpot = [topSpot, bottomSpot, leftSpot, rightSpot].reduce(
(max, e) => (e.field + chaos * Math.random() > max.field ? e : max),
{ field: -Infinity }
);
const potentialNewGridSpot = maxFieldSpot;
if (potentialNewGridSpot.busyAge === 0 || potentialNewGridSpot.busyAge > 15) {
p.ageSinceStuck = 0;
p.attractor.oldIndex = index;
p.attractor.gridSpotIndex = potentialNewGridSpot.spotIndex;
gridSpot = potentialNewGridSpot;
gridSpot.busyAge = 1;
} else {
p.ageSinceStuck++;
}
} else {
p.ageSinceStuck++;
}
if (p.ageSinceStuck === 10) this.kill(p.name);
}
// Spring attractor to center with viscosity
const k = 8;
const visc = 0.4;
const dx = p.x - gridSpot.x;
const dy = p.y - gridSpot.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// Spring
const xAcc = -k * dx;
const yAcc = -k * dy;
p.xSpeed += xAcc;
p.ySpeed += yAcc;
// Calm the f*ck down
p.xSpeed *= visc;
p.ySpeed *= visc;
// Store stuff in particle brain
p.speed = Math.sqrt(p.xSpeed * p.xSpeed + p.ySpeed * p.ySpeed);
p.dist = dist;
// Update position
p.x += 0.1 * p.xSpeed;
p.y += 0.1 * p.ySpeed;
// Get older
p.age++;
// Kill if too old
if (p.age > this.lifespan) {
this.kill(p.name);
this.deathCount++;
}
}
};
App.initDraw = function () {
this.ctx.beginPath();
this.ctx.rect(0, 0, this.width, this.height);
this.ctx.fillStyle = 'black';
this.ctx.fill();
this.ctx.closePath();
};
App.draw = function () {
this.drawnInLastFrame = 0;
if (!this.particles.length) return false;
this.ctx.beginPath();
this.ctx.rect(0, 0, this.width, this.height);
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
this.ctx.fill();
this.ctx.closePath();
for (let i = 0; i < this.particles.length; i++) {
// Draw particle
const p = this.particles[i];
let h, s, l, a;
h = p.hue + this.stepCount / 30;
s = p.sat;
l = p.lum;
a = 1;
const last = this.dataXYtoCanvasXY(p.xLast, p.yLast);
const now = this.dataXYtoCanvasXY(p.x, p.y);
const attracSpot = this.grid[p.attractor.gridSpotIndex];
const attracXY = this.dataXYtoCanvasXY(attracSpot.x, attracSpot.y);
const oldAttracSpot = this.grid[p.attractor.oldIndex];
const oldAttracXY = this.dataXYtoCanvasXY(oldAttracSpot.x, oldAttracSpot.y);
this.ctx.beginPath();
this.ctx.strokeStyle = `hsla(${h}, ${s}%, ${l}%, ${a})`;
this.ctx.fillStyle = `hsla(${h}, ${s}%, ${l}%, ${a})`;
// Particle trail
this.ctx.moveTo(last.x, last.y);
this.ctx.lineTo(now.x, now.y);
this.ctx.lineWidth = 1.5 * this.dataToImageRatio;
this.ctx.stroke();
this.ctx.closePath();
// Attractor positions
this.ctx.beginPath();
this.ctx.lineWidth = 1.5 * this.dataToImageRatio;
this.ctx.moveTo(oldAttracXY.x, oldAttracXY.y);
this.ctx.lineTo(attracXY.x, attracXY.y);
this.ctx.arc(attracXY.x, attracXY.y, 1.5 * this.dataToImageRatio, 0, 2 * Math.PI, false);
this.ctx.strokeStyle = `hsla(${h}, ${s}%, ${l}%, ${a})`;
this.ctx.fillStyle = `hsla(${h}, ${s}%, ${l}%, ${a})`;
this.ctx.stroke();
this.ctx.fill();
this.ctx.closePath();
// UI counter
this.drawnInLastFrame++;
}
};
App.dataXYtoCanvasXY = function (x, y) {
const zoom = 1.6;
const xx = this.xC + x * zoom * this.dataToImageRatio;
const yy = this.yC + y * zoom * this.dataToImageRatio;
return { x: xx, y: yy };
};
App.setup();
App.draw();
const frame = function () {
App.evolve();
requestAnimationFrame(frame);
};
frame();
}, []);
return (
<SpipaWrapper>
<canvas ref={canvasRef}></canvas>
<UI ref={uiRef} className="ui">
<p className="zoom">
<span className="zoom zoomin">+</span>
<span className="zoom zoomout">-</span>
</p>
<p className="zoomlevel">
<span className="percent">100</span> % - (<span className="width"></span>px)(<span className="height"></span>px)
</p>
<p>
Dead: <span className="dead">0</span>
</p>
<p>
Alive: <span className="alive">0</span>
</p>
<p>
Drawn: <span className="drawn">0</span>
</p>
<p>
<span className="fps">0</span> FPS
</p>
<a className="save" href="" download="capture.png">
Save
</a>
</UI>
{children}
</SpipaWrapper>
);
};
export default SpipaBackground;