Volle Höhe, eine Kante statt eines Streifens

Der Aufmacher nimmt jetzt achtundachtzig Prozent der sichtbaren Höhe, statt
sich seine Höhe aus 16:9 auszurechnen. Ein Aufmacher, der ein Drittel hoch ist,
sieht aus wie ein breites Vorschaubild; die zwölf Prozent Rest sind Absicht,
dort lugt die erste Reihe herein und sagt, dass es weitergeht. Auf schmalen
Geräten bleibt es beim Hochformat.

`FlixyArtwork` kann dafür den Rahmen ausfüllen, den es bekommt. Das
Seitenverhältnis bleibt trotzdem wichtig — danach werden die Bilder beim Server
angefordert.

Und `glassEffect` ist raus. Er zeichnet an seiner Kante einen Lichtsaum, und
über einem hellen Aufmacher wurde daraus ein heller Balken von mehreren Punkten
Breite neben der Leiste — kein Rand, sondern ein Streifen. Von einer Kante war
gleichzeitig nichts zu erkennen.

Von Hand sind es drei Lagen: ein trübes Material, ein Schleier gegen die
durchziehende Startseite, und ein Haarstrich, der oben links aufhellt und unten
rechts nachlässt. Dazu ein Schatten — was schwebt, wirft einen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-13 20:12:42 +02:00
co-authored by Claude Opus 5
parent d0f732402c
commit 873b8a4405
5 changed files with 98 additions and 22 deletions
+28
View File
@@ -85,6 +85,16 @@ struct HeroCarousel: View {
.offset(x: (1 - cover) * (isMovingForward ? Self.slide : -Self.slide))
}
}
// Die Höhe: fast das ganze Fenster.
//
// Ein Aufmacher, der nur ein Drittel hoch ist, sieht aus wie ein
// breites Vorschaubild. Die zwölf Prozent, die übrig bleiben,
// sind Absicht — dort lugt die erste Reihe herein und sagt, dass
// es weitergeht.
//
// Auf einem schmalen Gerät nicht: dort ist der Aufmacher hochkant
// und rechnet sich seine Höhe aus der Breite aus.
.modifier(HeroHeight(fraction: sizeClass == .compact ? nil : 0.88))
// **Kein** Seitenverhältnis hier: der Aufmacher bringt sein
// eigenes mit, und eines darüber legte ihn auf die Breite des
// Inhaltsbereichs fest. Weil er nach links unter die Seitenleiste
@@ -184,3 +194,21 @@ struct HeroCarousel: View {
}
}
}
/// Gibt dem Aufmacher einen Anteil an der sichtbaren Höhe — oder gar nichts.
///
/// Eigener Baustein, weil `containerRelativeFrame` sich nicht abschalten lässt:
/// es wirkt, sobald es dasteht. Ein `if` in der Ansicht brächte den beiden
/// Zweigen außerdem verschiedene Identitäten ein, und der Aufmacher würde beim
/// Drehen neu geladen statt mitzuwandern.
private struct HeroHeight: ViewModifier {
let fraction: CGFloat?
func body(content: Content) -> some View {
if let fraction {
content.containerRelativeFrame(.vertical) { height, _ in height * fraction }
} else {
content
}
}
}
+6 -1
View File
@@ -22,7 +22,12 @@ struct HeroSection: View {
logoURL: session.backend.flatMap { HeroArtwork.logoURL(for: item, backend: $0) },
// Auf dem iPhone hochkant: 16:9 ließ dem Text so wenig Platz, dass
// Schriftzug und Beschreibung übereinanderlagen.
aspectRatio: HeroArtwork.aspectRatio(compact: sizeClass == .compact)
aspectRatio: HeroArtwork.aspectRatio(compact: sizeClass == .compact),
// Auf einem großen Bildschirm nimmt der Aufmacher die Höhe, die
// das Karussell ihm gibt — sonst wäre er so hoch, wie 16:9 es
// vorschreibt, und darüber und darunter bliebe der halbe
// Bildschirm leer.
fillsAvailableSpace: sizeClass != .compact
) {
Button {
playback.play(item)
+40 -17
View File
@@ -54,6 +54,40 @@ struct FlixySidebar: View {
/// kommt eine Mediathek dazu, soll nicht plötzlich die falsche zuklappen.
@State private var collapsed: Set<String> = []
/// Die Fläche, auf der die Leiste liegt.
///
/// Von Hand gebaut und nicht mit `glassEffect`. Der zeichnet an seiner
/// Kante einen Lichtsaum, und über einem hellen Aufmacher wurde daraus ein
/// heller Balken von mehreren Punkten Breite, der neben der Leiste stand —
/// kein Rand, sondern ein Streifen. Gleichzeitig war von einer Kante nichts
/// zu erkennen.
///
/// Drei Lagen: ein trübes Material, damit die Leiste eine eigene Fläche
/// ist; ein Schleier, damit die durchziehende Startseite nicht mitgelesen
/// wird; und ein Haarstrich, der oben links aufhellt und unten rechts
/// nachlässt — so fängt Glas das Licht. Dazu ein Schatten, denn was
/// schwebt, wirft einen.
@ViewBuilder
static func surface(in shape: some InsettableShape) -> some View {
shape
.fill(.ultraThinMaterial)
.overlay { shape.fill(Color.flixyCanvas.opacity(0.55)) }
.overlay {
shape.strokeBorder(
LinearGradient(
stops: [
.init(color: .white.opacity(0.55), location: 0),
.init(color: .white.opacity(0.10), location: 0.5),
.init(color: .white.opacity(0.30), location: 1),
],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
lineWidth: 1
)
}
}
static let width: CGFloat = 235
/// Der Abstand zum Fensterrand. Er ist der eigentliche Punkt der Sache.
static let margin: CGFloat = 12
@@ -81,21 +115,9 @@ struct FlixySidebar: View {
.padding(.vertical, FlixySpacing.snug)
}
.frame(width: Self.width)
// Ein Schleier unter dem Glas.
//
// Reines Glas zeigt alles, was dahinter vorbeizieht — und beim Scrollen
// zieht die ganze Startseite vorbei. Der Aufmacher, die Plakatreihen,
// die Überschriften: alles wanderte durch die Leiste hindurch, und
// neben den ruhigen Beschriftungen wurde daraus ein unruhiger Brei.
//
// Der Schleier nimmt der Bewegung die Schärfe, ohne die Fläche
// zuzumauern: man sieht noch, dass dort etwas ist, aber man liest es
// nicht mehr mit.
.background(
Color.flixyCanvas.opacity(0.72),
in: .rect(cornerRadius: FlixyRadius.panel, style: .continuous)
)
.glassEffect(.regular, in: .rect(cornerRadius: FlixyRadius.panel))
.background { Self.surface(in: .rect(cornerRadius: FlixyRadius.panel, style: .continuous)) }
.clipShape(.rect(cornerRadius: FlixyRadius.panel, style: .continuous))
.shadow(color: .black.opacity(0.45), radius: 22, y: 8)
.padding(Self.margin)
}
@@ -223,8 +245,9 @@ struct FlixyTopBar: View {
}
.padding(.horizontal, FlixySpacing.snug)
.padding(.vertical, FlixySpacing.tight)
.background(Color.flixyCanvas.opacity(0.72), in: .capsule)
.glassEffect(.regular, in: .capsule)
.background { FlixySidebar.surface(in: .capsule) }
.clipShape(.capsule)
.shadow(color: .black.opacity(0.45), radius: 18, y: 6)
.padding(.top, FlixySpacing.snug)
}