地圖區域揭示
台灣輪廓地圖上依序揭示五大區域:北部(藍)、中部(綠)、南部(橘)、東部(紫)、外島(紅),每個區域以半透明漸層色塊覆蓋對應地理位置,城市標記依序以 spring 動畫彈出,外島另以小圓圈象徵標示。
地圖區域台灣揭示彈簧動畫地理
提示詞(可直接修改內容)
import {
AbsoluteFill,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from "remotion";
import React from "react";
// 投影函數
const GEO = { lngMin: 119.5, lngMax: 122.5, latMin: 21.7, latMax: 25.5 };
const MAP = { x: 200, y: 60, w: 500, h: 760 };
const project = (lat: number, lng: number) => ({
x: MAP.x + ((lng - GEO.lngMin) / (GEO.lngMax - GEO.lngMin)) * MAP.w,
y: MAP.y + ((GEO.latMax - lat) / (GEO.latMax - GEO.latMin)) * MAP.h,
});
// 台灣輪廓
const TAIWAN-OUTLINE: [number, number][] = [
[25.3, 121.54], [25.13, 121.74], [25.0, 122.0], [24.87, 121.83],
[24.72, 121.77], [24.6, 121.87], [23.98, 121.61], [23.55, 121.55],
[23.09, 121.37], [22.75, 121.1], [22.3, 120.9], [21.9, 120.85],
[22.17, 120.7], [22.38, 120.49], [22.62, 120.26], [23.05, 120.1],
[23.42, 120.12], [23.71, 120.29], [23.96, 120.43], [24.2, 120.57],
[24.56, 120.78], [24.85, 120.88], [25.0, 121.35], [25.17, 121.43],
[25.3, 121.54],
];
const taiwanPath = TAIWAN-OUTLINE.map(([lat, lng], i) => {
const p = project(lat, lng);
return `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`;
}).join(" ") + " Z";
// 5 大區域定義
const REGIONS = [
{
id: "north",
name: "北部",
color: "#3b82f6",
glowColor: "#60a5fa",
startFrame: 0,
// 中心位置(地理座標)
centerLat: 25.0,
centerLng: 121.4,
// 橢圓覆蓋範圍(SVG 空間)
rx: 100,
ry: 70,
cities: [
{ name: "台北", lat: 25.033, lng: 121.565 },
{ name: "基隆", lat: 25.128, lng: 121.739 },
{ name: "桃園", lat: 24.994, lng: 121.301 },
{ name: "新竹", lat: 24.807, lng: 120.969 },
],
},
{
id: "central",
name: "中部",
color: "#22c55e",
glowColor: "#4ade80",
startFrame: 25,
centerLat: 24.1,
centerLng: 120.7,
rx: 100,
ry: 90,
cities: [
{ name: "台中", lat: 24.148, lng: 120.674 },
{ name: "彰化", lat: 24.052, lng: 120.516 },
{ name: "南投", lat: 23.961, lng: 120.972 },
],
},
{
id: "south",
name: "南部",
color: "#f97316",
glowColor: "#fb923c",
startFrame: 50,
centerLat: 22.85,
centerLng: 120.35,
rx: 100,
ry: 100,
cities: [
{ name: "台南", lat: 23.0, lng: 120.227 },
{ name: "高雄", lat: 22.627, lng: 120.301 },
{ name: "屏東", lat: 22.671, lng: 120.488 },
],
},
{
id: "east",
name: "東部",
color: "#a855f7",
glowColor: "#c084fc",
startFrame: 75,
centerLat: 23.9,
centerLng: 121.6,
rx: 55,
ry: 130,
cities: [
{ name: "花蓮", lat: 23.991, lng: 121.611 },
{ name: "台東", lat: 22.798, lng: 121.1 },
],
},
{
id: "offshore",
name: "外島",
color: "#ef4444",
glowColor: "#f87171",
startFrame: 100,
centerLat: 23.5,
centerLng: 119.5,
rx: 0,
ry: 0,
cities: [],
// 外島用小圓圈象徵,位置在地圖左側
isOffshore: true,
},
].map((r) => ({
...r,
center: project(r.centerLat, r.centerLng),
cities: r.cities.map((c) => ({ ...c, ...project(c.lat, c.lng) })),
}));
// 外島象徵位置(在台灣地圖左側)
const OFFSHORE-ICONS = [
{ name: "澎湖", x: 90, y: 530 },
{ name: "金門", x: 55, y: 460 },
];
export const MapAreaReveal: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
return (
<AbsoluteFill
style={{
background: "#050d1a",
fontFamily: "sans-serif",
}}
>
{/* 標題 */}
<div
style={{
position: "absolute",
top: 40,
left: 0,
right: 0,
textAlign: "center",
opacity: interpolate(frame, [0, 20], [0, 1], { extrapolateRight: "clamp" }),
}}
>
<div style={{ color: "#64748b", fontSize: 15, letterSpacing: 4, marginBottom: 6 }}>
地理區域揭示
</div>
<div style={{ color: "#f1f5f9", fontSize: 44, fontWeight: 800, letterSpacing: 6 }}>
台灣五大區域
</div>
</div>
<svg
width="1920"
height="1080"
viewBox="0 0 1920 1080"
style={{ position: "absolute", top: 0, left: 0 }}
>
<defs>
<pattern id="marGrid" width="60" height="60" patternUnits="userSpaceOnUse">
<path d="M 60 0 L 0 0 0 60" fill="none" stroke="#0a1828" strokeWidth="1" />
</pattern>
{REGIONS.map((r) => (
<radialGradient key={r.id} id={`rgrad-${r.id}`} cx="50%" cy="50%" r="50%">
<stop offset="0%" stopColor={r.color} stopOpacity="0.55" />
<stop offset="100%" stopColor={r.color} stopOpacity="0" />
</radialGradient>
))}
</defs>
{/* 背景格線 */}
<rect width="1920" height="1080" fill="url(#marGrid)" />
{/* 台灣輪廓 */}
<path
d={taiwanPath}
fill="#0f2040"
stroke="#1e3a5f"
strokeWidth="2"
strokeLinejoin="round"
/>
{/* 區域色塊(依序揭示) */}
{REGIONS.filter((r) => !r.isOffshore).map((region) => {
const sp = spring({
frame: frame - region.startFrame,
fps,
config: { damping: 20, stiffness: 140 },
});
const areaOpacity = interpolate(sp, [0, 1], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const areaScale = interpolate(sp, [0, 1], [0.3, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<g key={region.id}>
{/* 漸層橢圓色塊 */}
<ellipse
cx={region.center.x}
cy={region.center.y}
rx={(region as any).rx * areaScale}
ry={(region as any).ry * areaScale}
fill={`url(#rgrad-${region.id})`}
opacity={areaOpacity}
/>
{/* 輪廓線 */}
<ellipse
cx={region.center.x}
cy={region.center.y}
rx={(region as any).rx * areaScale}
ry={(region as any).ry * areaScale}
fill="none"
stroke={region.color}
strokeWidth="1.5"
opacity={areaOpacity * 0.6}
strokeDasharray="6 4"
/>
</g>
);
})}
{/* 城市標記點(依序彈出) */}
{REGIONS.filter((r) => !r.isOffshore).map((region) =>
(region.cities as any[]).map((city, ci) => {
const cityDelay = region.startFrame + ci * 8;
const sp = spring({
frame: frame - cityDelay,
fps,
config: { damping: 16, stiffness: 220 },
});
const scale = interpolate(sp, [0, 1], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const labelOpacity = interpolate(frame - cityDelay - 5, [0, 15], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<g key={city.name} transform={`translate(${city.x}, ${city.y})`}>
<circle
r={8 * scale}
fill={region.color}
stroke="#050d1a"
strokeWidth="2"
style={{ filter: scale > 0.8 ? `drop-shadow(0 0 6px ${region.glowColor})` : "none" }}
/>
<text
x={city.x > 400 ? -16 : 14}
y="5"
fill="#f1f5f9"
fontSize="17"
fontWeight="600"
textAnchor={city.x > 400 ? "end" : "start"}
opacity={labelOpacity}
>
{city.name}
</text>
</g>
);
})
)}
{/* 外島圖示(左側小圓圈) */}
{OFFSHORE-ICONS.map((icon, i) => {
const delay = REGIONS.find((r) => r.isOffshore)!.startFrame + i * 12;
const sp = spring({
frame: frame - delay,
fps,
config: { damping: 16, stiffness: 220 },
});
const scale = interpolate(sp, [0, 1], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const labelOpacity = interpolate(frame - delay - 5, [0, 15], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const offshoreColor = "#ef4444";
const offshoreGlow = "#f87171";
return (
<g key={icon.name} transform={`translate(${icon.x}, ${icon.y})`}>
{/* 虛線連接到台灣 */}
<line
x1={0}
y1={0}
x2={MAP.x - icon.x + 20}
y2={0}
stroke={offshoreColor}
strokeWidth="1"
strokeDasharray="4 4"
opacity={labelOpacity * 0.4}
/>
{/* 圓圈 */}
<circle
r={18 * scale}
fill="none"
stroke={offshoreColor}
strokeWidth="2"
opacity={scale}
style={{ filter: scale > 0.8 ? `drop-shadow(0 0 6px ${offshoreGlow})` : "none" }}
/>
<circle
r={6 * scale}
fill={offshoreColor}
style={{ filter: scale > 0.8 ? `drop-shadow(0 0 4px ${offshoreGlow})` : "none" }}
/>
<text
x={0}
y={-24}
textAnchor="middle"
fill="#f1f5f9"
fontSize="15"
fontWeight="600"
opacity={labelOpacity}
>
{icon.name}
</text>
</g>
);
})}
</svg>
{/* 右側圖例 */}
<div
style={{
position: "absolute",
right: 80,
top: "50%",
transform: "translateY(-50%)",
width: 280,
background: "rgba(5, 13, 26, 0.92)",
border: "1px solid #1e3a5f",
borderRadius: 14,
padding: "28px 32px",
opacity: interpolate(frame, [10, 40], [0, 1], { extrapolateRight: "clamp" }),
}}
>
<div style={{ color: "#64748b", fontSize: 13, letterSpacing: 3, marginBottom: 20 }}>
區域分類
</div>
{REGIONS.map((region) => {
const sp = spring({
frame: frame - region.startFrame,
fps,
config: { damping: 20, stiffness: 140 },
});
const itemOpacity = interpolate(sp, [0, 0.5], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div
key={region.id}
style={{
display: "flex",
alignItems: "center",
gap: 14,
marginBottom: 18,
opacity: itemOpacity,
}}
>
<div
style={{
width: 14,
height: 14,
borderRadius: "50%",
background: region.color,
boxShadow: `0 0 6px ${region.glowColor}`,
flexShrink: 0,
}}
/>
<span style={{ color: "#e2e8f0", fontSize: 20, fontWeight: 600 }}>
{region.name}
</span>
</div>
);
})}
</div>
</AbsoluteFill>
);
};登入後查看完整程式碼