Compare commits
No commits in common. "main" and "wall-gap-work" have entirely different histories.
main
...
wall-gap-w
1759
classic.js
@ -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"},
|
||||
|
||||
291
index.html
@ -24,7 +24,7 @@
|
||||
</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">
|
||||
<div class="flex items-center gap-3">
|
||||
@ -37,7 +37,7 @@
|
||||
<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-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">
|
||||
@ -155,44 +155,41 @@
|
||||
</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">Tap or click on canvas to sample a balloon’s 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="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 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="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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
@ -254,24 +251,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 1–8</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 +274,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 +302,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 +344,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,10 +398,6 @@
|
||||
<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>
|
||||
@ -517,42 +450,42 @@
|
||||
</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 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>
|
||||
|
||||
<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>
|
||||
<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 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 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="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 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>
|
||||
@ -583,8 +516,7 @@
|
||||
</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%;">
|
||||
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>
|
||||
@ -693,18 +625,5 @@
|
||||
<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>
|
||||
|
||||
135
organic.js
@ -143,13 +143,6 @@
|
||||
const garlandAccentChip = document.getElementById('garland-accent-chip');
|
||||
const garlandAccentClearBtn = document.getElementById('garland-accent-clear');
|
||||
const garlandControls = document.getElementById('garland-controls');
|
||||
// Optional dropdowns (may not be present in current layout)
|
||||
const garlandColorMain1Sel = document.getElementById('garland-color-main-1');
|
||||
const garlandColorMain2Sel = document.getElementById('garland-color-main-2');
|
||||
const garlandColorMain3Sel = document.getElementById('garland-color-main-3');
|
||||
const garlandColorMain4Sel = document.getElementById('garland-color-main-4');
|
||||
const garlandColorAccentSel = document.getElementById('garland-color-accent');
|
||||
const updateGarlandSwatches = () => {}; // stub for layouts without dropdown swatches
|
||||
|
||||
const sizePresetGroup = document.getElementById('size-preset-group');
|
||||
const toggleShineBtn = null;
|
||||
@ -1113,31 +1106,50 @@
|
||||
if (mode === 'garland') requestDraw();
|
||||
persist();
|
||||
});
|
||||
bindActiveChipPicker();
|
||||
|
||||
// ====== UI Rendering (Palettes) ======
|
||||
function renderAllowedPalette() {
|
||||
if (!paletteBox) return;
|
||||
paletteBox.innerHTML = '';
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'btn-dark w-full';
|
||||
btn.textContent = 'Choose color';
|
||||
btn.addEventListener('click', () => {
|
||||
if (!window.openColorPicker) return;
|
||||
window.openColorPicker({
|
||||
title: 'Choose active color',
|
||||
subtitle: 'Applies to drawing and path tools',
|
||||
items: (FLAT_COLORS || []).map((c, idx) => ({ label: c.name || c.hex, metaText: c.family || '', idx })),
|
||||
onSelect: (item) => {
|
||||
if (!Number.isInteger(item.idx)) return;
|
||||
selectedColorIdx = item.idx;
|
||||
(window.PALETTE || []).forEach(group => {
|
||||
const title = document.createElement('div');
|
||||
title.className = 'family-title';
|
||||
title.textContent = group.family;
|
||||
paletteBox.appendChild(title);
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'swatch-row';
|
||||
(group.colors || []).forEach(c => {
|
||||
const idx =
|
||||
FLAT_COLORS.find(fc => fc.name === c.name && fc.hex === c.hex && fc.family === group.family)?._idx
|
||||
?? HEX_TO_FIRST_IDX.get(normalizeHex(c.hex));
|
||||
const sw = document.createElement('button');
|
||||
sw.type = 'button';
|
||||
sw.className = 'swatch';
|
||||
sw.setAttribute('aria-label', c.name);
|
||||
|
||||
if (c.image) {
|
||||
const meta = FLAT_COLORS[idx] || {};
|
||||
sw.style.backgroundImage = `url("${c.image}")`;
|
||||
sw.style.backgroundSize = `${100 * SWATCH_TEXTURE_ZOOM}%`;
|
||||
sw.style.backgroundPosition = `${(meta.imageFocus?.x ?? 0.5) * 100}% ${(meta.imageFocus?.y ?? 0.5) * 100}%`;
|
||||
} else {
|
||||
sw.style.backgroundColor = c.hex;
|
||||
}
|
||||
|
||||
if (idx === selectedColorIdx) sw.classList.add('active');
|
||||
sw.title = c.name;
|
||||
|
||||
sw.addEventListener('click', () => {
|
||||
selectedColorIdx = idx ?? 0;
|
||||
renderAllowedPalette();
|
||||
updateCurrentColorChip();
|
||||
persist();
|
||||
}
|
||||
});
|
||||
row.appendChild(sw);
|
||||
});
|
||||
paletteBox.appendChild(row);
|
||||
});
|
||||
paletteBox.appendChild(btn);
|
||||
}
|
||||
|
||||
function getUsedColors() {
|
||||
@ -1184,29 +1196,6 @@
|
||||
updateChip('mobile-active-color-chip', null, { showLabel: false });
|
||||
}
|
||||
|
||||
function bindActiveChipPicker() {
|
||||
const chips = ['current-color-chip', 'mobile-active-color-chip'];
|
||||
chips.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.style.cursor = 'pointer';
|
||||
el.addEventListener('click', () => {
|
||||
if (!window.openColorPicker) return;
|
||||
window.openColorPicker({
|
||||
title: 'Choose active color',
|
||||
subtitle: 'Applies to drawing and path tools',
|
||||
items: (FLAT_COLORS || []).map((c, idx) => ({ label: c.name || c.hex, metaText: c.family || '', idx })),
|
||||
onSelect: (item) => {
|
||||
if (!Number.isInteger(item.idx)) return;
|
||||
selectedColorIdx = item.idx;
|
||||
updateCurrentColorChip();
|
||||
persist();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.organic = {
|
||||
getColor: () => selectedColorIdx,
|
||||
updateCurrentColorChip,
|
||||
@ -1223,8 +1212,58 @@
|
||||
|
||||
function renderUsedPalette() {
|
||||
if (!usedPaletteBox) return;
|
||||
usedPaletteBox.innerHTML = '<div class="hint">Palette opens in modal.</div>';
|
||||
if (replaceFromSel) replaceFromSel.innerHTML = '';
|
||||
usedPaletteBox.innerHTML = '';
|
||||
const used = getUsedColors();
|
||||
if (used.length === 0) {
|
||||
usedPaletteBox.innerHTML = '<div class="hint">No colors yet.</div>';
|
||||
if (replaceFromSel) replaceFromSel.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
const row = document.createElement('div');
|
||||
row.className = 'swatch-row';
|
||||
used.forEach(item => {
|
||||
const sw = document.createElement('button');
|
||||
sw.type = 'button';
|
||||
sw.className = 'swatch';
|
||||
const name = item.name || NAME_BY_HEX.get(item.hex) || item.hex;
|
||||
sw.setAttribute('aria-label', `${name} - Count: ${item.count}`);
|
||||
|
||||
if (item.image) {
|
||||
const meta = FLAT_COLORS[HEX_TO_FIRST_IDX.get(item.hex)] || {};
|
||||
sw.style.backgroundImage = `url("${item.image}")`;
|
||||
sw.style.backgroundSize = `${100 * SWATCH_TEXTURE_ZOOM}%`;
|
||||
sw.style.backgroundPosition = `${(meta.imageFocus?.x ?? 0.5) * 100}% ${(meta.imageFocus?.y ?? 0.5) * 100}%`;
|
||||
} else {
|
||||
sw.style.backgroundColor = item.hex;
|
||||
}
|
||||
if (normalizeHex(FLAT_COLORS[selectedColorIdx]?.hex) === item.hex) sw.classList.add('active');
|
||||
sw.title = `${name} — ${item.count}`;
|
||||
sw.addEventListener('click', () => {
|
||||
selectedColorIdx = HEX_TO_FIRST_IDX.get(item.hex) ?? 0;
|
||||
renderAllowedPalette();
|
||||
renderUsedPalette();
|
||||
});
|
||||
|
||||
const badge = document.createElement('div');
|
||||
badge.className = 'badge';
|
||||
badge.textContent = String(item.count);
|
||||
sw.appendChild(badge);
|
||||
row.appendChild(sw);
|
||||
});
|
||||
usedPaletteBox.appendChild(row);
|
||||
|
||||
// fill "replace from"
|
||||
if (replaceFromSel) {
|
||||
replaceFromSel.innerHTML = '';
|
||||
used.forEach(item => {
|
||||
const opt = document.createElement('option');
|
||||
const name = item.name || NAME_BY_HEX.get(item.hex) || item.hex;
|
||||
opt.value = item.hex;
|
||||
opt.textContent = `${name} (${item.count})`;
|
||||
replaceFromSel.appendChild(opt);
|
||||
});
|
||||
updateReplaceChips();
|
||||
}
|
||||
}
|
||||
|
||||
// ====== Balloon Ops & Data/Export ======
|
||||
|
||||
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 23 KiB |
@ -526,10 +526,10 @@
|
||||
let current = '#tab-organic';
|
||||
const isMobileView = () => window.matchMedia('(max-width: 1023px)').matches;
|
||||
const updateMobileActionBarVisibility = () => {
|
||||
if (!mobileActionBar) return;
|
||||
const modalOpen = !!document.querySelector('.color-modal:not(.hidden)');
|
||||
const isMobile = isMobileView();
|
||||
const showOrganic = isMobile && !modalOpen && current === '#tab-organic';
|
||||
if (mobileActionBar) mobileActionBar.classList.toggle('hidden', !showOrganic);
|
||||
const shouldShow = current === '#tab-organic' && isMobileView() && !modalOpen;
|
||||
mobileActionBar.classList.toggle('hidden', !shouldShow);
|
||||
};
|
||||
const wireMobileActionButtons = () => {
|
||||
const guardOrganic = () => current === '#tab-organic';
|
||||
@ -549,7 +549,6 @@
|
||||
on('mobile-act-export', () => clickBtn('[data-export="png"]'));
|
||||
};
|
||||
wireMobileActionButtons();
|
||||
|
||||
window.addEventListener('resize', updateMobileActionBarVisibility);
|
||||
function setTab(id, isInitial = false) {
|
||||
if (!id || !document.querySelector(id)) id = '#tab-organic';
|
||||
|
||||
22
shared.js
@ -13,8 +13,6 @@
|
||||
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) {
|
||||
@ -31,24 +29,6 @@
|
||||
}
|
||||
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 => {
|
||||
@ -226,8 +206,6 @@
|
||||
imageToDataUrl,
|
||||
imageUrlToDataUrl,
|
||||
download,
|
||||
getActiveColor,
|
||||
setActiveColor
|
||||
};
|
||||
});
|
||||
})();
|
||||
|
||||
119
style.css
@ -29,15 +29,7 @@ body[data-active-tab="#tab-wall"] #clear-canvas-btn-top {
|
||||
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;
|
||||
@ -396,7 +388,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%); }
|
||||
@ -438,7 +429,7 @@ height: 92%;
|
||||
#classic-display,
|
||||
#wall-display,
|
||||
#balloon-canvas {
|
||||
margin-bottom: 6.5rem;
|
||||
margin-bottom: 5rem;
|
||||
}
|
||||
|
||||
/* Stack switching: show only the active mobile tab stack across panels */
|
||||
@ -473,8 +464,6 @@ height: 92%;
|
||||
.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 {
|
||||
@ -618,57 +607,6 @@ height: 92%;
|
||||
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;
|
||||
@ -687,29 +625,13 @@ height: 92%;
|
||||
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 */
|
||||
margin-bottom: 5.5rem;
|
||||
}
|
||||
}
|
||||
.mobile-tabbar .mobile-tab-btn {
|
||||
@ -744,39 +666,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 +673,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;
|
||||
|
||||
18
svg.sh
@ -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/"
|
||||
263
wall.js
@ -18,6 +18,7 @@
|
||||
|
||||
let wallState = null;
|
||||
let selectedColorIdx = 0; // This should be synced with organic's selectedColorIdx
|
||||
let wallToolMode = 'paint';
|
||||
|
||||
// DOM elements
|
||||
const wallDisplay = document.getElementById('wall-display');
|
||||
@ -325,18 +326,6 @@
|
||||
return { mode: 'auto' };
|
||||
};
|
||||
|
||||
// Shared stroke helpers:
|
||||
// - Outline only when filled AND outline is enabled.
|
||||
// - Wireframe only when empty AND wireframes are enabled.
|
||||
const strokeFor = (isEmpty, { outline = '#111827', wire = '#cbd5e1' } = {}) => {
|
||||
if (isEmpty) return showWireframes ? wire : 'none';
|
||||
return showOutline ? outline : 'none';
|
||||
};
|
||||
const strokeWidthFor = (isEmpty, { outline = 0.6, wire = 1.4 } = {}) => {
|
||||
if (isEmpty) return showWireframes ? wire : 0;
|
||||
return showOutline ? outline : 0;
|
||||
};
|
||||
|
||||
// Helper to create a shine ellipse with coordinates relative to (0,0)
|
||||
const shineNodeRelative = (rx, ry, hex, rot = -20) => {
|
||||
const shine = shineStyle(hex || WALL_FALLBACK_COLOR);
|
||||
@ -362,14 +351,13 @@
|
||||
|
||||
const meta = wallColorMeta(customIdx);
|
||||
const patId = ensurePattern(meta);
|
||||
const fill = invisible ? hitFill : (isEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
|
||||
const stroke = invisible ? 'none' : strokeFor(isEmpty);
|
||||
const strokeW = invisible ? 0 : strokeWidthFor(isEmpty);
|
||||
const fill = invisible ? hitFill : (isEmpty ? hitFill : (patId ? `url(#${patId})` : meta.hex));
|
||||
const stroke = invisible ? 'none' : (isEmpty ? (showWireframes ? '#cbd5e1' : 'none') : (showOutline ? '#111827' : 'none'));
|
||||
const strokeW = invisible ? 0 : (isEmpty ? (showWireframes ? 1.4 : 0) : (showOutline ? 0.6 : 0));
|
||||
const filter = (isEmpty || invisible) ? '' : `filter="url(#${smallShadow})"`;
|
||||
const shine = isEmpty ? '' : shineNodeRelative(fiveInchDims.rx, fiveInchDims.ry, meta.hex);
|
||||
|
||||
const displayIdx = isEmpty ? -1 : (customIdx ?? -1);
|
||||
smallNodes.push(`<g data-wall-cell="1" data-wall-key="${keyId}" data-wall-color="${displayIdx}" style="cursor:pointer; pointer-events:all;" transform="translate(${pos.x},${pos.y})">
|
||||
smallNodes.push(`<g data-wall-cell="1" data-wall-key="${keyId}" style="cursor:pointer; pointer-events:all;" transform="translate(${pos.x},${pos.y})">
|
||||
<circle cx="0" cy="0" r="${fiveInchDims.rx}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
|
||||
${shine}
|
||||
</g>`);
|
||||
@ -392,14 +380,14 @@
|
||||
|
||||
const meta = wallColorMeta(customIdx);
|
||||
const patId = ensurePattern(meta);
|
||||
const fill = invisible ? hitFill : (isEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
|
||||
const stroke = invisible ? 'none' : strokeFor(isEmpty);
|
||||
const strokeW = invisible ? 0 : strokeWidthFor(isEmpty, { outline: 0.6, wire: 1.4 });
|
||||
const fill = invisible ? hitFill : (isEmpty ? hitFill : (patId ? `url(#${patId})` : meta.hex));
|
||||
console.log(`h-r-c: keyId: ${keyId}, customIdx: ${customIdx}, isEmpty: ${isEmpty}, invisible: ${invisible}, fill: ${fill}, meta:`, meta);
|
||||
const stroke = invisible ? 'none' : (isEmpty ? '#cbd5e1' : (showOutline ? '#111827' : 'none'));
|
||||
const strokeW = invisible ? 0 : (isEmpty ? 1.4 : (showOutline ? 0.6 : 0));
|
||||
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
|
||||
const shine = isEmpty ? '' : shineNodeRelative(linkDims.rx, linkDims.ry, meta.hex);
|
||||
|
||||
const displayIdx = isEmpty ? -1 : (customIdx ?? -1);
|
||||
bigNodes.push(`<g data-wall-cell="1" data-wall-key="${keyId}" data-wall-color="${displayIdx}" style="cursor:pointer; pointer-events:all;" transform="translate(${mid.x},${mid.y})">
|
||||
bigNodes.push(`<g data-wall-cell="1" data-wall-key="${keyId}" style="cursor:pointer; pointer-events:all;" transform="translate(${mid.x},${mid.y})">
|
||||
<ellipse cx="0" cy="0" rx="${linkDims.rx}" ry="${linkDims.ry}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
|
||||
${shine}
|
||||
</g>`);
|
||||
@ -421,14 +409,13 @@
|
||||
|
||||
const meta = wallColorMeta(customIdx);
|
||||
const patId = ensurePattern(meta);
|
||||
const fill = invisible ? hitFill : (isEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
|
||||
const stroke = invisible ? 'none' : strokeFor(isEmpty, { outline: '#111827', wire: '#cbd5e1' });
|
||||
const strokeW = invisible ? 0 : strokeWidthFor(isEmpty, { outline: 0.6, wire: 1.4 });
|
||||
const fill = invisible ? hitFill : (isEmpty ? hitFill : (patId ? `url(#${patId})` : meta.hex));
|
||||
const stroke = invisible ? 'none' : (isEmpty ? '#cbd5e1' : (showOutline ? '#111827' : 'none'));
|
||||
const strokeW = invisible ? 0 : (isEmpty ? 1.4 : (showOutline ? 0.6 : 0));
|
||||
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
|
||||
const shine = isEmpty ? '' : shineNodeRelative(linkDims.rx, linkDims.ry, meta.hex);
|
||||
|
||||
const displayIdx = isEmpty ? -1 : (customIdx ?? -1);
|
||||
bigNodes.push(`<g data-wall-cell="1" data-wall-key="${keyId}" data-wall-color="${displayIdx}" style="cursor:pointer; pointer-events:all;" transform="translate(${mid.x},${mid.y}) rotate(90)">
|
||||
bigNodes.push(`<g data-wall-cell="1" data-wall-key="${keyId}" style="cursor:pointer; pointer-events:all;" transform="translate(${mid.x},${mid.y}) rotate(90)">
|
||||
<ellipse cx="0" cy="0" rx="${linkDims.rx}" ry="${linkDims.ry}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
|
||||
${shine}
|
||||
</g>`);
|
||||
@ -451,14 +438,13 @@
|
||||
const invisible = isEmpty;
|
||||
const meta = wallColorMeta(gapIdx);
|
||||
const patId = ensurePattern(meta);
|
||||
const fill = invisible ? hitFill : (isEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
|
||||
const stroke = invisible ? 'none' : strokeFor(isEmpty);
|
||||
const strokeW = invisible ? 0 : strokeWidthFor(isEmpty, { outline: 0.6, wire: 1.4 });
|
||||
const fill = invisible ? hitFill : (patId ? `url(#${patId})` : meta.hex);
|
||||
const stroke = invisible || isEmpty ? 'none' : (showOutline ? '#111827' : 'none');
|
||||
const strokeW = invisible || isEmpty ? 0 : (showOutline ? 0.6 : 0);
|
||||
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
|
||||
const rGap = bigR * 0.82; // slightly smaller 11" gap balloon
|
||||
const shineGap = isEmpty ? '' : shineNodeRelative(rGap, rGap, meta.hex);
|
||||
const displayIdx = isEmpty ? -1 : (gapIdx ?? -1);
|
||||
bigNodes.push(`<g data-wall-gap="1" data-wall-key="${gapKey}" data-wall-color="${displayIdx}" style="cursor:pointer; pointer-events:all; cursor:crosshair;" transform="translate(${center.x},${center.y})">
|
||||
bigNodes.push(`<g data-wall-gap="1" data-wall-key="${gapKey}" style="cursor:pointer; pointer-events:all; cursor:crosshair;" transform="translate(${center.x},${center.y})">
|
||||
<circle cx="0" cy="0" r="${rGap}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
|
||||
${shineGap}
|
||||
</g>`);
|
||||
@ -482,13 +468,13 @@
|
||||
const meta = wallColorMeta(centerCustomIdx);
|
||||
const patId = ensurePattern(meta);
|
||||
const fill = invisible ? 'rgba(0,0,0,0.001)' : (centerIsEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
|
||||
const stroke = invisible ? 'none' : strokeFor(centerIsEmpty);
|
||||
const strokeW = invisible ? 0 : strokeWidthFor(centerIsEmpty, { outline: 0.6, wire: 1.4 });
|
||||
console.log(`c-r-c: keyId: ${centerKey}, customIdx: ${centerCustomIdx}, isEmpty: ${centerIsEmpty}, invisible: ${invisible}, fill: ${fill}, meta:`, meta);
|
||||
const stroke = invisible ? 'none' : (centerIsEmpty ? '#cbd5e1' : (showOutline ? '#111827' : 'none'));
|
||||
const strokeW = invisible ? 0 : (centerIsEmpty ? 1.4 : (showOutline ? 0.6 : 0));
|
||||
const filter = centerIsEmpty || invisible ? '' : `filter="url(#${smallShadow})"`;
|
||||
const shine = centerIsEmpty ? '' : shineNodeRelative(fiveInchDims.rx, fiveInchDims.ry, meta.hex);
|
||||
|
||||
const displayIdxCenter = centerCustomIdx ?? -1;
|
||||
smallNodes.push(`<g data-wall-cell="1" data-wall-key="${centerKey}" data-wall-color="${displayIdxCenter}" style="cursor:pointer; pointer-events:all;" transform="translate(${center.x},${center.y})">
|
||||
smallNodes.push(`<g data-wall-cell="1" data-wall-key="${centerKey}" style="cursor:pointer; pointer-events:all;" transform="translate(${center.x},${center.y})">
|
||||
<circle cx="0" cy="0" r="${fiveInchDims.rx}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
|
||||
${shine}
|
||||
</g>`);
|
||||
@ -506,21 +492,19 @@
|
||||
const linkCustomIdx = linkOverride.mode === 'color' ? linkOverride.idx : null;
|
||||
const linkIsEmpty = linkOverride.mode === 'empty' || linkCustomIdx === null;
|
||||
|
||||
const invisibleLink = linkIsEmpty && !showWireframes;
|
||||
|
||||
const meta = wallColorMeta(linkCustomIdx);
|
||||
const patId = ensurePattern(meta);
|
||||
const fill = linkIsEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex);
|
||||
// Wireframe shows hit area when empty; outline shows only when filled and outline enabled.
|
||||
const stroke = linkIsEmpty
|
||||
? (showWireframes ? '#cbd5e1' : 'none')
|
||||
: (showOutline ? '#111827' : 'none');
|
||||
const strokeW = linkIsEmpty
|
||||
? (showWireframes ? 1.0 : 0)
|
||||
: (showOutline ? 0.9 : 0);
|
||||
const filter = linkIsEmpty ? '' : `filter="url(#${bigShadow})"`;
|
||||
const fill = invisibleLink ? 'rgba(0,0,0,0.001)' : (linkIsEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
|
||||
console.log(`l#-r-c: keyId: ${linkKey}, customIdx: ${linkCustomIdx}, isEmpty: ${linkIsEmpty}, invisible: ${invisibleLink}, fill: ${fill}, meta:`, meta);
|
||||
// Outline only when filled; light wireframe when empty and wireframes shown.
|
||||
const stroke = invisibleLink ? 'none' : (linkIsEmpty ? (showWireframes ? '#cbd5e1' : 'none') : (showOutline ? '#111827' : 'none'));
|
||||
const strokeW = invisibleLink ? 0 : (linkIsEmpty ? (showWireframes ? 1.2 : 0) : (showOutline ? 0.8 : 0));
|
||||
const filter = invisibleLink || linkIsEmpty ? '' : `filter="url(#${bigShadow})"`;
|
||||
const shine = linkIsEmpty ? '' : shineNodeRelative(linkDims.rx, linkDims.ry, meta.hex);
|
||||
|
||||
const displayIdxLink = linkIsEmpty ? -1 : (linkCustomIdx ?? -1);
|
||||
bigNodes.push(`<g data-wall-cell="1" data-wall-key="${linkKey}" data-wall-color="${displayIdxLink}" style="cursor:pointer; pointer-events:all;" transform="translate(${mid.x},${mid.y}) rotate(${angle})">
|
||||
bigNodes.push(`<g data-wall-cell="1" data-wall-key="${linkKey}" style="cursor:pointer; pointer-events:all;" transform="translate(${mid.x},${mid.y}) rotate(${angle})">
|
||||
<ellipse cx="0" cy="0" rx="${linkDims.rx}" ry="${linkDims.ry}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
|
||||
${shine}
|
||||
</g>`);
|
||||
@ -543,9 +527,9 @@
|
||||
const fillerInvisible = fillerEmpty && !showWireframes;
|
||||
const fillerMeta = wallColorMeta(fillerIdx);
|
||||
const fillerPat = ensurePattern(fillerMeta);
|
||||
const fillerFill = fillerInvisible ? 'rgba(0,0,0,0.001)' : (fillerEmpty ? 'none' : (fillerPat ? `url(#${fillerPat})` : fillerMeta.hex));
|
||||
const fillerStroke = fillerInvisible ? 'none' : strokeFor(fillerEmpty);
|
||||
const fillerStrokeW = fillerInvisible ? 0 : strokeWidthFor(fillerEmpty, { outline: 0.6, wire: 1.2 });
|
||||
const fillerFill = fillerInvisible ? 'rgba(0,0,0,0.001)' : (fillerEmpty ? (showWireframes ? 'none' : 'rgba(0,0,0,0.001)') : (fillerPat ? `url(#${fillerPat})` : fillerMeta.hex));
|
||||
const fillerStroke = fillerInvisible ? 'none' : (fillerEmpty ? (showWireframes ? '#cbd5e1' : 'none') : 'none');
|
||||
const fillerStrokeW = fillerInvisible ? 0 : (fillerEmpty ? (showWireframes ? 1.2 : 0) : 0);
|
||||
const fillerFilter = fillerInvisible || fillerEmpty ? '' : `filter="url(#${smallShadow})"`;
|
||||
const fillerShine = fillerEmpty ? '' : shineNodeRelative(fiveInchDims.rx, fiveInchDims.ry, fillerMeta.hex);
|
||||
smallNodes.push(`<g data-wall-cell="1" data-wall-key="${fillerKey}" style="cursor:pointer; pointer-events:all;" transform="translate(${pos.x},${pos.y})">
|
||||
@ -569,13 +553,12 @@
|
||||
const patId = ensurePattern(meta);
|
||||
const invisible = isEmpty && !showGaps;
|
||||
const fill = invisible ? 'rgba(0,0,0,0.001)' : (isEmpty ? 'none' : (patId ? `url(#${patId})` : meta.hex));
|
||||
const stroke = invisible ? 'none' : strokeFor(isEmpty);
|
||||
const strokeW = invisible ? 0 : strokeWidthFor(isEmpty, { outline: 0.6, wire: 1.4 });
|
||||
const stroke = invisible ? 'none' : (isEmpty ? '#cbd5e1' : (showOutline ? '#111827' : 'none'));
|
||||
const strokeW = invisible ? 0 : (isEmpty ? 1.4 : (showOutline ? 0.6 : 0));
|
||||
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
|
||||
const rGap = bigR * 0.82;
|
||||
const shineGap = isEmpty ? '' : shineNodeRelative(rGap, rGap, meta.hex);
|
||||
const displayIdx = isEmpty ? -1 : (gapIdx ?? -1);
|
||||
bigNodes.push(`<g data-wall-gap="1" data-wall-key="${key}" data-wall-color="${displayIdx}" style="cursor:pointer; pointer-events:all; cursor:crosshair;" transform="translate(${mid.x},${mid.y})">
|
||||
bigNodes.push(`<g data-wall-gap="1" data-wall-key="${key}" style="cursor:pointer; pointer-events:all; cursor:crosshair;" transform="translate(${mid.x},${mid.y})">
|
||||
<circle cx="0" cy="0" r="${rGap}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
|
||||
${shineGap}
|
||||
</g>`);
|
||||
@ -601,8 +584,7 @@
|
||||
const filter = invisible || isEmpty ? '' : `filter="url(#${bigShadow})"`;
|
||||
const rGap = bigR * 0.82;
|
||||
const shineGap = isEmpty ? '' : shineNodeRelative(rGap, rGap, meta.hex);
|
||||
const displayIdx = isEmpty ? -1 : (gapIdx ?? -1);
|
||||
bigNodes.push(`<g data-wall-gap="1" data-wall-key="${key}" data-wall-color="${displayIdx}" style="cursor:pointer; pointer-events:all; cursor:crosshair;" transform="translate(${mid.x},${mid.y})">
|
||||
bigNodes.push(`<g data-wall-gap="1" data-wall-key="${key}" style="cursor:pointer; pointer-events:all; cursor:crosshair;" transform="translate(${mid.x},${mid.y})">
|
||||
<circle cx="0" cy="0" r="${rGap}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeW}" ${filter} pointer-events="all" />
|
||||
${shineGap}
|
||||
</g>`);
|
||||
@ -641,7 +623,36 @@
|
||||
|
||||
function renderWallUsedPalette() {
|
||||
if (!wallUsedPaletteEl) return;
|
||||
wallUsedPaletteEl.innerHTML = '<div class="text-xs text-gray-500">Palette opens in modal.</div>';
|
||||
const used = wallUsedColors();
|
||||
wallUsedPaletteEl.innerHTML = '';
|
||||
if (!used.length) {
|
||||
wallUsedPaletteEl.innerHTML = '<div class="text-xs text-gray-500">No colors yet.</div>';
|
||||
populateWallReplaceSelects();
|
||||
updateWallReplacePreview();
|
||||
return;
|
||||
}
|
||||
const row = document.createElement('div');
|
||||
row.className = 'swatch-row';
|
||||
used.forEach(item => {
|
||||
const sw = document.createElement('button');
|
||||
sw.type = 'button';
|
||||
sw.className = 'swatch';
|
||||
if (item.image) {
|
||||
sw.style.backgroundImage = `url("${item.image}")`;
|
||||
sw.style.backgroundSize = `${100 * 2.5}%`;
|
||||
} else {
|
||||
sw.style.backgroundColor = item.hex;
|
||||
}
|
||||
sw.title = `${item.name || item.hex} (${item.count})`;
|
||||
sw.addEventListener('click', () => {
|
||||
if (!Number.isInteger(item.idx)) return;
|
||||
setActiveColor(normalizeColorIdx(item.idx));
|
||||
renderWallPalette();
|
||||
renderWallUsedPalette();
|
||||
});
|
||||
row.appendChild(sw);
|
||||
});
|
||||
wallUsedPaletteEl.appendChild(row);
|
||||
populateWallReplaceSelects();
|
||||
updateWallReplacePreview();
|
||||
}
|
||||
@ -865,17 +876,6 @@
|
||||
return val;
|
||||
}
|
||||
|
||||
// Resolve current color (custom override only).
|
||||
function getCurrentColorIdxForKey(key) {
|
||||
if (!wallState) wallState = wallDefaultState();
|
||||
ensureWallGridSize(wallState.rows, wallState.cols);
|
||||
const raw = wallState.customColors?.[key];
|
||||
const parsed = Number.isInteger(raw) ? raw : Number.parseInt(raw, 10);
|
||||
if (!Number.isInteger(parsed)) return null;
|
||||
if (parsed < 0) return null;
|
||||
return normalizeColorIdx(parsed);
|
||||
}
|
||||
|
||||
function updateWallActiveChip(idx) {
|
||||
if (!wallActiveChip || !wallActiveLabel) return;
|
||||
ensureFlatColors();
|
||||
@ -895,6 +895,19 @@
|
||||
wallActiveLabel.textContent = meta.name || meta.hex || '';
|
||||
}
|
||||
|
||||
function setWallToolMode(mode) {
|
||||
wallToolMode = mode === 'erase' ? 'erase' : 'paint';
|
||||
if (wallToolPaintBtn && wallToolEraseBtn) {
|
||||
const isErase = wallToolMode === 'erase';
|
||||
wallToolPaintBtn.setAttribute('aria-pressed', String(!isErase));
|
||||
wallToolEraseBtn.setAttribute('aria-pressed', String(isErase));
|
||||
wallToolPaintBtn.classList.toggle('tab-active', !isErase);
|
||||
wallToolEraseBtn.classList.toggle('tab-active', isErase);
|
||||
wallToolPaintBtn.classList.toggle('tab-idle', isErase);
|
||||
wallToolEraseBtn.classList.toggle('tab-idle', !isErase);
|
||||
}
|
||||
}
|
||||
|
||||
// Paint a specific group of nodes with the active color.
|
||||
function paintWallGroup(group) {
|
||||
ensureWallGridSize(wallState.rows, wallState.cols);
|
||||
@ -990,25 +1003,47 @@
|
||||
if (!wallPaletteEl) return;
|
||||
wallPaletteEl.innerHTML = '';
|
||||
populateWallReplaceSelects();
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'btn-dark w-full';
|
||||
btn.textContent = 'Choose color';
|
||||
btn.addEventListener('click', () => {
|
||||
if (!window.openColorPicker) return;
|
||||
window.openColorPicker({
|
||||
title: 'Choose active wall color',
|
||||
subtitle: 'Applies to wall fill tools',
|
||||
items: (FLAT_COLORS || []).map((c, idx) => ({ label: c.name || c.hex, metaText: c.family || '', idx })),
|
||||
onSelect: (item) => {
|
||||
if (!Number.isInteger(item.idx)) return;
|
||||
setActiveColor(item.idx);
|
||||
updateWallActiveChip(getActiveWallColorIdx());
|
||||
updateWallReplacePreview();
|
||||
(window.PALETTE || []).forEach(group => {
|
||||
const title = document.createElement('div');
|
||||
title.className = 'family-title';
|
||||
title.textContent = group.family;
|
||||
wallPaletteEl.appendChild(title);
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'swatch-row';
|
||||
(group.colors || []).forEach(c => {
|
||||
const normHex = (c.hex || '').toLowerCase();
|
||||
let idx = FLAT_COLORS.findIndex(fc => fc.name === c.name && fc.hex === c.hex && fc.family === group.family);
|
||||
if (idx < 0 && window.shared?.HEX_TO_FIRST_IDX?.has(normHex)) {
|
||||
idx = window.shared.HEX_TO_FIRST_IDX.get(normHex);
|
||||
}
|
||||
idx = normalizeColorIdx(idx);
|
||||
const sw = document.createElement('button');
|
||||
sw.type = 'button';
|
||||
sw.className = 'swatch';
|
||||
if (c.image) {
|
||||
const meta = FLAT_COLORS[idx] || {};
|
||||
sw.style.backgroundImage = `url("${c.image}")`;
|
||||
sw.style.backgroundSize = `${100 * 2.5}%`;
|
||||
sw.style.backgroundPosition = `${(meta.imageFocus?.x ?? 0.5) * 100}% ${(meta.imageFocus?.y ?? 0.5) * 100}%`;
|
||||
} else {
|
||||
sw.style.backgroundColor = c.hex;
|
||||
}
|
||||
if (idx === selectedColorIdx) sw.classList.add('active');
|
||||
sw.title = c.name;
|
||||
sw.addEventListener('click', () => {
|
||||
setActiveColor(idx);
|
||||
window.organic?.updateCurrentColorChip?.(selectedColorIdx);
|
||||
// Also update the global chip explicitly
|
||||
if (window.organic?.updateCurrentColorChip) {
|
||||
window.organic.updateCurrentColorChip(selectedColorIdx);
|
||||
}
|
||||
renderWallPalette();
|
||||
});
|
||||
row.appendChild(sw);
|
||||
});
|
||||
wallPaletteEl.appendChild(row);
|
||||
});
|
||||
wallPaletteEl.appendChild(btn);
|
||||
renderWallUsedPalette();
|
||||
updateWallActiveChip(getActiveWallColorIdx());
|
||||
updateWallReplacePreview();
|
||||
@ -1038,47 +1073,7 @@
|
||||
else if (window.organic?.getColor) selectedColorIdx = normalizeColorIdx(window.organic.getColor());
|
||||
else selectedColorIdx = defaultActiveColorIdx();
|
||||
setActiveColor(selectedColorIdx);
|
||||
// Hide legacy paint/erase toggles; behavior is always click-to-paint, click-again-to-clear.
|
||||
if (wallToolPaintBtn) {
|
||||
wallToolPaintBtn.classList.add('hidden');
|
||||
wallToolPaintBtn.setAttribute('aria-hidden', 'true');
|
||||
wallToolPaintBtn.tabIndex = -1;
|
||||
}
|
||||
if (wallToolEraseBtn) {
|
||||
wallToolEraseBtn.classList.add('hidden');
|
||||
wallToolEraseBtn.setAttribute('aria-hidden', 'true');
|
||||
wallToolEraseBtn.tabIndex = -1;
|
||||
}
|
||||
// Hide legacy paint/erase toggles; always use click-to-paint/click-again-to-clear.
|
||||
if (wallToolPaintBtn) {
|
||||
wallToolPaintBtn.classList.add('hidden');
|
||||
wallToolPaintBtn.setAttribute('aria-hidden', 'true');
|
||||
wallToolPaintBtn.tabIndex = -1;
|
||||
}
|
||||
if (wallToolEraseBtn) {
|
||||
wallToolEraseBtn.classList.add('hidden');
|
||||
wallToolEraseBtn.setAttribute('aria-hidden', 'true');
|
||||
wallToolEraseBtn.tabIndex = -1;
|
||||
}
|
||||
|
||||
// Allow picking active wall color by clicking the chip.
|
||||
if (wallActiveChip && window.openColorPicker) {
|
||||
wallActiveChip.style.cursor = 'pointer';
|
||||
wallActiveChip.addEventListener('click', () => {
|
||||
window.openColorPicker({
|
||||
title: 'Choose wall color',
|
||||
subtitle: 'Sets the active wall color',
|
||||
items: (FLAT_COLORS || []).map((c, idx) => ({ label: c.name || c.hex, metaText: c.family || '', idx })),
|
||||
onSelect: (item) => {
|
||||
if (!Number.isInteger(item.idx)) return;
|
||||
setActiveColor(item.idx);
|
||||
renderWallPalette();
|
||||
renderWallUsedPalette();
|
||||
renderWall();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
setWallToolMode('paint');
|
||||
loadPatternState(patternKey());
|
||||
ensureWallGridSize(wallState.rows, wallState.cols);
|
||||
syncWallInputs();
|
||||
@ -1131,7 +1126,8 @@
|
||||
wallReplaceToSel?.addEventListener('change', updateWallReplacePreview);
|
||||
wallReplaceFromChip?.addEventListener('click', () => openWallReplacePicker('from'));
|
||||
wallReplaceToChip?.addEventListener('click', () => openWallReplacePicker('to'));
|
||||
// Remove explicit paint/erase toggles; behavior is always click-to-paint, click-again-to-clear.
|
||||
wallToolPaintBtn?.addEventListener('click', () => setWallToolMode('paint'));
|
||||
wallToolEraseBtn?.addEventListener('click', () => setWallToolMode('erase'));
|
||||
|
||||
const findWallNode = (el) => {
|
||||
let cur = el;
|
||||
@ -1156,11 +1152,12 @@
|
||||
const key = hit.dataset.wallKey;
|
||||
if (!key) return;
|
||||
|
||||
const activeColor = normalizeColorIdx(getActiveWallColorIdx());
|
||||
const activeColor = getActiveWallColorIdx();
|
||||
if (!Number.isInteger(activeColor)) return;
|
||||
const datasetColor = Number.parseInt(hit.dataset.wallColor ?? '', 10);
|
||||
const currentColor = Number.isInteger(datasetColor) ? datasetColor : getCurrentColorIdxForKey(key);
|
||||
const hasCurrent = Number.isInteger(currentColor) && currentColor >= 0;
|
||||
const rawStored = wallState.customColors?.[key];
|
||||
const parsedStored = Number.isInteger(rawStored) ? rawStored : Number.parseInt(rawStored, 10);
|
||||
const storedColor = Number.isInteger(parsedStored) && parsedStored >= 0 ? normalizeColorIdx(parsedStored) : null;
|
||||
const hasStoredColor = Number.isInteger(storedColor) && storedColor >= 0;
|
||||
|
||||
if (e.altKey) {
|
||||
if (Number.isInteger(storedColor)) {
|
||||
@ -1171,9 +1168,9 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Simple toggle: click paints with active; clicking again with the same active clears it.
|
||||
const sameAsActive = hasCurrent && currentColor === activeColor;
|
||||
wallState.customColors[key] = sameAsActive ? -1 : activeColor;
|
||||
// Paint/erase based on tool mode; modifiers still erase.
|
||||
const isEraseClick = wallToolMode === 'erase' || e.shiftKey || e.metaKey || e.ctrlKey;
|
||||
wallState.customColors[key] = isEraseClick ? -1 : activeColor;
|
||||
|
||||
saveActivePatternState();
|
||||
saveWallState();
|
||||
|
||||