3 D で 五 目 並 べ を 作 る ①
キモ過ぎるタイトルとサムネの先には…?
(特に何も無い)
AIを駆使して
3D五目並べ
を作ります。
コード書くのってマジで大変


<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>インタラクティブ正二十面体</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
#canvas-container {
width: 100%;
height: 100%;
cursor: grab;
}
#canvas-container:active {
cursor: grabbing;
}
#info {
position: absolute;
top: 20px;
left: 20px;
color: white;
background: rgba(0, 0, 0, 0.5);
padding: 20px;
border-radius: 10px;
backdrop-filter: blur(10px);
font-size: 14px;
line-height: 1.6;
}
#debug {
position: absolute;
top: 150px;
left: 20px;
color: yellow;
background: rgba(255, 0, 0, 0.7);
padding: 15px;
border-radius: 10px;
font-size: 16px;
font-weight: bold;
min-width: 200px;
}
#info h1 {
font-size: 24px;
margin-bottom: 10px;
font-weight: 600;
}
#info p {
margin: 5px 0;
}
</style>
</head>
<body>
<div id="info">
<h1>正二十面体</h1>
<p>🖱️ 左クリック長押しでドラッグして回転</p>
<p>⭕ 左クリック: ○を描画</p>
<p>⭕ 右クリック: ○を描画</p>
<p>📐 面の数: 20</p>
<p>🔺 頂点の数: 12</p>
</div>
<div id="debug">デバッグ情報がここに表示されます</div>
<div id="canvas-container"></div>
<script src="/https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// シーン、カメラ、レンダラーの設定
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.getElementById('canvas-container').appendChild(renderer.domElement);
// カメラの位置
camera.position.z = 5;
// 照明の設定
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const directionalLight1 = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight1.position.set(5, 5, 5);
scene.add(directionalLight1);
const directionalLight2 = new THREE.DirectionalLight(0x6667ab, 0.5);
directionalLight2.position.set(-5, -5, -5);
scene.add(directionalLight2);
// 正二十面体の作成
const geometry = new THREE.IcosahedronGeometry(2, 0);
// メインマテリアル(統一された色)
const material = new THREE.MeshBasicMaterial({
color: 0x00d4ff,
side: THREE.DoubleSide
});
const icosahedron = new THREE.Mesh(geometry, material);
// ワイヤーフレームの追加(エッジを強調)
const wireframeGeometry = new THREE.EdgesGeometry(geometry);
const wireframeMaterial = new THREE.LineBasicMaterial({
color: 0xffffff,
linewidth: 2,
transparent: true,
opacity: 0.6
});
const wireframe = new THREE.LineSegments(wireframeGeometry, wireframeMaterial);
icosahedron.add(wireframe);
// 一つの面が正面を向くように回転を調整
const faces = geometry.attributes.position;
const v0 = new THREE.Vector3(faces.getX(0), faces.getY(0), faces.getZ(0));
const v1 = new THREE.Vector3(faces.getX(1), faces.getY(1), faces.getZ(1));
const v2 = new THREE.Vector3(faces.getX(2), faces.getY(2), faces.getZ(2));
const edge1 = new THREE.Vector3().subVectors(v1, v0);
const edge2 = new THREE.Vector3().subVectors(v2, v0);
const faceNormal = new THREE.Vector3().crossVectors(edge1, edge2).normalize();
const targetNormal = new THREE.Vector3(0, 0, 1);
const quaternion = new THREE.Quaternion().setFromUnitVectors(faceNormal, targetNormal);
icosahedron.quaternion.copy(quaternion);
scene.add(icosahedron);
// Raycaster for click detection
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
// Store face labels
const faceLabels = new Map();
// Function to create 3D circle (torus)
function create3DCircle(color = 0xff0000) {
const geometry = new THREE.TorusGeometry(0.5, 0.1, 16, 32);
const material = new THREE.MeshBasicMaterial({ color: color, side: THREE.DoubleSide });
const torus = new THREE.Mesh(geometry, material);
return torus;
}
// Function to get face center
function getFaceCenter(faceIndex) {
const positions = geometry.attributes.position;
const indices = geometry.index;
let i1, i2, i3;
if (indices) {
// インデックスがある場合
i1 = indices.array[faceIndex * 3];
i2 = indices.array[faceIndex * 3 + 1];
i3 = indices.array[faceIndex * 3 + 2];
} else {
// インデックスがない場合(非インデックスジオメトリ)
i1 = faceIndex * 3;
i2 = faceIndex * 3 + 1;
i3 = faceIndex * 3 + 2;
}
const v1 = new THREE.Vector3(positions.array[i1 * 3], positions.array[i1 * 3 + 1], positions.array[i1 * 3 + 2]);
const v2 = new THREE.Vector3(positions.array[i2 * 3], positions.array[i2 * 3 + 1], positions.array[i2 * 3 + 2]);
const v3 = new THREE.Vector3(positions.array[i3 * 3], positions.array[i3 * 3 + 1], positions.array[i3 * 3 + 2]);
const center = new THREE.Vector3();
center.add(v1).add(v2).add(v3).divideScalar(3);
return center;
}
// 初期化:全ての面に○を配置(非表示)
function initializeFaceCircles() {
const faceCount = 20; // 正二十面体は常に20面
console.log('🎯 全', faceCount, '面に○を配置開始...');
for (let faceIndex = 0; faceIndex < faceCount; faceIndex++) {
const circle = create3DCircle(0xff0000);
// 面の中心を取得
const faceCenter = getFaceCenter(faceIndex);
// 正二十面体の中心から面の中心へ向かうベクトルが法線
const normal = faceCenter.clone().normalize();
// トーラスを面の法線に向ける
const up = new THREE.Vector3(0, 0, 1);
const quat = new THREE.Quaternion().setFromUnitVectors(up, normal);
circle.quaternion.copy(quat);
// 面の中心から少し外側に配置
const position = faceCenter.clone().add(normal.multiplyScalar(0.2));
circle.position.copy(position);
// 非表示にして追加
circle.visible = false;
icosahedron.add(circle);
faceLabels.set(faceIndex, circle);
}
console.log('✅ 全ての面に○を配置完了(非表示)。数:', faceLabels.size);
}
// 起動時に全面に○を配置
console.log('初期化関数を呼び出します...');
try {
initializeFaceCircles();
} catch (e) {
console.error('初期化関数でエラー:', e);
}
// 面の○を表示
function showFaceLabel(faceIndex) {
if (faceLabels.has(faceIndex)) {
const circle = faceLabels.get(faceIndex);
circle.visible = true;
console.log('✅ 面', faceIndex, 'の○を表示しました');
}
}
// マウスドラッグによる回転の実装
let isDragging = false;
let hasDragged = false;
let mouseDownTime = 0;
let mouseDownPos = { x: 0, y: 0 };
let previousMousePosition = { x: 0, y: 0 };
let rotationVelocity = { x: 0, y: 0 };
renderer.domElement.addEventListener('mousedown', (e) => {
if (e.button === 0) {
isDragging = true;
hasDragged = false;
mouseDownTime = Date.now();
mouseDownPos = { x: e.clientX, y: e.clientY };
previousMousePosition = { x: e.clientX, y: e.clientY };
rotationVelocity = { x: 0, y: 0 };
}
});
window.addEventListener('mousemove', (e) => {
if (isDragging) {
const deltaX = e.clientX - previousMousePosition.x;
const deltaY = e.clientY - previousMousePosition.y;
if (Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3) {
hasDragged = true;
}
rotationVelocity.x = deltaY * 0.005;
rotationVelocity.y = deltaX * 0.005;
const rotationSpeed = 0.005;
const quaternionY = new THREE.Quaternion();
quaternionY.setFromAxisAngle(new THREE.Vector3(0, 1, 0), deltaX * rotationSpeed);
const quaternionX = new THREE.Quaternion();
const xAxis = new THREE.Vector3(1, 0, 0);
xAxis.applyQuaternion(camera.quaternion);
quaternionX.setFromAxisAngle(xAxis, deltaY * rotationSpeed);
icosahedron.quaternion.multiplyQuaternions(quaternionY, icosahedron.quaternion);
icosahedron.quaternion.multiplyQuaternions(quaternionX, icosahedron.quaternion);
previousMousePosition = { x: e.clientX, y: e.clientY };
}
});
window.addEventListener('mouseup', (e) => {
if (e.button === 0) {
const clickDuration = Date.now() - mouseDownTime;
const distMoved = Math.sqrt(
Math.pow(e.clientX - mouseDownPos.x, 2) +
Math.pow(e.clientY - mouseDownPos.y, 2)
);
if (!hasDragged && clickDuration < 300 && distMoved < 5) {
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObject(icosahedron, false);
if (intersects.length > 0) {
const faceIndex = intersects[0].faceIndex;
console.log('左クリック検出 - 面:', faceIndex);
showFaceLabel(faceIndex);
}
}
isDragging = false;
hasDragged = false;
}
});
// Right click handler
renderer.domElement.addEventListener('contextmenu', (e) => {
e.preventDefault();
const debugDiv = document.getElementById('debug');
debugDiv.innerHTML = '右クリック検出!<br>マウス位置: ' + e.clientX + ', ' + e.clientY;
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
debugDiv.innerHTML += '<br>正規化座標: ' + mouse.x.toFixed(2) + ', ' + mouse.y.toFixed(2);
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObject(icosahedron, false);
debugDiv.innerHTML += '<br>交点数: ' + intersects.length;
if (intersects.length > 0) {
const faceIndex = intersects[0].faceIndex;
debugDiv.innerHTML += '<br>面番号: ' + faceIndex;
if (faceIndex !== undefined && faceIndex !== null) {
console.log('右クリック検出 - 面:', faceIndex);
showFaceLabel(faceIndex);
debugDiv.innerHTML += '<br>✅ ○を表示!';
}
} else {
debugDiv.innerHTML += '<br>❌ 面が見つかりません';
}
return false;
});
// タッチデバイス対応
let touchStartPos = { x: 0, y: 0 };
renderer.domElement.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = e.touches[0];
touchStartPos = { x: touch.clientX, y: touch.clientY };
previousMousePosition = { x: touch.clientX, y: touch.clientY };
isDragging = true;
});
renderer.domElement.addEventListener('touchmove', (e) => {
e.preventDefault();
if (isDragging && e.touches.length === 1) {
const touch = e.touches[0];
const deltaX = touch.clientX - previousMousePosition.x;
const deltaY = touch.clientY - previousMousePosition.y;
rotationVelocity.x = deltaY * 0.005;
rotationVelocity.y = deltaX * 0.005;
const rotationSpeed = 0.005;
const quaternionY = new THREE.Quaternion();
quaternionY.setFromAxisAngle(new THREE.Vector3(0, 1, 0), deltaX * rotationSpeed);
const quaternionX = new THREE.Quaternion();
const xAxis = new THREE.Vector3(1, 0, 0);
xAxis.applyQuaternion(camera.quaternion);
quaternionX.setFromAxisAngle(xAxis, deltaY * rotationSpeed);
icosahedron.quaternion.multiplyQuaternions(quaternionY, icosahedron.quaternion);
icosahedron.quaternion.multiplyQuaternions(quaternionX, icosahedron.quaternion);
previousMousePosition = { x: touch.clientX, y: touch.clientY };
}
});
renderer.domElement.addEventListener('touchend', (e) => {
e.preventDefault();
isDragging = false;
});
// ウィンドウリサイズ対応
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// アニメーションループ
function animate() {
requestAnimationFrame(animate);
if (!isDragging) {
rotationVelocity.x *= 0.95;
rotationVelocity.y *= 0.95;
if (Math.abs(rotationVelocity.x) > 0.0001 || Math.abs(rotationVelocity.y) > 0.0001) {
const quaternionY = new THREE.Quaternion();
quaternionY.setFromAxisAngle(new THREE.Vector3(0, 1, 0), rotationVelocity.y);
const quaternionX = new THREE.Quaternion();
const xAxis = new THREE.Vector3(1, 0, 0);
xAxis.applyQuaternion(camera.quaternion);
quaternionX.setFromAxisAngle(xAxis, rotationVelocity.x);
icosahedron.quaternion.multiplyQuaternions(quaternionY, icosahedron.quaternion);
icosahedron.quaternion.multiplyQuaternions(quaternionX, icosahedron.quaternion);
}
}
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>Created with assistance from AI
まだ作成途中なので、あまり期待しないで💦。
いいなと思ったら応援しよう!
⚠️警告⚠️
もしあなたがチップをくれたら、その分あなたのお金が減ります(?)。お金は大切に。