Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Hinge

Give your MacBook desktop a little bend. Close the lid and watch your screen softly fold and blur. Open it and everything comes back.
Give your MacBook desktop a little bend. Choose the original Classic fold or Liquid Edge, a responsive glass band that ripples across the top of the display as the lid closes. Open it and everything comes back.

## For the nerds

Hinge reads the lid angle as often as the sensor updates it, in hundredths of a degree where your Mac reports them, and turns it into a continuous animation. Slow tilt, slow bend. Quick tilt, quick bend. A little smoothing fills in between readings.

ScreenCaptureKit supplies your live desktop, and Metal adds perspective and progressive blur at 60 fps. Everything stays in memory on your Mac. No recordings, no uploads.
ScreenCaptureKit supplies your live desktop, and Metal renders either perspective with progressive blur or a localized refractive edge at 60 fps. Everything stays in memory on your Mac. No recordings, no uploads.

## Install

Expand Down
47 changes: 47 additions & 0 deletions Resources/Fold.metal
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ struct FoldParameters {
float taper;
float crop;
float depth;
float velocity;
float ripple;
float time;
float mode;
};

struct FoldVertex {
Expand All @@ -33,6 +37,49 @@ fragment float4 foldFragment(FoldVertex in [[stage_in]],
texture2d<float> sides [[texture(4)]],
constant FoldParameters &p [[buffer(0)]]) {
constexpr sampler sampleMode(coord::normalized, address::clamp_to_edge, filter::linear);
if (p.mode >= 0.5) {
float2 uv = in.uv;
float entrance = smoothstep(0.0, 0.045, p.progress);
float edgeProgress = smoothstep(0.0, 0.45, p.progress);
float depth = mix(0.035, 0.42, edgeProgress);
float lensWidth = 0.044 + p.ripple * 0.030;
float distanceToFront = abs(uv.y - depth);
float front = 1.0 - smoothstep(0.0, lensWidth, distanceToFront);
float body = 1.0 - smoothstep(depth - 0.025, depth + 0.025, uv.y);

float wavePhase = p.time * (8.0 + abs(p.velocity) * 4.0);
float settledWave = sin(uv.x * 20.0 + p.progress * 5.0);
float movingWave = sin(uv.x * 34.0 + wavePhase) * p.ripple;
float edgeSafety = smoothstep(0.0, 0.025, min(uv.x, 1.0 - uv.x));
float influence = max(front, body * 0.32) * entrance * edgeSafety;
float2 normal = float2(
settledWave * 0.32 + movingWave * 0.70 + (uv.x - 0.5) * 0.16,
-0.18 + cos(uv.x * 17.0 - wavePhase) * p.ripple * 0.30);
float2 offset = normal * influence * (0.0045 + p.ripple * 0.0065);
float2 glassUV = clamp(uv + offset, float2(0.0), float2(1.0));
float2 dispersion = normal * influence * (0.0007 + p.ripple * 0.0008);

float3 color;
color.r = source.sample(
sampleMode, clamp(glassUV + dispersion, float2(0.0), float2(1.0))).r;
color.g = source.sample(sampleMode, glassUV).g;
color.b = source.sample(
sampleMode, clamp(glassUV - dispersion, float2(0.0), float2(1.0))).b;

float2 blurUV = float2(p.blurInset + uv.x * p.blurSpan, uv.y);
float3 blurred = soft.sample(sampleMode, blurUV).rgb;
color = mix(color, blurred, influence * (0.10 + p.progress * 0.16));

float caustic = 0.5 + 0.5 * sin(uv.x * 25.0 + settledWave * 1.4);
float highlight = front * (0.16 + caustic * 0.13 + p.ripple * 0.12);
float3 glassTint = float3(0.76, 0.91, 1.0);
color += glassTint * highlight * entrance;

float bodyAlpha = body * (0.28 + edgeProgress * 0.32);
float alpha = p.opacity * entrance * max(bodyAlpha, front * 0.94);
return float4(color * alpha, alpha);
}

float turn = p.progress * 1.5707964;
float taper = p.taper * p.progress;
float q = (1.0 + taper) / (1.0 + taper * in.uv.y);
Expand Down
6 changes: 6 additions & 0 deletions Resources/Localizations/ar.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@
"Your desktop bends as the lid closes." = "ينحني سطح المكتب عند إغلاق الغطاء.";
"Your desktop follows your lid." = "سطح المكتب يتبع الغطاء.";
"⌃⌥H anywhere" = "⌃⌥H من أي مكان";
"A liquid glass edge follows the lid." = "حافة زجاج سائل تتبع الشاشة.";
"A responsive liquid glass band along the top edge" = "شريط زجاج سائل متجاوب على طول الحافة العلوية";
"Classic" = "كلاسيكي";
"Effect" = "التأثير";
"Liquid Edge" = "الحافة السائلة";
"The original fold and progressive blur" = "الطي الأصلي والتمويه التدريجي";
6 changes: 6 additions & 0 deletions Resources/Localizations/de.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@
"Your desktop bends as the lid closes." = "Dein Schreibtisch biegt sich beim Zuklappen.";
"Your desktop follows your lid." = "Dein Schreibtisch folgt dem Deckel.";
"⌃⌥H anywhere" = "⌃⌥H funktioniert überall";
"A liquid glass edge follows the lid." = "Eine Flüssigglaskante folgt dem Displaydeckel.";
"A responsive liquid glass band along the top edge" = "Ein reaktionsschnelles Flüssigglasband am oberen Rand";
"Classic" = "Klassisch";
"Effect" = "Effekt";
"Liquid Edge" = "Flüssige Kante";
"The original fold and progressive blur" = "Der ursprüngliche Falteffekt mit zunehmender Weichzeichnung";
6 changes: 6 additions & 0 deletions Resources/Localizations/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,9 @@
"Your desktop follows your lid." = "Your desktop follows your lid.";
"⌃⌥H anywhere" = "⌃⌥H anywhere";
"⌃⌥H" = "⌃⌥H";
"A liquid glass edge follows the lid." = "A liquid glass edge follows the lid.";
"A responsive liquid glass band along the top edge" = "A responsive liquid glass band along the top edge";
"Classic" = "Classic";
"Effect" = "Effect";
"Liquid Edge" = "Liquid Edge";
"The original fold and progressive blur" = "The original fold and progressive blur";
6 changes: 6 additions & 0 deletions Resources/Localizations/es.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@
"Your desktop bends as the lid closes." = "Tu escritorio se dobla al cerrar la tapa.";
"Your desktop follows your lid." = "Tu escritorio sigue a la tapa.";
"⌃⌥H anywhere" = "⌃⌥H desde cualquier lugar";
"A liquid glass edge follows the lid." = "Un borde de cristal líquido sigue la tapa.";
"A responsive liquid glass band along the top edge" = "Una franja de cristal líquido reactiva en el borde superior";
"Classic" = "Clásico";
"Effect" = "Efecto";
"Liquid Edge" = "Borde líquido";
"The original fold and progressive blur" = "El plegado original y el desenfoque progresivo";
6 changes: 6 additions & 0 deletions Resources/Localizations/fr.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@
"Your desktop bends as the lid closes." = "Votre bureau se plie quand vous rabattez l’écran.";
"Your desktop follows your lid." = "Votre bureau suit l’écran.";
"⌃⌥H anywhere" = "⌃⌥H depuis n’importe où";
"A liquid glass edge follows the lid." = "Un bord en verre liquide suit l’écran.";
"A responsive liquid glass band along the top edge" = "Une bande de verre liquide réactive le long du bord supérieur";
"Classic" = "Classique";
"Effect" = "Effet";
"Liquid Edge" = "Bord liquide";
"The original fold and progressive blur" = "Le pliage original et le flou progressif";
6 changes: 6 additions & 0 deletions Resources/Localizations/hi.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,9 @@
"Your desktop follows your lid." = "आपका डेस्कटॉप ढक्कन की हलचल के साथ बदलता है।";
"⌃⌥H anywhere" = "कहीं भी ⌃⌥H";
"⌃⌥H" = "⌃⌥H";
"A liquid glass edge follows the lid." = "लिक्विड ग्लास किनारा स्क्रीन के साथ चलता है।";
"A responsive liquid glass band along the top edge" = "ऊपरी किनारे पर प्रतिक्रिया देने वाली लिक्विड ग्लास पट्टी";
"Classic" = "क्लासिक";
"Effect" = "प्रभाव";
"Liquid Edge" = "लिक्विड एज";
"The original fold and progressive blur" = "मूल फोल्ड और क्रमिक धुंधलापन";
6 changes: 6 additions & 0 deletions Resources/Localizations/it.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@
"Your desktop bends as the lid closes." = "La scrivania si piega quando chiudi lo schermo.";
"Your desktop follows your lid." = "La tua scrivania segue lo schermo.";
"⌃⌥H anywhere" = "⌃⌥H ovunque";
"A liquid glass edge follows the lid." = "Un bordo in vetro liquido segue lo schermo.";
"A responsive liquid glass band along the top edge" = "Una fascia reattiva in vetro liquido lungo il bordo superiore";
"Classic" = "Classico";
"Effect" = "Effetto";
"Liquid Edge" = "Bordo liquido";
"The original fold and progressive blur" = "La piega originale e la sfocatura progressiva";
6 changes: 6 additions & 0 deletions Resources/Localizations/ja.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@
"Your desktop bends as the lid closes." = "ふたを閉じるとデスクトップが折れ曲がります。";
"Your desktop follows your lid." = "デスクトップがふたの動きに追従します。";
"⌃⌥H anywhere" = "どこからでも⌃⌥H";
"A liquid glass edge follows the lid." = "液体ガラスのエッジが画面の開閉に追従します。";
"A responsive liquid glass band along the top edge" = "上端に沿って反応する液体ガラスの帯";
"Classic" = "クラシック";
"Effect" = "エフェクト";
"Liquid Edge" = "リキッドエッジ";
"The original fold and progressive blur" = "オリジナルの折りたたみと段階的なぼかし";
6 changes: 6 additions & 0 deletions Resources/Localizations/ko.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@
"Your desktop bends as the lid closes." = "덮개를 닫으면 데스크탑이 휘어집니다.";
"Your desktop follows your lid." = "데스크탑이 덮개를 따라 움직입니다.";
"⌃⌥H anywhere" = "어디서나 ⌃⌥H";
"A liquid glass edge follows the lid." = "리퀴드 글래스 가장자리가 화면을 따라 움직입니다.";
"A responsive liquid glass band along the top edge" = "상단 가장자리를 따라 반응하는 리퀴드 글래스 띠";
"Classic" = "클래식";
"Effect" = "효과";
"Liquid Edge" = "리퀴드 엣지";
"The original fold and progressive blur" = "원래 접기 효과와 점진적 흐림";
6 changes: 6 additions & 0 deletions Resources/Localizations/nl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@
"Your desktop bends as the lid closes." = "Je bureaublad buigt mee als de klep dichtgaat.";
"Your desktop follows your lid." = "Je bureaublad volgt de klep.";
"⌃⌥H anywhere" = "⌃⌥H werkt overal";
"A liquid glass edge follows the lid." = "Een rand van vloeibaar glas volgt het scherm.";
"A responsive liquid glass band along the top edge" = "Een reagerende strook vloeibaar glas langs de bovenrand";
"Classic" = "Klassiek";
"Effect" = "Effect";
"Liquid Edge" = "Vloeibare rand";
"The original fold and progressive blur" = "De oorspronkelijke vouw en geleidelijke vervaging";
6 changes: 6 additions & 0 deletions Resources/Localizations/pl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@
"Your desktop bends as the lid closes." = "Biurko wygina się, gdy zamykasz pokrywę.";
"Your desktop follows your lid." = "Twoje biurko podąża za pokrywą.";
"⌃⌥H anywhere" = "⌃⌥H działa wszędzie";
"A liquid glass edge follows the lid." = "Krawędź płynnego szkła podąża za ekranem.";
"A responsive liquid glass band along the top edge" = "Reagujący pasek płynnego szkła wzdłuż górnej krawędzi";
"Classic" = "Klasyczny";
"Effect" = "Efekt";
"Liquid Edge" = "Płynna krawędź";
"The original fold and progressive blur" = "Oryginalne zagięcie i stopniowe rozmycie";
6 changes: 6 additions & 0 deletions Resources/Localizations/pt-BR.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@
"Your desktop bends as the lid closes." = "Sua mesa se dobra quando a tampa fecha.";
"Your desktop follows your lid." = "Sua mesa acompanha a tampa.";
"⌃⌥H anywhere" = "⌃⌥H em qualquer lugar";
"A liquid glass edge follows the lid." = "Uma borda de vidro líquido acompanha a tela.";
"A responsive liquid glass band along the top edge" = "Uma faixa responsiva de vidro líquido ao longo da borda superior";
"Classic" = "Clássico";
"Effect" = "Efeito";
"Liquid Edge" = "Borda líquida";
"The original fold and progressive blur" = "A dobra original e o desfoque progressivo";
6 changes: 6 additions & 0 deletions Resources/Localizations/ru.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@
"Your desktop bends as the lid closes." = "Рабочий стол изгибается, когда вы закрываете крышку.";
"Your desktop follows your lid." = "Рабочий стол следует за крышкой.";
"⌃⌥H anywhere" = "⌃⌥H работает везде";
"A liquid glass edge follows the lid." = "Край из жидкого стекла следует за экраном.";
"A responsive liquid glass band along the top edge" = "Интерактивная полоса жидкого стекла вдоль верхнего края";
"Classic" = "Классический";
"Effect" = "Эффект";
"Liquid Edge" = "Жидкий край";
"The original fold and progressive blur" = "Исходное складывание и постепенное размытие";
6 changes: 6 additions & 0 deletions Resources/Localizations/tr.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@
"Your desktop bends as the lid closes." = "Kapak kapanırken masaüstünüz bükülür.";
"Your desktop follows your lid." = "Masaüstünüz kapağınızı takip eder.";
"⌃⌥H anywhere" = "Her yerden ⌃⌥H";
"A liquid glass edge follows the lid." = "Sıvı cam kenarı ekranı takip eder.";
"A responsive liquid glass band along the top edge" = "Üst kenar boyunca tepki veren sıvı cam şeridi";
"Classic" = "Klasik";
"Effect" = "Efekt";
"Liquid Edge" = "Sıvı Kenar";
"The original fold and progressive blur" = "Orijinal katlanma ve kademeli bulanıklık";
6 changes: 6 additions & 0 deletions Resources/Localizations/vi.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@
"Your desktop bends as the lid closes." = "Màn hình nền uốn cong khi bạn gập nắp.";
"Your desktop follows your lid." = "Màn hình nền chuyển động theo nắp máy.";
"⌃⌥H anywhere" = "⌃⌥H ở bất kỳ đâu";
"A liquid glass edge follows the lid." = "Viền kính lỏng chuyển động theo màn hình.";
"A responsive liquid glass band along the top edge" = "Dải kính lỏng phản hồi dọc theo cạnh trên";
"Classic" = "Cổ điển";
"Effect" = "Hiệu ứng";
"Liquid Edge" = "Viền lỏng";
"The original fold and progressive blur" = "Hiệu ứng gập nguyên bản và làm mờ dần";
6 changes: 6 additions & 0 deletions Resources/Localizations/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@
"Your desktop bends as the lid closes." = "合上屏幕时桌面会随之弯折。";
"Your desktop follows your lid." = "你的桌面随屏幕开合而动。";
"⌃⌥H anywhere" = "随时按 ⌃⌥H";
"A liquid glass edge follows the lid." = "液态玻璃边缘随屏幕开合而移动。";
"A responsive liquid glass band along the top edge" = "沿屏幕顶部响应开合的液态玻璃带";
"Classic" = "经典";
"Effect" = "效果";
"Liquid Edge" = "液态边缘";
"The original fold and progressive blur" = "原始折叠与渐进模糊";
6 changes: 6 additions & 0 deletions Resources/Localizations/zh-Hant.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@
"Your desktop bends as the lid closes." = "闔上螢幕時桌面會隨之彎折。";
"Your desktop follows your lid." = "你的桌面隨螢幕開合而動。";
"⌃⌥H anywhere" = "隨時按 ⌃⌥H";
"A liquid glass edge follows the lid." = "液態玻璃邊緣隨螢幕開合而移動。";
"A responsive liquid glass band along the top edge" = "沿螢幕頂部響應開合的液態玻璃帶";
"Classic" = "經典";
"Effect" = "效果";
"Liquid Edge" = "液態邊緣";
"The original fold and progressive blur" = "原始摺疊與漸進模糊";
49 changes: 42 additions & 7 deletions Sources/DesktopRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,40 @@ struct FoldParameters {
var taper = DesktopRenderer.taper
var crop: Float = 0
var depth: Float = 0
var velocity: Float = 0
var ripple: Float = 0
var time: Float = 0
var mode: Float = 1
}

enum SideFill: String {
case blur
case black
}

enum FoldMode: String, CaseIterable, Identifiable {
case classic
case liquidEdge

var id: String { rawValue }

var title: String {
switch self {
case .classic: String(localized: "Classic")
case .liquidEdge: String(localized: "Liquid Edge")
}
}

var subtitle: String {
switch self {
case .classic: String(localized: "The original fold and progressive blur")
case .liquidEdge: String(localized: "A responsive liquid glass band along the top edge")
}
}

var shaderValue: Float { self == .classic ? 0 : 1 }
}

final class DesktopRenderer: NSObject, MTKViewDelegate {
static let taper: Float = 0.30
let device: MTLDevice
Expand All @@ -41,6 +68,7 @@ final class DesktopRenderer: NSObject, MTKViewDelegate {
private let motion: LidMotion
private var wasPresented = false
var effectStrength: Float = 1
var foldMode = FoldMode.liquidEdge
var cropsTop = true
var blursByDistance = true
var sideFill = SideFill.blur {
Expand Down Expand Up @@ -127,7 +155,9 @@ final class DesktopRenderer: NSObject, MTKViewDelegate {
pass.colorAttachments[0].loadAction = .clear
pass.colorAttachments[0].storeAction = .store
guard
self.encodeFold(command: command, pass: pass, texture: source, progress: 0.5, opacity: 1)
self.encodeFold(
command: command, pass: pass, texture: source,
sample: FoldSample(progress: 0.5, velocity: 0, ripple: 0), opacity: 1)
else {
throw DesktopError.message(String(localized: "Could not prepare the fold pipeline."))
}
Expand Down Expand Up @@ -235,7 +265,9 @@ final class DesktopRenderer: NSObject, MTKViewDelegate {
func draw(in view: MTKView) {
let time = presentationTime ?? CACurrentMediaTime()
presentationTime = nil
let progress = motion.sample(at: time) * effectStrength
var sample = motion.sample(at: time)
sample.progress *= effectStrength
let progress = sample.progress
guard progress > 0 else {
clear(view)
return
Expand Down Expand Up @@ -263,10 +295,10 @@ final class DesktopRenderer: NSObject, MTKViewDelegate {
}
let blend = min(progress / 0.025, 1)
let opacity = blend * blend * (3 - 2 * blend)
pass.colorAttachments[0].clearColor = MTLClearColorMake(0, 0, 0, Double(opacity))
pass.colorAttachments[0].clearColor = MTLClearColorMake(0, 0, 0, 0)
guard
encodeFold(
command: command, pass: pass, texture: texture, progress: progress, opacity: opacity)
command: command, pass: pass, texture: texture, sample: sample, opacity: opacity)
else {
blurredGeneration = nil
inFlight.signal()
Expand All @@ -291,15 +323,18 @@ final class DesktopRenderer: NSObject, MTKViewDelegate {
}

private func encodeFold(
command: MTLCommandBuffer, pass: MTLRenderPassDescriptor, texture: MTLTexture, progress: Float,
command: MTLCommandBuffer, pass: MTLRenderPassDescriptor, texture: MTLTexture,
sample: FoldSample,
opacity: Float
) -> Bool {
guard let encoder = command.makeRenderCommandEncoder(descriptor: pass) else { return false }
let paddedWidth = Float(sideTexture?.width ?? 1)
var parameters = FoldParameters(
progress: progress, opacity: opacity, blurInset: Float(blurPadding) / paddedWidth,
progress: sample.progress, opacity: opacity, blurInset: Float(blurPadding) / paddedWidth,
blurSpan: (paddedWidth - Float(2 * blurPadding)) / paddedWidth, crop: cropsTop ? 1 : 0,
depth: blursByDistance ? 1 : 0)
depth: blursByDistance ? 1 : 0, velocity: sample.velocity, ripple: sample.ripple,
time: Float(CACurrentMediaTime().truncatingRemainder(dividingBy: 1000)),
mode: foldMode.shaderValue)
encoder.setRenderPipelineState(pipeline)
encoder.setVertexBytes(&parameters, length: MemoryLayout<FoldParameters>.stride, index: 0)
encoder.setFragmentBytes(&parameters, length: MemoryLayout<FoldParameters>.stride, index: 0)
Expand Down
Loading