379 lines
13 KiB
TypeScript
379 lines
13 KiB
TypeScript
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,
|
||
}
|
||
} |