タイトル: 「歪んだ光のプログラム」
東京の夜は、まるで無数の星がビルの窓に宿ったかのように輝いている。中でも、渋谷のスクランブル交差点は、その光の海で最も煌びやかな場所だ。だが、プログラマーの翔太には、その光すらまるで別の世界の出来事のように見えていた。彼の目の前にあるのは、ディスプレイに映し出された何千ものコードの行列。それらが彼のすべての意識を占領していた。
「また、うまくいかないか…」
翔太は椅子にもたれかかり、大きくため息をついた。彼は現在、宇宙の現象をシミュレートする新しいプログラムに取り組んでいた。テーマは「重力レンズ効果」。ブラックホールや巨大な惑星が、その強大な重力で光を曲げる現象だ。光すら逃れられない巨大な力、しかしそれを美しく表現することは、意外にも繊細な調整を必要としていた。
「これさえ完成すれば…」
クライアントは彼にこの技術を使った新しいアートアプリケーションのプロトタイプを求めていた。ユーザーが画像をアップロードし、マウスでブラックホールのような歪みを操作して視覚的に楽しむ――そんな構想だ。翔太はそのアイデアに興味を持ち、数週間にわたり、昼夜を問わずこのプロジェクトに没頭していた。
今日も、翔太はディスプレイの前で時間を忘れていた。彼が最新の試作コードを実行すると、キャンバス上に画像が浮かび上がり、彼の指が触れるごとに微妙に歪み始める。しかし、何かがまだ違う。計算された歪み方が不自然なのだ。
「効果が強すぎるのか…?」
彼はスライダーを使って、重力の強さを調整できるようにした新しいコードの一部に目を通す。プログラムはマウスの動きを感知し、その座標に基づいて、光を引き寄せるかのようにピクセルを歪ませていく。計算式はシンプルだが、結果は複雑だ。ピクセルの距離、光のゆがみ、その距離の逆数を使った重力の増減。全てが微妙なバランスで成り立っていた。
「もう少し、リアルな挙動を再現できれば…」
翔太は、宇宙の無限の力を感じながらコードをいじり続けた。歪みの強さを表すスライダーを触るたびに、彼の画面上の世界が変わる。まるで、彼自身がその重力源を操作しているかのような感覚が湧いてきた。
ふと、彼はふいに思い出した。大学時代、夜空を見上げながら宇宙の果てに思いを馳せたことを。あの頃は、ブラックホールや銀河の膨張をただ遠い理論だと思っていた。だが今、彼の手の中でその力を操っている。どんなに小さくても、彼のプログラムはこの無限の宇宙の縮図のようだった。
「これでいい」
翔太は静かにマウスを動かし、コードの動作を確認した。光が滑らかに歪んでいく。ブラックホールが作り出す時空の歪みが、彼のコードによって現実のものとなっていた。
そしてその時、彼は思った。宇宙は巨大で遠いものだけれど、プログラムによってその一端を感じ、触れることができる。彼のコードが世界を歪め、光の道筋を変える。その美しさに、彼は少しだけ満足感を覚えた。
外では、東京の光が変わらず瞬いていたが、翔太の世界は少し違って見えていた。
簡単に重力レンズ効果を試すことができます。
コードをメモ帳などのテキストエディタに貼り付け、ファイル名を「index.html」として保存します。その後、保存したファイルをブラウザで開けば、コードが実行されます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>重力レンズ効果シミュレーション</title>
<style>
/* キャンバスに枠線を追加 */
#canvas {
border: 1px solid black;
}
/* 画像アップロードボタンのスタイル調整 */
#upload {
margin-bottom: 10px;
}
/* ステータス表示用のスタイル。画像の読み込み状態を表示 */
#status {
font-size: 24px;
font-weight: bold;
position: absolute;
top: 10px;
left: 10px;
color: white;
background-color: rgba(0, 0, 0, 0.7);
padding: 5px;
border-radius: 5px;
}
/* スライダーのコンテナに余白を追加 */
#slider-container {
margin-top: 10px;
}
/* スライダーの値を表示する部分のスタイル */
#strength-value {
font-size: 16px;
margin-left: 10px;
}
</style>
</head>
<body>
<h2>重力レンズ効果シミュレーション</h2>
<!-- 画像をアップロードするためのボタン -->
<input type="file" id="upload" accept="image/*">
<!-- 描画用のキャンバス -->
<canvas id="canvas" width="512" height="512"></canvas>
<!-- 画像の状態を表示する領域 -->
<div id="status">状態: 画像なし</div>
<!-- 効果の強さを調整するスライダー -->
<div id="slider-container">
<label for="strength-slider">効果の強さ:</label>
<input type="range" id="strength-slider" min="50" max="300" value="100">
<span id="strength-value">100</span>
</div>
<script>
// キャンバスと2Dコンテキストを取得
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// ステータス表示用の要素
const statusDiv = document.getElementById('status');
// スライダーと強さ表示用の要素
const strengthSlider = document.getElementById('strength-slider');
const strengthValueDisplay = document.getElementById('strength-value');
// 画像データを保持する変数
let originalImageData = null;
let distortedImageData = null;
let strength = 100; // 初期の重力レンズ効果の強さ
// 画像がアップロードされた際の処理
document.getElementById('upload').addEventListener('change', (event) => {
const file = event.target.files[0]; // 選択されたファイルを取得
if (file) {
const reader = new FileReader(); // ファイルを読み込むためのFileReaderを作成
reader.onload = (e) => {
const img = new Image(); // イメージ要素を作成
img.onload = () => {
// 画像をキャンバスに描画
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, 512, 512);
// キャンバスから画像データを取得
originalImageData = ctx.getImageData(0, 0, 512, 512);
distortedImageData = ctx.createImageData(512, 512); // 歪み適用後のデータを保持する
// ステータスを更新
statusDiv.textContent = "状態: 画像読み込み完了";
};
img.src = e.target.result; // 読み込んだ画像をimg要素に設定
};
reader.readAsDataURL(file); // ファイルをデータURLとして読み込む
}
});
// スライダーの値が変わった際に効果の強さを更新
strengthSlider.addEventListener('input', (event) => {
strength = event.target.value; // 新しい強さを取得
strengthValueDisplay.textContent = strength; // 強さの値を表示
});
// キャンバス上でマウスが動いたときに歪みを適用
canvas.addEventListener('mousemove', (event) => {
if (originalImageData) {
const mouseX = event.offsetX; // マウスのX座標
const mouseY = event.offsetY; // マウスのY座標
applyGravityLensEffect(mouseX, mouseY); // 重力レンズ効果を適用
}
});
// 重力レンズ効果を適用する関数
function applyGravityLensEffect(mouseX, mouseY) {
const radius = 150; // 重力源の影響半径
// キャンバス全体のピクセルを走査
for (let y = 0; y < canvas.height; y++) {
for (let x = 0; x < canvas.width; x++) {
const dx = x - mouseX; // マウスからのX方向の距離
const dy = y - mouseY; // マウスからのY方向の距離
const distance = Math.sqrt(dx * dx + dy * dy); // マウスからの距離を計算
// 重力レンズの影響範囲内でのみ歪ませる
if (distance < radius) {
const distortionFactor = strength / (distance + 1); // 歪みの強さを計算
const offsetX = Math.floor(dx * distortionFactor); // X方向の歪み量
const offsetY = Math.floor(dy * distortionFactor); // Y方向の歪み量
// 元画像のピクセル位置を計算し、境界を超えないように調整
const srcX = Math.min(Math.max(0, x + offsetX), canvas.width - 1);
const srcY = Math.min(Math.max(0, y + offsetY), canvas.height - 1);
const srcIndex = (srcY * canvas.width + srcX) * 4; // 元画像のピクセル位置
const destIndex = (y * canvas.width + x) * 4; // 歪み後のピクセル位置
// 元の画像から歪んだピクセルを取得して適用
distortedImageData.data[destIndex] = originalImageData.data[srcIndex];
distortedImageData.data[destIndex + 1] = originalImageData.data[srcIndex + 1];
distortedImageData.data[destIndex + 2] = originalImageData.data[srcIndex + 2];
distortedImageData.data[destIndex + 3] = originalImageData.data[srcIndex + 3];
} else {
// 影響範囲外のピクセルはそのままコピー
const srcIndex = (y * canvas.width + x) * 4;
distortedImageData.data[srcIndex] = originalImageData.data[srcIndex];
distortedImageData.data[srcIndex + 1] = originalImageData.data[srcIndex + 1];
distortedImageData.data[srcIndex + 2] = originalImageData.data[srcIndex + 2];
distortedImageData.data[srcIndex + 3] = originalImageData.data[srcIndex + 3];
}
}
}
// 歪みを適用した画像データをキャンバスに描画
ctx.putImageData(distortedImageData, 0, 0);
}
</script>
</body>
</html>
追記。迫力の3次元重力レンズゲーム。
マウスで視点操作できます。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gravitational Lensing Effect with p5.js</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
</head>
<body>
<script>
let particles = [];
const gravitySource = { x: 0, y: 0, z: 0, mass: 3000 }; // 大きな重力源
const particleCount = 1000;
const emissionPoint = { x: 0, y: 0, z: -500 }; // パーティクルの放出点
let angleX = 0;
let angleY = 0;
// 初期設定
function setup() {
createCanvas(windowWidth, windowHeight, WEBGL);
for (let i = 0; i < particleCount; i++) {
particles.push(createParticle());
}
}
// パーティクルの生成(放出点からランダムな方向に飛ばす)
function createParticle() {
return {
position: createVector(emissionPoint.x, emissionPoint.y, emissionPoint.z),
velocity: createVector(random(-1, 1), random(-1, 1), random(0.5, 1)).mult(2), // ランダムな方向に加速
};
}
// 重力レンズ効果の計算
function applyGravitationalLensing(particle) {
let dir = p5.Vector.sub(createVector(gravitySource.x, gravitySource.y, gravitySource.z), particle.position);
let distance = dir.mag();
let force = gravitySource.mass / (distance * distance + 50); // 重力源からの力
dir.normalize();
particle.velocity.add(dir.mult(force * 0.05)); // 重力による加速
}
// アニメーション
function draw() {
background(0);
rotateX(angleX);
rotateY(angleY);
// 重力源の描画
fill(255, 0, 0);
noStroke();
sphere(20); // 重力源の中心
// パーティクルの更新と描画
for (let particle of particles) {
applyGravitationalLensing(particle);
particle.position.add(particle.velocity);
// パーティクルの描画
push();
translate(particle.position.x, particle.position.y, particle.position.z);
fill(255);
sphere(2);
pop();
}
// マウスによる視点変更
if (mouseIsPressed) {
angleX += (mouseY - pmouseY) * 0.01;
angleY += (mouseX - pmouseX) * 0.01;
}
}
// ウィンドウリサイズ対応
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
}
</script>
</body>
</html>