Skip to content

Instantly share code, notes, and snippets.

@kinjouj
Created April 10, 2026 02:30
Show Gist options
  • Select an option

  • Save kinjouj/a6590e743165bfd3a51895d1f110d661 to your computer and use it in GitHub Desktop.

Select an option

Save kinjouj/a6590e743165bfd3a51895d1f110d661 to your computer and use it in GitHub Desktop.
画像をジグソーパズル化するやつ
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Puzzle</title>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@700&family=Space+Mono&display=swap" rel="stylesheet">
<style>
:root {
--bg: #07080f;
--surface: #0d1117;
--surface2:#161b27;
--border: #1e2a42;
--accent: #00f5d4;
--accent2: #f72585;
--accent3: #7b2ff7;
--muted: #4a5568;
--text: #e2e8f8;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; background: var(--bg); color: var(--text); font-family: 'Space Mono', monospace; overflow: hidden; }
.shell { height: 100vh; display: flex; flex-direction: column; padding: 12px 14px; gap: 10px; }
/* Topbar */
.topbar { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.upload-btn {
position: relative; overflow: hidden;
background: var(--surface); border: 1px solid var(--border);
color: var(--accent); font-family: 'Orbitron', monospace;
font-size: 0.6rem; letter-spacing: 0.2em;
padding: 8px 14px; cursor: pointer; border-radius: 2px; white-space: nowrap;
}
.upload-btn input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.filename { font-size: 0.6rem; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 180px; }
.btn-start {
background: linear-gradient(135deg, var(--accent3), var(--accent2));
border: none; color: #fff;
font-family: 'Orbitron', monospace; font-size: 0.65rem; font-weight: 700;
letter-spacing: 0.2em; padding: 8px 20px; cursor: pointer; border-radius: 2px; white-space: nowrap;
}
.btn-start:disabled { opacity: .3; cursor: not-allowed; }
/* Progress */
.prog-row { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.prog-label { font-family: 'Orbitron', monospace; font-size: 0.55rem; letter-spacing: .3em; color: var(--muted); white-space: nowrap; }
.prog-label span { color: var(--accent); }
.prog-bar-wrap { flex: 1; height: 2px; background: var(--surface2); border-radius: 2px; overflow: hidden; }
.prog-bar { height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent3)); width: 0%; transition: width .3s; }
/* Game area: left=tray, right=board */
.game-area { display: flex; gap: 12px; flex: 1; min-height: 0; align-items: flex-start; justify-content: center; }
/* Labels */
.panel-label { font-family: 'Orbitron', monospace; font-size: 0.5rem; letter-spacing: .4em; color: var(--muted); margin-bottom: 10px; flex-shrink: 0; }
/* Tray */
.tray-col { display: flex; flex-direction: column; flex-shrink: 0; margin-bottom: 12px; margin-right: 24px; }
#tray { display: grid; background: var(--surface); border: 1px solid var(--border); border-radius: 2px; padding: 4px; gap: 3px; flex-shrink: 0; align-content: start; }
/* Board */
.board-col { display: flex; flex-direction: column; flex-shrink: 0; margin-bottom: 12px; }
#board { display: grid; background: var(--surface); border: 1px solid var(--border); border-radius: 2px; padding: 4px; gap: 3px; flex-shrink: 0; }
.slot { border-radius: 2px; border: 1px dashed rgba(255,255,255,0.07); background: var(--surface2); overflow: hidden; position: relative; flex-shrink: 0; }
.slot.over { border-color: var(--accent); background: rgba(0,245,212,.07); }
.slot.filled { border: none; background: transparent; }
.board-empty {
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 8px; color: var(--muted); text-align: center; pointer-events: none;
}
.board-empty .icon { font-size: 1.8rem; opacity: .2; }
.board-empty p { font-size: 0.6rem; line-height: 1.7; }
.piece { border-radius: 2px; background-size: 100% 100%; cursor: grab; display: block; flex-shrink: 0; border: 1px solid rgba(255,255,255,0.6); }
.piece:active { cursor: grabbing; }
.piece.dragging { opacity: .25; }
.piece.locked { cursor: default; pointer-events: none; }
@keyframes ok { 0% { outline: 2px solid var(--accent); } 100% { outline: 2px solid transparent; } }
@keyframes ng { 0%,100%{transform:translateX(0);outline:2px solid var(--accent2)} 30%{transform:translateX(-3px)} 70%{transform:translateX(3px)} }
.piece.ok { animation: ok .45s ease forwards; }
.piece.ng { animation: ng .3s ease forwards; }
</style>
</head>
<body>
<div class="shell">
<div class="topbar">
<label class="upload-btn">
<input type="file" id="file-input" accept="image/*">
📁 IMAGE UPLOAD
</label>
</div>
<div class="prog-row" id="prog-row" style="display:none">
<div class="prog-label">SOLVED <span id="prog-text">0 / 16</span></div>
<div class="prog-bar-wrap"><div class="prog-bar" id="prog-bar"></div></div>
</div>
<div class="game-area" id="game-area" style="display:none">
<div class="tray-col">
<div class="panel-label">PIECES</div>
<div id="tray"></div>
</div>
<div class="board-col">
<div class="panel-label">BOARD</div>
<div id="board">
<div class="board-empty" id="board-empty">
<span class="icon">🧩</span>
<p>画像をアップロードして<br>START を押してください</p>
</div>
</div>
</div>
</div>
</div>
<script>
const N = 4;
const S = { img: null, board: [], tray: [], trayOrder: [], slices: [], drag: null, pSize: 0 };
const $ = id => document.getElementById(id);
const fileInput = $('file-input');
const boardEl = $('board'), trayEl = $('tray');
const progText = $('prog-text'), progBar = $('prog-bar');
fileInput.addEventListener('change', e => {
const f = e.target.files[0];
if (!f || !f.type.startsWith('image/')) return;
const r = new FileReader();
r.onload = ev => {
fileInput.value = ''; // reset so same file can be re-selected
S.img = ev.target.result;
document.getElementById('prog-row').style.display = 'flex';
document.getElementById('game-area').style.display = 'flex';
sliceImage(S.img, N, slices => {
S.slices = slices;
S.board = Array(N * N).fill(null);
S.tray = shuffle(Array.from({ length: N * N }, (_, i) => i));
S.trayOrder = [...S.tray];
buildLayout();
updateProg();
});
};
r.readAsDataURL(f);
});
// Compute piece size so board fits in viewport
function buildLayout() {
const GAP = 3, PAD = 4;
const gameArea = document.querySelector('.game-area');
const availH = gameArea.clientHeight;
const availW = gameArea.clientWidth - 12;
const LABEL_H = 22 + 12; // panel-label + margin-bottom of col
const byH = Math.floor((availH - PAD*2 - GAP*(N-1) - LABEL_H) / N);
const byW = Math.floor((availW - PAD*4 - GAP*(N-1)*2) / (N * 2));
const pSize = Math.max(4, Math.min(byH, byW));
S.pSize = pSize;
const boardSide = pSize * N + PAD*2 + GAP*(N-1);
// Board grid
boardEl.style.width = boardSide + 'px';
boardEl.style.height = boardSide + 'px';
boardEl.style.gridTemplateColumns = `repeat(${N}, ${pSize}px)`;
trayEl.style.width = boardSide + 'px';
trayEl.style.height = boardSide + 'px';
trayEl.style.gridTemplateColumns = `repeat(${N}, ${pSize}px)`;
renderBoard();
renderTray();
}
function renderBoard() {
boardEl.innerHTML = '';
for (let i = 0; i < N * N; i++) {
const slot = document.createElement('div');
slot.className = 'slot' + (S.board[i] !== null ? ' filled' : '');
slot.style.width = slot.style.height = S.pSize + 'px';
if (S.board[i] !== null) slot.appendChild(makePiece(S.board[i], true));
slot.addEventListener('dragover', e => { if (S.board[i] !== null) return; e.preventDefault(); slot.classList.add('over'); });
slot.addEventListener('dragleave', () => slot.classList.remove('over'));
slot.addEventListener('drop', e => {
e.preventDefault(); slot.classList.remove('over');
if (S.board[i] !== null || !S.drag) return;
placePiece(i, slot);
});
boardEl.appendChild(slot);
}
}
function renderTray() {
trayEl.innerHTML = '';
const p = S.pSize;
// S.trayOrder is the fixed shuffled order — show ghost for placed pieces
S.trayOrder.forEach(idx => {
if (S.tray.includes(idx)) {
trayEl.appendChild(makePiece(idx, false));
} else {
const ghost = document.createElement('div');
ghost.style.cssText = `width:${p}px;height:${p}px;border-radius:2px;background:var(--surface2);opacity:.15`;
trayEl.appendChild(ghost);
}
});
}
function makePiece(idx, locked) {
const el = document.createElement('div');
el.className = 'piece' + (locked ? ' locked' : '');
el.draggable = true;
el.style.width = el.style.height = S.pSize + 'px';
el.style.backgroundImage = `url(${S.slices[idx]})`;
el.addEventListener('dragstart', e => {
S.drag = { pieceIdx: idx };
requestAnimationFrame(() => el.classList.add('dragging'));
e.dataTransfer.effectAllowed = 'move';
});
el.addEventListener('dragend', () => el.classList.remove('dragging'));
return el;
}
function placePiece(slotIdx, slotEl) {
const { pieceIdx } = S.drag; S.drag = null;
// If this piece was already on the board, clear its old slot
const prevSlot = S.board.indexOf(pieceIdx);
if (prevSlot !== -1) {
S.board[prevSlot] = null;
const oldSlotEl = boardEl.children[prevSlot];
oldSlotEl.innerHTML = '';
oldSlotEl.classList.remove('filled');
} else {
// Remove from tray
const ti = S.tray.indexOf(pieceIdx);
if (ti !== -1) S.tray.splice(ti, 1);
}
// If target slot already has a piece, send it back to tray
if (S.board[slotIdx] !== null) {
S.tray.push(S.board[slotIdx]);
S.board[slotIdx] = null;
slotEl.innerHTML = '';
slotEl.classList.remove('filled');
}
S.board[slotIdx] = pieceIdx;
slotEl.classList.add('filled');
const p = makePiece(pieceIdx, false);
p.classList.add(pieceIdx === slotIdx ? 'ok' : 'ng');
setTimeout(() => p.classList.remove('ok', 'ng'), 500);
slotEl.appendChild(p);
renderTray();
updateProg();
if (S.board.every((v, i) => v === i)) boardEl.style.pointerEvents = 'none';
}
function updateProg() {
const correct = S.board.filter((v, i) => v === i).length;
progText.textContent = `${correct} / ${N * N}`;
progBar.style.width = `${(correct / (N * N)) * 100}%`;
}
function sliceImage(src, n, cb) {
const img = new Image();
img.onload = () => {
const sw = img.width / n, sh = img.height / n;
const out = [];
for (let r = 0; r < n; r++) for (let c = 0; c < n; c++) {
const cv = document.createElement('canvas');
cv.width = sw; cv.height = sh;
cv.getContext('2d').drawImage(img, c*sw, r*sh, sw, sh, 0, 0, sw, sh);
out.push(cv.toDataURL());
}
cb(out);
};
img.src = src;
}
function shuffle(a) {
for (let i = a.length-1; i > 0; i--) { const j = Math.floor(Math.random()*(i+1)); [a[i],a[j]]=[a[j],a[i]]; }
return a;
}
document.addEventListener('dragover', e => e.preventDefault());
// Drop onto tray → return piece to tray
trayEl.addEventListener('dragover', e => e.preventDefault());
trayEl.addEventListener('drop', e => {
e.preventDefault();
if (!S.drag) return;
const { pieceIdx } = S.drag; S.drag = null;
// Only handle pieces coming from the board
const prevSlot = S.board.indexOf(pieceIdx);
if (prevSlot === -1) return; // already in tray, ignore
S.board[prevSlot] = null;
const oldSlotEl = boardEl.children[prevSlot];
oldSlotEl.innerHTML = '';
oldSlotEl.classList.remove('filled');
S.tray.push(pieceIdx);
renderTray();
updateProg();
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment