| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184 |
- import type { Graphics } from 'pixi.js';
- import { OUTLINE_COLOR } from '../config';
- /** Everything a painter needs to draw one frame. Local space: facing right, body radius `r`, origin at the center. */
- export interface Pose {
- /** Seconds since the view was created. */
- time: number;
- /** Walk cycle phase (radians). */
- walk: number;
- moving: boolean;
- grounded: boolean;
- attack: boolean;
- special: boolean;
- stunned: boolean;
- /** Unit look direction in local space. */
- lookX: number;
- lookY: number;
- /** 0 at base size, 1 at max size. */
- fat: number;
- /** Player color. */
- color: number;
- /** Outline width in local units. */
- ink: number;
- }
- export type Painter = (g: Graphics, r: number, pose: Pose) => void;
- const INK = OUTLINE_COLOR;
- function ellipsePoly(cx: number, cy: number, rx: number, ry: number, angle: number, steps = 20): number[] {
- const pts: number[] = [];
- const c = Math.cos(angle);
- const s = Math.sin(angle);
- for (let i = 0; i < steps; i++) {
- const a = (i / steps) * Math.PI * 2;
- const x = Math.cos(a) * rx;
- const y = Math.sin(a) * ry;
- pts.push(cx + x * c - y * s, cy + x * s + y * c);
- }
- return pts;
- }
- function eyes(g: Graphics, pose: Pose, spots: [number, number][], size: number): void {
- for (const [ex, ey] of spots) {
- if (pose.stunned) {
- const d = size * 0.6;
- g.moveTo(ex - d, ey - d).lineTo(ex + d, ey + d).moveTo(ex + d, ey - d).lineTo(ex - d, ey + d).stroke({ width: pose.ink, color: INK, cap: 'round' });
- continue;
- }
- g.ellipse(ex, ey, size, size * 1.2).fill(0xffffff).stroke({ width: pose.ink * 0.8, color: INK });
- const px = ex + pose.lookX * size * 0.4;
- const py = ey + pose.lookY * size * 0.45;
- g.circle(px, py, size * 0.5).fill(INK);
- g.circle(px - size * 0.18, py - size * 0.2, size * 0.17).fill(0xffffff);
- }
- }
- function foot(g: Graphics, x: number, y: number, r: number, color: number, ink: number): void {
- g.ellipse(x, y, r * 0.24, r * 0.13).fill(color).stroke({ width: ink, color: INK });
- }
- function stars(g: Graphics, r: number, pose: Pose): void {
- if (!pose.stunned) return;
- for (let i = 0; i < 3; i++) {
- const a = pose.time * 6 + (i * Math.PI * 2) / 3;
- const cx = Math.cos(a) * r * 0.8;
- const cy = -r * 1.25 + Math.sin(a) * r * 0.2;
- const pts: number[] = [];
- for (let k = 0; k < 10; k++) {
- const rr = k % 2 === 0 ? r * 0.2 : r * 0.08;
- const aa = (k / 10) * Math.PI * 2 - Math.PI / 2;
- pts.push(cx + Math.cos(aa) * rr, cy + Math.sin(aa) * rr);
- }
- g.poly(pts).fill(0xffe14d).stroke({ width: pose.ink * 0.6, color: INK });
- }
- }
- /** Castor Glouton: brown, big teeth, paddle tail, cheeks that swell as he eats. */
- export const paintCastor: Painter = (g, r, pose) => {
- const fur = 0xa0652d;
- const dark = 0x6b4226;
- const step = pose.moving && pose.grounded ? Math.sin(pose.walk) : 0;
- const lift = pose.moving && pose.grounded ? Math.abs(Math.cos(pose.walk)) * 0.12 * r : 0;
- const chomp = pose.special && Math.sin(pose.time * 28) > 0;
- const mouthOpen = pose.attack || chomp;
- // Tail (behind), wagging.
- const wag = pose.moving ? Math.sin(pose.walk * 0.5) * 0.15 : Math.sin(pose.time * 2) * 0.06;
- g.poly(ellipsePoly(-0.95 * r, 0.42 * r, 0.62 * r, 0.24 * r, -0.35 + wag)).fill(dark).stroke({ width: pose.ink, color: INK });
- for (let i = -1; i <= 1; i++) {
- const ox = -0.95 * r + i * 0.22 * r;
- g.moveTo(ox - 0.08 * r, 0.3 * r + i * 0.08 * r).lineTo(ox + 0.1 * r, 0.52 * r + i * 0.08 * r).stroke({ width: pose.ink * 0.5, color: 0x4a2c17 });
- }
- foot(g, -0.38 * r - step * 0.18 * r, 0.93 * r - (step < 0 ? lift : 0), r, dark, pose.ink);
- // Body + belly.
- g.circle(0, 0, r).fill(fur).stroke({ width: pose.ink, color: INK });
- g.ellipse(0.22 * r, 0.32 * r, 0.52 * r, 0.5 * r).fill(0xe0b07a);
- // Fur tufts.
- g.moveTo(-0.55 * r, -0.2 * r).lineTo(-0.42 * r, -0.12 * r).moveTo(-0.6 * r, 0.05 * r).lineTo(-0.46 * r, 0.1 * r).stroke({ width: pose.ink * 0.5, color: dark });
- // Ears.
- for (const [ex, ey, er] of [
- [-0.32, -0.86, 0.21],
- [0.22, -0.94, 0.18],
- ]) {
- g.circle(ex * r, ey * r, er * r).fill(fur).stroke({ width: pose.ink, color: INK });
- g.circle(ex * r, ey * r, er * r * 0.5).fill(0xf2a5a5);
- }
- // Cheeks swell with food.
- const cheek = 0.2 + pose.fat * 0.22 + (pose.special ? 0.04 : 0);
- g.circle(0.5 * r, 0.22 * r, cheek * r).fill(0xb8763b).stroke({ width: pose.ink * 0.7, color: INK });
- g.circle(0.44 * r, 0.16 * r, cheek * r * 0.35).fill({ color: 0xff8fa3, alpha: 0.6 });
- eyes(
- g,
- pose,
- [
- [0.3 * r, -0.34 * r],
- [0.64 * r, -0.3 * r],
- ],
- 0.17 * r,
- );
- // Mouth and the famous teeth.
- const jaw = mouthOpen ? 0.22 * r : 0;
- if (mouthOpen) g.ellipse(0.8 * r, 0.24 * r, 0.2 * r, 0.12 * r + jaw * 0.5).fill(0x5a1e1e).stroke({ width: pose.ink * 0.8, color: INK });
- for (const tx of [0.66, 0.8]) {
- g.roundRect(tx * r, 0.05 * r, 0.13 * r, 0.26 * r, 0.03 * r).fill(0xfffbe8).stroke({ width: pose.ink * 0.7, color: INK });
- }
- // Nose and whiskers.
- g.ellipse(0.96 * r, -0.06 * r, 0.14 * r, 0.1 * r).fill(INK);
- g.circle(0.93 * r, -0.09 * r, 0.04 * r).fill(0xffffff);
- g.moveTo(0.98 * r, 0.02 * r)
- .lineTo(1.32 * r, -0.08 * r)
- .moveTo(0.98 * r, 0.06 * r)
- .lineTo(1.32 * r, 0.1 * r)
- .stroke({ width: pose.ink * 0.4, color: INK });
- foot(g, 0.36 * r + step * 0.18 * r, 0.93 * r - (step > 0 ? lift : 0), r, dark, pose.ink);
- stars(g, r, pose);
- };
- /** The Tester: a squishy jelly bean with a sprout on its head. */
- export const paintGeneric: Painter = (g, r, pose) => {
- const step = pose.moving && pose.grounded ? Math.sin(pose.walk) : 0;
- const dark = darken(pose.color, 0.7);
- foot(g, -0.35 * r - step * 0.18 * r, 0.93 * r, r, dark, pose.ink);
- // Sprout.
- const sway = Math.sin(pose.time * 3) * 0.1 * r;
- g.moveTo(0, -r).quadraticCurveTo(0.05 * r, -1.25 * r, sway, -1.4 * r).stroke({ width: pose.ink, color: INK });
- g.poly(ellipsePoly(sway + 0.18 * r, -1.42 * r, 0.2 * r, 0.1 * r, -0.4)).fill(0x7ccf5a).stroke({ width: pose.ink * 0.7, color: INK });
- g.circle(0, 0, r).fill(pose.color).stroke({ width: pose.ink, color: INK });
- g.ellipse(-0.35 * r, -0.45 * r, 0.25 * r, 0.15 * r).fill({ color: 0xffffff, alpha: 0.55 });
- g.circle(0.55 * r, 0.25 * r, 0.14 * r).fill({ color: 0xff3b6b, alpha: 0.35 });
- eyes(
- g,
- pose,
- [
- [0.12 * r, -0.2 * r],
- [0.5 * r, -0.18 * r],
- ],
- 0.2 * r,
- );
- if (pose.attack) g.ellipse(0.4 * r, 0.32 * r, 0.16 * r, 0.14 * r).fill(0x5a1e1e).stroke({ width: pose.ink * 0.7, color: INK });
- else g.moveTo(0.18 * r, 0.3 * r).quadraticCurveTo(0.38 * r, 0.45 * r, 0.6 * r, 0.28 * r).stroke({ width: pose.ink * 0.8, color: INK, cap: 'round' });
- foot(g, 0.35 * r + step * 0.18 * r, 0.93 * r, r, dark, pose.ink);
- stars(g, r, pose);
- };
- function darken(color: number, k: number): number {
- return (Math.round((color >> 16) * k) << 16) | (Math.round(((color >> 8) & 0xff) * k) << 8) | Math.round((color & 0xff) * k);
- }
- export const PAINTERS: Record<string, Painter> = {
- castor: paintCastor,
- generic: paintGeneric,
- };
|