mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-05-14 19:25:37 +08:00
style(pfd): refine colors, narrow heading tape, and rework VS scale
This commit is contained in:
@@ -16,6 +16,8 @@ const pitchScale = computed(() => props.size / 60)
|
|||||||
const clipId = computed(() => `att-clip-${uid}`)
|
const clipId = computed(() => `att-clip-${uid}`)
|
||||||
const skyGradId = computed(() => `att-sky-${uid}`)
|
const skyGradId = computed(() => `att-sky-${uid}`)
|
||||||
const groundGradId = computed(() => `att-ground-${uid}`)
|
const groundGradId = computed(() => `att-ground-${uid}`)
|
||||||
|
const WHITE = '#f4f6fb'
|
||||||
|
const YELLOW = '#ffe100'
|
||||||
|
|
||||||
const pitchMarks = computed(() => {
|
const pitchMarks = computed(() => {
|
||||||
const marks: Array<{ deg: number; y: number; isLabel: boolean; width: number }> = []
|
const marks: Array<{ deg: number; y: number; isLabel: boolean; width: number }> = []
|
||||||
@@ -82,12 +84,12 @@ const dotRadius = computed(() => 4)
|
|||||||
<circle :cx="cx" :cy="cy" :r="size * 0.46" />
|
<circle :cx="cx" :cy="cy" :r="size * 0.46" />
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<linearGradient :id="skyGradId" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient :id="skyGradId" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="0%" stop-color="#26aef0" />
|
<stop offset="0%" stop-color="#22a7eb" />
|
||||||
<stop offset="100%" stop-color="#1e9ddf" />
|
<stop offset="100%" stop-color="#22a7eb" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient :id="groundGradId" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient :id="groundGradId" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="0%" stop-color="#b56517" />
|
<stop offset="0%" stop-color="#b26113" />
|
||||||
<stop offset="100%" stop-color="#9b5312" />
|
<stop offset="100%" stop-color="#b26113" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
@@ -117,7 +119,7 @@ const dotRadius = computed(() => 4)
|
|||||||
:y1="0"
|
:y1="0"
|
||||||
:x2="size * 2"
|
:x2="size * 2"
|
||||||
:y2="0"
|
:y2="0"
|
||||||
stroke="white"
|
:stroke="WHITE"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -128,14 +130,14 @@ const dotRadius = computed(() => 4)
|
|||||||
:y1="mark.y"
|
:y1="mark.y"
|
||||||
:x2="mark.width / 2"
|
:x2="mark.width / 2"
|
||||||
:y2="mark.y"
|
:y2="mark.y"
|
||||||
stroke="white"
|
:stroke="WHITE"
|
||||||
:stroke-width="mark.isLabel ? 1.5 : 1"
|
:stroke-width="mark.isLabel ? 1.5 : 1"
|
||||||
/>
|
/>
|
||||||
<template v-if="mark.isLabel">
|
<template v-if="mark.isLabel">
|
||||||
<text
|
<text
|
||||||
:x="-mark.width / 2 - 6"
|
:x="-mark.width / 2 - 6"
|
||||||
:y="mark.y + 4"
|
:y="mark.y + 4"
|
||||||
fill="#f4f6fb"
|
:fill="WHITE"
|
||||||
font-size="11"
|
font-size="11"
|
||||||
text-anchor="end"
|
text-anchor="end"
|
||||||
font-family="monospace"
|
font-family="monospace"
|
||||||
@@ -145,7 +147,7 @@ const dotRadius = computed(() => 4)
|
|||||||
<text
|
<text
|
||||||
:x="mark.width / 2 + 6"
|
:x="mark.width / 2 + 6"
|
||||||
:y="mark.y + 4"
|
:y="mark.y + 4"
|
||||||
fill="#f4f6fb"
|
:fill="WHITE"
|
||||||
font-size="11"
|
font-size="11"
|
||||||
text-anchor="start"
|
text-anchor="start"
|
||||||
font-family="monospace"
|
font-family="monospace"
|
||||||
@@ -165,21 +167,21 @@ const dotRadius = computed(() => 4)
|
|||||||
:y1="tick.y1"
|
:y1="tick.y1"
|
||||||
:x2="tick.x2"
|
:x2="tick.x2"
|
||||||
:y2="tick.y2"
|
:y2="tick.y2"
|
||||||
stroke="#f4f6fb"
|
:stroke="WHITE"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Zero-bank reference triangle (fixed, top center) -->
|
<!-- Zero-bank reference triangle (fixed, top center) -->
|
||||||
<polygon
|
<polygon
|
||||||
:points="`${cx},${cy - size * 0.42} ${cx - 6},${cy - size * 0.42 - 10} ${cx + 6},${cy - size * 0.42 - 10}`"
|
:points="`${cx},${cy - size * 0.42} ${cx - 6},${cy - size * 0.42 - 10} ${cx + 6},${cy - size * 0.42 - 10}`"
|
||||||
fill="#f4f6fb"
|
:fill="WHITE"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Bank pointer (rotates with bank) -->
|
<!-- Bank pointer (rotates with bank) -->
|
||||||
<polygon
|
<polygon
|
||||||
:points="bankPointer"
|
:points="bankPointer"
|
||||||
:transform="bankPointerTransform"
|
:transform="bankPointerTransform"
|
||||||
fill="#facc15"
|
:fill="YELLOW"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Fixed aircraft reference symbol -->
|
<!-- Fixed aircraft reference symbol -->
|
||||||
@@ -190,7 +192,7 @@ const dotRadius = computed(() => 4)
|
|||||||
:y="cy - wingThickness / 2"
|
:y="cy - wingThickness / 2"
|
||||||
:width="wingSpan"
|
:width="wingSpan"
|
||||||
:height="wingThickness"
|
:height="wingThickness"
|
||||||
fill="#fbbf24"
|
:fill="YELLOW"
|
||||||
rx="1"
|
rx="1"
|
||||||
/>
|
/>
|
||||||
<!-- Right wing -->
|
<!-- Right wing -->
|
||||||
@@ -199,7 +201,7 @@ const dotRadius = computed(() => 4)
|
|||||||
:y="cy - wingThickness / 2"
|
:y="cy - wingThickness / 2"
|
||||||
:width="wingSpan"
|
:width="wingSpan"
|
||||||
:height="wingThickness"
|
:height="wingThickness"
|
||||||
fill="#fbbf24"
|
:fill="YELLOW"
|
||||||
rx="1"
|
rx="1"
|
||||||
/>
|
/>
|
||||||
<!-- Center dot -->
|
<!-- Center dot -->
|
||||||
@@ -207,7 +209,7 @@ const dotRadius = computed(() => 4)
|
|||||||
:cx="cx"
|
:cx="cx"
|
||||||
:cy="cy"
|
:cy="cy"
|
||||||
:r="dotRadius"
|
:r="dotRadius"
|
||||||
fill="#fbbf24"
|
:fill="YELLOW"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -56,11 +56,11 @@ const vsPos = computed(() => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const headingPos = computed(() => ({
|
const headingPos = computed(() => ({
|
||||||
left: tapeWidth.value,
|
left: attitudePos.value.left - gap.value,
|
||||||
top: attSize.value + gap.value,
|
top: attSize.value + gap.value,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const headingWidth = computed(() => attSize.value + tapeWidth.value * 2)
|
const headingWidth = computed(() => attSize.value + gap.value * 2)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -4,44 +4,55 @@ const props = withDefaults(defineProps<{
|
|||||||
width?: number
|
width?: number
|
||||||
height?: number
|
height?: number
|
||||||
}>(), {
|
}>(), {
|
||||||
width: 40,
|
width: 48,
|
||||||
height: 300,
|
height: 300,
|
||||||
})
|
})
|
||||||
|
|
||||||
const uid = useId()
|
|
||||||
const clipId = computed(() => `vs-clip-${uid}`)
|
|
||||||
|
|
||||||
const centerY = computed(() => props.height / 2)
|
const centerY = computed(() => props.height / 2)
|
||||||
const scaleHeight = computed(() => props.height * 0.42)
|
const axisX = computed(() => props.width * 0.56)
|
||||||
|
const wingX = computed(() => props.width * 0.92)
|
||||||
|
const scaleSpan = computed(() => props.height * 0.43)
|
||||||
|
const WHITE = '#f4f6fb'
|
||||||
|
const GREEN = '#19e34a'
|
||||||
|
const majorMarks = [1000, 2000, 4000, 6000] as const
|
||||||
|
|
||||||
function vsToY(fpm: number): number {
|
function speedFraction(absFpm: number): number {
|
||||||
const normalized = Math.sign(fpm) * Math.sqrt(Math.abs(fpm) / 6000)
|
const f = Math.max(0, Math.min(6000, absFpm))
|
||||||
return centerY.value - normalized * scaleHeight.value
|
if (f <= 1000) return (f / 1000) * 0.24
|
||||||
|
if (f <= 2000) return 0.24 + ((f - 1000) / 1000) * 0.14
|
||||||
|
if (f <= 4000) return 0.38 + ((f - 2000) / 2000) * 0.28
|
||||||
|
return 0.66 + ((f - 4000) / 2000) * 0.24
|
||||||
}
|
}
|
||||||
|
|
||||||
const majorMarks = [-6000, -4000, -2000, -1000, 0, 1000, 2000, 4000, 6000]
|
function vsToY(fpm: number): number {
|
||||||
|
const sign = Math.sign(fpm)
|
||||||
|
if (sign === 0) return centerY.value
|
||||||
|
const frac = speedFraction(Math.abs(fpm))
|
||||||
|
return centerY.value - sign * frac * scaleSpan.value
|
||||||
|
}
|
||||||
|
|
||||||
const marks = computed(() => {
|
const markPositions = computed(() => {
|
||||||
return majorMarks.map(fpm => ({
|
return majorMarks.map((mark) => ({
|
||||||
fpm,
|
mark,
|
||||||
y: vsToY(fpm),
|
topY: vsToY(mark),
|
||||||
label: fpm === 0 ? '0' : (Math.abs(fpm) / 1000).toString(),
|
bottomY: vsToY(-mark),
|
||||||
|
label: String(mark / 1000),
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
const needleY = computed(() => {
|
const clampedVs = computed(() => Math.max(-6000, Math.min(6000, props.verticalSpeed)))
|
||||||
const clamped = Math.max(-6000, Math.min(6000, props.verticalSpeed))
|
const needleY = computed(() => vsToY(clampedVs.value))
|
||||||
return vsToY(clamped)
|
const showReadout = computed(() => Math.abs(props.verticalSpeed) >= 100)
|
||||||
})
|
const readoutText = computed(() => Math.round(Math.abs(props.verticalSpeed) / 100).toString().padStart(2, '0'))
|
||||||
|
|
||||||
const readoutText = computed(() => {
|
const channelShape = computed(() => {
|
||||||
const rounded = Math.round(props.verticalSpeed / 50) * 50
|
const left = props.width * 0.18
|
||||||
if (rounded === 0) return '0'
|
const right = props.width
|
||||||
return rounded > 0 ? `+${rounded}` : `${rounded}`
|
const shoulder = props.width * 0.86
|
||||||
|
const topInset = props.height * 0.11
|
||||||
|
const bottomInset = props.height * 0.89
|
||||||
|
return `${left},0 ${shoulder},0 ${right},${topInset} ${right},${bottomInset} ${shoulder},${props.height} ${left},${props.height}`
|
||||||
})
|
})
|
||||||
|
|
||||||
const bandY = computed(() => Math.min(centerY.value, needleY.value))
|
|
||||||
const bandHeight = computed(() => Math.abs(needleY.value - centerY.value))
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -51,84 +62,105 @@ const bandHeight = computed(() => Math.abs(needleY.value - centerY.value))
|
|||||||
:viewBox="`0 0 ${width} ${height}`"
|
:viewBox="`0 0 ${width} ${height}`"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<defs>
|
<!-- Background cutout -->
|
||||||
<clipPath :id="clipId">
|
<rect x="0" y="0" :width="width" :height="height" fill="#030712" />
|
||||||
<rect x="0" y="0" :width="width" :height="height" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Airbus-like VS channel -->
|
<!-- Airbus-style VS channel -->
|
||||||
<rect
|
<polygon
|
||||||
x="0"
|
:points="channelShape"
|
||||||
y="0"
|
fill="#8f9198"
|
||||||
:width="width"
|
stroke="#d2d4da"
|
||||||
:height="height"
|
stroke-width="0.8"
|
||||||
fill="#0b1126"
|
|
||||||
rx="1"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<g :clip-path="`url(#${clipId})`">
|
<!-- Main vertical axis -->
|
||||||
<!-- Scale line -->
|
<line
|
||||||
|
:x1="axisX"
|
||||||
|
y1="8"
|
||||||
|
:x2="axisX"
|
||||||
|
:y2="height - 8"
|
||||||
|
:stroke="WHITE"
|
||||||
|
stroke-width="1.1"
|
||||||
|
opacity="0.95"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Zero marker -->
|
||||||
|
<line
|
||||||
|
:x1="axisX - 12"
|
||||||
|
:y1="centerY"
|
||||||
|
:x2="axisX + 12"
|
||||||
|
:y2="centerY"
|
||||||
|
:stroke="WHITE"
|
||||||
|
stroke-width="1.5"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Major marks and labels -->
|
||||||
|
<g v-for="mark in markPositions" :key="mark.mark">
|
||||||
<line
|
<line
|
||||||
:x1="width * 0.3"
|
:x1="axisX - 10"
|
||||||
:y1="vsToY(6000)"
|
:y1="mark.topY"
|
||||||
:x2="width * 0.3"
|
:x2="axisX + 10"
|
||||||
:y2="vsToY(-6000)"
|
:y2="mark.topY"
|
||||||
stroke="white"
|
:stroke="WHITE"
|
||||||
stroke-width="1"
|
stroke-width="1.2"
|
||||||
opacity="0.4"
|
/>
|
||||||
|
<line
|
||||||
|
:x1="axisX - 10"
|
||||||
|
:y1="mark.bottomY"
|
||||||
|
:x2="axisX + 10"
|
||||||
|
:y2="mark.bottomY"
|
||||||
|
:stroke="WHITE"
|
||||||
|
stroke-width="1.2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Major marks -->
|
<text
|
||||||
<g v-for="mark in marks" :key="mark.fpm">
|
:x="axisX + 14"
|
||||||
<line
|
:y="mark.topY + 4"
|
||||||
:x1="width * 0.15"
|
:fill="WHITE"
|
||||||
:y1="mark.y"
|
font-size="8.5"
|
||||||
:x2="width * 0.45"
|
font-family="monospace"
|
||||||
:y2="mark.y"
|
>
|
||||||
stroke="#f4f6fb"
|
{{ mark.label }}
|
||||||
:stroke-width="mark.fpm === 0 ? 1.5 : 1"
|
</text>
|
||||||
/>
|
<text
|
||||||
<text
|
:x="axisX + 14"
|
||||||
v-if="mark.fpm !== 0"
|
:y="mark.bottomY + 4"
|
||||||
:x="width * 0.55"
|
:fill="WHITE"
|
||||||
:y="mark.y + 4"
|
font-size="8.5"
|
||||||
fill="#f4f6fb"
|
font-family="monospace"
|
||||||
font-size="9"
|
>
|
||||||
text-anchor="start"
|
{{ mark.label }}
|
||||||
font-family="monospace"
|
</text>
|
||||||
>
|
|
||||||
{{ mark.label }}
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- VS band from center to needle -->
|
|
||||||
<rect
|
|
||||||
v-if="Math.abs(verticalSpeed) > 10"
|
|
||||||
:x="width * 0.15"
|
|
||||||
:y="bandY"
|
|
||||||
:width="width * 0.3"
|
|
||||||
:height="Math.max(bandHeight, 1)"
|
|
||||||
fill="#2fe5ff"
|
|
||||||
opacity="0.7"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Needle indicator -->
|
|
||||||
<polygon
|
|
||||||
:points="`${width * 0.05},${needleY} ${width * 0.3},${needleY - 4} ${width * 0.3},${needleY + 4}`"
|
|
||||||
fill="#2fe5ff"
|
|
||||||
/>
|
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<!-- Readout -->
|
<!-- Trend vector (center to current VS) -->
|
||||||
|
<line
|
||||||
|
:x1="axisX"
|
||||||
|
:y1="centerY"
|
||||||
|
:x2="wingX"
|
||||||
|
:y2="needleY"
|
||||||
|
:stroke="GREEN"
|
||||||
|
stroke-width="3.6"
|
||||||
|
stroke-linecap="round"
|
||||||
|
opacity="0.95"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<circle
|
||||||
|
:cx="axisX"
|
||||||
|
:cy="centerY"
|
||||||
|
r="2"
|
||||||
|
:fill="GREEN"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Numeric readout in hundreds fpm -->
|
||||||
<text
|
<text
|
||||||
v-if="Math.abs(verticalSpeed) > 50"
|
v-if="showReadout"
|
||||||
:x="width / 2"
|
:x="wingX - 1"
|
||||||
:y="height - 6"
|
:y="height - 8"
|
||||||
fill="#2fe5ff"
|
:fill="GREEN"
|
||||||
font-size="9"
|
font-size="10"
|
||||||
font-weight="bold"
|
font-weight="bold"
|
||||||
text-anchor="middle"
|
text-anchor="end"
|
||||||
font-family="monospace"
|
font-family="monospace"
|
||||||
>
|
>
|
||||||
{{ readoutText }}
|
{{ readoutText }}
|
||||||
|
|||||||
Reference in New Issue
Block a user