我手搓了一個會回應寫作狀態的光學宇宙
- 1天前
- 讀畢需時 6 分鐘
已更新:3小时前
我手搓了一個會回應寫作狀態的光學宇宙。
並將這個宇宙獻給每一位默默創作的人。
它是一個能被鍵盤觸發、改變的數字宇宙。
每一次按鍵,都會在空間外圍點亮一束星火,
星光穿越茫茫宇宙,消失在虛空結構的深處。
當你懈怠時,宇宙沉寂黯淡;
當你爆發時,宇宙隨之熠熠生輝。
你可以用外接小屏幕充當「宇宙監視器」
寫作時,它會靜靜陪著你,像一個會呼吸的世界。
有時候,孤獨的作者們也只是需要這樣一點小樂趣,不是嗎?
如果你有更好的創意和改動,也請分享給我。
效果展示
// ===== 鍵盤事件 =====
document.addEventListener("keydown", fireUserSignal);
// ===== 基本設定 =====
document.body.style.margin = "0";
document.body.style.overflow = "hidden";
const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x05030a, 40, 260);
const camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
500
);
camera.position.set(0, 0, 40);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(0x050308, 1);
document.body.appendChild(renderer.domElement);
// ===== 顏色設定 =====
const lineColor = new THREE.Color("#262b3a");
const autoSignalColor = new THREE.Color("#5a6b7a");
const userSignalMain = new THREE.Color("#e8ffff");
const userSignalExtra1 = new THREE.Color("#ff4fa8");
const userSignalExtra2 = new THREE.Color("#ffd45a");
// ===== 光源 =====
scene.add(new THREE.AmbientLight(0x404060, 1.4));
const pointLight = new THREE.PointLight(0x88ccff, 2.5, 320);
scene.add(pointLight);
// ===== 線條結構:Fractal Harmonic Field(Cosmic Flow)+ 分布修正 =====
const LINE_COUNT = 130;
const AUTO_SIGNAL_COUNT = 80;
const lines = [];
const autoSignals = [];
const userSignals = [];
const lineMat = new THREE.LineBasicMaterial({
color: lineColor,
transparent: true,
opacity: 0.85
});
// Fractal Harmonic 參數(避免堆疊、保持流動)
const FH_FREQ1 = 4.2;
const FH_FREQ2 = 7.9;
const FH_FREQ3 = 13.3;
const FH_AMP1 = 3.2;
const FH_AMP2 = 2.1;
const FH_AMP3 = 1.4;
for (let i = 0; i < LINE_COUNT; i++) {
const points = [];
// 角度基準:均勻分布 + 輕微非線性偏移,避免空區域
const angleBase =
(i / LINE_COUNT) * Math.PI * 2 + Math.sin(i * 0.7) * 0.2;
const baseRadius = 50;
const depth = 260;
for (let j = 0; j <= 320; j++) {
const t = j / 320;
// 半徑:前端穩定圓環,後段非線性收束
let radius;
if (t < 0.03) {
radius = baseRadius;
} else {
const radialMain = baseRadius * Math.pow(1.0 - t, 1.18);
const radialShell = 8 * (1.0 - t);
radius = radialMain + radialShell;
}
// 角度:Cosmic Flow 的方向性扭轉
let angle = angleBase;
if (t > 0.03) {
angle += t * 1.1;
angle += Math.sin(t * 3.5 + i * 0.27) * 0.18;
angle += Math.cos(t * 4.7 - i * 0.19) * 0.13;
}
let x = Math.cos(angle) * radius;
let y = Math.sin(angle) * radius;
// Fractal Harmonic:多頻率、多尺度流動
if (t > 0.03) {
const fh1 =
Math.sin(t * FH_FREQ1 + i * 0.31) * FH_AMP1 +
Math.cos(t * FH_FREQ1 * 0.7 - i * 0.23) * (FH_AMP1 * 0.6);
const fh2 =
Math.sin(t * FH_FREQ2 + i * 0.41) * FH_AMP2 +
Math.cos(t * FH_FREQ2 * 0.8 - i * 0.37) * (FH_AMP2 * 0.7);
const fh3 =
Math.sin(t * FH_FREQ3 + i * 0.53) * FH_AMP3 +
Math.cos(t * FH_FREQ3 * 0.9 - i * 0.29) * (FH_AMP3 * 0.8);
const flow = fh1 * 0.7 + fh2 * 0.6 + fh3 * 0.5;
x += flow * 0.9;
y += flow * 0.7;
}
// 深度:非線性 + 每條線輕微偏移,避免大量交叉堆疊
const z =
-Math.pow(t, 1.22) * depth +
40 +
Math.sin(i * 0.3) * 2.5;
points.push(new THREE.Vector3(x, y, z));
}
const geo = new THREE.BufferGeometry().setFromPoints(points);
const line = new THREE.Line(geo, lineMat);
scene.add(line);
lines.push({ line, points });
}
// ===== 背景信號(暗、慢) =====
const autoGeo = new THREE.SphereGeometry(0.18, 12, 12);
function createAutoSignal() {
const mat = new THREE.MeshBasicMaterial({
color: autoSignalColor,
transparent: true,
opacity: 0.4
});
const mesh = new THREE.Mesh(autoGeo, mat);
scene.add(mesh);
const lineIndex = Math.floor(Math.random() * lines.length);
const speed = 0.005 + Math.random() * 0.004;
return {
mesh,
lineIndex,
t: Math.random(),
speed
};
}
for (let i = 0; i < AUTO_SIGNAL_COUNT; i++) {
autoSignals.push(createAutoSignal());
}
// ===== 使用者信號(光束 + 尾跡) =====
const userGeo = new THREE.CylinderGeometry(0.05, 0.05, 1.8, 12);
const TRAIL_POINTS = 16;
function fireUserSignal() {
if (!lines.length) return;
let color = userSignalMain;
const r = Math.random();
if (r < 0.25) color = userSignalExtra1;
else if (r < 0.5) color = userSignalExtra2;
const mat = new THREE.MeshBasicMaterial({
color,
transparent: true,
opacity: 1,
blending: THREE.AdditiveBlending
});
const mesh = new THREE.Mesh(userGeo, mat);
mesh.rotation.x = Math.PI / 2;
scene.add(mesh);
const lineIndex = Math.floor(Math.random() * lines.length);
const lineData = lines[lineIndex];
if (!lineData || !lineData.points || lineData.points.length < 2) {
scene.remove(mesh);
return;
}
const startPoint = lineData.points[0];
mesh.position.copy(startPoint);
const baseSpeed = 0.0012 + Math.random() * 0.0015;
const trailPositions = new Float32Array(TRAIL_POINTS * 3);
for (let i = 0; i < TRAIL_POINTS; i++) {
trailPositions[i * 3] = startPoint.x;
trailPositions[i * 3 + 1] = startPoint.y;
trailPositions[i * 3 + 2] = startPoint.z;
}
const trailGeo = new THREE.BufferGeometry();
trailGeo.setAttribute(
"position",
new THREE.BufferAttribute(trailPositions, 3)
);
const trailMat = new THREE.PointsMaterial({
color,
size: 0.22,
transparent: true,
opacity: 0.55,
blending: THREE.AdditiveBlending,
depthWrite: false
});
const trailPoints = new THREE.Points(trailGeo, trailMat);
scene.add(trailPoints);
userSignals.push({
mesh,
lineIndex,
t: 0,
baseSpeed,
trailPositions,
trailGeo,
trailPoints
});
}
window.addEventListener("keydown", () => {
fireUserSignal();
});
// ===== 星空球殼分布 =====
const starGeo = new THREE.BufferGeometry();
const starCount = 2000;
const starPositions = new Float32Array(starCount * 3);
const starColors = new Float32Array(starCount * 3);
const colorOptions = [
new THREE.Color(0x99ccff),
new THREE.Color(0xffffff),
new THREE.Color(0xffddaa),
new THREE.Color(0xff99cc)
];
for (let i = 0; i < starCount; i++) {
// 半徑略縮小,集中在視野範圍
const r = 140 + Math.random() * 80;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
// 讓星星主要分布在 Z ≈ -90 附近
around Z ≈ -90
const zOffset = -90;
const x = r * Math.sin(phi) * Math.cos(theta);
const y = r * Math.sin(phi) * Math.sin(theta);
const z = r * Math.cos(phi) + zOffset;
starPositions[i * 3] = x;
starPositions[i * 3 + 1] = y;
starPositions[i * 3 + 2] = z;
const c = colorOptions[Math.floor(Math.random() * colorOptions.length)];
starColors[i * 3] = c.r;
starColors[i * 3 + 1] = c.g;
starColors[i * 3 + 2] = c.b;
}
starGeo.setAttribute("position", new THREE.BufferAttribute(starPositions, 3));
starGeo.setAttribute("color", new THREE.BufferAttribute(starColors, 3));
const starMat = new THREE.PointsMaterial({
size: 0.9,
transparent: true,
opacity: 1.0,
vertexColors: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
depthTest: false
});
const stars = new THREE.Points(starGeo, starMat);
scene.add(stars);
// ===== 視窗縮放 =====
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// ===== 動畫 =====
let t = 0;
function animate() {
requestAnimationFrame(animate);
t += 0.0018;
// 相機運動
camera.position.x = Math.sin(t * 1.4) * 6;
camera.position.y = Math.sin(t * 0.9) * 4;
camera.position.z = 40 + Math.sin(t * 0.6) * 4;
camera.lookAt(0, 0, -90);
// 星空:整體緩慢旋轉
stars.rotation.z += 0.00025;
// 背景信號
autoSignals.forEach((s) => {
s.t += s.speed;
if (s.t > 1) s.t = 0;
const lineData = lines[s.lineIndex];
const idx = Math.floor(s.t * (lineData.points.length - 1));
const p = lineData.points[idx];
s.mesh.position.copy(p);
s.mesh.material.opacity = 0.2 + Math.pow(1.0 - s.t, 2.0) * 0.3;
});
// 使用者信號
for (let i = userSignals.length - 1; i >= 0; i--) {
const s = userSignals[i];
// 固定速度:每個光束從生成到消失都保持自己的 baseSpeed
s.t += s.baseSpeed;
if (s.t > 1) {
scene.remove(s.mesh);
scene.remove(s.trailPoints);
userSignals.splice(i, 1);
continue;
}
const lineData = lines[s.lineIndex];
const idx = Math.floor(s.t * (lineData.points.length - 1));
const p = lineData.points[idx];
s.mesh.position.copy(p);
const pulse = 0.12 * Math.sin(performance.now() * 0.01 + i);
s.mesh.material.opacity =
1.0 + Math.pow(1.0 - s.t, 2.0) * 1.1 + pulse;
const tp = s.trailPositions;
for (let k = TRAIL_POINTS - 1; k > 0; k--) {
tp[k * 3] = tp[(k - 1) * 3];
tp[k * 3 + 1] = tp[(k - 1) * 3 + 1];
tp[k * 3 + 2] = tp[(k - 1) * 3 + 2];
}
tp[0] = p.x;
tp[1] = p.y;
tp[2] = p.z;
s.trailGeo.attributes.position.needsUpdate = true;
}
renderer.render(scene, camera);
}
animate();By VON(壹叔瘋神)



留言