Compare commits

..

No commits in common. "4b9fee0a3ec64b4da05e3957dbe5c3f67ceec161" and "f22319737eadade20ad9b12d71c8c4699e7920c6" have entirely different histories.

19 changed files with 2450 additions and 7093 deletions

1815
classic.js

File diff suppressed because it is too large Load Diff

View File

@ -12,8 +12,7 @@ const PALETTE = [
]},
{ family: "Oranges & Browns & Yellows", colors: [
{name:"Pastel Yellow",hex:"#fcfd96"},{name:"Yellow",hex:"#f5e812"},{name:"Goldenrod",hex:"#f7b615"},
{name:"Orange",hex:"#ef6b24"},{name:"Coffee",hex:"#957461"},{name:"Burnt Orange",hex:"#9d4223"},
{name:"Blended Brown",hex:"#c9aea0"}
{name:"Orange",hex:"#ef6b24"},{name:"Coffee",hex:"#957461"},{name:"Burnt Orange",hex:"#9d4223"}
]},
{ family: "Greens", colors: [
{name:"Eucalyptus",hex:"#a3bba3"},{name:"Pastel Green",hex:"#acdba7"},{name:"Lime Green",hex:"#8fc73e"},
@ -64,4 +63,4 @@ const PALETTE = [
];
window.CLASSIC_COLORS = ['#D92E3A', '#FFFFFF', '#0055A4', '#40E0D0'];
window.PALETTE = window.PALETTE || (typeof PALETTE !== "undefined" ? PALETTE : []);
window.PALETTE = window.PALETTE || (typeof PALETTE !== "undefined" ? PALETTE : []);

View File

@ -24,9 +24,9 @@
</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(97vh-2rem)] overflow-hidden ring-1 ring-black/5">
<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">
<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>
@ -37,10 +37,14 @@
<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</button>
<button type="button" class="tab-btn tab-idle" data-target="#tab-wall" aria-pressed="false">Wall</button>
<button type="button" class="tab-btn tab-idle" data-target="#tab-classic" aria-pressed="false">Classic (Arch/Column)</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>
@ -74,11 +78,11 @@
</button>
</div>
<div class="grid grid-cols-3 gap-2 mb-3">
<button id="tool-undo" class="tool-btn" aria-label="Undo">
<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" aria-label="Redo">
<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>
@ -94,17 +98,31 @@
<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="grid grid-cols-1 gap-2">
<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>
<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&quot; 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>
@ -119,7 +137,7 @@
<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>
<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>
@ -155,44 +173,38 @@
</div>
<div class="control-stack" data-mobile-tab="colors">
<div class="panel-heading">Organic Colors</div>
<div class="panel-card space-y-4">
<div class="space-y-2">
<div class="flex items-center gap-3">
<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 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 balloons 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="space-y-2">
<div class="flex items-center justify-between">
<span class="text-sm font-semibold text-gray-700">Project Palette</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">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>
<div class="space-y-2">
<div class="text-sm font-semibold text-gray-700">Replace Color</div>
<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">
<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>
<label class="text-sm font-medium">To (library):</label>
<select id="replace-to" class="select"></select>
<div class="space-y-2">
<div class="text-sm font-semibold text-gray-700">Color Library</div>
<div id="color-palette" class="palette-box"></div>
<button id="replace-btn" class="btn-blue">Replace</button>
<p id="replace-msg" class="hint"></p>
</div>
</div>
</div>
@ -254,24 +266,9 @@
</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 flex-wrap">
<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>
<button type="button" class="tab-btn tab-idle" id="classic-manual-btn" aria-pressed="false">Manual paint</button>
</div>
<div id="classic-expanded-row" class="flex items-center gap-2 hidden">
<label class="text-sm inline-flex items-center gap-2 font-medium">
<input id="classic-expanded-toggle" type="checkbox" class="align-middle" checked>
Expanded spacing
</label>
<p class="hint m-0">Separate clusters for easier taps.</p>
</div>
<div id="classic-focus-row" class="flex items-center gap-2 hidden">
<button type="button" class="btn-dark text-xs px-3 py-2 hidden" id="classic-focus-prev" aria-hidden="true" tabindex="-1">◀ Prev</button>
<span id="classic-focus-label" class="text-sm text-gray-700">Clusters 18</span>
<button type="button" class="btn-dark text-xs px-3 py-2 hidden" id="classic-focus-next" aria-hidden="true" tabindex="-1">Next ▶</button>
<button type="button" class="btn-dark text-xs px-3 py-2 hidden" id="classic-focus-zoomout" aria-hidden="true" tabindex="-1">Zoom Out</button>
<button type="button" class="btn-dark text-xs px-3 py-2 hidden" id="classic-quad-reset" aria-hidden="true" tabindex="-1">Reset Quad</button>
</div>
</div>
<select id="classic-pattern" class="select align-middle hidden" aria-hidden="true" tabindex="-1">
@ -292,7 +289,7 @@
<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
Add Topper (24")
</label>
</div>
@ -320,6 +317,18 @@
</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>
@ -350,106 +359,49 @@
<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="flex items-center gap-3 mb-2">
<span class="text-sm text-gray-700 classic-label">Active color</span>
<div id="classic-active-chip" class="current-color-chip cursor-pointer" title="Tap to scroll to palette">
<span id="classic-active-label" class="text-xs font-semibold text-slate-700"></span>
<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>
<div id="classic-topper-color-block" class="mb-3 hidden">
<div class="text-sm text-gray-700 classic-label">Topper Color</div>
<div class="flex items-center gap-3 mt-1">
<button id="classic-topper-color-swatch" class="slot-swatch" title="Click to change topper color">T</button>
<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>
<div id="classic-project-block" class="space-y-1">
<div class="text-sm text-gray-700 classic-label">Project Palette</div>
<div id="classic-project-palette" class="palette-box min-h-[2.4rem]"></div>
</div>
<div id="classic-replace-block" class="mt-2">
<div class="text-sm text-gray-700 classic-label">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="classic-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="classic-replace-to-chip" aria-label="Pick replacement color"></button>
<span id="classic-replace-count" class="text-xs text-slate-500 ml-auto"></span>
</div>
<div class="grid grid-cols-1 gap-2">
<select id="classic-replace-from" class="sr-only"></select>
<select id="classic-replace-to" class="sr-only"></select>
<button id="classic-replace-btn" class="btn-blue">Replace</button>
<p id="classic-replace-msg" class="hint"></p>
<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 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>
</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 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>
<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 grid grid-rows-[1fr] lg:grid-rows-[minmax(0,1fr)] gap-2 shadow-x3 rounded-2xl overflow-hidden bg-white">
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="classic-mobile-bar" class="mobile-action-bar hidden">
<div class="mobile-action-chip" id="classic-active-chip-floating" title="Active">
<span class="color-dot" id="classic-active-dot-floating"></span>
</div>
<div class="mobile-action-row">
<button type="button" class="mobile-action-btn" id="classic-undo-manual" 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="classic-redo-manual" 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="classic-pick-manual" aria-label="Eyedropper" aria-pressed="false">
<i class="fa-solid fa-eye-dropper" aria-hidden="true"></i>
<span>Pick</span>
</button>
<button type="button" class="mobile-action-btn" id="classic-erase-manual" aria-label="Toggle Erase" aria-pressed="false">
<i class="fa-solid fa-eraser" aria-hidden="true"></i>
<span>Erase</span>
</button>
<button type="button" class="mobile-action-btn danger" id="classic-clear-manual" aria-label="Clear">
<i class="fa-solid fa-trash" aria-hidden="true"></i>
<span>Clear</span>
</button>
<button type="button" class="mobile-action-btn" id="classic-export-manual" aria-label="Export">
<i class="fa-solid fa-download" aria-hidden="true"></i>
<span>Export</span>
</button>
</div>
</div>
<div id="floating-topper-nudge" class="floating-nudge hidden">
<div class="floating-nudge-header">
<div class="panel-heading">Nudge & Size Topper</div>
<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 space-y-3">
<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>
@ -461,140 +413,11 @@
<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 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>
</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">Wall Colors</div>
<div class="panel-card space-y-4">
<div class="space-y-2">
<div class="flex items-center gap-3">
<span class="text-sm font-medium text-gray-700">Active color</span>
<div id="wall-active-color-chip" class="current-color-chip">
<span id="wall-active-color-label" class="text-xs font-semibold text-slate-700"></span>
</div>
</div>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-sm font-semibold text-gray-700">Project Palette</span>
</div>
<div id="wall-used-palette" class="palette-box min-h-[2.4rem]"></div>
</div>
<div class="space-y-2">
<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">
<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 class="space-y-2">
<div class="text-sm font-semibold text-gray-700">Color Library</div>
<div id="wall-palette" class="palette-box min-h-[3rem]"></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"
style="height:92%;">
<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">
@ -612,71 +435,6 @@
</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>
@ -686,25 +444,9 @@
<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>
<div id="classic-quad-modal" class="quad-modal hidden" aria-hidden="true">
<div class="quad-modal-backdrop"></div>
<div class="quad-modal-panel" role="dialog" aria-modal="true" aria-label="Quad detail">
<div class="quad-modal-header">
<div class="quad-modal-title">Quad Detail</div>
<button type="button" id="classic-quad-modal-close" class="btn-dark text-xs px-3 py-2">Close</button>
</div>
<div class="quad-modal-body">
<div id="classic-quad-modal-display" class="quad-modal-display"></div>
</div>
</div>
</div>
</body>
</html>

2166
organic.js

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 27 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 28 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 23 KiB

2552
script.js

File diff suppressed because it is too large Load Diff

233
shared.js
View File

@ -1,233 +0,0 @@
(() => {
'use strict';
// Shared helpers & palette flattening
document.addEventListener('DOMContentLoaded', () => {
const PX_PER_INCH = 4;
const SIZE_PRESETS = [24, 18, 11, 9, 5];
const TEXTURE_ZOOM_DEFAULT = 1.8;
const TEXTURE_FOCUS_DEFAULT = { x: 0.5, y: 0.5 };
const SWATCH_TEXTURE_ZOOM = 2.5;
const PNG_EXPORT_SCALE = 3;
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const clamp01 = v => clamp(v, 0, 1);
const normalizeHex = h => (h || '').toLowerCase();
const ACTIVE_COLOR_KEY = 'app:activeColor:v1';
let ACTIVE_COLOR_CACHE = null;
function hexToRgb(hex) {
const h = normalizeHex(hex).replace('#','');
if (h.length === 3) {
const r = parseInt(h[0] + h[0], 16);
const g = parseInt(h[1] + h[1], 16);
const b = parseInt(h[2] + h[2], 16);
return { r, g, b };
}
if (h.length === 6) {
const r = parseInt(h.slice(0,2), 16);
const g = parseInt(h.slice(2,4), 16);
const b = parseInt(h.slice(4,6), 16);
return { r, g, b };
}
return { r: 0, g: 0, b: 0 };
}
function getActiveColor() {
if (ACTIVE_COLOR_CACHE) return ACTIVE_COLOR_CACHE;
try {
const saved = JSON.parse(localStorage.getItem(ACTIVE_COLOR_KEY));
if (saved && saved.hex) {
ACTIVE_COLOR_CACHE = { hex: normalizeHex(saved.hex), image: saved.image || null };
return ACTIVE_COLOR_CACHE;
}
} catch {}
ACTIVE_COLOR_CACHE = { hex: '#ff6b6b', image: null };
return ACTIVE_COLOR_CACHE;
}
function setActiveColor(color) {
const clean = { hex: normalizeHex(color?.hex || '#ffffff'), image: color?.image || null };
ACTIVE_COLOR_CACHE = clean;
try { localStorage.setItem(ACTIVE_COLOR_KEY, JSON.stringify(clean)); } catch {}
return clean;
}
function luminance(hex) {
const { r, g, b } = hexToRgb(hex || '#000');
const norm = [r,g,b].map(v => {
const c = v / 255;
return c <= 0.03928 ? c/12.92 : Math.pow((c+0.055)/1.055, 2.4);
});
return 0.2126*norm[0] + 0.7152*norm[1] + 0.0722*norm[2];
}
function shineStyle(colorHex) {
const hex = normalizeHex(colorHex);
const isRetroWhite = hex === '#e8e3d9';
const isPureWhite = hex === '#ffffff';
const lum = luminance(hex);
if (isPureWhite || isRetroWhite) {
return { fill: 'rgba(220,220,220,0.22)', stroke: null };
}
if (lum > 0.7) {
const t = clamp01((lum - 0.7) / 0.3);
const fillAlpha = 0.08 + (0.04 - 0.08) * t;
return { fill: `rgba(0,0,0,${fillAlpha})`, stroke: null };
}
const base = 0.20;
const softened = lum > 0.4 ? base * 0.7 : base;
const finalAlpha = isRetroWhite ? softened * 0.6 : softened;
return { fill: `rgba(255,255,255,${finalAlpha})`, stroke: null };
}
const FLAT_COLORS = [];
const NAME_BY_HEX = new Map();
const HEX_TO_FIRST_IDX = new Map();
const allowedSet = new Set();
(function buildFlat() {
if (!Array.isArray(window.PALETTE)) return;
window.PALETTE.forEach(group => {
(group.colors || []).forEach(c => {
if (!c?.hex) return;
const item = { ...c, family: group.family };
item.imageZoom = Number.isFinite(c.imageZoom) ? Math.max(1, c.imageZoom) : TEXTURE_ZOOM_DEFAULT;
item.imageFocus = {
x: clamp01(c.imageFocusX ?? c.imageFocus?.x ?? TEXTURE_FOCUS_DEFAULT.x),
y: clamp01(c.imageFocusY ?? c.imageFocus?.y ?? TEXTURE_FOCUS_DEFAULT.y)
};
item._idx = FLAT_COLORS.length;
FLAT_COLORS.push(item);
const key = (c.hex || '').toLowerCase();
if (!NAME_BY_HEX.has(key)) NAME_BY_HEX.set(key, c.name);
if (!HEX_TO_FIRST_IDX.has(key)) HEX_TO_FIRST_IDX.set(key, item._idx);
allowedSet.add(key);
});
});
})();
const IMG_CACHE = new Map();
function getImage(path, onLoad) {
if (!path) return null;
let img = IMG_CACHE.get(path);
if (!img) {
img = new Image();
// Avoid CORS issues on file:// by only setting crossOrigin for http/https
const href = (() => { try { return new URL(path, window.location.href); } catch { return null; } })();
const isFile = href?.protocol === 'file:' || window.location.protocol === 'file:';
if (!isFile) img.crossOrigin = 'anonymous';
img.decoding = 'async';
img.loading = 'eager';
img.src = path;
if (onLoad) img.onload = onLoad;
IMG_CACHE.set(path, img);
}
return img;
}
const DATA_URL_CACHE = new Map();
const XLINK_NS = 'http://www.w3.org/1999/xlink';
const blobToDataUrl = blob => new Promise((resolve, reject) => {
const r = new FileReader();
r.onloadend = () => resolve(r.result);
r.onerror = reject;
r.readAsDataURL(blob);
});
function imageToDataUrl(img) {
if (!img || !img.complete || img.naturalWidth === 0) return null;
// On file:// origins, drawing may be blocked; return null to fall back to original href.
if (window.location.protocol === 'file:') return null;
try {
const c = document.createElement('canvas');
c.width = img.naturalWidth;
c.height = img.naturalHeight;
c.getContext('2d').drawImage(img, 0, 0);
return c.toDataURL('image/png');
} catch (err) {
console.warn('[Export] imageToDataUrl failed:', err);
return null;
}
}
async function imageUrlToDataUrl(src) {
if (!src || src.startsWith('data:')) return src;
if (DATA_URL_CACHE.has(src)) return DATA_URL_CACHE.get(src);
const abs = (() => { try { return new URL(src, window.location.href).href; } catch { return src; } })();
const urlObj = (() => { try { return new URL(abs); } catch { return null; } })();
const isFile = urlObj?.protocol === 'file:';
const cachedImg = IMG_CACHE.get(src);
const cachedUrl = imageToDataUrl(cachedImg);
if (cachedUrl) { DATA_URL_CACHE.set(src, cachedUrl); return cachedUrl; }
let dataUrl = null;
try {
if (isFile) {
// On file:// we cannot safely read pixels; return the original path.
dataUrl = src;
} else {
const resp = await fetch(abs);
if (!resp.ok) throw new Error(`Status ${resp.status}`);
dataUrl = await blobToDataUrl(await resp.blob());
}
} catch (err) {
if (!isFile) console.warn('[Export] Fetch failed for', abs, err);
dataUrl = await new Promise(resolve => {
const img = new Image();
if (!isFile) img.crossOrigin = 'anonymous';
img.onload = () => {
try {
const c = document.createElement('canvas');
c.width = img.naturalWidth || 1;
c.height = img.naturalHeight || 1;
c.getContext('2d').drawImage(img, 0, 0);
resolve(c.toDataURL('image/png'));
} catch (e) {
console.error('[Export] Canvas fallback failed for', abs, e);
resolve(null);
}
};
img.onerror = () => resolve(null);
img.src = abs;
});
}
if (!dataUrl) dataUrl = abs;
DATA_URL_CACHE.set(src, dataUrl);
return dataUrl;
}
function download(href, suggestedFilename) {
const a = document.createElement('a');
a.href = href;
a.download = suggestedFilename || 'download';
a.rel = 'noopener';
document.body.appendChild(a);
a.click();
a.remove();
}
window.shared = {
PX_PER_INCH,
SIZE_PRESETS,
TEXTURE_ZOOM_DEFAULT,
TEXTURE_FOCUS_DEFAULT,
SWATCH_TEXTURE_ZOOM,
PNG_EXPORT_SCALE,
clamp,
clamp01,
normalizeHex,
hexToRgb,
shineStyle,
luminance,
FLAT_COLORS,
NAME_BY_HEX,
HEX_TO_FIRST_IDX,
allowedSet,
getImage,
DATA_URL_CACHE,
XLINK_NS,
blobToDataUrl,
imageToDataUrl,
imageUrlToDataUrl,
download,
getActiveColor,
setActiveColor
};
});
})();

407
style.css
View File

@ -1,43 +1,7 @@
/* Minimal extras (Tailwind handles most styling) */
body { color: #1f2937; }
body[data-active-tab="#tab-classic"] #clear-canvas-btn-top,
body[data-active-tab="#tab-wall"] #clear-canvas-btn-top {
display: none !important;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
#app-header {
position: sticky;
top: 0;
z-index: 35;
background: rgba(255,255,255,0.92);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
padding: .75rem 0.5rem;
border-radius: 1.25rem;
box-shadow: 0 8px 24px rgba(15,23,42,0.08);
}
#balloon-canvas { touch-action: none;
height: 95%}
.classic-expanded-canvas {
height: 130vh !important;
min-height: 130vh;
overflow: auto;
}
#balloon-canvas { touch-action: none; }
.balloon-canvas {
background: #fff;
@ -48,31 +12,6 @@ height: 95%}
}
/* Buttons */
.btn-dark,
.btn-blue,
.btn-green,
.btn-yellow,
.btn-danger,
.btn-indigo {
display: inline-flex;
align-items: center;
justify-content: center;
gap: .35rem;
font-weight: 700;
letter-spacing: -0.01em;
border: 0;
font-size: 0.95rem;
}
.btn-dark:focus-visible,
.btn-blue:focus-visible,
.btn-green:focus-visible,
.btn-yellow:focus-visible,
.btn-danger:focus-visible,
.btn-indigo:focus-visible,
.tool-btn:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
.tool-btn {
display: flex;
align-items: center;
@ -128,14 +67,12 @@ height: 95%}
flex-direction: column;
gap: .5rem;
padding: .5rem;
background: rgba(255,255,255,0.82);
border: 1px solid rgba(226,232,240,0.9);
border-radius: .9rem;
background: rgba(255,255,255,0.6); /* More transparent */
border: 1px solid #e5e7eb;
border-radius: .75rem;
max-height: 260px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.05);
touch-action: pan-y;
}
.swatch {
@ -172,8 +109,6 @@ height: 95%}
.swatch-row { display:flex; flex-wrap:wrap; gap:.5rem; }
.family-title { font-weight:700; color:#334155; margin-top:.25rem; font-size:.9rem; letter-spacing: -0.01em; }
#wall-display { min-height: 60vh; }
#wall-display svg { width: 100%; height: 100%; display: block; }
.badge {
position:absolute;
@ -252,34 +187,6 @@ height: 95%}
}
.slot-swatch.active::after { display: none; }
.replace-row {
padding: 0.35rem;
background: rgba(248,250,252,0.9);
border: 1px solid rgba(226,232,240,0.9);
border-radius: 12px;
}
.replace-chip {
width: 40px;
height: 40px;
border-radius: 12px;
border: 2px solid rgba(51,65,85,0.18);
box-shadow: 0 3px 8px rgba(0,0,0,0.06);
background-size: cover;
background-position: center;
background-color: #fff;
cursor: pointer;
}
.wall-toolbar {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.5rem;
}
.wall-toolbar .tool-btn {
width: 100%;
justify-content: center;
}
.topper-type-group {
display: flex;
gap: .5rem;
@ -365,13 +272,13 @@ height: 95%}
letter-spacing: -0.02em;
}
.panel-card {
background: rgba(255,255,255,0.82);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border: 1px solid rgba(226,232,240,0.9);
background: rgba(255,255,255,0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255,255,255,0.6);
border-radius: 1rem;
padding: 1rem;
box-shadow: 0 12px 30px rgba(15,23,42,0.06);
box-shadow: 0 4px 20px rgba(0,0,0,0.03);
}
.control-stack {
display: flex;
@ -396,7 +303,6 @@ height: 95%}
z-index: 30;
-webkit-overflow-scrolling: touch;
transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1);
height: 92%;
}
.control-sheet.hidden { display: none; }
.control-sheet.minimized { transform: translateY(100%); }
@ -432,243 +338,18 @@ height: 92%;
@media (max-width: 1023px) {
body { padding-bottom: 0; overflow: auto; }
html, body { height: auto; overflow: auto; }
#current-color-chip-global { display: none; }
#clear-canvas-btn-top { display: none !important; }
/* Add breathing room under canvases so sheets/tabbar dont cover content */
#classic-display,
#wall-display,
#balloon-canvas {
margin-bottom: 6.5rem;
}
/* Stack switching: show only the active mobile tab stack across panels */
.control-sheet .control-stack { display: none; }
body[data-mobile-tab="controls"] #controls-panel [data-mobile-tab="controls"],
body[data-mobile-tab="colors"] #controls-panel [data-mobile-tab="colors"],
body[data-mobile-tab="save"] #controls-panel [data-mobile-tab="save"],
body[data-mobile-tab="colors"] #controls-panel [data-mobile-tab="colors"],
body[data-mobile-tab="save"] #controls-panel [data-mobile-tab="save"],
body[data-mobile-tab="controls"] #classic-controls-panel [data-mobile-tab="controls"],
body[data-mobile-tab="colors"] #classic-controls-panel [data-mobile-tab="colors"],
body[data-mobile-tab="save"] #classic-controls-panel [data-mobile-tab="save"],
body[data-mobile-tab="controls"] #wall-controls-panel [data-mobile-tab="controls"],
body[data-mobile-tab="colors"] #wall-controls-panel [data-mobile-tab="colors"],
body[data-mobile-tab="save"] #wall-controls-panel [data-mobile-tab="save"] {
body[data-mobile-tab="colors"] #classic-controls-panel [data-mobile-tab="colors"],
body[data-mobile-tab="save"] #classic-controls-panel [data-mobile-tab="save"] {
display: block;
}
.control-sheet { bottom: 4.5rem; max-height: 55vh; }
.control-sheet.minimized { transform: translateY(115%); }
/* Larger tap targets and spacing */
.tool-btn,
.btn-dark,
.btn-blue,
.btn-green,
.btn-yellow,
.btn-danger,
.btn-indigo {
min-height: 44px;
padding: 0.75rem 0.85rem;
font-size: 1rem;
}
.swatch { width: 2.4rem; height: 2.4rem; }
.swatch.tiny { width: 1.8rem; height: 1.8rem; }
.select { min-height: 44px; }
.panel-card { padding: 0.85rem; }
.manual-hub { position: sticky; top: 0; z-index: 6; }
.manual-detail-stage { min-height: 260px; }
}
.mobile-action-bar {
position: fixed;
left: 0;
right: 0;
bottom: 4.75rem;
padding: 0.35rem 0.75rem 0.7rem;
background: linear-gradient(180deg, rgba(255,255,255,0.72) 0%, rgba(255,255,255,0.96) 100%);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
border-top: 1px solid rgba(226,232,240,0.9);
box-shadow: 0 -10px 30px rgba(15,23,42,0.08);
display: flex;
align-items: center;
gap: 0.5rem;
z-index: 20; /* below control sheets (30) and modals (60) */
}
.color-modal {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 60;
}
.color-modal.hidden { display: none; }
.color-modal-backdrop {
position: absolute;
inset: 0;
background: rgba(15,23,42,0.35);
backdrop-filter: blur(4px);
}
.color-modal-card {
position: relative;
width: min(640px, 92vw);
max-height: 80vh;
background: #fff;
border-radius: 1.25rem;
padding: 1.1rem 1.1rem 1.25rem;
box-shadow: 0 24px 60px rgba(15,23,42,0.2);
display: flex;
flex-direction: column;
gap: 0.75rem;
z-index: 1;
}
.color-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.color-modal-title { font-size: 1.1rem; font-weight: 800; color: #0f172a; letter-spacing: -0.01em; }
.color-modal-subtitle { font-size: 0.9rem; color: #475569; }
.color-modal-close {
background: #e2e8f0;
border: none;
width: 36px;
height: 36px;
border-radius: 12px;
font-size: 1.4rem;
color: #0f172a;
}
.color-modal-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
gap: 0.75rem;
overflow-y: auto;
padding: 0.25rem;
}
.color-option {
border: 1px solid rgba(226,232,240,0.9);
border-radius: 12px;
padding: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.35rem;
align-items: center;
justify-content: center;
cursor: pointer;
background: #fff;
box-shadow: 0 4px 12px rgba(15,23,42,0.05);
transition: transform 0.1s ease;
}
.color-option:hover { transform: translateY(-1px); }
.color-option .swatch {
width: 2.4rem;
height: 2.4rem;
border-width: 2px;
}
.color-option .label {
font-size: 0.78rem;
font-weight: 700;
color: #0f172a;
text-align: center;
line-height: 1.1;
}
.color-option .meta {
font-size: 0.72rem;
color: #475569;
text-align: center;
}
.mobile-action-bar.hidden { display: none; }
.mobile-action-chip {
width: 44px;
height: 44px;
border-radius: 14px;
border: 2px solid rgba(51,65,85,0.18);
box-shadow: 0 4px 10px rgba(0,0,0,0.08);
background-size: cover;
background-position: center;
background-color: #fff;
}
.mobile-action-row {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 0.35rem;
width: 100%;
}
.mobile-action-btn {
background: rgba(255,255,255,0.92);
border: 1px solid rgba(226,232,240,0.9);
border-radius: 14px;
padding: 0.55rem 0.35rem;
font-size: 0.9rem;
font-weight: 700;
color: #0f172a;
box-shadow: 0 4px 14px rgba(15,23,42,0.08);
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.15rem;
}
.mobile-action-btn i { font-size: 1rem; }
.mobile-action-btn.danger { color: #dc2626; border-color: rgba(248,113,113,0.35); }
.mobile-action-btn:active { transform: translateY(1px); }
.mobile-action-btn.active {
border-color: #2563eb;
box-shadow: 0 0 0 2px rgba(37,99,235,0.18), 0 6px 16px rgba(37,99,235,0.2);
}
.manual-hub {
background: linear-gradient(135deg, rgba(255,255,255,0.9), rgba(240,249,255,0.92));
border: 1px solid rgba(226,232,240,0.9);
border-radius: 1rem;
padding: 0.9rem 1rem;
box-shadow: 0 12px 28px rgba(15,23,42,0.08);
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.manual-hub-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.75rem;
}
.manual-hub-title { font-weight: 800; color: #0f172a; letter-spacing: -0.015em; }
.manual-hub-subtitle { font-size: 0.85rem; color: #475569; line-height: 1.3; }
.manual-hub-actions { display: flex; gap: 0.35rem; }
.manual-hub-track {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 0.55rem;
}
.manual-hub-count { grid-column: 1 / -1; text-align: right; }
.manual-range {
width: 100%;
accent-color: #2563eb;
}
.manual-detail {
display: none;
}
.manual-detail-stage { display: none; }
.manual-detail-empty { display: none; }
.manual-hub-hint { line-height: 1.4; }
.chip-btn {
background: rgba(255,255,255,0.85);
border: 1px solid rgba(148,163,184,0.4);
border-radius: 999px;
padding: 0.45rem 0.75rem;
font-weight: 700;
font-size: 0.85rem;
color: #0f172a;
box-shadow: 0 6px 16px rgba(15,23,42,0.08);
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.chip-btn:active { transform: translateY(1px); }
.chip-btn:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.mobile-tabbar {
position: fixed;
inset-inline: 0;
@ -686,32 +367,6 @@ height: 92%;
box-shadow: 0 -6px 30px rgba(15, 23, 42, 0.12);
border-top: 1px solid rgba(148, 163, 184, 0.25);
}
.mobile-tabbar.hidden { display: none; }
@media (min-width: 1024px) {
.mobile-tabbar { display: none !important; }
}
@media (max-width: 1023px) {
/* Tuck canvases above the tabbar */
#classic-display,
#wall-display,
#balloon-canvas {
margin-bottom: 0;
height: calc(100vh - 190px) !important; /* tie to viewport minus header/controls */
max-height: calc(100vh - 190px) !important;
}
#classic-display{
height: 92%;
}
/* Keep the main canvas panels above the tabbar/action bar */
#canvas-panel,
#classic-canvas-panel {
padding-bottom: 12vh;
}
#classic-canvas-panel {
padding-bottom: 14vh; /* leave space for action bar */
}
}
.mobile-tabbar .mobile-tab-btn {
flex: 1 1 0;
display: flex;
@ -744,39 +399,6 @@ height: 92%;
transform: translateY(-2px);
}
.canvas-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: .75rem;
padding: .6rem .85rem;
border-bottom: 1px solid #e5e7eb;
background: linear-gradient(90deg, #f8fafc, #fff);
position: sticky;
top: 0;
z-index: 5;
}
.canvas-toolbar .toolbar-left,
.canvas-toolbar .toolbar-right {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: .5rem;
}
.canvas-toolbar button { white-space: nowrap; }
.quad-modal{position:fixed;inset:0;z-index:999;display:flex;align-items:center;justify-content:center;pointer-events:none;}
.quad-modal.hidden{display:none;}
.quad-modal:not(.hidden){pointer-events:auto;}
.quad-modal-backdrop{position:absolute;inset:0;background:rgba(15,23,42,0.45);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);}
.quad-modal-panel{position:relative;z-index:1;pointer-events:auto;background:#fff;border-radius:1rem;padding:1rem;box-shadow:0 22px 50px rgba(15,23,42,0.25);width:min(560px,90vw);}
.quad-modal-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:.5rem;}
.quad-modal-title{font-weight:700;font-size:1rem;color:#0f172a;}
.quad-modal-body{border:1px solid #e5e7eb;border-radius:.75rem;overflow:hidden;background:#f8fafc;}
.quad-modal-display{width:100%;height:360px;max-height:70vh;position:relative;background:#fff;perspective:1200px;}
.quad-modal-display svg{transform-style:preserve-3d;transition:transform 240ms ease, opacity 200ms ease;}
.quad-modal-display svg{width:100%;height:100%;}
@media (min-width: 1024px) {
.control-sheet {
@ -784,7 +406,7 @@ height: 92%;
top: 7rem;
bottom: auto;
width: 340px;
max-height: calc(93vh - 8rem);
max-height: calc(100vh - 8rem);
border-radius: 1.5rem;
position: sticky;
overflow-y: auto;
@ -792,7 +414,6 @@ height: 92%;
border: 1px solid rgba(255,255,255,0.4);
}
body { padding-bottom: 0; overflow: auto; }
.mobile-action-bar { display: none !important; }
}
/* Compact viewport fallback */

18
svg.sh
View File

@ -1,18 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
# Backup originals once
mkdir -p output_webp_backup
cp --no-clobber output_webp/*.svg output_webp_backup/ || true
# Convert strokes to filled shapes and force a solid fill
FILL="#ffffff" # solid white interior for mask; stroke stays black for outline
for f in output_webp/*.svg; do
echo "Fixing $f"
inkscape "$f" --batch-process \
--actions="select-all;object-stroke-to-path;object-to-path;object-set-attribute:fill,$FILL;object-set-attribute:stroke,#000000;object-set-attribute:fill-rule,evenodd;object-set-attribute:style," \
--export-type=svg --export-filename="$f" --export-overwrite
done
echo "Done. Originals in output_webp_backup/"

1309
wall.js

File diff suppressed because it is too large Load Diff