fix(frame): give OLD FILM PORTRAIT a real portrait output

The sheet was stretched over the visitor's frame, so the portrait variant
only turned the paper inside whatever shape the photo had: a landscape
photo stayed landscape. The film now has its own opening like the walls
do — the frame is scaled up to the photo, the photo is cover-cropped into
the whole sheet, and `old-film-portrait` is the PNG turned 90° CW, so the
pair is one frame standing and one lying. The torn edge has no straight
sides, so the photo runs under all of it instead of being cut against a
measured window.
This commit is contained in:
2026-10-02 19:54:42 +07:00
parent 177e3c3a9c
commit 8d0a46e7f5
+53 -22
View File
@@ -765,11 +765,13 @@ function loadWallframe(assets: RenderAssets) {
}
// 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.
// it is, `old-film-portrait` turns it 90° CW, so the pair is one frame standing
// and one lying — the same two shapes the walls offer, in a sheet whose middle
// is a transparent window a couple of percent of paper wide around it.
const isOldFilm = (id: FrameId) => id === 'old-film' || id === 'old-film-portrait';
// A frame that carries its own opening: the aspect chip has nothing to say to
// it (both walls, both films).
const hasOwnAspect = (id: FrameId) => isWallframe(id) || isOldFilm(id);
function loadOldFilm(assets: RenderAssets) {
if (oldFilmImage) return oldFilmImage;
@@ -812,10 +814,10 @@ export async function renderPhoto(input: RenderInput): Promise<RenderResult | nu
const aspectWH = options?.aspect && options.aspect !== 'full' ? ASPECT_RATIO_W_H[options.aspect] : null;
const cropRect = options?.cropRect ?? null;
const ratioWH = cropRect ? null : options?.cropWH ?? aspectWH;
// A wall is an artwork with its own opening, so the aspect chip (ratioWH)
// has nothing to say there — but the visitor's own crop does, or a photo
// that was cropped and applied comes back whole behind the frame.
if (cropRect || (ratioWH && !isWallframe(frameId))) {
// A wall or a film is an artwork with its own opening, so the aspect chip
// (ratioWH) has nothing to say there — but the visitor's own crop does, or a
// photo that was cropped and applied comes back whole behind the frame.
if (cropRect || (ratioWH && !hasOwnAspect(frameId))) {
const curAspect = width / height;
let cropW = width;
let cropH = height;
@@ -1460,36 +1462,65 @@ export async function renderPhoto(input: RenderInput): Promise<RenderResult | nu
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.
// The sheet has the frame's SHAPE, like the walls: `old-film-portrait` is
// the PNG turned 90° CW, so the pair is one standing frame and one lying.
// The sheet's own 914x609 is far too small to hold a photo, so the frame
// is scaled UP to the photo instead of the photo down to the frame: the
// deckle stays a couple of percent of the print at any export size, which
// is also why the photo is cover-cropped into the whole sheet and not
// into a measured window — the sheet's edge is torn, so it has no straight
// sides to cut against; the photo simply runs under all of it.
flush(surface);
const photoImg = own(surface.makeImageSnapshot());
cardSource = photoImg;
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();
const sheetW = portrait ? film.height() : film.width();
const sheetH = portrait ? film.width() : film.height();
const scale = Math.max(width / sheetW, height / sheetH);
const outW = Math.round(sheetW * scale);
const outH = Math.round(sheetH * scale);
const cardSurface = createSurface(outW, outH);
if (!cardSurface) return null;
own(cardSurface);
const cardCanvas = cardSurface.getCanvas();
const plainPaint = own(Skia.Paint());
const cover = Math.max(outW / width, outH / height);
const coverW = outW / cover;
const coverH = outH / cover;
cardCanvas.drawImageRectOptions(
photoImg,
Skia.XYWHRect((width - coverW) / 2, (height - coverH) / 2, coverW, coverH),
Skia.XYWHRect(0, 0, outW, outH),
Skia.FilterMode.Linear,
Skia.MipmapMode.None,
plainPaint
);
cardCanvas.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);
cardCanvas.translate(outW, 0);
cardCanvas.rotate(90, 0, 0);
}
canvas.drawImageRectOptions(
cardCanvas.drawImageRectOptions(
film,
Skia.XYWHRect(0, 0, film.width(), film.height()),
portrait ? Skia.XYWHRect(0, 0, height, width) : Skia.XYWHRect(0, 0, width, height),
portrait ? Skia.XYWHRect(0, 0, outH, outW) : Skia.XYWHRect(0, 0, outW, outH),
Skia.FilterMode.Linear,
Skia.MipmapMode.None,
filmPaint
plainPaint
);
canvas.restore();
cardCanvas.restore();
outputSurface = cardSurface;
stampCanvas = cardCanvas;
stampW = outW;
stampH = outH;
} else {
drawFrameOnCanvas(canvas, width, height, frameId);
}