v3.5.16小程序首页动画
This commit is contained in:
@@ -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 开场图(仅 weapp;H5 的 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);
|
||||
}
|
||||
@@ -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 开场图(仅 weapp;H5 的 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(() => {});
|
||||
}
|
||||
Reference in New Issue
Block a user