drawface-live / js /effects.js
ingon1's picture
sync from a4623ed60bfdc48500dc98de93020c59fe69bf34
4635d45 verified
Raw
History Blame Contribute Delete
3.09 kB
// Expression-triggered sticker effects — canvas-drawn shapes (no emoji font
// dependency) that spawn on strong smiles / surprise / kiss and float up over
// the character. Drawn on the output canvas after the character, so it never
// touches the sprite pipeline.
const TRIGGERS = [
// kind -> test on smoothed blendshapes, threshold, spawn cooldown (ms)
{ kind: "star", test: (b) => (b.mouthSmileLeft + b.mouthSmileRight) / 2, on: 0.45, gap: 220 },
{ kind: "burst", test: (b) => b.jawOpen, on: 0.55, gap: 600 },
{ kind: "heart", test: (b) => b.mouthPucker, on: 0.55, gap: 260 },
];
function star(ctx, r) {
ctx.beginPath();
for (let i = 0; i < 8; i++) {
const a = (Math.PI / 4) * i;
const rad = i % 2 ? r * 0.42 : r;
ctx[i ? "lineTo" : "moveTo"](Math.cos(a) * rad, Math.sin(a) * rad);
}
ctx.closePath();
ctx.fillStyle = "#ffcf3f";
ctx.fill();
}
function heart(ctx, r) {
ctx.beginPath();
ctx.moveTo(0, r * 0.35);
ctx.bezierCurveTo(r, -r * 0.5, r * 0.5, -r, 0, -r * 0.35);
ctx.bezierCurveTo(-r * 0.5, -r, -r, -r * 0.5, 0, r * 0.35);
ctx.closePath();
ctx.fillStyle = "#ff5c8a";
ctx.fill();
}
function burst(ctx, r) {
ctx.strokeStyle = "#ff8a3d";
ctx.lineWidth = Math.max(2, r * 0.16);
ctx.lineCap = "round";
for (let i = 0; i < 6; i++) {
const a = (Math.PI / 3) * i;
ctx.beginPath();
ctx.moveTo(Math.cos(a) * r * 0.4, Math.sin(a) * r * 0.4);
ctx.lineTo(Math.cos(a) * r, Math.sin(a) * r);
ctx.stroke();
}
}
const SHAPES = { star, heart, burst };
export class StickerFx {
constructor(canvasSize = 512) {
this.size = canvasSize;
this.particles = [];
this.cooldown = {};
this.seq = 0;
}
/** Spawn/age particles from the current smoothed blendshapes. */
update(blend, now) {
for (const t of TRIGGERS) {
const v = t.test(blend);
if (v >= t.on && now >= (this.cooldown[t.kind] ?? 0)) {
this.cooldown[t.kind] = now + t.gap;
const strength = Math.min(1, (v - t.on) / (1 - t.on));
const count = 1 + Math.round(strength * 2);
for (let i = 0; i < count; i++) {
this.seq++;
this.particles.push({
x: this.size * (0.26 + 0.48 * ((this.seq * 0.618) % 1)),
y: this.size * 0.34, kind: t.kind,
vx: (((this.seq * 0.37) % 1) - 0.5) * 0.5,
spin: (((this.seq * 0.29) % 1) - 0.5) * 0.02,
born: now, life: 1100 + strength * 500, r: 13 + strength * 8,
});
}
}
}
this.particles = this.particles.filter((p) => now - p.born < p.life);
}
/** Draw active particles onto ctx (rising + fading + spinning). */
draw(ctx, now) {
for (const p of this.particles) {
const age = now - p.born;
const t = age / p.life; // 0..1
ctx.save();
ctx.globalAlpha = Math.max(0, t < 0.15 ? t / 0.15 : 1 - (t - 0.15) / 0.85);
ctx.translate(p.x + p.vx * age * 0.05, p.y - t * this.size * 0.22);
ctx.rotate(p.spin * age);
SHAPES[p.kind](ctx, p.r * (0.7 + 0.3 * t));
ctx.restore();
}
}
}