add toolbar
This commit is contained in:
parent
17743458e4
commit
c6a59c48a5
@ -3,6 +3,7 @@ precision highp float;
|
|||||||
|
|
||||||
uniform sampler2D u_tex;
|
uniform sampler2D u_tex;
|
||||||
uniform sampler2D u_lut;
|
uniform sampler2D u_lut;
|
||||||
|
uniform float u_opacity;
|
||||||
|
|
||||||
in vec2 lonlat;
|
in vec2 lonlat;
|
||||||
out vec4 fragColor;
|
out vec4 fragColor;
|
||||||
@ -42,5 +43,5 @@ void main() {
|
|||||||
if (alpha <= 0.001) discard;
|
if (alpha <= 0.001) discard;
|
||||||
|
|
||||||
vec4 lutColor = texture(u_lut, vec2(value, 0.5));
|
vec4 lutColor = texture(u_lut, vec2(value, 0.5));
|
||||||
fragColor = vec4(lutColor.rgb, alpha);
|
fragColor = vec4(lutColor.rgb, alpha * u_opacity);
|
||||||
}
|
}
|
||||||
@ -20,6 +20,8 @@ interface MapContextType {
|
|||||||
setTime: (date: Date) => void
|
setTime: (date: Date) => void
|
||||||
setTimelineTime: (date: Date) => void
|
setTimelineTime: (date: Date) => void
|
||||||
isMapReady: boolean
|
isMapReady: boolean
|
||||||
|
setOpacity: (opacity: number) => void
|
||||||
|
opacity: React.RefObject<number>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -43,6 +45,7 @@ export function MapProvider({ children }: MapProviderProps) {
|
|||||||
const [mapState, setMapState] = useState<MapState>({
|
const [mapState, setMapState] = useState<MapState>({
|
||||||
zoomLevel: 11
|
zoomLevel: 11
|
||||||
});
|
});
|
||||||
|
const mapOpacityRef = useRef(100)
|
||||||
|
|
||||||
const layersRef = useRef<any[]>([])
|
const layersRef = useRef<any[]>([])
|
||||||
const [currentDatetime, setCurrentDatetime] = useState<Date | null>(null)
|
const [currentDatetime, setCurrentDatetime] = useState<Date | null>(null)
|
||||||
@ -133,6 +136,13 @@ export function MapProvider({ children }: MapProviderProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setOpacity = (opacity: number) => {
|
||||||
|
if (mapRef.current) {
|
||||||
|
mapOpacityRef.current = opacity
|
||||||
|
mapRef.current.triggerRepaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const value: MapContextType = {
|
const value: MapContextType = {
|
||||||
timelineDatetime,
|
timelineDatetime,
|
||||||
setTimelineTime: setTimelineDatetime,
|
setTimelineTime: setTimelineDatetime,
|
||||||
@ -148,7 +158,9 @@ export function MapProvider({ children }: MapProviderProps) {
|
|||||||
zoomTo,
|
zoomTo,
|
||||||
reset,
|
reset,
|
||||||
clearMap,
|
clearMap,
|
||||||
isMapReady
|
isMapReady,
|
||||||
|
setOpacity,
|
||||||
|
opacity: mapOpacityRef
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { Timeline } from '@/app/tl';
|
|||||||
import { WSProvider } from './ws-context'
|
import { WSProvider } from './ws-context'
|
||||||
import StatusBar from './status-bar'
|
import StatusBar from './status-bar'
|
||||||
import { getSiteConfigs } from '@/lib/fetchers';
|
import { getSiteConfigs } from '@/lib/fetchers';
|
||||||
|
import Toolbar from './toolbar';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
params: Promise<{ id: string }>
|
params: Promise<{ id: string }>
|
||||||
@ -38,10 +39,13 @@ export default function Page() {
|
|||||||
<div className="absolute top-0 left-0 right-0 z-10">
|
<div className="absolute top-0 left-0 right-0 z-10">
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
</div>
|
</div>
|
||||||
{/* Timeline with responsive layout - single row on desktop, double row on mobile */}
|
|
||||||
<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">
|
<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 className="absolute top-10 left-2 z-2 w-10">
|
||||||
|
<Toolbar />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</WSProvider>
|
</WSProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
106
app/toolbar.tsx
Normal file
106
app/toolbar.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { GlassButton } from "@/components/glass-button"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
import { LightbulbIcon, LightbulbOffIcon, User2Icon } from "lucide-react"
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useMap } from "./map-context";
|
||||||
|
|
||||||
|
export default function Toolbar() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { setOpacity, opacity } = useMap();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6 items-center h-full">
|
||||||
|
<GlassButton className="p-2" onClick={
|
||||||
|
() => {
|
||||||
|
router.push('/me');
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
<User2Icon className="w-3 h-3" />
|
||||||
|
</GlassButton>
|
||||||
|
|
||||||
|
<Opacity initOpacity={opacity.current} setOpacity={setOpacity} />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Opacity({ initOpacity, setOpacity }: { initOpacity: number, setOpacity: (opacity: number) => void }) {
|
||||||
|
|
||||||
|
const [opacity, setIOpacity] = useState(initOpacity);
|
||||||
|
|
||||||
|
const handleOpacityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newOpacity = parseFloat(e.target.value);
|
||||||
|
setOpacity(newOpacity);
|
||||||
|
setIOpacity(newOpacity);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 items-center h-full w-full">
|
||||||
|
<LightbulbIcon className="w-3 h-3" onClick={() => {
|
||||||
|
setOpacity(100);
|
||||||
|
setIOpacity(100);
|
||||||
|
}} />
|
||||||
|
<div
|
||||||
|
className="relative bg-black/20 border border-white/10 backdrop-blur-xl rounded-xl shadow-2xl overflow-hidden w-full h-30"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
step="1"
|
||||||
|
value={opacity}
|
||||||
|
onChange={handleOpacityChange}
|
||||||
|
className="absolute inset-0 cursor-pointer"
|
||||||
|
style={{
|
||||||
|
width: '120px',
|
||||||
|
height: '100%',
|
||||||
|
left: '50%',
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translate(-50%, -50%) rotate(270deg)',
|
||||||
|
transformOrigin: 'center',
|
||||||
|
background: 'transparent',
|
||||||
|
borderRadius: '12px',
|
||||||
|
WebkitAppearance: 'none',
|
||||||
|
MozAppearance: 'none',
|
||||||
|
appearance: 'none',
|
||||||
|
overflow: 'hidden',
|
||||||
|
transition: 'height 0.1s',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
{
|
||||||
|
`
|
||||||
|
input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
box-shadow: -200px 0 0 200px #ffffffdd;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-moz-range-thumb {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
border: none;
|
||||||
|
box-shadow: -200px 0 0 200px #ffffffdd;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<LightbulbOffIcon className="w-3 h-3" onClick={() => {
|
||||||
|
setOpacity(0);
|
||||||
|
setIOpacity(0);
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
29
components/glass-button.tsx
Normal file
29
components/glass-button.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export interface GlassButtonProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
onClick?: () => void;
|
||||||
|
className?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
type?: "button" | "submit" | "reset";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GlassButton: React.FC<GlassButtonProps> = ({
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
className = "",
|
||||||
|
disabled = false,
|
||||||
|
type = "button",
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type={type}
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
className={`bg-black/20 backdrop-blur-xl rounded-xl shadow-2xl overflow-hidden border border-white/10 ${disabled ? "opacity-50 cursor-not-allowed" : "hover:[transform:translateZ(2em)]"
|
||||||
|
} ${className}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
382
components/liquid-glass-button.tsx
Normal file
382
components/liquid-glass-button.tsx
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-primary-foreground hover:bg-destructive/90",
|
||||||
|
cool: "dark:inset-shadow-2xs dark:inset-shadow-white/10 bg-linear-to-t border border-b-2 border-zinc-950/40 from-primary to-primary/85 shadow-md shadow-primary/20 ring-1 ring-inset ring-white/25 transition-[filter] duration-200 hover:brightness-110 active:brightness-90 dark:border-x-0 text-primary-foreground dark:text-primary-foreground dark:border-t-0 dark:border-primary/50 dark:ring-white/5",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2",
|
||||||
|
sm: "h-8 rounded-md px-3 text-xs",
|
||||||
|
lg: "h-10 rounded-md px-8",
|
||||||
|
icon: "h-9 w-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants, liquidbuttonVariants, LiquidButton }
|
||||||
|
|
||||||
|
const liquidbuttonVariants = cva(
|
||||||
|
"inline-flex items-center transition-colors justify-center cursor-pointer gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-transparent hover:scale-105 duration-300 transition text-primary",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
sm: "h-8 text-xs gap-1.5 px-4 has-[>svg]:px-4",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
xl: "h-12 rounded-md px-8 has-[>svg]:px-6",
|
||||||
|
xxl: "h-14 rounded-md px-10 has-[>svg]:px-8",
|
||||||
|
icon: "size-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "xxl",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function LiquidButton({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
asChild = false,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof liquidbuttonVariants> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
className={cn(
|
||||||
|
"relative",
|
||||||
|
liquidbuttonVariants({ variant, size, className })
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="absolute top-0 left-0 z-0 h-full w-full rounded-full
|
||||||
|
shadow-[0_0_6px_rgba(0,0,0,0.03),0_2px_6px_rgba(0,0,0,0.08),inset_3px_3px_0.5px_-3px_rgba(0,0,0,0.9),inset_-3px_-3px_0.5px_-3px_rgba(0,0,0,0.85),inset_1px_1px_1px_-0.5px_rgba(0,0,0,0.6),inset_-1px_-1px_1px_-0.5px_rgba(0,0,0,0.6),inset_0_0_6px_6px_rgba(0,0,0,0.12),inset_0_0_2px_2px_rgba(0,0,0,0.06),0_0_12px_rgba(255,255,255,0.15)]
|
||||||
|
transition-all
|
||||||
|
dark:shadow-[0_0_8px_rgba(0,0,0,0.03),0_2px_6px_rgba(0,0,0,0.08),inset_3px_3px_0.5px_-3.5px_rgba(255,255,255,0.09),inset_-3px_-3px_0.5px_-3.5px_rgba(255,255,255,0.85),inset_1px_1px_1px_-0.5px_rgba(255,255,255,0.6),inset_-1px_-1px_1px_-0.5px_rgba(255,255,255,0.6),inset_0_0_6px_6px_rgba(255,255,255,0.12),inset_0_0_2px_2px_rgba(255,255,255,0.06),0_0_12px_rgba(0,0,0,0.15)]" />
|
||||||
|
<div
|
||||||
|
className="absolute top-0 left-0 isolate -z-10 h-full w-full overflow-hidden rounded-md"
|
||||||
|
style={{ backdropFilter: 'url("#container-glass")' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="pointer-events-none z-10 ">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<GlassFilter />
|
||||||
|
</Comp>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function GlassFilter() {
|
||||||
|
return (
|
||||||
|
<svg className="hidden">
|
||||||
|
<defs>
|
||||||
|
<filter
|
||||||
|
id="container-glass"
|
||||||
|
x="0%"
|
||||||
|
y="0%"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
colorInterpolationFilters="sRGB"
|
||||||
|
>
|
||||||
|
{/* Generate turbulent noise for distortion */}
|
||||||
|
<feTurbulence
|
||||||
|
type="fractalNoise"
|
||||||
|
baseFrequency="0.05 0.05"
|
||||||
|
numOctaves="1"
|
||||||
|
seed="1"
|
||||||
|
result="turbulence"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Blur the turbulence pattern slightly */}
|
||||||
|
<feGaussianBlur in="turbulence" stdDeviation="2" result="blurredNoise" />
|
||||||
|
|
||||||
|
{/* Displace the source graphic with the noise */}
|
||||||
|
<feDisplacementMap
|
||||||
|
in="SourceGraphic"
|
||||||
|
in2="blurredNoise"
|
||||||
|
scale="70"
|
||||||
|
xChannelSelector="R"
|
||||||
|
yChannelSelector="B"
|
||||||
|
result="displaced"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Apply overall blur on the final result */}
|
||||||
|
<feGaussianBlur in="displaced" stdDeviation="4" result="finalBlur" />
|
||||||
|
|
||||||
|
{/* Output the result */}
|
||||||
|
<feComposite in="finalBlur" in2="finalBlur" operator="over" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ColorVariant =
|
||||||
|
| "default"
|
||||||
|
| "primary"
|
||||||
|
| "success"
|
||||||
|
| "error"
|
||||||
|
| "gold"
|
||||||
|
| "bronze";
|
||||||
|
|
||||||
|
interface MetalButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
variant?: ColorVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorVariants: Record<
|
||||||
|
ColorVariant,
|
||||||
|
{
|
||||||
|
outer: string;
|
||||||
|
inner: string;
|
||||||
|
button: string;
|
||||||
|
textColor: string;
|
||||||
|
textShadow: string;
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
default: {
|
||||||
|
outer: "bg-gradient-to-b from-[#000] to-[#A0A0A0]",
|
||||||
|
inner: "bg-gradient-to-b from-[#FAFAFA] via-[#3E3E3E] to-[#E5E5E5]",
|
||||||
|
button: "bg-gradient-to-b from-[#B9B9B9] to-[#969696]",
|
||||||
|
textColor: "text-white",
|
||||||
|
textShadow: "[text-shadow:_0_-1px_0_rgb(80_80_80_/_100%)]",
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
outer: "bg-gradient-to-b from-[#000] to-[#A0A0A0]",
|
||||||
|
inner: "bg-gradient-to-b from-primary via-secondary to-muted",
|
||||||
|
button: "bg-gradient-to-b from-primary to-primary/40",
|
||||||
|
textColor: "text-white",
|
||||||
|
textShadow: "[text-shadow:_0_-1px_0_rgb(30_58_138_/_100%)]",
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
outer: "bg-gradient-to-b from-[#005A43] to-[#7CCB9B]",
|
||||||
|
inner: "bg-gradient-to-b from-[#E5F8F0] via-[#00352F] to-[#D1F0E6]",
|
||||||
|
button: "bg-gradient-to-b from-[#9ADBC8] to-[#3E8F7C]",
|
||||||
|
textColor: "text-[#FFF7F0]",
|
||||||
|
textShadow: "[text-shadow:_0_-1px_0_rgb(6_78_59_/_100%)]",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
outer: "bg-gradient-to-b from-[#5A0000] to-[#FFAEB0]",
|
||||||
|
inner: "bg-gradient-to-b from-[#FFDEDE] via-[#680002] to-[#FFE9E9]",
|
||||||
|
button: "bg-gradient-to-b from-[#F08D8F] to-[#A45253]",
|
||||||
|
textColor: "text-[#FFF7F0]",
|
||||||
|
textShadow: "[text-shadow:_0_-1px_0_rgb(146_64_14_/_100%)]",
|
||||||
|
},
|
||||||
|
gold: {
|
||||||
|
outer: "bg-gradient-to-b from-[#917100] to-[#EAD98F]",
|
||||||
|
inner: "bg-gradient-to-b from-[#FFFDDD] via-[#856807] to-[#FFF1B3]",
|
||||||
|
button: "bg-gradient-to-b from-[#FFEBA1] to-[#9B873F]",
|
||||||
|
textColor: "text-[#FFFDE5]",
|
||||||
|
textShadow: "[text-shadow:_0_-1px_0_rgb(178_140_2_/_100%)]",
|
||||||
|
},
|
||||||
|
bronze: {
|
||||||
|
outer: "bg-gradient-to-b from-[#864813] to-[#E9B486]",
|
||||||
|
inner: "bg-gradient-to-b from-[#EDC5A1] via-[#5F2D01] to-[#FFDEC1]",
|
||||||
|
button: "bg-gradient-to-b from-[#FFE3C9] to-[#A36F3D]",
|
||||||
|
textColor: "text-[#FFF7F0]",
|
||||||
|
textShadow: "[text-shadow:_0_-1px_0_rgb(124_45_18_/_100%)]",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const metalButtonVariants = (
|
||||||
|
variant: ColorVariant = "default",
|
||||||
|
isPressed: boolean,
|
||||||
|
isHovered: boolean,
|
||||||
|
isTouchDevice: boolean,
|
||||||
|
) => {
|
||||||
|
const colors = colorVariants[variant];
|
||||||
|
const transitionStyle = "all 250ms cubic-bezier(0.1, 0.4, 0.2, 1)";
|
||||||
|
|
||||||
|
return {
|
||||||
|
wrapper: cn(
|
||||||
|
"relative inline-flex transform-gpu rounded-md p-[1.25px] will-change-transform",
|
||||||
|
colors.outer,
|
||||||
|
),
|
||||||
|
wrapperStyle: {
|
||||||
|
transform: isPressed
|
||||||
|
? "translateY(2.5px) scale(0.99)"
|
||||||
|
: "translateY(0) scale(1)",
|
||||||
|
boxShadow: isPressed
|
||||||
|
? "0 1px 2px rgba(0, 0, 0, 0.15)"
|
||||||
|
: isHovered && !isTouchDevice
|
||||||
|
? "0 4px 12px rgba(0, 0, 0, 0.12)"
|
||||||
|
: "0 3px 8px rgba(0, 0, 0, 0.08)",
|
||||||
|
transition: transitionStyle,
|
||||||
|
transformOrigin: "center center",
|
||||||
|
},
|
||||||
|
inner: cn(
|
||||||
|
"absolute inset-[1px] transform-gpu rounded-lg will-change-transform",
|
||||||
|
colors.inner,
|
||||||
|
),
|
||||||
|
innerStyle: {
|
||||||
|
transition: transitionStyle,
|
||||||
|
transformOrigin: "center center",
|
||||||
|
filter:
|
||||||
|
isHovered && !isPressed && !isTouchDevice ? "brightness(1.05)" : "none",
|
||||||
|
},
|
||||||
|
button: cn(
|
||||||
|
"relative z-10 m-[1px] rounded-md inline-flex h-11 transform-gpu cursor-pointer items-center justify-center overflow-hidden rounded-md px-6 py-2 text-sm leading-none font-semibold will-change-transform outline-none",
|
||||||
|
colors.button,
|
||||||
|
colors.textColor,
|
||||||
|
colors.textShadow,
|
||||||
|
),
|
||||||
|
buttonStyle: {
|
||||||
|
transform: isPressed ? "scale(0.97)" : "scale(1)",
|
||||||
|
transition: transitionStyle,
|
||||||
|
transformOrigin: "center center",
|
||||||
|
filter:
|
||||||
|
isHovered && !isPressed && !isTouchDevice ? "brightness(1.02)" : "none",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const ShineEffect = ({ isPressed }: { isPressed: boolean }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none absolute inset-0 z-20 overflow-hidden transition-opacity duration-300",
|
||||||
|
isPressed ? "opacity-20" : "opacity-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 rounded-md bg-gradient-to-r from-transparent via-neutral-100 to-transparent" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MetalButton = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
MetalButtonProps
|
||||||
|
>(({ children, className, variant = "default", ...props }, ref) => {
|
||||||
|
const [isPressed, setIsPressed] = React.useState(false);
|
||||||
|
const [isHovered, setIsHovered] = React.useState(false);
|
||||||
|
const [isTouchDevice, setIsTouchDevice] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setIsTouchDevice("ontouchstart" in window || navigator.maxTouchPoints > 0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const buttonText = children || "Button";
|
||||||
|
const variants = metalButtonVariants(
|
||||||
|
variant,
|
||||||
|
isPressed,
|
||||||
|
isHovered,
|
||||||
|
isTouchDevice,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInternalMouseDown = () => {
|
||||||
|
setIsPressed(true);
|
||||||
|
};
|
||||||
|
const handleInternalMouseUp = () => {
|
||||||
|
setIsPressed(false);
|
||||||
|
};
|
||||||
|
const handleInternalMouseLeave = () => {
|
||||||
|
setIsPressed(false);
|
||||||
|
setIsHovered(false);
|
||||||
|
};
|
||||||
|
const handleInternalMouseEnter = () => {
|
||||||
|
if (!isTouchDevice) {
|
||||||
|
setIsHovered(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleInternalTouchStart = () => {
|
||||||
|
setIsPressed(true);
|
||||||
|
};
|
||||||
|
const handleInternalTouchEnd = () => {
|
||||||
|
setIsPressed(false);
|
||||||
|
};
|
||||||
|
const handleInternalTouchCancel = () => {
|
||||||
|
setIsPressed(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={variants.wrapper} style={variants.wrapperStyle}>
|
||||||
|
<div className={variants.inner} style={variants.innerStyle}></div>
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
className={cn(variants.button, className)}
|
||||||
|
style={variants.buttonStyle}
|
||||||
|
{...props}
|
||||||
|
onMouseDown={handleInternalMouseDown}
|
||||||
|
onMouseUp={handleInternalMouseUp}
|
||||||
|
onMouseLeave={handleInternalMouseLeave}
|
||||||
|
onMouseEnter={handleInternalMouseEnter}
|
||||||
|
onTouchStart={handleInternalTouchStart}
|
||||||
|
onTouchEnd={handleInternalTouchEnd}
|
||||||
|
onTouchCancel={handleInternalTouchCancel}
|
||||||
|
>
|
||||||
|
<ShineEffect isPressed={isPressed} />
|
||||||
|
{buttonText}
|
||||||
|
{isHovered && !isPressed && !isTouchDevice && (
|
||||||
|
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t rounded-lg from-transparent to-white/5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
MetalButton.displayName = "MetalButton";
|
||||||
@ -51,7 +51,7 @@ export function MapComponent({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
const mapContainer = useRef<HTMLDivElement>(null)
|
const mapContainer = useRef<HTMLDivElement>(null)
|
||||||
const { setMap, mapRef, currentDatetime, isMapReady } = useMap()
|
const { setMap, mapRef, currentDatetime, isMapReady, opacity } = useMap()
|
||||||
const { location } = useMapLocation()
|
const { location } = useMapLocation()
|
||||||
const texRef = useRef<WebGLTexture | null>(null)
|
const texRef = useRef<WebGLTexture | null>(null)
|
||||||
const lutTexRef = useRef<WebGLTexture | null>(null)
|
const lutTexRef = useRef<WebGLTexture | null>(null)
|
||||||
@ -254,7 +254,8 @@ export function MapComponent({
|
|||||||
'u_projection_clipping_plane': gl.getUniformLocation(program, 'u_projection_clipping_plane'),
|
'u_projection_clipping_plane': gl.getUniformLocation(program, 'u_projection_clipping_plane'),
|
||||||
'u_projection_transition': gl.getUniformLocation(program, 'u_projection_transition'),
|
'u_projection_transition': gl.getUniformLocation(program, 'u_projection_transition'),
|
||||||
'u_tex': gl.getUniformLocation(program, 'u_tex'),
|
'u_tex': gl.getUniformLocation(program, 'u_tex'),
|
||||||
'u_lut': gl.getUniformLocation(program, 'u_lut')
|
'u_lut': gl.getUniformLocation(program, 'u_lut'),
|
||||||
|
'u_opacity': gl.getUniformLocation(program, 'u_opacity')
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建并绑定顶点缓冲区
|
// 创建并绑定顶点缓冲区
|
||||||
@ -474,6 +475,10 @@ export function MapComponent({
|
|||||||
gl.uniform1i(locations['u_lut'], 1);
|
gl.uniform1i(locations['u_lut'], 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (locations['u_opacity']) {
|
||||||
|
gl.uniform1f(locations['u_opacity'], opacity.current / 100);
|
||||||
|
}
|
||||||
|
|
||||||
gl.bindVertexArray(this.vao);
|
gl.bindVertexArray(this.vao);
|
||||||
gl.enable(gl.BLEND);
|
gl.enable(gl.BLEND);
|
||||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user