303 lines
10 KiB
HTML
303 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Asteroid Splitting Editor</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { background: #1a1a2e; color: #e0e0e0; font-family: 'Segoe UI', system-ui, sans-serif; }
|
|
#toolbar {
|
|
position: fixed; top: 0; left: 0; right: 0; z-index: 1000;
|
|
background: #16213e; border-bottom: 1px solid #0f3460;
|
|
display: flex; align-items: center; gap: 12px; padding: 8px 16px; font-size: 14px;
|
|
}
|
|
#toolbar button {
|
|
padding: 6px 14px; border: 1px solid #0f3460; border-radius: 6px;
|
|
background: #1a1a2e; color: #e0e0e0; cursor: pointer; font-size: 13px;
|
|
}
|
|
#toolbar button:hover { background: #0f3460; }
|
|
#toolbar button.primary { background: #533483; border-color: #7b2d8e; }
|
|
#toolbar button.save { background: #1b8a5a; border-color: #239b6e; }
|
|
#page-indicator { color: #a0a0c0; font-weight: 600; min-width: 100px; text-align: center; }
|
|
.spacer { flex: 1; }
|
|
#statusbar {
|
|
position: fixed; bottom: 0; left: 0; right: 0; z-index: 1000;
|
|
background: #16213e; border-top: 1px solid #0f3460;
|
|
padding: 6px 16px; font-size: 12px; color: #a0a0c0;
|
|
}
|
|
#worksheet-container {
|
|
margin-top: 52px; margin-bottom: 32px;
|
|
display: flex; flex-direction: column; align-items: center; gap: 20px; padding: 20px;
|
|
}
|
|
.page-label {
|
|
position: absolute; top: -24px; left: 50%; transform: translateX(-50%);
|
|
background: #533483; color: white; font-size: 11px; font-weight: 700;
|
|
padding: 2px 12px; border-radius: 4px;
|
|
}
|
|
.editor-selected { outline: 3px solid #7c3aed !important; outline-offset: 4px; }
|
|
.editor-changed { position: relative; }
|
|
.editor-changed::after {
|
|
content: ''; position: absolute; top: -2px; right: -2px;
|
|
width: 8px; height: 8px; background: #f97316; border-radius: 50%; z-index: 10;
|
|
}
|
|
#toast {
|
|
position: fixed; bottom: 50px; left: 50%; transform: translateX(-50%);
|
|
background: #533483; color: white; padding: 8px 20px; border-radius: 8px;
|
|
font-size: 13px; font-weight: 600; opacity: 0; transition: opacity 0.3s;
|
|
z-index: 2000; pointer-events: none;
|
|
}
|
|
#toast.show { opacity: 1; }
|
|
#coord-tooltip {
|
|
position: fixed; display: none; background: rgba(0,0,0,0.8); color: #e0e0e0;
|
|
padding: 3px 8px; border-radius: 4px; font-size: 11px; z-index: 2000;
|
|
pointer-events: none; white-space: nowrap;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="toolbar">
|
|
<button id="btn-prev">← Prev</button>
|
|
<span id="page-indicator">Page 1 / ?</span>
|
|
<button id="btn-next">Next →</button>
|
|
<div class="spacer"></div>
|
|
<span id="selection-info" style="color: #c4b5fd; font-size: 12px;">Loading...</span>
|
|
<div class="spacer"></div>
|
|
<button id="btn-reset">Reset Page</button>
|
|
<button id="btn-copy" class="primary">Copy JSON</button>
|
|
<button id="btn-copy-changes" class="primary">Copy Changes</button>
|
|
<button id="btn-save" class="save">Save</button>
|
|
</div>
|
|
<div id="worksheet-container"></div>
|
|
<div id="statusbar">
|
|
<span id="status-text">Keys: +/- scale • [/] rotate • 0 reset</span>
|
|
</div>
|
|
<div id="toast"></div>
|
|
<div id="coord-tooltip"></div>
|
|
|
|
<script src="../../src/editor/editor-core.js"></script>
|
|
<script>
|
|
(function() {
|
|
'use strict';
|
|
|
|
const sectionEls = []; // [{container, bigImg, smallImgs, pageNum, secIndex}]
|
|
const originalTransforms = new Map(); // "pageNum-secIndex" → {scale, rotate}
|
|
let selected = null;
|
|
|
|
// --- Transform helpers ---
|
|
|
|
function parseTransform(img) {
|
|
const t = img.style.transform || '';
|
|
const sm = t.match(/scale\(([\d.]+)\)/);
|
|
const rm = t.match(/rotate\(([-\d.]+)deg\)/);
|
|
return {
|
|
scale: sm ? parseFloat(sm[1]) : 1,
|
|
rotate: rm ? parseFloat(rm[1]) : 0
|
|
};
|
|
}
|
|
|
|
function setTransform(img, scale, rotate) {
|
|
if (scale === 1 && rotate === 0) {
|
|
img.style.transform = '';
|
|
} else {
|
|
img.style.transform = 'scale(' + scale + ') rotate(' + rotate + 'deg)';
|
|
}
|
|
}
|
|
|
|
function secKey(sec) {
|
|
return sec.pageNum + '-' + sec.secIndex;
|
|
}
|
|
|
|
// --- Find all sections in loaded pages ---
|
|
|
|
function findSections(pages) {
|
|
sectionEls.length = 0;
|
|
pages.forEach(function(page, pi) {
|
|
const glows = page.querySelectorAll('.asteroid-glow');
|
|
glows.forEach(function(container, si) {
|
|
const bigImg = container.querySelector('img');
|
|
// Walk up to find the section row containing formula small imgs
|
|
let sectionRow = container.parentElement;
|
|
while (sectionRow && !sectionRow.classList.contains('gap-[8mm]')) {
|
|
sectionRow = sectionRow.parentElement;
|
|
}
|
|
if (!sectionRow) sectionRow = container.parentElement;
|
|
|
|
const allImgs = sectionRow.querySelectorAll('img[src*="pack3-asteroids"]');
|
|
const smallImgs = [];
|
|
allImgs.forEach(function(img) { if (img !== bigImg) smallImgs.push(img); });
|
|
|
|
const sec = { container, bigImg, smallImgs, pageNum: pi + 1, secIndex: si };
|
|
sectionEls.push(sec);
|
|
|
|
// Store original transform (from template, before data.json applied)
|
|
originalTransforms.set(secKey(sec), { scale: 1, rotate: 0 });
|
|
|
|
container.style.cursor = 'pointer';
|
|
container.addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
selectSection(sec);
|
|
});
|
|
});
|
|
});
|
|
|
|
document.getElementById('status-text').textContent =
|
|
sectionEls.length + ' sections. Keys: +/- scale \u2022 [/] rotate (Shift=fine) \u2022 0 reset';
|
|
document.getElementById('selection-info').textContent = 'Click a big asteroid to select';
|
|
}
|
|
|
|
// --- Selection ---
|
|
|
|
function selectSection(sec) {
|
|
deselectAll();
|
|
selected = sec;
|
|
sec.container.classList.add('editor-selected');
|
|
updateInfo();
|
|
}
|
|
|
|
function deselectAll() {
|
|
if (selected) {
|
|
selected.container.classList.remove('editor-selected');
|
|
selected = null;
|
|
}
|
|
updateInfo();
|
|
}
|
|
|
|
function updateInfo() {
|
|
const el = document.getElementById('selection-info');
|
|
if (!selected) { el.textContent = 'Click a big asteroid to select'; return; }
|
|
const t = parseTransform(selected.bigImg);
|
|
el.textContent = 'Page ' + selected.pageNum + ' Sec ' + (selected.secIndex + 1) +
|
|
' \u2014 scale: ' + t.scale.toFixed(2) + ' rotate: ' + t.rotate.toFixed(1) + '\u00B0';
|
|
}
|
|
|
|
// --- Apply transform to section (big + small imgs) ---
|
|
|
|
function applyToSection(sec, scale, rotate) {
|
|
setTransform(sec.bigImg, scale, rotate);
|
|
sec.smallImgs.forEach(function(img) { setTransform(img, scale, rotate); });
|
|
markChanged(sec);
|
|
updateInfo();
|
|
}
|
|
|
|
// --- Change tracking ---
|
|
|
|
function markChanged(sec) {
|
|
const orig = originalTransforms.get(secKey(sec));
|
|
const cur = parseTransform(sec.bigImg);
|
|
const changed = Math.abs(cur.scale - orig.scale) > 0.001 || Math.abs(cur.rotate - orig.rotate) > 0.1;
|
|
sec.container.classList.toggle('editor-changed', changed);
|
|
}
|
|
|
|
// --- Apply data.json to DOM (called by EditorCore.onDataLoaded) ---
|
|
|
|
function applyData(data) {
|
|
if (!data || !data.pages) return;
|
|
let count = 0;
|
|
data.pages.forEach(function(p) {
|
|
(p.sections || []).forEach(function(s) {
|
|
const sec = sectionEls.find(function(el) {
|
|
return el.pageNum === p.page && el.secIndex === s.index;
|
|
});
|
|
if (sec) {
|
|
const scale = s.scale ?? 1;
|
|
const rotate = s.rotate ?? 0;
|
|
applyToSection(sec, scale, rotate);
|
|
// After applying saved data, update originals so "changed" is relative to saved state
|
|
originalTransforms.set(secKey(sec), { scale, rotate });
|
|
sec.container.classList.remove('editor-changed');
|
|
count++;
|
|
}
|
|
});
|
|
});
|
|
if (count > 0) core.showToast('Loaded ' + count + ' edits');
|
|
}
|
|
|
|
// --- Serialization ---
|
|
|
|
function buildConfig(changesOnly) {
|
|
const pagesMap = {};
|
|
sectionEls.forEach(function(sec) {
|
|
const cur = parseTransform(sec.bigImg);
|
|
const orig = originalTransforms.get(secKey(sec));
|
|
|
|
if (changesOnly) {
|
|
const changed = Math.abs(cur.scale - orig.scale) > 0.001 || Math.abs(cur.rotate - orig.rotate) > 0.1;
|
|
if (!changed) return;
|
|
}
|
|
|
|
if (!pagesMap[sec.pageNum]) pagesMap[sec.pageNum] = { page: sec.pageNum, sections: [] };
|
|
pagesMap[sec.pageNum].sections.push({
|
|
index: sec.secIndex,
|
|
scale: +cur.scale.toFixed(3),
|
|
rotate: +cur.rotate.toFixed(1)
|
|
});
|
|
});
|
|
return { pages: Object.values(pagesMap) };
|
|
}
|
|
|
|
// --- Reset ---
|
|
|
|
function resetCurrentPage(pageNum) {
|
|
sectionEls.forEach(function(sec) {
|
|
if (sec.pageNum !== pageNum) return;
|
|
const orig = originalTransforms.get(secKey(sec));
|
|
applyToSection(sec, orig.scale, orig.rotate);
|
|
sec.container.classList.remove('editor-changed');
|
|
});
|
|
if (selected && selected.pageNum === pageNum) updateInfo();
|
|
}
|
|
|
|
// --- Keyboard ---
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') { deselectAll(); return; }
|
|
if (!selected) return;
|
|
|
|
const t = parseTransform(selected.bigImg);
|
|
const step = e.shiftKey ? 0.01 : 0.05;
|
|
const rotStep = e.shiftKey ? 1 : 5;
|
|
|
|
switch (e.key) {
|
|
case '+': case '=':
|
|
applyToSection(selected, Math.min(2, +(t.scale + step).toFixed(3)), t.rotate);
|
|
e.preventDefault(); break;
|
|
case '-': case '_':
|
|
applyToSection(selected, Math.max(0.3, +(t.scale - step).toFixed(3)), t.rotate);
|
|
e.preventDefault(); break;
|
|
case '[':
|
|
applyToSection(selected, t.scale, +(t.rotate - rotStep).toFixed(1));
|
|
e.preventDefault(); break;
|
|
case ']':
|
|
applyToSection(selected, t.scale, +(t.rotate + rotStep).toFixed(1));
|
|
e.preventDefault(); break;
|
|
case '0':
|
|
applyToSection(selected, 1, 0);
|
|
e.preventDefault(); break;
|
|
}
|
|
});
|
|
|
|
document.addEventListener('click', function(e) {
|
|
if (selected && !e.target.closest('.asteroid-glow')) {
|
|
deselectAll();
|
|
}
|
|
});
|
|
|
|
// --- Init via EditorCore ---
|
|
|
|
const core = EditorCore.init({
|
|
taskType: 'asteroid-splitting',
|
|
serialize: buildConfig,
|
|
onReset: resetCurrentPage,
|
|
onReady: function(pages) {
|
|
findSections(pages);
|
|
},
|
|
onDataLoaded: function(data) {
|
|
if (data) applyData(data);
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|