mosaicmap/app/timeline.tsx
2025-07-18 21:32:46 +08:00

379 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useRef, useEffect, useState } from "react";
import vsSource from './glsl/timeline/vert.glsl';
import fsSource from './glsl/timeline/frag.glsl';
interface Uniforms {
startDate: number;
endDate: number;
currentDate: number;
radius: number;
d: number;
viewportSize: [number, number];
}
interface Instants {
position: Float32Array;
color: Float32Array;
}
interface Props extends React.HTMLAttributes<HTMLDivElement> {
boxSize?: [number, number];
startDate?: Date;
endDate?: Date;
currentDate?: Date;
onDateChange?: (date: Date) => void;
onPlay?: () => void;
onPause?: () => void;
}
export const Timeline: React.FC<Props> = ({ startDate, endDate, currentDate, onDateChange, onPlay, onPause, boxSize = [4, 8], ...props }) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const vaoRef = useRef<WebGLVertexArrayObject | null>(null);
const vertex_bfRef = useRef<WebGLBuffer | null>(null);
const uniform_bfRef = useRef<WebGLBuffer | null>(null);
const instants_bfRef = useRef<WebGLBuffer | null>(null);
const programRef = useRef<WebGLProgram | null>(null);
const instants_countRef = useRef<number>(0);
const { radius, d } = calcVersicaUni(boxSize);
console.log(radius, d);
const current_uniforms = useRef<Uniforms>({
startDate: 0,
endDate: 0,
currentDate: 0,
radius: radius,
d: d,
viewportSize: [0, 0],
})
useEffect(() => {
if (!canvasRef.current) return;
const { radius, d } = calcVersicaUni(boxSize);
const width = canvasRef.current.clientWidth;
const height = canvasRef.current.clientHeight;
// 设置canvas的实际像素尺寸
canvasRef.current.width = width;
canvasRef.current.height = height;
console.log(`Canvas初始化: ${width}x${height}, vesica: r=${radius.toFixed(2)}, d=${d.toFixed(2)}`);
current_uniforms.current.viewportSize = [width, height];
current_uniforms.current.radius = radius;
current_uniforms.current.d = d;
const gl = (canvasRef.current.getContext('webgl2') as WebGL2RenderingContext);
if (!gl) {
console.error('WebGL2 not supported');
return;
}
const program = createProgram(gl);
if (!program) {
console.error('Failed to create program');
return;
}
// 绑定uniform buffer到着色器程序
const uniformBlockIndex = gl.getUniformBlockIndex(program, 'Uniforms');
if (uniformBlockIndex !== gl.INVALID_INDEX) {
gl.uniformBlockBinding(program, uniformBlockIndex, 0);
}
const vao = gl.createVertexArray();
if (!vao) {
console.error('Failed to create vertex array');
return;
}
gl.bindVertexArray(vao);
const vertex_bf = defaultVb(gl);
const instants_bf = defaultIb(gl);
const uniform_bf = defaultUb(gl, current_uniforms.current);
gl.bindVertexArray(null);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindBuffer(gl.UNIFORM_BUFFER, null);
vaoRef.current = vao;
vertex_bfRef.current = vertex_bf;
uniform_bfRef.current = uniform_bf;
instants_bfRef.current = instants_bf;
programRef.current = program;
instants_countRef.current = 15; // 设置实例数量(时间轴刻度)
function render() {
gl.clearColor(0.1, 0.1, 0.1, 1); // 深灰背景,便于看到刻度
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.bindVertexArray(vaoRef.current);
// 绑定uniform buffer
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, uniform_bfRef.current);
console.log(`绘制实例数量: ${instants_countRef.current}`);
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, instants_countRef.current);
gl.bindVertexArray(null);
}
function updateUniforms(gl: WebGL2RenderingContext, uniforms: Uniforms) {
gl.bindBuffer(gl.UNIFORM_BUFFER, uniform_bfRef.current!);
const uniformData = new Float32Array([
uniforms.startDate,
uniforms.endDate,
uniforms.currentDate,
uniforms.radius,
uniforms.d,
0.0, // padding1 - 填充以对齐vec2到8字节边界
uniforms.viewportSize[0],
uniforms.viewportSize[1]
]);
gl.bufferData(gl.UNIFORM_BUFFER, uniformData, gl.DYNAMIC_DRAW);
}
function updateInstants(instants: Instants[]) {
gl.bindBuffer(gl.ARRAY_BUFFER, instants_bfRef.current!);
// 使用明确的数组顺序确保内存布局正确
const instantsData = new Float32Array(instants.length * 6); // 每个实例6个float (2个position + 4个color)
for (let i = 0; i < instants.length; i++) {
const offset = i * 6;
const instant = instants[i];
// position (2个float)
instantsData[offset + 0] = instant.position[0];
instantsData[offset + 1] = instant.position[1];
// color (4个float)
instantsData[offset + 2] = instant.color[0];
instantsData[offset + 3] = instant.color[1];
instantsData[offset + 4] = instant.color[2];
instantsData[offset + 5] = instant.color[3];
}
gl.bufferData(gl.ARRAY_BUFFER, instantsData, gl.DYNAMIC_DRAW);
instants_countRef.current = instants.length;
render(); // 更新后重新渲染
}
// TODO: 可以通过props传入自定义的时间轴刻度数据
// 或使用useImperativeHandle暴露更新方法
gl.viewport(0, 0, width, height);
render();
const handleMove = (e: MouseEvent) => {
const rect = canvasRef.current?.getBoundingClientRect();
if (!rect) return;
const px = e.clientX - rect.left;
const py = e.clientY - rect.top;
console.log(px, py);
}
canvasRef.current?.addEventListener('mousemove', handleMove);
// 使用ResizeObserver监听canvas尺寸变化
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width: new_width, height: new_height } = entry.contentRect;
// 更新canvas的实际像素尺寸
canvasRef.current!.width = new_width;
canvasRef.current!.height = new_height;
current_uniforms.current.viewportSize = [new_width, new_height];
updateUniforms(gl, current_uniforms.current);
gl.viewport(0, 0, new_width, new_height);
render();
}
});
resizeObserver.observe(canvasRef.current!);
return () => {
// 移除事件监听器
canvasRef.current?.removeEventListener('mousemove', handleMove);
resizeObserver.disconnect();
// 清理WebGL资源
if (vaoRef.current) gl.deleteVertexArray(vaoRef.current);
if (vertex_bfRef.current) gl.deleteBuffer(vertex_bfRef.current);
if (uniform_bfRef.current) gl.deleteBuffer(uniform_bfRef.current);
if (instants_bfRef.current) gl.deleteBuffer(instants_bfRef.current);
if (programRef.current) gl.deleteProgram(programRef.current);
}
}, [boxSize])
return (
<div {...props}>
<canvas ref={canvasRef} className="w-full h-full" />
</div>
);
}
function createProgram(gl: WebGL2RenderingContext) {
const [vs, fs] = createShader(gl)!;
const prog = gl.createProgram();
gl.attachShader(prog, vs);
gl.attachShader(prog, fs);
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
console.error('Failed to link program');
console.error(gl.getProgramInfoLog(prog));
// 清理着色器对象
gl.deleteShader(vs);
gl.deleteShader(fs);
return null;
}
// 程序链接成功后,删除着色器对象以释放内存
gl.deleteShader(vs);
gl.deleteShader(fs);
return prog;
}
function createShader(gl: WebGL2RenderingContext) {
const vs = gl.createShader(gl.VERTEX_SHADER);
if (!vs) {
console.error('Failed to create vertex shader');
return null;
}
gl.shaderSource(vs, vsSource);
gl.compileShader(vs);
if (!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) {
console.error('Failed to compile vertex shader:', gl.getShaderInfoLog(vs));
gl.deleteShader(vs);
return null;
}
const fs = gl.createShader(gl.FRAGMENT_SHADER);
if (!fs) {
console.error('Failed to create fragment shader');
gl.deleteShader(vs);
return null;
}
gl.shaderSource(fs, fsSource);
gl.compileShader(fs);
if (!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) {
console.error('Failed to compile fragment shader:', gl.getShaderInfoLog(fs));
gl.deleteShader(vs);
gl.deleteShader(fs);
return null;
}
return [vs, fs] as [WebGLShader, WebGLShader];
}
function defaultVb(gl: WebGL2RenderingContext) {
const plane = new Float32Array([
-1, -1,
-1, 1,
1, -1,
1, 1,
]);
const vertex_bf = gl.createBuffer()!;
gl.bindBuffer(gl.ARRAY_BUFFER, vertex_bf);
gl.bufferData(gl.ARRAY_BUFFER, plane, gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.vertexAttribDivisor(0, 0)
return vertex_bf;
}
function defaultUb(gl: WebGL2RenderingContext, uniforms: Uniforms) {
const ub = gl.createBuffer()!;
gl.bindBuffer(gl.UNIFORM_BUFFER, ub);
// std140布局需要添加填充以正确对齐vec2
const uniformData = new Float32Array([
uniforms.startDate,
uniforms.endDate,
uniforms.currentDate,
uniforms.radius,
uniforms.d,
0.0, // padding1 - 填充以对齐vec2到8字节边界
uniforms.viewportSize[0],
uniforms.viewportSize[1]
]);
gl.bufferData(gl.UNIFORM_BUFFER, uniformData, gl.DYNAMIC_DRAW);
return ub;
}
// 时间轴刻度生成函数
function generateTimelineMarks(startX: number, y: number, interval: number, count: number): Instants[] {
const marks = [];
for (let i = 0; i < count; i++) {
marks.push({
position: new Float32Array([startX + i * interval, y]),
color: new Float32Array([0.8, 0.8, 0.8, 1]) // 统一的灰色刻度
});
}
return marks;
}
function defaultIb(gl: WebGL2RenderingContext, canvasWidth: number, canvasHeight: number) {
// 时间轴刻度根据canvas尺寸调整位置
const startX = 20; // 左边距
const centerY = Math.floor(canvasHeight * 0.5); // 垂直居中
const interval = 12; // 刻度间距比vesica宽度大
const maxCount = Math.floor((canvasWidth - startX - 20) / interval); // 根据宽度计算数量
const count = Math.min(maxCount, 20); // 最多20个刻度
console.log(`生成刻度: 起始=${startX}, Y=${centerY}, 间距=${interval}, 数量=${count}`);
const instants: Instants[] = generateTimelineMarks(startX, centerY, interval, count);
const instants_bf = gl.createBuffer()!;
gl.bindBuffer(gl.ARRAY_BUFFER, instants_bf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(instants.flatMap(i => [...i.position, ...i.color])), gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(1);
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 24, 0); // 6 floats * 4 bytes = 24 bytes stride
gl.vertexAttribDivisor(1, 1);
gl.enableVertexAttribArray(2);
gl.vertexAttribPointer(2, 4, gl.FLOAT, false, 24, 8); // 2 floats * 4 bytes = 8 bytes offset
gl.vertexAttribDivisor(2, 1);
return instants_bf;
}
function calcVersicaUni(box_size: [number, number]) {
const [w, h] = [box_size[0], box_size[1]];
// 正确的vesica参数
// radius: 每个圆的半径,应该稍大于高度的一半以包含整个形状
// d: 两个圆心之间的距离,必须 < 2*radius
const radius = Math.max(w * 0.6, h * 0.6); // 确保能包含形状
const d = Math.min(w * 0.4, radius * 1.5); // 确保 d < 2*radius
console.log(`vesica参数: w=${w}, h=${h}, radius=${radius.toFixed(2)}, d=${d.toFixed(2)}`);
console.log(`验证: d < 2*radius? ${d} < ${2 * radius} = ${d < 2 * radius}`);
console.log(`验证: r²-d² = ${radius * radius} - ${d * d} = ${radius * radius - d * d}`);
return {
radius,
d,
}
}