style(pfd): refine colors, narrow heading tape, and rework VS scale

This commit is contained in:
itsrubberduck
2026-02-21 00:25:32 +01:00
parent 064ce39e79
commit c9dee79605
3 changed files with 143 additions and 109 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 }}