EXCELLENT!!!
おはこんばんにちは
Rikurikuです~。
え?うるさい?
わかりました。音量調節しますね。
本題
これまで、いくつかコードを書いてきましたが、
怪しいと思うのでダウンロードはしなくて良いです。(???)
実は、それぞれ
正二十面体、正十二面体、正八面体がモチーフなんです。
なら次は立方体だろ
と思って作りました。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>立方体の展開図パズル (Flashy Victory)</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #ffffff;
font-family: sans-serif;
}
canvas {
display: block;
}
#victory {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 100px;
font-weight: 900;
color: gold;
text-shadow:
4px 4px 0px #b8860b,
8px 8px 0px rgba(0, 0, 0, 0.2);
display: none;
pointer-events: none;
z-index: 10;
animation: popIn 0.5s ease-out forwards;
white-space: nowrap;
}
@keyframes popIn {
0% {
transform: translate(-50%, -50%) scale(0) rotate(-30deg);
opacity: 0;
}
80% {
transform: translate(-50%, -50%) scale(1.2) rotate(5deg);
opacity: 1;
}
100% {
transform: translate(-50%, -50%) scale(1) rotate(0deg);
opacity: 1;
}
}
</style>
</head>
<body>
<div id="victory">EXCELLENT!</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
// --- シーン設定 ---
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
const aspect = window.innerWidth / window.innerHeight;
const camera = new THREE.PerspectiveCamera(45, aspect, 0.1, 1000);
camera.position.set(0, 0, 35);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(5, 10, 10);
scene.add(dirLight);
// --- グリッド設定 ---
const SIZE = 2;
const GRID_COLS = 10;
const GRID_ROWS = 10;
const TOTAL_WIDTH = SIZE * GRID_COLS;
const TOTAL_HEIGHT = SIZE * GRID_ROWS;
const GRID_OFFSET_X = -TOTAL_WIDTH / 2;
const GRID_OFFSET_Y = TOTAL_HEIGHT / 2;
const faceMaterial = new THREE.MeshPhongMaterial({
color: 0x88ccff, side: THREE.DoubleSide,
polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1
});
const faceMaterialVictory = new THREE.MeshPhongMaterial({
color: 0xffd700, side: THREE.DoubleSide,
shininess: 100,
emissive: 0x442200,
polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1
});
const yellowMaterial = new THREE.MeshPhongMaterial({
color: 0xffff00, side: THREE.DoubleSide,
polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1
});
const sparkleMaterial = new THREE.MeshBasicMaterial({
color: 0x0088ff, transparent: true, opacity: 0.5, side: THREE.DoubleSide, depthTest: false
});
const teleportMaterial = new THREE.MeshBasicMaterial({
color: 0xffaa00, transparent: true, opacity: 0.5, side: THREE.DoubleSide, depthTest: false
});
const lineMaterial = new THREE.LineBasicMaterial({ color: 0x000000 });
const gridLineMaterial = new THREE.LineBasicMaterial({ color: 0xcccccc });
function getCellPosition(col, row) {
const x = GRID_OFFSET_X + (col * SIZE) + (SIZE / 2);
const y = GRID_OFFSET_Y - (row * SIZE) - (SIZE / 2);
return { x, y };
}
// --- 1. グリッド描画 ---
const gridGroup = new THREE.Group();
scene.add(gridGroup);
for (let i = 0; i <= GRID_COLS; i++) {
const x = GRID_OFFSET_X + (i * SIZE);
const pts = [new THREE.Vector3(x, GRID_OFFSET_Y, 0), new THREE.Vector3(x, GRID_OFFSET_Y - TOTAL_HEIGHT, 0)];
gridGroup.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), gridLineMaterial));
}
for (let j = 0; j <= GRID_ROWS; j++) {
const y = GRID_OFFSET_Y - (j * SIZE);
const pts = [new THREE.Vector3(GRID_OFFSET_X, y, 0), new THREE.Vector3(GRID_OFFSET_X + TOTAL_WIDTH, y, 0)];
gridGroup.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), gridLineMaterial));
}
// --- 2. 展開図定義 (11種類) ---
const PATTERNS = [
// Group A
[{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 3, y: 1 }, { x: 1, y: 2 }],
[{ x: 2, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 3, y: 1 }, { x: 1, y: 2 }],
[{ x: 3, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 3, y: 1 }, { x: 1, y: 2 }],
[{ x: 3, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 3, y: 1 }, { x: 0, y: 2 }],
[{ x: 0, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 3, y: 1 }, { x: 0, y: 2 }],
[{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 3, y: 1 }, { x: 0, y: 2 }],
// Group B
[{ x: 0, y: 2 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 2, y: 0 }, { x: 3, y: 0 }],
[{ x: 1, y: 2 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 2, y: 0 }, { x: 3, y: 0 }],
[{ x: 2, y: 2 }, { x: 0, y: 1 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 2, y: 0 }, { x: 3, y: 0 }],
// Group C
[{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 1, y: 1 }, { x: 2, y: 1 }, { x: 2, y: 2 }, { x: 3, y: 2 }],
// Group D
[{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 2, y: 0 }, { x: 2, y: 1 }, { x: 3, y: 1 }, { x: 4, y: 1 }]
];
const validSignatures = new Set();
function normalizeNet(cells) {
let minX = Infinity, minY = Infinity;
cells.forEach(c => { minX = Math.min(minX, c.x); minY = Math.min(minY, c.y); });
const shifted = cells.map(c => ({ x: c.x - minX, y: c.y - minY }));
shifted.sort((a, b) => (a.y - b.y) || (a.x - b.x));
return shifted.map(p => `${p.x},${p.y}`).join('|');
}
PATTERNS.forEach(base => {
let current = base.map(p => ({ ...p }));
for (let f = 0; f < 2; f++) {
for (let r = 0; r < 4; r++) {
validSignatures.add(normalizeNet(current));
current = current.map(p => ({ x: -p.y, y: p.x }));
}
current = current.map(p => ({ x: -p.x, y: p.y }));
}
});
// --- 4. 生成ロジック ---
// MOVED DECLARATIONS UP TO AVOID TEMPORAL DEAD ZONE
const occupiedCells = new Set();
let netMeshes = [];
let yellowList = [];
let confettiActive = false; // Fixed: Declared before initGame usage
let particles = []; // Fixed: Declared before initGame usage
function initGame() {
netMeshes.forEach(o => { scene.remove(o.mesh); scene.remove(o.line); });
netMeshes = [];
yellowList.forEach(o => { scene.remove(o.mesh); scene.remove(o.line); });
yellowList = [];
occupiedCells.clear();
document.getElementById('victory').style.display = 'none';
// Stop confetti
confettiActive = false;
particles.forEach(p => scene.remove(p));
particles = [];
// Net
let attempts = 0;
let success = false;
let netCells = [];
while (!success && attempts < 100) {
occupiedCells.clear();
netCells = [];
attempts++;
const patternBase = PATTERNS[Math.floor(Math.random() * PATTERNS.length)];
const rot = Math.floor(Math.random() * 4);
const flip = Math.random() < 0.5;
const pattern = patternBase.map(p => {
let x = p.x;
let y = p.y;
if (flip) { x = -x; }
for (let r = 0; r < rot; r++) { [x, y] = [-y, x]; }
return { x, y };
});
const minX = Math.min(...pattern.map(p => p.x));
const minY = Math.min(...pattern.map(p => p.y));
const normPattern = pattern.map(p => ({ x: p.x - minX, y: p.y - minY }));
const width = Math.max(...normPattern.map(p => p.x)) + 1;
const height = Math.max(...normPattern.map(p => p.y)) + 1;
const maxCol = GRID_COLS - width;
const maxRow = GRID_ROWS - height;
if (maxCol < 0 || maxRow < 0) continue;
const startCol = Math.floor(Math.random() * (maxCol + 1));
const startRow = Math.floor(Math.random() * (maxRow + 1));
for (let p of normPattern) {
const c = startCol + p.x;
const r = startRow + p.y;
netCells.push({ c, r });
occupiedCells.add(`${c},${r}`);
}
success = true;
}
netCells.forEach(cell => {
const pos = getCellPosition(cell.c, cell.r);
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(SIZE, SIZE), faceMaterial.clone());
mesh.position.set(pos.x, pos.y, 0);
mesh.userData = { col: cell.c, row: cell.r, isFace: true };
scene.add(mesh);
const line = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.PlaneGeometry(SIZE, SIZE)), lineMaterial.clone());
line.position.set(pos.x, pos.y, 0);
scene.add(line);
netMeshes.push({ mesh, line, col: cell.c, row: cell.r });
});
// Yellow Cells
const OFFSETS_RULES = [
{ v: 1, h: 0 }, { v: 2, h: 0 }, { v: 3, h: 0 },
{ v: 0, h: 1 }, { v: 0, h: 2 }, { v: 0, h: 3 },
{ v: 2, h: 2 }, { v: 3, h: 3 },
];
let yellowPlaced = false;
attempts = 0;
while (!yellowPlaced && attempts < 200) {
attempts++;
const c1 = 3 + Math.floor(Math.random() * 4);
const r1 = 3 + Math.floor(Math.random() * 4);
if (occupiedCells.has(`${c1},${r1}`)) continue;
const rule = OFFSETS_RULES[Math.floor(Math.random() * OFFSETS_RULES.length)];
const dirH = Math.random() < 0.5 ? 1 : -1;
const dirV = Math.random() < 0.5 ? 1 : -1;
const c2 = c1 + (rule.h * dirH);
const r2 = r1 + (rule.v * dirV);
if (c2 < 0 || c2 >= GRID_COLS || r2 < 0 || r2 >= GRID_ROWS) continue;
if (occupiedCells.has(`${c2},${r2}`)) continue;
if (c1 === c2 && r1 === r2) continue;
[{ c: c1, r: r1 }, { c: c2, r: r2 }].forEach(t => {
const pos = getCellPosition(t.c, t.r);
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(SIZE, SIZE), yellowMaterial);
mesh.position.set(pos.x, pos.y, 0);
scene.add(mesh);
const line = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.PlaneGeometry(SIZE, SIZE)), lineMaterial);
line.position.set(pos.x, pos.y, 0);
scene.add(line);
yellowList.push({ mesh, line, col: t.c, row: t.r });
});
yellowPlaced = true;
}
}
initGame();
// --- 5. インタラクション ---
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let selectedFace = null;
let highlightMeshes = [];
let gameLocked = false;
window.addEventListener('mousemove', (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
});
window.addEventListener('contextmenu', (event) => {
if (gameLocked) return;
event.preventDefault();
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(netMeshes.map(n => n.mesh));
if (intersects.length > 0) {
const clickedMesh = intersects[0].object;
const target = netMeshes.find(n => n.mesh === clickedMesh);
if (selectedFace === target) {
deselectFace();
} else {
if (selectedFace) deselectFace();
selectFace(target);
}
} else {
if (selectedFace) deselectFace();
}
});
window.addEventListener('click', (event) => {
if (gameLocked || !selectedFace) return;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(highlightMeshes);
if (intersects.length > 0) {
const hit = intersects[0].object;
const targetCol = hit.userData.col;
const targetRow = hit.userData.row;
const isTeleport = hit.userData.isTeleport;
moveFace(selectedFace, targetCol, targetRow, isTeleport);
}
});
function selectFace(faceObj) {
selectedFace = faceObj;
animateScale(faceObj.mesh, 1.2);
animateScale(faceObj.line, 1.2);
faceObj.mesh.renderOrder = 1;
showValidMoves(faceObj);
}
function deselectFace() {
if (!selectedFace) return;
animateScale(selectedFace.mesh, 1.0);
animateScale(selectedFace.line, 1.0);
selectedFace.mesh.renderOrder = 0;
selectedFace = null;
clearHighlights();
}
function clearHighlights() {
highlightMeshes.forEach(m => scene.remove(m));
highlightMeshes = [];
}
function showValidMoves(faceObj) {
clearHighlights();
const otherFaces = netMeshes.filter(f => f !== faceObj);
const candidates = [];
const isValidPos = (c, r) => c >= 0 && c < GRID_COLS && r >= 0 && r < GRID_ROWS;
otherFaces.forEach(f => {
[[0, 1], [0, -1], [1, 0], [-1, 0]].forEach(d => {
const nc = f.col + d[0];
const nr = f.row + d[1];
if (isValidPos(nc, nr)) {
const occupiedByNet = netMeshes.some(n => n.col === nc && n.row === nr);
const isYellow = yellowList.find(y => y.col === nc && y.row === nr);
if (!occupiedByNet && isYellow) {
const otherYellow = yellowList.find(y => y !== isYellow);
if (otherYellow) {
const tc = otherYellow.col;
const tr = otherYellow.row;
if (!netMeshes.some(n => n.col === tc && n.row === tr)) {
candidates.push({ c: nc, r: nr, isTeleport: true, destC: tc, destR: tr });
}
}
} else if (!occupiedByNet && !isYellow) {
if (!candidates.some(cand => cand.c === nc && cand.r === nr)) {
candidates.push({ c: nc, r: nr, isTeleport: false });
}
}
}
});
});
candidates.forEach(cand => {
let trialCells;
if (cand.isTeleport) {
trialCells = otherFaces.map(f => ({ x: f.col, y: f.row }));
trialCells.push({ x: cand.destC, y: cand.destR });
} else {
trialCells = otherFaces.map(f => ({ x: f.col, y: f.row }));
trialCells.push({ x: cand.c, y: cand.r });
}
const sig = normalizeNet(trialCells);
if (validSignatures.has(sig)) {
const pos = getCellPosition(cand.c, cand.r);
const mat = cand.isTeleport ? teleportMaterial : sparkleMaterial;
const hl = new THREE.Mesh(new THREE.PlaneGeometry(SIZE, SIZE), mat);
hl.position.set(pos.x, pos.y, 0.1);
hl.userData = { col: cand.c, row: cand.r, isTeleport: cand.isTeleport };
scene.add(hl);
highlightMeshes.push(hl);
}
});
}
function moveFace(faceObj, targetCol, targetRow, isTeleport) {
occupiedCells.delete(`${faceObj.col},${faceObj.row}`);
let finalC = targetCol;
let finalR = targetRow;
const yellowEntry = yellowList.find(y => y.col === targetCol && y.row === targetRow);
const yellowExit = yellowList.find(y => y !== yellowEntry);
if (isTeleport && yellowExit) {
finalC = yellowExit.col;
finalR = yellowExit.row;
}
faceObj.col = finalC;
faceObj.row = finalR;
occupiedCells.add(`${finalC},${finalR}`);
faceObj.mesh.userData.col = finalC;
faceObj.mesh.userData.row = finalR;
deselectFace();
const targetPos = getCellPosition(targetCol, targetRow);
if (isTeleport) {
// Phase 1: Spiral Suck In
animateComplex(faceObj.mesh, {
type: 'teleportIn',
targetPos: targetPos,
duration: 500,
onComplete: () => {
// Phase 2: Instant Move & Spiral Spit Out
const exitPos = getCellPosition(finalC, finalR);
faceObj.mesh.position.set(exitPos.x, exitPos.y, faceObj.mesh.position.z);
faceObj.line.position.set(exitPos.x, exitPos.y, faceObj.line.position.z);
animateComplex(faceObj.mesh, {
type: 'teleportOut',
targetPos: exitPos,
duration: 500,
onComplete: triggerVictory
});
animateComplex(faceObj.line, {
type: 'teleportOut',
targetPos: exitPos,
duration: 500
});
}
});
animateComplex(faceObj.line, {
type: 'teleportIn',
targetPos: targetPos,
duration: 500
});
} else {
const finalPos = getCellPosition(finalC, finalR);
animateMove(faceObj.mesh, finalPos);
animateMove(faceObj.line, finalPos);
}
}
// --- Victory ---
// MOVED DECLARATIONS UP
function triggerVictory() {
gameLocked = true;
document.getElementById('victory').style.display = 'block';
yellowList.forEach(y => { scene.remove(y.mesh); scene.remove(y.line); });
yellowList = [];
netMeshes.forEach(n => { n.mesh.material = faceMaterialVictory; });
// Start Confetti
confettiActive = true;
for (let i = 0; i < 100; i++) {
const geom = new THREE.PlaneGeometry(0.5, 0.5);
const mat = new THREE.MeshBasicMaterial({ color: Math.random() * 0xffffff, side: THREE.DoubleSide });
const p = new THREE.Mesh(geom, mat);
p.position.set(0, 0, 5); // Start center
// Random velocity
p.userData = {
vx: (Math.random() - 0.5) * 1,
vy: (Math.random() - 0.5) * 1 + 0.5, // slightly up
vz: (Math.random() - 0.5) * 1,
rotx: Math.random() * 0.2,
roty: Math.random() * 0.2
};
scene.add(p);
particles.push(p);
}
}
// --- 6. アニメーション ---
const animations = [];
function animateScale(obj, targetScale) {
animations.push({
obj: obj,
type: 'scale',
target: targetScale,
start: obj.scale.x,
startTime: Date.now(),
duration: 200
});
}
function animateMove(obj, targetPos, onComplete) {
animations.push({
obj: obj,
type: 'move',
target: targetPos,
startX: obj.position.x,
startY: obj.position.y,
startTime: Date.now(),
duration: 300,
onComplete: onComplete
});
}
function animateComplex(obj, params) {
animations.push({
obj: obj,
type: params.type,
target: params.targetPos,
startParams: {
x: obj.position.x, y: obj.position.y,
sx: obj.scale.x, rot: obj.rotation.z
},
startTime: Date.now(),
duration: params.duration,
onComplete: params.onComplete
});
}
function updateAnimations() {
const now = Date.now();
for (let i = animations.length - 1; i >= 0; i--) {
const anim = animations[i];
const elapsed = now - anim.startTime;
const progress = Math.min(elapsed / anim.duration, 1);
const ease = 1 - Math.pow(1 - progress, 3);
if (anim.type === 'scale') {
const s = anim.start + (anim.target - anim.start) * ease;
anim.obj.scale.set(s, s, 1);
} else if (anim.type === 'move') {
const x = anim.startX + (anim.target.x - anim.startX) * ease;
const y = anim.startY + (anim.target.y - anim.startY) * ease;
anim.obj.position.set(x, y, anim.obj.position.z);
} else if (anim.type === 'teleportIn') {
const x = anim.startParams.x + (anim.target.x - anim.startParams.x) * ease;
const y = anim.startParams.y + (anim.target.y - anim.startParams.y) * ease;
anim.obj.position.set(x, y, anim.obj.position.z);
const s = Math.max(0, anim.startParams.sx * (1 - ease));
anim.obj.scale.set(s, s, 1);
anim.obj.rotation.z = anim.startParams.rot + (4 * Math.PI * ease);
} else if (anim.type === 'teleportOut') {
const s = ease;
anim.obj.scale.set(s, s, 1);
anim.obj.rotation.z = 4 * Math.PI * (1 - ease);
}
if (progress >= 1) {
if (anim.type === 'teleportOut') anim.obj.rotation.z = 0;
if (anim.onComplete) anim.onComplete();
animations.splice(i, 1);
}
}
}
// --- メインループ ---
let time = 0;
function animate() {
time += 0.05;
highlightMeshes.forEach((mesh, idx) => {
mesh.material.opacity = 0.5 + Math.sin(time + idx) * 0.3;
});
updateAnimations();
if (confettiActive) {
particles.forEach(p => {
p.position.x += p.userData.vx;
p.position.y += p.userData.vy;
p.position.z += p.userData.vz;
p.rotation.x += p.userData.rotx;
p.rotation.y += p.userData.roty;
// Gravity
p.userData.vy -= 0.02;
// Reset if too low
if (p.position.y < -20) {
p.position.set(0, 0, 5);
p.userData.vy = Math.random() * 1 + 0.5;
}
});
// Pulse Net
const scale = 1 + Math.sin(time * 0.2) * 0.05;
netMeshes.forEach(n => {
n.mesh.scale.set(scale, scale, 1);
n.line.scale.set(scale, scale, 1);
});
}
renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.render(scene, camera);
});
</script>
</body>
</html>
10×10の升目に、立方体の展開図と黄色の面が二つ表示されます。

一つの面を拡大し、

でてきたマスをクリックで面を移動。

黄色い面が光ったら、そこをクリックで、

EXCELLENT!!!
いいなと思ったら応援しよう!
⚠️警告⚠️
もしあなたがチップをくれたら、その分あなたのお金が減ります(?)。お金は大切に。