feat: the camera's own colour, a reading that keeps its place, and cloud links in Settings
- RAW opens at the colour the camera wrote into it: the develop carries the difference against the file's own preview (previewResidual) instead of the per-channel cubic fit, dE00 6.04 -> 0.54; previewMatch.ts keeps only the grid. - Immich reading: keyset position kept across visits, one reading per node, a page in flight retried, timeline buckets for the years on offer. - The wall draws a server frame at the address the proxy serves it (no blob minted for it), and the 80-thumbnail pre-cache on open is gone. - Settings lists the cloud links, flags two that answer at the same address, and forgets one. - Custom frames imported from a file (engine/customFrame.ts, IndexedDB). - Upload an edited frame from Studio to Immich Cloud.
This commit is contained in:
@@ -100,6 +100,13 @@ export interface RenderOptions {
|
||||
// quarter turn and the fine angle (they ride on top of both).
|
||||
photoFlipH?: boolean;
|
||||
photoFlipV?: boolean;
|
||||
customFrame?: {
|
||||
bytes: Uint8Array;
|
||||
hole: { x: number; y: number; w: number; h: number };
|
||||
scale?: number;
|
||||
panX?: number;
|
||||
panY?: number;
|
||||
} | null;
|
||||
// The strip the FILE carries: the amber `#TAG` over the photo and a caption
|
||||
// band appended below it, mirroring the landing page's `.lp-shot` +
|
||||
// `.lp-frame-meta`. Left off the live preview, which must not show it.
|
||||
@@ -770,8 +777,8 @@ function loadWallframe(assets: RenderAssets) {
|
||||
// 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);
|
||||
// it (both walls, both films, custom frames).
|
||||
const hasOwnAspect = (id: FrameId) => isWallframe(id) || isOldFilm(id) || id === 'custom';
|
||||
|
||||
function loadOldFilm(assets: RenderAssets) {
|
||||
if (oldFilmImage) return oldFilmImage;
|
||||
@@ -1521,6 +1528,73 @@ export async function renderPhoto(input: RenderInput): Promise<RenderResult | nu
|
||||
stampCanvas = cardCanvas;
|
||||
stampW = outW;
|
||||
stampH = outH;
|
||||
} else if (frameId === 'custom' && options?.customFrame) {
|
||||
flush(surface);
|
||||
const photoImg = own(surface.makeImageSnapshot());
|
||||
cardSource = photoImg;
|
||||
const pw = photoImg.width();
|
||||
const ph = photoImg.height();
|
||||
|
||||
const customImg = own(Skia.Image.MakeImageFromEncoded(options.customFrame.bytes));
|
||||
if (!customImg) {
|
||||
console.error('Failed to decode custom frame');
|
||||
return null;
|
||||
}
|
||||
const fw = customImg.width();
|
||||
const fh = customImg.height();
|
||||
|
||||
const cardSurface = createSurface(fw, fh);
|
||||
if (!cardSurface) return null;
|
||||
own(cardSurface);
|
||||
const cardCanvas = cardSurface.getCanvas();
|
||||
const plainPaint = own(Skia.Paint());
|
||||
|
||||
const hole = options.customFrame.hole;
|
||||
const holeX = hole.x * fw;
|
||||
const holeY = hole.y * fh;
|
||||
const holeW = hole.w * fw;
|
||||
const holeH = hole.h * fh;
|
||||
const holeCenterX = holeX + holeW / 2;
|
||||
const holeCenterY = holeY + holeH / 2;
|
||||
|
||||
// Base cover scale to fill the transparent hole
|
||||
const baseScale = Math.max(holeW / pw, holeH / ph);
|
||||
const zoom = options.customFrame.scale ?? 1.0;
|
||||
const finalScale = baseScale * zoom;
|
||||
|
||||
const drawW = pw * finalScale;
|
||||
const drawH = ph * finalScale;
|
||||
|
||||
const panX = options.customFrame.panX ?? 0;
|
||||
const panY = options.customFrame.panY ?? 0;
|
||||
|
||||
const drawX = holeCenterX + panX - drawW / 2;
|
||||
const drawY = holeCenterY + panY - drawH / 2;
|
||||
|
||||
// 1. Draw photo underneath
|
||||
cardCanvas.drawImageRectOptions(
|
||||
photoImg,
|
||||
Skia.XYWHRect(0, 0, pw, ph),
|
||||
Skia.XYWHRect(drawX, drawY, drawW, drawH),
|
||||
Skia.FilterMode.Linear,
|
||||
Skia.MipmapMode.None,
|
||||
plainPaint
|
||||
);
|
||||
|
||||
// 2. Draw custom frame on top so transparent area displays the photo
|
||||
cardCanvas.drawImageRectOptions(
|
||||
customImg,
|
||||
Skia.XYWHRect(0, 0, fw, fh),
|
||||
Skia.XYWHRect(0, 0, fw, fh),
|
||||
Skia.FilterMode.Linear,
|
||||
Skia.MipmapMode.None,
|
||||
plainPaint
|
||||
);
|
||||
|
||||
outputSurface = cardSurface;
|
||||
stampCanvas = cardCanvas;
|
||||
stampW = fw;
|
||||
stampH = fh;
|
||||
} else {
|
||||
drawFrameOnCanvas(canvas, width, height, frameId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user