Skip to content
Merged
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 MOTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ The final portion of that video shows a separate website miniature. Its 72-degre

## Current reconstruction

The new projection keeps the top and bottom edges at their original height. Its homogeneous horizontal taper grows from zero to a maximum top-edge inset of approximately 11.5 percent on each side. This is a visual approximation of the native footage, not a recovered native coefficient.
The new projection keeps the top and bottom edges at their original height. Its homogeneous horizontal taper grows from zero to a maximum top-edge inset of approximately 11.5 percent on each side. This is a visual approximation of the native footage, not a recovered native coefficient. Crop from the top, on by default in Settings, also shortens the desktop toward the hinge the way a closing lid shortens when seen from the front: the bottom row stays at the hinge and the shader samples only the lower cos(0.65 x 90 degrees x progress) of the desktop, so the top slides out of view. The full cosine read as too much stretch from a normal seat above the screen, so the angle is scaled by 0.65.

Three cached Gaussian blur levels at nominal widths of 6, 16, and 36 pixels per 786-pixel reference width provide continuous progressive blur. Blur strength varies with closure and fades toward the lower tenth of the desktop. Subtle top-corner shading and side feathering complete the single effect. The side gaps are filled with the nearest desktop edge, stretched from a broad blur of the reduced frame, or with black, as chosen in Settings. The fill and the desktop are blurred as one surface: every blur level is computed from a composite that places the fill in a side margin around the reduced frame, so blurred content spreads across the fold's edge toward the top while the lower edge stays sharp. Each captured frame adds one scale and one copy into that composite. The blurred fill also runs a quarter-size broad blur and one stretch; the black fill clears the margin instead. On an M1 Max the blurred fill's chain measured about 0.2 ms per captured frame, up from 0.11 ms. An inverse projection in the fragment shader preserves the fold geometry while covering the full overlay.
Three cached Gaussian blur levels at nominal widths of 6, 16, and 36 pixels per 786-pixel reference width provide continuous progressive blur. With Blur by distance, on by default, blur strength follows each point's distance from where the open screen stood: its height above the hinge times sin(90 degrees x progress), so the hinge stays sharp and the top edge of a fully folded screen reaches the 36-pixel level. With it off, blur strength varies with closure and fades toward the lower tenth of the desktop. Subtle top-corner shading and side feathering complete the single effect. The side gaps are filled with the nearest desktop edge, stretched from a broad blur of the reduced frame, or with black, as chosen in Settings. The fill and the desktop are blurred as one surface: every blur level is computed from a composite that places the fill in a side margin around the reduced frame, so blurred content spreads across the fold's edge toward the top while the lower edge stays sharp. Each captured frame adds one scale and one copy into that composite. The blurred fill also runs a quarter-size broad blur and one stretch; the black fill clears the margin instead. On an M1 Max the blurred fill's chain measured about 0.2 ms per captured frame, up from 0.11 ms. An inverse projection in the fragment shader preserves the fold geometry while covering the full overlay.

The effect covers the built-in screen's full display frame, including the menu bar and Dock. A non-activating panel joins fullscreen Spaces without taking keyboard focus. Space changes keep the same capture area and bring the panel back to the front.

Expand Down
13 changes: 9 additions & 4 deletions Resources/Fold.metal
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ struct FoldParameters {
float blurInset;
float blurSpan;
float taper;
float crop;
float depth;
};

struct FoldVertex {
Expand All @@ -31,14 +33,17 @@ 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);
float turn = p.progress * 1.5707964;
float taper = p.taper * p.progress;
float q = (1.0 + taper) / (1.0 + taper * in.uv.y);
float2 uv = float2((in.uv.x - 0.5) * q + 0.5, in.uv.y * q);
float2 fold = float2((in.uv.x - 0.5) * q + 0.5, in.uv.y * q);
float2 uv = float2(fold.x, 1.0 - mix(1.0, cos(turn * 0.65), p.crop) * (1.0 - fold.y));
float edge = min(uv.x, 1.0 - uv.x);
float2 blurUV = float2(p.blurInset + uv.x * p.blurSpan, uv.y);
float feather = smoothstep(0.0, max(0.0001, p.progress * 0.012 * (1.0 - uv.y)), edge);
float feather = smoothstep(0.0, max(0.0001, p.progress * 0.012 * (1.0 - fold.y)), edge);
float3 sharp = mix(sides.sample(sampleMode, blurUV).rgb, source.sample(sampleMode, uv).rgb, feather);
float amount = 36.0 * p.progress * (1.0 - smoothstep(0.0, 0.9, uv.y));
float falloff = p.progress * (1.0 - smoothstep(0.0, 0.9, fold.y));
float amount = 36.0 * mix(falloff, sin(turn) * (1.0 - in.uv.y), p.depth);
float3 color;
if (amount < 6.0) {
color = mix(sharp, soft.sample(sampleMode, blurUV).rgb, amount / 6.0);
Expand All @@ -47,7 +52,7 @@ fragment float4 foldFragment(FoldVertex in [[stage_in]],
} else {
color = mix(medium.sample(sampleMode, blurUV).rgb, broad.sample(sampleMode, blurUV).rgb, (amount - 16.0) / 20.0);
}
float upper = 1.0 - smoothstep(0.0, 0.85, uv.y);
float upper = 1.0 - smoothstep(0.0, 0.85, fold.y);
float corners = (1.0 - smoothstep(0.0, 0.19, edge)) * upper;
color *= 1.0 - p.progress * (0.50 * corners + 0.10 * upper);
return float4(color * p.opacity, p.opacity);
Expand Down
7 changes: 6 additions & 1 deletion Sources/DesktopRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ struct FoldParameters {
var blurInset: Float = 0
var blurSpan: Float = 1
var taper = DesktopRenderer.taper
var crop: Float = 0
var depth: Float = 0
}

enum SideFill: String {
Expand Down Expand Up @@ -39,6 +41,8 @@ final class DesktopRenderer: NSObject, MTKViewDelegate {
private let motion: LidMotion
private var wasPresented = false
var effectStrength: Float = 1
var cropsTop = true
var blursByDistance = true
var sideFill = SideFill.blur {
didSet { if sideFill != oldValue { blurredGeneration = nil } }
}
Expand Down Expand Up @@ -292,7 +296,8 @@ final class DesktopRenderer: NSObject, MTKViewDelegate {
let paddedWidth = Float(sideTexture?.width ?? 1)
var parameters = FoldParameters(
progress: progress, opacity: opacity, blurInset: Float(blurPadding) / paddedWidth,
blurSpan: (paddedWidth - Float(2 * blurPadding)) / paddedWidth)
blurSpan: (paddedWidth - Float(2 * blurPadding)) / paddedWidth, crop: cropsTop ? 1 : 0,
depth: blursByDistance ? 1 : 0)
encoder.setRenderPipelineState(pipeline)
encoder.setVertexBytes(&parameters, length: MemoryLayout<FoldParameters>.stride, index: 0)
encoder.setFragmentBytes(&parameters, length: MemoryLayout<FoldParameters>.stride, index: 0)
Expand Down
18 changes: 18 additions & 0 deletions Sources/LiveDesktop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ final class LiveDesktop: NSObject, ObservableObject {
@Published private(set) var openAngle: Double
@Published private(set) var effectStrength: Double
@Published private(set) var sideFill: SideFill
@Published private(set) var cropsTop: Bool
@Published private(set) var blursByDistance: Bool
@Published private(set) var error: String?
@Published private(set) var needsPermission = false
@Published private(set) var isEnabled = UserDefaults.standard.bool(forKey: "effectEnabled")
Expand Down Expand Up @@ -70,6 +72,8 @@ final class LiveDesktop: NSObject, ObservableObject {
self.openAngle = openAngle
self.effectStrength = effectStrength
sideFill = UserDefaults.standard.string(forKey: "sideFill").flatMap(SideFill.init) ?? .blur
cropsTop = UserDefaults.standard.object(forKey: "cropsTop") as? Bool ?? true
blursByDistance = UserDefaults.standard.object(forKey: "blursByDistance") as? Bool ?? true
motion = LidMotion(openAngle: openAngle)
super.init()
let motion = motion
Expand Down Expand Up @@ -174,6 +178,18 @@ final class LiveDesktop: NSObject, ObservableObject {
renderer?.sideFill = fill
}

func setCropsTop(_ enabled: Bool) {
cropsTop = enabled
UserDefaults.standard.set(enabled, forKey: "cropsTop")
renderer?.cropsTop = enabled
}

func setBlursByDistance(_ enabled: Bool) {
blursByDistance = enabled
UserDefaults.standard.set(enabled, forKey: "blursByDistance")
renderer?.blursByDistance = enabled
}

func start(promptForPermission: Bool = true) async {
guard !isStarting, !isActive else { return }
error = nil
Expand All @@ -198,6 +214,8 @@ final class LiveDesktop: NSObject, ObservableObject {
let renderer = try DesktopRenderer(resources: .main, motion: motion)
renderer.effectStrength = Float(effectStrength)
renderer.sideFill = sideFill
renderer.cropsTop = cropsTop
renderer.blursByDistance = blursByDistance
let content = try await SCShareableContent.excludingDesktopWindows(
false, onScreenWindowsOnly: false)
guard self.session == session else { return }
Expand Down
28 changes: 28 additions & 0 deletions Sources/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,34 @@ struct SettingsView: View {
.fixedSize()
.controlSize(.small)
}
SettingsDivider()
SettingsRow(
"crop", tint: .teal, title: "Crop from the top",
subtitle: "The top of the desktop slides out of view as the lid closes."
) {
Toggle(
"Crop from the top",
isOn: Binding(get: { desktop.cropsTop }, set: { desktop.setCropsTop($0) })
)
.toggleStyle(.switch)
.controlSize(.small)
.labelsHidden()
}
SettingsDivider()
SettingsRow(
"camera.aperture", tint: .purple, title: "Blur by distance",
subtitle: desktop.blursByDistance
? "Blur grows with distance from the open screen, so the hinge edge stays sharp."
: "Blur builds toward the top and fades out near the hinge."
) {
Toggle(
"Blur by distance",
isOn: Binding(get: { desktop.blursByDistance }, set: { desktop.setBlursByDistance($0) })
)
.toggleStyle(.switch)
.controlSize(.small)
.labelsHidden()
}
}
}

Expand Down