完整效果
引言
本文是通过Canvas的序列帧动画,canvas的缩放,旋转,来代替页面渲染模型的问题,如果只是模型的缩放和旋转等简单的动画,可以考虑通过序列帧来实现。
功能实现
一、什么是Canvas
<canvas> 是一个可以使用脚本 (通常为JavaScript 来绘制图形的 HTML元素。例如,它可以用于绘制图表、制作图片构图或者制作简单的动画。
我们平常使用的Echarts表格就是使用Canvas进行渲染的。
二、序列帧动画的封装和和应用
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
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/…