点击查看html编辑器说明文档

Canvas 陨石特效edit icon

|
|
Fork(复制)
|
|
作者:
穿越者X57

👉 新版编辑器已上线,点击进行体验吧!

BUG反馈
嵌入
设置
下载
HTML
格式化
支持Emmet,输入 p 后按 Tab键试试吧!
<head> ...
展开
</head>
<body>
            
            <!-- 引入 gasp 动画库 -->
<script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-y/gsap/3.9.1/gsap.min.js" type="application/javascript"></script>

<div class="line">
    <canvas class="meteor"></canvas>
</div>
<div class="line">
    <canvas class="meteor"></canvas>
</div>
<article>
</article>
<div class="line line--collider">
    <canvas class="meteor" data-offset="-40"></canvas>
</div>
<div class="line line--collider">
    <canvas class="meteor" data-offset="50"></canvas>
</div>
<svg class="sr-only" viewBox="0 0 30 10" xmlns="http://www.w3.org/2000/svg">
    <defs>
        <filter id="drop">
            <feDropShadow dx="0" dy="0" stdDeviation="16" flood-color="orange" />
        </filter>
    </defs>
</svg>
        
</body>
CSS
格式化
            
            *,
*:after,
*:before {
    box-sizing: border-box;
}

body {
    display: grid;
    place-items: center;
    min-height: 100vh;
    font-family:  "SF Pro Text", "SF Pro Icons", "AOS Icons", "Helvetica Neue", Helvetica, Arial, sans-serif, system-ui;
    background: linear-gradient(50deg, hsl(0 0% 50% / 0.25), transparent), hsl(0 0% 0%);
    overflow: hidden;
}

.meteor {
    aspect-ratio: 1;
    width: 200px;
    filter: url(#drop);
/*    background: radial-gradient(hsl(30 80% 50% / 0.05), transparent 50%);*/
/*    outline: 2px dashed hsl(0 0% 50% / 0.15);*/
    position: absolute;
    left: 50%;
    bottom: 100%;
    translate: -50% 0%;
    animation: scale var(--speed) calc(var(--delay, var(--speed) * -0.5)) infinite linear ;
}

@keyframes scale {
    to { transform: translateY(calc(var(--distance, 100cqh) + (var(--buffer, 100%)))); }
}

.line:not(.line--collider) {
    top: 10%;
    translate: 0 -50%;
}

.line {
    --speed: 6s;
    position: relative;
    height: 150vmax;
    width: 2px;
    background: hsl(0 0% 40% / 0.15);
    rotate: 50deg;
    position: fixed;
    left: 50%;
    container-type: size;
}

.line:nth-of-type(2) {
    --speed: 10s;
    top: 90%;
}

article {
    width: 40vmin;
    aspect-ratio: 4 / 3;
    background: hsl(0 0% 25% / 0.25);
    border: 1px solid hsl(0 0% 40%);
    border-radius: 12px;
    z-index: 2;
    backdrop-filter: blur(4px);
    position: relative;
}

.line--collider {
    --distance: 200cqh;
    --buffer: 100%;
    position: absolute;
    top: unset;
    bottom: calc(50% + ((3 / 4) * 20vmin));
    translate: 0 0;
    transform-origin: 50% 100%;
}

.line:nth-of-type(3) {
    position: absolute;
    top: unset;
    bottom: 50%;
    translate: 0 0;
    transform-origin: 50% 100%;
    left: calc(50% + 20vmin);
}

.line--collider:nth-of-type(3) {
    --speed: 8s;
    --delay: -2s;
}
.line--collider:nth-of-type(4) {
    --speed: 14s;
    --delay: -6s;
}
.line--collider::after {
    content: "";
    position: absolute;
    top: 100%;
    left: 50%;
    height: 100%;
    width: 2px;
    background: hsl(0 0% 40% / 0.15);
    translate: -50% 0;
    z-index: -1;
}

.line--collider .meteor {
    animation-iteration-count: 1;
    animation-fill-mode: forwards;
}

.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border-width: 0;
}
        
JS
格式化
            
            class Meteor {
  constructor(options) {
    const that = this
    this.canvas = options.element
    this.options = options
    this.context = this.canvas.getContext('2d')
    this.canvas.height = this.canvas.offsetHeight * window.devicePixelRatio
    this.canvas.width = this.canvas.offsetWidth * window.devicePixelRatio
    this.meteorWidth = that.options.width
    this.meteorSpeed = gsap.utils.mapRange(4, 12, 6, 12, that.meteorWidth)
    this.meteorLength = gsap.utils.mapRange(4, 12, 50, 80, that.meteorWidth)
    // Create a reusable gradient
    that.gradient = that.context.createLinearGradient(
      that.canvas.width * 0.5,
      that.canvas.height * 0.5,
      that.canvas.width * 0.5,
      0
    )
    that.gradient.addColorStop(0, 'hsl(30, 100%, 100%)')
    that.gradient.addColorStop(0.025, 'hsl(30, 100%, 70%)')
    that.gradient.addColorStop(0.15, 'hsl(30, 100%, 40%)')
    that.gradient.addColorStop(0.55, 'hsl(30, 100%, 20%)')
    that.gradient.addColorStop(1, 'transparent')

    this.particles = that.genParticles(gsap.utils.random(50, 100, 1))
    this.setParticlesMotion()
    gsap.ticker.add(this.draw.bind(that))

    /**
     * If the meteor has a collision, we need to detect that and use it.
     * The trick here will be to detect when the canvas animationend event happens in CSS.
     * Then make a "special" animation sequence where the sparks etc. no when to stop
     * We creat a blowing up effect and then sparks that shoot out. Extra marks here for
     * in front and behind sparks that give the 3D effect
     * */
    if (options.collision) {
      that.exploder = {
        size: 0,
        alpha: 1,
      }
      const explode = () => {
        // When this collision happens, we need to do some GSAP trickery to take the tail down, etc.
        // Big ole timeline of making the explosion
        // Generate some colliders
        if (that.sparks) that.sparks.length = 0
        that.sparks = []
        for (let p = 0; p < gsap.utils.random(10, 50, 1); p++) {
          const spark = {
            destination: {
              x: gsap.utils.random(
                that.canvas.width * 0.5 - that.options.width * 10,
                that.canvas.width * 0.5 + that.options.width * 10,
                1
              ),
              y: gsap.utils.random(
                that.canvas.width * 0.5 - that.options.width * 10,
                that.canvas.width * 0.5 + that.options.width * 10,
                1
              ),
            },
            alpha: 1.5,
            x: that.canvas.width * 0.5,
            y: that.canvas.height * 0.5,
            size: gsap.utils.random(
              that.options.width * 0.2,
              that.options.width,
              1
            ),
          }
          that.sparks.push(spark)
        }
        gsap
          .timeline({
            onStart: function () {
              that.collided = true
            },
            onComplete: function () {
              that.ignite(Math.random() > 0.25)
            },
          })
          .to(that, {
            duration: 0.5,
            meteorLength: 0,
            meteorWidth: 0,
          })
          .to(
            that.exploder,
            {
              size: that.options.width * 20,
              duration: 0.5,
            },
            0
          )
          .to(
            that.sparks,
            {
              x: function (index) {
                return that.sparks[index].destination.x
              },
              y: function (index) {
                return that.sparks[index].destination.y
              },
              size: 0,
              duration: () => gsap.utils.random(0.5, 1.5),
            },
            0
          )
          .to(that.exploder, {
            alpha: 0,
            duration: 0.5,
          }, 0.25)
        // It's all good taking the size down but you also need the explosion of particles
        // These could be like, debris pieces or something at different angles...
        // Use GSAP's Physics2D for that and then have them with different gravity
      }

      // Don't use the CSS animation for this one. Use GSAP's animation hooks.
      // gsap.set(that.canvas, { animation: 'none' })
      that.canvas.addEventListener('animationend', () => {
        console.info('reset animation')
        if (that.imminentCollision) explode()
        else that.ignite(Math.random() > 0.25)
      })

      that.ignite = (ignite) => {
        // If there was a collision, reset all the things
        if (that.collided) {
          that.collided = false
          that.particles = that.genParticles(gsap.utils.random(50, 100, 1))
          that.setParticlesMotion()
          that.exploder.size = 0
          that.exploder.alpha = 1
          that.options.length = gsap.utils.random(50, 80, 1)
          that.options.width = gsap.utils.random(4, 8, 1)
          that.meteorWidth = that.options.width
          that.meteorLength = that.options.length
        }
        const speed = ignite ? gsap.utils.random(4, 8, 0.1) : gsap.utils.random(8, 16, 0.1)
        const delay = ignite ? 0 : gsap.utils.random(-5, 0, 0.1)
        that.imminentCollision = ignite
        that.canvas.style.setProperty('--distance', ignite ? '100cqh' : '200cqh')
        that.canvas.style.setProperty('--buffer', ignite ? '50%' : '100%')
        that.canvas.style.setProperty('--speed', `${speed}s`)
        that.canvas.style.setProperty('--delay', `${delay}s`)
        // Restart the animation
        const anim = that.canvas.getAnimations()[0]
        anim.cancel()
        anim.play()
      }
      that.ignite(false)
      // that.scaleMeteor = (collide) => {
      //   if (that.scale) that.scale.kill()
      //   that.scale = gsap.timeline({
      //     onStart: function() {
      
      //     }
      //   })
      //     .set(that.canvas, { yPercent: 0, y: 0 })
      //     .to(that.canvas, {
      //       ease: 'none',
      //       duration: collide ? gsap.utils.random(5, 10, 1) : gsap.utils.random(10, 20, 1),
      //       y: collide ? '100cqh' : '200cqh',
      //       yPercent: 50,
      //       onComplete: function () {
      //         if (collide) explode()
      //         else that.scaleMeteor(Math.random() > 0.25)
      //       },
      //     })
      //   that.scale.play()
      // }
      // that.scaleMeteor(false)
    }
  }
  /**
   * Generate some particles
   * Only care about sizing
   * */
  genParticles(amount) {
    const that = this
    const particles = []
    for (let p = 0; p < amount; p++) {
      const particle = {
        size:
          gsap.utils.random(1, that.options.width * 1.25, 1) *
          window.devicePixelRatio,
      }
      particles.push(particle)
    }
    return particles
  }
  /**
   * Drawing loop.
   * 1. Draw the head
   * 2. Draw the tail
   * 3. Draw the sparks
   * 4. Draw the colliders
   * */
  draw() {
    const that = this
    that.context.clearRect(0, 0, that.canvas.width, that.canvas.height)
    // Set a blur on the canvas
    that.context.shadowBlur = 10 * window.devicePixelRatio
    that.context.shadowColor = 'hsl(30, 100%, 40%)'

    // 0. Iterate over and render the particles
    for (const particle of that.particles.filter(p => !p.dead)) {
      that.context.beginPath()
      that.context.fillStyle = that.gradient
      that.context.arc(
        particle.x,
        particle.y,
        particle.size / 2,
        0,
        2 * Math.PI
      )
      that.context.fill()
    }

    // 1. Base of the meteor (A semicircle)
    that.context.beginPath()
    const radius = (that.meteorWidth / 2) * window.devicePixelRatio
    that.context.arc(
      that.canvas.width * 0.5,
      that.canvas.height * 0.5 - radius,
      radius,
      0,
      1 * Math.PI
    )
    that.context.fill()
    // 2. The tail of the meteor (A triangle)
    that.context.fillStyle = that.gradient
    that.context.moveTo(
      that.canvas.width * 0.5 - radius,
      that.canvas.height * 0.5 - radius
    )
    that.context.lineTo(
      that.canvas.width * 0.5,
      that.canvas.height * 0.5 - that.meteorLength * window.devicePixelRatio
    )
    that.context.lineTo(
      that.canvas.width * 0.5 + radius,
      that.canvas.height * 0.5 - radius
    )
    that.context.fill()
    // If there's a collision make sure to include collision specific renders
    if (that.collided) {
      // Don't like this but it should work... Create a fill style for the gradient here
      const sparkGradient = that.context.createRadialGradient(that.canvas.width * 0.5, that.canvas.width * 0.5, that.options.width * 0.5, that.canvas.width * 0.5, that.canvas.width * 0.5, that.canvas.width * 0.5);
      const alpha = that.exploder.alpha * 0.25
      sparkGradient.addColorStop(0, `hsl(30 100% 80% / ${alpha})`)
      sparkGradient.addColorStop(0.05, `hsl(30 100% 70% / ${alpha})`)
      sparkGradient.addColorStop(0.2, `hsl(30 100% 40% / ${alpha})`)
      sparkGradient.addColorStop(0.25,`hsl(30 100% 20% / ${alpha})`)
      sparkGradient.addColorStop(1, `hsl(30 100% 20% / 0)`)
      // const sparkGradient = 'hsl(10 80% 100% / 0.1)'
      // 5. If there are collider sparks and we've collided, make sure you use them here...
      for (const spark of that.sparks) {
        that.context.beginPath()
        that.context.fillStyle = sparkGradient
        that.context.arc(spark.x, spark.y, spark.size / 2, 0, 2 * Math.PI)
        that.context.fill()
      }
      // 4. If there's a collision make sure you draw out the exploder...
      that.context.beginPath()
      // Is there a way to apply some alpha to this gradient potentially??
      // Can you draw an offscreen canvas and then render it in perhaps with sizing?

      that.context.fillStyle = sparkGradient
      // You know 360 is 2 * Math.PI so 50 is 2 * Math.PI / 360 * 50
      const offset = ((2 * Math.PI) / 360) * that.options.offset
      that.context.arc(
        that.canvas.width * 0.5,
        that.canvas.height * 0.5,
        that.exploder.size / 2,
        -Math.PI - offset,
        -offset
      )
      that.context.fill()
    }
  }
  /**
   * Sets the sparks in motion
   * */
  setParticlesMotion() {
    const that = this
    for (const particle of that.particles) {
      // Given an angle and distance, you can create an x and y destination
      particle.x = that.canvas.width * 0.5
      particle.y = that.canvas.height * 0.5
      particle.speed = gsap.utils.mapRange(4, 12, 2, 1, that.meteorWidth)
      particle.dead = false
      if (particle.size === 0)
        particle.size =
          gsap.utils.random(1, that.options.width * 1.25, 1) *
          window.devicePixelRatio
      if (particle.tl) particle.tl.kill()
      particle.tl = gsap.timeline().to(particle, {
        x: () =>
          gsap.utils.random(
            that.canvas.width * 0.5 - that.meteorWidth * 2.5,
            that.canvas.width * 0.5 + that.meteorWidth * 2.5,
            1
          ),
        y: () => gsap.utils.random(0, 0, 1),
        size: 0,
        repeat: -1,
        onRepeat: function () {
          if (that.collided) {
            // Kill particles on collision fade until we bring them back
            particle.dead = true
            particle.tl.kill()
          }
        },
        ease: 'power4.out',
        repeatDelay: Math.random(),
        delay: particle.speed * -1,
        duration: particle.speed,
      })
    }
  }
}

// gsap.ticker.fps(24)
const SOARERS = document.querySelectorAll('canvas')
SOARERS.forEach((c) => {
  const collision = c.dataset.offset !== undefined
  new Meteor({
    collision,
    element: c,
    offset: c.dataset.offset,
    width: gsap.utils.random(4, 8, 1),
  })
})

        
预览
控制台
清空