1/* DESIGN-SYSTEM §5: geometri ekspor (piksel), bukan token tampilan, jadi nilai
2 mentahnya hidup di sini dan dipakai bersama oleh kanvas hias (react-konva),
3 langkah kamera, pemilih layout, dan lapisan uji. FR-03 menambah Grid 2 x 2:
4 kanvas selebar lembar 4R dengan dua kolom slot. */
5
6export type LayoutId = "strip-4" | "strip-3" | "grid-2x2"
7
8export interface Rect {
9 x: number
10 y: number
11 width: number
12 height: number
13}
14
15interface StripLayout {
16 id: LayoutId
17 label: string
18 shotCount: number
19 canvas: { width: number; height: number }
20 margin: number
21 gap: number
22 footer: { height: number }
23 slot: { width: number; height: number }
24 /** Jumlah kolom slot: 1 untuk strip, 2 untuk grid. */
25 columns: number
26 slotRects: Rect[]
27 footerRect: Rect
28}
29
30const CANVAS = { width: 600, height: 1800 } as const
31const MARGIN = 36
32const GAP = 24
33const FOOTER_HEIGHT = 204
34const SLOT_WIDTH = 528
35
36/* Baris wordmark dan tanggal di kaki strip (DESIGN-SYSTEM §10.3). Dipakai
37 StripStage untuk menggambar dan store untuk menaruh teks pengguna tepat di
38 bawahnya, di layout mana pun. */
39export const FOOTER_WORDMARK_ROW = 100
40
41/* SH-04: QR 168 x 168 px (sekitar 14 mm pada cetakan 300 dpi) di kanan kaki
42 strip, dengan jarak 16 px ke kotak teks di sebelahnya. Ukurannya di ruang
43 kanvas, jadi ekspor selalu menghasilkan QR sebesar ini. */
44export const QR_SIZE = 168
45export const QR_GAP = 16
46
47export function qrRect(layout: StripLayout): Rect {
48 return {
49 x: layout.footerRect.x + layout.footerRect.width - QR_SIZE,
50 y:
51 layout.footerRect.y +
52 Math.floor((layout.footerRect.height - QR_SIZE) / 2),
53 width: QR_SIZE,
54 height: QR_SIZE,
55 }
56}
57
58/** Lebar baris teks kaki strip: selebar kaki, atau menyempit bila QR aktif
59 * (tanggal dan teks pengguna tidak boleh tertimpa QR). */
60export function footerTextWidth(layout: StripLayout, withQr: boolean): number {
61 return withQr
62 ? layout.footerRect.width - QR_SIZE - QR_GAP
63 : layout.footerRect.width
64}
65
66interface LayoutSpec {
67 id: LayoutId
68 label: string
69 shotCount: number
70 slotHeight: number
71 slotWidth?: number
72 margin?: number
73 gap?: number
74 footer?: number
75 canvas?: { width: number; height: number }
76 columns?: number
77}
78
79function buildLayout(spec: LayoutSpec): StripLayout {
80 const canvas = spec.canvas ?? { ...CANVAS }
81 const margin = spec.margin ?? MARGIN
82 const gap = spec.gap ?? GAP
83 const footerHeight = spec.footer ?? FOOTER_HEIGHT
84 const slotWidth = spec.slotWidth ?? SLOT_WIDTH
85 const columns = spec.columns ?? 1
86 const slot = { width: slotWidth, height: spec.slotHeight }
87 const slotRects: Rect[] = Array.from(
88 { length: spec.shotCount },
89 (_, index) => ({
90 x: margin + (index % columns) * (slotWidth + gap),
91 y: margin + Math.floor(index / columns) * (slot.height + gap),
92 width: slotWidth,
93 height: slot.height,
94 })
95 )
96
97 return {
98 id: spec.id,
99 label: spec.label,
100 shotCount: spec.shotCount,
101 canvas,
102 margin,
103 gap,
104 footer: { height: footerHeight },
105 slot,
106 columns,
107 slotRects,
108 footerRect: {
109 x: margin,
110 y: canvas.height - footerHeight,
111 width: canvas.width - margin * 2,
112 height: footerHeight,
113 },
114 }
115}
116
117export const LAYOUTS: Record<LayoutId, StripLayout> = {
118 "strip-4": buildLayout({
119 id: "strip-4",
120 label: "Strip 4 foto",
121 shotCount: 4,
122 slotHeight: 372,
123 }),
124 "strip-3": buildLayout({
125 id: "strip-3",
126 label: "Strip 3 foto",
127 shotCount: 3,
128 slotHeight: 504,
129 }),
130 "grid-2x2": buildLayout({
131 id: "grid-2x2",
132 label: "Grid 2 x 2",
133 shotCount: 4,
134 slotWidth: 540,
135 slotHeight: 744,
136 margin: 48,
137 gap: 24,
138 footer: 240,
139 canvas: { width: 1200, height: 1800 },
140 columns: 2,
141 }),
142}
143
144export function getLayout(id: LayoutId): StripLayout {
145 return LAYOUTS[id]
146}
147