balloonDesign/index.html

337 lines
20 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Balloon Designer — 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" />
<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-4 md:p-8 flex flex-col items-center justify-center min-h-screen bg-gray-100 text-gray-900">
<div class="container mx-auto p-6 bg-white rounded-2xl shadow-x3 flex flex-col gap-6 max-w-7xl lg:h-[calc(100vh-4rem)]">
<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>
<div id="global-export-bar" class="p-3 bg-gray-100/80 backdrop-blur-sm border border-gray-200 rounded-lg flex flex-wrap items-center justify-center gap-4 sticky top-4 z-20">
<h3 class="text-base font-semibold text-gray-700 mr-2 hidden sm:block">Export Design:</h3>
<button id="export-png-btn" class="btn-dark">Export as PNG</button>
<button id="export-svg-btn" class="btn-dark">Export as SVG</button>
<p class="text-xs text-gray-500 w-full text-center mt-1">(PNG for both modes, SVG for Classic mode)</p>
</div>
<section id="tab-organic" class="flex flex-col lg:flex-row gap-8 lg:h-[calc(100vh-12rem)]">
<aside id="controls-panel"
class="w-full lg:w-1/3 p-6 bg-gray-50 rounded-xl shadow-md flex flex-col gap-4
lg:min-h-0 lg:h-full lg:overflow-y-auto lg:pr-2">
<h1 class="text-3xl font-bold text-center text-blue-800">Organic Balloon Designer</h1>
<p class="text-gray-600 text-sm text-center">Click adds. Double-click deletes. Tools below for erase/select.</p>
<div id="controls-toolbar"
class="sticky top-0 z-10 -mx-6 px-6 pb-2 bg-gray-50/95 backdrop-blur supports-[backdrop-filter]:bg-gray-50/70 flex flex-wrap items-center gap-2">
<button id="expand-all" class="btn-dark">Expand all</button>
<button id="collapse-all" class="btn-dark">Collapse all</button>
<button id="toggle-reorder" class="btn-dark" aria-pressed="false">Reorder panels</button>
</div>
<details class="group" data-acc-id="tools" open>
<summary class="cursor-pointer select-none flex items-center justify-between rounded-lg bg-white/70 px-3 py-2 shadow-sm hover:bg-white">
<span class="flex items-center gap-2">
<span class="drag-handle text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing" draggable="true" title="Drag to reorder">⋮⋮</span>
<span class="text-lg font-semibold text-gray-800">Tools</span>
</span>
<svg class="h-4 w-4 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor"><path d="M5.23 7.21a.75.75 0 011.06.02L10 11.086l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0l-4.24-4.41a.75.75 0 01.02-1.06z"/></svg>
</summary>
<div class="border border-t-0 rounded-b-lg bg-white/50 px-3 pb-4 pt-3">
<div class="grid grid-cols-3 gap-2 mb-3">
<button id="tool-draw" class="tool-btn" aria-pressed="true" title="V">Draw</button>
<button id="tool-erase" class="tool-btn" aria-pressed="false" title="E">Eraser</button>
<button id="tool-select" class="tool-btn" aria-pressed="false" title="S">Select</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 Selected</button>
<button id="duplicate-selected" class="btn-dark" disabled>Duplicate</button>
</div>
<p class="hint">Click a balloon to select. <kbd>Del</kbd>/<kbd>Backspace</kbd> removes. <kbd>Esc</kbd> clears.</p>
</div>
</div>
</details>
<details class="group" data-acc-id="share">
<summary class="cursor-pointer select-none flex items-center justify-between rounded-lg bg-white/70 px-3 py-2 shadow-sm hover:bg-white">
<span class="flex items-center gap-2">
<span class="drag-handle text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing" draggable="true" title="Drag to reorder">⋮⋮</span>
<span class="text-lg font-semibold text-gray-800">Share</span>
</span>
<svg class="h-4 w-4 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor"><path d="M5.23 7.21a.75.75 0 011.06.02L10 11.086l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0l-4.24-4.41a.75.75 0 01.02-1.06z"/></svg>
</summary>
<div class="border border-t-0 rounded-b-lg bg-white/50 px-3 pb-4 pt-3">
<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">Generate Shareable Link</button>
</div>
</details>
<details class="group" data-acc-id="save">
<summary class="cursor-pointer select-none flex items-center justify-between rounded-lg bg-white/70 px-3 py-2 shadow-sm hover:bg-white">
<span class="flex items-center gap-2">
<span class="drag-handle text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing" draggable="true" title="Drag to reorder">⋮⋮</span>
<span class="text-lg font-semibold text-gray-800">Save & Load</span>
</span>
<svg class="h-4 w-4 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor"><path d="M5.23 7.21a.75.75 0 011.06.02L10 11.086l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0l-4.24-4.41a.75.75 0 01.02-1.06z"/></svg>
</summary>
<div class="border border-t-0 rounded-b-lg bg-white/50 px-3 pb-4 pt-3">
<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>
</details>
<details class="group" data-acc-id="allowed" open>
<summary class="cursor-pointer select-none flex items-center justify-between rounded-lg bg-white/70 px-3 py-2 shadow-sm hover:bg-white">
<span class="flex items-center gap-2">
<span class="drag-handle text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing" draggable="true" title="Drag to reorder">⋮⋮</span>
<span class="text-lg font-semibold text-gray-800">Colors</span>
</span>
<svg class="h-4 w-4 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor"><path d="M5.23 7.21a.75.75 0 011.06.02L10 11.086l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0l-4.24-4.41a.75.75 0 01.02-1.06z"/></svg>
</summary>
<div class="border border-t-0 rounded-b-lg bg-white/50 px-3 pb-4 pt-3">
<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>
</details>
<details class="group" data-acc-id="replace">
<summary class="cursor-pointer select-none flex items-center justify-between rounded-lg bg-white/70 px-3 py-2 shadow-sm hover:bg-white">
<span class="flex items-center gap-2">
<span class="drag-handle text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing" draggable="true" title="Drag to reorder">⋮⋮</span>
<span class="text-lg font-semibold text-gray-800">Replace Color</span>
</span>
<svg class="h-4 w-4 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor"><path d="M5.23 7.21a.75.75 0 011.06.02L10 11.086l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0l-4.24-4.41a.75.75 0 01.02-1.06z"/></svg>
</summary>
<div class="border border-t-0 rounded-b-lg bg-white/50 px-3 pb-4 pt-3">
<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>
</details>
<details class="group" data-acc-id="size">
<summary class="cursor-pointer select-none flex items-center justify-between rounded-lg bg-white/70 px-3 py-2 shadow-sm hover:bg-white">
<span class="flex items-center gap-2">
<span class="drag-handle text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing" draggable="true" title="Drag to reorder">⋮⋮</span>
<span class="text-lg font-semibold text-gray-800">Balloon Size (Diameter)</span>
</span>
<svg class="h-4 w-4 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor"><path d="M5.23 7.21a.75.75 0 011.06.02L10 11.086l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0l-4.24-4.41a.75.75 0 01.02-1.06z"/></svg>
</summary>
<div class="border border-t-0 rounded-b-lg bg-white/50 px-3 pb-4 pt-3">
<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>
<button id="toggle-shine-btn" class="btn-dark">Turn Off Shine</button>
</div>
</details>
<details class="group" data-acc-id="used" open>
<summary class="cursor-pointer select-none flex items-center justify-between rounded-lg bg-white/70 px-3 py-2 shadow-sm hover:bg-white">
<span class="flex items-center gap-2">
<span class="drag-handle text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing" draggable="true" title="Drag to reorder">⋮⋮</span>
<span class="text-lg font-semibold text-gray-800">Color Palette</span>
</span>
<svg class="h-4 w-4 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor"><path d="M5.23 7.21a.75.75 0 011.06.02L10 11.086l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0l-4.24-4.41a.75.75 0 01.02-1.06z"/></svg>
</summary>
<div class="border border-t-0 rounded-b-lg bg-white/50 px-3 pb-4 pt-3">
<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>
</details>
</aside>
<section id="canvas-panel" class="w-full lg:flex-1 flex flex-col items-stretch lg:sticky lg:top-8 lg:self-start shadow-x3">
<div class="flex gap-2 mb-3">
<button id="expand-workspace-btn" class="bg-gray-700 text-white px-3 py-2 rounded">Expand workspace</button>
<button id="fullscreen-btn" class="bg-gray-700 text-white px-3 py-2 rounded">Fullscreen</button>
</div>
<canvas id="balloon-canvas" class="balloon-canvas w-full aspect-video"></canvas>
</section>
</section>
<section id="tab-classic" class="hidden flex flex-col lg:flex-row gap-8 lg:h-[calc(100vh-12rem)]">
<aside id="classic-controls-panel"
class="w-full lg:w-1/3 p-6 bg-gray-50 rounded-xl shadow-md flex flex-col gap-4
lg:min-h-0 lg:h-full lg:overflow-y-auto lg:pr-2">
<h2 class="text-2xl font-bold text-blue-800">Classic Designer (Arch / Column)</h2>
<p class="text-gray-600 text-sm">Quad-wrap column or arch with a 4-color spiral.</p>
<div id="classic-toolbar"
class="sticky top-0 z-10 -mx-6 px-6 pb-2 bg-gray-50/95 backdrop-blur supports-[backdrop-filter]:bg-gray-50/70
flex flex-wrap items-center gap-2">
<button id="classic-expand-all" class="btn-dark">Expand all</button>
<button id="classic-collapse-all" class="btn-dark">Collapse all</button>
<button id="classic-toggle-reorder" class="btn-dark" aria-pressed="false">Reorder panels</button>
</div>
<details class="group" data-acc-id="classic-layout" open>
<summary class="cursor-pointer select-none flex items-center justify-between rounded-lg bg-white/70 px-3 py-2 shadow-sm hover:bg-white">
<span class="flex items-center gap-2">
<span class="drag-handle text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing" draggable="true" title="Drag to reorder">⋮⋮</span>
<span class="text-lg font-semibold text-gray-800">Pattern & Layout</span>
</span>
<svg class="h-4 w-4 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor"><path d="M5.23 7.21a.75.75 0 011.06.02L10 11.086l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0l-4.24-4.41a.75.75 0 01.02-1.06z"/></svg>
</summary>
<div class="border border-t-0 rounded-b-lg bg-white/50 px-3 pb-4 pt-3 space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<label class="text-sm">Pattern:
<select id="classic-pattern" class="select align-middle">
<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>
</select>
</label>
<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="topper-controls" class="hidden grid grid-cols-1 sm:grid-cols-4 gap-3 items-end pt-2 border-t border-gray-200">
<label class="text-sm sm:col-span-2">Topper Type:
<select id="classic-topper-type" class="select align-middle" disabled>
<option value="round">24" Round</option>
<option value="star">24" Star</option>
<option value="heart">24" Heart</option>
</select>
</label>
<label class="text-sm inline-flex items-center gap-2 font-medium">
<input id="classic-topper-enabled" type="checkbox" class="align-middle">
Add Topper
</label>
<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">Topper Color:
<div id="classic-topper-color-swatch" class="slot-swatch mx-auto" title="Click to change topper color">T</div>
</label>
<label class="text-sm">X Offset:
<input id="classic-topper-offset-x" type="number" step="0.5" value="0" class="w-full px-2 py-1 border rounded align-middle">
</label>
<label class="text-sm">Y Offset:
<input id="classic-topper-offset-y" type="number" step="0.5" value="0" class="w-full px-2 py-1 border rounded align-middle">
</label>
<label class="text-sm">Topper Size:
<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">
</label>
</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">
<input id="classic-reverse" type="checkbox" class="align-middle">
Reverse spiral
</label>
<button id="classic-rerender" class="btn-blue ml-auto">Rebuild</button>
</div>
</div>
</details>
<details class="group" data-acc-id="classic-colors" open>
<summary class="cursor-pointer select-none flex items-center justify-between rounded-lg bg-white/70 px-3 py-2 shadow-sm hover:bg-white">
<span class="flex items-center gap-2">
<span class="drag-handle text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing" draggable="true" title="Drag to reorder">⋮⋮</span>
<span class="text-lg font-semibold text-gray-800">Classic Colors</span>
</span>
<svg class="h-4 w-4 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="currentColor"><path d="M5.23 7.21a.75.75 0 011.06.02L10 11.086l3.71-3.855a.75.75 0 111.08 1.04l-4.24 4.41a.75.75 0 01-1.08 0l-4.24-4.41a.75.75 0 01.02-1.06z"/></svg>
</summary>
<div class="border border-t-0 rounded-b-lg bg-white/50 px-3 pb-4 pt-3">
<div id="classic-slots" class="flex items-center gap-2 mb-3">
<button type="button" class="slot-btn tab-btn" data-slot="1">#1</button>
<button type="button" class="slot-btn tab-btn" data-slot="2">#2</button>
<button type="button" class="slot-btn tab-btn" data-slot="3">#3</button>
<button type="button" class="slot-btn tab-btn" data-slot="4">#4</button>
<button type="button" class="slot-btn tab-btn" data-slot="5">#5</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>
</details>
</aside>
<section id="classic-canvas-panel"
class="w-full lg:flex-1 flex flex-col items-stretch lg:sticky lg:top-8 lg:self-start shadow-x3">
<div id="classic-display"
class="rounded-xl"
style="width:100%;height:72vh;border:1px solid #e5e7eb;background:#fff;overflow:auto;"></div>
</section>
</section>
<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>
</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="app.js" defer></script>
<script src="organic.js" defer></script>
<script src="classic.js" defer></script>
<script>
</script>
</body>
</html>