Move project from bordanlage/ to repo root

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-26 14:31:08 +01:00
parent 946c0a5377
commit 77123a0df5
56 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
// Animated SVG compass rose.
const CX = 96, CY = 96, R = 80
const CARDINALS = [
{ label: 'N', angle: 0 },
{ label: 'E', angle: 90 },
{ label: 'S', angle: 180 },
{ label: 'W', angle: 270 },
]
function polarXY(cx, cy, r, angleDeg) {
const rad = (angleDeg - 90) * Math.PI / 180
return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) }
}
export default function Compass({ heading = 0, cog }) {
const hdg = heading ?? 0
const hasCog = cog != null
return (
<svg width={192} height={192} viewBox="0 0 192 192">
{/* Outer ring */}
<circle cx={CX} cy={CY} r={R} fill="none" stroke="var(--border)" strokeWidth={2} />
<circle cx={CX} cy={CY} r={R - 12} fill="none" stroke="var(--border)" strokeWidth={1} />
{/* Rotating rose */}
<g style={{
transformOrigin: `${CX}px ${CY}px`,
transform: `rotate(${-hdg}deg)`,
transition: 'transform 0.8s ease',
}}>
{/* 36 tick marks */}
{Array.from({ length: 36 }, (_, i) => {
const angle = i * 10
const outer = polarXY(CX, CY, R, angle)
const inner = polarXY(CX, CY, R - (i % 3 === 0 ? 10 : 5), angle)
return (
<line key={i}
x1={outer.x} y1={outer.y}
x2={inner.x} y2={inner.y}
stroke={i % 9 === 0 ? 'var(--accent)' : 'var(--muted)'}
strokeWidth={i % 9 === 0 ? 2 : 1}
/>
)
})}
{/* Cardinal labels */}
{CARDINALS.map(c => {
const p = polarXY(CX, CY, R - 22, c.angle)
return (
<text key={c.label}
x={p.x} y={p.y + 4}
textAnchor="middle"
fontFamily="var(--font-mono)" fontSize={12} fontWeight={700}
fill={c.label === 'N' ? 'var(--danger)' : 'var(--text)'}
>
{c.label}
</text>
)
})}
</g>
{/* Fixed lubber line (ship's bow = top) */}
<line x1={CX} y1={CY - R + 2} x2={CX} y2={CY - R + 16}
stroke="var(--accent)" strokeWidth={3} strokeLinecap="round" />
{/* COG indicator */}
{hasCog && (() => {
const cogAngle = cog - hdg
const tip = polarXY(CX, CY, R - 6, cogAngle)
return (
<line x1={CX} y1={CY} x2={tip.x} y2={tip.y}
stroke="var(--warning)" strokeWidth={2} strokeDasharray="4,3"
style={{ transformOrigin: `${CX}px ${CY}px`, transition: 'all 0.8s ease' }}
/>
)
})()}
{/* Center */}
<circle cx={CX} cy={CY} r={4} fill="var(--accent)" />
{/* Heading value */}
<text x={CX} y={CY + 26} textAnchor="middle"
fontFamily="var(--font-mono)" fontSize={18} fill="var(--text)">
{Math.round(hdg).toString().padStart(3, '0')}°
</text>
<text x={CX} y={186} textAnchor="middle"
fontFamily="var(--font-ui)" fontSize={11} fill="var(--muted)" fontWeight={600}>
HEADING
</text>
</svg>
)
}

View File

@@ -0,0 +1,16 @@
// Depth sounder with alarm threshold.
import Gauge from './Gauge.jsx'
export default function DepthSounder({ depth }) {
return (
<Gauge
value={depth}
min={0}
max={30}
label="Depth"
unit="m"
warning={3}
danger={1.5}
/>
)
}

View File

@@ -0,0 +1,118 @@
// Analog round gauge with animated needle.
const R = 80
const CX = 96
const CY = 96
const START_ANGLE = -225
const SWEEP = 270
function polarToXY(cx, cy, r, angleDeg) {
const rad = (angleDeg - 90) * Math.PI / 180
return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) }
}
function arcPath(cx, cy, r, startDeg, endDeg) {
const start = polarToXY(cx, cy, r, startDeg)
const end = polarToXY(cx, cy, r, endDeg)
const large = endDeg - startDeg > 180 ? 1 : 0
return `M ${start.x} ${start.y} A ${r} ${r} 0 ${large} 1 ${end.x} ${end.y}`
}
function buildTicks(startDeg, sweep, count) {
return Array.from({ length: count + 1 }, (_, i) => {
const angle = startDeg + (sweep / count) * i
const outer = polarToXY(CX, CY, R, angle)
const inner = polarToXY(CX, CY, R - (i % 2 === 0 ? 10 : 6), angle)
return { outer, inner, major: i % 2 === 0 }
})
}
export default function Gauge({ value, min = 0, max = 10, label = '', unit = '', danger, warning }) {
const clampedVal = Math.min(max, Math.max(min, value ?? min))
const ratio = (clampedVal - min) / (max - min)
const needleAngle = START_ANGLE + ratio * SWEEP
const warnRatio = warning != null ? (warning - min) / (max - min) : null
const dangRatio = danger != null ? (danger - min) / (max - min) : null
const ticks = buildTicks(START_ANGLE, SWEEP, 10)
const needleTip = polarToXY(CX, CY, R - 12, needleAngle)
const needleBase1 = polarToXY(CX, CY, 6, needleAngle + 90)
const needleBase2 = polarToXY(CX, CY, 6, needleAngle - 90)
const isWarning = warning != null && clampedVal >= warning
const isDanger = danger != null && clampedVal >= danger
const needleColor = isDanger ? 'var(--danger)' : isWarning ? 'var(--warning)' : 'var(--accent)'
return (
<svg width={192} height={192} viewBox="0 0 192 192" style={{ overflow: 'visible' }}>
{/* Background arc */}
<path
d={arcPath(CX, CY, R, START_ANGLE, START_ANGLE + SWEEP)}
fill="none" stroke="var(--border)" strokeWidth={8} strokeLinecap="round"
/>
{/* Warning zone */}
{warnRatio != null && (
<path
d={arcPath(CX, CY, R,
START_ANGLE + warnRatio * SWEEP,
START_ANGLE + (dangRatio ?? 1) * SWEEP)}
fill="none" stroke="#f59e0b33" strokeWidth={8}
/>
)}
{/* Danger zone */}
{dangRatio != null && (
<path
d={arcPath(CX, CY, R, START_ANGLE + dangRatio * SWEEP, START_ANGLE + SWEEP)}
fill="none" stroke="#ef444433" strokeWidth={8}
/>
)}
{/* Progress arc */}
<path
d={arcPath(CX, CY, R, START_ANGLE, needleAngle)}
fill="none" stroke={needleColor} strokeWidth={4} strokeLinecap="round"
style={{ transition: 'all 0.6s ease' }}
/>
{/* Ticks */}
{ticks.map((t, i) => (
<line key={i}
x1={t.outer.x} y1={t.outer.y}
x2={t.inner.x} y2={t.inner.y}
stroke={t.major ? 'var(--muted)' : 'var(--border)'}
strokeWidth={t.major ? 1.5 : 1}
/>
))}
{/* Needle */}
<polygon
points={`${needleTip.x},${needleTip.y} ${needleBase1.x},${needleBase1.y} ${needleBase2.x},${needleBase2.y}`}
fill={needleColor}
style={{ transition: 'all 0.6s ease', transformOrigin: `${CX}px ${CY}px` }}
/>
{/* Center cap */}
<circle cx={CX} cy={CY} r={6} fill="var(--surface2)" stroke={needleColor} strokeWidth={2} />
{/* Value text */}
<text x={CX} y={CY + 28} textAnchor="middle"
fontFamily="var(--font-mono)" fontSize={18} fill="var(--text)"
style={{ transition: 'all 0.3s' }}>
{value != null ? (Number.isInteger(max - min) && max - min <= 20
? Math.round(clampedVal)
: clampedVal.toFixed(1)) : '--'}
</text>
<text x={CX} y={CY + 42} textAnchor="middle" fontFamily="var(--font-mono)" fontSize={10} fill="var(--muted)">
{unit}
</text>
{/* Label */}
<text x={CX} y={186} textAnchor="middle" fontFamily="var(--font-ui)" fontSize={11} fill="var(--muted)" fontWeight={600}>
{label.toUpperCase()}
</text>
</svg>
)
}

View File

@@ -0,0 +1,14 @@
// Speed through water / SOG gauge.
import Gauge from './Gauge.jsx'
export default function SpeedLog({ sog }) {
return (
<Gauge
value={sog}
min={0}
max={12}
label="Speed"
unit="kn"
/>
)
}

View File

@@ -0,0 +1,76 @@
// Wind rose showing apparent wind angle and speed.
const CX = 96, CY = 96, R = 70
function polarXY(cx, cy, r, angleDeg) {
const rad = (angleDeg - 90) * Math.PI / 180
return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) }
}
export default function WindRose({ windAngle = 0, windSpeed = 0 }) {
const angle = windAngle ?? 0
const speed = windSpeed ?? 0
const tipLen = Math.min(R - 10, 20 + speed * 2.5)
const tip = polarXY(CX, CY, tipLen, angle)
const left = polarXY(CX, CY, 10, angle + 120)
const right = polarXY(CX, CY, 10, angle - 120)
const color = speed > 18 ? 'var(--danger)' : speed > 12 ? 'var(--warning)' : 'var(--accent)'
return (
<svg width={192} height={192} viewBox="0 0 192 192">
{/* Background rings */}
<circle cx={CX} cy={CY} r={R} fill="none" stroke="var(--border)" strokeWidth={1} />
<circle cx={CX} cy={CY} r={R * 0.6} fill="none" stroke="var(--border)" strokeWidth={1} strokeDasharray="3,4" />
{/* Dividers every 45° */}
{Array.from({ length: 8 }, (_, i) => {
const p = polarXY(CX, CY, R, i * 45)
return <line key={i} x1={CX} y1={CY} x2={p.x} y2={p.y}
stroke="var(--border)" strokeWidth={1} />
})}
{/* Wind arrow */}
<g style={{ transition: 'all 0.6s ease' }}>
<polygon
points={`${tip.x},${tip.y} ${left.x},${left.y} ${right.x},${right.y}`}
fill={color} opacity={0.85}
style={{ transformOrigin: `${CX}px ${CY}px`, transition: 'all 0.6s ease' }}
/>
<line x1={CX} y1={CY} x2={tip.x} y2={tip.y}
stroke={color} strokeWidth={2}
style={{ transformOrigin: `${CX}px ${CY}px`, transition: 'all 0.6s ease' }}
/>
</g>
<circle cx={CX} cy={CY} r={5} fill="var(--surface2)" stroke={color} strokeWidth={2} />
{/* Speed value */}
<text x={CX} y={CY + 26} textAnchor="middle"
fontFamily="var(--font-mono)" fontSize={18} fill="var(--text)">
{speed.toFixed(1)}
</text>
<text x={CX} y={CY + 40} textAnchor="middle"
fontFamily="var(--font-mono)" fontSize={10} fill="var(--muted)">
kn
</text>
{/* Labels */}
{[['N', 0], ['E', 90], ['S', 180], ['W', 270]].map(([l, a]) => {
const p = polarXY(CX, CY, R + 12, a)
return (
<text key={l} x={p.x} y={p.y + 4} textAnchor="middle"
fontFamily="var(--font-mono)" fontSize={10} fill="var(--muted)">
{l}
</text>
)
})}
<text x={CX} y={186} textAnchor="middle"
fontFamily="var(--font-ui)" fontSize={11} fill="var(--muted)" fontWeight={600}>
WIND
</text>
</svg>
)
}