Skip to content

放大缩小

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

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

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

class Chart {
  width
  height
  El
  ctx
  shapes
  doc
  scale
  maxScale
  minScale
  step
  offsetX
  offsetY
  wrapDom
  mousedownOriginX
  mousedownOriginY
  targetX
  targetY
  bindScroll
  bindDrag
  activeShape

  constructor(params) {
    const wrapDomStyle = getComputedStyle(params.el)
    this.width = parseInt(wrapDomStyle.width, 10)
    this.height = parseInt(wrapDomStyle.height, 10)

    this.El = document.createElement('canvas')
    this.El.width = this.width
    this.El.height = this.height
    this.ctx = this.El.getContext('2d')
    params.el.appendChild(this.El)

    this.shapes = []
    this.doc = params.doc

    this.scale = 1
    this.maxScale = 3
    this.minScale = 1
    this.step = 0.1
    this.offsetX = 0
    this.offsetY = 0
    this.addScaleFunc()

    this.wrapDom = params.el
    this.mousedownOriginX = 0
    this.mousedownOriginY = 0
    this.targetX = 0
    this.targetY = 0
    this.addDragFunc()

    this.bindScroll = null
    this.bindDrag = null
  }

  bindScrollFunc() {
    this.bindScroll = this.scrollFunc.bind(this)
  }

  bindDragFunc() {
    this.bindDrag = this.moveCanvasFunc.bind(this)
  }

  addScaleFunc() {
    this.El.addEventListener('mouseenter', this.addMouseWheel.bind(this))
    this.El.addEventListener('mouseleave', this.removeMouseWheel.bind(this))
  }

  addDragFunc() {
    this.El.addEventListener('mousedown', this.addMouseDrag.bind(this))
    this.doc.addEventListener('mouseup', this.removeMouseDrag.bind(this))
  }

  addMouseDrag(e) {
    this.bindDragFunc()
    this.targetX = e.offsetX
    this.targetY = e.offsetY

    this.mousedownOriginX = this.offsetX
    this.mousedownOriginY = this.offsetY

    this.activeShape = null

    this.wrapDom.style.cursor = 'grabbing'
    this.El.addEventListener('mousemove', this.bindDrag, false)
  }

  removeMouseDrag() {
    this.wrapDom.style.cursor = ''
    this.El.removeEventListener('mousemove', this.bindDrag, false)
  }

  moveCanvasFunc(e) {
    const maxDragX = this.El.width / 2
    const maxDragY = this.El.height / 2

    const offsetX = this.mousedownOriginX + (e.offsetX - this.targetX)
    const offsetY = this.mousedownOriginY + (e.offsetY - this.targetY)

    this.offsetX = Math.abs(offsetX) > maxDragX ? this.offsetX : offsetX
    this.offsetY = Math.abs(offsetY) > maxDragY ? this.offsetY : offsetY

    this.render()
  }

  addMouseWheel() {
    this.bindScrollFunc()
    this.doc.addEventListener('mousewheel', this.bindScroll, {
      passive: false
    })
  }

  removeMouseWheel() {
    this.doc.removeEventListener('mousewheel', this.bindScroll, {
      passive: false
    })
  }

  scrollFunc(e) {
    e.preventDefault()

    if (e.wheelDelta) {
      const x = e.offsetX - this.offsetX
      const y = e.offsetY - this.offsetY

      const offsetX = (x / this.scale) * this.step
      const offsetY = (y / this.scale) * this.step

      if (e.wheelDelta > 0) {
        this.offsetX -= this.scale >= this.maxScale ? 0 : offsetX
        this.offsetY -= this.scale >= this.maxScale ? 0 : offsetY

        this.scale += this.step
      } else {
        this.offsetX += this.scale <= this.minScale ? 0 : offsetX
        this.offsetY += this.scale <= this.minScale ? 0 : offsetY

        this.scale -= this.step
      }

      this.scale = Math.min(this.maxScale, Math.max(this.scale, this.minScale))

      this.render()
    }
  }

  drawCircle(data) {
    this.ctx.beginPath()
    this.ctx.fillStyle = data.fillStyle
    this.ctx.arc(data.x, data.y, data.r, 0, 2 * Math.PI)
    this.ctx.fill()
  }

  drawLine(data) {
    const arr = data.data.concat()

    this.ctx.beginPath()
    this.ctx.moveTo(arr.shift(), arr.shift())
    this.ctx.lineWidth = data.lineWidth || 1
    this.ctx.strokeStyle = data.strokeStyle || 'black'
    do {
      this.ctx.lineTo(arr.shift(), arr.shift())
    } while (arr.length)

    this.ctx.stroke()
  }

  drawRect(data) {
    this.ctx.beginPath()
    this.ctx.fillStyle = data.fillStyle
    this.ctx.fillRect(...data.data)
  }

  draw(info) {
    switch (info.type) {
      case 'line':
        this.drawLine(info)
        break
      case 'rect':
        this.drawRect(info)
        break
      case 'circle':
        this.drawCircle(info)
        break
      default:
        break
    }
  }

  push(data) {
    this.shapes.push(data)
  }

  render() {
    // 先重置为单位矩阵
    this.ctx.setTransform(1, 0, 0, 1, 0, 0)
    // 再清除画布
    this.ctx.clearRect(0, 0, this.width, this.height)
    // 最后再设置矩阵
    this.ctx.setTransform(this.scale, 0, 0, this.scale, this.offsetX, this.offsetY)

    this.shapes.forEach(item => {
      this.draw(item)
    })
  }
}

const onRunning = async() => {
  await nextTick()
  const chartObj = new Chart({
    el: document.getElementById('zoomAndDrag'),
    doc: document
  })

  chartObj.push({
  	type: 'circle',
    fillStyle: 'pink',
    x: 400,
    y: 400,
    r: 50
  })

  chartObj.push({
    type: 'line',
    lineWidth: 2,
    strokeStyle: 'orange',
    data: [100, 300, 200, 90, 250, 200, 400, 200]
  })

  chartObj.push({
    type: 'rect',
    fillStyle: '#0f00ff',
    data: [200, 300, 100, 100]
  })

  chartObj.render()
}

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>文字:{{ text || '--' }}</div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <canvas v-if="isRunning" id="clickEvent" class="stage"></canvas>
  </div>
</template>

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

const requestID = ref<any>()
const isRunning = ref(false)
let canvas
let ctx
const idPool = {}
const text = ref('')

const ActionType = {
  Down: 'down',
  Up: 'up',
  Move: 'move'
}
const EventNames = {
  click: 'click',
  mousedown: 'mousedown',
  mousemove: 'mousemove',
  mouseup: 'mouseup',
  mouseenter: 'mouseenter',
  mouseleave: 'mouseleave'
}

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

const createOnceId = () => {
  return Array(3).fill(0).map(() => parseInt(String(Math.random() * 255))).concat(255).join('-')
}

const rgbaToId = (rgba) => {
  return rgba.join('-')
}

const idToRgba = (id) => {
  return id.split('-')
}

const createId = () => {
  let id = createOnceId()
  while (idPool[id]) {
    id = createOnceId()
  }
  return id
}

class EventSimulator {
  listenersMap
  lastMoveId
  lastDownId
  constructor() {
    this.listenersMap = {}
    this.lastMoveId = null
    this.lastDownId = null
  }

  addAction(action, evt) {
    const {
      type,
      id
    } = action

    // mousemove
    if (type === ActionType.Move) {
      this.fire(id, EventNames.mousemove, evt)
    }

    // mouseover
    // mouseenter
    if (type === ActionType.Move && (!this.lastMoveId || this.lastMoveId !== id)) {
      this.fire(id, EventNames.mouseenter, evt)
      this.fire(this.lastMoveId, EventNames.mouseleave, evt)
    }

    // mousedown
    if (type === ActionType.Down) {
      this.fire(id, EventNames.mousedown, evt)
    }

    // mouseup
    if (type === ActionType.Up) {
      this.fire(id, EventNames.mouseup, evt)
    }

    // click
    if (type === ActionType.Up && this.lastDownId === id) {
      this.fire(id, EventNames.click, evt)
    }

    if (type === ActionType.Move) {
      this.lastMoveId = action.id
    } else if (type === ActionType.Down) {
      this.lastDownId = action.id
    }
  }

  addListeners(id, listeners) {
    this.listenersMap[id] = listeners
  }

  fire(id, eventName, evt) {
    if (this.listenersMap[id] && this.listenersMap[id][eventName]) {
      console.log(this.listenersMap)
      this.listenersMap[id][eventName].forEach(listener => listener(evt))
    }
  }
}

class Stage {
  canvas
  ctx
  osCanvas
  osCtx
  shapeIds
  shapeList
  eventSimulator
  constructor(customCanvas) {
    this.canvas = customCanvas
    this.canvas.width = parseInt(customCanvas.width)
    this.canvas.height = parseInt(customCanvas.height)

    
    this.ctx = this.canvas.getContext('2d')
    this.osCanvas = new OffscreenCanvas(this.canvas.width, this.canvas.height)
    this.osCtx = this.osCanvas.getContext('2d')

    this.canvas.addEventListener('mousedown', this.handleCreator(ActionType.Down))
    this.canvas.addEventListener('mouseup', this.handleCreator(ActionType.Up))
    this.canvas.addEventListener('mousemove', this.handleCreator(ActionType.Move))

    this.shapeIds = new Set()
    this.shapeList = new Set()
    this.eventSimulator = new EventSimulator()
  }

  add(shape) {
    const id = shape.getId()
    this.eventSimulator.addListeners(id, shape.getListeners())
    this.shapeIds.add(id)
    this.shapeList.add(shape)
  }

  start() {
    this.clearCanvas()
    this.collision()
    this.shapeList.forEach(item => {
      item.draw(this.ctx, this.osCtx)
    })
    window.requestAnimationFrame(this.start.bind(this))
  }


  collision() {
    // this.shapeList.forEach(item => {
    // 	console.log(item.x, item.y)
    // })
  }

  clearCanvas() {
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
    this.osCtx.clearRect(0, 0, this.canvas.width, this.canvas.height)
  }

  handleCreator(type) {
    return evt => {
      const x = evt.offsetX
      const y = evt.offsetY
      const id = this.hitJudge(x, y)
      // 首先判断是否点击元素,并移动
      // 防止冒泡
      // 最后再判断是否触发元素的on事件
      if (id) {
        this.eventSimulator.addAction({ type, id }, evt)
      }
    }
  }

  hitJudge(x, y) {
    const rgba = Array.from(this.osCtx.getImageData(x, y, 1, 1).data)
    const id = rgbaToId(rgba)
    return this.shapeIds.has(id) ? id : undefined
  }
}

class Base {
  listeners
  id
  constructor() {
    this.listeners = { }
    this.id = createId()
  }

  getId() {
    return this.id
  }

  draw(ctx, osCtx) {
    throw new Error('Method not implemented.')
  }

  on(eventName, listener) {
    if (this.listeners[eventName]) {
      this.listeners[eventName].push(listener)
    } else {
      this.listeners[eventName] = [listener]
    }
  }

  getListeners() {
    return this.listeners
  }
}

class Arc extends Base {
  props
  ctx
  osCtx
  isCollision
  x
  y
  radius
  fillColor

  constructor(p) {
    super()
    this.props = p
    this.ctx = null
    this.osCtx = null
    this.isCollision = false

  }

  move({ x,  y }) {
    // 暂停不给移动
    if (!this.isCollision) {
      this.props.x = x
      this.props.y = y
    }
    this.draw(this.ctx, this.osCtx)

  }

  collision() {
    this.isCollision = true
  }

  draw(ctx, osCtx) {
    this.ctx = ctx
    this.osCtx = osCtx
    const {
      x,
      y,
      radius,
      fillColor
    } = this.props
    this.x = x
    this.y = y
    this.radius = radius
    this.fillColor = fillColor

    ctx.save()
    ctx.beginPath()
    ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)
    ctx.fillStyle = this.fillColor
    ctx.fill()
    ctx.restore()

    const [r, g, b, a] = idToRgba(this.id)
    osCtx.save()
    osCtx.beginPath()
    osCtx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)
    osCtx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a})`
    osCtx.fill()
    osCtx.restore()
  }
}

const onRunning = async () => {
  await nextTick()
  canvas = document.getElementById('clickEvent')
  console.log(canvas)
  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 stage = new Stage(canvas)
  const arc1 = new Arc({
    x: 20,
    y: 120,
    radius: 10,
    fillColor: 'red'
  })

  const arc2 = new Arc({
    x: 220,
    y: 120,
    radius: 10,
    fillColor: 'orange'
  })

  stage.add(arc1)
  stage.add(arc2)
  stage.start()

  arc1.on(EventNames.click, () => { text.value = '点击了红色' })
  // arc2.on(EventNames.click, () => console.log('arc2 click'))
  arc2.on(EventNames.mousedown, (e) => { text.value = `点击了橙色,x:${e.offsetX},y:${e.offsetY}` })
  // arc2.on(EventNames.mouseup, () => console.log('arc2 click'))

  let x = 220
	let y = 120

  const runAnimate = () => {
    x = x - 1
    if(x <= 10) x = canvas.width / 2
    arc2.move({ x, y })
    requestID.value = requestAnimationFrame(runAnimate)
  }

  runAnimate()
}

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="easing1" class="frameExplosion"></canvas>
  </div>
</template>

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

const requestID = ref<any>()
const isRunning = ref(false)
// 图片数量 8
const len = 8
// 宽高 256
const size = 256
// 当前帧
let fm = 0
let start = 0


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

const onRunning = async() => {
  await nextTick()
  const canvas: any = document.getElementById('easing1')
  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 tempCanvas = document.createElement('canvas')
  tempCanvas.width = size * len
  tempCanvas.height = size
  const tempCtx = tempCanvas.getContext('2d')


  const draw = () => {
    tempCtx?.drawImage(img, 0, 0, size * len, size)
  }

  // 建立图像源
  const img = new Image()
  img.src = '/images/bomb.jpg'
  img.onload = draw


  const runAnimate = () => {
    // ctx.clearRect(0, 0, width, height)

    const end = Date.now()
    if (end - start >= 120) {
      start = end

      const data = tempCtx?.getImageData(fm * size, 0, size, size)
      ctx.putImageData(data, 0, 0)

      fm++
      if (fm >= len) {
        fm = 0
      }
    }

    requestID.value = requestAnimationFrame(runAnimate)
  }

  runAnimate()
}

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

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