qs_xinchun2026_h5/pages/testcanvas.vue

459 lines
11 KiB
Vue

<template>
<view class="container">
<canvas
ref="canvasRef"
class="drawing-canvas"
:style="{
width: `${canvasWidth}px`,
height: `${canvasHeight}px`
}"
:width="canvasWidth * devicePixelRatio"
:height="canvasHeight * devicePixelRatio"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
/>
<!-- 控制面板 -->
<view class="control-panel">
<button @click="addImage">添加图片</button>
<button @click="clearAll">清除所有</button>
<button @click="toggleDebug">{{ showDebug ? '隐藏调试' : '显示调试' }}</button>
<text>图片数量: {{ images.length }}</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
canvasWidth: 0,
canvasHeight: 0,
devicePixelRatio: 1,
ctx: null,
offscreenCanvas: null, // 离屏 Canvas
offscreenCtx: null,
images: [], // 所有图片
imageCache: new Map(), // 图片缓存
draggingIndex: -1, // 当前拖动的图片索引
// 性能优化
renderQueue: [], // 渲染队列
lastRenderTime: 0,
batchRender: true, // 批量渲染
showDebug: false
}
},
mounted() {
this.initCanvas()
// 初始化图片
for (let i = 0; i < 5; i++) {
this.addImage()
}
},
methods: {
/**
* 初始化 Canvas
*/
initCanvas() {
const systemInfo = uni.getSystemInfoSync()
this.devicePixelRatio = systemInfo.pixelRatio || 1
this.canvasWidth = systemInfo.windowWidth
this.canvasHeight = systemInfo.windowHeight
// 初始化主 Canvas
const query = uni.createSelectorQuery().in(this)
query.select('.drawing-canvas')
.fields({ node: true, size: true })
.exec((res) => {
if (res[0]) {
const canvas = res[0].node
this.ctx = canvas.getContext('2d')
// 设置高分辨率
canvas.width = this.canvasWidth * this.devicePixelRatio
canvas.height = this.canvasHeight * this.devicePixelRatio
this.ctx.scale(this.devicePixelRatio, this.devicePixelRatio)
// 初始化离屏 Canvas
this.initOffscreenCanvas()
// 开始渲染循环
this.startRenderLoop()
}
})
},
/**
* 初始化离屏 Canvas
*/
initOffscreenCanvas() {
this.offscreenCanvas = document.createElement('canvas')
this.offscreenCanvas.width = this.canvasWidth
this.offscreenCanvas.height = this.canvasHeight
this.offscreenCtx = this.offscreenCanvas.getContext('2d')
},
/**
* 添加图片
*/
addImage() {
const image = {
id: Date.now() + Math.random(),
x: Math.random() * (this.canvasWidth - 100),
y: Math.random() * (this.canvasHeight - 100),
width: 80 + Math.random() * 40,
height: 80 + Math.random() * 40,
rotation: Math.random() * Math.PI * 2,
scale: 0.8 + Math.random() * 0.4,
color: this.getRandomColor(),
isDragging: false
}
this.images.push(image)
// 预渲染到离屏 Canvas
this.prerenderImage(image)
// 标记需要重新渲染
this.scheduleRender()
},
/**
* 预渲染图片
*/
prerenderImage(image) {
if (this.imageCache.has(image.id)) return
const cacheCanvas = document.createElement('canvas')
cacheCanvas.width = image.width
cacheCanvas.height = image.height
const cacheCtx = cacheCanvas.getContext('2d')
// 绘制到缓存 Canvas
cacheCtx.fillStyle = image.color
cacheCtx.fillRect(0, 0, image.width, image.height)
// 添加边框
cacheCtx.strokeStyle = 'rgba(255, 255, 255, 0.8)'
cacheCtx.lineWidth = 2
cacheCtx.strokeRect(1, 1, image.width - 2, image.height - 2)
// 添加编号
cacheCtx.fillStyle = 'white'
cacheCtx.font = '20px Arial'
cacheCtx.textAlign = 'center'
cacheCtx.textBaseline = 'middle'
cacheCtx.fillText(
String(this.images.indexOf(image) + 1),
image.width / 2,
image.height / 2
)
this.imageCache.set(image.id, cacheCanvas)
},
/**
* 处理触摸开始
*/
handleTouchStart(e) {
const touch = e.touches[0]
const rect = e.currentTarget.getBoundingClientRect()
const x = touch.clientX - rect.left
const y = touch.clientY - rect.top
// 从后往前检查,点击在最后面的图片上
for (let i = this.images.length - 1; i >= 0; i--) {
const image = this.images[i]
if (this.isPointInImage(x, y, image)) {
this.draggingIndex = i
image.isDragging = true
image.dragOffset = {
x: x - image.x,
y: y - image.y
}
// 将图片移到最前面
this.bringToFront(i)
this.scheduleRender()
break
}
}
},
/**
* 处理触摸移动
*/
handleTouchMove(e) {
e.preventDefault()
if (this.draggingIndex === -1) return
const touch = e.touches[0]
const rect = e.currentTarget.getBoundingClientRect()
const x = touch.clientX - rect.left
const y = touch.clientY - rect.top
const image = this.images[this.draggingIndex]
image.x = x - image.dragOffset.x
image.y = y - image.dragOffset.y
// 边界检查
image.x = Math.max(0, Math.min(this.canvasWidth - image.width, image.x))
image.y = Math.max(0, Math.min(this.canvasHeight - image.height, image.y))
this.scheduleRender()
},
/**
* 处理触摸结束
*/
handleTouchEnd() {
if (this.draggingIndex !== -1) {
this.images[this.draggingIndex].isDragging = false
this.draggingIndex = -1
this.scheduleRender()
}
},
/**
* 检查点是否在图片内
*/
isPointInImage(x, y, image) {
// 简化的矩形检测
return (
x >= image.x &&
x <= image.x + image.width &&
y >= image.y &&
y <= image.y + image.height
)
},
/**
* 将图片移到最前面
*/
bringToFront(index) {
if (index === this.images.length - 1) return
const image = this.images[index]
this.images.splice(index, 1)
this.images.push(image)
this.draggingIndex = this.images.length - 1
},
/**
* 调度渲染
*/
scheduleRender() {
if (!this.renderScheduled) {
this.renderScheduled = true
requestAnimationFrame(() => {
this.render()
this.renderScheduled = false
})
}
},
/**
* 开始渲染循环
*/
startRenderLoop() {
const renderLoop = () => {
this.renderFrameId = requestAnimationFrame(renderLoop)
// 如果正在拖动,强制渲染
if (this.draggingIndex !== -1) {
this.render()
} else {
// 非拖动状态,可以降低渲染频率
const now = Date.now()
if (now - this.lastRenderTime > 16) { // 约 60fps
this.render()
}
}
}
renderLoop()
},
/**
* 渲染
*/
render() {
if (!this.ctx || !this.offscreenCtx) return
const now = Date.now()
if (this.batchRender) {
// 批量渲染到离屏 Canvas
this.renderToOffscreen()
// 从离屏 Canvas 复制到主 Canvas
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
this.ctx.drawImage(this.offscreenCanvas, 0, 0)
} else {
// 直接渲染
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
this.renderImages()
}
this.lastRenderTime = now
},
/**
* 渲染到离屏 Canvas
*/
renderToOffscreen() {
this.offscreenCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
// 绘制背景
this.offscreenCtx.fillStyle = '#f5f5f5'
this.offscreenCtx.fillRect(0, 0, this.canvasWidth, this.canvasHeight)
// 绘制所有图片
this.images.forEach(image => {
this.renderImage(this.offscreenCtx, image)
})
},
/**
* 渲染图片
*/
renderImages() {
this.images.forEach(image => {
this.renderImage(this.ctx, image)
})
},
/**
* 渲染单个图片
*/
renderImage(ctx, image) {
const cache = this.imageCache.get(image.id)
if (!cache) return
ctx.save()
// 设置变换
ctx.translate(image.x + image.width / 2, image.y + image.height / 2)
ctx.rotate(image.rotation)
ctx.scale(image.scale, image.scale)
// 绘制阴影
if (image.isDragging) {
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)'
ctx.shadowBlur = 15
ctx.shadowOffsetY = 5
} else {
ctx.shadowColor = 'rgba(0, 0, 0, 0.2)'
ctx.shadowBlur = 5
ctx.shadowOffsetY = 2
}
// 绘制图片
ctx.drawImage(cache, -image.width / 2, -image.height / 2)
// 绘制拖动状态指示器
if (image.isDragging) {
ctx.strokeStyle = '#4A90E2'
ctx.lineWidth = 3
ctx.strokeRect(
-image.width / 2 - 2,
-image.height / 2 - 2,
image.width + 4,
image.height + 4
)
}
ctx.restore()
},
/**
* 清除所有
*/
clearAll() {
this.images = []
this.imageCache.clear()
this.scheduleRender()
},
/**
* 切换调试模式
*/
toggleDebug() {
this.showDebug = !this.showDebug
this.scheduleRender()
},
/**
* 获取随机颜色
*/
getRandomColor() {
const colors = [
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
'#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9'
]
return colors[Math.floor(Math.random() * colors.length)]
}
}
}
</script>
<style>
.container {
width: 100vw;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
overflow: hidden;
}
.drawing-canvas {
width: 100vw;
height: 100vh;
display: block;
touch-action: none;
user-select: none;
cursor: move;
}
.control-panel {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.9);
padding: 10px 20px;
border-radius: 20px;
display: flex;
gap: 10px;
align-items: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
.control-panel button {
padding: 8px 16px;
border: none;
border-radius: 8px;
background: #4A90E2;
color: white;
font-weight: 500;
cursor: pointer;
transition: background 0.3s;
}
.control-panel button:active {
background: #357AE8;
transform: translateY(1px);
}
.control-panel text {
font-size: 14px;
color: #333;
font-weight: 500;
}
</style>