test17.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Puzzle Layout Tester v16 (Wide & Scroll)</title>
<style>
/* --- Core Layout --- */
body {
font-family: 'Segoe UI', sans-serif;
background-color: #f0f2f5;
margin: 0; padding: 0;
overflow: hidden;
height: 100vh;
display: flex;
flex-direction: column;
user-select: none;
}
/* --- Top Control Bar (Expanded) --- */
.top-bar {
height: 280px; /* 높이 넉넉하게 확보 */
background: rgba(255, 255, 255, 0.98);
border-bottom: 1px solid #ccc;
display: flex;
justify-content: center;
align-items: flex-start;
gap: 20px; /* 패널 간격 증가 */
padding-top: 15px;
z-index: 2000;
box-shadow: 0 5px 15px rgba(0,0,0,0.05);
flex-shrink: 0;
}
/* --- Control Panel (Wide & Scrollable) --- */
.control-panel {
background: #f8f9fa;
border: 1px solid #ccc;
border-radius: 8px;
padding: 12px;
display: flex;
flex-direction: column;
align-items: center;
/* [요청 2] PC 전용 넓은 폭 */
width: 340px;
height: 95%; /* 상단바 꽉 채우기 */
box-sizing: border-box;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
/* [요청 1] 내용 넘치면 스크롤 발생 */
overflow-y: auto;
overflow-x: hidden;
}
/* 스크롤바 스타일링 (Chrome/Safari) */
.control-panel::-webkit-scrollbar { width: 6px; }
.control-panel::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; }
.control-panel::-webkit-scrollbar-thumb { background: #ccc; border-radius: 4px; }
.control-panel::-webkit-scrollbar-thumb:hover { background: #bbb; }
.panel-title { font-weight: bold; font-size: 14px; margin-bottom: 10px; width: 100%; text-align: center; border-bottom: 2px solid #ddd; padding-bottom: 5px; color:#333; }
.sub-title { font-size: 11px; color:#555; font-weight:bold; margin-top:10px; margin-bottom:6px; width:100%; text-align:left; padding-left:8px; border-left:3px solid #aaa; background: rgba(0,0,0,0.03); padding-top:2px; padding-bottom:2px;}
.btn-row { display: flex; gap: 6px; justify-content: center; width: 100%; margin-bottom: 6px; flex-wrap: wrap; }
.sm-btn { padding: 5px 12px; font-size: 11px; cursor: pointer; background: #fff; border: 1px solid #999; border-radius: 4px; min-width: 30px; transition: 0.1s; }
.sm-btn:hover { background: #eee; }
.file-btn { background: #e3f2fd; border-color: #2196f3; color: #1565c0; width: 45%; }
.toggle-row {
display: flex; align-items: center; justify-content: center; gap: 10px; margin-bottom: 10px;
background: #fff; padding: 5px 15px; border-radius: 20px; border: 1px solid #ccc; width: fit-content;
}
.toggle-label { font-size: 12px; font-weight: bold; color: #555; cursor: pointer; }
.toggle-switch {
position: relative; width: 40px; height: 20px; background: #ccc; border-radius: 20px; cursor: pointer; transition: 0.3s;
}
.toggle-switch::after {
content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
background: white; border-radius: 50%; transition: 0.3s; box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.toggle-active { background: #2196f3; }
.toggle-active::after { left: 22px; }
.slider-row { width: 100%; display: flex; justify-content: space-between; align-items: center; font-size: 11px; margin-top: 5px; }
input[type=range] { flex: 1; margin: 0 10px; cursor: pointer; }
/* --- [요청 2] New Rotator Layout (Side by Side) --- */
.rotator-wrapper-wide {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
background: white;
padding: 8px;
border: 1px dashed #ddd;
border-radius: 6px;
box-sizing: border-box;
}
.ball-area {
display: flex; flex-direction: column; align-items: center; gap: 4px;
border-right: 1px solid #eee; padding-right: 10px; margin-right: 10px;
}
.slider-area {
display: flex; flex-direction: column; gap: 8px; flex: 1;
}
.slider-item {
display: flex; align-items: center; gap: 5px; font-size: 11px;
}
.slider-label-mini { width: 40px; font-weight: bold; color: #555; }
.trackball-container { width: 50px; height: 50px; perspective: 200px; }
.trackball {
width: 100%; height: 100%; border-radius: 50%;
background: radial-gradient(circle at 30% 30%, #fff, #bbb, #444);
box-shadow: 0 3px 6px rgba(0,0,0,0.3); cursor: grab;
}
.trackball:active { cursor: grabbing; transform: scale(0.95); }
.trackball::after {
content: ''; position: absolute; top:0; left:0; width:100%; height:100%; border-radius: 50%;
background: linear-gradient(45deg, transparent 45%, rgba(0,0,0,0.3) 45%, rgba(0,0,0,0.3) 55%, transparent 55%),
linear-gradient(-45deg, transparent 45%, rgba(0,0,0,0.3) 45%, rgba(0,0,0,0.3) 55%, transparent 55%);
pointer-events: none;
}
/* D-Pad Horizontal Layout */
.d-pad-wide {
display: flex; align-items: center; justify-content: center; gap: 5px;
background: #fff; padding: 5px; border-radius: 4px; border: 1px solid #eee; width: 100%; box-sizing: border-box;
}
.d-btn { padding: 2px 8px; font-size: 10px; cursor: pointer; background: #fff; border: 1px solid #999; border-radius: 3px; min-width: 24px;}
.d-btn:hover { background: #ddd; }
/* --- 3D Scene --- */
.scene-split {
flex: 1; display: flex; width: 100%; height: 100%; position: relative;
background: radial-gradient(circle at center, #f0f0f0, #dcdcdc);
overflow: hidden;
}
.viewport {
flex: 1; position: relative; overflow: hidden;
perspective: 1000px; display: flex; justify-content: center; align-items: center;
}
.divider { width: 2px; background: rgba(0,0,0,0.1); z-index: 100; }
.world-root { position: relative; width: 0; height: 0; transform-style: preserve-3d; transition: transform 0.1s linear; }
.board-plane { position: absolute; top: 0; left: 0; width: 0; height: 0; transform-style: preserve-3d; }
/* --- Cells --- */
.cell {
position: absolute; transform-style: preserve-3d; cursor: pointer;
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.3s;
}
.face-top {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background-color: #f0f0f0; display: flex; justify-content: center; align-items: center;
font-size: 10px; font-weight: bold; color: #555;
backface-visibility: hidden; border: 1px solid rgba(0,0,0,0.15); box-sizing: border-box;
}
.hex .face-top {
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
border: none; filter: drop-shadow(0 0 1px #888);
}
.wall {
position: absolute; background-color: var(--pillar-color, #a5d6a7);
backface-visibility: visible; border: 1px solid rgba(0,0,0,0.05); pointer-events: none;
}
:root { --lift-h: 40px; --face-color: #2e7d32; --pillar-color: #81c784; }
/* --- States --- */
.cell.clicked { z-index: 1000 !important; }
.cell.clicked .face-top {
background-color: var(--face-color) !important; color: white !important;
filter: none; border: 1px solid rgba(255,255,255,0.4);
}
.cell:hover { transform: translateZ(10px); z-index: 200; }
.cell:hover .face-top { background-color: #fff9c4; color: black; }
.cell.highlight .face-top { background-color: #fff9c4 !important; box-shadow: inset 0 0 10px gold; }
.zone-0 .face-top { background-color: #ffffff; }
.zone-1 .face-top { background-color: #e8ebf0; }
.hex.zone-null { display: none !important; }
.square.zone-null { pointer-events: none; }
.square.zone-null .face-top {
background-color: #e0e0e0;
background-image: repeating-linear-gradient(45deg, transparent, transparent 5px, rgba(0,0,0,0.05) 5px, rgba(0,0,0,0.05) 10px);
color: #ccc; border-color: #ddd;
}
.square.zone-null:hover { transform: none !important; }
.square.zone-null .face-top { box-shadow: none; }
#file-input { display: none; }
</style>
</head>
<body>
<div class="top-bar">
<div class="control-panel">
<div class="panel-title">Global Settings</div>
<div class="toggle-row" onclick="toggleViewMode()">
<span class="toggle-label" id="lbl-default">Default</span>
<div id="view-toggle" class="toggle-switch"></div>
<span class="toggle-label" id="lbl-hex" style="color:#aaa;">Hexagon</span>
</div>
<div class="btn-row">
<button class="sm-btn" onclick="changeMode(-1)"><</button>
<span id="indicator" style="font-weight:bold; margin:0 5px;">■ □ □ □</span>
<button class="sm-btn" onclick="changeMode(1)">></button>
</div>
<div id="mode-desc" style="font-size:10px; margin-bottom:5px; text-align:center;">기본 모드</div>
<div class="slider-row" style="background:#e8f5e9; padding:8px; border-radius:4px; margin-top: auto;">
<span style="color:#2e7d32; font-weight:bold;">Pop-Up Height</span>
<input type="range" min="0" max="100" value="40" oninput="updateLift(this.value)">
<span id="val-lift" style="width:25px; text-align:right;">40</span>
</div>
<div class="sub-title" style="margin-top:10px;">Save/Load</div>
<div class="btn-row">
<button class="sm-btn file-btn" onclick="saveToJson()">Save JSON</button>
<button class="sm-btn file-btn" onclick="document.getElementById('file-input').click()">Load JSON</button>
<input type="file" id="file-input" accept=".json" onchange="loadFromJson(this)">
</div>
<div style="font-size:9px; color:#888; text-align:center; margin-top:2px;">*Auto-saved</div>
</div>
<div class="control-panel">
<div class="panel-title">Hexagon</div>
<div class="sub-title">Rotation & View</div>
<div class="rotator-wrapper-wide">
<div class="ball-area">
<div class="trackball-container"><div id="ball-hex" class="trackball"></div></div>
<div style="font-size:9px; color:#888;">Trackball</div>
</div>
<div class="slider-area">
<div class="slider-item">
<span class="slider-label-mini">Tilt (X)</span>
<input type="range" id="slider-hex-x" min="0" max="85" value="55" oninput="manualRotate('hex', 'x', this.value)">
</div>
<div class="slider-item">
<span class="slider-label-mini">Spin (Z)</span>
<input type="range" id="slider-hex-z" min="-180" max="180" value="0" oninput="manualRotate('hex', 'z', this.value)">
</div>
</div>
</div>
<div class="sub-title">Position & Zoom</div>
<div class="d-pad-wide">
<button class="d-btn" onclick="moveCam('hex', 0, -20)">▲</button>
<button class="d-btn" onclick="moveCam('hex', 0, 20)">▼</button>
<button class="d-btn" onclick="moveCam('hex', -20, 0)">◀</button>
<button class="d-btn" onclick="moveCam('hex', 20, 0)">▶</button>
<button class="d-btn" onclick="resetCam('hex')" title="Reset" style="border-color:#f44336; color:#f44336;">R</button>
</div>
<div class="slider-row" style="margin-top:5px;">
<span style="font-weight:bold;">Zoom</span>
<input type="range" min="-300" max="300" value="0" step="10" oninput="zoomCam('hex', this.value)" id="zoom-hex">
</div>
<div class="sub-title">Layout Data</div>
<div class="btn-row">
<button class="sm-btn" onclick="updateScale('hex', 'w', 0.1)">W+</button><button class="sm-btn" onclick="updateScale('hex', 'w', -0.1)">W-</button>
<button class="sm-btn" onclick="updateScale('hex', 'h', 0.1)">H+</button><button class="sm-btn" onclick="updateScale('hex', 'h', -0.1)">H-</button>
</div>
<div class="slider-row"><span>Cell Gap</span><input type="range" max="20" value="0" oninput="updateGap('hex','cell',this.value)" id="gap-hex-c"></div>
<div class="slider-row"><span>Zone Gap</span><input type="range" max="40" value="0" oninput="updateGap('hex','zone',this.value)" id="gap-hex-z"></div>
</div>
<div class="control-panel">
<div class="panel-title">Square</div>
<div class="sub-title">Rotation & View</div>
<div class="rotator-wrapper-wide">
<div class="ball-area">
<div class="trackball-container"><div id="ball-sq" class="trackball"></div></div>
<div style="font-size:9px; color:#888;">Trackball</div>
</div>
<div class="slider-area">
<div class="slider-item">
<span class="slider-label-mini">Tilt (X)</span>
<input type="range" id="slider-sq-x" min="0" max="85" value="55" oninput="manualRotate('sq', 'x', this.value)">
</div>
<div class="slider-item">
<span class="slider-label-mini">Spin (Z)</span>
<input type="range" id="slider-sq-z" min="-180" max="180" value="0" oninput="manualRotate('sq', 'z', this.value)">
</div>
</div>
</div>
<div class="sub-title">Position & Zoom</div>
<div class="d-pad-wide">
<button class="d-btn" onclick="moveCam('sq', 0, -20)">▲</button>
<button class="d-btn" onclick="moveCam('sq', 0, 20)">▼</button>
<button class="d-btn" onclick="moveCam('sq', -20, 0)">◀</button>
<button class="d-btn" onclick="moveCam('sq', 20, 0)">▶</button>
<button class="d-btn" onclick="resetCam('sq')" title="Reset" style="border-color:#f44336; color:#f44336;">R</button>
</div>
<div class="slider-row" style="margin-top:5px;">
<span style="font-weight:bold;">Zoom</span>
<input type="range" min="-300" max="300" value="0" step="10" oninput="zoomCam('sq', this.value)" id="zoom-sq">
</div>
<div class="sub-title">Layout Data</div>
<div class="btn-row">
<button class="sm-btn" onclick="updateScale('sq', 'w', 2)">W+</button><button class="sm-btn" onclick="updateScale('sq', 'w', -2)">W-</button>
<button class="sm-btn" onclick="updateScale('sq', 'h', 2)">H+</button><button class="sm-btn" onclick="updateScale('sq', 'h', -2)">H-</button>
</div>
<div class="slider-row"><span>Cell Gap</span><input type="range" max="20" value="0" oninput="updateGap('sq','cell',this.value)" id="gap-sq-c"></div>
<div class="slider-row"><span>Zone Gap</span><input type="range" max="40" value="0" oninput="updateGap('sq','zone',this.value)" id="gap-sq-z"></div>
</div>
</div>
<div class="scene-split">
<div class="viewport"><div id="hex-root" class="world-root"><div id="hex-board" class="board-plane"></div></div></div>
<div class="divider"></div>
<div class="viewport"><div id="sq-root" class="world-root"><div id="sq-board" class="board-plane"></div></div></div>
</div>
<script>
const GRID_SIZE = 9;
const BASE_HEX_SIDE = 24;
const BASE_SQ_SIDE = 35;
// Default State
const defaultState = {
clicked: [],
modeIdx: 0,
hexView: false,
scales: { hex: { w: 1.0, h: 1.0 }, sq: { w: BASE_SQ_SIDE, h: BASE_SQ_SIDE } },
gaps: { hex: { cell: 0, zone: 0 }, sq: { cell: 0, zone: 0 } },
lift: 40,
camera: {
hex: { panX: 0, panY: 0, zoom: 0, rotX: 55, rotZ: 0 },
sq: { panX: 0, panY: 0, zoom: 0, rotX: 55, rotZ: 0 }
}
};
let state = JSON.parse(JSON.stringify(defaultState));
state.clicked = new Set();
const MODES = [
{ label: "■ □ □ □", desc: "1. 기본 (Pointy)", rotate: false, vec_q: [1, 0], vec_r: [0.5, 0.75] },
{ label: "□ ■ □ □", desc: "2. Y반전 (Pointy)", rotate: false, vec_q: [1, 0], vec_r: [-0.5, 0.75] },
{ label: "□ □ ■ □", desc: "3. 세로 (Flat) - 우상향", rotate: true, vec_r: [0, 1], vec_q: [0.75, -0.5] },
{ label: "□ □ □ ■", desc: "4. 세로 (Flat) - 우하향", rotate: true, vec_r: [0, 1], vec_q: [0.75, 0.5] }
];
const hexBoard = document.getElementById('hex-board');
const sqBoard = document.getElementById('sq-board');
const hexRoot = document.getElementById('hex-root');
const sqRoot = document.getElementById('sq-root');
function init() {
loadFromLocalStorage();
createGrids();
setupTrackball('ball-hex', 'hex');
setupTrackball('ball-sq', 'sq');
syncUI();
updateView();
updateCamera('hex'); updateCamera('sq');
}
// --- Persistence ---
function saveToLocalStorage() {
const saveObj = { ...state, clicked: Array.from(state.clicked) };
localStorage.setItem('puzzleConfig_v16', JSON.stringify(saveObj));
}
function loadFromLocalStorage() {
try {
const raw = localStorage.getItem('puzzleConfig_v16');
if (raw) {
const loaded = JSON.parse(raw);
if (loaded.camera && loaded.scales) {
state = loaded;
state.clicked = new Set(loaded.clicked);
return;
}
}
} catch (e) {
console.error("Load failed", e);
localStorage.removeItem('puzzleConfig_v16');
}
state = JSON.parse(JSON.stringify(defaultState));
state.clicked = new Set();
}
function saveToJson() {
const saveObj = { ...state, clicked: Array.from(state.clicked) };
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(saveObj, null, 2));
const downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute("download", "puzzle_layout_save.json");
document.body.appendChild(downloadAnchorNode);
downloadAnchorNode.click();
downloadAnchorNode.remove();
}
function loadFromJson(input) {
const file = input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const loaded = JSON.parse(e.target.result);
state = loaded;
state.clicked = new Set(loaded.clicked);
syncUI();
createGrids();
updateView();
updateCamera('hex'); updateCamera('sq');
saveToLocalStorage();
} catch(err) {
alert("Invalid JSON file");
}
};
reader.readAsText(file);
input.value = '';
}
function syncUI() {
document.getElementById('val-lift').innerText = state.lift;
document.querySelector('input[oninput*="updateLift"]').value = state.lift;
const toggle = document.getElementById('view-toggle');
const lblDef = document.getElementById('lbl-default');
const lblHex = document.getElementById('lbl-hex');
if(state.hexView) {
toggle.classList.add('toggle-active');
lblDef.style.color = '#aaa'; lblHex.style.color = '#1565c0';
} else {
toggle.classList.remove('toggle-active');
lblDef.style.color = '#1565c0'; lblHex.style.color = '#aaa';
}
['hex', 'sq'].forEach(t => {
document.getElementById(`slider-${t}-x`).value = state.camera[t].rotX;
document.getElementById(`slider-${t}-z`).value = state.camera[t].rotZ;
document.getElementById(`zoom-${t}`).value = state.camera[t].zoom;
document.getElementById(`gap-${t}-c`).value = state.gaps[t].cell;
document.getElementById(`gap-${t}-z`).value = state.gaps[t].zone;
});
}
// --- Rotation Logic ---
function updateRotators(target) {
const c = state.camera[target];
const ball = document.getElementById('ball-'+target);
ball.style.transform = `rotateX(${c.rotX}deg) rotateZ(${c.rotZ}deg)`;
document.getElementById(`slider-${target}-x`).value = c.rotX;
document.getElementById(`slider-${target}-z`).value = c.rotZ;
}
function manualRotate(target, axis, val) {
state.camera[target][axis === 'x' ? 'rotX' : 'rotZ'] = parseFloat(val);
updateCamera(target);
saveToLocalStorage();
}
function setupTrackball(id, t) {
const ball = document.getElementById(id);
let drag = false, sx, sy, srx, srz;
ball.onmousedown = e => {
drag=true; sx=e.clientX; sy=e.clientY;
srx=state.camera[t].rotX; srz=state.camera[t].rotZ;
ball.style.cursor='grabbing';
window.addEventListener('mousemove', move);
window.addEventListener('mouseup', up);
};
function move(e) {
if(!drag)return;
const dx=e.clientX-sx, dy=e.clientY-sy;
state.camera[t].rotX = Math.max(0, Math.min(85, srx+dy));
state.camera[t].rotZ = srz+(dx*0.5);
updateCamera(t);
saveToLocalStorage();
}
function up() {
drag=false; ball.style.cursor='grab';
window.removeEventListener('mousemove', move);
window.removeEventListener('mouseup', up);
}
}
function updateCamera(target) {
const root = target === 'hex' ? hexRoot : sqRoot;
const c = state.camera[target];
root.style.transform = `translateX(${c.panX}px) translateY(${c.panY}px) translateZ(${c.zoom}px) rotateX(${c.rotX}deg) rotateZ(${c.rotZ}deg)`;
updateRotators(target);
}
function moveCam(t, dx, dy) { state.camera[t].panX += dx; state.camera[t].panY += dy; updateCamera(t); saveToLocalStorage(); }
function zoomCam(t, val) { state.camera[t].zoom = parseInt(val); updateCamera(t); saveToLocalStorage(); }
function resetCam(t) {
state.camera[t] = { panX: 0, panY: 0, zoom: 0, rotX: 55, rotZ: 0 };
syncUI(); updateCamera(t); saveToLocalStorage();
}
function toggleViewMode() {
state.hexView = !state.hexView;
syncUI();
if(state.hexView) cleanupInvalidSelections();
createGrids(); updateView(); renderClickState();
saveToLocalStorage();
}
function cleanupInvalidSelections() {
const toRemove = [];
state.clicked.forEach(key => {
const [q, r] = key.split(',').map(Number);
if (getZoneType(q, r) === 'zone-null') toRemove.push(key);
});
toRemove.forEach(k => state.clicked.delete(k));
}
function getZoneType(q, r) {
if (!state.hexView) return (Math.floor(q/3) + Math.floor(r/3)) % 2 === 0 ? 'zone-0' : 'zone-1';
const dq = q - 4; const dr = r - 4; const ds = -dq - dr;
const dist = Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
if (dist > 4) return 'zone-null';
return (dist % 2 === 0) ? 'zone-0' : 'zone-1';
}
function createGrids() {
hexBoard.innerHTML = ''; sqBoard.innerHTML = '';
for(let r=0; r<GRID_SIZE; r++){
for(let q=0; q<GRID_SIZE; q++){
const id = `${q},${r}`;
const zoneType = getZoneType(q, r);
const hex = document.createElement('div');
hex.className = `cell hex ${zoneType}`;
hex.dataset.id = id; hex.dataset.q = q; hex.dataset.r = r;
hex.innerHTML = `<div class="face-top"><span>${id}</span></div>`;
addEvents(hex, q, r);
hexBoard.appendChild(hex);
const sq = document.createElement('div');
sq.className = `cell square ${zoneType}`;
sq.dataset.id = id; sq.dataset.q = q; sq.dataset.r = r;
sq.innerHTML = `<div class="face-top"><span>${id}</span></div>`;
addEvents(sq, q, r);
sqBoard.appendChild(sq);
}
}
}
function addEvents(el, q, r) {
el.onmousedown = e => e.stopPropagation();
el.onclick = e => { e.stopPropagation(); toggleClick(q, r); };
el.onmouseenter = () => highlight(q, r);
el.onmouseleave = () => unhighlight();
}
function toggleClick(q, r) {
if (state.hexView && getZoneType(q,r) === 'zone-null') return;
const key = `${q},${r}`;
if (state.clicked.has(key)) state.clicked.delete(key);
else state.clicked.add(key);
renderClickState();
saveToLocalStorage();
}
function renderClickState() {
document.querySelectorAll('.cell').forEach(el => {
if (el.classList.contains('zone-null')) return;
const isClicked = state.clicked.has(el.dataset.id);
if (isClicked) {
el.classList.add('clicked');
el.style.transform = `translate(-50%, -50%) translate3d(${el.dataset.tx}px, ${el.dataset.ty}px, ${state.lift}px) rotate(${el.dataset.rot}deg)`;
const w = parseFloat(el.style.width); const h = parseFloat(el.style.height);
if(el.classList.contains('hex')) createHexWalls(el, w, h, state.lift);
else createSquareWalls(el, w, h, state.lift);
} else {
el.classList.remove('clicked');
el.style.transform = `translate(-50%, -50%) translate3d(${el.dataset.tx}px, ${el.dataset.ty}px, 0px) rotate(${el.dataset.rot}deg)`;
el.querySelectorAll('.wall').forEach(w => w.remove());
}
});
}
function highlight(q, r) {
if (state.hexView && getZoneType(q,r) === 'zone-null') return;
document.querySelectorAll(`[data-id="${q},${r}"]`).forEach(el => el.classList.add('highlight'));
}
function unhighlight() { document.querySelectorAll('.highlight').forEach(el => el.classList.remove('highlight')); }
function createSquareWalls(cell, w, h, depth) {
cell.querySelectorAll('.wall').forEach(el => el.remove());
const edges = [
{ x: w/2, y: 0, ang: 0, len: w }, { x: w, y: h/2, ang: 90, len: h },
{ x: w/2, y: h, ang: 180, len: w }, { x: 0, y: h/2, ang: 270, len: h }
];
edges.forEach(e => {
const div = document.createElement('div');
div.className = 'wall'; div.style.width = `${e.len + 1}px`; div.style.height = `${depth}px`;
div.style.left = `${e.x}px`; div.style.top = `${e.y}px`;
div.style.transformOrigin = 'top center';
div.style.transform = `translate(-50%, 0) rotateZ(${e.ang}deg) rotateX(-90deg)`;
cell.appendChild(div);
});
}
function createHexWalls(cell, w, h, depth) {
cell.querySelectorAll('.wall').forEach(el => el.remove());
const points = [
{x: 0.5 * w, y: 0}, {x: 1.0 * w, y: 0.25 * h}, {x: 1.0 * w, y: 0.75 * h},
{x: 0.5 * w, y: 1.0 * h}, {x: 0, y: 0.75 * h}, {x: 0, y: 0.25 * h}
];
for(let i=0; i<6; i++) {
const p1 = points[i]; const p2 = points[(i+1)%6];
const mx = (p1.x + p2.x) / 2; const my = (p1.y + p2.y) / 2;
const dx = p2.x - p1.x; const dy = p2.y - p1.y;
const len = Math.sqrt(dx*dx + dy*dy); const ang = Math.atan2(dy, dx) * (180 / Math.PI);
const div = document.createElement('div');
div.className = 'wall'; div.style.width = `${len + 1}px`; div.style.height = `${depth}px`;
div.style.left = `${mx}px`; div.style.top = `${my}px`;
div.style.transformOrigin = 'top center';
div.style.transform = `translate(-50%, 0) rotateZ(${ang}deg) rotateX(-90deg)`;
cell.appendChild(div);
}
}
function changeMode(d) { state.modeIdx = (state.modeIdx + d + MODES.length) % MODES.length; updateView(); saveToLocalStorage(); }
function updateScale(t, a, d) {
if(t==='hex') state.scales.hex[a] = Math.max(0.2, parseFloat((state.scales.hex[a]+d).toFixed(1)));
else state.scales.sq[a] = Math.max(10, state.scales.sq[a]+d);
updateView(); saveToLocalStorage();
}
function updateGap(t, k, v) { state.gaps[t][k] = parseInt(v); updateView(); saveToLocalStorage(); }
function updateLift(v) { state.lift = parseInt(v); document.getElementById('val-lift').innerText=v; renderClickState(); saveToLocalStorage(); }
function updateView() {
const mode = MODES[state.modeIdx];
document.getElementById('indicator').innerText = mode.label;
document.getElementById('mode-desc').innerText = mode.desc;
const hSc = state.scales.hex; const hGp = state.gaps.hex;
let hW, hH;
if(mode.rotate) { hH = (2*BASE_HEX_SIDE)*hSc.w; hW = (Math.sqrt(3)*BASE_HEX_SIDE)*hSc.h; }
else { hW = (Math.sqrt(3)*BASE_HEX_SIDE)*hSc.w; hH = (2*BASE_HEX_SIDE)*hSc.h; }
let vW = mode.rotate ? hH : hW; let vH = mode.rotate ? hW : hH;
hexBoard.querySelectorAll('.cell').forEach(hex => {
if(hex.classList.contains('zone-null')) return;
const q = parseInt(hex.dataset.q); const r = parseInt(hex.dataset.r);
const relQ = q - 4; const relR = r - 4;
const gQ = Math.floor(q/3) - 1; const gR = Math.floor(r/3) - 1;
const zX = gQ * hGp.zone; const zY = gR * hGp.zone;
let x, y;
if(!mode.rotate) {
const sW = vW + hGp.cell; const sH = (vH * 0.75) + (hGp.cell * 0.8);
x = relQ * sW + relR * (sW * 0.5);
if(mode.vec_r[0] < 0) x = relQ * sW + relR * (sW * -0.5);
y = relR * sH;
} else {
const sW = (vW * 0.75) + (hGp.cell * 0.8); const sH = vH + hGp.cell;
const sign = mode.vec_q[1] > 0 ? 1 : -1;
x = relQ * sW; y = relQ * (sH * 0.5 * sign) + relR * sH;
}
hex.style.width = `${hW}px`; hex.style.height = `${hH}px`;
hex.dataset.tx = x + zX; hex.dataset.ty = y + zY; hex.dataset.rot = mode.rotate ? 90 : 0;
if(!hex.classList.contains('clicked')) {
hex.style.transform = `translate(-50%, -50%) translate3d(${x+zX}px, ${y+zY}px, 0) rotate(${hex.dataset.rot}deg)`;
} else {
hex.style.transform = `translate(-50%, -50%) translate3d(${x+zX}px, ${y+zY}px, ${state.lift}px) rotate(${hex.dataset.rot}deg)`;
createHexWalls(hex, hW, hH, state.lift);
}
hex.querySelector('span').style.transform = `rotate(${mode.rotate?-90:0}deg)`;
});
const sSc = state.scales.sq; const sGp = state.gaps.sq;
sqBoard.querySelectorAll('.cell').forEach(sq => {
const q = parseInt(sq.dataset.q); const r = parseInt(sq.dataset.r);
const relQ = q - 4; const relR = r - 4;
const gQ = Math.floor(q/3) - 1; const gR = Math.floor(r/3) - 1;
const x = relQ * (sSc.w + sGp.cell) + (gQ * sGp.zone);
const y = relR * (sSc.h + sGp.cell) + (gR * sGp.zone);
sq.style.width = `${sSc.w}px`; sq.style.height = `${sSc.h}px`;
sq.dataset.tx = x; sq.dataset.ty = y; sq.dataset.rot = 0;
if(!sq.classList.contains('clicked')) {
sq.style.transform = `translate(-50%, -50%) translate3d(${x}px, ${y}px, 0)`;
} else {
sq.style.transform = `translate(-50%, -50%) translate3d(${x}px, ${y}px, ${state.lift}px)`;
createSquareWalls(sq, sSc.w, sSc.h, state.lift);
}
});
}
init();
</script>
</body>
</html>