1import "./photo-grid.css";
2
3export interface Input {
4 base?: string;
5 cols: number[]; // weights
6 rows?: number[]; // pixel heights, based on a 768px container
7 style?: Marko.AttrStyle;
8 gapless?: boolean;
9 gap?: number;
10 // images are placed left to right, top to bottom
11 img: Marko.AttrTag<{
12 src: string;
13 w?: number; // column span
14 h?: number; // row span
15 s?: number; // skipped columns before placement
16 mutedLoop?: boolean;
17 align?: "top" | "center" | "bottom" | (string & {});
18 }>;
19}
20
21static interface Positioned {
22 x: number;
23 y: number;
24 w: number;
25 h: number;
26 align?: string;
27 src: string;
28}
29
30static const alignments = {
31 bottom: "50% 80%",
32 "bottom-end": "50% 100%",
33 top: "50% 30%",
34 "top-end": "50% 0%",
35 left: "30% 50%",
36 "left-end": "0% 50%",
37 right: "80% 50%",
38 "right-end": "100% 50%",
39};
40
41<const/{ base, cols, style, gapless, gap = gapless ? 4 : 11 }=input>
42<const/rows=input.rows ?? [325]>
43<const/colCount=input.cols.length>
44<const/rowSum=rows.reduce((a, b) => a + b, 0) + gap * (rows.length - 1)>
45
46<const/items=(() => {
47 let occupied: boolean[][] = [];
48 const row = (y: number) => (occupied[y] ??= new Array(colCount).fill(false));
49 const anyCollide = (x: number, y: number, w: number, h: number) => {
50 for (let yy = 0; yy < h; yy++) {
51 const r = row(y + yy);
52 for (let xx = 0; xx < w; xx++) if (r[x + xx]) return true;
53 }
54 return false;
55 };
56
57 let x = 0;
58 let y = 0;
59 let result: Positioned[] = [];
60 for (const img of input.img) {
61 const w = img.w ?? 1;
62 const h = img.h ?? 1;
63 const s = img.s ?? 0;
64 if (w > colCount)
65 throw new Error(`image ${img.src} is wider than the grid`);
66
67 x += s;
68 while (x > colCount - w || anyCollide(x, y, w, h)) {
69 x += 1;
70 if (x > colCount - w) ((x = 0), (y += 1));
71 }
72
73 result.push({
74 x,
75 y,
76 w,
77 h,
78 src: base ? base + "/" + img.src : img.src,
79 align: alignments[img.align] ?? img.align,
80 });
81
82 for (let yy = 0; yy < h; yy++) {
83 const r = row(y + yy);
84 for (let xx = 0; xx < w; xx++) r[x + xx] = true;
85 }
86
87 x += w;
88 if (x >= colCount) ((x = 0), (y += 1));
89 }
90 return result;
91})()>
92
93<${"photo-grid"} style={
94 "grid-template-columns": input.cols.map((x) => x + "fr").join(" "),
95 "grid-template-rows": rows
96 .map((x) => ((100 * x) / rowSum).toFixed(1) + "%")
97 .join(" "),
98 "aspect-ratio": `768/${rowSum}`,
99 gap: gap !== 11 ? gap + "px" : undefined,
100 ...style,
101}>
102 <for|{ x, y, w, h, src, align }| of=items>
103 <${"photo-grid-item"} style={
104 "grid-area": `${y + 1} / ${x + 1} / ${1 + y + h} / ${1 + x + w}`,
105 }>
106 <clover-media ...{ src, align }/>
107 </>
108 </for>
109</>