Spaces:
Running
Running
| <html lang="en" style="background: #120458; position: fixed; width: 100%; height: 100%; overflow: hidden;"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AI/ML Card Builder</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Inter:wght@300;400;600;700&display=swap'); | |
| :root { | |
| --bg-1: #120458; | |
| --bg-2: #000000; | |
| --bg-3: #1a0933; | |
| --surface-1: #1a1a2e; | |
| --surface-2: #16213e; | |
| --accent-1: #8b45ff; | |
| --accent-2: #ff1493; | |
| --accent-3: #00d4ff; | |
| --accent-warm-1: #ff6b35; | |
| --accent-warm-2: #f7931e; | |
| --text-1: #ffffff; | |
| --text-2: #dddddd; | |
| --text-3: #aaaaaa; | |
| --card-radius: 25px; | |
| --inner-radius: 22px; | |
| --trans-fast: 0.15s ease; | |
| --trans-med: 0.3s ease; | |
| --shadow-outer: 0 25px 50px rgba(0, 0, 0, 0.5); | |
| --shadow-outer-strong: 0 35px 70px rgba(0, 0, 0, 0.7); | |
| --shadow-inner: inset 0 1px 0 rgba(255, 255, 255, 0.1); | |
| --glass: linear-gradient(135deg, rgba(255,255,255,0.1), rgba(255,255,255,0.05)); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background: radial-gradient(circle at 20% 80%, var(--bg-1) 0%, var(--bg-2) 50%, var(--bg-3) 100%); | |
| background-attachment: fixed; | |
| min-height: 100vh; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| justify-content: center; | |
| align-items: flex-start; | |
| padding: 20px; | |
| padding-top: 40px; | |
| position: relative; | |
| overflow: auto; | |
| overscroll-behavior: contain; | |
| } | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| top: 50%; | |
| left: 50%; | |
| width: 140vmax; | |
| height: 140vmax; | |
| transform: translate(-50%, -50%) rotate(0deg); | |
| background: | |
| radial-gradient(circle at 25% 25%, rgba(139, 69, 255, 0.1) 0%, transparent 50%), | |
| radial-gradient(circle at 75% 75%, rgba(255, 20, 147, 0.1) 0%, transparent 50%); | |
| animation: float 20s ease-in-out infinite; | |
| z-index: -1; | |
| pointer-events: none; | |
| will-change: transform; | |
| } | |
| @keyframes float { | |
| 0%, 100% { transform: translate(-50%, -50%) rotate(0deg); } | |
| 50% { transform: translate(-50%, -50%) rotate(180deg); } | |
| } | |
| .container { | |
| display: flex; | |
| gap: 50px; | |
| align-items: flex-start; | |
| max-width: 1200px; | |
| width: 100%; | |
| margin: 0 auto; | |
| justify-content: center; | |
| } | |
| /* Center card when builder is hidden */ | |
| .builder-hidden .container { justify-content: center; gap: 0; } | |
| .builder-hidden .controls { display: none; } | |
| /* Floating toggle button */ | |
| .builder-toggle { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| z-index: 1000; | |
| } | |
| .export-toggle { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 140px; /* keep clear of builder toggle */ | |
| z-index: 1000; | |
| } | |
| .card { | |
| width: 380px; | |
| height: 650px; | |
| background: linear-gradient(145deg, var(--surface-1), var(--surface-2)); | |
| border-radius: var(--card-radius); | |
| padding: 0; | |
| position: relative; | |
| overflow: hidden; | |
| box-shadow: | |
| 0 0 0 2px rgba(255, 255, 255, 0.1), | |
| var(--shadow-outer), | |
| var(--shadow-inner); | |
| transform: perspective(1000px) rotateX(5deg); | |
| transition: transform var(--trans-med), box-shadow var(--trans-med); | |
| } | |
| .card:hover { | |
| /* Keep dynamic JS-driven transform; only enhance shadow on hover */ | |
| box-shadow: | |
| 0 0 0 2px rgba(139, 69, 255, 0.3), | |
| var(--shadow-outer-strong), | |
| inset 0 1px 0 rgba(255, 255, 255, 0.2); | |
| } | |
| .card-border { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| border-radius: var(--card-radius); | |
| background: linear-gradient(45deg, | |
| var(--accent-warm-1) 0%, | |
| var(--accent-warm-2) 25%, | |
| var(--accent-1) 50%, | |
| var(--accent-2) 75%, | |
| var(--accent-3) 100%); | |
| background-size: 400% 400%; | |
| animation: gradientShift 3s ease-in-out infinite; | |
| padding: 3px; | |
| } | |
| @keyframes gradientShift { | |
| 0%, 100% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| } | |
| .card-inner { | |
| background: linear-gradient(145deg, var(--surface-1), var(--surface-2)); | |
| height: 100%; | |
| border-radius: var(--inner-radius); | |
| padding: 50px 25px 25px 25px; | |
| position: relative; | |
| } | |
| .card-rarity { | |
| position: absolute; | |
| top: 8px; | |
| right: 8px; | |
| width: 28px; | |
| height: 28px; | |
| background: linear-gradient(135deg, #ffd700, #ffed4e); | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: 900; | |
| color: #333; | |
| font-size: 14px; | |
| box-shadow: 0 5px 15px rgba(255, 215, 0, 0.3); | |
| z-index: 1; | |
| } | |
| .card-header { | |
| display: grid; | |
| grid-template-columns: 1fr auto; | |
| align-items: baseline; | |
| gap: 10px; | |
| margin-bottom: 10px; | |
| margin-top: 15px; | |
| } | |
| .card-name { | |
| font-family: 'Orbitron', monospace; | |
| font-size: 20px; | |
| font-weight: 900; | |
| background: linear-gradient(135deg, #ffffff, #a0a0a0); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| margin-bottom: 8px; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| text-shadow: 0 0 20px rgba(255, 255, 255, 0.5); | |
| word-wrap: break-word; | |
| hyphens: auto; | |
| } | |
| .card-title { font-size: 11px; color: var(--accent-1); text-transform: uppercase; letter-spacing: 2px; font-weight: 600; opacity: 0.9; grid-column: 1 / -1; word-wrap: break-word; hyphens: auto; } | |
| .hp { font-family: 'Orbitron', monospace; font-size: 18px; font-weight: 900; color: var(--text-1); display: flex; align-items: baseline; gap: 4px; } | |
| .hp .label { font-size: 11px; opacity: 0.8; } | |
| .type-badge { position: absolute; top: 8px; right: 44px; width: 32px; height: 32px; border-radius: 50%; display: grid; place-items: center; font-weight: 900; color: #111; box-shadow: 0 6px 16px rgba(0,0,0,0.35); border: 2px solid rgba(255,255,255,0.3); background: linear-gradient(135deg, #fff, #ddd); z-index: 2; } | |
| .card-level { | |
| position: absolute; | |
| top: 8px; | |
| left: 8px; | |
| background: linear-gradient(135deg, var(--accent-warm-1), var(--accent-warm-2)); | |
| color: white; | |
| padding: 4px 12px; | |
| border-radius: 15px; | |
| font-size: 11px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| } | |
| .artwork-frame { | |
| width: 100%; | |
| height: 200px; | |
| margin: 8px auto 12px; | |
| position: relative; | |
| background: linear-gradient(135deg, #2d1b69, #11998e); | |
| border-radius: 20px; | |
| padding: 15px; | |
| box-shadow: | |
| inset 0 0 20px rgba(0, 0, 0, 0.3), | |
| 0 10px 30px rgba(0, 0, 0, 0.3); | |
| } | |
| .artwork-frame::before { | |
| content: ''; | |
| position: absolute; | |
| top: -2px; | |
| left: -2px; | |
| right: -2px; | |
| bottom: -2px; | |
| background: linear-gradient(45deg, var(--accent-1), var(--accent-2), var(--accent-3), var(--accent-1)); | |
| border-radius: 22px; | |
| z-index: -1; | |
| opacity: 0.7; | |
| } | |
| .shape-container { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; border-radius: 12px; background: var(--glass); backdrop-filter: blur(10px); position: relative; overflow: hidden; } | |
| .art-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: none; } | |
| .shape { | |
| transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); | |
| filter: drop-shadow(0 5px 15px rgba(255, 255, 255, 0.3)); | |
| } | |
| .shape.circle { | |
| width: 80px; | |
| height: 80px; | |
| background: linear-gradient(135deg, #fff, #f0f0f0); | |
| border-radius: 50%; | |
| } | |
| .shape.square { | |
| width: 80px; | |
| height: 80px; | |
| background: linear-gradient(135deg, #fff, #f0f0f0); | |
| border-radius: 12px; | |
| } | |
| .shape.triangle { | |
| width: 0; | |
| height: 0; | |
| border-left: 40px solid transparent; | |
| border-right: 40px solid transparent; | |
| border-bottom: 70px solid #fff; | |
| filter: drop-shadow(0 5px 15px rgba(255, 255, 255, 0.3)); | |
| } | |
| .shape.diamond { | |
| width: 80px; | |
| height: 80px; | |
| background: linear-gradient(135deg, #fff, #f0f0f0); | |
| transform: rotate(45deg); | |
| border-radius: 12px; | |
| } | |
| /* Attacks section */ | |
| .attacks { margin-top: 8px; display: grid; gap: 10px; } | |
| .attack-row { background: var(--glass); border: 1px solid rgba(255,255,255,0.12); border-radius: 12px; padding: 10px 12px; } | |
| .attack-head { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 8px; } | |
| .cost { display: flex; gap: 4px; flex-wrap: wrap; } | |
| .atk-name { font-weight: 800; color: var(--text-1); letter-spacing: 0.5px; } | |
| .atk-dmg { font-family: 'Orbitron', monospace; font-weight: 900; color: var(--text-1); } | |
| .atk-text { color: var(--text-2); font-size: 12px; margin-top: 6px; line-height: 1.4; } | |
| .energy { width: 18px; height: 18px; border-radius: 50%; display: grid; place-items: center; font-size: 11px; font-weight: 900; color: #111; border: 1px solid rgba(255,255,255,0.3); box-shadow: 0 2px 6px rgba(0,0,0,0.25); } | |
| /* AI/ML themed energy chips */ | |
| .e-generalist { background: linear-gradient(135deg, #f5f5f5, #e0e0e0); } | |
| .e-python { background: linear-gradient(135deg, #3776AB, #FFD43B); } | |
| .e-data { background: linear-gradient(135deg, #34d399, #0ea5e9); } | |
| .e-gpu { background: linear-gradient(135deg, #a7f3d0, #065f46); color: #0a0a0a; } | |
| .e-cloud { background: linear-gradient(135deg, #93c5fd, #e5e7eb); } | |
| .e-research { background: linear-gradient(135deg, #a78bfa, #f472b6); } | |
| .e-math { background: linear-gradient(135deg, #cbd5e1, #64748b); color: #0a0a0a; } | |
| .e-systems { background: linear-gradient(135deg, #f59e0b, #b45309); } | |
| .e-security { background: linear-gradient(135deg, #111827, #ef4444); color: #fafafa; } | |
| .e-mlops { background: linear-gradient(135deg, #10b981, #3b82f6); } | |
| .card-description { background: linear-gradient(135deg, rgba(139, 69, 255, 0.1), rgba(255, 20, 147, 0.1)); border: 1px solid rgba(139, 69, 255, 0.2); padding: 10px; border-radius: 12px; font-size: 12px; color: var(--text-2); line-height: 1.4; text-align: center; font-style: italic; backdrop-filter: blur(10px); margin-top: 10px; } | |
| .card-footer { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 10px; } | |
| .footer-box { background: var(--glass); border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; padding: 8px; text-align: center; } | |
| .footer-box .label { font-size: 10px; color: var(--text-3); text-transform: uppercase; letter-spacing: 1px; } | |
| .footer-box .content { margin-top: 6px; display: flex; justify-content: center; gap: 4px; align-items: center; color: var(--text-2); font-weight: 700; word-wrap: break-word; text-align: center; } | |
| .card-watermark { | |
| position: absolute; | |
| bottom: 15px; | |
| right: 15px; | |
| font-family: 'Orbitron', monospace; | |
| font-size: 24px; | |
| font-weight: 900; | |
| color: rgba(255, 255, 255, 0.08); | |
| transform: rotate(-10deg); | |
| pointer-events: none; | |
| user-select: none; | |
| } | |
| .card-info { | |
| position: absolute; | |
| bottom: 8px; | |
| left: 15px; | |
| right: 15px; | |
| display: flex; | |
| justify-content: space-between; | |
| font-family: 'Inter', sans-serif; | |
| font-size: 9px; | |
| color: rgba(255, 255, 255, 0.3); | |
| pointer-events: none; | |
| user-select: none; | |
| } | |
| .card-info span { | |
| letter-spacing: 0.5px; | |
| } | |
| .controls { | |
| background: linear-gradient(145deg, rgba(26, 26, 46, 0.95), rgba(22, 33, 62, 0.95)); | |
| backdrop-filter: blur(20px); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| padding: 30px; | |
| border-radius: 25px; | |
| width: 420px; | |
| box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3); | |
| color: var(--text-1); | |
| } | |
| .controls h3 { | |
| font-family: 'Orbitron', monospace; | |
| margin-bottom: 25px; | |
| color: var(--text-1); | |
| text-align: center; | |
| font-size: 18px; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .control-group { | |
| margin-bottom: 20px; | |
| } | |
| .control-group label { | |
| display: block; | |
| margin-bottom: 8px; | |
| font-weight: 600; | |
| color: var(--accent-1); | |
| font-size: 12px; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .control-group input, | |
| .control-group textarea, | |
| .control-group select { | |
| width: 100%; | |
| padding: 12px 15px; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 12px; | |
| color: var(--text-1); | |
| font-size: 14px; | |
| transition: all 0.3s ease; | |
| backdrop-filter: blur(10px); | |
| } | |
| .control-group input:focus, | |
| .control-group textarea:focus, | |
| .control-group select:focus { | |
| outline: none; | |
| border-color: var(--accent-1); | |
| box-shadow: 0 0 0 3px rgba(139, 69, 255, 0.25); | |
| background: rgba(255, 255, 255, 0.1); | |
| } | |
| .control-group input[type="range"] { | |
| padding: 0; | |
| height: 6px; | |
| background: rgba(255, 255, 255, 0.1); | |
| -webkit-appearance: none; | |
| border-radius: 3px; | |
| } | |
| .control-group input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 20px; | |
| height: 20px; | |
| background: linear-gradient(135deg, var(--accent-1), var(--accent-2)); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| box-shadow: 0 5px 15px rgba(139, 69, 255, 0.4); | |
| } | |
| .control-group textarea { | |
| resize: vertical; | |
| height: 80px; | |
| font-family: 'Inter', sans-serif; | |
| } | |
| .shape-buttons { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 10px; | |
| margin-top: 8px; | |
| } | |
| .shape-btn { | |
| padding: 12px; | |
| background: rgba(255, 255, 255, 0.05); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| color: var(--text-1); | |
| border-radius: 12px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| font-size: 12px; | |
| text-transform: uppercase; | |
| font-weight: 600; | |
| letter-spacing: 1px; | |
| backdrop-filter: blur(10px); | |
| outline: none; | |
| } | |
| .shape-btn:hover { | |
| border-color: var(--accent-1); | |
| background: rgba(139, 69, 255, 0.2); | |
| transform: translateY(-2px); | |
| } | |
| .shape-btn.active { | |
| border-color: var(--accent-1); | |
| background: linear-gradient(135deg, var(--accent-1), var(--accent-2)); | |
| color: white; | |
| box-shadow: 0 10px 25px rgba(139, 69, 255, 0.4); | |
| } | |
| .shape-btn:focus-visible { | |
| box-shadow: 0 0 0 3px rgba(139,69,255,0.4); | |
| border-color: var(--accent-1); | |
| } | |
| .range-display { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 5px; | |
| } | |
| .range-value { | |
| font-family: 'Orbitron', monospace; | |
| color: var(--accent-1); | |
| font-weight: 700; | |
| font-size: 14px; | |
| } | |
| .helper-row { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-top: 6px; | |
| color: var(--text-3); | |
| font-size: 12px; | |
| } | |
| .actions { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 10px; | |
| margin-top: 20px; | |
| } | |
| .btn { | |
| padding: 10px 12px; | |
| border-radius: 10px; | |
| border: 1px solid rgba(255,255,255,0.15); | |
| background: rgba(255,255,255,0.06); | |
| color: var(--text-1); | |
| font-weight: 600; | |
| letter-spacing: 0.5px; | |
| cursor: pointer; | |
| transition: transform var(--trans-fast), background var(--trans-fast), border-color var(--trans-fast); | |
| } | |
| .btn:hover { transform: translateY(-1px); border-color: var(--accent-1); } | |
| .btn.primary { background: linear-gradient(135deg, var(--accent-1), var(--accent-2)); border-color: transparent; } | |
| .btn.danger { background: linear-gradient(135deg, #ff5a5f, #ff2d55); border-color: transparent; } | |
| .toggles { display: grid; grid-template-columns: 1fr; gap: 10px; margin-top: 10px; } | |
| .toggle { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--text-2); } | |
| .toggle input { accent-color: var(--accent-1); } | |
| .attack-editor { background: rgba(255,255,255,0.04); border: 1px dashed rgba(255,255,255,0.15); border-radius: 12px; padding: 12px; margin-bottom: 12px; } | |
| .attack-editor .row { display: grid; grid-template-columns: 1fr 100px; gap: 8px; margin-bottom: 8px; } | |
| .attack-editor .row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px; } | |
| .attack-editor .row-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; margin-bottom: 8px; } | |
| .energy-palette { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; } | |
| .energy-palette button:focus-visible { outline: 2px solid var(--accent-1); outline-offset: 2px; } | |
| .energy-chip { display: inline-flex; align-items: center; gap: 6px; padding: 6px 8px; border-radius: 999px; background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.15); color: var(--text-2); font-size: 12px; } | |
| .energy-chip button { background: transparent; border: 0; color: var(--text-2); cursor: pointer; font-size: 14px; } | |
| .toast { | |
| position: fixed; | |
| left: 50%; | |
| bottom: 20px; | |
| transform: translateX(-50%); | |
| background: rgba(0,0,0,0.7); | |
| color: #fff; | |
| padding: 10px 14px; | |
| border-radius: 8px; | |
| font-size: 13px; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity var(--trans-med), transform var(--trans-med); | |
| } | |
| .toast.show { opacity: 1; transform: translateX(-50%) translateY(-4px); } | |
| @media (max-width: 768px) { | |
| .container { | |
| flex-direction: column; | |
| gap: 30px; | |
| } | |
| .card { | |
| transform: none; | |
| } | |
| .controls { width: 100%; max-width: 420px; } | |
| } | |
| /* Reduced motion support */ | |
| @media (prefers-reduced-motion: reduce) { | |
| * { animation: none ; transition: none ; } | |
| } | |
| body.reduced-motion * { animation: none ; transition: none ; } | |
| body.no-glow .card, body.no-glow .shape, body.no-glow .shape-btn.active { box-shadow: none ; filter: none ; } | |
| body.high-contrast { --text-2: #f2f2f2; --text-3: #e0e0e0; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="card" role="img" aria-label="Card preview for AI/ML engineer"> | |
| <div class="card-border"> | |
| <div class="card-inner"> | |
| <div class="card-rarity" id="cardRarity" aria-hidden="true">★</div> | |
| <div class="card-level">XP <span id="cardLevel" aria-live="polite">100</span></div> | |
| <div class="card-header"> | |
| <div> | |
| <div class="card-name" id="cardName" aria-live="polite">SMART PERSON</div> | |
| <div class="card-title" id="cardTitle" aria-live="polite">Future Builder</div> | |
| </div> | |
| <div class="hp"><span class="label">HP</span> <span id="hpValue">1337</span></div> | |
| <div class="type-badge" id="typeBadge" title="Stack">Ge</div> | |
| </div> | |
| <div class="artwork-frame"> | |
| <div class="shape-container"> | |
| <img id="artImage" class="art-img" alt="Card artwork"/> | |
| <div class="shape circle" id="cardShape" aria-label="Avatar shape"></div> | |
| </div> | |
| </div> | |
| <div class="attacks" id="attacks" aria-live="polite"></div> | |
| <div class="card-description" id="cardDescription" aria-live="polite">Builds robust ML systems, from data to deployment.</div> | |
| <div class="card-footer"> | |
| <div class="footer-box"> | |
| <div class="label">Secret Power</div> | |
| <div class="content" id="secretPowerDisplay">GPU whisper</div> | |
| </div> | |
| <div class="footer-box"> | |
| <div class="label">Handle</div> | |
| <div class="content" id="handleDisplay">@user</div> | |
| </div> | |
| </div> | |
| <div class="card-watermark">HF</div> | |
| <div class="card-info"> | |
| <span>© 2024 HF</span> | |
| <span id="cardId">001/150</span> | |
| <span>PROMO</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <h3>AI/ML Card Builder</h3> | |
| <div class="control-group"> | |
| <label for="nameInput">Engineer</label> | |
| <input type="text" id="nameInput" value="Smart Person" maxlength="40" aria-describedby="nameCount" /> | |
| <div class="helper-row"><span id="nameCount">0/40</span></div> | |
| </div> | |
| <div class="control-group"> | |
| <label for="titleInput">Role</label> | |
| <input type="text" id="titleInput" value="Future Builder" maxlength="40" aria-describedby="titleCount" /> | |
| <div class="helper-row"><span id="titleCount">0/40</span></div> | |
| </div> | |
| <div class="control-group"> | |
| <label for="hpInput">HP</label> | |
| <input type="number" id="hpInput" min="10" max="9999" step="10" value="1337" /> | |
| </div> | |
| <div class="control-group"> | |
| <label for="typeInput">Stack</label> | |
| <select id="typeInput"></select> | |
| </div> | |
| <div class="control-group"> | |
| <label for="levelInput">Experience</label> | |
| <input type="range" id="levelInput" min="1" max="100" value="100" /> | |
| <div class="range-display"> | |
| <span>XP</span> | |
| <span class="range-value" id="levelValue">100</span> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <label id="shapeLabel">Avatar Shape</label> | |
| <div class="shape-buttons" role="radiogroup" aria-labelledby="shapeLabel"> | |
| <button class="shape-btn active" role="radio" aria-checked="true" tabindex="0" data-shape="circle">Circle</button> | |
| <button class="shape-btn" role="radio" aria-checked="false" tabindex="-1" data-shape="square">Square</button> | |
| <button class="shape-btn" role="radio" aria-checked="false" tabindex="-1" data-shape="triangle">Triangle</button> | |
| <button class="shape-btn" role="radio" aria-checked="false" tabindex="-1" data-shape="diamond">Diamond</button> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <label for="artInput">Artwork (optional)</label> | |
| <input type="file" id="artInput" accept="image/*" /> | |
| </div> | |
| <div class="control-group"> | |
| <label>Skills</label> | |
| <div id="attacksEditor"></div> | |
| <button class="btn" id="addAttackBtn" type="button">+ Add Skill</button> | |
| </div> | |
| <div class="control-group"> | |
| <label>Identity</label> | |
| <div class="attack-editor"> | |
| <div class="row-2"> | |
| <div> | |
| <label for="secretPowerInput">Secret Power</label> | |
| <input type="text" id="secretPowerInput" value="GPU whisper" placeholder="Enter symbol/text" maxlength="10" /> | |
| </div> | |
| <div> | |
| <label for="handleInput">Handle</label> | |
| <input type="text" id="handleInput" value="@user" placeholder="@username" maxlength="20" /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <label for="descriptionInput">Bio</label> | |
| <textarea id="descriptionInput" maxlength="160" aria-describedby="descCount">Builds robust ML systems, from data to deployment.</textarea> | |
| <div class="helper-row"><span id="descCount">0/160</span></div> | |
| </div> | |
| <div class="toggles" aria-label="Display options"> | |
| <label class="toggle"><input type="checkbox" id="toggleGlow" checked /> Enable glow effects</label> | |
| <label class="toggle"><input type="checkbox" id="toggleMotion" /> Reduced motion</label> | |
| <label class="toggle"><input type="checkbox" id="toggleContrast" /> High contrast text</label> | |
| </div> | |
| <div class="actions"> | |
| <button class="btn primary" id="saveBtn">Save</button> | |
| <button class="btn" id="loadBtn">Load</button> | |
| <button class="btn" id="shareBtn">Copy Link</button> | |
| <button class="btn" id="exportBtn">Export PNG</button> | |
| <button class="btn danger" id="resetBtn">Reset</button> | |
| </div> | |
| </div> | |
| </div> | |
| <button class="btn builder-toggle" id="toggleBuilderBtn" type="button" aria-pressed="false" aria-label="Toggle builder panel">Hide Builder</button> | |
| <button class="btn export-toggle" id="exportFloatingBtn" type="button" aria-label="Export card as PNG">Export PNG</button> | |
| <div class="toast" id="toast" role="status" aria-live="polite"></div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> | |
| <script> | |
| // Get DOM elements | |
| const cardName = document.getElementById('cardName'); | |
| const cardTitle = document.getElementById('cardTitle'); | |
| const cardLevel = document.getElementById('cardLevel'); | |
| const cardShape = document.getElementById('cardShape'); | |
| const cardDescription = document.getElementById('cardDescription'); | |
| const hpValue = document.getElementById('hpValue'); | |
| const typeBadge = document.getElementById('typeBadge'); | |
| const attacksEl = document.getElementById('attacks'); | |
| const secretPowerDisplay = document.getElementById('secretPowerDisplay'); | |
| const handleDisplay = document.getElementById('handleDisplay'); | |
| const artImage = document.getElementById('artImage'); | |
| const nameInput = document.getElementById('nameInput'); | |
| const titleInput = document.getElementById('titleInput'); | |
| const levelInput = document.getElementById('levelInput'); | |
| const hpInput = document.getElementById('hpInput'); | |
| const typeInput = document.getElementById('typeInput'); | |
| const secretPowerInput = document.getElementById('secretPowerInput'); | |
| const handleInput = document.getElementById('handleInput'); | |
| const artInput = document.getElementById('artInput'); | |
| const descriptionInput = document.getElementById('descriptionInput'); | |
| const levelValue = document.getElementById('levelValue'); | |
| const shapeButtons = document.querySelectorAll('.shape-btn'); | |
| const shapeGroup = document.querySelector('.shape-buttons'); | |
| const attacksEditor = document.getElementById('attacksEditor'); | |
| const addAttackBtn = document.getElementById('addAttackBtn'); | |
| const nameCount = document.getElementById('nameCount'); | |
| const titleCount = document.getElementById('titleCount'); | |
| const descCount = document.getElementById('descCount'); | |
| const toggleGlow = document.getElementById('toggleGlow'); | |
| const toggleMotion = document.getElementById('toggleMotion'); | |
| const toggleContrast = document.getElementById('toggleContrast'); | |
| const saveBtn = document.getElementById('saveBtn'); | |
| const loadBtn = document.getElementById('loadBtn'); | |
| const shareBtn = document.getElementById('shareBtn'); | |
| const exportBtn = document.getElementById('exportBtn'); | |
| const exportFloatingBtn = document.getElementById('exportFloatingBtn'); | |
| const resetBtn = document.getElementById('resetBtn'); | |
| const toast = document.getElementById('toast'); | |
| const toggleBuilderBtn = document.getElementById('toggleBuilderBtn'); | |
| // Update functions | |
| function updateName() { | |
| cardName.textContent = (nameInput.value || 'ENGINEER NAME').toUpperCase(); | |
| updateCounters(); | |
| } | |
| function updateTitle() { | |
| cardTitle.textContent = titleInput.value || 'ML Engineer'; | |
| updateCounters(); | |
| } | |
| function updateLevel() { | |
| const level = levelInput.value; | |
| cardLevel.textContent = level; | |
| levelValue.textContent = level; | |
| } | |
| function updateHPType() { | |
| const hp = parseInt(hpInput.value || '0', 10); | |
| hpValue.textContent = isNaN(hp) ? 0 : hp; | |
| const t = typeInput.value || 'generalist'; | |
| const map = energyTypes[t] || energyTypes.generalist; | |
| typeBadge.textContent = map.short; | |
| typeBadge.className = 'type-badge'; | |
| typeBadge.style.background = map.bg; | |
| renderFooter(); | |
| } | |
| function updateDescription() { | |
| cardDescription.textContent = descriptionInput.value || 'Builds robust ML systems, from data to deployment.'; | |
| updateCounters(); | |
| } | |
| function updateShape(shape, opts = { focus: true }) { | |
| cardShape.className = `shape ${shape}`; | |
| // Update active button | |
| shapeButtons.forEach(btn => { | |
| btn.classList.remove('active'); | |
| btn.setAttribute('aria-checked', 'false'); | |
| btn.setAttribute('tabindex', '-1'); | |
| if (btn.dataset.shape === shape) { | |
| btn.classList.add('active'); | |
| btn.setAttribute('aria-checked', 'true'); | |
| btn.setAttribute('tabindex', '0'); | |
| if (opts.focus) btn.focus({ preventScroll: true }); | |
| } | |
| }); | |
| } | |
| function updateCounters() { | |
| nameCount.textContent = `${nameInput.value.length}/${nameInput.maxLength}`; | |
| titleCount.textContent = `${titleInput.value.length}/${titleInput.maxLength}`; | |
| descCount.textContent = `${descriptionInput.value.length}/${descriptionInput.maxLength}`; | |
| } | |
| // Energy/type map | |
| const energyTypes = { | |
| generalist: { key: 'generalist', name: 'Generalist', short: 'Ge', class: 'e-generalist', bg: 'linear-gradient(135deg,#f5f5f5,#e0e0e0)' }, | |
| python: { key: 'python', name: 'Python', short: 'Py', class: 'e-python', bg: 'linear-gradient(135deg,#3776AB,#FFD43B)' }, | |
| data: { key: 'data', name: 'Data', short: 'Ds', class: 'e-data', bg: 'linear-gradient(135deg,#34d399,#0ea5e9)' }, | |
| gpu: { key: 'gpu', name: 'GPU', short: 'GPU', class: 'e-gpu', bg: 'linear-gradient(135deg,#a7f3d0,#065f46)' }, | |
| cloud: { key: 'cloud', name: 'Cloud', short: 'Cl', class: 'e-cloud', bg: 'linear-gradient(135deg,#93c5fd,#e5e7eb)' }, | |
| research: { key: 'research', name: 'Research', short: 'R', class: 'e-research', bg: 'linear-gradient(135deg,#a78bfa,#f472b6)' }, | |
| math: { key: 'math', name: 'Math', short: '∑', class: 'e-math', bg: 'linear-gradient(135deg,#cbd5e1,#64748b)' }, | |
| systems: { key: 'systems', name: 'Systems', short: 'Sys', class: 'e-systems', bg: 'linear-gradient(135deg,#f59e0b,#b45309)' }, | |
| security: { key: 'security', name: 'Security', short: 'Sec', class: 'e-security', bg: 'linear-gradient(135deg,#111827,#ef4444)' }, | |
| mlops: { key: 'mlops', name: 'MLOps', short: 'Ops', class: 'e-mlops', bg: 'linear-gradient(135deg,#10b981,#3b82f6)' }, | |
| }; | |
| function renderFooter() { | |
| secretPowerDisplay.textContent = secretPowerInput.value || '🚀'; | |
| handleDisplay.textContent = handleInput.value || '@drbh'; | |
| } | |
| function renderAttacks() { | |
| const state = getState(); | |
| attacksEl.innerHTML = state.attacks.map(atk => { | |
| const cost = (atk.cost || []).map(k => { | |
| const m = energyTypes[k] || energyTypes.generalist; | |
| return `<span class="energy ${m.class}">${m.short}</span>`; | |
| }).join(''); | |
| const dmg = atk.damage ? `<div class="atk-dmg">${atk.damage}</div>` : ''; | |
| const text = atk.text ? `<div class="atk-text">${atk.text}</div>` : ''; | |
| return `<div class="attack-row"><div class="attack-head"><div class="cost">${cost}</div><div class="atk-name">${atk.name || '—'}</div>${dmg}</div>${text}</div>`; | |
| }).join(''); | |
| } | |
| function populateTypeSelects() { | |
| const keys = Object.keys(energyTypes); | |
| // Populate type select | |
| typeInput.innerHTML = keys.map(k => `<option value="${k}">${energyTypes[k].name}</option>`).join(''); | |
| if (!typeInput.value) typeInput.value = 'generalist'; | |
| } | |
| function buildAttackEditor() { | |
| const state = getState(); | |
| attacksEditor.innerHTML = ''; | |
| state.attacks.forEach((atk, index) => { | |
| const wrap = document.createElement('div'); | |
| wrap.className = 'attack-editor'; | |
| const row = document.createElement('div'); | |
| row.className = 'row'; | |
| row.innerHTML = ` | |
| <input type="text" placeholder="Skill name" value="${atk.name || ''}" aria-label="Skill name"/> | |
| <input type="text" placeholder="Impact (e.g. 30+)" value="${atk.damage || ''}" aria-label="Skill impact"/> | |
| `; | |
| const [nameEl, dmgEl] = row.querySelectorAll('input'); | |
| nameEl.addEventListener('input', () => { state.attacks[index].name = nameEl.value; applyState(state, { rebuildEditors: false }); setUrlHashFromState(); saveDraftDebounced(); }); | |
| dmgEl.addEventListener('input', () => { state.attacks[index].damage = dmgEl.value; applyState(state, { rebuildEditors: false }); setUrlHashFromState(); saveDraftDebounced(); }); | |
| const textArea = document.createElement('textarea'); | |
| textArea.placeholder = 'Skill details'; | |
| textArea.value = atk.text || ''; | |
| textArea.addEventListener('input', () => { state.attacks[index].text = textArea.value; applyState(state, { rebuildEditors: false }); setUrlHashFromState(); saveDraftDebounced(); }); | |
| const palette = document.createElement('div'); | |
| palette.className = 'energy-palette'; | |
| palette.innerHTML = Object.keys(energyTypes).map(k => { | |
| const m = energyTypes[k]; | |
| return `<button type="button" data-k="${k}" class="energy ${m.class}" title="Add ${m.name}">${m.short}</button>`; | |
| }).join(''); | |
| palette.addEventListener('click', (e) => { | |
| const btn = e.target.closest('button[data-k]'); | |
| if (!btn) return; | |
| const k = btn.dataset.k; | |
| state.attacks[index].cost = state.attacks[index].cost || []; | |
| state.attacks[index].cost.push(k); | |
| applyState(state, { rebuildEditors: false }); | |
| renderChips(); | |
| setUrlHashFromState(); saveDraftDebounced(); | |
| }); | |
| const chips = document.createElement('div'); | |
| chips.className = 'energy-palette'; | |
| function renderChips() { | |
| const current = state.attacks[index] || { cost: [] }; | |
| chips.innerHTML = (current.cost || []).map((k, i) => { | |
| const m = energyTypes[k] || energyTypes.generalist; | |
| return `<span class="energy-chip"><span class="energy ${m.class}">${m.short}</span><button type="button" data-i="${i}">×</button></span>`; | |
| }).join(''); | |
| } | |
| chips.addEventListener('click', (e) => { | |
| const b = e.target.closest('button[data-i]'); | |
| if (!b) return; | |
| const i = parseInt(b.dataset.i, 10); | |
| state.attacks[index].cost.splice(i, 1); | |
| applyState(state, { rebuildEditors: false }); | |
| renderChips(); | |
| setUrlHashFromState(); saveDraftDebounced(); | |
| }); | |
| const actions = document.createElement('div'); | |
| actions.style.display = 'flex'; | |
| actions.style.justifyContent = 'space-between'; | |
| actions.style.marginTop = '8px'; | |
| const removeBtn = document.createElement('button'); | |
| removeBtn.className = 'btn danger'; | |
| removeBtn.type = 'button'; | |
| removeBtn.textContent = 'Remove Skill'; | |
| removeBtn.addEventListener('click', () => { | |
| state.attacks.splice(index, 1); | |
| applyState(state); setUrlHashFromState(); saveDraftDebounced(); | |
| }); | |
| actions.appendChild(removeBtn); | |
| wrap.appendChild(row); | |
| wrap.appendChild(textArea); | |
| const label1 = document.createElement('div'); label1.textContent = 'Add tokens:'; label1.style.marginTop = '12px'; label1.style.fontSize='12px'; label1.style.color='var(--text-3)'; | |
| wrap.appendChild(label1); | |
| wrap.appendChild(palette); | |
| const label2 = document.createElement('div'); label2.textContent = 'Tokens:'; label2.style.marginTop = '12px'; label2.style.fontSize='12px'; label2.style.color='var(--text-3)'; | |
| wrap.appendChild(label2); | |
| wrap.appendChild(chips); | |
| wrap.appendChild(actions); | |
| attacksEditor.appendChild(wrap); | |
| renderChips(); | |
| }); | |
| } | |
| function showToast(message) { | |
| toast.textContent = message; | |
| toast.classList.add('show'); | |
| clearTimeout(showToast._t); | |
| showToast._t = setTimeout(() => toast.classList.remove('show'), 1600); | |
| } | |
| function getState() { | |
| return { | |
| name: nameInput.value, | |
| title: titleInput.value, | |
| level: Number(levelInput.value), | |
| hp: Number(hpInput.value) || 0, | |
| type: typeInput.value, | |
| secretPower: secretPowerInput.value, | |
| handle: handleInput.value, | |
| description: descriptionInput.value, | |
| shape: [...shapeButtons].find(b => b.classList.contains('active'))?.dataset.shape || 'circle', | |
| artwork: artImage?.src && artImage.style.display !== 'none' ? artImage.src : '', | |
| attacks: window.__attacksState || [], | |
| options: { | |
| glow: toggleGlow.checked, | |
| reducedMotion: toggleMotion.checked, | |
| highContrast: toggleContrast.checked | |
| }, | |
| ui: { | |
| builderHidden: document.body.classList.contains('builder-hidden') | |
| } | |
| }; | |
| } | |
| function applyState(state, options = {}) { | |
| const opts = { rebuildEditors: true, renderAttacks: true, ...options }; | |
| if (!state) return; | |
| nameInput.value = state.name ?? nameInput.value; | |
| titleInput.value = state.title ?? titleInput.value; | |
| levelInput.value = state.level ?? levelInput.value; | |
| hpInput.value = state.hp ?? hpInput.value; | |
| typeInput.value = state.type ?? typeInput.value; | |
| secretPowerInput.value = state.secretPower ?? secretPowerInput.value; | |
| handleInput.value = state.handle ?? handleInput.value; | |
| descriptionInput.value = state.description ?? descriptionInput.value; | |
| updateShape(state.shape ?? 'circle', { focus: false }); | |
| if (state.artwork) { | |
| artImage.src = state.artwork; | |
| artImage.style.display = 'block'; | |
| cardShape.style.display = 'none'; | |
| } else { | |
| artImage.removeAttribute('src'); | |
| artImage.style.display = 'none'; | |
| cardShape.style.display = ''; | |
| } | |
| window.__attacksState = Array.isArray(state.attacks) ? state.attacks.slice(0, 4) : []; | |
| if (opts.renderAttacks) renderAttacks(); | |
| if (opts.rebuildEditors) buildAttackEditor(); | |
| toggleGlow.checked = state.options?.glow ?? true; | |
| toggleMotion.checked = state.options?.reducedMotion ?? false; | |
| toggleContrast.checked = state.options?.highContrast ?? false; | |
| syncBodyOptions(); | |
| // Apply UI state (builder visibility) | |
| const hidden = !!state.ui?.builderHidden; | |
| document.body.classList.toggle('builder-hidden', hidden); | |
| if (typeof toggleBuilderBtn !== 'undefined' && toggleBuilderBtn) { | |
| toggleBuilderBtn.textContent = hidden ? 'Show Builder' : 'Hide Builder'; | |
| toggleBuilderBtn.setAttribute('aria-pressed', String(hidden)); | |
| } | |
| updateName(); | |
| updateTitle(); | |
| updateLevel(); | |
| updateHPType(); | |
| updateDescription(); | |
| renderFooter(); | |
| } | |
| function syncBodyOptions() { | |
| document.body.classList.toggle('no-glow', !toggleGlow.checked); | |
| document.body.classList.toggle('reduced-motion', toggleMotion.checked); | |
| document.body.classList.toggle('high-contrast', toggleContrast.checked); | |
| } | |
| function saveToLocal() { | |
| try { | |
| localStorage.setItem('cardBuilderState', JSON.stringify(getState())); | |
| showToast('Saved'); | |
| } catch (e) { showToast('Save failed'); } | |
| } | |
| function loadFromLocal() { | |
| try { | |
| const raw = localStorage.getItem('cardBuilderState'); | |
| if (!raw) { showToast('Nothing to load'); return; } | |
| const state = JSON.parse(raw); | |
| applyState(state); | |
| showToast('Loaded'); | |
| } catch (e) { showToast('Load failed'); } | |
| } | |
| function setUrlHashFromState() { | |
| try { | |
| const s = getState(); | |
| // Do not serialize artwork into URL (too large) | |
| delete s.artwork; | |
| const json = JSON.stringify(s); | |
| const enc = encodeURIComponent(json); | |
| const newHash = `#state=${enc}`; | |
| const newUrl = `${location.pathname}${location.search}${newHash}`; | |
| if (location.hash !== newHash) history.replaceState(null, '', newUrl); | |
| } catch (_) {} | |
| } | |
| function loadStateFromHash() { | |
| if (!location.hash.startsWith('#state=')) return null; | |
| try { | |
| const enc = location.hash.slice('#state='.length); | |
| const json = decodeURIComponent(enc); | |
| return JSON.parse(json); | |
| } catch (_) { return null; } | |
| } | |
| async function exportCardAsPNG() { | |
| const cardEl = document.querySelector('.card'); | |
| const rect = cardEl.getBoundingClientRect(); | |
| const scale = 2; // Fixed scale to avoid memory issues | |
| try { | |
| // Try html2canvas approach if available | |
| if (typeof html2canvas !== 'undefined') { | |
| // Create a wrapper div with padding to prevent cropping | |
| const wrapper = document.createElement('div'); | |
| wrapper.style.padding = '20px'; | |
| wrapper.style.display = 'inline-block'; | |
| wrapper.style.backgroundColor = 'transparent'; | |
| // Clone the card | |
| const cardClone = cardEl.cloneNode(true); | |
| // Fix glow effect for export - text-shadow doesn't work well with transparent text | |
| const nameEl = cardClone.querySelector('.card-name'); | |
| if (nameEl) { | |
| nameEl.style.webkitTextFillColor = '#ffffff'; | |
| nameEl.style.background = 'none'; | |
| nameEl.style.color = '#ffffff'; | |
| nameEl.style.textShadow = '0 0 15px rgba(255, 255, 255, 0.8), 0 0 30px rgba(255, 255, 255, 0.4)'; | |
| } | |
| // Fix artwork frame background for export - replace complex pseudo-element | |
| const artworkFrame = cardClone.querySelector('.artwork-frame'); | |
| if (artworkFrame) { | |
| // Preserve original dimensions | |
| artworkFrame.style.width = '100%'; | |
| artworkFrame.style.height = '200px'; | |
| artworkFrame.style.position = 'relative'; | |
| // Create a simple gradient border that html2canvas can render | |
| artworkFrame.style.background = 'linear-gradient(45deg, #8b45ff, #ff1493, #00d4ff, #8b45ff)'; | |
| artworkFrame.style.padding = '4px'; | |
| artworkFrame.style.borderRadius = '22px'; | |
| // Create inner container for the actual artwork background | |
| const innerFrame = document.createElement('div'); | |
| innerFrame.style.background = 'linear-gradient(135deg, #2d1b69, #11998e)'; | |
| innerFrame.style.borderRadius = '18px'; | |
| innerFrame.style.width = '100%'; | |
| innerFrame.style.height = '100%'; | |
| innerFrame.style.padding = '15px'; | |
| innerFrame.style.boxSizing = 'border-box'; | |
| innerFrame.style.position = 'relative'; | |
| // Move all children to inner frame | |
| while (artworkFrame.firstChild) { | |
| innerFrame.appendChild(artworkFrame.firstChild); | |
| } | |
| artworkFrame.appendChild(innerFrame); | |
| // Ensure shape container maintains exact dimensions | |
| const shapeContainer = cardClone.querySelector('.shape-container'); | |
| if (shapeContainer) { | |
| const originalContainer = cardEl.querySelector('.shape-container'); | |
| const computedContainerStyle = window.getComputedStyle(originalContainer); | |
| shapeContainer.style.position = 'relative'; | |
| shapeContainer.style.width = '100%'; | |
| shapeContainer.style.height = '100%'; | |
| shapeContainer.style.overflow = 'hidden'; | |
| shapeContainer.style.borderRadius = '12px'; | |
| shapeContainer.style.display = 'flex'; | |
| shapeContainer.style.alignItems = 'center'; | |
| shapeContainer.style.justifyContent = 'center'; | |
| // If there's an image, remove the glass background | |
| const hasImage = cardClone.querySelector('.art-img')?.src; | |
| if (hasImage) { | |
| shapeContainer.style.background = 'transparent'; | |
| shapeContainer.style.backdropFilter = 'none'; | |
| } | |
| } | |
| // Fix artwork image aspect ratio - maintain original dimensions | |
| const artImg = cardClone.querySelector('.art-img'); | |
| if (artImg && artImg.src && artImg.style.display !== 'none') { | |
| // Copy computed styles from original to ensure exact match | |
| const originalImg = cardEl.querySelector('.art-img'); | |
| const computedStyle = window.getComputedStyle(originalImg); | |
| artImg.style.position = 'absolute'; | |
| artImg.style.top = '0'; | |
| artImg.style.left = '0'; | |
| artImg.style.width = '100%'; | |
| artImg.style.height = '100%'; | |
| artImg.style.objectFit = computedStyle.objectFit || 'cover'; | |
| artImg.style.objectPosition = computedStyle.objectPosition || 'center'; | |
| artImg.style.borderRadius = '12px'; | |
| artImg.style.display = 'block'; | |
| // Ensure the shape container doesn't interfere | |
| const shape = cardClone.querySelector('.shape'); | |
| if (shape) { | |
| shape.style.display = 'none'; | |
| } | |
| } | |
| } | |
| wrapper.appendChild(cardClone); | |
| // Temporarily add wrapper to document (off-screen to prevent flicker) | |
| wrapper.style.position = 'fixed'; | |
| wrapper.style.left = '0'; | |
| wrapper.style.top = '0'; | |
| wrapper.style.zIndex = '-9999'; | |
| wrapper.style.pointerEvents = 'none'; | |
| document.body.appendChild(wrapper); | |
| const canvas = await html2canvas(wrapper, { | |
| scale: scale, | |
| useCORS: true, | |
| allowTaint: true, | |
| backgroundColor: null, | |
| scrollX: 0, | |
| scrollY: 0 | |
| }); | |
| // Remove wrapper | |
| document.body.removeChild(wrapper); | |
| const dataUrl = canvas.toDataURL('image/png'); | |
| const a = document.createElement('a'); | |
| a.href = dataUrl; | |
| a.download = 'card.png'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| a.remove(); | |
| return; | |
| } | |
| } catch (e) { | |
| console.warn('html2canvas failed, falling back to manual canvas approach:', e); | |
| } | |
| // Fallback: Direct canvas drawing with simplified approach | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = Math.floor(rect.width * scale); | |
| canvas.height = Math.floor(rect.height * scale); | |
| const ctx = canvas.getContext('2d'); | |
| // Set white background | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Scale the context | |
| ctx.scale(scale, scale); | |
| // Try to draw the element directly using browser API | |
| try { | |
| // Create a simple representation | |
| ctx.fillStyle = '#333'; | |
| ctx.font = '16px Arial'; | |
| ctx.fillText('Card export - please use browser screenshot', 10, 30); | |
| ctx.fillText('for better quality', 10, 50); | |
| const dataUrl = canvas.toDataURL('image/png'); | |
| const a = document.createElement('a'); | |
| a.href = dataUrl; | |
| a.download = 'card.png'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| a.remove(); | |
| } catch (e) { | |
| throw new Error('Canvas export failed: ' + e.message); | |
| } | |
| } | |
| // Event listeners | |
| nameInput.addEventListener('input', () => { updateName(); setUrlHashFromState(); }); | |
| titleInput.addEventListener('input', () => { updateTitle(); setUrlHashFromState(); }); | |
| levelInput.addEventListener('input', () => { updateLevel(); setUrlHashFromState(); }); | |
| hpInput.addEventListener('input', () => { updateHPType(); setUrlHashFromState(); }); | |
| typeInput.addEventListener('change', () => { updateHPType(); setUrlHashFromState(); }); | |
| secretPowerInput.addEventListener('input', () => { renderFooter(); setUrlHashFromState(); }); | |
| handleInput.addEventListener('input', () => { renderFooter(); setUrlHashFromState(); }); | |
| descriptionInput.addEventListener('input', () => { updateDescription(); setUrlHashFromState(); }); | |
| shapeButtons.forEach((button, idx) => { | |
| button.addEventListener('click', () => { | |
| updateShape(button.dataset.shape); | |
| setUrlHashFromState(); | |
| }); | |
| }); | |
| // Keyboard navigation for radiogroup | |
| shapeGroup.addEventListener('keydown', (e) => { | |
| const currentIndex = [...shapeButtons].findIndex(b => b.getAttribute('aria-checked') === 'true'); | |
| if (['ArrowRight', 'ArrowDown'].includes(e.key)) { | |
| e.preventDefault(); | |
| const next = (currentIndex + 1) % shapeButtons.length; | |
| shapeButtons[next].click(); | |
| } else if (['ArrowLeft', 'ArrowUp'].includes(e.key)) { | |
| e.preventDefault(); | |
| const prev = (currentIndex - 1 + shapeButtons.length) % shapeButtons.length; | |
| shapeButtons[prev].click(); | |
| } else if ([' ', 'Enter'].includes(e.key)) { | |
| e.preventDefault(); | |
| const active = [...shapeButtons][currentIndex] || shapeButtons[0]; | |
| active.click(); | |
| } | |
| }); | |
| // Artwork upload | |
| artInput.addEventListener('change', (e) => { | |
| const f = e.target.files && e.target.files[0]; | |
| if (!f) return; | |
| const reader = new FileReader(); | |
| reader.onload = () => { | |
| artImage.src = reader.result; | |
| artImage.style.display = 'block'; | |
| cardShape.style.display = 'none'; | |
| setUrlHashFromState(); | |
| saveDraftDebounced(); | |
| }; | |
| reader.readAsDataURL(f); | |
| }); | |
| // Option toggles | |
| toggleGlow.addEventListener('change', () => { syncBodyOptions(); setUrlHashFromState(); }); | |
| toggleMotion.addEventListener('change', () => { syncBodyOptions(); setUrlHashFromState(); }); | |
| toggleContrast.addEventListener('change', () => { syncBodyOptions(); setUrlHashFromState(); }); | |
| // Actions | |
| saveBtn.addEventListener('click', saveToLocal); | |
| loadBtn.addEventListener('click', loadFromLocal); | |
| shareBtn.addEventListener('click', async () => { | |
| setUrlHashFromState(); | |
| try { | |
| await navigator.clipboard.writeText(location.href); | |
| showToast('Link copied'); | |
| } catch (e) { showToast('Copy failed'); } | |
| }); | |
| exportBtn.addEventListener('click', async () => { | |
| try { | |
| await exportCardAsPNG(); | |
| showToast('PNG exported'); | |
| } catch (e) { console.error(e); showToast('Export failed'); } | |
| }); | |
| exportFloatingBtn.addEventListener('click', async () => { | |
| try { | |
| await exportCardAsPNG(); | |
| showToast('PNG exported'); | |
| } catch (e) { console.error(e); showToast('Export failed'); } | |
| }); | |
| resetBtn.addEventListener('click', () => { | |
| if (!confirm('Reset all values?')) return; | |
| nameInput.value = 'Smart Person'; | |
| titleInput.value = 'Future Builder'; | |
| levelInput.value = 100; | |
| hpInput.value = 1337; | |
| typeInput.value = 'generalist'; | |
| secretPowerInput.value = 'GPU whisper'; | |
| handleInput.value = '@user'; | |
| descriptionInput.value = 'Builds robust ML systems, from data to deployment.'; | |
| artImage.removeAttribute('src'); | |
| artImage.style.display = 'none'; | |
| cardShape.style.display = ''; | |
| window.__attacksState = [ | |
| {name: 'Code Review', cost: [{type: 'generalist', count: 2}], damage: '42'}, | |
| {name: 'Debug Session', cost: [{type: 'generalist', count: 3}], damage: '1337'} | |
| ]; | |
| attacksEl.innerHTML = ''; | |
| attacksEditor.innerHTML = ''; | |
| updateShape('circle'); | |
| toggleGlow.checked = true; | |
| toggleMotion.checked = false; | |
| toggleContrast.checked = false; | |
| syncBodyOptions(); | |
| updateName(); updateTitle(); updateLevel(); updateHPType(); updateDescription(); renderFooter(); | |
| setUrlHashFromState(); | |
| showToast('Reset'); | |
| }); | |
| // Show/Hide Builder toggle | |
| toggleBuilderBtn.addEventListener('click', () => { | |
| const isHidden = document.body.classList.toggle('builder-hidden'); | |
| toggleBuilderBtn.textContent = isHidden ? 'Show Builder' : 'Hide Builder'; | |
| toggleBuilderBtn.setAttribute('aria-pressed', String(isHidden)); | |
| setUrlHashFromState(); | |
| }); | |
| // Initialize card | |
| updateName(); | |
| updateTitle(); | |
| updateLevel(); | |
| updateHPType(); | |
| updateDescription(); | |
| // Generate random card ID | |
| const cardNum = Math.floor(Math.random() * 150) + 1; | |
| document.getElementById('cardId').textContent = `${String(cardNum).padStart(3, '0')}/150`; | |
| updateCounters(); | |
| syncBodyOptions(); | |
| populateTypeSelects(); | |
| window.__attacksState = window.__attacksState || [ | |
| {name: 'Code Review', cost: [{type: 'generalist', count: 2}], damage: '42'}, | |
| {name: 'Debug Session', cost: [{type: 'generalist', count: 3}], damage: '1337'} | |
| ]; | |
| buildAttackEditor(); | |
| renderAttacks(); | |
| addAttackBtn.addEventListener('click', () => { | |
| const state = getState(); | |
| if (!Array.isArray(state.attacks)) state.attacks = []; | |
| if (state.attacks.length >= 4) { showToast('Max 4 skills'); return; } | |
| state.attacks.push({ name: 'New Skill', damage: '', text: '', cost: [] }); | |
| applyState(state); setUrlHashFromState(); saveDraftDebounced(); | |
| }); | |
| function saveDraftDebounced() { clearTimeout(saveDraftDebounced._t); saveDraftDebounced._t = setTimeout(saveToLocal, 300); } | |
| // Load from URL hash or localStorage | |
| const hashState = loadStateFromHash(); | |
| if (hashState) { | |
| applyState(hashState); | |
| } else { | |
| try { const local = JSON.parse(localStorage.getItem('cardBuilderState') || 'null'); applyState(local); } catch(_) {} | |
| } | |
| // Add interactive tilt (all directions) | |
| const card = document.querySelector('.card'); | |
| let tiltRAF = null; | |
| function setCardTransform(rx, ry, tz = 0) { | |
| card.style.transform = `perspective(1000px) rotateX(${rx}deg) rotateY(${ry}deg) translateZ(${tz}px)`; | |
| } | |
| function handleMove(e) { | |
| if (document.body.classList.contains('reduced-motion')) return; | |
| const rect = card.getBoundingClientRect(); | |
| const px = (e.clientX - rect.left) / rect.width; // 0..1 | |
| const py = (e.clientY - rect.top) / rect.height; // 0..1 | |
| const dx = (px - 0.5) * 2; // -1..1 | |
| const dy = (py - 0.5) * 2; // -1..1 | |
| const maxTilt = 10; // degrees | |
| const rx = -dy * maxTilt; // invert Y for natural tilt | |
| const ry = dx * maxTilt; | |
| if (tiltRAF) cancelAnimationFrame(tiltRAF); | |
| tiltRAF = requestAnimationFrame(() => setCardTransform(rx, ry)); | |
| } | |
| function handleEnter() { | |
| if (document.body.classList.contains('reduced-motion')) return; | |
| card.addEventListener('mousemove', handleMove); | |
| } | |
| function handleLeave() { | |
| card.removeEventListener('mousemove', handleMove); | |
| // Reset to subtle default tilt when leaving | |
| setCardTransform(5, 0, 0); | |
| } | |
| card.addEventListener('mouseenter', handleEnter); | |
| card.addEventListener('mouseleave', handleLeave); | |
| // Initialize default tilt | |
| setCardTransform(5, 0, 0); | |
| </script> | |
| </body> | |
| </html> | |