Compare commits
No commits in common. "04c7310749f571c107f5c8b28b3810660b290533" and "13f4d61fe070f0a2537a13d706ae33ad5dd32200" have entirely different histories.
04c7310749
...
13f4d61fe0
|
|
@ -1 +0,0 @@
|
||||||
{"sessionId":"dec1e8b5-dc16-43d2-b20f-0fad3fe0354e","pid":148363,"procStart":"635354","acquiredAt":1777523259923}
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
# Sonic Universe Theme
|
|
||||||
|
|
||||||
Sonic the Hedgehog universe theme. Cinematic, photorealistic visual style — movie-poster quality renders with dramatic lighting, detailed textures, and dynamic compositions.
|
|
||||||
|
|
||||||
## Visual Style
|
|
||||||
|
|
||||||
- **Cinematic photorealistic** — like Sonic movie posters / Unreal Engine 5 renders
|
|
||||||
- Dramatic lighting, volumetric effects, detailed fur/quill textures
|
|
||||||
- Characters should feel alive and dimensional, not flat or cartoon-like
|
|
||||||
- White or transparent backgrounds for standalone assets
|
|
||||||
- Concept sketches stored in `sketches/` subfolders during exploration phase
|
|
||||||
|
|
||||||
## Character Roster
|
|
||||||
|
|
||||||
### Heroes
|
|
||||||
|
|
||||||
| Character | File | Description | Status |
|
|
||||||
|-----------|------|-------------|--------|
|
|
||||||
| Sonic | `sonic.png` | Blue hedgehog, super speed, iconic quills | Sketch |
|
|
||||||
| Shadow | `shadow.png` | Black/red hedgehog, chaos energy, rival | Sketch |
|
|
||||||
| Tails | `tails.png` | Two-tailed fox, flight, tech genius | Sketch |
|
|
||||||
| Knuckles | `knuckles.png` | Red echidna, super strength, guardian | Sketch |
|
|
||||||
| Amy Rose | `amy.png` | Pink hedgehog, Piko Piko Hammer | Sketch |
|
|
||||||
| Silver | `silver.png` | Silver-white hedgehog, psychokinesis, futuristic | Sketch |
|
|
||||||
| Cream | `cream.png` | Cream rabbit with Chao companion Cheese | Sketch |
|
|
||||||
|
|
||||||
### Villains / Anti-heroes
|
|
||||||
|
|
||||||
| Character | File | Description | Status |
|
|
||||||
|-----------|------|-------------|--------|
|
|
||||||
| Dr. Eggman | `eggman.png` | Main villain, genius inventor, red military jacket | Sketch |
|
|
||||||
| Metal Sonic | `metal-sonic.png` | Robot hedgehog, blue chrome, red eyes | Sketch |
|
|
||||||
| Rouge | `rouge.png` | White bat spy, jewel hunter, bat wings | Sketch |
|
|
||||||
| Chaos | `chaos.png` | Translucent water creature, green glowing brain | Sketch |
|
|
||||||
| Zavok | `zavok.png` | Red demon, leader of Deadly Six, muscular | Sketch |
|
|
||||||
| Infinite | `infinite.png` | Black jackal, silver mask, phantom ruby | Sketch |
|
|
||||||
| Egg Robo | `egg-robo.png` | Egg-shaped robot soldier, red/silver metal | Sketch |
|
|
||||||
|
|
||||||
## Items
|
|
||||||
|
|
||||||
| Item | Description | Status |
|
|
||||||
|------|-------------|--------|
|
|
||||||
| Chaos Emeralds | 7 colored crystals of immense power | Planned |
|
|
||||||
| Gold Rings | Collectible rings, life energy | Planned |
|
|
||||||
| More TBD | — | — |
|
|
||||||
|
|
||||||
## Asset Inventory
|
|
||||||
|
|
||||||
| Category | Path | Count | Description |
|
|
||||||
|----------|------|-------|-------------|
|
|
||||||
| Protagonist sketches | `sketches/protagonists/{name}/` | 7×5 | Base sketch, character sheet, lvl1-3 per hero |
|
|
||||||
| Antagonist sketches | `sketches/antagonists/{name}/` | 7×5 | Base sketch, character sheet, lvl1-3 per villain |
|
|
||||||
|
|
||||||
### Per-character file structure
|
|
||||||
|
|
||||||
Each character folder contains:
|
|
||||||
- `{name}.png` — base presentation sketch (standing pose, white bg)
|
|
||||||
- `{name}-sheet.png` — character sheet (views + expressions, 16:9)
|
|
||||||
- `{name}-lvl1.png` — Level 1: child (age 5-6), cute accessories, shy smile
|
|
||||||
- `{name}-lvl2.png` — Level 2: teen (age 12-14), gadgets, stylish, show-off
|
|
||||||
- `{name}-lvl3.png` — Level 3: young adult (18-20), cyberpunk gear, serious hero/villain
|
|
||||||
|
|
||||||
**Exceptions:**
|
|
||||||
- Eggman: lvl1 = early Robotnik (government suit), lvl2 = unhinged mad scientist, lvl3 = Gerald Robotnik (elderly grandfather)
|
|
||||||
- Metal Sonic: lvl1 = crude prototype, lvl2 = upgraded combat model, lvl3 = ultimate war machine
|
|
||||||
- Egg Robo: lvl1 = basic tin robot, lvl2 = combat model, lvl3 = elite destroyer mech
|
|
||||||
|
|
||||||
**Total: 70 files** (14 characters × 5 files each)
|
|
||||||
|
|
||||||
## Usage in Templates
|
|
||||||
|
|
||||||
All paths relative to project root:
|
|
||||||
- HTML templates: `/assets/themes/sonic/{category}/{file}`
|
|
||||||
- Generator scripts (from docs/): `../../assets/themes/sonic/{category}/{file}`
|
|
||||||
- Generator scripts (from project root): `assets/themes/sonic/{category}/{file}`
|
|
||||||
|
|
||||||
## Color Palette
|
|
||||||
|
|
||||||
TBD — will be defined after initial sketches are reviewed and art direction is confirmed.
|
|
||||||
|
|
||||||
## Reference Policy
|
|
||||||
|
|
||||||
- `heroes/` — use existing hero images as `--ref` for consistency within character
|
|
||||||
- `items/` — use existing items as `--ref` within same item type
|
|
||||||
- Cross-character refs: use any hero sketch to maintain style consistency
|
|
||||||
|
|
||||||
## Adding New Assets
|
|
||||||
|
|
||||||
1. Generate via `/gen-image` with cinematic photorealistic style keywords
|
|
||||||
2. Save sketches/concepts to `{category}/sketches/`
|
|
||||||
3. Final production assets go directly to `{category}/`
|
|
||||||
4. Use `--ref` from same category for visual consistency
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Component Editor — sonic-diamond</title>
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--bg: #f5f5f7;
|
|
||||||
--panel: #ffffff;
|
|
||||||
--line: #e5e5ea;
|
|
||||||
--text: #1c1c1e;
|
|
||||||
--muted: #8e8e93;
|
|
||||||
--accent: #0a84ff;
|
|
||||||
--invalid: #fafafa;
|
|
||||||
--checker: #c8c8c8;
|
|
||||||
}
|
|
||||||
* { box-sizing: border-box; }
|
|
||||||
body { margin: 0; font: 13px/1.4 -apple-system, system-ui, sans-serif;
|
|
||||||
background: var(--bg); color: var(--text); }
|
|
||||||
header { position: sticky; top: 0; z-index: 10;
|
|
||||||
background: var(--panel); border-bottom: 1px solid var(--line);
|
|
||||||
padding: 12px 20px; display: flex; align-items: center; gap: 16px; }
|
|
||||||
header h1 { margin: 0; font-size: 15px; font-weight: 600; }
|
|
||||||
header .meta { color: var(--muted); font-size: 12px; }
|
|
||||||
header button { font: inherit; padding: 6px 14px;
|
|
||||||
background: var(--accent); color: white;
|
|
||||||
border: none; border-radius: 6px; cursor: pointer; }
|
|
||||||
header button:hover { opacity: 0.9; }
|
|
||||||
header button.ghost { background: transparent; color: var(--text);
|
|
||||||
border: 1px solid var(--line); }
|
|
||||||
main { padding: 20px; }
|
|
||||||
|
|
||||||
/* ---- Matrix view ---- */
|
|
||||||
|
|
||||||
section { margin-bottom: 32px; }
|
|
||||||
section > h2 { margin: 0 0 12px; font-size: 13px; font-weight: 600;
|
|
||||||
color: var(--muted); text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px; }
|
|
||||||
.grid { display: grid; gap: 12px; background: var(--panel);
|
|
||||||
padding: 16px; border-radius: 10px; border: 1px solid var(--line); }
|
|
||||||
.cell { display: flex; flex-direction: column; align-items: center;
|
|
||||||
gap: 6px; padding: 12px; border: 1px solid var(--line);
|
|
||||||
border-radius: 8px; background: white; min-height: 110px;
|
|
||||||
justify-content: center; cursor: pointer; transition: transform 0.1s, border-color 0.1s; }
|
|
||||||
.cell:hover { border-color: var(--accent); transform: translateY(-1px); }
|
|
||||||
.cell .preview { position: relative; width: 24mm; height: 24mm;
|
|
||||||
display: flex; align-items: center; justify-content: center; }
|
|
||||||
.cell .label { font-size: 10px; color: var(--muted);
|
|
||||||
text-align: center; line-height: 1.3; }
|
|
||||||
.cell .key { font-family: ui-monospace, Menlo, monospace;
|
|
||||||
font-size: 9px; color: var(--text); }
|
|
||||||
|
|
||||||
/* ---- Single view ---- */
|
|
||||||
|
|
||||||
.single-toolbar { display: flex; align-items: center; gap: 16px;
|
|
||||||
padding: 12px 16px; background: var(--panel);
|
|
||||||
border: 1px solid var(--line); border-radius: 10px;
|
|
||||||
margin-bottom: 16px; }
|
|
||||||
.single-toolbar code { background: var(--bg); padding: 2px 6px;
|
|
||||||
border-radius: 4px; font-size: 12px; }
|
|
||||||
.single-body { display: grid; grid-template-columns: 280px 1fr; gap: 16px; }
|
|
||||||
.single-controls { background: var(--panel); border: 1px solid var(--line);
|
|
||||||
border-radius: 10px; padding: 16px;
|
|
||||||
display: flex; flex-direction: column; gap: 14px;
|
|
||||||
align-self: start; }
|
|
||||||
.ctrl-row { display: flex; flex-direction: column; gap: 4px; }
|
|
||||||
.ctrl-row > label { font-size: 11px; color: var(--muted);
|
|
||||||
text-transform: uppercase; letter-spacing: 0.5px; }
|
|
||||||
.ctrl-row select { font: inherit; padding: 6px 8px;
|
|
||||||
border: 1px solid var(--line); border-radius: 6px;
|
|
||||||
background: white; }
|
|
||||||
.ctrl-row.checkbox { flex-direction: row; align-items: center;
|
|
||||||
gap: 8px; padding-top: 4px; }
|
|
||||||
.ctrl-row.checkbox label { font-size: 13px; color: var(--text);
|
|
||||||
text-transform: none; letter-spacing: 0; }
|
|
||||||
.ctrl-row.checkbox input[disabled] + label { color: var(--muted); }
|
|
||||||
.ctrl-info { margin-top: 12px; padding-top: 12px;
|
|
||||||
border-top: 1px solid var(--line);
|
|
||||||
display: flex; flex-direction: column; gap: 8px;
|
|
||||||
font-size: 11px; color: var(--muted); }
|
|
||||||
.ctrl-info code { font-family: ui-monospace, Menlo, monospace;
|
|
||||||
font-size: 10px; color: var(--text);
|
|
||||||
word-break: break-all; }
|
|
||||||
.single-stage { background: var(--panel); border: 1px solid var(--line);
|
|
||||||
border-radius: 10px; padding: 24px;
|
|
||||||
display: flex; align-items: center; justify-content: center; }
|
|
||||||
.single-frame { position: relative;
|
|
||||||
width: 60mm; height: 60mm;
|
|
||||||
background-color: white;
|
|
||||||
background-image:
|
|
||||||
linear-gradient(45deg, var(--checker) 25%, transparent 25%),
|
|
||||||
linear-gradient(-45deg, var(--checker) 25%, transparent 25%),
|
|
||||||
linear-gradient(45deg, transparent 75%, var(--checker) 75%),
|
|
||||||
linear-gradient(-45deg, transparent 75%, var(--checker) 75%);
|
|
||||||
background-size: 4mm 4mm;
|
|
||||||
background-position: 0 0, 0 2mm, 2mm -2mm, -2mm 0;
|
|
||||||
border: 1px solid var(--line); border-radius: 4px; }
|
|
||||||
|
|
||||||
/* ---- Component centering inside any preview frame ---- */
|
|
||||||
|
|
||||||
.cell .preview sonic-diamond,
|
|
||||||
.single-frame sonic-diamond {
|
|
||||||
position: absolute !important;
|
|
||||||
left: 50% !important;
|
|
||||||
top: 50% !important;
|
|
||||||
transform: translate(-50%, -50%) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#toast { position: fixed; bottom: 20px; left: 50%;
|
|
||||||
transform: translateX(-50%); padding: 10px 20px;
|
|
||||||
background: var(--text); color: white; border-radius: 8px;
|
|
||||||
opacity: 0; pointer-events: none; transition: opacity 0.2s;
|
|
||||||
font-size: 12px; }
|
|
||||||
#toast.show { opacity: 1; }
|
|
||||||
</style>
|
|
||||||
<script type="module" src="./diamond.mjs"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>sonic-diamond</h1>
|
|
||||||
<span class="meta" id="meta"></span>
|
|
||||||
<span style="flex: 1"></span>
|
|
||||||
<button id="btn-copy">Copy snapshot</button>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<div id="matrix-view"></div>
|
|
||||||
|
|
||||||
<div id="single-view" hidden>
|
|
||||||
<div class="single-toolbar">
|
|
||||||
<button class="ghost" id="btn-back">← Matrix</button>
|
|
||||||
<span>Variant: <code id="info-variant"></code></span>
|
|
||||||
</div>
|
|
||||||
<div class="single-body">
|
|
||||||
<aside class="single-controls">
|
|
||||||
<div class="ctrl-row">
|
|
||||||
<label for="ctrl-shape">Shape</label>
|
|
||||||
<select id="ctrl-shape"></select>
|
|
||||||
</div>
|
|
||||||
<div class="ctrl-row">
|
|
||||||
<label for="ctrl-color">Color</label>
|
|
||||||
<select id="ctrl-color"></select>
|
|
||||||
</div>
|
|
||||||
<div class="ctrl-row checkbox">
|
|
||||||
<input type="checkbox" id="ctrl-chipped">
|
|
||||||
<label for="ctrl-chipped" id="ctrl-chipped-label">Chipped</label>
|
|
||||||
</div>
|
|
||||||
<div class="ctrl-info">
|
|
||||||
<div>baseSize: <code id="info-basesize"></code></div>
|
|
||||||
<div>origin: <code id="info-origin"></code></div>
|
|
||||||
<div>image: <code id="info-img"></code></div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
<main class="single-stage">
|
|
||||||
<div class="single-frame" id="single-frame"></div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<div id="toast">Snapshot copied</div>
|
|
||||||
<script type="module" src="./diamond.editor.mjs"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,223 +0,0 @@
|
||||||
/**
|
|
||||||
* Component-editor for sonic-diamond.
|
|
||||||
*
|
|
||||||
* Modes:
|
|
||||||
* - Matrix (default): grid of all valid preset combinations. Click → Single.
|
|
||||||
* - Single: one component instance with dropdown/checkbox controls and
|
|
||||||
* a checker-pattern preview frame. Diamond has no per-variant tuning,
|
|
||||||
* so this mode here only demonstrates the prop-switching flow.
|
|
||||||
*
|
|
||||||
* Mode is driven by the URL hash:
|
|
||||||
* '' → Matrix
|
|
||||||
* 'shape=...&color=...&chipped=...' → Single
|
|
||||||
*
|
|
||||||
* "Copy snapshot" works in either mode and produces JS source ready to
|
|
||||||
* paste between `@editor:anchors-*` and `@editor:variants-*` markers.
|
|
||||||
*/
|
|
||||||
await customElements.whenDefined('sonic-diamond');
|
|
||||||
|
|
||||||
const Comp = customElements.get('sonic-diamond');
|
|
||||||
const { presetProps, chippedShapes } = Comp;
|
|
||||||
|
|
||||||
document.getElementById('meta').textContent =
|
|
||||||
`${Object.keys(Comp.variants).length} variants · baseSize ${Comp.baseSize.w}×${Comp.baseSize.h}mm · origin (${Comp.origin.x}, ${Comp.origin.y})`;
|
|
||||||
|
|
||||||
initMatrix();
|
|
||||||
initSingle();
|
|
||||||
window.addEventListener('hashchange', route);
|
|
||||||
route();
|
|
||||||
|
|
||||||
document.getElementById('btn-copy').addEventListener('click', async () => {
|
|
||||||
const text = serializeSnapshot(Comp.anchorsDefault, Comp.variants);
|
|
||||||
await navigator.clipboard.writeText(text);
|
|
||||||
showToast('Snapshot copied');
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---- Routing ----
|
|
||||||
|
|
||||||
function route() {
|
|
||||||
const params = new URLSearchParams(location.hash.slice(1));
|
|
||||||
const matrixView = document.getElementById('matrix-view');
|
|
||||||
const singleView = document.getElementById('single-view');
|
|
||||||
if (params.has('shape')) {
|
|
||||||
matrixView.hidden = true;
|
|
||||||
singleView.hidden = false;
|
|
||||||
applySingle(params);
|
|
||||||
} else {
|
|
||||||
matrixView.hidden = false;
|
|
||||||
singleView.hidden = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setHashParams(updates) {
|
|
||||||
const params = new URLSearchParams(location.hash.slice(1));
|
|
||||||
for (const [k, v] of Object.entries(updates)) params.set(k, String(v));
|
|
||||||
location.hash = params.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Matrix mode ----
|
|
||||||
|
|
||||||
function initMatrix() {
|
|
||||||
const root = document.getElementById('matrix-view');
|
|
||||||
renderGrid(root, 'Full', false);
|
|
||||||
renderGrid(root, 'Chipped', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderGrid(root, title, chipped) {
|
|
||||||
const shapes = chipped
|
|
||||||
? presetProps.shape.filter(s => chippedShapes.has(s))
|
|
||||||
: presetProps.shape;
|
|
||||||
const colors = presetProps.color;
|
|
||||||
|
|
||||||
const section = document.createElement('section');
|
|
||||||
const h2 = document.createElement('h2');
|
|
||||||
h2.textContent = `${title} — ${shapes.length} × ${colors.length}`;
|
|
||||||
section.appendChild(h2);
|
|
||||||
|
|
||||||
const grid = document.createElement('div');
|
|
||||||
grid.className = 'grid';
|
|
||||||
grid.style.gridTemplateColumns = `repeat(${colors.length}, 1fr)`;
|
|
||||||
|
|
||||||
for (const shape of shapes) {
|
|
||||||
for (const color of colors) {
|
|
||||||
grid.appendChild(makeCell(shape, color, chipped));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section.appendChild(grid);
|
|
||||||
root.appendChild(section);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeCell(shape, color, chipped) {
|
|
||||||
const cell = document.createElement('div');
|
|
||||||
cell.className = 'cell';
|
|
||||||
cell.title = 'Open in single mode';
|
|
||||||
cell.addEventListener('click', () => {
|
|
||||||
location.hash = `shape=${shape}&color=${color}&chipped=${chipped}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const preview = document.createElement('div');
|
|
||||||
preview.className = 'preview';
|
|
||||||
|
|
||||||
const inst = document.createElement('sonic-diamond');
|
|
||||||
inst.setAttribute('shape', shape);
|
|
||||||
inst.setAttribute('color', color);
|
|
||||||
inst.setAttribute('chipped', String(chipped));
|
|
||||||
preview.appendChild(inst);
|
|
||||||
|
|
||||||
const label = document.createElement('div');
|
|
||||||
label.className = 'label';
|
|
||||||
label.innerHTML = `<div>${shape} · ${color}</div>`;
|
|
||||||
|
|
||||||
const key = document.createElement('div');
|
|
||||||
key.className = 'key';
|
|
||||||
key.textContent = inst._variantKey();
|
|
||||||
|
|
||||||
cell.appendChild(preview);
|
|
||||||
cell.appendChild(label);
|
|
||||||
cell.appendChild(key);
|
|
||||||
return cell;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Single mode ----
|
|
||||||
|
|
||||||
let singleInstance = null;
|
|
||||||
|
|
||||||
function initSingle() {
|
|
||||||
fillSelect('ctrl-shape', presetProps.shape);
|
|
||||||
fillSelect('ctrl-color', presetProps.color);
|
|
||||||
|
|
||||||
document.getElementById('ctrl-shape').addEventListener('change', e =>
|
|
||||||
setHashParams({ shape: e.target.value })
|
|
||||||
);
|
|
||||||
document.getElementById('ctrl-color').addEventListener('change', e =>
|
|
||||||
setHashParams({ color: e.target.value })
|
|
||||||
);
|
|
||||||
document.getElementById('ctrl-chipped').addEventListener('change', e =>
|
|
||||||
setHashParams({ chipped: e.target.checked })
|
|
||||||
);
|
|
||||||
document.getElementById('btn-back').addEventListener('click', () => {
|
|
||||||
location.hash = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function applySingle(params) {
|
|
||||||
if (!singleInstance) {
|
|
||||||
singleInstance = document.createElement('sonic-diamond');
|
|
||||||
document.getElementById('single-frame').appendChild(singleInstance);
|
|
||||||
}
|
|
||||||
|
|
||||||
const shape = params.get('shape') ?? presetProps.shape[0];
|
|
||||||
const color = params.get('color') ?? presetProps.color[0];
|
|
||||||
const chipped = params.get('chipped') === 'true';
|
|
||||||
|
|
||||||
singleInstance.setAttribute('shape', shape);
|
|
||||||
singleInstance.setAttribute('color', color);
|
|
||||||
singleInstance.setAttribute('chipped', String(chipped));
|
|
||||||
|
|
||||||
document.getElementById('ctrl-shape').value = shape;
|
|
||||||
document.getElementById('ctrl-color').value = color;
|
|
||||||
const cb = document.getElementById('ctrl-chipped');
|
|
||||||
const cbLabel = document.getElementById('ctrl-chipped-label');
|
|
||||||
const supportsChipped = chippedShapes.has(shape);
|
|
||||||
cb.checked = chipped && supportsChipped;
|
|
||||||
cb.disabled = !supportsChipped;
|
|
||||||
cbLabel.textContent = supportsChipped ? 'Chipped' : 'Chipped (n/a for this shape)';
|
|
||||||
|
|
||||||
const key = singleInstance._variantKey();
|
|
||||||
document.getElementById('info-variant').textContent = key;
|
|
||||||
document.getElementById('info-basesize').textContent = `${Comp.baseSize.w}×${Comp.baseSize.h}mm`;
|
|
||||||
document.getElementById('info-origin').textContent = `(${Comp.origin.x}, ${Comp.origin.y})`;
|
|
||||||
document.getElementById('info-img').textContent = Comp.variants[key]?.img ?? '—';
|
|
||||||
}
|
|
||||||
|
|
||||||
function fillSelect(id, options) {
|
|
||||||
const sel = document.getElementById(id);
|
|
||||||
sel.innerHTML = '';
|
|
||||||
for (const opt of options) {
|
|
||||||
const o = document.createElement('option');
|
|
||||||
o.value = opt;
|
|
||||||
o.textContent = opt;
|
|
||||||
sel.appendChild(o);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Snapshot serialization ----
|
|
||||||
|
|
||||||
function serializeSnapshot(anchorsDefault, variants) {
|
|
||||||
const lines = [];
|
|
||||||
lines.push(' // @editor:anchors-start');
|
|
||||||
lines.push(` static anchorsDefault = ${formatAnchors(anchorsDefault, ' ')};`);
|
|
||||||
lines.push(' // @editor:anchors-end');
|
|
||||||
lines.push('');
|
|
||||||
lines.push(' // @editor:variants-start');
|
|
||||||
lines.push(' static variants = {');
|
|
||||||
for (const [key, v] of Object.entries(variants)) {
|
|
||||||
const a = formatAnchorsInline(v.anchors || {});
|
|
||||||
lines.push(` ${key}: { img: ${JSON.stringify(v.img)}, dx: ${v.dx}, dy: ${v.dy}, scale: ${v.scale}, anchors: ${a} },`);
|
|
||||||
}
|
|
||||||
lines.push(' };');
|
|
||||||
lines.push(' // @editor:variants-end');
|
|
||||||
return lines.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatAnchors(anchors, indent) {
|
|
||||||
const keys = Object.keys(anchors);
|
|
||||||
if (keys.length === 0) return '{}';
|
|
||||||
const inner = keys.map(k => `${indent} ${k}: { x: ${anchors[k].x}, y: ${anchors[k].y} }`).join(',\n');
|
|
||||||
return `{\n${inner},\n${indent}}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatAnchorsInline(anchors) {
|
|
||||||
const keys = Object.keys(anchors);
|
|
||||||
if (keys.length === 0) return '{}';
|
|
||||||
const parts = keys.map(k => `${k}: { x: ${anchors[k].x}, y: ${anchors[k].y} }`);
|
|
||||||
return `{ ${parts.join(', ')} }`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showToast(msg) {
|
|
||||||
const toast = document.getElementById('toast');
|
|
||||||
toast.textContent = msg;
|
|
||||||
toast.classList.add('show');
|
|
||||||
setTimeout(() => toast.classList.remove('show'), 1500);
|
|
||||||
}
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
import { FunPenComponent } from '/src/components/base.mjs';
|
|
||||||
|
|
||||||
class SonicDiamond extends FunPenComponent {
|
|
||||||
static origin = { x: 5, y: 5 };
|
|
||||||
static baseSize = { w: 10, h: 10 };
|
|
||||||
|
|
||||||
static presetProps = {
|
|
||||||
shape: ['classic', 'elongated', 'flat', 'round', 'raw'],
|
|
||||||
color: ['blue', 'cyan', 'green', 'magenta', 'pink', 'red'],
|
|
||||||
chipped: ['false', 'true'],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Chipped variants exist only for these shapes. Other combinations
|
|
||||||
// silently fall back to the non-chipped variant.
|
|
||||||
static chippedShapes = new Set(['classic', 'round']);
|
|
||||||
|
|
||||||
// @editor:anchors-start
|
|
||||||
static anchorsDefault = {};
|
|
||||||
// @editor:anchors-end
|
|
||||||
|
|
||||||
// @editor:variants-start
|
|
||||||
static variants = {
|
|
||||||
classic_blue: { img: '/assets/themes/sonic/items/diamonds/01-classic-blue.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
classic_cyan: { img: '/assets/themes/sonic/items/diamonds/01-classic-cyan.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
classic_green: { img: '/assets/themes/sonic/items/diamonds/01-classic-green.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
classic_magenta: { img: '/assets/themes/sonic/items/diamonds/01-classic-magenta.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
classic_pink: { img: '/assets/themes/sonic/items/diamonds/01-classic-pink.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
classic_red: { img: '/assets/themes/sonic/items/diamonds/01-classic-red.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
|
|
||||||
elongated_blue: { img: '/assets/themes/sonic/items/diamonds/02-elongated-blue.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
elongated_cyan: { img: '/assets/themes/sonic/items/diamonds/02-elongated-cyan.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
elongated_green: { img: '/assets/themes/sonic/items/diamonds/02-elongated-green.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
elongated_magenta: { img: '/assets/themes/sonic/items/diamonds/02-elongated-magenta.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
elongated_pink: { img: '/assets/themes/sonic/items/diamonds/02-elongated-pink.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
elongated_red: { img: '/assets/themes/sonic/items/diamonds/02-elongated-red.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
|
|
||||||
flat_blue: { img: '/assets/themes/sonic/items/diamonds/03-flat-blue.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
flat_cyan: { img: '/assets/themes/sonic/items/diamonds/03-flat-cyan.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
flat_green: { img: '/assets/themes/sonic/items/diamonds/03-flat-green.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
flat_magenta: { img: '/assets/themes/sonic/items/diamonds/03-flat-magenta.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
flat_pink: { img: '/assets/themes/sonic/items/diamonds/03-flat-pink.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
flat_red: { img: '/assets/themes/sonic/items/diamonds/03-flat-red.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
|
|
||||||
round_blue: { img: '/assets/themes/sonic/items/diamonds/04-round-blue.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
round_cyan: { img: '/assets/themes/sonic/items/diamonds/04-round-cyan.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
round_green: { img: '/assets/themes/sonic/items/diamonds/04-round-green.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
round_magenta: { img: '/assets/themes/sonic/items/diamonds/04-round-magenta.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
round_pink: { img: '/assets/themes/sonic/items/diamonds/04-round-pink.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
round_red: { img: '/assets/themes/sonic/items/diamonds/04-round-red.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
|
|
||||||
raw_blue: { img: '/assets/themes/sonic/items/diamonds/05-raw-blue.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
raw_cyan: { img: '/assets/themes/sonic/items/diamonds/05-raw-cyan.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
raw_green: { img: '/assets/themes/sonic/items/diamonds/05-raw-green.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
raw_magenta: { img: '/assets/themes/sonic/items/diamonds/05-raw-magenta.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
raw_pink: { img: '/assets/themes/sonic/items/diamonds/05-raw-pink.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
raw_red: { img: '/assets/themes/sonic/items/diamonds/05-raw-red.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
|
|
||||||
classic_blue_chipped: { img: '/assets/themes/sonic/items/diamonds/06-classic-blue-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
classic_cyan_chipped: { img: '/assets/themes/sonic/items/diamonds/06-classic-cyan-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
classic_green_chipped: { img: '/assets/themes/sonic/items/diamonds/06-classic-green-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
classic_magenta_chipped: { img: '/assets/themes/sonic/items/diamonds/06-classic-magenta-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
classic_pink_chipped: { img: '/assets/themes/sonic/items/diamonds/06-classic-pink-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
classic_red_chipped: { img: '/assets/themes/sonic/items/diamonds/06-classic-red-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
|
|
||||||
round_blue_chipped: { img: '/assets/themes/sonic/items/diamonds/07-round-blue-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
round_cyan_chipped: { img: '/assets/themes/sonic/items/diamonds/07-round-cyan-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
round_green_chipped: { img: '/assets/themes/sonic/items/diamonds/07-round-green-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
round_magenta_chipped: { img: '/assets/themes/sonic/items/diamonds/07-round-magenta-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
round_pink_chipped: { img: '/assets/themes/sonic/items/diamonds/07-round-pink-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
round_red_chipped: { img: '/assets/themes/sonic/items/diamonds/07-round-red-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} },
|
|
||||||
};
|
|
||||||
// @editor:variants-end
|
|
||||||
|
|
||||||
_variantKey() {
|
|
||||||
const shape = this.getAttribute('shape') ?? this.constructor.presetProps.shape[0];
|
|
||||||
const color = this.getAttribute('color') ?? this.constructor.presetProps.color[0];
|
|
||||||
const chipped = this.getAttribute('chipped') === 'true';
|
|
||||||
const supportsChipped = this.constructor.chippedShapes.has(shape);
|
|
||||||
return supportsChipped && chipped
|
|
||||||
? `${shape}_${color}_chipped`
|
|
||||||
: `${shape}_${color}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define('sonic-diamond', SonicDiamond);
|
|
||||||
|
Before Width: | Height: | Size: 2.9 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 710 KiB |
|
Before Width: | Height: | Size: 819 KiB |
|
Before Width: | Height: | Size: 817 KiB |
|
Before Width: | Height: | Size: 880 KiB |
|
Before Width: | Height: | Size: 988 KiB |
|
Before Width: | Height: | Size: 1006 KiB |
|
Before Width: | Height: | Size: 860 KiB |
|
Before Width: | Height: | Size: 755 KiB |
|
Before Width: | Height: | Size: 784 KiB |
|
Before Width: | Height: | Size: 962 KiB |
|
Before Width: | Height: | Size: 737 KiB |
|
Before Width: | Height: | Size: 718 KiB |
|
Before Width: | Height: | Size: 1009 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 862 KiB |
|
Before Width: | Height: | Size: 820 KiB |
|
Before Width: | Height: | Size: 868 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1011 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 972 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 997 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 995 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 998 KiB |
|
Before Width: | Height: | Size: 895 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 987 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1015 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1022 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1005 KiB |
|
Before Width: | Height: | Size: 907 KiB |
|
Before Width: | Height: | Size: 890 KiB |
|
Before Width: | Height: | Size: 911 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 979 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
|
@ -1,15 +0,0 @@
|
||||||
/**
|
|
||||||
* Sonic theme component loader.
|
|
||||||
* Scans the document for `sonic-*` custom elements and lazy-imports
|
|
||||||
* only those component modules. See src/components/CLAUDE.md.
|
|
||||||
*/
|
|
||||||
const PREFIX = 'sonic-';
|
|
||||||
const tags = new Set();
|
|
||||||
document.querySelectorAll('*').forEach(el => {
|
|
||||||
const t = el.tagName.toLowerCase();
|
|
||||||
if (t.startsWith(PREFIX)) tags.add(t);
|
|
||||||
});
|
|
||||||
await Promise.all([...tags].map(t => {
|
|
||||||
const name = t.slice(PREFIX.length);
|
|
||||||
return import(`./components/${name}/${name}.mjs`);
|
|
||||||
}));
|
|
||||||