mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
283 lines
8.4 KiB
TypeScript
283 lines
8.4 KiB
TypeScript
import { bookElementId } from './book-ids';
|
|
import type { BookElement, BookPageSetup } from './book.schema';
|
|
|
|
/**
|
|
* Photo grids: a set of frames placed together, in one gesture.
|
|
*
|
|
* Moved from the Studio (client/src/components/Studio/grids.ts) unchanged, so
|
|
* the assistant's book tools offer the same grids the elements panel does.
|
|
*
|
|
* ── How this differs from a layout ────────────────────────────────────────
|
|
*
|
|
* A layout (`book-layouts.ts`) rearranges the *whole spread*: it takes what is
|
|
* already there, pours it into a new arrangement and parks the rest. A grid
|
|
* adds a block of empty frames wherever you put it, and touches nothing else.
|
|
* You reach for a layout when you are deciding what a page is; you reach for a
|
|
* grid when you have decided, and want four pictures in a row on the left half.
|
|
*
|
|
* Canva keeps them apart for the same reason, and calling both "templates"
|
|
* would make the difference invisible at exactly the moment it matters.
|
|
*
|
|
* ── Why fractions ────────────────────────────────────────────────────────
|
|
*
|
|
* Cells are stored as fractions of the block, so one definition serves a grid
|
|
* dropped across a whole spread and the same grid dropped into a quarter of a
|
|
* page. The gutter is the exception: it is a real measurement in millimetres,
|
|
* because 2mm between two pictures is 2mm whether the block is 60mm or 300mm
|
|
* wide: scaling the gap with the block would make small grids look glued
|
|
* together and big ones look scattered.
|
|
*/
|
|
|
|
export interface BookGridCell {
|
|
/** Fractions of the block, 0..1. */
|
|
x: number;
|
|
y: number;
|
|
w: number;
|
|
h: number;
|
|
}
|
|
|
|
export interface BookGridDef {
|
|
id: string;
|
|
cells: BookGridCell[];
|
|
}
|
|
|
|
/** `rows` of equal cells, each row split into that many columns. */
|
|
function rows(...counts: number[]): BookGridCell[] {
|
|
const h = 1 / counts.length;
|
|
const out: BookGridCell[] = [];
|
|
counts.forEach((cols, r) => {
|
|
for (let c = 0; c < cols; c++) out.push({ x: c / cols, y: r * h, w: 1 / cols, h });
|
|
});
|
|
return out;
|
|
}
|
|
|
|
/** `cols` of equal columns, each split into that many rows. */
|
|
function cols(...counts: number[]): BookGridCell[] {
|
|
const w = 1 / counts.length;
|
|
const out: BookGridCell[] = [];
|
|
counts.forEach((n, c) => {
|
|
for (let r = 0; r < n; r++) out.push({ x: c * w, y: r / n, w, h: 1 / n });
|
|
});
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* The catalogue.
|
|
*
|
|
* Ordered by how often a photo book actually wants them: one big picture, then
|
|
* pairs, then the asymmetric arrangements that carry a page, then the dense
|
|
* grids that end a chapter.
|
|
*/
|
|
export const BOOK_GRIDS: BookGridDef[] = [
|
|
{ id: 'single', cells: [{ x: 0, y: 0, w: 1, h: 1 }] },
|
|
|
|
{ id: 'two-across', cells: rows(2) },
|
|
{ id: 'two-down', cells: cols(2) },
|
|
{ id: 'three-across', cells: rows(3) },
|
|
{ id: 'three-down', cells: cols(3) },
|
|
|
|
// One picture carrying the block, the rest in support. The workhorse of a
|
|
// travel page: a landscape wide enough to hold the eye, and the details
|
|
// underneath it.
|
|
{
|
|
id: 'hero-two',
|
|
cells: [
|
|
{ x: 0, y: 0, w: 1, h: 0.62 },
|
|
{ x: 0, y: 0.62, w: 0.5, h: 0.38 },
|
|
{ x: 0.5, y: 0.62, w: 0.5, h: 0.38 },
|
|
],
|
|
},
|
|
{
|
|
id: 'hero-three',
|
|
cells: [{ x: 0, y: 0, w: 1, h: 0.64 }, ...[0, 1, 2].map((i) => ({ x: i / 3, y: 0.64, w: 1 / 3, h: 0.36 }))],
|
|
},
|
|
{
|
|
id: 'two-hero',
|
|
cells: [
|
|
{ x: 0, y: 0, w: 0.5, h: 0.36 },
|
|
{ x: 0.5, y: 0, w: 0.5, h: 0.36 },
|
|
{ x: 0, y: 0.36, w: 1, h: 0.64 },
|
|
],
|
|
},
|
|
{
|
|
id: 'side-hero',
|
|
cells: [
|
|
{ x: 0, y: 0, w: 0.62, h: 1 },
|
|
{ x: 0.62, y: 0, w: 0.38, h: 0.5 },
|
|
{ x: 0.62, y: 0.5, w: 0.38, h: 0.5 },
|
|
],
|
|
},
|
|
{
|
|
id: 'side-hero-left',
|
|
cells: [
|
|
{ x: 0, y: 0, w: 0.38, h: 0.5 },
|
|
{ x: 0, y: 0.5, w: 0.38, h: 0.5 },
|
|
{ x: 0.38, y: 0, w: 0.62, h: 1 },
|
|
],
|
|
},
|
|
{
|
|
id: 'side-three',
|
|
cells: [{ x: 0, y: 0, w: 0.6, h: 1 }, ...[0, 1, 2].map((i) => ({ x: 0.6, y: i / 3, w: 0.4, h: 1 / 3 }))],
|
|
},
|
|
|
|
{ id: 'grid-4', cells: rows(2, 2) },
|
|
{ id: 'grid-6', cells: rows(3, 3) },
|
|
{ id: 'grid-9', cells: rows(3, 3, 3) },
|
|
{ id: 'grid-8', cells: rows(4, 4) },
|
|
|
|
{
|
|
id: 'one-two',
|
|
cells: [
|
|
{ x: 0, y: 0, w: 0.5, h: 1 },
|
|
{ x: 0.5, y: 0, w: 0.5, h: 0.5 },
|
|
{ x: 0.5, y: 0.5, w: 0.5, h: 0.5 },
|
|
],
|
|
},
|
|
{
|
|
id: 'two-one',
|
|
cells: [
|
|
{ x: 0, y: 0, w: 0.5, h: 0.5 },
|
|
{ x: 0, y: 0.5, w: 0.5, h: 0.5 },
|
|
{ x: 0.5, y: 0, w: 0.5, h: 1 },
|
|
],
|
|
},
|
|
{
|
|
id: 'rows-1-2',
|
|
cells: [
|
|
{ x: 0, y: 0, w: 1, h: 0.5 },
|
|
{ x: 0, y: 0.5, w: 0.5, h: 0.5 },
|
|
{ x: 0.5, y: 0.5, w: 0.5, h: 0.5 },
|
|
],
|
|
},
|
|
{
|
|
id: 'rows-2-1',
|
|
cells: [
|
|
{ x: 0, y: 0, w: 0.5, h: 0.5 },
|
|
{ x: 0.5, y: 0, w: 0.5, h: 0.5 },
|
|
{ x: 0, y: 0.5, w: 1, h: 0.5 },
|
|
],
|
|
},
|
|
{
|
|
id: 'rows-2-3',
|
|
cells: [
|
|
...rows(2).map((c) => ({ ...c, h: 0.52 })),
|
|
...[0, 1, 2].map((i) => ({ x: i / 3, y: 0.52, w: 1 / 3, h: 0.48 })),
|
|
],
|
|
},
|
|
{
|
|
id: 'rows-3-2',
|
|
cells: [
|
|
...[0, 1, 2].map((i) => ({ x: i / 3, y: 0, w: 1 / 3, h: 0.48 })),
|
|
...rows(2).map((c) => ({ ...c, y: 0.48, h: 0.52 })),
|
|
],
|
|
},
|
|
|
|
{ id: 'cols-1-2', cells: cols(1, 2) },
|
|
{ id: 'cols-2-1', cells: cols(2, 1) },
|
|
{ id: 'cols-2-1-2', cells: cols(2, 1, 2) },
|
|
{ id: 'cols-1-2-1', cells: cols(1, 2, 1) },
|
|
|
|
// A band across the middle: three pictures at the same height with room above
|
|
// and below for words. The arrangement a chapter opener usually wants.
|
|
{ id: 'band-three', cells: [0, 1, 2].map((i) => ({ x: i / 3, y: 0.26, w: 1 / 3, h: 0.48 })) },
|
|
{ id: 'band-four', cells: [0, 1, 2, 3].map((i) => ({ x: i / 4, y: 0.3, w: 0.25, h: 0.4 })) },
|
|
|
|
// Staggered: alternate cells drop by a fraction of their height, which is what
|
|
// stops a row of four squares reading as a contact sheet.
|
|
{
|
|
id: 'stagger-four',
|
|
cells: [0, 1, 2, 3].map((i) => ({
|
|
x: i / 4,
|
|
y: i % 2 ? 0.14 : 0,
|
|
w: 0.25,
|
|
h: 0.86,
|
|
})),
|
|
},
|
|
{
|
|
id: 'stagger-three',
|
|
cells: [0, 1, 2].map((i) => ({
|
|
x: i / 3,
|
|
y: i === 1 ? 0.16 : 0,
|
|
w: 1 / 3,
|
|
h: 0.84,
|
|
})),
|
|
},
|
|
|
|
{ id: 'filmstrip', cells: [0, 1, 2, 3, 4].map((i) => ({ x: i / 5, y: 0.32, w: 0.2, h: 0.36 })) },
|
|
{ id: 'grid-12', cells: rows(4, 4, 4) },
|
|
];
|
|
|
|
/** A grid by id, or undefined. */
|
|
export function findBookGrid(id: string): BookGridDef | undefined {
|
|
return BOOK_GRIDS.find((g) => g.id === id);
|
|
}
|
|
|
|
/**
|
|
* Turn a grid into elements, inside a box on the page.
|
|
*
|
|
* The gutter is taken out of each cell rather than added between them, so the
|
|
* block occupies exactly the box it was given: a grid asked to fill the safe
|
|
* area fills the safe area, instead of overhanging it by one gutter.
|
|
*/
|
|
export function bookGridElements(
|
|
grid: BookGridDef,
|
|
box: { x: number; y: number; w: number; h: number },
|
|
gutter = 3,
|
|
radius = 0,
|
|
): BookElement[] {
|
|
const half = gutter / 2;
|
|
return grid.cells.map((cell) => {
|
|
const x = box.x + cell.x * box.w;
|
|
const y = box.y + cell.y * box.h;
|
|
const w = cell.w * box.w;
|
|
const h = cell.h * box.h;
|
|
// Only interior edges get half a gutter; the block's outer edge is the box.
|
|
const left = cell.x > 0.001 ? half : 0;
|
|
const top = cell.y > 0.001 ? half : 0;
|
|
const right = cell.x + cell.w < 0.999 ? half : 0;
|
|
const bottom = cell.y + cell.h < 0.999 ? half : 0;
|
|
return {
|
|
id: bookElementId('p'),
|
|
kind: 'photo',
|
|
frame: {
|
|
x: x + left,
|
|
y: y + top,
|
|
w: Math.max(2, w - left - right),
|
|
h: Math.max(2, h - top - bottom),
|
|
},
|
|
rotation: 0,
|
|
opacity: 1,
|
|
locked: false,
|
|
photoId: null,
|
|
fit: 'cover',
|
|
focalX: 0.5,
|
|
focalY: 0.5,
|
|
radius,
|
|
filter: 'none',
|
|
mask: null,
|
|
frameStyle: 'none',
|
|
} as BookElement;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* The box a grid lands in when it is simply clicked rather than placed.
|
|
*
|
|
* The safe area of the page you are looking at, which is almost always what was
|
|
* meant: a grid that lands half in the gutter has to be dragged before it is
|
|
* any use.
|
|
*/
|
|
export function defaultBookGridBox(
|
|
page: BookPageSetup,
|
|
single: boolean,
|
|
): { x: number; y: number; w: number; h: number } {
|
|
const W = single ? page.pageWidth : page.pageWidth * 2;
|
|
const margin = Math.max(page.safe, Math.min(W, page.pageHeight) * 0.075);
|
|
return {
|
|
x: margin,
|
|
y: margin,
|
|
w: W - margin * 2,
|
|
h: page.pageHeight - margin * 2,
|
|
};
|
|
}
|