Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Generative Shape Explorer 3000</title> | |
| <!-- FontAwesome for Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --bg-color: #0f172a; | |
| --card-bg: #1e293b; | |
| --primary: #3b82f6; | |
| --primary-hover: #2563eb; | |
| --accent: #8b5cf6; | |
| --text-main: #f8fafc; | |
| --text-muted: #94a3b8; | |
| --border: #334155; | |
| --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| --header-height: 70px; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; | |
| background-color: var(--bg-color); | |
| color: var(--text-main); | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| overflow-x: hidden; | |
| } | |
| /* Header */ | |
| header { | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| background: rgba(15, 23, 42, 0.85); | |
| backdrop-filter: blur(12px); | |
| border-bottom: 1px solid var(--border); | |
| height: var(--header-height); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0 2rem; | |
| box-shadow: var(--shadow); | |
| } | |
| .brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| font-size: 1.25rem; | |
| font-weight: 700; | |
| color: var(--text-main); | |
| } | |
| .brand i { | |
| color: var(--primary); | |
| } | |
| .anycoder-link { | |
| font-size: 0.9rem; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| transition: color 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| background: rgba(255, 255, 255, 0.05); | |
| padding: 6px 12px; | |
| border-radius: 20px; | |
| border: 1px solid var(--border); | |
| } | |
| .anycoder-link:hover { | |
| color: var(--primary); | |
| border-color: var(--primary); | |
| background: rgba(59, 130, 246, 0.1); | |
| } | |
| /* Controls Section */ | |
| .controls { | |
| padding: 2rem; | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 1rem; | |
| align-items: center; | |
| justify-content: center; | |
| background: linear-gradient(180deg, rgba(15, 23, 42, 1) 0%, rgba(15, 23, 42, 0) 100%); | |
| } | |
| .btn { | |
| background-color: var(--card-bg); | |
| color: var(--text-main); | |
| border: 1px solid var(--border); | |
| padding: 0.75rem 1.5rem; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| transition: all 0.2s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 0.95rem; | |
| } | |
| .btn:hover { | |
| background-color: var(--border); | |
| transform: translateY(-1px); | |
| } | |
| .btn-primary { | |
| background-color: var(--primary); | |
| border-color: var(--primary); | |
| color: white; | |
| box-shadow: 0 0 15px rgba(59, 130, 246, 0.4); | |
| } | |
| .btn-primary:hover { | |
| background-color: var(--primary-hover); | |
| box-shadow: 0 0 20px rgba(59, 130, 246, 0.6); | |
| } | |
| .btn-danger { | |
| color: #ef4444; | |
| border-color: rgba(239, 68, 68, 0.3); | |
| } | |
| .btn-danger:hover { | |
| background-color: rgba(239, 68, 68, 0.1); | |
| } | |
| /* Gallery Grid */ | |
| main { | |
| flex: 1; | |
| padding: 1rem 2rem 4rem 2rem; | |
| max-width: 1600px; | |
| margin: 0 auto; | |
| width: 100%; | |
| } | |
| .gallery-stats { | |
| margin-bottom: 1rem; | |
| color: var(--text-muted); | |
| font-size: 0.9rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .grid-container { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); | |
| gap: 1.5rem; | |
| width: 100%; | |
| } | |
| .shape-card { | |
| background-color: var(--card-bg); | |
| border-radius: 12px; | |
| overflow: hidden; | |
| border: 1px solid var(--border); | |
| transition: transform 0.3s ease, box-shadow 0.3s ease; | |
| position: relative; | |
| aspect-ratio: 1; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .shape-card:hover { | |
| transform: translateY(-5px) scale(1.02); | |
| box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3); | |
| border-color: var(--primary); | |
| z-index: 2; | |
| } | |
| .shape-card img { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: contain; | |
| background: radial-gradient(circle at center, #263345 0%, #1e293b 100%); | |
| } | |
| .shape-meta { | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| background: rgba(15, 23, 42, 0.9); | |
| padding: 8px; | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| transform: translateY(100%); | |
| transition: transform 0.2s ease; | |
| display: flex; | |
| justify-content: space-between; | |
| } | |
| .shape-card:hover .shape-meta { | |
| transform: translateY(0); | |
| } | |
| /* Loading Overlay */ | |
| .loader-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(15, 23, 42, 0.8); | |
| backdrop-filter: blur(5px); | |
| z-index: 200; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.3s ease; | |
| } | |
| .loader-overlay.active { | |
| opacity: 1; | |
| pointer-events: all; | |
| } | |
| .spinner { | |
| width: 50px; | |
| height: 50px; | |
| border: 4px solid var(--border); | |
| border-top: 4px solid var(--primary); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| margin-bottom: 1rem; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .loading-text { | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| } | |
| /* Toast Notification */ | |
| .toast { | |
| position: fixed; | |
| bottom: 2rem; | |
| right: 2rem; | |
| background: var(--card-bg); | |
| border: 1px solid var(--primary); | |
| padding: 1rem 1.5rem; | |
| border-radius: 8px; | |
| color: var(--text-main); | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3); | |
| transform: translateY(100px); | |
| opacity: 0; | |
| transition: all 0.4s cubic-bezier(0.68, -0.55, 0.27, 1.55); | |
| z-index: 300; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .toast.show { | |
| transform: translateY(0); | |
| opacity: 1; | |
| } | |
| .toast i { | |
| color: #10b981; | |
| } | |
| /* Footer */ | |
| footer { | |
| text-align: center; | |
| padding: 2rem; | |
| color: var(--text-muted); | |
| font-size: 0.85rem; | |
| border-top: 1px solid var(--border); | |
| margin-top: auto; | |
| } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| header { | |
| padding: 0 1rem; | |
| } | |
| .brand span { | |
| display: none; | |
| } | |
| .controls { | |
| padding: 1rem; | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| .grid-container { | |
| grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); | |
| gap: 1rem; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Header --> | |
| <header> | |
| <div class="brand"> | |
| <i class="fa-solid fa-shapes"></i> | |
| <span>ShapeGen 3000</span> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link"> | |
| <i class="fa-solid fa-code"></i> Built with anycoder | |
| </a> | |
| </header> | |
| <!-- Controls --> | |
| <section class="controls"> | |
| <button class="btn btn-primary" id="generateBtn"> | |
| <i class="fa-solid fa-wand-magic-sparkles"></i> Generate 300 Images | |
| </button> | |
| <button class="btn btn-danger" id="clearBtn"> | |
| <i class="fa-solid fa-trash-can"></i> Clear Gallery | |
| </button> | |
| </section> | |
| <!-- Main Content --> | |
| <main> | |
| <div class="gallery-stats"> | |
| <span id="statusText">Ready to generate.</span> | |
| <span id="countText">Items: 0</span> | |
| </div> | |
| <div class="grid-container" id="gallery"> | |
| <!-- Images will be injected here --> | |
| </div> | |
| </main> | |
| <!-- Footer --> | |
| <footer> | |
| <p>Procedural Generation using HTML5 Canvas & JavaScript</p> | |
| </footer> | |
| <!-- Loading Overlay --> | |
| <div class="loader-overlay" id="loader"> | |
| <div class="spinner"></div> | |
| <div class="loading-text" id="loadingText">Initializing...</div> | |
| </div> | |
| <!-- Toast Notification --> | |
| <div class="toast" id="toast"> | |
| <i class="fa-solid fa-check-circle"></i> | |
| <span id="toastMessage">Operation successful</span> | |
| </div> | |
| <script> | |
| /** | |
| * Shape Generator Logic | |
| * Handles math for 2D and simulated 3D shapes | |
| */ | |
| const ShapeGenerator = { | |
| canvas: document.createElement('canvas'), | |
| ctx: null, | |
| width: 400, | |
| height: 400, | |
| init() { | |
| this.canvas.width = this.width; | |
| this.canvas.height = this.height; | |
| this.ctx = this.canvas.getContext('2d'); | |
| }, | |
| // Helper: Random Integer | |
| randomInt(min, max) { | |
| return Math.floor(Math.random() * (max - min + 1)) + min; | |
| }, | |
| // Helper: Random Color (HSL for vibrancy) | |
| randomColor() { | |
| const h = this.randomInt(0, 360); | |
| const s = this.randomInt(60, 100); | |
| const l = this.randomInt(40, 70); | |
| return `hsl(${h}, ${s}%, ${l}%)`; | |
| }, | |
| // Helper: 3D Rotation Matrix | |
| rotate3D(x, y, z, angleX, angleY) { | |
| // Rotate around Y | |
| let cosY = Math.cos(angleY); | |
| let sinY = Math.sin(angleY); | |
| let x1 = x * cosY - z * sinY; | |
| let z1 = z * cosY + x * sinY; | |
| // Rotate around X | |
| let cosX = Math.cos(angleX); | |
| let sinX = Math.sin(angleX); | |
| let y2 = y * cosX - z1 * sinX; | |
| let z2 = z1 * cosX + y * sinX; | |
| return { x: x1, y: y2, z: z2 }; | |
| }, | |
| // Helper: Project 3D point to 2D canvas | |
| project(x, y, z) { | |
| const scale = 400; // Perspective scale | |
| const distance = 4; // Camera distance | |
| const factor = scale / (distance - z); | |
| return { | |
| x: x * factor + this.width / 2, | |
| y: y * factor + this.height / 2 | |
| }; | |
| }, | |
| clear() { | |
| this.ctx.clearRect(0, 0, this.width, this.height); | |
| // Add a subtle background gradient | |
| const grad = this.ctx.createRadialGradient(200, 200, 50, 200, 200, 300); | |
| grad.addColorStop(0, '#263345'); | |
| grad.addColorStop(1, '#0f172a'); | |
| this.ctx.fillStyle = grad; | |
| this.ctx.fillRect(0, 0, this.width, this.height); | |
| }, | |
| generateImage(id) { | |
| this.clear(); | |
| const type = Math.random() > 0.5 ? '3d' : '2d'; | |
| const color = this.randomColor(); | |
| const accentColor = this.randomColor(); | |
| // Randomize parameters | |
| const params = { | |
| angle: Math.random() * Math.PI * 2, // Rotation angle | |
| depth: Math.random() * 0.5 + 0.5, // Depth scale | |
| size: Math.random() * 0.5 + 0.5, // Overall size | |
| lineWidth: Math.random() * 3 + 1, | |
| filled: Math.random() > 0.3 | |
| }; | |
| let shapeName = ""; | |
| if (type === '2d') { | |
| // 2D Shapes with simulated depth (extrusion) | |
| const shapes = ['square', 'triangle', 'pentagon', 'hexagon', 'circle', 'star']; | |
| const shape = shapes[Math.floor(Math.random() * shapes.length)]; | |
| shapeName = shape.toUpperCase(); | |
| this.draw2DShape(shape, color, params); | |
| } else { | |
| // 3D Shapes | |
| const shapes = ['cube', 'pyramid', 'octahedron', 'torus']; | |
| const shape = shapes[Math.floor(Math.random() * shapes.length)]; | |
| shapeName = shape.toUpperCase(); | |
| this.draw3DShape(shape, color, params); | |
| } | |
| return { | |
| src: this.canvas.toDataURL('image/png'), | |
| meta: `${shapeName} | Angle: ${Math.round(params.angle * 57)}° | Depth: ${params.depth.toFixed(2)}` | |
| }; | |
| }, | |
| draw2DShape(shape, color, params) { | |
| const cx = this.width / 2; | |
| const cy = this.height / 2; | |
| const size = 100 * params.size; | |
| this.ctx.save(); | |
| this.ctx.translate(cx, cy); | |
| this.ctx.rotate(params.angle); | |
| this.ctx.strokeStyle = color; | |
| this.ctx.fillStyle = color.replace(')', ', 0.2)'); // Transparent fill | |
| this.ctx.lineWidth = params.lineWidth; | |
| this.ctx.lineJoin = 'round'; | |
| // Simulate depth by drawing multiple layers (Extrusion effect) | |
| const layers = Math.floor(params.depth * 10); | |
| const offset = 4; | |
| this.ctx.beginPath(); | |
| for(let i = 0; i < layers; i++) { | |
| this.drawPath(shape, size - (i * offset / 2)); | |
| if (params.filled && i === 0) this.ctx.fill(); | |
| if (i < layers - 1) { | |
| this.ctx.globalAlpha = 1 - (i / layers); | |
| this.ctx.stroke(); | |
| this.ctx.beginPath(); | |
| } | |
| } | |
| this.ctx.globalAlpha = 1; | |
| this.ctx.stroke(); // Final stroke | |
| // Add a "center point" or highlight | |
| this.ctx.fillStyle = '#fff'; | |
| this.ctx.beginPath(); | |
| this.ctx.arc(0, 0, 3, 0, Math.PI * 2); | |
| this.ctx.fill(); | |
| this.ctx.restore(); | |
| }, | |
| drawPath(shape, size) { | |
| if (shape === 'circle') { | |
| this.ctx.arc(0, 0, size, 0, Math.PI * 2); | |
| } else if (shape === 'square') { | |
| this.ctx.rect(-size, -size, size * 2, size * 2); | |
| } else if (shape === 'triangle') { | |
| this.ctx.moveTo(0, -size); | |
| this.ctx.lineTo(size, size); | |
| this.ctx.lineTo(-size, size); | |
| this.ctx.closePath(); | |
| } else if (shape === 'pentagon' || shape === 'hexagon') { | |
| const sides = shape === 'pentagon' ? 5 : 6; | |
| for (let i = 0; i <= sides; i++) { | |
| const angle = (i * 2 * Math.PI) / sides - Math.PI / 2; | |
| const x = size * Math.cos(angle); | |
| const y = size * Math.sin(angle); | |
| if (i === 0) this.ctx.moveTo(x, y); | |
| else this.ctx.lineTo(x, y); | |
| } | |
| } else if (shape === 'star') { | |
| const points = 5; | |
| const outer = size; | |
| const inner = size / 2; | |
| for (let i = 0; i < points * 2; i++) { | |
| const r = (i % 2 === 0) ? outer : inner; | |
| const angle = (i * Math.PI) / points - Math.PI / 2; | |
| const x = r * Math.cos(angle); | |
| const y = r * Math.sin(angle); | |
| if (i === 0) this.ctx.moveTo(x, y); | |
| else this.ctx.lineTo(x, y); | |
| } | |
| this.ctx.closePath(); | |
| } | |
| }, | |
| draw3DShape(shape, color, params) { | |
| const vertices = []; | |
| const edges = []; | |
| const size = 120 * params.size; | |
| // Define Wireframes | |
| if (shape === 'cube') { | |
| // 8 Vertices | |
| for(let x=-1; x<=1; x+=2) { | |
| for(let y=-1; y<=1; y+=2) { | |
| for(let z=-1; z<=1; z+=2) { | |
| vertices.push({x: x*size, y: y*size, z: z*size}); | |
| } | |
| } | |
| } | |
| // 12 Edges | |
| for(let i=0; i<8; i++) { | |
| for(let j=i+1; j<8; j++) { | |
| // Connect if differ by exactly 1 coordinate | |
| const diff = Math.abs(vertices[i].x - vertices[j].x) + | |
| Math.abs(vertices[i].y - vertices[j].y) + | |
| Math.abs(vertices[i].z - vertices[j].z); | |
| if(diff === size*2) edges.push([i, j]); | |
| } | |
| } | |
| } else if (shape === 'pyramid') { | |
| // 5 Vertices (Square base, point top) | |
| const h = size * 1.5; | |
| vertices.push({x:0, y:-h, z:0}); // Tip | |
| vertices.push({x:-size, y:size, z:-size}); | |
| vertices.push({x:size, y:size, z:-size}); | |
| vertices.push({x:size, y:size, z:size}); | |
| vertices.push({x:-size, y:size, z:size}); | |
| edges.push([0,1], [0,2], [0,3], [0,4], [1,2], [2,3], [3,4], [4,1]); | |
| } else if (shape === 'octahedron') { | |
| // 6 Vertices | |
| vertices.push({x:size, y:0, z:0}); | |
| vertices.push({x:-size, y:0, z:0}); | |
| vertices.push({x:0, y:size, z:0}); | |
| vertices.push({x:0, y:-size, z:0}); | |
| vertices.push({x:0, y:0, z:size}); | |
| vertices.push({x:0, y:0, z:-size}); | |
| // Connect all non-opposites | |
| for(let i=0; i<6; i++){ | |
| for(let j=i+1; j<6; j++){ | |
| if (i+j !== 1 && i+j !== 5 && i+j !== 9) { // Simple logic to skip exact opposites | |
| // Better logic: distance check | |
| const v1 = vertices[i]; | |
| const v2 = vertices[j]; | |
| const dist = Math.sqrt((v1.x-v2.x)**2 + (v1.y-v2.y)**2 + (v1.z-v2.z)**2); | |
| if (Math.abs(dist - size*1.414) < 1) edges.push([i,j]); | |
| } | |
| } | |
| } | |
| } else if (shape === 'torus') { | |
| // Simulated Torus (Loops) | |
| const rings = 3; | |
| const segments = 16; | |
| const R = size; | |
| const r = size * 0.4; | |
| // Draw 3 circles rotated around Y | |
| for(let ring=0; ring<rings; ring++) { | |
| const ringAngle = (ring / rings) * Math.PI * 2; | |
| this.ctx.beginPath(); | |
| for(let i=0; i<=segments; i++) { | |
| const theta = (i/segments) * Math.PI * 2; | |
| // Torus parametric equation | |
| let x = (R + r * Math.cos(theta)) * Math.cos(ringAngle); | |
| let y = r * Math.sin(theta); | |
| let z = (R + r * Math.cos(theta)) * Math.sin(ringAngle); | |
| // Apply rotation | |
| const rot = this.rotate3D(x, y, z, params.angle, params.angle * 0.5); | |
| const proj = this.project(rot.x, rot.y, rot.z); | |
| if(i===0) this.ctx.moveTo(proj.x, proj.y); | |
| else this.ctx.lineTo(proj.x, proj.y); | |
| } | |
| this.ctx.strokeStyle = color; | |
| this.ctx.lineWidth = params.lineWidth; | |
| this.ctx.stroke(); | |
| } | |
| return; // Torus handles its own drawing | |
| } | |
| // Transform and Draw Vertices/Edges | |
| this.ctx.strokeStyle = color; | |
| this.ctx.fillStyle = color; | |
| this.ctx.lineWidth = params.lineWidth; | |
| this.ctx.lineCap = 'round'; | |
| // Draw Edges | |
| this.ctx.beginPath(); | |
| edges.forEach(edge => { | |
| const v1 = this.rotate3D(vertices[edge[0]].x, vertices[edge[0]].y, vertices[edge[0]].z, params.angle, params.angle * 0.7); | |
| const p1 = this.project(v1.x, v1.y, v1.z); | |
| const v2 = this.rotate3D(vertices[edge[1]].x, vertices[edge[1]].y, vertices[edge[1]].z, params.angle, params.angle * 0.7); | |
| const p2 = this.project(v2.x, v2.y, v2.z); | |
| this.ctx.moveTo(p1.x, p1.y); | |
| this.ctx.lineTo(p2.x, p2.y); | |
| }); | |
| this.ctx.stroke(); | |
| // Draw Vertices (Dots) for depth perception | |
| vertices.forEach(v => { | |
| const rot = this.rotate3D(v.x, v.y, v.z, params.angle, params.angle * 0.7); | |
| const proj = this.project(rot.x, rot.y, rot.z); | |
| // Size based on Z depth (simple fog effect) | |
| const scale = (rot.z + 300) / 400; | |
| this.ctx.beginPath(); | |
| this.ctx.arc(proj.x, proj.y, 4 * scale, 0, Math.PI*2); | |
| this.ctx.fill(); | |
| }); | |
| } | |
| }; | |
| /** | |
| * Application Logic | |
| */ | |
| const App = { | |
| totalToGenerate: 300, | |
| gallery: document.getElementById('gallery'), | |
| loader: document.getElementById('loader'), | |
| loadingText: document.getElementById('loadingText'), | |
| countText: document.getElementById('countText'), | |
| statusText: document.getElementById('statusText'), | |
| init() { | |
| ShapeGenerator.init(); | |
| document.getElementById('generateBtn').addEventListener('click', () => this.runGeneration()); | |
| document.getElementById('clearBtn').addEventListener('click', () => this.clearGallery()); | |
| }, | |
| async runGeneration() { | |
| this.showLoader(true); | |
| this.gallery.innerHTML = ''; // Clear previous | |
| // Process in batches to keep UI responsive | |
| const batchSize = 10; | |
| let generated = 0; | |
| while (generated < this.totalToGenerate) { | |
| const batch = []; | |
| for (let i = 0; i < batchSize && generated < this.totalToGenerate; i++) { | |
| batch.push(generated); | |
| generated++; | |
| } | |
| // Process batch | |
| await new Promise(resolve => setTimeout(resolve, 0)); // Yield to main thread | |
| batch.forEach(id => { | |
| const imgData = ShapeGenerator.generateImage(id); | |
| this.addImageToGrid(imgData, id); | |
| }); | |
| this.updateLoader(Math.floor((generated / this.totalToGenerate) * 100), generated); | |
| } | |
| this.showLoader(false); | |
| this.showToast(`Successfully generated ${this.totalToGenerate} unique shapes!`); | |
| this.statusText.textContent = "Generation complete."; | |
| }, | |
| addImageToGrid(data, index) { | |
| const card = document.createElement('div'); | |
| card.className = 'shape-card'; | |
| const img = document.createElement('img'); | |
| img.src = data.src; | |
| img.alt = `Generated shape ${index}`; | |
| const meta = document.createElement('div'); | |
| meta.className = 'shape-meta'; | |
| meta.innerHTML = `<span>#${index + 1}</span> <span>${data.meta.split('|')[0]}</span>`; | |
| card.appendChild(img); | |
| card.appendChild(meta); | |
| this.gallery.appendChild(card); | |
| this.countText.textContent = `Items: ${this.gallery.children.length}`; | |
| }, | |
| clearGallery() { | |
| this.gallery.innerHTML = ''; | |
| this.countText.textContent = 'Items: 0'; | |
| this.statusText.textContent = "Gallery cleared."; | |
| this.showToast("Gallery cleared."); | |
| }, | |
| showLoader(show) { | |
| if (show) { | |
| this.loader.classList.add('active'); | |
| this.loadingText.textContent = "Generating 300 images..."; | |
| } else { | |
| this.loader.classList.remove('active'); | |
| } | |
| }, | |
| updateLoader(percent, count) { | |
| this.loadingText.textContent = `Generating... ${percent}% (${count}/${this.totalToGenerate})`; | |
| }, | |
| showToast(message) { | |
| const toast = document.getElementById('toast'); | |
| const msgSpan = document.getElementById('toastMessage'); | |
| msgSpan.textContent = message; | |
| toast.classList.add('show'); | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| }, 3000); | |
| } | |
| }; | |
| // Initialize App | |
| document.addEventListener('DOMContentLoaded', () => { | |
| App.init(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |