Skip to content

Instantly share code, notes, and snippets.

@Sheraff
Last active August 12, 2022 06:23
Show Gist options
  • Select an option

  • Save Sheraff/69698952120d0e0f117b6895ebeb469f to your computer and use it in GitHub Desktop.

Select an option

Save Sheraff/69698952120d0e0f117b6895ebeb469f to your computer and use it in GitHub Desktop.
particles focus effect
{
"scripts": [],
"styles": [],
"themePreview": false
}
<ul>
<li><button><span>Artists</span></button></li>
<li><button><span>Albums</span></button></li>
<li><button><span>Genres</span></button></li>
<li><button><span>Podcasts & Shows</span></button></li>
</ul>
const buttons = document.querySelectorAll('button')
const MIN_DELAY = 80
const MAX_DELAY = 300
buttons.forEach(button => {
const canvas = document.createElement('canvas')
const {width, height} = button.getBoundingClientRect()
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
ctx.filter = 'blur(1px)'
button.appendChild(canvas)
const particles = []
let looping = false
function loopCanvasAnimate(ctx, particles) {
looping = true
requestAnimationFrame((timestamp) => {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
particles.forEach((particle, i) => {
if (particle instanceof Particle) {
particle.update(timestamp)
if (particle.opacity === 0) {
particles.splice(i, 1)
} else {
particle.draw(ctx)
}
} else if (particle instanceof Placeholder) {
particles.push(new Particle(particle.x, particle.y, timestamp))
particles.splice(i, 1)
}
})
if (particles.length > 0) {
loopCanvasAnimate(ctx, particles)
} else {
looping = false
}
})
}
let lastParticle = -Infinity
let idleId
function loopMakeParticles() {
const now = performance.now()
if (now - lastParticle > MIN_DELAY) {
particles.push(new Placeholder(x, y))
lastParticle = now
if (!looping) {
loopCanvasAnimate(ctx, particles)
}
}
idleId = requestIdleCallback(loopMakeParticles, {timeout: MAX_DELAY})
}
let x, y
button.addEventListener('pointermove', (e) => {
x = e.offsetX
y = e.offsetY
})
button.addEventListener('pointerenter', loopMakeParticles)
button.addEventListener('pointerleave', () => cancelIdleCallback(idleId))
button.addEventListener('pointerdown', () => {
particles.forEach((particle) => {
oscillatorToLinear(particle, x, y)
})
lastParticle = performance.now() + 500
})
})
class Placeholder {
constructor(x, y) {
this.x = x
this.y = y
}
}
class Particle {
constructor(x, y, time) {
this.start = time
this.last = time
this.type = Oscillator
this.pointX = new Oscillator(x + Math.random() * 30 - 15)
this.pointY = new Oscillator(y + Math.random() * 30 - 15)
this.decay = 0.005
this.opacity = 1
this.radius = Math.random() * .75 + .75
}
update(time) {
const total = time - this.start
const delta = (time - this.last) / 10
this.x = this.pointX.update(delta, total)
this.y = this.pointY.update(delta, total)
this.opacity -= this.decay * delta
if (this.opacity < 0) {
this.opacity = 0
}
this.last = time
}
draw(ctx) {
ctx.beginPath()
ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)
ctx.globalAlpha = this.opacity
ctx.fillStyle = `lime`
ctx.fill()
}
}
class Oscillator {
constructor(value) {
this.math = Math.random() < .5 ? Math.sin : Math.cos
this.period = Math.random() * 300 + 200
this.amplitude = Math.random() * 4 + 4
this.anchor = value
}
update(_, time) {
return this.anchor + this.math(time / this.period) * this.amplitude
}
}
class Linear {
constructor(value, speed) {
this.value = value
this.speed = speed
}
update(delta) {
return this.value += delta * this.speed
}
}
function oscillatorToLinear(particle, x, y) {
if (particle.type !== Oscillator) {
return
}
particle.type = Linear
const dx = particle.x - x
const dy = particle.y - y
const total = Math.abs(dx) + Math.abs(dy)
const speed = Math.max(.6, (Math.random() * 7 + 10) / total)
particle.start = particle.last
particle.decay = 0.01
particle.pointX = new Linear(particle.x, speed * dx / total)
particle.pointY = new Linear(particle.y, speed * dy / total)
particle.opacity = 1
}
body {
background-color: #1f1f1f;
color: white;
font-size: 14px;
}
ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
gap: 12px;
justify-content: center;
padding: 1em;
}
button {
$vertical-spacing: 12px;
position: relative;
z-index: 0;
padding: #{$vertical-spacing} 24px;
white-space: nowrap;
line-height: 1;
color: inherit;
font-size: inherit;
border-radius: calc(1em + #{$vertical-spacing});
border: 1px solid white;
overflow: hidden;
cursor: pointer;
outline: none;
background-color: rgba(0, 0, 0, 0.25);
}
canvas {
position: absolute;
z-index: 0;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
span {
position: relative;
z-index: 1;
pointer-events: none;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment