实现了 REFORM TIME 的视觉动态,根据碎片路径带动画地重新拼合并渐隐去除文字部分
This commit is contained in:
1 parent
9ee1022865
commit
820c8ab35f
1 file changed
+131
-79
+131
-79
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user