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 { boxSize?: [number, number]; startDate?: Date; endDate?: Date; currentDate?: Date; onDateChange?: (date: Date) => void; onPlay?: () => void; onPause?: () => void; } export const Timeline: React.FC = ({ startDate, endDate, currentDate, onDateChange, onPlay, onPause, boxSize = [4, 8], ...props }) => { const canvasRef = useRef(null); const vaoRef = useRef(null); const vertex_bfRef = useRef(null); const uniform_bfRef = useRef(null); const instants_bfRef = useRef(null); const programRef = useRef(null); const instants_countRef = useRef(0); const { radius, d } = calcVersicaUni(boxSize); console.log(radius, d); const current_uniforms = useRef({ 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 (
); } 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, } }