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'
|
| 'classic-white'
|
||||||
| 'cinematic'
|
| 'cinematic'
|
||||||
| 'wallframe' // WALL FRAME PORTRAIT — the artwork hung 90° CW
|
| '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
|
// Viewfinder/composition aspect-ratio selector. Convention is an UPRIGHT
|
||||||
// (portrait) picture: "4:3" = standing 3:4 photo (w/h = 3/4), "3:2" = standing
|
// (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 ALGORITHM = 'xor16-v1';
|
||||||
|
|
||||||
const BASE_FILTERS: BaseFilter[] = [...FILM_SIMS.map((s) => s.baseFilter), 'none'];
|
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 => {
|
const hash = (s: string): number => {
|
||||||
let h = 0x811c9dc5;
|
let h = 0x811c9dc5;
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ const FRAMES: { id: FrameId; label: MsgKey }[] = [
|
|||||||
{ id: 'cinematic', label: 'frame.cinematic' },
|
{ id: 'cinematic', label: 'frame.cinematic' },
|
||||||
{ id: 'wallframe', label: 'frame.wall' },
|
{ id: 'wallframe', label: 'frame.wall' },
|
||||||
{ id: 'wallframe-landscape', label: 'frame.wallLand' },
|
{ 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 —
|
// 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 }> {
|
export async function loadEngine(): Promise<{ assets: RenderAssets; gpu: boolean }> {
|
||||||
await initSkia({ locateFile: (file) => `/wasm/${file}` });
|
await initSkia({ locateFile: (file) => `/wasm/${file}` });
|
||||||
if (!assets) {
|
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/Cousine-Regular.ttf'),
|
||||||
buf('/assets/NotoEmoji-GPS.ttf'),
|
buf('/assets/NotoEmoji-GPS.ttf'),
|
||||||
buf('/assets/wallframe.png'),
|
buf('/assets/wallframe.png'),
|
||||||
|
buf('/assets/old_film.png'),
|
||||||
Promise.all(MARK_FACES.map(async (face) => ({ family: face.family, bytes: await buf(face.file) }))),
|
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() };
|
return { assets, gpu: probeGpu() };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export interface RenderAssets {
|
|||||||
textFont: ArrayBuffer; // assets/Cousine-Regular.ttf
|
textFont: ArrayBuffer; // assets/Cousine-Regular.ttf
|
||||||
emojiFont: ArrayBuffer; // assets/NotoEmoji-GPS.ttf
|
emojiFont: ArrayBuffer; // assets/NotoEmoji-GPS.ttf
|
||||||
wallframe: Uint8Array; // wallframe.png
|
wallframe: Uint8Array; // wallframe.png
|
||||||
|
oldFilm: Uint8Array; // old_film.png
|
||||||
// The FONT option's faces beside DEFAULT (see assets.ts MARK_FACES).
|
// The FONT option's faces beside DEFAULT (see assets.ts MARK_FACES).
|
||||||
markFonts: { family: string; bytes: ArrayBuffer }[];
|
markFonts: { family: string; bytes: ArrayBuffer }[];
|
||||||
}
|
}
|
||||||
@@ -690,6 +691,7 @@ function replaceThrough(
|
|||||||
|
|
||||||
let faces: { typeface: any; iconTypeface: any } | null = null;
|
let faces: { typeface: any; iconTypeface: any } | null = null;
|
||||||
let wallframeImage: any = null;
|
let wallframeImage: any = null;
|
||||||
|
let oldFilmImage: any = null;
|
||||||
|
|
||||||
function loadFaces(assets: RenderAssets) {
|
function loadFaces(assets: RenderAssets) {
|
||||||
if (faces) return faces;
|
if (faces) return faces;
|
||||||
@@ -762,6 +764,19 @@ function loadWallframe(assets: RenderAssets) {
|
|||||||
return wallframeImage;
|
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> {
|
export async function renderPhoto(input: RenderInput): Promise<RenderResult | null> {
|
||||||
const { recipe, frameId, options } = input;
|
const { recipe, frameId, options } = input;
|
||||||
const owned: SkDisposable[] = [];
|
const owned: SkDisposable[] = [];
|
||||||
@@ -1444,6 +1459,37 @@ export async function renderPhoto(input: RenderInput): Promise<RenderResult | nu
|
|||||||
stampCanvas = cardCanvas;
|
stampCanvas = cardCanvas;
|
||||||
stampW = outW;
|
stampW = outW;
|
||||||
stampH = outH;
|
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 {
|
} else {
|
||||||
drawFrameOnCanvas(canvas, width, height, frameId);
|
drawFrameOnCanvas(canvas, width, height, frameId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ export const en: Dict = {
|
|||||||
'frame.cinematic': 'CINEMATIC',
|
'frame.cinematic': 'CINEMATIC',
|
||||||
'frame.wall': 'WALL FRAME PORTRAIT',
|
'frame.wall': 'WALL FRAME PORTRAIT',
|
||||||
'frame.wallLand': 'WALL FRAME LANDSCAPE',
|
'frame.wallLand': 'WALL FRAME LANDSCAPE',
|
||||||
|
'frame.oldFilm': 'OLD FILM LANDSCAPE',
|
||||||
|
'frame.oldFilmPort': 'OLD FILM PORTRAIT',
|
||||||
|
|
||||||
'crop.none': 'NO CROP',
|
'crop.none': 'NO CROP',
|
||||||
'crop.1:1': '1:1',
|
'crop.1:1': '1:1',
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ export const vi = {
|
|||||||
'frame.cinematic': 'ĐIỆN ẢNH',
|
'frame.cinematic': 'ĐIỆN ẢNH',
|
||||||
'frame.wall': 'WALL FRAME PORTRAIT',
|
'frame.wall': 'WALL FRAME PORTRAIT',
|
||||||
'frame.wallLand': 'WALL FRAME LANDSCAPE',
|
'frame.wallLand': 'WALL FRAME LANDSCAPE',
|
||||||
|
'frame.oldFilm': 'OLD FILM LANDSCAPE',
|
||||||
|
'frame.oldFilmPort': 'OLD FILM PORTRAIT',
|
||||||
|
|
||||||
'crop.none': 'KHÔNG CẮT',
|
'crop.none': 'KHÔNG CẮT',
|
||||||
'crop.1:1': '1:1',
|
'crop.1:1': '1:1',
|
||||||
|
|||||||
Reference in New Issue
Block a user