竹猫的Blog

他们说传奇不会飞,我愿意当那个飞物

WebGL 背景与内容的解耦

#astro #webgl #three.js

这是一篇用来验证代码块高亮的测试文章,内容本身也是这个项目正在做的事。

背景层为什么不能是组件

组件模型假设「挂载 / 卸载」,但 Astro 的多页导航本质是整页刷新, 每进入一个新页面,全景贴图会重新下载、WebGL context 会重新创建。

// 自包含的运行时:只依赖一个已经存在的 canvas
export async function startBackground(
    canvas: HTMLCanvasElement,
    basePath = '/panorama',
): Promise<BackgroundHandle> {
    const renderer = new WebGLRenderer({canvas, alpha: false});
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    // ...
}

唯一的通信通道

内容层不 import 背景代码,只通过 CSS 变量单向传递参数。

:root {
    --bg-yaw-speed: 0.018;
}

JavaScript 侧读取:

const raw = getComputedStyle(document.documentElement)
    .getPropertyValue('--bg-yaw-speed')
    .trim();

相机逻辑的全部

零交互之后,相机只是一个时间驱动的函数:

function frame(now) {
    const delta = Math.min((now - lastTime) / 1000, 0.1);
    lastTime = now;
    yaw += options.yawSpeed * delta;
    camera.rotation.set(0, yaw, 0, 'YXZ');
    renderer.render(scene, camera);
}

行内代码测试:setAnimationLoop(null) 才是真正停掉 RAF 的调用, renderer.dispose() 不会。

一段较长的代码,测试横向滚动

use std::collections::HashMap;

#[derive(Debug, Clone)]
pub struct PanoramaFrame {
    faces: [u32; 6],
    yaw: f32,
}

impl PanoramaFrame {
    pub fn new(order: &[usize; 6]) -> Self {
        let mut faces = [0u32; 6];
        for (slot, &src) in order.iter().enumerate() {
            faces[slot] = src as u32;
        }
        Self { faces, yaw: 0.0 }
    }

    pub fn advance(&mut self, delta: f32, speed: f32) {
        self.yaw = (self.yaw + speed * delta) % std::f32::consts::TAU;
    }
}