time
This commit is contained in:
parent
fcbc406989
commit
83683995eb
43
app/glsl/timeline/frag.glsl
Normal file
43
app/glsl/timeline/frag.glsl
Normal file
@ -0,0 +1,43 @@
|
||||
#version 300 es
|
||||
|
||||
precision mediump float;
|
||||
|
||||
layout(std140) uniform Uniforms {
|
||||
float startDate;
|
||||
float endDate;
|
||||
float currentDate;
|
||||
float radius;
|
||||
float d;
|
||||
float padding1; // 填充以对齐到8字节边界
|
||||
vec2 viewportSize;
|
||||
};
|
||||
|
||||
struct Instant {
|
||||
vec2 position;
|
||||
vec4 color;
|
||||
};
|
||||
|
||||
in Instant i_instant;
|
||||
|
||||
out vec4 FragColor;
|
||||
|
||||
float sdVesica(vec2 p, float r, float d)
|
||||
{
|
||||
p = abs(p);
|
||||
float b = sqrt(r*r-d*d); // can delay this sqrt by rewriting the comparison
|
||||
return ((p.y-b)*d > p.x*b) ? length(p-vec2(0.0,b))*sign(d)
|
||||
: length(p-vec2(-d,0.0))-r;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 p = gl_FragCoord.xy - i_instant.position;
|
||||
float sdf = sdVesica(p, radius, d);
|
||||
|
||||
// 简化逻辑:内部完全不透明,外部丢弃
|
||||
if (sdf > 0.0) {
|
||||
discard;
|
||||
}
|
||||
|
||||
// 测试:固定颜色确保能看到
|
||||
FragColor = vec4(1.0, 1.0, 1.0, 1.0); // 纯白色
|
||||
}
|
||||
19
app/glsl/timeline/vert.glsl
Normal file
19
app/glsl/timeline/vert.glsl
Normal file
@ -0,0 +1,19 @@
|
||||
#version 300 es
|
||||
|
||||
layout(location = 0) in vec2 a_position;
|
||||
layout(location = 1) in vec2 i_position;
|
||||
layout(location = 2) in vec4 i_color;
|
||||
|
||||
struct Instant {
|
||||
vec2 position;
|
||||
vec4 color;
|
||||
};
|
||||
|
||||
out Instant i_instant;
|
||||
|
||||
void main() {
|
||||
i_instant.position = i_position;
|
||||
i_instant.color = i_color;
|
||||
|
||||
gl_Position = vec4(a_position, 0.0, 1.0);
|
||||
}
|
||||
15
app/page.tsx
15
app/page.tsx
@ -19,7 +19,7 @@ import { useEffect, useRef } from 'react';
|
||||
import { MapProvider } from './map-context';
|
||||
import { MapComponent } from '@/components/map-component';
|
||||
import { ThemeToggle } from '@/components/theme-toggle';
|
||||
import { Timeline } from '@/components/timeline';
|
||||
import { Timeline } from '@/app/timeline';
|
||||
|
||||
import { Dock } from "@/app/dock"
|
||||
|
||||
@ -85,20 +85,9 @@ export default function Page() {
|
||||
</div>
|
||||
</header>
|
||||
<div className="relative h-full w-full">
|
||||
<Timeline className="absolute bottom-0 left-1/2 -translate-x-1/2 bg-red-500 z-10 w-full h-10 rounded-t-lg" />
|
||||
<MapComponent />
|
||||
<Dock items={items} className="absolute top-1/2 right-4 -translate-y-1/2" />
|
||||
{/* <Timeline
|
||||
className="absolute bottom-4 left-1/2 -translate-x-1/2"
|
||||
startDate={new Date('2024-01-01')}
|
||||
endDate={new Date('2024-12-31')}
|
||||
currentDate={new Date()}
|
||||
onDateChange={(date) => console.log('Date changed:', date)}
|
||||
onPlay={() => console.log('Play')}
|
||||
onPause={() => console.log('Pause')}
|
||||
isPlaying={false}
|
||||
speed="normal"
|
||||
onSpeedChange={(speed) => console.log('Speed changed:', speed)}
|
||||
/> */}
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
|
||||
379
app/timeline.tsx
Normal file
379
app/timeline.tsx
Normal file
@ -0,0 +1,379 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,15 @@ import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
|
||||
webpack(config) {
|
||||
|
||||
config.module.rules.push({
|
||||
test: /\.glsl$/,
|
||||
use: 'raw-loader',
|
||||
});
|
||||
return config;
|
||||
}
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
1838
package-lock.json
generated
1838
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
@ -40,6 +40,7 @@
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"raw-loader": "^4.0.2",
|
||||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
"typescript": "^5"
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@ -19,9 +23,23 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./types",
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"global.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
4
types/global.d.ts
vendored
Normal file
4
types/global.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
declare module '*.glsl' {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user