561 lines
35 KiB
HTML
561 lines
35 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://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 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">
|
||
<div class="flex items-center gap-1 px-2 py-1 rounded-xl bg-white/70 border border-gray-200 shadow-sm" title="Active Color">
|
||
<div id="current-color-chip-global" class="current-color-chip">
|
||
<span id="current-color-label-global" class="text-[10px] font-semibold text-slate-700"></span>
|
||
</div>
|
||
</div>
|
||
<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" 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>
|
||
<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="grid grid-cols-1 gap-2">
|
||
<div class="flex items-center gap-2">
|
||
<label for="garland-color-main1" class="font-medium w-24">Main A</label>
|
||
<select id="garland-color-main1" class="select text-sm flex-1"></select>
|
||
<span id="garland-swatch-main1" class="swatch tiny"></span>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<label for="garland-color-main2" class="font-medium w-24">Main B</label>
|
||
<select id="garland-color-main2" class="select text-sm flex-1"></select>
|
||
<span id="garland-swatch-main2" class="swatch tiny"></span>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<label for="garland-color-main3" class="font-medium w-24">Main C</label>
|
||
<select id="garland-color-main3" class="select text-sm flex-1"></select>
|
||
<span id="garland-swatch-main3" class="swatch tiny"></span>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<label for="garland-color-main4" class="font-medium w-24">Main D</label>
|
||
<select id="garland-color-main4" class="select text-sm flex-1"></select>
|
||
<span id="garland-swatch-main4" class="swatch tiny"></span>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<label for="garland-color-accent" class="font-medium w-24">5" Accent</label>
|
||
<select id="garland-color-accent" class="select text-sm flex-1"></select>
|
||
<span id="garland-swatch-accent" class="swatch tiny"></span>
|
||
</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 keyboard arrows 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">Alt+click on canvas to sample a balloon’s color.</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">
|
||
<div class="grid grid-cols-1 gap-2">
|
||
<label class="text-sm font-medium">From (in design):</label>
|
||
<select id="replace-from" class="select"></select>
|
||
|
||
<label class="text-sm font-medium">To (library):</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 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>
|
||
<div class="text-sm font-medium flex flex-col gap-1 col-span-2">
|
||
<span>Spacing</span>
|
||
<span class="text-xs text-gray-500" id="wall-spacing-label">75 px (fixed)</span>
|
||
</div>
|
||
<div class="text-sm font-medium flex flex-col gap-1 col-span-2">
|
||
<span>Balloon Size</span>
|
||
<span class="text-xs text-gray-500" id="wall-size-label">52 px (fixed)</span>
|
||
</div>
|
||
<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">
|
||
Outline balloons
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="control-stack" data-mobile-tab="colors">
|
||
<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 grid grid-cols-1 gap-2">
|
||
<label class="text-sm font-medium flex flex-col gap-1">
|
||
Replace
|
||
<select id="wall-replace-from" class="select text-sm"></select>
|
||
</label>
|
||
<label class="text-sm font-medium flex flex-col gap-1">
|
||
With
|
||
<select id="wall-replace-to" class="select text-sm"></select>
|
||
</label>
|
||
<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 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" Nodes</button>
|
||
<button type="button" id="wall-paint-gaps" class="btn-blue text-xs px-2 py-2">Paint 11" 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>
|
||
|
||
<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>
|