<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>月の満ち欠け立体シミュレーター</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Helvetica Neue', Arial, 'Hiragino Kaku Gothic ProN', 'Hiragino Sans', Meiryo, sans-serif;
user-select: none;
}
body {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
background-color:
#0b0f19;
color: white;
}
header {
background-color:
#161d31;
padding: 12px 20px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid
#25314f;
z-index: 10;
}
header h1 {
font-size: 20px;
color:
#f1c40f;
display: flex;
align-items: center;
gap: 8px;
}
.main-container {
display: flex;
flex: 1;
position: relative;
overflow: hidden;
}
/* 左画面:地球から見た空(地上視点) */
.earth-view {
flex: 1;
border-right: 2px solid
#25314f;
position: relative;
display: flex;
flex-direction: column;
background: linear-gradient(to bottom, #050811 0%,
#0a1128 70%,
#1a2a6c 100%);
}
/* 右画面:宇宙から見た配置(俯瞰視点) */
.space-view {
flex: 1;
position: relative;
background-color:
#05070f;
}
.view-title {
position: absolute;
top: 15px;
left: 20px;
background: rgba(22, 29, 49, 0.85);
padding: 6px 14px;
border-radius: 20px;
font-size: 14px;
font-weight: bold;
border: 1px solid
#34495e;
pointer-events: none;
z-index: 5;
}
#canvas-space {
width: 100%;
height: 100%;
}
/* 地上視点の夜空表現 */
.sky-display {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.moon-look-container {
width: 260px;
height: 260px;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
border: 4px solid
#34495e;
position: relative;
overflow: hidden;
box-shadow: 0 0 30px rgba(241, 196, 15, 0.1);
}
/* 月の満ち欠けを2Dマスクで表現(小学生に一番わかりやすい影絵方式) */
.moon-look-bg {
width: 100%;
height: 100%;
background-color:
#2c3e50; /* 影の部分 */
position: relative;
}
.moon-look-light {
width: 100%;
height: 100%;
background-color:
#f1c40f; /* 光の部分 */
position: absolute;
top: 0;
left: 0;
border-radius: 50%;
box-shadow: 0 0 20px
#f1c40f;
}
.moon-look-shadow {
width: 100%;
height: 100%;
background-color:
#2c3e50;
position: absolute;
top: 0;
border-radius: 50%;
}
.direction-label {
position: absolute;
bottom: 20px;
font-size: 18px;
font-weight: bold;
background:
#e74c3c;
padding: 4px 15px;
border-radius: 4px;
}
/* 下部コントロールエリア */
.control-panel {
background-color:
#161d31;
padding: 15px 20px;
display: flex;
flex-direction: column;
gap: 12px;
border-top: 2px solid
#25314f;
z-index: 10;
}
.button-row {
display: flex;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
}
.btn {
background-color:
#232d4b;
color: white;
border: 2px solid
#34495e;
padding: 10px 18px;
border-radius: 8px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.btn:hover {
background-color:
#2e3c64;
border-color:
#f1c40f;
}
.btn.active {
background-color:
#f1c40f;
color:
#161d31;
border-color:
#f39c12;
}
.btn-icon {
font-size: 18px;
}
.slider-row {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
background: rgba(0,0,0,0.2);
padding: 8px 20px;
border-radius: 30px;
max-width: 600px;
margin: 0 auto;
width: 100%;
}
.slider-row label {
font-size: 14px;
font-weight: bold;
color:
#a3b1cc;
white-space: nowrap;
}
.moon-slider {
flex: 1;
-webkit-appearance: none;
background:
#25314f;
height: 8px;
border-radius: 4px;
outline: none;
}
.moon-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background:
#f1c40f;
cursor: pointer;
box-shadow: 0 0 8px
#f1c40f;
}
/* 太陽光のガイダンス矢印表示 */
.sun-light-guide {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
color:
#f39c12;
font-weight: bold;
font-size: 12px;
pointer-events: none;
background: rgba(0,0,0,0.4);
padding: 10px;
border-radius: 6px;
}
.sun-arrow {
font-size: 20px;
animation: blink 1.5s infinite;
}
@keyframes blink {
0% { opacity: 0.3; }
50% { opacity: 1; }
100% { opacity: 0.3; }
}
</style>
<!-- Three.js via CDN -->
<script src="
cdnjs.cloudflare.com/ajax/li…"></script>
</head>
<body>
<header>
<h1>🌙 月の満ち欠け(みちかけ)シミュレーター</h1>
</header>
<div class="main-container">
<!-- 左画面:地球からの見え方 -->
<div class="earth-view">
<div class="view-title" style="color:
#f1c40f;">① 地球(まんなか)から見た月の形</div>
<div class="sky-display">
<div class="moon-look-container">
<div class="moon-look-bg">
<div class="moon-look-light" id="moon-light"></div>
<div class="moon-look-shadow" id="moon-shadow"></div>
</div>
</div>
<div class="direction-label">みなみの空</div>
</div>
</div>
<!-- 右画面:宇宙からの配置 -->
<div class="space-view">
<div class="view-title" style="color:
#3498db;">② 宇宙から見た「地球」と「月」のならび</div>
<div id="canvas-space"></div>
<div class="sun-light-guide">
<span>たいようの光</span>
<span class="sun-arrow">⬅</span>
<span class="sun-arrow">⬅</span>
<span class="sun-arrow">⬅</span>
</div>
</div>
</div>
<!-- コントロールパネル -->
<div class="control-panel">
<!-- スライダー操作 -->
<div class="slider-row">
<label>月をうごかす:</label>
<input type="range" id="moon-orbit-slider" class="moon-slider" min="0" max="360" value="180">
</div>
<!-- かんたんボタン -->
<div class="button-row">
<button class="btn" id="btn-new" onclick="setMoonPhase(0)">
<span class="btn-icon">🌑</span><span>新月(しんげつ)</span>
</button>
<button class="btn" id="btn-crescent" onclick="setMoonPhase(45)">
<span class="btn-icon">🌙</span><span>三日月(みかづき)</span>
</button>
<button class="btn" id="btn-half1" onclick="setMoonPhase(90)">
<span class="btn-icon">🌓</span><span>上弦の月(じょうげん)</span>
</button>
<button class="btn" id="btn-full" onclick="setMoonPhase(180)">
<span class="btn-icon">🌕</span><span>満月(まんげつ)</span>
</button>
<button class="btn" id="btn-half2" onclick="setMoonPhase(270)">
<span class="btn-icon">🌗</span><span>下弦の月(かげん)</span>
</button>
</div>
</div>
<script>
// --- Three.js 宇宙視点のセットアップ ---
let scene, camera, renderer;
let earth, moon, moonOrbitGroup;
let currentAngle = 180; // 初期状態は満月位置
const container = document.getElementById('canvas-space');
init3D();
updateSimulation();
function init3D() {
scene = new THREE.Scene();
// 宇宙のカメラ(真上から見下ろす)
camera = new THREE.PerspectiveCamera(40, container.clientWidth / container.clientHeight, 0.1, 1000);
camera.position.set(0, 45, 0);
camera.lookAt(0, 0, 0);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
container.appendChild(renderer.domElement);
// ライト(太陽光:右側から左側へ平行に照らす)
const sunLight = new THREE.DirectionalLight(0xffffff, 1.2);
sunLight.position.set(100, 0, 0); // 右から
scene.add(sunLight);
// 宇宙のうっすらとした全体光
const ambientLight = new THREE.AmbientLight(0x222222);
scene.add(ambientLight);
// --- 地球の作成 ---
const earthGeom = new THREE.SphereGeometry(3.5, 32, 32);
const earthMat = new THREE.MeshStandardMaterial({
color: 0x3498db,
roughness: 0.5,
flatShading: false
});
earth = new THREE.Mesh(earthGeom, earthMat);
scene.add(earth);
// 地球の「昼(太陽側)」と「夜(かげ)」をわかりやすく
// (Three.jsのライトが自動で陰影を作ってくれます)
// --- 月の公転グループ(回転の軸) ---
moonOrbitGroup = new
THREE.Group();
scene.add(moonOrbitGroup);
// --- 月の作成 ---
const moonGeom = new THREE.SphereGeometry(1.2, 32, 32);
const moonMat = new THREE.MeshStandardMaterial({
color: 0xf1c40f,
emissive: 0x332200,
roughness: 0.8
});
moon = new THREE.Mesh(moonGeom, moonMat);
// 軌道半径14のところに配置
moon.position.set(14, 0, 0);
moonOrbitGroup.add(moon);
// 公転の軌道線(ガイド)
const ringGeom = new THREE.RingGeometry(13.9, 14.1, 64);
ringGeom.rotateX(Math.PI / 2);
const ringMat = new THREE.MeshBasicMaterial({ color: 0x25314f, side: THREE.DoubleSide });
const orbitLine = new THREE.Mesh(ringGeom, ringMat);
scene.add(orbitLine);
// イベント登録
document.getElementById('moon-orbit-slider').addEventListener('input', onSliderMove);
window.addEventListener('resize', onWindowResize);
}
// --- シミュレーションの連動処理 ---
function onSliderMove(e) {
currentAngle = parseInt(
e.target.value);
updateSimulation();
}
function setMoonPhase(angle) {
currentAngle = angle;
document.getElementById('moon-orbit-slider').value = angle;
updateSimulation();
}
function updateSimulation() {
// 1. 3D空間の月を動かす(ラジアンに変換)
// 太陽光が「右(東)」から来ているため、角度計算を合わせます
const rad = (currentAngle * Math.PI) / 180;
moonOrbitGroup.rotation.y = rad;
// 2. 左画面の「地球から見た形」を2D影絵マスクで計算・反映
const shadow = document.getElementById('moon-shadow');
const light = document.getElementById('moon-light');
// ボタンのハイライト更新
document.querySelectorAll('.btn').forEach(b => b.classList.remove('active'));
if (currentAngle === 0) document.getElementById('btn-new').classList.add('active');
if (currentAngle === 45) document.getElementById('btn-crescent').classList.add('active');
if (currentAngle === 90) document.getElementById('btn-half1').classList.add('active');
if (currentAngle === 180) document.getElementById('btn-full').classList.add('active');
if (currentAngle === 270) document.getElementById('btn-half2').classList.add('active');
// 満ち欠けビジュアルロジック(CSSマスク・変形による滑らかな表現)
// 角度(0=新月, 90=上弦, 180=満月, 270=下弦)
let percent = (currentAngle % 360);
if (percent >= 0 && percent < 90) {
// 新月 〜 上弦(右側が少しずつ満ちていく三日月)
let factor = percent / 90; // 0 -> 1
light.style.transform = 'scaleX(1)';
shadow.style.transform = `scaleX(${1 - factor})`;
shadow.style.left = '0';
shadow.style.borderRadius = '0 50% 50% 0 / 0 50% 50% 0';
if(percent === 0) {
shadow.style.width = '100%';
shadow.style.borderRadius = '50%';
} else {
shadow.style.width = '50%';
}
}
else if (percent >= 90 && percent <= 180) {
// 上弦 〜 満月(右側がふくらんでいく)
let factor = (percent - 90) / 90; // 0 -> 1
light.style.transform = 'scaleX(1)';
shadow.style.transform = `scaleX(${factor})`;
shadow.style.left = '0';
shadow.style.width = '50%';
shadow.style.borderRadius = '50% 0 0 50% / 50% 0 0 50%';
// 影を反転させて光を太らせる
shadow.style.backgroundColor = '
#f1c40f';
if (percent === 180) {
shadow.style.transform = 'scaleX(0)';
}
}
else if (percent > 180 && percent <= 270) {
// 満月 〜 下弦(右側から欠けていく)
let factor = (percent - 180) / 90; // 0 -> 1
light.style.transform = 'scaleX(1)';
shadow.style.backgroundColor = '
#2c3e50'; // 影に戻す
shadow.style.transform = `scaleX(${factor})`;
shadow.style.left = '0';
shadow.style.width = '50%';
shadow.style.borderRadius = '50% 0 0 50% / 50% 0 0 50%';
}
else if (percent > 270 && percent < 360) {
// 下弦 〜 新月(左側の細い月になって消えていく)
let factor = (percent - 270) / 90; // 0 -> 1
light.style.transform = 'scaleX(1)';
shadow.style.backgroundColor = '
#2c3e50';
shadow.style.transform = `scaleX(${1 - factor})`;
shadow.style.left = '50%';
shadow.style.width = '50%';
shadow.style.borderRadius = '0 50% 50% 0 / 0 50% 50% 0';
}
// 描画実行
renderer.render(scene, camera);
}
function onWindowResize() {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
updateSimulation();
}
// ループアニメーション(地球の自転をうっすら表現)
function animate() {
requestAnimationFrame(animate);
if (earth) earth.rotation.y = 0.002;
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>