feat(frame): add OLD FILM LANDSCAPE and OLD FILM PORTRAIT from old_film.png
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
@@ -4,7 +4,9 @@ export type FrameId =
|
||||
| 'classic-white'
|
||||
| 'cinematic'
|
||||
| 'wallframe' // WALL FRAME PORTRAIT — the artwork hung 90° CW
|
||||
| 'wallframe-landscape'; // the same artwork upright: WALL FRAME LANDSCAPE
|
||||
| 'wallframe-landscape' // the same artwork upright: WALL FRAME LANDSCAPE
|
||||
| 'old-film' // OLD FILM LANDSCAPE — the distressed sheet as it is
|
||||
| 'old-film-portrait'; // OLD FILM PORTRAIT — the same sheet turned 90° CW
|
||||
|
||||
// Viewfinder/composition aspect-ratio selector. Convention is an UPRIGHT
|
||||
// (portrait) picture: "4:3" = standing 3:4 photo (w/h = 3/4), "3:2" = standing
|
||||
|
||||
@@ -13,7 +13,10 @@ const KEY = 'RecipesCam::recipe-share::v1';
|
||||
const ALGORITHM = 'xor16-v1';
|
||||
|
||||
const BASE_FILTERS: BaseFilter[] = [...FILM_SIMS.map((s) => s.baseFilter), 'none'];
|
||||
const FRAMES: FrameId[] = ['none', 'polaroid', 'classic-white', 'cinematic', 'wallframe', 'wallframe-landscape'];
|
||||
const FRAMES: FrameId[] = [
|
||||
'none', 'polaroid', 'classic-white', 'cinematic', 'wallframe', 'wallframe-landscape',
|
||||
'old-film', 'old-film-portrait',
|
||||
];
|
||||
|
||||
const hash = (s: string): number => {
|
||||
let h = 0x811c9dc5;
|
||||
|
||||
@@ -111,6 +111,8 @@ const FRAMES: { id: FrameId; label: MsgKey }[] = [
|
||||
{ id: 'cinematic', label: 'frame.cinematic' },
|
||||
{ id: 'wallframe', label: 'frame.wall' },
|
||||
{ id: 'wallframe-landscape', label: 'frame.wallLand' },
|
||||
{ id: 'old-film', label: 'frame.oldFilm' },
|
||||
{ id: 'old-film-portrait', label: 'frame.oldFilmPort' },
|
||||
];
|
||||
|
||||
// The recipe a saved photo's base is rendered through: the neutral stock —
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -93,6 +93,8 @@ export const en: Dict = {
|
||||
'frame.cinematic': 'CINEMATIC',
|
||||
'frame.wall': 'WALL FRAME PORTRAIT',
|
||||
'frame.wallLand': 'WALL FRAME LANDSCAPE',
|
||||
'frame.oldFilm': 'OLD FILM LANDSCAPE',
|
||||
'frame.oldFilmPort': 'OLD FILM PORTRAIT',
|
||||
|
||||
'crop.none': 'NO CROP',
|
||||
'crop.1:1': '1:1',
|
||||
|
||||
@@ -105,6 +105,8 @@ export const vi = {
|
||||
'frame.cinematic': 'ĐIỆN ẢNH',
|
||||
'frame.wall': 'WALL FRAME PORTRAIT',
|
||||
'frame.wallLand': 'WALL FRAME LANDSCAPE',
|
||||
'frame.oldFilm': 'OLD FILM LANDSCAPE',
|
||||
'frame.oldFilmPort': 'OLD FILM PORTRAIT',
|
||||
|
||||
'crop.none': 'KHÔNG CẮT',
|
||||
'crop.1:1': '1:1',
|
||||
|
||||
Reference in New Issue
Block a user