1、初步实现canvas中图片的拖动功能及目标区域的设定
This commit is contained in:
Wenzhe 2026-01-31 18:19:24 +08:00
parent 9c83d6a588
commit c0897a3d86
9 changed files with 3290 additions and 379 deletions

580
canvas.html Normal file
View File

@ -0,0 +1,580 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas图片拖放功能</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
color: #333;
}
.header {
text-align: center;
margin-bottom: 30px;
max-width: 800px;
}
h1 {
font-size: 2.5rem;
color: #2c3e50;
margin-bottom: 10px;
text-shadow: 1px 1px 3px rgba(0,0,0,0.1);
}
.subtitle {
font-size: 1.2rem;
color: #7f8c8d;
margin-bottom: 20px;
}
.container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 30px;
max-width: 1200px;
width: 100%;
}
.canvas-container {
background-color: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 20px;
flex: 1;
min-width: 300px;
max-width: 700px;
}
canvas {
display: block;
border-radius: 8px;
background-color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
margin: 0 auto;
}
.controls {
background-color: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 25px;
flex: 0 0 300px;
display: flex;
flex-direction: column;
gap: 20px;
}
.control-group {
padding: 15px;
border-radius: 8px;
background-color: #f8f9fa;
}
h2 {
font-size: 1.5rem;
color: #2c3e50;
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 2px solid #eaeaea;
}
h3 {
font-size: 1.2rem;
color: #3498db;
margin-bottom: 10px;
}
.instructions {
line-height: 1.6;
color: #555;
}
.instructions li {
margin-bottom: 8px;
padding-left: 5px;
}
.target-area {
background-color: #e8f4fc;
border: 2px dashed #3498db;
border-radius: 8px;
padding: 15px;
text-align: center;
margin-top: 10px;
}
.target-area.active {
background-color: #d1f2eb;
border-color: #2ecc71;
}
.status {
padding: 12px;
border-radius: 8px;
background-color: #f9f9f9;
text-align: center;
font-weight: 500;
transition: all 0.3s;
}
.status.dragging {
background-color: #fff9e6;
color: #e67e22;
}
.status.success {
background-color: #d4f8e8;
color: #27ae60;
}
.reset-btn {
background-color: #3498db;
color: white;
border: none;
padding: 14px 20px;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
width: 100%;
}
.reset-btn:hover {
background-color: #2980b9;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(41, 128, 185, 0.2);
}
.character-info {
background-color: #fff8e1;
border-left: 4px solid #ffb300;
padding: 12px 15px;
border-radius: 0 8px 8px 0;
}
.stats {
display: flex;
justify-content: space-between;
background-color: #f8f9fa;
padding: 10px 15px;
border-radius: 8px;
font-size: 0.9rem;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 1.3rem;
font-weight: 700;
color: #3498db;
}
.stat-label {
color: #7f8c8d;
font-size: 0.9rem;
}
footer {
margin-top: 40px;
text-align: center;
color: #7f8c8d;
font-size: 0.9rem;
padding: 20px;
}
@media (max-width: 768px) {
.container {
flex-direction: column;
align-items: center;
}
.controls {
width: 100%;
max-width: 700px;
}
}
</style>
</head>
<body>
<div class="header">
<h1>Canvas图片拖放功能</h1>
<p class="subtitle">将橙色角色拖动到目标区域体验HTML5 Canvas的交互功能</p>
</div>
<div class="container">
<div class="canvas-container">
<h2>拖放画布</h2>
<canvas id="myCanvas" width="650" height="500"></canvas>
<div class="character-info">
<p><strong>角色说明:</strong>这是一个橙色卡通角色,正在向后转身。角色背部有白色椭圆形特征,右臂向后摆动,左臂略微向前,呈现动态效果。</p>
</div>
</div>
<div class="controls">
<div class="control-group">
<h3>操作说明</h3>
<ul class="instructions">
<li>1. 在左侧画布上点击并按住橙色角色</li>
<li>2. 拖动角色到任意位置</li>
<li>3. 将角色移动到下方目标区域</li>
<li>4. 释放鼠标放下角色</li>
</ul>
<div class="target-area" id="targetArea">
<strong>目标区域</strong><br>
将角色拖放到这里
</div>
</div>
<div class="stats">
<div class="stat-item">
<div class="stat-value" id="dragCount">0</div>
<div class="stat-label">拖放次数</div>
</div>
<div class="stat-item">
<div class="stat-value" id="targetHits">0</div>
<div class="stat-label">命中目标</div>
</div>
<div class="stat-item">
<div class="stat-value" id="distance">0</div>
<div class="stat-label">移动距离(px)</div>
</div>
</div>
<div class="status" id="status">
点击并拖动角色开始体验
</div>
<button class="reset-btn" id="resetBtn">重置角色位置</button>
</div>
</div>
<footer>
<p>HTML5 Canvas 拖放功能示例 | 基于用户提供的图片描述实现</p>
</footer>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 获取Canvas和上下文
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 获取DOM元素
const status = document.getElementById('status');
const resetBtn = document.getElementById('resetBtn');
const targetArea = document.getElementById('targetArea');
const dragCountElement = document.getElementById('dragCount');
const targetHitsElement = document.getElementById('targetHits');
const distanceElement = document.getElementById('distance');
// 图片对象
const character = {
x: 100,
y: 100,
width: 150,
height: 200,
isDragging: false,
offsetX: 0,
offsetY: 0,
color: '#FF8C00', // 橙色
text: 'A'
};
// 目标区域
const target = {
x: 400,
y: 300,
width: 200,
height: 150
};
// 统计数据
let stats = {
dragCount: 0,
targetHits: 0,
totalDistance: 0
};
// 更新统计显示
function updateStats() {
dragCountElement.textContent = stats.dragCount;
targetHitsElement.textContent = stats.targetHits;
distanceElement.textContent = Math.round(stats.totalDistance);
}
// 绘制角色
function drawCharacter() {
// 绘制角色主体
ctx.fillStyle = character.color;
// 绘制身体
ctx.beginPath();
ctx.ellipse(
character.x + character.width/2,
character.y + character.height/2,
character.width/3,
character.height/2.5,
0, 0, Math.PI * 2
);
ctx.fill();
// 绘制头部
ctx.beginPath();
ctx.arc(character.x + character.width/2, character.y + 40, 30, 0, Math.PI * 2);
ctx.fill();
// 绘制背部白色标记
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.ellipse(
character.x + character.width/2,
character.y + character.height/2,
15,
25,
0, 0, Math.PI * 2
);
ctx.fill();
// 绘制四肢
ctx.fillStyle = character.color;
// 左臂
ctx.fillRect(character.x + 10, character.y + 80, 20, 60);
// 右臂
ctx.fillRect(character.x + character.width - 30, character.y + 80, 20, 60);
// 左腿
ctx.fillRect(character.x + 40, character.y + 150, 20, 50);
// 右腿
ctx.fillRect(character.x + 90, character.y + 150, 20, 50);
// 绘制鞋子
ctx.fillStyle = '#333';
ctx.fillRect(character.x + 35, character.y + 195, 30, 10);
ctx.fillRect(character.x + 85, character.y + 195, 30, 10);
// 绘制面部特征
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(character.x + character.width/2 - 10, character.y + 35, 5, 0, Math.PI * 2);
ctx.arc(character.x + character.width/2 + 10, character.y + 35, 5, 0, Math.PI * 2);
ctx.fill();
// 绘制嘴部
ctx.beginPath();
ctx.arc(character.x + character.width/2, character.y + 50, 8, 0, Math.PI);
ctx.stroke();
// 绘制角色上的文字
ctx.fillStyle = 'white';
ctx.font = 'bold 24px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(character.text, character.x + character.width/2, character.y + 100);
// 如果正在拖拽,绘制高亮边框
if (character.isDragging) {
ctx.strokeStyle = '#3498db';
ctx.lineWidth = 3;
ctx.setLineDash([5, 5]);
ctx.strokeRect(character.x - 5, character.y - 5, character.width + 10, character.height + 10);
ctx.setLineDash([]);
}
}
// 绘制目标区域
function drawTarget() {
ctx.fillStyle = 'rgba(52, 152, 219, 0.1)';
ctx.strokeStyle = '#3498db';
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
ctx.fillRect(target.x, target.y, target.width, target.height);
ctx.strokeRect(target.x, target.y, target.width, target.height);
ctx.setLineDash([]);
// 绘制目标区域内的文字
ctx.fillStyle = '#3498db';
ctx.font = 'bold 20px Arial';
ctx.textAlign = 'center';
ctx.fillText('放置区域', target.x + target.width/2, target.y + target.height/2);
}
// 绘制Canvas
function drawCanvas() {
// 清除Canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制目标区域
drawTarget();
// 绘制角色
drawCharacter();
// 检查角色是否在目标区域内
checkTargetHit();
}
// 检查角色是否在目标区域内
function checkTargetHit() {
const charCenterX = character.x + character.width/2;
const charCenterY = character.y + character.height/2;
const isInTarget =
charCenterX > target.x &&
charCenterX < target.x + target.width &&
charCenterY > target.y &&
charCenterY < target.y + target.height;
if (isInTarget) {
targetArea.classList.add('active');
status.textContent = "成功!角色已在目标区域内";
status.className = "status success";
} else {
targetArea.classList.remove('active');
if (character.isDragging) {
status.textContent = "拖动角色到目标区域";
status.className = "status dragging";
} else {
status.textContent = "点击并拖动角色开始体验";
status.className = "status";
}
}
return isInTarget;
}
// 检查点击是否在角色上
function isPointInCharacter(x, y) {
return x > character.x &&
x < character.x + character.width &&
y > character.y &&
y < character.y + character.height;
}
// 计算两点之间的距离
function calculateDistance(x1, y1, x2, y2) {
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
}
// 鼠标按下事件
function handleMouseDown(e) {
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
if (isPointInCharacter(mouseX, mouseY)) {
character.isDragging = true;
character.offsetX = mouseX - character.x;
character.offsetY = mouseY - character.y;
// 记录开始拖拽的位置
character.startDragX = character.x;
character.startDragY = character.y;
drawCanvas();
}
}
// 鼠标移动事件
function handleMouseMove(e) {
if (!character.isDragging) return;
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// 计算移动距离
const distanceMoved = calculateDistance(
character.x + character.width/2,
character.y + character.height/2,
mouseX - character.offsetX + character.width/2,
mouseY - character.offsetY + character.height/2
);
stats.totalDistance += distanceMoved;
// 更新角色位置
character.x = mouseX - character.offsetX;
character.y = mouseY - character.offsetY;
// 限制角色不超出Canvas边界
character.x = Math.max(0, Math.min(canvas.width - character.width, character.x));
character.y = Math.max(0, Math.min(canvas.height - character.height, character.y));
drawCanvas();
updateStats();
}
// 鼠标释放事件
function handleMouseUp() {
if (character.isDragging) {
character.isDragging = false;
stats.dragCount++;
// 检查是否在目标区域内
if (checkTargetHit()) {
stats.targetHits++;
}
drawCanvas();
updateStats();
}
}
// 重置角色位置
function resetCharacter() {
character.x = 100;
character.y = 100;
character.isDragging = false;
// 重置目标区域状态
targetArea.classList.remove('active');
status.textContent = "点击并拖动角色开始体验";
status.className = "status";
drawCanvas();
}
// 初始化
function init() {
drawCanvas();
// 添加事件监听器
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mouseup', handleMouseUp);
// 添加重置按钮事件监听器
resetBtn.addEventListener('click', resetCharacter);
// 更新统计
updateStats();
}
// 启动应用
init();
});
</script>
</body>
</html>

View File

@ -1,22 +1,22 @@
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, onUnmounted, computed, nextTick, getCurrentInstance as vueGetCurrentInstance } from 'vue'
import FuClickArea from './FuClickArea.vue' import FuClickArea from './FuClickArea.vue'
// Vue
const instance = vueGetCurrentInstance()
// //
const props = defineProps({ const props = defineProps({
//
active: { active: {
type: Boolean, type: Boolean,
default: false default: false
}, },
//
scrollPosition: { scrollPosition: {
type: Number, type: Number,
default: 0 default: 0
} }
}) })
//
const emit = defineEmits(['collect-seal']) const emit = defineEmits(['collect-seal'])
// //
@ -28,7 +28,6 @@ const sq3ImageVisible = ref(false)
// //
const parallaxOffset = computed(() => { const parallaxOffset = computed(() => {
// 1/10
return props.scrollPosition * 0.1 return props.scrollPosition * 0.1
}) })
@ -41,120 +40,316 @@ const handleFuClick = () => {
// //
const isDragging = ref(false) const isDragging = ref(false)
const showDuck = ref(false) const showDuck = ref(true) //
const deskImage = ref('/static/dzm/img_desk1.png') const deskImage = ref('/static/dzm/img_desk1.png')
const showGuideElements = ref(true) const showGuideElements = ref(true)
// // Canvas
const duckElement = ref(null) const ctx = ref(null)
// 使 //
let duckX = 0 const duckImagePath = ref(null)
let duckY = 0
// (540, 1781, 100, 100) // Canvas
const dragStartArea = { x: 590, y: 1831 } const duckX = ref(275)
const duckY = ref(110)
// (: 13, 1665, 441, 382) //
const targetArea = { x: 100, y: 1750, width: 300, height: 200 } const duckWidth = 36
const duckHeight = 70
// //
const startDrag = (e) => { const dragOffsetX = ref(0)
e.preventDefault() const dragOffsetY = ref(0)
// // Canvas
const canvasRect = ref({
left: 0,
top: 0,
width: 0,
height: 0
})
// () - Canvas
const targetArea = ref({
x: 0,
y: 80,
width: 200,
height: 150
})
// ID
const animationId = ref(null)
// Canvas
const initCanvas = () => {
console.log('正在初始化 Canvas...')
// uni-app 使 uni.createCanvasContext
ctx.value = uni.createCanvasContext('dragDuckCanvas', instance)
if (ctx.value) {
console.log('Canvas 上下文创建成功')
//
loadDuckImage()
// Canvas
getCanvasPosition()
//
startAnimation()
} else {
console.error('Canvas 上下文创建失败')
}
}
//
const loadDuckImage = () => {
uni.getImageInfo({
src: '/static/dzm/img_duck.png',
success: (res) => {
console.log('鸭子图片加载成功:', res)
duckImagePath.value = res.path
},
fail: (err) => {
console.error('鸭子图片加载失败:', err)
}
})
}
// Canvas
const getCanvasPosition = () => {
const query = uni.createSelectorQuery().in(instance)
query.select('#dragDuckCanvas').boundingClientRect(res => {
if (res) {
canvasRect.value = {
left: res.left,
top: res.top,
width: res.width,
height: res.height
}
console.log('Canvas 位置信息:', canvasRect.value)
} else {
console.error('无法获取 Canvas 位置信息')
}
}).exec()
}
//
const startAnimation = () => {
const animate = () => {
drawCanvas()
animationId.value = requestAnimationFrame(animate)
}
animationId.value = requestAnimationFrame(animate)
}
// Canvas
const drawCanvas = () => {
if (!ctx.value) return
// Canvas
ctx.value.clearRect(0, 0, canvasRect.value.width || 400, canvasRect.value.height || 600)
//
drawTarget()
//
drawDuck()
//
ctx.value.draw(true)
}
//
const drawTarget = () => {
const target = targetArea.value
//
ctx.value.setFillStyle('rgba(52, 152, 219, 0.3)')
ctx.value.fillRect(target.x, target.y, target.width, target.height)
//
ctx.value.setStrokeStyle('#3498db')
ctx.value.setLineWidth(2)
ctx.value.setLineDash([5, 5])
ctx.value.strokeRect(target.x, target.y, target.width, target.height)
ctx.value.setLineDash([])
//
ctx.value.setFontSize(14)
ctx.value.setFillStyle('#3498db')
ctx.value.setTextAlign('center')
ctx.value.fillText(
'目标区域',
target.x + target.width / 2,
target.y + target.height / 2
)
}
//
const drawDuck = () => {
if (!showDuck.value) return
//
if (duckImagePath.value) {
ctx.value.drawImage(duckImagePath.value, duckX.value, duckY.value, duckWidth, duckHeight)
} else {
//
ctx.value.setFillStyle('#ffcc00')
ctx.value.fillRect(duckX.value, duckY.value, duckWidth, duckHeight)
//
ctx.value.setStrokeStyle('#333')
ctx.value.setLineWidth(2)
ctx.value.strokeRect(duckX.value, duckY.value, duckWidth, duckHeight)
//
ctx.value.setFontSize(14)
ctx.value.setFillStyle('#FFFFFF')
ctx.value.setTextAlign('center')
ctx.value.setTextBaseline('middle')
ctx.value.fillText('鸭', duckX.value + duckWidth / 2, duckY.value + duckHeight / 2)
}
//
if (isDragging.value) { if (isDragging.value) {
endDrag() //
} ctx.value.setFillStyle('rgba(231, 76, 60, 0.3)')
ctx.value.fillRect(duckX.value, duckY.value, duckWidth, duckHeight)
isDragging.value = true
showDuck.value = true
//
const clientX = e.touches ? e.touches[0].clientX : e.clientX
const clientY = e.touches ? e.touches[0].clientY : e.clientY
// //
updateDuckPosition(clientX, clientY) ctx.value.setStrokeStyle('#e74c3c')
ctx.value.setLineWidth(2)
// ctx.value.setLineDash([5, 5])
document.removeEventListener('mousemove', onDrag) ctx.value.strokeRect(duckX.value - 3, duckY.value - 3, duckWidth + 6, duckHeight + 6)
document.removeEventListener('mouseup', endDrag) ctx.value.setLineDash([])
document.removeEventListener('touchmove', onDrag) }
document.removeEventListener('touchend', endDrag)
//
document.addEventListener('mousemove', onDrag)
document.addEventListener('mouseup', endDrag)
document.addEventListener('touchmove', onDrag, { passive: false })
document.addEventListener('touchend', endDrag)
} }
// //
const onDrag = (e) => { const checkTouchInDuck = (x, y) => {
return x >= duckX.value &&
x <= duckX.value + duckWidth &&
y >= duckY.value &&
y <= duckY.value + duckHeight
}
//
const checkDuckInTarget = () => {
const target = targetArea.value
//
const duckCenterX = duckX.value + duckWidth / 2
const duckCenterY = duckY.value + duckHeight / 2
//
return duckCenterX >= target.x &&
duckCenterX <= target.x + target.width &&
duckCenterY >= target.y &&
duckCenterY <= target.y + target.height
}
//
const handleTouchStart = (e) => {
console.log('触摸开始事件触发', e)
//
const touch = e.touches[0]
if (!touch) {
console.error('没有触摸点信息')
return
}
// Canvas
getCanvasPosition()
// Canvas
setTimeout(() => {
// Canvas
const touchX = touch.clientX - canvasRect.value.left
const touchY = touch.clientY - canvasRect.value.top
console.log(`触摸点相对 Canvas: (${touchX}, ${touchY})`)
console.log(`鸭子位置: (${duckX.value}, ${duckY.value})`)
//
if (checkTouchInDuck(touchX, touchY)) {
console.log('触摸点在鸭子内,开始拖拽')
//
isDragging.value = true
dragOffsetX.value = touchX - duckX.value
dragOffsetY.value = touchY - duckY.value
showDuck.value = true
} else {
console.log('触摸点不在鸭子内')
}
}, 50)
}
//
const handleTouchMove = (e) => {
if (!isDragging.value) return if (!isDragging.value) return
e.preventDefault() e.preventDefault()
const clientX = e.touches ? e.touches[0].clientX : e.clientX const touch = e.touches[0]
const clientY = e.touches ? e.touches[0].clientY : e.clientY if (!touch) return
updateDuckPosition(clientX, clientY) // Canvas
const touchX = touch.clientX - canvasRect.value.left
const touchY = touch.clientY - canvasRect.value.top
//
duckX.value = touchX - dragOffsetX.value
duckY.value = touchY - dragOffsetY.value
//
const canvasW = canvasRect.value.width || 400
const canvasH = canvasRect.value.height || 600
duckX.value = Math.max(0, Math.min(canvasW - duckWidth, duckX.value))
duckY.value = Math.max(0, Math.min(canvasH - duckHeight, duckY.value))
console.log(`鸭子新位置: (${duckX.value}, ${duckY.value})`)
} }
// - DOM //
const updateDuckPosition = (clientX, clientY) => { const handleTouchEnd = () => {
duckX = clientX console.log('触摸结束事件触发')
duckY = clientY
if (duckElement.value) {
// 使 transform3d GPU
duckElement.value.style.transform = `translate3d(${clientX}px, ${clientY}px, 0) translate(-50%, -50%) scale(1.2)`
}
}
// if (isDragging.value) {
const endDrag = () => { isDragging.value = false
if (!isDragging.value) return
//
// if (checkDuckInTarget()) {
const container = document.querySelector('.dongzhimen-scene-container') console.log('成功放入目标区域!')
if (container) {
const rect = container.getBoundingClientRect()
//
const duckRelX = duckX - rect.left
const duckRelY = duckY - rect.top
// rpx px (稿 750rpx rect.width )
const rpxToPx = rect.width / 750
const targetX = targetArea.x * rpxToPx
const targetY = targetArea.y * rpxToPx
const targetW = targetArea.width * rpxToPx
const targetH = targetArea.height * rpxToPx
const inTargetArea = (
duckRelX >= targetX &&
duckRelX <= targetX + targetW &&
duckRelY >= targetY &&
duckRelY <= targetY + targetH
)
if (inTargetArea) {
// //
deskImage.value = '/static/dzm/img_desk2.png' deskImage.value = '/static/dzm/img_desk2.png'
// //
showGuideElements.value = false showGuideElements.value = false
//
showDuck.value = false
} }
} }
}
//
showDuck.value = false //
isDragging.value = false const startDrag = (e) => {
console.log('开始拖拽', e)
//
document.removeEventListener('mousemove', onDrag) // Canvas
document.removeEventListener('mouseup', endDrag) if (!ctx.value) {
document.removeEventListener('touchmove', onDrag) initCanvas()
document.removeEventListener('touchend', endDrag) }
//
duckX.value = 50
duckY.value = 50
showDuck.value = true
//
handleTouchStart(e)
} }
// //
@ -164,17 +359,29 @@ onMounted(() => {
if (container) { if (container) {
container.classList.add('animate-in') container.classList.add('animate-in')
} }
// Canvas
setTimeout(() => {
initCanvas()
}, 500)
})
//
onUnmounted(() => {
//
if (animationId.value) {
cancelAnimationFrame(animationId.value)
}
}) })
</script> </script>
<template> <template>
<section class="dongzhimen-scene-container" :class="{ 'active': active }"> <view class="dongzhimen-scene-container" :class="{ 'active': active }">
<!-- 背景图片层 --> <!-- 背景图片层 -->
<div class="background-layer" :style="{ transform: `translateY(${parallaxOffset}px)` }"> <view class="background-layer" :style="{ transform: `translateY(${parallaxOffset}px)` }">
<!-- 使用东直门商圈背景图片 --> <image src="/static/bg/bg5.jpg" alt="东直门商圈" class="background-image" mode="widthFix" />
<img src="/static/bg/bg5.jpg" alt="东直门商圈" class="background-image" /> </view>
</div>
<!-- 福字点击区域 --> <!-- 福字点击区域 -->
<FuClickArea <FuClickArea
:visible="fuClickAreaVisible" :visible="fuClickAreaVisible"
@ -185,46 +392,46 @@ onMounted(() => {
:fu-height="100" :fu-height="100"
@click="handleFuClick" @click="handleFuClick"
/> />
<!-- sq3图片 --> <!-- sq3图片 -->
<img <image
v-if="sq3ImageVisible" v-if="sq3ImageVisible"
src="/static/images/sq5.png" src="/static/images/sq5.png"
alt="新春祝福" alt="新春祝福"
class="sq-image" class="sq-image"
mode="widthFix"
/> />
<!-- 装饰图片 --> <!-- 装饰图片 -->
<img :src="deskImage" alt="餐桌" class="deco-img desk-img" /> <image :src="deskImage" alt="餐桌" class="deco-img desk-img" mode="widthFix" />
<img src="/static/dzm/img_stove1.png" alt="灶台" class="deco-img stove-img" /> <image src="/static/dzm/img_stove1.png" alt="灶台" class="deco-img stove-img" mode="widthFix" />
<img v-if="showGuideElements" src="/static/dzm/img_line.png" alt="线条" class="deco-img line-img" /> <image v-if="showGuideElements" src="/static/dzm/img_line.png" alt="线条" class="deco-img line-img" mode="widthFix" />
<img v-if="showGuideElements" src="/static/images/icon_hand.png" alt="手势" class="deco-img hand-img" /> <image v-if="showGuideElements" src="/static/images/icon_hand.png" alt="手势" class="deco-img hand-img" mode="widthFix" />
<!-- Canvas 拖拽区域 -->
<canvas
canvas-id="dragDuckCanvas"
id="dragDuckCanvas"
class="drag-canvas"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
></canvas>
<!-- 拖拽触发区域 --> <!-- 拖拽触发区域 -->
<div <view
v-if="showGuideElements" v-if="showGuideElements"
class="drag-trigger-area" class="drag-trigger-area"
@mousedown="startDrag"
@touchstart="startDrag" @touchstart="startDrag"
></div> ></view>
</view>
<!-- 跟随拖拽的鸭子图片 -->
<img
v-show="showDuck"
src="/static/dzm/img_duck.png"
alt="烤鸭"
class="drag-duck"
ref="duckElement"
/>
</section>
</template> </template>
<style scoped> <style scoped>
.dongzhimen-scene-container { .dongzhimen-scene-container {
position: relative; position: relative;
width: 100%; width: 100%;
height: auto; /* 高度由内容决定 */ height: auto;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -232,12 +439,10 @@ onMounted(() => {
background-color: #ff6b35; background-color: #ff6b35;
} }
/* 背景图片层 */
.background-layer { .background-layer {
position: relative; position: relative;
width: 100%; width: 100%;
transition: transform 0.1s ease; transition: transform 0.1s ease;
/* 背景图片决定容器高度 */
height: auto; height: auto;
} }
@ -245,192 +450,16 @@ onMounted(() => {
width: 100%; width: 100%;
height: auto; height: auto;
display: block; display: block;
/* 确保图片完整显示,决定容器高度 */
object-fit: contain;
} }
/* 增强动效层 */ .sq-image {
.enhancement-layer {
position: absolute; position: absolute;
top: 0; top: 220rpx;
left: 0; right: -6rpx;
width: 100%; width: 300rpx;
height: 100%; height: auto;
pointer-events: none;
z-index: 10;
}
/* 灯笼增强动效 */
.lanterns {
position: absolute;
top: 15%;
width: 100%;
display: flex;
justify-content: space-between;
padding: 0 30px;
box-sizing: border-box;
}
.lantern {
font-size: 2.5rem;
animation: swing 3s infinite ease-in-out;
opacity: 0.9;
filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.8));
color: #ffd700;
}
.left-lantern {
animation-delay: 0s;
}
.right-lantern {
animation-delay: 1.5s;
}
@keyframes swing {
0%, 100% { transform: rotate(-10deg); }
50% { transform: rotate(10deg); }
}
/* 福字增强动效 */
.fu-word {
position: absolute;
top: 30%;
left: 65%;
transform: translateX(-50%) rotate(15deg);
font-size: 2rem;
color: #ffd700;
text-shadow: 2px 2px 10px rgba(255, 215, 0, 0.9);
animation: float 4s infinite ease-in-out;
}
@keyframes float {
0%, 100% { transform: translateX(-50%) rotate(15deg) translateY(0); }
50% { transform: translateX(-50%) rotate(15deg) translateY(-15px); }
}
/* 点击指示器 */
.click-indicator {
position: absolute;
top: 55%;
left: 75%;
transform: translate(-50%, -50%);
width: 60px;
height: 60px;
pointer-events: none;
}
.pulse-circle {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: rgba(255, 215, 0, 0.3);
border: 2px solid rgba(255, 215, 0, 0.6);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
transform: scale(0.8);
opacity: 0.8;
}
100% {
transform: scale(2);
opacity: 0;
}
}
.click-indicator.animate-pulse {
display: block;
}
/* 交互区域 */
.interaction-area {
position: absolute;
top: 55%;
right: 15%;
width: 120px;
height: 100px;
cursor: pointer;
z-index: 20; z-index: 20;
}
/* 响应式调整交互区域位置 */
@media (max-width: 640px) {
.interaction-area {
top: 52%;
right: 10%;
width: 100px;
height: 80px;
}
}
/* 烟花效果 */
.fireworks {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 20;
}
.firework {
position: absolute;
font-size: 2rem;
opacity: 0;
animation: firework 3s infinite;
}
.firework-1 {
top: 10%;
left: 20%;
animation-delay: 0s;
}
.firework-2 {
top: 15%;
right: 25%;
animation-delay: 1s;
}
.firework-3 {
top: 8%;
right: 15%;
animation-delay: 2s;
}
.firework-4 {
top: 12%;
left: 25%;
animation-delay: 3s;
}
@keyframes firework {
0%, 100% { opacity: 0; transform: scale(0); }
50% { opacity: 1; transform: scale(1.5); }
}
/* 福印收集标记 */
.seal-collected-mark {
position: absolute;
top: 20px;
right: 20px;
background-color: rgba(255, 107, 53, 0.9);
color: #fff;
padding: 10px 15px;
border-radius: 20px;
font-size: 14px;
display: flex;
align-items: center;
gap: 5px;
animation: fadeIn 0.5s ease; animation: fadeIn 0.5s ease;
z-index: 30;
}
.seal-icon {
font-size: 20px;
} }
@keyframes fadeIn { @keyframes fadeIn {
@ -438,29 +467,6 @@ onMounted(() => {
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
/* 入场动画 */
.dongzhimen-scene-container.animate-in {
animation: sceneFadeIn 1s ease-out;
}
@keyframes sceneFadeIn {
from { opacity: 0; transform: translateY(50px); }
to { opacity: 1; transform: translateY(0); }
}
/* sq图片 */
.sq-image {
position: absolute;
top: 220rpx;
right: -6rpx;
width: auto;
height: auto;
max-width: 300rpx;
z-index: 20;
animation: fadeIn 0.5s ease;
}
/* 装饰图片 */
.deco-img { .deco-img {
position: absolute; position: absolute;
z-index: 25; z-index: 25;
@ -470,85 +476,58 @@ onMounted(() => {
left: 13rpx; left: 13rpx;
top: 1665rpx; top: 1665rpx;
width: 441rpx; width: 441rpx;
height: 382rpx; height: auto;
} }
.stove-img { .stove-img {
left: 492rpx; left: 492rpx;
top: 1711rpx; top: 1711rpx;
width: 241rpx; width: 241rpx;
height: 363rpx; height: auto;
} }
.line-img { .line-img {
left: 250rpx; left: 250rpx;
top: 1842rpx; top: 1842rpx;
width: 360rpx; width: 360rpx;
height: 80rpx; height: auto;
} }
.hand-img { .hand-img {
left: 440rpx; left: 440rpx;
top: 1900rpx; top: 1900rpx;
width: 38rpx; width: 38rpx;
height: 40rpx; height: auto;
animation: arcSlideLeft 1.2s ease-in-out infinite; animation: arcSlideLeft 1.2s ease-in-out infinite;
} }
/* 向左弧形滑动动效 */
@keyframes arcSlideLeft { @keyframes arcSlideLeft {
0% { 0% {
transform: translateX(0) translateY(0); transform: translateX(0) translateY(0);
opacity: 1; opacity: 1;
} }
100% { 100% {
transform: translateX(-80rpx) translateY(3rpx); transform: translateX(-80rpx) translateY(3rpx);
opacity: 1; opacity: 1;
} }
} }
/* 拖拽触发区域 */ .drag-canvas {
position: absolute;
left: 0;
top: 800px;
width: 375px;
height: 300px;
z-index: 100;
pointer-events: auto;
}
.drag-trigger-area { .drag-trigger-area {
position: absolute; position: absolute;
left: 540rpx; left: 540rpx;
top: 1781rpx; top: 1781rpx;
width: 100rpx; width: 100rpx;
height: 100rpx; height: 100rpx;
cursor: grab;
z-index: 30; z-index: 30;
} }
</style>
.drag-trigger-area:active {
cursor: grabbing;
}
/* 跟随拖拽的鸭子图片 */
.drag-duck {
position: fixed;
left: 0;
top: 0;
width: 54rpx;
height: 105rpx;
opacity: 0.8;
pointer-events: none;
z-index: 1000;
will-change: transform;
}
/* 响应式设计 */
@media (max-width: 640px) {
.fu-word {
font-size: 1.5rem;
}
.lantern {
font-size: 2rem;
}
.interaction-area {
width: 100px;
height: 80px;
}
}
</style>

View File

@ -356,7 +356,7 @@ onMounted(() => {
.image-gallery { .image-gallery {
position: absolute; position: absolute;
left: 40rpx; left: 40rpx;
top: 1430rpx; top: 1380rpx;
width: 670rpx; width: 670rpx;
background-color: #d72717; background-color: #d72717;
border-radius: 20rpx; border-radius: 20rpx;

View File

@ -1,6 +1,6 @@
{ {
"name" : "xinchun2026", "name" : "xinchun2026",
"appid" : "", "appid" : "__UNI__F0A5E90",
"description" : "", "description" : "",
"versionName" : "1.0.0", "versionName" : "1.0.0",
"versionCode" : "100", "versionCode" : "100",
@ -68,5 +68,8 @@
"uniStatistics" : { "uniStatistics" : {
"enable" : false "enable" : false
}, },
"vueVersion" : "3" "vueVersion" : "3",
"h5" : {
"title" : "2026新春H5"
}
} }

View File

@ -14,7 +14,14 @@
"navigationBarTitleText": "四大特色主题区", "navigationBarTitleText": "四大特色主题区",
"navigationStyle": "default" "navigationStyle": "default"
} }
} },
{
"path": "pages/canvas3",
"style": {
"navigationBarTitleText": "四大特色主题区",
"navigationStyle": "default"
}
}
], ],
"globalStyle": { "globalStyle": {
"navigationBarTextStyle": "white", "navigationBarTextStyle": "white",

263
pages/canvas.vue Normal file
View File

@ -0,0 +1,263 @@
<template>
<view>
<page-head :title="title"></page-head>
<view class="page-body">
<view class="page-body-wrapper">
<!-- #ifdef APP-PLUS || H5 -->
<canvas canvas-id="canvas" class="canvas" :start="startStatus" :change:start="animate.start"
:data-width="canvasWidth" :data-height="canvasWidth"></canvas>
<!-- #endif -->
<!-- #ifndef APP-PLUS || H5 -->
<canvas canvas-id="canvas" id="canvas" class="canvas"></canvas>
<!-- #endif -->
</view>
</view>
</view>
</template>
<script module="animate" lang="renderjs">
function Ball({
x,
y,
vx,
vy,
canvasWidth,
canvasHeight,
ctx
}) {
this.canvasWidth = canvasWidth
this.canvasHeight = canvasHeight
this.ctx = ctx
this.x = x
this.y = y
this.vx = vx
this.vy = vy
this.radius = 5
}
Ball.prototype.draw = function() {
this.ctx.beginPath()
this.ctx.fillStyle = '#007AFF'
this.ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)
this.ctx.closePath()
this.ctx.fill()
}
Ball.prototype.move = function() {
this.x += this.vx
this.y += this.vy
//
// if (getDistance(this.x - this.canvasWidth / 2, this.y - this.canvasHeight / 2) >
// getDistance(this.canvasWidth / 2, this.canvasHeight / 2) + this.radius) {
// this.x = this.canvasWidth / 2
// this.y = this.canvasHeight / 2
// }
//
if (this.x < this.radius) {
this.vx = Math.abs(this.vx)
return
}
if (this.x > this.canvasWidth - this.radius) {
this.vx = -Math.abs(this.vx)
}
if (this.y < this.radius) {
this.vy = Math.abs(this.vy)
return
}
if (this.y > this.canvasWidth - this.radius) {
this.vy = -Math.abs(this.vy)
}
}
function getDistance(x, y) {
return Math.pow(Math.pow(x, 2) + Math.pow(y, 2), 0.5)
}
export default {
methods: {
start(newVal, oldVal, owner, ins) {
let canvasWidth = ins.getDataset().width,
canvasHeight = ins.getDataset().height,
canvasEle = document.querySelectorAll('.canvas>canvas')[0],
ctx = canvasEle.getContext('2d'),
speed = 3,
ballList = [],
layer = 3,
ballInlayer = 20
for (let i = 0; i < layer; i++) {
let radius = getDistance(canvasWidth / 2, canvasHeight / 2) / layer * i
for (let j = 0; j < ballInlayer; j++) {
let deg = j * 2 * Math.PI / ballInlayer,
sin = Math.sin(deg),
cos = Math.cos(deg),
x = radius * cos + canvasWidth / 2,
y = radius * sin + canvasHeight / 2,
vx = speed * cos,
vy = speed * sin
ballList.push(new Ball({
x,
y,
vx,
vy,
canvasWidth,
canvasHeight,
ctx,
radius: 5
}))
}
}
function animate(ballList) {
ctx.clearRect(0, 0, canvasEle.width, canvasEle.height)
ballList.forEach(function(item) {
item.move()
item.draw()
})
requestAnimationFrame(function() {
animate(ballList)
})
}
animate(ballList)
}
}
}
</script>
<script>
// #ifndef APP-PLUS || H5
let ctx = null,
interval = null;
function Ball(x, y, vx, vy, canvasWidth, canvasHeight, ctx) {
this.canvasWidth = canvasWidth
this.canvasHeight = canvasHeight
this.ctx = ctx
this.x = x
this.y = y
this.vx = vx
this.vy = vy
this.radius = 5
}
Ball.prototype.draw = function() {
this.ctx.setFillStyle('#007AFF')
this.ctx.beginPath()
this.ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)
this.ctx.closePath()
this.ctx.fill()
}
Ball.prototype.move = function() {
this.x += this.vx
this.y += this.vy
//
// if (getDistance(this.x - this.canvasWidth / 2, this.y - this.canvasHeight / 2) >
// getDistance(this.canvasWidth / 2, this.canvasHeight / 2) + this.radius) {
// this.x = this.canvasWidth / 2
// this.y = this.canvasHeight / 2
// }
//
if (this.x < this.radius) {
this.vx = Math.abs(this.vx)
return
}
if (this.x > this.canvasWidth - this.radius) {
this.vx = -Math.abs(this.vx)
}
if (this.y < this.radius) {
this.vy = Math.abs(this.vy)
return
}
if (this.y > this.canvasWidth - this.radius) {
this.vy = -Math.abs(this.vy)
}
}
function getDistance(x, y) {
return Math.pow(Math.pow(x, 2) + Math.pow(y, 2), 0.5)
}
// #endif
export default {
data() {
return {
title: 'canvas',
canvasWidth: 0,
startStatus: false,
ballList: []
}
},
onReady: function() {
this.$nextTick(() => {
uni.createSelectorQuery().select(".canvas").boundingClientRect(data => {
this.canvasWidth = data.width
// #ifdef APP-PLUS || H5
this.startStatus = true
// #endif
// #ifndef APP-PLUS || H5
ctx = uni.createCanvasContext('canvas')
this.drawBall()
// #endif
}).exec()
})
},
// #ifndef APP-PLUS || H5
onUnload: function() {
clearInterval(interval);
},
methods: {
drawBall: function() {
let canvasWidth = this.canvasWidth,
canvasHeight = this.canvasWidth,
speed = 3,
ballList = [],
layer = 3,
ballInlayer = 20
for (let i = 0; i < layer; i++) {
let radius = getDistance(canvasWidth / 2, canvasHeight / 2) / layer * i
for (let j = 0; j < ballInlayer; j++) {
let deg = j * 2 * Math.PI / ballInlayer,
sin = Math.sin(deg),
cos = Math.cos(deg),
x = radius * cos + canvasWidth / 2,
y = radius * sin + canvasHeight / 2,
vx = speed * cos,
vy = speed * sin
ballList.push(new Ball(x, y, vx, vy, canvasWidth, canvasHeight, ctx))
}
}
function animate(ballList) {
// ctx.clearRect(0, 0, canvasWidth, canvasHeight)
ballList.forEach(function(item) {
item.move()
item.draw()
})
ctx.draw()
}
interval = setInterval(function() {
animate(ballList)
}, 17)
}
}
// #endif
}
</script>
<style>
.page-body-wrapper {
text-align: center;
}
.canvas {
width: 610rpx;
height: 610rpx;
margin: auto;
background-color: #fff;
}
</style>

791
pages/canvas2.vue Normal file
View File

@ -0,0 +1,791 @@
<template>
<view class="canvas-container">
<view class="header">
<text class="title">Canvas图片拖拽示例</text>
<text class="subtitle">点击并拖动图片到目标区域</text>
</view>
<view class="main-content">
<view class="canvas-wrapper">
<canvas
canvas-id="imageCanvas"
id="imageCanvas"
class="canvas-element"
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
></canvas>
<view class="canvas-info">
<text>Canvas尺寸: {{ canvasWidth }} × {{ canvasHeight }} px</text>
<text>图片状态: {{ imageStatus }}</text>
</view>
</view>
<view class="controls">
<view class="control-card">
<text class="control-title">操作说明</text>
<view class="instructions">
<text>1. 点击并按住图片开始拖拽</text>
<text>2. 拖动图片到蓝色目标区域</text>
<text>3. 释放手指完成拖放</text>
</view>
<view class="image-url">
<text>图片URL:</text>
<text class="url-text">{{ imageUrl }}</text>
</view>
</view>
<view class="control-card">
<text class="control-title">目标区域</text>
<view :class="['target-area', { 'target-hit': isInTarget }]">
<text>目标位置: ({{ target.x }}, {{ target.y }})</text>
<text>尺寸: {{ target.width }} × {{ target.height }}</text>
<text v-if="isInTarget" class="success-text"> 图片已在目标区域内</text>
</view>
</view>
<view class="control-card">
<text class="control-title">状态信息</text>
<view class="status">
<text :class="['status-text', { 'dragging': isDragging }, { 'success': isInTarget }]">
{{ statusText }}
</text>
</view>
<view class="stats">
<view class="stat-item">
<text class="stat-value">{{ dragCount }}</text>
<text class="stat-label">拖拽次数</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ targetHits }}</text>
<text class="stat-label">命中目标</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ Math.round(dragDistance) }}</text>
<text class="stat-label">移动距离</text>
</view>
</view>
</view>
<view class="control-card">
<text class="control-title">图片位置</text>
<view class="position-info">
<text>X坐标: {{ image.x.toFixed(0) }} px</text>
<text>Y坐标: {{ image.y.toFixed(0) }} px</text>
<text>宽度: {{ image.width }} px</text>
<text>高度: {{ image.height }} px</text>
</view>
</view>
<button class="reset-btn" @tap="resetImage">重置图片位置</button>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
// Canvas
canvasWidth: 350,
canvasHeight: 500,
// Canvas
ctx: null,
// URL
imageUrl: 'https://static.vecteezy.com/system/resources/previews/026/105/544/non_2x/example-icon-design-free-vector.jpg',
//
image: {
x: 50,
y: 50,
width: 120,
height: 120,
isDragging: false,
dragOffsetX: 0,
dragOffsetY: 0,
startX: 0,
startY: 0,
img: null,
loaded: false
},
//
target: {
x: 150,
y: 200,
width: 140,
height: 100
},
// Canvas
canvasRect: {
left: 0,
top: 0,
width: 0,
height: 0
},
//
isDragging: false,
isInTarget: false,
dragCount: 0,
targetHits: 0,
dragDistance: 0,
statusText: '点击图片开始拖拽',
imageStatus: '加载中...',
// ID
animationFrameId: null,
lastRenderTime: 0,
renderInterval: 16 // ~60fps
}
},
onLoad() {
// Canvas
this.$nextTick(() => {
setTimeout(() => {
this.initCanvas()
}, 100)
})
},
onUnload() {
//
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId)
}
},
onShow() {
//
this.startAnimation()
},
onHide() {
//
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId)
this.animationFrameId = null
}
},
methods: {
// Canvas
initCanvas() {
// Canvas
this.ctx = uni.createCanvasContext('imageCanvas', this)
// Canvas
this.getCanvasPosition()
//
this.loadImage()
//
this.startAnimation()
},
// Canvas
getCanvasPosition() {
const query = uni.createSelectorQuery().in(this)
query.select('#imageCanvas').boundingClientRect(res => {
if (res) {
this.canvasRect = {
left: res.left,
top: res.top,
width: res.width,
height: res.height
}
}
}).exec()
},
//
loadImage() {
const img = new Image()
img.onload = () => {
console.log('图片加载成功')
this.image.img = img
this.image.loaded = true
this.imageStatus = '已加载'
this.statusText = '点击图片开始拖拽'
//
const maxWidth = 150
const maxHeight = 150
if (img.width > maxWidth || img.height > maxHeight) {
const ratio = Math.min(maxWidth / img.width, maxHeight / img.height)
this.image.width = img.width * ratio
this.image.height = img.height * ratio
}
}
img.onerror = () => {
console.error('图片加载失败,使用备选方案')
this.imageStatus = '加载失败,使用备选图片'
this.createFallbackImage()
}
this.createFallbackImage()
// img.src = '/static/dzm/img_duck.png'
},
//
createFallbackImage() {
// Canvas
const fallbackCanvas = document.createElement('canvas')
const fallbackCtx = fallbackCanvas.getContext('2d')
fallbackCanvas.width = this.image.width
fallbackCanvas.height = this.image.height
//
fallbackCtx.fillStyle = '#FF8C00'
fallbackCtx.fillRect(0, 0, this.image.width, this.image.height)
//
fallbackCtx.strokeStyle = '#333'
fallbackCtx.lineWidth = 2
fallbackCtx.strokeRect(0, 0, this.image.width, this.image.height)
//
fallbackCtx.fillStyle = 'white'
fallbackCtx.font = 'bold 14px Arial'
fallbackCtx.textAlign = 'center'
fallbackCtx.textBaseline = 'middle'
fallbackCtx.fillText('示例', this.image.width / 2, this.image.height / 2 - 10)
fallbackCtx.font = '12px Arial'
fallbackCtx.fillText('图片', this.image.width / 2, this.image.height / 2 + 10)
//
const img = new Image()
img.src = fallbackCanvas.toDataURL('image/png')
img.onload = () => {
this.image.img = img
this.image.loaded = true
}
},
//
startAnimation() {
const animate = (timestamp) => {
if (timestamp - this.lastRenderTime >= this.renderInterval) {
this.drawCanvas()
this.lastRenderTime = timestamp
}
this.animationFrameId = requestAnimationFrame(animate)
}
this.animationFrameId = requestAnimationFrame(animate)
},
// Canvas
drawCanvas() {
if (!this.ctx) return
// Canvas
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
//
this.drawTargetArea()
//
this.drawImage()
//
this.drawInfo()
//
this.ctx.draw(true)
},
//
drawTargetArea() {
this.ctx.setFillStyle(this.isInTarget ? 'rgba(46, 204, 113, 0.2)' : 'rgba(52, 152, 219, 0.1)')
this.ctx.setStrokeStyle(this.isInTarget ? '#2ecc71' : '#3498db')
this.ctx.setLineWidth(2)
this.ctx.setLineDash([5, 5])
this.ctx.fillRect(this.target.x, this.target.y, this.target.width, this.target.height)
this.ctx.strokeRect(this.target.x, this.target.y, this.target.width, this.target.height)
this.ctx.setLineDash([])
//
this.ctx.setFontSize(12)
this.ctx.setFillStyle('#3498db')
this.ctx.setTextAlign('center')
this.ctx.fillText(
'目标区域',
this.target.x + this.target.width / 2,
this.target.y + this.target.height / 2
)
},
//
drawImage() {
if (this.image.loaded && this.image.img) {
//
this.ctx.drawImage(
this.image.img,
this.image.x,
this.image.y,
this.image.width,
this.image.height
)
//
if (this.image.isDragging) {
this.ctx.setStrokeStyle('#e74c3c')
this.ctx.setLineWidth(3)
this.ctx.setLineDash([5, 5])
this.ctx.strokeRect(
this.image.x - 5,
this.image.y - 5,
this.image.width + 10,
this.image.height + 10
)
this.ctx.setLineDash([])
}
} else {
//
this.ctx.setFillStyle('#FF8C00')
this.ctx.fillRect(this.image.x, this.image.y, this.image.width, this.image.height)
this.ctx.setStrokeStyle('#333')
this.ctx.setLineWidth(2)
this.ctx.strokeRect(this.image.x, this.image.y, this.image.width, this.image.height)
this.ctx.setFontSize(14)
this.ctx.setFillStyle('#FFFFFF')
this.ctx.setTextAlign('center')
this.ctx.fillText(
'加载中...',
this.image.x + this.image.width / 2,
this.image.y + this.image.height / 2
)
}
},
//
drawInfo() {
this.ctx.setFontSize(10)
this.ctx.setFillStyle('#666')
this.ctx.setTextAlign('left')
this.ctx.setTextBaseline('top')
//
this.ctx.fillText(
`图片位置: (${Math.round(this.image.x)}, ${Math.round(this.image.y)})`,
10, 10
)
//
this.ctx.fillText(
`目标位置: (${this.target.x}, ${this.target.y})`,
10, 25
)
//
this.ctx.fillText(
`拖拽次数: ${this.dragCount}`,
10, 40
)
},
//
isPointInImage(x, y) {
return x >= this.image.x &&
x <= this.image.x + this.image.width &&
y >= this.image.y &&
y <= this.image.y + this.image.height
},
//
checkImageInTarget() {
const img = this.image
const imgCenterX = img.x + img.width / 2
const imgCenterY = img.y + img.height / 2
const hit =
imgCenterX >= this.target.x &&
imgCenterX <= this.target.x + this.target.width &&
imgCenterY >= this.target.y &&
imgCenterY <= this.target.y + this.target.height
this.isInTarget = hit
return hit
},
//
calculateDistance(x1, y1, x2, y2) {
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
},
//
handleTouchStart(e) {
const touch = e.touches[0]
if (!touch) return
// Canvas
this.getCanvasPosition()
// Canvas
setTimeout(() => {
const touchX = touch.clientX - this.canvasRect.left
const touchY = touch.clientY - this.canvasRect.top
if (this.isPointInImage(touchX, touchY)) {
this.image.isDragging = true
this.image.dragOffsetX = touchX - this.image.x
this.image.dragOffsetY = touchY - this.image.y
this.image.startX = this.image.x
this.image.startY = this.image.y
this.isDragging = true
this.statusText = '正在拖拽图片...'
}
}, 50)
},
//
handleTouchMove(e) {
if (!this.image.isDragging) return
e.preventDefault()
const touch = e.touches[0]
if (!touch) return
const touchX = touch.clientX - this.canvasRect.left
const touchY = touch.clientY - this.canvasRect.top
//
const distance = this.calculateDistance(
this.image.x,
this.image.y,
touchX - this.image.dragOffsetX,
touchY - this.image.dragOffsetY
)
this.dragDistance += distance
//
this.image.x = touchX - this.image.dragOffsetX
this.image.y = touchY - this.image.dragOffsetY
//
this.image.x = Math.max(0, Math.min(
this.canvasWidth - this.image.width,
this.image.x
))
this.image.y = Math.max(0, Math.min(
this.canvasHeight - this.image.height,
this.image.y
))
//
this.checkImageInTarget()
},
//
handleTouchEnd() {
if (this.image.isDragging) {
this.image.isDragging = false
this.isDragging = false
this.dragCount++
if (this.checkImageInTarget()) {
this.targetHits++
this.statusText = '图片已在目标区域内'
} else {
this.statusText = '拖拽完成'
}
}
},
//
resetImage() {
this.image.x = 50
this.image.y = 50
this.image.isDragging = false
this.isDragging = false
this.isInTarget = false
this.statusText = '位置已重置,点击图片开始拖拽'
}
}
}
</script>
<style>
.canvas-container {
padding: 20px;
background-color: #f5f7fa;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif;
}
.header {
text-align: center;
margin-bottom: 20px;
padding: 20px;
background-color: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.title {
display: block;
font-size: 22px;
font-weight: 700;
color: #2c3e50;
margin-bottom: 8px;
}
.subtitle {
display: block;
font-size: 16px;
color: #7f8c8d;
}
.main-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.canvas-wrapper {
background-color: white;
border-radius: 12px;
padding: 15px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
align-items: center;
}
.canvas-element {
border-radius: 8px;
background-color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
touch-action: none;
}
.canvas-info {
margin-top: 10px;
padding: 10px;
background-color: #f8f9fa;
border-radius: 8px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.canvas-info text {
font-size: 12px;
color: #666;
text-align: center;
}
.controls {
display: flex;
flex-direction: column;
gap: 15px;
}
.control-card {
background-color: white;
border-radius: 12px;
padding: 15px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.control-title {
font-size: 16px;
font-weight: 600;
color: #2c3e50;
margin-bottom: 12px;
display: block;
padding-bottom: 8px;
border-bottom: 2px solid #f0f0f0;
}
.instructions {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 15px;
}
.instructions text {
display: block;
color: #555;
font-size: 14px;
line-height: 1.5;
}
.image-url {
background-color: #f8f9fa;
padding: 10px;
border-radius: 6px;
display: flex;
flex-direction: column;
gap: 5px;
}
.image-url text {
font-size: 12px;
color: #666;
}
.url-text {
word-break: break-all;
color: #3498db !important;
font-family: monospace;
font-size: 11px !important;
}
.target-area {
margin-top: 10px;
padding: 12px;
border: 2px dashed #3498db;
border-radius: 8px;
background-color: #e8f4fc;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
transition: all 0.3s;
}
.target-area text {
display: block;
color: #3498db;
font-size: 13px;
text-align: center;
}
.target-hit {
background-color: #d1f2eb;
border-color: #2ecc71;
border-style: solid;
}
.success-text {
color: #27ae60 !important;
font-weight: bold;
margin-top: 5px !important;
}
.status {
padding: 12px;
border-radius: 8px;
background-color: #f8f9fa;
margin-bottom: 15px;
}
.status-text {
display: block;
text-align: center;
font-weight: 500;
color: #666;
}
.status-text.dragging {
color: #e67e22;
background-color: #fff9e6;
padding: 8px;
border-radius: 6px;
}
.status-text.success {
color: #27ae60;
background-color: #d4f8e8;
padding: 8px;
border-radius: 6px;
}
.stats {
display: flex;
justify-content: space-between;
background-color: #f8f9fa;
padding: 12px;
border-radius: 8px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.stat-value {
font-size: 18px;
font-weight: 700;
color: #3498db;
margin-bottom: 4px;
}
.stat-label {
font-size: 11px;
color: #7f8c8d;
}
.position-info {
display: flex;
flex-direction: column;
gap: 8px;
background-color: #f8f9fa;
padding: 12px;
border-radius: 8px;
}
.position-info text {
display: block;
font-size: 13px;
color: #666;
}
.reset-btn {
background-color: #3498db;
color: white;
border: none;
padding: 15px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
text-align: center;
transition: all 0.3s;
margin-top: 10px;
}
.reset-btn:active {
background-color: #2980b9;
transform: scale(0.98);
}
@media (min-width: 768px) {
.main-content {
flex-direction: row;
}
.canvas-wrapper {
flex: 1;
}
.controls {
flex: 1;
min-width: 300px;
}
}
</style>

829
pages/canvas3.vue Normal file
View File

@ -0,0 +1,829 @@
<template>
<view class="canvas-container">
<view class="header">
<text class="title">Canvas拖拽功能完全修复版</text>
<text class="subtitle">已修复方块不跟随拖动的根本问题</text>
</view>
<view class="main-content">
<view class="canvas-wrapper">
<canvas
canvas-id="dragCanvas"
id="dragCanvas"
class="canvas-element"
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
></canvas>
<view class="canvas-status">
<text>{{ statusText }}</text>
</view>
</view>
<view class="controls">
<view class="control-section">
<text class="section-title">操作说明</text>
<view class="instructions">
<view class="instruction-item">
<view class="step-number">1</view>
<text>点击橙色方块</text>
</view>
<view class="instruction-item">
<view class="step-number">2</view>
<text>拖动到蓝色目标区域</text>
</view>
<view class="instruction-item">
<view class="step-number">3</view>
<text>释放手指完成拖放</text>
</view>
</view>
</view>
<view class="control-section">
<text class="section-title">调试信息</text>
<view class="debug-info">
<text class="debug-item">方块位置: X={{ rect.x.toFixed(0) }}, Y={{ rect.y.toFixed(0) }}</text>
<text class="debug-item">触摸位置: X={{ touchPos.x.toFixed(0) }}, Y={{ touchPos.y.toFixed(0) }}</text>
<text class="debug-item">触摸状态: {{ isDragging ? '拖拽中' : '未拖拽' }}</text>
<text class="debug-item">事件触发: {{ lastEvent }}</text>
</view>
</view>
<view class="control-section">
<text class="section-title">目标区域</text>
<view :class="['target-display', { 'target-hit': isInTarget }]">
<text>目标: ({{ target.x }}, {{ target.y }})</text>
<text>尺寸: {{ target.width }}×{{ target.height }}</text>
<text v-if="isInTarget" class="success-text"> 成功放入目标!</text>
</view>
</view>
<view class="control-section">
<text class="section-title">统计数据</text>
<view class="stats">
<view class="stat-box">
<text class="stat-value">{{ dragCount }}</text>
<text class="stat-label">拖拽次数</text>
</view>
<view class="stat-box">
<text class="stat-value">{{ successCount }}</text>
<text class="stat-label">成功次数</text>
</view>
<view class="stat-box">
<text class="stat-value">{{ Math.round(dragDistance) }}</text>
<text class="stat-label">拖拽距离</text>
</view>
</view>
</view>
<view class="buttons">
<button class="btn reset-btn" @tap="resetAll">重置所有</button>
<button class="btn debug-btn" @tap="toggleDebug">调试模式: {{ debugMode ? '开启' : '关闭' }}</button>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
// Canvas
canvasWidth: 350,
canvasHeight: 500,
// Canvas
ctx: null,
//
duckImage: null,
//
rect: {
x: 50,
y: 50,
width: 54,
height: 105,
color: '#FF8C00',
isDragging: false,
dragOffsetX: 0,
dragOffsetY: 0,
lastX: 50,
lastY: 50
},
//
target: {
x: 150,
y: 200,
width: 130,
height: 90,
color: 'rgba(52, 152, 219, 0.3)'
},
//
touchPos: {
x: 0,
y: 0,
clientX: 0,
clientY: 0
},
// Canvas
canvasRect: {
left: 0,
top: 0,
width: 0,
height: 0
},
//
isDragging: false,
isInTarget: false,
dragCount: 0,
successCount: 0,
dragDistance: 0,
statusText: '点击橙色方块开始拖拽',
lastEvent: '无',
debugMode: true,
//
animationId: null,
lastRenderTime: 0,
renderInterval: 16 // ~60fps
}
},
onLoad() {
// Canvas
this.$nextTick(() => {
setTimeout(() => {
this.initCanvas()
this.getCanvasPosition()
}, 100)
})
},
onUnload() {
//
if (this.animationId) {
cancelAnimationFrame(this.animationId)
}
},
onShow() {
//
this.startAnimation()
},
onHide() {
//
if (this.animationId) {
cancelAnimationFrame(this.animationId)
this.animationId = null
}
},
methods: {
// Canvas
initCanvas() {
console.log('正在初始化Canvas...')
// uni-app使uni.createCanvasContext
this.ctx = uni.createCanvasContext('dragCanvas', this)
if (this.ctx) {
console.log('Canvas上下文创建成功')
this.lastEvent = 'Canvas初始化成功'
//
this.loadDuckImage()
} else {
console.error('Canvas上下文创建失败')
this.lastEvent = 'Canvas初始化失败'
}
},
//
loadDuckImage() {
// 使 uni.getImageInfo
uni.getImageInfo({
src: '/static/dzm/img_duck.png',
success: (res) => {
console.log('鸭子图片加载成功:', res)
this.duckImage = res.path
this.lastEvent = '图片加载成功'
},
fail: (err) => {
console.error('鸭子图片加载失败:', err)
this.lastEvent = '图片加载失败'
}
})
},
// Canvas -
getCanvasPosition() {
const query = uni.createSelectorQuery().in(this)
query.select('#dragCanvas').boundingClientRect(res => {
if (res) {
this.canvasRect = {
left: res.left,
top: res.top,
width: res.width,
height: res.height
}
console.log('Canvas位置信息:', this.canvasRect)
this.lastEvent = `Canvas位置: left=${res.left.toFixed(1)}, top=${res.top.toFixed(1)}`
} else {
console.error('无法获取Canvas位置信息')
this.lastEvent = '无法获取Canvas位置'
}
}).exec()
},
//
startAnimation() {
const animate = (timestamp) => {
//
if (timestamp - this.lastRenderTime >= this.renderInterval) {
this.drawCanvas()
this.lastRenderTime = timestamp
}
this.animationId = requestAnimationFrame(animate)
}
this.animationId = requestAnimationFrame(animate)
},
// Canvas
drawCanvas() {
if (!this.ctx) return
// Canvas
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
//
this.drawTarget()
//
this.drawDraggableRect()
//
this.drawDebugInfo()
//
this.ctx.draw(true) // true使
},
//
drawTarget() {
//
this.ctx.setFillStyle(this.target.color)
this.ctx.fillRect(this.target.x, this.target.y, this.target.width, this.target.height)
//
this.ctx.setStrokeStyle(this.isInTarget ? '#2ecc71' : '#3498db')
this.ctx.setLineWidth(2)
this.ctx.setLineDash([5, 5])
this.ctx.strokeRect(this.target.x, this.target.y, this.target.width, this.target.height)
this.ctx.setLineDash([])
//
this.ctx.setFontSize(14)
this.ctx.setFillStyle('#3498db')
this.ctx.setTextAlign('center')
this.ctx.fillText(
'目标区域',
this.target.x + this.target.width / 2,
this.target.y + this.target.height / 2
)
},
//
drawDraggableRect() {
const rect = this.rect
//
if (this.duckImage) {
this.ctx.drawImage(this.duckImage, rect.x, rect.y, rect.width, rect.height)
} else {
//
this.ctx.setFillStyle(rect.color)
this.ctx.fillRect(rect.x, rect.y, rect.width, rect.height)
//
this.ctx.setStrokeStyle('#333')
this.ctx.setLineWidth(2)
this.ctx.strokeRect(rect.x, rect.y, rect.width, rect.height)
// ""
this.ctx.setFontSize(20)
this.ctx.setFillStyle('#FFFFFF')
this.ctx.setTextAlign('center')
this.ctx.setTextBaseline('middle')
this.ctx.fillText(
'拖我',
rect.x + rect.width / 2,
rect.y + rect.height / 2
)
}
//
if (rect.isDragging) {
//
this.ctx.setFillStyle('rgba(231, 76, 60, 0.3)')
this.ctx.fillRect(rect.x, rect.y, rect.width, rect.height)
//
this.ctx.setStrokeStyle('#e74c3c')
this.ctx.setLineWidth(3)
this.ctx.setLineDash([5, 5])
this.ctx.strokeRect(rect.x - 5, rect.y - 5, rect.width + 10, rect.height + 10)
this.ctx.setLineDash([])
}
},
//
drawDebugInfo() {
if (!this.debugMode) return
this.ctx.setFontSize(12)
this.ctx.setFillStyle('#666666')
this.ctx.setTextAlign('left')
//
this.ctx.fillText(
`方块: (${Math.round(this.rect.x)}, ${Math.round(this.rect.y)})`,
10, 20
)
//
this.ctx.fillText(
`触摸: (${Math.round(this.touchPos.x)}, ${Math.round(this.touchPos.y)})`,
10, 40
)
//
this.ctx.fillText(
`状态: ${this.isDragging ? '拖拽中' : '静止'}`,
10, 60
)
//
this.ctx.fillText(
`次数: ${this.dragCount}`,
10, 80
)
},
//
checkTouchInRect(x, y) {
const rect = this.rect
return x >= rect.x &&
x <= rect.x + rect.width &&
y >= rect.y &&
y <= rect.y + rect.height
},
//
checkRectInTarget() {
const rect = this.rect
const target = this.target
//
const rectCenterX = rect.x + rect.width / 2
const rectCenterY = rect.y + rect.height / 2
//
const isInTarget =
rectCenterX >= target.x &&
rectCenterX <= target.x + target.width &&
rectCenterY >= target.y &&
rectCenterY <= target.y + target.height
this.isInTarget = isInTarget
return isInTarget
},
//
calculateDistance(x1, y1, x2, y2) {
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
},
//
handleTouchStart(e) {
console.log('触摸开始事件触发', e)
this.lastEvent = 'touchstart'
//
const touch = e.touches[0]
if (!touch) {
console.error('没有触摸点信息')
return
}
// Canvas
this.getCanvasPosition()
// Canvas
setTimeout(() => {
// Canvas
const touchX = touch.clientX - this.canvasRect.left
const touchY = touch.clientY - this.canvasRect.top
console.log(`触摸点 - 客户端: (${touch.clientX}, ${touch.clientY})`)
console.log(`Canvas位置: left=${this.canvasRect.left}, top=${this.canvasRect.top}`)
console.log(`触摸点相对Canvas: (${touchX}, ${touchY})`)
//
this.touchPos = {
x: touchX,
y: touchY,
clientX: touch.clientX,
clientY: touch.clientY
}
//
if (this.checkTouchInRect(touchX, touchY)) {
console.log('触摸点在方块内,开始拖拽')
//
this.rect.isDragging = true
this.rect.dragOffsetX = touchX - this.rect.x
this.rect.dragOffsetY = touchY - this.rect.y
this.rect.lastX = this.rect.x
this.rect.lastY = this.rect.y
this.isDragging = true
this.statusText = '拖拽中...'
console.log(`拖拽偏移量: offsetX=${this.rect.dragOffsetX}, offsetY=${this.rect.dragOffsetY}`)
} else {
console.log('触摸点不在方块内')
this.statusText = '请点击橙色方块'
}
}, 50)
},
// -
handleTouchMove(e) {
//
if (!this.rect.isDragging) {
return
}
e.preventDefault()
e.stopPropagation()
this.lastEvent = 'touchmove'
const touch = e.touches[0]
if (!touch) return
// Canvas
const touchX = touch.clientX - this.canvasRect.left
const touchY = touch.clientY - this.canvasRect.top
//
this.touchPos = {
x: touchX,
y: touchY,
clientX: touch.clientX,
clientY: touch.clientY
}
//
const distance = this.calculateDistance(
this.rect.x,
this.rect.y,
touchX - this.rect.dragOffsetX,
touchY - this.rect.dragOffsetY
)
this.dragDistance += distance
//
const newX = touchX - this.rect.dragOffsetX
const newY = touchY - this.rect.dragOffsetY
console.log(`触摸移动: 新位置(${newX}, ${newY}),旧位置(${this.rect.x}, ${this.rect.y})`)
//
this.rect.x = newX
this.rect.y = newY
//
this.rect.x = Math.max(0, Math.min(
this.canvasWidth - this.rect.width,
this.rect.x
))
this.rect.y = Math.max(0, Math.min(
this.canvasHeight - this.rect.height,
this.rect.y
))
//
this.checkRectInTarget()
// -
this.forceRedraw()
},
//
forceRedraw() {
if (this.ctx) {
//
this.drawCanvas()
}
},
//
handleTouchEnd() {
console.log('触摸结束事件触发')
this.lastEvent = 'touchend'
if (this.rect.isDragging) {
this.rect.isDragging = false
this.isDragging = false
//
this.dragCount++
//
if (this.checkRectInTarget()) {
this.successCount++
this.statusText = '成功放入目标区域!'
} else {
this.statusText = '拖拽完成,未放入目标区域'
}
console.log(`拖拽结束,总次数: ${this.dragCount}`)
//
this.forceRedraw()
}
},
//
resetAll() {
this.rect.x = 50
this.rect.y = 50
this.rect.isDragging = false
this.isDragging = false
this.isInTarget = false
this.dragCount = 0
this.successCount = 0
this.dragDistance = 0
this.statusText = '已重置,点击橙色方块开始拖拽'
this.lastEvent = '重置'
// Canvas
this.getCanvasPosition()
//
this.forceRedraw()
},
//
toggleDebug() {
this.debugMode = !this.debugMode
this.forceRedraw()
}
}
}
</script>
<style>
.canvas-container {
padding: 20px;
background-color: #f5f7fa;
min-height: 100vh;
}
.header {
text-align: center;
margin-bottom: 20px;
padding: 20px;
background-color: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.title {
display: block;
font-size: 20px;
font-weight: 700;
color: #2c3e50;
margin-bottom: 8px;
}
.subtitle {
display: block;
font-size: 14px;
color: #7f8c8d;
}
.main-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.canvas-wrapper {
background-color: white;
border-radius: 12px;
padding: 15px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
align-items: center;
}
.canvas-element {
border-radius: 8px;
background-color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
touch-action: none;
}
.canvas-status {
margin-top: 10px;
padding: 10px 15px;
background-color: #f8f9fa;
border-radius: 8px;
width: 100%;
text-align: center;
}
.canvas-status text {
font-size: 14px;
color: #495057;
font-weight: 500;
}
.controls {
display: flex;
flex-direction: column;
gap: 15px;
}
.control-section {
background-color: white;
border-radius: 12px;
padding: 15px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #2c3e50;
margin-bottom: 12px;
display: block;
padding-bottom: 8px;
border-bottom: 2px solid #f0f0f0;
}
.instructions {
display: flex;
flex-direction: column;
gap: 10px;
}
.instruction-item {
display: flex;
align-items: center;
gap: 10px;
}
.step-number {
width: 24px;
height: 24px;
background-color: #3498db;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
}
.debug-info {
display: flex;
flex-direction: column;
gap: 6px;
background-color: #f8f9fa;
padding: 12px;
border-radius: 8px;
}
.debug-item {
font-size: 12px;
color: #666;
font-family: monospace;
}
.target-display {
background-color: #e8f4fc;
border: 2px dashed #3498db;
border-radius: 8px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 6px;
transition: all 0.3s;
}
.target-display.target-hit {
background-color: #d1f2eb;
border-color: #2ecc71;
border-style: solid;
}
.target-display text {
font-size: 13px;
color: #3498db;
}
.success-text {
color: #27ae60 !important;
font-weight: 600;
margin-top: 5px;
}
.stats {
display: flex;
justify-content: space-between;
gap: 10px;
}
.stat-box {
flex: 1;
background-color: #f8f9fa;
border-radius: 8px;
padding: 12px;
display: flex;
flex-direction: column;
align-items: center;
}
.stat-value {
font-size: 20px;
font-weight: 700;
color: #3498db;
margin-bottom: 4px;
}
.stat-label {
font-size: 11px;
color: #7f8c8d;
}
.buttons {
display: flex;
gap: 10px;
}
.btn {
flex: 1;
padding: 12px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
text-align: center;
border: none;
transition: all 0.2s;
}
.btn:active {
transform: scale(0.98);
}
.reset-btn {
background-color: #3498db;
color: white;
}
.debug-btn {
background-color: #6c757d;
color: white;
}
@media (min-width: 768px) {
.main-content {
flex-direction: row;
}
.canvas-wrapper {
flex: 1;
}
.controls {
flex: 1;
min-width: 300px;
}
}
</style>

459
pages/testcanvas.vue Normal file
View File

@ -0,0 +1,459 @@
<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>