diff --git a/src/components/WarMap.tsx b/src/components/WarMap.tsx
index 040f911..b4a3786 100644
--- a/src/components/WarMap.tsx
+++ b/src/components/WarMap.tsx
@@ -4,15 +4,40 @@ import type { MapRef } from 'react-map-gl'
import type { Map as MapboxMap } from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css'
import { useSituationStore } from '@/store/situationStore'
-import { ATTACKED_TARGETS } from '@/data/mapLocations'
+import {
+ ATTACKED_TARGETS,
+ ALLIED_STRIKE_LOCATIONS,
+ LINCOLN_COORDS,
+ LINCOLN_STRIKE_TARGETS,
+ FORD_COORDS,
+ FORD_STRIKE_TARGETS,
+ ISRAEL_STRIKE_SOURCE,
+ ISRAEL_STRIKE_TARGETS,
+} from '@/data/mapLocations'
const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN || ''
-const DEFAULT_VIEW = { longitude: 52.5, latitude: 26.5, zoom: 5.2 }
+// 相关区域 bbox:伊朗、以色列、胡塞区 (minLng, minLat, maxLng, maxLat),覆盖红蓝区域
+const THEATER_BBOX = [22, 11, 64, 41] as const
+const THEATER_CENTER = {
+ longitude: (THEATER_BBOX[0] + THEATER_BBOX[2]) / 2,
+ latitude: (THEATER_BBOX[1] + THEATER_BBOX[3]) / 2,
+}
+const DEFAULT_VIEW = { ...THEATER_CENTER, zoom: 4.2 }
const COUNTRIES_GEOJSON =
'https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_110m_admin_0_countries.geojson'
+// 胡塞武装失控区 [lng, lat] 闭环
+const HOUTHI_POLYGON: [number, number][] = [
+ [42.7, 15.8],
+ [43.3, 16.5],
+ [45.1, 17.2],
+ [45.8, 15.1],
+ [44.2, 13.5],
+ [42.7, 15.8],
+]
+
const IRAN_ADMIN = 'Iran'
const ALLIES_ADMIN = [
'Qatar',
@@ -33,14 +58,39 @@ const ALLIES_ADMIN = [
// 伊朗攻击源 德黑兰 [lng, lat]
const TEHRAN_SOURCE: [number, number] = [51.389, 35.6892]
+/** 二次贝塞尔曲线路径,更平滑的弧线 height 控制弧高 */
function parabolaPath(
start: [number, number],
end: [number, number],
- height = 2
+ height = 3
): [number, number][] {
- const midLng = (start[0] + end[0]) / 2
- const midLat = (start[1] + end[1]) / 2 + height
- return [start, [midLng, midLat], end]
+ const ctrl: [number, number] = [
+ (start[0] + end[0]) / 2,
+ (start[1] + end[1]) / 2 + height,
+ ]
+ // 生成多段点使曲线更平滑
+ const pts: [number, number][] = [start]
+ for (let i = 1; i < 12; i++) {
+ const s = i / 12
+ const t = 1 - s
+ const x = t * t * start[0] + 2 * t * s * ctrl[0] + s * s * end[0]
+ const y = t * t * start[1] + 2 * t * s * ctrl[1] + s * s * end[1]
+ pts.push([x, y])
+ }
+ pts.push(end)
+ return pts
+}
+
+/** 沿路径插值,t ∈ [0,1],支持多点路径 */
+function interpolateOnPath(path: [number, number][], t: number): [number, number] {
+ if (t <= 0) return path[0]
+ if (t >= 1) return path[path.length - 1]
+ const n = path.length - 1
+ const seg = Math.min(Math.floor(t * n), n - 1)
+ const u = (t * n) - seg
+ const a = path[seg]
+ const b = path[seg + 1]
+ return [a[0] + u * (b[0] - a[0]), a[1] + u * (b[1] - a[1])]
}
type BaseStatus = 'operational' | 'damaged' | 'attacked'
@@ -69,10 +119,16 @@ function toFeature(loc: KeyLoc, side: 'us' | 'iran', status?: BaseStatus) {
}
}
+const FLIGHT_DURATION_MS = 2500 // 光点飞行单程时间
+
export function WarMap() {
const mapRef = useRef
(null)
const animRef = useRef(0)
const startRef = useRef(0)
+ const attackPathsRef = useRef<[number, number][][]>([])
+ const lincolnPathsRef = useRef<[number, number][][]>([])
+ const fordPathsRef = useRef<[number, number][][]>([])
+ const israelPathsRef = useRef<[number, number][][]>([])
const { situation } = useSituationStore()
const { usForces, iranForces } = situation
@@ -113,20 +169,74 @@ export function WarMap() {
}
}, [usForces.keyLocations, iranForces.keyLocations])
- // 德黑兰到 27 个被袭目标的攻击曲线
+ // 德黑兰到 27 个被袭目标的攻击路径(静态线条)
+ const attackPaths = useMemo(
+ () => ATTACKED_TARGETS.map((target) => parabolaPath(TEHRAN_SOURCE, target as [number, number])),
+ []
+ )
+
+ attackPathsRef.current = attackPaths
+
+ const lincolnPaths = useMemo(
+ () => LINCOLN_STRIKE_TARGETS.map((t) => parabolaPath(LINCOLN_COORDS, t)),
+ []
+ )
+ const fordPaths = useMemo(
+ () => FORD_STRIKE_TARGETS.map((t) => parabolaPath(FORD_COORDS, t)),
+ []
+ )
+ const israelPaths = useMemo(
+ () => ISRAEL_STRIKE_TARGETS.map((t) => parabolaPath(ISRAEL_STRIKE_SOURCE, t)),
+ []
+ )
+ lincolnPathsRef.current = lincolnPaths
+ fordPathsRef.current = fordPaths
+ israelPathsRef.current = israelPaths
+
+ const lincolnLinesGeoJson = useMemo(
+ () => ({
+ type: 'FeatureCollection' as const,
+ features: lincolnPaths.map((coords) => ({
+ type: 'Feature' as const,
+ properties: {},
+ geometry: { type: 'LineString' as const, coordinates: coords },
+ })),
+ }),
+ [lincolnPaths]
+ )
+ const fordLinesGeoJson = useMemo(
+ () => ({
+ type: 'FeatureCollection' as const,
+ features: fordPaths.map((coords) => ({
+ type: 'Feature' as const,
+ properties: {},
+ geometry: { type: 'LineString' as const, coordinates: coords },
+ })),
+ }),
+ [fordPaths]
+ )
+ const israelLinesGeoJson = useMemo(
+ () => ({
+ type: 'FeatureCollection' as const,
+ features: israelPaths.map((coords) => ({
+ type: 'Feature' as const,
+ properties: {},
+ geometry: { type: 'LineString' as const, coordinates: coords },
+ })),
+ }),
+ [israelPaths]
+ )
+
const attackLinesGeoJson = useMemo(
() => ({
type: 'FeatureCollection' as const,
- features: ATTACKED_TARGETS.map((target) => ({
+ features: attackPaths.map((coords) => ({
type: 'Feature' as const,
properties: {},
- geometry: {
- type: 'LineString' as const,
- coordinates: parabolaPath(TEHRAN_SOURCE, target as [number, number]),
- },
+ geometry: { type: 'LineString' as const, coordinates: coords },
})),
}),
- []
+ [attackPaths]
)
const hideNonBelligerentLabels = (map: MapboxMap) => {
@@ -147,18 +257,27 @@ export function WarMap() {
}
}
- useEffect(() => {
- const map = mapRef.current?.getMap()
- if (!map) return
+ const initAnimation = useRef<(map: MapboxMap) => void>(null!)
+ initAnimation.current = (map: MapboxMap) => {
startRef.current = performance.now()
const tick = (t: number) => {
const elapsed = t - startRef.current
try {
- if (map.getLayer('attack-lines')) {
- const offset = (elapsed / 16) * 0.8
- map.setPaintProperty('attack-lines', 'line-dasharray', [2, 2])
- map.setPaintProperty('attack-lines', 'line-dash-offset', -offset)
+ // 光点从起点飞向目标的循环动画
+ const src = map.getSource('attack-dots') as { setData: (d: GeoJSON.FeatureCollection) => void } | undefined
+ const paths = attackPathsRef.current
+ if (src && paths.length > 0) {
+ const features: GeoJSON.Feature[] = paths.map((path, i) => {
+ const progress = ((elapsed / FLIGHT_DURATION_MS + i / paths.length) % 1)
+ const coord = interpolateOnPath(path, progress)
+ return {
+ type: 'Feature' as const,
+ properties: {},
+ geometry: { type: 'Point' as const, coordinates: coord },
+ }
+ })
+ src.setData({ type: 'FeatureCollection', features })
}
// damaged: 橙色闪烁 opacity 0.5 ~ 1, 约 1s 周期
if (map.getLayer('points-damaged')) {
@@ -174,21 +293,100 @@ export function WarMap() {
map.setPaintProperty('points-attacked-pulse', 'circle-radius', r)
map.setPaintProperty('points-attacked-pulse', 'circle-opacity', opacity)
}
+ // 林肯号打击伊朗:蓝色光点
+ const lincolnSrc = map.getSource('allied-strike-dots-lincoln') as
+ | { setData: (d: GeoJSON.FeatureCollection) => void }
+ | undefined
+ const lincolnPaths = lincolnPathsRef.current
+ if (lincolnSrc && lincolnPaths.length > 0) {
+ const features: GeoJSON.Feature[] = lincolnPaths.map(
+ (path, i) => {
+ const progress =
+ (elapsed / FLIGHT_DURATION_MS + 0.5 + i / Math.max(lincolnPaths.length, 1)) % 1
+ const coord = interpolateOnPath(path, progress)
+ return {
+ type: 'Feature' as const,
+ properties: {},
+ geometry: { type: 'Point' as const, coordinates: coord },
+ }
+ }
+ )
+ lincolnSrc.setData({ type: 'FeatureCollection', features })
+ }
+ // 福特号打击伊朗:青色光点
+ const fordSrc = map.getSource('allied-strike-dots-ford') as
+ | { setData: (d: GeoJSON.FeatureCollection) => void }
+ | undefined
+ const fordPaths = fordPathsRef.current
+ if (fordSrc && fordPaths.length > 0) {
+ const features: GeoJSON.Feature[] = fordPaths.map(
+ (path, i) => {
+ const progress =
+ (elapsed / FLIGHT_DURATION_MS + 0.3 + i / Math.max(fordPaths.length, 1)) % 1
+ const coord = interpolateOnPath(path, progress)
+ return {
+ type: 'Feature' as const,
+ properties: {},
+ geometry: { type: 'Point' as const, coordinates: coord },
+ }
+ }
+ )
+ fordSrc.setData({ type: 'FeatureCollection', features })
+ }
+ // 以色列打击伊朗:浅青/白色光点
+ const israelSrc = map.getSource('allied-strike-dots-israel') as
+ | { setData: (d: GeoJSON.FeatureCollection) => void }
+ | undefined
+ const israelPaths = israelPathsRef.current
+ if (israelSrc && israelPaths.length > 0) {
+ const features: GeoJSON.Feature[] = israelPaths.map(
+ (path, i) => {
+ const progress =
+ (elapsed / FLIGHT_DURATION_MS + 0.1 + i / Math.max(israelPaths.length, 1)) % 1
+ const coord = interpolateOnPath(path, progress)
+ return {
+ type: 'Feature' as const,
+ properties: {},
+ geometry: { type: 'Point' as const, coordinates: coord },
+ }
+ }
+ )
+ israelSrc.setData({ type: 'FeatureCollection', features })
+ }
+ // 伊朗被打击目标:蓝色脉冲 (2s 周期)
+ if (map.getLayer('allied-strike-targets-pulse')) {
+ const cycle = 2000
+ const phase = (elapsed % cycle) / cycle
+ const r = 35 * phase
+ const opacity = Math.max(0, 1 - phase * 1.2)
+ map.setPaintProperty('allied-strike-targets-pulse', 'circle-radius', r)
+ map.setPaintProperty('allied-strike-targets-pulse', 'circle-opacity', opacity)
+ }
} catch (_) {}
animRef.current = requestAnimationFrame(tick)
}
const start = () => {
hideNonBelligerentLabels(map)
- if (map.getLayer('attack-lines')) {
+ map.fitBounds(
+ [[THEATER_BBOX[0], THEATER_BBOX[1]], [THEATER_BBOX[2], THEATER_BBOX[3]]],
+ { padding: 40, maxZoom: 5, duration: 0 }
+ )
+ const hasAnim =
+ (map.getSource('attack-dots') && attackPathsRef.current.length > 0) ||
+ (map.getSource('allied-strike-dots-lincoln') && lincolnPathsRef.current.length > 0) ||
+ (map.getSource('allied-strike-dots-ford') && fordPathsRef.current.length > 0) ||
+ (map.getSource('allied-strike-dots-israel') && israelPathsRef.current.length > 0)
+ if (hasAnim) {
animRef.current = requestAnimationFrame(tick)
} else {
animRef.current = requestAnimationFrame(start)
}
}
- if (map.isStyleLoaded()) start()
- else map.once('load', start)
+ start()
+ }
+ useEffect(() => {
return () => cancelAnimationFrame(animRef.current)
}, [])
@@ -215,6 +413,33 @@ export function WarMap() {
return (
+ {/* 图例 - 随容器自适应,避免遮挡 */}
+
+
+ 基地
+
+
+ 遭袭
+
+
+ 海军
+
+
+ 伊朗
+
+
+ 胡塞武装
+
+
+ 林肯打击
+
+
+ 福特打击
+
+
+ 以色列打击
+
+