<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>8つの三角すいを合わせた立体</title>
<style>
body { margin: 0; overflow: hidden; background-color: #f3f4f6; font-family: 'Inter', sans-serif; }
#canvas-container { width: 100vw; height: 100vh; position: absolute; top: 0; left: 0; z-index: 1; }
#ui-container { position: absolute; top: 20px; right: 20px; z-index: 10; background: rgba(255, 255, 255, 0.95); padding: 20px; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.1); color: #1f2937; width: 300px; max-height: 90vh; overflow-y: auto; border: 1px solid #e5e7eb; }
.label { position: absolute; color: #111827; font-weight: bold; text-shadow: 1px 1px 2px white, -1px -1px 2px white, 1px -1px 2px white, -1px 1px 2px white; pointer-events: none; z-index: 5; font-size: 1.2rem; transform: translate(-50%, -50%); }
.checkbox-row { display: flex; align-items: center; margin-bottom: 8px; cursor: pointer; padding: 4px; border-radius: 6px; transition: background 0.2s; }
.checkbox-row:hover { background: rgba(0,0,0,0.05); }
.checkbox-row input { margin-right: 10px; cursor: pointer; }
.color-box { width: 16px; height: 16px; border-radius: 4px; margin-right: 10px; border: 1px solid rgba(0,0,0,0.2); }
button { padding: 8px 12px; border-radius: 6px; border: none; cursor: pointer; font-weight: bold; background-color: #3b82f6; color: white; transition: background 0.2s; flex: 1; margin: 0 4px; }
button:hover { background-color: #2563eb; }
</style>
</head>
<body>
<div id="canvas-container"></div>
<div id="labels-container"></div>
<div id="ui-container">
<h2 class="text-lg font-bold mb-4 border-b border-gray-300 pb-2">表示設定 (Visibility)</h2>
<div class="flex mb-4">
<button id="btn-show-all">すべて表示</button>
<button id="btn-hide-all" class="bg-gray-500 hover:bg-gray-600">すべて隠す</button>
</div>
<div class="border-b border-gray-300 pb-2 mb-2">
<label class="checkbox-row">
<input type="checkbox" id="toggle-cube" checked>
<span class="text-sm font-bold">基本の立方体 (Reference Cube)</span>
</label>
</div>
<div id="pyramid-toggles">
<!-- Toggles injected by JS -->
</div>
</div>
<!-- Three.js and OrbitControls -->
<script>
// Setup Scene, Camera, Renderer
const scene = new THREE.Scene();
scene.background = new THREE.Color('#f3f4f6');
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(4, 4, 6);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('canvas-container').appendChild(renderer.domElement);
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
dirLight.position.set(5, 10, 7);
scene.add(dirLight);
// Define Cube Vertices
const s = 1.5; // size multiplier
const verts = {
A: new THREE.Vector3(-s, s, s), // front-left-top
B: new THREE.Vector3( s, s, s), // front-right-top
C: new THREE.Vector3( s, s, -s), // back-right-top
D: new THREE.Vector3(-s, s, -s), // back-left-top
E: new THREE.Vector3(-s, -s, s), // front-left-bottom
F: new THREE.Vector3( s, -s, s), // front-right-bottom
G: new THREE.Vector3( s, -s, -s), // back-right-bottom
H: new THREE.Vector3(-s, -s, -s) // back-left-bottom
};
// Draw Reference Cube (Dashed)
const cubeGeometry = new THREE.BoxGeometry(s*2, s*2, s*2);
const cubeEdges = new THREE.EdgesGeometry(cubeGeometry);
const cubeMaterial = new THREE.LineDashedMaterial({ color: 0x555555, dashSize: 0.1, gapSize: 0.1 });
const cubeLines = new THREE.LineSegments(cubeEdges, cubeMaterial);
cubeLines.computeLineDistances();
scene.add(cubeLines);
// HTML Labels for Vertices
const labelsContainer = document.getElementById('labels-container');
const labels = {};
for (const [key, vec] of Object.entries(verts)) {
const el = document.createElement('div');
el.className = 'label';
el.textContent = key;
labelsContainer.appendChild(el);
labels[key] = { element: el, pos: vec.clone().multiplyScalar(1.1) }; // Offset slightly
}
// Define the 8 Pyramids
const pyramidsData = [
{ id: 'E-AHF', name: 'E - AHF', color: 0xef4444, v: [verts.E, verts.A, verts.H, verts.F] },
{ id: 'F-BEG', name: 'F - BEG', color: 0xf97316, v: [verts.F, verts.B, verts.E, verts.G] },
{ id: 'G-CFH', name: 'G - CFH', color: 0xeab308, v: [verts.G, verts.C, verts.F, verts.H] },
{ id: 'H-DGE', name: 'H - DGE', color: 0x22c55e, v: [verts.H, verts.D, verts.G, verts.E] },
{ id: 'A-BDE', name: 'A - BDE', color: 0x0ea5e9, v: [verts.A, verts.B, verts.D, verts.E] },
{ id: 'B-AFC', name: 'B - AFC', color: 0x3b82f6, v: [verts.B, verts.A, verts.F, verts.C] },
{ id: 'C-BGD', name: 'C - BGD', color: 0x8b5cf6, v: [verts.C, verts.B, verts.G, verts.D] },
{ id: 'D-ACH', name: 'D - ACH', color: 0xd946ef, v: [verts.D, verts.A, verts.C, verts.H] }
];
const pyramidGroup = new THREE.Group();
scene.add(pyramidGroup);
const pyramidMeshes = {};
const togglesContainer = document.getElementById('pyramid-toggles');
function createTetrahedron(data) {
const geom = new THREE.BufferGeometry();
// 4 faces, 3 vertices each
const v = data.v;
const vertices = new Float32Array([
// Face 1: 0, 1, 2
v[0].x, v[0].y, v[0].z, v[1].x, v[1].y, v[1].z, v[2].x, v[2].y, v[2].z,
// Face 2: 0, 2, 3
v[0].x, v[0].y, v[0].z, v[2].x, v[2].y, v[2].z, v[3].x, v[3].y, v[3].z,
// Face 3: 0, 3, 1
v[0].x, v[0].y, v[0].z, v[3].x, v[3].y, v[3].z, v[1].x, v[1].y, v[1].z,
// Face 4: 1, 3, 2
v[1].x, v[1].y, v[1].z, v[3].x, v[3].y, v[3].z, v[2].x, v[2].y, v[2].z,
]);
geom.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geom.computeVertexNormals();
// Solid mesh
const material = new THREE.MeshStandardMaterial({
color: data.color,
side: THREE.DoubleSide,
roughness: 0.6,
metalness: 0.1
});
const mesh = new THREE.Mesh(geom, material);
// Wireframe
const edges = new THREE.EdgesGeometry(geom);
const lineMat = new THREE.LineBasicMaterial({ color: 0x000000, linewidth: 2 });
const line = new THREE.LineSegments(edges, lineMat);
mesh.add(line);
pyramidGroup.add(mesh);
pyramidMeshes[data.id] = mesh;
// UI Checkbox
const hexColor = '#' + data.color.toString(16).padStart(6, '0');
const label = document.createElement('label');
label.className = 'checkbox-row';
label.innerHTML = `
<div class="color-box" style="background-color: ${hexColor}"></div>
<input type="checkbox" id="chk-${data.id}" checked>
<span class="text-sm">三角すい (Tetrahedron) ${data.name}</span>
`;
togglesContainer.appendChild(label);
document.getElementById(`chk-${data.id}`).addEventListener('change', (e) => {
mesh.visible = e.target.checked;
});
}
pyramidsData.forEach(createTetrahedron);
// Global UI events
document.getElementById('toggle-cube').addEventListener('change', (e) => {
cubeLines.visible = e.target.checked;
labelsContainer.style.display = e.target.checked ? 'block' : 'none';
});
document.getElementById('btn-show-all').addEventListener('click', () => {
pyramidsData.forEach(p => {
pyramidMeshes[p.id].visible = true;
document.getElementById(`chk-${p.id}`).checked = true;
});
});
document.getElementById('btn-hide-all').addEventListener('click', () => {
pyramidsData.forEach(p => {
pyramidMeshes[p.id].visible = false;
document.getElementById(`chk-${p.id}`).checked = false;
});
});
// Update Label Positions
function updateLabels() {
const widthHalf = window.innerWidth / 2;
const heightHalf = window.innerHeight / 2;
for (const key in labels) {
const l = labels[key];
const vec = l.pos.clone();
vec.project(camera);
// Only show if in front of camera
if (vec.z > 1) {
l.element.style.display = 'none';
continue;
}
l.element.style.display = cubeLines.visible ? 'block' : 'none';
l.element.style.left = (vec.x * widthHalf + widthHalf) + 'px';
l.element.style.top = (-(vec.y * heightHalf) + heightHalf) + 'px';
}
}
// Render Loop
function animate() {
requestAnimationFrame(animate);
controls.update();
updateLabels();
renderer.render(scene, camera);
}
window.onload = function() {
animate();
};
// Resize Handle
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>