{
+ let occupied: boolean[][] = [];
+ const row = (y: number) => (occupied[y] ??= new Array(colCount).fill(false));
+ const anyCollide = (x: number, y: number, w: number, h: number) => {
+ for (let yy = 0; yy < h; yy++) {
+ const r = row(y + yy);
+ for (let xx = 0; xx < w; xx++) if (r[x + xx]) return true;
+ }
+ return false;
+ };
+
+ let x = 0, y = 0;
+ let result: Positioned[] = [];
+ for (const img of input.img) {
+ const w = img.w ?? 1;
+ const h = img.h ?? 1;
+ const s = img.s ?? 0;
+ if (w > colCount) throw new Error(`image ${img.src} is wider than the grid`);
+
+ x += s;
+ while (x > colCount - w || anyCollide(x, y, w, h)) {
+ x += 1;
+ if (x > colCount - w) x = 0, y += 1;
+ }
+
+ result.push({
+ x,
+ y,
+ w,
+ h,
+ src: base ? base + '/' + img.src : img.src,
+ align: alignments[img.align] ?? img.align,
+ });
+
+ for (let yy = 0; yy < h; yy++) {
+ const r = row(y + yy);
+ for (let xx = 0; xx < w; xx++) r[x + xx] = true;
+ }
+
+ x += w;
+ if (x >= colCount) x = 0, y += 1;
+ }
+ return result;
+})() />
+
+<${'photo-grid'}
+ style={
+ 'grid-template-columns': input.cols.map(x => x + 'fr').join(' '),
+ 'grid-template-rows': rows.map(x => (100 * x / rowSum).toFixed(1) + '%').join(' '),
+ 'aspect-ratio': `768/${rowSum}`,
+ ...style,
+ }
+>
+
+ <${'photo-grid-item'} style={
+ 'grid-area': `${y + 1} / ${x + 1} / ${1 + y + h} / ${1 + x + w}`,
+ }>
+
+ >
+
+>
diff --git a/src/tags/photo-grid.tsx b/src/tags/photo-grid.tsx
deleted file mode 100644
index 447ef74fb12ee5878b85fff63c3e941080b9ab12..0000000000000000000000000000000000000000
--- a/src/tags/photo-grid.tsx
+++ /dev/null
@@ -1,142 +0,0 @@
-const { isArray } = Array;
-
-export interface PhotoGridProps {
- style: any;
- base: string;
- width: number;
- widths?: string[];
- heights?: number[];
- items: Item[];
-}
-
-type Item = [img: string, options?: ItemOptions];
-
-interface ItemOptions {
- s?: number;
- w?: number;
- h?: number;
- align?: keyof typeof alignments;
-}
-
-const alignments = {
- bottom: "50% 80%",
- "bottom-end": "50% 100%",
- top: "50% 30%",
- "top-end": "50% 0%",
- left: "30% 50%",
- "left-end": "0% 50%",
- right: "80% 50%",
- "right-end": "100% 50%",
-};
-
-const typicalPageWidth = 768;
-
-export function PhotoGrid({
- style,
- base,
- width,
- widths,
- heights,
- items,
-}: PhotoGridProps) {
- if (!base.endsWith("/")) base += "/";
- let rows: boolean[][] = [];
- const row = (y: number) => (rows[y] ??= new Array(width).fill(false));
- const anyCollide = (x: number, y: number, w: number, h: number) => {
- for (let yy = 0; yy < h; yy++) {
- const r = row(y + yy);
- for (let xx = 0; xx < w; xx++) if (r[x + xx]) return true;
- }
- return false;
- };
-
- const firstRowHeight = heights?.[0] ?? 325;
-
- let x = 0;
- let y = 0;
- let contents = [];
- for (const item of items) {
- const [
- img,
- { w = 1, h = 1, align, s } = {},
- ] = isArray(item) ? item : [item];
- if (w > width) throw new Error(`Item ${img} too wide (${w} > ${width})`);
-
- x += s ?? 0;
-
- while (anyCollide(x, y, w, h)) {
- x += 1;
- if (x > width - w) x = 0, y += 1;
- }
- for (let yy = 0; yy < h; yy++) {
- const r = row(y + yy);
- for (let xx = 0; xx < w; xx++) r[x + xx] = true;
- }
-
- const info = fileInfo(base, img);
- contents.push(
-
- )[align] ?? align
- : "50% 50%",
- }}
- />
-
,
- );
- }
-
- const height = rows.length;
- return (
- (h / firstRowHeight) + "fr")
- .concat(new Array(height - heights.length).fill("1fr"))
- .join(" ")
- : `repeat(${height}, 1fr)`,
- }}
- >
- {contents}
-
- );
-}
-
-function fileInfo(base: string, img: string) {
- const filePath = path.join(base, img);
- const file = MediaFile.getByPath(filePath);
- const dimensions = file?.parseDimensions();
- if (!dimensions) {
- throw new Error(`File does not exist or has no dimensions: ${filePath}`);
- }
- const alt = path.basename(img, path.extname(img));
- return { src: file, alt, width: dimensions.width, height: dimensions.height };
-}
-
-function mapSizes(sizes: Array) {
- return sizes.map(mapSize);
-}
-function mapSize(size: string | number) {
- return typeof size === "number" ? size + "px" : size;
-}
-
-import "./photo-grid.css";
-import { MediaFile } from "#src/file-viewer/models/MediaFile.ts";
-import Image from "./image.tsx";
-import * as path from "node:path";
-import { ASSERT } from "@clo/lib/assert";