mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 07:53:09 +00:00
09ffc7d946
A chat column in the Studio with its own privacy notice, a live page snapshot through modern-screenshot that leaves out photos of other people and foreign map tiles, and flushes before every turn and apply.
92 lines
3.3 KiB
TypeScript
92 lines
3.3 KiB
TypeScript
import { bookPageSetupForPreset, bookShapeElementSchema, bookTextElementSchema, type BookSpread } from '@trek/shared';
|
|
import { createElement } from 'react';
|
|
import { flushSync } from 'react-dom';
|
|
import { createRoot } from 'react-dom/client';
|
|
|
|
import { SpreadView } from '../../src/components/Studio/SpreadView';
|
|
import { sheetSizePx, snapshotSpread } from '../../src/components/Studio/pageSnapshot';
|
|
|
|
/**
|
|
* Loaded by the page snapshot flows inside the running app, where the dev
|
|
* server serves it as a module: draws a seeded spread with the editor's own
|
|
* SpreadView and turns it into the JPEG the Studio sends the assistant, so a
|
|
* real Chromium and a real WebKit prove the rasteriser works end to end. A map
|
|
* tile from another server sits on the sheet and must be left out.
|
|
*/
|
|
|
|
export interface SeededSnapshot {
|
|
type: string;
|
|
bytes: number;
|
|
/** The first bytes of the file: a JPEG starts FF D8 FF. */
|
|
head: number[];
|
|
width: number;
|
|
height: number;
|
|
/** The image decoded again, as the browser reads it. */
|
|
decodedWidth: number;
|
|
decodedHeight: number;
|
|
drawnPhotoIds: number[];
|
|
/** Whether the snapshot reached the map tile's server at all. */
|
|
tileRequested: boolean;
|
|
}
|
|
|
|
const TILE = 'https://tile.openstreetmap.org/5/16/10.png';
|
|
|
|
function seededSpread(): BookSpread {
|
|
return {
|
|
id: 'sp-seeded',
|
|
role: 'inner',
|
|
background: '#f4efe6',
|
|
elements: [
|
|
bookShapeElementSchema.parse({ id: 'shape', kind: 'shape', frame: { x: 20, y: 20, w: 160, h: 90 } }),
|
|
bookTextElementSchema.parse({
|
|
id: 'title',
|
|
kind: 'text',
|
|
frame: { x: 230, y: 30, w: 160, h: 30 },
|
|
text: 'Lisbon',
|
|
}),
|
|
],
|
|
parked: [],
|
|
entryId: null,
|
|
};
|
|
}
|
|
|
|
export async function snapshotSeededSpread(): Promise<SeededSnapshot | null> {
|
|
const page = bookPageSetupForPreset('square-210');
|
|
const spread = seededSpread();
|
|
const size = sheetSizePx(spread, page);
|
|
// Off screen as the Studio's stage is; the sheet inside it is what is drawn.
|
|
const host = document.createElement('div');
|
|
host.style.cssText = 'position:fixed;top:0;inset-inline-start:-20000px';
|
|
const sheet = document.createElement('div');
|
|
sheet.style.cssText = `position:relative;width:${size.width}px;height:${size.height}px`;
|
|
host.appendChild(sheet);
|
|
document.body.appendChild(host);
|
|
const root = createRoot(sheet);
|
|
flushSync(() => root.render(createElement(SpreadView, { spread, page, print: true })));
|
|
const tile = document.createElement('img');
|
|
tile.src = TILE;
|
|
sheet.firstElementChild?.appendChild(tile);
|
|
const before = performance.getEntriesByType('resource').length;
|
|
try {
|
|
const shot = await snapshotSpread(sheet, size, new Set());
|
|
if (!shot) return null;
|
|
const head = [...new Uint8Array(await shot.image.slice(0, 3).arrayBuffer())];
|
|
const bitmap = await createImageBitmap(shot.image);
|
|
const fetched = performance.getEntriesByType('resource').slice(before);
|
|
return {
|
|
type: shot.image.type,
|
|
bytes: shot.image.size,
|
|
head,
|
|
width: shot.width,
|
|
height: shot.height,
|
|
decodedWidth: bitmap.width,
|
|
decodedHeight: bitmap.height,
|
|
drawnPhotoIds: shot.drawnPhotoIds,
|
|
tileRequested: fetched.some((entry) => entry.name === TILE && entry.initiatorType !== 'img'),
|
|
};
|
|
} finally {
|
|
root.unmount();
|
|
host.remove();
|
|
}
|
|
}
|