stars
This commit is contained in:
parent
66f118171d
commit
42b158a6ed
3 changed files with 368 additions and 0 deletions
127
src/app/(frontend)/customgrey.css
Normal file
127
src/app/(frontend)/customgrey.css
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
:root {
|
||||||
|
--card: #f5f5f5;
|
||||||
|
--ring: #606060;
|
||||||
|
--input: #e0e0e0;
|
||||||
|
--muted: #d9d9d9;
|
||||||
|
--accent: #c0c0c0;
|
||||||
|
--border: #d0d0d0;
|
||||||
|
--radius: 0.35rem;
|
||||||
|
--chart-1: #606060;
|
||||||
|
--chart-2: #476666;
|
||||||
|
--chart-3: #909090;
|
||||||
|
--chart-4: #a8a8a8;
|
||||||
|
--chart-5: #c0c0c0;
|
||||||
|
--popover: #f5f5f5;
|
||||||
|
--primary: #606060;
|
||||||
|
--sidebar: #eaeaea;
|
||||||
|
--font-mono: Fira Code, monospace;
|
||||||
|
--font-sans: Montserrat, sans-serif;
|
||||||
|
--secondary: #e0e0e0;
|
||||||
|
--background: #f0f0f0;
|
||||||
|
--font-serif: Georgia, serif;
|
||||||
|
--foreground: #333333;
|
||||||
|
--destructive: #cc3333;
|
||||||
|
--shadow-blur: 0px;
|
||||||
|
--shadow-color: hsl(0 0% 20% / 0.1);
|
||||||
|
--sidebar-ring: #606060;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.15;
|
||||||
|
--sidebar-accent: #c0c0c0;
|
||||||
|
--sidebar-border: #d0d0d0;
|
||||||
|
--card-foreground: #333333;
|
||||||
|
--shadow-offset-x: 0px;
|
||||||
|
--shadow-offset-y: 2px;
|
||||||
|
--sidebar-primary: #606060;
|
||||||
|
--muted-foreground: #666666;
|
||||||
|
--accent-foreground: #333333;
|
||||||
|
--popover-foreground: #333333;
|
||||||
|
--primary-foreground: #ffffff;
|
||||||
|
--sidebar-foreground: #333333;
|
||||||
|
--secondary-foreground: #333333;
|
||||||
|
--destructive-foreground: #ffffff;
|
||||||
|
--sidebar-accent-foreground: #333333;
|
||||||
|
--sidebar-primary-foreground: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--card: #202020;
|
||||||
|
--ring: #a0a0a0;
|
||||||
|
--input: #303030;
|
||||||
|
--muted: #2a2a2a;
|
||||||
|
--accent: #404040;
|
||||||
|
--border: #353535;
|
||||||
|
--chart-1: #a0a0a0;
|
||||||
|
--chart-2: #7e9ca0;
|
||||||
|
--chart-3: #707070;
|
||||||
|
--chart-4: #585858;
|
||||||
|
--chart-5: #404040;
|
||||||
|
--popover: #202020;
|
||||||
|
--primary: #a0a0a0;
|
||||||
|
--sidebar: #1f1f1f;
|
||||||
|
--font-mono: Fira Code, monospace;
|
||||||
|
--font-sans: Inter, sans-serif;
|
||||||
|
--secondary: #303030;
|
||||||
|
--background: #1a1a1a;
|
||||||
|
--font-serif: Georgia, serif;
|
||||||
|
--foreground: #d9d9d9;
|
||||||
|
--destructive: #e06666;
|
||||||
|
--sidebar-ring: #a0a0a0;
|
||||||
|
--sidebar-accent: #404040;
|
||||||
|
--sidebar-border: #353535;
|
||||||
|
--card-foreground: #d9d9d9;
|
||||||
|
--sidebar-primary: #a0a0a0;
|
||||||
|
--muted-foreground: #808080;
|
||||||
|
--accent-foreground: #d9d9d9;
|
||||||
|
--popover-foreground: #d9d9d9;
|
||||||
|
--primary-foreground: #1a1a1a;
|
||||||
|
--sidebar-foreground: #d9d9d9;
|
||||||
|
--secondary-foreground: #d9d9d9;
|
||||||
|
--destructive-foreground: #ffffff;
|
||||||
|
--sidebar-accent-foreground: #d9d9d9;
|
||||||
|
--sidebar-primary-foreground: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-radius: var(--radius);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-font-mono: var(--font-mono);
|
||||||
|
--color-font-sans: var(--font-sans);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-font-serif: var(--font-serif);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-shadow-blur: var(--shadow-blur);
|
||||||
|
--color-shadow-color: var(--shadow-color);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--color-shadow-spread: var(--shadow-spread);
|
||||||
|
--color-shadow-opacity: var(--shadow-opacity);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-shadow-offset-x: var(--shadow-offset-x);
|
||||||
|
--color-shadow-offset-y: var(--shadow-offset-y);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,16 @@
|
||||||
import PageTemplate, { generateMetadata } from './[slug]/page'
|
import PageTemplate, { generateMetadata } from './[slug]/page'
|
||||||
|
|
||||||
|
import HeroBackground from "@/components/HeroBackground";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<div style={{ width: "100vw", height: "100vh" }}>
|
||||||
|
<HeroBackground />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export default PageTemplate
|
export default PageTemplate
|
||||||
|
|
||||||
export { generateMetadata }
|
export { generateMetadata }
|
||||||
|
|
|
||||||
230
src/components/HeroBackground.tsx
Normal file
230
src/components/HeroBackground.tsx
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
const STAR_TONES = [255, 210, 160, 110, 65];
|
||||||
|
const WAVE_TONES = [55, 90, 125, 165, 205];
|
||||||
|
|
||||||
|
const WAVE_CONFIGS = [
|
||||||
|
{ amp: 26, freq: 0.011, speed: 0.35, phase: 0.0, yBase: 0.38, width: 1.6, toneIdx: 0 },
|
||||||
|
{ amp: 20, freq: 0.015, speed: 0.50, phase: 1.3, yBase: 0.52, width: 1.8, toneIdx: 1 },
|
||||||
|
{ amp: 30, freq: 0.008, speed: 0.28, phase: 2.6, yBase: 0.63, width: 2.2, toneIdx: 2 },
|
||||||
|
{ amp: 16, freq: 0.019, speed: 0.65, phase: 0.8, yBase: 0.74, width: 2.0, toneIdx: 3 },
|
||||||
|
{ amp: 22, freq: 0.013, speed: 0.42, phase: 3.2, yBase: 0.85, width: 2.6, toneIdx: 4 },
|
||||||
|
];
|
||||||
|
|
||||||
|
interface Star {
|
||||||
|
x: number; y: number; r: number;
|
||||||
|
v: number; baseAlpha: number;
|
||||||
|
phase: number; twinkleSpeed: number;
|
||||||
|
vx: number; vy: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Wave {
|
||||||
|
amp: number; freq: number; speed: number;
|
||||||
|
phase: number; yBase: number; width: number;
|
||||||
|
v: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initStars(W: number, H: number): Star[] {
|
||||||
|
return Array.from({ length: 180 }, () => {
|
||||||
|
const toneIdx = Math.floor(Math.random() * STAR_TONES.length);
|
||||||
|
return {
|
||||||
|
x: Math.random() * W,
|
||||||
|
y: Math.random() * H,
|
||||||
|
r: 0.4 + Math.random() * 1.6,
|
||||||
|
v: STAR_TONES[toneIdx],
|
||||||
|
baseAlpha: 0.5 + toneIdx * 0.1,
|
||||||
|
phase: Math.random() * Math.PI * 2,
|
||||||
|
twinkleSpeed: 0.8 + Math.random() * 2.0,
|
||||||
|
vx: (Math.random() - 0.5) * 0.3,
|
||||||
|
vy: (Math.random() - 0.5) * 0.15,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initWaves(): Wave[] {
|
||||||
|
return WAVE_CONFIGS.map(c => ({ ...c, v: WAVE_TONES[c.toneIdx] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawStars(ctx: CanvasRenderingContext2D, stars: Star[], W: number, H: number, ga: number) {
|
||||||
|
const now = Date.now() / 1000;
|
||||||
|
for (const s of stars) {
|
||||||
|
s.x += s.vx; s.y += s.vy;
|
||||||
|
if (s.x < 0) s.x = W; if (s.x > W) s.x = 0;
|
||||||
|
if (s.y < 0) s.y = H; if (s.y > H) s.y = 0;
|
||||||
|
const twinkle = 0.35 + 0.65 * ((Math.sin(now * s.twinkleSpeed + s.phase) + 1) / 2);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = `rgba(${s.v},${s.v},${s.v},${s.baseAlpha * twinkle * ga})`;
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWavePts(w: Wave, W: number, H: number, now: number) {
|
||||||
|
const pts: { x: number; y: number }[] = [];
|
||||||
|
for (let x = 0; x <= W; x += 8) {
|
||||||
|
pts.push({ x, y: H * w.yBase + Math.sin(x * w.freq + w.phase + now * w.speed) * w.amp });
|
||||||
|
}
|
||||||
|
return pts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawWavePath(ctx: CanvasRenderingContext2D, pts: { x: number; y: number }[], W: number, H: number) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(pts[0].x, pts[0].y);
|
||||||
|
for (let i = 0; i < pts.length - 1; i++) {
|
||||||
|
const mx = (pts[i].x + pts[i + 1].x) / 2;
|
||||||
|
const my = (pts[i].y + pts[i + 1].y) / 2;
|
||||||
|
ctx.quadraticCurveTo(pts[i].x, pts[i].y, mx, my);
|
||||||
|
}
|
||||||
|
ctx.lineTo(pts[pts.length - 1].x, pts[pts.length - 1].y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawWaves(ctx: CanvasRenderingContext2D, waves: Wave[], W: number, H: number, ga: number, bgR: number) {
|
||||||
|
const now = Date.now() / 1000;
|
||||||
|
const bg = Math.round(bgR);
|
||||||
|
|
||||||
|
for (const w of waves) {
|
||||||
|
const v = w.v;
|
||||||
|
const yBase = H * w.yBase;
|
||||||
|
const pts = buildWavePts(w, W, H, now);
|
||||||
|
|
||||||
|
const grad = ctx.createLinearGradient(0, yBase - w.amp, 0, yBase + w.amp * 3);
|
||||||
|
grad.addColorStop(0, `rgba(${v},${v},${v},${0.030 * ga})`);
|
||||||
|
grad.addColorStop(0.4, `rgba(${v},${v},${v},${0.012 * ga})`);
|
||||||
|
grad.addColorStop(1, `rgba(${bg},${bg},${bg},0)`);
|
||||||
|
|
||||||
|
drawWavePath(ctx, pts, W, H);
|
||||||
|
ctx.lineTo(W, H); ctx.lineTo(0, H); ctx.closePath();
|
||||||
|
ctx.fillStyle = grad;
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
drawWavePath(ctx, pts, W, H);
|
||||||
|
ctx.strokeStyle = `rgba(${v},${v},${v},${0.85 * ga})`;
|
||||||
|
ctx.lineWidth = w.width;
|
||||||
|
ctx.lineJoin = "round";
|
||||||
|
ctx.lineCap = "round";
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Component ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface HeroBackgroundProps {
|
||||||
|
/** Controlled from outside if you have a global theme toggle. */
|
||||||
|
isDark?: boolean;
|
||||||
|
/** Fires when the internal toggle is clicked (omit to hide the button). */
|
||||||
|
onToggle?: (isDark: boolean) => void;
|
||||||
|
/** Show the built-in toggle button. Defaults to true. */
|
||||||
|
showToggle?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HeroBackground({
|
||||||
|
isDark: externalIsDark,
|
||||||
|
onToggle,
|
||||||
|
showToggle = true,
|
||||||
|
}: HeroBackgroundProps) {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const [internalDark, setInternalDark] = useState(true);
|
||||||
|
|
||||||
|
// Use external control if provided, otherwise internal state
|
||||||
|
const isDark = externalIsDark !== undefined ? externalIsDark : internalDark;
|
||||||
|
|
||||||
|
function handleToggle() {
|
||||||
|
const next = !isDark;
|
||||||
|
setInternalDark(next);
|
||||||
|
onToggle?.(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
let W = 0, H = 0;
|
||||||
|
let stars: Star[] = [];
|
||||||
|
let waves: Wave[] = [];
|
||||||
|
let rafId: number;
|
||||||
|
|
||||||
|
// Animated bg colour
|
||||||
|
let bgR = 0, bgG = 0, bgB = 0;
|
||||||
|
let crossfade = 0;
|
||||||
|
|
||||||
|
function resize() {
|
||||||
|
W = canvas!.width = canvas!.offsetWidth;
|
||||||
|
H = canvas!.height = canvas!.offsetHeight;
|
||||||
|
stars = initStars(W, H);
|
||||||
|
waves = initWaves();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loop() {
|
||||||
|
// Read current target from closure — updated via ref below
|
||||||
|
const dark = darkRef.current;
|
||||||
|
const targetBg = dark ? 0 : 255;
|
||||||
|
const targetCross = dark ? 0 : 1;
|
||||||
|
|
||||||
|
bgR += (targetBg - bgR) * 0.04;
|
||||||
|
bgG += (targetBg - bgG) * 0.04;
|
||||||
|
bgB += (targetBg - bgB) * 0.04;
|
||||||
|
crossfade += (targetCross - crossfade) * 0.03;
|
||||||
|
|
||||||
|
ctx!.fillStyle = `rgb(${Math.round(bgR)},${Math.round(bgG)},${Math.round(bgB)})`;
|
||||||
|
ctx!.fillRect(0, 0, W, H);
|
||||||
|
|
||||||
|
const sa = 1 - crossfade;
|
||||||
|
const wa = crossfade;
|
||||||
|
if (sa > 0.01) drawStars(ctx!, stars, W, H, sa);
|
||||||
|
if (wa > 0.01) drawWaves(ctx!, waves, W, H, wa, bgR);
|
||||||
|
|
||||||
|
rafId = requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ro = new ResizeObserver(resize);
|
||||||
|
ro.observe(canvas);
|
||||||
|
resize();
|
||||||
|
rafId = requestAnimationFrame(loop);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
ro.disconnect();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Keep a ref so the loop closure always reads the latest isDark
|
||||||
|
const darkRef = useRef(isDark);
|
||||||
|
useEffect(() => { darkRef.current = isDark; }, [isDark]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: "relative", width: "100%", height: "100%" }}>
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
style={{ display: "block", width: "100%", height: "100%" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{showToggle && (
|
||||||
|
<button
|
||||||
|
onClick={handleToggle}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 20,
|
||||||
|
right: 20,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
padding: "8px 16px",
|
||||||
|
fontSize: 13,
|
||||||
|
cursor: "pointer",
|
||||||
|
borderRadius: 999,
|
||||||
|
border: `1px solid ${isDark ? "rgba(255,255,255,0.2)" : "rgba(0,0,0,0.18)"}`,
|
||||||
|
background: isDark ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.07)",
|
||||||
|
color: isDark ? "#fff" : "#111",
|
||||||
|
transition: "background 0.4s, color 0.4s, border-color 0.4s",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isDark ? "☀" : "☾"} {isDark ? "Light mode" : "Dark mode"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue