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>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, onUnmounted, computed, nextTick, getCurrentInstance as vueGetCurrentInstance } from 'vue'
|
||||||
import FuClickArea from './FuClickArea.vue'
|
import FuClickArea from './FuClickArea.vue'
|
||||||
|
|
||||||
|
// 获取 Vue 实例
|
||||||
|
const instance = vueGetCurrentInstance()
|
||||||
|
|
||||||
// 组件属性
|
// 组件属性
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
// 是否为活动状态
|
|
||||||
active: {
|
active: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
// 滚动位置,用于实现视差效果
|
|
||||||
scrollPosition: {
|
scrollPosition: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 组件事件
|
|
||||||
const emit = defineEmits(['collect-seal'])
|
const emit = defineEmits(['collect-seal'])
|
||||||
|
|
||||||
// 是否收集福印
|
// 是否收集福印
|
||||||
|
|
@ -28,7 +28,6 @@ const sq3ImageVisible = ref(false)
|
||||||
|
|
||||||
// 计算视差效果的偏移量
|
// 计算视差效果的偏移量
|
||||||
const parallaxOffset = computed(() => {
|
const parallaxOffset = computed(() => {
|
||||||
// 滚动位置的1/10作为视差偏移
|
|
||||||
return props.scrollPosition * 0.1
|
return props.scrollPosition * 0.1
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -41,120 +40,316 @@ const handleFuClick = () => {
|
||||||
|
|
||||||
// 拖拽状态
|
// 拖拽状态
|
||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
const showDuck = ref(false)
|
const showDuck = ref(true) // 调试时默认显示鸭子
|
||||||
const deskImage = ref('/static/dzm/img_desk1.png')
|
const deskImage = ref('/static/dzm/img_desk1.png')
|
||||||
const showGuideElements = ref(true)
|
const showGuideElements = ref(true)
|
||||||
|
|
||||||
// 鸭子元素引用
|
// Canvas 上下文
|
||||||
const duckElement = ref(null)
|
const ctx = ref(null)
|
||||||
|
|
||||||
// 鸭子当前位置(使用普通变量,避免响应式带来的性能开销)
|
// 鸭子图片路径
|
||||||
let duckX = 0
|
const duckImagePath = ref(null)
|
||||||
let duckY = 0
|
|
||||||
|
|
||||||
// 拖拽开始区域 (540, 1781, 100, 100) 的中心点
|
// 鸭子位置(相对于 Canvas)
|
||||||
const dragStartArea = { x: 590, y: 1831 }
|
const duckX = ref(275)
|
||||||
|
const duckY = ref(110)
|
||||||
|
|
||||||
// 目标区域 (餐桌区域: 13, 1665, 441, 382)
|
// 鸭子尺寸(与图片实际尺寸一致)
|
||||||
const targetArea = { x: 100, y: 1750, width: 300, height: 200 }
|
const duckWidth = 36
|
||||||
|
const duckHeight = 70
|
||||||
|
|
||||||
// 开始拖拽
|
// 拖拽偏移量
|
||||||
const startDrag = (e) => {
|
const dragOffsetX = ref(0)
|
||||||
e.preventDefault()
|
const dragOffsetY = ref(0)
|
||||||
|
|
||||||
// 如果已经在拖拽中,先结束之前的
|
// Canvas 位置信息
|
||||||
|
const canvasRect = ref({
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 目标区域 (餐桌区域) - 相对于 Canvas 的坐标
|
||||||
|
const targetArea = ref({
|
||||||
|
x: 0,
|
||||||
|
y: 80,
|
||||||
|
width: 200,
|
||||||
|
height: 150
|
||||||
|
})
|
||||||
|
|
||||||
|
// 动画帧 ID
|
||||||
|
const animationId = ref(null)
|
||||||
|
|
||||||
|
// 初始化 Canvas
|
||||||
|
const initCanvas = () => {
|
||||||
|
console.log('正在初始化 Canvas...')
|
||||||
|
|
||||||
|
// 在 uni-app 中使用 uni.createCanvasContext
|
||||||
|
ctx.value = uni.createCanvasContext('dragDuckCanvas', instance)
|
||||||
|
|
||||||
|
if (ctx.value) {
|
||||||
|
console.log('Canvas 上下文创建成功')
|
||||||
|
// 加载鸭子图片
|
||||||
|
loadDuckImage()
|
||||||
|
// 获取 Canvas 位置
|
||||||
|
getCanvasPosition()
|
||||||
|
// 开始动画循环
|
||||||
|
startAnimation()
|
||||||
|
} else {
|
||||||
|
console.error('Canvas 上下文创建失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载鸭子图片
|
||||||
|
const loadDuckImage = () => {
|
||||||
|
uni.getImageInfo({
|
||||||
|
src: '/static/dzm/img_duck.png',
|
||||||
|
success: (res) => {
|
||||||
|
console.log('鸭子图片加载成功:', res)
|
||||||
|
duckImagePath.value = res.path
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('鸭子图片加载失败:', err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 Canvas 元素位置
|
||||||
|
const getCanvasPosition = () => {
|
||||||
|
const query = uni.createSelectorQuery().in(instance)
|
||||||
|
query.select('#dragDuckCanvas').boundingClientRect(res => {
|
||||||
|
if (res) {
|
||||||
|
canvasRect.value = {
|
||||||
|
left: res.left,
|
||||||
|
top: res.top,
|
||||||
|
width: res.width,
|
||||||
|
height: res.height
|
||||||
|
}
|
||||||
|
console.log('Canvas 位置信息:', canvasRect.value)
|
||||||
|
} else {
|
||||||
|
console.error('无法获取 Canvas 位置信息')
|
||||||
|
}
|
||||||
|
}).exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始动画循环
|
||||||
|
const startAnimation = () => {
|
||||||
|
const animate = () => {
|
||||||
|
drawCanvas()
|
||||||
|
animationId.value = requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
animationId.value = requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制 Canvas
|
||||||
|
const drawCanvas = () => {
|
||||||
|
if (!ctx.value) return
|
||||||
|
|
||||||
|
// 清除 Canvas
|
||||||
|
ctx.value.clearRect(0, 0, canvasRect.value.width || 400, canvasRect.value.height || 600)
|
||||||
|
|
||||||
|
// 绘制目标区域
|
||||||
|
drawTarget()
|
||||||
|
|
||||||
|
// 绘制鸭子
|
||||||
|
drawDuck()
|
||||||
|
|
||||||
|
// 执行绘制
|
||||||
|
ctx.value.draw(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制目标区域
|
||||||
|
const drawTarget = () => {
|
||||||
|
const target = targetArea.value
|
||||||
|
|
||||||
|
// 绘制目标区域背景
|
||||||
|
ctx.value.setFillStyle('rgba(52, 152, 219, 0.3)')
|
||||||
|
ctx.value.fillRect(target.x, target.y, target.width, target.height)
|
||||||
|
|
||||||
|
// 绘制目标区域边框
|
||||||
|
ctx.value.setStrokeStyle('#3498db')
|
||||||
|
ctx.value.setLineWidth(2)
|
||||||
|
ctx.value.setLineDash([5, 5])
|
||||||
|
ctx.value.strokeRect(target.x, target.y, target.width, target.height)
|
||||||
|
ctx.value.setLineDash([])
|
||||||
|
|
||||||
|
// 绘制目标区域文字
|
||||||
|
ctx.value.setFontSize(14)
|
||||||
|
ctx.value.setFillStyle('#3498db')
|
||||||
|
ctx.value.setTextAlign('center')
|
||||||
|
ctx.value.fillText(
|
||||||
|
'目标区域',
|
||||||
|
target.x + target.width / 2,
|
||||||
|
target.y + target.height / 2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制鸭子
|
||||||
|
const drawDuck = () => {
|
||||||
|
if (!showDuck.value) return
|
||||||
|
|
||||||
|
// 如果图片已加载,绘制图片
|
||||||
|
if (duckImagePath.value) {
|
||||||
|
ctx.value.drawImage(duckImagePath.value, duckX.value, duckY.value, duckWidth, duckHeight)
|
||||||
|
} else {
|
||||||
|
// 图片未加载时,绘制占位方块
|
||||||
|
ctx.value.setFillStyle('#ffcc00')
|
||||||
|
ctx.value.fillRect(duckX.value, duckY.value, duckWidth, duckHeight)
|
||||||
|
|
||||||
|
// 绘制边框
|
||||||
|
ctx.value.setStrokeStyle('#333')
|
||||||
|
ctx.value.setLineWidth(2)
|
||||||
|
ctx.value.strokeRect(duckX.value, duckY.value, duckWidth, duckHeight)
|
||||||
|
|
||||||
|
// 绘制文字
|
||||||
|
ctx.value.setFontSize(14)
|
||||||
|
ctx.value.setFillStyle('#FFFFFF')
|
||||||
|
ctx.value.setTextAlign('center')
|
||||||
|
ctx.value.setTextBaseline('middle')
|
||||||
|
ctx.value.fillText('鸭', duckX.value + duckWidth / 2, duckY.value + duckHeight / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果正在拖拽,绘制拖拽效果
|
||||||
if (isDragging.value) {
|
if (isDragging.value) {
|
||||||
endDrag()
|
// 绘制拖拽阴影
|
||||||
}
|
ctx.value.setFillStyle('rgba(231, 76, 60, 0.3)')
|
||||||
|
ctx.value.fillRect(duckX.value, duckY.value, duckWidth, duckHeight)
|
||||||
isDragging.value = true
|
|
||||||
showDuck.value = true
|
|
||||||
|
|
||||||
// 获取触摸或鼠标位置
|
|
||||||
const clientX = e.touches ? e.touches[0].clientX : e.clientX
|
|
||||||
const clientY = e.touches ? e.touches[0].clientY : e.clientY
|
|
||||||
|
|
||||||
// 设置鸭子初始位置
|
// 绘制拖拽边框
|
||||||
updateDuckPosition(clientX, clientY)
|
ctx.value.setStrokeStyle('#e74c3c')
|
||||||
|
ctx.value.setLineWidth(2)
|
||||||
// 先移除可能存在的旧监听器,防止重复绑定
|
ctx.value.setLineDash([5, 5])
|
||||||
document.removeEventListener('mousemove', onDrag)
|
ctx.value.strokeRect(duckX.value - 3, duckY.value - 3, duckWidth + 6, duckHeight + 6)
|
||||||
document.removeEventListener('mouseup', endDrag)
|
ctx.value.setLineDash([])
|
||||||
document.removeEventListener('touchmove', onDrag)
|
}
|
||||||
document.removeEventListener('touchend', endDrag)
|
|
||||||
|
|
||||||
// 添加全局事件监听
|
|
||||||
document.addEventListener('mousemove', onDrag)
|
|
||||||
document.addEventListener('mouseup', endDrag)
|
|
||||||
document.addEventListener('touchmove', onDrag, { passive: false })
|
|
||||||
document.addEventListener('touchend', endDrag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拖拽中
|
// 检查触摸点是否在鸭子内
|
||||||
const onDrag = (e) => {
|
const checkTouchInDuck = (x, y) => {
|
||||||
|
return x >= duckX.value &&
|
||||||
|
x <= duckX.value + duckWidth &&
|
||||||
|
y >= duckY.value &&
|
||||||
|
y <= duckY.value + duckHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查鸭子是否在目标区域内
|
||||||
|
const checkDuckInTarget = () => {
|
||||||
|
const target = targetArea.value
|
||||||
|
|
||||||
|
// 计算鸭子中心点
|
||||||
|
const duckCenterX = duckX.value + duckWidth / 2
|
||||||
|
const duckCenterY = duckY.value + duckHeight / 2
|
||||||
|
|
||||||
|
// 检查中心点是否在目标区域内
|
||||||
|
return duckCenterX >= target.x &&
|
||||||
|
duckCenterX <= target.x + target.width &&
|
||||||
|
duckCenterY >= target.y &&
|
||||||
|
duckCenterY <= target.y + target.height
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触摸开始事件
|
||||||
|
const handleTouchStart = (e) => {
|
||||||
|
console.log('触摸开始事件触发', e)
|
||||||
|
|
||||||
|
// 获取触摸点
|
||||||
|
const touch = e.touches[0]
|
||||||
|
if (!touch) {
|
||||||
|
console.error('没有触摸点信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保 Canvas 位置信息是最新的
|
||||||
|
getCanvasPosition()
|
||||||
|
|
||||||
|
// 异步获取 Canvas 位置后执行检查
|
||||||
|
setTimeout(() => {
|
||||||
|
// 计算相对于 Canvas 的坐标
|
||||||
|
const touchX = touch.clientX - canvasRect.value.left
|
||||||
|
const touchY = touch.clientY - canvasRect.value.top
|
||||||
|
|
||||||
|
console.log(`触摸点相对 Canvas: (${touchX}, ${touchY})`)
|
||||||
|
console.log(`鸭子位置: (${duckX.value}, ${duckY.value})`)
|
||||||
|
|
||||||
|
// 检查是否点击在鸭子上
|
||||||
|
if (checkTouchInDuck(touchX, touchY)) {
|
||||||
|
console.log('触摸点在鸭子内,开始拖拽')
|
||||||
|
|
||||||
|
// 设置拖拽状态和偏移量
|
||||||
|
isDragging.value = true
|
||||||
|
dragOffsetX.value = touchX - duckX.value
|
||||||
|
dragOffsetY.value = touchY - duckY.value
|
||||||
|
|
||||||
|
showDuck.value = true
|
||||||
|
} else {
|
||||||
|
console.log('触摸点不在鸭子内')
|
||||||
|
}
|
||||||
|
}, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触摸移动事件
|
||||||
|
const handleTouchMove = (e) => {
|
||||||
if (!isDragging.value) return
|
if (!isDragging.value) return
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
const clientX = e.touches ? e.touches[0].clientX : e.clientX
|
const touch = e.touches[0]
|
||||||
const clientY = e.touches ? e.touches[0].clientY : e.clientY
|
if (!touch) return
|
||||||
|
|
||||||
updateDuckPosition(clientX, clientY)
|
// 计算相对于 Canvas 的坐标
|
||||||
|
const touchX = touch.clientX - canvasRect.value.left
|
||||||
|
const touchY = touch.clientY - canvasRect.value.top
|
||||||
|
|
||||||
|
// 更新鸭子位置
|
||||||
|
duckX.value = touchX - dragOffsetX.value
|
||||||
|
duckY.value = touchY - dragOffsetY.value
|
||||||
|
|
||||||
|
// 限制边界
|
||||||
|
const canvasW = canvasRect.value.width || 400
|
||||||
|
const canvasH = canvasRect.value.height || 600
|
||||||
|
|
||||||
|
duckX.value = Math.max(0, Math.min(canvasW - duckWidth, duckX.value))
|
||||||
|
duckY.value = Math.max(0, Math.min(canvasH - duckHeight, duckY.value))
|
||||||
|
|
||||||
|
console.log(`鸭子新位置: (${duckX.value}, ${duckY.value})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新鸭子位置 - 直接操作 DOM 实现硬件加速
|
// 触摸结束事件
|
||||||
const updateDuckPosition = (clientX, clientY) => {
|
const handleTouchEnd = () => {
|
||||||
duckX = clientX
|
console.log('触摸结束事件触发')
|
||||||
duckY = clientY
|
|
||||||
|
|
||||||
if (duckElement.value) {
|
|
||||||
// 使用 transform3d 启用 GPU 加速
|
|
||||||
duckElement.value.style.transform = `translate3d(${clientX}px, ${clientY}px, 0) translate(-50%, -50%) scale(1.2)`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 结束拖拽
|
if (isDragging.value) {
|
||||||
const endDrag = () => {
|
isDragging.value = false
|
||||||
if (!isDragging.value) return
|
|
||||||
|
// 检查是否成功放入目标区域
|
||||||
// 获取容器在视口中的位置
|
if (checkDuckInTarget()) {
|
||||||
const container = document.querySelector('.dongzhimen-scene-container')
|
console.log('成功放入目标区域!')
|
||||||
if (container) {
|
|
||||||
const rect = container.getBoundingClientRect()
|
|
||||||
// 鸭子相对于容器的位置
|
|
||||||
const duckRelX = duckX - rect.left
|
|
||||||
const duckRelY = duckY - rect.top
|
|
||||||
|
|
||||||
// 将 rpx 转换为 px (假设设计稿宽度 750rpx,实际宽度通过 rect.width 计算)
|
|
||||||
const rpxToPx = rect.width / 750
|
|
||||||
const targetX = targetArea.x * rpxToPx
|
|
||||||
const targetY = targetArea.y * rpxToPx
|
|
||||||
const targetW = targetArea.width * rpxToPx
|
|
||||||
const targetH = targetArea.height * rpxToPx
|
|
||||||
|
|
||||||
const inTargetArea = (
|
|
||||||
duckRelX >= targetX &&
|
|
||||||
duckRelX <= targetX + targetW &&
|
|
||||||
duckRelY >= targetY &&
|
|
||||||
duckRelY <= targetY + targetH
|
|
||||||
)
|
|
||||||
|
|
||||||
if (inTargetArea) {
|
|
||||||
// 更换餐桌图片
|
// 更换餐桌图片
|
||||||
deskImage.value = '/static/dzm/img_desk2.png'
|
deskImage.value = '/static/dzm/img_desk2.png'
|
||||||
// 隐藏引导元素
|
// 隐藏引导元素
|
||||||
showGuideElements.value = false
|
showGuideElements.value = false
|
||||||
|
// 隐藏鸭子
|
||||||
|
showDuck.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 隐藏鸭子
|
|
||||||
showDuck.value = false
|
// 开始拖拽(从触发区域开始)
|
||||||
isDragging.value = false
|
const startDrag = (e) => {
|
||||||
|
console.log('开始拖拽', e)
|
||||||
// 移除全局事件监听
|
|
||||||
document.removeEventListener('mousemove', onDrag)
|
// 初始化 Canvas
|
||||||
document.removeEventListener('mouseup', endDrag)
|
if (!ctx.value) {
|
||||||
document.removeEventListener('touchmove', onDrag)
|
initCanvas()
|
||||||
document.removeEventListener('touchend', endDrag)
|
}
|
||||||
|
|
||||||
|
// 设置鸭子初始位置(在触发区域附近)
|
||||||
|
duckX.value = 50
|
||||||
|
duckY.value = 50
|
||||||
|
showDuck.value = true
|
||||||
|
|
||||||
|
// 触发触摸开始事件处理
|
||||||
|
handleTouchStart(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 页面挂载时的初始化
|
// 页面挂载时的初始化
|
||||||
|
|
@ -164,17 +359,29 @@ onMounted(() => {
|
||||||
if (container) {
|
if (container) {
|
||||||
container.classList.add('animate-in')
|
container.classList.add('animate-in')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 调试时自动初始化 Canvas
|
||||||
|
setTimeout(() => {
|
||||||
|
initCanvas()
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件卸载时清理
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 停止动画
|
||||||
|
if (animationId.value) {
|
||||||
|
cancelAnimationFrame(animationId.value)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="dongzhimen-scene-container" :class="{ 'active': active }">
|
<view class="dongzhimen-scene-container" :class="{ 'active': active }">
|
||||||
<!-- 背景图片层 -->
|
<!-- 背景图片层 -->
|
||||||
<div class="background-layer" :style="{ transform: `translateY(${parallaxOffset}px)` }">
|
<view class="background-layer" :style="{ transform: `translateY(${parallaxOffset}px)` }">
|
||||||
<!-- 使用东直门商圈背景图片 -->
|
<image src="/static/bg/bg5.jpg" alt="东直门商圈" class="background-image" mode="widthFix" />
|
||||||
<img src="/static/bg/bg5.jpg" alt="东直门商圈" class="background-image" />
|
</view>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 福字点击区域 -->
|
<!-- 福字点击区域 -->
|
||||||
<FuClickArea
|
<FuClickArea
|
||||||
:visible="fuClickAreaVisible"
|
:visible="fuClickAreaVisible"
|
||||||
|
|
@ -185,46 +392,46 @@ onMounted(() => {
|
||||||
:fu-height="100"
|
:fu-height="100"
|
||||||
@click="handleFuClick"
|
@click="handleFuClick"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- sq3图片 -->
|
<!-- sq3图片 -->
|
||||||
<img
|
<image
|
||||||
v-if="sq3ImageVisible"
|
v-if="sq3ImageVisible"
|
||||||
src="/static/images/sq5.png"
|
src="/static/images/sq5.png"
|
||||||
alt="新春祝福"
|
alt="新春祝福"
|
||||||
class="sq-image"
|
class="sq-image"
|
||||||
|
mode="widthFix"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 装饰图片 -->
|
<!-- 装饰图片 -->
|
||||||
<img :src="deskImage" alt="餐桌" class="deco-img desk-img" />
|
<image :src="deskImage" alt="餐桌" class="deco-img desk-img" mode="widthFix" />
|
||||||
<img src="/static/dzm/img_stove1.png" alt="灶台" class="deco-img stove-img" />
|
<image src="/static/dzm/img_stove1.png" alt="灶台" class="deco-img stove-img" mode="widthFix" />
|
||||||
<img v-if="showGuideElements" src="/static/dzm/img_line.png" alt="线条" class="deco-img line-img" />
|
<image v-if="showGuideElements" src="/static/dzm/img_line.png" alt="线条" class="deco-img line-img" mode="widthFix" />
|
||||||
<img v-if="showGuideElements" src="/static/images/icon_hand.png" alt="手势" class="deco-img hand-img" />
|
<image v-if="showGuideElements" src="/static/images/icon_hand.png" alt="手势" class="deco-img hand-img" mode="widthFix" />
|
||||||
|
|
||||||
|
<!-- Canvas 拖拽区域 -->
|
||||||
|
<canvas
|
||||||
|
canvas-id="dragDuckCanvas"
|
||||||
|
id="dragDuckCanvas"
|
||||||
|
class="drag-canvas"
|
||||||
|
@touchstart="handleTouchStart"
|
||||||
|
@touchmove="handleTouchMove"
|
||||||
|
@touchend="handleTouchEnd"
|
||||||
|
></canvas>
|
||||||
|
|
||||||
<!-- 拖拽触发区域 -->
|
<!-- 拖拽触发区域 -->
|
||||||
<div
|
<view
|
||||||
v-if="showGuideElements"
|
v-if="showGuideElements"
|
||||||
class="drag-trigger-area"
|
class="drag-trigger-area"
|
||||||
@mousedown="startDrag"
|
|
||||||
@touchstart="startDrag"
|
@touchstart="startDrag"
|
||||||
></div>
|
></view>
|
||||||
|
</view>
|
||||||
<!-- 跟随拖拽的鸭子图片 -->
|
|
||||||
<img
|
|
||||||
v-show="showDuck"
|
|
||||||
src="/static/dzm/img_duck.png"
|
|
||||||
alt="烤鸭"
|
|
||||||
class="drag-duck"
|
|
||||||
ref="duckElement"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.dongzhimen-scene-container {
|
.dongzhimen-scene-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto; /* 高度由内容决定 */
|
height: auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -232,12 +439,10 @@ onMounted(() => {
|
||||||
background-color: #ff6b35;
|
background-color: #ff6b35;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 背景图片层 */
|
|
||||||
.background-layer {
|
.background-layer {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: transform 0.1s ease;
|
transition: transform 0.1s ease;
|
||||||
/* 背景图片决定容器高度 */
|
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,192 +450,16 @@ onMounted(() => {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
display: block;
|
display: block;
|
||||||
/* 确保图片完整显示,决定容器高度 */
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 增强动效层 */
|
.sq-image {
|
||||||
.enhancement-layer {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 220rpx;
|
||||||
left: 0;
|
right: -6rpx;
|
||||||
width: 100%;
|
width: 300rpx;
|
||||||
height: 100%;
|
height: auto;
|
||||||
pointer-events: none;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 灯笼增强动效 */
|
|
||||||
.lanterns {
|
|
||||||
position: absolute;
|
|
||||||
top: 15%;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 30px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lantern {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
animation: swing 3s infinite ease-in-out;
|
|
||||||
opacity: 0.9;
|
|
||||||
filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.8));
|
|
||||||
color: #ffd700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-lantern {
|
|
||||||
animation-delay: 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-lantern {
|
|
||||||
animation-delay: 1.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes swing {
|
|
||||||
0%, 100% { transform: rotate(-10deg); }
|
|
||||||
50% { transform: rotate(10deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 福字增强动效 */
|
|
||||||
.fu-word {
|
|
||||||
position: absolute;
|
|
||||||
top: 30%;
|
|
||||||
left: 65%;
|
|
||||||
transform: translateX(-50%) rotate(15deg);
|
|
||||||
font-size: 2rem;
|
|
||||||
color: #ffd700;
|
|
||||||
text-shadow: 2px 2px 10px rgba(255, 215, 0, 0.9);
|
|
||||||
animation: float 4s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes float {
|
|
||||||
0%, 100% { transform: translateX(-50%) rotate(15deg) translateY(0); }
|
|
||||||
50% { transform: translateX(-50%) rotate(15deg) translateY(-15px); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 点击指示器 */
|
|
||||||
.click-indicator {
|
|
||||||
position: absolute;
|
|
||||||
top: 55%;
|
|
||||||
left: 75%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pulse-circle {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: rgba(255, 215, 0, 0.3);
|
|
||||||
border: 2px solid rgba(255, 215, 0, 0.6);
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0% {
|
|
||||||
transform: scale(0.8);
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(2);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-indicator.animate-pulse {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 交互区域 */
|
|
||||||
.interaction-area {
|
|
||||||
position: absolute;
|
|
||||||
top: 55%;
|
|
||||||
right: 15%;
|
|
||||||
width: 120px;
|
|
||||||
height: 100px;
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式调整交互区域位置 */
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.interaction-area {
|
|
||||||
top: 52%;
|
|
||||||
right: 10%;
|
|
||||||
width: 100px;
|
|
||||||
height: 80px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 烟花效果 */
|
|
||||||
.fireworks {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.firework {
|
|
||||||
position: absolute;
|
|
||||||
font-size: 2rem;
|
|
||||||
opacity: 0;
|
|
||||||
animation: firework 3s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.firework-1 {
|
|
||||||
top: 10%;
|
|
||||||
left: 20%;
|
|
||||||
animation-delay: 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.firework-2 {
|
|
||||||
top: 15%;
|
|
||||||
right: 25%;
|
|
||||||
animation-delay: 1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.firework-3 {
|
|
||||||
top: 8%;
|
|
||||||
right: 15%;
|
|
||||||
animation-delay: 2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.firework-4 {
|
|
||||||
top: 12%;
|
|
||||||
left: 25%;
|
|
||||||
animation-delay: 3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes firework {
|
|
||||||
0%, 100% { opacity: 0; transform: scale(0); }
|
|
||||||
50% { opacity: 1; transform: scale(1.5); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 福印收集标记 */
|
|
||||||
.seal-collected-mark {
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
background-color: rgba(255, 107, 53, 0.9);
|
|
||||||
color: #fff;
|
|
||||||
padding: 10px 15px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 14px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
animation: fadeIn 0.5s ease;
|
animation: fadeIn 0.5s ease;
|
||||||
z-index: 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
.seal-icon {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
|
|
@ -438,29 +467,6 @@ onMounted(() => {
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 入场动画 */
|
|
||||||
.dongzhimen-scene-container.animate-in {
|
|
||||||
animation: sceneFadeIn 1s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes sceneFadeIn {
|
|
||||||
from { opacity: 0; transform: translateY(50px); }
|
|
||||||
to { opacity: 1; transform: translateY(0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* sq图片 */
|
|
||||||
.sq-image {
|
|
||||||
position: absolute;
|
|
||||||
top: 220rpx;
|
|
||||||
right: -6rpx;
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
max-width: 300rpx;
|
|
||||||
z-index: 20;
|
|
||||||
animation: fadeIn 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 装饰图片 */
|
|
||||||
.deco-img {
|
.deco-img {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 25;
|
z-index: 25;
|
||||||
|
|
@ -470,85 +476,58 @@ onMounted(() => {
|
||||||
left: 13rpx;
|
left: 13rpx;
|
||||||
top: 1665rpx;
|
top: 1665rpx;
|
||||||
width: 441rpx;
|
width: 441rpx;
|
||||||
height: 382rpx;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stove-img {
|
.stove-img {
|
||||||
left: 492rpx;
|
left: 492rpx;
|
||||||
top: 1711rpx;
|
top: 1711rpx;
|
||||||
width: 241rpx;
|
width: 241rpx;
|
||||||
height: 363rpx;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-img {
|
.line-img {
|
||||||
left: 250rpx;
|
left: 250rpx;
|
||||||
top: 1842rpx;
|
top: 1842rpx;
|
||||||
width: 360rpx;
|
width: 360rpx;
|
||||||
height: 80rpx;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hand-img {
|
.hand-img {
|
||||||
left: 440rpx;
|
left: 440rpx;
|
||||||
top: 1900rpx;
|
top: 1900rpx;
|
||||||
width: 38rpx;
|
width: 38rpx;
|
||||||
height: 40rpx;
|
height: auto;
|
||||||
animation: arcSlideLeft 1.2s ease-in-out infinite;
|
animation: arcSlideLeft 1.2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 向左弧形滑动动效 */
|
|
||||||
@keyframes arcSlideLeft {
|
@keyframes arcSlideLeft {
|
||||||
0% {
|
0% {
|
||||||
transform: translateX(0) translateY(0);
|
transform: translateX(0) translateY(0);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
transform: translateX(-80rpx) translateY(3rpx);
|
transform: translateX(-80rpx) translateY(3rpx);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 拖拽触发区域 */
|
.drag-canvas {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 800px;
|
||||||
|
width: 375px;
|
||||||
|
height: 300px;
|
||||||
|
z-index: 100;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.drag-trigger-area {
|
.drag-trigger-area {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 540rpx;
|
left: 540rpx;
|
||||||
top: 1781rpx;
|
top: 1781rpx;
|
||||||
width: 100rpx;
|
width: 100rpx;
|
||||||
height: 100rpx;
|
height: 100rpx;
|
||||||
cursor: grab;
|
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
.drag-trigger-area:active {
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 跟随拖拽的鸭子图片 */
|
|
||||||
.drag-duck {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 54rpx;
|
|
||||||
height: 105rpx;
|
|
||||||
opacity: 0.8;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 1000;
|
|
||||||
will-change: transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式设计 */
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.fu-word {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lantern {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.interaction-area {
|
|
||||||
width: 100px;
|
|
||||||
height: 80px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -356,7 +356,7 @@ onMounted(() => {
|
||||||
.image-gallery {
|
.image-gallery {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 40rpx;
|
left: 40rpx;
|
||||||
top: 1430rpx;
|
top: 1380rpx;
|
||||||
width: 670rpx;
|
width: 670rpx;
|
||||||
background-color: #d72717;
|
background-color: #d72717;
|
||||||
border-radius: 20rpx;
|
border-radius: 20rpx;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name" : "xinchun2026",
|
"name" : "xinchun2026",
|
||||||
"appid" : "",
|
"appid" : "__UNI__F0A5E90",
|
||||||
"description" : "",
|
"description" : "",
|
||||||
"versionName" : "1.0.0",
|
"versionName" : "1.0.0",
|
||||||
"versionCode" : "100",
|
"versionCode" : "100",
|
||||||
|
|
@ -68,5 +68,8 @@
|
||||||
"uniStatistics" : {
|
"uniStatistics" : {
|
||||||
"enable" : false
|
"enable" : false
|
||||||
},
|
},
|
||||||
"vueVersion" : "3"
|
"vueVersion" : "3",
|
||||||
|
"h5" : {
|
||||||
|
"title" : "2026新春H5"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,14 @@
|
||||||
"navigationBarTitleText": "四大特色主题区",
|
"navigationBarTitleText": "四大特色主题区",
|
||||||
"navigationStyle": "default"
|
"navigationStyle": "default"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/canvas3",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "四大特色主题区",
|
||||||
|
"navigationStyle": "default"
|
||||||
|
}
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"globalStyle": {
|
"globalStyle": {
|
||||||
"navigationBarTextStyle": "white",
|
"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