Blitz weiß mit Rand — und er fadet weiter

Beides, nicht eines von beiden: das Zeichen ist weiß mit dünnem dunklem
Rand, und beim Laden fadet es.

Der Rand ist dabei immer die Gegenfarbe zur Füllung. Damit bleibt das
Zeichen in jedem Moment des Fadens lesbar — über der hellen Füllung trägt
es der Rand, über dem leeren Teil die Füllung selbst.

Das Pausenzeichen ist niedriger als der Blitz. Es ist von Haus aus höher
gebaut und stieß oben und unten an den Rahmen des Akkus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-08-13 13:04:31 +02:00
parent 20963f53e2
commit 8545cc421c
2 changed files with 89 additions and 35 deletions

View File

@@ -68,6 +68,25 @@ public enum BatteryGlyph {
return (filled, empty)
}
/// Wie dick der Rand um das Zeichen ist.
public static let outlineWidth: CGFloat = 1
/// Der Rahmen für den Rand dasselbe Zeichen, etwas größer.
public static func outlineRect(_ glyph: CGRect) -> CGRect {
glyph.insetBy(dx: -outlineWidth, dy: -outlineWidth)
}
/// Wie hoch das Zeichen im Verhältnis zum Akku ist.
///
/// Das Pausenzeichen ist von Haus aus höher als der Blitz und stößt sonst
/// oben und unten an den Rahmen.
public static func glyphHeight(for indicator: Indicator, in height: CGFloat) -> CGFloat {
switch indicator {
case .paused: (height - 1) * 0.62
default: height - 1
}
}
/// Wie lange ein voller Hin- und Rückweg des Blitzes dauert.
public static let fadeDuration: TimeInterval = 2.2
@@ -149,32 +168,25 @@ public enum BatteryGlyph {
isPluggedIn: isPluggedIn).symbolName,
let glyph = NSImage(systemSymbolName: name, accessibilityDescription: nil)
else { return }
let glyphHeight = rect.height - 1
let indicator = self.indicator(isCharging: isCharging, isPluggedIn: isPluggedIn)
let glyphHeight = glyphHeight(for: indicator, in: rect.height)
let glyphWidth = (glyph.size.width / glyph.size.height) * glyphHeight
let glyphRect = CGRect(x: body.midX - glyphWidth / 2,
y: rect.midY - glyphHeight / 2,
width: glyphWidth, height: glyphHeight)
if isCharging {
// Der Blitz **fadet** zwischen Weiß und Schwarz.
//
// Bei wenig Ladung sitzt er im leeren Teil des Akkus und braucht
// Weiß, über der Füllung braucht er Schwarz. Ein fester Wert ist in
// einem der beiden Fälle immer unlesbar; ein Faden ist in beiden
// irgendwann gut zu sehen und die Bewegung zeigt zusätzlich, dass
// gerade geladen wird.
let whiteness = boltWhiteness(phase: fadePhase)
let boltColor = NSColor(white: whiteness, alpha: 1)
if let tinted = tint(glyph, with: boltColor) {
tinted.draw(in: glyphRect, from: .zero, operation: .sourceOver, fraction: 1)
}
return
// Weiß mit dünnem schwarzem Rand und beim Laden fadend.
//
// Der Rand ist immer die Gegenfarbe zur Füllung. Damit bleibt das
// Zeichen in **jedem** Moment des Fadens lesbar: über der hellen
// Füllung trägt es der Rand, über dem leeren Teil die Füllung selbst.
let whiteness = isCharging ? boltWhiteness(phase: fadePhase) : 1
if let outline = tint(glyph, with: NSColor(white: 1 - whiteness, alpha: 1)) {
outline.draw(in: outlineRect(glyphRect), from: .zero,
operation: .sourceOver, fraction: 1)
}
if let body = tint(glyph, with: NSColor(white: whiteness, alpha: 1)) {
body.draw(in: glyphRect, from: .zero, operation: .sourceOver, fraction: 1)
}
// Der Stecker wird ausgestanzt: er steht nur, wenn nicht geladen wird,
// und dann bewegt sich ohnehin nichts.
NSGraphicsContext.saveGraphicsState()
glyph.draw(in: glyphRect, from: .zero, operation: .destinationOut, fraction: 1)
NSGraphicsContext.restoreGraphicsState()
}
}
@@ -222,22 +234,27 @@ public struct BatteryGlyphView: View {
}
.frame(width: (BatteryGlyph.size.width - 3) * scale, height: height)
.overlay {
if isCharging {
// Dasselbe Faden wie in der Menüleiste. `TimelineView` gibt
// die Zeit her, ohne dass hier ein Zeitgeber laufen muss.
let indicator = BatteryGlyph.indicator(isCharging: isCharging,
isPluggedIn: isPluggedIn)
if let name = indicator.symbolName {
// `TimelineView` gibt die Zeit her, ohne dass hier ein
// Zeitgeber laufen muss.
TimelineView(.animation) { context in
let phase = context.date.timeIntervalSinceReferenceDate
/ BatteryGlyph.fadeDuration
Image(systemName: BatteryGlyph.Indicator.bolt.symbolName ?? "bolt.fill")
.font(.system(size: height * 0.68))
.foregroundStyle(
Color(white: BatteryGlyph.boltWhiteness(phase: phase)))
let phase = isCharging
? context.date.timeIntervalSinceReferenceDate / BatteryGlyph.fadeDuration
: 0
let whiteness = isCharging
? BatteryGlyph.boltWhiteness(phase: phase) : 1
let size = BatteryGlyph.glyphHeight(for: indicator, in: height)
ZStack {
Image(systemName: name)
.font(.system(size: size + 2 * BatteryGlyph.outlineWidth))
.foregroundStyle(Color(white: 1 - whiteness))
Image(systemName: name)
.font(.system(size: size))
.foregroundStyle(Color(white: whiteness))
}
}
} else if let name = BatteryGlyph.indicator(isCharging: false,
isPluggedIn: isPluggedIn).symbolName {
Image(systemName: name)
.font(.system(size: height * 0.68))
.blendMode(.destinationOut)
}
}
.compositingGroup()