Lightning Animation With JavaScript | Source Code

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)
  }
}

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! 🚀✨

Leave a Reply

Your email address will not be published. Required fields are marked *