;
top of page

我手搓了一個會回應寫作狀態的光學宇宙

  • 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(壹叔瘋神)

留言


​請作者喝咖啡

Copyright © 2025 VON(壹叔瘋神). All rights reserved.  
本網站由  次元合同会社  授權運營。

本網站及其內容受適用的版權法律及國際公約保護。

bottom of page