console.log("Initializing animation"); const canvas = document.getElementById('mainCanvas'); const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio; const cw = window.innerWidth; const ch = window.innerHeight; canvas.width = cw * dpr; canvas.height = ch * dpr; ctx.scale(dpr, dpr); const center = { x: canvas.width / 2, y: canvas.height /2 }; console.log(center); function random(min, max) { return min + Math.random() * ( max - min ); } const initialOrbCount = 100; const state = { total: 0, dt: 1, orbs: [], }; const options = { speed: 65, scale: 1.0, radius: 300, fadeout: 10, centerLight: true, lightAlpha: 5, perfectRed: false, spiralInwards: false, }; const Orb = function(x, y) { const scale = options.scale; const dx = ( x / scale ) - ( center.x / scale ); const dy = ( y / scale ) - ( center.y / scale ); const initialAngle = Math.atan2(dy, dx); this.angle = initialAngle; this.lastAngle = initialAngle; this.radius = Math.sqrt(dx * dx + dy * dy); this.size = ( this.radius / 300 ) + 1; this.speed = ( random(1, 10) / 300000 ) * ( this.radius ) + 0.015; this.removed = false; }; Orb.prototype.update = function() { const speed = options.speed; this.lastAngle = this.angle; this.angle += this.speed * ( speed / 50 ) * state.dt; if (options.spiralInwards) { if (this.radius === 0) { this.removed = true; } else { this.radius -= 1; } } this.x = this.radius * Math.cos( this.angle ); this.y = this.radius * Math.sin( this.angle ); }; Orb.prototype.render = function(timestamp) { this.update(); const radius = this.radius > 0 ? this.radius : 0; // perfect-red = 0 const offset = timestamp / options.speed; const hueAngle = (this.angle + 90) / (Math.PI / 180) + offset; let style; if (options.perfectRed) { style = `hsl(0, 100%, 50%)`; } else { style = `hsl(${hueAngle}, 100%, 50%)`; } ctx.strokeStyle = style; ctx.lineWidth = this.size; ctx.beginPath(); const increment = 0.001; let start; let end; if (options.speed >= 0) { start = this.lastAngle; end = this.angle + increment; } else { end = this.lastAngle; start = this.angle + increment; } ctx.arc(0, 0, radius, start, end, false); ctx.stroke(); ctx.closePath(); if (options.centerLight) { const alpha = options.lightAlpha / 100; const style = `hsla(${hueAngle}, 100%, 70%, ${alpha}`; ctx.lineWidth = .5; ctx.strokeStyle = style; ctx.beginPath(); ctx.moveTo( 0, 0 ); ctx.lineTo( this.x, this.y ); ctx.stroke(); } }; const createOrb = (x, y) => { console.log(`Creating orb ${x}, ${y}`); state.orbs.push(new Orb(x, y)); }; const setup = () => { const radius = options.radius; for (let n = initialOrbCount; n >= 0; n--) { const x = random(canvas.width / 2 - radius, canvas.width / 2 + radius); const y = random(canvas.height / 2 - radius, canvas.height / 2 + radius); createOrb(x, y); } } const clear = () => { const alpha = options.fadeout / 100; const style = `rgba(0, 0, 0, ${alpha})`; ctx.globalCompositeOperation = 'destination-out'; ctx.fillStyle = style; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.globalCompositeOperation = 'lighter'; } const controls = new dat.GUI( { autoPlace: false } ) const container = document.querySelector("#controlPanel"); controls.add(state, "total" ).name("Total Orbs").listen(); controls.add(options, "speed" ).min(-300).max(300).step(10).name("Speed"); controls.add(options, "scale" ).min(0.5).max(5).step(0.1).name("Scale"); controls.add(options, "fadeout").min(0).max(100).step(1).name("Fadeout (/100)"); controls.add(options, "centerLight" ).name("Toggle Light"); controls.add(options, "lightAlpha" ).min(0).max(100).step(1).name("Light Alpha"); controls.add(options, "perfectRed").name("Perfect Red"); controls.add(options, "spiralInwards").name("Spiral Inwwards"); container.appendChild(controls.domElement); const loop = (timestamp) => { window.requestAnimationFrame(loop); clear(); const scale = options.scale; ctx.save(); ctx.translate(center.x, center.y); ctx.scale(scale, scale); state.orbs.forEach((orb) => { orb.render(timestamp); }); state.orbs = state.orbs.filter((orb) => !orb.removed); state.total = state.orbs.length; ctx.restore(); }; setup(); loop();