鼠标事件-Canvas实现序列帧动画

519 阅读4分钟

完整效果

GIF 2023-11-17 11-41-52.gif

引言

本文是通过Canvas的序列帧动画,canvas的缩放,旋转,来代替页面渲染模型的问题,如果只是模型的缩放和旋转等简单的动画,可以考虑通过序列帧来实现。

功能实现

一、什么是Canvas

<canvas> 是一个可以使用脚本 (通常为JavaScript 来绘制图形的 HTML元素。例如,它可以用于绘制图表、制作图片构图或者制作简单的动画。

我们平常使用的Echarts表格就是使用Canvas进行渲染的。

图片.png

二、序列帧动画的封装和和应用

1.编写标签和和给canvas添加鼠标事件

注意:DOMMouseScroll是来兼容火狐浏览器的,mousewheel是来兼容一般浏览器的。

<template>

  <div id="animationContainer" ref="container">

    <canvas

      class="canvas"

      :id="canvasId"

      @mousedown="onMousedown"

      @mouseup="onMouseup"

      @mouseleave="onMouseleave"

      @DOMMouseScroll="handleMouseWheel"

      @mousewheel="handleMouseWheel"

    ></canvas>

  </div>

</template>

2.给容器和canvas添加样式和定位

这样是不用多说了吧,添加的相对定位和绝对定位。并在容器中垂直水平居中。

<style scoped>

#animationContainer {

  background-color: #f6f7f9;

  width: 100%;

  height: 100%;

  position: relative;

  overflow: hidden;

}


#canvas {

  position: absolute;

  top: 50%;

  left: 50%;

  transform: translate(-50%, -50%);

  cursor: pointer;

  width: 100%;

}

</style>

3.添加参数和引入父组件传递过来的配置参数


import { ref, onMounted, onUnmounted, watch, type Ref } from 'vue'

const maxAngle: number = 30 // 最大角度(可根据需求调整)

let currentFrame: Ref<number> = ref(1)

let mouseX: Ref<number> = ref(0)

let containerWidth: Ref<number> = ref(0)

const container = ref()

let canvas: HTMLCanvasElement

let initScale: Ref<number> = ref(0.6) //缩放倍数 0.6为默认值

let ctx: CanvasRenderingContext2D | null = null

// 监听鼠标按下事件

let oldAngle: number = 0

var loadedImgsArr: any = []

let isLoadImgEvent = ref(false)

let preMouseX: Ref<number> = ref(0)

// 最终要用的图片总张数

let finalTotalFrames: number = 0

let beforeScale: Ref<number> = ref(0.6)

beforeScale.value = initScale.value

let imgList = new Array()

var imgObj = null

// 默认缩放最大倍数1.5倍

const MAX_SCALE: Ref<number> = ref(1.5)

// 默认缩放最小倍数0.3倍

const MIN_SCALE: Ref<number> = ref(0.3)

const props = defineProps(['myCanvasList'])

let {

  canvasId,

  totalFrames,

  imagePrefix,

  imageExtension,

  fileUrlPath,

  part,

  scale,

  isScaleActive,

  maxScale,

  minScale

} = props.myCanvasList

  
if (!isScaleActive) initScale.value = scale

if (maxScale) MAX_SCALE.value = maxScale

if (minScale) MIN_SCALE.value = minScale

4.在挂载前初始所有的图片对象

这里是防止,canvas渲染或旋转的时候出现闪烁或空白图片的画面。

这里的我通过if判断主要是图片的名字问题,如果图片序列号没有000这种前缀,可以直接渲染。

我的个位数图片名称 c15_0341001.jpg,
四位数图片名称为:c15_0341018.jpg 百位数图片名称为:c15_0341359.jpg

图片.png


if (canvasId) {

  finalTotalFrames = totalFrames / part

  for (let i = 0; i < totalFrames; i = i + part) {

    if (i < 10) imgList.push(`${imagePrefix}00${i}${imageExtension}`)

    else if (i < 100) imgList.push(`${imagePrefix}0${i}${imageExtension}`)

    else if (i < 1000) imgList.push(`${imagePrefix}${i}${imageExtension}`)

  }

}

5.添加监听事件,监听缩放


watch(

  props,

  async (val) => {

    if (val.myCanvasList.canvasId && !isScaleActive) {

      initScale.value = val.myCanvasList.scale

      dramImageByScale(initScale.value, currentFrame.value)

    }

  },

  { deep: true }

)

6.加载所有的图片对象

function loadImgs(): void {

  try {

    var imgsArr = imgList

    isLoadImgEvent.value = false

    for (let index = 1; index < imgsArr.length; index++) {

      imgObj = new Image()

      imgObj.src = `${fileUrlPath}${imgsArr[index]}`

      imgObj.onload = () => {

        isLoadImgEvent.value = true

        if (index == 1) {

          getCanvas(index)

        }

      }

      loadedImgsArr.push(imgObj)

    }

  } catch (error) {

    console.log('图片加载失败')

  }

}

7.加载图片和渲染Canvas

// 加载图片

function getCanvas(index: number): void {

  dramImageByScale(initScale.value, index)

}

// 根据缩放来画canvas

function dramImageByScale(scale: number, index: number): void {

  if (ctx) {

    let imgWidth = loadedImgsArr[0].width //图片的宽

    let imgHeight = loadedImgsArr[0].height //图片的高

    canvas.width = imgWidth //画布的宽

    canvas.height = imgHeight //画布的高

    var width = imgWidth * scale //缩放后的图片大小

    var height = imgHeight * scale //缩放后的图片大小

    var dx = canvas.width / 2 - width / 2 //x坐标

    var dy = canvas.height / 2 - height / 2 //y坐标

  


    ctx.clearRect(0, 0, canvas.width, canvas.height)

    ctx.drawImage(loadedImgsArr[index], dx, dy, width, height)

    beforeScale.value = scale

  }

}

8.鼠标事件控制Canvas的放大缩小效果

// 鼠标滚轮滚动事件

function handleMouseWheel(event: any): void {

  if (isScaleActive) {

    if (isLoadImgEvent.value) {

      var delta = event.detail | event.wheelDelta

      if (delta < 0) {

        // 鼠标往上滚动

        lessen()

      } else if (delta > 0) {

        // 鼠标往下滚动 }

        magnify()

      }

    }

  }

}

// 放大

function magnify(): void {

  if (initScale.value < MAX_SCALE.value) {

    initScale.value += 0.1

    dramImageByScale(initScale.value, currentFrame.value)

  }

}

// 缩小

function lessen(): void {

  if (initScale.value > MIN_SCALE.value) {

    initScale.value = formatDecimal(initScale.value - 0.1, 1)

    dramImageByScale(initScale.value, currentFrame.value)

  }

}

// 监听鼠标松开事件

const onMouseup = (): void => {

  if (isLoadImgEvent.value) {

    document.onmousemove = null

  }

}

// 监听鼠标松开事件

const onMouseleave = (): void => {

  if (isLoadImgEvent.value) {

    document.onmousemove = null

  }

}

// 保留两位小数

function formatDecimal(first: number, decimal: number): number {

  let num = first.toString()

  let index = num.indexOf('.')

  if (index !== -1) {

    num = num.substring(0, decimal + index + 1)

  } else {

    num = num.substring(0)

  }

  let decimalNum = parseFloat(num).toFixed(decimal)

  return parseFloat(decimalNum)

}

9.鼠标事件控制Canvas的旋转效果


// 监听鼠标按下事件

const onMousedown = (): void => {

  if (isLoadImgEvent.value) {

    document.onmousemove = function (moveEvent: MouseEvent) {

      moveEvent = moveEvent

      const containerRect = container.value.getBoundingClientRect()

      mouseX.value = moveEvent.clientX - containerRect.left

      containerWidth.value = containerRect.width

      const mousePercentage = mouseX.value / containerWidth.value

      const angle = maxAngle * (2 * mousePercentage - 1)

      if (oldAngle == 0) oldAngle = angle

      // 按张渲染,调整速度

      if (Math.abs(angle - oldAngle) > 0.1) {

        // console.log('开始转动');

        animateFrames()

        oldAngle = angle

      }

    }

  }

}

function animateFrames(): void {

  if (mouseX.value > preMouseX.value) {

    // 从左往右

    if (currentFrame.value >= finalTotalFrames - 1) {

      currentFrame.value = 0

    } else {

      currentFrame.value += part

      if (currentFrame.value >= finalTotalFrames) {

        currentFrame.value = 0

      }

    }

    preMouseX.value = mouseX.value

  } else if (mouseX.value < preMouseX.value) {

    // 从右往左

    if (currentFrame.value <= 0) {

      currentFrame.value = finalTotalFrames - 1

    } else {

      currentFrame.value -= part

      if (currentFrame.value <= 0) {

        currentFrame.value = finalTotalFrames - 1

      }

    }

    preMouseX.value = mouseX.value

  }

  getCanvas(currentFrame.value)

}

10.挂载方法和销毁


onMounted(() => {

  loadImgs()

  canvas = <HTMLCanvasElement>document.getElementById(canvasId)

  if (canvas) {

    ctx = canvas.getContext('2d')

  }

})

onUnmounted(() => {

  document.onmousemove = null

})

11.父组件的应用

<script setup lang="ts">

import { ref, reactive } from 'vue'

import myCanvas from './canvas/Canvas.vue'

interface CanvasData {

  canvasId: string

  totalFrames: number

  imagePrefix: string

  imageExtension: string

  fileUrlPath: string

  part: number

  initScale: number

  maxScale: number

  minScale: number

}

let myCanvasList = reactive<any>({

  canvasId: 'canvas',

  totalFrames: 361,//图片总数

  imagePrefix: 'c15_0341',//图片的前缀名

  imageExtension: '.jpg',//图片后缀,图片类型

  fileUrlPath: '/src/components/white/',//图片所在的路径

  part: 1,//Canvas渲染多少张,1为360张,2为180

  // 当isScaleActive为false时scale无效

  scale: 0.6,

  isScaleActive: true,

  // 默认为1.5,可以自定义

  maxScale: 1,

  // 默认为0.3,可以自定义

  minScale: 0.3

})

let showCanvas = ref(true)

</script>

<template>

  <div class="canvasContainer">

    <myCanvas v-if="showCanvas" :myCanvasList="myCanvasList"></myCanvas>

  </div>

</template>

  


<style scoped>

.canvasContainer {

  width: 1024px;

  height: 1024px;

}

</style>

引用

文章中还有一些不足之处,还有很大的优化空间。

Canvas 教程: developer.mozilla.org/zh-CN/docs/…

canvas序列帧动画 & pixi.js: www.cnblogs.com/catherLee/p…

记一次序列帧动画预渲染解决方案 : www.6hu.cc/archives/72…