v3.5.16小程序首页动画

This commit is contained in:
2026-08-30 15:07:58 +08:00
parent 9c8d5f2cad
commit b375fab44a
13 changed files with 546 additions and 759 deletions
+21
View File
@@ -0,0 +1,21 @@
import Taro from '@tarojs/taro';
import { HOME_SPLASH_BOTTLE_URL } from '@dukang/shared-types';
/** 冷启动会话内是否已播过首页开场(进程级,切 Tab 不重播) */
let played = false;
export function hasHomeSplashPlayed() {
return played;
}
export function markHomeSplashPlayed() {
played = true;
}
/** 冷启动预拉 OSS 开场图(仅 weappH5 的 getImageInfo 会走 CORS */
export function prefetchHomeSplashAssets() {
if (played) return;
if (process.env.TARO_ENV !== 'weapp') return;
void Taro.getImageInfo({ src: HOME_SPLASH_BOTTLE_URL }).catch(() => {});
}
@@ -1,316 +0,0 @@
/** Canvas 金龙:盘成一圈,仿照立体金龙的鳞片、须、角、爪与光晕 */
export type DragonCanvasNode = {
width: number;
height: number;
getContext: (type: '2d') => CanvasRenderingContext2D;
requestAnimationFrame?: (cb: (time: number) => void) => number;
cancelAnimationFrame?: (id: number) => void;
};
type SpinePt = {
x: number;
y: number;
ang: number;
nx: number;
ny: number;
w: number;
};
const GOLD_HI = '#fff6c8';
const GOLD = '#ffbf00';
const GOLD_MID = '#e8a800';
const GOLD_DEEP = '#b87500';
function lerp(a: number, b: number, t: number) {
return a + (b - a) * t;
}
function fillOval(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
rw: number,
rh: number,
rot: number,
) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(rot);
ctx.scale(Math.max(0.01, rw), Math.max(0.01, rh));
ctx.beginPath();
ctx.arc(0, 0, 1, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function easeInCubic(t: number) {
return t * t * t;
}
function buildSpine(cx: number, cy: number, r: number, phase: number, segs: number): SpinePt[] {
const pts: SpinePt[] = [];
const turns = 0.94;
for (let i = 0; i < segs; i++) {
const u = i / (segs - 1);
const ang = -Math.PI / 2 + u * Math.PI * 2 * turns;
const wobble = Math.sin(u * 14 + phase) * r * 0.042 + Math.sin(u * 5.5 - phase * 0.7) * r * 0.02;
const rr = r + wobble;
const nx = Math.cos(ang);
const ny = Math.sin(ang);
pts.push({
x: cx + nx * rr,
y: cy + ny * rr,
ang,
nx,
ny,
w: lerp(20, 6.5, u ** 0.62),
});
}
return pts;
}
function strokeRibbon(
ctx: CanvasRenderingContext2D,
pts: SpinePt[],
widthScale: number,
color: string,
alpha: number,
) {
if (pts.length < 2) return;
ctx.save();
ctx.globalAlpha = alpha;
ctx.strokeStyle = color;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
ctx.lineWidth = pts[Math.floor(pts.length * 0.15)].w * widthScale;
ctx.stroke();
ctx.restore();
}
function drawScales(ctx: CanvasRenderingContext2D, pts: SpinePt[]) {
for (let i = 2; i < pts.length - 1; i += 1) {
const p = pts[i];
const u = i / (pts.length - 1);
const ox = p.x + p.nx * p.w * 0.18;
const oy = p.y + p.ny * p.w * 0.18;
ctx.save();
ctx.translate(ox, oy);
ctx.rotate(p.ang + Math.PI / 2);
ctx.fillStyle = i % 2 === 0 ? GOLD_HI : GOLD;
ctx.globalAlpha = 0.55 + (1 - u) * 0.25;
fillOval(ctx, 0, 0, p.w * 0.55, p.w * 0.38, 0);
ctx.restore();
}
}
function drawSpines(ctx: CanvasRenderingContext2D, pts: SpinePt[]) {
ctx.fillStyle = GOLD_HI;
for (let i = 3; i < pts.length - 6; i += 3) {
const p = pts[i];
const len = p.w * 1.35;
ctx.save();
ctx.globalAlpha = 0.85;
ctx.beginPath();
ctx.moveTo(p.x + p.nx * p.w * 0.2, p.y + p.ny * p.w * 0.2);
ctx.lineTo(
p.x + p.nx * (p.w + len),
p.y + p.ny * (p.w + len),
);
const tx = -p.ny;
const ty = p.nx;
ctx.lineTo(p.x + tx * 2.2, p.y + ty * 2.2);
ctx.closePath();
ctx.fill();
ctx.restore();
}
}
function drawClaw(ctx: CanvasRenderingContext2D, p: SpinePt, side: number) {
const tx = -p.ny * side;
const ty = p.nx * side;
const baseX = p.x + tx * p.w * 0.7;
const baseY = p.y + ty * p.w * 0.7;
ctx.save();
ctx.translate(baseX, baseY);
ctx.rotate(Math.atan2(ty, tx));
ctx.fillStyle = GOLD;
ctx.strokeStyle = GOLD_DEEP;
ctx.lineWidth = 0.8;
for (let k = -1; k <= 1; k++) {
ctx.beginPath();
ctx.moveTo(0, k * 4);
ctx.quadraticCurveTo(10, k * 6 - 2, 18, k * 5);
ctx.quadraticCurveTo(10, k * 4, 0, k * 3);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
ctx.restore();
}
function drawHead(ctx: CanvasRenderingContext2D, p: SpinePt, phase: number) {
ctx.save();
ctx.translate(p.x + p.nx * 10, p.y + p.ny * 10);
ctx.rotate(Math.atan2(p.ny, p.nx) + Math.PI / 2);
const mane = 6;
for (let i = 0; i < mane; i++) {
const a = -0.9 + (i / (mane - 1)) * 1.8;
ctx.beginPath();
ctx.strokeStyle = i % 2 ? GOLD_HI : GOLD;
ctx.globalAlpha = 0.7;
ctx.lineWidth = 2.2;
ctx.moveTo(Math.sin(a) * 6, -4);
ctx.quadraticCurveTo(Math.sin(a) * 16, -18 - Math.sin(phase + i) * 3, Math.sin(a) * 8, -28);
ctx.stroke();
}
ctx.globalAlpha = 1;
ctx.beginPath();
ctx.moveTo(-7, -18);
ctx.quadraticCurveTo(-16, -32, -5, -38);
ctx.quadraticCurveTo(-2, -26, -3, -16);
ctx.fillStyle = GOLD_MID;
ctx.fill();
ctx.beginPath();
ctx.moveTo(7, -18);
ctx.quadraticCurveTo(16, -32, 5, -38);
ctx.quadraticCurveTo(2, -26, 3, -16);
ctx.fill();
const g = ctx.createRadialGradient(-4, -4, 2, 0, 4, 20);
g.addColorStop(0, GOLD_HI);
g.addColorStop(0.45, GOLD);
g.addColorStop(1, GOLD_DEEP);
ctx.fillStyle = g;
fillOval(ctx, 0, 2, 16, 18, 0);
ctx.fillStyle = GOLD_MID;
fillOval(ctx, 0, 10, 9, 8, 0);
for (const sx of [-6.5, 6.5]) {
ctx.fillStyle = '#3a1a00';
fillOval(ctx, sx, -2, 3.2, 3.6, 0);
ctx.fillStyle = '#ffe566';
fillOval(ctx, sx, -2.4, 1.5, 1.7, 0);
ctx.fillStyle = '#fff';
fillOval(ctx, sx - 0.5, -3, 0.6, 0.6, 0);
}
ctx.strokeStyle = GOLD_HI;
ctx.lineWidth = 1.15;
ctx.globalAlpha = 0.9;
for (const side of [-1, 1]) {
ctx.beginPath();
ctx.moveTo(side * 12, 6);
ctx.quadraticCurveTo(side * 36, 10 + Math.sin(phase) * 2, side * 42, 22);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(side * 10, 9);
ctx.quadraticCurveTo(side * 28, 18, side * 34, 28);
ctx.stroke();
}
ctx.globalAlpha = 1;
ctx.restore();
}
function drawSparks(
ctx: CanvasRenderingContext2D,
cx: number,
cy: number,
r: number,
phase: number,
) {
for (let i = 0; i < 28; i++) {
const a = (i / 28) * Math.PI * 2 + phase * 0.35;
const rr = r * (0.72 + ((i * 17) % 10) / 40);
const x = cx + Math.cos(a) * rr + Math.sin(phase * 1.4 + i) * 4;
const y = cy + Math.sin(a) * rr + Math.cos(phase * 1.1 + i) * 3;
const s = 1.1 + (i % 5) * 0.35;
ctx.beginPath();
ctx.globalAlpha = 0.25 + (Math.sin(phase * 2 + i) + 1) * 0.25;
ctx.fillStyle = i % 3 === 0 ? GOLD_HI : GOLD;
ctx.arc(x, y, s, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
export function drawJiuzuDragonFrame(
ctx: CanvasRenderingContext2D,
width: number,
height: number,
elapsedMs: number,
) {
const cx = width / 2;
const cy = height * 0.42;
const radius = Math.min(width, height) * 0.3;
const fadeIn = Math.min(1, elapsedMs / 380);
const spinT = Math.min(1, Math.max(0, (elapsedMs - 120) / 2050));
const flyT = Math.min(1, Math.max(0, (elapsedMs - 2200) / 1200));
const spin = spinT * Math.PI * 2;
const fly = easeInCubic(flyT);
const phase = elapsedMs / 220;
ctx.clearRect(0, 0, width, height);
ctx.save();
ctx.globalAlpha = fadeIn * (1 - fly);
ctx.translate(cx, cy + fly * -height * 0.42);
ctx.scale(1 + fly * 0.55, 1 + fly * 0.55);
ctx.rotate(spin);
ctx.translate(-cx, -cy);
const pts = buildSpine(cx, cy, radius, phase, 56);
strokeRibbon(ctx, pts, 2.4, 'rgba(255, 191, 0, 0.18)', 1);
strokeRibbon(ctx, pts, 1.55, 'rgba(255, 214, 80, 0.4)', 1);
ctx.save();
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
const bodyGrad = ctx.createLinearGradient(cx - radius, cy, cx + radius, cy);
bodyGrad.addColorStop(0, GOLD_DEEP);
bodyGrad.addColorStop(0.5, GOLD);
bodyGrad.addColorStop(1, GOLD_HI);
ctx.strokeStyle = bodyGrad;
ctx.lineWidth = pts[0].w * 1.15;
ctx.shadowColor = 'rgba(255, 191, 0, 0.7)';
ctx.shadowBlur = 16;
ctx.stroke();
ctx.shadowBlur = 0;
ctx.restore();
drawScales(ctx, pts);
drawSpines(ctx, pts);
drawClaw(ctx, pts[Math.floor(pts.length * 0.32)], 1);
drawClaw(ctx, pts[Math.floor(pts.length * 0.68)], -1);
drawHead(ctx, pts[0], phase);
drawSparks(ctx, cx, cy, radius, phase);
ctx.restore();
}
export function scheduleDragonFrame(
canvas: DragonCanvasNode,
cb: (time: number) => void,
): number {
if (typeof canvas.requestAnimationFrame === 'function') {
return canvas.requestAnimationFrame(cb);
}
return requestAnimationFrame(cb);
}
export function cancelDragonFrame(canvas: DragonCanvasNode, id: number) {
if (typeof canvas.cancelAnimationFrame === 'function') {
canvas.cancelAnimationFrame(id);
return;
}
cancelAnimationFrame(id);
}
-22
View File
@@ -1,22 +0,0 @@
import Taro from '@tarojs/taro';
import { JIUZU_SPLASH_GIF_URL, JIUZU_SPLASH_MARK_URL } from '@dukang/shared-types';
/** 冷启动会话内是否已播过「酒祖杜康」开场(进程级,切 Tab 不重播) */
let played = false;
export function hasJiuzuSplashPlayed() {
return played;
}
export function markJiuzuSplashPlayed() {
played = true;
}
/** 冷启动预拉 OSS 开场图(仅 weappH5 的 getImageInfo 会走 CORS */
export function prefetchJiuzuSplashAssets() {
if (played) return;
if (process.env.TARO_ENV !== 'weapp') return;
void Taro.getImageInfo({ src: JIUZU_SPLASH_GIF_URL }).catch(() => {});
void Taro.getImageInfo({ src: JIUZU_SPLASH_MARK_URL }).catch(() => {});
}