Blitz fadet, Einstellungsfenster mittig und veränderbar
Der Blitz fadet beim Laden zwischen Weiß und Schwarz — so gewünscht, und ich hätte vorher fragen sollen statt eine eigene Lösung zu bauen und sie hinterher zu begründen. Über den Kosinus statt linear: ein linearer Ping-Pong knickt an den Umkehrpunkten sichtbar. Der Zeitgeber läuft mit 30 Bildern je Sekunde und nur, solange geladen wird — steht das Kabel nicht drin, läuft gar nichts. Der Stecker für „am Netz, lädt nicht" bleibt ausgestanzt; dort bewegt sich ohnehin nichts. Das Einstellungsfenster lässt sich jetzt vergrößern und startet mit 620 × 620 statt 520 × 420. Der Bereich „Displays" passt mit der Vorschau der Auslösefläche nicht mehr in die alte Höhe, und ein Fenster, in dem man scrollt, obwohl der Bildschirm frei ist, ist eine Zumutung. Die feste Größe in der View musste dafür weichen — sonst wächst das Fenster und der Inhalt bleibt stehen. Und es öffnet jetzt wirklich mittig. Mein erster Versuch mit 60 Punkten Abstand von oben war zu zaghaft: der Titelbalken lag weiter so hoch, dass man auf dem Weg zu seinen Knöpfen durch die Auslösefläche fuhr. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,28 @@ public enum BatteryGlyph {
|
||||
return (filled, empty)
|
||||
}
|
||||
|
||||
/// Wie lange ein voller Hin- und Rückweg des Blitzes dauert.
|
||||
public static let fadeDuration: TimeInterval = 2.2
|
||||
|
||||
/// Der Ping-Pong von 0 nach 1 und zurück.
|
||||
///
|
||||
/// Über den Kosinus statt linear: ein linearer Ping-Pong knickt an den
|
||||
/// Umkehrpunkten sichtbar, der Kosinus wird dort flach. In der Menüleiste
|
||||
/// fällt so ein Knick auf.
|
||||
public static func fadeAmount(phase: Double) -> Double {
|
||||
(1 - cos(phase.truncatingRemainder(dividingBy: 1) * 2 * .pi)) / 2
|
||||
}
|
||||
|
||||
/// Wie weiß der Blitz gerade ist, 1 = weiß, 0 = schwarz.
|
||||
///
|
||||
/// Darum geht es: bei wenig Ladung sitzt der Blitz im leeren Teil des
|
||||
/// Akkus und braucht Weiß, über der Füllung braucht er Schwarz. Ein Faden
|
||||
/// dazwischen ist in beiden Fällen irgendwann gut lesbar — und man sieht
|
||||
/// am Wandern zusätzlich, dass geladen wird.
|
||||
public static func boltWhiteness(phase: Double) -> Double {
|
||||
1 - fadeAmount(phase: phase)
|
||||
}
|
||||
|
||||
public static func isLow(charge: Double, isCharging: Bool = false) -> Bool {
|
||||
// Ein roter Akku, der gerade lädt, wäre eine Warnung vor etwas, das
|
||||
// sich schon erledigt.
|
||||
@@ -83,6 +105,7 @@ public enum BatteryGlyph {
|
||||
/// kritischem Ladestand weicht die Füllung davon ab — Farbe ist in der
|
||||
/// Menüleiste ein Signal, keine Dekoration.
|
||||
public static func draw(charge: Double, isCharging: Bool, isPluggedIn: Bool = false,
|
||||
fadePhase: Double = 0,
|
||||
color: NSColor, in rect: CGRect) {
|
||||
let capWidth: CGFloat = 2
|
||||
let body = CGRect(x: rect.minX, y: rect.minY,
|
||||
@@ -127,19 +150,26 @@ public enum BatteryGlyph {
|
||||
let glyphRect = CGRect(x: body.midX - glyphWidth / 2,
|
||||
y: rect.midY - glyphHeight / 2,
|
||||
width: glyphWidth, height: glyphHeight)
|
||||
// Geteilt zeichnen: über der Füllung ein Loch, daneben ein Glyph.
|
||||
let split = boltSplit(charge: charge, inner: inner)
|
||||
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
NSBezierPath(rect: split.filled).setClip()
|
||||
glyph.draw(in: glyphRect, from: .zero, operation: .destinationOut, fraction: 1)
|
||||
NSGraphicsContext.restoreGraphicsState()
|
||||
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
NSBezierPath(rect: split.empty).setClip()
|
||||
if let tinted = tint(glyph, with: color) {
|
||||
tinted.draw(in: glyphRect, from: .zero, operation: .sourceOver, fraction: 1)
|
||||
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
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
@@ -188,31 +218,22 @@ public struct BatteryGlyphView: View {
|
||||
}
|
||||
.frame(width: (BatteryGlyph.size.width - 3) * scale, height: height)
|
||||
.overlay {
|
||||
// Geteilt wie in der Menüleiste: über der Füllung ein Loch,
|
||||
// daneben ein ausgemaltes Glyph. Nur auszustanzen hieße, dass
|
||||
// der Blitz bei wenig Ladung im Leeren verschwindet.
|
||||
if let name = BatteryGlyph.indicator(isCharging: isCharging,
|
||||
isPluggedIn: isPluggedIn).symbolName {
|
||||
GeometryReader { geometry in
|
||||
let inner = CGRect(origin: .zero, size: geometry.size)
|
||||
.insetBy(dx: 2 * scale, dy: 2 * scale)
|
||||
let split = BatteryGlyph.boltSplit(charge: charge, inner: inner)
|
||||
let glyph = Image(systemName: name)
|
||||
if isCharging {
|
||||
// Dasselbe Faden wie in der Menüleiste. `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))
|
||||
.frame(width: geometry.size.width, height: geometry.size.height)
|
||||
|
||||
glyph.blendMode(.destinationOut)
|
||||
.mask(alignment: .topLeading) {
|
||||
Rectangle().frame(width: split.filled.width,
|
||||
height: geometry.size.height)
|
||||
.offset(x: split.filled.minX)
|
||||
}
|
||||
glyph.mask(alignment: .topLeading) {
|
||||
Rectangle().frame(width: split.empty.width,
|
||||
height: geometry.size.height)
|
||||
.offset(x: split.empty.minX)
|
||||
}
|
||||
.foregroundStyle(
|
||||
Color(white: BatteryGlyph.boltWhiteness(phase: phase)))
|
||||
}
|
||||
} else if let name = BatteryGlyph.indicator(isCharging: false,
|
||||
isPluggedIn: isPluggedIn).symbolName {
|
||||
Image(systemName: name)
|
||||
.font(.system(size: height * 0.68))
|
||||
.blendMode(.destinationOut)
|
||||
}
|
||||
}
|
||||
.compositingGroup()
|
||||
|
||||
Reference in New Issue
Block a user