balloonDesign/index.html
2025-12-01 09:18:19 -05:00

367 lines
22 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Balloon Studio — Organic & Classic</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
<script src="https://unpkg.com/mithril/mithril.js" defer></script>
<script src="colors.js"></script>
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
.tab-btn{padding:.5rem .75rem;border-radius:.5rem;font-size:.875rem;font-weight:600;transition:background-color .2s,color .2s,box-shadow .2s}
.tab-active{background:#2563eb;color:#fff;box-shadow:0 2px 6px rgba(37,99,235,.35)}
.tab-idle{background:#e5e7eb;color:#1f2937}.tab-idle:hover{background:#d1d5db}
#classic-display{width:100%;max-width:1200px;height:70vh;border:1px solid #e5e7eb;background:#fff;overflow:auto;border-radius:.75rem}
.copy-message{opacity:0;pointer-events:none;transition:opacity .2s}.copy-message.show{opacity:1}
</style>
</head>
<body class="p-0 md:p-6 flex flex-col items-center justify-start min-h-screen bg-[conic-gradient(at_top_left,_var(--tw-gradient-stops))] from-indigo-100 via-white to-pink-100 text-slate-800 overflow-hidden">
<div class="container mx-auto mt-2 p-4 lg:p-6 bg-white/80 lg:backdrop-blur-xl rounded-3xl border border-white/50 shadow-2xl flex flex-col gap-4 max-w-7xl lg:h-[calc(100vh-2rem)] overflow-hidden ring-1 ring-black/5">
<header class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3 px-1 lg:px-0">
<div class="flex items-center gap-3">
<div>
<div class="text-3xl font-black text-transparent bg-clip-text bg-gradient-to-r from-indigo-600 to-pink-600 tracking-tight filter drop-shadow-sm">Balloon Studio</div>
<div class="text-xs text-indigo-500 font-bold uppercase tracking-wider">Professional Design Tool</div>
</div>
</div>
<nav id="mode-tabs" class="flex gap-2">
<button type="button" class="tab-btn tab-active" data-target="#tab-organic" aria-pressed="true">Organic</button>
<button type="button" class="tab-btn tab-idle" data-target="#tab-classic" aria-pressed="false">Classic (Arch/Column)</button>
</nav>
</header>
<section id="tab-organic" class="flex flex-col lg:flex-row gap-4 lg:h-[calc(100vh-10rem)]">
<aside id="controls-panel" class="control-sheet lg:static lg:w-[360px] lg:max-h-none lg:overflow-y-auto">
<div class="panel-header-row">
<h2 class="panel-title">Organic Controls</h2>
</div>
<div class="control-stack" data-mobile-tab="controls">
<div class="panel-heading">Tools</div>
<div class="panel-card">
<div class="grid grid-cols-3 gap-2 mb-3">
<button id="tool-draw" class="tool-btn" aria-pressed="true" title="V">
<svg viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
<span class="hidden sm:inline">Draw</span>
</button>
<button id="tool-erase" class="tool-btn" aria-pressed="false" title="E">
<svg viewBox="0 0 24 24"><path d="M16.24 3.56l4.95 4.94c.78.79.78 2.05 0 2.84L12 20.53a4.008 4.008 0 0 1-5.66 0L2.81 17c-.78-.79-.78-2.05 0-2.84l10.6-10.6c.79-.78 2.05-.78 2.83 0zM4.22 15.58l3.54 3.53c.78.79 2.04.79 2.83 0l8.48-8.48-3.54-3.54-8.48 8.48c-.79.79-.79 2.05 0 2.84z"/></svg>
<span class="hidden sm:inline">Erase</span>
</button>
<button id="tool-select" class="tool-btn" aria-pressed="false" title="S">
<svg viewBox="0 0 24 24"><path d="M7 2l12 11.2-5.8.5 3.3 7.3-2.2.9-3.2-7.4-4.4 4V2z"/></svg>
<span class="hidden sm:inline">Select</span>
</button>
</div>
<div class="grid grid-cols-3 gap-2 mb-3">
<button id="tool-undo" class="tool-btn" title="Ctrl+Z" aria-label="Undo">
<svg viewBox="0 0 24 24"><path d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z"/></svg>
<span class="hidden sm:inline">Undo</span>
</button>
<button id="tool-redo" class="tool-btn" title="Ctrl+Y" aria-label="Redo">
<svg viewBox="0 0 24 24"><path d="M18.4 10.6C16.55 9 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7l-3.6 3.6z"/></svg>
<span class="hidden sm:inline">Redo</span>
</button>
<button id="tool-eyedropper" class="tool-btn" title="Pick Color" aria-label="Eyedropper" aria-pressed="false">
<svg viewBox="0 0 24 24"><path d="M20.71 5.63l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-3.12 3.12-1.93-1.91-1.41 1.41 1.42 1.42L3 16.25V21h4.75l8.92-8.92 1.42 1.42 1.41-1.41-1.92-1.92 3.12-3.12c.4-.4.4-1.03.01-1.42zM6.92 19L5 17.08l8.06-8.06 1.92 1.92L6.92 19z"/></svg>
<span class="hidden sm:inline">Picker</span>
</button>
</div>
<div id="eraser-controls" class="hidden flex flex-col gap-2">
<label class="text-sm font-medium text-gray-700">Eraser Size: <span id="eraser-size-label">30</span>px</label>
<input type="range" id="eraser-size" min="10" max="120" value="30" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<p class="hint">Click-drag to erase. Preview circle shows the area.</p>
</div>
<div id="select-controls" class="hidden flex flex-col gap-2">
<div class="flex gap-2">
<button id="delete-selected" class="btn-danger" disabled>Delete</button>
<button id="duplicate-selected" class="btn-dark" disabled>Duplicate</button>
</div>
<div class="mt-2">
<div class="flex items-center gap-2 text-xs text-gray-600 mb-1">
<span class="font-semibold">Move</span>
<span class="hint">↑↓←→</span>
</div>
<div class="grid grid-cols-4 gap-1 max-w-xs">
<button type="button" class="btn-dark nudge-selected text-sm py-2" data-dx="0" data-dy="-5" aria-label="Move Selection Up"></button>
<button type="button" class="btn-dark nudge-selected text-sm py-2" data-dx="5" data-dy="0" aria-label="Move Selection Right"></button>
<button type="button" class="btn-dark nudge-selected text-sm py-2" data-dx="0" data-dy="5" aria-label="Move Selection Down"></button>
<button type="button" class="btn-dark nudge-selected text-sm py-2" data-dx="-5" data-dy="0" aria-label="Move Selection Left"></button>
</div>
</div>
<div class="mt-2 flex items-center gap-2 text-xs text-gray-600">
<span class="font-semibold">Resize</span>
<input type="range" id="selected-size" min="5" max="200" value="40" class="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" disabled>
<span id="selected-size-label" class="text-xs w-10 text-right">0</span>
</div>
<div class="mt-2 grid grid-cols-2 gap-2">
<button type="button" class="btn-dark text-sm py-2" id="bring-forward" disabled>Bring Forward</button>
<button type="button" class="btn-dark text-sm py-2" id="send-backward" disabled>Send Backward</button>
</div>
<div class="mt-2">
<button type="button" class="btn-blue w-full" id="apply-selected-color" disabled>Apply Current Color</button>
<p class="hint">Uses the color/texture currently picked in the palette.</p>
</div>
<p class="hint">Click a balloon to select. Del/Backspace removes. Esc clears.</p>
</div>
</div>
<div class="panel-heading mt-4">Size & Shine</div>
<div class="panel-card">
<div id="size-preset-group" class="grid grid-cols-5 gap-2 mb-2"></div>
<p class="hint mb-3">Global scale lives in <code>PX_PER_INCH</code> (see <code>script.js</code>).</p>
<label class="text-sm inline-flex items-center gap-2 font-medium">
<input id="toggle-shine-checkbox" type="checkbox" class="align-middle" checked>
Enable Shine
</label>
</div>
</div>
<div class="control-stack" data-mobile-tab="colors">
<div class="panel-heading">Used Colors</div>
<div class="panel-card">
<div class="flex items-center justify-between mb-2">
<span class="text-sm text-gray-600">Built from the current design. Click a swatch to select that color.</span>
<button id="sort-used-toggle" class="text-sm underline">Sort: Most → Least</button>
</div>
<div id="used-palette" class="palette-box min-h-[3rem]"></div>
</div>
<div class="panel-heading mt-4">Allowed Colors</div>
<div class="panel-card">
<p class="hint mb-2">Alt+Click a balloon on canvas to pick its color.</p>
<div id="color-palette" class="palette-box"></div>
</div>
<div class="panel-heading mt-4">Replace Color</div>
<div class="panel-card">
<div class="grid grid-cols-1 gap-2">
<label class="text-sm font-medium">From (used):</label>
<select id="replace-from" class="select"></select>
<label class="text-sm font-medium">To (allowed):</label>
<select id="replace-to" class="select"></select>
<button id="replace-btn" class="btn-blue">Replace</button>
<p id="replace-msg" class="hint"></p>
</div>
</div>
</div>
<div class="control-stack" data-mobile-tab="save">
<div class="panel-heading">Share</div>
<div class="panel-card">
<div class="relative mb-3">
<input type="text" id="share-link-output" class="w-full p-3 pr-10 border border-gray-300 rounded-lg text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-500" readonly placeholder="Click 'Generate Link' to create a shareable URL">
<div id="copy-message" class="copy-message absolute right-2 top-1/2 -translate-y-1/2 bg-blue-500 text-white px-2 py-1 text-xs rounded-full">Copied!</div>
</div>
<button id="generate-link-btn" class="btn-indigo w-full">Generate Shareable Link</button>
</div>
<div class="panel-heading mt-4">Save & Load</div>
<div class="panel-card">
<div class="flex flex-wrap gap-3 mb-3">
<button id="clear-canvas-btn" class="btn-danger">Clear Canvas</button>
<button id="save-json-btn" class="btn-green">Save Design</button>
<label for="load-json-input" class="btn-yellow text-center cursor-pointer">Load JSON</label>
<input type="file" id="load-json-input" class="hidden" accept=".json">
</div>
<div class="flex flex-wrap gap-3 mt-2">
<button class="btn-dark bg-blue-600" data-export="png">Export PNG</button>
<button class="btn-dark bg-blue-700" data-export="svg">Export SVG</button>
<p class="hint w-full">SVG currently Classic only.</p>
</div>
</div>
</div>
</aside>
<section id="canvas-panel" class="order-1 lg:order-2 w-full lg:flex-1 flex flex-col items-stretch rounded-2xl overflow-hidden bg-white/50 shadow-inner ring-1 ring-black/5">
<canvas id="balloon-canvas" class="balloon-canvas w-full min-h-[65vh]"></canvas>
</section>
</section>
<section id="tab-classic" class="hidden flex flex-col lg:flex-row gap-4 lg:h-[calc(100vh-10rem)]">
<aside id="classic-controls-panel" class="control-sheet lg:static lg:w-[360px] lg:max-h-none lg:overflow-y-auto">
<div class="panel-header-row">
<h2 class="panel-title">Classic Controls</h2>
</div>
<div class="control-stack" data-mobile-tab="controls">
<div class="panel-heading">Pattern & Layout</div>
<div class="panel-card space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2">
<div class="text-sm font-medium text-gray-700">Shape</div>
<div class="flex gap-2">
<button type="button" class="tab-btn tab-active pattern-btn" data-pattern-shape="arch" aria-pressed="true">Arch</button>
<button type="button" class="tab-btn tab-idle pattern-btn" data-pattern-shape="column" aria-pressed="false">Column</button>
</div>
</div>
<div class="space-y-2">
<div class="text-sm font-medium text-gray-700">Balloon Count</div>
<div class="flex gap-2">
<button type="button" class="tab-btn tab-active pattern-btn" data-pattern-count="4" aria-pressed="true">4 Colors</button>
<button type="button" class="tab-btn tab-idle pattern-btn" data-pattern-count="5" aria-pressed="false">5 Colors</button>
</div>
</div>
<div class="md:col-span-2 space-y-2">
<div class="text-sm font-medium text-gray-700">Layout</div>
<div class="flex gap-2">
<button type="button" class="tab-btn tab-active pattern-btn" data-pattern-layout="spiral" aria-pressed="true">Spiral</button>
<button type="button" class="tab-btn tab-idle pattern-btn" data-pattern-layout="stacked" aria-pressed="false">Stacked</button>
</div>
</div>
<select id="classic-pattern" class="select align-middle hidden" aria-hidden="true" tabindex="-1">
<option value="Arch 4">Arch 4 (4-color spiral)</option>
<option value="Column 4">Column 4 (quad wrap)</option>
<option value="Arch 5">Arch 5 (5-color spiral)</option>
<option value="Column 5">Column 5 (5-balloon wrap)</option>
<option value="Arch 4 Stacked">Arch 4 (stacked)</option>
<option value="Column 4 Stacked">Column 4 (stacked)</option>
<option value="Arch 5 Stacked">Arch 5 (stacked)</option>
<option value="Column 5 Stacked">Column 5 (stacked)</option>
</select>
<label class="text-sm">Length (ft):
<input id="classic-length-ft" type="number" min="1" max="100" step="0.5" value="5" class="w-full px-2 py-1 border rounded align-middle">
</label>
</div>
<div id="classic-topper-toggle-row" class="flex items-center gap-3 pt-2 border-t border-gray-200 hidden">
<label class="text-sm inline-flex items-center gap-2 font-medium">
<input id="classic-topper-enabled" type="checkbox" class="align-middle">
Add Topper (24")
</label>
</div>
<div id="topper-controls" class="hidden grid grid-cols-1 sm:grid-cols-4 gap-3 items-start pt-2">
<div class="sm:col-span-2">
<div class="text-sm font-medium mb-2">Topper Shape</div>
<div id="topper-type-group" class="topper-type-group">
<button type="button" class="tab-btn topper-type-btn tab-active" data-type="round" aria-pressed="true"><i class="fa-regular fa-circle-dot"></i><span class="hidden sm:inline">Round</span></button>
<button type="button" class="tab-btn topper-type-btn tab-idle" data-type="star" aria-pressed="false"><i class="fa-solid fa-star"></i><span class="hidden sm:inline">Star</span></button>
<button type="button" class="tab-btn topper-type-btn tab-idle" data-type="heart" aria-pressed="false"><i class="fa-solid fa-heart"></i><span class="hidden sm:inline">Heart</span></button>
</div>
</div>
<div class="sm:col-span-2">
<div class="text-sm font-medium mb-1">Topper Size</div>
<input id="classic-topper-size" type="range" min="0.5" max="2" step="0.05" value="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
</div>
</div>
<div class="text-xs text-gray-500">
<span id="classic-cluster-hint">≈ 10 clusters (rule: 2 clusters/ft)</span>
</div>
<div class="flex flex-wrap items-center gap-x-6 gap-y-3 pt-2 border-t border-gray-200">
<label class="text-sm inline-flex items-center gap-2 font-medium">
<input id="classic-shine-enabled" type="checkbox" class="align-middle" checked>
Enable Shine
</label>
<label class="text-sm inline-flex items-center gap-2">
<input id="classic-reverse" type="checkbox" class="align-middle">
Reverse spiral
</label>
</div>
</div>
</div>
<div class="control-stack" data-mobile-tab="colors">
<div class="panel-heading">Classic Colors</div>
<div class="panel-card">
<div class="flex items-center justify-between mb-2">
<div id="classic-slots" class="flex items-center gap-2"></div>
<button id="classic-add-slot" class="btn-dark text-sm px-3 py-2 hidden" type="button" title="Add color slot">+</button>
</div>
<div class="text-sm text-gray-600 mb-1">Pick a color for <span id="classic-active-label" class="font-bold">Slot #1</span> (from colors.js):</div>
<div id="classic-swatch-grid" class="palette-box min-h-[3rem]"></div>
<div class="flex flex-wrap gap-2 mt-3">
<button id="classic-randomize-colors" class="btn-dark">Randomize 5</button>
</div>
<div class="panel-heading mt-3">Topper Color</div>
<div class="flex items-center gap-3">
<button id="classic-topper-color-swatch" class="slot-swatch" title="Click to change topper color">T</button>
<p class="hint">Select a color then click to apply.</p>
</div>
</div>
</div>
<div class="control-stack" data-mobile-tab="save">
<div class="panel-heading">Save & Share</div>
<div class="panel-card space-y-3">
<div class="flex flex-wrap gap-3">
<button class="btn-dark bg-blue-600" data-export="png">Export PNG</button>
<button class="btn-dark bg-blue-700" data-export="svg">Export SVG</button>
<p class="hint w-full">SVG recommended for Classic.</p>
</div>
<p class="hint text-red-500">Classic JSON save/load not available yet.</p>
</div>
</div>
</aside>
<section id="classic-canvas-panel"
class="order-1 w-full lg:flex-1 flex flex-col items-stretch shadow-x3 rounded-2xl overflow-hidden bg-white">
<div id="classic-display"
class="rounded-xl"
style="width:100%;height:72vh;border:1px solid #e5e7eb;background:#fff;overflow:auto;"></div>
<div id="floating-topper-nudge" class="floating-nudge hidden">
<div class="floating-nudge-header">
<div class="panel-heading">Nudge Topper</div>
<button type="button" id="floating-nudge-toggle" class="btn-dark text-xs px-3 py-2">Hide</button>
</div>
<div class="floating-nudge-body">
<div class="grid grid-cols-3 gap-2">
<div></div>
<button type="button" class="btn-dark nudge-topper" data-dx="0" data-dy="0.5" aria-label="Move Topper Up"></button>
<div></div>
<button type="button" class="btn-dark nudge-topper" data-dx="-0.5" data-dy="0" aria-label="Move Topper Left"></button>
<div></div>
<button type="button" class="btn-dark nudge-topper" data-dx="0.5" data-dy="0" aria-label="Move Topper Right"></button>
<div></div>
<button type="button" class="btn-dark nudge-topper" data-dx="0" data-dy="-0.5" aria-label="Move Topper Down"></button>
<div></div>
</div>
</div>
</div>
</section>
</section>
</div>
<div id="mobile-tabbar" class="mobile-tabbar">
<button type="button" class="mobile-tab-btn" data-mobile-tab="controls" aria-pressed="true" aria-label="Tools">
<i class="mobile-tab-icon fa-solid fa-wand-magic-sparkles" aria-hidden="true"></i>
<span class="sr-only">Tools</span>
</button>
<button type="button" class="mobile-tab-btn" data-mobile-tab="colors" aria-pressed="false" aria-label="Colors">
<i class="mobile-tab-icon fa-solid fa-palette" aria-hidden="true"></i>
<span class="sr-only">Colors</span>
</button>
<button type="button" class="mobile-tab-btn" data-mobile-tab="save" aria-pressed="false" aria-label="Save and Share">
<i class="mobile-tab-icon fa-solid fa-cloud-arrow-up" aria-hidden="true"></i>
<span class="sr-only">Save</span>
</button>
</div>
<div id="message-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-gray-900 bg-opacity-50">
<div class="bg-white p-6 rounded-lg shadow-lg max-w-sm text-center">
<p id="modal-text" class="text-gray-800 text-lg"></p>
<button id="modal-close-btn" class="mt-4 btn-blue">OK</button>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/lz-string@1.5.0/libs/lz-string.min.js" defer></script>
<script src="script.js" defer></script>
<script src="classic.js" defer></script>
</body>
</html>