update style
This commit is contained in:
parent
bc81aeec1d
commit
039197c5da
@ -5,7 +5,7 @@ import { cookies } from "next/headers";
|
|||||||
|
|
||||||
export const revalidate = 60; // ISR: 60秒后重新验证
|
export const revalidate = 60; // ISR: 60秒后重新验证
|
||||||
|
|
||||||
export default async function Page({ params }: { params: { slug: string } }) {
|
export default async function Page({ params }: any) {
|
||||||
|
|
||||||
const { slug } = await params
|
const { slug } = await params
|
||||||
const jwt = (await cookies()).get('jwt')?.value
|
const jwt = (await cookies()).get('jwt')?.value
|
||||||
|
|||||||
@ -1,48 +0,0 @@
|
|||||||
'use client';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import { motion, Transition } from 'framer-motion';
|
|
||||||
|
|
||||||
type BorderTrailProps = {
|
|
||||||
className?: string;
|
|
||||||
size?: number;
|
|
||||||
transition?: Transition;
|
|
||||||
delay?: number;
|
|
||||||
onAnimationComplete?: () => void;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function BorderTrail({
|
|
||||||
className,
|
|
||||||
size = 60,
|
|
||||||
transition,
|
|
||||||
delay,
|
|
||||||
onAnimationComplete,
|
|
||||||
style,
|
|
||||||
}: BorderTrailProps) {
|
|
||||||
const BASE_TRANSITION = {
|
|
||||||
repeat: Infinity,
|
|
||||||
duration: 5,
|
|
||||||
ease: 'linear',
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='pointer-events-none absolute inset-0 rounded-[inherit] border border-transparent [mask-clip:padding-box,border-box] [mask-composite:intersect] [mask-image:linear-gradient(transparent,transparent),linear-gradient(#000,#000)]'>
|
|
||||||
<motion.div
|
|
||||||
className={cn('absolute aspect-square bg-zinc-500', className)}
|
|
||||||
style={{
|
|
||||||
width: size,
|
|
||||||
offsetPath: `rect(0 auto auto 0 round ${size}px)`,
|
|
||||||
...style,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
offsetDistance: ['0%', '100%'],
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
...(transition ?? BASE_TRANSITION),
|
|
||||||
delay: delay,
|
|
||||||
}}
|
|
||||||
onAnimationComplete={onAnimationComplete}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
BIN
app/favicon.ico
BIN
app/favicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
27
app/icon.svg
Normal file
27
app/icon.svg
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||||
|
<!-- 背景圆盘 -->
|
||||||
|
<circle cx="32" cy="32" r="30" fill="#0a1f33" stroke="#00ffcc" stroke-width="2" />
|
||||||
|
|
||||||
|
<!-- 雷达同心圆 -->
|
||||||
|
<circle cx="32" cy="32" r="10" fill="none" stroke="#00ffcc" stroke-width="1" opacity="0.5"/>
|
||||||
|
<circle cx="32" cy="32" r="20" fill="none" stroke="#00ffcc" stroke-width="1" opacity="0.5"/>
|
||||||
|
|
||||||
|
<!-- 十字刻度线 -->
|
||||||
|
<line x1="32" y1="2" x2="32" y2="62" stroke="#00ffcc" stroke-width="1" opacity="0.3"/>
|
||||||
|
<line x1="2" y1="32" x2="62" y2="32" stroke="#00ffcc" stroke-width="1" opacity="0.3"/>
|
||||||
|
|
||||||
|
<!-- 雷达扫描扇形 -->
|
||||||
|
<path d="M32 32 L32 2 A30 30 0 0 1 56.57 14.43 Z"
|
||||||
|
fill="url(#scanGradient)" opacity="0.6" />
|
||||||
|
|
||||||
|
<!-- 扫描渐变定义 -->
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="scanGradient" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#00ffcc" stop-opacity="0.6"/>
|
||||||
|
<stop offset="100%" stop-color="#00ffcc" stop-opacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- 中心点 -->
|
||||||
|
<circle cx="32" cy="32" r="2" fill="#00ffcc"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -15,8 +15,8 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "LiDAR",
|
||||||
description: "Generated by create next app",
|
description: "LiDAR for Radar",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|||||||
10
app/page.tsx
10
app/page.tsx
@ -33,12 +33,12 @@ export default function Page() {
|
|||||||
<div className="flex flex-row h-full">
|
<div className="flex flex-row h-full">
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
<WSProvider>
|
<WSProvider>
|
||||||
<div className="flex-1 flex flex-col min-h-0">
|
<div className="flex-1 relative min-h-0">
|
||||||
<StatusBar />
|
<MapComponent />
|
||||||
<div className="flex-1 min-h-0">
|
<div className="absolute top-0 left-0 right-0 z-10">
|
||||||
<MapComponent />
|
<StatusBar />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0">
|
<div className="absolute bottom-0 left-0 right-0 z-10 bg-black/20 backdrop-blur-xl m-3 border border-white/10 rounded-xl shadow-2xl overflow-hidden">
|
||||||
<Timeline />
|
<Timeline />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,48 +0,0 @@
|
|||||||
'use client';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import { motion, Transition } from 'framer-motion';
|
|
||||||
|
|
||||||
type BorderTrailProps = {
|
|
||||||
className?: string;
|
|
||||||
size?: number;
|
|
||||||
transition?: Transition;
|
|
||||||
delay?: number;
|
|
||||||
onAnimationComplete?: () => void;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function BorderTrail({
|
|
||||||
className,
|
|
||||||
size = 60,
|
|
||||||
transition,
|
|
||||||
delay,
|
|
||||||
onAnimationComplete,
|
|
||||||
style,
|
|
||||||
}: BorderTrailProps) {
|
|
||||||
const BASE_TRANSITION = {
|
|
||||||
repeat: Infinity,
|
|
||||||
duration: 5,
|
|
||||||
ease: 'linear',
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='pointer-events-none absolute inset-0 rounded-[inherit] border border-transparent [mask-clip:padding-box,border-box] [mask-composite:intersect] [mask-image:linear-gradient(transparent,transparent),linear-gradient(#000,#000)]'>
|
|
||||||
<motion.div
|
|
||||||
className={cn('absolute aspect-square bg-zinc-500', className)}
|
|
||||||
style={{
|
|
||||||
width: size,
|
|
||||||
offsetPath: `rect(0 auto auto 0 round ${size}px)`,
|
|
||||||
...style,
|
|
||||||
}}
|
|
||||||
animate={{
|
|
||||||
offsetDistance: ['0%', '100%'],
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
...(transition ?? BASE_TRANSITION),
|
|
||||||
delay: delay,
|
|
||||||
}}
|
|
||||||
onAnimationComplete={onAnimationComplete}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { useWS } from "./ws-context"
|
import { useWS } from "./ws-context"
|
||||||
|
import { useMap } from "./map-context"
|
||||||
|
|
||||||
export default function StatusBar() {
|
export default function StatusBar() {
|
||||||
|
|
||||||
const { wsStatus } = useWS()
|
const { wsStatus } = useWS()
|
||||||
|
const { currentDatetime } = useMap()
|
||||||
|
|
||||||
// 根据WebSocket状态返回对应颜色的圆点
|
// 根据WebSocket状态返回对应颜色的圆点
|
||||||
const getStatusDot = () => {
|
const getStatusDot = () => {
|
||||||
@ -21,7 +23,15 @@ export default function StatusBar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-8 flex items-center justify-end px-4">
|
<div className="h-8 flex items-center justify-between px-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Label className="text-xs font-bold text-muted-foreground">
|
||||||
|
Data Time:
|
||||||
|
</Label>
|
||||||
|
<Label className="ml-2 text-xs font-bold text-muted-foreground">
|
||||||
|
{currentDatetime?.toLocaleString()}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{getStatusDot()}
|
{getStatusDot()}
|
||||||
<Label className="ml-2 text-xs font-bold text-muted-foreground">
|
<Label className="ml-2 text-xs font-bold text-muted-foreground">
|
||||||
|
|||||||
1546
app/timeline.tsx
1546
app/timeline.tsx
File diff suppressed because it is too large
Load Diff
43
app/tl.tsx
43
app/tl.tsx
@ -1,9 +1,13 @@
|
|||||||
import React, { useRef, useEffect, useState, useCallback } from "react";
|
import React, { useRef, useEffect, useState, useCallback } from "react";
|
||||||
import vsSource from './glsl/timeline/vert.glsl';
|
import { Calendar } from "@/components/ui/calendar"
|
||||||
import fsSource from './glsl/timeline/frag.glsl';
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover"
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ChevronLeft, ChevronRight, HomeIcon, LockIcon, Pause, Play, RefreshCwIcon, UnlockIcon } from "lucide-react";
|
import { CalendarIcon, ChevronLeft, ChevronRight, HomeIcon, LockIcon, Pause, Play, RefreshCwIcon, UnlockIcon } from "lucide-react";
|
||||||
import { formatInTimeZone } from "date-fns-tz";
|
import { formatInTimeZone } from "date-fns-tz";
|
||||||
import { parse } from "date-fns"
|
import { parse } from "date-fns"
|
||||||
|
|
||||||
@ -80,6 +84,8 @@ export const Timeline: React.FC<Props> = React.memo(({
|
|||||||
visibleRange: [0, 0] as [number, number],
|
visibleRange: [0, 0] as [number, number],
|
||||||
currentLevel: null as any
|
currentLevel: null as any
|
||||||
});
|
});
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [time, setDateTime] = useState(new Date())
|
||||||
|
|
||||||
const { data } = useWS()
|
const { data } = useWS()
|
||||||
|
|
||||||
@ -252,7 +258,7 @@ export const Timeline: React.FC<Props> = React.memo(({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(props.className, "w-full h-10 flex flex-row")}>
|
<div className={cn(props.className, "w-full h-10 flex flex-row")}>
|
||||||
<div className="h-full flex flex-row items-center px-3 gap-2 bg-black/60" style={{ boxShadow: '8px 0 24px rgba(0, 0, 0, 0.15), 4px 0 12px rgba(0, 0, 0, 0.1)' }}>
|
<div className="h-full flex flex-row items-center px-3 gap-2 bg-black/30" >
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="icon"
|
size="icon"
|
||||||
@ -264,7 +270,7 @@ export const Timeline: React.FC<Props> = React.memo(({
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant={isPlaying ? "default" : "secondary"}
|
||||||
size="icon"
|
size="icon"
|
||||||
className="size-5"
|
className="size-5"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -319,7 +325,7 @@ export const Timeline: React.FC<Props> = React.memo(({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant={lock ? "default" : "secondary"}
|
||||||
size="icon"
|
size="icon"
|
||||||
className="size-5"
|
className="size-5"
|
||||||
onClick={() => setLock(!lock)}
|
onClick={() => setLock(!lock)}
|
||||||
@ -340,6 +346,31 @@ export const Timeline: React.FC<Props> = React.memo(({
|
|||||||
<RefreshCwIcon size={10} />
|
<RefreshCwIcon size={10} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
className="size-5"
|
||||||
|
title="设置时间"
|
||||||
|
>
|
||||||
|
<CalendarIcon size={10} />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto overflow-hidden p-0" align="end">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
captionLayout="dropdown"
|
||||||
|
onSelect={(date) => {
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cn("relative", "w-full h-full bg-gray-800/20")}>
|
<div className={cn("relative", "w-full h-full bg-gray-800/20")}>
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export function MapComponent({
|
|||||||
const [currentColorMapType, setCurrentColorMapType] = useState<ColorMapType>(colorMapType)
|
const [currentColorMapType, setCurrentColorMapType] = useState<ColorMapType>(colorMapType)
|
||||||
|
|
||||||
// 拖动状态
|
// 拖动状态
|
||||||
const [colorbarPosition, setColorbarPosition] = useState({ x: 16, y: 16 }) // 从右边和下边的距离
|
const [colorbarPosition, setColorbarPosition] = useState({ x: 16, y: 36 }) // 从右边和下边的距离
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
const [isDragging, setIsDragging] = useState(false)
|
||||||
|
|
||||||
// 使用ref来避免频繁的状态更新
|
// 使用ref来避免频繁的状态更新
|
||||||
|
|||||||
139
lib/timeline.ts
139
lib/timeline.ts
@ -583,6 +583,9 @@ class RealTimeTimeline {
|
|||||||
private config: Required<TimelineConfig>;
|
private config: Required<TimelineConfig>;
|
||||||
private animationFrameId: number | null = null;
|
private animationFrameId: number | null = null;
|
||||||
private currentTimeUpdateInterval: number | null = null;
|
private currentTimeUpdateInterval: number | null = null;
|
||||||
|
private mousePosition: { x: number; y: number } | null = null;
|
||||||
|
private showMouseIndicator: boolean = false;
|
||||||
|
private pendingRender: boolean = false;
|
||||||
|
|
||||||
constructor(canvas: HTMLCanvasElement, config?: TimelineConfig) {
|
constructor(canvas: HTMLCanvasElement, config?: TimelineConfig) {
|
||||||
this.canvas = canvas;
|
this.canvas = canvas;
|
||||||
@ -690,17 +693,19 @@ class RealTimeTimeline {
|
|||||||
|
|
||||||
// 鼠标移动
|
// 鼠标移动
|
||||||
this.canvas.addEventListener('mousemove', (e) => {
|
this.canvas.addEventListener('mousemove', (e) => {
|
||||||
if (e.button === 0) {
|
const rect = this.canvas.getBoundingClientRect();
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
const x = e.clientX - rect.left;
|
||||||
const x = e.clientX - rect.left;
|
const y = e.clientY - rect.top;
|
||||||
const y = e.clientY - rect.top;
|
|
||||||
this.interaction.handleMouseMove(x, y, this.viewport);
|
|
||||||
|
|
||||||
if (this.interaction.getIsDragging()) {
|
// 更新鼠标位置并显示指示线
|
||||||
this.render();
|
this.mousePosition = { x, y };
|
||||||
}
|
this.showMouseIndicator = true;
|
||||||
}
|
|
||||||
|
|
||||||
|
// 处理拖拽逻辑
|
||||||
|
this.interaction.handleMouseMove(x, y, this.viewport);
|
||||||
|
|
||||||
|
// 使用节流渲染以优化性能
|
||||||
|
this.requestRender();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 鼠标释放
|
// 鼠标释放
|
||||||
@ -717,6 +722,8 @@ class RealTimeTimeline {
|
|||||||
this.canvas.addEventListener('mouseleave', () => {
|
this.canvas.addEventListener('mouseleave', () => {
|
||||||
this.interaction.endDrag();
|
this.interaction.endDrag();
|
||||||
this.canvas.style.cursor = 'crosshair';
|
this.canvas.style.cursor = 'crosshair';
|
||||||
|
this.showMouseIndicator = false;
|
||||||
|
this.requestRender(); // 重新渲染以隐藏鼠标指示线
|
||||||
});
|
});
|
||||||
|
|
||||||
this.canvas.addEventListener('mouseup', (e) => {
|
this.canvas.addEventListener('mouseup', (e) => {
|
||||||
@ -779,6 +786,17 @@ class RealTimeTimeline {
|
|||||||
}, 1000); // 每秒更新一次
|
}, 1000); // 每秒更新一次
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 请求渲染(使用 requestAnimationFrame 节流) */
|
||||||
|
private requestRender(): void {
|
||||||
|
if (!this.pendingRender) {
|
||||||
|
this.pendingRender = true;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.render();
|
||||||
|
this.pendingRender = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 渲染时间轴 */
|
/** 渲染时间轴 */
|
||||||
render(): void {
|
render(): void {
|
||||||
const width = this.viewport.getWidth();
|
const width = this.viewport.getWidth();
|
||||||
@ -810,6 +828,9 @@ class RealTimeTimeline {
|
|||||||
|
|
||||||
// 绘制时间标记
|
// 绘制时间标记
|
||||||
this.drawTimeMarks();
|
this.drawTimeMarks();
|
||||||
|
|
||||||
|
// 绘制鼠标位置指示线
|
||||||
|
this.drawMouseIndicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
private changeTime(date: Date): void {
|
private changeTime(date: Date): void {
|
||||||
@ -920,31 +941,59 @@ class RealTimeTimeline {
|
|||||||
|
|
||||||
if (x >= 0 && x <= this.viewport.getWidth()) {
|
if (x >= 0 && x <= this.viewport.getWidth()) {
|
||||||
const height = this.viewport.getHeight();
|
const height = this.viewport.getHeight();
|
||||||
|
const currentTimeColor = this.config.colors.currentTime ?? '#ff4444';
|
||||||
|
|
||||||
// 绘制时间线
|
this.ctx.save();
|
||||||
this.ctx.strokeStyle = this.config.colors.currentTime ?? '#ff4444';
|
|
||||||
this.ctx.lineWidth = 1;
|
// 绘制毛玻璃背景条带
|
||||||
this.ctx.setLineDash([5, 5]);
|
const backgroundWidth = 14;
|
||||||
|
const gradient = this.ctx.createLinearGradient(x - backgroundWidth / 2, 0, x + backgroundWidth / 2, 0);
|
||||||
|
// 使用当前时间颜色的红色调
|
||||||
|
gradient.addColorStop(0, `rgba(255, 68, 68, 0.02)`);
|
||||||
|
gradient.addColorStop(0.5, `rgba(255, 68, 68, 0.18)`);
|
||||||
|
gradient.addColorStop(1, `rgba(255, 68, 68, 0.02)`);
|
||||||
|
|
||||||
|
this.ctx.fillStyle = gradient;
|
||||||
|
this.ctx.fillRect(x - backgroundWidth / 2, 0, backgroundWidth, height);
|
||||||
|
|
||||||
|
// 绘制外层辉光效果
|
||||||
|
this.ctx.shadowColor = `rgba(255, 68, 68, 0.7)`;
|
||||||
|
this.ctx.shadowBlur = 10;
|
||||||
|
this.ctx.shadowOffsetX = 0;
|
||||||
|
this.ctx.shadowOffsetY = 0;
|
||||||
|
|
||||||
|
// 绘制主指示线(实线)
|
||||||
|
this.ctx.strokeStyle = `rgba(255, 68, 68, 0.95)`;
|
||||||
|
this.ctx.lineWidth = 2.5;
|
||||||
this.ctx.beginPath();
|
this.ctx.beginPath();
|
||||||
this.ctx.moveTo(x, 0);
|
this.ctx.moveTo(x, 0);
|
||||||
this.ctx.lineTo(x, height);
|
this.ctx.lineTo(x, height);
|
||||||
this.ctx.stroke();
|
this.ctx.stroke();
|
||||||
this.ctx.setLineDash([]);
|
|
||||||
|
|
||||||
// 绘制时间标签背景
|
// 重置阴影,绘制内层亮线增强辉光效果
|
||||||
|
this.ctx.shadowBlur = 0;
|
||||||
|
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
|
||||||
|
this.ctx.lineWidth = 1;
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(x, 0);
|
||||||
|
this.ctx.lineTo(x, height);
|
||||||
|
this.ctx.stroke();
|
||||||
|
|
||||||
|
this.ctx.restore();
|
||||||
|
|
||||||
|
// 绘制时间标签(可选,目前被注释掉)
|
||||||
const timeStr = TimeUtils.format(new Date(now), 'HH:mm:ss');
|
const timeStr = TimeUtils.format(new Date(now), 'HH:mm:ss');
|
||||||
this.ctx.font = `bold ${this.config.sizes.primaryFontSize ?? 13}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`;
|
this.ctx.font = `bold ${this.config.sizes.primaryFontSize ?? 13}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`;
|
||||||
const metrics = this.ctx.measureText(timeStr);
|
const metrics = this.ctx.measureText(timeStr);
|
||||||
const labelWidth = metrics.width + 10;
|
const labelWidth = metrics.width + 10;
|
||||||
const labelHeight = 20;
|
const labelHeight = 20;
|
||||||
|
|
||||||
this.ctx.fillStyle = this.config.colors.currentTime ?? '#ff4444';
|
// this.ctx.fillStyle = currentTimeColor;
|
||||||
// this.ctx.fillRect(x - labelWidth / 2, 0, labelWidth, labelHeight);
|
// this.ctx.fillRect(x - labelWidth / 2, 0, labelWidth, labelHeight);
|
||||||
|
|
||||||
// 绘制时间文本
|
// this.ctx.fillStyle = '#ffffff';
|
||||||
this.ctx.fillStyle = '#ffffff';
|
// this.ctx.textAlign = 'center';
|
||||||
this.ctx.textAlign = 'center';
|
// this.ctx.textBaseline = 'middle';
|
||||||
this.ctx.textBaseline = 'middle';
|
|
||||||
// this.ctx.fillText(timeStr, x, labelHeight / 2);
|
// this.ctx.fillText(timeStr, x, labelHeight / 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -995,6 +1044,56 @@ class RealTimeTimeline {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 绘制鼠标位置指示线 */
|
||||||
|
private drawMouseIndicator(): void {
|
||||||
|
if (!this.showMouseIndicator || !this.mousePosition) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const height = this.viewport.getHeight();
|
||||||
|
const x = this.mousePosition.x;
|
||||||
|
|
||||||
|
// 确保指示线在画布范围内
|
||||||
|
if (x >= 0 && x <= this.viewport.getWidth()) {
|
||||||
|
this.ctx.save();
|
||||||
|
|
||||||
|
// 绘制毛玻璃背景条带
|
||||||
|
const backgroundWidth = 12;
|
||||||
|
const gradient = this.ctx.createLinearGradient(x - backgroundWidth / 2, 0, x + backgroundWidth / 2, 0);
|
||||||
|
gradient.addColorStop(0, 'rgba(135, 206, 235, 0.02)');
|
||||||
|
gradient.addColorStop(0.5, 'rgba(135, 206, 235, 0.15)');
|
||||||
|
gradient.addColorStop(1, 'rgba(135, 206, 235, 0.02)');
|
||||||
|
|
||||||
|
this.ctx.fillStyle = gradient;
|
||||||
|
this.ctx.fillRect(x - backgroundWidth / 2, 0, backgroundWidth, height);
|
||||||
|
|
||||||
|
// 绘制外层辉光效果
|
||||||
|
this.ctx.shadowColor = 'rgba(135, 206, 235, 0.6)';
|
||||||
|
this.ctx.shadowBlur = 8;
|
||||||
|
this.ctx.shadowOffsetX = 0;
|
||||||
|
this.ctx.shadowOffsetY = 0;
|
||||||
|
|
||||||
|
// 绘制主指示线(实线)
|
||||||
|
this.ctx.strokeStyle = 'rgba(135, 206, 235, 0.9)';
|
||||||
|
this.ctx.lineWidth = 2;
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(x, 0);
|
||||||
|
this.ctx.lineTo(x, height);
|
||||||
|
this.ctx.stroke();
|
||||||
|
|
||||||
|
// 重置阴影,绘制内层亮线增强辉光效果
|
||||||
|
this.ctx.shadowBlur = 0;
|
||||||
|
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
|
||||||
|
this.ctx.lineWidth = 1;
|
||||||
|
this.ctx.beginPath();
|
||||||
|
this.ctx.moveTo(x, 0);
|
||||||
|
this.ctx.lineTo(x, height);
|
||||||
|
this.ctx.stroke();
|
||||||
|
|
||||||
|
this.ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** 获取当前视口信息 */
|
/** 获取当前视口信息 */
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user