initial commit
Hunter
Wednesday, October 15th, 2025 at 4:13 PM
1 +<!DOCTYPE html>
2 +<html lang="en">
3 +<head>
4 + <meta charset="UTF-8">
5 + <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 + <title>memori</title>
7 + <style>
8 + * {
9 + margin: 0;
10 + padding: 0;
11 + box-sizing: border-box;
12 + }
13 +
14 + body {
15 + background: #f5f5f0;
16 + display: flex;
17 + justify-content: center;
18 + align-items: flex-start;
19 + min-height: 100vh;
20 + padding: 20px;
21 + }
22 +
23 + .page {
24 + width: 8.5in;
25 + height: 11in;
26 + background: #fafaf8;
27 + position: relative;
28 + box-shadow: 0 2px 10px rgba(0,0,0,0.1);
29 + transform-origin: top center;
30 + }
31 +
32 + .grid {
33 + position: absolute;
34 + top: 0.25in;
35 + left: 0.25in;
36 + display: grid;
37 + grid-template-columns: repeat(50, 4mm);
38 + grid-template-rows: repeat(66, 4mm);
39 + gap: 0;
40 + line-height: 0;
41 + }
42 +
43 + .grid-cell {
44 + width: 4mm;
45 + height: 4mm;
46 + box-sizing: border-box;
47 + border-right: 1px solid #d0d0d0;
48 + border-bottom: 1px solid #d0d0d0;
49 + margin: 0;
50 + padding: 0;
51 + display: block;
52 + }
53 +
54 + .grid-cell:nth-child(-n+50) {
55 + border-top: 1px solid #d0d0d0;
56 + }
57 +
58 + .grid-cell:nth-child(50n+1) {
59 + border-left: 1px solid #d0d0d0;
60 + }
61 +
62 + .image-container {
63 + position: absolute;
64 + cursor: move;
65 + outline: 2px solid transparent;
66 + outline-offset: -2px;
67 + transition: outline-color 0.2s;
68 + }
69 +
70 + .image-container:hover {
71 + outline-color: #4a90e2;
72 + }
73 +
74 + .image-container.dragging {
75 + opacity: 0.7;
76 + outline-color: #4a90e2;
77 + }
78 +
79 + .image-container.resizing {
80 + outline-color: #4a90e2;
81 + }
82 +
83 + .image-container img {
84 + width: 100%;
85 + height: 100%;
86 + object-fit: cover;
87 + display: block;
88 + pointer-events: none;
89 + }
90 +
91 + .resize-handle {
92 + position: absolute;
93 + background: #4a90e2;
94 + opacity: 0;
95 + transition: opacity 0.2s;
96 + }
97 +
98 + .image-container:hover .resize-handle {
99 + opacity: 0.8;
100 + }
101 +
102 + .resize-handle.corner {
103 + width: 10px;
104 + height: 10px;
105 + border-radius: 50%;
106 + }
107 +
108 + .resize-handle.edge {
109 + background: transparent;
110 + }
111 +
112 + .resize-handle.n { top: -5px; left: 50%; transform: translateX(-50%); width: 30px; height: 10px; cursor: n-resize; }
113 + .resize-handle.s { bottom: -5px; left: 50%; transform: translateX(-50%); width: 30px; height: 10px; cursor: s-resize; }
114 + .resize-handle.e { right: -5px; top: 50%; transform: translateY(-50%); width: 10px; height: 30px; cursor: e-resize; }
115 + .resize-handle.w { left: -5px; top: 50%; transform: translateY(-50%); width: 10px; height: 30px; cursor: w-resize; }
116 + .resize-handle.ne { top: -5px; right: -5px; cursor: ne-resize; }
117 + .resize-handle.nw { top: -5px; left: -5px; cursor: nw-resize; }
118 + .resize-handle.se { bottom: -5px; right: -5px; cursor: se-resize; }
119 + .resize-handle.sw { bottom: -5px; left: -5px; cursor: sw-resize; }
120 +
121 + @media print {
122 + * {
123 + -webkit-print-color-adjust: exact !important;
124 + print-color-adjust: exact !important;
125 + }
126 +
127 + body {
128 + background: white;
129 + padding: 0;
130 + margin: 0;
131 + }
132 +
133 + .page {
134 + width: 8.5in;
135 + height: 11in;
136 + box-shadow: none;
137 + margin: 0;
138 + padding: 0;
139 + background: white;
140 + transform: none !important;
141 + }
142 +
143 + .grid {
144 + position: absolute;
145 + top: 0.25in;
146 + left: 0.25in;
147 + }
148 +
149 + .grid-cell {
150 + border-right: 1px solid #d0d0d0 !important;
151 + border-bottom: 1px solid #d0d0d0 !important;
152 + box-sizing: border-box !important;
153 + width: 4mm !important;
154 + height: 4mm !important;
155 + margin: 0 !important;
156 + padding: 0 !important;
157 + }
158 +
159 + .grid-cell:nth-child(-n+50) {
160 + border-top: 1px solid #d0d0d0 !important;
161 + }
162 +
163 + .grid-cell:nth-child(50n+1) {
164 + border-left: 1px solid #d0d0d0 !important;
165 + }
166 +
167 + .image-container {
168 + outline: none !important;
169 + position: absolute !important;
170 + }
171 +
172 + .image-container img {
173 + width: 100% !important;
174 + height: 100% !important;
175 + object-fit: cover !important;
176 + }
177 +
178 + .resize-handle {
179 + display: none !important;
180 + }
181 + }
182 +
183 + @page {
184 + size: 8.5in 11in;
185 + margin: 0;
186 + }
187 + </style>
188 +</head>
189 +<body>
190 + <div class="page">
191 + <div class="grid" id="grid"></div>
192 + </div>
193 +
194 + <script>
195 + const MM_TO_PX = 96 / 25.4;
196 + const CELL_SIZE_MM = 4;
197 + const CELL_SIZE_PX = CELL_SIZE_MM * MM_TO_PX;
198 + const GRID_OFFSET_IN = 0.25;
199 + const GRID_OFFSET_PX = GRID_OFFSET_IN * 96;
200 + const GRID_COLS = 50;
201 + const GRID_ROWS = 66;
202 +
203 + const grid = document.getElementById('grid');
204 + const page = document.querySelector('.page');
205 +
206 + // Create grid cells
207 + for (let i = 0; i < GRID_COLS * GRID_ROWS; i++) {
208 + const cell = document.createElement('div');
209 + cell.className = 'grid-cell';
210 + grid.appendChild(cell);
211 + }
212 +
213 + let images = [];
214 + let dragState = null;
215 + let resizeState = null;
216 +
217 + // Prevent default drag behavior
218 + page.addEventListener('dragover', (e) => {
219 + e.preventDefault();
220 + e.stopPropagation();
221 + });
222 +
223 + page.addEventListener('drop', (e) => {
224 + e.preventDefault();
225 + e.stopPropagation();
226 +
227 + const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));
228 +
229 + files.forEach((file, idx) => {
230 + const reader = new FileReader();
231 + reader.onload = (event) => {
232 + const img = new Image();
233 + img.onload = () => {
234 + const aspectRatio = img.width / img.height;
235 +
236 + // Calculate best fit in grid cells
237 + let widthCells = Math.round(Math.sqrt(16 * aspectRatio));
238 + let heightCells = Math.round(widthCells / aspectRatio);
239 +
240 + // Ensure minimum size
241 + widthCells = Math.max(2, widthCells);
242 + heightCells = Math.max(2, heightCells);
243 +
244 + // Calculate position in grid cells
245 + const xCell = 2 + (idx * 2) % 20;
246 + const yCell = 2 + Math.floor((idx * 2) / 20) * 2;
247 +
248 + addImage(event.target.result, xCell, yCell, widthCells, heightCells);
249 + };
250 + img.src = event.target.result;
251 + };
252 + reader.readAsDataURL(file);
253 + });
254 + });
255 +
256 + function addImage(src, xCell, yCell, widthCells, heightCells) {
257 + const container = document.createElement('div');
258 + container.className = 'image-container';
259 +
260 + const img = document.createElement('img');
261 + img.src = src;
262 + container.appendChild(img);
263 +
264 + // Add resize handles
265 + const handles = ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'];
266 + handles.forEach(dir => {
267 + const handle = document.createElement('div');
268 + handle.className = `resize-handle ${dir.length === 1 ? 'edge' : 'corner'} ${dir}`;
269 + handle.dataset.direction = dir;
270 + container.appendChild(handle);
271 + });
272 +
273 + const imageData = { container, xCell, yCell, widthCells, heightCells };
274 + images.push(imageData);
275 +
276 + updateImagePosition(imageData);
277 + grid.appendChild(container);
278 +
279 + setupImageHandlers(imageData);
280 + }
281 +
282 + function updateImagePosition(img) {
283 + img.container.style.left = img.xCell * CELL_SIZE_PX + 'px';
284 + img.container.style.top = img.yCell * CELL_SIZE_PX + 'px';
285 + img.container.style.width = img.widthCells * CELL_SIZE_PX + 'px';
286 + img.container.style.height = img.heightCells * CELL_SIZE_PX + 'px';
287 + }
288 +
289 + function setupImageHandlers(imageData) {
290 + const container = imageData.container;
291 +
292 + // Moving
293 + container.addEventListener('mousedown', (e) => {
294 + if (e.target.classList.contains('resize-handle')) return;
295 +
296 + e.preventDefault();
297 + dragState = {
298 + image: imageData,
299 + startX: e.clientX,
300 + startY: e.clientY,
301 + startXCell: imageData.xCell,
302 + startYCell: imageData.yCell
303 + };
304 + container.classList.add('dragging');
305 + });
306 +
307 + // Resizing
308 + container.querySelectorAll('.resize-handle').forEach(handle => {
309 + handle.addEventListener('mousedown', (e) => {
310 + e.preventDefault();
311 + e.stopPropagation();
312 +
313 + resizeState = {
314 + image: imageData,
315 + direction: handle.dataset.direction,
316 + startX: e.clientX,
317 + startY: e.clientY,
318 + startXCell: imageData.xCell,
319 + startYCell: imageData.yCell,
320 + startWidthCells: imageData.widthCells,
321 + startHeightCells: imageData.heightCells
322 + };
323 + container.classList.add('resizing');
324 + });
325 + });
326 + }
327 +
328 + document.addEventListener('mousemove', (e) => {
329 + if (dragState) {
330 + const dx = e.clientX - dragState.startX;
331 + const dy = e.clientY - dragState.startY;
332 +
333 + const dxCells = Math.round(dx / CELL_SIZE_PX);
334 + const dyCells = Math.round(dy / CELL_SIZE_PX);
335 +
336 + const newXCell = Math.max(0, dragState.startXCell + dxCells);
337 + const newYCell = Math.max(0, dragState.startYCell + dyCells);
338 +
339 + dragState.image.xCell = newXCell;
340 + dragState.image.yCell = newYCell;
341 + updateImagePosition(dragState.image);
342 + }
343 +
344 + if (resizeState) {
345 + const dx = e.clientX - resizeState.startX;
346 + const dy = e.clientY - resizeState.startY;
347 +
348 + const dxCells = Math.round(dx / CELL_SIZE_PX);
349 + const dyCells = Math.round(dy / CELL_SIZE_PX);
350 +
351 + const dir = resizeState.direction;
352 + const img = resizeState.image;
353 +
354 + let newX = resizeState.startXCell;
355 + let newY = resizeState.startYCell;
356 + let newW = resizeState.startWidthCells;
357 + let newH = resizeState.startHeightCells;
358 +
359 + if (dir.includes('e')) {
360 + newW = Math.max(1, resizeState.startWidthCells + dxCells);
361 + }
362 + if (dir.includes('w')) {
363 + const delta = Math.min(dxCells, resizeState.startWidthCells - 1);
364 + newX = resizeState.startXCell + delta;
365 + newW = resizeState.startWidthCells - delta;
366 + }
367 + if (dir.includes('s')) {
368 + newH = Math.max(1, resizeState.startHeightCells + dyCells);
369 + }
370 + if (dir.includes('n')) {
371 + const delta = Math.min(dyCells, resizeState.startHeightCells - 1);
372 + newY = resizeState.startYCell + delta;
373 + newH = resizeState.startHeightCells - delta;
374 + }
375 +
376 + img.xCell = Math.max(0, newX);
377 + img.yCell = Math.max(0, newY);
378 + img.widthCells = newW;
379 + img.heightCells = newH;
380 + updateImagePosition(img);
381 + }
382 + });
383 +
384 + document.addEventListener('mouseup', () => {
385 + if (dragState) {
386 + dragState.image.container.classList.remove('dragging');
387 + dragState = null;
388 + }
389 + if (resizeState) {
390 + resizeState.image.container.classList.remove('resizing');
391 + resizeState = null;
392 + }
393 + });
394 + </script>
395 +</body>
396 +</html>