feat: add dynamic weather hero effects
This commit is contained in:
75
components/weather/weather-effects.tsx
Normal file
75
components/weather/weather-effects.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import type { SynopStation, WeatherMood } from "@/types/imgw";
|
||||
|
||||
const rainDrops = Array.from({ length: 32 }, (_, index) => ({
|
||||
left: `${(index * 29 + 7) % 100}%`,
|
||||
delay: (index % 11) * 0.13,
|
||||
duration: 0.72 + (index % 5) * 0.12,
|
||||
height: 12 + (index % 4) * 4,
|
||||
}));
|
||||
|
||||
const windLines = Array.from({ length: 7 }, (_, index) => ({
|
||||
top: `${18 + index * 11}%`,
|
||||
delay: index * 0.22,
|
||||
width: 70 + (index % 3) * 34,
|
||||
}));
|
||||
|
||||
const stars = Array.from({ length: 16 }, (_, index) => ({
|
||||
left: `${(index * 37 + 11) % 96}%`,
|
||||
top: `${(index * 23 + 8) % 72}%`,
|
||||
delay: (index % 6) * 0.35,
|
||||
}));
|
||||
|
||||
export function WeatherEffects({ station, mood }: { station: SynopStation; mood: WeatherMood }) {
|
||||
const reduceMotion = useReducedMotion();
|
||||
const rainfall = station.rainfall ?? 0;
|
||||
const isRaining = rainfall >= 0.1;
|
||||
const visibleDrops = rainfall >= 5 ? rainDrops : rainDrops.slice(0, 18);
|
||||
const isWindy = (station.windSpeed ?? 0) >= 8;
|
||||
|
||||
return (
|
||||
<div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
|
||||
{mood === "warm" && (
|
||||
<motion.div
|
||||
animate={reduceMotion ? undefined : { scale: [1, 1.08, 1], opacity: [0.4, 0.58, 0.4] }}
|
||||
transition={{ duration: 7, repeat: Infinity, ease: "easeInOut" }}
|
||||
className="absolute -right-16 -top-20 size-64 rounded-full bg-amber-200/45 blur-3xl"
|
||||
/>
|
||||
)}
|
||||
{mood === "night" && stars.map((star, index) => (
|
||||
<motion.span
|
||||
key={index}
|
||||
animate={reduceMotion ? undefined : { opacity: [0.2, 0.75, 0.2] }}
|
||||
transition={{ duration: 2.4, delay: star.delay, repeat: Infinity, ease: "easeInOut" }}
|
||||
className="absolute size-1 rounded-full bg-white/75"
|
||||
style={{ left: star.left, top: star.top }}
|
||||
/>
|
||||
))}
|
||||
{isWindy && windLines.map((line, index) => (
|
||||
<motion.span
|
||||
key={index}
|
||||
initial={{ x: "-30%", opacity: 0 }}
|
||||
animate={reduceMotion ? { opacity: 0.28 } : { x: ["-30%", "135%"], opacity: [0, 0.35, 0] }}
|
||||
transition={{ duration: 3.4, delay: line.delay, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute h-px rounded-full bg-white/60"
|
||||
style={{ top: line.top, width: line.width }}
|
||||
/>
|
||||
))}
|
||||
{isRaining && !reduceMotion && visibleDrops.map((drop, index) => (
|
||||
<motion.span
|
||||
key={index}
|
||||
initial={{ y: "-8vh", opacity: 0 }}
|
||||
animate={{ y: ["-8vh", "85vh"], x: [0, -12], opacity: [0, 0.5, 0.12] }}
|
||||
transition={{ duration: drop.duration, delay: drop.delay, repeat: Infinity, ease: "linear" }}
|
||||
className="absolute -top-8 w-px rounded-full bg-cyan-100/80"
|
||||
style={{ height: drop.height, left: drop.left, transform: "rotate(14deg)" }}
|
||||
/>
|
||||
))}
|
||||
{mood === "cold" && (
|
||||
<div className="absolute inset-x-0 bottom-0 h-28 bg-gradient-to-t from-cyan-100/20 to-transparent" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "@/lib/weather-utils";
|
||||
import type { SynopStation } from "@/types/imgw";
|
||||
import { WeatherIcon } from "@/components/weather/weather-icon";
|
||||
import { WeatherEffects } from "@/components/weather/weather-effects";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
|
||||
export function WeatherHero({ station, locationName, distanceKm }: { station: SynopStation; locationName?: string; distanceKm?: number }) {
|
||||
@@ -36,6 +37,7 @@ export function WeatherHero({ station, locationName, distanceKm }: { station: Sy
|
||||
transition={{ duration: 0.55, ease: "easeOut" }}
|
||||
className={`relative isolate overflow-hidden rounded-[2rem] bg-gradient-to-br ${moodGradient(mood)} px-5 py-6 text-white shadow-[0_24px_75px_rgba(15,23,42,0.24)] sm:px-8 sm:py-8 lg:px-10`}
|
||||
>
|
||||
<WeatherEffects station={station} mood={mood} />
|
||||
<div className="absolute -right-20 -top-20 size-72 rounded-full bg-white/15 blur-3xl" />
|
||||
<div className="absolute -bottom-24 -left-16 size-72 rounded-full bg-cyan-200/15 blur-3xl" />
|
||||
<div className="relative">
|
||||
@@ -45,7 +47,7 @@ export function WeatherHero({ station, locationName, distanceKm }: { station: Sy
|
||||
</div>
|
||||
<div className="mt-8 flex items-end justify-between gap-3 sm:mt-10">
|
||||
<div>
|
||||
<div className="text-[5.8rem] font-extralight leading-[0.85] tracking-[-0.11em] sm:text-[8rem]">
|
||||
<div className="text-[5.8rem] font-medium leading-[0.85] tracking-[-0.11em] drop-shadow-[0_10px_24px_rgba(15,23,42,0.16)] sm:text-[8rem]">
|
||||
{formatTemperature(station.temperature, language)}
|
||||
</div>
|
||||
<p className="mt-5 text-xl font-medium tracking-tight sm:text-2xl">{getWeatherDescription(station, language)}</p>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { CloudRain, CloudSun, MoonStar, Snowflake, Sun, Wind } from "lucide-react";
|
||||
import { CloudRain, CloudSun, MoonStar, Snowflake, ThermometerSun, Wind } from "lucide-react";
|
||||
import type { WeatherMood } from "@/types/imgw";
|
||||
|
||||
export function WeatherIcon({ mood, className = "" }: { mood: WeatherMood; className?: string }) {
|
||||
const Icon = {
|
||||
clear: Sun,
|
||||
warm: ThermometerSun,
|
||||
rain: CloudRain,
|
||||
wind: Wind,
|
||||
cold: Snowflake,
|
||||
|
||||
@@ -160,11 +160,11 @@ export function getWindDirection(degrees: number) {
|
||||
|
||||
export function getWeatherMoodFromData(station: SynopStation, date = new Date()): WeatherMood {
|
||||
const hour = date.getHours();
|
||||
if (hour < 6 || hour >= 21) return "night";
|
||||
if ((station.rainfall ?? 0) >= 0.1) return "rain";
|
||||
if (hour < 6 || hour >= 21) return "night";
|
||||
if ((station.windSpeed ?? 0) >= 8) return "wind";
|
||||
if ((station.temperature ?? 15) <= 3) return "cold";
|
||||
if ((station.temperature ?? 15) >= 20) return "clear";
|
||||
if ((station.temperature ?? 15) >= 20) return "warm";
|
||||
return "mild";
|
||||
}
|
||||
|
||||
@@ -178,11 +178,11 @@ export function getWeatherDescription(station: SynopStation, language: Language
|
||||
|
||||
export function moodGradient(mood: WeatherMood) {
|
||||
return {
|
||||
clear: "from-sky-500 via-blue-500 to-indigo-700",
|
||||
rain: "from-slate-500 via-slate-600 to-indigo-900",
|
||||
wind: "from-cyan-600 via-slate-500 to-blue-900",
|
||||
cold: "from-cyan-400 via-blue-500 to-indigo-800",
|
||||
warm: "from-sky-400 via-blue-500 to-indigo-700",
|
||||
rain: "from-slate-500 via-blue-700 to-slate-950",
|
||||
wind: "from-cyan-600 via-slate-600 to-blue-950",
|
||||
cold: "from-cyan-300 via-blue-500 to-indigo-900",
|
||||
night: "from-slate-800 via-indigo-950 to-slate-950",
|
||||
mild: "from-sky-500 via-cyan-600 to-blue-800",
|
||||
mild: "from-sky-500 via-cyan-700 to-blue-900",
|
||||
}[mood];
|
||||
}
|
||||
|
||||
@@ -115,4 +115,4 @@ export interface WeatherWarning {
|
||||
office: string | null;
|
||||
}
|
||||
|
||||
export type WeatherMood = "clear" | "rain" | "wind" | "cold" | "night" | "mild";
|
||||
export type WeatherMood = "warm" | "rain" | "wind" | "cold" | "night" | "mild";
|
||||
|
||||
Reference in New Issue
Block a user