UNPKG

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
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;