Introduction:
Hey there, fellow developers! Today, I’m excited to share something that’ll add a spark to your web projects – literally! We’re diving into creating an impressive lightning animation effect using JavaScript that responds to user interaction. This tutorial will show you how to implement dynamic light strings that react to mouse movements, creating an engaging and modern visual experience for your website visitors.
HTML Structure
<canvas></canvas>
<!-- glowing image base64 -->
<img class="asset-img" id="light-img" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAMAAADzN3VRAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAACnVBMVEUAAAD40ED40D740Dv40En40Dr40FH40Df40D/40D340Dn40Dz40Dj40AD40Ff40Db40DT40EH40EL40ED40ED40Dv40D/4zz/4zz74zz/4zz/40D74zz/4zz/4zz/40UH40kD40UL40kH4zz/4zz340D34zz740kH41UT410f410j410j410f40D/40D/4zz/40ED40EH400T410f42Er4103410/411D41UT40kH40D/4z0D4zz/41UX410j42E7411L42Ff42Vv42Vz42En41kb40UH4zz/40Dz40ED41UT42Ej4107411b42l743WT432r4323432r410/42En4zz340Dn400H410f42mD432v44nf45IP45Yn45IT44nj432r411b400L40D/4zz/41UT42Er411P432v44oD46Zv47q/48Lj46Zr432n40UH410f42Ff44nf48r/499T4997499T48r744nf43WP40D741EH42Vv4+O34+PP4+O347q745IL432j40UD432345Yj48LX49934+PP4+Pj4993477T45Ib432z411D400D40D/40UH432n45IP47a7432j42Vv410/400H40Dr4zz340UD410b410346Zj48r343WP42Ff410f40D/41kT42Er42l744nv46Zf42l3411L410r41kX40ED410f42E7411b421/45IL45Ib432n421740UH40D340Dz4zz741EP410j410743WT432f432v43mj43GT42Ej41kT4zz740ED41EX411P42Ff42Fv411f410j41kX400P41034107411D42E340D740UH410j41kb41ET400H40D/40ED4z0D4zz74zz340D////+WdN8lAAAA3nRSTlMAAAAAAAAAAAAAAAAAAAAAAAAAAgMCBAcJDAkDCA0SFRsbGxMDBQscJSwwMS0SDAUFFyExQExUVyIYDQULJzlOZHeDhDonFwoDASE5VXGNpbO4tFM5BwEcMJa3zdjb2Mu2cBwDESM+Y7bU4ebo4bUUK3bL6vDy7+rKogIZgPb49ebXsBu02+jx9/jw6Nu0VRsBGbDX5q9/UhkCCRUqS+HqoHMqESI9i9Thi2E9IhEvTG+V1tq0kxsDAQchN1Ggr7SvoDghChUjYXJ/cjgnIUlSVEkFGi8qIhsFFAcICAaJi/ApAAAAAWJLR0Te6W7imwAAAAd0SU1FB+YMHBEUINvSacMAAAIKSURBVCjPY2AgBBgZmZiFRYQZgQBVnEVUTFxCUlJSSlxMlAVJjplVWkZWTl5BUUleWVZGhZkZYZKqmpy6hqaWtpaOhrqunj7MREZGA1lDI2MTUzNzM1MTYwtLK2uIFCOjiI2hrZ29g6OTs5Ojg72Lq5u7Bxs7SMZTXNfL28fXzz8gMMjfzzc4xEI3NIwVrEUyPMLeNzIqOiY2Lj4hMtE+Ikk2mYkRKCOekpqW7peRmZWdk52bmeeXnqYhJ84BkpHKLzAt9C/KKi4pLSvPragsNNXMl/JkZGCqUqvWMqsJiMkuqa2rL2tobKox06qWTOZg4EiWbNY2d25pbWvv6Ozo6u7pde7T6pf04GTgmGAzEahn0uSGsvqO+rIpjVOnTdeaoTaTmYGLfdbsOXMLKyvmzS/rKps/r2jBwrmL5KWAfmViEl+8ZGn6srzlK6Z0T1mxPG/lqtVr1oLdxp0sqbRu/YaNeUWbNm8q2rJ1w/p12yS38wDDh3PHTrldu/dsWLZ33/4DB5dt2HPosO7OMFBwM/KKHjl6bPf64yemOZ88dXz96TPyR6T5+EFByi6ganP07LrV585fmH5x6bqzs21UmZhg0XDJXe7ykjnrtK5cvXZdzv0GMycs6jiZpXdKLs532+Z2M0VSXAQ5KTAKeorcuu1uc+TO3e07hNDSCCMjNyczKz8nN8FkxgAAgH2hXtQzzA4AAAAldEVYdGRhdGU6Y3JlYXRlADIwMjItMTItMjhUMTc6MjA6MzIrMDA6MDA1MNVoAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIyLTEyLTI4VDE3OjIwOjMyKzAwOjAwRG1t1AAAAABJRU5ErkJggg==">
Key Features
- When you implement this animation, you’ll see these awesome features in action:
- Full mobile support with touch interactions
- Light strings that smoothly follow your mouse or touch input
- Natural physics-based movement with realistic stretching and bouncing
- Beautiful glowing effects at the end of each string
- Smooth performance across all modern browsers
CSS
html, body, canvas {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
background: #191919;
}
.asset-img {
display: none;
}
JS
class Mouse {
constructor(canvas) {
this.pos = new Vector(-1000, -1000)
this.radius = 40
canvas.onmousemove = e => this.pos.setXY(e.clientX, e.clientY)
canvas.ontouchmove = e => this.pos.setXY(e.touches[0].clientX, e.touches[0].clientY)
canvas.ontouchcancel = () => this.pos.setXY(-1000, -1000)
canvas.ontouchend = () => this.pos.setXY(-1000, -1000)
}
}
class Dot {
constructor(x, y) {
this.pos = new Vector(x, y)
this.oldPos = new Vector(x, y)
this.friction = 0.97
this.gravity = new Vector(0, 0.6)
this.mass = 1
this.pinned = false
this.lightImg = document.querySelector('#light-img')
this.lightSize = 15
}
update(mouse) {
if (this.pinned) return
let vel = Vector.sub(this.pos, this.oldPos)
this.oldPos.setXY(this.pos.x, this.pos.y)
vel.mult(this.friction)
vel.add(this.gravity)
let { x: dx, y: dy } = Vector.sub(mouse.pos, this.pos)
const dist = Math.sqrt(dx * dx + dy * dy)
const direction = new Vector(dx / dist, dy / dist)
const force = Math.max((mouse.radius - dist) / mouse.radius, 0)
if (force > 0.6) this.pos.setXY(mouse.pos.x, mouse.pos.y)
else {
this.pos.add(vel)
this.pos.add(direction.mult(force))
}
}
drawLight(ctx) {
ctx.drawImage(
this.lightImg,
this.pos.x - this.lightSize / 2, this.pos.y - this.lightSize / 2, this.lightSize, this.lightSize
)
}
draw(ctx) {
ctx.fillStyle = '#aaa'
ctx.fillRect(this.pos.x - this.mass, this.pos.y - this.mass, this.mass * 2, this.mass * 2)
}
}
class Stick {
constructor(p1, p2) {
this.startPoint = p1
this.endPoint = p2
this.length = this.startPoint.pos.dist(this.endPoint.pos)
this.tension = 0.3
}
update() {
const dx = this.endPoint.pos.x - this.startPoint.pos.x
const dy = this.endPoint.pos.y - this.startPoint.pos.y
const dist = Math.sqrt(dx * dx + dy * dy)
const diff = (dist - this.length) / dist
const offsetX = diff * dx * this.tension
const offsetY = diff * dy * this.tension
const m = this.startPoint.mass + this.endPoint.mass
const m1 = this.endPoint.mass / m
const m2 = this.startPoint.mass / m
if (!this.startPoint.pinned) {
this.startPoint.pos.x += offsetX * m1
this.startPoint.pos.y += offsetY * m1
}
if (!this.endPoint.pinned) {
this.endPoint.pos.x -= offsetX * m2
this.endPoint.pos.y -= offsetY * m2
}
}
draw(ctx) {
ctx.beginPath()
ctx.strokeStyle = '#999'
ctx.moveTo(this.startPoint.pos.x, this.startPoint.pos.y)
ctx.lineTo(this.endPoint.pos.x, this.endPoint.pos.y)
ctx.stroke()
ctx.closePath()
}
}
class Rope {
constructor(config) {
this.x = config.x
this.y = config.y
this.segments = config.segments || 10
this.gap = config.gap || 15
this.color = config.color || 'gray'
this.dots = []
this.sticks = []
this.iterations = 10
this.create()
}
pin(index) {
this.dots[index].pinned = true
}
create() {
for (let i = 0; i < this.segments; i++) {
this.dots.push(new Dot(this.x, this.y + i * this.gap))
}
for (let i = 0; i < this.segments - 1; i++) {
this.sticks.push(new Stick(this.dots[i], this.dots[i + 1]))
}
}
update(mouse) {
this.dots.forEach(dot => {
dot.update(mouse)
})
for (let i = 0; i < this.iterations; i++) {
this.sticks.forEach(stick => {
stick.update()
})
}
}
draw(ctx) {
this.dots.forEach(dot => {
dot.draw(ctx)
})
this.sticks.forEach(stick => {
stick.draw(ctx)
})
this.dots[this.dots.length - 1].drawLight(ctx)
}
}
class App {
static width = innerWidth
static height = innerHeight
static dpr = devicePixelRatio > 1 ? 2 : 1
static interval = 1000 / 60
constructor() {
this.canvas = document.querySelector('canvas')
this.ctx = this.canvas.getContext('2d')
this.mouse = new Mouse(this.canvas)
this.resize()
window.addEventListener('resize', this.resize.bind(this))
this.createRopes()
}
createRopes() {
this.ropes = []
const TOTAL = App.width * 0.06
for (let i = 0; i < TOTAL + 1; i++) {
const x = randomNumBetween(App.width * 0.3, App.width * 0.7)
const y = 0
const gap = randomNumBetween(App.height * 0.05, App.height * 0.08)
const segments = 10
const rope = new Rope({ x, y, gap, segments })
rope.pin(0)
this.ropes.push(rope)
}
}
resize() {
App.width = innerWidth
App.height = innerHeight
this.canvas.style.width = '100%'
this.canvas.style.height = '100%'
this.canvas.width = App.width * App.dpr
this.canvas.height = App.height * App.dpr
this.ctx.scale(App.dpr, App.dpr)
this.createRopes()
}
render() {
let now, delta
let then = Date.now()
const frame = () => {
requestAnimationFrame(frame)
now = Date.now()
delta = now - then
if (delta < App.interval) return
then = now - (delta % App.interval)
this.ctx.clearRect(0, 0, App.width, App.height)
// draw here
this.ropes.forEach(rope => {
rope.update(this.mouse)
rope.draw(this.ctx)
})
}
requestAnimationFrame(frame)
}
}
function randomNumBetween(min, max) {
return Math.random() * (max - min) + min
}
window.addEventListener('load', () => {
const app = new App()
app.render()
})
export default class Vector {
constructor(x, y) {
this.x = x || 0
this.y = y || 0
}
static add(v1, v2) {
return new Vector(v1.x + v2.x, v1.y + v2.y)
}
static sub(v1, v2) {
return new Vector(v1.x - v2.x, v1.y - v2.y)
}
add(x, y) {
if (arguments.length === 1) {
this.x += x.x
this.y += x.y
} else if (arguments.length === 2) {
this.x += x
this.y += y
}
return this
}
sub(x, y) {
if (arguments.length === 1) {
this.x -= x.x
this.y -= x.y
} else if (arguments.length === 2) {
this.x -= x
this.y -= y
}
return this
}
mult(v) {
if (typeof v === 'number') {
this.x *= v
this.y *= v
} else {
this.x *= v.x
this.y *= v.y
}
return this
}
setXY(x, y) {
this.x = x
this.y = y
return this
}
dist(v) {
const dx = this.x - v.x
const dy = this.y - v.y
return Math.sqrt(dx * dx + dy * dy)
}
}
![](https://mzdev.online/wp-content/uploads/2024/12/image-1.png)
Customization Options
The really cool thing about this animation is how customizable it is. You can easily adjust things like:
- The number of light strings
- How long each string is
- How strongly they react to movement
- The intensity of the glow effect
- The physics properties like gravity and bounce
Conclusion
Happy coding, folks! Keep creating amazing things, and let’s make the web a more beautiful and interactive place together! 🚀✨