459 lines
11 KiB
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> |