support arbitrary grid sizes; add banana theme
Hunter
Monday, November 17th, 2025 at 11:00 AM
22 ## usage
23 <img src="readme_images/usage.gif">
24
-Different Techo types use differently-spaced grid paper. Use the links below to select the appropriate spacing for your Techo.
25 +Different Techo types use differently-sized grid paper. Use the links below to select the appropriate grid size for your Techo.
26
27 | <b>Techo</b> | <b>Link</b> |
28 |-------------------|-------------|
-| Planner A6 | [Use 4mm grid ↗](link)<br>(default) |
-| Original<br>Cousin<br>HON A6<br>HON A5<br>Original Avec<br>Cousin Avec<br>5-Year Techo A6<br>5-Year Techo A5<br>Day-Free A6<br>Day-Free A5 | [Use 3.7mm grid ↗](link) |
-| Weeks<br>Weeks Mega | [Use 3.55mm grid ↗](link) |
29 +| Planner A6 | [Use 4mm grid ↗](https://hunterirving.github.io/memori?grid-size=4mm)<br>(default) |
30 +| Original<br>Cousin<br>HON A6<br>HON A5<br>Original Avec<br>Cousin Avec<br>5-Year Techo A6<br>5-Year Techo A5<br>Day-Free A6<br>Day-Free A5 | [Use 3.7mm grid ↗](https://hunterirving.github.io/memori?grid-size=3.7mm) |
31 +| Weeks<br>Weeks Mega | [Use 3.55mm grid ↗](https://hunterirving.github.io/memori?grid-size=3.55mm) |
32
-After opening memori in your web browser, drag one or more images from your desktop onto the grid.
33
34 +
35 +### controls
36 +
37 +- After opening memori in your web browser, drag one or more images from your desktop onto the grid.<br>
38 <img src="readme_images/drag_n_drop.gif">
39
40 - click and drag images to move them<br>
-const GRID_COLS = 49;
-const GRID_ROWS = 66;
-const CELL_SIZE_MM = 4; // Physical size of each cell when printed
1 +// Paper dimensions in mm
2 +const LETTER_WIDTH_MM = 215.9; // 8.5 inches
3 +const LETTER_HEIGHT_MM = 279.4; // 11 inches
4 +const A4_WIDTH_MM = 210;
5 +const A4_HEIGHT_MM = 297;
6 +const MARGIN_MM = 6.35; // 0.25 inches
7 +
8 +// Calculate maximum grid dimensions that fit both Letter and A4 with margins
9 +function calculateGridDimensions(cellSize) {
10 + // Available printable area (limiting factor is the smaller of Letter/A4 for each dimension)
11 + const availableWidth = Math.min(LETTER_WIDTH_MM, A4_WIDTH_MM) - (2 * MARGIN_MM);
12 + const availableHeight = Math.min(LETTER_HEIGHT_MM, A4_HEIGHT_MM) - (2 * MARGIN_MM);
13 +
14 + // Calculate how many cells fit
15 + const cols = Math.floor(availableWidth / cellSize);
16 + const rows = Math.floor(availableHeight / cellSize);
17 +
18 + return { cols, rows };
19 +}
20 +
21 +// Calculate grid dimensions as percentage of Letter paper (used for screen layout)
22 +function calculateGridPercentages(cellSize, cols, rows) {
23 + const gridWidthMM = cols * cellSize;
24 + const gridHeightMM = rows * cellSize;
25 + return {
26 + widthPercent: (gridWidthMM / LETTER_WIDTH_MM) * 100,
27 + heightPercent: (gridHeightMM / LETTER_HEIGHT_MM) * 100
28 + };
29 +}
30 +
31 +// Cell size bounds (in mm)
32 +const MIN_CELL_SIZE_MM = 2;
33 +const MAX_CELL_SIZE_MM = 10;
34 +
35 +let GRID_COLS = 49;
36 +let GRID_ROWS = 66;
37 +let CELL_SIZE_MM = 4; // Physical size of each cell when printed (can be overridden by URL parameter)
38
39 const grid = document.getElementById('grid');
40 const page = document.querySelector('.page');
42 let popupElement = null;
43 let popupTimeout = null;
44
45 +// Parse URL parameters for custom cell size
46 +function parseCellSizeFromURL() {
47 + const urlParams = new URLSearchParams(window.location.search);
48 + const gridSizeParam = urlParams.get('grid-size');
49 +
50 + if (gridSizeParam) {
51 + // Remove 'mm' suffix if present
52 + const sizeStr = gridSizeParam.toLowerCase().replace('mm', '').trim();
53 + const size = parseFloat(sizeStr);
54 +
55 + // Check if size is valid
56 + if (isNaN(size)) {
57 + showPopup('Invalid grid size. Using default 4mm.', 3000);
58 + return 4;
59 + }
60 +
61 + // Check if size is within reasonable bounds
62 + if (size < MIN_CELL_SIZE_MM || size > MAX_CELL_SIZE_MM) {
63 + showPopup(`Grid size must be between ${MIN_CELL_SIZE_MM}mm and ${MAX_CELL_SIZE_MM}mm. Using default 4mm.`, 3500);
64 + return 4;
65 + }
66 +
67 + // Valid size
68 + showPopup(`Grid set to ${size}mm`);
69 + return size;
70 + }
71 +
72 + return 4; // Default
73 +}
74 +
75 +// Initialize cell size from URL
76 +CELL_SIZE_MM = parseCellSizeFromURL();
77 +
78 +// Calculate grid dimensions based on cell size
79 +const gridDimensions = calculateGridDimensions(CELL_SIZE_MM);
80 +GRID_COLS = gridDimensions.cols;
81 +GRID_ROWS = gridDimensions.rows;
82 +
83 +// Calculate grid percentages for screen layout
84 +const gridPercentages = calculateGridPercentages(CELL_SIZE_MM, GRID_COLS, GRID_ROWS);
85 +
86 +// Update CSS variables for both screen and print
87 +document.documentElement.style.setProperty('--cell-size-mm', `${CELL_SIZE_MM}mm`);
88 +document.documentElement.style.setProperty('--grid-cols', GRID_COLS);
89 +document.documentElement.style.setProperty('--grid-rows', GRID_ROWS);
90 +document.documentElement.style.setProperty('--grid-width-percent', `${gridPercentages.widthPercent}%`);
91 +document.documentElement.style.setProperty('--grid-height-percent', `${gridPercentages.heightPercent}%`);
92 +
93 // Calculate cell size dynamically based on actual grid dimensions
94 function getCellSize() {
95 const gridRect = grid.getBoundingClientRect();
160 for (let i = 0; i < GRID_COLS * GRID_ROWS; i++) {
161 const cell = document.createElement('div');
162 cell.className = 'grid-cell';
163 +
164 + // Add right border to rightmost column
165 + const col = i % GRID_COLS;
166 + if (col === GRID_COLS - 1) {
167 + cell.classList.add('right-edge');
168 + }
169 +
170 + // Add bottom border to bottom row
171 + const row = Math.floor(i / GRID_COLS);
172 + if (row === GRID_ROWS - 1) {
173 + cell.classList.add('bottom-edge');
174 + }
175 +
176 grid.appendChild(cell);
177 }
178
179 +// Apply solid border style for grids 3.56mm or smaller
180 +if (CELL_SIZE_MM <= 3.56) {
181 + document.documentElement.classList.add('solid-grid');
182 +}
183 +
184 let images = [];
185 let dragState = null;
186 let resizeState = null;
1097 // Theme system
1098 let currentThemeIndex = 0;
1099 let isF2Pressed = false;
-const themes = ['sea-breeze', 'grape-soda', 'grapefruit', 'guac', 'mojito', 'toast'];
1100 +const themes = ['sea-breeze', 'grape-soda', 'grapefruit', 'guac', 'mojito', 'banana'];
1101
1102 function setTheme(theme) {
1103 document.documentElement.setAttribute('data-theme', theme);
12 --lime: #a9cf51;
13 --avocado: #7a8520;
14
- /* toast theme colors */
- --butter: #ffe598;
- --white-wheat: #fffef9;
- --crumb: #decca3;
- --peanut-butter: #be852a;
15 + /* banana theme colors */
16 + --peel: #ffcc12;
17 + --flesh: #f8f5e3;
18 + --unripe: #a5d269;
19 + --bruise: #4d4235;
20
21 /* mojito theme colors */
22 --mint: #9febaa;
56 --desk: var(--cerulean);
57 --page: var(--mist);
58 --grid-line: var(--horizon);
59 + --grid-line-light: color-mix(in srgb, var(--grid-line) 65%, transparent);
60 --accent: var(--lagoon);
61
62 /* Set text color for all themes */
74 --accent: var(--onion);
75 }
76
-:root[data-theme="toast"] {
- --desk: var(--butter);
- --page: var(--white-wheat);
- --grid-line: var(--crumb);
- --accent: var(--peanut-butter);
77 +:root[data-theme="banana"] {
78 + --desk: var(--peel);
79 + --page: var(--flesh);
80 + --grid-line: var(--unripe);
81 + --accent: var(--bruise);
82 }
83
84 :root[data-theme="mojito"] {
166 display: block;
167 }
168
-/* Add right border to rightmost column (every 49th cell starting from cell 49) */
-.grid-cell:nth-child(49n) {
169 +/* Add right border to rightmost column */
170 +.grid-cell.right-edge {
171 border-right: 1px dashed var(--grid-line);
172 width: calc(100% + 1px);
173 }
174
-/* Add bottom border to bottom row (last 49 cells: 3185-3234) */
-.grid-cell:nth-child(n + 3185) {
175 +/* Add bottom border to bottom row */
176 +.grid-cell.bottom-edge {
177 border-bottom: 1px dashed var(--grid-line);
178 height: calc(100% + 1px);
179 }
180
181 +/* Solid grid lines for cell sizes <= 3.56mm (screen preview only) */
182 +.solid-grid .grid-cell {
183 + border-top-style: solid;
184 + border-left-style: solid;
185 + border-top-color: var(--grid-line-light);
186 + border-left-color: var(--grid-line-light);
187 +}
188 +
189 +.solid-grid .grid-cell.right-edge {
190 + border-right-style: solid;
191 + border-right-color: var(--grid-line-light);
192 +}
193 +
194 +.solid-grid .grid-cell.bottom-edge {
195 + border-bottom-style: solid;
196 + border-bottom-color: var(--grid-line-light);
197 +}
198 +
199 @media (max-width: 680px) {
200 body {
201 padding: 0;
209 .grid-cell {
210 border-top-style: solid;
211 border-left-style: solid;
- border-top-color: color-mix(in srgb, var(--grid-line) 65%, transparent);
- border-left-color: color-mix(in srgb, var(--grid-line) 65%, transparent);
212 + border-top-color: var(--grid-line-light);
213 + border-left-color: var(--grid-line-light);
214 }
215
- .grid-cell:nth-child(49n) {
216 + .grid-cell.right-edge {
217 border-right-style: solid;
- border-right-color: color-mix(in srgb, var(--grid-line) 65%, transparent);
218 + border-right-color: var(--grid-line-light);
219 width: calc(100% + 1px);
220 }
221
- .grid-cell:nth-child(n + 3185) {
222 + .grid-cell.bottom-edge {
223 border-bottom-style: solid;
- border-bottom-color: color-mix(in srgb, var(--grid-line) 65%, transparent);
224 + border-bottom-color: var(--grid-line-light);
225 height: calc(100% + 1px);
226 }
227 }
502 padding: 0 !important;
503 }
504
- .grid-cell:nth-child(49n) {
505 + .grid-cell.right-edge {
506 border-right: 1px dashed var(--default-grid) !important;
507 width: calc(100% + 1px) !important;
508 }
509
- .grid-cell:nth-child(n + 3185) {
510 + .grid-cell.bottom-edge {
511 border-bottom: 1px dashed var(--default-grid) !important;
512 height: calc(100% + 1px) !important;
513 }
514
- /* Remove bottom border from cell above bottom-right corner to prevent artifact */
- .grid-cell:nth-child(3185) {
- border-bottom: none !important;
- height: 100% !important;
- }
-
515 .image-container {
516 outline: none !important;
517 position: absolute !important;
556 .add-images-btn {
557 display: none !important;
558 }
559 +
560 + .popup-notification {
561 + display: none !important;
562 + }
563 }
564
565 @page {
613 transform: translateX(-50%);
614 background-color: var(--accent);
615 color: var(--page);
- border: 2px solid var(--page);
616 padding: 0.7rem 0.9rem;
617 border-radius: 50rem;
618 font-size: 1.2rem;