<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<canvas id="canvasId" height="500" width="500"></canvas>
</body>
</html>
<script>
const c = document.getElementById('canvasId')
const ctx = c.getContext('2d')
const s = c.width = c.height = 500
const opts = {
particles: 200,
particleBaseSize: 4,
particleAddedSize: 1,
particleMaxSize: 5,
particleBaseLight: 5,
particleAddedLight: 30,
particleBaseBaseAngSpeed: 0.001,
particleAddedBaseAngSpeed: 0.001,
particleBaseVariedAngSpeed: 0.0005,
particleAddedVariedAngSpeed: 0.0005,
sourceBaseSize: 3,
sourceAddedSize: 3,
sourceBaseAngSpeed: -0.01,
sourceVariedAngSpeed: 0.005,
sourceBaseDist: 130,
sourceVariedDist: 50,
particleTemplateColor: 'hsla(hue,80%,light%,alp)',
repaintColor: 'rgba(24,24,24,.1)',
enableTrails: false
}
const util = {
square: x => x * x,
tau: 6.2831853071795864769252867665590057683943
}
const particles = []
const source = new Source()
let tick = 0
function Particle () {
this.dist = (Math.sqrt(Math.random())) * s / 2
this.rad = Math.random() * util.tau
this.baseAngSpeed = opts.particleBaseBaseAngSpeed + opts.particleAddedBaseAngSpeed * Math.random()
this.variedAngSpeed = opts.particleBaseVariedAngSpeed + opts.particleAddedVariedAngSpeed * Math.random()
this.size = opts.particleBaseSize + opts.particleAddedSize * Math.random()
}
Particle.prototype.step = function () {
const angSpeed = this.baseAngSpeed + this.variedAngSpeed * Math.sin(this.rad * 7 + tick / 100)
this.rad += angSpeed
const x = this.dist * Math.cos(this.rad)
const y = this.dist * Math.sin(this.rad)
const squareDist = util.square(x - source.x) + util.square(y - source.y)
const sizeProp = s ** (1 / 2) / squareDist ** (1 / 2)
const color = opts.particleTemplateColor
.replace('hue', this.rad / util.tau * 360 + tick)
.replace('light', opts.particleBaseLight + sizeProp * opts.particleAddedLight)
.replace('alp', 0.8)
ctx.fillStyle = color
ctx.beginPath()
ctx.arc(x, y, Math.min(this.size * sizeProp, opts.particleMaxSize), 0, util.tau)
ctx.fill()
}
function Source () {
this.x = 0
this.y = 0
this.rad = Math.random() * util.tau
}
Source.prototype.step = function () {
if (!this.mouseControlled) {
const angSpeed = opts.sourceBaseAngSpeed + Math.sin(this.rad * 6 + tick / 100) * opts.sourceVariedAngSpeed
this.rad += angSpeed
const dist = opts.sourceBaseDist + Math.sin(this.rad * 5 + tick / 100) * opts.sourceVariedDist
this.x = dist * Math.cos(this.rad)
this.y = dist * Math.sin(this.rad)
}
ctx.fillStyle = 'white'
ctx.beginPath()
ctx.arc(this.x, this.y, 1, 0, util.tau)
ctx.fill()
}
function anim () {
window.requestAnimationFrame(anim)
++tick
if (!opts.enableTrails) { ctx.globalCompositeOperation = 'source-over' }
ctx.fillStyle = opts.repaintColor
ctx.fillRect(0, 0, s, s)
ctx.globalCompositeOperation = 'lighter'
if (particles.length < opts.particles) { particles.push(new Particle()) }
ctx.translate(s / 2, s / 2)
source.step()
particles.map(particle => particle.step())
ctx.translate(-s / 2, -s / 2)
}
ctx.fillStyle = '#222'
ctx.fillRect(0, 0, s, s)
anim()
c.addEventListener('mousemove', e => {
const bbox = c.getBoundingClientRect()
source.x = e.clientX - bbox.left - s / 2
source.y = e.clientY - bbox.top - s / 2
source.mouseControlled = true
})
c.addEventListener('mouseleave', e => {
const bbox = c.getBoundingClientRect()
source.x = e.clientX - bbox.left - s / 2
source.y = e.clientY - bbox.top - s / 2
source.rad = Math.atan(source.y / source.x)
if (source.x < 0) { source.rad += Math.PI }
source.mouseControlled = false
})
</script>