Compare commits
16 Commits
f22319737e
...
4b9fee0a3e
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b9fee0a3e | |||
| c20ceccab7 | |||
| 0f325ca1d8 | |||
| 8bb46389f3 | |||
| 7ba62b3d2b | |||
| 346f6ff917 | |||
| 99d943643b | |||
| f546662143 | |||
| 540acedcab | |||
| 5adff65791 | |||
| 54024d6bcd | |||
| a5016b4fa7 | |||
| 7e6ac4cf4b | |||
| 242a9f1ab0 | |||
| 57423a1d88 | |||
| 22075cadb4 |
1819
classic.js
@ -12,7 +12,8 @@ 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:"Orange",hex:"#ef6b24"},{name:"Coffee",hex:"#957461"},{name:"Burnt Orange",hex:"#9d4223"},
|
||||
{name:"Blended Brown",hex:"#c9aea0"}
|
||||
]},
|
||||
{ family: "Greens", colors: [
|
||||
{name:"Eucalyptus",hex:"#a3bba3"},{name:"Pastel Green",hex:"#acdba7"},{name:"Lime Green",hex:"#8fc73e"},
|
||||
@ -63,4 +64,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 : []);
|
||||
|
||||
460
index.html
@ -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(100vh-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(97vh-2rem)] overflow-hidden ring-1 ring-black/5">
|
||||
|
||||
<header class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3 px-1 lg:px-0">
|
||||
<header id="app-header" class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3 px-1 lg:px-0">
|
||||
<div class="flex items-center gap-3">
|
||||
|
||||
<div>
|
||||
@ -37,14 +37,10 @@
|
||||
<div class="flex items-center gap-4">
|
||||
<nav id="mode-tabs" class="flex gap-2">
|
||||
<button type="button" class="tab-btn tab-active" data-target="#tab-organic" aria-pressed="true">Organic</button>
|
||||
<button type="button" class="tab-btn tab-idle" data-target="#tab-classic" aria-pressed="false">Classic (Arch/Column)</button>
|
||||
<button type="button" class="tab-btn tab-idle" data-target="#tab-classic" aria-pressed="false">Classic</button>
|
||||
<button type="button" class="tab-btn tab-idle" data-target="#tab-wall" aria-pressed="false">Wall</button>
|
||||
</nav>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-1 px-2 py-1 rounded-xl bg-white/70 border border-gray-200 shadow-sm" title="Active Color">
|
||||
<div id="current-color-chip-global" class="current-color-chip">
|
||||
<span id="current-color-label-global" class="text-[10px] font-semibold text-slate-700"></span>
|
||||
</div>
|
||||
</div>
|
||||
<button id="app-fullscreen-toggle" class="btn-dark text-xs px-3 py-2" aria-label="Toggle fullscreen">Fullscreen</button>
|
||||
<button id="clear-canvas-btn-top" class="btn-danger text-xs px-3 py-2">Start Fresh</button>
|
||||
</div>
|
||||
@ -78,11 +74,11 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-2 mb-3">
|
||||
<button id="tool-undo" class="tool-btn" title="Ctrl+Z" aria-label="Undo">
|
||||
<button id="tool-undo" class="tool-btn" aria-label="Undo">
|
||||
<svg viewBox="0 0 24 24"><path d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z"/></svg>
|
||||
<span class="hidden sm:inline">Undo</span>
|
||||
</button>
|
||||
<button id="tool-redo" class="tool-btn" title="Ctrl+Y" aria-label="Redo">
|
||||
<button id="tool-redo" class="tool-btn" aria-label="Redo">
|
||||
<svg viewBox="0 0 24 24"><path d="M18.4 10.6C16.55 9 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7l-3.6 3.6z"/></svg>
|
||||
<span class="hidden sm:inline">Redo</span>
|
||||
</button>
|
||||
@ -98,31 +94,17 @@
|
||||
<input id="garland-density" type="range" min="0.6" max="1.6" step="0.1" value="1" class="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
||||
<span id="garland-density-label" class="w-10 text-right text-xs text-gray-500">1.0</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="garland-color-main1" class="font-medium w-24">Main A</label>
|
||||
<select id="garland-color-main1" class="select text-sm flex-1"></select>
|
||||
<span id="garland-swatch-main1" class="swatch tiny"></span>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium text-sm text-gray-700">Main Colors</span>
|
||||
<button type="button" id="garland-add-color" class="btn-blue text-xs px-3 py-1">+ Add</button>
|
||||
</div>
|
||||
<div id="garland-main-chips" class="flex flex-wrap gap-2"></div>
|
||||
<p class="hint text-xs">Tap a chip to change it. You can add up to 10 main colors.</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="garland-color-main2" class="font-medium w-24">Main B</label>
|
||||
<select id="garland-color-main2" class="select text-sm flex-1"></select>
|
||||
<span id="garland-swatch-main2" class="swatch tiny"></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="garland-color-main3" class="font-medium w-24">Main C</label>
|
||||
<select id="garland-color-main3" class="select text-sm flex-1"></select>
|
||||
<span id="garland-swatch-main3" class="swatch tiny"></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="garland-color-main4" class="font-medium w-24">Main D</label>
|
||||
<select id="garland-color-main4" class="select text-sm flex-1"></select>
|
||||
<span id="garland-swatch-main4" class="swatch tiny"></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="garland-color-accent" class="font-medium w-24">5" Accent</label>
|
||||
<select id="garland-color-accent" class="select text-sm flex-1"></select>
|
||||
<span id="garland-swatch-accent" class="swatch tiny"></span>
|
||||
<span class="font-medium text-sm text-gray-700">Accent</span>
|
||||
<button type="button" id="garland-accent-chip" class="replace-chip" aria-label="Pick accent color"></button>
|
||||
<button type="button" id="garland-accent-clear" class="btn-yellow text-xs px-3 py-1">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -137,7 +119,7 @@
|
||||
<button id="duplicate-selected" class="btn-dark" disabled>Duplicate</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<p class="hint">Drag balloons to reposition. Use keyboard arrows for fine nudges.</p>
|
||||
<p class="hint">Drag balloons to reposition. Use arrows/touches for fine nudges.</p>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center gap-2 text-xs text-gray-600">
|
||||
<span class="font-semibold">Resize</span>
|
||||
@ -173,38 +155,44 @@
|
||||
</div>
|
||||
|
||||
<div class="control-stack" data-mobile-tab="colors">
|
||||
<div class="panel-heading">Project Palette</div>
|
||||
<div class="panel-card">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-sm text-gray-600">Built from the current design. Click a swatch to select that color.</span>
|
||||
<button id="sort-used-toggle" class="text-sm underline">Sort: Most → Least</button>
|
||||
</div>
|
||||
<div id="used-palette" class="palette-box min-h-[3rem]"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel-heading mt-4">Color Library</div>
|
||||
<div class="panel-card">
|
||||
<p class="hint mb-2">Alt+click on canvas to sample a balloon’s color.</p>
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<span class="text-sm font-medium text-gray-700">Active Color</span>
|
||||
<div id="current-color-chip" class="current-color-chip">
|
||||
<span id="current-color-label" class="text-xs font-semibold text-slate-700"></span>
|
||||
<div 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>
|
||||
</div>
|
||||
<div id="color-palette" class="palette-box"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel-heading mt-4">Replace Color</div>
|
||||
<div class="panel-card">
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
<label class="text-sm font-medium">From (in design):</label>
|
||||
<select id="replace-from" class="select"></select>
|
||||
<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>
|
||||
|
||||
<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">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>
|
||||
|
||||
<button id="replace-btn" class="btn-blue">Replace</button>
|
||||
<p id="replace-msg" class="hint"></p>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -266,9 +254,24 @@
|
||||
</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">
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<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">
|
||||
@ -289,7 +292,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 (24")
|
||||
Add Topper
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@ -317,18 +320,6 @@
|
||||
</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>
|
||||
@ -359,49 +350,106 @@
|
||||
<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 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>
|
||||
<div class="text-sm text-gray-600 mb-1">Pick a color for <span id="classic-active-label" class="font-bold">Slot #1</span> (from colors.js):</div>
|
||||
<div id="classic-swatch-grid" class="palette-box min-h-[3rem]"></div>
|
||||
<div class="flex flex-wrap gap-2 mt-3">
|
||||
<button id="classic-randomize-colors" class="btn-dark">Randomize</button>
|
||||
</div>
|
||||
<div id="classic-topper-color-block" class="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>
|
||||
<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 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>
|
||||
</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>
|
||||
<p class="hint text-red-500">Classic JSON save/load coming soon.</p>
|
||||
</div>
|
||||
<div class="control-stack" data-mobile-tab="save">
|
||||
<div class="panel-heading">Save & Share</div>
|
||||
<div class="panel-card space-y-3">
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button class="btn-dark bg-blue-600" data-export="png">Export PNG</button>
|
||||
<button class="btn-dark bg-blue-700" data-export="svg">Export SVG</button>
|
||||
<p class="hint w-full">SVG keeps the vector Classic layout; PNG is raster.</p>
|
||||
</div>
|
||||
<p class="hint text-red-500">Classic JSON save/load coming soon.</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section id="classic-canvas-panel"
|
||||
class="order-1 w-full lg:flex-1 flex flex-col items-stretch shadow-x3 rounded-2xl overflow-hidden bg-white">
|
||||
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">
|
||||
<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 Topper</div>
|
||||
<div class="panel-heading">Nudge & Size Topper</div>
|
||||
<button type="button" id="floating-nudge-toggle" class="btn-dark text-xs px-3 py-2" aria-label="Close nudge panel">×</button>
|
||||
</div>
|
||||
<div class="floating-nudge-body">
|
||||
<div class="floating-nudge-body space-y-3">
|
||||
<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>
|
||||
@ -413,11 +461,140 @@
|
||||
<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" Nodes</button>
|
||||
<button type="button" id="wall-paint-gaps" class="btn-blue text-xs px-2 py-2">Paint 11" Gaps</button>
|
||||
</div>
|
||||
<p class="hint mt-2">Fills only that group; pattern-aware for Grid vs X.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button type="button" id="wall-clear" class="btn-danger text-sm px-3 py-2 flex-1">Clear</button>
|
||||
<button type="button" id="wall-fill-all" class="btn-blue text-sm px-3 py-2 flex-1">Fill All</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section id="wall-canvas-panel"
|
||||
class="order-1 lg:order-2 w-full lg:flex-1 flex flex-col items-stretch rounded-2xl overflow-hidden bg-white/50 shadow-inner ring-1 ring-black/5"
|
||||
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">
|
||||
@ -435,6 +612,71 @@
|
||||
</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">×</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">×</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>
|
||||
@ -444,9 +686,25 @@
|
||||
|
||||
<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
Normal file
58
output_webp/0.svg
Normal file
|
After Width: | Height: | Size: 19 KiB |
59
output_webp/1.svg
Normal file
|
After Width: | Height: | Size: 22 KiB |
58
output_webp/2.svg
Normal file
|
After Width: | Height: | Size: 27 KiB |
58
output_webp/3.svg
Normal file
|
After Width: | Height: | Size: 31 KiB |
57
output_webp/4.svg
Normal file
|
After Width: | Height: | Size: 26 KiB |
57
output_webp/5.svg
Normal file
|
After Width: | Height: | Size: 26 KiB |
58
output_webp/6.svg
Normal file
|
After Width: | Height: | Size: 23 KiB |
57
output_webp/7.svg
Normal file
|
After Width: | Height: | Size: 18 KiB |
57
output_webp/8.svg
Normal file
|
After Width: | Height: | Size: 28 KiB |
57
output_webp/9.svg
Normal file
|
After Width: | Height: | Size: 23 KiB |
233
shared.js
Normal file
@ -0,0 +1,233 @@
|
||||
(() => {
|
||||
'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
@ -1,7 +1,43 @@
|
||||
/* 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;
|
||||
@ -12,6 +48,31 @@ body { color: #1f2937; }
|
||||
}
|
||||
|
||||
/* 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;
|
||||
@ -67,12 +128,14 @@ body { color: #1f2937; }
|
||||
flex-direction: column;
|
||||
gap: .5rem;
|
||||
padding: .5rem;
|
||||
background: rgba(255,255,255,0.6); /* More transparent */
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: .75rem;
|
||||
background: rgba(255,255,255,0.82);
|
||||
border: 1px solid rgba(226,232,240,0.9);
|
||||
border-radius: .9rem;
|
||||
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 {
|
||||
@ -109,6 +172,8 @@ body { color: #1f2937; }
|
||||
|
||||
.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;
|
||||
@ -187,6 +252,34 @@ body { color: #1f2937; }
|
||||
}
|
||||
.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;
|
||||
@ -272,13 +365,13 @@ body { color: #1f2937; }
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.panel-card {
|
||||
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);
|
||||
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);
|
||||
border-radius: 1rem;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.03);
|
||||
box-shadow: 0 12px 30px rgba(15,23,42,0.06);
|
||||
}
|
||||
.control-stack {
|
||||
display: flex;
|
||||
@ -303,6 +396,7 @@ body { color: #1f2937; }
|
||||
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%); }
|
||||
@ -338,18 +432,243 @@ body { color: #1f2937; }
|
||||
@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 don’t 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="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"] {
|
||||
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;
|
||||
@ -367,6 +686,32 @@ body { color: #1f2937; }
|
||||
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;
|
||||
@ -399,6 +744,39 @@ body { color: #1f2937; }
|
||||
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 {
|
||||
@ -406,7 +784,7 @@ body { color: #1f2937; }
|
||||
top: 7rem;
|
||||
bottom: auto;
|
||||
width: 340px;
|
||||
max-height: calc(100vh - 8rem);
|
||||
max-height: calc(93vh - 8rem);
|
||||
border-radius: 1.5rem;
|
||||
position: sticky;
|
||||
overflow-y: auto;
|
||||
@ -414,6 +792,7 @@ body { color: #1f2937; }
|
||||
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
Executable file
@ -0,0 +1,18 @@
|
||||
#!/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/"
|
||||