design/halide

4 min czytania 916 słów
design/halide screenshot

Halide: Profesjonalne funkcje w przystępnej formie

„Złożoność jest obecna — po prostu nie przytłacza użytkownika. Gdy oferuje się nieco bardziej przystępne wejście do tego świata, można rozbudzić entuzjazm bez wywoływania onieśmielenia."

Halide dowodzi, że profesjonalne narzędzia nie wymagają profesjonalnych interfejsów. Laureat Apple Design Award 2022 ukrywa funkcje na poziomie lustrzanek cyfrowych za intuicyjnymi gestami, odsłaniając możliwości dopiero wtedy, gdy użytkownik po nie sięga.


Dlaczego Halide ma znaczenie

Stworzona przez byłego projektanta Apple Sebastiaana de Witha i byłego inżyniera Twittera Bena Sandofsky'ego aplikacja Halide wypełnia lukę między prostym aparatem Apple a onieśmielającymi aplikacjami dla profesjonalistów.

Kluczowe osiągnięcia: - Apple Design Award 2022 - Uczyniła fotografię RAW przystępną dzięki funkcji Instant RAW - Spopularyzowała sterowanie aparatem za pomocą gestów - Obsługa jedną ręką na wszystkich rozmiarach iPhone'a - Ponad 10 000 recenzji z oceną 5 gwiazdek


Najważniejsze wnioski

  1. Ukrywaj złożoność, nie usuwaj jej — profesjonalne funkcje istnieją, ale pozostają niewidoczne do momentu wywołania; interfejs „symulatora lotu" nie jest jedynym sposobem udostępniania zaawansowanych możliwości
  2. Inteligentna aktywacja jest lepsza niż przyciski przełączające — narzędzia pojawiające się podczas interakcji (lupa ostrości podczas przeciągania) są bardziej intuicyjne niż ręczne sterowanie widocznością
  3. Gesty powinny być fizyczne w odczuciu — przeciągnięcie w pionie dla ekspozycji, w poziomie dla ostrości — bezpośrednio odwzorowuje zachowanie pokręteł prawdziwego aparatu
  4. Ucz podczas użytkowania — krótkie etykiety przy zmianach stanu pomagają użytkownikom naturalnie poznawać terminologię fotograficzną, bez osobnego samouczka
  5. Obsługa jedną ręką jako ograniczenie projektowe — projektowanie z myślą o zasięgu kciuka wymusza dobrą hierarchię informacji i priorytetyzację najważniejszych elementów sterowania

Podstawowa filozofia projektowania

„Nie przeszkadzaj"

Nadrzędna zasada Halide: narzędzia powinny pojawiać się, gdy są potrzebne, i znikać, gdy nie są.

INNE PROFESJONALNE APLIKACJE              PODEJŚCIE HALIDE
───────────────────────────────────────────────────────────────────
Interfejs „symulatora lotu"               Czysty wizjer
Wszystkie kontrolki widoczne zawsze       Kontrolki pojawiają się na żądanie
Nauka UI przed użyciem                    Nauka podczas użytkowania
Stały układ                               Konfigurowalny pasek narzędzi
Tylko tryb manualny                       Tryb automatyczny + manualny dostępne

Kluczowa obserwacja: „Inne aplikacje do fotografii wyglądały jak symulatory lotu z mnóstwem pokręteł, co było onieśmielające, nawet dla kogoś takiego jak ja, kto uwielbia aparaty analogowe."

Inspiracja aparatami analogowymi

Halide przenosi dotykową przyjemność z aparatów analogowych na gesty na ekranie dotykowym.

APARAT ANALOGOWY                         ODPOWIEDNIK W HALIDE
───────────────────────────────────────────────────────────────────
Obrót pierścienia przysłony              Przeciągnięcie w pionie dla ekspozycji
Obrót pierścienia ostrości               Przeciągnięcie w poziomie dla ostrości
Mechaniczne rastry                        Wibracje haptyczne
Fizyczny wizjer                           Kompozycja pełnoekranowa
Wskaźnik światłomierza                    Cyfrowy histogram
Przełącznik Manual/Auto                   Przycisk przełączający AF

Cel projektowy: „Daj dziecku aparat, a zacznie bawić się pierścieniem przysłony, pokrętłami i przełącznikami. Może uda nam się przenieść odrobinę tej radości do aplikacji na kawałku szkła."


Biblioteka wzorców

1. Inteligentna aktywacja

Kontrolki pojawiają się kontekstowo, gdy są potrzebne, a następnie znikają. Nie wymaga ręcznego pokazywania/ukrywania.

Przykład: Lupa ostrości

STAN DOMYŚLNY
┌─────────────────────────────────────────────┐
│                                             │
│              [Wizjer]                       │
│                                             │
│                                             │
│                                             │
│  [Pokrętło ostrości na dole]                │
└─────────────────────────────────────────────┘

GDY UŻYTKOWNIK DOTYKA POKRĘTŁA OSTROŚCI
┌─────────────────────────────────────────────┐
│  ┌──────────┐                               │
│  │ LUPA     │  ← Lupa ostrości pojawia się  │
│  │ OSTROŚCI │    automatycznie              │
│  │ (zoom)   │                               │
│  └──────────┘                               │
│                                             │
│  [Pokrętło ostrości aktywne]                │
└─────────────────────────────────────────────┘

GDY UŻYTKOWNIK PUSZCZA
┌─────────────────────────────────────────────┐
│                                             │
│              [Wizjer]                       │
│                   ↑                         │
│         Lupa zanika                         │
│                                             │
│  [Pokrętło ostrości w spoczynku]            │
└─────────────────────────────────────────────┘

Koncepcja implementacji w SwiftUI:

struct IntelligentActivation<Content: View, Tool: View>: View {
    @Binding var isInteracting: Bool
    let content: Content
    let tool: Tool

    var body: some View {
        ZStack {
            content

            tool
                .opacity(isInteracting ? 1 : 0)
                .animation(.easeInOut(duration: 0.2), value: isInteracting)
        }
    }
}

struct FocusControl: View {
    @State private var isDragging = false
    @State private var focusValue: Double = 0.5

    var body: some View {
        ZStack {
            // Wizjer
            CameraPreview()

            // Lupa ostrości - pojawia się podczas przeciągania
            IntelligentActivation(isInteracting: $isDragging, content: EmptyView()) {
                FocusLoupeView(zoomLevel: 3.0)
                    .frame(width: 120, height: 120)
                    .position(x: 80, y: 80)
            }

            // Pokrętło ostrości
            VStack {
                Spacer()
                FocusDial(value: $focusValue)
                    .gesture(
                        DragGesture()
                            .onChanged { _ in
                                isDragging = true
                            }
                            .onEnded { _ in
                                isDragging = false
                            }
                    )
            }
        }
    }
}

2. Stopniowe ujawnianie

Proste domyślnie, złożone w razie potrzeby. Interfejs przekształca się w zależności od trybu.

Transformacja trybów:

TRYB AUTO (Domyślny)
┌─────────────────────────────────────────────┐
│                                             │
│              [Wizjer]                       │
│                                             │
│                                             │
│ ┌───┐                               ┌───┐   │
│ │ [F] │                               │ AF │   │
│ └───┘                               └───┘   │
│              ┌───────────┐                  │
│              │   (●)     │  ← Migawka       │
│              └───────────┘                  │
└─────────────────────────────────────────────┘
Minimalne kontrolki. Po prostu fotografuj.

TRYB MANUALNY (Po dotknięciu „AF" → „MF")
┌─────────────────────────────────────────────┐
│  [Histogram]                   [Focus Peak] │
│                                             │
│              [Wizjer]                       │
│                                             │
│ ┌───┐ ┌───┐ ┌───┐              ┌────┐      │
│ │ [F] │ │WB │ │ISO│              │ MF │      │
│ └───┘ └───┘ └───┘              └────┘      │
│              ┌───────────┐                  │
│  [────────────────●───────] ← Pokrętło ostrości   │
│              │   (●)     │                  │
│              └───────────┘                  │
└─────────────────────────────────────────────┘
Pełne kontrolki ujawnione. Profesjonalne narzędzia dostępne.

Implementacja:

enum CameraMode {
    case auto
    case manual

    var visibleControls: [CameraControl] {
        switch self {
        case .auto:
            return [.flash, .shutter, .autoFocus]
        case .manual:
            return [.flash, .whiteBalance, .iso, .shutter,
                    .manualFocus, .histogram, .focusPeaking]
        }
    }
}

struct AdaptiveToolbar: View {
    @Binding var mode: CameraMode

    var body: some View {
        HStack {
            ForEach(mode.visibleControls, id: \.self) { control in
                ControlButton(control: control)
                    .transition(.asymmetric(
                        insertion: .scale.combined(with: .opacity),
                        removal: .scale.combined(with: .opacity)
                    ))
            }
        }
        .animation(.spring(response: 0.3), value: mode)
    }
}

3. Sterowanie oparte na gestach

Ekspozycja i ostrość kontrolowane poprzez przeciąganie, jak fizyczne pokrętła aparatu.

KONTROLA EKSPOZYCJI (Przeciągnięcie w pionie w dowolnym miejscu)
           ↑ Przeciągnij w górę = jaśniej
           │
    ───────┼─────── Aktualna ekspozycja
           │
           ↓ Przeciągnij w dół = ciemniej

KONTROLA OSTROŚCI (Przeciągnięcie w poziomie na pokrętle)
    Blisko ←────────●────────→ Daleko
                  │
            Aktualna ostrość

SZYBKIE AKCJE (Przeciągnięcia od krawędzi)
    ← Lewa krawędź: Przełącz do biblioteki zdjęć
    → Prawa krawędź: Otwórz panel kontrolek manualnych

Implementacja:

struct GestureCamera: View {
    @State private var exposure: Double = 0
    @State private var focus: Double = 0.5

    var body: some View {
        ZStack {
            CameraPreview()

            // Gest ekspozycji (w dowolnym miejscu ekranu)
            Color.clear
                .contentShape(Rectangle())
                .gesture(
                    DragGesture()
                        .onChanged { value in
                            // Przesunięcie w pionie mapowane na ekspozycję
                            let delta = -value.translation.height / 500
                            exposure = max(-2, min(2, exposure + delta))
                            HapticsEngine.impact(.light)
                        }
                )

            // Wizualna informacja zwrotna
            VStack {
                Spacer()
                ExposureIndicator(value: exposure)
                    .opacity(exposure != 0 ? 1 : 0)
            }
        }
    }
}

struct ExposureIndicator: View {
    let value: Double

    var body: some View {
        HStack {
            Image(systemName: value > 0 ? "sun.max.fill" : "moon.fill")
            Text(String(format: "%+.1f", value))
                .monospacedDigit()
        }
        .font(.caption)
        .padding(8)
        .background(.ultraThinMaterial)
        .clipShape(Capsule())
    }
}

4. Edukacyjne mikroteksty

Gdy użytkownicy wchodzą w interakcję z kontrolkami, krótkie etykiety uczą ich terminologii.

UŻYTKOWNIK DOTYKA „AF" → Przełącza na „MF"
┌────────────────────────────────┐
│  Ostrość manualna              │  ← Pojawia się na chwilę
│  Przeciągnij, aby dostosować odległość      │
└────────────────────────────────┘
   Znika po 2 sekundach

UŻYTKOWNIK WŁĄCZA FOCUS PEAKING
┌────────────────────────────────┐
│  Focus Peaking                 │
│  Czerwone podświetlenie = ostrość           │
└────────────────────────────────┘

Implementacja:

struct EducationalToast: View {
    let title: String
    let description: String
    @Binding var isVisible: Bool

    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text(title)
                .font(.headline)
            Text(description)
                .font(.caption)
                .foregroundStyle(.secondary)
        }
        .padding()
        .background(.ultraThinMaterial)
        .clipShape(RoundedRectangle(cornerRadius: 12))
        .opacity(isVisible ? 1 : 0)
        .onAppear {
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                withAnimation { isVisible = false }
            }
        }
    }
}

// Użycie
struct FocusModeToggle: View {
    @State private var showToast = false
    @Binding var isManual: Bool

    var body: some View {
        Button(isManual ? "MF" : "AF") {
            isManual.toggle()
            showToast = true
        }
        .overlay(alignment: .top) {
            EducationalToast(
                title: isManual ? "Ostrość manualna" : "Autofokus",
                description: isManual
                    ? "Przeciągnij, aby dostosować odległość"
                    : "Dotknij, aby ustawić ostrość na obiekcie",
                isVisible: $showToast
            )
            .offset(y: -60)
        }
    }
}

5. Profesjonalne narzędzia (Histogram, Focus Peaking, Zebry)

Profesjonalne narzędzia wizualizacji dostępne, ale nieprzytłaczające.

Opcje histogramu:

OPCJE UMIEJSCOWIENIA
┌─────────────────────────────────────────────┐
│ [▁▂▃▅▇█▅▃▁]                                 │  ← Górny róg (minimalny)
│                                             │
│              [Wizjer]                       │
│                                             │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│              [Wizjer]                       │
│                                             │
│   ┌─────────────────────────────────────┐   │
│   │ ▁▂▃▅▇██▅▃▂▁ RGB                     │   │  ← Dół (szczegółowy)
│   └─────────────────────────────────────┘   │
└─────────────────────────────────────────────┘

TYPY HISTOGRAMU (przełączane dotknięciem)
1. Luminancja (monochromatyczny)
2. RGB (kanały kolorów)
3. Waveform (rozkład pionowy)

Nakładka focus peaking:

struct FocusPeakingOverlay: View {
    let enabled: Bool
    let peakingColor: Color = .red

    var body: some View {
        if enabled {
            // W wersji produkcyjnej użyto by shadera Metal
            // Podświetla krawędzie w płaszczyźnie ostrości
            GeometryReader { _ in
                // Nakładka podświetlająca obszary w ostrości
                // Na podstawie detekcji krawędzi przy aktualnej odległości ostrzenia
            }
            .blendMode(.plusLighter)
        }
    }
}

struct ZebraOverlay: View {
    let threshold: Double  // 0.0 do 1.0
    let pattern: ZebraPattern = .diagonal

    enum ZebraPattern {
        case diagonal
        case horizontal
    }

    var body: some View {
        // Nakładka w paski na prześwietlonych obszarach
        // Pomaga zidentyfikować przycinanie przed zrobieniem zdjęcia
    }
}

6. Konfigurowalny pasek narzędzi

Użytkownicy mogą zmieniać układ narzędzi, aby dopasować go do swojego przepływu pracy.

DOMYŚLNY PASEK NARZĘDZI
[Lampa] [Siatka] [Timer] ─(●)─ [RAW] [AF] [Ustawienia]

PRZYTRZYMAJ, ABY DOSTOSOWAĆ
┌─────────────────────────────────────────────┐
│  Przeciągnij, aby zmienić kolejność         │
│                                             │
│  [Lampa]   [Siatka]   [Timer]               │
│     ↕         ↕         ↕                   │
│  [RAW]    [Makro]  [Ustawienia]             │
│                                             │
│  Ukryte:                                    │
│  [Zebra] [Waveform] [Głębia]                │
│                                             │
│  [Przywróć domyślne]          [Gotowe]      │
└─────────────────────────────────────────────┘

Implementacja:

struct CustomizableToolbar: View {
    @State private var tools: [CameraTool] = CameraTool.defaults
    @State private var isEditing = false

    var body: some View {
        HStack {
            ForEach(tools) { tool in
                ToolButton(tool: tool)
                    .draggable(tool) // iOS 16+
            }
        }
        .onLongPressGesture {
            isEditing = true
        }
        .sheet(isPresented: $isEditing) {
            ToolbarEditor(tools: $tools)
        }
    }
}

struct ToolbarEditor: View {
    @Binding var tools: [CameraTool]

    var body: some View {
        NavigationStack {
            List {
                Section("Aktywne narzędzia") {
                    ForEach($tools) { $tool in
                        ToolRow(tool: tool)
                    }
                    .onMove { from, to in
                        tools.move(fromOffsets: from, toOffset: to)
                    }
                }

                Section("Dostępne narzędzia") {
                    ForEach(CameraTool.hidden(from: tools)) { tool in
                        ToolRow(tool: tool)
                            .onTapGesture {
                                tools.append(tool)
                            }
                    }
                }
            }
            .navigationTitle("Dostosuj pasek narzędzi")
            .toolbar {
                Button("Gotowe") { /* zamknij */ }
            }
        }
    }
}

System projektowania wizualnego

Paleta kolorów

extension Color {
    // Halide używa minimalnej palety kolorów dla elementów UI
    static let halideBlack = Color(hex: "#000000")
    static let halideWhite = Color(hex: "#FFFFFF")
    static let halideGray = Color(hex: "#8E8E93")

    // Akcent dla stanów aktywnych
    static let halideYellow = Color(hex: "#FFD60A")  // Aktywne wskaźniki

    // Focus peaking
    static let halideFocusPeak = Color(hex: "#FF3B30")  // Czerwona nakładka
    static let halideZebra = Color(hex: "#FFFFFF").opacity(0.5)
}

Typografia

struct HalideTypography {
    // Etykiety UI (małe, kapitaliki)
    static let controlLabel = Font.system(size: 10, weight: .bold, design: .rounded)
        .smallCaps()

    // Wartości (monospace dla liczb)
    static let valueDisplay = Font.system(size: 14, weight: .medium, design: .monospaced)

    // Podpowiedzi edukacyjne
    static let tooltipTitle = Font.system(size: 14, weight: .semibold)
    static let tooltipBody = Font.system(size: 12, weight: .regular)
}

Animacje i wibracje haptyczne

System informacji zwrotnej haptycznej

struct HapticsEngine {
    static func impact(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
        let generator = UIImpactFeedbackGenerator(style: style)
        generator.impactOccurred()
    }

    static func exposureChange() {
        // Lekka wibracja przy zmianie ekspozycji
        impact(.light)
    }

    static func focusLock() {
        // Średnia wibracja przy zablokowaniu ostrości
        impact(.medium)
    }

    static func shutterPress() {
        // Mocna wibracja dla migawki
        impact(.heavy)
    }

    static func modeSwitch() {
        // Informacja zwrotna wyboru przy zmianie trybu
        let generator = UISelectionFeedbackGenerator()
        generator.selectionChanged()
    }
}

Animacje kontrolek

// Animacja obrotu pokrętła
struct FocusDial: View {
    @Binding var value: Double

    var body: some View {
        GeometryReader { geo in
            // Wizualizacja pokrętła ze znacznikami
            Circle()
                .stroke(Color.white.opacity(0.3), lineWidth: 2)
                .overlay {
                    // Znaczniki obracają się wraz z wartością
                    ForEach(0..<12) { i in
                        Rectangle()
                            .fill(Color.white)
                            .frame(width: 2, height: 10)
                            .offset(y: -geo.size.height / 2 + 15)
                            .rotationEffect(.degrees(Double(i) * 30))
                    }
                }
                .rotationEffect(.degrees(value * 360))
                .animation(.spring(response: 0.2, dampingFraction: 0.8), value: value)
        }
    }
}

Wnioski dla naszej pracy

1. Ukrywaj złożoność, nie usuwaj jej

Profesjonalne funkcje istnieją, ale pozostają poza zasięgiem wzroku, dopóki nie zostaną wywołane.

2. Inteligentna aktywacja > Przyciski przełączające

Narzędzia pojawiające się podczas interakcji (lupa ostrości podczas przeciągania) wygrywają z ręcznym pokazywaniem/ukrywaniem.

3. Gesty powinny być fizyczne w odczuciu

Przeciągnięcie w pionie dla ekspozycji, w poziomie dla ostrości — odwzorowuje prawdziwe pokrętła aparatu.

4. Ucz podczas użytkowania

Krótkie etykiety przy zmianach stanu pomagają użytkownikom naturalnie poznawać terminologię.

5. Obsługa jedną ręką to ograniczenie projektowe

Projektowanie z myślą o zasięgu kciuka wymusza dobrą hierarchię informacji.


Najczęściej zadawane pytania

Czym Halide różni się od innych profesjonalnych aplikacji aparatu?

Większość profesjonalnych aplikacji aparatu wyświetla wszystkie kontrolki jednocześnie, tworząc to, co twórcy Halide nazywają interfejsami „symulatorów lotu". Halide zaczyna od czystego wizjera i ujawnia kontrolki tylko wtedy, gdy są potrzebne. Tryb automatyczny pokazuje minimalne UI; przełączenie na tryb manualny stopniowo odsłania histogram, focus peaking, ISO i kontrolki balansu bieli. To podejście sprawia, że funkcje na poziomie lustrzanek są dostępne bez przytłaczania nowych użytkowników.

Czym jest inteligentna aktywacja i dlaczego ma znaczenie?

Inteligentna aktywacja oznacza, że narzędzia pojawiają się automatycznie, gdy wchodzisz w interakcję z powiązanymi kontrolkami, a następnie znikają, gdy przestajesz. Na przykład lupa ostrości pojawia się, gdy dotkniesz pokrętła ostrości, i zanika, gdy je puścisz. Eliminuje to potrzebę przycisków przełączających do pokazywania/ukrywania widoków pomocniczych, redukując wizualny bałagan i obciążenie poznawcze, jednocześnie zapewniając, że narzędzia są zawsze dostępne, gdy są potrzebne.

Jak działają gesty sterowania w Halide?

Halide mapuje gesty na ekranie dotykowym na fizyczne kontrolki aparatu. Przeciągnięcie w pionie w dowolnym miejscu ekranu dostosowuje ekspozycję (jak pierścień przysłony), przeciągnięcie w poziomie na pokrętle ostrości dostosowuje odległość ostrzenia (jak pierścień ostrości). Te gesty obejmują informację zwrotną haptyczną, która naśladuje mechaniczne rastry prawdziwych pokręteł aparatu. Przeciągnięcia od krawędzi zapewniają szybki dostęp do biblioteki zdjęć i panelu kontrolek manualnych.

Jakie profesjonalne narzędzia wizualizacji oferuje Halide?

Halide oferuje histogram (tryby luminancji, RGB lub waveform), focus peaking (czerwone podświetlenie na ostrych krawędziach) i zebry (ukośne paski na prześwietlonych obszarach). Te narzędzia pojawiają się jako nakładki na wizjerze i mogą być umieszczone w różnych lokalizacjach. Każde narzędzie jest opcjonalne i dostępne z konfigurowalnego paska narzędzi, który użytkownicy mogą przestawiać, aby dopasować do swojego przepływu pracy.

Dlaczego Halide używa metafor z aparatów analogowych w swoim interfejsie?

Projektanci Halide zaobserwowali, że dzieci instynktownie bawią się pierścieniami przysłony, pokrętłami i przełącznikami aparatów analogowych. Ta dotykowa radość jest nieobecna w aplikacjach na ekrany dotykowe. Tłumacząc mechaniczne kontrolki aparatu na gesty przeciągania z informacją zwrotną haptyczną, Halide odtwarza część tej fizyczności. Podejście to wykorzystuje również dekady ewolucji UX aparatów, używając znanych modeli mentalnych zamiast wymyślania nowych paradygmatów.