Enhance ChinaMap component by improving mode handling and background image rendering; introduce dynamic surface paint configurations for local and world modes; refine map styling and texture application for better visual fidelity.

main
Swanky 1 day ago
parent c9503210c5
commit cdb7ba045e
  1. 331
      src/components/ChinaMap.vue

@ -59,9 +59,18 @@
</div> </div>
</div> </div>
<div class="map-stage" :class="{ 'map-stage--local': currentMode === 'local' }"> <div class="map-stage" :class="{ 'map-stage--local': mapStageLocal }">
<div class="map-tilt-layer"> <!-- 红原模式整页 dashboard-bg 已铺底此处不重复显示避免倾斜后与背景错位 -->
<img class="map-world-bg" :src="mapWorldBgSrc" alt="" /> <img
v-show="currentMode !== 'local'"
class="map-world-bg"
:src="mapWorldBgSrc"
alt=""
/>
<div
class="map-tilt-layer"
:class="{ 'map-tilt-layer--switching': isMapContentSwitching }"
>
<div ref="chartRef" class="chart-container"></div> <div ref="chartRef" class="chart-container"></div>
</div> </div>
<div class="map-legend" aria-hidden="true"> <div class="map-legend" aria-hidden="true">
@ -100,6 +109,10 @@ const chartRef = ref(null)
const sourceChartRef = ref(null) const sourceChartRef = ref(null)
const salesChartRef = ref(null) const salesChartRef = ref(null)
const currentMode = ref('outflow') const currentMode = ref('outflow')
const isMapContentSwitching = ref(false)
const mapStageLocal = computed(() => (
currentMode.value === 'local' && !isMapContentSwitching.value
))
let chartInstance = null let chartInstance = null
let sourceChartInstance = null let sourceChartInstance = null
let salesChartInstance = null let salesChartInstance = null
@ -113,6 +126,7 @@ let unitedMapKey = ''
const mapBgImageCache = new Map() const mapBgImageCache = new Map()
const mapBgImageLoading = new Map() const mapBgImageLoading = new Map()
let mapSurfaceTexture = null let mapSurfaceTexture = null
let mapSurfaceTextureDataUrl = ''
let mapSurfaceTextureKey = '' let mapSurfaceTextureKey = ''
// //
@ -150,26 +164,27 @@ const MAP_SHADOW_OFFSET_FAR = 1.85
const MAP_SHADOW_OFFSET_MID = 1.2 const MAP_SHADOW_OFFSET_MID = 1.2
const MAP_SHADOW_OFFSET_NEAR = 0.62 const MAP_SHADOW_OFFSET_NEAR = 0.62
const MAP_SHADOW_OFFSET_LNG = -0.42 const MAP_SHADOW_OFFSET_LNG = -0.42
const MAP_SHADOW_OFFSET_FAR_LOCAL = 5 const MAP_SHADOW_OFFSET_FAR_LOCAL = 2.6
const MAP_SHADOW_OFFSET_MID_LOCAL = 3.5 const MAP_SHADOW_OFFSET_MID_LOCAL = 1.8
const MAP_SHADOW_OFFSET_NEAR_LOCAL = 2.2 const MAP_SHADOW_OFFSET_NEAR_LOCAL = 1.0
const MAP_SHADOW_OFFSET_LNG_LOCAL = -2 const MAP_SHADOW_OFFSET_LNG_LOCAL = -1.5
const MAP_WIREFRAME_OFFSET = -0.72 const MAP_WIREFRAME_OFFSET = -0.72
const MAP_WIREFRAME_OFFSET_LNG = 0.1 const MAP_WIREFRAME_OFFSET_LNG = 0.1
const MAP_WIREFRAME_OFFSET_LOCAL = -3.5 const MAP_WIREFRAME_OFFSET_LOCAL = -1.8
const MAP_WIREFRAME_OFFSET_LNG_LOCAL = 0.6 const MAP_WIREFRAME_OFFSET_LNG_LOCAL = 0.6
// // 稿
const MAP_WIREFRAME_COLOR = '#C8E8F4' const MAP_WIREFRAME_COLOR = '#B8ECF6'
const MAP_WIREFRAME_HALO_LAYERS = [ const MAP_WIREFRAME_HALO_LAYERS = [
{ width: 12, color: 'rgba(148, 208, 228, 0.08)' }, { width: 10, color: 'rgba(128, 208, 228, 0.06)' },
{ width: 8.5, color: 'rgba(162, 220, 238, 0.15)' }, { width: 7, color: 'rgba(148, 218, 236, 0.12)' },
{ width: 5.8, color: 'rgba(176, 232, 246, 0.25)' } { width: 4.6, color: 'rgba(168, 232, 246, 0.22)' }
] ]
const MAP_WIREFRAME_CORE_WIDTH = 3.1 const MAP_WIREFRAME_CORE_WIDTH = 2.6
const MAP_WIREFRAME_CORE_SHADOW_BLUR = 8 const MAP_WIREFRAME_CORE_SHADOW_BLUR = 7
const MAP_WIREFRAME_CORE_SHADOW_COLOR = 'rgba(120, 198, 228, 0.24)' const MAP_WIREFRAME_CORE_SHADOW_COLOR = 'rgba(110, 200, 228, 0.22)'
const MAP_INNER_BORDER = 'rgba(4, 32, 52, 0.28)' const MAP_INNER_BORDER = 'rgba(1, 12, 22, 0.82)'
const MAP_INNER_BORDER_WIDTH = 0.45 const MAP_INNER_BORDER_WIDTH = 0.95
const MAP_INNER_BORDER_WIDTH_LOCAL = 1.2
const MAP_SHADOW_FILL_FAR = '#010204' const MAP_SHADOW_FILL_FAR = '#010204'
const MAP_SHADOW_FILL_MID = '#020408' const MAP_SHADOW_FILL_MID = '#020408'
const MAP_SHADOW_FILL_NEAR = '#030a12' const MAP_SHADOW_FILL_NEAR = '#030a12'
@ -183,8 +198,141 @@ const MAP_BG_CROP_CHINA = { x: 1160, y: 108, width: 2000, height: 828 }
const MAP_BG_CROP_LOCAL = { x: 1860, y: 332, width: 820, height: 520 } const MAP_BG_CROP_LOCAL = { x: 1860, y: 332, width: 820, height: 520 }
const MAP_SURFACE_TEXTURE_WIDTH = 1536 const MAP_SURFACE_TEXTURE_WIDTH = 1536
const MAP_SURFACE_TEXTURE_HEIGHT = 1152 const MAP_SURFACE_TEXTURE_HEIGHT = 1152
const MAP_SURFACE_FALLBACK = 'rgb(92, 202, 206)' const MAP_SURFACE_FALLBACK = 'rgb(49, 183, 183)'
const MAP_SURFACE_COLOR = 'rgba(0, 0, 0, 0)' const MAP_SURFACE_COLOR = 'rgba(0, 0, 0, 0)'
const MAP_SURFACE_PAINT_VERSION = 5
// color + overlay
const WORLD_SURFACE_PAINT = {
brightness: 1.22,
contrast: 1.26,
lift: {
enabled: true,
composite: 'screen',
color: 'rgba(220, 248, 244, 0.16)'
},
tint: {
composite: 'color',
mode: 'linear',
linearX0: 0,
linearY0: 0.5,
linearX1: 1,
linearY1: 0.5,
stops: [
{ stop: 0, color: 'rgb(85, 212, 205)' },
{ stop: 0.45, color: 'rgb(52, 175, 188)' },
{ stop: 1, color: 'rgb(14, 131, 184)' }
]
},
satBoost: {
enabled: true,
composite: 'overlay',
mode: 'linear',
linearX0: 0,
linearY0: 0.5,
linearX1: 1,
linearY1: 0.5,
stops: [
{ stop: 0, color: 'rgba(145, 245, 236, 0.22)' },
{ stop: 0.5, color: 'rgba(80, 205, 208, 0.12)' },
{ stop: 1, color: 'rgba(18, 135, 180, 0.16)' }
]
},
depth: { enabled: false },
softLight: { enabled: true, color: 'rgba(125, 222, 215, 0.09)' },
glow: {
enabled: true,
centerX: 0.34,
centerY: 0.44,
radius: 0.64,
stops: [
{ stop: 0, color: 'rgba(228, 252, 248, 0.22)' },
{ stop: 0.48, color: 'rgba(185, 238, 232, 0.08)' },
{ stop: 1, color: 'rgba(0, 0, 0, 0)' }
]
}
}
// 西稿
const LOCAL_SURFACE_PAINT = {
brightness: 1.22,
contrast: 1.28,
lift: {
enabled: true,
composite: 'screen',
color: 'rgba(232, 252, 248, 0.18)'
},
tint: {
composite: 'color',
mode: 'radial',
centerX: 0.3,
centerY: 0.47,
radius: 0.78,
stops: [
{ stop: 0, color: 'rgb(188, 246, 238)' },
{ stop: 0.28, color: 'rgb(82, 200, 205)' },
{ stop: 0.62, color: 'rgb(35, 150, 178)' },
{ stop: 1, color: 'rgb(14, 131, 184)' }
]
},
satBoost: {
enabled: true,
composite: 'overlay',
mode: 'radial',
centerX: 0.3,
centerY: 0.47,
radius: 0.74,
stops: [
{ stop: 0, color: 'rgba(225, 252, 246, 0.34)' },
{ stop: 0.35, color: 'rgba(98, 218, 212, 0.17)' },
{ stop: 1, color: 'rgba(14, 131, 184, 0.22)' }
]
},
depth: { enabled: false },
softLight: { enabled: true, color: 'rgba(112, 220, 212, 0.1)' },
glow: {
enabled: true,
centerX: 0.28,
centerY: 0.46,
radius: 0.5,
stops: [
{ stop: 0, color: 'rgba(255, 255, 253, 0.36)' },
{ stop: 0.5, color: 'rgba(205, 242, 236, 0.11)' },
{ stop: 1, color: 'rgba(0, 0, 0, 0)' }
]
}
}
const getMapSurfacePaintConfig = () => (
currentMode.value === 'local' ? LOCAL_SURFACE_PAINT : WORLD_SURFACE_PAINT
)
const createSurfacePaintGradient = (ctx, width, height, layer) => {
if (layer.mode === 'linear') {
return ctx.createLinearGradient(
width * layer.linearX0,
height * layer.linearY0,
width * layer.linearX1,
height * layer.linearY1
)
}
return ctx.createRadialGradient(
width * layer.centerX,
height * layer.centerY,
0,
width * layer.centerX,
height * layer.centerY,
Math.max(width, height) * layer.radius
)
}
const applySurfacePaintLayer = (ctx, width, height, layer) => {
const gradient = createSurfacePaintGradient(ctx, width, height, layer)
layer.stops.forEach(({ stop, color }) => gradient.addColorStop(stop, color))
ctx.globalCompositeOperation = layer.composite
ctx.fillStyle = gradient
ctx.fillRect(0, 0, width, height)
}
const getMapBgImageSrc = (mode = currentMode.value) => ( const getMapBgImageSrc = (mode = currentMode.value) => (
mode === 'local' ? MAP_BG_IMAGE_LOCAL : MAP_BG_IMAGE_WORLD mode === 'local' ? MAP_BG_IMAGE_LOCAL : MAP_BG_IMAGE_WORLD
@ -783,10 +931,11 @@ const loadMapBgImage = (mode = currentMode.value) => {
} }
const paintMapSurfaceTexture = (ctx, width, height, image, crop) => { const paintMapSurfaceTexture = (ctx, width, height, image, crop) => {
const paint = getMapSurfacePaintConfig()
ctx.clearRect(0, 0, width, height) ctx.clearRect(0, 0, width, height)
// 1. // 1. /
ctx.filter = 'brightness(1.28) contrast(1.18)' ctx.filter = `brightness(${paint.brightness}) contrast(${paint.contrast})`
ctx.drawImage( ctx.drawImage(
image, image,
crop.x, crop.x,
@ -800,43 +949,46 @@ const paintMapSurfaceTexture = (ctx, width, height, image, crop) => {
) )
ctx.filter = 'none' ctx.filter = 'none'
// 2. overlay G/B // 1.5 color
ctx.globalCompositeOperation = 'overlay' if (paint.lift?.enabled) {
const tintGradient = ctx.createRadialGradient( ctx.globalCompositeOperation = paint.lift.composite || 'screen'
width * 0.44, ctx.fillStyle = paint.lift.color
height * 0.48,
0,
width * 0.44,
height * 0.48,
Math.max(width, height) * 0.86
)
tintGradient.addColorStop(0, 'rgba(158, 236, 228, 0.94)')
tintGradient.addColorStop(0.42, 'rgba(108, 212, 214, 0.90)')
tintGradient.addColorStop(0.78, 'rgba(82, 194, 200, 0.86)')
tintGradient.addColorStop(1, 'rgba(68, 180, 188, 0.82)')
ctx.fillStyle = tintGradient
ctx.fillRect(0, 0, width, height) ctx.fillRect(0, 0, width, height)
}
// 2. color
applySurfacePaintLayer(ctx, width, height, paint.tint)
// 2.5 overlay overlay
if (paint.satBoost?.enabled) {
applySurfacePaintLayer(ctx, width, height, paint.satBoost)
}
// 3. soft-light // 3. /
if (paint.depth?.enabled) {
applySurfacePaintLayer(ctx, width, height, paint.depth)
}
// 4. soft-light
if (paint.softLight?.enabled) {
ctx.globalCompositeOperation = 'soft-light' ctx.globalCompositeOperation = 'soft-light'
ctx.fillStyle = 'rgba(118, 218, 214, 0.36)' ctx.fillStyle = paint.softLight.color
ctx.fillRect(0, 0, width, height) ctx.fillRect(0, 0, width, height)
}
// 4. screen // 5.
if (paint.glow?.enabled) {
ctx.globalCompositeOperation = 'screen' ctx.globalCompositeOperation = 'screen'
const glowGradient = ctx.createRadialGradient( const glowGradient = createSurfacePaintGradient(ctx, width, height, {
width * 0.43, mode: 'radial',
height * 0.46, centerX: paint.glow.centerX,
0, centerY: paint.glow.centerY,
width * 0.43, radius: paint.glow.radius
height * 0.46, })
Math.max(width, height) * 0.58 paint.glow.stops.forEach(({ stop, color }) => glowGradient.addColorStop(stop, color))
)
glowGradient.addColorStop(0, 'rgba(205, 248, 242, 0.34)')
glowGradient.addColorStop(0.55, 'rgba(175, 235, 228, 0.14)')
glowGradient.addColorStop(1, 'rgba(175, 235, 228, 0)')
ctx.fillStyle = glowGradient ctx.fillStyle = glowGradient
ctx.fillRect(0, 0, width, height) ctx.fillRect(0, 0, width, height)
}
ctx.globalCompositeOperation = 'source-over' ctx.globalCompositeOperation = 'source-over'
} }
@ -856,23 +1008,39 @@ const createMapSurfaceTexture = (image) => {
return canvas return canvas
} }
const ensureMapSurfaceTexture = async () => { const invalidateMapSurfaceTexture = () => {
const key = currentMode.value mapSurfaceTexture = null
if (mapSurfaceTexture && mapSurfaceTextureKey === key) { mapSurfaceTextureDataUrl = ''
mapSurfaceTextureKey = ''
}
const syncMapSurfaceTextureOutput = (canvas) => {
mapSurfaceTexture = canvas
mapSurfaceTextureDataUrl = canvas ? canvas.toDataURL('image/png') : ''
return mapSurfaceTexture
}
const ensureMapSurfaceTexture = async (force = false) => {
const key = `${currentMode.value}@v${MAP_SURFACE_PAINT_VERSION}`
if (!force && mapSurfaceTexture && mapSurfaceTextureKey === key) {
return mapSurfaceTexture return mapSurfaceTexture
} }
const image = await loadMapBgImage(key) const image = await loadMapBgImage(key)
mapSurfaceTexture = createMapSurfaceTexture(image) const canvas = createMapSurfaceTexture(image)
if (!canvas) {
return null
}
syncMapSurfaceTextureOutput(canvas)
mapSurfaceTextureKey = key mapSurfaceTextureKey = key
return mapSurfaceTexture return mapSurfaceTexture
} }
const getMapSurfaceAreaStyle = () => { const getMapSurfaceAreaStyle = () => {
if (mapSurfaceTexture) { if (mapSurfaceTextureDataUrl) {
return { return {
areaColor: { areaColor: {
image: mapSurfaceTexture, image: mapSurfaceTextureDataUrl,
repeat: 'no-repeat' repeat: 'no-repeat'
} }
} }
@ -887,7 +1055,8 @@ const refreshMapSurfaceTexture = async () => {
} }
try { try {
await ensureMapSurfaceTexture() invalidateMapSurfaceTexture()
await ensureMapSurfaceTexture(true)
const mapBounds = calculateMapBounds() const mapBounds = calculateMapBounds()
const mapName = getMapName(currentMode.value) const mapName = getMapName(currentMode.value)
const unitedMapName = registerUnitedMap(currentMode.value) const unitedMapName = registerUnitedMap(currentMode.value)
@ -1252,7 +1421,9 @@ const getChartOption = () => {
itemStyle: { itemStyle: {
areaColor: MAP_SURFACE_COLOR, areaColor: MAP_SURFACE_COLOR,
borderColor: MAP_INNER_BORDER, borderColor: MAP_INNER_BORDER,
borderWidth: MAP_INNER_BORDER_WIDTH borderWidth: currentMode.value === 'local'
? MAP_INNER_BORDER_WIDTH_LOCAL
: MAP_INNER_BORDER_WIDTH
}, },
label: { label: {
show: true, show: true,
@ -1496,19 +1667,27 @@ const updateChart = async (needReloadMap = false) => {
} }
} }
// //
watch(currentMode, (newMode, oldMode) => { watch(currentMode, async (newMode, oldMode) => {
emit('mode-change', newMode) emit('mode-change', newMode)
nextTick(async () => {
// if (oldMode === undefined) {
const oldMapName = getMapName(oldMode) return
const newMapName = getMapName(newMode) }
const needReloadMap = oldMapName !== newMapName
const needReloadMap = getMapName(oldMode) !== getMapName(newMode)
if (needReloadMap) {
isMapContentSwitching.value = true
}
await updateChart(needReloadMap) await updateChart(needReloadMap)
setTimeout(handleChartResize, 580) await nextTick()
}) isMapContentSwitching.value = false
}, { immediate: true }) handleChartResize()
if (needReloadMap) {
requestAnimationFrame(() => handleChartResize())
}
}, { flush: 'pre', immediate: true })
onMounted(() => { onMounted(() => {
initChart() initChart()
@ -1636,28 +1815,28 @@ onUnmounted(() => {
inset: 0; inset: 0;
transform-style: preserve-3d; transform-style: preserve-3d;
transform-origin: 50% 55%; transform-origin: 50% 55%;
transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1); will-change: transform, opacity;
will-change: transform;
backface-visibility: hidden; backface-visibility: hidden;
} }
.map-tilt-layer--switching {
opacity: 0;
pointer-events: none;
}
.map-stage--local .map-tilt-layer { .map-stage--local .map-tilt-layer {
transform: rotateX(22deg) translate3d(0, -6%, 22px) scale(0.92); transform: rotateX(33deg) translate3d(0, -9%, 22px) scale(0.88);
transition: transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
} }
.map-stage--local .chart-container { .map-stage--local .chart-container {
transform: none; transform: none;
} }
.map-tilt-layer .map-world-bg,
.map-tilt-layer .chart-container { .map-tilt-layer .chart-container {
transform-style: preserve-3d; transform-style: preserve-3d;
} }
.map-stage--local .map-world-bg {
transform: translate3d(-50%, -50%, 0);
}
.map-world-bg { .map-world-bg {
position: absolute; position: absolute;
width: var(--screen-width, 5120px); width: var(--screen-width, 5120px);

Loading…
Cancel
Save