phenixrhyder commited on
Commit
cc1ba69
·
unverified ·
1 Parent(s): 6ab51e0

Create Index.html

Browse files
Files changed (1) hide show
  1. Index.html +380 -0
Index.html ADDED
@@ -0,0 +1,380 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Frame Studio</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
11
+ <style>
12
+ body {
13
+ font-family: 'Poppins', sans-serif;
14
+ background-color: #111827; /* gray-900 */
15
+ }
16
+ /* Custom scrollbar for a sleeker look */
17
+ ::-webkit-scrollbar { width: 8px; }
18
+ ::-webkit-scrollbar-track { background: #1f2937; } /* gray-800 */
19
+ ::-webkit-scrollbar-thumb { background: #4f46e5; border-radius: 4px; } /* indigo-600 */
20
+ ::-webkit-scrollbar-thumb:hover { background: #6366f1; } /* indigo-500 */
21
+
22
+ /* Custom styles for range inputs */
23
+ input[type="range"] {
24
+ -webkit-appearance: none; appearance: none; width: 100%; height: 6px;
25
+ background: #374151; /* gray-700 */ border-radius: 3px;
26
+ outline: none; transition: background 0.2s;
27
+ }
28
+ input[type="range"]::-webkit-slider-thumb {
29
+ -webkit-appearance: none; appearance: none; width: 20px; height: 20px;
30
+ border-radius: 50%; background: #fff; cursor: pointer;
31
+ border: 4px solid #4f46e5; /* indigo-600 */
32
+ transition: transform 0.2s ease-in-out;
33
+ }
34
+ input[type="range"]:hover::-webkit-slider-thumb { transform: scale(1.1); }
35
+
36
+ /* Custom styles for color inputs */
37
+ input[type="color"] {
38
+ -webkit-appearance: none; -moz-appearance: none; appearance: none;
39
+ width: 48px; height: 48px; background-color: transparent;
40
+ border: none; cursor: pointer; border-radius: 0.5rem;
41
+ }
42
+ input[type="color"]::-webkit-color-swatch {
43
+ border-radius: 0.5rem;
44
+ border: 2px solid #4b5563; /* gray-600 */
45
+ }
46
+
47
+ /* Modal styles */
48
+ #saveModal { display: none; }
49
+ .modal-enter { opacity: 0; transform: scale(0.95); }
50
+ .modal-enter-active { opacity: 1; transform: scale(1); transition: all 300ms ease-out; }
51
+ .modal-leave { opacity: 1; transform: scale(1); }
52
+ .modal-leave-active { opacity: 0; transform: scale(0.95); transition: all 200ms ease-in; }
53
+ </style>
54
+ </head>
55
+ <body class="text-gray-200">
56
+
57
+ <div class="container mx-auto p-4 lg:p-8">
58
+ <!-- Header -->
59
+ <header class="text-center mb-8 lg:mb-12">
60
+ <h1 class="text-4xl lg:text-5xl font-bold text-white tracking-tight">Frame Studio</h1>
61
+ <p class="text-lg text-gray-400 mt-2">Craft your perfect checkerboard frame.</p>
62
+ </header>
63
+
64
+ <main class="grid grid-cols-1 lg:grid-cols-5 gap-8">
65
+ <!-- Controls Column -->
66
+ <div class="lg:col-span-2 space-y-8">
67
+ <!-- Appearance Controls -->
68
+ <div class="bg-gray-800/50 backdrop-blur-sm p-6 rounded-2xl border border-gray-700">
69
+ <h2 class="text-2xl font-semibold text-white mb-5">Appearance</h2>
70
+ <div class="flex items-center justify-around space-x-4">
71
+ <!-- Color 1 -->
72
+ <div class="text-center space-y-2">
73
+ <input id="color1" type="color" value="#334155">
74
+ <label for="color1" class="block text-sm font-medium text-gray-300">Color 1</label>
75
+ <span id="color1Value" class="font-mono text-xs text-indigo-400">#334155</span>
76
+ <label class="flex items-center justify-center gap-2 text-gray-400 text-sm pt-1"><input id="color1Transparent" type="checkbox" class="h-4 w-4 rounded bg-gray-600 border-gray-500 text-indigo-500 focus:ring-indigo-500"> Transp.</label>
77
+ </div>
78
+ <!-- Swap & Randomize Buttons -->
79
+ <div class="space-y-4">
80
+ <button id="swapBtn" title="Swap Colors" class="p-3 bg-gray-700 hover:bg-indigo-600 rounded-full transition-all duration-200 group">
81
+ <svg class="w-6 h-6 text-gray-300 group-hover:text-white transition-transform group-hover:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"></path></svg>
82
+ </button>
83
+ <button id="randomizeBtn" title="Randomize" class="p-3 bg-gray-700 hover:bg-indigo-600 rounded-full transition-all duration-200 group">
84
+ <svg class="w-6 h-6 text-gray-300 group-hover:text-white transition-transform group-hover:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 16v-2m8-6h-2m-16 0H4m14.485 8.485l-1.414-1.414M5.929 5.929L4.515 4.515m14.97 14.97l-1.414-1.414M5.929 18.071l-1.414 1.414M12 18a6 6 0 100-12 6 6 0 000 12z"></path></svg>
85
+ </button>
86
+ </div>
87
+ <!-- Color 2 -->
88
+ <div class="text-center space-y-2">
89
+ <input id="color2" type="color" value="#94a3b8">
90
+ <label for="color2" class="block text-sm font-medium text-gray-300">Color 2</label>
91
+ <span id="color2Value" class="font-mono text-xs text-indigo-400">#94a3b8</span>
92
+ <label class="flex items-center justify-center gap-2 text-gray-400 text-sm pt-1"><input id="color2Transparent" type="checkbox" class="h-4 w-4 rounded bg-gray-600 border-gray-500 text-indigo-500 focus:ring-indigo-500"> Transp.</label>
93
+ </div>
94
+ </div>
95
+ </div>
96
+
97
+ <!-- Pattern Controls -->
98
+ <div class="bg-gray-800/50 backdrop-blur-sm p-6 rounded-2xl border border-gray-700 space-y-6">
99
+ <h2 class="text-2xl font-semibold text-white mb-2">Pattern</h2>
100
+ <div>
101
+ <label for="frameThickness" class="flex justify-between items-center font-medium text-gray-300 mb-2">
102
+ <span>Frame Thickness</span>
103
+ <span id="frameThicknessValue" class="font-mono text-indigo-400 bg-gray-900 px-2 py-1 text-sm rounded-md">3</span>
104
+ </label>
105
+ <input id="frameThickness" type="range" min="1" max="20" value="3" class="w-full">
106
+ </div>
107
+ <div>
108
+ <label for="checkerSize" class="flex justify-between items-center font-medium text-gray-300 mb-2">
109
+ <span>Checker Size</span>
110
+ <span id="checkerSizeValue" class="font-mono text-indigo-400 bg-gray-900 px-2 py-1 text-sm rounded-md">25</span>
111
+ </label>
112
+ <input id="checkerSize" type="range" min="5" max="100" value="25" class="w-full">
113
+ </div>
114
+ </div>
115
+
116
+ <!-- Export Button -->
117
+ <button id="showSaveModalBtn" class="w-full text-lg font-bold py-4 px-4 rounded-xl transition-all duration-300 bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 text-white shadow-lg hover:shadow-indigo-500/50 transform hover:scale-105">
118
+ Export Images...
119
+ </button>
120
+ </div>
121
+
122
+ <!-- Canvas Column -->
123
+ <div class="lg:col-span-3">
124
+ <div class="aspect-square bg-gray-900/50 rounded-2xl overflow-hidden border-2 border-gray-700 sticky top-8">
125
+ <canvas id="checkerboardCanvas"></canvas>
126
+ </div>
127
+ </div>
128
+ </main>
129
+ </div>
130
+
131
+ <!-- Save Modal -->
132
+ <div id="saveModal" class="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4">
133
+ <div id="modalContent" class="bg-gray-800 rounded-2xl shadow-2xl p-8 w-full max-w-lg border border-gray-700">
134
+ <!-- View 1: Options -->
135
+ <div id="modalOptionsView">
136
+ <h2 class="text-2xl font-bold text-white mb-2">Export Options</h2>
137
+ <p class="text-gray-400 mb-6">Select sizes to generate. You can pick multiple.</p>
138
+ <div id="sizeOptions" class="grid grid-cols-2 sm:grid-cols-3 gap-3 mb-6"></div>
139
+ <div class="mb-6 space-y-4">
140
+ <label class="block text-gray-300 font-medium">Custom Size</label>
141
+ <div class="flex items-center gap-3">
142
+ <input type="number" id="customWidth" placeholder="Width" class="w-full bg-gray-700 border border-gray-600 rounded-md px-3 py-2 text-white placeholder-gray-400 focus:ring-indigo-500 focus:border-indigo-500">
143
+ <span class="text-gray-500">x</span>
144
+ <input type="number" id="customHeight" placeholder="Height" class="w-full bg-gray-700 border border-gray-600 rounded-md px-3 py-2 text-white placeholder-gray-400 focus:ring-indigo-500 focus:border-indigo-500">
145
+ </div>
146
+ </div>
147
+ <div class="flex gap-4">
148
+ <button id="cancelSaveBtn" class="w-full bg-gray-600 hover:bg-gray-500 text-white font-bold py-3 px-4 rounded-lg transition-colors">Cancel</button>
149
+ <button id="generateBtn" class="w-full bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 text-white font-bold py-3 px-4 rounded-lg transition-all">Generate</button>
150
+ </div>
151
+ </div>
152
+
153
+ <!-- View 2: Results -->
154
+ <div id="modalResultsView" class="hidden">
155
+ <h2 class="text-2xl font-bold text-white mb-2">Your Images</h2>
156
+ <p class="text-gray-400 mb-6">Long-press or right-click an image to save.</p>
157
+ <div id="generatedImagesContainer" class="space-y-4 max-h-[60vh] overflow-y-auto p-1 -mr-4 pr-4"></div>
158
+ <div class="flex mt-6">
159
+ <button id="backToOptionsBtn" class="w-full bg-gray-600 hover:bg-gray-500 text-white font-bold py-3 px-4 rounded-lg transition-colors">Back</button>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </div>
164
+
165
+ <script>
166
+ document.addEventListener('DOMContentLoaded', () => {
167
+ // --- STATE MANAGEMENT ---
168
+ const state = {
169
+ frameThickness: 3,
170
+ checkerSize: 25,
171
+ color1: '#334155',
172
+ color2: '#94a3b8',
173
+ color1Transparent: false,
174
+ color2Transparent: false,
175
+ };
176
+
177
+ // --- DOM ELEMENT SELECTION ---
178
+ const dom = {
179
+ canvas: document.getElementById('checkerboardCanvas'),
180
+ controls: {
181
+ frameThickness: document.getElementById('frameThickness'),
182
+ checkerSize: document.getElementById('checkerSize'),
183
+ color1: document.getElementById('color1'),
184
+ color2: document.getElementById('color2'),
185
+ color1Transparent: document.getElementById('color1Transparent'),
186
+ color2Transparent: document.getElementById('color2Transparent'),
187
+ swapBtn: document.getElementById('swapBtn'),
188
+ randomizeBtn: document.getElementById('randomizeBtn'),
189
+ },
190
+ values: {
191
+ frameThickness: document.getElementById('frameThicknessValue'),
192
+ checkerSize: document.getElementById('checkerSizeValue'),
193
+ color1: document.getElementById('color1Value'),
194
+ color2: document.getElementById('color2Value'),
195
+ },
196
+ modal: {
197
+ element: document.getElementById('saveModal'),
198
+ content: document.getElementById('modalContent'),
199
+ showBtn: document.getElementById('showSaveModalBtn'),
200
+ optionsView: document.getElementById('modalOptionsView'),
201
+ cancelBtn: document.getElementById('cancelSaveBtn'),
202
+ generateBtn: document.getElementById('generateBtn'),
203
+ sizeOptions: document.getElementById('sizeOptions'),
204
+ customWidth: document.getElementById('customWidth'),
205
+ customHeight: document.getElementById('customHeight'),
206
+ resultsView: document.getElementById('modalResultsView'),
207
+ imagesContainer: document.getElementById('generatedImagesContainer'),
208
+ backBtn: document.getElementById('backToOptionsBtn'),
209
+ }
210
+ };
211
+ const ctx = dom.canvas.getContext('2d');
212
+
213
+ // --- CONFIGURATION ---
214
+ const PREDEFINED_SIZES = [
215
+ { w: 256, h: 256 }, { w: 512, h: 512 }, { w: 1024, h: 1024 },
216
+ { w: 1920, h: 1080 }, { w: 1080, h: 1920 }, { w: 1600, h: 900 }
217
+ ];
218
+
219
+ // --- CORE LOGIC ---
220
+ const drawPattern = (targetCtx, width, height, config) => {
221
+ const { frameThickness, checkerSize, color1, color2, color1Transparent, color2Transparent } = config;
222
+ const c1 = color1Transparent ? 'transparent' : color1;
223
+ const c2 = color2Transparent ? 'transparent' : color2;
224
+
225
+ const cols = Math.ceil(width / checkerSize);
226
+ const rows = Math.ceil(height / checkerSize);
227
+ targetCtx.clearRect(0, 0, width, height);
228
+
229
+ for (let i = 0; i < rows; i++) {
230
+ for (let j = 0; j < cols; j++) {
231
+ const isFrame = i < frameThickness || i >= rows - frameThickness || j < frameThickness || j >= cols - frameThickness;
232
+ if (isFrame) {
233
+ targetCtx.fillStyle = (i + j) % 2 === 0 ? c1 : c2;
234
+ if (targetCtx.fillStyle !== 'transparent') {
235
+ targetCtx.fillRect(j * checkerSize, i * checkerSize, checkerSize, checkerSize);
236
+ }
237
+ }
238
+ }
239
+ }
240
+ };
241
+
242
+ const drawLiveCanvas = () => {
243
+ const boardSize = dom.canvas.parentElement.clientWidth;
244
+ dom.canvas.width = boardSize;
245
+ dom.canvas.height = boardSize;
246
+ const liveConfig = { ...state, checkerSize: state.checkerSize * (boardSize / 1000) }; // Scale checker size for preview
247
+ drawPattern(ctx, boardSize, boardSize, liveConfig);
248
+ };
249
+
250
+ const updateUI = () => {
251
+ // Update sliders and colors from state
252
+ for (const key in dom.controls) {
253
+ if (state.hasOwnProperty(key)) {
254
+ const el = dom.controls[key];
255
+ const value = state[key];
256
+ if (el.type === 'checkbox') el.checked = value;
257
+ else el.value = value;
258
+ }
259
+ }
260
+ // Update text value displays
261
+ for (const key in dom.values) {
262
+ if (state.hasOwnProperty(key)) {
263
+ dom.values[key].textContent = state[key];
264
+ }
265
+ }
266
+ drawLiveCanvas();
267
+ };
268
+
269
+ // --- EVENT HANDLERS ---
270
+ const handleControlInput = (e) => {
271
+ const { id, value, type, checked } = e.target;
272
+ state[id] = type === 'checkbox' ? checked : value;
273
+ if(type === 'range') state[id] = parseInt(value, 10);
274
+ updateUI();
275
+ };
276
+
277
+ const handleSwap = () => {
278
+ [state.color1, state.color2] = [state.color2, state.color1];
279
+ [state.color1Transparent, state.color2Transparent] = [state.color2Transparent, state.color1Transparent];
280
+ updateUI();
281
+ };
282
+
283
+ const handleRandomize = () => {
284
+ const randomHex = () => '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
285
+ state.color1 = randomHex();
286
+ state.color2 = randomHex();
287
+ state.frameThickness = Math.floor(Math.random() * 19) + 1;
288
+ state.checkerSize = Math.floor(Math.random() * 95) + 5;
289
+ state.color1Transparent = false;
290
+ state.color2Transparent = Math.random() < 0.2; // 20% chance of being transparent
291
+ updateUI();
292
+ };
293
+
294
+ const handleGenerate = () => {
295
+ const sizesToGenerate = [];
296
+ document.querySelectorAll('#sizeOptions input:checked').forEach(cb => {
297
+ const [w, h] = cb.value.split('x').map(Number);
298
+ sizesToGenerate.push({ w, h });
299
+ });
300
+ const customW = parseInt(dom.modal.customWidth.value, 10);
301
+ const customH = parseInt(dom.modal.customHeight.value, 10);
302
+ if (customW > 0 && customH > 0) {
303
+ sizesToGenerate.push({ w: customW, h: customH });
304
+ }
305
+
306
+ if (sizesToGenerate.length === 0) { /* ... error handling ... */ return; }
307
+
308
+ dom.modal.imagesContainer.innerHTML = '';
309
+ sizesToGenerate.forEach(size => {
310
+ const offscreenCanvas = document.createElement('canvas');
311
+ offscreenCanvas.width = size.w;
312
+ offscreenCanvas.height = size.h;
313
+ drawPattern(offscreenCanvas.getContext('2d'), size.w, size.h, state);
314
+
315
+ const imageCard = document.createElement('div');
316
+ imageCard.className = 'bg-gray-700/50 p-3 rounded-lg';
317
+ imageCard.innerHTML = `
318
+ <p class="text-gray-300 font-semibold mb-2">${size.w} x ${size.h} px</p>
319
+ <img src="${offscreenCanvas.toDataURL('image/png')}" alt="Generated frame" class="rounded-md border border-gray-600 w-full">`;
320
+ dom.modal.imagesContainer.appendChild(imageCard);
321
+ });
322
+
323
+ // Clear inputs and switch view
324
+ dom.modal.customWidth.value = dom.modal.customHeight.value = '';
325
+ document.querySelectorAll('#sizeOptions input:checked').forEach(cb => cb.checked = false);
326
+ dom.modal.optionsView.classList.add('hidden');
327
+ dom.modal.resultsView.classList.remove('hidden');
328
+ };
329
+
330
+ // --- MODAL ---
331
+ const openModal = () => {
332
+ dom.modal.element.style.display = 'flex';
333
+ requestAnimationFrame(() => dom.modal.content.classList.add('modal-enter-active'));
334
+ };
335
+ const closeModal = () => {
336
+ dom.modal.content.classList.remove('modal-enter-active');
337
+ dom.modal.content.classList.add('modal-leave-active');
338
+ setTimeout(() => {
339
+ dom.modal.element.style.display = 'none';
340
+ dom.modal.content.classList.remove('modal-leave-active');
341
+ // Reset modal to options view
342
+ dom.modal.resultsView.classList.add('hidden');
343
+ dom.modal.optionsView.classList.remove('hidden');
344
+ }, 200);
345
+ };
346
+
347
+ // --- INITIALIZATION ---
348
+ const initialize = () => {
349
+ // Populate size options
350
+ PREDEFINED_SIZES.forEach(size => {
351
+ const val = `${size.w}x${size.h}`;
352
+ dom.modal.sizeOptions.innerHTML += `
353
+ <label class="flex items-center gap-2 p-3 bg-gray-700 rounded-lg cursor-pointer hover:bg-gray-600 transition-colors">
354
+ <input type="checkbox" value="${val}" class="h-4 w-4 rounded bg-gray-600 border-gray-500 text-indigo-500 focus:ring-indigo-500">
355
+ <span class="text-gray-300 text-sm font-medium">${val}</span>
356
+ </label>`;
357
+ });
358
+
359
+ // Attach event listeners
360
+ Object.values(dom.controls).forEach(el => el.addEventListener('input', handleControlInput));
361
+ dom.controls.swapBtn.addEventListener('click', handleSwap);
362
+ dom.controls.randomizeBtn.addEventListener('click', handleRandomize);
363
+ dom.modal.showBtn.addEventListener('click', openModal);
364
+ dom.modal.cancelBtn.addEventListener('click', closeModal);
365
+ dom.modal.generateBtn.addEventListener('click', handleGenerate);
366
+ dom.modal.backBtn.addEventListener('click', () => {
367
+ dom.modal.resultsView.classList.add('hidden');
368
+ dom.modal.optionsView.classList.remove('hidden');
369
+ });
370
+
371
+ // Initial setup
372
+ new ResizeObserver(drawLiveCanvas).observe(dom.canvas.parentElement);
373
+ updateUI();
374
+ };
375
+
376
+ initialize();
377
+ });
378
+ </script>
379
+ </body>
380
+ </html>