feat(frame): add OLD FILM LANDSCAPE and OLD FILM PORTRAIT from old_film.png

This commit is contained in:
2026-10-02 19:46:06 +07:00
parent 256fe6c2e2
commit 84b8e64652
8 changed files with 68 additions and 4 deletions
+9 -2
View File
@@ -34,13 +34,20 @@ export function probeGpu(): boolean {
export async function loadEngine(): Promise<{ assets: RenderAssets; gpu: boolean }> {
await initSkia({ locateFile: (file) => `/wasm/${file}` });
if (!assets) {
const [textFont, emojiFont, wallframePng, markFonts] = await Promise.all([
const [textFont, emojiFont, wallframePng, oldFilmPng, markFonts] = await Promise.all([
buf('/assets/Cousine-Regular.ttf'),
buf('/assets/NotoEmoji-GPS.ttf'),
buf('/assets/wallframe.png'),
buf('/assets/old_film.png'),
Promise.all(MARK_FACES.map(async (face) => ({ family: face.family, bytes: await buf(face.file) }))),
]);
assets = { textFont, emojiFont, wallframe: new Uint8Array(wallframePng), markFonts };
assets = {
textFont,
emojiFont,
wallframe: new Uint8Array(wallframePng),
oldFilm: new Uint8Array(oldFilmPng),
markFonts,
};
}
return { assets, gpu: probeGpu() };
}
@@ -76,6 +76,7 @@ export interface RenderAssets {
textFont: ArrayBuffer; // assets/Cousine-Regular.ttf
emojiFont: ArrayBuffer; // assets/NotoEmoji-GPS.ttf
wallframe: Uint8Array; // wallframe.png
oldFilm: Uint8Array; // old_film.png
// The FONT option's faces beside DEFAULT (see assets.ts MARK_FACES).
markFonts: { family: string; bytes: ArrayBuffer }[];
}
@@ -690,6 +691,7 @@ function replaceThrough(
let faces: { typeface: any; iconTypeface: any } | null = null;
let wallframeImage: any = null;
let oldFilmImage: any = null;
function loadFaces(assets: RenderAssets) {
if (faces) return faces;
@@ -762,6 +764,19 @@ function loadWallframe(assets: RenderAssets) {
return wallframeImage;
}
// Both films are the same sheet of distressed paper: `old-film` lays it down as
// it is, `old-film-portrait` turns it 90° CW so the torn edge and the dust lie
// the other way round. Unlike the walls the sheet has no mat of its own — its
// middle is a transparent window and its opaque part is a border a couple of
// percent wide — so it is drawn OVER the photo and never crops it.
const isOldFilm = (id: FrameId) => id === 'old-film' || id === 'old-film-portrait';
function loadOldFilm(assets: RenderAssets) {
if (oldFilmImage) return oldFilmImage;
oldFilmImage = Skia.Image.MakeImageFromEncoded(assets.oldFilm);
return oldFilmImage;
}
export async function renderPhoto(input: RenderInput): Promise<RenderResult | null> {
const { recipe, frameId, options } = input;
const owned: SkDisposable[] = [];
@@ -1444,6 +1459,37 @@ export async function renderPhoto(input: RenderInput): Promise<RenderResult | nu
stampCanvas = cardCanvas;
stampW = outW;
stampH = outH;
} else if (isOldFilm(frameId)) {
// The sheet is stretched over the whole frame and drawn on top of the
// photo, so the paper eats the outer percent of the picture instead of
// cropping it — the inverse of the walls above, whose mat window crops.
// The stretch is deliberately non-uniform: a cover-scale of a 3:2 sheet
// onto a 4:3 frame would cut the torn edge off two sides, and the deckle
// is the whole point of the frame.
const film = loadOldFilm(input.assets);
if (!film) {
console.error('Failed to decode old_film.png');
return null;
}
const portrait = frameId === 'old-film-portrait';
const filmPaint = own(Skia.Paint());
canvas.save();
if (portrait) {
// The wall-frame portrait's own turn: origin to the top-right corner,
// then 90° CW, so the sheet's width runs down the frame and the drawn
// rect is the frame's height by its width.
canvas.translate(width, 0);
canvas.rotate(90, 0, 0);
}
canvas.drawImageRectOptions(
film,
Skia.XYWHRect(0, 0, film.width(), film.height()),
portrait ? Skia.XYWHRect(0, 0, height, width) : Skia.XYWHRect(0, 0, width, height),
Skia.FilterMode.Linear,
Skia.MipmapMode.None,
filmPaint
);
canvas.restore();
} else {
drawFrameOnCanvas(canvas, width, height, frameId);
}