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:
@@ -68,6 +68,25 @@ public enum BatteryGlyph {
|
|||||||
return (filled, empty)
|
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.
|
/// Wie lange ein voller Hin- und Rückweg des Blitzes dauert.
|
||||||
public static let fadeDuration: TimeInterval = 2.2
|
public static let fadeDuration: TimeInterval = 2.2
|
||||||
|
|
||||||
@@ -149,32 +168,25 @@ public enum BatteryGlyph {
|
|||||||
isPluggedIn: isPluggedIn).symbolName,
|
isPluggedIn: isPluggedIn).symbolName,
|
||||||
let glyph = NSImage(systemSymbolName: name, accessibilityDescription: nil)
|
let glyph = NSImage(systemSymbolName: name, accessibilityDescription: nil)
|
||||||
else { return }
|
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 glyphWidth = (glyph.size.width / glyph.size.height) * glyphHeight
|
||||||
let glyphRect = CGRect(x: body.midX - glyphWidth / 2,
|
let glyphRect = CGRect(x: body.midX - glyphWidth / 2,
|
||||||
y: rect.midY - glyphHeight / 2,
|
y: rect.midY - glyphHeight / 2,
|
||||||
width: glyphWidth, height: glyphHeight)
|
width: glyphWidth, height: glyphHeight)
|
||||||
if isCharging {
|
// Weiß mit dünnem schwarzem Rand — und beim Laden fadend.
|
||||||
// Der Blitz **fadet** zwischen Weiß und Schwarz.
|
//
|
||||||
//
|
// Der Rand ist immer die Gegenfarbe zur Füllung. Damit bleibt das
|
||||||
// Bei wenig Ladung sitzt er im leeren Teil des Akkus und braucht
|
// Zeichen in **jedem** Moment des Fadens lesbar: über der hellen
|
||||||
// Weiß, über der Füllung braucht er Schwarz. Ein fester Wert ist in
|
// Füllung trägt es der Rand, über dem leeren Teil die Füllung selbst.
|
||||||
// einem der beiden Fälle immer unlesbar; ein Faden ist in beiden
|
let whiteness = isCharging ? boltWhiteness(phase: fadePhase) : 1
|
||||||
// irgendwann gut zu sehen — und die Bewegung zeigt zusätzlich, dass
|
if let outline = tint(glyph, with: NSColor(white: 1 - whiteness, alpha: 1)) {
|
||||||
// gerade geladen wird.
|
outline.draw(in: outlineRect(glyphRect), from: .zero,
|
||||||
let whiteness = boltWhiteness(phase: fadePhase)
|
operation: .sourceOver, fraction: 1)
|
||||||
let boltColor = NSColor(white: whiteness, alpha: 1)
|
}
|
||||||
if let tinted = tint(glyph, with: boltColor) {
|
if let body = tint(glyph, with: NSColor(white: whiteness, alpha: 1)) {
|
||||||
tinted.draw(in: glyphRect, from: .zero, operation: .sourceOver, fraction: 1)
|
body.draw(in: glyphRect, from: .zero, operation: .sourceOver, fraction: 1)
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
.frame(width: (BatteryGlyph.size.width - 3) * scale, height: height)
|
||||||
.overlay {
|
.overlay {
|
||||||
if isCharging {
|
let indicator = BatteryGlyph.indicator(isCharging: isCharging,
|
||||||
// Dasselbe Faden wie in der Menüleiste. `TimelineView` gibt
|
isPluggedIn: isPluggedIn)
|
||||||
// die Zeit her, ohne dass hier ein Zeitgeber laufen muss.
|
if let name = indicator.symbolName {
|
||||||
|
// `TimelineView` gibt die Zeit her, ohne dass hier ein
|
||||||
|
// Zeitgeber laufen muss.
|
||||||
TimelineView(.animation) { context in
|
TimelineView(.animation) { context in
|
||||||
let phase = context.date.timeIntervalSinceReferenceDate
|
let phase = isCharging
|
||||||
/ BatteryGlyph.fadeDuration
|
? context.date.timeIntervalSinceReferenceDate / BatteryGlyph.fadeDuration
|
||||||
Image(systemName: BatteryGlyph.Indicator.bolt.symbolName ?? "bolt.fill")
|
: 0
|
||||||
.font(.system(size: height * 0.68))
|
let whiteness = isCharging
|
||||||
.foregroundStyle(
|
? BatteryGlyph.boltWhiteness(phase: phase) : 1
|
||||||
Color(white: BatteryGlyph.boltWhiteness(phase: phase)))
|
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()
|
.compositingGroup()
|
||||||
|
|||||||
@@ -194,3 +194,40 @@ struct BatteryBoltFadeTests {
|
|||||||
#expect(BatteryGlyph.boltWhiteness(phase: 0.5) == 0)
|
#expect(BatteryGlyph.boltWhiteness(phase: 0.5) == 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suite("Umrandetes Zeichen")
|
||||||
|
struct BatteryOutlineTests {
|
||||||
|
|
||||||
|
private let glyph = CGRect(x: 10, y: 4, width: 8, height: 12)
|
||||||
|
|
||||||
|
@Test("Der Rand liegt außen herum und bleibt mittig")
|
||||||
|
func outlineSurroundsAndCentres() {
|
||||||
|
let outline = BatteryGlyph.outlineRect(glyph)
|
||||||
|
#expect(outline.minX < glyph.minX)
|
||||||
|
#expect(outline.maxX > glyph.maxX)
|
||||||
|
#expect(abs(outline.midX - glyph.midX) < 0.001)
|
||||||
|
#expect(abs(outline.midY - glyph.midY) < 0.001)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Er ist dünn")
|
||||||
|
func outlineIsThin() {
|
||||||
|
// Ein dicker Rand frisst das Zeichen auf.
|
||||||
|
#expect(BatteryGlyph.outlineRect(glyph).width - glyph.width <= 2.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Das Pausenzeichen ist niedriger als der Blitz")
|
||||||
|
func pauseIsShorter() {
|
||||||
|
// Es ist von Haus aus höher gebaut und stößt sonst oben und unten an
|
||||||
|
// den Rahmen des Akkus.
|
||||||
|
let bolt = BatteryGlyph.glyphHeight(for: .bolt, in: 12)
|
||||||
|
let paused = BatteryGlyph.glyphHeight(for: .paused, in: 12)
|
||||||
|
#expect(paused < bolt)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Beide bleiben im Akku")
|
||||||
|
func bothFitInside() {
|
||||||
|
for indicator in BatteryGlyph.Indicator.allCases {
|
||||||
|
#expect(BatteryGlyph.glyphHeight(for: indicator, in: 12) < 12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user