Compare commits
No commits in common. "fd020b935d925ed5d9d7a37d6e7f195c8c66dcd5" and "858addfb47ab45c514ab11244a879415fc47a90c" have entirely different histories.
fd020b935d
...
858addfb47
580
canvas.html
580
canvas.html
|
|
@ -1,580 +0,0 @@
|
||||||
<!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,6 +1,5 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import FuClickArea from './FuClickArea.vue'
|
|
||||||
|
|
||||||
// 组件属性
|
// 组件属性
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -17,43 +16,38 @@ const props = defineProps({
|
||||||
})
|
})
|
||||||
|
|
||||||
// 组件事件
|
// 组件事件
|
||||||
const emit = defineEmits(['collect-seal'])
|
const emit = defineEmits(['collect-seal', 'play-drum'])
|
||||||
|
|
||||||
// 是否收集福印
|
// 是否收集福印
|
||||||
const sealCollected = ref(false)
|
const sealCollected = ref(false)
|
||||||
|
|
||||||
// 福字点击区域状态
|
|
||||||
const fuClickAreaVisible = ref(true)
|
|
||||||
const sq2ImageVisible = ref(false)
|
|
||||||
|
|
||||||
// 计算视差效果的偏移量
|
// 计算视差效果的偏移量
|
||||||
const parallaxOffset = computed(() => {
|
const parallaxOffset = computed(() => {
|
||||||
// 滚动位置的1/10作为视差偏移
|
// 滚动位置的1/10作为视差偏移
|
||||||
return props.scrollPosition * 0.1
|
return props.scrollPosition * 0.1
|
||||||
})
|
})
|
||||||
|
|
||||||
// 点击福字区域
|
// 收集福印
|
||||||
const handleFuClick = () => {
|
const collectSeal = () => {
|
||||||
fuClickAreaVisible.value = false
|
if (!sealCollected.value) {
|
||||||
sq2ImageVisible.value = true
|
sealCollected.value = true
|
||||||
emit('collect-seal')
|
emit('collect-seal')
|
||||||
|
showToast({
|
||||||
|
message: '恭喜获得国潮福字!',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开webview页面
|
// 播放音效
|
||||||
const openWebview = () => {
|
const playDrum = () => {
|
||||||
uni.navigateTo({
|
emit('play-drum')
|
||||||
url: '/pages/webview/webview',
|
collectSeal()
|
||||||
success: () => {
|
showToast({
|
||||||
console.log('Webview opened successfully')
|
message: '滑动探索商圈,收集国潮福字!',
|
||||||
},
|
icon: 'info',
|
||||||
fail: (err) => {
|
duration: 1500
|
||||||
console.error('Failed to open webview:', err)
|
|
||||||
showToast({
|
|
||||||
message: '页面打开失败',
|
|
||||||
icon: 'error',
|
|
||||||
duration: 1500
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,37 +65,45 @@ onMounted(() => {
|
||||||
<section class="chongwen-scene-container" :class="{ 'active': active }">
|
<section class="chongwen-scene-container" :class="{ 'active': active }">
|
||||||
<!-- 背景图片层 -->
|
<!-- 背景图片层 -->
|
||||||
<div class="background-layer" :style="{ transform: `translateY(${parallaxOffset}px)` }">
|
<div class="background-layer" :style="{ transform: `translateY(${parallaxOffset}px)` }">
|
||||||
<!-- 使用崇文门商圈背景图片 -->
|
<!-- 使用前门商圈的背景图片作为占位 -->
|
||||||
<img src="/static/bg/bg2.jpg" alt="崇文门商圈" class="background-image" />
|
<img src="/static/qianmen-bg.jpg" alt="崇文门商圈" class="background-image" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 福字点击区域 -->
|
<!-- 增强动效层 -->
|
||||||
<FuClickArea
|
<div class="enhancement-layer">
|
||||||
:visible="fuClickAreaVisible"
|
<!-- 灯笼增强动效 -->
|
||||||
:x-range="630"
|
<div class="lanterns">
|
||||||
:y-range="300"
|
<div class="lantern left-lantern">🏮</div>
|
||||||
:y-start="150"
|
<div class="lantern right-lantern">🏮</div>
|
||||||
:fu-width="100"
|
</div>
|
||||||
:fu-height="100"
|
|
||||||
@click="handleFuClick"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- sq2图片 -->
|
<!-- 福字增强动效 -->
|
||||||
<img
|
<div class="fu-word">福</div>
|
||||||
v-if="sq2ImageVisible"
|
|
||||||
src="/static/images/sq2.png"
|
|
||||||
alt="新春祝福"
|
|
||||||
class="sq2-image"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 查看按钮 -->
|
<!-- 点击提示 -->
|
||||||
<img
|
<div class="click-indicator" :class="{ 'animate-pulse': !sealCollected }">
|
||||||
src="/static/images/btn_view.png"
|
<div class="pulse-circle"></div>
|
||||||
alt="查看详情"
|
</div>
|
||||||
class="btn-view"
|
</div>
|
||||||
@click="openWebview"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
<!-- 交互区域 -->
|
||||||
|
<div class="interaction-area" @click="playDrum">
|
||||||
|
<!-- 覆盖在图片上的点击区域 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 烟花效果 -->
|
||||||
|
<div class="fireworks">
|
||||||
|
<div class="firework firework-1">🎆</div>
|
||||||
|
<div class="firework firework-2">🎇</div>
|
||||||
|
<div class="firework firework-3">🎆</div>
|
||||||
|
<div class="firework firework-4">🎇</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 福印收集标记 -->
|
||||||
|
<div v-if="sealCollected" class="seal-collected-mark">
|
||||||
|
<div class="seal-icon">🏮</div>
|
||||||
|
<div class="seal-text">已收集国潮福字</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -109,7 +111,8 @@ onMounted(() => {
|
||||||
.chongwen-scene-container {
|
.chongwen-scene-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto; /* 高度由内容决定 */
|
height: auto;
|
||||||
|
min-height: var(--scene-height, 100vh);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -122,16 +125,17 @@ onMounted(() => {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: transform 0.1s ease;
|
transition: transform 0.1s ease;
|
||||||
/* 背景图片决定容器高度 */
|
|
||||||
height: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.background-image {
|
.background-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
display: block;
|
display: block;
|
||||||
/* 确保图片完整显示,决定容器高度 */
|
}
|
||||||
object-fit: contain;
|
|
||||||
|
/* 为背景图片容器添加与图片主色调匹配的背景色,避免在小屏幕上出现黑边 */
|
||||||
|
.background-layer {
|
||||||
|
background-color: #ff6b35;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 增强动效层 */
|
/* 增强动效层 */
|
||||||
|
|
@ -333,34 +337,6 @@ onMounted(() => {
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* sq2图片 */
|
|
||||||
.sq2-image {
|
|
||||||
position: absolute;
|
|
||||||
top: 220rpx;
|
|
||||||
right: -6rpx;
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
max-width: 300rpx;
|
|
||||||
z-index: 20;
|
|
||||||
animation: fadeIn 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 查看按钮 */
|
|
||||||
.btn-view {
|
|
||||||
position: absolute;
|
|
||||||
left: 156rpx;
|
|
||||||
top: 597rpx;
|
|
||||||
width: 439rpx;
|
|
||||||
height: 84rpx;
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 25;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-view:active {
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.fu-word {
|
.fu-word {
|
||||||
|
|
|
||||||
|
|
@ -1,484 +1,56 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, computed, nextTick, getCurrentInstance as vueGetCurrentInstance } from 'vue'
|
import { ref, onMounted, computed } from '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', 'play-drum'])
|
||||||
|
|
||||||
// 是否收集福印
|
// 是否收集福印
|
||||||
const sealCollected = ref(false)
|
const sealCollected = ref(false)
|
||||||
|
|
||||||
// 福字点击区域状态
|
|
||||||
const fuClickAreaVisible = ref(true)
|
|
||||||
const sq3ImageVisible = ref(false)
|
|
||||||
|
|
||||||
// 计算视差效果的偏移量
|
// 计算视差效果的偏移量
|
||||||
const parallaxOffset = computed(() => {
|
const parallaxOffset = computed(() => {
|
||||||
|
// 滚动位置的1/10作为视差偏移
|
||||||
return props.scrollPosition * 0.1
|
return props.scrollPosition * 0.1
|
||||||
})
|
})
|
||||||
|
|
||||||
// 点击福字区域
|
// 收集福印
|
||||||
const handleFuClick = () => {
|
const collectSeal = () => {
|
||||||
fuClickAreaVisible.value = false
|
if (!sealCollected.value) {
|
||||||
sq3ImageVisible.value = true
|
sealCollected.value = true
|
||||||
emit('collect-seal')
|
emit('collect-seal')
|
||||||
}
|
showToast({
|
||||||
|
message: '恭喜获得团圆福筷!',
|
||||||
// 拖拽状态
|
icon: 'success',
|
||||||
const isDragging = ref(false)
|
duration: 2000
|
||||||
const showDuck = ref(true) // 调试时默认显示鸭子
|
})
|
||||||
const deskImage = ref('/static/dzm/img_desk1.png')
|
|
||||||
const showGuideElements = ref(true)
|
|
||||||
|
|
||||||
// Canvas 触摸是否禁用
|
|
||||||
const canvasDisabled = ref(false)
|
|
||||||
|
|
||||||
// 调试模式
|
|
||||||
const debugMode = ref(false)
|
|
||||||
|
|
||||||
// Canvas 上下文
|
|
||||||
const ctx = ref(null)
|
|
||||||
|
|
||||||
// 鸭子图片路径
|
|
||||||
const duckImagePath = ref(null)
|
|
||||||
|
|
||||||
// rpx 转换比例
|
|
||||||
const rpxRatio = ref(0.5) // 默认 iPhone 6/7/8 的 375px / 750rpx = 0.5
|
|
||||||
|
|
||||||
// 鸭子位置和尺寸(使用 rpx 单位,在 750rpx 设计稿下)
|
|
||||||
const duckRpx = ref({
|
|
||||||
x: 550, // 275px * 2 = 550rpx
|
|
||||||
y: 220, // 110px * 2 = 220rpx
|
|
||||||
width: 72, // 36px * 2 = 72rpx
|
|
||||||
height: 140 // 70px * 2 = 140rpx
|
|
||||||
})
|
|
||||||
|
|
||||||
// 鸭子当前位置(px 单位,用于绘制)
|
|
||||||
const duckX = ref(0)
|
|
||||||
const duckY = ref(0)
|
|
||||||
const duckWidth = ref(0)
|
|
||||||
const duckHeight = ref(0)
|
|
||||||
|
|
||||||
// 拖拽偏移量
|
|
||||||
const dragOffsetX = ref(0)
|
|
||||||
const dragOffsetY = ref(0)
|
|
||||||
|
|
||||||
// Canvas 位置和尺寸(使用 rpx 单位)
|
|
||||||
const canvasRpx = ref({
|
|
||||||
x: 0,
|
|
||||||
y: 1550, // 800px * 2 = 1600rpx
|
|
||||||
width: 750, // 375px * 2 = 750rpx (全屏宽度)
|
|
||||||
height: 600 // 300px * 2 = 600rpx
|
|
||||||
})
|
|
||||||
|
|
||||||
// Canvas 当前位置和尺寸(px 单位)
|
|
||||||
const canvasRect = ref({
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
width: 0,
|
|
||||||
height: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// 目标区域 (餐桌区域) - 使用 rpx 单位
|
|
||||||
const targetAreaRpx = ref({
|
|
||||||
x: 0,
|
|
||||||
y: 160, // 80px * 2 = 160rpx
|
|
||||||
width: 400, // 200px * 2 = 400rpx
|
|
||||||
height: 300 // 150px * 2 = 300rpx
|
|
||||||
})
|
|
||||||
|
|
||||||
// 目标区域当前位置和尺寸(px 单位)
|
|
||||||
const targetArea = ref({
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 0,
|
|
||||||
height: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// 动画帧 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 = () => {
|
const playDrum = () => {
|
||||||
uni.getImageInfo({
|
emit('play-drum')
|
||||||
src: '/static/dzm/img_duck.png',
|
collectSeal()
|
||||||
success: (res) => {
|
showToast({
|
||||||
console.log('鸭子图片加载成功:', res)
|
message: '移动烤鸭,领取团圆福筷!',
|
||||||
duckImagePath.value = res.path
|
icon: 'info',
|
||||||
},
|
duration: 1500
|
||||||
fail: (err) => {
|
|
||||||
console.error('鸭子图片加载失败:', err)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// rpx 转 px
|
|
||||||
const rpxToPx = (rpx) => {
|
|
||||||
return rpx * rpxRatio.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新鸭子位置(根据 rpx 计算 px)
|
|
||||||
const updateDuckPosition = () => {
|
|
||||||
duckX.value = rpxToPx(duckRpx.value.x)
|
|
||||||
duckY.value = rpxToPx(duckRpx.value.y)
|
|
||||||
duckWidth.value = rpxToPx(duckRpx.value.width)
|
|
||||||
duckHeight.value = rpxToPx(duckRpx.value.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新目标区域(根据 rpx 计算 px)
|
|
||||||
const updateTargetArea = () => {
|
|
||||||
targetArea.value = {
|
|
||||||
x: rpxToPx(targetAreaRpx.value.x),
|
|
||||||
y: rpxToPx(targetAreaRpx.value.y),
|
|
||||||
width: rpxToPx(targetAreaRpx.value.width),
|
|
||||||
height: rpxToPx(targetAreaRpx.value.height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新 Canvas 位置和尺寸(根据 rpx 计算 px)
|
|
||||||
const updateCanvasRect = () => {
|
|
||||||
canvasRect.value = {
|
|
||||||
left: rpxToPx(canvasRpx.value.x),
|
|
||||||
top: rpxToPx(canvasRpx.value.y),
|
|
||||||
width: rpxToPx(canvasRpx.value.width),
|
|
||||||
height: rpxToPx(canvasRpx.value.height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Canvas 在视口中的实际位置(用于触摸计算)
|
|
||||||
const canvasBoundingRect = ref({
|
|
||||||
left: 0,
|
|
||||||
top: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取 Canvas 元素位置
|
|
||||||
const getCanvasPosition = () => {
|
|
||||||
// 计算 rpx 比例:屏幕宽度 / 750
|
|
||||||
const systemInfo = uni.getSystemInfoSync()
|
|
||||||
rpxRatio.value = systemInfo.windowWidth / 750
|
|
||||||
console.log('屏幕宽度:', systemInfo.windowWidth, 'rpxRatio:', rpxRatio.value)
|
|
||||||
|
|
||||||
// 更新 Canvas、鸭子和目标区域的 px 值
|
|
||||||
updateCanvasRect()
|
|
||||||
updateDuckPosition()
|
|
||||||
updateTargetArea()
|
|
||||||
|
|
||||||
console.log('Canvas 位置信息:', canvasRect.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取 Canvas 在视口中的实际位置(用于触摸坐标计算)
|
|
||||||
const getCanvasBoundingRect = () => {
|
|
||||||
const query = uni.createSelectorQuery().in(instance)
|
|
||||||
query.select('#dragDuckCanvas').boundingClientRect(res => {
|
|
||||||
if (res) {
|
|
||||||
canvasBoundingRect.value = {
|
|
||||||
left: res.left,
|
|
||||||
top: res.top
|
|
||||||
}
|
|
||||||
console.log('Canvas 视口位置:', canvasBoundingRect.value)
|
|
||||||
}
|
|
||||||
}).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
|
|
||||||
|
|
||||||
if (debugMode.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 (!debugMode.value && !isDragging.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果正在拖拽,设置半透明
|
|
||||||
if (isDragging.value) {
|
|
||||||
ctx.value.globalAlpha = 0.6
|
|
||||||
} else {
|
|
||||||
ctx.value.globalAlpha = 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果图片已加载,绘制图片
|
|
||||||
if (duckImagePath.value) {
|
|
||||||
ctx.value.drawImage(duckImagePath.value, duckX.value, duckY.value, duckWidth.value, duckHeight.value)
|
|
||||||
} else {
|
|
||||||
// 图片未加载时,绘制占位方块
|
|
||||||
ctx.value.setFillStyle('#ffcc00')
|
|
||||||
ctx.value.fillRect(duckX.value, duckY.value, duckWidth.value, duckHeight.value)
|
|
||||||
|
|
||||||
// 绘制边框
|
|
||||||
ctx.value.setStrokeStyle('#333')
|
|
||||||
ctx.value.setLineWidth(2)
|
|
||||||
ctx.value.strokeRect(duckX.value, duckY.value, duckWidth.value, duckHeight.value)
|
|
||||||
|
|
||||||
// 绘制文字
|
|
||||||
ctx.value.setFontSize(14)
|
|
||||||
ctx.value.setFillStyle('#FFFFFF')
|
|
||||||
ctx.value.setTextAlign('center')
|
|
||||||
ctx.value.setTextBaseline('middle')
|
|
||||||
ctx.value.fillText('鸭', duckX.value + duckWidth.value / 2, duckY.value + duckHeight.value / 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 恢复透明度
|
|
||||||
ctx.value.globalAlpha = 1.0
|
|
||||||
|
|
||||||
// 调试模式下显示拖拽效果红框
|
|
||||||
if (debugMode.value && isDragging.value) {
|
|
||||||
// 绘制拖拽阴影
|
|
||||||
ctx.value.setFillStyle('rgba(231, 76, 60, 0.3)')
|
|
||||||
ctx.value.fillRect(duckX.value, duckY.value, duckWidth.value, duckHeight.value)
|
|
||||||
|
|
||||||
// 绘制拖拽边框
|
|
||||||
ctx.value.setStrokeStyle('#e74c3c')
|
|
||||||
ctx.value.setLineWidth(2)
|
|
||||||
ctx.value.setLineDash([5, 5])
|
|
||||||
ctx.value.strokeRect(duckX.value - 3, duckY.value - 3, duckWidth.value + 6, duckHeight.value + 6)
|
|
||||||
ctx.value.setLineDash([])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查触摸点是否在鸭子内
|
|
||||||
const checkTouchInDuck = (x, y) => {
|
|
||||||
return x >= duckX.value &&
|
|
||||||
x <= duckX.value + duckWidth.value &&
|
|
||||||
y >= duckY.value &&
|
|
||||||
y <= duckY.value + duckHeight.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查鸭子是否在目标区域内
|
|
||||||
const checkDuckInTarget = () => {
|
|
||||||
const target = targetArea.value
|
|
||||||
|
|
||||||
// 计算鸭子中心点
|
|
||||||
const duckCenterX = duckX.value + duckWidth.value / 2
|
|
||||||
const duckCenterY = duckY.value + duckHeight.value / 2
|
|
||||||
|
|
||||||
// 检查中心点是否在目标区域内
|
|
||||||
return duckCenterX >= target.x &&
|
|
||||||
duckCenterX <= target.x + target.width &&
|
|
||||||
duckCenterY >= target.y &&
|
|
||||||
duckCenterY <= target.y + target.height
|
|
||||||
}
|
|
||||||
|
|
||||||
// 触摸开始事件
|
|
||||||
const handleTouchStart = (e) => {
|
|
||||||
// 如果 Canvas 已禁用,不处理触摸事件
|
|
||||||
if (canvasDisabled.value) {
|
|
||||||
console.log('Canvas 已禁用,忽略触摸事件')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('触摸开始事件触发', e)
|
|
||||||
|
|
||||||
// 获取触摸点
|
|
||||||
const touch = e.touches[0]
|
|
||||||
if (!touch) {
|
|
||||||
console.error('没有触摸点信息')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取 Canvas 在视口中的实际位置
|
|
||||||
getCanvasBoundingRect()
|
|
||||||
|
|
||||||
// 异步获取 Canvas 位置后执行检查
|
|
||||||
setTimeout(() => {
|
|
||||||
// 计算相对于 Canvas 的坐标(使用视口位置)
|
|
||||||
const touchX = touch.clientX - canvasBoundingRect.value.left
|
|
||||||
const touchY = touch.clientY - canvasBoundingRect.value.top
|
|
||||||
|
|
||||||
console.log(`触摸点 client: (${touch.clientX}, ${touch.clientY})`)
|
|
||||||
console.log(`Canvas 视口位置: (${canvasBoundingRect.value.left}, ${canvasBoundingRect.value.top})`)
|
|
||||||
console.log(`触摸点相对 Canvas: (${touchX}, ${touchY})`)
|
|
||||||
console.log(`鸭子位置: (${duckX.value}, ${duckY.value})`)
|
|
||||||
console.log(`鸭子尺寸: (${duckWidth.value}, ${duckHeight.value})`)
|
|
||||||
|
|
||||||
// 检查是否点击在鸭子上
|
|
||||||
if (checkTouchInDuck(touchX, touchY)) {
|
|
||||||
console.log('触摸点在鸭子内,开始拖拽')
|
|
||||||
|
|
||||||
// 设置拖拽状态和偏移量
|
|
||||||
isDragging.value = true
|
|
||||||
dragOffsetX.value = touchX - duckX.value
|
|
||||||
dragOffsetY.value = touchY - duckY.value
|
|
||||||
|
|
||||||
showDuck.value = true
|
|
||||||
} else {
|
|
||||||
console.log('触摸点不在鸭子内')
|
|
||||||
}
|
|
||||||
}, 50)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 触摸移动事件
|
|
||||||
const handleTouchMove = (e) => {
|
|
||||||
if (!isDragging.value) return
|
|
||||||
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
const touch = e.touches[0]
|
|
||||||
if (!touch) return
|
|
||||||
|
|
||||||
// 计算相对于 Canvas 的坐标(使用视口位置)
|
|
||||||
const touchX = touch.clientX - canvasBoundingRect.value.left
|
|
||||||
const touchY = touch.clientY - canvasBoundingRect.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.value, duckX.value))
|
|
||||||
duckY.value = Math.max(0, Math.min(canvasH - duckHeight.value, duckY.value))
|
|
||||||
|
|
||||||
console.log(`鸭子新位置: (${duckX.value}, ${duckY.value})`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 触摸结束事件
|
|
||||||
const handleTouchEnd = () => {
|
|
||||||
console.log('触摸结束事件触发')
|
|
||||||
|
|
||||||
if (isDragging.value) {
|
|
||||||
isDragging.value = false
|
|
||||||
|
|
||||||
// 检查是否成功放入目标区域
|
|
||||||
if (checkDuckInTarget()) {
|
|
||||||
console.log('成功放入目标区域!')
|
|
||||||
// 更换餐桌图片
|
|
||||||
deskImage.value = '/static/dzm/img_desk2.png'
|
|
||||||
// 隐藏引导元素
|
|
||||||
showGuideElements.value = false
|
|
||||||
// 隐藏鸭子
|
|
||||||
showDuck.value = false
|
|
||||||
// 禁用 Canvas 触摸事件
|
|
||||||
canvasDisabled.value = true
|
|
||||||
console.log('Canvas 触摸事件已禁用')
|
|
||||||
} else {
|
|
||||||
// 未放入目标区域,回归到原始位置
|
|
||||||
console.log('未放入目标区域,回归原始位置')
|
|
||||||
resetDuckPosition()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置鸭子位置到初始值
|
|
||||||
const resetDuckPosition = () => {
|
|
||||||
// 恢复原始 rpx 值
|
|
||||||
duckRpx.value.x = 550
|
|
||||||
duckRpx.value.y = 220
|
|
||||||
// 更新 px 值
|
|
||||||
updateDuckPosition()
|
|
||||||
console.log('鸭子已回归原始位置:', duckX.value, duckY.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开始拖拽(从触发区域开始)
|
|
||||||
const startDrag = (e) => {
|
|
||||||
console.log('开始拖拽', e)
|
|
||||||
|
|
||||||
// 初始化 Canvas
|
|
||||||
if (!ctx.value) {
|
|
||||||
initCanvas()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置鸭子初始位置(使用 rpx 值,会自动转换为 px)
|
|
||||||
duckRpx.value.x = 100 // 100rpx 对应约 50px (iPhone 6/7/8)
|
|
||||||
duckRpx.value.y = 100
|
|
||||||
updateDuckPosition()
|
|
||||||
showDuck.value = true
|
|
||||||
|
|
||||||
// 触发触摸开始事件处理
|
|
||||||
handleTouchStart(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面挂载时的初始化
|
// 页面挂载时的初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 添加动画类,触发入场动画
|
// 添加动画类,触发入场动画
|
||||||
|
|
@ -486,78 +58,53 @@ 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>
|
||||||
<view class="dongzhimen-scene-container" :class="{ 'active': active }">
|
<section class="dongzhimen-scene-container" :class="{ 'active': active }">
|
||||||
<!-- 背景图片层 -->
|
<!-- 背景图片层 -->
|
||||||
<view class="background-layer" :style="{ transform: `translateY(${parallaxOffset}px)` }">
|
<div class="background-layer" :style="{ transform: `translateY(${parallaxOffset}px)` }">
|
||||||
<image src="/static/bg/bg5.jpg" alt="东直门商圈" class="background-image" mode="widthFix" />
|
<!-- 使用前门商圈的背景图片作为占位 -->
|
||||||
</view>
|
<img src="/static/qianmen-bg.jpg" alt="东直门商圈" class="background-image" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 福字点击区域 -->
|
<!-- 增强动效层 -->
|
||||||
<FuClickArea
|
<div class="enhancement-layer">
|
||||||
:visible="fuClickAreaVisible"
|
<!-- 灯笼增强动效 -->
|
||||||
:x-range="630"
|
<div class="lanterns">
|
||||||
:y-range="400"
|
<div class="lantern left-lantern">🏮</div>
|
||||||
:y-start="150"
|
<div class="lantern right-lantern">🏮</div>
|
||||||
:fu-width="100"
|
</div>
|
||||||
:fu-height="100"
|
|
||||||
@click="handleFuClick"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- sq3图片 -->
|
<!-- 福字增强动效 -->
|
||||||
<image
|
<div class="fu-word">福</div>
|
||||||
v-if="sq3ImageVisible"
|
|
||||||
src="/static/images/sq5.png"
|
|
||||||
alt="新春祝福"
|
|
||||||
class="sq-image"
|
|
||||||
mode="widthFix"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 装饰图片 -->
|
<!-- 点击提示 -->
|
||||||
<image :src="deskImage" alt="餐桌" class="deco-img desk-img" mode="widthFix" />
|
<div class="click-indicator" :class="{ 'animate-pulse': !sealCollected }">
|
||||||
<image src="/static/dzm/img_stove1.png" alt="灶台" class="deco-img stove-img" mode="widthFix" />
|
<div class="pulse-circle"></div>
|
||||||
<image v-if="showGuideElements" src="/static/dzm/img_line.png" alt="线条" class="deco-img line-img" mode="widthFix" />
|
</div>
|
||||||
<image v-if="showGuideElements" src="/static/images/icon_hand.png" alt="手势" class="deco-img hand-img" mode="widthFix" />
|
</div>
|
||||||
|
|
||||||
<!-- Canvas 拖拽区域 -->
|
<!-- 交互区域 -->
|
||||||
<canvas
|
<div class="interaction-area" @click="playDrum">
|
||||||
canvas-id="dragDuckCanvas"
|
<!-- 覆盖在图片上的点击区域 -->
|
||||||
id="dragDuckCanvas"
|
</div>
|
||||||
class="drag-canvas"
|
|
||||||
:style="{
|
|
||||||
left: canvasRect.left + 'px',
|
|
||||||
top: canvasRect.top + 'px',
|
|
||||||
width: canvasRect.width + 'px',
|
|
||||||
height: canvasRect.height + 'px'
|
|
||||||
}"
|
|
||||||
@touchstart="handleTouchStart"
|
|
||||||
@touchmove="handleTouchMove"
|
|
||||||
@touchend="handleTouchEnd"
|
|
||||||
></canvas>
|
|
||||||
|
|
||||||
<!-- 拖拽触发区域 -->
|
<!-- 烟花效果 -->
|
||||||
<view
|
<div class="fireworks">
|
||||||
v-if="showGuideElements"
|
<div class="firework firework-1">🎆</div>
|
||||||
class="drag-trigger-area"
|
<div class="firework firework-2">🎇</div>
|
||||||
@touchstart="startDrag"
|
<div class="firework firework-3">🎆</div>
|
||||||
></view>
|
<div class="firework firework-4">🎇</div>
|
||||||
</view>
|
</div>
|
||||||
|
|
||||||
|
<!-- 福印收集标记 -->
|
||||||
|
<div v-if="sealCollected" class="seal-collected-mark">
|
||||||
|
<div class="seal-icon">🏮</div>
|
||||||
|
<div class="seal-text">已收集团圆福筷</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -565,6 +112,7 @@ onUnmounted(() => {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
min-height: var(--scene-height, 100vh);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -572,11 +120,11 @@ onUnmounted(() => {
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.background-image {
|
.background-image {
|
||||||
|
|
@ -585,14 +133,193 @@ onUnmounted(() => {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sq-image {
|
/* 为背景图片容器添加与图片主色调匹配的背景色,避免在小屏幕上出现黑边 */
|
||||||
|
.background-layer {
|
||||||
|
background-color: #ff6b35;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 增强动效层 */
|
||||||
|
.enhancement-layer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 220rpx;
|
top: 0;
|
||||||
right: -6rpx;
|
left: 0;
|
||||||
width: 300rpx;
|
width: 100%;
|
||||||
height: auto;
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 灯笼增强动效 */
|
||||||
|
.lanterns {
|
||||||
|
position: absolute;
|
||||||
|
top: 15%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 30px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lantern {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
animation: swing 3s infinite ease-in-out;
|
||||||
|
opacity: 0.9;
|
||||||
|
filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.8));
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-lantern {
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-lantern {
|
||||||
|
animation-delay: 1.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes swing {
|
||||||
|
0%, 100% { transform: rotate(-10deg); }
|
||||||
|
50% { transform: rotate(10deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 福字增强动效 */
|
||||||
|
.fu-word {
|
||||||
|
position: absolute;
|
||||||
|
top: 30%;
|
||||||
|
left: 65%;
|
||||||
|
transform: translateX(-50%) rotate(15deg);
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #ffd700;
|
||||||
|
text-shadow: 2px 2px 10px rgba(255, 215, 0, 0.9);
|
||||||
|
animation: float 4s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateX(-50%) rotate(15deg) translateY(0); }
|
||||||
|
50% { transform: translateX(-50%) rotate(15deg) translateY(-15px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 点击指示器 */
|
||||||
|
.click-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 55%;
|
||||||
|
left: 75%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse-circle {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: rgba(255, 215, 0, 0.3);
|
||||||
|
border: 2px solid rgba(255, 215, 0, 0.6);
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(2);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.click-indicator.animate-pulse {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 交互区域 */
|
||||||
|
.interaction-area {
|
||||||
|
position: absolute;
|
||||||
|
top: 55%;
|
||||||
|
right: 15%;
|
||||||
|
width: 120px;
|
||||||
|
height: 100px;
|
||||||
|
cursor: pointer;
|
||||||
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 {
|
||||||
|
|
@ -600,63 +327,29 @@ onUnmounted(() => {
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.deco-img {
|
/* 入场动画 */
|
||||||
position: absolute;
|
.dongzhimen-scene-container.animate-in {
|
||||||
z-index: 25;
|
animation: sceneFadeIn 1s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.desk-img {
|
@keyframes sceneFadeIn {
|
||||||
left: 13rpx;
|
from { opacity: 0; transform: translateY(50px); }
|
||||||
top: 1665rpx;
|
to { opacity: 1; transform: translateY(0); }
|
||||||
width: 441rpx;
|
|
||||||
height: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stove-img {
|
/* 响应式设计 */
|
||||||
left: 492rpx;
|
@media (max-width: 640px) {
|
||||||
top: 1711rpx;
|
.fu-word {
|
||||||
width: 241rpx;
|
font-size: 1.5rem;
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-img {
|
|
||||||
left: 250rpx;
|
|
||||||
top: 1842rpx;
|
|
||||||
width: 360rpx;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hand-img {
|
|
||||||
left: 440rpx;
|
|
||||||
top: 1900rpx;
|
|
||||||
width: 38rpx;
|
|
||||||
height: auto;
|
|
||||||
animation: arcSlideLeft 1.2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes arcSlideLeft {
|
|
||||||
0% {
|
|
||||||
transform: translateX(0) translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
}
|
||||||
100% {
|
|
||||||
transform: translateX(-80rpx) translateY(3rpx);
|
.lantern {
|
||||||
opacity: 1;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.drag-canvas {
|
.interaction-area {
|
||||||
position: absolute;
|
width: 100px;
|
||||||
z-index: 100;
|
height: 80px;
|
||||||
pointer-events: auto;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.drag-trigger-area {
|
|
||||||
position: absolute;
|
|
||||||
left: 540rpx;
|
|
||||||
top: 1781rpx;
|
|
||||||
width: 100rpx;
|
|
||||||
height: 100rpx;
|
|
||||||
z-index: 30;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,399 +0,0 @@
|
||||||
<template>
|
|
||||||
<section class="scene-section end-section" :class="{ 'active': isActive }">
|
|
||||||
<!-- 背景图片层 -->
|
|
||||||
<div class="background-layer">
|
|
||||||
<img src="/static/bg/bg_finish.jpg" alt="结束页背景" class="background-image" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 内容层 -->
|
|
||||||
<div class="content-layer">
|
|
||||||
<div class="end-page-content">
|
|
||||||
<!-- 上方标题图片 -->
|
|
||||||
<image
|
|
||||||
src="/static/images/finish_title2.png"
|
|
||||||
mode="widthFix"
|
|
||||||
class="finish-title-top"
|
|
||||||
></image>
|
|
||||||
|
|
||||||
<!-- 福印收集展示区域 -->
|
|
||||||
<div class="seal-collection-display">
|
|
||||||
<!-- 前门 - 左上角 -->
|
|
||||||
<div class="gift-item gift-pos-1" :class="{ collected: collectedSeals.qianmen }">
|
|
||||||
<image
|
|
||||||
src="/static/images/gift1.png"
|
|
||||||
mode="widthFit"
|
|
||||||
class="gift-image"
|
|
||||||
></image>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 崇文门 - 右上角 -->
|
|
||||||
<div class="gift-item gift-pos-2" :class="{ collected: collectedSeals.chongwen }">
|
|
||||||
<image
|
|
||||||
src="/static/images/gift2.png"
|
|
||||||
mode="widthFit"
|
|
||||||
class="gift-image"
|
|
||||||
></image>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 王府井 - 左下角 -->
|
|
||||||
<div class="gift-item gift-pos-3" :class="{ collected: collectedSeals.wangfujing }">
|
|
||||||
<image
|
|
||||||
src="/static/images/gift3.png"
|
|
||||||
mode="widthFit"
|
|
||||||
class="gift-image"
|
|
||||||
></image>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 隆福寺 - 底部中间 -->
|
|
||||||
<div class="gift-item gift-pos-4" :class="{ collected: collectedSeals.longfusi }">
|
|
||||||
<image
|
|
||||||
src="/static/images/gift4.png"
|
|
||||||
mode="widthFit"
|
|
||||||
class="gift-image"
|
|
||||||
></image>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 东直门 - 右下角 -->
|
|
||||||
<div class="gift-item gift-pos-5" :class="{ collected: collectedSeals.dongzhimen }">
|
|
||||||
<image
|
|
||||||
src="/static/images/gift5.png"
|
|
||||||
mode="widthFit"
|
|
||||||
class="gift-image"
|
|
||||||
></image>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 功能按钮区域 -->
|
|
||||||
<div class="end-buttons">
|
|
||||||
<image
|
|
||||||
src="/static/images/btn_lottery.png"
|
|
||||||
mode="aspectFit"
|
|
||||||
class="function-image lottery-image"
|
|
||||||
@click="handleLottery"
|
|
||||||
></image>
|
|
||||||
<image
|
|
||||||
src="/static/images/btn_ai.png"
|
|
||||||
mode="aspectFit"
|
|
||||||
class="function-image couplet-image"
|
|
||||||
@click="handleCouplet"
|
|
||||||
></image>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 下方标题图片 -->
|
|
||||||
<image
|
|
||||||
src="/static/images/finish_title.png"
|
|
||||||
mode="widthFix"
|
|
||||||
class="finish-title-bottom"
|
|
||||||
></image>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
const props = defineProps({
|
|
||||||
isActive: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
collectionProgress: {
|
|
||||||
type: Number,
|
|
||||||
default: 0
|
|
||||||
},
|
|
||||||
collectedCount: {
|
|
||||||
type: Number,
|
|
||||||
default: 0
|
|
||||||
},
|
|
||||||
totalCount: {
|
|
||||||
type: Number,
|
|
||||||
default: 5
|
|
||||||
},
|
|
||||||
collectedSeals: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({
|
|
||||||
qianmen: false,
|
|
||||||
chongwen: false,
|
|
||||||
wangfujing: false,
|
|
||||||
longfusi: false,
|
|
||||||
dongzhimen: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['lottery', 'couplet', 'restart'])
|
|
||||||
|
|
||||||
const handleLottery = () => {
|
|
||||||
emit('lottery')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCouplet = () => {
|
|
||||||
emit('couplet')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRestart = () => {
|
|
||||||
emit('restart')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.scene-section {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scene-section.active {
|
|
||||||
/* 当前活动场景样式 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 背景图片层 */
|
|
||||||
.background-layer {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.background-image {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover; /* 使用裁切方式展示,保持图片比例 */
|
|
||||||
object-position: center center; /* 从中心开始裁切 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 内容层 */
|
|
||||||
.content-layer {
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
padding-top: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.end-page-content {
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.end-title {
|
|
||||||
font-size: 32px;
|
|
||||||
color: #FF6B35;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.seal-collection-section {
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 24px;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collection-progress {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
width: 100%;
|
|
||||||
height: 10px;
|
|
||||||
background-color: #ddd;
|
|
||||||
border-radius: 5px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
background-color: #FF6B35;
|
|
||||||
transition: width 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-text {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #666;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.end-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.function-btn {
|
|
||||||
cursor: pointer;
|
|
||||||
width: 306rpx;
|
|
||||||
height: 97rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lottery-btn {
|
|
||||||
background-color: #FFD700;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lottery-btn:hover {
|
|
||||||
background-color: #FFC107;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.couplet-btn {
|
|
||||||
background-color: #4CAF50;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.couplet-btn:hover {
|
|
||||||
background-color: #45a049;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.restart-btn-container {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.restart-btn {
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: 16px;
|
|
||||||
background-color: #999;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.restart-btn:hover {
|
|
||||||
background-color: #777;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 福印收集展示区域 - 绝对定位布局 */
|
|
||||||
.seal-collection-display {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 600rpx;
|
|
||||||
margin: 80rpx 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 福印基础样式 */
|
|
||||||
.gift-item {
|
|
||||||
position: absolute;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
opacity: 0.4;
|
|
||||||
filter: grayscale(100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gift-item.collected {
|
|
||||||
opacity: 1;
|
|
||||||
filter: grayscale(0%);
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gift-image {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 前门 - 左上位置 */
|
|
||||||
.gift-pos-1 {
|
|
||||||
width: 237rpx;
|
|
||||||
height: 233rpx;
|
|
||||||
left: 80rpx;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 崇文门 - 右上位置 */
|
|
||||||
.gift-pos-2 {
|
|
||||||
width: 344rpx;
|
|
||||||
height: 343rpx;
|
|
||||||
right: 80rpx;
|
|
||||||
top: -64rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 王府井 - 左下位置 */
|
|
||||||
.gift-pos-3 {
|
|
||||||
width: 268rpx;
|
|
||||||
height: 254rpx;
|
|
||||||
left: 40rpx;
|
|
||||||
bottom: 0rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隆福寺 - 底部中间位置 */
|
|
||||||
.gift-pos-4 {
|
|
||||||
width: 153rpx;
|
|
||||||
height: 350rpx;
|
|
||||||
left: calc(50% + 20rpx);
|
|
||||||
bottom: 0;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gift-pos-4.collected {
|
|
||||||
transform: translateX(-50%) scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 东直门 - 右下位置 */
|
|
||||||
.gift-pos-5 {
|
|
||||||
width: 238rpx;
|
|
||||||
height: 282rpx;
|
|
||||||
right: 40rpx;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gift-label {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #333;
|
|
||||||
margin: 0;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collection-status {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #FF6B35;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 标题图片 */
|
|
||||||
.finish-title-top {
|
|
||||||
width: 747rpx;
|
|
||||||
margin-top: 110rpx;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.finish-title-bottom {
|
|
||||||
width: 733rpx;
|
|
||||||
margin-top: 30rpx;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 功能按钮图片:假设原图约400px*120px,使用rpx单位 */
|
|
||||||
.function-image {
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
width: 306rpx;
|
|
||||||
height: 97rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.function-image:active {
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式相关样式已移除 - 本页面仅用于手机端 */
|
|
||||||
/* 如需调整各元素位置,请直接修改对应选择器的样式 */
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,168 +0,0 @@
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted } from 'vue'
|
|
||||||
|
|
||||||
// 组件属性
|
|
||||||
const props = defineProps({
|
|
||||||
// 是否可见
|
|
||||||
visible: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
// x范围(总宽度)
|
|
||||||
xRange: {
|
|
||||||
type: Number,
|
|
||||||
default: 630
|
|
||||||
},
|
|
||||||
// y范围(总高度)
|
|
||||||
yRange: {
|
|
||||||
type: Number,
|
|
||||||
default: 400
|
|
||||||
},
|
|
||||||
// y起始位置
|
|
||||||
yStart: {
|
|
||||||
type: Number,
|
|
||||||
default: 350
|
|
||||||
},
|
|
||||||
// 福字宽度
|
|
||||||
fuWidth: {
|
|
||||||
type: Number,
|
|
||||||
default: 100
|
|
||||||
},
|
|
||||||
// 福字高度
|
|
||||||
fuHeight: {
|
|
||||||
type: Number,
|
|
||||||
default: 100
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 组件事件
|
|
||||||
const emit = defineEmits(['click'])
|
|
||||||
|
|
||||||
// 福字位置
|
|
||||||
const position = ref({
|
|
||||||
x: 0,
|
|
||||||
y: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算随机位置
|
|
||||||
const calculateRandomPosition = () => {
|
|
||||||
position.value = {
|
|
||||||
x: Math.random() * (props.xRange - props.fuWidth),
|
|
||||||
y: Math.random() * (props.yRange - props.fuHeight) + props.yStart
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 点击处理
|
|
||||||
const handleClick = () => {
|
|
||||||
emit('click')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件挂载时计算位置
|
|
||||||
onMounted(() => {
|
|
||||||
calculateRandomPosition()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
v-if="visible"
|
|
||||||
class="fu-click-area"
|
|
||||||
:style="{
|
|
||||||
left: `${position.x}rpx`,
|
|
||||||
top: `${position.y}rpx`,
|
|
||||||
width: `${fuWidth}rpx`,
|
|
||||||
height: `${fuHeight}rpx`
|
|
||||||
}"
|
|
||||||
@click="handleClick"
|
|
||||||
>
|
|
||||||
<img src="/static/images/icon_fu.png" alt="福字" class="fu-icon" />
|
|
||||||
<img src="/static/images/icon_hand.png" alt="点击手势" class="hand-icon" />
|
|
||||||
<div class="click-indicator">
|
|
||||||
<div class="pulse-circle"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* 福字点击区域 */
|
|
||||||
.fu-click-area {
|
|
||||||
position: absolute;
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 25;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fu-icon {
|
|
||||||
position: absolute;
|
|
||||||
width: 94rpx;
|
|
||||||
height: 74rpx;
|
|
||||||
top: 0;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
animation: sway 3s infinite ease-in-out;
|
|
||||||
transform-origin: center center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes sway {
|
|
||||||
0%, 100% {
|
|
||||||
transform: translateX(-50%) rotate(0deg);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
transform: translateX(-50%) rotate(-3deg);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
transform: translateX(-50%) rotate(3deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hand-icon {
|
|
||||||
position: absolute;
|
|
||||||
width: 38rpx;
|
|
||||||
height: 40rpx;
|
|
||||||
top: 70%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
animation: clickUp 2s infinite ease-in-out;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes clickUp {
|
|
||||||
0%, 100% {
|
|
||||||
transform: translate(-50%, -50%) scale(1.2);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translate(-50%, -50%) scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.click-indicator {
|
|
||||||
position: absolute;
|
|
||||||
top: 30rpx;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 60rpx;
|
|
||||||
height: 60rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pulse-circle {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: rgba(255, 215, 0, 0.3);
|
|
||||||
border: 2rpx solid rgba(255, 215, 0, 0.6);
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
animation-delay: 1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0% {
|
|
||||||
transform: scale(0.8);
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(2);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
<script setup>
|
|
||||||
import { ref, watch } from 'vue'
|
|
||||||
|
|
||||||
// 组件属性
|
|
||||||
const props = defineProps({
|
|
||||||
// 是否显示
|
|
||||||
visible: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
// 图片数组
|
|
||||||
images: {
|
|
||||||
type: Array,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
// 当前索引
|
|
||||||
currentIndex: {
|
|
||||||
type: Number,
|
|
||||||
default: 0
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 组件事件
|
|
||||||
const emit = defineEmits(['close', 'update:currentIndex'])
|
|
||||||
|
|
||||||
// 当前显示的图片索引
|
|
||||||
const currentImageIndex = ref(props.currentIndex)
|
|
||||||
|
|
||||||
// 监听props变化
|
|
||||||
watch(() => props.currentIndex, (newVal) => {
|
|
||||||
currentImageIndex.value = newVal
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(() => props.visible, (newVal) => {
|
|
||||||
if (newVal) {
|
|
||||||
currentImageIndex.value = props.currentIndex
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 切换图片
|
|
||||||
const prevImage = () => {
|
|
||||||
currentImageIndex.value = (currentImageIndex.value - 1 + props.images.length) % props.images.length
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextImage = () => {
|
|
||||||
currentImageIndex.value = (currentImageIndex.value + 1) % props.images.length
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭弹窗
|
|
||||||
const closeModal = () => {
|
|
||||||
emit('close')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 点击遮罩关闭
|
|
||||||
const handleOverlayClick = () => {
|
|
||||||
closeModal()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="gallery-modal" v-if="visible">
|
|
||||||
<!-- 遮罩层 -->
|
|
||||||
<div class="modal-overlay" @click="handleOverlayClick"></div>
|
|
||||||
|
|
||||||
<!-- 弹窗内容 -->
|
|
||||||
<div class="modal-content">
|
|
||||||
<!-- 图片区域 -->
|
|
||||||
<div class="gallery-image-wrapper">
|
|
||||||
<img :src="images[currentImageIndex]?.src" :alt="images[currentImageIndex]?.title" class="gallery-image" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 控制区域 -->
|
|
||||||
<div class="gallery-controls">
|
|
||||||
<div class="nav-btn prev-btn" @click="prevImage">
|
|
||||||
<img src="/static/images/btn_prev.png" alt="上一张" class="nav-icon" />
|
|
||||||
</div>
|
|
||||||
<div class="gallery-title">{{ images[currentImageIndex]?.title }}</div>
|
|
||||||
<div class="nav-btn next-btn" @click="nextImage">
|
|
||||||
<img src="/static/images/btn_next.png" alt="下一张" class="nav-icon" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 关闭按钮(在对话框外边下方) -->
|
|
||||||
<div class="close-btn" @click="closeModal">
|
|
||||||
<img src="/static/images/btn_close.png" alt="关闭" class="close-icon" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* 弹窗容器 */
|
|
||||||
.gallery-modal {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 遮罩层 */
|
|
||||||
.modal-overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: rgba(0, 0, 0, 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 弹窗内容 */
|
|
||||||
.modal-content {
|
|
||||||
position: relative;
|
|
||||||
width: 700rpx;
|
|
||||||
background-color: #d72717;
|
|
||||||
border-radius: 20rpx;
|
|
||||||
padding: 30rpx;
|
|
||||||
box-sizing: border-box;
|
|
||||||
z-index: 1001;
|
|
||||||
animation: modalIn 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes modalIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.9);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 关闭按钮(在对话框外边下方) */
|
|
||||||
.close-btn {
|
|
||||||
width: 80rpx;
|
|
||||||
height: 80rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 1001;
|
|
||||||
margin-top: 30rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-icon {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 上方图片区域 */
|
|
||||||
.gallery-image-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-image {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
max-height: 600rpx;
|
|
||||||
object-fit: contain;
|
|
||||||
border-radius: 12rpx;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 下方控制区域 */
|
|
||||||
.gallery-controls {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
height: 80rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn {
|
|
||||||
width: 70rpx;
|
|
||||||
height: 70rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-icon {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-title {
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 40rpx;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: 6rpx;
|
|
||||||
padding: 0 20rpx;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import FuClickArea from './FuClickArea.vue'
|
|
||||||
import ImageGalleryModal from './ImageGalleryModal.vue'
|
|
||||||
|
|
||||||
// 组件属性
|
// 组件属性
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -18,48 +16,39 @@ const props = defineProps({
|
||||||
})
|
})
|
||||||
|
|
||||||
// 组件事件
|
// 组件事件
|
||||||
const emit = defineEmits(['collect-seal'])
|
const emit = defineEmits(['collect-seal', 'play-drum'])
|
||||||
|
|
||||||
// 是否收集福印
|
// 是否收集福印
|
||||||
const sealCollected = ref(false)
|
const sealCollected = ref(false)
|
||||||
|
|
||||||
// 福字点击区域状态
|
|
||||||
const fuClickAreaVisible = ref(true)
|
|
||||||
const sq3ImageVisible = ref(false)
|
|
||||||
|
|
||||||
// 计算视差效果的偏移量
|
// 计算视差效果的偏移量
|
||||||
const parallaxOffset = computed(() => {
|
const parallaxOffset = computed(() => {
|
||||||
// 滚动位置的1/10作为视差偏移
|
// 滚动位置的1/10作为视差偏移
|
||||||
return props.scrollPosition * 0.1
|
return props.scrollPosition * 0.1
|
||||||
})
|
})
|
||||||
|
|
||||||
// 点击福字区域
|
// 收集福印
|
||||||
const handleFuClick = () => {
|
const collectSeal = () => {
|
||||||
fuClickAreaVisible.value = false
|
if (!sealCollected.value) {
|
||||||
sq3ImageVisible.value = true
|
sealCollected.value = true
|
||||||
emit('collect-seal')
|
emit('collect-seal')
|
||||||
|
showToast({
|
||||||
|
message: '恭喜获得文化福灯!',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 图片浏览数据
|
// 播放音效
|
||||||
const lfsImages = [
|
const playDrum = () => {
|
||||||
{ src: '/static/lfs/img1.png', title: '传艺承福阁' },
|
emit('play-drum')
|
||||||
{ src: '/static/lfs/img2.png', title: '京味福食巷' },
|
collectSeal()
|
||||||
{ src: '/static/lfs/img3.png', title: '雅趣福玩斋' }
|
showToast({
|
||||||
]
|
message: '点击文创物品,点亮文化福灯!',
|
||||||
|
icon: 'info',
|
||||||
// 图片浏览器弹窗状态
|
duration: 1500
|
||||||
const galleryVisible = ref(false)
|
})
|
||||||
const currentGalleryIndex = ref(0)
|
|
||||||
|
|
||||||
// 点击热点打开图片浏览器
|
|
||||||
const openGallery = (index) => {
|
|
||||||
currentGalleryIndex.value = index
|
|
||||||
galleryVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭图片浏览器
|
|
||||||
const closeGallery = () => {
|
|
||||||
galleryVisible.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 页面挂载时的初始化
|
// 页面挂载时的初始化
|
||||||
|
|
@ -76,58 +65,45 @@ onMounted(() => {
|
||||||
<section class="longfusi-scene-container" :class="{ 'active': active }">
|
<section class="longfusi-scene-container" :class="{ 'active': active }">
|
||||||
<!-- 背景图片层 -->
|
<!-- 背景图片层 -->
|
||||||
<div class="background-layer" :style="{ transform: `translateY(${parallaxOffset}px)` }">
|
<div class="background-layer" :style="{ transform: `translateY(${parallaxOffset}px)` }">
|
||||||
<!-- 使用隆福寺商圈背景图片 -->
|
<!-- 使用前门商圈的背景图片作为占位 -->
|
||||||
<img src="/static/bg/bg4.jpg" alt="隆福寺商圈" class="background-image" />
|
<img src="/static/qianmen-bg.jpg" alt="隆福寺商圈" class="background-image" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 福字点击区域 -->
|
<!-- 增强动效层 -->
|
||||||
<FuClickArea
|
<div class="enhancement-layer">
|
||||||
:visible="fuClickAreaVisible"
|
<!-- 灯笼增强动效 -->
|
||||||
:x-range="630"
|
<div class="lanterns">
|
||||||
:y-range="400"
|
<div class="lantern left-lantern">🏮</div>
|
||||||
:y-start="150"
|
<div class="lantern right-lantern">🏮</div>
|
||||||
:fu-width="100"
|
</div>
|
||||||
:fu-height="100"
|
|
||||||
@click="handleFuClick"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- sq3图片 -->
|
<!-- 福字增强动效 -->
|
||||||
<img
|
<div class="fu-word">福</div>
|
||||||
v-if="sq3ImageVisible"
|
|
||||||
src="/static/images/sq4.png"
|
|
||||||
alt="新春祝福"
|
|
||||||
class="sq-image"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 热点点击区域1 - 左下方 -->
|
<!-- 点击提示 -->
|
||||||
<div class="hotspot-area" style="left: 163rpx; top: 950rpx;" @click="openGallery(0)">
|
<div class="click-indicator" :class="{ 'animate-pulse': !sealCollected }">
|
||||||
<div class="pulse-indicator">
|
|
||||||
<div class="pulse-circle"></div>
|
<div class="pulse-circle"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 热点点击区域2 - 右下方 -->
|
<!-- 交互区域 -->
|
||||||
<div class="hotspot-area" style="left: 450rpx; top: 900rpx;" @click="openGallery(1)">
|
<div class="interaction-area" @click="playDrum">
|
||||||
<div class="pulse-indicator">
|
<!-- 覆盖在图片上的点击区域 -->
|
||||||
<div class="pulse-circle"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 热点点击区域3 - 上方 -->
|
<!-- 烟花效果 -->
|
||||||
<div class="hotspot-area" style="left: 320rpx; top: 738rpx;" @click="openGallery(2)">
|
<div class="fireworks">
|
||||||
<div class="pulse-indicator">
|
<div class="firework firework-1">🎆</div>
|
||||||
<div class="pulse-circle"></div>
|
<div class="firework firework-2">🎇</div>
|
||||||
</div>
|
<div class="firework firework-3">🎆</div>
|
||||||
|
<div class="firework firework-4">🎇</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 图片浏览器弹窗 -->
|
<!-- 福印收集标记 -->
|
||||||
<ImageGalleryModal
|
<div v-if="sealCollected" class="seal-collected-mark">
|
||||||
:visible="galleryVisible"
|
<div class="seal-icon">🏮</div>
|
||||||
:images="lfsImages"
|
<div class="seal-text">已收集文化福灯</div>
|
||||||
:currentIndex="currentGalleryIndex"
|
</div>
|
||||||
@close="closeGallery"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -135,7 +111,8 @@ onMounted(() => {
|
||||||
.longfusi-scene-container {
|
.longfusi-scene-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto; /* 高度由内容决定 */
|
height: auto;
|
||||||
|
min-height: var(--scene-height, 100vh);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -148,16 +125,17 @@ onMounted(() => {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: transform 0.1s ease;
|
transition: transform 0.1s ease;
|
||||||
/* 背景图片决定容器高度 */
|
|
||||||
height: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.background-image {
|
.background-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
display: block;
|
display: block;
|
||||||
/* 确保图片完整显示,决定容器高度 */
|
}
|
||||||
object-fit: contain;
|
|
||||||
|
/* 为背景图片容器添加与图片主色调匹配的背景色,避免在小屏幕上出现黑边 */
|
||||||
|
.background-layer {
|
||||||
|
background-color: #ff6b35;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 增强动效层 */
|
/* 增强动效层 */
|
||||||
|
|
@ -185,8 +163,8 @@ onMounted(() => {
|
||||||
.lantern {
|
.lantern {
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
animation: swing 3s infinite ease-in-out;
|
animation: swing 3s infinite ease-in-out;
|
||||||
opacity: 1;
|
opacity: 0.9;
|
||||||
filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.9));
|
filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.8));
|
||||||
color: #ffd700;
|
color: #ffd700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -359,61 +337,6 @@ onMounted(() => {
|
||||||
to { opacity: 1; transform: translateY(0); }
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 热点点击区域 */
|
|
||||||
.hotspot-area {
|
|
||||||
position: absolute;
|
|
||||||
width: 150rpx;
|
|
||||||
height: 150rpx;
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 25;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 脉冲动效 */
|
|
||||||
.pulse-indicator {
|
|
||||||
position: relative;
|
|
||||||
width: 100rpx;
|
|
||||||
height: 100rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pulse-circle {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: rgba(255, 215, 0, 0.4);
|
|
||||||
border: 3rpx solid rgba(255, 215, 0, 0.8);
|
|
||||||
animation: pulse 1.5s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0% {
|
|
||||||
transform: translate(-50%, -50%) scale(0.8);
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate(-50%, -50%) scale(2);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.fu-word {
|
.fu-word {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
|
import { ref, onMounted, computed, watch } from 'vue'
|
||||||
import FuClickArea from './FuClickArea.vue'
|
|
||||||
|
|
||||||
// 组件属性
|
// 组件属性
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -17,19 +16,11 @@ const props = defineProps({
|
||||||
})
|
})
|
||||||
|
|
||||||
// 组件事件
|
// 组件事件
|
||||||
const emit = defineEmits(['collect-seal', 'height-changed'])
|
const emit = defineEmits(['collect-seal', 'play-drum', 'height-changed'])
|
||||||
|
|
||||||
// 是否收集福印
|
// 是否收集福印
|
||||||
const sealCollected = ref(false)
|
const sealCollected = ref(false)
|
||||||
|
|
||||||
// 音乐播放状态
|
|
||||||
const isMusicPlaying = ref(false)
|
|
||||||
const musicPlayer = ref(null)
|
|
||||||
|
|
||||||
// 福字点击区域状态
|
|
||||||
const fuClickAreaVisible = ref(true)
|
|
||||||
const sq1ImageVisible = ref(false)
|
|
||||||
|
|
||||||
// 场景高度
|
// 场景高度
|
||||||
const sceneHeight = ref(0)
|
const sceneHeight = ref(0)
|
||||||
|
|
||||||
|
|
@ -72,47 +63,28 @@ const parallaxOffset = computed(() => {
|
||||||
return offset
|
return offset
|
||||||
})
|
})
|
||||||
|
|
||||||
// 点击福字区域
|
// 收集福印
|
||||||
const handleFuClick = () => {
|
const collectSeal = () => {
|
||||||
fuClickAreaVisible.value = false
|
if (!sealCollected.value) {
|
||||||
sq1ImageVisible.value = true
|
sealCollected.value = true
|
||||||
emit('collect-seal')
|
emit('collect-seal')
|
||||||
|
showToast({
|
||||||
|
message: '恭喜获得非遗福印!',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 控制音乐播放/暂停
|
// 播放大鼓音效
|
||||||
const toggleMusic = () => {
|
const playDrum = () => {
|
||||||
if (!musicPlayer.value) {
|
emit('play-drum')
|
||||||
// 创建音频对象
|
collectSeal()
|
||||||
musicPlayer.value = uni.createInnerAudioContext()
|
showToast({
|
||||||
musicPlayer.value.src = '/static/music/bgm1.mp3'
|
message: '京韵大鼓,非遗福印!',
|
||||||
musicPlayer.value.loop = false
|
icon: 'info',
|
||||||
|
duration: 1500
|
||||||
// 监听播放结束
|
})
|
||||||
musicPlayer.value.onEnded(() => {
|
|
||||||
isMusicPlaying.value = false
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听错误
|
|
||||||
musicPlayer.value.onError((err) => {
|
|
||||||
console.error('音乐播放失败:', err)
|
|
||||||
isMusicPlaying.value = false
|
|
||||||
showToast({
|
|
||||||
message: '音乐播放失败',
|
|
||||||
icon: 'error',
|
|
||||||
duration: 1500
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMusicPlaying.value) {
|
|
||||||
// 正在播放,点击后停止
|
|
||||||
musicPlayer.value.stop()
|
|
||||||
isMusicPlaying.value = false
|
|
||||||
} else {
|
|
||||||
// 未播放,点击后开始播放
|
|
||||||
musicPlayer.value.play()
|
|
||||||
isMusicPlaying.value = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 页面挂载时的初始化
|
// 页面挂载时的初始化
|
||||||
|
|
@ -144,15 +116,6 @@ const calculateHeight = () => {
|
||||||
emit('height-changed', sceneHeight.value)
|
emit('height-changed', sceneHeight.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件卸载时清理
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (musicPlayer.value) {
|
|
||||||
musicPlayer.value.stop()
|
|
||||||
musicPlayer.value.destroy()
|
|
||||||
musicPlayer.value = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -160,7 +123,7 @@ onUnmounted(() => {
|
||||||
<!-- 背景图片层 -->
|
<!-- 背景图片层 -->
|
||||||
<div class="background-layer" ref="backgroundLayerRef" :style="{ transform: `translateY(${parallaxOffset}px)` }">
|
<div class="background-layer" ref="backgroundLayerRef" :style="{ transform: `translateY(${parallaxOffset}px)` }">
|
||||||
<!-- 使用复制到public目录的背景图片 -->
|
<!-- 使用复制到public目录的背景图片 -->
|
||||||
<img src="/static/bg/bg1.jpg" alt="前门商圈" class="background-image" />
|
<img src="/static/qianmen-bg.jpg" alt="前门商圈" class="background-image" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 增强动效层 -->
|
<!-- 增强动效层 -->
|
||||||
|
|
@ -170,40 +133,54 @@ onUnmounted(() => {
|
||||||
<div class="lantern left-lantern">🏮</div>
|
<div class="lantern left-lantern">🏮</div>
|
||||||
<div class="lantern right-lantern">🏮</div>
|
<div class="lantern right-lantern">🏮</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 福字增强动效 -->
|
||||||
|
<div class="fu-word">福</div>
|
||||||
|
|
||||||
|
<!-- 点击提示 -->
|
||||||
|
<div class="click-indicator" :class="{ 'animate-pulse': !sealCollected }">
|
||||||
|
<div class="pulse-circle"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 音乐控制按钮 -->
|
<!-- 大鼓交互区域 -->
|
||||||
<div class="music-control" @click="toggleMusic">
|
<div class="drum-interactive-area" @click="playDrum">
|
||||||
<img
|
<!-- 覆盖在图片大鼓上的点击区域 -->
|
||||||
src="/static/images/icon_music.png"
|
|
||||||
alt="音乐控制"
|
|
||||||
class="music-icon"
|
|
||||||
:class="{ 'playing': isMusicPlaying }"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 福字点击区域 -->
|
<!-- 烟花效果 -->
|
||||||
<FuClickArea
|
<div class="fireworks">
|
||||||
:visible="fuClickAreaVisible"
|
<div class="firework firework-1">🎆</div>
|
||||||
:x-range="630"
|
<div class="firework firework-2">🎇</div>
|
||||||
:y-range="400"
|
<div class="firework firework-3">🎆</div>
|
||||||
:y-start="350"
|
<div class="firework firework-4">🎇</div>
|
||||||
:fu-width="100"
|
</div>
|
||||||
:fu-height="100"
|
|
||||||
@click="handleFuClick"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- sq1图片 -->
|
<!-- 人物元素 -->
|
||||||
<img
|
<div
|
||||||
v-if="sq1ImageVisible"
|
class="character"
|
||||||
src="/static/images/sq1.png"
|
:style="{
|
||||||
alt="新春祝福"
|
left: `${characterPosition.x}%`,
|
||||||
class="sq1-image"
|
top: `${characterPosition.y}%`,
|
||||||
/>
|
transform: `translate(-50%, -50%)`
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
🧧
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 舞狮动画 -->
|
<!-- 烟花效果 -->
|
||||||
<div class="lion-dance"></div>
|
<div class="fireworks" v-if="showFireworks">
|
||||||
|
<div class="firework firework-1">🎆</div>
|
||||||
|
<div class="firework firework-2">🎇</div>
|
||||||
|
<div class="firework firework-3">🎆</div>
|
||||||
|
<div class="firework firework-4">🎇</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 福印收集标记 -->
|
||||||
|
<div v-if="sealCollected" class="seal-collected-mark">
|
||||||
|
<div class="seal-icon">🏮</div>
|
||||||
|
<div class="seal-text">已收集非遗福印</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -211,7 +188,8 @@ onUnmounted(() => {
|
||||||
.qianmen-scene-container {
|
.qianmen-scene-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto; /* 高度由内容决定 */
|
height: auto;
|
||||||
|
min-height: var(--scene-height, 100vh);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -224,16 +202,17 @@ onUnmounted(() => {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: transform 0.1s ease;
|
transition: transform 0.1s ease;
|
||||||
/* 背景图片决定容器高度 */
|
|
||||||
height: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.background-image {
|
.background-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
display: block;
|
display: block;
|
||||||
/* 确保图片完整显示,决定容器高度 */
|
}
|
||||||
object-fit: contain;
|
|
||||||
|
/* 为背景图片容器添加与图片主色调匹配的背景色,避免在小屏幕上出现黑边 */
|
||||||
|
.background-layer {
|
||||||
|
background-color: #ff6b35;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 增强动效层 */
|
/* 增强动效层 */
|
||||||
|
|
@ -420,51 +399,6 @@ onUnmounted(() => {
|
||||||
50% { opacity: 1; transform: scale(1.5); }
|
50% { opacity: 1; transform: scale(1.5); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 音乐控制按钮 */
|
|
||||||
.music-control {
|
|
||||||
position: absolute;
|
|
||||||
top: 810rpx;
|
|
||||||
left: 500rpx;
|
|
||||||
width: 82rpx;
|
|
||||||
height: 82rpx;
|
|
||||||
cursor: pointer;
|
|
||||||
z-index: 30;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.music-control:active {
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
.music-icon {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
opacity: 1;
|
|
||||||
animation: scale 1.5s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes scale {
|
|
||||||
0%, 100% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(1.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* sq1图片 */
|
|
||||||
.sq1-image {
|
|
||||||
position: absolute;
|
|
||||||
top: 220rpx;
|
|
||||||
right: -6rpx;
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
max-width: 300rpx;
|
|
||||||
z-index: 20;
|
|
||||||
animation: fadeIn 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 福印收集标记 */
|
/* 福印收集标记 */
|
||||||
.seal-collected-mark {
|
.seal-collected-mark {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -501,29 +435,6 @@ onUnmounted(() => {
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 舞狮动画(背景图切换) ========== */
|
|
||||||
.lion-dance {
|
|
||||||
position: absolute;
|
|
||||||
left: 0rpx;
|
|
||||||
top: 740rpx;
|
|
||||||
width: 425rpx;
|
|
||||||
height: 475rpx;
|
|
||||||
background-size: contain;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center;
|
|
||||||
animation: lionDance 0.9s infinite;
|
|
||||||
z-index: 25;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes lionDance {
|
|
||||||
0%, 16.66% { background-image: url('/static/animate/lion/lion1.png'); }
|
|
||||||
16.67%, 33.33% { background-image: url('/static/animate/lion/lion2.png'); }
|
|
||||||
33.34%, 50% { background-image: url('/static/animate/lion/lion3.png'); }
|
|
||||||
50.01%, 66.66% { background-image: url('/static/animate/lion/lion4.png'); }
|
|
||||||
66.67%, 83.33% { background-image: url('/static/animate/lion/lion5.png'); }
|
|
||||||
83.34%, 100% { background-image: url('/static/animate/lion/lion6.png'); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.fu-word {
|
.fu-word {
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,12 @@ import DongzhimenScene from './DongzhimenScene.vue'
|
||||||
import LongfusiScene from './LongfusiScene.vue'
|
import LongfusiScene from './LongfusiScene.vue'
|
||||||
import WangfujingScene from './WangfujingScene.vue'
|
import WangfujingScene from './WangfujingScene.vue'
|
||||||
import ChongwenScene from './ChongwenScene.vue'
|
import ChongwenScene from './ChongwenScene.vue'
|
||||||
import EndPage from './EndPage.vue'
|
|
||||||
|
|
||||||
const sceneStore = useSceneStore()
|
const sceneStore = useSceneStore()
|
||||||
const collectionStore = useCollectionStore()
|
const collectionStore = useCollectionStore()
|
||||||
|
|
||||||
// 当前活动场景索引
|
// 当前活动场景索引
|
||||||
const activeSceneIndex = ref(0)
|
const activeSceneIndex = ref(0)
|
||||||
// 标题图片是否已显示过(用于保持显示状态)
|
|
||||||
const titleImageShown = ref(false)
|
|
||||||
// 滑动容器引用
|
// 滑动容器引用
|
||||||
const scrollContainer = ref(null)
|
const scrollContainer = ref(null)
|
||||||
// 是否在滚动中
|
// 是否在滚动中
|
||||||
|
|
@ -63,6 +60,7 @@ const scenes = ref([
|
||||||
name: '东直门商圈',
|
name: '东直门商圈',
|
||||||
description: '京城东部的交通枢纽和商业中心',
|
description: '京城东部的交通枢纽和商业中心',
|
||||||
image: 'https://picsum.photos/id/1019/750/1600',
|
image: 'https://picsum.photos/id/1019/750/1600',
|
||||||
|
thumbnail: 'https://picsum.photos/id/1019/200/100',
|
||||||
interactionTip: '移动烤鸭,领取团圆福筷',
|
interactionTip: '移动烤鸭,领取团圆福筷',
|
||||||
collectedItem: '团圆福筷'
|
collectedItem: '团圆福筷'
|
||||||
},
|
},
|
||||||
|
|
@ -71,6 +69,7 @@ const scenes = ref([
|
||||||
name: '隆福寺商圈',
|
name: '隆福寺商圈',
|
||||||
description: '传统文化与现代艺术融合的潮流地标',
|
description: '传统文化与现代艺术融合的潮流地标',
|
||||||
image: 'https://picsum.photos/id/1018/750/1600',
|
image: 'https://picsum.photos/id/1018/750/1600',
|
||||||
|
thumbnail: 'https://picsum.photos/id/1018/200/100',
|
||||||
interactionTip: '点击文创物品,点亮文化福灯',
|
interactionTip: '点击文创物品,点亮文化福灯',
|
||||||
collectedItem: '文化福灯'
|
collectedItem: '文化福灯'
|
||||||
},
|
},
|
||||||
|
|
@ -79,6 +78,7 @@ const scenes = ref([
|
||||||
name: '王府井商圈',
|
name: '王府井商圈',
|
||||||
description: '北京最繁华的商业中心之一',
|
description: '北京最繁华的商业中心之一',
|
||||||
image: 'https://picsum.photos/id/1018/750/1600',
|
image: 'https://picsum.photos/id/1018/750/1600',
|
||||||
|
thumbnail: 'https://picsum.photos/id/1018/200/100',
|
||||||
interactionTip: '双指放大,收集金袋福卡',
|
interactionTip: '双指放大,收集金袋福卡',
|
||||||
collectedItem: '金袋福卡'
|
collectedItem: '金袋福卡'
|
||||||
},
|
},
|
||||||
|
|
@ -87,6 +87,7 @@ const scenes = ref([
|
||||||
name: '崇文门商圈',
|
name: '崇文门商圈',
|
||||||
description: '融合传统文化与现代商业的活力区域',
|
description: '融合传统文化与现代商业的活力区域',
|
||||||
image: 'https://picsum.photos/id/1016/750/1600',
|
image: 'https://picsum.photos/id/1016/750/1600',
|
||||||
|
thumbnail: 'https://picsum.photos/id/1016/200/100',
|
||||||
interactionTip: '滑动探索商圈,收集国潮福字',
|
interactionTip: '滑动探索商圈,收集国潮福字',
|
||||||
collectedItem: '国潮福字'
|
collectedItem: '国潮福字'
|
||||||
},
|
},
|
||||||
|
|
@ -133,17 +134,6 @@ const collectionProgress = computed(() => {
|
||||||
return Math.round((collected / total) * 100)
|
return Math.round((collected / total) * 100)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 计算福印收集对象(传递给 EndPage)
|
|
||||||
const collectedSeals = computed(() => {
|
|
||||||
return {
|
|
||||||
qianmen: sealCollectedStates.value[5] || false, // 前门 (索引 5)
|
|
||||||
chongwen: sealCollectedStates.value[4] || false, // 崇文门 (索引 4)
|
|
||||||
wangfujing: sealCollectedStates.value[3] || false, // 王府井 (索引 3)
|
|
||||||
longfusi: sealCollectedStates.value[2] || false, // 隆福寺 (索引 2)
|
|
||||||
dongzhimen: sealCollectedStates.value[1] || false // 东直门 (索引 1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 组件挂载后初始化
|
// 组件挂载后初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 检查并初始化场景交互状态
|
// 检查并初始化场景交互状态
|
||||||
|
|
@ -236,7 +226,6 @@ onMounted(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// 标记页面准备就绪,隐藏加载提示并显示页面
|
// 标记页面准备就绪,隐藏加载提示并显示页面
|
||||||
isPageReady.value = true
|
isPageReady.value = true
|
||||||
titleImageShown.value = true
|
|
||||||
console.log('设置 isPageReady = true,准备显示页面')
|
console.log('设置 isPageReady = true,准备显示页面')
|
||||||
}, 50)
|
}, 50)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -244,7 +233,6 @@ onMounted(() => {
|
||||||
// 滚动成功后显示容器
|
// 滚动成功后显示容器
|
||||||
// 标记页面准备就绪,隐藏加载提示并显示页面
|
// 标记页面准备就绪,隐藏加载提示并显示页面
|
||||||
isPageReady.value = true
|
isPageReady.value = true
|
||||||
titleImageShown.value = true
|
|
||||||
console.log('设置 isPageReady = true,准备显示页面')
|
console.log('设置 isPageReady = true,准备显示页面')
|
||||||
}
|
}
|
||||||
}, 300)
|
}, 300)
|
||||||
|
|
@ -285,10 +273,6 @@ const handleScroll = (event) => {
|
||||||
if (activeSceneIndex.value > 0 && activeSceneIndex.value < scenes.value.length - 1) {
|
if (activeSceneIndex.value > 0 && activeSceneIndex.value < scenes.value.length - 1) {
|
||||||
sceneStore.activateScene(scenes.value[activeSceneIndex.value].id)
|
sceneStore.activateScene(scenes.value[activeSceneIndex.value].id)
|
||||||
}
|
}
|
||||||
// 如果滚动到首页,标记标题图片已显示
|
|
||||||
if (clampedIndex === 6) {
|
|
||||||
titleImageShown.value = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -316,6 +300,18 @@ const scrollToScene = (index) => {
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 点击场景交互区域
|
||||||
|
const handleSceneInteraction = (index) => {
|
||||||
|
if (sceneInteractiveStates.value[index]) return
|
||||||
|
|
||||||
|
sceneInteractiveStates.value[index] = true
|
||||||
|
|
||||||
|
// 收集福印
|
||||||
|
setTimeout(() => {
|
||||||
|
collectSeal(index)
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
// 收集福印
|
// 收集福印
|
||||||
const collectSeal = (index) => {
|
const collectSeal = (index) => {
|
||||||
if (index < 1 || index >= scenes.value.length - 1) return
|
if (index < 1 || index >= scenes.value.length - 1) return
|
||||||
|
|
@ -487,23 +483,34 @@ onUnmounted(() => {
|
||||||
:scroll-into-view="homeSectionId"
|
:scroll-into-view="homeSectionId"
|
||||||
>
|
>
|
||||||
<!-- 结束页 -->
|
<!-- 结束页 -->
|
||||||
<EndPage
|
<section class="scene-section end-section" :class="{ 'active': activeSceneIndex === 0 }">
|
||||||
:is-active="activeSceneIndex === 0"
|
<div class="end-page-content">
|
||||||
:title="scenes[0].description"
|
<h1 class="end-title"><EFBFBD><EFBFBD> {{ scenes[0].description }} <EFBFBD><EFBFBD></h1>
|
||||||
:collection-progress="collectionProgress"
|
<div class="seal-collection-section">
|
||||||
:collected-count="collectedItems.length"
|
<h2 class="section-title">福印收集成果</h2>
|
||||||
:total-count="scenes.length - 1"
|
<div class="collection-progress">
|
||||||
:collected-seals="collectedSeals"
|
<div class="progress-bar">
|
||||||
@lottery="openLotteryForm"
|
<div class="progress-fill" :style="{ width: `${collectionProgress}%` }"></div>
|
||||||
@couplet="openAICoupletForm"
|
</div>
|
||||||
@restart="scrollToTop"
|
<p class="progress-text">已收集 {{ collectedItems.length }} / {{ scenes.length - 1 }} 个福印</p>
|
||||||
/>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="end-buttons">
|
||||||
|
<button class="function-btn lottery-btn" @click="openLotteryForm"><EFBFBD><EFBFBD> 参与抽奖</button>
|
||||||
|
<button class="function-btn couplet-btn" @click="openAICoupletForm">✍️ AI春联</button>
|
||||||
|
</div>
|
||||||
|
<div class="restart-btn-container">
|
||||||
|
<button class="restart-btn" @click="scrollToTop"><EFBFBD><EFBFBD> 重新游览</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- 东直门商圈 -->
|
<!-- 东直门商圈 -->
|
||||||
<DongzhimenScene
|
<DongzhimenScene
|
||||||
:active="activeSceneIndex === 1"
|
:active="activeSceneIndex === 1"
|
||||||
:scroll-position="scrollContainer && scrollContainer.value ? scrollContainer.value.scrollTop : 0"
|
:scroll-position="scrollContainer && scrollContainer.value ? scrollContainer.value.scrollTop : 0"
|
||||||
@collect-seal="collectSeal(1)"
|
@collect-seal="collectSeal(1)"
|
||||||
|
@play-drum="handleSceneInteraction(1)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 隆福寺商圈 -->
|
<!-- 隆福寺商圈 -->
|
||||||
|
|
@ -511,6 +518,7 @@ onUnmounted(() => {
|
||||||
:active="activeSceneIndex === 2"
|
:active="activeSceneIndex === 2"
|
||||||
:scroll-position="scrollContainer && scrollContainer.value ? scrollContainer.value.scrollTop : 0"
|
:scroll-position="scrollContainer && scrollContainer.value ? scrollContainer.value.scrollTop : 0"
|
||||||
@collect-seal="collectSeal(2)"
|
@collect-seal="collectSeal(2)"
|
||||||
|
@play-drum="handleSceneInteraction(2)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 王府井商圈 -->
|
<!-- 王府井商圈 -->
|
||||||
|
|
@ -518,6 +526,7 @@ onUnmounted(() => {
|
||||||
:active="activeSceneIndex === 3"
|
:active="activeSceneIndex === 3"
|
||||||
:scroll-position="scrollContainer?.value?.scrollTop || 0"
|
:scroll-position="scrollContainer?.value?.scrollTop || 0"
|
||||||
@collect-seal="collectSeal(3)"
|
@collect-seal="collectSeal(3)"
|
||||||
|
@play-drum="handleSceneInteraction(3)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 崇文门商圈 -->
|
<!-- 崇文门商圈 -->
|
||||||
|
|
@ -525,6 +534,7 @@ onUnmounted(() => {
|
||||||
:active="activeSceneIndex === 4"
|
:active="activeSceneIndex === 4"
|
||||||
:scroll-position="scrollContainer?.value?.scrollTop || 0"
|
:scroll-position="scrollContainer?.value?.scrollTop || 0"
|
||||||
@collect-seal="collectSeal(4)"
|
@collect-seal="collectSeal(4)"
|
||||||
|
@play-drum="handleSceneInteraction(4)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 前门商圈 -->
|
<!-- 前门商圈 -->
|
||||||
|
|
@ -532,43 +542,31 @@ onUnmounted(() => {
|
||||||
:active="activeSceneIndex === 5"
|
:active="activeSceneIndex === 5"
|
||||||
:scroll-position="scrollContainer?.value?.scrollTop || 0"
|
:scroll-position="scrollContainer?.value?.scrollTop || 0"
|
||||||
@collect-seal="collectSeal(5)"
|
@collect-seal="collectSeal(5)"
|
||||||
|
@play-drum="handleSceneInteraction(5)"
|
||||||
@height-changed="handleQianmenHeightChanged"
|
@height-changed="handleQianmenHeightChanged"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 新年文案模块 -->
|
|
||||||
<section class="new-year-section">
|
|
||||||
<div class="new-year-content">
|
|
||||||
<img
|
|
||||||
src="/static/images/main_text.png"
|
|
||||||
alt="新年文案"
|
|
||||||
class="new-year-text"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 首页 -->
|
<!-- 首页 -->
|
||||||
<section id="home-section" class="scene-section home-section" :class="{ 'active': activeSceneIndex === 6 }">
|
<section id="home-section" class="scene-section home-section" :class="{ 'active': activeSceneIndex === 6 }">
|
||||||
<!-- 背景图片层 -->
|
<!-- 背景图片层 -->
|
||||||
<div class="home-bg">
|
<div class="home-bg">
|
||||||
<img
|
<img
|
||||||
src="/static/bg/bg_main.jpg"
|
src="/static/bg/bg1.jpg"
|
||||||
alt="2026新春东城商圈"
|
alt="2026新春东城商圈"
|
||||||
class="bg-image"
|
class="bg-image"
|
||||||
@error="handleImageError"
|
@error="handleImageError"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 标题图片 -->
|
<!-- 内容层 -->
|
||||||
<img
|
<div class="scene-content">
|
||||||
src="/static/images/img_title.png"
|
<h1 class="title">{{ scenes[6].description }}</h1>
|
||||||
alt="马年新春2026"
|
<p class="subtitle">探索东城五大商圈</p>
|
||||||
class="title-image"
|
</div>
|
||||||
:class="{ 'title-image-active': titleImageShown }"
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 向上滑动提示 -->
|
<!-- 向上滑动提示 -->
|
||||||
<!-- <div class="scroll-tip-bottom">
|
<!-- <div class="scroll-tip-bottom">
|
||||||
<div class="tip-icon"></div>
|
<div class="tip-icon"><EFBFBD><EFBFBD></div>
|
||||||
<p>向上滑动探索商圈</p>
|
<p>向上滑动探索商圈</p>
|
||||||
</div> -->
|
</div> -->
|
||||||
|
|
||||||
|
|
@ -705,9 +703,10 @@ onUnmounted(() => {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: scroll; /* 改为 scroll 确保可以滚动 */
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
-webkit-overflow-scrolling: touch; /* 启用iOS平滑滚动 */
|
-webkit-overflow-scrolling: touch; /* 启用iOS平滑滚动 */
|
||||||
|
scrollbar-width: thin; /* Firefox滚动条样式 */
|
||||||
/* 使用 visibility 而不是 opacity,确保完全不可见且不可交互 */
|
/* 使用 visibility 而不是 opacity,确保完全不可见且不可交互 */
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
touch-action: pan-y; /* 允许垂直方向的触摸滚动 */
|
touch-action: pan-y; /* 允许垂直方向的触摸滚动 */
|
||||||
|
|
@ -725,40 +724,22 @@ onUnmounted(() => {
|
||||||
to { opacity: 1; }
|
to { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== 隐藏滚动条样式(所有浏览器)========== */
|
/* 滚动条样式 */
|
||||||
|
|
||||||
/* WebKit 浏览器(Chrome、Safari、新版 Edge)*/
|
|
||||||
.single-page-container::-webkit-scrollbar {
|
.single-page-container::-webkit-scrollbar {
|
||||||
width: 0 !important;
|
width: 6px;
|
||||||
height: 0 !important;
|
|
||||||
display: none !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.single-page-container::-webkit-scrollbar-track {
|
.single-page-container::-webkit-scrollbar-track {
|
||||||
background: transparent !important;
|
background: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.single-page-container::-webkit-scrollbar-thumb {
|
.single-page-container::-webkit-scrollbar-thumb {
|
||||||
background: transparent !important;
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.single-page-container::-webkit-scrollbar-corner {
|
.single-page-container::-webkit-scrollbar-thumb:hover {
|
||||||
background: transparent !important;
|
background: rgba(0, 0, 0, 0.5);
|
||||||
}
|
|
||||||
|
|
||||||
/* Firefox */
|
|
||||||
.single-page-container {
|
|
||||||
scrollbar-width: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* IE 和旧版 Edge */
|
|
||||||
.single-page-container {
|
|
||||||
-ms-overflow-style: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 通用样式 */
|
|
||||||
.single-page-container {
|
|
||||||
scrollbar-color: transparent transparent !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 场景部分样式 */
|
/* 场景部分样式 */
|
||||||
|
|
@ -864,6 +845,118 @@ onUnmounted(() => {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 结束页样式 */
|
||||||
|
.end-section {
|
||||||
|
background-color: #FDE9DF;
|
||||||
|
height: var(--scene-height, 100vh);
|
||||||
|
}
|
||||||
|
|
||||||
|
.end-page-content {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 640px;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.end-title {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #FF6B35;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seal-collection-section {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-progress {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 10px;
|
||||||
|
background-color: #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #FF6B35;
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.end-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-btn {
|
||||||
|
padding: 15px 30px;
|
||||||
|
font-size: 18px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lottery-btn {
|
||||||
|
background-color: #FFD700;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lottery-btn:hover {
|
||||||
|
background-color: #FFC107;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.couplet-btn {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.couplet-btn:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.restart-btn-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restart-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #999;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.restart-btn:hover {
|
||||||
|
background-color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
/* 弹窗样式 */
|
/* 弹窗样式 */
|
||||||
.form-modal,
|
.form-modal,
|
||||||
.couplet-display-modal {
|
.couplet-display-modal {
|
||||||
|
|
@ -1116,9 +1209,8 @@ onUnmounted(() => {
|
||||||
.home-section {
|
.home-section {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: center;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
@ -1139,6 +1231,17 @@ onUnmounted(() => {
|
||||||
object-position: center center;
|
object-position: center center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.home-section .scene-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.85);
|
||||||
|
border-radius: 15px;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
/* 向上滑动提示样式 */
|
/* 向上滑动提示样式 */
|
||||||
.scroll-tip-bottom {
|
.scroll-tip-bottom {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
@ -1164,43 +1267,17 @@ onUnmounted(() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 新年文案模块样式 */
|
.home-section .title {
|
||||||
.new-year-section {
|
font-size: 36px;
|
||||||
width: 100%;
|
color: #FF6B35;
|
||||||
background-color: #f94332;
|
margin-bottom: 15px;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
position: relative;
|
|
||||||
padding: 15px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-year-content {
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-year-text {
|
.home-section .subtitle {
|
||||||
display: inline-block;
|
font-size: 18px;
|
||||||
width: 546rpx;
|
color: #666;
|
||||||
height: auto;
|
text-align: center;
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 标题图片样式 */
|
|
||||||
.title-image {
|
|
||||||
width: 80%;
|
|
||||||
max-width: 400px;
|
|
||||||
height: auto;
|
|
||||||
margin: 40rpx auto 20px;
|
|
||||||
display: block;
|
|
||||||
transform: scale(0);
|
|
||||||
transition: transform 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-image-active {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll-tip {
|
.scroll-tip {
|
||||||
|
|
@ -1236,6 +1313,19 @@ onUnmounted(() => {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.end-title {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.end-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-btn {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
.couplet-item {
|
.couplet-item {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import FuClickArea from './FuClickArea.vue'
|
|
||||||
|
|
||||||
// 组件属性
|
// 组件属性
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -17,43 +16,39 @@ const props = defineProps({
|
||||||
})
|
})
|
||||||
|
|
||||||
// 组件事件
|
// 组件事件
|
||||||
const emit = defineEmits(['collect-seal'])
|
const emit = defineEmits(['collect-seal', 'play-drum'])
|
||||||
|
|
||||||
// 是否收集福印
|
// 是否收集福印
|
||||||
const sealCollected = ref(false)
|
const sealCollected = ref(false)
|
||||||
|
|
||||||
// 福字点击区域状态
|
|
||||||
const fuClickAreaVisible = ref(true)
|
|
||||||
const sq3ImageVisible = ref(false)
|
|
||||||
|
|
||||||
// 计算视差效果的偏移量
|
// 计算视差效果的偏移量
|
||||||
const parallaxOffset = computed(() => {
|
const parallaxOffset = computed(() => {
|
||||||
// 滚动位置的1/10作为视差偏移
|
// 滚动位置的1/10作为视差偏移
|
||||||
return props.scrollPosition * 0.1
|
return props.scrollPosition * 0.1
|
||||||
})
|
})
|
||||||
|
|
||||||
// 点击福字区域
|
// 收集福印
|
||||||
const handleFuClick = () => {
|
const collectSeal = () => {
|
||||||
fuClickAreaVisible.value = false
|
if (!sealCollected.value) {
|
||||||
sq3ImageVisible.value = true
|
sealCollected.value = true
|
||||||
emit('collect-seal')
|
emit('collect-seal')
|
||||||
|
showToast({
|
||||||
|
message: '恭喜获得金袋福卡!',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 图片浏览数据
|
// 播放音效
|
||||||
const images = [
|
const playDrum = () => {
|
||||||
{ src: '/static/wfj/img1.png', title: '传艺承福阁' },
|
emit('play-drum')
|
||||||
{ src: '/static/wfj/img2.png', title: '京味福食巷' },
|
collectSeal()
|
||||||
{ src: '/static/wfj/img3.png', title: '雅趣福玩斋' }
|
showToast({
|
||||||
]
|
message: '双指放大,收集金袋福卡!',
|
||||||
const currentImageIndex = ref(0)
|
icon: 'info',
|
||||||
|
duration: 1500
|
||||||
// 切换图片
|
})
|
||||||
const prevImage = () => {
|
|
||||||
currentImageIndex.value = (currentImageIndex.value - 1 + images.length) % images.length
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextImage = () => {
|
|
||||||
currentImageIndex.value = (currentImageIndex.value + 1) % images.length
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 页面挂载时的初始化
|
// 页面挂载时的初始化
|
||||||
|
|
@ -70,45 +65,45 @@ onMounted(() => {
|
||||||
<section class="wangfujing-scene-container" :class="{ 'active': active }">
|
<section class="wangfujing-scene-container" :class="{ 'active': active }">
|
||||||
<!-- 背景图片层 -->
|
<!-- 背景图片层 -->
|
||||||
<div class="background-layer" :style="{ transform: `translateY(${parallaxOffset}px)` }">
|
<div class="background-layer" :style="{ transform: `translateY(${parallaxOffset}px)` }">
|
||||||
<!-- 使用王府井商圈背景图片 -->
|
<!-- 使用前门商圈的背景图片作为占位 -->
|
||||||
<img src="/static/bg/bg3.jpg" alt="王府井商圈" class="background-image" />
|
<img src="/static/qianmen-bg.jpg" alt="王府井商圈" class="background-image" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 福字点击区域 -->
|
<!-- 增强动效层 -->
|
||||||
<FuClickArea
|
<div class="enhancement-layer">
|
||||||
:visible="fuClickAreaVisible"
|
<!-- 灯笼增强动效 -->
|
||||||
:x-range="630"
|
<div class="lanterns">
|
||||||
:y-range="900"
|
<div class="lantern left-lantern">🏮</div>
|
||||||
:y-start="150"
|
<div class="lantern right-lantern">🏮</div>
|
||||||
:fu-width="100"
|
|
||||||
:fu-height="100"
|
|
||||||
@click="handleFuClick"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- sq3图片 -->
|
|
||||||
<img
|
|
||||||
v-if="sq3ImageVisible"
|
|
||||||
src="/static/images/sq3.png"
|
|
||||||
alt="新春祝福"
|
|
||||||
class="sq3-image"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 图片浏览组件 -->
|
|
||||||
<div class="image-gallery">
|
|
||||||
<div class="gallery-image-wrapper">
|
|
||||||
<img :src="images[currentImageIndex].src" :alt="images[currentImageIndex].title" class="gallery-image" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="gallery-controls">
|
|
||||||
<div class="nav-btn prev-btn" @click="prevImage">
|
<!-- 福字增强动效 -->
|
||||||
<img src="/static/images/btn_prev.png" alt="上一张" class="nav-icon" />
|
<div class="fu-word">福</div>
|
||||||
</div>
|
|
||||||
<div class="gallery-title">{{ images[currentImageIndex].title }}</div>
|
<!-- 点击提示 -->
|
||||||
<div class="nav-btn next-btn" @click="nextImage">
|
<div class="click-indicator" :class="{ 'animate-pulse': !sealCollected }">
|
||||||
<img src="/static/images/btn_next.png" alt="下一张" class="nav-icon" />
|
<div class="pulse-circle"></div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 交互区域 -->
|
||||||
|
<div class="interaction-area" @click="playDrum">
|
||||||
|
<!-- 覆盖在图片上的点击区域 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 烟花效果 -->
|
||||||
|
<div class="fireworks">
|
||||||
|
<div class="firework firework-1">🎆</div>
|
||||||
|
<div class="firework firework-2">🎇</div>
|
||||||
|
<div class="firework firework-3">🎆</div>
|
||||||
|
<div class="firework firework-4">🎇</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 福印收集标记 -->
|
||||||
|
<div v-if="sealCollected" class="seal-collected-mark">
|
||||||
|
<div class="seal-icon">🏮</div>
|
||||||
|
<div class="seal-text">已收集金袋福卡</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -116,7 +111,8 @@ onMounted(() => {
|
||||||
.wangfujing-scene-container {
|
.wangfujing-scene-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto; /* 高度由内容决定 */
|
height: auto;
|
||||||
|
min-height: var(--scene-height, 100vh);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -129,16 +125,17 @@ onMounted(() => {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: transform 0.1s ease;
|
transition: transform 0.1s ease;
|
||||||
/* 背景图片决定容器高度 */
|
|
||||||
height: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.background-image {
|
.background-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
display: block;
|
display: block;
|
||||||
/* 确保图片完整显示,决定容器高度 */
|
}
|
||||||
object-fit: contain;
|
|
||||||
|
/* 为背景图片容器添加与图片主色调匹配的背景色,避免在小屏幕上出现黑边 */
|
||||||
|
.background-layer {
|
||||||
|
background-color: #ff6b35;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 增强动效层 */
|
/* 增强动效层 */
|
||||||
|
|
@ -340,83 +337,6 @@ onMounted(() => {
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* sq3图片 */
|
|
||||||
.sq3-image {
|
|
||||||
position: absolute;
|
|
||||||
top: 220rpx;
|
|
||||||
right: -6rpx;
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
max-width: 300rpx;
|
|
||||||
z-index: 20;
|
|
||||||
animation: fadeIn 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 图片浏览组件 */
|
|
||||||
.image-gallery {
|
|
||||||
position: absolute;
|
|
||||||
left: 40rpx;
|
|
||||||
top: 1380rpx;
|
|
||||||
width: 670rpx;
|
|
||||||
background-color: #d72717;
|
|
||||||
border-radius: 20rpx;
|
|
||||||
padding: 20rpx;
|
|
||||||
box-sizing: border-box;
|
|
||||||
z-index: 30;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 上方图片区域 */
|
|
||||||
.gallery-image-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-image {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
object-fit: contain;
|
|
||||||
border-radius: 12rpx;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 下方控制区域 */
|
|
||||||
.gallery-controls {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
height: 80rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn {
|
|
||||||
width: 70rpx;
|
|
||||||
height: 70rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-icon {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery-title {
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 40rpx;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: 6rpx;
|
|
||||||
padding: 0 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.fu-word {
|
.fu-word {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name" : "xinchun2026",
|
"name" : "xinchun2026",
|
||||||
"appid" : "__UNI__F0A5E90",
|
"appid" : "",
|
||||||
"description" : "",
|
"description" : "",
|
||||||
"versionName" : "1.0.0",
|
"versionName" : "1.0.0",
|
||||||
"versionCode" : "100",
|
"versionCode" : "100",
|
||||||
|
|
@ -68,8 +68,5 @@
|
||||||
"uniStatistics" : {
|
"uniStatistics" : {
|
||||||
"enable" : false
|
"enable" : false
|
||||||
},
|
},
|
||||||
"vueVersion" : "3",
|
"vueVersion" : "3"
|
||||||
"h5" : {
|
|
||||||
"title" : "2026新春H5"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -380,15 +380,6 @@
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hammerjs": {
|
|
||||||
"version": "2.0.8",
|
|
||||||
"resolved": "https://registry.npmmirror.com/hammerjs/-/hammerjs-2.0.8.tgz",
|
|
||||||
"integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hookable": {
|
"node_modules/hookable": {
|
||||||
"version": "5.5.3",
|
"version": "5.5.3",
|
||||||
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
|
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"json": "bower.json"
|
|
||||||
}
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
{
|
|
||||||
"excludeFiles": [
|
|
||||||
"*.js",
|
|
||||||
"tests/**/assets",
|
|
||||||
"node_modules/**"
|
|
||||||
],
|
|
||||||
"requireCurlyBraces": [
|
|
||||||
"if",
|
|
||||||
"else",
|
|
||||||
"for",
|
|
||||||
"while",
|
|
||||||
"do",
|
|
||||||
"try",
|
|
||||||
"catch"
|
|
||||||
],
|
|
||||||
"requireOperatorBeforeLineBreak": true,
|
|
||||||
"requireCamelCaseOrUpperCaseIdentifiers": true,
|
|
||||||
"maximumLineLength": {
|
|
||||||
"value": 120,
|
|
||||||
"allowComments": true,
|
|
||||||
"allowRegex": true
|
|
||||||
},
|
|
||||||
"validateIndentation": 4,
|
|
||||||
"validateQuoteMarks": "'",
|
|
||||||
"disallowMultipleLineStrings": true,
|
|
||||||
"disallowMixedSpacesAndTabs": true,
|
|
||||||
"disallowTrailingWhitespace": true,
|
|
||||||
"disallowSpaceAfterPrefixUnaryOperators": true,
|
|
||||||
"requireSpaceAfterKeywords": [
|
|
||||||
"if",
|
|
||||||
"else",
|
|
||||||
"for",
|
|
||||||
"while",
|
|
||||||
"do",
|
|
||||||
"switch",
|
|
||||||
"return",
|
|
||||||
"try",
|
|
||||||
"catch"
|
|
||||||
],
|
|
||||||
"requireSpaceBeforeBinaryOperators": [
|
|
||||||
"=",
|
|
||||||
"+=",
|
|
||||||
"-=",
|
|
||||||
"*=",
|
|
||||||
"/=",
|
|
||||||
"%=",
|
|
||||||
"<<=",
|
|
||||||
">>=",
|
|
||||||
">>>=",
|
|
||||||
"&=",
|
|
||||||
"|=",
|
|
||||||
"^=",
|
|
||||||
"+=",
|
|
||||||
"+",
|
|
||||||
"-",
|
|
||||||
"*",
|
|
||||||
"/",
|
|
||||||
"%",
|
|
||||||
"<<",
|
|
||||||
">>",
|
|
||||||
">>>",
|
|
||||||
"&",
|
|
||||||
"|",
|
|
||||||
"^",
|
|
||||||
"&&",
|
|
||||||
"||",
|
|
||||||
"===",
|
|
||||||
"==",
|
|
||||||
">=",
|
|
||||||
"<=",
|
|
||||||
"<",
|
|
||||||
">",
|
|
||||||
"!=",
|
|
||||||
"!=="
|
|
||||||
],
|
|
||||||
"requireSpaceAfterBinaryOperators": true,
|
|
||||||
"requireSpacesInConditionalExpression": true,
|
|
||||||
"requireSpaceBeforeBlockStatements": true,
|
|
||||||
"requireLineFeedAtFileEnd": true,
|
|
||||||
"requireSpacesInFunctionExpression": {
|
|
||||||
"beforeOpeningCurlyBrace": true
|
|
||||||
},
|
|
||||||
"disallowSpacesInAnonymousFunctionExpression": {
|
|
||||||
"beforeOpeningRoundBrace": true
|
|
||||||
},
|
|
||||||
"disallowSpacesInsideObjectBrackets": "all",
|
|
||||||
"disallowSpacesInsideArrayBrackets": "all",
|
|
||||||
"disallowSpacesInsideParentheses": true,
|
|
||||||
"validateJSDoc": {
|
|
||||||
"checkParamNames": true,
|
|
||||||
"requireParamTypes": true
|
|
||||||
},
|
|
||||||
"disallowMultipleLineBreaks": true,
|
|
||||||
"disallowNewlineBeforeBlockStatements": true
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
{
|
|
||||||
"browser": true,
|
|
||||||
"curly": true,
|
|
||||||
"eqnull": true,
|
|
||||||
"expr": true,
|
|
||||||
"maxerr": 100,
|
|
||||||
"freeze": true,
|
|
||||||
"newcap": true,
|
|
||||||
"node": true,
|
|
||||||
"quotmark": "single",
|
|
||||||
"strict": true,
|
|
||||||
"sub": true,
|
|
||||||
"trailing": true,
|
|
||||||
"undef": true,
|
|
||||||
"unused": true,
|
|
||||||
"camelcase": true,
|
|
||||||
"indent": 4,
|
|
||||||
"validthis": true,
|
|
||||||
"globals": {
|
|
||||||
"define": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
# ide
|
|
||||||
.idea
|
|
||||||
.iml
|
|
||||||
|
|
||||||
# node
|
|
||||||
lib-cov
|
|
||||||
*.seed
|
|
||||||
*.log
|
|
||||||
*.csv
|
|
||||||
*.dat
|
|
||||||
*.out
|
|
||||||
*.pid
|
|
||||||
*.gz
|
|
||||||
|
|
||||||
pids
|
|
||||||
logs
|
|
||||||
results
|
|
||||||
tests/build.js
|
|
||||||
|
|
||||||
npm-debug.log
|
|
||||||
node_modules
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
language: node_js
|
|
||||||
node_js:
|
|
||||||
- "0.10"
|
|
||||||
|
|
||||||
sudo: false
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- npm install -g grunt-cli
|
|
||||||
|
|
||||||
script:
|
|
||||||
- grunt test-travis
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
# Changelog
|
|
||||||
|
|
||||||
### 2.0.6, 2015-12-23
|
|
||||||
- Add Assign method and deprecate merge and extend ([#895](https://github.com/hammerjs/hammer.js/pull/895)[fc01eae](https://github.com/hammerjs/hammer.js/commit/fc01eaea678acc430c664eb374555fbe3d403bdd))
|
|
||||||
- Expose Hammer on window or self if either is defined to avoid issues when AMD is present but not used. ( [356f795](https://github.com/hammerjs/hammer.js/commit/356f7955b01f3679c29d6c45931679256b45036e))
|
|
||||||
- Add support for PointerEvent instead of MSPointerEvent if supported. ([#754](https://github.com/hammerjs/hammer.js/issues/754), [439c7a6](https://github.com/hammerjs/hammer.js/commit/439c7a6c46978ab387b4b8289399e904d1c49535))
|
|
||||||
- Fixed moz-prefix, prefix should be Moz not moz. ([3ea47f3](https://github.com/hammerjs/hammer.js/commit/3ea47f3aebadc9d3bb6bf52bc8402cad135ef8a9))
|
|
||||||
- Removed non-existant recognizer ([f1c2d3b](https://github.com/hammerjs/hammer.js/commit/f1c2d3bf05f530ae092ecfc2335fceeff0e9eec9))
|
|
||||||
- Fixed config leaking between instances([189098f](https://github.com/hammerjs/hammer.js/commit/189098ff7736f6ed2fce9a3d3e1f5a3afee085ba))
|
|
||||||
- Fixed gaps in gesture configs and update tests to match ([70c2902](https://github.com/hammerjs/hammer.js/commit/70c2902d773a750e92ce8c423f8a4165c07eab97))
|
|
||||||
- Fixed Manager off method ([#768](https://github.com/hammerjs/hammer.js/issues/768), [da49a27](https://github.com/hammerjs/hammer.js/commit/da49a2730779ecc3b4dd147cc418a0df7c70fad9))
|
|
||||||
- Added compatibility with requirejs optimizer namespaces ( [70075f2](https://github.com/hammerjs/hammer.js/commit/70075f2df1b855f7c6d8d3caac49b9276b88c8d6))
|
|
||||||
- Made touchaction test zoomable ( [50264a7](https://github.com/hammerjs/hammer.js/commit/50264a70251ca88bbaf7b666401e527eee616de5))
|
|
||||||
- Fixed preventing default when for `pan-x pan-y` case ( [95eaafa](https://github.com/hammerjs/hammer.js/commit/95eaafadad27bd1b25d20cf976811a451922f1c4))
|
|
||||||
- Fixed incorrect touch action pan direction ( [a81da57](https://github.com/hammerjs/hammer.js/commit/a81da57a82ebf37e695e7c443e4e2715e7f32856))
|
|
||||||
- Fixed combined pan-x pan-y to resolve to none ( [fdae07b](https://github.com/hammerjs/hammer.js/commit/fdae07bc2ba3c90aad28da6791b3d5df627bc612))
|
|
||||||
- Fixed inverted touch-action for pan recognizer ([#728](https://github.com/hammerjs/hammer.js/issues/728), [605bd3b](https://github.com/hammerjs/hammer.js/commit/605bd3beca780be91dd43f9da8b809d155a43d1a))
|
|
||||||
- Fixed dependency on non standard touch list ordering ([#610](https://github.com/hammerjs/hammer.js/issues/610), [#791](https://github.com/hammerjs/hammer.js/issues/791), [287720a](https://github.com/hammerjs/hammer.js/commit/287720a6e5067e7f28be8b8b3b266d22905361c4))
|
|
||||||
- Fixed swipe to not trigger after multitouch gesture ([#640](https://github.com/hammerjs/hammer.js/issues/640), [711d8a1](https://github.com/hammerjs/hammer.js/commit/711d8a1df1aa5057ecb536454a36257e3c0d6d91))
|
|
||||||
- Fixed swipe recognizer to use overall gesture direction and velocity ( [963fe69](https://github.com/hammerjs/hammer.js/commit/963fe697515273fee508414bc29e2656465cea55))
|
|
||||||
- Fixed getDirection returning reversed direction ( [e40dcde](https://github.com/hammerjs/hammer.js/commit/e40dcde43bdac7a74c8ce5c05a4f62121089cd91))
|
|
||||||
- Fixed detection of tap when multi touch gestures are present ( [c46cbba](https://github.com/hammerjs/hammer.js/commit/c46cbba1c2cbbf874b59913416858d9dae297e64))
|
|
||||||
- Fixed incorrect event order ([#824](https://github.com/hammerjs/hammer.js/issues/824), [92f2d76](https://github.com/hammerjs/hammer.js/commit/92f2d76188480d967e738a19cd508d0b94a31329))
|
|
||||||
- Fixed leaking options between recognizer instances ([#813](https://github.com/hammerjs/hammer.js/issues/813), [af32c9b](https://github.com/hammerjs/hammer.js/commit/af32c9bace3f04bb34bee852ff56a33cc8fc27cd))
|
|
||||||
- Fixed detection when element has no style attribute ( [5ca6d8c](https://github.com/hammerjs/hammer.js/commit/5ca6d8cbead02c71929a8073e95ddf98e11c0e06))
|
|
||||||
|
|
||||||
### 2.0.4, 2014-09-28
|
|
||||||
- Fix IE pointer issue. [#665](https://github.com/hammerjs/hammer.js/pull/665)
|
|
||||||
- Fix multi-touch at different elements. [#668](https://github.com/hammerjs/hammer.js/pull/668)
|
|
||||||
- Added experimental [single-user Touch input handler](src/input/singletouch.js). This to improve performance/ux when only a single user has to be supported. Plans are to release 2.1 with this as default, and a settings to enable the multi-user handler.
|
|
||||||
|
|
||||||
### 2.0.3, 2014-09-10
|
|
||||||
- Manager.set improvements.
|
|
||||||
- Fix requireFailure() call in Manager.options.recognizers.
|
|
||||||
- Make DIRECTION_ALL for pan and swipe gestures less blocking.
|
|
||||||
- Fix Swipe recognizer threshold option.
|
|
||||||
- Expose the Input classes.
|
|
||||||
- Added the option `inputClass` to set the used input handler.
|
|
||||||
|
|
||||||
### 2.0.2, 2014-07-26
|
|
||||||
- Improved mouse and pointer-events input, now able to move outside the window.
|
|
||||||
- Added the export name (`Hammer`) as an argument to the wrapper.
|
|
||||||
- Add the option *experimental* `inputTarget` to change the element that receives the events.
|
|
||||||
- Improved performance when only one touch being active.
|
|
||||||
- Fixed the jumping deltaXY bug when going from single to multi-touch.
|
|
||||||
- Improved velocity calculations.
|
|
||||||
|
|
||||||
### 2.0.1, 2014-07-15
|
|
||||||
- Fix issue when no document.body is available
|
|
||||||
- Added pressup event for the press recognizer
|
|
||||||
- Removed alternative for Object.create
|
|
||||||
|
|
||||||
### 2.0.0, 2014-07-11
|
|
||||||
- Full rewrite of the library.
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
# Contributing to Hammer.js
|
|
||||||
|
|
||||||
Looking to contribute something to Hammer.js? **Here's how you can help.**
|
|
||||||
|
|
||||||
|
|
||||||
## Reporting issues
|
|
||||||
|
|
||||||
We only accept issues that are bug reports or feature requests. Bugs must be
|
|
||||||
isolated and reproducible problems that can be fixed within the Hammer.js.
|
|
||||||
Please read the following guidelines before opening any issue.
|
|
||||||
|
|
||||||
1. [**Read the documentation**](https://hammerjs.github.io)
|
|
||||||
|
|
||||||
2. **Search for existing issues.** We get a lot of duplicate issues, and you'd
|
|
||||||
help us out a lot by first checking if someone else has reported the same issue.
|
|
||||||
Moreover, the issue may have already been resolved with a fix available. Also
|
|
||||||
take a look if your problem is explained at the Wiki.
|
|
||||||
|
|
||||||
3. **Create an isolated and reproducible test case.** Be sure the problem exists
|
|
||||||
in Hammer's code with a reduced test case that should be included in each bug
|
|
||||||
report.
|
|
||||||
|
|
||||||
4. **Include a live example.** Make use of jsFiddle or jsBin to share your
|
|
||||||
isolated test cases. Also, a screen capture would work, with tools like LICEcap.
|
|
||||||
|
|
||||||
5. **Share as much information as possible.** Include operating system and
|
|
||||||
version, browser and version, version of Hammer.js, customized or vanilla build,
|
|
||||||
etc. where appropriate. Also include steps to reproduce the bug.
|
|
||||||
|
|
||||||
## Pull requests
|
|
||||||
|
|
||||||
1. Changes must be done in `/src` files, never just the compiled files. Also, don't
|
|
||||||
commit the compiled files.
|
|
||||||
|
|
||||||
2. Try not to pollute your pull request with unintended changes. Keep them simple
|
|
||||||
and small
|
|
||||||
|
|
||||||
3. Try to share which browsers your code has been tested in before submitting a
|
|
||||||
pull request
|
|
||||||
|
|
||||||
4. Write tests for your code, these can be found in `/tests`.
|
|
||||||
|
|
@ -1,124 +0,0 @@
|
||||||
module.exports = (grunt) ->
|
|
||||||
grunt.initConfig
|
|
||||||
pkg: grunt.file.readJSON 'package.json'
|
|
||||||
|
|
||||||
usebanner:
|
|
||||||
taskName:
|
|
||||||
options:
|
|
||||||
position: 'top'
|
|
||||||
banner: '
|
|
||||||
/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n
|
|
||||||
* <%= pkg.homepage %>\n
|
|
||||||
*\n
|
|
||||||
* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;\n
|
|
||||||
* Licensed under the <%= pkg.license %> license */'
|
|
||||||
linebreak: true
|
|
||||||
files:
|
|
||||||
src: ['./hammer.js','./hammer.min.js']
|
|
||||||
|
|
||||||
concat:
|
|
||||||
build:
|
|
||||||
src: [
|
|
||||||
'src/hammer.prefix.js'
|
|
||||||
'src/utils.js'
|
|
||||||
'src/input.js'
|
|
||||||
'src/input/*.js'
|
|
||||||
'src/touchaction.js'
|
|
||||||
'src/recognizer.js'
|
|
||||||
'src/recognizers/*.js'
|
|
||||||
'src/hammer.js'
|
|
||||||
'src/manager.js'
|
|
||||||
'src/expose.js'
|
|
||||||
'src/hammer.suffix.js']
|
|
||||||
dest: 'hammer.js'
|
|
||||||
|
|
||||||
uglify:
|
|
||||||
min:
|
|
||||||
options:
|
|
||||||
report: 'gzip'
|
|
||||||
sourceMap: 'hammer.min.map'
|
|
||||||
files:
|
|
||||||
'hammer.min.js': ['hammer.js']
|
|
||||||
# special test build that exposes everything so it's testable
|
|
||||||
test:
|
|
||||||
options:
|
|
||||||
wrap: "$H"
|
|
||||||
comments: 'all'
|
|
||||||
exportAll: true
|
|
||||||
mangle: false
|
|
||||||
beautify: true
|
|
||||||
compress:
|
|
||||||
global_defs:
|
|
||||||
exportName: 'Hammer'
|
|
||||||
files:
|
|
||||||
'tests/build.js': [
|
|
||||||
'src/utils.js'
|
|
||||||
'src/input.js'
|
|
||||||
'src/input/*.js'
|
|
||||||
'src/touchaction.js'
|
|
||||||
'src/recognizer.js'
|
|
||||||
'src/recognizers/*.js'
|
|
||||||
'src/hammer.js'
|
|
||||||
'src/manager.js'
|
|
||||||
'src/expose.js']
|
|
||||||
|
|
||||||
'string-replace':
|
|
||||||
version:
|
|
||||||
files:
|
|
||||||
'hammer.js': 'hammer.js'
|
|
||||||
options:
|
|
||||||
replacements: [
|
|
||||||
pattern: '{{PKG_VERSION}}'
|
|
||||||
replacement: '<%= pkg.version %>'
|
|
||||||
]
|
|
||||||
|
|
||||||
jshint:
|
|
||||||
options:
|
|
||||||
jshintrc: true
|
|
||||||
build:
|
|
||||||
src: ['hammer.js']
|
|
||||||
|
|
||||||
jscs:
|
|
||||||
src: [
|
|
||||||
'src/**/*.js',
|
|
||||||
'!src/hammer.prefix.js',
|
|
||||||
'!src/hammer.suffix.js'
|
|
||||||
]
|
|
||||||
options:
|
|
||||||
config: "./.jscsrc"
|
|
||||||
force: true
|
|
||||||
|
|
||||||
watch:
|
|
||||||
scripts:
|
|
||||||
files: ['src/**/*.js']
|
|
||||||
tasks: ['concat','string-replace','uglify','jshint','jscs']
|
|
||||||
options:
|
|
||||||
interrupt: true
|
|
||||||
|
|
||||||
connect:
|
|
||||||
server:
|
|
||||||
options:
|
|
||||||
hostname: "0.0.0.0"
|
|
||||||
port: 8000
|
|
||||||
|
|
||||||
qunit:
|
|
||||||
all: ['tests/unit/index.html']
|
|
||||||
|
|
||||||
|
|
||||||
# Load tasks
|
|
||||||
grunt.loadNpmTasks 'grunt-contrib-concat'
|
|
||||||
grunt.loadNpmTasks 'grunt-contrib-uglify'
|
|
||||||
grunt.loadNpmTasks 'grunt-contrib-qunit'
|
|
||||||
grunt.loadNpmTasks 'grunt-contrib-watch'
|
|
||||||
grunt.loadNpmTasks 'grunt-contrib-jshint'
|
|
||||||
grunt.loadNpmTasks 'grunt-contrib-connect'
|
|
||||||
grunt.loadNpmTasks 'grunt-string-replace'
|
|
||||||
grunt.loadNpmTasks 'grunt-banner'
|
|
||||||
grunt.loadNpmTasks 'grunt-jscs'
|
|
||||||
|
|
||||||
# Default task(s)
|
|
||||||
grunt.registerTask 'default', ['connect', 'watch']
|
|
||||||
grunt.registerTask 'default-test', ['connect', 'uglify:test', 'watch']
|
|
||||||
grunt.registerTask 'build', ['concat', 'string-replace', 'uglify:min', 'usebanner', 'test']
|
|
||||||
grunt.registerTask 'test', ['jshint', 'jscs', 'uglify:test', 'qunit']
|
|
||||||
grunt.registerTask 'test-travis', ['build']
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (C) 2011-2014 by Jorik Tangelder (Eight Media)
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
# Hammer.js 2.0.6
|
|
||||||
|
|
||||||
[](https://travis-ci.org/hammerjs/hammer.js)
|
|
||||||
|
|
||||||
## Support, Questions, and Collaboration
|
|
||||||
|
|
||||||
[](https://hammerjs.herokuapp.com/)
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
Visit [hammerjs.github.io](http://hammerjs.github.io) for detailed documentation.
|
|
||||||
|
|
||||||
```js
|
|
||||||
// get a reference to an element
|
|
||||||
var stage = document.getElementById('stage');
|
|
||||||
|
|
||||||
// create a manager for that element
|
|
||||||
var mc = new Hammer.Manager(stage);
|
|
||||||
|
|
||||||
// create a recognizer
|
|
||||||
var Rotate = new Hammer.Rotate();
|
|
||||||
|
|
||||||
// add the recognizer
|
|
||||||
mc.add(Rotate);
|
|
||||||
|
|
||||||
// subscribe to events
|
|
||||||
mc.on('rotate', function(e) {
|
|
||||||
// do something cool
|
|
||||||
var rotation = Math.round(e.rotation);
|
|
||||||
stage.style.transform = 'rotate('+rotation+'deg)';
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
An advanced demo is available here: [http://codepen.io/runspired/full/ZQBGWd/](http://codepen.io/runspired/full/ZQBGWd/)
|
|
||||||
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Read the [contributing guidelines](./CONTRIBUTING.md).
|
|
||||||
|
|
||||||
For PRs.
|
|
||||||
|
|
||||||
- Use [Angular Style commit messages](https://github.com/angular/angular.js/blob/v1.4.8/CONTRIBUTING.md#commit)
|
|
||||||
- Rebase your PR branch when necessary
|
|
||||||
- If you add a feature or fix a bug, please add or fix any necessary tests.
|
|
||||||
- If a new feature, open a docs PR to go with.
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
You can get the pre-build versions from the Hammer.js website, or do this by yourself running
|
|
||||||
`npm install -g grunt-cli && npm install && grunt build`
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"name": "hammerjs",
|
|
||||||
"main": "hammer.js",
|
|
||||||
"ignore": [
|
|
||||||
"tests",
|
|
||||||
"src",
|
|
||||||
".bowerrc",
|
|
||||||
".gitignore",
|
|
||||||
".jscsrc",
|
|
||||||
".jshintrc",
|
|
||||||
".travis.yml",
|
|
||||||
"component.json",
|
|
||||||
"Gruntfile.coffee",
|
|
||||||
"package.json"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
var changelog = require( "changelogplease" );
|
|
||||||
var gittags = require( "git-tags" ).get( function( error, tags ) {
|
|
||||||
if ( error ) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
console.log( tags[ 1 ] + ".." + tags[ 0 ] );
|
|
||||||
var exclude = [ "Merge", "Whitespace", "Fixup", "Cleanup", "Formatting", "Ignore" ];
|
|
||||||
changelog( {
|
|
||||||
ticketUrl: "https://github.com/hammerjs/hammer.js/issues/{id}",
|
|
||||||
commitUrl: "https://github.com/hammerjs/hammerjs/commit/{id}",
|
|
||||||
sort: false,
|
|
||||||
repo: "./",
|
|
||||||
committish: tags[ 1 ] + ".." + tags[ 0 ]
|
|
||||||
}, function( error, log ) {
|
|
||||||
if ( error ) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
log = parseLog( log );
|
|
||||||
console.log( log );
|
|
||||||
} );
|
|
||||||
function parseLog( log ) {
|
|
||||||
var lines = log.split( "\n" );
|
|
||||||
var newLog = [];
|
|
||||||
var log = [];
|
|
||||||
var currentComponent;
|
|
||||||
|
|
||||||
|
|
||||||
lines.shift();
|
|
||||||
lines.forEach( function( line ) {
|
|
||||||
var newLine = parseLine( line );
|
|
||||||
if ( newLine ) {
|
|
||||||
log.push( line );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
var log = log.join( "\n" );
|
|
||||||
return log.replace( /\*/g, "-" ).replace( /__TICKETREF__,/g, "" );
|
|
||||||
}
|
|
||||||
function parseLine( line ) {
|
|
||||||
var parts = getParts( line );
|
|
||||||
|
|
||||||
if ( exclude.indexOf( parts.component ) > -1 ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return parts;
|
|
||||||
}
|
|
||||||
function getParts( line ) {
|
|
||||||
var parts = line.split( ":" );
|
|
||||||
var component = "";
|
|
||||||
var message;
|
|
||||||
var commits = line.match( /\{\{([A-Za-z0-9 ]){0,99}\}\}/ )
|
|
||||||
|
|
||||||
if ( parts.length > 1 && parts[ 0 ].length <= 20 ) {
|
|
||||||
component = parts[ 0 ];
|
|
||||||
parts.shift();
|
|
||||||
message = parts.join( ":" );
|
|
||||||
} else {
|
|
||||||
parts = line.split( " " );
|
|
||||||
component = parts[ 1 ];
|
|
||||||
parts.shift();
|
|
||||||
message = parts.join( " " );
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( component ) {
|
|
||||||
component = component.replace( /\* |,/, "" );
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
component: component,
|
|
||||||
message: message
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"name": "hammerjs",
|
|
||||||
"version": "2.0.6",
|
|
||||||
"main": "hammer.js",
|
|
||||||
"scripts": [
|
|
||||||
"hammer.js"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,57 +0,0 @@
|
||||||
{
|
|
||||||
"name": "hammerjs",
|
|
||||||
"title": "Hammer.JS",
|
|
||||||
"description": "A javascript library for multi-touch gestures",
|
|
||||||
"version": "2.0.8",
|
|
||||||
"homepage": "http://hammerjs.github.io/",
|
|
||||||
"license": "MIT",
|
|
||||||
"keywords": [
|
|
||||||
"touch",
|
|
||||||
"gestures"
|
|
||||||
],
|
|
||||||
"author": {
|
|
||||||
"name": "Jorik Tangelder",
|
|
||||||
"email": "j.tangelder@gmail.com"
|
|
||||||
},
|
|
||||||
"contributors": [
|
|
||||||
{
|
|
||||||
"name": "Alexander Schmitz",
|
|
||||||
"email": "arschmitz@gmail.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Chris Thoburn",
|
|
||||||
"email": ""
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git://github.com/hammerjs/hammer.js.git"
|
|
||||||
},
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/hammerjs/hammer.js/issues"
|
|
||||||
},
|
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
|
||||||
"changelogplease": "^1.2.0",
|
|
||||||
"git-tags": "^0.2.4",
|
|
||||||
"grunt": "0.4.x",
|
|
||||||
"grunt-banner": "^0.2.3",
|
|
||||||
"grunt-contrib-concat": "0.4.x",
|
|
||||||
"grunt-contrib-connect": "0.7.x",
|
|
||||||
"grunt-contrib-jshint": "0.10.x",
|
|
||||||
"grunt-contrib-qunit": "^0.5.1",
|
|
||||||
"grunt-contrib-uglify": "0.7.x",
|
|
||||||
"grunt-contrib-watch": "0.6.x",
|
|
||||||
"grunt-jscs": "^0.8.0",
|
|
||||||
"grunt-string-replace": "^0.2.7",
|
|
||||||
"hammer-simulator": "git://github.com/hammerjs/simulator#master",
|
|
||||||
"jquery-hammerjs": "2.0.x"
|
|
||||||
},
|
|
||||||
"main": "hammer.js",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.8.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"test": "grunt test"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
assign(Hammer, {
|
|
||||||
INPUT_START: INPUT_START,
|
|
||||||
INPUT_MOVE: INPUT_MOVE,
|
|
||||||
INPUT_END: INPUT_END,
|
|
||||||
INPUT_CANCEL: INPUT_CANCEL,
|
|
||||||
|
|
||||||
STATE_POSSIBLE: STATE_POSSIBLE,
|
|
||||||
STATE_BEGAN: STATE_BEGAN,
|
|
||||||
STATE_CHANGED: STATE_CHANGED,
|
|
||||||
STATE_ENDED: STATE_ENDED,
|
|
||||||
STATE_RECOGNIZED: STATE_RECOGNIZED,
|
|
||||||
STATE_CANCELLED: STATE_CANCELLED,
|
|
||||||
STATE_FAILED: STATE_FAILED,
|
|
||||||
|
|
||||||
DIRECTION_NONE: DIRECTION_NONE,
|
|
||||||
DIRECTION_LEFT: DIRECTION_LEFT,
|
|
||||||
DIRECTION_RIGHT: DIRECTION_RIGHT,
|
|
||||||
DIRECTION_UP: DIRECTION_UP,
|
|
||||||
DIRECTION_DOWN: DIRECTION_DOWN,
|
|
||||||
DIRECTION_HORIZONTAL: DIRECTION_HORIZONTAL,
|
|
||||||
DIRECTION_VERTICAL: DIRECTION_VERTICAL,
|
|
||||||
DIRECTION_ALL: DIRECTION_ALL,
|
|
||||||
|
|
||||||
Manager: Manager,
|
|
||||||
Input: Input,
|
|
||||||
TouchAction: TouchAction,
|
|
||||||
|
|
||||||
TouchInput: TouchInput,
|
|
||||||
MouseInput: MouseInput,
|
|
||||||
PointerEventInput: PointerEventInput,
|
|
||||||
TouchMouseInput: TouchMouseInput,
|
|
||||||
SingleTouchInput: SingleTouchInput,
|
|
||||||
|
|
||||||
Recognizer: Recognizer,
|
|
||||||
AttrRecognizer: AttrRecognizer,
|
|
||||||
Tap: TapRecognizer,
|
|
||||||
Pan: PanRecognizer,
|
|
||||||
Swipe: SwipeRecognizer,
|
|
||||||
Pinch: PinchRecognizer,
|
|
||||||
Rotate: RotateRecognizer,
|
|
||||||
Press: PressRecognizer,
|
|
||||||
|
|
||||||
on: addEventListeners,
|
|
||||||
off: removeEventListeners,
|
|
||||||
each: each,
|
|
||||||
merge: merge,
|
|
||||||
extend: extend,
|
|
||||||
assign: assign,
|
|
||||||
inherit: inherit,
|
|
||||||
bindFn: bindFn,
|
|
||||||
prefixed: prefixed
|
|
||||||
});
|
|
||||||
|
|
||||||
// this prevents errors when Hammer is loaded in the presence of an AMD
|
|
||||||
// style loader but by script tag, not by the loader.
|
|
||||||
var freeGlobal = (typeof window !== 'undefined' ? window : (typeof self !== 'undefined' ? self : {})); // jshint ignore:line
|
|
||||||
freeGlobal.Hammer = Hammer;
|
|
||||||
|
|
||||||
if (typeof define === 'function' && define.amd) {
|
|
||||||
define(function() {
|
|
||||||
return Hammer;
|
|
||||||
});
|
|
||||||
} else if (typeof module != 'undefined' && module.exports) {
|
|
||||||
module.exports = Hammer;
|
|
||||||
} else {
|
|
||||||
window[exportName] = Hammer;
|
|
||||||
}
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
/**
|
|
||||||
* Simple way to create a manager with a default set of recognizers.
|
|
||||||
* @param {HTMLElement} element
|
|
||||||
* @param {Object} [options]
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
function Hammer(element, options) {
|
|
||||||
options = options || {};
|
|
||||||
options.recognizers = ifUndefined(options.recognizers, Hammer.defaults.preset);
|
|
||||||
return new Manager(element, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @const {string}
|
|
||||||
*/
|
|
||||||
Hammer.VERSION = '{{PKG_VERSION}}';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* default settings
|
|
||||||
* @namespace
|
|
||||||
*/
|
|
||||||
Hammer.defaults = {
|
|
||||||
/**
|
|
||||||
* set if DOM events are being triggered.
|
|
||||||
* But this is slower and unused by simple implementations, so disabled by default.
|
|
||||||
* @type {Boolean}
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
domEvents: false,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The value for the touchAction property/fallback.
|
|
||||||
* When set to `compute` it will magically set the correct value based on the added recognizers.
|
|
||||||
* @type {String}
|
|
||||||
* @default compute
|
|
||||||
*/
|
|
||||||
touchAction: TOUCH_ACTION_COMPUTE,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {Boolean}
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
enable: true,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* EXPERIMENTAL FEATURE -- can be removed/changed
|
|
||||||
* Change the parent input target element.
|
|
||||||
* If Null, then it is being set the to main element.
|
|
||||||
* @type {Null|EventTarget}
|
|
||||||
* @default null
|
|
||||||
*/
|
|
||||||
inputTarget: null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* force an input class
|
|
||||||
* @type {Null|Function}
|
|
||||||
* @default null
|
|
||||||
*/
|
|
||||||
inputClass: null,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default recognizer setup when calling `Hammer()`
|
|
||||||
* When creating a new Manager these will be skipped.
|
|
||||||
* @type {Array}
|
|
||||||
*/
|
|
||||||
preset: [
|
|
||||||
// RecognizerClass, options, [recognizeWith, ...], [requireFailure, ...]
|
|
||||||
[RotateRecognizer, {enable: false}],
|
|
||||||
[PinchRecognizer, {enable: false}, ['rotate']],
|
|
||||||
[SwipeRecognizer, {direction: DIRECTION_HORIZONTAL}],
|
|
||||||
[PanRecognizer, {direction: DIRECTION_HORIZONTAL}, ['swipe']],
|
|
||||||
[TapRecognizer],
|
|
||||||
[TapRecognizer, {event: 'doubletap', taps: 2}, ['tap']],
|
|
||||||
[PressRecognizer]
|
|
||||||
],
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Some CSS properties can be used to improve the working of Hammer.
|
|
||||||
* Add them to this method and they will be set when creating a new Manager.
|
|
||||||
* @namespace
|
|
||||||
*/
|
|
||||||
cssProps: {
|
|
||||||
/**
|
|
||||||
* Disables text selection to improve the dragging gesture. Mainly for desktop browsers.
|
|
||||||
* @type {String}
|
|
||||||
* @default 'none'
|
|
||||||
*/
|
|
||||||
userSelect: 'none',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disable the Windows Phone grippers when pressing an element.
|
|
||||||
* @type {String}
|
|
||||||
* @default 'none'
|
|
||||||
*/
|
|
||||||
touchSelect: 'none',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disables the default callout shown when you touch and hold a touch target.
|
|
||||||
* On iOS, when you touch and hold a touch target such as a link, Safari displays
|
|
||||||
* a callout containing information about the link. This property allows you to disable that callout.
|
|
||||||
* @type {String}
|
|
||||||
* @default 'none'
|
|
||||||
*/
|
|
||||||
touchCallout: 'none',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specifies whether zooming is enabled. Used by IE10>
|
|
||||||
* @type {String}
|
|
||||||
* @default 'none'
|
|
||||||
*/
|
|
||||||
contentZooming: 'none',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers.
|
|
||||||
* @type {String}
|
|
||||||
* @default 'none'
|
|
||||||
*/
|
|
||||||
userDrag: 'none',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Overrides the highlight color shown when the user taps a link or a JavaScript
|
|
||||||
* clickable element in iOS. This property obeys the alpha value, if specified.
|
|
||||||
* @type {String}
|
|
||||||
* @default 'rgba(0,0,0,0)'
|
|
||||||
*/
|
|
||||||
tapHighlightColor: 'rgba(0,0,0,0)'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
(function(window, document, exportName, undefined) {
|
|
||||||
'use strict';
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
})(window, document, 'Hammer');
|
|
||||||
|
|
@ -1,394 +0,0 @@
|
||||||
var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
|
|
||||||
|
|
||||||
var SUPPORT_TOUCH = ('ontouchstart' in window);
|
|
||||||
var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined;
|
|
||||||
var SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent);
|
|
||||||
|
|
||||||
var INPUT_TYPE_TOUCH = 'touch';
|
|
||||||
var INPUT_TYPE_PEN = 'pen';
|
|
||||||
var INPUT_TYPE_MOUSE = 'mouse';
|
|
||||||
var INPUT_TYPE_KINECT = 'kinect';
|
|
||||||
|
|
||||||
var COMPUTE_INTERVAL = 25;
|
|
||||||
|
|
||||||
var INPUT_START = 1;
|
|
||||||
var INPUT_MOVE = 2;
|
|
||||||
var INPUT_END = 4;
|
|
||||||
var INPUT_CANCEL = 8;
|
|
||||||
|
|
||||||
var DIRECTION_NONE = 1;
|
|
||||||
var DIRECTION_LEFT = 2;
|
|
||||||
var DIRECTION_RIGHT = 4;
|
|
||||||
var DIRECTION_UP = 8;
|
|
||||||
var DIRECTION_DOWN = 16;
|
|
||||||
|
|
||||||
var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT;
|
|
||||||
var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN;
|
|
||||||
var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL;
|
|
||||||
|
|
||||||
var PROPS_XY = ['x', 'y'];
|
|
||||||
var PROPS_CLIENT_XY = ['clientX', 'clientY'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* create new input type manager
|
|
||||||
* @param {Manager} manager
|
|
||||||
* @param {Function} callback
|
|
||||||
* @returns {Input}
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
function Input(manager, callback) {
|
|
||||||
var self = this;
|
|
||||||
this.manager = manager;
|
|
||||||
this.callback = callback;
|
|
||||||
this.element = manager.element;
|
|
||||||
this.target = manager.options.inputTarget;
|
|
||||||
|
|
||||||
// smaller wrapper around the handler, for the scope and the enabled state of the manager,
|
|
||||||
// so when disabled the input events are completely bypassed.
|
|
||||||
this.domHandler = function(ev) {
|
|
||||||
if (boolOrFn(manager.options.enable, [manager])) {
|
|
||||||
self.handler(ev);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.init();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Input.prototype = {
|
|
||||||
/**
|
|
||||||
* should handle the inputEvent data and trigger the callback
|
|
||||||
* @virtual
|
|
||||||
*/
|
|
||||||
handler: function() { },
|
|
||||||
|
|
||||||
/**
|
|
||||||
* bind the events
|
|
||||||
*/
|
|
||||||
init: function() {
|
|
||||||
this.evEl && addEventListeners(this.element, this.evEl, this.domHandler);
|
|
||||||
this.evTarget && addEventListeners(this.target, this.evTarget, this.domHandler);
|
|
||||||
this.evWin && addEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* unbind the events
|
|
||||||
*/
|
|
||||||
destroy: function() {
|
|
||||||
this.evEl && removeEventListeners(this.element, this.evEl, this.domHandler);
|
|
||||||
this.evTarget && removeEventListeners(this.target, this.evTarget, this.domHandler);
|
|
||||||
this.evWin && removeEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* create new input type manager
|
|
||||||
* called by the Manager constructor
|
|
||||||
* @param {Hammer} manager
|
|
||||||
* @returns {Input}
|
|
||||||
*/
|
|
||||||
function createInputInstance(manager) {
|
|
||||||
var Type;
|
|
||||||
var inputClass = manager.options.inputClass;
|
|
||||||
|
|
||||||
if (inputClass) {
|
|
||||||
Type = inputClass;
|
|
||||||
} else if (SUPPORT_POINTER_EVENTS) {
|
|
||||||
Type = PointerEventInput;
|
|
||||||
} else if (SUPPORT_ONLY_TOUCH) {
|
|
||||||
Type = TouchInput;
|
|
||||||
} else if (!SUPPORT_TOUCH) {
|
|
||||||
Type = MouseInput;
|
|
||||||
} else {
|
|
||||||
Type = TouchMouseInput;
|
|
||||||
}
|
|
||||||
return new (Type)(manager, inputHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* handle input events
|
|
||||||
* @param {Manager} manager
|
|
||||||
* @param {String} eventType
|
|
||||||
* @param {Object} input
|
|
||||||
*/
|
|
||||||
function inputHandler(manager, eventType, input) {
|
|
||||||
var pointersLen = input.pointers.length;
|
|
||||||
var changedPointersLen = input.changedPointers.length;
|
|
||||||
var isFirst = (eventType & INPUT_START && (pointersLen - changedPointersLen === 0));
|
|
||||||
var isFinal = (eventType & (INPUT_END | INPUT_CANCEL) && (pointersLen - changedPointersLen === 0));
|
|
||||||
|
|
||||||
input.isFirst = !!isFirst;
|
|
||||||
input.isFinal = !!isFinal;
|
|
||||||
|
|
||||||
if (isFirst) {
|
|
||||||
manager.session = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// source event is the normalized value of the domEvents
|
|
||||||
// like 'touchstart, mouseup, pointerdown'
|
|
||||||
input.eventType = eventType;
|
|
||||||
|
|
||||||
// compute scale, rotation etc
|
|
||||||
computeInputData(manager, input);
|
|
||||||
|
|
||||||
// emit secret event
|
|
||||||
manager.emit('hammer.input', input);
|
|
||||||
|
|
||||||
manager.recognize(input);
|
|
||||||
manager.session.prevInput = input;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* extend the data with some usable properties like scale, rotate, velocity etc
|
|
||||||
* @param {Object} manager
|
|
||||||
* @param {Object} input
|
|
||||||
*/
|
|
||||||
function computeInputData(manager, input) {
|
|
||||||
var session = manager.session;
|
|
||||||
var pointers = input.pointers;
|
|
||||||
var pointersLength = pointers.length;
|
|
||||||
|
|
||||||
// store the first input to calculate the distance and direction
|
|
||||||
if (!session.firstInput) {
|
|
||||||
session.firstInput = simpleCloneInputData(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
// to compute scale and rotation we need to store the multiple touches
|
|
||||||
if (pointersLength > 1 && !session.firstMultiple) {
|
|
||||||
session.firstMultiple = simpleCloneInputData(input);
|
|
||||||
} else if (pointersLength === 1) {
|
|
||||||
session.firstMultiple = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var firstInput = session.firstInput;
|
|
||||||
var firstMultiple = session.firstMultiple;
|
|
||||||
var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center;
|
|
||||||
|
|
||||||
var center = input.center = getCenter(pointers);
|
|
||||||
input.timeStamp = now();
|
|
||||||
input.deltaTime = input.timeStamp - firstInput.timeStamp;
|
|
||||||
|
|
||||||
input.angle = getAngle(offsetCenter, center);
|
|
||||||
input.distance = getDistance(offsetCenter, center);
|
|
||||||
|
|
||||||
computeDeltaXY(session, input);
|
|
||||||
input.offsetDirection = getDirection(input.deltaX, input.deltaY);
|
|
||||||
|
|
||||||
var overallVelocity = getVelocity(input.deltaTime, input.deltaX, input.deltaY);
|
|
||||||
input.overallVelocityX = overallVelocity.x;
|
|
||||||
input.overallVelocityY = overallVelocity.y;
|
|
||||||
input.overallVelocity = (abs(overallVelocity.x) > abs(overallVelocity.y)) ? overallVelocity.x : overallVelocity.y;
|
|
||||||
|
|
||||||
input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1;
|
|
||||||
input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0;
|
|
||||||
|
|
||||||
input.maxPointers = !session.prevInput ? input.pointers.length : ((input.pointers.length >
|
|
||||||
session.prevInput.maxPointers) ? input.pointers.length : session.prevInput.maxPointers);
|
|
||||||
|
|
||||||
computeIntervalInputData(session, input);
|
|
||||||
|
|
||||||
// find the correct target
|
|
||||||
var target = manager.element;
|
|
||||||
if (hasParent(input.srcEvent.target, target)) {
|
|
||||||
target = input.srcEvent.target;
|
|
||||||
}
|
|
||||||
input.target = target;
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeDeltaXY(session, input) {
|
|
||||||
var center = input.center;
|
|
||||||
var offset = session.offsetDelta || {};
|
|
||||||
var prevDelta = session.prevDelta || {};
|
|
||||||
var prevInput = session.prevInput || {};
|
|
||||||
|
|
||||||
if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) {
|
|
||||||
prevDelta = session.prevDelta = {
|
|
||||||
x: prevInput.deltaX || 0,
|
|
||||||
y: prevInput.deltaY || 0
|
|
||||||
};
|
|
||||||
|
|
||||||
offset = session.offsetDelta = {
|
|
||||||
x: center.x,
|
|
||||||
y: center.y
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
input.deltaX = prevDelta.x + (center.x - offset.x);
|
|
||||||
input.deltaY = prevDelta.y + (center.y - offset.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* velocity is calculated every x ms
|
|
||||||
* @param {Object} session
|
|
||||||
* @param {Object} input
|
|
||||||
*/
|
|
||||||
function computeIntervalInputData(session, input) {
|
|
||||||
var last = session.lastInterval || input,
|
|
||||||
deltaTime = input.timeStamp - last.timeStamp,
|
|
||||||
velocity, velocityX, velocityY, direction;
|
|
||||||
|
|
||||||
if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined)) {
|
|
||||||
var deltaX = input.deltaX - last.deltaX;
|
|
||||||
var deltaY = input.deltaY - last.deltaY;
|
|
||||||
|
|
||||||
var v = getVelocity(deltaTime, deltaX, deltaY);
|
|
||||||
velocityX = v.x;
|
|
||||||
velocityY = v.y;
|
|
||||||
velocity = (abs(v.x) > abs(v.y)) ? v.x : v.y;
|
|
||||||
direction = getDirection(deltaX, deltaY);
|
|
||||||
|
|
||||||
session.lastInterval = input;
|
|
||||||
} else {
|
|
||||||
// use latest velocity info if it doesn't overtake a minimum period
|
|
||||||
velocity = last.velocity;
|
|
||||||
velocityX = last.velocityX;
|
|
||||||
velocityY = last.velocityY;
|
|
||||||
direction = last.direction;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.velocity = velocity;
|
|
||||||
input.velocityX = velocityX;
|
|
||||||
input.velocityY = velocityY;
|
|
||||||
input.direction = direction;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* create a simple clone from the input used for storage of firstInput and firstMultiple
|
|
||||||
* @param {Object} input
|
|
||||||
* @returns {Object} clonedInputData
|
|
||||||
*/
|
|
||||||
function simpleCloneInputData(input) {
|
|
||||||
// make a simple copy of the pointers because we will get a reference if we don't
|
|
||||||
// we only need clientXY for the calculations
|
|
||||||
var pointers = [];
|
|
||||||
var i = 0;
|
|
||||||
while (i < input.pointers.length) {
|
|
||||||
pointers[i] = {
|
|
||||||
clientX: round(input.pointers[i].clientX),
|
|
||||||
clientY: round(input.pointers[i].clientY)
|
|
||||||
};
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
timeStamp: now(),
|
|
||||||
pointers: pointers,
|
|
||||||
center: getCenter(pointers),
|
|
||||||
deltaX: input.deltaX,
|
|
||||||
deltaY: input.deltaY
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get the center of all the pointers
|
|
||||||
* @param {Array} pointers
|
|
||||||
* @return {Object} center contains `x` and `y` properties
|
|
||||||
*/
|
|
||||||
function getCenter(pointers) {
|
|
||||||
var pointersLength = pointers.length;
|
|
||||||
|
|
||||||
// no need to loop when only one touch
|
|
||||||
if (pointersLength === 1) {
|
|
||||||
return {
|
|
||||||
x: round(pointers[0].clientX),
|
|
||||||
y: round(pointers[0].clientY)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var x = 0, y = 0, i = 0;
|
|
||||||
while (i < pointersLength) {
|
|
||||||
x += pointers[i].clientX;
|
|
||||||
y += pointers[i].clientY;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: round(x / pointersLength),
|
|
||||||
y: round(y / pointersLength)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* calculate the velocity between two points. unit is in px per ms.
|
|
||||||
* @param {Number} deltaTime
|
|
||||||
* @param {Number} x
|
|
||||||
* @param {Number} y
|
|
||||||
* @return {Object} velocity `x` and `y`
|
|
||||||
*/
|
|
||||||
function getVelocity(deltaTime, x, y) {
|
|
||||||
return {
|
|
||||||
x: x / deltaTime || 0,
|
|
||||||
y: y / deltaTime || 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get the direction between two points
|
|
||||||
* @param {Number} x
|
|
||||||
* @param {Number} y
|
|
||||||
* @return {Number} direction
|
|
||||||
*/
|
|
||||||
function getDirection(x, y) {
|
|
||||||
if (x === y) {
|
|
||||||
return DIRECTION_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (abs(x) >= abs(y)) {
|
|
||||||
return x < 0 ? DIRECTION_LEFT : DIRECTION_RIGHT;
|
|
||||||
}
|
|
||||||
return y < 0 ? DIRECTION_UP : DIRECTION_DOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* calculate the absolute distance between two points
|
|
||||||
* @param {Object} p1 {x, y}
|
|
||||||
* @param {Object} p2 {x, y}
|
|
||||||
* @param {Array} [props] containing x and y keys
|
|
||||||
* @return {Number} distance
|
|
||||||
*/
|
|
||||||
function getDistance(p1, p2, props) {
|
|
||||||
if (!props) {
|
|
||||||
props = PROPS_XY;
|
|
||||||
}
|
|
||||||
var x = p2[props[0]] - p1[props[0]],
|
|
||||||
y = p2[props[1]] - p1[props[1]];
|
|
||||||
|
|
||||||
return Math.sqrt((x * x) + (y * y));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* calculate the angle between two coordinates
|
|
||||||
* @param {Object} p1
|
|
||||||
* @param {Object} p2
|
|
||||||
* @param {Array} [props] containing x and y keys
|
|
||||||
* @return {Number} angle
|
|
||||||
*/
|
|
||||||
function getAngle(p1, p2, props) {
|
|
||||||
if (!props) {
|
|
||||||
props = PROPS_XY;
|
|
||||||
}
|
|
||||||
var x = p2[props[0]] - p1[props[0]],
|
|
||||||
y = p2[props[1]] - p1[props[1]];
|
|
||||||
return Math.atan2(y, x) * 180 / Math.PI;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* calculate the rotation degrees between two pointersets
|
|
||||||
* @param {Array} start array of pointers
|
|
||||||
* @param {Array} end array of pointers
|
|
||||||
* @return {Number} rotation
|
|
||||||
*/
|
|
||||||
function getRotation(start, end) {
|
|
||||||
return getAngle(end[1], end[0], PROPS_CLIENT_XY) + getAngle(start[1], start[0], PROPS_CLIENT_XY);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* calculate the scale factor between two pointersets
|
|
||||||
* no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
|
|
||||||
* @param {Array} start array of pointers
|
|
||||||
* @param {Array} end array of pointers
|
|
||||||
* @return {Number} scale
|
|
||||||
*/
|
|
||||||
function getScale(start, end) {
|
|
||||||
return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY);
|
|
||||||
}
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
var MOUSE_INPUT_MAP = {
|
|
||||||
mousedown: INPUT_START,
|
|
||||||
mousemove: INPUT_MOVE,
|
|
||||||
mouseup: INPUT_END
|
|
||||||
};
|
|
||||||
|
|
||||||
var MOUSE_ELEMENT_EVENTS = 'mousedown';
|
|
||||||
var MOUSE_WINDOW_EVENTS = 'mousemove mouseup';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mouse events input
|
|
||||||
* @constructor
|
|
||||||
* @extends Input
|
|
||||||
*/
|
|
||||||
function MouseInput() {
|
|
||||||
this.evEl = MOUSE_ELEMENT_EVENTS;
|
|
||||||
this.evWin = MOUSE_WINDOW_EVENTS;
|
|
||||||
|
|
||||||
this.pressed = false; // mousedown state
|
|
||||||
|
|
||||||
Input.apply(this, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
inherit(MouseInput, Input, {
|
|
||||||
/**
|
|
||||||
* handle mouse events
|
|
||||||
* @param {Object} ev
|
|
||||||
*/
|
|
||||||
handler: function MEhandler(ev) {
|
|
||||||
var eventType = MOUSE_INPUT_MAP[ev.type];
|
|
||||||
|
|
||||||
// on start we want to have the left mouse button down
|
|
||||||
if (eventType & INPUT_START && ev.button === 0) {
|
|
||||||
this.pressed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType & INPUT_MOVE && ev.which !== 1) {
|
|
||||||
eventType = INPUT_END;
|
|
||||||
}
|
|
||||||
|
|
||||||
// mouse must be down
|
|
||||||
if (!this.pressed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType & INPUT_END) {
|
|
||||||
this.pressed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.callback(this.manager, eventType, {
|
|
||||||
pointers: [ev],
|
|
||||||
changedPointers: [ev],
|
|
||||||
pointerType: INPUT_TYPE_MOUSE,
|
|
||||||
srcEvent: ev
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
var POINTER_INPUT_MAP = {
|
|
||||||
pointerdown: INPUT_START,
|
|
||||||
pointermove: INPUT_MOVE,
|
|
||||||
pointerup: INPUT_END,
|
|
||||||
pointercancel: INPUT_CANCEL,
|
|
||||||
pointerout: INPUT_CANCEL
|
|
||||||
};
|
|
||||||
|
|
||||||
// in IE10 the pointer types is defined as an enum
|
|
||||||
var IE10_POINTER_TYPE_ENUM = {
|
|
||||||
2: INPUT_TYPE_TOUCH,
|
|
||||||
3: INPUT_TYPE_PEN,
|
|
||||||
4: INPUT_TYPE_MOUSE,
|
|
||||||
5: INPUT_TYPE_KINECT // see https://twitter.com/jacobrossi/status/480596438489890816
|
|
||||||
};
|
|
||||||
|
|
||||||
var POINTER_ELEMENT_EVENTS = 'pointerdown';
|
|
||||||
var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel';
|
|
||||||
|
|
||||||
// IE10 has prefixed support, and case-sensitive
|
|
||||||
if (window.MSPointerEvent && !window.PointerEvent) {
|
|
||||||
POINTER_ELEMENT_EVENTS = 'MSPointerDown';
|
|
||||||
POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pointer events input
|
|
||||||
* @constructor
|
|
||||||
* @extends Input
|
|
||||||
*/
|
|
||||||
function PointerEventInput() {
|
|
||||||
this.evEl = POINTER_ELEMENT_EVENTS;
|
|
||||||
this.evWin = POINTER_WINDOW_EVENTS;
|
|
||||||
|
|
||||||
Input.apply(this, arguments);
|
|
||||||
|
|
||||||
this.store = (this.manager.session.pointerEvents = []);
|
|
||||||
}
|
|
||||||
|
|
||||||
inherit(PointerEventInput, Input, {
|
|
||||||
/**
|
|
||||||
* handle mouse events
|
|
||||||
* @param {Object} ev
|
|
||||||
*/
|
|
||||||
handler: function PEhandler(ev) {
|
|
||||||
var store = this.store;
|
|
||||||
var removePointer = false;
|
|
||||||
|
|
||||||
var eventTypeNormalized = ev.type.toLowerCase().replace('ms', '');
|
|
||||||
var eventType = POINTER_INPUT_MAP[eventTypeNormalized];
|
|
||||||
var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType;
|
|
||||||
|
|
||||||
var isTouch = (pointerType == INPUT_TYPE_TOUCH);
|
|
||||||
|
|
||||||
// get index of the event in the store
|
|
||||||
var storeIndex = inArray(store, ev.pointerId, 'pointerId');
|
|
||||||
|
|
||||||
// start and mouse must be down
|
|
||||||
if (eventType & INPUT_START && (ev.button === 0 || isTouch)) {
|
|
||||||
if (storeIndex < 0) {
|
|
||||||
store.push(ev);
|
|
||||||
storeIndex = store.length - 1;
|
|
||||||
}
|
|
||||||
} else if (eventType & (INPUT_END | INPUT_CANCEL)) {
|
|
||||||
removePointer = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// it not found, so the pointer hasn't been down (so it's probably a hover)
|
|
||||||
if (storeIndex < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update the event in the store
|
|
||||||
store[storeIndex] = ev;
|
|
||||||
|
|
||||||
this.callback(this.manager, eventType, {
|
|
||||||
pointers: store,
|
|
||||||
changedPointers: [ev],
|
|
||||||
pointerType: pointerType,
|
|
||||||
srcEvent: ev
|
|
||||||
});
|
|
||||||
|
|
||||||
if (removePointer) {
|
|
||||||
// remove from the store
|
|
||||||
store.splice(storeIndex, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
var SINGLE_TOUCH_INPUT_MAP = {
|
|
||||||
touchstart: INPUT_START,
|
|
||||||
touchmove: INPUT_MOVE,
|
|
||||||
touchend: INPUT_END,
|
|
||||||
touchcancel: INPUT_CANCEL
|
|
||||||
};
|
|
||||||
|
|
||||||
var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart';
|
|
||||||
var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Touch events input
|
|
||||||
* @constructor
|
|
||||||
* @extends Input
|
|
||||||
*/
|
|
||||||
function SingleTouchInput() {
|
|
||||||
this.evTarget = SINGLE_TOUCH_TARGET_EVENTS;
|
|
||||||
this.evWin = SINGLE_TOUCH_WINDOW_EVENTS;
|
|
||||||
this.started = false;
|
|
||||||
|
|
||||||
Input.apply(this, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
inherit(SingleTouchInput, Input, {
|
|
||||||
handler: function TEhandler(ev) {
|
|
||||||
var type = SINGLE_TOUCH_INPUT_MAP[ev.type];
|
|
||||||
|
|
||||||
// should we handle the touch events?
|
|
||||||
if (type === INPUT_START) {
|
|
||||||
this.started = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.started) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var touches = normalizeSingleTouches.call(this, ev, type);
|
|
||||||
|
|
||||||
// when done, reset the started state
|
|
||||||
if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) {
|
|
||||||
this.started = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.callback(this.manager, type, {
|
|
||||||
pointers: touches[0],
|
|
||||||
changedPointers: touches[1],
|
|
||||||
pointerType: INPUT_TYPE_TOUCH,
|
|
||||||
srcEvent: ev
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @this {TouchInput}
|
|
||||||
* @param {Object} ev
|
|
||||||
* @param {Number} type flag
|
|
||||||
* @returns {undefined|Array} [all, changed]
|
|
||||||
*/
|
|
||||||
function normalizeSingleTouches(ev, type) {
|
|
||||||
var all = toArray(ev.touches);
|
|
||||||
var changed = toArray(ev.changedTouches);
|
|
||||||
|
|
||||||
if (type & (INPUT_END | INPUT_CANCEL)) {
|
|
||||||
all = uniqueArray(all.concat(changed), 'identifier', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [all, changed];
|
|
||||||
}
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
var TOUCH_INPUT_MAP = {
|
|
||||||
touchstart: INPUT_START,
|
|
||||||
touchmove: INPUT_MOVE,
|
|
||||||
touchend: INPUT_END,
|
|
||||||
touchcancel: INPUT_CANCEL
|
|
||||||
};
|
|
||||||
|
|
||||||
var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Multi-user touch events input
|
|
||||||
* @constructor
|
|
||||||
* @extends Input
|
|
||||||
*/
|
|
||||||
function TouchInput() {
|
|
||||||
this.evTarget = TOUCH_TARGET_EVENTS;
|
|
||||||
this.targetIds = {};
|
|
||||||
|
|
||||||
Input.apply(this, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
inherit(TouchInput, Input, {
|
|
||||||
handler: function MTEhandler(ev) {
|
|
||||||
var type = TOUCH_INPUT_MAP[ev.type];
|
|
||||||
var touches = getTouches.call(this, ev, type);
|
|
||||||
if (!touches) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.callback(this.manager, type, {
|
|
||||||
pointers: touches[0],
|
|
||||||
changedPointers: touches[1],
|
|
||||||
pointerType: INPUT_TYPE_TOUCH,
|
|
||||||
srcEvent: ev
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @this {TouchInput}
|
|
||||||
* @param {Object} ev
|
|
||||||
* @param {Number} type flag
|
|
||||||
* @returns {undefined|Array} [all, changed]
|
|
||||||
*/
|
|
||||||
function getTouches(ev, type) {
|
|
||||||
var allTouches = toArray(ev.touches);
|
|
||||||
var targetIds = this.targetIds;
|
|
||||||
|
|
||||||
// when there is only one touch, the process can be simplified
|
|
||||||
if (type & (INPUT_START | INPUT_MOVE) && allTouches.length === 1) {
|
|
||||||
targetIds[allTouches[0].identifier] = true;
|
|
||||||
return [allTouches, allTouches];
|
|
||||||
}
|
|
||||||
|
|
||||||
var i,
|
|
||||||
targetTouches,
|
|
||||||
changedTouches = toArray(ev.changedTouches),
|
|
||||||
changedTargetTouches = [],
|
|
||||||
target = this.target;
|
|
||||||
|
|
||||||
// get target touches from touches
|
|
||||||
targetTouches = allTouches.filter(function(touch) {
|
|
||||||
return hasParent(touch.target, target);
|
|
||||||
});
|
|
||||||
|
|
||||||
// collect touches
|
|
||||||
if (type === INPUT_START) {
|
|
||||||
i = 0;
|
|
||||||
while (i < targetTouches.length) {
|
|
||||||
targetIds[targetTouches[i].identifier] = true;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter changed touches to only contain touches that exist in the collected target ids
|
|
||||||
i = 0;
|
|
||||||
while (i < changedTouches.length) {
|
|
||||||
if (targetIds[changedTouches[i].identifier]) {
|
|
||||||
changedTargetTouches.push(changedTouches[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanup removed touches
|
|
||||||
if (type & (INPUT_END | INPUT_CANCEL)) {
|
|
||||||
delete targetIds[changedTouches[i].identifier];
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!changedTargetTouches.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
// merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel'
|
|
||||||
uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true),
|
|
||||||
changedTargetTouches
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
/**
|
|
||||||
* Combined touch and mouse input
|
|
||||||
*
|
|
||||||
* Touch has a higher priority then mouse, and while touching no mouse events are allowed.
|
|
||||||
* This because touch devices also emit mouse events while doing a touch.
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
* @extends Input
|
|
||||||
*/
|
|
||||||
|
|
||||||
var DEDUP_TIMEOUT = 2500;
|
|
||||||
var DEDUP_DISTANCE = 25;
|
|
||||||
|
|
||||||
function TouchMouseInput() {
|
|
||||||
Input.apply(this, arguments);
|
|
||||||
|
|
||||||
var handler = bindFn(this.handler, this);
|
|
||||||
this.touch = new TouchInput(this.manager, handler);
|
|
||||||
this.mouse = new MouseInput(this.manager, handler);
|
|
||||||
|
|
||||||
this.primaryTouch = null;
|
|
||||||
this.lastTouches = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
inherit(TouchMouseInput, Input, {
|
|
||||||
/**
|
|
||||||
* handle mouse and touch events
|
|
||||||
* @param {Hammer} manager
|
|
||||||
* @param {String} inputEvent
|
|
||||||
* @param {Object} inputData
|
|
||||||
*/
|
|
||||||
handler: function TMEhandler(manager, inputEvent, inputData) {
|
|
||||||
var isTouch = (inputData.pointerType == INPUT_TYPE_TOUCH),
|
|
||||||
isMouse = (inputData.pointerType == INPUT_TYPE_MOUSE);
|
|
||||||
|
|
||||||
if (isMouse && inputData.sourceCapabilities && inputData.sourceCapabilities.firesTouchEvents) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// when we're in a touch event, record touches to de-dupe synthetic mouse event
|
|
||||||
if (isTouch) {
|
|
||||||
recordTouches.call(this, inputEvent, inputData);
|
|
||||||
} else if (isMouse && isSyntheticEvent.call(this, inputData)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.callback(manager, inputEvent, inputData);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* remove the event listeners
|
|
||||||
*/
|
|
||||||
destroy: function destroy() {
|
|
||||||
this.touch.destroy();
|
|
||||||
this.mouse.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function recordTouches(eventType, eventData) {
|
|
||||||
if (eventType & INPUT_START) {
|
|
||||||
this.primaryTouch = eventData.changedPointers[0].identifier;
|
|
||||||
setLastTouch.call(this, eventData);
|
|
||||||
} else if (eventType & (INPUT_END | INPUT_CANCEL)) {
|
|
||||||
setLastTouch.call(this, eventData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLastTouch(eventData) {
|
|
||||||
var touch = eventData.changedPointers[0];
|
|
||||||
|
|
||||||
if (touch.identifier === this.primaryTouch) {
|
|
||||||
var lastTouch = {x: touch.clientX, y: touch.clientY};
|
|
||||||
this.lastTouches.push(lastTouch);
|
|
||||||
var lts = this.lastTouches;
|
|
||||||
var removeLastTouch = function() {
|
|
||||||
var i = lts.indexOf(lastTouch);
|
|
||||||
if (i > -1) {
|
|
||||||
lts.splice(i, 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
setTimeout(removeLastTouch, DEDUP_TIMEOUT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSyntheticEvent(eventData) {
|
|
||||||
var x = eventData.srcEvent.clientX, y = eventData.srcEvent.clientY;
|
|
||||||
for (var i = 0; i < this.lastTouches.length; i++) {
|
|
||||||
var t = this.lastTouches[i];
|
|
||||||
var dx = Math.abs(x - t.x), dy = Math.abs(y - t.y);
|
|
||||||
if (dx <= DEDUP_DISTANCE && dy <= DEDUP_DISTANCE) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
@ -1,312 +0,0 @@
|
||||||
var STOP = 1;
|
|
||||||
var FORCED_STOP = 2;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manager
|
|
||||||
* @param {HTMLElement} element
|
|
||||||
* @param {Object} [options]
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
function Manager(element, options) {
|
|
||||||
this.options = assign({}, Hammer.defaults, options || {});
|
|
||||||
|
|
||||||
this.options.inputTarget = this.options.inputTarget || element;
|
|
||||||
|
|
||||||
this.handlers = {};
|
|
||||||
this.session = {};
|
|
||||||
this.recognizers = [];
|
|
||||||
this.oldCssProps = {};
|
|
||||||
|
|
||||||
this.element = element;
|
|
||||||
this.input = createInputInstance(this);
|
|
||||||
this.touchAction = new TouchAction(this, this.options.touchAction);
|
|
||||||
|
|
||||||
toggleCssProps(this, true);
|
|
||||||
|
|
||||||
each(this.options.recognizers, function(item) {
|
|
||||||
var recognizer = this.add(new (item[0])(item[1]));
|
|
||||||
item[2] && recognizer.recognizeWith(item[2]);
|
|
||||||
item[3] && recognizer.requireFailure(item[3]);
|
|
||||||
}, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
Manager.prototype = {
|
|
||||||
/**
|
|
||||||
* set options
|
|
||||||
* @param {Object} options
|
|
||||||
* @returns {Manager}
|
|
||||||
*/
|
|
||||||
set: function(options) {
|
|
||||||
assign(this.options, options);
|
|
||||||
|
|
||||||
// Options that need a little more setup
|
|
||||||
if (options.touchAction) {
|
|
||||||
this.touchAction.update();
|
|
||||||
}
|
|
||||||
if (options.inputTarget) {
|
|
||||||
// Clean up existing event listeners and reinitialize
|
|
||||||
this.input.destroy();
|
|
||||||
this.input.target = options.inputTarget;
|
|
||||||
this.input.init();
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* stop recognizing for this session.
|
|
||||||
* This session will be discarded, when a new [input]start event is fired.
|
|
||||||
* When forced, the recognizer cycle is stopped immediately.
|
|
||||||
* @param {Boolean} [force]
|
|
||||||
*/
|
|
||||||
stop: function(force) {
|
|
||||||
this.session.stopped = force ? FORCED_STOP : STOP;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* run the recognizers!
|
|
||||||
* called by the inputHandler function on every movement of the pointers (touches)
|
|
||||||
* it walks through all the recognizers and tries to detect the gesture that is being made
|
|
||||||
* @param {Object} inputData
|
|
||||||
*/
|
|
||||||
recognize: function(inputData) {
|
|
||||||
var session = this.session;
|
|
||||||
if (session.stopped) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// run the touch-action polyfill
|
|
||||||
this.touchAction.preventDefaults(inputData);
|
|
||||||
|
|
||||||
var recognizer;
|
|
||||||
var recognizers = this.recognizers;
|
|
||||||
|
|
||||||
// this holds the recognizer that is being recognized.
|
|
||||||
// so the recognizer's state needs to be BEGAN, CHANGED, ENDED or RECOGNIZED
|
|
||||||
// if no recognizer is detecting a thing, it is set to `null`
|
|
||||||
var curRecognizer = session.curRecognizer;
|
|
||||||
|
|
||||||
// reset when the last recognizer is recognized
|
|
||||||
// or when we're in a new session
|
|
||||||
if (!curRecognizer || (curRecognizer && curRecognizer.state & STATE_RECOGNIZED)) {
|
|
||||||
curRecognizer = session.curRecognizer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var i = 0;
|
|
||||||
while (i < recognizers.length) {
|
|
||||||
recognizer = recognizers[i];
|
|
||||||
|
|
||||||
// find out if we are allowed try to recognize the input for this one.
|
|
||||||
// 1. allow if the session is NOT forced stopped (see the .stop() method)
|
|
||||||
// 2. allow if we still haven't recognized a gesture in this session, or the this recognizer is the one
|
|
||||||
// that is being recognized.
|
|
||||||
// 3. allow if the recognizer is allowed to run simultaneous with the current recognized recognizer.
|
|
||||||
// this can be setup with the `recognizeWith()` method on the recognizer.
|
|
||||||
if (session.stopped !== FORCED_STOP && ( // 1
|
|
||||||
!curRecognizer || recognizer == curRecognizer || // 2
|
|
||||||
recognizer.canRecognizeWith(curRecognizer))) { // 3
|
|
||||||
recognizer.recognize(inputData);
|
|
||||||
} else {
|
|
||||||
recognizer.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the recognizer has been recognizing the input as a valid gesture, we want to store this one as the
|
|
||||||
// current active recognizer. but only if we don't already have an active recognizer
|
|
||||||
if (!curRecognizer && recognizer.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED)) {
|
|
||||||
curRecognizer = session.curRecognizer = recognizer;
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get a recognizer by its event name.
|
|
||||||
* @param {Recognizer|String} recognizer
|
|
||||||
* @returns {Recognizer|Null}
|
|
||||||
*/
|
|
||||||
get: function(recognizer) {
|
|
||||||
if (recognizer instanceof Recognizer) {
|
|
||||||
return recognizer;
|
|
||||||
}
|
|
||||||
|
|
||||||
var recognizers = this.recognizers;
|
|
||||||
for (var i = 0; i < recognizers.length; i++) {
|
|
||||||
if (recognizers[i].options.event == recognizer) {
|
|
||||||
return recognizers[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* add a recognizer to the manager
|
|
||||||
* existing recognizers with the same event name will be removed
|
|
||||||
* @param {Recognizer} recognizer
|
|
||||||
* @returns {Recognizer|Manager}
|
|
||||||
*/
|
|
||||||
add: function(recognizer) {
|
|
||||||
if (invokeArrayArg(recognizer, 'add', this)) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove existing
|
|
||||||
var existing = this.get(recognizer.options.event);
|
|
||||||
if (existing) {
|
|
||||||
this.remove(existing);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.recognizers.push(recognizer);
|
|
||||||
recognizer.manager = this;
|
|
||||||
|
|
||||||
this.touchAction.update();
|
|
||||||
return recognizer;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* remove a recognizer by name or instance
|
|
||||||
* @param {Recognizer|String} recognizer
|
|
||||||
* @returns {Manager}
|
|
||||||
*/
|
|
||||||
remove: function(recognizer) {
|
|
||||||
if (invokeArrayArg(recognizer, 'remove', this)) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
recognizer = this.get(recognizer);
|
|
||||||
|
|
||||||
// let's make sure this recognizer exists
|
|
||||||
if (recognizer) {
|
|
||||||
var recognizers = this.recognizers;
|
|
||||||
var index = inArray(recognizers, recognizer);
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
recognizers.splice(index, 1);
|
|
||||||
this.touchAction.update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* bind event
|
|
||||||
* @param {String} events
|
|
||||||
* @param {Function} handler
|
|
||||||
* @returns {EventEmitter} this
|
|
||||||
*/
|
|
||||||
on: function(events, handler) {
|
|
||||||
if (events === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (handler === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var handlers = this.handlers;
|
|
||||||
each(splitStr(events), function(event) {
|
|
||||||
handlers[event] = handlers[event] || [];
|
|
||||||
handlers[event].push(handler);
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* unbind event, leave emit blank to remove all handlers
|
|
||||||
* @param {String} events
|
|
||||||
* @param {Function} [handler]
|
|
||||||
* @returns {EventEmitter} this
|
|
||||||
*/
|
|
||||||
off: function(events, handler) {
|
|
||||||
if (events === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var handlers = this.handlers;
|
|
||||||
each(splitStr(events), function(event) {
|
|
||||||
if (!handler) {
|
|
||||||
delete handlers[event];
|
|
||||||
} else {
|
|
||||||
handlers[event] && handlers[event].splice(inArray(handlers[event], handler), 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* emit event to the listeners
|
|
||||||
* @param {String} event
|
|
||||||
* @param {Object} data
|
|
||||||
*/
|
|
||||||
emit: function(event, data) {
|
|
||||||
// we also want to trigger dom events
|
|
||||||
if (this.options.domEvents) {
|
|
||||||
triggerDomEvent(event, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// no handlers, so skip it all
|
|
||||||
var handlers = this.handlers[event] && this.handlers[event].slice();
|
|
||||||
if (!handlers || !handlers.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
data.type = event;
|
|
||||||
data.preventDefault = function() {
|
|
||||||
data.srcEvent.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
var i = 0;
|
|
||||||
while (i < handlers.length) {
|
|
||||||
handlers[i](data);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* destroy the manager and unbinds all events
|
|
||||||
* it doesn't unbind dom events, that is the user own responsibility
|
|
||||||
*/
|
|
||||||
destroy: function() {
|
|
||||||
this.element && toggleCssProps(this, false);
|
|
||||||
|
|
||||||
this.handlers = {};
|
|
||||||
this.session = {};
|
|
||||||
this.input.destroy();
|
|
||||||
this.element = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* add/remove the css properties as defined in manager.options.cssProps
|
|
||||||
* @param {Manager} manager
|
|
||||||
* @param {Boolean} add
|
|
||||||
*/
|
|
||||||
function toggleCssProps(manager, add) {
|
|
||||||
var element = manager.element;
|
|
||||||
if (!element.style) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var prop;
|
|
||||||
each(manager.options.cssProps, function(value, name) {
|
|
||||||
prop = prefixed(element.style, name);
|
|
||||||
if (add) {
|
|
||||||
manager.oldCssProps[prop] = element.style[prop];
|
|
||||||
element.style[prop] = value;
|
|
||||||
} else {
|
|
||||||
element.style[prop] = manager.oldCssProps[prop] || '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!add) {
|
|
||||||
manager.oldCssProps = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* trigger dom event
|
|
||||||
* @param {String} event
|
|
||||||
* @param {Object} data
|
|
||||||
*/
|
|
||||||
function triggerDomEvent(event, data) {
|
|
||||||
var gestureEvent = document.createEvent('Event');
|
|
||||||
gestureEvent.initEvent(event, true, true);
|
|
||||||
gestureEvent.gesture = data;
|
|
||||||
data.target.dispatchEvent(gestureEvent);
|
|
||||||
}
|
|
||||||
|
|
@ -1,327 +0,0 @@
|
||||||
/**
|
|
||||||
* Recognizer flow explained; *
|
|
||||||
* All recognizers have the initial state of POSSIBLE when a input session starts.
|
|
||||||
* The definition of a input session is from the first input until the last input, with all it's movement in it. *
|
|
||||||
* Example session for mouse-input: mousedown -> mousemove -> mouseup
|
|
||||||
*
|
|
||||||
* On each recognizing cycle (see Manager.recognize) the .recognize() method is executed
|
|
||||||
* which determines with state it should be.
|
|
||||||
*
|
|
||||||
* If the recognizer has the state FAILED, CANCELLED or RECOGNIZED (equals ENDED), it is reset to
|
|
||||||
* POSSIBLE to give it another change on the next cycle.
|
|
||||||
*
|
|
||||||
* Possible
|
|
||||||
* |
|
|
||||||
* +-----+---------------+
|
|
||||||
* | |
|
|
||||||
* +-----+-----+ |
|
|
||||||
* | | |
|
|
||||||
* Failed Cancelled |
|
|
||||||
* +-------+------+
|
|
||||||
* | |
|
|
||||||
* Recognized Began
|
|
||||||
* |
|
|
||||||
* Changed
|
|
||||||
* |
|
|
||||||
* Ended/Recognized
|
|
||||||
*/
|
|
||||||
var STATE_POSSIBLE = 1;
|
|
||||||
var STATE_BEGAN = 2;
|
|
||||||
var STATE_CHANGED = 4;
|
|
||||||
var STATE_ENDED = 8;
|
|
||||||
var STATE_RECOGNIZED = STATE_ENDED;
|
|
||||||
var STATE_CANCELLED = 16;
|
|
||||||
var STATE_FAILED = 32;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recognizer
|
|
||||||
* Every recognizer needs to extend from this class.
|
|
||||||
* @constructor
|
|
||||||
* @param {Object} options
|
|
||||||
*/
|
|
||||||
function Recognizer(options) {
|
|
||||||
this.options = assign({}, this.defaults, options || {});
|
|
||||||
|
|
||||||
this.id = uniqueId();
|
|
||||||
|
|
||||||
this.manager = null;
|
|
||||||
|
|
||||||
// default is enable true
|
|
||||||
this.options.enable = ifUndefined(this.options.enable, true);
|
|
||||||
|
|
||||||
this.state = STATE_POSSIBLE;
|
|
||||||
|
|
||||||
this.simultaneous = {};
|
|
||||||
this.requireFail = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
Recognizer.prototype = {
|
|
||||||
/**
|
|
||||||
* @virtual
|
|
||||||
* @type {Object}
|
|
||||||
*/
|
|
||||||
defaults: {},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* set options
|
|
||||||
* @param {Object} options
|
|
||||||
* @return {Recognizer}
|
|
||||||
*/
|
|
||||||
set: function(options) {
|
|
||||||
assign(this.options, options);
|
|
||||||
|
|
||||||
// also update the touchAction, in case something changed about the directions/enabled state
|
|
||||||
this.manager && this.manager.touchAction.update();
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* recognize simultaneous with an other recognizer.
|
|
||||||
* @param {Recognizer} otherRecognizer
|
|
||||||
* @returns {Recognizer} this
|
|
||||||
*/
|
|
||||||
recognizeWith: function(otherRecognizer) {
|
|
||||||
if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
var simultaneous = this.simultaneous;
|
|
||||||
otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
|
|
||||||
if (!simultaneous[otherRecognizer.id]) {
|
|
||||||
simultaneous[otherRecognizer.id] = otherRecognizer;
|
|
||||||
otherRecognizer.recognizeWith(this);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* drop the simultaneous link. it doesnt remove the link on the other recognizer.
|
|
||||||
* @param {Recognizer} otherRecognizer
|
|
||||||
* @returns {Recognizer} this
|
|
||||||
*/
|
|
||||||
dropRecognizeWith: function(otherRecognizer) {
|
|
||||||
if (invokeArrayArg(otherRecognizer, 'dropRecognizeWith', this)) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
|
|
||||||
delete this.simultaneous[otherRecognizer.id];
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* recognizer can only run when an other is failing
|
|
||||||
* @param {Recognizer} otherRecognizer
|
|
||||||
* @returns {Recognizer} this
|
|
||||||
*/
|
|
||||||
requireFailure: function(otherRecognizer) {
|
|
||||||
if (invokeArrayArg(otherRecognizer, 'requireFailure', this)) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
var requireFail = this.requireFail;
|
|
||||||
otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
|
|
||||||
if (inArray(requireFail, otherRecognizer) === -1) {
|
|
||||||
requireFail.push(otherRecognizer);
|
|
||||||
otherRecognizer.requireFailure(this);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* drop the requireFailure link. it does not remove the link on the other recognizer.
|
|
||||||
* @param {Recognizer} otherRecognizer
|
|
||||||
* @returns {Recognizer} this
|
|
||||||
*/
|
|
||||||
dropRequireFailure: function(otherRecognizer) {
|
|
||||||
if (invokeArrayArg(otherRecognizer, 'dropRequireFailure', this)) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
|
|
||||||
var index = inArray(this.requireFail, otherRecognizer);
|
|
||||||
if (index > -1) {
|
|
||||||
this.requireFail.splice(index, 1);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* has require failures boolean
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
hasRequireFailures: function() {
|
|
||||||
return this.requireFail.length > 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if the recognizer can recognize simultaneous with an other recognizer
|
|
||||||
* @param {Recognizer} otherRecognizer
|
|
||||||
* @returns {Boolean}
|
|
||||||
*/
|
|
||||||
canRecognizeWith: function(otherRecognizer) {
|
|
||||||
return !!this.simultaneous[otherRecognizer.id];
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* You should use `tryEmit` instead of `emit` directly to check
|
|
||||||
* that all the needed recognizers has failed before emitting.
|
|
||||||
* @param {Object} input
|
|
||||||
*/
|
|
||||||
emit: function(input) {
|
|
||||||
var self = this;
|
|
||||||
var state = this.state;
|
|
||||||
|
|
||||||
function emit(event) {
|
|
||||||
self.manager.emit(event, input);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 'panstart' and 'panmove'
|
|
||||||
if (state < STATE_ENDED) {
|
|
||||||
emit(self.options.event + stateStr(state));
|
|
||||||
}
|
|
||||||
|
|
||||||
emit(self.options.event); // simple 'eventName' events
|
|
||||||
|
|
||||||
if (input.additionalEvent) { // additional event(panleft, panright, pinchin, pinchout...)
|
|
||||||
emit(input.additionalEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// panend and pancancel
|
|
||||||
if (state >= STATE_ENDED) {
|
|
||||||
emit(self.options.event + stateStr(state));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that all the require failure recognizers has failed,
|
|
||||||
* if true, it emits a gesture event,
|
|
||||||
* otherwise, setup the state to FAILED.
|
|
||||||
* @param {Object} input
|
|
||||||
*/
|
|
||||||
tryEmit: function(input) {
|
|
||||||
if (this.canEmit()) {
|
|
||||||
return this.emit(input);
|
|
||||||
}
|
|
||||||
// it's failing anyway
|
|
||||||
this.state = STATE_FAILED;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* can we emit?
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
canEmit: function() {
|
|
||||||
var i = 0;
|
|
||||||
while (i < this.requireFail.length) {
|
|
||||||
if (!(this.requireFail[i].state & (STATE_FAILED | STATE_POSSIBLE))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* update the recognizer
|
|
||||||
* @param {Object} inputData
|
|
||||||
*/
|
|
||||||
recognize: function(inputData) {
|
|
||||||
// make a new copy of the inputData
|
|
||||||
// so we can change the inputData without messing up the other recognizers
|
|
||||||
var inputDataClone = assign({}, inputData);
|
|
||||||
|
|
||||||
// is is enabled and allow recognizing?
|
|
||||||
if (!boolOrFn(this.options.enable, [this, inputDataClone])) {
|
|
||||||
this.reset();
|
|
||||||
this.state = STATE_FAILED;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// reset when we've reached the end
|
|
||||||
if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) {
|
|
||||||
this.state = STATE_POSSIBLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state = this.process(inputDataClone);
|
|
||||||
|
|
||||||
// the recognizer has recognized a gesture
|
|
||||||
// so trigger an event
|
|
||||||
if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) {
|
|
||||||
this.tryEmit(inputDataClone);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* return the state of the recognizer
|
|
||||||
* the actual recognizing happens in this method
|
|
||||||
* @virtual
|
|
||||||
* @param {Object} inputData
|
|
||||||
* @returns {Const} STATE
|
|
||||||
*/
|
|
||||||
process: function(inputData) { }, // jshint ignore:line
|
|
||||||
|
|
||||||
/**
|
|
||||||
* return the preferred touch-action
|
|
||||||
* @virtual
|
|
||||||
* @returns {Array}
|
|
||||||
*/
|
|
||||||
getTouchAction: function() { },
|
|
||||||
|
|
||||||
/**
|
|
||||||
* called when the gesture isn't allowed to recognize
|
|
||||||
* like when another is being recognized or it is disabled
|
|
||||||
* @virtual
|
|
||||||
*/
|
|
||||||
reset: function() { }
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get a usable string, used as event postfix
|
|
||||||
* @param {Const} state
|
|
||||||
* @returns {String} state
|
|
||||||
*/
|
|
||||||
function stateStr(state) {
|
|
||||||
if (state & STATE_CANCELLED) {
|
|
||||||
return 'cancel';
|
|
||||||
} else if (state & STATE_ENDED) {
|
|
||||||
return 'end';
|
|
||||||
} else if (state & STATE_CHANGED) {
|
|
||||||
return 'move';
|
|
||||||
} else if (state & STATE_BEGAN) {
|
|
||||||
return 'start';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* direction cons to string
|
|
||||||
* @param {Const} direction
|
|
||||||
* @returns {String}
|
|
||||||
*/
|
|
||||||
function directionStr(direction) {
|
|
||||||
if (direction == DIRECTION_DOWN) {
|
|
||||||
return 'down';
|
|
||||||
} else if (direction == DIRECTION_UP) {
|
|
||||||
return 'up';
|
|
||||||
} else if (direction == DIRECTION_LEFT) {
|
|
||||||
return 'left';
|
|
||||||
} else if (direction == DIRECTION_RIGHT) {
|
|
||||||
return 'right';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get a recognizer by name if it is bound to a manager
|
|
||||||
* @param {Recognizer|String} otherRecognizer
|
|
||||||
* @param {Recognizer} recognizer
|
|
||||||
* @returns {Recognizer}
|
|
||||||
*/
|
|
||||||
function getRecognizerByNameIfManager(otherRecognizer, recognizer) {
|
|
||||||
var manager = recognizer.manager;
|
|
||||||
if (manager) {
|
|
||||||
return manager.get(otherRecognizer);
|
|
||||||
}
|
|
||||||
return otherRecognizer;
|
|
||||||
}
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
/**
|
|
||||||
* This recognizer is just used as a base for the simple attribute recognizers.
|
|
||||||
* @constructor
|
|
||||||
* @extends Recognizer
|
|
||||||
*/
|
|
||||||
function AttrRecognizer() {
|
|
||||||
Recognizer.apply(this, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
inherit(AttrRecognizer, Recognizer, {
|
|
||||||
/**
|
|
||||||
* @namespace
|
|
||||||
* @memberof AttrRecognizer
|
|
||||||
*/
|
|
||||||
defaults: {
|
|
||||||
/**
|
|
||||||
* @type {Number}
|
|
||||||
* @default 1
|
|
||||||
*/
|
|
||||||
pointers: 1
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to check if it the recognizer receives valid input, like input.distance > 10.
|
|
||||||
* @memberof AttrRecognizer
|
|
||||||
* @param {Object} input
|
|
||||||
* @returns {Boolean} recognized
|
|
||||||
*/
|
|
||||||
attrTest: function(input) {
|
|
||||||
var optionPointers = this.options.pointers;
|
|
||||||
return optionPointers === 0 || input.pointers.length === optionPointers;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process the input and return the state for the recognizer
|
|
||||||
* @memberof AttrRecognizer
|
|
||||||
* @param {Object} input
|
|
||||||
* @returns {*} State
|
|
||||||
*/
|
|
||||||
process: function(input) {
|
|
||||||
var state = this.state;
|
|
||||||
var eventType = input.eventType;
|
|
||||||
|
|
||||||
var isRecognized = state & (STATE_BEGAN | STATE_CHANGED);
|
|
||||||
var isValid = this.attrTest(input);
|
|
||||||
|
|
||||||
// on cancel input and we've recognized before, return STATE_CANCELLED
|
|
||||||
if (isRecognized && (eventType & INPUT_CANCEL || !isValid)) {
|
|
||||||
return state | STATE_CANCELLED;
|
|
||||||
} else if (isRecognized || isValid) {
|
|
||||||
if (eventType & INPUT_END) {
|
|
||||||
return state | STATE_ENDED;
|
|
||||||
} else if (!(state & STATE_BEGAN)) {
|
|
||||||
return STATE_BEGAN;
|
|
||||||
}
|
|
||||||
return state | STATE_CHANGED;
|
|
||||||
}
|
|
||||||
return STATE_FAILED;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
/**
|
|
||||||
* Pan
|
|
||||||
* Recognized when the pointer is down and moved in the allowed direction.
|
|
||||||
* @constructor
|
|
||||||
* @extends AttrRecognizer
|
|
||||||
*/
|
|
||||||
function PanRecognizer() {
|
|
||||||
AttrRecognizer.apply(this, arguments);
|
|
||||||
|
|
||||||
this.pX = null;
|
|
||||||
this.pY = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
inherit(PanRecognizer, AttrRecognizer, {
|
|
||||||
/**
|
|
||||||
* @namespace
|
|
||||||
* @memberof PanRecognizer
|
|
||||||
*/
|
|
||||||
defaults: {
|
|
||||||
event: 'pan',
|
|
||||||
threshold: 10,
|
|
||||||
pointers: 1,
|
|
||||||
direction: DIRECTION_ALL
|
|
||||||
},
|
|
||||||
|
|
||||||
getTouchAction: function() {
|
|
||||||
var direction = this.options.direction;
|
|
||||||
var actions = [];
|
|
||||||
if (direction & DIRECTION_HORIZONTAL) {
|
|
||||||
actions.push(TOUCH_ACTION_PAN_Y);
|
|
||||||
}
|
|
||||||
if (direction & DIRECTION_VERTICAL) {
|
|
||||||
actions.push(TOUCH_ACTION_PAN_X);
|
|
||||||
}
|
|
||||||
return actions;
|
|
||||||
},
|
|
||||||
|
|
||||||
directionTest: function(input) {
|
|
||||||
var options = this.options;
|
|
||||||
var hasMoved = true;
|
|
||||||
var distance = input.distance;
|
|
||||||
var direction = input.direction;
|
|
||||||
var x = input.deltaX;
|
|
||||||
var y = input.deltaY;
|
|
||||||
|
|
||||||
// lock to axis?
|
|
||||||
if (!(direction & options.direction)) {
|
|
||||||
if (options.direction & DIRECTION_HORIZONTAL) {
|
|
||||||
direction = (x === 0) ? DIRECTION_NONE : (x < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT;
|
|
||||||
hasMoved = x != this.pX;
|
|
||||||
distance = Math.abs(input.deltaX);
|
|
||||||
} else {
|
|
||||||
direction = (y === 0) ? DIRECTION_NONE : (y < 0) ? DIRECTION_UP : DIRECTION_DOWN;
|
|
||||||
hasMoved = y != this.pY;
|
|
||||||
distance = Math.abs(input.deltaY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
input.direction = direction;
|
|
||||||
return hasMoved && distance > options.threshold && direction & options.direction;
|
|
||||||
},
|
|
||||||
|
|
||||||
attrTest: function(input) {
|
|
||||||
return AttrRecognizer.prototype.attrTest.call(this, input) &&
|
|
||||||
(this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input)));
|
|
||||||
},
|
|
||||||
|
|
||||||
emit: function(input) {
|
|
||||||
|
|
||||||
this.pX = input.deltaX;
|
|
||||||
this.pY = input.deltaY;
|
|
||||||
|
|
||||||
var direction = directionStr(input.direction);
|
|
||||||
|
|
||||||
if (direction) {
|
|
||||||
input.additionalEvent = this.options.event + direction;
|
|
||||||
}
|
|
||||||
this._super.emit.call(this, input);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
/**
|
|
||||||
* Pinch
|
|
||||||
* Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out).
|
|
||||||
* @constructor
|
|
||||||
* @extends AttrRecognizer
|
|
||||||
*/
|
|
||||||
function PinchRecognizer() {
|
|
||||||
AttrRecognizer.apply(this, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
inherit(PinchRecognizer, AttrRecognizer, {
|
|
||||||
/**
|
|
||||||
* @namespace
|
|
||||||
* @memberof PinchRecognizer
|
|
||||||
*/
|
|
||||||
defaults: {
|
|
||||||
event: 'pinch',
|
|
||||||
threshold: 0,
|
|
||||||
pointers: 2
|
|
||||||
},
|
|
||||||
|
|
||||||
getTouchAction: function() {
|
|
||||||
return [TOUCH_ACTION_NONE];
|
|
||||||
},
|
|
||||||
|
|
||||||
attrTest: function(input) {
|
|
||||||
return this._super.attrTest.call(this, input) &&
|
|
||||||
(Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN);
|
|
||||||
},
|
|
||||||
|
|
||||||
emit: function(input) {
|
|
||||||
if (input.scale !== 1) {
|
|
||||||
var inOut = input.scale < 1 ? 'in' : 'out';
|
|
||||||
input.additionalEvent = this.options.event + inOut;
|
|
||||||
}
|
|
||||||
this._super.emit.call(this, input);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
/**
|
|
||||||
* Press
|
|
||||||
* Recognized when the pointer is down for x ms without any movement.
|
|
||||||
* @constructor
|
|
||||||
* @extends Recognizer
|
|
||||||
*/
|
|
||||||
function PressRecognizer() {
|
|
||||||
Recognizer.apply(this, arguments);
|
|
||||||
|
|
||||||
this._timer = null;
|
|
||||||
this._input = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
inherit(PressRecognizer, Recognizer, {
|
|
||||||
/**
|
|
||||||
* @namespace
|
|
||||||
* @memberof PressRecognizer
|
|
||||||
*/
|
|
||||||
defaults: {
|
|
||||||
event: 'press',
|
|
||||||
pointers: 1,
|
|
||||||
time: 251, // minimal time of the pointer to be pressed
|
|
||||||
threshold: 9 // a minimal movement is ok, but keep it low
|
|
||||||
},
|
|
||||||
|
|
||||||
getTouchAction: function() {
|
|
||||||
return [TOUCH_ACTION_AUTO];
|
|
||||||
},
|
|
||||||
|
|
||||||
process: function(input) {
|
|
||||||
var options = this.options;
|
|
||||||
var validPointers = input.pointers.length === options.pointers;
|
|
||||||
var validMovement = input.distance < options.threshold;
|
|
||||||
var validTime = input.deltaTime > options.time;
|
|
||||||
|
|
||||||
this._input = input;
|
|
||||||
|
|
||||||
// we only allow little movement
|
|
||||||
// and we've reached an end event, so a tap is possible
|
|
||||||
if (!validMovement || !validPointers || (input.eventType & (INPUT_END | INPUT_CANCEL) && !validTime)) {
|
|
||||||
this.reset();
|
|
||||||
} else if (input.eventType & INPUT_START) {
|
|
||||||
this.reset();
|
|
||||||
this._timer = setTimeoutContext(function() {
|
|
||||||
this.state = STATE_RECOGNIZED;
|
|
||||||
this.tryEmit();
|
|
||||||
}, options.time, this);
|
|
||||||
} else if (input.eventType & INPUT_END) {
|
|
||||||
return STATE_RECOGNIZED;
|
|
||||||
}
|
|
||||||
return STATE_FAILED;
|
|
||||||
},
|
|
||||||
|
|
||||||
reset: function() {
|
|
||||||
clearTimeout(this._timer);
|
|
||||||
},
|
|
||||||
|
|
||||||
emit: function(input) {
|
|
||||||
if (this.state !== STATE_RECOGNIZED) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input && (input.eventType & INPUT_END)) {
|
|
||||||
this.manager.emit(this.options.event + 'up', input);
|
|
||||||
} else {
|
|
||||||
this._input.timeStamp = now();
|
|
||||||
this.manager.emit(this.options.event, this._input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
/**
|
|
||||||
* Rotate
|
|
||||||
* Recognized when two or more pointer are moving in a circular motion.
|
|
||||||
* @constructor
|
|
||||||
* @extends AttrRecognizer
|
|
||||||
*/
|
|
||||||
function RotateRecognizer() {
|
|
||||||
AttrRecognizer.apply(this, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
inherit(RotateRecognizer, AttrRecognizer, {
|
|
||||||
/**
|
|
||||||
* @namespace
|
|
||||||
* @memberof RotateRecognizer
|
|
||||||
*/
|
|
||||||
defaults: {
|
|
||||||
event: 'rotate',
|
|
||||||
threshold: 0,
|
|
||||||
pointers: 2
|
|
||||||
},
|
|
||||||
|
|
||||||
getTouchAction: function() {
|
|
||||||
return [TOUCH_ACTION_NONE];
|
|
||||||
},
|
|
||||||
|
|
||||||
attrTest: function(input) {
|
|
||||||
return this._super.attrTest.call(this, input) &&
|
|
||||||
(Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
/**
|
|
||||||
* Swipe
|
|
||||||
* Recognized when the pointer is moving fast (velocity), with enough distance in the allowed direction.
|
|
||||||
* @constructor
|
|
||||||
* @extends AttrRecognizer
|
|
||||||
*/
|
|
||||||
function SwipeRecognizer() {
|
|
||||||
AttrRecognizer.apply(this, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
inherit(SwipeRecognizer, AttrRecognizer, {
|
|
||||||
/**
|
|
||||||
* @namespace
|
|
||||||
* @memberof SwipeRecognizer
|
|
||||||
*/
|
|
||||||
defaults: {
|
|
||||||
event: 'swipe',
|
|
||||||
threshold: 10,
|
|
||||||
velocity: 0.3,
|
|
||||||
direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL,
|
|
||||||
pointers: 1
|
|
||||||
},
|
|
||||||
|
|
||||||
getTouchAction: function() {
|
|
||||||
return PanRecognizer.prototype.getTouchAction.call(this);
|
|
||||||
},
|
|
||||||
|
|
||||||
attrTest: function(input) {
|
|
||||||
var direction = this.options.direction;
|
|
||||||
var velocity;
|
|
||||||
|
|
||||||
if (direction & (DIRECTION_HORIZONTAL | DIRECTION_VERTICAL)) {
|
|
||||||
velocity = input.overallVelocity;
|
|
||||||
} else if (direction & DIRECTION_HORIZONTAL) {
|
|
||||||
velocity = input.overallVelocityX;
|
|
||||||
} else if (direction & DIRECTION_VERTICAL) {
|
|
||||||
velocity = input.overallVelocityY;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._super.attrTest.call(this, input) &&
|
|
||||||
direction & input.offsetDirection &&
|
|
||||||
input.distance > this.options.threshold &&
|
|
||||||
input.maxPointers == this.options.pointers &&
|
|
||||||
abs(velocity) > this.options.velocity && input.eventType & INPUT_END;
|
|
||||||
},
|
|
||||||
|
|
||||||
emit: function(input) {
|
|
||||||
var direction = directionStr(input.offsetDirection);
|
|
||||||
if (direction) {
|
|
||||||
this.manager.emit(this.options.event + direction, input);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.manager.emit(this.options.event, input);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
/**
|
|
||||||
* A tap is ecognized when the pointer is doing a small tap/click. Multiple taps are recognized if they occur
|
|
||||||
* between the given interval and position. The delay option can be used to recognize multi-taps without firing
|
|
||||||
* a single tap.
|
|
||||||
*
|
|
||||||
* The eventData from the emitted event contains the property `tapCount`, which contains the amount of
|
|
||||||
* multi-taps being recognized.
|
|
||||||
* @constructor
|
|
||||||
* @extends Recognizer
|
|
||||||
*/
|
|
||||||
function TapRecognizer() {
|
|
||||||
Recognizer.apply(this, arguments);
|
|
||||||
|
|
||||||
// previous time and center,
|
|
||||||
// used for tap counting
|
|
||||||
this.pTime = false;
|
|
||||||
this.pCenter = false;
|
|
||||||
|
|
||||||
this._timer = null;
|
|
||||||
this._input = null;
|
|
||||||
this.count = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
inherit(TapRecognizer, Recognizer, {
|
|
||||||
/**
|
|
||||||
* @namespace
|
|
||||||
* @memberof PinchRecognizer
|
|
||||||
*/
|
|
||||||
defaults: {
|
|
||||||
event: 'tap',
|
|
||||||
pointers: 1,
|
|
||||||
taps: 1,
|
|
||||||
interval: 300, // max time between the multi-tap taps
|
|
||||||
time: 250, // max time of the pointer to be down (like finger on the screen)
|
|
||||||
threshold: 9, // a minimal movement is ok, but keep it low
|
|
||||||
posThreshold: 10 // a multi-tap can be a bit off the initial position
|
|
||||||
},
|
|
||||||
|
|
||||||
getTouchAction: function() {
|
|
||||||
return [TOUCH_ACTION_MANIPULATION];
|
|
||||||
},
|
|
||||||
|
|
||||||
process: function(input) {
|
|
||||||
var options = this.options;
|
|
||||||
|
|
||||||
var validPointers = input.pointers.length === options.pointers;
|
|
||||||
var validMovement = input.distance < options.threshold;
|
|
||||||
var validTouchTime = input.deltaTime < options.time;
|
|
||||||
|
|
||||||
this.reset();
|
|
||||||
|
|
||||||
if ((input.eventType & INPUT_START) && (this.count === 0)) {
|
|
||||||
return this.failTimeout();
|
|
||||||
}
|
|
||||||
|
|
||||||
// we only allow little movement
|
|
||||||
// and we've reached an end event, so a tap is possible
|
|
||||||
if (validMovement && validTouchTime && validPointers) {
|
|
||||||
if (input.eventType != INPUT_END) {
|
|
||||||
return this.failTimeout();
|
|
||||||
}
|
|
||||||
|
|
||||||
var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true;
|
|
||||||
var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold;
|
|
||||||
|
|
||||||
this.pTime = input.timeStamp;
|
|
||||||
this.pCenter = input.center;
|
|
||||||
|
|
||||||
if (!validMultiTap || !validInterval) {
|
|
||||||
this.count = 1;
|
|
||||||
} else {
|
|
||||||
this.count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._input = input;
|
|
||||||
|
|
||||||
// if tap count matches we have recognized it,
|
|
||||||
// else it has began recognizing...
|
|
||||||
var tapCount = this.count % options.taps;
|
|
||||||
if (tapCount === 0) {
|
|
||||||
// no failing requirements, immediately trigger the tap event
|
|
||||||
// or wait as long as the multitap interval to trigger
|
|
||||||
if (!this.hasRequireFailures()) {
|
|
||||||
return STATE_RECOGNIZED;
|
|
||||||
} else {
|
|
||||||
this._timer = setTimeoutContext(function() {
|
|
||||||
this.state = STATE_RECOGNIZED;
|
|
||||||
this.tryEmit();
|
|
||||||
}, options.interval, this);
|
|
||||||
return STATE_BEGAN;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return STATE_FAILED;
|
|
||||||
},
|
|
||||||
|
|
||||||
failTimeout: function() {
|
|
||||||
this._timer = setTimeoutContext(function() {
|
|
||||||
this.state = STATE_FAILED;
|
|
||||||
}, this.options.interval, this);
|
|
||||||
return STATE_FAILED;
|
|
||||||
},
|
|
||||||
|
|
||||||
reset: function() {
|
|
||||||
clearTimeout(this._timer);
|
|
||||||
},
|
|
||||||
|
|
||||||
emit: function() {
|
|
||||||
if (this.state == STATE_RECOGNIZED) {
|
|
||||||
this._input.tapCount = this.count;
|
|
||||||
this.manager.emit(this.options.event, this._input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction');
|
|
||||||
var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined;
|
|
||||||
|
|
||||||
// magical touchAction value
|
|
||||||
var TOUCH_ACTION_COMPUTE = 'compute';
|
|
||||||
var TOUCH_ACTION_AUTO = 'auto';
|
|
||||||
var TOUCH_ACTION_MANIPULATION = 'manipulation'; // not implemented
|
|
||||||
var TOUCH_ACTION_NONE = 'none';
|
|
||||||
var TOUCH_ACTION_PAN_X = 'pan-x';
|
|
||||||
var TOUCH_ACTION_PAN_Y = 'pan-y';
|
|
||||||
var TOUCH_ACTION_MAP = getTouchActionProps();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Touch Action
|
|
||||||
* sets the touchAction property or uses the js alternative
|
|
||||||
* @param {Manager} manager
|
|
||||||
* @param {String} value
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
function TouchAction(manager, value) {
|
|
||||||
this.manager = manager;
|
|
||||||
this.set(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
TouchAction.prototype = {
|
|
||||||
/**
|
|
||||||
* set the touchAction value on the element or enable the polyfill
|
|
||||||
* @param {String} value
|
|
||||||
*/
|
|
||||||
set: function(value) {
|
|
||||||
// find out the touch-action by the event handlers
|
|
||||||
if (value == TOUCH_ACTION_COMPUTE) {
|
|
||||||
value = this.compute();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (NATIVE_TOUCH_ACTION && this.manager.element.style && TOUCH_ACTION_MAP[value]) {
|
|
||||||
this.manager.element.style[PREFIXED_TOUCH_ACTION] = value;
|
|
||||||
}
|
|
||||||
this.actions = value.toLowerCase().trim();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* just re-set the touchAction value
|
|
||||||
*/
|
|
||||||
update: function() {
|
|
||||||
this.set(this.manager.options.touchAction);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* compute the value for the touchAction property based on the recognizer's settings
|
|
||||||
* @returns {String} value
|
|
||||||
*/
|
|
||||||
compute: function() {
|
|
||||||
var actions = [];
|
|
||||||
each(this.manager.recognizers, function(recognizer) {
|
|
||||||
if (boolOrFn(recognizer.options.enable, [recognizer])) {
|
|
||||||
actions = actions.concat(recognizer.getTouchAction());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return cleanTouchActions(actions.join(' '));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* this method is called on each input cycle and provides the preventing of the browser behavior
|
|
||||||
* @param {Object} input
|
|
||||||
*/
|
|
||||||
preventDefaults: function(input) {
|
|
||||||
var srcEvent = input.srcEvent;
|
|
||||||
var direction = input.offsetDirection;
|
|
||||||
|
|
||||||
// if the touch action did prevented once this session
|
|
||||||
if (this.manager.session.prevented) {
|
|
||||||
srcEvent.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var actions = this.actions;
|
|
||||||
var hasNone = inStr(actions, TOUCH_ACTION_NONE) && !TOUCH_ACTION_MAP[TOUCH_ACTION_NONE];
|
|
||||||
var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y) && !TOUCH_ACTION_MAP[TOUCH_ACTION_PAN_Y];
|
|
||||||
var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X) && !TOUCH_ACTION_MAP[TOUCH_ACTION_PAN_X];
|
|
||||||
|
|
||||||
if (hasNone) {
|
|
||||||
//do not prevent defaults if this is a tap gesture
|
|
||||||
|
|
||||||
var isTapPointer = input.pointers.length === 1;
|
|
||||||
var isTapMovement = input.distance < 2;
|
|
||||||
var isTapTouchTime = input.deltaTime < 250;
|
|
||||||
|
|
||||||
if (isTapPointer && isTapMovement && isTapTouchTime) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasPanX && hasPanY) {
|
|
||||||
// `pan-x pan-y` means browser handles all scrolling/panning, do not prevent
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNone ||
|
|
||||||
(hasPanY && direction & DIRECTION_HORIZONTAL) ||
|
|
||||||
(hasPanX && direction & DIRECTION_VERTICAL)) {
|
|
||||||
return this.preventSrc(srcEvent);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* call preventDefault to prevent the browser's default behavior (scrolling in most cases)
|
|
||||||
* @param {Object} srcEvent
|
|
||||||
*/
|
|
||||||
preventSrc: function(srcEvent) {
|
|
||||||
this.manager.session.prevented = true;
|
|
||||||
srcEvent.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* when the touchActions are collected they are not a valid value, so we need to clean things up. *
|
|
||||||
* @param {String} actions
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
function cleanTouchActions(actions) {
|
|
||||||
// none
|
|
||||||
if (inStr(actions, TOUCH_ACTION_NONE)) {
|
|
||||||
return TOUCH_ACTION_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X);
|
|
||||||
var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y);
|
|
||||||
|
|
||||||
// if both pan-x and pan-y are set (different recognizers
|
|
||||||
// for different directions, e.g. horizontal pan but vertical swipe?)
|
|
||||||
// we need none (as otherwise with pan-x pan-y combined none of these
|
|
||||||
// recognizers will work, since the browser would handle all panning
|
|
||||||
if (hasPanX && hasPanY) {
|
|
||||||
return TOUCH_ACTION_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// pan-x OR pan-y
|
|
||||||
if (hasPanX || hasPanY) {
|
|
||||||
return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y;
|
|
||||||
}
|
|
||||||
|
|
||||||
// manipulation
|
|
||||||
if (inStr(actions, TOUCH_ACTION_MANIPULATION)) {
|
|
||||||
return TOUCH_ACTION_MANIPULATION;
|
|
||||||
}
|
|
||||||
|
|
||||||
return TOUCH_ACTION_AUTO;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTouchActionProps() {
|
|
||||||
if (!NATIVE_TOUCH_ACTION) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var touchMap = {};
|
|
||||||
var cssSupports = window.CSS && window.CSS.supports;
|
|
||||||
['auto', 'manipulation', 'pan-y', 'pan-x', 'pan-x pan-y', 'none'].forEach(function(val) {
|
|
||||||
|
|
||||||
// If css.supports is not supported but there is native touch-action assume it supports
|
|
||||||
// all values. This is the case for IE 10 and 11.
|
|
||||||
touchMap[val] = cssSupports ? window.CSS.supports('touch-action', val) : true;
|
|
||||||
});
|
|
||||||
return touchMap;
|
|
||||||
}
|
|
||||||
|
|
@ -1,371 +0,0 @@
|
||||||
var VENDOR_PREFIXES = ['', 'webkit', 'Moz', 'MS', 'ms', 'o'];
|
|
||||||
var TEST_ELEMENT = document.createElement('div');
|
|
||||||
|
|
||||||
var TYPE_FUNCTION = 'function';
|
|
||||||
|
|
||||||
var round = Math.round;
|
|
||||||
var abs = Math.abs;
|
|
||||||
var now = Date.now;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* set a timeout with a given scope
|
|
||||||
* @param {Function} fn
|
|
||||||
* @param {Number} timeout
|
|
||||||
* @param {Object} context
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
function setTimeoutContext(fn, timeout, context) {
|
|
||||||
return setTimeout(bindFn(fn, context), timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if the argument is an array, we want to execute the fn on each entry
|
|
||||||
* if it aint an array we don't want to do a thing.
|
|
||||||
* this is used by all the methods that accept a single and array argument.
|
|
||||||
* @param {*|Array} arg
|
|
||||||
* @param {String} fn
|
|
||||||
* @param {Object} [context]
|
|
||||||
* @returns {Boolean}
|
|
||||||
*/
|
|
||||||
function invokeArrayArg(arg, fn, context) {
|
|
||||||
if (Array.isArray(arg)) {
|
|
||||||
each(arg, context[fn], context);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* walk objects and arrays
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {Function} iterator
|
|
||||||
* @param {Object} context
|
|
||||||
*/
|
|
||||||
function each(obj, iterator, context) {
|
|
||||||
var i;
|
|
||||||
|
|
||||||
if (!obj) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (obj.forEach) {
|
|
||||||
obj.forEach(iterator, context);
|
|
||||||
} else if (obj.length !== undefined) {
|
|
||||||
i = 0;
|
|
||||||
while (i < obj.length) {
|
|
||||||
iterator.call(context, obj[i], i, obj);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (i in obj) {
|
|
||||||
obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* wrap a method with a deprecation warning and stack trace
|
|
||||||
* @param {Function} method
|
|
||||||
* @param {String} name
|
|
||||||
* @param {String} message
|
|
||||||
* @returns {Function} A new function wrapping the supplied method.
|
|
||||||
*/
|
|
||||||
function deprecate(method, name, message) {
|
|
||||||
var deprecationMessage = 'DEPRECATED METHOD: ' + name + '\n' + message + ' AT \n';
|
|
||||||
return function() {
|
|
||||||
var e = new Error('get-stack-trace');
|
|
||||||
var stack = e && e.stack ? e.stack.replace(/^[^\(]+?[\n$]/gm, '')
|
|
||||||
.replace(/^\s+at\s+/gm, '')
|
|
||||||
.replace(/^Object.<anonymous>\s*\(/gm, '{anonymous}()@') : 'Unknown Stack Trace';
|
|
||||||
|
|
||||||
var log = window.console && (window.console.warn || window.console.log);
|
|
||||||
if (log) {
|
|
||||||
log.call(window.console, deprecationMessage, stack);
|
|
||||||
}
|
|
||||||
return method.apply(this, arguments);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* extend object.
|
|
||||||
* means that properties in dest will be overwritten by the ones in src.
|
|
||||||
* @param {Object} target
|
|
||||||
* @param {...Object} objects_to_assign
|
|
||||||
* @returns {Object} target
|
|
||||||
*/
|
|
||||||
var assign;
|
|
||||||
if (typeof Object.assign !== 'function') {
|
|
||||||
assign = function assign(target) {
|
|
||||||
if (target === undefined || target === null) {
|
|
||||||
throw new TypeError('Cannot convert undefined or null to object');
|
|
||||||
}
|
|
||||||
|
|
||||||
var output = Object(target);
|
|
||||||
for (var index = 1; index < arguments.length; index++) {
|
|
||||||
var source = arguments[index];
|
|
||||||
if (source !== undefined && source !== null) {
|
|
||||||
for (var nextKey in source) {
|
|
||||||
if (source.hasOwnProperty(nextKey)) {
|
|
||||||
output[nextKey] = source[nextKey];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
assign = Object.assign;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* extend object.
|
|
||||||
* means that properties in dest will be overwritten by the ones in src.
|
|
||||||
* @param {Object} dest
|
|
||||||
* @param {Object} src
|
|
||||||
* @param {Boolean} [merge=false]
|
|
||||||
* @returns {Object} dest
|
|
||||||
*/
|
|
||||||
var extend = deprecate(function extend(dest, src, merge) {
|
|
||||||
var keys = Object.keys(src);
|
|
||||||
var i = 0;
|
|
||||||
while (i < keys.length) {
|
|
||||||
if (!merge || (merge && dest[keys[i]] === undefined)) {
|
|
||||||
dest[keys[i]] = src[keys[i]];
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
return dest;
|
|
||||||
}, 'extend', 'Use `assign`.');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* merge the values from src in the dest.
|
|
||||||
* means that properties that exist in dest will not be overwritten by src
|
|
||||||
* @param {Object} dest
|
|
||||||
* @param {Object} src
|
|
||||||
* @returns {Object} dest
|
|
||||||
*/
|
|
||||||
var merge = deprecate(function merge(dest, src) {
|
|
||||||
return extend(dest, src, true);
|
|
||||||
}, 'merge', 'Use `assign`.');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* simple class inheritance
|
|
||||||
* @param {Function} child
|
|
||||||
* @param {Function} base
|
|
||||||
* @param {Object} [properties]
|
|
||||||
*/
|
|
||||||
function inherit(child, base, properties) {
|
|
||||||
var baseP = base.prototype,
|
|
||||||
childP;
|
|
||||||
|
|
||||||
childP = child.prototype = Object.create(baseP);
|
|
||||||
childP.constructor = child;
|
|
||||||
childP._super = baseP;
|
|
||||||
|
|
||||||
if (properties) {
|
|
||||||
assign(childP, properties);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* simple function bind
|
|
||||||
* @param {Function} fn
|
|
||||||
* @param {Object} context
|
|
||||||
* @returns {Function}
|
|
||||||
*/
|
|
||||||
function bindFn(fn, context) {
|
|
||||||
return function boundFn() {
|
|
||||||
return fn.apply(context, arguments);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* let a boolean value also be a function that must return a boolean
|
|
||||||
* this first item in args will be used as the context
|
|
||||||
* @param {Boolean|Function} val
|
|
||||||
* @param {Array} [args]
|
|
||||||
* @returns {Boolean}
|
|
||||||
*/
|
|
||||||
function boolOrFn(val, args) {
|
|
||||||
if (typeof val == TYPE_FUNCTION) {
|
|
||||||
return val.apply(args ? args[0] || undefined : undefined, args);
|
|
||||||
}
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* use the val2 when val1 is undefined
|
|
||||||
* @param {*} val1
|
|
||||||
* @param {*} val2
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
function ifUndefined(val1, val2) {
|
|
||||||
return (val1 === undefined) ? val2 : val1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* addEventListener with multiple events at once
|
|
||||||
* @param {EventTarget} target
|
|
||||||
* @param {String} types
|
|
||||||
* @param {Function} handler
|
|
||||||
*/
|
|
||||||
function addEventListeners(target, types, handler) {
|
|
||||||
each(splitStr(types), function(type) {
|
|
||||||
target.addEventListener(type, handler, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* removeEventListener with multiple events at once
|
|
||||||
* @param {EventTarget} target
|
|
||||||
* @param {String} types
|
|
||||||
* @param {Function} handler
|
|
||||||
*/
|
|
||||||
function removeEventListeners(target, types, handler) {
|
|
||||||
each(splitStr(types), function(type) {
|
|
||||||
target.removeEventListener(type, handler, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* find if a node is in the given parent
|
|
||||||
* @method hasParent
|
|
||||||
* @param {HTMLElement} node
|
|
||||||
* @param {HTMLElement} parent
|
|
||||||
* @return {Boolean} found
|
|
||||||
*/
|
|
||||||
function hasParent(node, parent) {
|
|
||||||
while (node) {
|
|
||||||
if (node == parent) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
node = node.parentNode;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* small indexOf wrapper
|
|
||||||
* @param {String} str
|
|
||||||
* @param {String} find
|
|
||||||
* @returns {Boolean} found
|
|
||||||
*/
|
|
||||||
function inStr(str, find) {
|
|
||||||
return str.indexOf(find) > -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* split string on whitespace
|
|
||||||
* @param {String} str
|
|
||||||
* @returns {Array} words
|
|
||||||
*/
|
|
||||||
function splitStr(str) {
|
|
||||||
return str.trim().split(/\s+/g);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* find if a array contains the object using indexOf or a simple polyFill
|
|
||||||
* @param {Array} src
|
|
||||||
* @param {String} find
|
|
||||||
* @param {String} [findByKey]
|
|
||||||
* @return {Boolean|Number} false when not found, or the index
|
|
||||||
*/
|
|
||||||
function inArray(src, find, findByKey) {
|
|
||||||
if (src.indexOf && !findByKey) {
|
|
||||||
return src.indexOf(find);
|
|
||||||
} else {
|
|
||||||
var i = 0;
|
|
||||||
while (i < src.length) {
|
|
||||||
if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* convert array-like objects to real arrays
|
|
||||||
* @param {Object} obj
|
|
||||||
* @returns {Array}
|
|
||||||
*/
|
|
||||||
function toArray(obj) {
|
|
||||||
return Array.prototype.slice.call(obj, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* unique array with objects based on a key (like 'id') or just by the array's value
|
|
||||||
* @param {Array} src [{id:1},{id:2},{id:1}]
|
|
||||||
* @param {String} [key]
|
|
||||||
* @param {Boolean} [sort=False]
|
|
||||||
* @returns {Array} [{id:1},{id:2}]
|
|
||||||
*/
|
|
||||||
function uniqueArray(src, key, sort) {
|
|
||||||
var results = [];
|
|
||||||
var values = [];
|
|
||||||
var i = 0;
|
|
||||||
|
|
||||||
while (i < src.length) {
|
|
||||||
var val = key ? src[i][key] : src[i];
|
|
||||||
if (inArray(values, val) < 0) {
|
|
||||||
results.push(src[i]);
|
|
||||||
}
|
|
||||||
values[i] = val;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sort) {
|
|
||||||
if (!key) {
|
|
||||||
results = results.sort();
|
|
||||||
} else {
|
|
||||||
results = results.sort(function sortUniqueArray(a, b) {
|
|
||||||
return a[key] > b[key];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get the prefixed property
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {String} property
|
|
||||||
* @returns {String|Undefined} prefixed
|
|
||||||
*/
|
|
||||||
function prefixed(obj, property) {
|
|
||||||
var prefix, prop;
|
|
||||||
var camelProp = property[0].toUpperCase() + property.slice(1);
|
|
||||||
|
|
||||||
var i = 0;
|
|
||||||
while (i < VENDOR_PREFIXES.length) {
|
|
||||||
prefix = VENDOR_PREFIXES[i];
|
|
||||||
prop = (prefix) ? prefix + camelProp : property;
|
|
||||||
|
|
||||||
if (prop in obj) {
|
|
||||||
return prop;
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get a unique id
|
|
||||||
* @returns {number} uniqueId
|
|
||||||
*/
|
|
||||||
var _uniqueId = 1;
|
|
||||||
function uniqueId() {
|
|
||||||
return _uniqueId++;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get the window object of an element
|
|
||||||
* @param {HTMLElement} element
|
|
||||||
* @returns {DocumentView|Window}
|
|
||||||
*/
|
|
||||||
function getWindowForElement(element) {
|
|
||||||
var doc = element.ownerDocument || element;
|
|
||||||
return (doc.defaultView || doc.parentWindow || window);
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
@import url(http://fonts.googleapis.com/css?family=Open+Sans);
|
|
||||||
|
|
||||||
*, *:after, *:before {
|
|
||||||
box-sizing: border-box;
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
background: #eee;
|
|
||||||
font: 13px/1.5em 'Open Sans', Helvetica, Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #4986e7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg1, .green { background: #42d692; }
|
|
||||||
.bg2, .blue { background: #4986e7; }
|
|
||||||
.bg3, .red { background: #d06b64; }
|
|
||||||
.bg4, .purple { background: #cd74e6; }
|
|
||||||
.bg5, .azure { background: #9fe1e7; }
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
background: #fff;
|
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear { clear: both; }
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
|
||||||
<title></title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
Open the inspector and play a bit with the touchAction property.
|
|
||||||
<script src="../../hammer.min.js"></script>
|
|
||||||
<script>
|
|
||||||
var mc = new Hammer(document.body);
|
|
||||||
mc.add(new Hammer.Swipe({ direction: Hammer.DIRECTION_HORIZONTAL }));
|
|
||||||
mc.add(new Hammer.Pan({ direction: Hammer.DIRECTION_HORIZONTAL }));
|
|
||||||
|
|
||||||
console.log(document.body.style.touchAction)
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
|
|
||||||
<meta name="msapplication-tap-highlight" content="no"/>
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
<title>Hammer.js</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div id="hit" class="bg1" style="padding: 30px; height: 200px;">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<pre id="debug" style="overflow:hidden; background: #eee; padding: 15px;"></pre>
|
|
||||||
|
|
||||||
<pre id="log" style="overflow:hidden;"></pre>
|
|
||||||
</div>
|
|
||||||
<script src="../../hammer.js"></script>
|
|
||||||
<script>
|
|
||||||
|
|
||||||
Object.prototype.toDirString = function() {
|
|
||||||
var output = [];
|
|
||||||
for(var key in this) {
|
|
||||||
if(this.hasOwnProperty(key)) {
|
|
||||||
var value = this[key];
|
|
||||||
if(Array.isArray(value)) {
|
|
||||||
value = "Array("+ value.length +"):"+ value;
|
|
||||||
} else if(value instanceof HTMLElement) {
|
|
||||||
value = value +" ("+ value.outerHTML.substring(0, 50) +"...)";
|
|
||||||
}
|
|
||||||
output.push(key +": "+ value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return output.join("\n")
|
|
||||||
};
|
|
||||||
|
|
||||||
var el = document.querySelector("#hit");
|
|
||||||
var log = document.querySelector("#log");
|
|
||||||
var debug = document.querySelector("#debug");
|
|
||||||
|
|
||||||
var mc = new Hammer(el);
|
|
||||||
mc.get('pinch').set({ enable: true });
|
|
||||||
|
|
||||||
mc.on("hammer.input", function(ev) {
|
|
||||||
debug.innerHTML = [ev.srcEvent.type, ev.pointers.length, ev.isFinal, ev.deltaX, ev.deltaY].join("<br>");
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
<title>Hammer.js</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
|
|
||||||
<div id="hit" class="bg1" style="padding: 30px;">
|
|
||||||
<span id="target" class="bg5" style="display: block; height: 100px;"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<pre id="debug" style="overflow:hidden; background: #eee; padding: 15px;"></pre>
|
|
||||||
|
|
||||||
<pre id="log" style="overflow:hidden;"></pre>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<script src="../../hammer.min.js"></script>
|
|
||||||
<script>
|
|
||||||
|
|
||||||
Object.prototype.toDirString = function() {
|
|
||||||
var output = [];
|
|
||||||
for(var key in this) {
|
|
||||||
if(this.hasOwnProperty(key)) {
|
|
||||||
var value = this[key];
|
|
||||||
if(Array.isArray(value)) {
|
|
||||||
value = "Array("+ value.length +"):"+ value;
|
|
||||||
} else if(value instanceof HTMLElement) {
|
|
||||||
value = value +" ("+ value.outerHTML.substring(0, 50) +"...)";
|
|
||||||
}
|
|
||||||
output.push(key +": "+ value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return output.join("\n")
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
var el = document.querySelector("#hit");
|
|
||||||
var log = document.querySelector("#log");
|
|
||||||
var debug = document.querySelector("#debug");
|
|
||||||
|
|
||||||
var mc = new Hammer(el);
|
|
||||||
mc.get('pinch').set({ enable: true });
|
|
||||||
mc.get('rotate').set({ enable: true });
|
|
||||||
mc.on("swipe pan panstart panmove panend pancancel multipan press pressup pinch rotate tap doubletap",
|
|
||||||
logGesture);
|
|
||||||
|
|
||||||
function DEBUG(str) {
|
|
||||||
debug.textContent = str;
|
|
||||||
}
|
|
||||||
function logGesture(ev) {
|
|
||||||
console.log(ev.timeStamp, ev.type, ev);
|
|
||||||
log.textContent = ev.toDirString();
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
<title>Hammer.js</title>
|
|
||||||
<style>
|
|
||||||
|
|
||||||
#right,
|
|
||||||
#left {
|
|
||||||
display: block;
|
|
||||||
width: 50%;
|
|
||||||
height: 500px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#left { float: left; }
|
|
||||||
#right { float: right; }
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<pre id="left" class="bg1"></pre>
|
|
||||||
<pre id="right" class="bg5"></pre>
|
|
||||||
|
|
||||||
<div class="clear"></div>
|
|
||||||
|
|
||||||
<h1>Multiple instances the same time</h1>
|
|
||||||
<p>You can run multiple instances of Hammer on your page and they will recognize each completely isolated
|
|
||||||
from each other. This makes it possible to build multi-user interfaces.</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<script src="../../hammer.min.js"></script>
|
|
||||||
<script>
|
|
||||||
|
|
||||||
Object.prototype.toDirString = function() {
|
|
||||||
var output = [];
|
|
||||||
for(var key in this) {
|
|
||||||
if(this.hasOwnProperty(key)) {
|
|
||||||
var value = this[key];
|
|
||||||
if(Array.isArray(value)) {
|
|
||||||
value = "Array("+ value.length +"):"+ value;
|
|
||||||
} else if(value instanceof HTMLElement) {
|
|
||||||
value = value +" ("+ value.outerHTML.substring(0, 50) +"...)";
|
|
||||||
}
|
|
||||||
output.push(key +": "+ value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return output.join("\n")
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
function addHammer(el) {
|
|
||||||
var mc = new Hammer(el, { multiUser: true });
|
|
||||||
mc.get('pan').set({ direction: Hammer.DIRECTION_ALL });
|
|
||||||
mc.get('swipe').set({ direction: Hammer.DIRECTION_ALL });
|
|
||||||
mc.get('pinch').set({ enable: true });
|
|
||||||
mc.get('rotate').set({ enable: true });
|
|
||||||
|
|
||||||
mc.on("swipe pan press pinch rotate tap doubletap", function (ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
el.innerText = ev.toDirString();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addHammer(document.querySelector("#left"));
|
|
||||||
addHammer(document.querySelector("#right"));
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,217 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head lang="en">
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
|
|
||||||
<meta name="msapplication-tap-highlight" content="no"/>
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
<title>Hammer.js</title>
|
|
||||||
<style>
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panes.wrapper {
|
|
||||||
max-height: 400px;
|
|
||||||
max-width: 800px;
|
|
||||||
background: #666;
|
|
||||||
margin: 40px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panes {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pane {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
text-align: center;
|
|
||||||
font: bold 60px/250px 'Open Sans', Helvetica, Arial, sans-serif;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panes.animate > .pane {
|
|
||||||
transition: all .3s;
|
|
||||||
-webkit-transition: all .3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="panes wrapper">
|
|
||||||
<div class="pane bg1">
|
|
||||||
<div class="panes">
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,0);">1.1</div>
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,.2);">1.2</div>
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,.4);">1.3</div>
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,.6);">1.4</div>
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,.8);">1.5</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pane bg2">
|
|
||||||
<div class="panes">
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,0);">2.1</div>
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,.2);">2.2</div>
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,.4);">2.3</div>
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,.6);">2.4</div>
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,.8);">2.5</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pane bg3">
|
|
||||||
<div class="panes">
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,0);">3.1</div>
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,.2);">3.2</div>
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,.4);">3.3</div>
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,.6);">3.4</div>
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,.8);">3.5</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pane bg4">
|
|
||||||
<div class="panes">
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,0);">4.1</div>
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,.2);">4.2</div>
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,.4);">4.3</div>
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,.6);">4.4</div>
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,.8);">4.5</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pane bg5">
|
|
||||||
<div class="panes">
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,0);">5.1</div>
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,.2);">5.2</div>
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,.4);">5.3</div>
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,.6);">5.4</div>
|
|
||||||
<div class="pane" style="background: rgba(0,0,0,.8);">5.5</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<h1>Nested Pan recognizers</h1>
|
|
||||||
|
|
||||||
<p>Nested recognizers are possible with some threshold and with use of <code>requireFailure()</code>.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="../../hammer.js"></script>
|
|
||||||
<script>
|
|
||||||
|
|
||||||
var reqAnimationFrame = (function() {
|
|
||||||
return window[Hammer.prefixed(window, "requestAnimationFrame")] || function(callback) {
|
|
||||||
setTimeout(callback, 1000 / 60);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
function dirProp(direction, hProp, vProp) {
|
|
||||||
return (direction & Hammer.DIRECTION_HORIZONTAL) ? hProp : vProp
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Carousel
|
|
||||||
* @param container
|
|
||||||
* @param direction
|
|
||||||
* @constructor
|
|
||||||
*/
|
|
||||||
function HammerCarousel(container, direction) {
|
|
||||||
this.container = container;
|
|
||||||
this.direction = direction;
|
|
||||||
|
|
||||||
this.panes = Array.prototype.slice.call(this.container.children, 0);
|
|
||||||
this.containerSize = this.container[dirProp(direction, 'offsetWidth', 'offsetHeight')];
|
|
||||||
|
|
||||||
this.currentIndex = 0;
|
|
||||||
|
|
||||||
this.hammer = new Hammer.Manager(this.container);
|
|
||||||
this.hammer.add(new Hammer.Pan({ direction: this.direction, threshold: 10 }));
|
|
||||||
this.hammer.on("panstart panmove panend pancancel", Hammer.bindFn(this.onPan, this));
|
|
||||||
|
|
||||||
this.show(this.currentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
HammerCarousel.prototype = {
|
|
||||||
/**
|
|
||||||
* show a pane
|
|
||||||
* @param {Number} showIndex
|
|
||||||
* @param {Number} [percent] percentage visible
|
|
||||||
* @param {Boolean} [animate]
|
|
||||||
*/
|
|
||||||
show: function(showIndex, percent, animate){
|
|
||||||
showIndex = Math.max(0, Math.min(showIndex, this.panes.length - 1));
|
|
||||||
percent = percent || 0;
|
|
||||||
|
|
||||||
var className = this.container.className;
|
|
||||||
if(animate) {
|
|
||||||
if(className.indexOf('animate') === -1) {
|
|
||||||
this.container.className += ' animate';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if(className.indexOf('animate') !== -1) {
|
|
||||||
this.container.className = className.replace('animate', '').trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var paneIndex, pos, translate;
|
|
||||||
for (paneIndex = 0; paneIndex < this.panes.length; paneIndex++) {
|
|
||||||
pos = (this.containerSize / 100) * (((paneIndex - showIndex) * 100) + percent);
|
|
||||||
if(this.direction & Hammer.DIRECTION_HORIZONTAL) {
|
|
||||||
translate = 'translate3d(' + pos + 'px, 0, 0)';
|
|
||||||
} else {
|
|
||||||
translate = 'translate3d(0, ' + pos + 'px, 0)'
|
|
||||||
}
|
|
||||||
this.panes[paneIndex].style.transform = translate;
|
|
||||||
this.panes[paneIndex].style.mozTransform = translate;
|
|
||||||
this.panes[paneIndex].style.webkitTransform = translate;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentIndex = showIndex;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* handle pan
|
|
||||||
* @param {Object} ev
|
|
||||||
*/
|
|
||||||
onPan : function (ev) {
|
|
||||||
var delta = dirProp(this.direction, ev.deltaX, ev.deltaY);
|
|
||||||
var percent = (100 / this.containerSize) * delta;
|
|
||||||
var animate = false;
|
|
||||||
|
|
||||||
if (ev.type == 'panend' || ev.type == 'pancancel') {
|
|
||||||
if (Math.abs(percent) > 20 && ev.type == 'panend') {
|
|
||||||
this.currentIndex += (percent < 0) ? 1 : -1;
|
|
||||||
}
|
|
||||||
percent = 0;
|
|
||||||
animate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.show(this.currentIndex, percent, animate);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// the horizontal pane scroller
|
|
||||||
var outer = new HammerCarousel(document.querySelector(".panes.wrapper"), Hammer.DIRECTION_HORIZONTAL);
|
|
||||||
|
|
||||||
// each pane should contain a vertical pane scroller
|
|
||||||
Hammer.each(document.querySelectorAll(".pane .panes"), function(container) {
|
|
||||||
// setup the inner scroller
|
|
||||||
var inner = new HammerCarousel(container, Hammer.DIRECTION_VERTICAL);
|
|
||||||
|
|
||||||
// only recognize the inner pan when the outer is failing.
|
|
||||||
// they both have a threshold of some px
|
|
||||||
outer.hammer.get('pan').requireFailure(inner.hammer.get('pan'));
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head lang="en">
|
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
<title>Hammer.js</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<div id="maps" style="height: 500px; margin-bottom: 20px;"></div>
|
|
||||||
|
|
||||||
<h1>Gestures simulator</h1>
|
|
||||||
<p>Used for unit-testing Hammer.js. To test it on the Google Maps view, you should open your
|
|
||||||
<a href="https://developer.chrome.com/devtools/docs/mobile-emulation#emulate-touch-events">
|
|
||||||
Inspector and emulate a touch-screen.</a>
|
|
||||||
Or just open it on your touch-device.</p>
|
|
||||||
<p>Currently, it only triggers touchEvents.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="../../node_modules/hammer-simulator/index.js"></script>
|
|
||||||
<script src="https://maps.googleapis.com/maps/api/js?v=3.exp"></script>
|
|
||||||
<script>
|
|
||||||
|
|
||||||
Simulator.events.touch.fakeSupport();
|
|
||||||
|
|
||||||
var el, map;
|
|
||||||
var container = document.getElementById('maps');
|
|
||||||
var program = [
|
|
||||||
['pan', ['deltaX','deltaY']],
|
|
||||||
['pinch', ['scale']],
|
|
||||||
['tap', ['pos']],
|
|
||||||
['swipe', ['deltaX','deltaY']],
|
|
||||||
['pinch', ['pos','scale']],
|
|
||||||
['pan', ['pos']],
|
|
||||||
['rotate', ['pos','rotation']],
|
|
||||||
['doubleTap', ['pos']],
|
|
||||||
['pinchRotate', ['pos','scale','rotation']],
|
|
||||||
];
|
|
||||||
|
|
||||||
function randomRange(min, max) {
|
|
||||||
return Math.random() * (max - min) + min;
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomRangeInt(min, max) {
|
|
||||||
return Math.round(randomRange(min, max));
|
|
||||||
}
|
|
||||||
|
|
||||||
function gestureOption(name) {
|
|
||||||
var max = map.getDiv().offsetWidth * .9;
|
|
||||||
switch(name) {
|
|
||||||
case 'deltaY':
|
|
||||||
case 'deltaX':
|
|
||||||
return randomRangeInt(10, max * .5);
|
|
||||||
case 'pos':
|
|
||||||
return [randomRangeInt(10, max), randomRangeInt(10, max)];
|
|
||||||
case 'scale':
|
|
||||||
return randomRange(.2, 2);
|
|
||||||
case 'rotation':
|
|
||||||
return randomRange(-180, 180);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function walkProgram(done) {
|
|
||||||
var clone = [].concat(program);
|
|
||||||
(function next() {
|
|
||||||
if(clone.length) {
|
|
||||||
var entry = clone.shift();
|
|
||||||
var options = {};
|
|
||||||
for(var i=0; i<entry[1].length; i++) {
|
|
||||||
options[entry[1][i]] = gestureOption(entry[1][i]);
|
|
||||||
}
|
|
||||||
Simulator.gestures[entry[0]](el, options, next);
|
|
||||||
} else {
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
}());
|
|
||||||
}
|
|
||||||
|
|
||||||
function startSimulator() {
|
|
||||||
walkProgram(startSimulator);
|
|
||||||
}
|
|
||||||
|
|
||||||
(function setupGoogleMaps() {
|
|
||||||
map = new google.maps.Map(container, {
|
|
||||||
zoom: 14,
|
|
||||||
center: new google.maps.LatLng(51.98, 5.91)
|
|
||||||
});
|
|
||||||
|
|
||||||
// collect the target element
|
|
||||||
google.maps.event.addListenerOnce(map, 'idle', function(){
|
|
||||||
el = container.childNodes[0].childNodes[0];
|
|
||||||
startSimulator();
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head lang="en">
|
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
<title>Hammer.js</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
|
|
||||||
<div id="hit" class="bg4" style="padding: 30px; height: 300px; position: relative;">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<pre id="debug" style="overflow:hidden; background: #eee; padding: 15px;"></pre>
|
|
||||||
|
|
||||||
<pre id="log" style="overflow:hidden;"></pre>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="../../node_modules/hammer-simulator/index.js"></script>
|
|
||||||
<script src="../../hammer.js"></script>
|
|
||||||
<script>
|
|
||||||
|
|
||||||
var program = [
|
|
||||||
['pan', ['deltaX','deltaY']],
|
|
||||||
['pinch', ['scale']],
|
|
||||||
['tap', ['pos']],
|
|
||||||
['swipe', ['deltaX','deltaY']],
|
|
||||||
['pinch', ['pos','scale']],
|
|
||||||
['pan', ['pos']],
|
|
||||||
['rotate', ['pos','rotation']],
|
|
||||||
['doubleTap', ['pos']],
|
|
||||||
['pinchRotate', ['pos','scale','rotation']],
|
|
||||||
];
|
|
||||||
|
|
||||||
function randomRange(min, max) {
|
|
||||||
return Math.random() * (max - min) + min;
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomRangeInt(min, max) {
|
|
||||||
return Math.round(randomRange(min, max));
|
|
||||||
}
|
|
||||||
|
|
||||||
function gestureOption(name) {
|
|
||||||
var max = el.offsetWidth * .9;
|
|
||||||
switch(name) {
|
|
||||||
case 'deltaY':
|
|
||||||
case 'deltaX':
|
|
||||||
return randomRangeInt(10, max * .5);
|
|
||||||
case 'pos':
|
|
||||||
return [randomRangeInt(10, max), randomRangeInt(10, max)];
|
|
||||||
case 'scale':
|
|
||||||
return randomRange(.2, 2);
|
|
||||||
case 'rotation':
|
|
||||||
return randomRange(-180, 180);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function walkProgram(done) {
|
|
||||||
var clone = [].concat(program);
|
|
||||||
(function next() {
|
|
||||||
if(clone.length) {
|
|
||||||
var entry = clone.shift();
|
|
||||||
var options = {};
|
|
||||||
for(var i=0; i<entry[1].length; i++) {
|
|
||||||
options[entry[1][i]] = gestureOption(entry[1][i]);
|
|
||||||
}
|
|
||||||
setTimeout(function() {
|
|
||||||
log.innerHTML = '';
|
|
||||||
Simulator.gestures[entry[0]](el, options, next);
|
|
||||||
}, 1000);
|
|
||||||
} else {
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
}());
|
|
||||||
}
|
|
||||||
|
|
||||||
function startSimulator() {
|
|
||||||
walkProgram(startSimulator);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var el = document.querySelector("#hit");
|
|
||||||
var log = document.querySelector("#log");
|
|
||||||
var debug = document.querySelector("#debug");
|
|
||||||
|
|
||||||
var mc = new Hammer(el);
|
|
||||||
mc.get('pinch').set({ enable: true, threshold:.1 });
|
|
||||||
mc.get('rotate').set({ enable: true });
|
|
||||||
mc.on("swipe pan multipan press pinch rotate tap doubletap", logGesture);
|
|
||||||
|
|
||||||
function logGesture(ev) {
|
|
||||||
log.textContent = ev.toDirString();
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.prototype.toDirString = function() {
|
|
||||||
var output = [];
|
|
||||||
for(var key in this) {
|
|
||||||
if(this.hasOwnProperty(key)) {
|
|
||||||
var value = this[key];
|
|
||||||
if(Array.isArray(value)) {
|
|
||||||
value = "Array("+ value.length +"):"+ value;
|
|
||||||
} else if(value instanceof HTMLElement) {
|
|
||||||
value = value +" ("+ value.outerHTML.substring(0, 50) +"...)";
|
|
||||||
}
|
|
||||||
output.push(key +": "+ value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return output.join("\n")
|
|
||||||
};
|
|
||||||
|
|
||||||
startSimulator();
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width">
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
<title>Hammer.js</title>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.tester {
|
|
||||||
margin: 20px 0;
|
|
||||||
padding: 10px;
|
|
||||||
height: 200px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-space {
|
|
||||||
height: 9000px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#native, #no-native {
|
|
||||||
color: #fff;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.1em;
|
|
||||||
padding: 10px 20px;
|
|
||||||
display: none;
|
|
||||||
margin: 25px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.show {
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<p>Hammer provides a <a href="../../src/touchaction.js">kind of polyfill</a>
|
|
||||||
for the browsers that don't support the <a href="http://www.w3.org/TR/pointerevents/#the-touch-action-css-property">touch-action</a> property.</p>
|
|
||||||
|
|
||||||
<div id="native" class="green">Your browser has support for the touch-action property!</div>
|
|
||||||
<div id="no-native" class="red">Your browser doesn't support the touch-action property,
|
|
||||||
so we use the polyfill.</div>
|
|
||||||
|
|
||||||
<h2>touch-action: auto</h2>
|
|
||||||
<p>Should prevent nothing.</p>
|
|
||||||
<div class="tester azure" id="auto"></div>
|
|
||||||
|
|
||||||
<h2>touch-action: pan-y</h2>
|
|
||||||
<p>Should prevent scrolling on horizontal movement. This is set by default when creating a Hammer instance.</p>
|
|
||||||
<div class="tester azure" id="pan-y"></div>
|
|
||||||
|
|
||||||
<h2>touch-action: pan-x</h2>
|
|
||||||
<p>Should prevent scrolling on vertical movement.</p>
|
|
||||||
<div class="tester azure" id="pan-x"></div>
|
|
||||||
|
|
||||||
<h2>touch-action: pan-x pan-y</h2>
|
|
||||||
<p>Should <strong>not</strong> prevent any scrolling on any movement. Horizontal and vertical scrolling handled by the browser directly.</p>
|
|
||||||
<div class="tester azure" id="pan-x-pan-y"></div>
|
|
||||||
|
|
||||||
<h2>touch-action: none</h2>
|
|
||||||
<p>Should prevent all.</p>
|
|
||||||
<div class="tester azure" id="none"></div>
|
|
||||||
</div>
|
|
||||||
<script src="../../hammer.js"></script>
|
|
||||||
<script>
|
|
||||||
var support = Hammer.prefixed(document.body.style, 'touchAction');
|
|
||||||
document.getElementById(support ? 'native' : 'no-native').className += ' show';
|
|
||||||
|
|
||||||
var touchActions = ['auto', 'pan-y', 'pan-x', 'pan-x pan-y', 'none'];
|
|
||||||
Hammer.each(touchActions, function(touchAction) {
|
|
||||||
var el = document.getElementById(touchAction.replace(" ", "-"));
|
|
||||||
|
|
||||||
var mc = Hammer(el, {
|
|
||||||
touchAction: touchAction
|
|
||||||
});
|
|
||||||
mc.get('pan').set({ direction: Hammer.DIRECTION_ALL });
|
|
||||||
mc.get('pinch').set({ enable: true });
|
|
||||||
mc.get('rotate').set({ enable: true });
|
|
||||||
|
|
||||||
mc.on("pan swipe rotate pinch tap doubletap press", function(ev) {
|
|
||||||
el.textContent = ev.type +" "+ el.textContent;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="scroll-space"></div>
|
|
||||||
<p>hi.</p>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,211 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
|
|
||||||
<link rel="stylesheet" href="assets/style.css">
|
|
||||||
<title>Hammer.js</title>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
-webkit-perspective: 500;
|
|
||||||
-moz-perspective: 500;
|
|
||||||
perspective: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate {
|
|
||||||
-webkit-transition: all .3s;
|
|
||||||
-moz-transition: all .3s;
|
|
||||||
transition: all .3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#hit {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#log {
|
|
||||||
position: absolute;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div id="log"></div>
|
|
||||||
<div id="hit" style="background: #42d692; width: 150px; height: 150px;"></div>
|
|
||||||
|
|
||||||
|
|
||||||
<script src="../../hammer.js"></script>
|
|
||||||
<script>
|
|
||||||
|
|
||||||
var reqAnimationFrame = (function () {
|
|
||||||
return window[Hammer.prefixed(window, 'requestAnimationFrame')] || function (callback) {
|
|
||||||
window.setTimeout(callback, 1000 / 60);
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
var log = document.querySelector("#log");
|
|
||||||
var el = document.querySelector("#hit");
|
|
||||||
|
|
||||||
var START_X = Math.round((window.innerWidth - el.offsetWidth) / 2);
|
|
||||||
var START_Y = Math.round((window.innerHeight - el.offsetHeight) / 2);
|
|
||||||
|
|
||||||
var ticking = false;
|
|
||||||
var transform;
|
|
||||||
var timer;
|
|
||||||
|
|
||||||
var mc = new Hammer.Manager(el);
|
|
||||||
|
|
||||||
mc.add(new Hammer.Pan({ threshold: 0, pointers: 0 }));
|
|
||||||
|
|
||||||
mc.add(new Hammer.Swipe()).recognizeWith(mc.get('pan'));
|
|
||||||
mc.add(new Hammer.Rotate({ threshold: 0 })).recognizeWith(mc.get('pan'));
|
|
||||||
mc.add(new Hammer.Pinch({ threshold: 0 })).recognizeWith([mc.get('pan'), mc.get('rotate')]);
|
|
||||||
|
|
||||||
mc.add(new Hammer.Tap({ event: 'doubletap', taps: 2 }));
|
|
||||||
mc.add(new Hammer.Tap());
|
|
||||||
|
|
||||||
mc.on("panstart panmove", onPan);
|
|
||||||
mc.on("rotatestart rotatemove", onRotate);
|
|
||||||
mc.on("pinchstart pinchmove", onPinch);
|
|
||||||
mc.on("swipe", onSwipe);
|
|
||||||
mc.on("tap", onTap);
|
|
||||||
mc.on("doubletap", onDoubleTap);
|
|
||||||
|
|
||||||
mc.on("hammer.input", function(ev) {
|
|
||||||
if(ev.isFinal) {
|
|
||||||
resetElement();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
function resetElement() {
|
|
||||||
el.className = 'animate';
|
|
||||||
transform = {
|
|
||||||
translate: { x: START_X, y: START_Y },
|
|
||||||
scale: 1,
|
|
||||||
angle: 0,
|
|
||||||
rx: 0,
|
|
||||||
ry: 0,
|
|
||||||
rz: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
requestElementUpdate();
|
|
||||||
|
|
||||||
if (log.textContent.length > 2000) {
|
|
||||||
log.textContent = log.textContent.substring(0, 2000) + "...";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateElementTransform() {
|
|
||||||
var value = [
|
|
||||||
'translate3d(' + transform.translate.x + 'px, ' + transform.translate.y + 'px, 0)',
|
|
||||||
'scale(' + transform.scale + ', ' + transform.scale + ')',
|
|
||||||
'rotate3d('+ transform.rx +','+ transform.ry +','+ transform.rz +','+ transform.angle + 'deg)'
|
|
||||||
];
|
|
||||||
|
|
||||||
value = value.join(" ");
|
|
||||||
el.textContent = value;
|
|
||||||
el.style.webkitTransform = value;
|
|
||||||
el.style.mozTransform = value;
|
|
||||||
el.style.transform = value;
|
|
||||||
ticking = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestElementUpdate() {
|
|
||||||
if(!ticking) {
|
|
||||||
reqAnimationFrame(updateElementTransform);
|
|
||||||
ticking = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function logEvent(str) {
|
|
||||||
//log.insertBefore(document.createTextNode(str +"\n"), log.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPan(ev) {
|
|
||||||
el.className = '';
|
|
||||||
transform.translate = {
|
|
||||||
x: START_X + ev.deltaX,
|
|
||||||
y: START_Y + ev.deltaY
|
|
||||||
};
|
|
||||||
|
|
||||||
requestElementUpdate();
|
|
||||||
logEvent(ev.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
var initScale = 1;
|
|
||||||
function onPinch(ev) {
|
|
||||||
if(ev.type == 'pinchstart') {
|
|
||||||
initScale = transform.scale || 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
el.className = '';
|
|
||||||
transform.scale = initScale * ev.scale;
|
|
||||||
|
|
||||||
requestElementUpdate();
|
|
||||||
logEvent(ev.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
var initAngle = 0;
|
|
||||||
function onRotate(ev) {
|
|
||||||
if(ev.type == 'rotatestart') {
|
|
||||||
initAngle = transform.angle || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
el.className = '';
|
|
||||||
transform.rz = 1;
|
|
||||||
transform.angle = initAngle + ev.rotation;
|
|
||||||
requestElementUpdate();
|
|
||||||
logEvent(ev.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSwipe(ev) {
|
|
||||||
var angle = 50;
|
|
||||||
transform.ry = (ev.direction & Hammer.DIRECTION_HORIZONTAL) ? 1 : 0;
|
|
||||||
transform.rx = (ev.direction & Hammer.DIRECTION_VERTICAL) ? 1 : 0;
|
|
||||||
transform.angle = (ev.direction & (Hammer.DIRECTION_RIGHT | Hammer.DIRECTION_UP)) ? angle : -angle;
|
|
||||||
|
|
||||||
clearTimeout(timer);
|
|
||||||
timer = setTimeout(function () {
|
|
||||||
resetElement();
|
|
||||||
}, 300);
|
|
||||||
requestElementUpdate();
|
|
||||||
logEvent(ev.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onTap(ev) {
|
|
||||||
transform.rx = 1;
|
|
||||||
transform.angle = 25;
|
|
||||||
|
|
||||||
clearTimeout(timer);
|
|
||||||
timer = setTimeout(function () {
|
|
||||||
resetElement();
|
|
||||||
}, 200);
|
|
||||||
requestElementUpdate();
|
|
||||||
logEvent(ev.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDoubleTap(ev) {
|
|
||||||
transform.rx = 1;
|
|
||||||
transform.angle = 80;
|
|
||||||
|
|
||||||
clearTimeout(timer);
|
|
||||||
timer = setTimeout(function () {
|
|
||||||
resetElement();
|
|
||||||
}, 500);
|
|
||||||
requestElementUpdate();
|
|
||||||
logEvent(ev.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
resetElement();
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
|
@ -1,237 +0,0 @@
|
||||||
/*!
|
|
||||||
* QUnit 1.14.0
|
|
||||||
* http://qunitjs.com/
|
|
||||||
*
|
|
||||||
* Copyright 2013 jQuery Foundation and other contributors
|
|
||||||
* Released under the MIT license
|
|
||||||
* http://jquery.org/license
|
|
||||||
*
|
|
||||||
* Date: 2014-01-31T16:40Z
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** Font Family and Sizes */
|
|
||||||
|
|
||||||
#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
|
|
||||||
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
|
|
||||||
#qunit-tests { font-size: smaller; }
|
|
||||||
|
|
||||||
|
|
||||||
/** Resets */
|
|
||||||
|
|
||||||
#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** Header */
|
|
||||||
|
|
||||||
#qunit-header {
|
|
||||||
padding: 0.5em 0 0.5em 1em;
|
|
||||||
|
|
||||||
color: #8699A4;
|
|
||||||
background-color: #0D3349;
|
|
||||||
|
|
||||||
font-size: 1.5em;
|
|
||||||
line-height: 1em;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
border-radius: 5px 5px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-header a {
|
|
||||||
text-decoration: none;
|
|
||||||
color: #C2CCD1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-header a:hover,
|
|
||||||
#qunit-header a:focus {
|
|
||||||
color: #FFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-testrunner-toolbar label {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0 0.5em 0 0.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-banner {
|
|
||||||
height: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-testrunner-toolbar {
|
|
||||||
padding: 0.5em 0 0.5em 2em;
|
|
||||||
color: #5E740B;
|
|
||||||
background-color: #EEE;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-userAgent {
|
|
||||||
padding: 0.5em 0 0.5em 2.5em;
|
|
||||||
background-color: #2B81AF;
|
|
||||||
color: #FFF;
|
|
||||||
text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-modulefilter-container {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Tests: Pass/Fail */
|
|
||||||
|
|
||||||
#qunit-tests {
|
|
||||||
list-style-position: inside;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-tests li {
|
|
||||||
padding: 0.4em 0.5em 0.4em 2.5em;
|
|
||||||
border-bottom: 1px solid #FFF;
|
|
||||||
list-style-position: inside;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-tests li strong {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-tests li a {
|
|
||||||
padding: 0.5em;
|
|
||||||
color: #C2CCD1;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
#qunit-tests li a:hover,
|
|
||||||
#qunit-tests li a:focus {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-tests li .runtime {
|
|
||||||
float: right;
|
|
||||||
font-size: smaller;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qunit-assert-list {
|
|
||||||
margin-top: 0.5em;
|
|
||||||
padding: 0.5em;
|
|
||||||
|
|
||||||
background-color: #FFF;
|
|
||||||
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qunit-collapsed {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-tests table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin-top: 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-tests th {
|
|
||||||
text-align: right;
|
|
||||||
vertical-align: top;
|
|
||||||
padding: 0 0.5em 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-tests td {
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-tests pre {
|
|
||||||
margin: 0;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-tests del {
|
|
||||||
background-color: #E0F2BE;
|
|
||||||
color: #374E0C;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-tests ins {
|
|
||||||
background-color: #FFCACA;
|
|
||||||
color: #500;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** Test Counts */
|
|
||||||
|
|
||||||
#qunit-tests b.counts { color: #000; }
|
|
||||||
#qunit-tests b.passed { color: #5E740B; }
|
|
||||||
#qunit-tests b.failed { color: #710909; }
|
|
||||||
|
|
||||||
#qunit-tests li li {
|
|
||||||
padding: 5px;
|
|
||||||
background-color: #FFF;
|
|
||||||
border-bottom: none;
|
|
||||||
list-style-position: inside;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** Passing Styles */
|
|
||||||
|
|
||||||
#qunit-tests li li.pass {
|
|
||||||
color: #3C510C;
|
|
||||||
background-color: #FFF;
|
|
||||||
border-left: 10px solid #C6E746;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
|
|
||||||
#qunit-tests .pass .test-name { color: #366097; }
|
|
||||||
|
|
||||||
#qunit-tests .pass .test-actual,
|
|
||||||
#qunit-tests .pass .test-expected { color: #999; }
|
|
||||||
|
|
||||||
#qunit-banner.qunit-pass { background-color: #C6E746; }
|
|
||||||
|
|
||||||
/*** Failing Styles */
|
|
||||||
|
|
||||||
#qunit-tests li li.fail {
|
|
||||||
color: #710909;
|
|
||||||
background-color: #FFF;
|
|
||||||
border-left: 10px solid #EE5757;
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-tests > li:last-child {
|
|
||||||
border-radius: 0 0 5px 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qunit-tests .fail { color: #000; background-color: #EE5757; }
|
|
||||||
#qunit-tests .fail .test-name,
|
|
||||||
#qunit-tests .fail .module-name { color: #000; }
|
|
||||||
|
|
||||||
#qunit-tests .fail .test-actual { color: #EE5757; }
|
|
||||||
#qunit-tests .fail .test-expected { color: #008000; }
|
|
||||||
|
|
||||||
#qunit-banner.qunit-fail { background-color: #EE5757; }
|
|
||||||
|
|
||||||
|
|
||||||
/** Result */
|
|
||||||
|
|
||||||
#qunit-testresult {
|
|
||||||
padding: 0.5em 0.5em 0.5em 2.5em;
|
|
||||||
|
|
||||||
color: #2B81AF;
|
|
||||||
background-color: #D2E0E6;
|
|
||||||
|
|
||||||
border-bottom: 1px solid #FFF;
|
|
||||||
}
|
|
||||||
#qunit-testresult .module-name {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Fixture */
|
|
||||||
|
|
||||||
#qunit-fixture {
|
|
||||||
position: absolute;
|
|
||||||
top: -10000px;
|
|
||||||
left: -10000px;
|
|
||||||
width: 1000px;
|
|
||||||
height: 1000px;
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,50 +0,0 @@
|
||||||
var utils = {
|
|
||||||
/**
|
|
||||||
* trigger simple dom event
|
|
||||||
* @param obj
|
|
||||||
* @param name
|
|
||||||
*/
|
|
||||||
triggerDomEvent: function(obj, name) {
|
|
||||||
var event = document.createEvent('Event');
|
|
||||||
event.initEvent(name, true, true);
|
|
||||||
obj.dispatchEvent(event);
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
createTouchEvent: function(name, x, y, identifier) {
|
|
||||||
var event = document.createEvent('Event');
|
|
||||||
event.initEvent('touch' + name, true, true);
|
|
||||||
|
|
||||||
event.touches = event.targetTouches = [{
|
|
||||||
clientX: x,
|
|
||||||
clientY: y,
|
|
||||||
identifier: identifier || 0
|
|
||||||
}];
|
|
||||||
|
|
||||||
//https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent.changedTouches
|
|
||||||
event.changedTouches = [{
|
|
||||||
clientX: x,
|
|
||||||
clientY: y,
|
|
||||||
identifier: identifier || 0
|
|
||||||
}];
|
|
||||||
|
|
||||||
return event;
|
|
||||||
},
|
|
||||||
|
|
||||||
dispatchTouchEvent: function(el, name, x, y, identifier) {
|
|
||||||
var event = utils.createTouchEvent(name, x, y, identifier);
|
|
||||||
el.dispatchEvent(event);
|
|
||||||
},
|
|
||||||
|
|
||||||
createHitArea: function(parent) {
|
|
||||||
if (parent == null) {
|
|
||||||
parent = document.getElementById('qunit-fixture')
|
|
||||||
}
|
|
||||||
var hitArea = document.createElement('div');
|
|
||||||
hitArea.style.background = '#eee';
|
|
||||||
hitArea.style.height = '300px';
|
|
||||||
|
|
||||||
parent.appendChild(hitArea);
|
|
||||||
return hitArea;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
var el,
|
|
||||||
hammer;
|
|
||||||
|
|
||||||
module('Pan Gesture', {
|
|
||||||
setup: function() {
|
|
||||||
el = document.createElement('div');
|
|
||||||
document.body.appendChild(el);
|
|
||||||
|
|
||||||
hammer = new Hammer(el, {recognizers: []});
|
|
||||||
},
|
|
||||||
teardown: function() {
|
|
||||||
document.body.removeChild(el);
|
|
||||||
hammer.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('`panstart` and `panmove` should be recognized', function() {
|
|
||||||
expect(2);
|
|
||||||
|
|
||||||
var panMoveCount = 0;
|
|
||||||
var pan = new Hammer.Pan({threshold: 1});
|
|
||||||
|
|
||||||
hammer.add(pan);
|
|
||||||
hammer.on('panstart', function() {
|
|
||||||
ok(true);
|
|
||||||
});
|
|
||||||
hammer.on('panmove', function() {
|
|
||||||
panMoveCount++;
|
|
||||||
});
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
utils.dispatchTouchEvent(el, 'move', 70, 50);
|
|
||||||
utils.dispatchTouchEvent(el, 'move', 90, 50);
|
|
||||||
|
|
||||||
equal(panMoveCount, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('Pan event flow should be start -> left -> end', function() {
|
|
||||||
expect(1);
|
|
||||||
var pan = new Hammer.Pan({threshold: 1});
|
|
||||||
hammer.add(pan);
|
|
||||||
|
|
||||||
var eventflow = "";
|
|
||||||
var isCalledPanleft = false;
|
|
||||||
hammer.on('panstart', function() {
|
|
||||||
eventflow += "start";
|
|
||||||
});
|
|
||||||
hammer.on('panleft', function() {
|
|
||||||
if(!isCalledPanleft){
|
|
||||||
isCalledPanleft = true;
|
|
||||||
eventflow += "left";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
hammer.on('panend', function() {
|
|
||||||
eventflow += "end";
|
|
||||||
isCalledPanleft = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
Simulator.gestures.pan(el, { deltaX: -100, deltaY: 0 }, function() {
|
|
||||||
equal(eventflow,"startleftend");
|
|
||||||
start();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
var el,
|
|
||||||
hammer;
|
|
||||||
|
|
||||||
module('Pinch Gesture', {
|
|
||||||
setup: function() {
|
|
||||||
el = document.createElement('div');
|
|
||||||
document.body.appendChild(el);
|
|
||||||
|
|
||||||
hammer = new Hammer(el, {recognizers: []});
|
|
||||||
},
|
|
||||||
teardown: function() {
|
|
||||||
document.body.removeChild(el);
|
|
||||||
hammer.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('Pinch event flow should be start -> in -> end', function() {
|
|
||||||
expect(1);
|
|
||||||
var pinch = new Hammer.Pinch({enable: true, threshold: .1});
|
|
||||||
hammer.add(pinch);
|
|
||||||
|
|
||||||
var eventflow = "";
|
|
||||||
var isFiredPinchin = false;
|
|
||||||
hammer.on('pinchstart', function() {
|
|
||||||
eventflow += "start";
|
|
||||||
});
|
|
||||||
hammer.on('pinchin', function() {
|
|
||||||
if(!isFiredPinchin){
|
|
||||||
isFiredPinchin = true;
|
|
||||||
eventflow += "in";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
hammer.on('pinchend', function() {
|
|
||||||
eventflow += "end";
|
|
||||||
isFiredPinchin = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
Simulator.gestures.pinch(el, { duration: 500, scale: .5 }, function() {
|
|
||||||
equal(eventflow,"startinend");
|
|
||||||
start();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
var el,
|
|
||||||
hammer,
|
|
||||||
swipeCount = 0;
|
|
||||||
|
|
||||||
module('Swipe Gesture', {
|
|
||||||
setup: function() {
|
|
||||||
el = utils.createHitArea();
|
|
||||||
hammer = new Hammer(el, {recognizers: []});
|
|
||||||
swipeCount = 0;
|
|
||||||
},
|
|
||||||
teardown: function() {
|
|
||||||
hammer.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('swipe can be recognized', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
var swipe = new Hammer.Swipe({threshold: 1});
|
|
||||||
hammer.add(swipe);
|
|
||||||
hammer.on('swipe', function() {
|
|
||||||
ok(true);
|
|
||||||
start();
|
|
||||||
});
|
|
||||||
|
|
||||||
stop();
|
|
||||||
|
|
||||||
Simulator.gestures.swipe(el);
|
|
||||||
});
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head lang="en">
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Tests</title>
|
|
||||||
<link rel="stylesheet" href="assets/qunit.css">
|
|
||||||
|
|
||||||
<script src="assets/jquery.min.js"></script>
|
|
||||||
<script src="assets/lodash.compat.js"></script>
|
|
||||||
<script src="assets/qunit.js"></script>
|
|
||||||
<!--[if !IE]> --><script src="assets/blanket.js"></script><!-- <![endif]-->
|
|
||||||
<script src="assets/utils.js"></script>
|
|
||||||
|
|
||||||
<script src="../../node_modules/hammer-simulator/index.js"></script>
|
|
||||||
<script>
|
|
||||||
Simulator.setType('touch');
|
|
||||||
Simulator.events.touch.fakeSupport();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script src="../build.js" data-cover></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="qunit"></div>
|
|
||||||
<div id="qunit-fixture"></div>
|
|
||||||
|
|
||||||
<script src="test_utils.js"></script>
|
|
||||||
<script src="test_enable.js"></script>
|
|
||||||
<script src="test_hammer.js"></script>
|
|
||||||
<script src="test_events.js"></script>
|
|
||||||
<script src="test_nested_gesture_recognizers.js"></script>
|
|
||||||
<script src="test_simultaneous_recognition.js"></script>
|
|
||||||
<script src="test_propagation_bubble.js"></script>
|
|
||||||
<script src="test_gestures.js"></script>
|
|
||||||
<script src="test_multiple_taps.js"></script>
|
|
||||||
<script src="test_require_failure.js"></script>
|
|
||||||
|
|
||||||
<script src="test_jquery_plugin.js"></script>
|
|
||||||
<script src="gestures/test_pan.js"></script>
|
|
||||||
<script src="gestures/test_pinch.js"></script>
|
|
||||||
<script src="gestures/test_swipe.js"></script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,171 +0,0 @@
|
||||||
var el,
|
|
||||||
hammer,
|
|
||||||
counter;
|
|
||||||
|
|
||||||
module('Test recognizer enable', {
|
|
||||||
setup: function() {
|
|
||||||
el = utils.createHitArea();
|
|
||||||
hammer = new Hammer.Manager(el, {recognizers: []});
|
|
||||||
counter = 0;
|
|
||||||
},
|
|
||||||
teardown: function() {
|
|
||||||
hammer && hammer.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should disable a recognizer through the `enable` constructor parameter', function() {
|
|
||||||
expect(1);
|
|
||||||
hammer.add(new Hammer.Tap({enable: false}));
|
|
||||||
hammer.on('tap', function() {
|
|
||||||
counter++;
|
|
||||||
});
|
|
||||||
|
|
||||||
stop();
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 50, 50);
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
start();
|
|
||||||
equal(counter, 0);
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should disable recognizing when the manager is disabled.', function() {
|
|
||||||
expect(1);
|
|
||||||
hammer.set({ enable: false });
|
|
||||||
hammer.add(new Hammer.Tap());
|
|
||||||
hammer.on('tap', function() {
|
|
||||||
counter++;
|
|
||||||
});
|
|
||||||
|
|
||||||
stop();
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 50, 50);
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
start();
|
|
||||||
equal(counter, 0);
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should toggle a recognizer using the `set` call to the recognizer enable property', function() {
|
|
||||||
expect(2);
|
|
||||||
|
|
||||||
hammer.add(new Hammer.Tap());
|
|
||||||
hammer.on('tap', function() {
|
|
||||||
counter++;
|
|
||||||
});
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 50, 50);
|
|
||||||
equal(counter, 1);
|
|
||||||
|
|
||||||
hammer.get('tap').set({ enable: false });
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 50, 50);
|
|
||||||
equal(counter, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should accept the `enable` constructor parameter as function', function() {
|
|
||||||
expect(2);
|
|
||||||
|
|
||||||
var canRecognizeTap = false;
|
|
||||||
|
|
||||||
var tap = new Hammer.Tap({
|
|
||||||
enable: function() {
|
|
||||||
return canRecognizeTap;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
hammer.add(tap);
|
|
||||||
hammer.on('tap', function() {
|
|
||||||
counter++;
|
|
||||||
});
|
|
||||||
|
|
||||||
stop();
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 50, 50);
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
start();
|
|
||||||
equal(counter, 0);
|
|
||||||
|
|
||||||
canRecognizeTap = true;
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 50, 50);
|
|
||||||
|
|
||||||
equal(counter, 1);
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should accept a function parameter with `set`', function() {
|
|
||||||
expect(3);
|
|
||||||
|
|
||||||
hammer.add(new Hammer.Tap());
|
|
||||||
hammer.on('tap', function() {
|
|
||||||
counter++;
|
|
||||||
});
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 50, 50);
|
|
||||||
equal(counter, 1);
|
|
||||||
|
|
||||||
var canRecognizeTap = false;
|
|
||||||
hammer.get('tap').set({ enable: function() {
|
|
||||||
return canRecognizeTap;
|
|
||||||
}});
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 50, 50);
|
|
||||||
equal(counter, 1);
|
|
||||||
|
|
||||||
canRecognizeTap = true;
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 50, 50);
|
|
||||||
equal(counter, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should pass the recognizer and optional the input parameter to the `enable` callback', function() {
|
|
||||||
expect(2);
|
|
||||||
|
|
||||||
var tap;
|
|
||||||
|
|
||||||
// the enable function is called initially to setup the touch-action property
|
|
||||||
// at that moment there isnt any input
|
|
||||||
var canEnable = function(recognizer, input) {
|
|
||||||
equal(recognizer, tap);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
tap = new Hammer.Tap({enable: canEnable});
|
|
||||||
hammer.add(tap);
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should toggle based on other object method', function() {
|
|
||||||
expect(2);
|
|
||||||
|
|
||||||
var view = {
|
|
||||||
state: 0,
|
|
||||||
canRecognizeTap: function(recognizer, input) {
|
|
||||||
return this.state !== 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
hammer.add(new Hammer.Tap({enable: function(rec, input) { return view.canRecognizeTap(rec, input); } }));
|
|
||||||
hammer.on('tap', function() {
|
|
||||||
counter++;
|
|
||||||
});
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 50, 50);
|
|
||||||
equal(counter, 0);
|
|
||||||
|
|
||||||
view.state = 1;
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 50, 50);
|
|
||||||
equal(counter, 1);
|
|
||||||
});
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
module('eventEmitter');
|
|
||||||
|
|
||||||
test('test the eventemitter', function() {
|
|
||||||
expect(6);
|
|
||||||
|
|
||||||
var ee = new Hammer.Manager(utils.createHitArea());
|
|
||||||
var inputData = {
|
|
||||||
target: document.body,
|
|
||||||
srcEvent: {
|
|
||||||
preventDefault: function() {
|
|
||||||
ok(true, 'preventDefault ref');
|
|
||||||
},
|
|
||||||
target: document.body
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function event3Handler() {
|
|
||||||
ok(true, 'emitted event3');
|
|
||||||
}
|
|
||||||
|
|
||||||
ee.on('testEvent1', function() {
|
|
||||||
ok(true, 'emitted event');
|
|
||||||
});
|
|
||||||
ee.on('testEvent2', function(ev) {
|
|
||||||
ok(true, 'emitted event');
|
|
||||||
ev.preventDefault();
|
|
||||||
ok(ev.target === document.body, 'target is the body');
|
|
||||||
});
|
|
||||||
ee.on('testEvent3', event3Handler);
|
|
||||||
|
|
||||||
ee.emit('testEvent1', inputData);
|
|
||||||
ee.emit('testEvent2', inputData);
|
|
||||||
ee.emit('testEvent3', inputData);
|
|
||||||
|
|
||||||
// unbind testEvent2
|
|
||||||
ee.off('testEvent2');
|
|
||||||
ee.off('testEvent3', event3Handler);
|
|
||||||
|
|
||||||
ee.emit('testEvent1', inputData); // should trigger testEvent1 again
|
|
||||||
ee.emit('testEvent2', inputData); // doenst trigger a thing
|
|
||||||
ee.emit('testEvent3', inputData); // doenst trigger a thing
|
|
||||||
|
|
||||||
// destroy
|
|
||||||
ee.destroy();
|
|
||||||
|
|
||||||
ee.emit('testEvent1', inputData); // doenst trigger a thing
|
|
||||||
ee.emit('testEvent2', inputData); // doenst trigger a thing
|
|
||||||
ee.emit('testEvent3', inputData); // doenst trigger a thing
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Hammer.Manager.off method : exception handling
|
|
||||||
*/
|
|
||||||
test("When Hammer.Manager didn't attach an event, 'off' method is ignored", function() {
|
|
||||||
var count = 0;
|
|
||||||
hammer = new Hammer(el, { inputTarget: document.body });
|
|
||||||
hammer.off("swipeleft", function(e) {
|
|
||||||
count++;
|
|
||||||
});
|
|
||||||
ok(true, "nothing");
|
|
||||||
});
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
// TODO: this tests fails because tapRecognizer changes
|
|
||||||
// it could be that tapRecognizer setup its BEGAN state and
|
|
||||||
// disable the other gesture recognition
|
|
||||||
var el, hammer, events;
|
|
||||||
var allGestureEvents = [
|
|
||||||
'tap doubletap press',
|
|
||||||
'pinch pinchin pinchout pinchstart pinchmove pinchend pinchcancel',
|
|
||||||
'rotate rotatestart rotatemove rotateend rotatecancel',
|
|
||||||
'pan panstart panmove panup pandown panleft panright panend pancancel',
|
|
||||||
'swipe swipeleft swiperight swipeup swipedown'
|
|
||||||
].join(' ');
|
|
||||||
|
|
||||||
module('Gesture recognition', {
|
|
||||||
setup: function() {
|
|
||||||
el = utils.createHitArea();
|
|
||||||
hammer = new Hammer(el);
|
|
||||||
hammer.get('pinch')
|
|
||||||
.set({ // some threshold, since the simulator doesnt stays at scale:1 when rotating
|
|
||||||
enable: true,
|
|
||||||
threshold: .1
|
|
||||||
});
|
|
||||||
|
|
||||||
hammer.get('rotate')
|
|
||||||
.set({ enable: true });
|
|
||||||
|
|
||||||
hammer.on(allGestureEvents, function(ev) {
|
|
||||||
events[ev.type] = true;
|
|
||||||
});
|
|
||||||
events = {};
|
|
||||||
},
|
|
||||||
teardown: function() {
|
|
||||||
hammer && hammer.destroy();
|
|
||||||
events = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('recognize pan', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
Simulator.gestures.pan(el, { duration: 500, deltaX: 100, deltaY: 0 }, function() {
|
|
||||||
start();
|
|
||||||
deepEqual(events, {
|
|
||||||
pan: true,
|
|
||||||
panstart: true,
|
|
||||||
panmove: true,
|
|
||||||
panright: true,
|
|
||||||
panend: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('recognize press', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
Simulator.gestures.press(el, null, function() {
|
|
||||||
start();
|
|
||||||
deepEqual(events, {
|
|
||||||
press: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('recognize swipe', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
Simulator.gestures.swipe(el, { duration: 300, deltaX: 400, deltaY: 0 }, function() {
|
|
||||||
start();
|
|
||||||
deepEqual(events, {
|
|
||||||
pan: true,
|
|
||||||
panstart: true,
|
|
||||||
panmove: true,
|
|
||||||
panright: true,
|
|
||||||
panend: true,
|
|
||||||
swipe: true,
|
|
||||||
swiperight: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('recognize pinch', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
Simulator.gestures.pinch(el, { duration: 500, scale: .5 }, function() {
|
|
||||||
start();
|
|
||||||
deepEqual(events, {
|
|
||||||
pinch: true,
|
|
||||||
pinchstart: true,
|
|
||||||
pinchmove: true,
|
|
||||||
pinchend: true,
|
|
||||||
pinchin: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('recognize children multitouch pinch', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
var el1 = utils.createHitArea(el),
|
|
||||||
el2 = utils.createHitArea(el);
|
|
||||||
|
|
||||||
Simulator.gestures.pinch([el1, el2], { duration: 500, scale: .5 }, function() {
|
|
||||||
start();
|
|
||||||
deepEqual(events, {
|
|
||||||
pinch: true,
|
|
||||||
pinchstart: true,
|
|
||||||
pinchmove: true,
|
|
||||||
pinchend: true,
|
|
||||||
pinchin: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('recognize parent-child multitouch pinch', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
var el1 = utils.createHitArea(el);
|
|
||||||
|
|
||||||
Simulator.gestures.pinch([el, el1], { duration: 100, scale: .5 }, function() {
|
|
||||||
start();
|
|
||||||
deepEqual(events, {
|
|
||||||
pinch: true,
|
|
||||||
pinchstart: true,
|
|
||||||
pinchmove: true,
|
|
||||||
pinchend: true,
|
|
||||||
pinchin: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('recognize rotate', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
Simulator.gestures.rotate(el, { duration: 500, scale: 1 }, function() {
|
|
||||||
start();
|
|
||||||
deepEqual(events, {
|
|
||||||
rotate: true,
|
|
||||||
rotatestart: true,
|
|
||||||
rotatemove: true,
|
|
||||||
rotateend: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('recognize multitouch rotate', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
var el1 = utils.createHitArea(el);
|
|
||||||
|
|
||||||
Simulator.gestures.rotate([el, el1], { duration: 500, scale: 1 }, function() {
|
|
||||||
start();
|
|
||||||
deepEqual(events, {
|
|
||||||
rotate: true,
|
|
||||||
rotatestart: true,
|
|
||||||
rotatemove: true,
|
|
||||||
rotateend: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('recognize rotate and pinch simultaneous', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
Simulator.gestures.pinchRotate(el, { duration: 500, scale: 2 }, function() {
|
|
||||||
start();
|
|
||||||
deepEqual(events, {
|
|
||||||
rotate: true,
|
|
||||||
rotatestart: true,
|
|
||||||
rotatemove: true,
|
|
||||||
rotateend: true,
|
|
||||||
pinch: true,
|
|
||||||
pinchstart: true,
|
|
||||||
pinchmove: true,
|
|
||||||
pinchend: true,
|
|
||||||
pinchout: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('don\'t recognize pan and swipe when moving down, when only horizontal is allowed', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
Simulator.gestures.swipe(el, { duration: 250, deltaX: 0, deltaZ: 200 }, function() {
|
|
||||||
start();
|
|
||||||
deepEqual(events, { });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('don\'t recognize press if duration is too short.', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
Simulator.gestures.press(el, { duration: 240 });
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
start();
|
|
||||||
deepEqual(events, { tap: true }, 'Tap gesture has been recognized.');
|
|
||||||
}, 275);
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('don\'t recognize tap if duration is too long.', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
Simulator.gestures.tap(el, { duration: 255 });
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
start();
|
|
||||||
deepEqual(events, { press: true }, 'Press gesture has been recognized.');
|
|
||||||
}, 275);
|
|
||||||
});
|
|
||||||
|
|
@ -1,187 +0,0 @@
|
||||||
var el, el2,
|
|
||||||
hammer, hammer2;
|
|
||||||
|
|
||||||
module('Tests', {
|
|
||||||
setup: function() {
|
|
||||||
el = utils.createHitArea();
|
|
||||||
el2 = utils.createHitArea();
|
|
||||||
},
|
|
||||||
|
|
||||||
teardown: function() {
|
|
||||||
if (hammer) {
|
|
||||||
hammer.destroy();
|
|
||||||
hammer = null;
|
|
||||||
}
|
|
||||||
if (hammer2) {
|
|
||||||
hammer2.destroy();
|
|
||||||
hammer2 = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('hammer shortcut', function() {
|
|
||||||
expect(2);
|
|
||||||
|
|
||||||
Hammer.defaults.touchAction = 'pan-y';
|
|
||||||
hammer = Hammer(el);
|
|
||||||
|
|
||||||
ok(hammer instanceof Hammer.Manager, 'returns an instance of Manager');
|
|
||||||
ok(hammer.touchAction.actions == Hammer.defaults.touchAction, 'set the default touchAction');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('hammer shortcut with options', function() {
|
|
||||||
expect(2);
|
|
||||||
|
|
||||||
hammer = Hammer(el, {
|
|
||||||
touchAction: 'none'
|
|
||||||
});
|
|
||||||
ok(hammer instanceof Hammer.Manager, 'returns an instance of Manager');
|
|
||||||
ok(hammer.touchAction.actions == 'none', 'set the default touchAction');
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Creating a hammer instance does not work on the same way
|
|
||||||
* when using Hammer or Hammer.Manager.
|
|
||||||
*
|
|
||||||
* This can confuse developers who read tests to use the library when doc is missing.
|
|
||||||
*/
|
|
||||||
test('Hammer and Hammer.Manager constructors work exactly on the same way.', function() {
|
|
||||||
expect(2);
|
|
||||||
|
|
||||||
hammer = new Hammer(el, {});
|
|
||||||
equal(Hammer.defaults.preset.length, hammer.recognizers.length);
|
|
||||||
|
|
||||||
hammer2 = new Hammer.Manager(el, {});
|
|
||||||
equal(0, hammer2.recognizers.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* DOC to disable default recognizers should be added.
|
|
||||||
*
|
|
||||||
* - Hammer(el). IMO: Currently, well done.
|
|
||||||
* - Hammer(el, {}) . IMO: should disable default recognizers
|
|
||||||
* - Hammer(el, {recognizers: null}). IMO: now, it fails.
|
|
||||||
* - Hammer(el, {recognizers: []}). It works, but it is likely not intuitive.
|
|
||||||
*/
|
|
||||||
test('A Hammer instance can be setup to not having default recognizers.', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
hammer = new Hammer(el, { recognizers: false });
|
|
||||||
equal(0, hammer.recognizers.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* The case was when I added a custom tap event which was added to the default
|
|
||||||
* recognizers, and my custom tap gesture wasn't working (I do not know exactly the reason),
|
|
||||||
* but removing the default recognizers solved the issue.
|
|
||||||
*/
|
|
||||||
test('Adding the same recognizer type should remove the old recognizer', function() {
|
|
||||||
expect(4);
|
|
||||||
|
|
||||||
hammer = new Hammer(el);
|
|
||||||
|
|
||||||
ok(!!hammer.get('tap'));
|
|
||||||
equal(7, hammer.recognizers.length);
|
|
||||||
|
|
||||||
var newTap = new Hammer.Tap({time: 1337});
|
|
||||||
hammer.add(newTap);
|
|
||||||
|
|
||||||
equal(7, hammer.recognizers.length);
|
|
||||||
equal(1337, hammer.get('tap').options.time);
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Swipe gesture:
|
|
||||||
* - in this tests, it does not update input.velocity ( always 0)
|
|
||||||
* - does not fire swipeleft or swiperight events
|
|
||||||
*/
|
|
||||||
asyncTest('Swiping to the left should fire swipeleft event', function() {
|
|
||||||
expect(2);
|
|
||||||
|
|
||||||
hammer = new Hammer(el, {recognizers: []});
|
|
||||||
hammer.add(new Hammer.Swipe());
|
|
||||||
hammer.on('swipe swipeleft', function() {
|
|
||||||
ok(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
Simulator.gestures.swipe(el, {pos: [300, 300], deltaY: 0, deltaX: -200}, function() {
|
|
||||||
start();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Input target change
|
|
||||||
*/
|
|
||||||
asyncTest('Should detect input while on other element', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
hammer = new Hammer(el, { inputTarget: document.body });
|
|
||||||
hammer.on('tap', function() {
|
|
||||||
ok(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
Simulator.gestures.tap(document.body, null, function() {
|
|
||||||
start();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Hammer.Manager constructor accepts a "recognizers" option in which each
|
|
||||||
* element is an array representation of a Recognizer.
|
|
||||||
*/
|
|
||||||
test('Hammer.Manager accepts recognizers as arrays.', function() {
|
|
||||||
expect(4);
|
|
||||||
|
|
||||||
hammer = new Hammer.Manager(el, {
|
|
||||||
recognizers: [
|
|
||||||
[Hammer.Swipe],
|
|
||||||
[Hammer.Pinch],
|
|
||||||
[Hammer.Rotate],
|
|
||||||
[Hammer.Pan, { direction: Hammer.DIRECTION_UP }, ['swipe', 'pinch'], ['rotate']]
|
|
||||||
]
|
|
||||||
});
|
|
||||||
equal(4, hammer.recognizers.length);
|
|
||||||
|
|
||||||
var recognizerActual = hammer.recognizers[3];
|
|
||||||
equal(recognizerActual.options.direction, Hammer.DIRECTION_UP);
|
|
||||||
equal(2, Object.keys(recognizerActual.simultaneous).length);
|
|
||||||
equal(1, recognizerActual.requireFail.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Removing a recognizer which cannot be found would errantly remove the last recognizer in the
|
|
||||||
* manager's list.
|
|
||||||
*/
|
|
||||||
test('Remove non-existent recognizer.', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
hammer = new Hammer(el, {recognizers: []});
|
|
||||||
hammer.add(new Hammer.Swipe());
|
|
||||||
hammer.remove('tap');
|
|
||||||
|
|
||||||
equal(1, hammer.recognizers.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('check whether Hammer.defaults.cssProps is restored', function() {
|
|
||||||
var beforeCssProps = {
|
|
||||||
userSelect: 'text',
|
|
||||||
touchSelect: 'grippers',
|
|
||||||
touchCallout: 'default',
|
|
||||||
contentZooming: 'chained',
|
|
||||||
userDrag: 'element',
|
|
||||||
tapHighlightColor: 'rgba(0, 1, 0, 0)'
|
|
||||||
};
|
|
||||||
var prop;
|
|
||||||
Hammer.each(Hammer.defaults.cssProps, function(value, name) {
|
|
||||||
prop = Hammer.prefixed(el.style, name);
|
|
||||||
if (prop) {
|
|
||||||
el.style[prop] = beforeCssProps[name];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
hammer = Hammer(el);
|
|
||||||
hammer.destroy();
|
|
||||||
hammer = null;
|
|
||||||
Hammer.each(Hammer.defaults.cssProps, function(value, name) {
|
|
||||||
prop = Hammer.prefixed(el.style, name);
|
|
||||||
if (prop) {
|
|
||||||
equal(el.style[prop], beforeCssProps[name], "check if " + name + " is restored");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
var el, hammer, events;
|
|
||||||
|
|
||||||
var jQueryPluginPath = '../../node_modules/jquery-hammerjs/jquery.hammer.js';
|
|
||||||
|
|
||||||
module('jQuery plugin', {
|
|
||||||
setup: function() {
|
|
||||||
el = utils.createHitArea();
|
|
||||||
events = {};
|
|
||||||
},
|
|
||||||
teardown: function() {
|
|
||||||
hammer && hammer.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('trigger pan with jQuery', function() {
|
|
||||||
expect(2);
|
|
||||||
|
|
||||||
$.getScript(jQueryPluginPath, function() {
|
|
||||||
jQuery(el).hammer();
|
|
||||||
jQuery(el).bind('panstart pan panmove panright panend', function(ev) {
|
|
||||||
if (ev.gesture) {
|
|
||||||
events[ev.type] = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Simulator.gestures.pan(el, { deltaX: 50, deltaY: 0 }, function() {
|
|
||||||
start();
|
|
||||||
deepEqual(events, {
|
|
||||||
pan: true,
|
|
||||||
panstart: true,
|
|
||||||
panmove: true,
|
|
||||||
panright: true,
|
|
||||||
panend: true
|
|
||||||
});
|
|
||||||
|
|
||||||
ok(jQuery(el).data('hammer') instanceof Hammer.Manager, 'data attribute refers to the instance');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('trigger pan without jQuery should still work', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
var hammer = Hammer(el);
|
|
||||||
hammer.on('panstart pan panmove panright panend', function(ev) {
|
|
||||||
events[ev.type] = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
Simulator.gestures.pan(el, { deltaX: 50, deltaY: 0 }, function() {
|
|
||||||
start();
|
|
||||||
deepEqual(events, {
|
|
||||||
pan: true,
|
|
||||||
panstart: true,
|
|
||||||
panmove: true,
|
|
||||||
panright: true,
|
|
||||||
panend: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
var el, hammer;
|
|
||||||
|
|
||||||
var tripleTapCount = 0,
|
|
||||||
doubleTapCount = 0,
|
|
||||||
tapCount = 0;
|
|
||||||
|
|
||||||
module('Tap delay', {
|
|
||||||
setup: function() {
|
|
||||||
el = utils.createHitArea();
|
|
||||||
hammer = new Hammer(el, {recognizers: []});
|
|
||||||
|
|
||||||
var tap = new Hammer.Tap();
|
|
||||||
var doubleTap = new Hammer.Tap({event: 'doubleTap', taps: 2 });
|
|
||||||
var tripleTap = new Hammer.Tap({event: 'tripleTap', taps: 3 });
|
|
||||||
|
|
||||||
hammer.add([tripleTap, doubleTap, tap]);
|
|
||||||
|
|
||||||
tripleTap.recognizeWith([doubleTap, tap]);
|
|
||||||
doubleTap.recognizeWith(tap);
|
|
||||||
|
|
||||||
doubleTap.requireFailure(tripleTap);
|
|
||||||
tap.requireFailure([tripleTap, doubleTap]);
|
|
||||||
|
|
||||||
tripleTapCount = 0;
|
|
||||||
doubleTapCount = 0;
|
|
||||||
tapCount = 0;
|
|
||||||
|
|
||||||
hammer.on('tap', function() {
|
|
||||||
tapCount++;
|
|
||||||
});
|
|
||||||
hammer.on('doubleTap', function() {
|
|
||||||
doubleTapCount++;
|
|
||||||
});
|
|
||||||
hammer.on('tripleTap', function() {
|
|
||||||
tripleTapCount++;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
teardown: function() {
|
|
||||||
hammer.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
asyncTest('When a tripleTap is fired, doubleTap and Tap should not be recognized', function() {
|
|
||||||
expect(3);
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 50, 50);
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 50, 50);
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 50, 50);
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
start();
|
|
||||||
equal(tripleTapCount, 1, 'one tripletap event');
|
|
||||||
equal(doubleTapCount, 0, 'no doubletap event');
|
|
||||||
equal(tapCount, 0, 'no singletap event');
|
|
||||||
}, 350);
|
|
||||||
});
|
|
||||||
asyncTest('When a doubleTap is fired, tripleTap and Tap should not be recognized', function() {
|
|
||||||
expect(3);
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 50, 50);
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 50, 50);
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
start();
|
|
||||||
equal(tripleTapCount, 0);
|
|
||||||
equal(doubleTapCount, 1);
|
|
||||||
equal(tapCount, 0);
|
|
||||||
}, 350);
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('When a tap is fired, tripleTap and doubleTap should not be recognized', function() {
|
|
||||||
expect(3);
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 50, 50);
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
start();
|
|
||||||
equal(tripleTapCount, 0);
|
|
||||||
equal(doubleTapCount, 0);
|
|
||||||
equal(tapCount, 1);
|
|
||||||
}, 350);
|
|
||||||
});
|
|
||||||
|
|
@ -1,167 +0,0 @@
|
||||||
var parent,
|
|
||||||
child,
|
|
||||||
hammerChild,
|
|
||||||
hammerParent;
|
|
||||||
|
|
||||||
module('Nested gesture recognizers (Tap Child + Pan Parent)', {
|
|
||||||
setup: function() {
|
|
||||||
parent = document.createElement('div');
|
|
||||||
child = document.createElement('div');
|
|
||||||
|
|
||||||
document.getElementById('qunit-fixture').appendChild(parent);
|
|
||||||
parent.appendChild(child);
|
|
||||||
|
|
||||||
hammerParent = new Hammer.Manager(parent, {
|
|
||||||
touchAction: 'none'
|
|
||||||
});
|
|
||||||
hammerChild = new Hammer.Manager(child, {
|
|
||||||
touchAction: 'none'
|
|
||||||
});
|
|
||||||
|
|
||||||
hammerChild.add(new Hammer.Tap());
|
|
||||||
hammerParent.add(new Hammer.Pan({threshold: 5, pointers: 1}));
|
|
||||||
},
|
|
||||||
teardown: function() {
|
|
||||||
hammerChild.destroy();
|
|
||||||
hammerParent.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Tap on the child', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
hammerChild.on('tap', function() {
|
|
||||||
ok(true);
|
|
||||||
});
|
|
||||||
hammerParent.on('tap', function() {
|
|
||||||
throw new Error('tap should not fire on parent');
|
|
||||||
});
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(child, 'start', 0, 10);
|
|
||||||
utils.dispatchTouchEvent(child, 'end', 0, 10);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Panning on the child should fire parent pan and should not fire child tap event', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
hammerChild.on('tap', function() {
|
|
||||||
throw new Error('tap should not fire on parent');
|
|
||||||
});
|
|
||||||
hammerParent.on('panend', function() {
|
|
||||||
ok(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(child, 'start', 10, 0);
|
|
||||||
utils.dispatchTouchEvent(child, 'move', 20, 0);
|
|
||||||
utils.dispatchTouchEvent(child, 'end', 30, 0);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
// test (optional pointers validation)
|
|
||||||
test('Panning with one finger down on child, other on parent', function () {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
var event,
|
|
||||||
touches;
|
|
||||||
|
|
||||||
hammerParent.on('panend', function () {
|
|
||||||
ok(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
// one finger one child
|
|
||||||
utils.dispatchTouchEvent(child, 'start', 10, 0, 0);
|
|
||||||
utils.dispatchTouchEvent(parent, 'start', 12, 0, 1);
|
|
||||||
|
|
||||||
touches = [
|
|
||||||
{clientX: 20, clientY: 0, identifier: 0 },
|
|
||||||
{clientX: 20, clientY: 0, identifier: 1 }
|
|
||||||
];
|
|
||||||
|
|
||||||
event = document.createEvent('Event');
|
|
||||||
event.initEvent('touchmove', true, true);
|
|
||||||
event.touches = touches;
|
|
||||||
event.changedTouches = touches;
|
|
||||||
|
|
||||||
parent.dispatchEvent(event);
|
|
||||||
|
|
||||||
touches = [
|
|
||||||
{clientX: 30, clientY: 0, identifier: 0 },
|
|
||||||
{clientX: 30, clientY: 0, identifier: 1 }
|
|
||||||
];
|
|
||||||
|
|
||||||
event = document.createEvent('Event');
|
|
||||||
event.initEvent('touchend', true, true);
|
|
||||||
event.touches = touches;
|
|
||||||
event.changedTouches = touches;
|
|
||||||
|
|
||||||
parent.dispatchEvent(event);
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
var pressPeriod = 600;
|
|
||||||
module('Nested gesture recognizers (Press Child + Pan Parent)', {
|
|
||||||
setup: function() {
|
|
||||||
parent = document.createElement('div');
|
|
||||||
child = document.createElement('div');
|
|
||||||
|
|
||||||
document.getElementById('qunit-fixture').appendChild(parent);
|
|
||||||
parent.appendChild(child);
|
|
||||||
|
|
||||||
hammerParent = new Hammer.Manager(parent, {
|
|
||||||
touchAction: 'none'
|
|
||||||
});
|
|
||||||
hammerChild = new Hammer.Manager(child, {
|
|
||||||
touchAction: 'none'
|
|
||||||
});
|
|
||||||
|
|
||||||
hammerChild.add(new Hammer.Press({time: pressPeriod}));
|
|
||||||
hammerParent.add(new Hammer.Pan({threshold: 5, pointers: 1}));
|
|
||||||
},
|
|
||||||
teardown: function() {
|
|
||||||
hammerChild.destroy();
|
|
||||||
hammerParent.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Press on the child', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
hammerChild.on('press', function() {
|
|
||||||
ok(true);
|
|
||||||
});
|
|
||||||
hammerParent.on('press', function() {
|
|
||||||
throw new Error('press should not fire on parent');
|
|
||||||
});
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(child, 'start', 0, 10);
|
|
||||||
|
|
||||||
stop();
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
start();
|
|
||||||
}, pressPeriod);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('When Press is followed by Pan on the same element, both gestures are recognized', function() {
|
|
||||||
expect(2);
|
|
||||||
hammerChild.on('press', function() {
|
|
||||||
ok(true);
|
|
||||||
});
|
|
||||||
hammerParent.on('panend', function() {
|
|
||||||
ok(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(child, 'start', 0, 10);
|
|
||||||
stop();
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
start();
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(child, 'move', 10, 10);
|
|
||||||
utils.dispatchTouchEvent(child, 'move', 20, 10);
|
|
||||||
utils.dispatchTouchEvent(child, 'move', 30, 10);
|
|
||||||
utils.dispatchTouchEvent(child, 'end', 30, 10);
|
|
||||||
|
|
||||||
}, pressPeriod);
|
|
||||||
});
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
var parent,
|
|
||||||
child,
|
|
||||||
hammerChild,
|
|
||||||
hammerParent;
|
|
||||||
|
|
||||||
module('Propagation (Tap in Child and Parent)', {
|
|
||||||
setup: function() {
|
|
||||||
parent = document.createElement('div');
|
|
||||||
child = document.createElement('div');
|
|
||||||
|
|
||||||
document.getElementById('qunit-fixture').appendChild(parent);
|
|
||||||
parent.appendChild(child);
|
|
||||||
|
|
||||||
hammerParent = new Hammer.Manager(parent);
|
|
||||||
hammerChild = new Hammer.Manager(child);
|
|
||||||
|
|
||||||
hammerChild.add(new Hammer.Tap());
|
|
||||||
hammerParent.add(new Hammer.Tap());
|
|
||||||
},
|
|
||||||
teardown: function() {
|
|
||||||
hammerChild.destroy();
|
|
||||||
hammerParent.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Tap on the child, fires also the tap event to the parent', function() {
|
|
||||||
expect(2);
|
|
||||||
|
|
||||||
hammerChild.on('tap', function() {
|
|
||||||
ok(true);
|
|
||||||
});
|
|
||||||
hammerParent.on('tap', function() {
|
|
||||||
ok(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(child, 'start', 0, 10);
|
|
||||||
utils.dispatchTouchEvent(child, 'end', 0, 10);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('When tap on the child and the child stops the input event propagation, the tap event does not get fired in the parent', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
hammerChild.on('tap', function() {
|
|
||||||
ok(true);
|
|
||||||
});
|
|
||||||
hammerParent.on('tap', function() {
|
|
||||||
throw new Error('parent tap gesture should not be recognized');
|
|
||||||
});
|
|
||||||
|
|
||||||
child.addEventListener('touchend', function(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
});
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(child, 'start', 0, 10);
|
|
||||||
utils.dispatchTouchEvent(child, 'end', 0, 10);
|
|
||||||
});
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
var el,
|
|
||||||
hammer,
|
|
||||||
pressPeriod = 200,
|
|
||||||
pressThreshold = 20,
|
|
||||||
pressCount = 0,
|
|
||||||
panStartCount = 0,
|
|
||||||
swipeCount = 0;
|
|
||||||
|
|
||||||
module('Require Failure ( Swipe & Press )', {
|
|
||||||
setup: function() {
|
|
||||||
el = utils.createHitArea();
|
|
||||||
hammer = new Hammer(el, {recognizers: []});
|
|
||||||
|
|
||||||
var swipe = new Hammer.Swipe({threshold: 1});
|
|
||||||
var press = new Hammer.Press({time: pressPeriod, threshold: pressThreshold});
|
|
||||||
|
|
||||||
hammer.add(swipe);
|
|
||||||
hammer.add(press);
|
|
||||||
|
|
||||||
swipe.recognizeWith(press);
|
|
||||||
press.requireFailure(swipe);
|
|
||||||
|
|
||||||
pressCount = 0;
|
|
||||||
swipeCount = 0;
|
|
||||||
hammer.on('press', function() {
|
|
||||||
pressCount++;
|
|
||||||
});
|
|
||||||
hammer.on('swipe', function() {
|
|
||||||
swipeCount++;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
teardown: function() {
|
|
||||||
hammer.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('When swipe does not recognize the gesture, a press gesture can be fired', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
start();
|
|
||||||
equal(pressCount, 1);
|
|
||||||
}, pressPeriod + 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('When swipe does recognize the gesture, a press gesture cannot be fired', function() {
|
|
||||||
expect(2);
|
|
||||||
|
|
||||||
Simulator.gestures.swipe(el, null, function() {
|
|
||||||
start();
|
|
||||||
|
|
||||||
ok(swipeCount > 0, 'swipe gesture should be recognizing');
|
|
||||||
equal(pressCount, 0, 'press gesture should not be recognized because swipe gesture is recognizing');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
module('Require Failure ( Pan & Press )', {
|
|
||||||
setup: function() {
|
|
||||||
el = document.createElement('div');
|
|
||||||
document.body.appendChild(el);
|
|
||||||
|
|
||||||
hammer = new Hammer(el, {recognizers: []});
|
|
||||||
|
|
||||||
var pan = new Hammer.Pan({threshold: 1});
|
|
||||||
var press = new Hammer.Press({time: pressPeriod, threshold: pressThreshold});
|
|
||||||
|
|
||||||
hammer.add([pan, press]);
|
|
||||||
|
|
||||||
pan.recognizeWith(press);
|
|
||||||
press.requireFailure(pan);
|
|
||||||
|
|
||||||
pressCount = 0;
|
|
||||||
panStartCount = 0;
|
|
||||||
hammer.on('press', function() {
|
|
||||||
pressCount++;
|
|
||||||
});
|
|
||||||
hammer.on('panstart', function() {
|
|
||||||
panStartCount++;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
teardown: function() {
|
|
||||||
document.body.removeChild(el);
|
|
||||||
hammer.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('When pan does not recognize the gesture, a press gesture can be fired', function() {
|
|
||||||
expect(1);
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
start();
|
|
||||||
equal(pressCount, 1);
|
|
||||||
}, pressPeriod + 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('When pan recognizes the gesture, a press gesture cannot be fired', function() {
|
|
||||||
expect(2);
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 50, 50);
|
|
||||||
utils.dispatchTouchEvent(el, 'move', 50 + pressThreshold / 4, 50);
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
start();
|
|
||||||
|
|
||||||
ok(panStartCount > 0, 'pan gesture should be recognizing');
|
|
||||||
equal(pressCount, 0, 'press gesture should not be recognized because pan gesture is recognizing');
|
|
||||||
}, pressPeriod + 100);
|
|
||||||
});
|
|
||||||
|
|
@ -1,234 +0,0 @@
|
||||||
var el,
|
|
||||||
hammer;
|
|
||||||
|
|
||||||
module('Simultaenous recognition', {
|
|
||||||
setup: function() {
|
|
||||||
el = utils.createHitArea()
|
|
||||||
},
|
|
||||||
teardown: function() {
|
|
||||||
hammer && hammer.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
asyncTest('should pinch and pan simultaneously be recognized when enabled', function() {
|
|
||||||
expect(4);
|
|
||||||
|
|
||||||
var panCount = 0,
|
|
||||||
pinchCount = 0;
|
|
||||||
|
|
||||||
hammer = new Hammer.Manager(el, {
|
|
||||||
touchAction: 'none'
|
|
||||||
});
|
|
||||||
|
|
||||||
hammer.add(new Hammer.Pan({threshold: 5, pointers: 2}));
|
|
||||||
|
|
||||||
var pinch = new Hammer.Pinch({ threshold: 0, pointers: 2});
|
|
||||||
hammer.add(pinch);
|
|
||||||
pinch.recognizeWith(hammer.get('pan'));
|
|
||||||
|
|
||||||
hammer.on('panend', function() {
|
|
||||||
panCount++;
|
|
||||||
});
|
|
||||||
hammer.on('pinchend', function() {
|
|
||||||
pinchCount++;
|
|
||||||
});
|
|
||||||
|
|
||||||
var executeGesture = function(cb) {
|
|
||||||
var event, touches;
|
|
||||||
|
|
||||||
touches = [
|
|
||||||
{clientX: 0, clientY: 10, identifier: 0, target: el },
|
|
||||||
{clientX: 10, clientY: 10, identifier: 1, target: el }
|
|
||||||
];
|
|
||||||
|
|
||||||
event = document.createEvent('Event');
|
|
||||||
event.initEvent('touchstart', true, true);
|
|
||||||
event.touches = touches;
|
|
||||||
event.targetTouches = touches;
|
|
||||||
event.changedTouches = touches;
|
|
||||||
el.dispatchEvent(event);
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
touches = [
|
|
||||||
{clientX: 10, clientY: 20, identifier: 0, target: el },
|
|
||||||
{clientX: 20, clientY: 20, identifier: 1, target: el }
|
|
||||||
];
|
|
||||||
|
|
||||||
event = document.createEvent('Event');
|
|
||||||
event.initEvent('touchmove', true, true);
|
|
||||||
event.touches = touches;
|
|
||||||
event.targetTouches = touches;
|
|
||||||
event.changedTouches = touches;
|
|
||||||
|
|
||||||
el.dispatchEvent(event);
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
touches = [
|
|
||||||
{clientX: 20, clientY: 30, identifier: 0, target: el },
|
|
||||||
{clientX: 40, clientY: 30, identifier: 1, target: el }
|
|
||||||
];
|
|
||||||
|
|
||||||
event = document.createEvent('Event');
|
|
||||||
event.initEvent('touchmove', true, true);
|
|
||||||
event.touches = touches;
|
|
||||||
event.targetTouches = touches;
|
|
||||||
event.changedTouches = touches;
|
|
||||||
el.dispatchEvent(event);
|
|
||||||
|
|
||||||
event = document.createEvent('Event');
|
|
||||||
event.initEvent('touchend', true, true);
|
|
||||||
event.touches = touches;
|
|
||||||
event.targetTouches = touches;
|
|
||||||
event.changedTouches = touches;
|
|
||||||
el.dispatchEvent(event);
|
|
||||||
|
|
||||||
cb();
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 2 gesture will be recognized
|
|
||||||
executeGesture(function() {
|
|
||||||
equal(panCount, 1);
|
|
||||||
equal(pinchCount, 1);
|
|
||||||
|
|
||||||
pinch.dropRecognizeWith(hammer.get('pan'));
|
|
||||||
|
|
||||||
// only the pan gesture will be recognized
|
|
||||||
executeGesture(function() {
|
|
||||||
equal(panCount, 2);
|
|
||||||
equal(pinchCount, 1);
|
|
||||||
|
|
||||||
start();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('the first gesture should block the following gestures (Tap & DoubleTap)', function() {
|
|
||||||
expect(4);
|
|
||||||
|
|
||||||
var tapCount = 0,
|
|
||||||
doubleTapCount = 0;
|
|
||||||
|
|
||||||
hammer = new Hammer.Manager(el, {
|
|
||||||
touchAction: 'none'
|
|
||||||
});
|
|
||||||
|
|
||||||
var tap = new Hammer.Tap();
|
|
||||||
var doubleTap = new Hammer.Tap({event: 'doubletap', taps: 2});
|
|
||||||
|
|
||||||
hammer.add(tap);
|
|
||||||
hammer.add(doubleTap);
|
|
||||||
|
|
||||||
hammer.on('tap', function() {
|
|
||||||
tapCount++;
|
|
||||||
});
|
|
||||||
hammer.on('doubletap', function() {
|
|
||||||
doubleTapCount++;
|
|
||||||
});
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 0, 10);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 0, 10);
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 0, 10);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 0, 10);
|
|
||||||
|
|
||||||
equal(tapCount, 2, 'on a double tap gesture, the tap gesture is recognized twice');
|
|
||||||
equal(doubleTapCount, 0, 'double tap gesture is not recognized because the prior tap gesture does not recognize it simultaneously');
|
|
||||||
|
|
||||||
doubleTap.recognizeWith(hammer.get('tap'));
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 0, 10);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 0, 10);
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 0, 10);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 0, 10);
|
|
||||||
|
|
||||||
equal(tapCount, 4);
|
|
||||||
equal(doubleTapCount, 1, 'when the tap gesture is configured to work simultaneously, tap & doubleTap can be recognized simultaneously');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('when disabled, the first gesture should not block gestures (Tap & DoubleTap )', function() {
|
|
||||||
expect(4);
|
|
||||||
|
|
||||||
var tapCount = 0,
|
|
||||||
doubleTapCount = 0;
|
|
||||||
|
|
||||||
hammer = new Hammer.Manager(el, {
|
|
||||||
touchAction: 'none'
|
|
||||||
});
|
|
||||||
|
|
||||||
var tap = new Hammer.Tap();
|
|
||||||
var doubleTap = new Hammer.Tap({event: 'doubletap', taps: 2});
|
|
||||||
|
|
||||||
hammer.add(tap);
|
|
||||||
hammer.add(doubleTap);
|
|
||||||
|
|
||||||
hammer.on('tap', function() {
|
|
||||||
tapCount++;
|
|
||||||
});
|
|
||||||
hammer.on('doubletap', function() {
|
|
||||||
doubleTapCount++;
|
|
||||||
});
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 0, 10);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 0, 10);
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 0, 10);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 0, 10);
|
|
||||||
|
|
||||||
equal(tapCount, 2, 'on a double tap gesture, the tap gesture is recognized twice');
|
|
||||||
equal(doubleTapCount, 0, 'double tap gesture is not recognized because the prior tap gesture does not recognize it simultaneously');
|
|
||||||
|
|
||||||
hammer.get('tap').set({ enable: false });
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 0, 10);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 0, 10);
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 0, 10);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 0, 10);
|
|
||||||
|
|
||||||
equal(tapCount, 2, 'tap gesture should not be recognized when the recognizer is disabled');
|
|
||||||
equal(doubleTapCount, 1, 'when the tap gesture is disabled, doubleTap can be recognized');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('the first gesture should block the following gestures (DoubleTap & Tap)', function() {
|
|
||||||
expect(4);
|
|
||||||
|
|
||||||
var tapCount = 0,
|
|
||||||
doubleTapCount = 0;
|
|
||||||
|
|
||||||
hammer = new Hammer.Manager(el, {
|
|
||||||
touchAction: 'none'
|
|
||||||
});
|
|
||||||
|
|
||||||
var tap = new Hammer.Tap();
|
|
||||||
var doubleTap = new Hammer.Tap({event: 'doubletap', taps: 2});
|
|
||||||
|
|
||||||
hammer.add(doubleTap);
|
|
||||||
hammer.add(tap);
|
|
||||||
|
|
||||||
hammer.on('tap', function() {
|
|
||||||
tapCount++;
|
|
||||||
});
|
|
||||||
hammer.on('doubletap', function() {
|
|
||||||
doubleTapCount++;
|
|
||||||
});
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 0, 10);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 0, 10);
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 0, 10);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 0, 10);
|
|
||||||
|
|
||||||
equal(doubleTapCount, 1, 'double tap is recognized');
|
|
||||||
equal(tapCount, 1, 'tap is detected, the doubletap is only catched by the doubletap recognizer');
|
|
||||||
|
|
||||||
// doubletap and tap together
|
|
||||||
doubleTap.recognizeWith(hammer.get('tap'));
|
|
||||||
doubleTapCount = 0;
|
|
||||||
tapCount = 0;
|
|
||||||
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 0, 10);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 0, 10);
|
|
||||||
utils.dispatchTouchEvent(el, 'start', 0, 10);
|
|
||||||
utils.dispatchTouchEvent(el, 'end', 0, 10);
|
|
||||||
|
|
||||||
equal(doubleTapCount, 1);
|
|
||||||
equal(tapCount, 2, 'when the tap gesture is configured to work simultaneously, tap & doubleTap can be recognized simultaneously');
|
|
||||||
});
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
module('utils');
|
|
||||||
|
|
||||||
// for the tests, all hammer properties and methods of Hammer are exposed to window.$H
|
|
||||||
|
|
||||||
test('get/set prefixed util', function() {
|
|
||||||
ok(_.isUndefined($H.prefixed(window, 'FakeProperty')), 'non existent property returns undefined');
|
|
||||||
|
|
||||||
window.webkitFakeProperty = 1337;
|
|
||||||
ok($H.prefixed(window, 'FakeProperty') == 'webkitFakeProperty', 'existent prefixed property returns the prefixed name');
|
|
||||||
|
|
||||||
delete window.webkitFakeProperty;
|
|
||||||
});
|
|
||||||
|
|
||||||
test('fnBind', function() {
|
|
||||||
var context = { a: true };
|
|
||||||
|
|
||||||
$H.bindFn(function(b) {
|
|
||||||
ok(this.a === true, 'bindFn scope');
|
|
||||||
ok(b === 123, 'bindFn argument');
|
|
||||||
}, context)(123);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Inherit objects', function() {
|
|
||||||
function Base() {
|
|
||||||
this.name = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Child() {
|
|
||||||
Base.call(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
$H.inherit(Child, Base, {
|
|
||||||
newMethod: function() {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var inst = new Child();
|
|
||||||
|
|
||||||
ok(inst.name == true, 'child has extended from base');
|
|
||||||
ok(inst.newMethod, 'child has a new method');
|
|
||||||
ok(Child.prototype.newMethod, 'child has a new prototype method');
|
|
||||||
ok(inst instanceof Child, 'is instanceof Child');
|
|
||||||
ok(inst instanceof Base, 'is instanceof Base');
|
|
||||||
ok(inst._super === Base.prototype, '_super is ref to prototype of Base');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('toArray', function() {
|
|
||||||
ok(_.isArray($H.toArray({ 0: true, 1: 'second', length: 2 })), 'converted an array-like object to an array');
|
|
||||||
ok(_.isArray($H.toArray([true, true])), 'array stays an array');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('inArray', function() {
|
|
||||||
ok($H.inArray([1, 2, 3, 4, 'hammer'], 'hammer') === 4, 'found item and returned the index');
|
|
||||||
ok($H.inArray([1, 2, 3, 4, 'hammer'], 'notfound') === -1, 'not found an item and returned -1');
|
|
||||||
ok($H.inArray([
|
|
||||||
{id: 2},
|
|
||||||
{id: 24}
|
|
||||||
], '24', 'id') === 1, 'find by key and return the index');
|
|
||||||
ok($H.inArray([
|
|
||||||
{id: 2},
|
|
||||||
{id: 24}
|
|
||||||
], '22', 'id') === -1, 'not found by key and return -1');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('splitStr', function() {
|
|
||||||
deepEqual($H.splitStr(' a b c d '), ['a', 'b', 'c', 'd'], 'str split valid');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('uniqueArray', function() {
|
|
||||||
deepEqual($H.uniqueArray([
|
|
||||||
{id: 1},
|
|
||||||
{id: 2},
|
|
||||||
{id: 2}
|
|
||||||
], 'id'), [
|
|
||||||
{id: 1},
|
|
||||||
{id: 2}
|
|
||||||
], 'remove duplicate ids')
|
|
||||||
});
|
|
||||||
|
|
||||||
test('boolOrFn', function() {
|
|
||||||
equal($H.boolOrFn(true), true, 'Passing an boolean');
|
|
||||||
equal($H.boolOrFn(false), false, 'Passing an boolean');
|
|
||||||
equal($H.boolOrFn(function() {
|
|
||||||
return true;
|
|
||||||
}), true, 'Passing an boolean');
|
|
||||||
equal($H.boolOrFn(1), true, 'Passing an integer');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('hasParent', function() {
|
|
||||||
var parent = document.createElement('div'),
|
|
||||||
child = document.createElement('div');
|
|
||||||
|
|
||||||
document.body.appendChild(parent);
|
|
||||||
parent.appendChild(child);
|
|
||||||
|
|
||||||
equal($H.hasParent(child, parent), true, 'Found parent');
|
|
||||||
equal($H.hasParent(parent, child), false, 'Not in parent');
|
|
||||||
|
|
||||||
document.body.removeChild(parent);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('each', function() {
|
|
||||||
var object = { hi: true };
|
|
||||||
var array = ['a', 'b', 'c'];
|
|
||||||
var loop;
|
|
||||||
|
|
||||||
loop = false;
|
|
||||||
$H.each(object, function(value, key) {
|
|
||||||
if (key == 'hi' && value === true) {
|
|
||||||
loop = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ok(loop, 'object loop');
|
|
||||||
|
|
||||||
loop = 0;
|
|
||||||
$H.each(array, function(value, key) {
|
|
||||||
if (value) {
|
|
||||||
loop++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ok(loop == 3, 'array loop');
|
|
||||||
|
|
||||||
loop = 0;
|
|
||||||
array.forEach = null;
|
|
||||||
$H.each(array, function(value, key) {
|
|
||||||
if (value) {
|
|
||||||
loop++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ok(loop == 3, 'array loop without Array.forEach');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('assign', function() {
|
|
||||||
expect(2);
|
|
||||||
deepEqual(
|
|
||||||
$H.assign(
|
|
||||||
{a: 1, b: 3},
|
|
||||||
{b: 2, c: 3}
|
|
||||||
),
|
|
||||||
{a: 1, b: 2, c: 3},
|
|
||||||
'Simple extend'
|
|
||||||
);
|
|
||||||
|
|
||||||
var src = { foo: true };
|
|
||||||
var dest = $H.assign({}, src);
|
|
||||||
src.foo = false;
|
|
||||||
deepEqual(dest, {foo: true}, 'Clone reference');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('test add/removeEventListener', function() {
|
|
||||||
function handleEvent() {
|
|
||||||
ok(true, 'triggered event');
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(2);
|
|
||||||
|
|
||||||
$H.addEventListeners(window, 'testEvent1 testEvent2 ', handleEvent);
|
|
||||||
utils.triggerDomEvent(window, 'testEvent1');
|
|
||||||
utils.triggerDomEvent(window, 'testEvent2');
|
|
||||||
|
|
||||||
$H.removeEventListeners(window, ' testEvent1 testEvent2 ', handleEvent);
|
|
||||||
utils.triggerDomEvent(window, 'testEvent1');
|
|
||||||
utils.triggerDomEvent(window, 'testEvent2');
|
|
||||||
});
|
|
||||||
|
|
@ -10,7 +10,6 @@
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dcloudio/uni-components": "^3.0.0-alpha-3000020210521001",
|
"@dcloudio/uni-components": "^3.0.0-alpha-3000020210521001",
|
||||||
"hammerjs": "^2.0.8",
|
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.26",
|
"vue": "^3.5.26",
|
||||||
"vue-lazyload": "^3.0.0"
|
"vue-lazyload": "^3.0.0"
|
||||||
|
|
@ -1157,15 +1156,6 @@
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hammerjs": {
|
|
||||||
"version": "2.0.8",
|
|
||||||
"resolved": "https://registry.npmmirror.com/hammerjs/-/hammerjs-2.0.8.tgz",
|
|
||||||
"integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hookable": {
|
"node_modules/hookable": {
|
||||||
"version": "5.5.3",
|
"version": "5.5.3",
|
||||||
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
|
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dcloudio/uni-components": "^3.0.0-alpha-3000020210521001",
|
"@dcloudio/uni-components": "^3.0.0-alpha-3000020210521001",
|
||||||
"hammerjs": "^2.0.8",
|
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.26",
|
"vue": "^3.5.26",
|
||||||
"vue-lazyload": "^3.0.0"
|
"vue-lazyload": "^3.0.0"
|
||||||
|
|
|
||||||
20
pages.json
20
pages.json
|
|
@ -7,26 +7,12 @@
|
||||||
"navigationStyle": "custom",
|
"navigationStyle": "custom",
|
||||||
"disableScroll": true
|
"disableScroll": true
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
{
|
|
||||||
"path": "pages/webview/webview",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "四大特色主题区",
|
|
||||||
"navigationStyle": "default"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "pages/canvas3",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "四大特色主题区",
|
|
||||||
"navigationStyle": "default"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"globalStyle": {
|
"globalStyle": {
|
||||||
"navigationBarTextStyle": "white",
|
"navigationBarTextStyle": "black",
|
||||||
"navigationBarTitleText": "2026新春H5",
|
"navigationBarTitleText": "2026新春H5",
|
||||||
"navigationBarBackgroundColor": "#fa4333",
|
"navigationBarBackgroundColor": "#F8F8F8",
|
||||||
"backgroundColor": "#F8F8F8"
|
"backgroundColor": "#F8F8F8"
|
||||||
},
|
},
|
||||||
"uniIdRouter": {}
|
"uniIdRouter": {}
|
||||||
|
|
|
||||||
263
pages/canvas.vue
263
pages/canvas.vue
|
|
@ -1,263 +0,0 @@
|
||||||
<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>
|
|
||||||
|
|
@ -1,791 +0,0 @@
|
||||||
<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>
|
|
||||||
|
|
@ -1,829 +0,0 @@
|
||||||
<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>
|
|
||||||
|
|
@ -98,7 +98,7 @@ const collectionStore = useCollectionStore();
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const backgroundImage = ref('/static/bg/bg_finish.jpg');
|
const backgroundImage = ref('https://placeholder.pics/svg/640x1136/FDF2E9/F39C12/结束页背景');
|
||||||
const uncollectedImage = ref('https://placeholder.pics/svg/100x100/CCCCCC/999999/未收集');
|
const uncollectedImage = ref('https://placeholder.pics/svg/100x100/CCCCCC/999999/未收集');
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
|
|
|
||||||
|
|
@ -1,459 +0,0 @@
|
||||||
<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>
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
<template>
|
|
||||||
<view class="webview-container">
|
|
||||||
<web-view :src="url"></web-view>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
const url = ref('')
|
|
||||||
|
|
||||||
// 页面加载时获取参数
|
|
||||||
onLoad((options) => {
|
|
||||||
if (options && options.url) {
|
|
||||||
// 解码URL参数
|
|
||||||
url.value = decodeURIComponent(options.url)
|
|
||||||
console.log('Webview loading URL:', url.value)
|
|
||||||
} else {
|
|
||||||
// 默认URL
|
|
||||||
url.value = 'https://www.720yun.com/t/1dvktq8b0fl?scene_id=74010726'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// onLoad 需要从 @dcloudio/uni-app 导入
|
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.webview-container {
|
|
||||||
width: 100%;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
import request from '../utils/request';
|
||||||
|
|
||||||
|
// AI春联生成
|
||||||
|
export const generateSpringCouple = (keyword) => {
|
||||||
|
// 由于是模拟环境,直接返回模拟数据
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
// 根据关键词生成不同的春联内容
|
||||||
|
const coupletDatabase = {
|
||||||
|
'吉祥': {
|
||||||
|
top: '吉祥如意庆新春',
|
||||||
|
bottom: '富贵平安迎佳节',
|
||||||
|
横批: '大吉大利'
|
||||||
|
},
|
||||||
|
'如意': {
|
||||||
|
top: '万事如意步步高',
|
||||||
|
bottom: '一帆风顺年年好',
|
||||||
|
横批: '吉祥如意'
|
||||||
|
},
|
||||||
|
'幸福': {
|
||||||
|
top: '幸福家庭多美满',
|
||||||
|
bottom: '和睦相处乐无边',
|
||||||
|
横批: '幸福安康'
|
||||||
|
},
|
||||||
|
'安康': {
|
||||||
|
top: '身体健康事业兴',
|
||||||
|
bottom: '家庭和睦幸福长',
|
||||||
|
横批: '平安健康'
|
||||||
|
},
|
||||||
|
'财源': {
|
||||||
|
top: '财源广进生意旺',
|
||||||
|
bottom: '富贵满堂家业兴',
|
||||||
|
横批: '招财进宝'
|
||||||
|
},
|
||||||
|
'富贵': {
|
||||||
|
top: '富贵吉祥财运亨',
|
||||||
|
bottom: '荣华富贵事业兴',
|
||||||
|
横批: '富贵双全'
|
||||||
|
},
|
||||||
|
'团圆': {
|
||||||
|
top: '团圆美满幸福年',
|
||||||
|
bottom: '和睦相处乐无边',
|
||||||
|
横批: '团圆幸福'
|
||||||
|
},
|
||||||
|
'快乐': {
|
||||||
|
top: '快乐时光无限好',
|
||||||
|
bottom: '幸福生活乐无边',
|
||||||
|
横批: '快乐幸福'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果有对应的关键词,返回对应的春联;否则返回默认春联
|
||||||
|
const result = coupletDatabase[keyword] || {
|
||||||
|
top: '新年快乐万事如意',
|
||||||
|
bottom: '新春大吉财源广进',
|
||||||
|
横批: '新年快乐'
|
||||||
|
};
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
code: 200,
|
||||||
|
data: result,
|
||||||
|
message: '生成成功'
|
||||||
|
});
|
||||||
|
}, 800); // 模拟网络延迟
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
import request from '../utils/request';
|
||||||
|
|
||||||
|
// 抽奖活动规则和奖项设置
|
||||||
|
const lotteryRules = {
|
||||||
|
totalDraws: 1000, // 总抽奖次数
|
||||||
|
remainingDraws: 500, // 剩余抽奖次数
|
||||||
|
prizes: [
|
||||||
|
{ id: 1, name: '一等奖', prize: 'iPhone 15', probability: 0.5 }, // 0.5%
|
||||||
|
{ id: 2, name: '二等奖', prize: 'AirPods Pro', probability: 2 }, // 2%
|
||||||
|
{ id: 3, name: '三等奖', prize: '京东购物卡200元', probability: 5 }, // 5%
|
||||||
|
{ id: 4, name: '四等奖', prize: '品牌充电宝', probability: 10 }, // 10%
|
||||||
|
{ id: 5, name: '参与奖', prize: '新春福袋', probability: 82.5 } // 82.5%
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交抽奖信息
|
||||||
|
export const submitLotteryInfo = (userInfo) => {
|
||||||
|
// 由于是模拟环境,直接返回模拟数据
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({
|
||||||
|
code: 200,
|
||||||
|
data: { success: true, message: '信息提交成功' },
|
||||||
|
message: '信息提交成功'
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 执行抽奖
|
||||||
|
export const drawLottery = (userId) => {
|
||||||
|
// 由于是模拟环境,直接返回模拟数据
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (lotteryRules.remainingDraws <= 0) {
|
||||||
|
resolve({
|
||||||
|
code: 200,
|
||||||
|
data: { won: false, message: '抽奖次数已用完' },
|
||||||
|
message: '抽奖次数已用完'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 随机生成抽奖结果
|
||||||
|
const random = Math.random() * 100;
|
||||||
|
let cumulativeProbability = 0;
|
||||||
|
let winningPrize = null;
|
||||||
|
|
||||||
|
// 根据概率确定中奖奖项
|
||||||
|
for (const prize of lotteryRules.prizes) {
|
||||||
|
cumulativeProbability += prize.probability;
|
||||||
|
if (random <= cumulativeProbability) {
|
||||||
|
winningPrize = prize;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新剩余抽奖次数
|
||||||
|
lotteryRules.remainingDraws--;
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
won: !!winningPrize,
|
||||||
|
prize: winningPrize,
|
||||||
|
remainingDraws: lotteryRules.remainingDraws
|
||||||
|
},
|
||||||
|
message: winningPrize ? '恭喜中奖!' : '很遗憾,未中奖。'
|
||||||
|
});
|
||||||
|
}, 800); // 模拟网络延迟
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取抽奖记录
|
||||||
|
export const getLotteryHistory = (userId) => {
|
||||||
|
// 由于是模拟环境,直接返回模拟数据
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({
|
||||||
|
code: 200,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
prizeName: '参与奖',
|
||||||
|
prize: '新春福袋',
|
||||||
|
drawTime: new Date(Date.now() - 86400000).toISOString(), // 1天前
|
||||||
|
status: 'unclaimed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
prizeName: '三等奖',
|
||||||
|
prize: '京东购物卡200元',
|
||||||
|
drawTime: new Date(Date.now() - 172800000).toISOString(), // 2天前
|
||||||
|
status: 'claimed'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
message: '获取抽奖记录成功'
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 领取奖品
|
||||||
|
export const claimPrize = (prizeId) => {
|
||||||
|
// 由于是模拟环境,直接返回模拟数据
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({
|
||||||
|
code: 200,
|
||||||
|
data: { success: true, message: '奖品领取成功' },
|
||||||
|
message: '奖品领取成功'
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/* 移动端适配方案 - Rem布局 */
|
||||||
|
/* 以750px设计稿为基准,1rem = 100px */
|
||||||
|
|
||||||
|
(function (doc, win) {
|
||||||
|
var docEl = doc.documentElement,
|
||||||
|
resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize',
|
||||||
|
recalc = function () {
|
||||||
|
var clientWidth = docEl.clientWidth;
|
||||||
|
if (!clientWidth) return;
|
||||||
|
// 最大宽度为750px,适配平板设备
|
||||||
|
if (clientWidth > 750) {
|
||||||
|
clientWidth = 750;
|
||||||
|
}
|
||||||
|
// 设置根字体大小,1rem = 100px
|
||||||
|
docEl.style.fontSize = 100 * (clientWidth / 750) + 'px';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!doc.addEventListener) return;
|
||||||
|
win.addEventListener(resizeEvt, recalc, false);
|
||||||
|
doc.addEventListener('DOMContentLoaded', recalc, false);
|
||||||
|
})(document, window);
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
/* 移动端适配方案 - Rem布局 */
|
||||||
|
/* 以750px设计稿为基准,1rem = 100px */
|
||||||
|
|
||||||
|
/* 媒体查询适配 */
|
||||||
|
/* 小屏手机(320px - 479px) */
|
||||||
|
@media screen and (max-width: 479px) {
|
||||||
|
html {
|
||||||
|
font-size: 42.67px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 中屏手机(480px - 599px) */
|
||||||
|
@media screen and (min-width: 480px) and (max-width: 599px) {
|
||||||
|
html {
|
||||||
|
font-size: 64px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 大屏手机(600px - 749px) */
|
||||||
|
@media screen and (min-width: 600px) and (max-width: 749px) {
|
||||||
|
html {
|
||||||
|
font-size: 80px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平板设备(750px及以上) */
|
||||||
|
@media screen and (min-width: 750px) {
|
||||||
|
html {
|
||||||
|
font-size: 100px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 横屏适配 */
|
||||||
|
@media screen and (orientation: landscape) {
|
||||||
|
/* 横屏时的特殊样式 */
|
||||||
|
.landscape-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.landscape-tip {
|
||||||
|
display: block !important;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #000;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.32rem;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 2rem;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 竖屏适配 */
|
||||||
|
@media screen and (orientation: portrait) {
|
||||||
|
.landscape-tip {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 触摸反馈样式 */
|
||||||
|
.touch-feedback {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.touch-feedback:active {
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: scale(0.95);
|
||||||
|
transition: all 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 高分辨率屏幕适配 */
|
||||||
|
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) {
|
||||||
|
/* 2倍屏样式 */
|
||||||
|
.retina-2x {
|
||||||
|
/* 2倍图样式 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (-webkit-min-device-pixel-ratio: 3), (min-resolution: 3dppx) {
|
||||||
|
/* 3倍屏样式 */
|
||||||
|
.retina-3x {
|
||||||
|
/* 3倍图样式 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 安全区域适配(iOS 11+) */
|
||||||
|
@supports (padding: env(safe-area-inset-top)) {
|
||||||
|
.safe-area-top {
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
|
}
|
||||||
|
|
||||||
|
.safe-area-bottom {
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
.safe-area-left {
|
||||||
|
padding-left: env(safe-area-inset-left);
|
||||||
|
}
|
||||||
|
|
||||||
|
.safe-area-right {
|
||||||
|
padding-right: env(safe-area-inset-right);
|
||||||
|
}
|
||||||
|
|
||||||
|
.safe-area-all {
|
||||||
|
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 禁止缩放 */
|
||||||
|
.no-zoom {
|
||||||
|
touch-action: pan-y;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 允许垂直滚动 */
|
||||||
|
.allow-scroll-y {
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 允许水平滚动 */
|
||||||
|
.allow-scroll-x {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,342 @@
|
||||||
|
/* 基础样式重置 */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #333;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 基础元素样式 */
|
||||||
|
a {
|
||||||
|
color: #007aff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:active {
|
||||||
|
color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 基础布局类 */
|
||||||
|
.container {
|
||||||
|
max-width: 750px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文本样式 */
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 间距类 */
|
||||||
|
.mt-10 { margin-top: 10px; }
|
||||||
|
.mt-20 { margin-top: 20px; }
|
||||||
|
.mt-30 { margin-top: 30px; }
|
||||||
|
|
||||||
|
.mb-10 { margin-bottom: 10px; }
|
||||||
|
.mb-20 { margin-bottom: 20px; }
|
||||||
|
.mb-30 { margin-bottom: 30px; }
|
||||||
|
|
||||||
|
.ml-10 { margin-left: 10px; }
|
||||||
|
.ml-20 { margin-left: 20px; }
|
||||||
|
|
||||||
|
.mr-10 { margin-right: 10px; }
|
||||||
|
.mr-20 { margin-right: 20px; }
|
||||||
|
|
||||||
|
.p-10 { padding: 10px; }
|
||||||
|
.p-20 { padding: 20px; }
|
||||||
|
.p-30 { padding: 30px; }
|
||||||
|
|
||||||
|
/* 圆角类 */
|
||||||
|
.rounded { border-radius: 8px; }
|
||||||
|
.rounded-lg { border-radius: 16px; }
|
||||||
|
.rounded-full { border-radius: 50%; }
|
||||||
|
|
||||||
|
/* 阴影类 */
|
||||||
|
.shadow { box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); }
|
||||||
|
.shadow-lg { box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15); }
|
||||||
|
|
||||||
|
/* 过渡动画 */
|
||||||
|
.transition {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏滚动条 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 防止文本选择 */
|
||||||
|
.no-select {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画类 */
|
||||||
|
|
||||||
|
/* 淡入效果 */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 淡入上移效果 */
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in-up {
|
||||||
|
animation: fadeInUp 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 淡入下移效果 */
|
||||||
|
@keyframes fadeInDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in-down {
|
||||||
|
animation: fadeInDown 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 淡入左移效果 */
|
||||||
|
@keyframes fadeInLeft {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in-left {
|
||||||
|
animation: fadeInLeft 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 淡入右移效果 */
|
||||||
|
@keyframes fadeInRight {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in-right {
|
||||||
|
animation: fadeInRight 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 缩放效果 */
|
||||||
|
@keyframes scaleIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scale-in {
|
||||||
|
animation: scaleIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹跳效果 */
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 20%, 50%, 80%, 100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bounce {
|
||||||
|
animation: bounce 1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 脉冲效果 */
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse {
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 旋转效果 */
|
||||||
|
@keyframes rotate {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rotate {
|
||||||
|
animation: rotate 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 闪光效果 */
|
||||||
|
@keyframes shine {
|
||||||
|
0% {
|
||||||
|
background-position: -200px 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: calc(200px + 100%) 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shine {
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(255, 255, 255, 0) 0%,
|
||||||
|
rgba(255, 255, 255, 0.5) 50%,
|
||||||
|
rgba(255, 255, 255, 0) 100%
|
||||||
|
);
|
||||||
|
background-size: 200px 100%;
|
||||||
|
animation: shine 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 悬停缩放效果 */
|
||||||
|
.hover-scale {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-scale:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 悬停上移效果 */
|
||||||
|
.hover-up {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-up:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 悬停阴影效果 */
|
||||||
|
.hover-shadow {
|
||||||
|
transition: box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-shadow:hover {
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 渐变色背景 */
|
||||||
|
.gradient-bg {
|
||||||
|
background: linear-gradient(135deg, #ff6b35 0%, #f7931e 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-bg-blue {
|
||||||
|
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-bg-green {
|
||||||
|
background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-bg-purple {
|
||||||
|
background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文字渐变色 */
|
||||||
|
.gradient-text {
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
background-image: linear-gradient(135deg, #ff6b35 0%, #f7931e 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载动画 */
|
||||||
|
.loading {
|
||||||
|
display: inline-block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: #fff;
|
||||||
|
animation: spin 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面过渡动画 */
|
||||||
|
.page-enter-active,
|
||||||
|
.page-leave-active {
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-enter-from,
|
||||||
|
.page-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,355 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
|
||||||
|
// 组件属性
|
||||||
|
const props = defineProps({
|
||||||
|
// 是否为活动状态
|
||||||
|
active: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 滚动位置,用于实现视差效果
|
||||||
|
scrollPosition: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件事件
|
||||||
|
const emit = defineEmits(['collect-seal', 'play-drum'])
|
||||||
|
|
||||||
|
// 是否收集福印
|
||||||
|
const sealCollected = ref(false)
|
||||||
|
|
||||||
|
// 计算视差效果的偏移量
|
||||||
|
const parallaxOffset = computed(() => {
|
||||||
|
// 滚动位置的1/10作为视差偏移
|
||||||
|
return props.scrollPosition * 0.1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 收集福印
|
||||||
|
const collectSeal = () => {
|
||||||
|
if (!sealCollected.value) {
|
||||||
|
sealCollected.value = true
|
||||||
|
emit('collect-seal')
|
||||||
|
showToast({
|
||||||
|
message: '恭喜获得国潮福字!',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 播放音效
|
||||||
|
const playDrum = () => {
|
||||||
|
emit('play-drum')
|
||||||
|
collectSeal()
|
||||||
|
showToast({
|
||||||
|
message: '滑动探索商圈,收集国潮福字!',
|
||||||
|
icon: 'info',
|
||||||
|
duration: 1500
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面挂载时的初始化
|
||||||
|
onMounted(() => {
|
||||||
|
// 添加动画类,触发入场动画
|
||||||
|
const container = document.querySelector('.chongwen-scene-container')
|
||||||
|
if (container) {
|
||||||
|
container.classList.add('animate-in')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="chongwen-scene-container" :class="{ 'active': active }">
|
||||||
|
<!-- 背景图片层 -->
|
||||||
|
<div class="background-layer" :style="{ transform: `translateY(${parallaxOffset}px)` }">
|
||||||
|
<!-- 使用前门商圈的背景图片作为占位 -->
|
||||||
|
<img src="/static/qianmen-bg.jpg" alt="崇文门商圈" class="background-image" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 增强动效层 -->
|
||||||
|
<div class="enhancement-layer">
|
||||||
|
<!-- 灯笼增强动效 -->
|
||||||
|
<div class="lanterns">
|
||||||
|
<div class="lantern left-lantern">🏮</div>
|
||||||
|
<div class="lantern right-lantern">🏮</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 福字增强动效 -->
|
||||||
|
<div class="fu-word">福</div>
|
||||||
|
|
||||||
|
<!-- 点击提示 -->
|
||||||
|
<div class="click-indicator" :class="{ 'animate-pulse': !sealCollected }">
|
||||||
|
<div class="pulse-circle"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 交互区域 -->
|
||||||
|
<div class="interaction-area" @click="playDrum">
|
||||||
|
<!-- 覆盖在图片上的点击区域 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 烟花效果 -->
|
||||||
|
<div class="fireworks">
|
||||||
|
<div class="firework firework-1">🎆</div>
|
||||||
|
<div class="firework firework-2">🎇</div>
|
||||||
|
<div class="firework firework-3">🎆</div>
|
||||||
|
<div class="firework firework-4">🎇</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 福印收集标记 -->
|
||||||
|
<div v-if="sealCollected" class="seal-collected-mark">
|
||||||
|
<div class="seal-icon">🏮</div>
|
||||||
|
<div class="seal-text">已收集国潮福字</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chongwen-scene-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
min-height: var(--scene-height, 100vh);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #ff6b35;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 背景图片层 */
|
||||||
|
.background-layer {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
transition: transform 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-image {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 为背景图片容器添加与图片主色调匹配的背景色,避免在小屏幕上出现黑边 */
|
||||||
|
.background-layer {
|
||||||
|
background-color: #ff6b35;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 增强动效层 */
|
||||||
|
.enhancement-layer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 灯笼增强动效 */
|
||||||
|
.lanterns {
|
||||||
|
position: absolute;
|
||||||
|
top: 15%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 30px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lantern {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
animation: swing 3s infinite ease-in-out;
|
||||||
|
opacity: 0.9;
|
||||||
|
filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.8));
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-lantern {
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-lantern {
|
||||||
|
animation-delay: 1.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes swing {
|
||||||
|
0%, 100% { transform: rotate(-10deg); }
|
||||||
|
50% { transform: rotate(10deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 福字增强动效 */
|
||||||
|
.fu-word {
|
||||||
|
position: absolute;
|
||||||
|
top: 30%;
|
||||||
|
left: 65%;
|
||||||
|
transform: translateX(-50%) rotate(15deg);
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #ffd700;
|
||||||
|
text-shadow: 2px 2px 10px rgba(255, 215, 0, 0.9);
|
||||||
|
animation: float 4s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateX(-50%) rotate(15deg) translateY(0); }
|
||||||
|
50% { transform: translateX(-50%) rotate(15deg) translateY(-15px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 点击指示器 */
|
||||||
|
.click-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 55%;
|
||||||
|
left: 75%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse-circle {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: rgba(255, 215, 0, 0.3);
|
||||||
|
border: 2px solid rgba(255, 215, 0, 0.6);
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(2);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.click-indicator.animate-pulse {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 交互区域 */
|
||||||
|
.interaction-area {
|
||||||
|
position: absolute;
|
||||||
|
top: 55%;
|
||||||
|
right: 15%;
|
||||||
|
width: 120px;
|
||||||
|
height: 100px;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整交互区域位置 */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.interaction-area {
|
||||||
|
top: 52%;
|
||||||
|
right: 10%;
|
||||||
|
width: 100px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 烟花效果 */
|
||||||
|
.fireworks {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firework {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 2rem;
|
||||||
|
opacity: 0;
|
||||||
|
animation: firework 3s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firework-1 {
|
||||||
|
top: 10%;
|
||||||
|
left: 20%;
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firework-2 {
|
||||||
|
top: 15%;
|
||||||
|
right: 25%;
|
||||||
|
animation-delay: 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firework-3 {
|
||||||
|
top: 8%;
|
||||||
|
right: 15%;
|
||||||
|
animation-delay: 2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firework-4 {
|
||||||
|
top: 12%;
|
||||||
|
left: 25%;
|
||||||
|
animation-delay: 3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes firework {
|
||||||
|
0%, 100% { opacity: 0; transform: scale(0); }
|
||||||
|
50% { opacity: 1; transform: scale(1.5); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 福印收集标记 */
|
||||||
|
.seal-collected-mark {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background-color: rgba(255, 107, 53, 0.9);
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
animation: fadeIn 0.5s ease;
|
||||||
|
z-index: 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seal-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(-20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 入场动画 */
|
||||||
|
.chongwen-scene-container.animate-in {
|
||||||
|
animation: sceneFadeIn 1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sceneFadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(50px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.fu-word {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lantern {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interaction-area {
|
||||||
|
width: 100px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useSceneStore } from '../store/scene'
|
||||||
|
import { useCollectionStore } from '../store/collection'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const sceneStore = useSceneStore()
|
||||||
|
const collectionStore = useCollectionStore()
|
||||||
|
|
||||||
|
// 当前路由路径
|
||||||
|
const currentRoute = computed(() => router.currentRoute.value.path)
|
||||||
|
|
||||||
|
// 是否在首页显示底部导航
|
||||||
|
const showHomeNav = computed(() => {
|
||||||
|
return currentRoute.value === '/'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 是否显示场景导航
|
||||||
|
const showSceneNav = computed(() => {
|
||||||
|
const sceneRoutes = ['/qianmen', '/chongwen', '/wangfujing', '/longfusi', '/dongzhimen']
|
||||||
|
return sceneRoutes.includes(currentRoute.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 福印收集进度
|
||||||
|
const collectionProgress = computed(() => {
|
||||||
|
return collectionStore.getCollectionProgress
|
||||||
|
})
|
||||||
|
|
||||||
|
// 跳转到AI春联生成页面
|
||||||
|
const goToAISpring = () => {
|
||||||
|
router.push('/ai-spring')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到抽奖页面
|
||||||
|
const goToLottery = () => {
|
||||||
|
uni.uni.showToast({title: '抽奖功能开发中...', duration: 2000})
|
||||||
|
// router.push('/lottery')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转到福印收集页面
|
||||||
|
const goToCollection = () => {
|
||||||
|
uni.uni.showToast({title: '福印收集功能开发中...', duration: 2000})
|
||||||
|
// router.push('/collection')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回首页
|
||||||
|
const goToHome = () => {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<footer class="common-footer" v-if="showHomeNav">
|
||||||
|
<div class="footer-nav">
|
||||||
|
<div class="nav-item" @click="goToAISpring">
|
||||||
|
<div class="nav-icon">📝</div>
|
||||||
|
<div class="nav-text">AI春联</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToLottery">
|
||||||
|
<div class="nav-icon">🎁</div>
|
||||||
|
<div class="nav-text">抽奖</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="goToCollection">
|
||||||
|
<div class="nav-icon">🏮</div>
|
||||||
|
<div class="nav-text">福印</div>
|
||||||
|
<div class="progress-badge" v-if="collectionProgress > 0">
|
||||||
|
{{ collectionProgress }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- 场景页面底部按钮 -->
|
||||||
|
<div class="scene-footer" v-if="showSceneNav">
|
||||||
|
<div class="scene-nav">
|
||||||
|
<Button type="primary" block @click="goToHome">
|
||||||
|
返回首页
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.common-footer {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 750px;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: #fff;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-nav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
height: 0.88rem;
|
||||||
|
padding: 0 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
font-size: 0.4rem;
|
||||||
|
margin-bottom: 0.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-text {
|
||||||
|
font-size: 0.2rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:active {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.1rem;
|
||||||
|
right: 0.2rem;
|
||||||
|
background-color: #ff6b35;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.16rem;
|
||||||
|
padding: 0.02rem 0.08rem;
|
||||||
|
border-radius: 0.1rem;
|
||||||
|
min-width: 0.3rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 场景页面底部按钮 */
|
||||||
|
.scene-footer {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 750px;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
padding: 0.2rem;
|
||||||
|
background-color: rgba(255, 255, 255, 0.95);
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-nav {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 适配安全区域 */
|
||||||
|
@supports (padding-bottom: env(safe-area-inset-bottom)) {
|
||||||
|
.common-footer {
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
height: calc(0.88rem + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-footer {
|
||||||
|
padding-bottom: calc(0.2rem + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useSceneStore } from '../store/scene'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const sceneStore = useSceneStore()
|
||||||
|
|
||||||
|
// 当前路由路径
|
||||||
|
const currentRoute = computed(() => router.currentRoute.value.path)
|
||||||
|
|
||||||
|
// 是否显示返回按钮
|
||||||
|
const showBackButton = computed(() => {
|
||||||
|
return currentRoute.value !== '/'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const goBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回首页
|
||||||
|
const goHome = () => {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取页面标题
|
||||||
|
const getPageTitle = computed(() => {
|
||||||
|
const titles = {
|
||||||
|
'/': '2026新春H5',
|
||||||
|
'/qianmen': '前门商圈',
|
||||||
|
'/chongwen': '崇文商圈',
|
||||||
|
'/wangfujing': '王府井商圈',
|
||||||
|
'/longfusi': '隆福寺商圈',
|
||||||
|
'/dongzhimen': '东直门商圈',
|
||||||
|
'/ai-spring': 'AI春联生成',
|
||||||
|
'/end': '活动结束'
|
||||||
|
}
|
||||||
|
return titles[currentRoute.value] || '2026新春H5'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header class="common-header">
|
||||||
|
<!-- 返回按钮 -->
|
||||||
|
<div class="header-left" v-if="showBackButton" @click="goBack">
|
||||||
|
<span class="back-icon">←</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空占位,保持标题居中 -->
|
||||||
|
<div class="header-left" v-else></div>
|
||||||
|
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="header-title">
|
||||||
|
{{ getPageTitle }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧操作按钮 -->
|
||||||
|
<div class="header-right">
|
||||||
|
<button class="home-btn" @click="goHome" v-if="currentRoute !== '/'">
|
||||||
|
首页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.common-header {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 750px;
|
||||||
|
height: 0.88rem;
|
||||||
|
background-color: #ff6b35;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 0.2rem;
|
||||||
|
font-size: 0.32rem;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: 0 2px 10px rgba(255, 107, 53, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left,
|
||||||
|
.header-right {
|
||||||
|
width: 1rem;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-icon {
|
||||||
|
font-size: 0.4rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-btn {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.15rem;
|
||||||
|
padding: 0.1rem 0.2rem;
|
||||||
|
font-size: 0.24rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-btn:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 适配安全区域 */
|
||||||
|
@supports (padding-top: env(safe-area-inset-top)) {
|
||||||
|
.common-header {
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
|
height: calc(0.88rem + env(safe-area-inset-top));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,355 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
|
||||||
|
// 组件属性
|
||||||
|
const props = defineProps({
|
||||||
|
// 是否为活动状态
|
||||||
|
active: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 滚动位置,用于实现视差效果
|
||||||
|
scrollPosition: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件事件
|
||||||
|
const emit = defineEmits(['collect-seal', 'play-drum'])
|
||||||
|
|
||||||
|
// 是否收集福印
|
||||||
|
const sealCollected = ref(false)
|
||||||
|
|
||||||
|
// 计算视差效果的偏移量
|
||||||
|
const parallaxOffset = computed(() => {
|
||||||
|
// 滚动位置的1/10作为视差偏移
|
||||||
|
return props.scrollPosition * 0.1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 收集福印
|
||||||
|
const collectSeal = () => {
|
||||||
|
if (!sealCollected.value) {
|
||||||
|
sealCollected.value = true
|
||||||
|
emit('collect-seal')
|
||||||
|
showToast({
|
||||||
|
message: '恭喜获得团圆福筷!',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 播放音效
|
||||||
|
const playDrum = () => {
|
||||||
|
emit('play-drum')
|
||||||
|
collectSeal()
|
||||||
|
showToast({
|
||||||
|
message: '移动烤鸭,领取团圆福筷!',
|
||||||
|
icon: 'info',
|
||||||
|
duration: 1500
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面挂载时的初始化
|
||||||
|
onMounted(() => {
|
||||||
|
// 添加动画类,触发入场动画
|
||||||
|
const container = document.querySelector('.dongzhimen-scene-container')
|
||||||
|
if (container) {
|
||||||
|
container.classList.add('animate-in')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="dongzhimen-scene-container" :class="{ 'active': active }">
|
||||||
|
<!-- 背景图片层 -->
|
||||||
|
<div class="background-layer" :style="{ transform: `translateY(${parallaxOffset}px)` }">
|
||||||
|
<!-- 使用前门商圈的背景图片作为占位 -->
|
||||||
|
<img src="/static/qianmen-bg.jpg" alt="东直门商圈" class="background-image" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 增强动效层 -->
|
||||||
|
<div class="enhancement-layer">
|
||||||
|
<!-- 灯笼增强动效 -->
|
||||||
|
<div class="lanterns">
|
||||||
|
<div class="lantern left-lantern">🏮</div>
|
||||||
|
<div class="lantern right-lantern">🏮</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 福字增强动效 -->
|
||||||
|
<div class="fu-word">福</div>
|
||||||
|
|
||||||
|
<!-- 点击提示 -->
|
||||||
|
<div class="click-indicator" :class="{ 'animate-pulse': !sealCollected }">
|
||||||
|
<div class="pulse-circle"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 交互区域 -->
|
||||||
|
<div class="interaction-area" @click="playDrum">
|
||||||
|
<!-- 覆盖在图片上的点击区域 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 烟花效果 -->
|
||||||
|
<div class="fireworks">
|
||||||
|
<div class="firework firework-1">🎆</div>
|
||||||
|
<div class="firework firework-2">🎇</div>
|
||||||
|
<div class="firework firework-3">🎆</div>
|
||||||
|
<div class="firework firework-4">🎇</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 福印收集标记 -->
|
||||||
|
<div v-if="sealCollected" class="seal-collected-mark">
|
||||||
|
<div class="seal-icon">🏮</div>
|
||||||
|
<div class="seal-text">已收集团圆福筷</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dongzhimen-scene-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
min-height: var(--scene-height, 100vh);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #ff6b35;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 背景图片层 */
|
||||||
|
.background-layer {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
transition: transform 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-image {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 为背景图片容器添加与图片主色调匹配的背景色,避免在小屏幕上出现黑边 */
|
||||||
|
.background-layer {
|
||||||
|
background-color: #ff6b35;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 增强动效层 */
|
||||||
|
.enhancement-layer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 灯笼增强动效 */
|
||||||
|
.lanterns {
|
||||||
|
position: absolute;
|
||||||
|
top: 15%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 30px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lantern {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
animation: swing 3s infinite ease-in-out;
|
||||||
|
opacity: 0.9;
|
||||||
|
filter: drop-shadow(0 0 15px rgba(255, 215, 0, 0.8));
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-lantern {
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-lantern {
|
||||||
|
animation-delay: 1.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes swing {
|
||||||
|
0%, 100% { transform: rotate(-10deg); }
|
||||||
|
50% { transform: rotate(10deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 福字增强动效 */
|
||||||
|
.fu-word {
|
||||||
|
position: absolute;
|
||||||
|
top: 30%;
|
||||||
|
left: 65%;
|
||||||
|
transform: translateX(-50%) rotate(15deg);
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #ffd700;
|
||||||
|
text-shadow: 2px 2px 10px rgba(255, 215, 0, 0.9);
|
||||||
|
animation: float 4s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateX(-50%) rotate(15deg) translateY(0); }
|
||||||
|
50% { transform: translateX(-50%) rotate(15deg) translateY(-15px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 点击指示器 */
|
||||||
|
.click-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 55%;
|
||||||
|
left: 75%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse-circle {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: rgba(255, 215, 0, 0.3);
|
||||||
|
border: 2px solid rgba(255, 215, 0, 0.6);
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(2);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.click-indicator.animate-pulse {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 交互区域 */
|
||||||
|
.interaction-area {
|
||||||
|
position: absolute;
|
||||||
|
top: 55%;
|
||||||
|
right: 15%;
|
||||||
|
width: 120px;
|
||||||
|
height: 100px;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整交互区域位置 */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.interaction-area {
|
||||||
|
top: 52%;
|
||||||
|
right: 10%;
|
||||||
|
width: 100px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 烟花效果 */
|
||||||
|
.fireworks {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firework {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 2rem;
|
||||||
|
opacity: 0;
|
||||||
|
animation: firework 3s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firework-1 {
|
||||||
|
top: 10%;
|
||||||
|
left: 20%;
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firework-2 {
|
||||||
|
top: 15%;
|
||||||
|
right: 25%;
|
||||||
|
animation-delay: 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firework-3 {
|
||||||
|
top: 8%;
|
||||||
|
right: 15%;
|
||||||
|
animation-delay: 2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firework-4 {
|
||||||
|
top: 12%;
|
||||||
|
left: 25%;
|
||||||
|
animation-delay: 3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes firework {
|
||||||
|
0%, 100% { opacity: 0; transform: scale(0); }
|
||||||
|
50% { opacity: 1; transform: scale(1.5); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 福印收集标记 */
|
||||||
|
.seal-collected-mark {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background-color: rgba(255, 107, 53, 0.9);
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
animation: fadeIn 0.5s ease;
|
||||||
|
z-index: 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seal-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(-20px); }
|
||||||
|
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); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.fu-word {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lantern {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interaction-area {
|
||||||
|
width: 100px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue