(日本語は下に)
Output :
==
Prompt = Title
I'll create a simple billiard game set in non-Euclidean space! This will be a fascinating physics experiment where the geometry itself affects how the balls move.I've created a fascinating non-Euclidean billiards game! Here's what makes it special:
Non-Euclidean Physics Features:
- Curved trajectories: Balls don't travel in straight lines - they curve through the space based on the geometry
- Geodesic motion: The balls follow curved paths that represent "straight lines" in non-Euclidean space
- Distance-dependent curvature: The further from center, the stronger the curvature effect
- Boundary effects: Collisions with walls add rotational components due to the geometry
Controls:
- Click and drag from the red cue ball to aim and shoot
- Adjust Curvature slider: positive values create spherical geometry (inward curves), negative creates hyperbolic geometry (outward curves)
- Adjust Friction to control how quickly balls slow down
Visual Elements:
- Concentric circles show the curvature field
- Velocity vectors display on moving balls
- Glowing effects for that cosmic billiards feel
- Power meter when aiming
The physics simulation includes proper collision detection with non-Euclidean twists - when balls collide, the curved geometry adds interesting spin effects. Try different curvature values to see how dramatically the ball behavior changes!
This demonstrates how geometry itself can be a game mechanic, making familiar physics feel alien and interesting.
Building a Non-Euclidean Billiards Game with JavaScript
Introduction
Tired of regular billiards games? Let's venture into the fascinating world of non-Euclidean geometry and create a billiards game where balls don't travel in straight lines! This project combines advanced mathematical concepts with practical game development, creating an educational yet entertaining physics simulation.
In this implementation, we'll explore how geometry itself can become a game mechanic, making familiar physics feel alien and intriguing.
Demo
First, let's see what we're building:
<!-- Complete HTML code provided at the end of the article -->
Understanding Non-Euclidean Geometry
The geometry we're familiar with is Euclidean geometry - flat planes where parallel lines never meet and triangle angles sum to 180 degrees.
Non-Euclidean geometry introduces curved spaces:
- Spherical geometry (positive curvature): Like Earth's surface - parallel lines converge
- Hyperbolic geometry (negative curvature): Saddle-shaped surfaces - parallel lines diverge
In these spaces, the concept of "straight lines" changes, and object motion becomes unique and fascinating.
Implementation Overview
1. Curvature-Based Trajectory Modification
The core of our non-Euclidean physics lies in how we modify velocity vectors based on position:
// Apply non-Euclidean curvature effects
const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
if (speed > 0) {
// Distance from center affects curvature strength
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const distFromCenter = Math.sqrt(
(ball.x - centerX) * (ball.x - centerX) +
(ball.y - centerY) * (ball.y - centerY)
);
// Geodesic deviation - velocity curves based on position
const curvatureEffect = curvature * distFromCenter * 0.01;
// Rotate velocity vector to simulate curved space
const angle = curvatureEffect;
const newVx = ball.vx * Math.cos(angle) - ball.vy * Math.sin(angle);
const newVy = ball.vx * Math.sin(angle) + ball.vy * Math.cos(angle);
ball.vx = newVx;
ball.vy = newVy;
}
2. Non-Euclidean Boundary Effects
Boundaries in curved space exhibit special properties:
// Boundary collisions with non-Euclidean effects
if (ball.x - ball.radius <= 0 || ball.x + ball.radius >= canvas.width) {
ball.vx = -ball.vx * 0.8;
ball.x = Math.max(ball.radius, Math.min(canvas.width - ball.radius, ball.x));
// Non-Euclidean boundary effect adds perpendicular velocity
ball.vy += curvature * 50;
}
if (ball.y - ball.radius <= 0 || ball.y + ball.radius >= canvas.height) {
ball.vy = -ball.vy * 0.8;
ball.y = Math.max(ball.radius, Math.min(canvas.height - ball.radius, ball.y));
// Cross-coupling due to curved geometry
ball.vx += curvature * 50;
}
3. Enhanced Collision Physics
Standard collision detection is augmented with curvature-dependent effects:
// After standard collision response, add non-Euclidean effects
const curvatureBonus = curvature * 200;
ball1.vx += curvatureBonus * cos;
ball1.vy += curvatureBonus * sin;
ball2.vx -= curvatureBonus * cos;
ball2.vy -= curvatureBonus * sin;
4. Visual Representation
The curvature field is visualized using concentric circles:
// Draw curvature field visualization
ctx.strokeStyle = '#222';
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
for (let r = 50; r < 300; r += 50) {
ctx.beginPath();
ctx.arc(centerX, centerY, r, 0, Math.PI * 2);
ctx.stroke();
}
Mathematical Foundation
Geodesic Motion
In non-Euclidean spaces, objects follow geodesics - the shortest paths in that geometry. While these are straight lines in flat space, they become curves in curved space.
Parallel Transport
When moving a vector through curved space, its direction changes even if it remains "parallel" to itself. This is implemented as velocity vector rotation.
Riemann Curvature
The Riemann curvature tensor describes how space is curved. We simplify this to a distance-proportional rotation effect that captures the essential behavior.
Advanced Customization Ideas
1. Variable Curvature Fields
// Gaussian curvature distribution
const gaussianCurvature = (x, y) => {
const dx = x - centerX;
const dy = y - centerY;
const r2 = dx*dx + dy*dy;
return baseCurvature * Math.exp(-r2 / 10000);
};
// Apply variable curvature
const localCurvature = gaussianCurvature(ball.x, ball.y);
const curvatureEffect = localCurvature * distFromCenter * 0.01;
2. Time-Varying Geometry
// Pulsating curvature creates dynamic geometry
const time = Date.now() * 0.001;
const pulsatingCurvature = baseCurvature * (1 + 0.5 * Math.sin(time * 2));
3. Multiple Curvature Centers
// Multiple "gravity wells" of curvature
const multiCenterCurvature = (x, y) => {
let totalCurvature = 0;
for (let center of curvatureCenters) {
const dist = Math.sqrt((x-center.x)**2 + (y-center.y)**2);
totalCurvature += center.strength / (dist + 1);
}
return totalCurvature;
};
4. Anisotropic Curvature
// Different curvature in different directions
const anisotropicCurvature = (x, y, vx, vy) => {
const angle = Math.atan2(vy, vx);
const curvatureX = baseCurvature * Math.cos(angle * 2);
const curvatureY = baseCurvature * Math.sin(angle * 2);
return {x: curvatureX, y: curvatureY};
};
Educational Value
This project demonstrates several advanced concepts:
- Differential Geometry: Curvature and geodesics in practice
- General Relativity: Object motion in curved spacetime
- Numerical Methods: Physics simulation implementation
- Game Physics: Advanced collision detection and response
- Visualization: Making abstract math concepts tangible
Performance Optimization
WebGL Implementation
For better performance with many balls:
// Vertex shader for curvature calculation
const vertexShader = `
attribute vec2 position;
attribute vec2 velocity;
uniform float curvature;
uniform vec2 center;
uniform float time;
vec2 applyCurvature(vec2 vel, vec2 pos) {
float dist = length(pos - center);
float angle = curvature * dist * 0.01;
mat2 rotation = mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
return rotation * vel;
}
void main() {
vec2 newVelocity = applyCurvature(velocity, position);
vec2 newPosition = position + newVelocity * time;
gl_Position = vec4(newPosition, 0.0, 1.0);
}
`;
Spatial Partitioning
// Quadtree for efficient collision detection
class Quadtree {
constructor(bounds, maxObjects = 10, maxLevels = 5, level = 0) {
this.bounds = bounds;
this.maxObjects = maxObjects;
this.maxLevels = maxLevels;
this.level = level;
this.objects = [];
this.nodes = [];
}
insert(obj) {
if (this.nodes.length > 0) {
const index = this.getIndex(obj);
if (index !== -1) {
this.nodes[index].insert(obj);
return;
}
}
this.objects.push(obj);
if (this.objects.length > this.maxObjects && this.level < this.maxLevels) {
this.split();
// Redistribute objects to child nodes
}
}
}
3D Extension with Three.js
For true 3D non-Euclidean billiards:
// Create a curved surface using Three.js
const geometry = new THREE.ParametricGeometry((u, v, target) => {
const x = (u - 0.5) * 20;
const y = (v - 0.5) * 20;
const z = curvature * (x*x + y*y) * 0.01; // Paraboloid surface
target.set(x, y, z);
}, 100, 100);
// Ball physics on curved surface
function updateBallOnSurface(ball) {
// Calculate surface normal at ball position
const normal = getSurfaceNormal(ball.position);
// Project velocity onto tangent plane
const projectedVelocity = ball.velocity.clone().projectOnPlane(normal);
// Apply geodesic transport
const transportedVelocity = parallelTransport(projectedVelocity, ball.position);
ball.velocity.copy(transportedVelocity);
ball.position.add(ball.velocity);
}
Testing and Validation
Unit Tests for Physics
describe('Non-Euclidean Physics', () => {
test('zero curvature behaves like Euclidean space', () => {
const ball = {x: 100, y: 100, vx: 1, vy: 0};
applyCurvature(ball, 0);
expect(ball.vx).toBeCloseTo(1);
expect(ball.vy).toBeCloseTo(0);
});
test('positive curvature curves trajectory clockwise', () => {
const ball = {x: 100, y: 100, vx: 1, vy: 0};
applyCurvature(ball, 0.01);
expect(ball.vy).toBeLessThan(0);
});
});
Performance Benchmarks
function benchmarkPhysics() {
const startTime = performance.now();
for (let i = 0; i < 10000; i++) {
updateBalls();
}
const endTime = performance.now();
console.log(`Physics update: ${endTime - startTime}ms for 10k iterations`);
}
Real-World Applications
This technique has applications beyond games:
- Scientific Visualization: Modeling particle behavior in curved spacetime
- Educational Software: Teaching general relativity concepts
- Architectural Design: Understanding geodesics in curved structures
- Robotics: Path planning on curved surfaces
Conclusion
We've successfully implemented a non-Euclidean billiards game that demonstrates:
- Practical Application of Abstract Mathematics: Making differential geometry tangible
- Advanced Game Physics: Going beyond standard collision detection
- Educational Technology: Learning through interactive experience
- Creative Programming: Exploring unconventional game mechanics
The project showcases how mathematical concepts can drive innovative gameplay mechanics, creating experiences that are both educational and entertaining.
Try modifying the code to create your own non-Euclidean physics worlds - perhaps with multiple curvature centers, time-varying geometry, or even higher-dimensional spaces!
Complete Source Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Non-Euclidean Billiards</title>
<style>
body {
margin: 0;
padding: 20px;
background: #0a0a0a;
color: white;
font-family: 'Courier New', monospace;
display: flex;
flex-direction: column;
align-items: center;
}
h1 {
color: #00ff88;
text-align: center;
margin-bottom: 10px;
}
.info {
text-align: center;
margin-bottom: 20px;
color: #888;
font-size: 14px;
}
canvas {
border: 2px solid #333;
background: #111;
cursor: crosshair;
}
.controls {
margin-top: 15px;
display: flex;
gap: 15px;
align-items: center;
}
button {
padding: 8px 16px;
background: #333;
border: 1px solid #555;
color: white;
cursor: pointer;
font-family: inherit;
transition: background 0.2s;
}
button:hover {
background: #444;
}
.slider-group {
display: flex;
align-items: center;
gap: 8px;
}
input[type="range"] {
width: 100px;
}
.geometry-info {
margin-top: 10px;
font-size: 12px;
color: #666;
text-align: center;
}
</style>
</head>
<body>
<h1>Non-Euclidean Billiards</h1>
<div class="info">
Click and drag to aim and shoot • Balls curve through hyperbolic space
</div>
<canvas id="gameCanvas" width="600" height="400"></canvas>
<div class="controls">
<button onclick="resetGame()">Reset</button>
<div class="slider-group">
<label>Curvature:</label>
<input type="range" id="curvatureSlider" min="-0.02" max="0.02" step="0.001" value="0.005">
<span id="curvatureValue">0.005</span>
</div>
<div class="slider-group">
<label>Friction:</label>
<input type="range" id="frictionSlider" min="0.98" max="1.0" step="0.001" value="0.995">
<span id="frictionValue">0.995</span>
</div>
</div>
<div class="geometry-info">
Positive curvature = spherical geometry • Negative curvature = hyperbolic geometry
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let balls = [];
let mouseDown = false;
let mouseStart = {x: 0, y: 0};
let mouseEnd = {x: 0, y: 0};
let curvature = 0.005;
let friction = 0.995;
// Initialize balls
function initBalls() {
balls = [
{x: 150, y: 200, vx: 0, vy: 0, radius: 12, color: '#ff4444', isCue: true},
{x: 400, y: 180, vx: 0, vy: 0, radius: 10, color: '#4444ff'},
{x: 420, y: 200, vx: 0, vy: 0, radius: 10, color: '#44ff44'},
{x: 440, y: 220, vx: 0, vy: 0, radius: 10, color: '#ffff44'},
{x: 400, y: 220, vx: 0, vy: 0, radius: 10, color: '#ff44ff'},
{x: 420, y: 240, vx: 0, vy: 0, radius: 10, color: '#44ffff'}
];
}
initBalls();
// Non-Euclidean physics simulation
function updateBalls() {
for (let ball of balls) {
if (Math.abs(ball.vx) < 0.01 && Math.abs(ball.vy) < 0.01) {
ball.vx = 0;
ball.vy = 0;
continue;
}
// Apply non-Euclidean curvature
const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
if (speed > 0) {
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const distFromCenter = Math.sqrt((ball.x - centerX) * (ball.x - centerX) +
(ball.y - centerY) * (ball.y - centerY));
const curvatureEffect = curvature * distFromCenter * 0.01;
const angle = curvatureEffect;
const newVx = ball.vx * Math.cos(angle) - ball.vy * Math.sin(angle);
const newVy = ball.vx * Math.sin(angle) + ball.vy * Math.cos(angle);
ball.vx = newVx;
ball.vy = newVy;
}
// Update position
ball.x += ball.vx;
ball.y += ball.vy;
// Apply friction
ball.vx *= friction;
ball.vy *= friction;
// Boundary collisions with non-Euclidean effects
if (ball.x - ball.radius <= 0 || ball.x + ball.radius >= canvas.width) {
ball.vx = -ball.vx * 0.8;
ball.x = Math.max(ball.radius, Math.min(canvas.width - ball.radius, ball.x));
ball.vy += curvature * 50;
}
if (ball.y - ball.radius <= 0 || ball.y + ball.radius >= canvas.height) {
ball.vy = -ball.vy * 0.8;
ball.y = Math.max(ball.radius, Math.min(canvas.height - ball.radius, ball.y));
ball.vx += curvature * 50;
}
}
// Ball collisions
for (let i = 0; i < balls.length; i++) {
for (let j = i + 1; j < balls.length; j++) {
const ball1 = balls[i];
const ball2 = balls[j];
const dx = ball2.x - ball1.x;
const dy = ball2.y - ball1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < ball1.radius + ball2.radius) {
const angle = Math.atan2(dy, dx);
const sin = Math.sin(angle);
const cos = Math.cos(angle);
const vx1 = ball1.vx * cos + ball1.vy * sin;
const vy1 = ball1.vy * cos - ball1.vx * sin;
const vx2 = ball2.vx * cos + ball2.vy * sin;
const vy2 = ball2.vy * cos - ball2.vx * sin;
const finalVx1 = vx2;
const finalVx2 = vx1;
ball1.vx = finalVx1 * cos - vy1 * sin;
ball1.vy = vy1 * cos + finalVx1 * sin;
ball2.vx = finalVx2 * cos - vy2 * sin;
ball2.vy = vy2 * cos + finalVx2 * sin;
const overlap = ball1.radius + ball2.radius - distance;
const separateX = cos * overlap * 0.5;
const separateY = sin * overlap * 0.5;
ball1.x -= separateX;
ball1.y -= separateY;
ball2.x += separateX;
ball2.y += separateY;
const curvatureBonus = curvature * 200;
ball1.vx += curvatureBonus * cos;
ball1.vy += curvatureBonus * sin;
ball2.vx -= curvatureBonus * cos;
ball2.vy -= curvatureBonus * sin;
}
}
}
}
function draw() {
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw curvature visualization
ctx.strokeStyle = '#222';
ctx.lineWidth = 1;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
for (let r = 50; r < 300; r += 50) {
ctx.beginPath();
ctx.arc(centerX, centerY, r, 0, Math.PI * 2);
ctx.stroke();
}
// Draw balls
for (let ball of balls) {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fillStyle = ball.color;
ctx.fill();
ctx.shadowBlur = 10;
ctx.shadowColor = ball.color;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius * 0.7, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
if (ball.vx !== 0 || ball.vy !== 0) {
ctx.strokeStyle = ball.color;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(ball.x, ball.y);
ctx.lineTo(ball.x + ball.vx * 5, ball.y + ball.vy * 5);
ctx.stroke();
}
}
// Draw aim line
if (mouseDown) {
const cueBall = balls[0];
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(cueBall.x, cueBall.y);
ctx.lineTo(mouseEnd.x, mouseEnd.y);
ctx.stroke();
ctx.setLineDash([]);
const power = Math.sqrt((mouseEnd.x - cueBall.x) ** 2 + (mouseEnd.y - cueBall.y) ** 2);
ctx.fillStyle = `hsl(${Math.max(0, 120 - power)}, 100%, 50%)`;
ctx.fillRect(10, 10, Math.min(200, power), 10);
}
}
function gameLoop() {
updateBalls();
draw();
requestAnimationFrame(gameLoop);
}
// Mouse controls
canvas.addEventListener('mousedown', (e) => {
const rect = canvas.getBoundingClientRect();
mouseStart.x = e.clientX - rect.left;
mouseStart.y = e.clientY - rect.top;
mouseEnd.x = mouseStart.x;
mouseEnd.y = mouseStart.y;
mouseDown = true;
});
canvas.addEventListener('mousemove', (e) => {
if (mouseDown) {
const rect = canvas.getBoundingClientRect();
mouseEnd.x = e.clientX - rect.left;
mouseEnd.y = e.clientY - rect.top;
}
});
canvas.addEventListener('mouseup', (e) => {
if (mouseDown) {
const cueBall = balls[0];
const power = 0.3;
cueBall.vx = (mouseEnd.x - cueBall.x) * power * 0.1;
cueBall.vy = (mouseEnd.y - cueBall.y) * power * 0.1;
mouseDown = false;
}
});
// Controls
document.getElementById('curvatureSlider').addEventListener('input', (e) => {
curvature = parseFloat(e.target.value);
document.getElementById('curvatureValue').textContent = curvature.toFixed(3);
});
document.getElementById('frictionSlider').addEventListener('input', (e) => {
friction = parseFloat(e.target.value);
document.getElementById('frictionValue').textContent = friction.toFixed(3);
});
function resetGame() {
initBalls();
}
gameLoop();
</script>
</body>
</html>
Further Reading
- "Riemann Geometry and General Relativity" by Stephanie Alexander
- "Game Physics Engine Development" by Ian Millington
- "Real-Time Rendering" by Tomas Akenine-Möller
- "The Geometry of Physics" by Theodore Frankel
非ユークリッド空間のビリヤードゲームを作成しました
Claude Code で記事タイトルのPromptですぐ作れます
以下が特徴です:
非ユークリッド物理の特徴:
- 曲線軌道: ボールは直線ではなく、空間の幾何学に基づいて曲線を描いて移動
- 測地線運動: ボールは非ユークリッド空間での「直線」を表す曲線経路を辿る
- 距離依存曲率: 中心から遠いほど、曲率効果が強くなる
- 境界効果: 壁との衝突で幾何学的な回転成分が追加される
操作方法:
- 赤いキューボールからクリック&ドラッグで狙いを定めて打つ
- 曲率スライダー調整: 正の値は球面幾何学(内向きカーブ)、負の値は双曲幾何学(外向きカーブ)を作成
- 摩擦でボールが減速する速度を調整
視覚要素:
- 同心円が曲率場を表示
- 移動中のボールに速度ベクトル表示
- 宇宙的ビリヤードの雰囲気を演出するグロー効果
- 狙い時のパワーメーター
物理シミュレーションには、非ユークリッド的なひねりを加えた適切な衝突検出が含まれています。ボール同士が衝突すると、曲がった幾何学が興味深いスピン効果を追加します。異なる曲率値を試して、ボールの挙動がどれほど劇的に変化するかを確認してみてください!
これは、幾何学自体がゲームメカニクスになり得ることを示し、馴染みのある物理を異質で興味深いものに変える例です。
JavaScript で作る非ユークリッド空間のビリヤードゲーム
はじめに
普通のビリヤードゲームに飽きていませんか?今回は、非ユークリッド幾何学の世界でビリヤードを楽しめるゲームを JavaScript で実装してみました。ボールが直線ではなく曲線を描いて移動する、とても不思議で面白い物理シミュレーションです。
数学的な理論を実際のゲームとして体験できる、教育的かつエンターテイメント性の高いプロジェクトになっています。
デモ
まずは実際に動くものを見てみましょう!
<!-- 完全なHTMLコードは記事末尾に掲載 -->
非ユークリッド幾何学とは?
私たちが普段慣れ親しんでいる幾何学はユークリッド幾何学です。平面上で平行線は交わらず、三角形の内角の和は180度になります。
しかし、非ユークリッド幾何学では:
- 球面幾何学(正の曲率):地球表面のような曲面。平行線は交わる
- 双曲幾何学(負の曲率):サドル型の曲面。平行線は離れていく
これらの空間では、「直線」の概念が変わり、物体の運動も独特になります。
実装のポイント
1. 曲率による軌道の変化
非ユークリッド空間での物体の運動を表現するため、速度ベクトルを位置に応じて回転させます:
// 非ユークリッド曲率の適用
const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
if (speed > 0) {
// 中心からの距離が曲率の強度に影響
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const distFromCenter = Math.sqrt(
(ball.x - centerX) * (ball.x - centerX) +
(ball.y - centerY) * (ball.y - centerY)
);
// 測地線偏差 - 位置に基づいて速度が曲がる
const curvatureEffect = curvature * distFromCenter * 0.01;
// 速度ベクトルを回転
const angle = curvatureEffect;
const newVx = ball.vx * Math.cos(angle) - ball.vy * Math.sin(angle);
const newVy = ball.vx * Math.sin(angle) + ball.vy * Math.cos(angle);
ball.vx = newVx;
ball.vy = newVy;
}
2. 境界での特殊効果
非ユークリッド空間では、境界での反射も特別な効果を持ちます:
// 境界衝突での非ユークリッド効果
if (ball.x - ball.radius <= 0 || ball.x + ball.radius >= canvas.width) {
ball.vx = -ball.vx * 0.8;
// 非ユークリッド境界効果
ball.vy += curvature * 50;
}
3. ボール同士の衝突
通常の物理衝突に加えて、曲率による追加効果を実装:
// 非ユークリッド衝突効果を追加
const curvatureBonus = curvature * 200;
ball1.vx += curvatureBonus * cos;
ball1.vy += curvatureBonus * sin;
ball2.vx -= curvatureBonus * cos;
ball2.vy -= curvatureBonus * sin;
4. 視覚的表現
曲率場を同心円で視覚化:
// 曲率の可視化
ctx.strokeStyle = '#222';
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
for (let r = 50; r < 300; r += 50) {
ctx.beginPath();
ctx.arc(centerX, centerY, r, 0, Math.PI * 2);
ctx.stroke();
}
物理シミュレーションの理論背景
測地線運動
非ユークリッド空間では、物体は測地線(その空間での最短経路)に沿って移動します。平面では直線ですが、曲がった空間では曲線になります。
平行輸送
ベクトルを曲がった空間で移動させると、その向きが変化します。これが速度ベクトルの回転として実装されています。
リーマン曲率
空間の曲がり具合を表すリーマン曲率を簡略化して、距離に比例する回転として実装しました。
カスタマイズのアイデア
1. 異なる曲率分布
// ガウシアン曲率分布
const gaussianCurvature = (x, y) => {
const dx = x - centerX;
const dy = y - centerY;
const r2 = dx*dx + dy*dy;
return curvature * Math.exp(-r2 / 10000);
};
2. 時間変化する曲率
// 脈動する曲率
const pulsatingCurvature = baseCurvature * Math.sin(time * 0.01);
3. 複数の曲率中心
// 複数の重力井戸のような効果
const multiCenterCurvature = (x, y) => {
let totalCurvature = 0;
for (let center of curvatureCenters) {
const dist = Math.sqrt((x-center.x)**2 + (y-center.y)**2);
totalCurvature += center.strength / (dist + 1);
}
return totalCurvature;
};
教育的価値
このゲームは以下の概念を体験的に学べます:
- 微分幾何学:曲率と測地線
- 相対性理論:曲がった時空での物体の運動
- 数値計算:物理シミュレーションの実装
- ゲーム物理:衝突検出と応答
発展的な実装
WebGL版での高性能化
// シェーダーでの曲率計算例
const fragmentShader = `
precision mediump float;
uniform float curvature;
uniform vec2 center;
varying vec2 position;
vec2 applyCurvature(vec2 velocity, vec2 pos) {
float dist = length(pos - center);
float angle = curvature * dist * 0.01;
return mat2(cos(angle), -sin(angle), sin(angle), cos(angle)) * velocity;
}
`;
3D非ユークリッド空間
Three.js を使用して、真の3D非ユークリッド空間でのビリヤードも実装可能です:
// Three.js での曲面ビリヤード
const geometry = new THREE.ParametricGeometry((u, v, target) => {
const x = u * 10 - 5;
const y = v * 10 - 5;
const z = curvature * (x*x + y*y); // 放物面
target.set(x, y, z);
}, 50, 50);
まとめ
非ユークリッド幾何学という抽象的な数学概念を、実際に触って楽しめるゲームとして実装しました。このプロジェクトを通じて:
- 理論と実装の橋渡し
- 数学の美しさと面白さの体験
- ゲーム開発の新しい可能性の探求
ができました。
ぜひコードを改造して、自分だけの非ユークリッド物理世界を作ってみてください!
完全なソースコード
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Non-Euclidean Billiards</title>
<style>
body {
margin: 0;
padding: 20px;
background: #0a0a0a;
color: white;
font-family: 'Courier New', monospace;
display: flex;
flex-direction: column;
align-items: center;
}
h1 {
color: #00ff88;
text-align: center;
margin-bottom: 10px;
}
.info {
text-align: center;
margin-bottom: 20px;
color: #888;
font-size: 14px;
}
canvas {
border: 2px solid #333;
background: #111;
cursor: crosshair;
}
.controls {
margin-top: 15px;
display: flex;
gap: 15px;
align-items: center;
}
button {
padding: 8px 16px;
background: #333;
border: 1px solid #555;
color: white;
cursor: pointer;
font-family: inherit;
transition: background 0.2s;
}
button:hover {
background: #444;
}
.slider-group {
display: flex;
align-items: center;
gap: 8px;
}
input[type="range"] {
width: 100px;
}
.geometry-info {
margin-top: 10px;
font-size: 12px;
color: #666;
text-align: center;
}
</style>
</head>
<body>
<h1>Non-Euclidean Billiards</h1>
<div class="info">
Click and drag to aim and shoot • Balls curve through hyperbolic space
</div>
<canvas id="gameCanvas" width="600" height="400"></canvas>
<div class="controls">
<button onclick="resetGame()">Reset</button>
<div class="slider-group">
<label>Curvature:</label>
<input type="range" id="curvatureSlider" min="-0.02" max="0.02" step="0.001" value="0.005">
<span id="curvatureValue">0.005</span>
</div>
<div class="slider-group">
<label>Friction:</label>
<input type="range" id="frictionSlider" min="0.98" max="1.0" step="0.001" value="0.995">
<span id="frictionValue">0.995</span>
</div>
</div>
<div class="geometry-info">
Positive curvature = spherical geometry • Negative curvature = hyperbolic geometry
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let balls = [];
let mouseDown = false;
let mouseStart = {x: 0, y: 0};
let mouseEnd = {x: 0, y: 0};
let curvature = 0.005;
let friction = 0.995;
// Initialize balls
function initBalls() {
balls = [
{x: 150, y: 200, vx: 0, vy: 0, radius: 12, color: '#ff4444', isCue: true},
{x: 400, y: 180, vx: 0, vy: 0, radius: 10, color: '#4444ff'},
{x: 420, y: 200, vx: 0, vy: 0, radius: 10, color: '#44ff44'},
{x: 440, y: 220, vx: 0, vy: 0, radius: 10, color: '#ffff44'},
{x: 400, y: 220, vx: 0, vy: 0, radius: 10, color: '#ff44ff'},
{x: 420, y: 240, vx: 0, vy: 0, radius: 10, color: '#44ffff'}
];
}
initBalls();
// Non-Euclidean physics simulation
function updateBalls() {
for (let ball of balls) {
if (Math.abs(ball.vx) < 0.01 && Math.abs(ball.vy) < 0.01) {
ball.vx = 0;
ball.vy = 0;
continue;
}
// Apply non-Euclidean curvature
const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
if (speed > 0) {
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const distFromCenter = Math.sqrt((ball.x - centerX) * (ball.x - centerX) +
(ball.y - centerY) * (ball.y - centerY));
const curvatureEffect = curvature * distFromCenter * 0.01;
const angle = curvatureEffect;
const newVx = ball.vx * Math.cos(angle) - ball.vy * Math.sin(angle);
const newVy = ball.vx * Math.sin(angle) + ball.vy * Math.cos(angle);
ball.vx = newVx;
ball.vy = newVy;
}
// Update position
ball.x += ball.vx;
ball.y += ball.vy;
// Apply friction
ball.vx *= friction;
ball.vy *= friction;
// Boundary collisions with non-Euclidean effects
if (ball.x - ball.radius <= 0 || ball.x + ball.radius >= canvas.width) {
ball.vx = -ball.vx * 0.8;
ball.x = Math.max(ball.radius, Math.min(canvas.width - ball.radius, ball.x));
ball.vy += curvature * 50;
}
if (ball.y - ball.radius <= 0 || ball.y + ball.radius >= canvas.height) {
ball.vy = -ball.vy * 0.8;
ball.y = Math.max(ball.radius, Math.min(canvas.height - ball.radius, ball.y));
ball.vx += curvature * 50;
}
}
// Ball collisions
for (let i = 0; i < balls.length; i++) {
for (let j = i + 1; j < balls.length; j++) {
const ball1 = balls[i];
const ball2 = balls[j];
const dx = ball2.x - ball1.x;
const dy = ball2.y - ball1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < ball1.radius + ball2.radius) {
const angle = Math.atan2(dy, dx);
const sin = Math.sin(angle);
const cos = Math.cos(angle);
const vx1 = ball1.vx * cos + ball1.vy * sin;
const vy1 = ball1.vy * cos - ball1.vx * sin;
const vx2 = ball2.vx * cos + ball2.vy * sin;
const vy2 = ball2.vy * cos - ball2.vx * sin;
const finalVx1 = vx2;
const finalVx2 = vx1;
ball1.vx = finalVx1 * cos - vy1 * sin;
ball1.vy = vy1 * cos + finalVx1 * sin;
ball2.vx = finalVx2 * cos - vy2 * sin;
ball2.vy = vy2 * cos + finalVx2 * sin;
const overlap = ball1.radius + ball2.radius - distance;
const separateX = cos * overlap * 0.5;
const separateY = sin * overlap * 0.5;
ball1.x -= separateX;
ball1.y -= separateY;
ball2.x += separateX;
ball2.y += separateY;
const curvatureBonus = curvature * 200;
ball1.vx += curvatureBonus * cos;
ball1.vy += curvatureBonus * sin;
ball2.vx -= curvatureBonus * cos;
ball2.vy -= curvatureBonus * sin;
}
}
}
}
function draw() {
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw curvature visualization
ctx.strokeStyle = '#222';
ctx.lineWidth = 1;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
for (let r = 50; r < 300; r += 50) {
ctx.beginPath();
ctx.arc(centerX, centerY, r, 0, Math.PI * 2);
ctx.stroke();
}
// Draw balls
for (let ball of balls) {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
ctx.fillStyle = ball.color;
ctx.fill();
ctx.shadowBlur = 10;
ctx.shadowColor = ball.color;
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius * 0.7, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
if (ball.vx !== 0 || ball.vy !== 0) {
ctx.strokeStyle = ball.color;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(ball.x, ball.y);
ctx.lineTo(ball.x + ball.vx * 5, ball.y + ball.vy * 5);
ctx.stroke();
}
}
// Draw aim line
if (mouseDown) {
const cueBall = balls[0];
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(cueBall.x, cueBall.y);
ctx.lineTo(mouseEnd.x, mouseEnd.y);
ctx.stroke();
ctx.setLineDash([]);
const power = Math.sqrt((mouseEnd.x - cueBall.x) ** 2 + (mouseEnd.y - cueBall.y) ** 2);
ctx.fillStyle = `hsl(${Math.max(0, 120 - power)}, 100%, 50%)`;
ctx.fillRect(10, 10, Math.min(200, power), 10);
}
}
function gameLoop() {
updateBalls();
draw();
requestAnimationFrame(gameLoop);
}
// Mouse controls
canvas.addEventListener('mousedown', (e) => {
const rect = canvas.getBoundingClientRect();
mouseStart.x = e.clientX - rect.left;
mouseStart.y = e.clientY - rect.top;
mouseEnd.x = mouseStart.x;
mouseEnd.y = mouseStart.y;
mouseDown = true;
});
canvas.addEventListener('mousemove', (e) => {
if (mouseDown) {
const rect = canvas.getBoundingClientRect();
mouseEnd.x = e.clientX - rect.left;
mouseEnd.y = e.clientY - rect.top;
}
});
canvas.addEventListener('mouseup', (e) => {
if (mouseDown) {
const cueBall = balls[0];
const power = 0.3;
cueBall.vx = (mouseEnd.x - cueBall.x) * power * 0.1;
cueBall.vy = (mouseEnd.y - cueBall.y) * power * 0.1;
mouseDown = false;
}
});
// Controls
document.getElementById('curvatureSlider').addEventListener('input', (e) => {
curvature = parseFloat(e.target.value);
document.getElementById('curvatureValue').textContent = curvature.toFixed(3);
});
document.getElementById('frictionSlider').addEventListener('input', (e) => {
friction = parseFloat(e.target.value);
document.getElementById('frictionValue').textContent = friction.toFixed(3);
});
function resetGame() {
initBalls();
}
gameLoop();
</script>
</body>
</html>
参考文献
- リーマン幾何学入門
- 一般相対性理論における測地線運動
- ゲーム物理学プログラミング
- Canvas APIリファレンス