diff --git a/BrokenClock.html b/BrokenClock.html
index e53b61a..bb9d0d2 100644
--- a/BrokenClock.html
+++ b/BrokenClock.html
@@ -78,6 +78,7 @@ let strikeTime = 0;
let strikeX = 0, strikeY = 0;
let textDirty = true;
let isStriking = false;
+let reformState = null;
function resize() {
W = canvas.width = window.innerWidth;
@@ -91,13 +92,14 @@ function resize() {
textCanvas.width = W; textCanvas.height = H;
textDirty = true;
- if (state === 'shattered') reset();
+ if (state !== 'intact') reset();
}
resize();
window.addEventListener('resize', resize);
canvas.addEventListener('mousedown', e => {
if (isStriking) return;
+ if (state !== 'intact' && state !== 'shattered') return;
const x = e.clientX, y = e.clientY;
if (state === 'intact') {
@@ -132,17 +134,41 @@ function triggerStrike(x, y, callback) {
setTimeout(callback, 130);
}
-reformBtn.addEventListener('click', reset);
+reformBtn.addEventListener('click', () => {
+ if (state === 'shattered') startReform();
+});
function reset() {
state = 'intact';
shards = [];
struck = false;
isStriking = false;
+ reformState = null;
hint.classList.remove('hidden');
reformBtn.classList.remove('show');
}
+function startReform() {
+ state = 'reforming';
+ reformBtn.classList.remove('show');
+
+ reformState = {
+ startTime: performance.now(),
+ moveDuration: 1000, // 阶段1:位移归位
+ fadeDuration: 600, // 阶段2:内容渐隐
+ initialStates: shards.map(s => ({
+ shard: s,
+ startOffsetX: s.offsetX,
+ startOffsetY: s.offsetY,
+ startRotation: s.rotation,
+ })),
+ };
+}
+
+function easeInOutCubic(t) {
+ return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
+}
+
const ROMANS = ['XII', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI'];
function drawClockTo(c) {
@@ -177,14 +203,13 @@ function drawClockTo(c) {
const secAngle = (s + ms/1000) * Math.PI / 30 - Math.PI/2;
c.lineCap = 'round';
- // 指针泛光:用两层描边模拟,不用shadowBlur
+
c.strokeStyle = 'rgba(180, 220, 255, 0.35)';
c.lineWidth = 6;
c.beginPath();
c.moveTo(cx - Math.cos(hourAngle) * radius * 0.08, cy - Math.sin(hourAngle) * radius * 0.08);
c.lineTo(cx + Math.cos(hourAngle) * radius * 0.45, cy + Math.sin(hourAngle) * radius * 0.45);
c.stroke();
-
c.strokeStyle = '#ffffff';
c.lineWidth = 2.8;
c.beginPath();
@@ -198,7 +223,6 @@ function drawClockTo(c) {
c.moveTo(cx - Math.cos(minAngle) * radius * 0.08, cy - Math.sin(minAngle) * radius * 0.08);
c.lineTo(cx + Math.cos(minAngle) * radius * 0.65, cy + Math.sin(minAngle) * radius * 0.65);
c.stroke();
-
c.strokeStyle = '#ffffff';
c.lineWidth = 1.8;
c.beginPath();
@@ -212,7 +236,6 @@ function drawClockTo(c) {
c.moveTo(cx - Math.cos(secAngle) * radius * 0.12, cy - Math.sin(secAngle) * radius * 0.12);
c.lineTo(cx + Math.cos(secAngle) * radius * 0.78, cy + Math.sin(secAngle) * radius * 0.78);
c.stroke();
-
c.strokeStyle = '#fff5cc';
c.lineWidth = 1;
c.beginPath();
@@ -353,21 +376,13 @@ function shatterInitial(clickX, clickY) {
for (let i = 0; i < numShards; i++) {
const nextI = (i + 1) % numShards;
-
const points = [];
- for (let k = 0; k < edges[i].length; k++) {
- points.push(edges[i][k]);
- }
- for (let k = 1; k < arcs[i].length; k++) {
- points.push(arcs[i][k]);
- }
- for (let k = edges[nextI].length - 2; k >= 0; k--) {
- points.push(edges[nextI][k]);
- }
+ for (let k = 0; k < edges[i].length; k++) points.push(edges[i][k]);
+ for (let k = 1; k < arcs[i].length; k++) points.push(arcs[i][k]);
+ for (let k = edges[nextI].length - 2; k >= 0; k--) points.push(edges[nextI][k]);
const centroid = computeCentroid(points);
const area = computeArea(points);
-
const fdx = centroid[0] - cp.x;
const fdy = centroid[1] - cp.y;
const fdist = Math.max(0.1, Math.sqrt(fdx * fdx + fdy * fdy));
@@ -397,7 +412,6 @@ function shatterInitial(clickX, clickY) {
function shatterShard(shard, clickX, clickY) {
const [lcx, lcy] = pointToLocal(shard, clickX, clickY);
-
const numRays = 3 + Math.floor(Math.random() * 2);
const baseAngle = Math.random() * Math.PI * 2;
const intersections = [];
@@ -426,11 +440,8 @@ function shatterShard(shard, clickX, clickY) {
const nextI = (i + 1) % numNew;
const start = intersections[i];
const end = intersections[nextI];
-
const points = [];
- for (let k = 0; k < edges[i].length; k++) {
- points.push(edges[i][k]);
- }
+ for (let k = 0; k < edges[i].length; k++) points.push(edges[i][k]);
if (start.edgeIndex !== end.edgeIndex) {
let ei = (start.edgeIndex + 1) % n;
while (ei !== end.edgeIndex) {
@@ -439,9 +450,7 @@ function shatterShard(shard, clickX, clickY) {
}
points.push(shard.points[ei]);
}
- for (let k = edges[nextI].length - 1; k >= 1; k--) {
- points.push(edges[nextI][k]);
- }
+ for (let k = edges[nextI].length - 1; k >= 1; k--) points.push(edges[nextI][k]);
const centroid = computeCentroid(points);
const area = computeArea(points);
@@ -454,11 +463,8 @@ function shatterShard(shard, clickX, clickY) {
const baseSpeed = 0.5 + Math.random() * 0.5;
newShards.push({
- points,
- centroid,
- area,
- offsetX: shard.offsetX,
- offsetY: shard.offsetY,
+ points, centroid, area,
+ offsetX: shard.offsetX, offsetY: shard.offsetY,
rotation: shard.rotation,
vx: (fdx / fdist) * baseSpeed + shard.vx * 0.4,
vy: (fdy / fdist) * baseSpeed + shard.vy * 0.4,
@@ -470,49 +476,33 @@ function shatterShard(shard, clickX, clickY) {
}
if (newShards.length < 2) return;
-
const idx = shards.indexOf(shard);
- if (idx >= 0) {
- shards.splice(idx, 1, ...newShards);
- }
+ if (idx >= 0) shards.splice(idx, 1, ...newShards);
}
function resolveCollisions() {
for (let i = 0; i < shards.length; i++) {
for (let j = i + 1; j < shards.length; j++) {
- const a = shards[i];
- const b = shards[j];
-
+ const a = shards[i], b = shards[j];
const ax = a.centroid[0] + a.offsetX;
const ay = a.centroid[1] + a.offsetY;
const bx = b.centroid[0] + b.offsetX;
const by = b.centroid[1] + b.offsetY;
-
- const dx = bx - ax;
- const dy = by - ay;
+ const dx = bx - ax, dy = by - ay;
const distSq = dx * dx + dy * dy;
const minDist = (a.effRadius + b.effRadius) * 0.82;
-
if (distSq < minDist * minDist && distSq > 0.01) {
const dist = Math.sqrt(distSq);
const overlap = minDist - dist;
- const nx = dx / dist;
- const ny = dy / dist;
-
+ const nx = dx / dist, ny = dy / dist;
const totalArea = a.area + b.area;
- const pushA = overlap * (b.area / totalArea);
- const pushB = overlap * (a.area / totalArea);
-
- a.offsetX -= nx * pushA;
- a.offsetY -= ny * pushA;
- b.offsetX += nx * pushB;
- b.offsetY += ny * pushB;
-
+ a.offsetX -= nx * overlap * (b.area / totalArea);
+ a.offsetY -= ny * overlap * (b.area / totalArea);
+ b.offsetX += nx * overlap * (a.area / totalArea);
+ b.offsetY += ny * overlap * (a.area / totalArea);
const sep = 0.08;
- a.vx -= nx * sep;
- a.vy -= ny * sep;
- b.vx += nx * sep;
- b.vy += ny * sep;
+ a.vx -= nx * sep; a.vy -= ny * sep;
+ b.vx += nx * sep; b.vy += ny * sep;
}
}
}
@@ -562,29 +552,21 @@ function findShardAt(x, y) {
}
function rayPolygonIntersect(ox, oy, dx, dy, points) {
- let closestT = Infinity;
- let closestEdge = -1;
- let closestPoint = null;
-
+ let closestT = Infinity, closestEdge = -1, closestPoint = null;
for (let i = 0; i < points.length; i++) {
const j = (i + 1) % points.length;
const ax = points[i][0], ay = points[i][1];
const bx = points[j][0], by = points[j][1];
const ex = bx - ax, ey = by - ay;
-
const det = dy * ex - dx * ey;
if (Math.abs(det) < 1e-10) continue;
-
const t = (ex * (ay - oy) - ey * (ax - ox)) / det;
const u = (dx * (ay - oy) - dy * (ax - ox)) / det;
-
if (t > 1e-6 && u >= 0 && u <= 1 && t < closestT) {
- closestT = t;
- closestEdge = i;
+ closestT = t; closestEdge = i;
closestPoint = [ox + t * dx, oy + t * dy];
}
}
-
return closestEdge >= 0 ? { point: closestPoint, edgeIndex: closestEdge } : null;
}
@@ -620,10 +602,12 @@ function computeCentroid(points) {
return [cx_ / points.length, cy_ / points.length];
}
-function drawShard(shard) {
+// reformProgress: 0=完全破碎态, 1=完全归位且文字渐隐完成
+// 在阶段1(位移归位)时 reformProgress=0,阶段2(内容渐隐)时从0升到1
+function drawShard(shard, reformProgress = 0, bgAlpha = 0.45) {
const age = (performance.now() - shard.birthTime) / 1000;
let glow = 0;
- if (age < 1.5) {
+ if (reformProgress === 0 && age < 1.5) {
glow = Math.max(0, 1 - age / 1.5);
glow = Math.pow(glow, 1.2);
}
@@ -640,20 +624,18 @@ function drawShard(shard) {
}
ctx.closePath();
- ctx.fillStyle = 'rgba(255, 255, 255, 0.45)';
- ctx.fill();
+ if (bgAlpha > 0.01) {
+ ctx.fillStyle = `rgba(255, 255, 255, ${bgAlpha})`;
+ ctx.fill();
+ }
- // 破裂边缘发光:三层不同粗细透明度的描边叠加,无shadowBlur
if (glow > 0.05) {
- // 外层泛光:粗线低透明度,模拟大范围弥散
ctx.strokeStyle = `rgba(150, 210, 255, ${glow * 0.25})`;
ctx.lineWidth = 10;
ctx.stroke();
- // 中层光晕:中等粗细
ctx.strokeStyle = `rgba(200, 230, 255, ${glow * 0.5})`;
ctx.lineWidth = 4.5;
ctx.stroke();
- // 内层高亮核心:细线高透明度
ctx.strokeStyle = `rgba(255, 255, 255, ${glow})`;
ctx.lineWidth = 1.2;
ctx.stroke();
@@ -661,10 +643,20 @@ function drawShard(shard) {
ctx.save();
ctx.clip();
- const srcCanvas = shard.type === 'text' ? textCanvas : clockCanvas;
- ctx.drawImage(srcCanvas, 0, 0);
- ctx.restore();
+ // 阶段2:文字碎片内容渐隐为时钟
+ if (shard.type === 'text' && reformProgress > 0) {
+ ctx.globalAlpha = 1 - reformProgress;
+ ctx.drawImage(textCanvas, 0, 0);
+ ctx.globalAlpha = reformProgress;
+ ctx.drawImage(clockCanvas, 0, 0);
+ ctx.globalAlpha = 1;
+ } else {
+ const srcCanvas = shard.type === 'text' ? textCanvas : clockCanvas;
+ ctx.drawImage(srcCanvas, 0, 0);
+ }
+
+ ctx.restore();
ctx.restore();
}
@@ -708,7 +700,67 @@ function animate() {
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.fill();
ctx.drawImage(clockCanvas, 0, 0);
- } else {
+ } else if (state === 'reforming') {
+ const elapsed = performance.now() - reformState.startTime;
+ const moveDur = reformState.moveDuration;
+ const fadeDur = reformState.fadeDuration;
+
+ // 判断当前处于哪个阶段
+ let phase, moveT = 0, fadeT = 0;
+ if (elapsed <= moveDur) {
+ phase = 1;
+ moveT = elapsed / moveDur;
+ } else if (elapsed <= moveDur + fadeDur) {
+ phase = 2;
+ moveT = 1;
+ fadeT = (elapsed - moveDur) / fadeDur;
+ } else {
+ phase = 2;
+ moveT = 1;
+ fadeT = 1;
+ }
+
+ const easedMove = easeInOutCubic(moveT);
+
+ // 更新每个碎片的位移和旋转,平滑归位
+ for (const init of reformState.initialStates) {
+ const s = init.shard;
+ s.offsetX = init.startOffsetX * (1 - easedMove);
+ s.offsetY = init.startOffsetY * (1 - easedMove);
+ // 旋转走最短路径归零
+ let rot = init.startRotation;
+ while (rot > Math.PI) rot -= 2 * Math.PI;
+ while (rot < -Math.PI) rot += 2 * Math.PI;
+ s.rotation = rot * (1 - easedMove);
+ s.vx = 0; s.vy = 0; s.rotationSpeed = 0;
+ }
+
+ // 统一背景圆透明度:阶段1从0升到0.45(填补缝隙),阶段2保持0.45
+ const bgAlpha = 0.45 * easedMove;
+ if (bgAlpha > 0.01) {
+ ctx.fillStyle = `rgba(255, 255, 255, ${bgAlpha})`;
+ ctx.beginPath();
+ ctx.arc(cx, cy, radius, 0, Math.PI * 2);
+ ctx.fill();
+ }
+
+ // 碎片各自背景填充:阶段1保持0.45,阶段2从0.45降到0(已被统一背景接管)
+ const shardBgAlpha = phase === 1 ? 0.45 : 0.45 * (1 - fadeT);
+ // 内容渐隐只在阶段2进行
+ const reformProgress = phase === 1 ? 0 : fadeT;
+
+ for (const s of shards) {
+ drawShard(s, reformProgress, shardBgAlpha);
+ }
+
+ // 动画结束,切回完整状态
+ if (elapsed > moveDur + fadeDur) {
+ state = 'intact';
+ shards = [];
+ reformState = null;
+ hint.classList.remove('hidden');
+ }
+ } else { // shattered
for (const s of shards) {
s.offsetX += s.vx;
s.offsetY += s.vy;
@@ -719,7 +771,7 @@ function animate() {
}
resolveCollisions();
for (const s of shards) {
- drawShard(s);
+ drawShard(s, 0, 0.45);
}
}