balloonDesign/index.html

630 lines
38 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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://cdn.jsdelivr.net/npm/sweetalert2@11"></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 id="app-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>
<div class="flex items-center gap-4">
<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>
<button type="button" class="tab-btn tab-idle" data-target="#tab-wall" aria-pressed="false">Wall</button>
</nav>
<div class="flex items-center gap-3">
<button id="app-fullscreen-toggle" class="btn-dark text-xs px-3 py-2" aria-label="Toggle fullscreen">Fullscreen</button>
<button id="clear-canvas-btn-top" class="btn-danger text-xs px-3 py-2">Start Fresh</button>
</div>
</div>
</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-4 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-garland" class="tool-btn" aria-pressed="false" title="G">
<svg viewBox="0 0 24 24"><path d="M4 17c3-4 6-6 9-6s5 2 7 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle cx="7" cy="14" r="1.4"/><circle cx="11.5" cy="12.5" r="1.4"/><circle cx="16" cy="13.5" r="1.4"/><circle cx="19" cy="16.5" r="1.4"/></svg>
<span class="hidden sm:inline">Path</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" 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" 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>
<p class="hint mt-1">Use Path to click-drag a line; balloons will be auto-placed along it.</p>
<div id="garland-controls" class="mt-2 flex flex-col gap-3 text-sm text-gray-700">
<div class="flex items-center gap-3">
<label for="garland-density" class="font-medium w-20">Density</label>
<input id="garland-density" type="range" min="0.6" max="1.6" step="0.1" value="1" class="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<span id="garland-density-label" class="w-10 text-right text-xs text-gray-500">1.0</span>
</div>
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="font-medium text-sm text-gray-700">Main Colors</span>
<button type="button" id="garland-add-color" class="btn-blue text-xs px-3 py-1">+ Add</button>
</div>
<div id="garland-main-chips" class="flex flex-wrap gap-2"></div>
<p class="hint text-xs">Tap a chip to change it. You can add up to 10 main colors.</p>
<div class="flex items-center gap-2">
<span class="font-medium text-sm text-gray-700">Accent</span>
<button type="button" id="garland-accent-chip" class="replace-chip" aria-label="Pick accent color"></button>
<button type="button" id="garland-accent-clear" class="btn-yellow text-xs px-3 py-1">Clear</button>
</div>
</div>
</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">Hover to preview. Click-drag to erase.</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">
<p class="hint">Drag balloons to reposition. Use arrows/touches for fine nudges.</p>
</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="32" step="0.5" value="11" class="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" disabled>
<span id="selected-size-label" class="text-xs w-12 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">Size presets adjust the diameter for new balloons.</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>
<label class="text-sm inline-flex items-center gap-2 font-medium">
<input id="toggle-border-checkbox" type="checkbox" class="align-middle">
Outline Balloons
</label>
<button type="button" id="fit-view-btn" class="btn-dark text-sm mt-3 w-full">Fit to Design</button>
</div>
</div>
<div class="control-stack" data-mobile-tab="colors">
<div class="panel-heading">Project Palette</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">Color Library</div>
<div class="panel-card">
<p class="hint mb-2">Tap or click on canvas to sample a balloons color (use the eyedropper).</p>
<div class="flex items-center gap-3 mb-2">
<span class="text-sm font-medium text-gray-700">Active Color</span>
<div id="current-color-chip" class="current-color-chip">
<span id="current-color-label" class="text-xs font-semibold text-slate-700"></span>
</div>
</div>
<div id="color-palette" class="palette-box"></div>
</div>
<div class="panel-heading mt-4">Replace Color</div>
<div class="panel-card space-y-3">
<div class="flex items-center gap-2 replace-row">
<button type="button" class="replace-chip" id="replace-from-chip" aria-label="Pick color to replace"></button>
<span class="text-xs font-semibold text-slate-500"></span>
<button type="button" class="replace-chip" id="replace-to-chip" aria-label="Pick replacement color"></button>
<span id="replace-count" class="text-xs text-slate-500 ml-auto"></span>
</div>
<div class="grid grid-cols-1 gap-2">
<p class="hint text-xs">Tap a chip to choose colors. “From” shows only colors used on canvas.</p>
<select id="replace-from" class="sr-only"></select>
<select id="replace-to" class="sr-only"></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 export keeps vectors in Classic; Organic embeds textures.</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 class="mt-2">
<div class="text-xs font-semibold text-gray-600 mb-1">Numbers</div>
<div class="topper-number-grid">
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-0" aria-pressed="false">0</button>
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-1" aria-pressed="false">1</button>
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-2" aria-pressed="false">2</button>
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-3" aria-pressed="false">3</button>
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-4" aria-pressed="false">4</button>
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-5" aria-pressed="false">5</button>
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-6" aria-pressed="false">6</button>
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-7" aria-pressed="false">7</button>
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-8" aria-pressed="false">8</button>
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-9" aria-pressed="false">9</button>
</div>
</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 id="classic-number-tint-row" class="sm:col-span-2 hidden number-tint-row">
<div class="number-tint-header">
<div class="text-sm font-semibold text-gray-700">Number Tint</div>
<span class="text-xs text-gray-500">Soft overlay for photo digits</span>
</div>
<input id="classic-number-tint" type="range" min="0" max="1" step="0.05" value="0.5" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<p class="hint">Pick a color in Classic Colors, select Topper (T), then adjust strength.</p>
</div>
<div class="sm:col-span-2 flex flex-wrap gap-2 justify-end">
<button type="button" id="classic-nudge-open" class="btn-dark text-xs px-3 py-2">Nudge Panel</button>
</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 font-medium">
<input id="classic-border-enabled" type="checkbox" class="align-middle">
Outline Balloons
</label>
<label class="text-sm inline-flex items-center gap-2">
<input id="classic-reverse" type="checkbox" class="align-middle">
Reverse spiral
</label>
<p class="hint">Use stacked for “same color per quad” layouts; reverse flips the spiral.</p>
</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</button>
</div>
<div id="classic-topper-color-block" class="mt-3 hidden">
<div class="panel-heading">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>
<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 keeps the vector Classic layout; PNG is raster.</p>
</div>
<p class="hint text-red-500">Classic JSON save/load coming soon.</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" aria-label="Close nudge panel">×</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>
<section id="tab-wall" class="hidden flex flex-col lg:flex-row gap-4 lg:h-[calc(100vh-10rem)]">
<aside id="wall-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">Wall Controls</h2>
</div>
<div class="control-stack" data-mobile-tab="controls">
<div class="panel-heading">Grid</div>
<div class="panel-card grid grid-cols-2 gap-3">
<label class="text-sm font-medium flex flex-col gap-1">Columns
<input id="wall-cols" type="number" min="2" max="20" step="1" value="9" class="w-full px-2 py-1 border rounded">
</label>
<label class="text-sm font-medium flex flex-col gap-1">Rows
<input id="wall-rows" type="number" min="2" max="20" step="1" value="7" class="w-full px-2 py-1 border rounded">
</label>
<label class="text-sm font-medium flex flex-col gap-1 col-span-2">Pattern
<select id="wall-pattern" class="select">
<option value="grid">Square Grid</option>
<option value="x">X / Diamond</option>
</select>
</label>
<label class="text-sm font-medium inline-flex items-center gap-2 col-span-2">
<input id="wall-show-wire" type="checkbox" class="align-middle" checked>
Show wireframe for empty spots
</label>
<label class="text-sm font-medium inline-flex items-center gap-2 col-span-2">
<input id="wall-outline" type="checkbox" class="align-middle" checked>
Outline balloons
</label>
</div>
<div class="panel-heading mt-4">Tools</div>
<div class="panel-card">
<div class="wall-toolbar">
<button type="button" id="wall-tool-paint" class="tool-btn" aria-pressed="true">
<i class="fa-solid fa-brush"></i>
<span>Paint</span>
</button>
<button type="button" id="wall-tool-erase" class="tool-btn" aria-pressed="false">
<i class="fa-solid fa-eraser"></i>
<span>Erase</span>
</button>
</div>
<p class="hint mt-2 text-xs">Paint applies the active color; Erase clears. Hold modifier on desktop to erase temporarily.</p>
</div>
</div>
<div class="control-stack" data-mobile-tab="colors">
<div class="panel-heading mt-4">Active Color</div>
<div class="panel-card">
<div class="flex items-center gap-3">
<span class="text-sm font-medium text-gray-700">Current</span>
<div id="wall-active-color-chip" class="current-color-chip">
<span id="wall-active-color-label" class="text-[10px] font-semibold text-slate-700"></span>
</div>
</div>
<p class="hint mt-2">Tap a swatch to set. Tap a balloon to paint; tap again (same color) to clear. Use the eyedropper to pick from the canvas.</p>
</div>
<div class="panel-heading mt-4">Used Colors</div>
<div class="panel-card">
<div class="flex items-center justify-between mb-2">
<div class="text-xs text-gray-600">Click to pick. Remove unused clears empty/transparent entries.</div>
<button type="button" id="wall-remove-unused" class="btn-yellow text-xs px-2 py-1">Remove Unused</button>
</div>
<div id="wall-used-palette" class="palette-box min-h-[2.4rem]"></div>
</div>
<div class="panel-heading mt-4">Wall Palette</div>
<div class="panel-card">
<div id="wall-palette" class="palette-box min-h-[3rem]"></div>
</div>
<div class="panel-heading mt-4">Replace Colors</div>
<div class="panel-card space-y-3">
<div class="flex items-center gap-2 replace-row">
<button type="button" class="replace-chip" id="wall-replace-from-chip" aria-label="Pick wall color to replace"></button>
<span class="text-xs font-semibold text-slate-500"></span>
<button type="button" class="replace-chip" id="wall-replace-to-chip" aria-label="Pick wall replacement color"></button>
<span id="wall-replace-count" class="text-xs text-slate-500 ml-auto"></span>
</div>
<div class="grid grid-cols-1 gap-2">
<p class="hint text-xs">Tap a chip to choose colors. “Replace” shows only colors used in this wall.</p>
<select id="wall-replace-from" class="sr-only"></select>
<select id="wall-replace-to" class="sr-only"></select>
<button type="button" id="wall-replace-btn" class="btn-dark text-sm">Replace</button>
<div id="wall-replace-msg" class="text-xs text-gray-500"></div>
</div>
</div>
</div>
<div class="control-stack" data-mobile-tab="save">
<div class="panel-heading">Save & Export</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">Exports the current wall view.</p>
</div>
<div>
<div class="panel-heading text-sm">Quick Paint (uses active color)</div>
<div class="grid grid-cols-2 gap-2 mt-2">
<button type="button" id="wall-paint-links" class="btn-blue text-xs px-2 py-2">Paint Links</button>
<button type="button" id="wall-paint-small" class="btn-blue text-xs px-2 py-2">Paint 5&quot; Nodes</button>
<button type="button" id="wall-paint-gaps" class="btn-blue text-xs px-2 py-2">Paint 11&quot; Gaps</button>
</div>
<p class="hint mt-2">Fills only that group; pattern-aware for Grid vs X.</p>
</div>
<div class="flex flex-wrap gap-3">
<button type="button" id="wall-clear" class="btn-danger text-sm px-3 py-2 flex-1">Clear</button>
<button type="button" id="wall-fill-all" class="btn-blue text-sm px-3 py-2 flex-1">Fill All</button>
</div>
</div>
</div>
</aside>
<section id="wall-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">
<div class="flex items-center justify-between px-4 py-3 border-b border-gray-200 bg-white/70">
<div class="text-base font-semibold text-slate-700">Balloon Wall</div>
<div class="text-sm text-gray-500">Columns/Rows: <span id="wall-grid-label">9 × 7</span></div>
</div>
<div id="wall-display" class="flex-1 bg-white relative overflow-auto">
<div class="p-6 text-gray-500 text-sm">Wall designer will load here.</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>
<!-- Mobile sticky action bar -->
<div id="mobile-action-bar" class="mobile-action-bar hidden">
<div class="mobile-action-chip" id="mobile-active-color-chip" title="Active color"></div>
<div class="mobile-action-row">
<button type="button" class="mobile-action-btn" id="mobile-act-undo" aria-label="Undo">
<i class="fa-solid fa-rotate-left" aria-hidden="true"></i>
<span>Undo</span>
</button>
<button type="button" class="mobile-action-btn" id="mobile-act-redo" aria-label="Redo">
<i class="fa-solid fa-rotate-right" aria-hidden="true"></i>
<span>Redo</span>
</button>
<button type="button" class="mobile-action-btn" id="mobile-act-eyedrop" aria-label="Eyedropper">
<i class="fa-solid fa-eye-dropper" aria-hidden="true"></i>
<span>Pick</span>
</button>
<button type="button" class="mobile-action-btn" id="mobile-act-erase" aria-label="Toggle Erase">
<i class="fa-solid fa-eraser" aria-hidden="true"></i>
<span>Erase</span>
</button>
<button type="button" class="mobile-action-btn danger" id="mobile-act-clear" aria-label="Clear canvas">
<i class="fa-solid fa-trash" aria-hidden="true"></i>
<span>Clear</span>
</button>
<button type="button" class="mobile-action-btn" id="mobile-act-export" aria-label="Export PNG">
<i class="fa-solid fa-download" aria-hidden="true"></i>
<span>Export</span>
</button>
</div>
</div>
<!-- Color picker modal -->
<div id="color-picker-modal" class="color-modal hidden" role="dialog" aria-modal="true" aria-labelledby="color-picker-title">
<div class="color-modal-backdrop"></div>
<div class="color-modal-card">
<div class="color-modal-header">
<div>
<div id="color-picker-title" class="color-modal-title">Choose a color</div>
<div id="color-picker-subtitle" class="color-modal-subtitle"></div>
</div>
<button type="button" id="color-picker-close" class="color-modal-close" aria-label="Close">&times;</button>
</div>
<div id="color-picker-grid" class="color-modal-grid"></div>
</div>
</div>
<!-- Export modal -->
<div id="export-modal" class="color-modal hidden" role="dialog" aria-modal="true" aria-labelledby="export-modal-title">
<div class="color-modal-backdrop"></div>
<div class="color-modal-card">
<div class="color-modal-header">
<div>
<div id="export-modal-title" class="color-modal-title">Export design</div>
<div class="color-modal-subtitle">Choose a format to download</div>
</div>
<button type="button" id="export-modal-close" class="color-modal-close" aria-label="Close">&times;</button>
</div>
<div class="flex flex-col sm:flex-row gap-3">
<button type="button" class="btn-blue flex-1" data-export-choice="png">Export PNG</button>
<button type="button" class="btn-dark flex-1" data-export-choice="svg">Export SVG</button>
</div>
<p class="hint mt-2 text-xs text-slate-500">SVG keeps vector shapes where possible (textures/images stay raster). PNG renders a high-res snapshot.</p>
</div>
</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>
<!-- Palette must load before shared.js; it is already included in the <head>. -->
<script src="shared.js" defer></script>
<script src="script.js" defer></script>
<script src="organic.js" defer></script>
<script src="wall.js" defer></script>
<script src="classic.js" defer></script>
</body>
</html>