Skip to content

烟花

鼠标点击生成烟花
点击运行
<template>
  <div id="fireWork-box">
    <div>鼠标点击生成烟花</div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <canvas v-if="isRunning" id="fireWork" class="stage"></canvas>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, ref, onUnmounted, nextTick } from 'vue'

const requestID = ref<any>()
const isRunning = ref(false)

const PI2 = Math.PI * 2
const random = (min, max) => Math.random() * (max - min + 1) + min | 0
const timestamp = () => new Date().getTime()

let box
let scene

class Scene {
  fireworks: any[] = []
  counter = 0
  width = 0
  height = 0
  center = 0
  spawnA = 0
  spawnB = 0
  spawnC = 0
  spawnD = 0
  ctx: any = null

  constructor(el) {
    const canvas = document.getElementById(el) as any
    this.ctx = canvas.getContext('2d')
    const width = Number(window.getComputedStyle(canvas).width.split('px')[0])
    const height = Number(window.getComputedStyle(canvas).height.split('px')[0])
    canvas.width = width
    canvas.height = height

    this.width = width
    this.height = height
    this.center = this.width / 2
    this.spawnA = this.center - this.center / 4 | 0
    this.spawnB = this.center + this.center / 4 | 0
    this.spawnC = this.height * 0.1
    this.spawnD = this.height * 0.5
  }

  onClick(e) {
    const x = e.offsetX
    const y = e.offsetY
    
    const count = random(3, 5) // 生成多个烟花

    for (let i = 0; i< count; i++) {
      const obj = new FireWork(this.fireworks, this.ctx, random(this.spawnA, this.spawnB), this.height, x, y, random(0, 260), random(30, 110))
      this.fireworks.push(obj)
      this.counter = -1
    }
  }

  update(delta) {
    this.ctx.globalCompositeOperation = 'hard-light'
    this.ctx.fillStyle = `rgba(20,20,20,${ 7 * delta })`
    this.ctx.fillRect(0, 0, this.width, this.height)
    this.ctx.globalCompositeOperation = 'lighter'

    for (let firework of this.fireworks) firework.update(delta)

    this.counter += delta * 3

    if (this.counter >= 1) {
      this.fireworks.push(
        new FireWork(
          this.fireworks,
          this.ctx,
          random(this.spawnA, this.spawnB),
          this.height,
          random(0, this.width),
          random(this.spawnC, this.spawnD),
          random(0, 360),
          random(30, 110)
        )
      )
      this.counter = 0
    }

    if (this.fireworks.length > 1000) this.fireworks = this.fireworks.filter(firework => !firework.dead)
  }
}

class FireWork {
  dead= false
  madeChildren = false
  x = 0
  y = 0
  targetX = 0
  targetY = 0
  shade = 0
  offsprings = 0
  history: any[] = []
  fireworks: any[] = []
  ctx:any = null

  constructor(fireworks, ctx, x, y, targetX, targetY, shade, offsprings) {
    this.fireworks = fireworks
    this.ctx = ctx

    this.dead = false
    this.offsprings = offsprings

    this.x = x
    this.y = y
    this.targetX = targetX
    this.targetY = targetY

    this.shade = shade
    this.history = []
  }

  update(delta) {
    if (this.dead) return

    const xDiff = this.targetX - this.x
    const yDiff = this.targetY - this.y
    if (Math.abs(xDiff) > 3 || Math.abs(yDiff) > 3) {
      this.x += xDiff * 2 * delta
      this.y += yDiff * 2 * delta

      this.history.push({
        x: this.x,
        y: this.y
      })

      if (this.history.length > 20) {
        this.history.shift()
      }

    } else {
      if (this.offsprings && !this.madeChildren) {
        // 生成子烟花
        const babies = this.offsprings / 2
        for (let i = 0; i < babies; i++) {
          const targetX = this.x + this.offsprings * Math.cos(PI2 * i / babies) | 0
          const targetY = this.y + this.offsprings * Math.sin(PI2 * i / babies) | 0
          this.fireworks.push(new FireWork(this.fireworks, this.ctx, this.x, this.y, targetX, targetY, this.shade, 0))
        }
      }
      this.madeChildren = true
      this.history.shift()
    }

    if (!this.history.length) {
      this.dead = true
    } else if (this.offsprings) {
      for (let i = 0; this.history.length > i; i++) {
        const point = this.history[i]
        this.ctx.beginPath()
        this.ctx.fillStyle = 'hsl(' + this.shade + ',100%,' + i + '%)'
        this.ctx.arc(point.x, point.y, 1, 0, PI2, false)
        this.ctx.fill()
      }
    } else {
      this.ctx.beginPath()
      this.ctx.fillStyle = 'hsl(' + this.shade + ',100%,50%)'
      this.ctx.arc(this.x, this.y, 1, 0, PI2, false)
      this.ctx.fill()
    }
  }
}

const onAddEvent = (evt) => {
  scene.onClick(evt)
}

const onRunning = async () => {
  await nextTick()
  box =  document.getElementById('fireWork-box') as any
  let then = timestamp()
  scene = new Scene('fireWork')
  box.addEventListener('click', onAddEvent)

  function loop() {
    const now = timestamp()
    const delta = now - then
    then = now

    // 不加这个if,可能会导致烟花回流
    if (delta < 50) {
      scene.update(delta / 1000)
    }

    requestID.value = window.requestAnimationFrame(loop)
  }
  loop()
}

const onTrigger = async () => {
  if (!isRunning.value) {
    isRunning.value = true
    await nextTick()
    onRunning()
  } else {
    isRunning.value = false
    destroy()
  }
}

const destroy = () => {
  box?.removeEventListener('click', onAddEvent)
  cancelAnimationFrame(requestID.value)
  scene = null
}


onMounted(async() => {
  await nextTick()
})


onUnmounted(() => {
  destroy()
})
</script>

傅里叶轨迹

点击运行
<template>
  <div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <canvas v-if="isRunning" id="fourier" class="stage"></canvas>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, onUnmounted, ref, nextTick } from 'vue'

const controls = ref(4)
const requestID = ref<any>()
const isRunning = ref(false)
let canvas
let ctx
let running
let draw

const onRunning = async () => {
  await nextTick()
  canvas = document.getElementById('fourier')
  ctx = canvas.getContext('2d')

  canvas?.addEventListener('click', onAddEvent)

  const width = Number(window.getComputedStyle(canvas).width.split('px')[0])
  const height = Number(window.getComputedStyle(canvas).height.split('px')[0])
  canvas.width = width
  canvas.height = height
  
  const arr: any = []
  const colorArr = ['#00bcd4', '#dd5866', '#335678', '#28a745']

  let time = 0

  const createDisc = (r, color, x?, y?) => {
    ctx.beginPath()
    ctx.arc(x || 0, y || 0, r, 0, Math.PI * 2, true)
    ctx.strokeStyle = color || '#00bcd4'
    ctx.stroke()
  }

  const createLine = (d, rotate) => {
    ctx.beginPath()
    ctx.moveTo(0, 0)
    ctx.lineTo(d, 0)
    ctx.stroke()
  }

  draw = () => {
    time -= Math.PI / 180
    // 设置canvas大小
    ctx.clearRect(0, 0, canvas.width, canvas.height)
    // 绘制左侧的圆
    ctx.save() // 将第一个状态推入到栈中
    ctx.translate(150, 150)
    let lastR = 50;
    for (let i = 0; i < controls.value; i++) {
      if (i === 0) {
        ctx.rotate(time)
      } else {
        ctx.translate(50 - (i - 1) * 15, 0)
        ctx.rotate(time * 2)
        lastR = 50 - i * 15
      }
      createDisc(50 - i * 15, colorArr[i])
      createLine(50 - i * 15, 2 * time)
    }
    // 绘制圆上的线
    ctx.translate(lastR, 0)
    ctx.rotate(-(controls.value * 2 - 1) * time)
    ctx.beginPath()
    ctx.moveTo(0, 0)
    let x = 150
    for (let i = 0; i < controls.value; i++) {
      x -= Math.cos((i * 2 + 1) * time) * (50 - i * 15)
    }
    ctx.lineTo(x, 0)
    ctx.stroke()
    // 绘制箭头
    ctx.translate(x, 0)
    ctx.save() // 将第二个状态推入到栈中
    ctx.rotate(-2.5)
    ctx.lineTo(5, 0)
    ctx.stroke()
    ctx.restore() // 取出堆栈 (第二个状态)
    ctx.save() // 将第三个状态推入到栈中
    ctx.moveTo(0, 0)
    ctx.rotate(2.5)
    ctx.lineTo(5, 0)
    ctx.stroke()
    ctx.restore() // 取出堆栈 (第三个状态)
    // 记录频域
    let arrY = 150
    for (let i = 0; i < controls.value; i++) {
      arrY += Math.sin((i * 2 + 1) * time) * (50 - i * 15)
    }
    arr.unshift(arrY)
    if (arr.length > 500) arr.pop()

    ctx.restore() // 取出堆栈 (第一个状态)
    ctx.strokeStyle = 'red'
    ctx.beginPath()
    ctx.moveTo(300, arrY)
    let arrX = 0;
    arr.forEach(v => {
      ctx.lineTo(300 + arrX++, v)
    })
    ctx.stroke()

    requestID.value = window.requestAnimationFrame(draw)
  }

  draw()
}

const onTrigger = async () => {
  if (!isRunning.value) {
    isRunning.value = true
    await nextTick()
    onRunning()
  } else {
    isRunning.value = false
    destroy()
  }
}

const onAddEvent = () => {
  if (isRunning.value) {
    running = !running
    running ? cancelAnimationFrame(requestID.value) : requestID.value = requestAnimationFrame(draw)
  }
}

const destroy = () => {
  cancelAnimationFrame(requestID.value)
  canvas?.removeEventListener('click', onAddEvent)
  if (draw) draw = null
}

onMounted(async() => {
  await nextTick()
})

onUnmounted(() => {
  destroy()
})
</script>

太阳系

点击运行
<template>
  <div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <canvas v-if="isRunning" id="solarSystem" class="stage"></canvas>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, onUnmounted, ref, nextTick } from 'vue'

const requestID = ref<any>()
const isRunning = ref(false)

const onRunning = async () => {
  const canvas: any = document.getElementById('solarSystem')
  const ctx = canvas.getContext('2d')

  const wrapDomStyle = getComputedStyle(canvas)
  const width = parseInt(wrapDomStyle.width, 10)
  const height = parseInt(wrapDomStyle.height, 10)

  // 需要设置canvas的width和height
  canvas.width = width
  canvas.height = height

  const drawLine = () => {
    for (let i = 0; i < 8; i++) {
      ctx.beginPath()
      ctx.arc(width / 2, height / 2, (i + 1) * 20, 0, 360, false)
      ctx.closePath()
      ctx.strokeStyle = '#00f77c'
      ctx.stroke()
    }
  }

  class Star {
    ctx
    x
    y
    radius
    cycle
    sColor
    eColor
    color
    time

    constructor(ctx, x, y, radius, cycle, sColor, eColor) {
      this.ctx = ctx
      this.x = x
      this.y = y
      this.radius = radius
      this.cycle = cycle
      this.sColor = sColor
      this.eColor = eColor

      this.color = null
      //设置一个计时器
      this.time = 0
    }

    draw() {
      this.ctx.save() // 保存之前的内容
      this.ctx.translate(width / 2, height / 2) // 重置0,0坐标
      this.ctx.rotate(this.time * (360 / this.cycle) * Math.PI / 180) //旋转角度   
      
      // 画星球
      this.ctx.beginPath()
      this.ctx.arc(this.x, this.y, this.radius, 0, 360, false)
      this.ctx.closePath()
      // //设置星球的填充颜色
      this.color = this.ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.radius)
      this.color.addColorStop(0, this.sColor)
      this.color.addColorStop(1, this.eColor)
      this.ctx.fillStyle = this.color
      this.ctx.fill()
      //恢复之前画布的内容
      this.ctx.restore()
      this.time += 1
    }
  }

  const sun = new Star(ctx, 0, 0, 10, 0, '#FFFF00', '#FF9900')
  const mercury = new Star(ctx, 0, -20, 5, 87.70, '#A69697', '#5C3E40')
  const venus = new Star(ctx, 0, -40, 5, 224.701, '#C4BBAC', '#1F1315')
  const earth = new Star(ctx, 0, -60, 5, 365.2422, '#78B1E8', '#050C12')
  const mars = new Star(ctx, 0, -80, 5, 686.98, '#CEC9B6', '#76422D')
  const jupiter = new Star(ctx, 0, -100, 5, 4332.589, '#C0A48E', '#322222')
  const saturn = new Star(ctx, 0, -120, 5, 10759.5, '#F7F9E3', '#5C4533')
  const uranus = new Star(ctx, 0, -140, 5, 30799.095, '#A7E1E5', '#19243A')
  const neptune = new Star(ctx, 0, -160, 5, 60152, '#0661B2', '#1E3B73')

  const runAnimate = () => {
    ctx.clearRect(0, 0, width, height)
    drawLine()
    sun.draw()
    mercury.draw()
    venus.draw()
    earth.draw()
    mars.draw()
    jupiter.draw()
    saturn.draw()
    uranus.draw()
    neptune.draw()
    requestID.value = requestAnimationFrame(runAnimate)
  }

  runAnimate()
}

const onTrigger = async () => {
  if (!isRunning.value) {
    isRunning.value = true
    await nextTick()
    onRunning()
  } else {
    isRunning.value = false
    destroy()
  }
}

const destroy = () => {
  cancelAnimationFrame(requestID.value)
}

onMounted(async() => {
  await nextTick()
})

onUnmounted(() => {
  destroy()
})
</script>

自动移动

点击运行
<template>
  <div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <canvas v-if="isRunning" id="autoMove1" class="stage"></canvas>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, ref, onUnmounted, nextTick } from 'vue'

const requestID = ref<any>()
const isRunning = ref(false)

const onRunning = async() => {
  await nextTick()
  const canvas: any = document.getElementById('autoMove1')
  const ctx = canvas.getContext('2d')

  const width = Number(window.getComputedStyle(canvas).width.split('px')[0])
  const height = Number(window.getComputedStyle(canvas).height.split('px')[0])
  canvas.width = width
  canvas.height = height

  const start = { x: 50, y: 50 }
  const middle = { x: 20, y: 360 }
  const end = { x: 600, y: 50 }
  let prevPos
  let nextPos
  let add = 0

  
  // 获取二次贝塞尔曲线上的x和y
  // 贝塞尔曲线方程B(t)=(1−t)^2 * P0 + 2 * t * (1−t) * P1 + t^2 * P2 
  const getBezierPoints = (p0, p1, p2, pointNum) => {
    const points: any = []
    for (let i = 0; i < pointNum; i++) {
      const t = i / pointNum
      const x = (1 - t) ** 2 * p0.x + 2 * t * (1 - t) * p1.x + t ** 2 * p2.x
      const y = (1 - t) ** 2 * p0.y + 2 * t * (1 - t) * p1.y + t ** 2 * p2.y
      points.push({ x, y })
    }
    return points
  }

  // 计算两点之间的角度
  const calculateAngle = (x1, y1, x2, y2) => {
    const dx = x2 - x1
    const dy = y2 - y1
    const angle = Math.atan2(dy, dx)
    return angle
  }

  const drawWay = (p0, p1, p2) => {
    ctx.beginPath()
    ctx.moveTo(p0.x, p0.y)
    ctx.quadraticCurveTo(p1.x, p1.y, p2.x, p2.y)
    ctx.strokeStyle = 'blue'
    ctx.lineWidth = 2
    ctx.stroke()
  }

  const drawLine = (arr) => {
    const start = arr.shift()
    const end = arr.pop()
    ctx.beginPath()
    ctx.moveTo(start.x + 10, start.y + 10)
    for (let i=0;i<arr.length;i++){
      ctx.lineTo(arr[i].x + 10, arr[i].y + 10)
    }
    ctx.lineTo(end.x + 10, end.y + 10)
    ctx.strokeStyle = 'red'
    ctx.lineWidth = 2
    ctx.stroke()
  }

  const drawCar = (prevPos, nextPos) => {
    // 保存当前的绘图状态
    ctx.save()

    // 移动绘图原点到矩形的中心
    ctx.translate(nextPos.x, nextPos.y)
    
    if(prevPos) {
      // 旋转绘图上下文
      const angle = calculateAngle(prevPos.x, prevPos.y, nextPos.x, nextPos.y)
      ctx.rotate(angle)
    }

    // 移动回矩形左上角的位置
    ctx.translate(-nextPos.x, -nextPos.y)

    ctx.beginPath()
    ctx.rect(nextPos.x - 10, nextPos.y - 5, 20, 10)
    ctx.fillStyle = 'orange'
    ctx.fill()

    // 恢复之前的绘图状态
    ctx.restore()
  }

  
  const dealPoints = (pointList) => {
    const newPointList: any = []
    for (let i = 0; i < pointList.length; i += 10) {
      newPointList.push(pointList[i])
    }
    return newPointList
  }

  const clearCanvas = () => {
    ctx.clearRect(0, 0, canvas.width, canvas.height)
  }

  const pointList = getBezierPoints(start, middle, end, 300)
  drawLine([...pointList])
  drawWay(start, middle, end)
  const newPointList = dealPoints(pointList)
  
  const firstPoint = newPointList.shift()
  nextPos = firstPoint

  requestID.value = setInterval(() => {
    clearCanvas()
    drawLine([...pointList])
    drawWay(start, middle, end)
    if(add >= newPointList.length) {
      add = 0
      prevPos = null
      nextPos = start
    }
    
    drawCar(prevPos, nextPos)

    prevPos = nextPos
    nextPos = newPointList[add]
    add += 1
    
  }, 1000 / 4)
}

const onTrigger = async () => {
  if (!isRunning.value) {
    isRunning.value = true
    await nextTick()
    onRunning()
  } else {
    isRunning.value = false
    destroy()
  }
}

const destroy = () => {
  cancelAnimationFrame(requestID.value)
}

onMounted(async() => {
  await nextTick()
})

onUnmounted(() => {
  destroy()
})
</script>

金属字体

点击运行
<template>
  <div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <canvas v-if="isRunning" id="heavyMetal" class="stage"></canvas>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, ref, onUnmounted, nextTick } from 'vue'

const requestID = ref<any>()
const isRunning = ref(false)

const onRunning = async() => {
  await nextTick()
  const canvas: any = document.getElementById('heavyMetal')
  const ctx = canvas.getContext('2d')

  const width = Number(window.getComputedStyle(canvas).width.split('px')[0])
  const height = Number(window.getComputedStyle(canvas).height.split('px')[0])
  canvas.width = width
  canvas.height = height

  const text = '天元突破'
  const fontSize = 120
  ctx.textAlign = 'left'
  ctx.textBaseline = 'top'
  ctx.font = 'italic bold 120px arial'
  const widthText = ctx.measureText(text).width
  ctx.translate(width / 2 - widthText / 2, height / 2 - fontSize / 2)

  ctx.save()
  ctx.miterLimit = 4
  ctx.shadowColor = 'rgba(0, 0, 0, 0.6)'
  ctx.shadowOffsetY = 5
  ctx.shadowBlur = 3
  const gradient1 = ctx.createLinearGradient(0, 0, 0, fontSize)
  gradient1.addColorStop(0, '#999')
  gradient1.addColorStop(0.2, '#333')
  gradient1.addColorStop(1, '#696970')
  ctx.strokeStyle = gradient1
  ctx.lineWidth = 10
  ctx.strokeText(text, 0, 0)
  ctx.restore()

  ctx.save()
  ctx.lineJoin = 'bevel'
  const gradient2 = ctx.createLinearGradient(0, 0, 0, fontSize)
  gradient2.addColorStop(0, '#868487')
  gradient2.addColorStop(0.3, '#3d3b3e')
  gradient2.addColorStop(1, '#fff')
  ctx.strokeStyle = gradient2
  ctx.lineWidth = 5
  ctx.strokeText(text, 0, 0)
  ctx.restore()

  ctx.save()
  const gradient3 = ctx.createLinearGradient(0, 0, 0, fontSize)
  gradient3.addColorStop(0, '#9d9d9d')
  gradient3.addColorStop(0.3, '#ffffff')
  gradient3.addColorStop(0.6, '#3e3f41')
  gradient3.addColorStop(1, '#67686b')
  ctx.fillStyle = gradient3
  ctx.fillText(text, 0, 0)
  ctx.restore()
}

const onTrigger = async () => {
  if (!isRunning.value) {
    isRunning.value = true
    await nextTick()
    onRunning()
  } else {
    isRunning.value = false
    destroy()
  }
}

const destroy = () => {
  cancelAnimationFrame(requestID.value)
}

onMounted(async() => {
  await nextTick()
})

onUnmounted(() => {
  destroy()
})
</script>