Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 68 additions & 80 deletions apps/client/src/preview-entry.ts
Original file line number Diff line number Diff line change
@@ -1,109 +1,97 @@
import {
AVATAR_HAIR_STYLES,
type AvatarAppearance,
type AvatarHairStyle,
DEFAULT_AVATAR_APPEARANCE,
} from "@tilezo/protocol/appearance";
import { Application, Container, Graphics } from "pixi.js";
import { Avatar, type AvatarRenderDirection, drawAvatarBody } from "./game/Avatar";
import { Application, Container, Graphics, Rectangle } from "pixi.js";
import { Avatar, drawAvatarBody } from "./game/Avatar";

const hairStyles: readonly AvatarHairStyle[] = AVATAR_HAIR_STYLES;

const bodyVariants: {
label: string;
appearance: AvatarAppearance;
direction: AvatarRenderDirection;
}[] = hairStyles.map((hair) => ({
label: `${hair}`,
appearance: { ...DEFAULT_AVATAR_APPEARANCE, hair },
direction: "south",
}));

const bubbleVariants: { label: string; appearance: AvatarAppearance }[] = hairStyles.map(
(hair) => ({
label: `bubble: ${hair}`,
appearance: { ...DEFAULT_AVATAR_APPEARANCE, hair },
}),
);

const grid = document.getElementById("grid");

if (!grid) {
throw new Error("Preview grid element is missing");
}

for (const variant of bodyVariants) {
// A single shared offscreen renderer (one WebGL context) snapshots every avatar variant
// into its own static 2D canvas. The previous harness created one Pixi Application — and
// therefore one WebGL context — per grid cell, which silently broke once the grid grew
// past the browser's live-context limit (~16): the excess contexts were lost and their
// shaders failed to compile.
const app = new Application();
await app.init({
antialias: false,
backgroundAlpha: 0,
width: 256,
height: 256,
roundPixels: true,
});
app.ticker.stop();

function addCell(label: string, view: HTMLCanvasElement, width: number, height: number): void {
const cell = document.createElement("div");
cell.className = "cell";
const label = document.createElement("div");
label.className = "label";
label.textContent = variant.label;
cell.appendChild(label);
grid.appendChild(cell);

const app = new Application();
await app.init({
antialias: false,
autoDensity: true,
backgroundAlpha: 0,
width: 120,
height: 160,
roundPixels: true,
});
app.canvas.style.width = "120px";
app.canvas.style.height = "160px";
app.canvas.style.imageRendering = "pixelated";
const labelElement = document.createElement("div");
labelElement.className = "label";
labelElement.textContent = label;
view.style.width = `${width.toString()}px`;
view.style.height = `${height.toString()}px`;
view.style.imageRendering = "pixelated";
cell.append(labelElement, view);
grid?.appendChild(cell);
}

const container = new Container();
container.scale.set(3);
container.x = 60;
container.y = 130;
app.stage.addChild(container);
function snapshot(target: Container, width: number, height: number): HTMLCanvasElement {
return app.renderer.extract.canvas({
target,
frame: new Rectangle(0, 0, width, height),
}) as HTMLCanvasElement;
}

for (const hair of hairStyles) {
const root = new Container();
const inner = new Container();
inner.scale.set(3);
inner.x = 60;
inner.y = 130;
const body = new Graphics();
container.addChild(body);
inner.addChild(body);
root.addChild(inner);
drawAvatarBody(body, {
appearance: variant.appearance,
direction: variant.direction,
appearance: { ...DEFAULT_AVATAR_APPEARANCE, hair },
direction: "south",
animationState: "idle",
stepFrame: 0,
});

cell.appendChild(app.canvas);
addCell(hair, snapshot(root, 120, 160), 120, 160);
root.destroy({ children: true });
}

for (const variant of bubbleVariants) {
const cell = document.createElement("div");
cell.className = "cell";
const label = document.createElement("div");
label.className = "label";
label.textContent = variant.label;
cell.appendChild(label);
grid.appendChild(cell);

const app = new Application();
await app.init({
antialias: false,
autoDensity: true,
backgroundAlpha: 0,
width: 240,
height: 80,
roundPixels: true,
});
app.canvas.style.width = "240px";
app.canvas.style.height = "80px";
app.canvas.style.imageRendering = "pixelated";

const stageContainer = new Container();
stageContainer.scale.set(2);
stageContainer.x = 60;
stageContainer.y = 130;
app.stage.addChild(stageContainer);

const avatar = new Avatar("preview", "Preview", { x: 0, y: 0 }, variant.appearance);
stageContainer.addChild(avatar.view);
stageContainer.addChild(avatar.overlayView);
for (const hair of hairStyles) {
const root = new Container();
const inner = new Container();
inner.scale.set(2);
inner.x = 60;
inner.y = 130;
const avatar = new Avatar(
"preview",
"Preview",
{ x: 0, y: 0 },
{
...DEFAULT_AVATAR_APPEARANCE,
hair,
},
);
inner.addChild(avatar.view, avatar.overlayView);
root.addChild(inner);
avatar.say("Hello!");
// Advance the avatar so the chat bubble has animated into view before the snapshot.
for (let frame = 0; frame < 16; frame += 1) {
avatar.update(0.05);
}

cell.appendChild(app.canvas);
addCell(`bubble: ${hair}`, snapshot(root, 240, 80), 240, 80);
avatar.destroy();
}
Loading