parent
9c83d6a588
commit
c0897a3d86
|
|
@ -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>
|
||||
|
|
@ -1,22 +1,22 @@
|
|||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, computed, nextTick, getCurrentInstance as vueGetCurrentInstance } from 'vue'
|
||||
import FuClickArea from './FuClickArea.vue'
|
||||
|
||||
// 获取 Vue 实例
|
||||
const instance = vueGetCurrentInstance()
|
||||
|
||||
// 组件属性
|
||||
const props = defineProps({
|
||||
// 是否为活动状态
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 滚动位置,用于实现视差效果
|
||||
scrollPosition: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
// 组件事件
|
||||
const emit = defineEmits(['collect-seal'])
|
||||
|
||||
// 是否收集福印
|
||||
|
|
@ -28,7 +28,6 @@ const sq3ImageVisible = ref(false)
|
|||
|
||||
// 计算视差效果的偏移量
|
||||
const parallaxOffset = computed(() => {
|
||||
// 滚动位置的1/10作为视差偏移
|
||||
return props.scrollPosition * 0.1
|
||||
})
|
||||
|
||||
|
|
@ -41,120 +40,316 @@ const handleFuClick = () => {
|
|||
|
||||
// 拖拽状态
|
||||
const isDragging = ref(false)
|
||||
const showDuck = ref(false)
|
||||
const showDuck = ref(true) // 调试时默认显示鸭子
|
||||
const deskImage = ref('/static/dzm/img_desk1.png')
|
||||
const showGuideElements = ref(true)
|
||||
|
||||
// 鸭子元素引用
|
||||
const duckElement = ref(null)
|
||||
// Canvas 上下文
|
||||
const ctx = ref(null)
|
||||
|
||||
// 鸭子当前位置(使用普通变量,避免响应式带来的性能开销)
|
||||
let duckX = 0
|
||||
let duckY = 0
|
||||
// 鸭子图片路径
|
||||
const duckImagePath = ref(null)
|
||||
|
||||
// 拖拽开始区域 (540, 1781, 100, 100) 的中心点
|
||||
const dragStartArea = { x: 590, y: 1831 }
|
||||
// 鸭子位置(相对于 Canvas)
|
||||
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) => {
|
||||
e.preventDefault()
|
||||
// 拖拽偏移量
|
||||
const dragOffsetX = ref(0)
|
||||
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) {
|
||||
endDrag()
|
||||
// 绘制拖拽阴影
|
||||
ctx.value.setFillStyle('rgba(231, 76, 60, 0.3)')
|
||||
ctx.value.fillRect(duckX.value, duckY.value, duckWidth, duckHeight)
|
||||
|
||||
// 绘制拖拽边框
|
||||
ctx.value.setStrokeStyle('#e74c3c')
|
||||
ctx.value.setLineWidth(2)
|
||||
ctx.value.setLineDash([5, 5])
|
||||
ctx.value.strokeRect(duckX.value - 3, duckY.value - 3, duckWidth + 6, duckHeight + 6)
|
||||
ctx.value.setLineDash([])
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// 先移除可能存在的旧监听器,防止重复绑定
|
||||
document.removeEventListener('mousemove', onDrag)
|
||||
document.removeEventListener('mouseup', endDrag)
|
||||
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
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
const clientX = e.touches ? e.touches[0].clientX : e.clientX
|
||||
const clientY = e.touches ? e.touches[0].clientY : e.clientY
|
||||
const touch = e.touches[0]
|
||||
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) => {
|
||||
duckX = clientX
|
||||
duckY = clientY
|
||||
// 触摸结束事件
|
||||
const handleTouchEnd = () => {
|
||||
console.log('触摸结束事件触发')
|
||||
|
||||
if (duckElement.value) {
|
||||
// 使用 transform3d 启用 GPU 加速
|
||||
duckElement.value.style.transform = `translate3d(${clientX}px, ${clientY}px, 0) translate(-50%, -50%) scale(1.2)`
|
||||
}
|
||||
}
|
||||
if (isDragging.value) {
|
||||
isDragging.value = false
|
||||
|
||||
// 结束拖拽
|
||||
const endDrag = () => {
|
||||
if (!isDragging.value) return
|
||||
|
||||
// 获取容器在视口中的位置
|
||||
const container = document.querySelector('.dongzhimen-scene-container')
|
||||
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) {
|
||||
// 检查是否成功放入目标区域
|
||||
if (checkDuckInTarget()) {
|
||||
console.log('成功放入目标区域!')
|
||||
// 更换餐桌图片
|
||||
deskImage.value = '/static/dzm/img_desk2.png'
|
||||
// 隐藏引导元素
|
||||
showGuideElements.value = false
|
||||
// 隐藏鸭子
|
||||
showDuck.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏鸭子
|
||||
showDuck.value = false
|
||||
isDragging.value = false
|
||||
// 开始拖拽(从触发区域开始)
|
||||
const startDrag = (e) => {
|
||||
console.log('开始拖拽', e)
|
||||
|
||||
// 移除全局事件监听
|
||||
document.removeEventListener('mousemove', onDrag)
|
||||
document.removeEventListener('mouseup', endDrag)
|
||||
document.removeEventListener('touchmove', onDrag)
|
||||
document.removeEventListener('touchend', endDrag)
|
||||
// 初始化 Canvas
|
||||
if (!ctx.value) {
|
||||
initCanvas()
|
||||
}
|
||||
|
||||
// 设置鸭子初始位置(在触发区域附近)
|
||||
duckX.value = 50
|
||||
duckY.value = 50
|
||||
showDuck.value = true
|
||||
|
||||
// 触发触摸开始事件处理
|
||||
handleTouchStart(e)
|
||||
}
|
||||
|
||||
// 页面挂载时的初始化
|
||||
|
|
@ -164,16 +359,28 @@ onMounted(() => {
|
|||
if (container) {
|
||||
container.classList.add('animate-in')
|
||||
}
|
||||
|
||||
// 调试时自动初始化 Canvas
|
||||
setTimeout(() => {
|
||||
initCanvas()
|
||||
}, 500)
|
||||
})
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
// 停止动画
|
||||
if (animationId.value) {
|
||||
cancelAnimationFrame(animationId.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<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)` }">
|
||||
<!-- 使用东直门商圈背景图片 -->
|
||||
<img src="/static/bg/bg5.jpg" alt="东直门商圈" class="background-image" />
|
||||
</div>
|
||||
<view class="background-layer" :style="{ transform: `translateY(${parallaxOffset}px)` }">
|
||||
<image src="/static/bg/bg5.jpg" alt="东直门商圈" class="background-image" mode="widthFix" />
|
||||
</view>
|
||||
|
||||
<!-- 福字点击区域 -->
|
||||
<FuClickArea
|
||||
|
|
@ -187,44 +394,44 @@ onMounted(() => {
|
|||
/>
|
||||
|
||||
<!-- sq3图片 -->
|
||||
<img
|
||||
<image
|
||||
v-if="sq3ImageVisible"
|
||||
src="/static/images/sq5.png"
|
||||
alt="新春祝福"
|
||||
class="sq-image"
|
||||
mode="widthFix"
|
||||
/>
|
||||
|
||||
<!-- 装饰图片 -->
|
||||
<img :src="deskImage" alt="餐桌" class="deco-img desk-img" />
|
||||
<img src="/static/dzm/img_stove1.png" alt="灶台" class="deco-img stove-img" />
|
||||
<img v-if="showGuideElements" src="/static/dzm/img_line.png" alt="线条" class="deco-img line-img" />
|
||||
<img v-if="showGuideElements" src="/static/images/icon_hand.png" alt="手势" class="deco-img hand-img" />
|
||||
<image :src="deskImage" alt="餐桌" class="deco-img desk-img" mode="widthFix" />
|
||||
<image src="/static/dzm/img_stove1.png" alt="灶台" class="deco-img stove-img" mode="widthFix" />
|
||||
<image v-if="showGuideElements" src="/static/dzm/img_line.png" alt="线条" class="deco-img line-img" mode="widthFix" />
|
||||
<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"
|
||||
class="drag-trigger-area"
|
||||
@mousedown="startDrag"
|
||||
@touchstart="startDrag"
|
||||
></div>
|
||||
|
||||
<!-- 跟随拖拽的鸭子图片 -->
|
||||
<img
|
||||
v-show="showDuck"
|
||||
src="/static/dzm/img_duck.png"
|
||||
alt="烤鸭"
|
||||
class="drag-duck"
|
||||
ref="duckElement"
|
||||
/>
|
||||
|
||||
</section>
|
||||
></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dongzhimen-scene-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: auto; /* 高度由内容决定 */
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -232,12 +439,10 @@ onMounted(() => {
|
|||
background-color: #ff6b35;
|
||||
}
|
||||
|
||||
/* 背景图片层 */
|
||||
.background-layer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
transition: transform 0.1s ease;
|
||||
/* 背景图片决定容器高度 */
|
||||
height: auto;
|
||||
}
|
||||
|
||||
|
|
@ -245,192 +450,16 @@ onMounted(() => {
|
|||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
/* 确保图片完整显示,决定容器高度 */
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 增强动效层 */
|
||||
.enhancement-layer {
|
||||
.sq-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
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;
|
||||
top: 220rpx;
|
||||
right: -6rpx;
|
||||
width: 300rpx;
|
||||
height: auto;
|
||||
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;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.seal-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
|
|
@ -438,29 +467,6 @@ onMounted(() => {
|
|||
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 {
|
||||
position: absolute;
|
||||
z-index: 25;
|
||||
|
|
@ -470,85 +476,58 @@ onMounted(() => {
|
|||
left: 13rpx;
|
||||
top: 1665rpx;
|
||||
width: 441rpx;
|
||||
height: 382rpx;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.stove-img {
|
||||
left: 492rpx;
|
||||
top: 1711rpx;
|
||||
width: 241rpx;
|
||||
height: 363rpx;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.line-img {
|
||||
left: 250rpx;
|
||||
top: 1842rpx;
|
||||
width: 360rpx;
|
||||
height: 80rpx;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.hand-img {
|
||||
left: 440rpx;
|
||||
top: 1900rpx;
|
||||
width: 38rpx;
|
||||
height: 40rpx;
|
||||
height: auto;
|
||||
animation: arcSlideLeft 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 向左弧形滑动动效 */
|
||||
@keyframes arcSlideLeft {
|
||||
0% {
|
||||
transform: translateX(0) translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(-80rpx) translateY(3rpx);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 拖拽触发区域 */
|
||||
.drag-canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 800px;
|
||||
width: 375px;
|
||||
height: 300px;
|
||||
z-index: 100;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.drag-trigger-area {
|
||||
position: absolute;
|
||||
left: 540rpx;
|
||||
top: 1781rpx;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
cursor: grab;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
|
@ -356,7 +356,7 @@ onMounted(() => {
|
|||
.image-gallery {
|
||||
position: absolute;
|
||||
left: 40rpx;
|
||||
top: 1430rpx;
|
||||
top: 1380rpx;
|
||||
width: 670rpx;
|
||||
background-color: #d72717;
|
||||
border-radius: 20rpx;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name" : "xinchun2026",
|
||||
"appid" : "",
|
||||
"appid" : "__UNI__F0A5E90",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.0",
|
||||
"versionCode" : "100",
|
||||
|
|
@ -68,5 +68,8 @@
|
|||
"uniStatistics" : {
|
||||
"enable" : false
|
||||
},
|
||||
"vueVersion" : "3"
|
||||
"vueVersion" : "3",
|
||||
"h5" : {
|
||||
"title" : "2026新春H5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,14 @@
|
|||
"navigationBarTitleText": "四大特色主题区",
|
||||
"navigationStyle": "default"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/canvas3",
|
||||
"style": {
|
||||
"navigationBarTitleText": "四大特色主题区",
|
||||
"navigationStyle": "default"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "white",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue