629 lines
25 KiB
HTML
629 lines
25 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||
<!-- <title>伪3D撕纸 · 初始顶层覆盖 · 拖拽后露出底层</title> -->
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||
background: #1a1a2e;
|
||
color: #eee;
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
user-select: none;
|
||
-webkit-user-select: none;
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 20px;
|
||
font-weight: 400;
|
||
color: #e94560;
|
||
margin-bottom: 8px;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.subtitle {
|
||
color: #aaa;
|
||
font-size: 13px;
|
||
margin-bottom: 30px;
|
||
text-align: center;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.tear-container {
|
||
position: relative;
|
||
width: 90vw;
|
||
height: 70vh;
|
||
background: transparent;
|
||
border-radius: 12px;
|
||
overflow: visible;
|
||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||
touch-action: none;
|
||
cursor: default;
|
||
}
|
||
|
||
.layer {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
pointer-events: none;
|
||
border-radius: 12px;
|
||
font-weight: bold;
|
||
text-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||
z-index: 1;
|
||
}
|
||
|
||
.layer-1 {
|
||
background: transparent;
|
||
color: rgba(0, 0, 0, 0.0);
|
||
font-size: 42px;
|
||
font-weight: 700;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.layer-1 img {
|
||
max-width: 80%;
|
||
max-height: 80%;
|
||
object-fit: contain;
|
||
display: none;
|
||
}
|
||
|
||
#tearCanvas {
|
||
position: absolute;
|
||
z-index: 10;
|
||
pointer-events: auto;
|
||
cursor: grab;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
#tearCanvas:active {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
.instructions {
|
||
margin-top: 20px;
|
||
color: #777;
|
||
font-size: 12px;
|
||
text-align: center;
|
||
line-height: 1.6;
|
||
max-width: 400px;
|
||
}
|
||
|
||
.reset-btn {
|
||
margin-top: 50px;
|
||
padding: 9px 24px;
|
||
background: transparent;
|
||
border: 1px solid #444;
|
||
color: #aaa;
|
||
border-radius: 20px;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
transition: all 0.2s;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.reset-btn:hover {
|
||
border-color: #e94560;
|
||
color: #e94560;
|
||
box-shadow: 0 0 12px rgba(233, 68, 96, 0.2);
|
||
}
|
||
|
||
@media (max-width: 440px) {
|
||
.tear-container {
|
||
width: 90vw;
|
||
height: 70vh;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 18px;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<!-- <h1>伪3D撕纸 · 初始完全覆盖</h1> -->
|
||
<!-- <p class="subtitle">从顶部边缘向下拖拽 → 逐步撕开顶层,露出底层</p> -->
|
||
<div class="tear-container" id="container">
|
||
<div class="layer layer-1">
|
||
<img data-src="" alt="底层">
|
||
</div>
|
||
<canvas id="tearCanvas"></canvas>
|
||
</div>
|
||
<!-- <p class="instructions">
|
||
⬇️ 从顶部边缘拖拽 → 顶层撕裂并弯曲,底层显露<br>
|
||
🖱️ 只有在锯齿线附近(±20px)移动,才能改变弯曲中心<br>
|
||
🌊 拖拽接近底部 → 顶层逐渐淡出消失,完全露出底层<br>
|
||
双击画布或点击按钮重置
|
||
</p> -->
|
||
<button class="reset-btn" id="resetBtn">重 置</button>
|
||
|
||
<script>
|
||
(function () {
|
||
const container = document.getElementById('container');
|
||
const canvas = document.getElementById('tearCanvas');
|
||
const ctx = canvas.getContext('2d');
|
||
const resetBtn = document.getElementById('resetBtn');
|
||
|
||
const PAD = 48;
|
||
const DRAG_THRESHOLD = 3;
|
||
const EDGE_THRESHOLD = 26;
|
||
const TEAR_TOLERANCE = 20;
|
||
const JAGGED_SEGMENTS = 18;
|
||
const JAGGED_AMPLITUDE = 5.0;
|
||
const JAGGED_FREQ = 3.5;
|
||
const MAX_BEND = 54;
|
||
const MIN_BEND = 5;
|
||
const SHADOW_OFFSET_FACTOR = 0.7;
|
||
const THICKNESS_COLOR = '#6B4226';
|
||
const FADE_START_DISTANCE = 48;
|
||
|
||
// 从URL参数或data-src获取图片地址
|
||
function getBottomImageSrc() {
|
||
const params = new URLSearchParams(window.location.search);
|
||
const urlParam = params.get('imageUrl');
|
||
const dataSrc = document.querySelector('.layer-1 img').dataset.src;
|
||
return urlParam || dataSrc || '';
|
||
}
|
||
|
||
const BOTTOM_IMAGE_SRC = getBottomImageSrc();
|
||
let bottomImageLoaded = false;
|
||
let bottomImgEl = null;
|
||
|
||
let isDragging = false;
|
||
let hasTorn = false;
|
||
let containerW = 0, containerH = 0;
|
||
let canvasW = 0, canvasH = 0;
|
||
let offsetX = PAD, offsetY = PAD;
|
||
|
||
let edgePosition = 0; // 撕裂线Y坐标(从0开始向下)
|
||
let freeEdgeOffset = 0; // 纸片翘起偏移量
|
||
let bendCenterX = 0; // 弯曲中心X
|
||
let dragStartX = 0, dragStartY = 0;
|
||
|
||
function getCoords(e) {
|
||
const rect = container.getBoundingClientRect();
|
||
return { x: e.clientX - rect.left, y: e.clientY - rect.top, w: rect.width, h: rect.height };
|
||
}
|
||
|
||
function getTouchCoords(e) {
|
||
const touch = e.touches[0] || e.changedTouches[0];
|
||
const rect = container.getBoundingClientRect();
|
||
return { x: touch.clientX - rect.left, y: touch.clientY - rect.top, w: rect.width, h: rect.height };
|
||
}
|
||
|
||
function dist(x1, y1, x2, y2) { return Math.hypot(x1 - x2, y1 - y2); }
|
||
function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
|
||
|
||
function getJaggedPoints(x1, y1, x2, y2, segments, amp, freq, seed) {
|
||
const points = [{ x: x1, y: y1 }];
|
||
const dx = x2 - x1, dy = y2 - y1;
|
||
const len = Math.hypot(dx, dy);
|
||
if (len < 1.5) { points.push({ x: x2, y: y2 }); return points; }
|
||
const ux = dx / len, uy = dy / len;
|
||
const px = -uy, py = ux;
|
||
for (let i = 1; i < segments; i++) {
|
||
const t = i / segments;
|
||
const bx = x1 + dx * t, by = y1 + dy * t;
|
||
const offset = amp * (Math.sin(t * freq * Math.PI + seed) * 0.7 +
|
||
Math.sin(t * freq * 2.3 * Math.PI + seed * 1.7) * 0.3);
|
||
points.push({ x: bx + px * offset, y: by + py * offset });
|
||
}
|
||
points.push({ x: x2, y: y2 });
|
||
return points;
|
||
}
|
||
|
||
function cx(x) { return x + offsetX; }
|
||
function cy(y) { return y + offsetY; }
|
||
|
||
// 计算顶层剩余部分的透明度(用于底部淡出效果)
|
||
function getTopLayerAlpha() {
|
||
if (!hasTorn) return 1;
|
||
const distanceToBottom = containerH - edgePosition;
|
||
if (distanceToBottom <= 0) return 0;
|
||
if (distanceToBottom >= FADE_START_DISTANCE) return 1;
|
||
return distanceToBottom / FADE_START_DISTANCE;
|
||
}
|
||
|
||
function updateEdgeTearAndCenter(mx, my) {
|
||
if (!hasTorn) return;
|
||
let newPos = clamp(my, 0, containerH);
|
||
if (newPos > edgePosition) edgePosition = newPos;
|
||
|
||
const desiredOffset = -my;
|
||
const absOffset = Math.abs(desiredOffset);
|
||
const clampedOffset = Math.min(absOffset, MAX_BEND);
|
||
const signedOffset = desiredOffset >= 0 ? clampedOffset : -clampedOffset;
|
||
if (Math.abs(signedOffset) > Math.abs(freeEdgeOffset)) {
|
||
freeEdgeOffset = signedOffset;
|
||
}
|
||
|
||
if (containerW > 0) {
|
||
bendCenterX = clamp(mx, 8, containerW - 8);
|
||
}
|
||
}
|
||
|
||
// 预加载底层图片
|
||
function preloadBottomImage(callback) {
|
||
if (!BOTTOM_IMAGE_SRC) {
|
||
callback();
|
||
return;
|
||
}
|
||
bottomImgEl = new Image();
|
||
bottomImgEl.src = BOTTOM_IMAGE_SRC;
|
||
bottomImgEl.onload = () => {
|
||
bottomImageLoaded = true;
|
||
callback();
|
||
};
|
||
bottomImgEl.onerror = () => {
|
||
bottomImageLoaded = false;
|
||
callback();
|
||
};
|
||
}
|
||
|
||
// 绘制明亮的底层(青瓷色,与顶层形成对比)
|
||
function drawBaseLayer() {
|
||
const w = containerW, h = containerH;
|
||
const grad = ctx.createLinearGradient(cx(0), cy(0), cx(w * 0.3), cy(h * 0.7));
|
||
grad.addColorStop(0, '#E9F5EF');
|
||
grad.addColorStop(0.5, '#CDE5D9');
|
||
grad.addColorStop(1, '#B0D2C2');
|
||
ctx.fillStyle = grad;
|
||
ctx.fillRect(cx(0), cy(0), w, h);
|
||
|
||
// 网格纹理
|
||
ctx.save();
|
||
ctx.globalAlpha = 0.2;
|
||
ctx.lineWidth = 0.8;
|
||
ctx.strokeStyle = '#5D8B7A';
|
||
for (let i = 0; i < w; i += 25) {
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx(i), cy(0));
|
||
ctx.lineTo(cx(i), cy(h));
|
||
ctx.stroke();
|
||
}
|
||
for (let i = 0; i < h; i += 25) {
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx(0), cy(i));
|
||
ctx.lineTo(cx(w), cy(i));
|
||
ctx.stroke();
|
||
}
|
||
ctx.restore();
|
||
|
||
// 底层图片
|
||
if (bottomImageLoaded && bottomImgEl) {
|
||
const scale = Math.min(w / bottomImgEl.width, h / bottomImgEl.height) * 0.8;
|
||
const drawW = bottomImgEl.width * scale;
|
||
const drawH = bottomImgEl.height * scale;
|
||
ctx.drawImage(bottomImgEl, cx(w / 2 - drawW / 2), cy(h / 2 - drawH / 2), drawW, drawH);
|
||
}
|
||
}
|
||
|
||
// 绘制未被撕裂的顶层部分(即撕裂线以下区域)
|
||
function drawUnrippedTop(alpha) {
|
||
if (!hasTorn) return;
|
||
if (alpha <= 0) return;
|
||
const w = containerW, h = containerH;
|
||
const tearY = edgePosition;
|
||
if (tearY >= h) return;
|
||
|
||
ctx.save();
|
||
ctx.globalAlpha = alpha;
|
||
ctx.beginPath();
|
||
ctx.rect(cx(0), cy(tearY), w, h - tearY);
|
||
ctx.clip();
|
||
|
||
// 顶层完整覆盖区域的材质(深色仿牛皮纸)
|
||
const grad = ctx.createLinearGradient(cx(0), cy(tearY), cx(w * 0.5), cy(h));
|
||
grad.addColorStop(0, '#B87C4E');
|
||
grad.addColorStop(0.6, '#9C633A');
|
||
grad.addColorStop(1, '#7A4A28');
|
||
ctx.fillStyle = grad;
|
||
ctx.fillRect(cx(0), cy(tearY), w, h - tearY);
|
||
|
||
// 添加细微纹理
|
||
ctx.globalCompositeOperation = 'overlay';
|
||
ctx.globalAlpha = 0.12;
|
||
for (let i = 0; i < 200; i++) {
|
||
ctx.fillStyle = '#FFF0D0';
|
||
ctx.beginPath();
|
||
ctx.arc(cx(Math.random() * w), cy(tearY + Math.random() * (h - tearY)), Math.random() * 2 + 0.5, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
}
|
||
ctx.globalCompositeOperation = 'source-over';
|
||
ctx.restore();
|
||
}
|
||
|
||
// 绘制翘起的纸片(撕裂线以上部分,带弯曲和厚度)
|
||
function drawTornPiece(alpha) {
|
||
if (!hasTorn) return;
|
||
if (alpha <= 0) return;
|
||
const w = containerW, h = containerH;
|
||
const absOff = Math.abs(freeEdgeOffset);
|
||
if (absOff < 1) return;
|
||
|
||
const bendDir = freeEdgeOffset > 0 ? 1 : -1;
|
||
const bendAmount = clamp(absOff, MIN_BEND, MAX_BEND);
|
||
|
||
const jaggedStart = { x: 0, y: edgePosition };
|
||
const jaggedEnd = { x: w, y: edgePosition };
|
||
const freeEdgeStart = { x: 0, y: edgePosition + freeEdgeOffset };
|
||
const freeEdgeEnd = { x: w, y: edgePosition + freeEdgeOffset };
|
||
|
||
let dynamicCenterX = bendCenterX;
|
||
if (isNaN(dynamicCenterX)) dynamicCenterX = w / 2;
|
||
dynamicCenterX = clamp(dynamicCenterX, 8, w - 8);
|
||
const arcMidX = dynamicCenterX;
|
||
const arcMidY = edgePosition + freeEdgeOffset + freeEdgeOffset * 0.5;
|
||
|
||
const seed = jaggedStart.x * 0.37 + jaggedEnd.y * 0.53;
|
||
const jagged = getJaggedPoints(jaggedStart.x, jaggedStart.y, jaggedEnd.x, jaggedEnd.y, JAGGED_SEGMENTS,
|
||
JAGGED_AMPLITUDE, JAGGED_FREQ, seed);
|
||
|
||
function buildPath(offX, offY) {
|
||
const pts = [];
|
||
for (const p of jagged) pts.push({ x: p.x + offX, y: p.y + offY });
|
||
const cpOff = bendAmount * 0.15;
|
||
pts.push({ x: freeEdgeEnd.x + offX + (bendDir * cpOff), y: freeEdgeEnd.y + offY });
|
||
pts.push({ x: arcMidX + offX, y: arcMidY + offY });
|
||
pts.push({ x: freeEdgeStart.x + offX + (bendDir * cpOff), y: freeEdgeStart.y + offY });
|
||
return pts;
|
||
}
|
||
|
||
const mainPath = buildPath(0, 0);
|
||
const shadowDir = -bendDir;
|
||
const shadowDist = bendAmount * SHADOW_OFFSET_FACTOR;
|
||
const shadowPath = buildPath(shadowDir * shadowDist, 0);
|
||
|
||
ctx.save();
|
||
ctx.globalAlpha = alpha;
|
||
|
||
// 阴影
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx(shadowPath[0].x), cy(shadowPath[0].y));
|
||
for (let i = 1; i < shadowPath.length; i++) ctx.lineTo(cx(shadowPath[i].x), cy(shadowPath[i].y));
|
||
ctx.closePath();
|
||
ctx.fillStyle = 'rgba(0,0,0,0.5)';
|
||
ctx.shadowBlur = 15;
|
||
ctx.shadowOffsetX = 3;
|
||
ctx.shadowOffsetY = 5;
|
||
ctx.fill();
|
||
|
||
// 厚度
|
||
const thick = bendAmount * 0.48;
|
||
function drawEdgeThick(p1, p2, sign) {
|
||
const dx = p2.x - p1.x, dy = p2.y - p1.y;
|
||
const len = Math.hypot(dx, dy);
|
||
if (len < 1) return;
|
||
const nx = -dy / len, ny = dx / len;
|
||
const offNx = nx * thick * sign, offNy = ny * thick * sign;
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx(p1.x), cy(p1.y));
|
||
ctx.lineTo(cx(p1.x + offNx), cy(p1.y + offNy));
|
||
ctx.lineTo(cx(p2.x + offNx), cy(p2.y + offNy));
|
||
ctx.lineTo(cx(p2.x), cy(p2.y));
|
||
ctx.closePath();
|
||
const grad = ctx.createLinearGradient(cx(p1.x), cy(p1.y), cx(p2.x), cy(p2.y));
|
||
grad.addColorStop(0, '#9B6A3C');
|
||
grad.addColorStop(1, '#553A22');
|
||
ctx.fillStyle = grad;
|
||
ctx.fill();
|
||
}
|
||
drawEdgeThick(jagged[jagged.length - 1], mainPath[jagged.length], -1);
|
||
drawEdgeThick(mainPath[mainPath.length - 1], jagged[0], 1);
|
||
|
||
// 纸片正面(深色,与底层形成反差)
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx(mainPath[0].x), cy(mainPath[0].y));
|
||
for (let i = 1; i < mainPath.length; i++) ctx.lineTo(cx(mainPath[i].x), cy(mainPath[i].y));
|
||
ctx.closePath();
|
||
const paperGrad = ctx.createLinearGradient(cx(freeEdgeStart.x), cy(freeEdgeStart.y), cx(jaggedStart.x), cy(jaggedStart.y));
|
||
paperGrad.addColorStop(0, '#D09E6E');
|
||
paperGrad.addColorStop(0.5, '#B47C48');
|
||
paperGrad.addColorStop(1, '#8B5A2E');
|
||
ctx.fillStyle = paperGrad;
|
||
ctx.fill();
|
||
ctx.strokeStyle = 'rgba(40,20,5,0.3)';
|
||
ctx.lineWidth = 1.2;
|
||
ctx.stroke();
|
||
|
||
// 弯曲高光
|
||
ctx.beginPath();
|
||
for (let i = jagged.length; i < mainPath.length; i++) ctx.lineTo(cx(mainPath[i].x), cy(mainPath[i].y));
|
||
ctx.strokeStyle = 'rgba(255,225,150,0.6)';
|
||
ctx.lineWidth = 2;
|
||
ctx.shadowBlur = 5;
|
||
ctx.stroke();
|
||
|
||
// 锯齿边缘金色高光
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx(jagged[0].x), cy(jagged[0].y));
|
||
for (let i = 1; i < jagged.length; i++) ctx.lineTo(cx(jagged[i].x), cy(jagged[i].y));
|
||
ctx.strokeStyle = 'rgba(245,195,80,0.9)';
|
||
ctx.lineWidth = 2.0;
|
||
ctx.stroke();
|
||
|
||
ctx.restore();
|
||
}
|
||
|
||
// 绘制锯齿边缘线(撕裂线位置,仅在撕裂存在时显示)
|
||
function drawTearLine(alpha) {
|
||
if (!hasTorn) return;
|
||
if (alpha <= 0) return;
|
||
const w = containerW;
|
||
const seed = 0.618;
|
||
const jagged = getJaggedPoints(0, edgePosition, w, edgePosition, JAGGED_SEGMENTS, JAGGED_AMPLITUDE, JAGGED_FREQ, seed);
|
||
ctx.save();
|
||
ctx.globalAlpha = alpha;
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx(jagged[0].x), cy(jagged[0].y));
|
||
for (let i = 1; i < jagged.length; i++) ctx.lineTo(cx(jagged[i].x), cy(jagged[i].y));
|
||
ctx.strokeStyle = 'rgba(210,150,70,0.8)';
|
||
ctx.lineWidth = 2.2;
|
||
ctx.shadowBlur = 6;
|
||
ctx.stroke();
|
||
ctx.restore();
|
||
}
|
||
|
||
// 绘制整个场景:底层 + 未被撕掉的顶层部分 + 翘起纸片 + 撕裂线
|
||
function drawScene() {
|
||
ctx.clearRect(0, 0, canvasW, canvasH);
|
||
drawBaseLayer(); // 先绘制底层(一直存在)
|
||
|
||
if (!hasTorn) {
|
||
// 未撕裂时,绘制完整顶层覆盖
|
||
ctx.save();
|
||
const w = containerW, h = containerH;
|
||
const grad = ctx.createLinearGradient(cx(0), cy(0), cx(w * 0.5), cy(h));
|
||
grad.addColorStop(0, '#B87C4E');
|
||
grad.addColorStop(0.6, '#9C633A');
|
||
grad.addColorStop(1, '#7A4A28');
|
||
ctx.fillStyle = grad;
|
||
ctx.fillRect(cx(0), cy(0), w, h);
|
||
// 添加纹理
|
||
ctx.globalCompositeOperation = 'overlay';
|
||
ctx.globalAlpha = 0.1;
|
||
for (let i = 0; i < 300; i++) {
|
||
ctx.fillStyle = '#FFF0D0';
|
||
ctx.beginPath();
|
||
ctx.arc(cx(Math.random() * w), cy(Math.random() * h), Math.random() * 2 + 0.5, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
}
|
||
ctx.restore();
|
||
return;
|
||
}
|
||
|
||
// 已撕裂状态:先绘制未被撕裂的顶层部分(撕裂线以下区域)
|
||
const alpha = getTopLayerAlpha();
|
||
drawUnrippedTop(alpha);
|
||
// 绘制翘起的纸片(撕裂线以上)
|
||
drawTornPiece(alpha);
|
||
// 绘制锯齿撕裂线
|
||
drawTearLine(alpha);
|
||
}
|
||
|
||
function resizeCanvas() {
|
||
const rect = container.getBoundingClientRect();
|
||
const newW = Math.round(rect.width);
|
||
const newH = Math.round(rect.height);
|
||
if (containerW === newW && containerH === newH && canvasW > 0) return;
|
||
|
||
if (hasTorn && containerW > 0 && containerH > 0) {
|
||
const sy = newH / containerH;
|
||
edgePosition = Math.min(edgePosition * sy, newH);
|
||
freeEdgeOffset *= sy;
|
||
}
|
||
containerW = newW;
|
||
containerH = newH;
|
||
canvasW = newW + 2 * PAD;
|
||
canvasH = newH + 2 * PAD;
|
||
offsetX = PAD;
|
||
offsetY = PAD;
|
||
canvas.width = canvasW;
|
||
canvas.height = canvasH;
|
||
canvas.style.width = canvasW + 'px';
|
||
canvas.style.height = canvasH + 'px';
|
||
canvas.style.left = -PAD + 'px';
|
||
canvas.style.top = -PAD + 'px';
|
||
|
||
if (containerW > 0) bendCenterX = clamp(bendCenterX, 5, containerW - 5);
|
||
else bendCenterX = containerW / 2;
|
||
drawScene();
|
||
}
|
||
|
||
// 交互逻辑
|
||
function onPointerDown(x, y) {
|
||
dragStartX = x;
|
||
dragStartY = y;
|
||
isDragging = true;
|
||
}
|
||
|
||
function onPointerMove(x, y) {
|
||
if (!isDragging) return;
|
||
|
||
if (!hasTorn) {
|
||
if (dist(x, y, dragStartX, dragStartY) < DRAG_THRESHOLD) return;
|
||
if (dragStartY > EDGE_THRESHOLD) {
|
||
isDragging = false;
|
||
return;
|
||
}
|
||
hasTorn = true;
|
||
edgePosition = clamp(dragStartY, 0, containerH);
|
||
let initOffset = -edgePosition;
|
||
initOffset = clamp(initOffset, -MAX_BEND, MAX_BEND);
|
||
freeEdgeOffset = initOffset;
|
||
bendCenterX = clamp(dragStartX, 8, containerW - 8);
|
||
drawScene();
|
||
updateEdgeTearAndCenter(x, y);
|
||
drawScene();
|
||
return;
|
||
}
|
||
|
||
const distToLine = Math.abs(y - edgePosition);
|
||
if (distToLine <= TEAR_TOLERANCE) {
|
||
updateEdgeTearAndCenter(x, y);
|
||
drawScene();
|
||
}
|
||
// 线外不更新任何状态
|
||
}
|
||
|
||
function onPointerUp() {
|
||
isDragging = false;
|
||
}
|
||
|
||
function resetTear() {
|
||
hasTorn = false;
|
||
isDragging = false;
|
||
edgePosition = 0;
|
||
freeEdgeOffset = 0;
|
||
if (containerW > 0) bendCenterX = containerW / 2;
|
||
drawScene();
|
||
}
|
||
|
||
// 事件绑定 - 修复:同时在 container 和 canvas 上绑定双击事件
|
||
container.addEventListener('mousedown', (e) => { e.preventDefault(); const c = getCoords(e); onPointerDown(c.x, c.y); });
|
||
window.addEventListener('mousemove', (e) => { if (!isDragging) return; e.preventDefault(); const c = getCoords(e); onPointerMove(c.x, c.y); });
|
||
window.addEventListener('mouseup', onPointerUp);
|
||
container.addEventListener('touchstart', (e) => { e.preventDefault(); const c = getTouchCoords(e); onPointerDown(c.x, c.y); }, { passive: false });
|
||
window.addEventListener('touchmove', (e) => { if (!isDragging) return; e.preventDefault(); const c = getTouchCoords(e); onPointerMove(c.x, c.y); }, { passive: false });
|
||
window.addEventListener('touchend', onPointerUp);
|
||
window.addEventListener('touchcancel', onPointerUp);
|
||
|
||
// 修复:在 container 和 canvas 上都添加双击事件
|
||
// container.addEventListener('dblclick', (e) => { e.preventDefault(); resetTear(); });
|
||
// canvas.addEventListener('dblclick', (e) => { e.preventDefault(); resetTear(); });
|
||
|
||
resetBtn.addEventListener('click', resetTear);
|
||
window.addEventListener('resize', () => { clearTimeout(window._rst); window._rst = setTimeout(resizeCanvas, 100); });
|
||
window.addEventListener('orientationchange', () => setTimeout(resizeCanvas, 200));
|
||
|
||
// 设置底层图片
|
||
const imgEl = document.querySelector('.layer-1 img');
|
||
imgEl.src = BOTTOM_IMAGE_SRC;
|
||
imgEl.dataset.src = BOTTOM_IMAGE_SRC;
|
||
|
||
// 预加载完成后初始化
|
||
preloadBottomImage(() => {
|
||
resizeCanvas();
|
||
resetTear();
|
||
});
|
||
resetTear();
|
||
})();
|
||
</script>
|
||
</body>
|
||
|
||
</html> |