Skip to content
Open
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: 1 addition & 3 deletions Reframed/Compositor/FrameRenderer+Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,9 @@ extension FrameRenderer {
height: pixelRect.height * scaleY
)

let minDim = min(scaledRect.width, scaledRect.height)

return ResolvedCamera(
rect: scaledRect,
cornerRadius: minDim * (region.cornerRadius / 100.0),
cornerRadius: region.cameraAspect.cornerRadius(in: scaledRect, percentage: region.cornerRadius),
borderWidth: region.borderWidth * scale,
borderColor: region.borderColor,
shadow: region.shadow,
Expand Down
10 changes: 7 additions & 3 deletions Reframed/Compositor/VideoCompositor+InstructionBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,13 @@ extension VideoCompositor {
},
cameraCornerRadius: {
guard let rect = cameraRect else { return 0 }
let scaledW = rect.width * scaleX
let scaledH = rect.height * scaleY
return min(scaledW, scaledH) * (config.cameraCornerRadius / 100.0)
let scaledRect = CGRect(
x: rect.origin.x * scaleX,
y: rect.origin.y * scaleY,
width: rect.width * scaleX,
height: rect.height * scaleY
)
return config.cameraAspect.cornerRadius(in: scaledRect, percentage: config.cameraCornerRadius)
}(),
cameraBorderWidth: config.cameraBorderWidth * scaleX,
cameraBorderColor: config.cameraBorderColor.cgColor,
Expand Down
18 changes: 11 additions & 7 deletions Reframed/Editor/CameraRegionEditPopover.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,15 @@ struct CameraRegionEditPopover: View {
}
}

SectionHeader(icon: "aspectratio", title: "Aspect Ratio")
SectionHeader(icon: "circle.square", title: "Camera Shape")

SegmentPicker(
items: CameraAspect.allCases,
label: { $0.label },
selection: $localAspect
)
ForEach(CameraAspect.pickerRows, id: \.self) { row in
SegmentPicker(
items: row,
label: { $0.label },
selection: $localAspect
)
}

SectionHeader(icon: "paintbrush", title: "Style")

Expand All @@ -171,8 +173,10 @@ struct CameraRegionEditPopover: View {
label: "Radius",
value: $localCornerRadius,
range: 0...50,
formattedValue: "\(Int(localCornerRadius))%"
formattedValue: localAspect.isCircle ? "Circle" : "\(Int(localCornerRadius))%"
)
.disabled(localAspect.isCircle)
.opacity(localAspect.isCircle ? 0.5 : 1)

SliderRow(
label: "Shadow",
Expand Down
1 change: 1 addition & 0 deletions Reframed/Editor/EditorState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ final class EditorState {
self.result = result
self.playerController = SyncedPlayerController(result: result)
self.projectName = result.screenVideoURL.deletingPathExtension().lastPathComponent
self.cameraAspect = ConfigService.shared.cameraAspect
}

func setup() async {
Expand Down
25 changes: 25 additions & 0 deletions Reframed/Editor/EditorTypes.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CoreGraphics
import Foundation

enum CanvasAspect: String, Codable, Sendable, CaseIterable, Identifiable {
Expand Down Expand Up @@ -35,29 +36,53 @@ enum CameraAspect: String, Codable, Sendable, CaseIterable, Identifiable {
case ratio16x9
case ratio1x1
case ratio4x3
case ratio3x2
case ratio2x3
case ratio9x16
case circle

var id: String { rawValue }

static let pickerRows: [[CameraAspect]] = [
[.original, .circle],
[.ratio16x9, .ratio1x1, .ratio4x3],
[.ratio3x2, .ratio2x3, .ratio9x16],
]

var label: String {
switch self {
case .original: "Original"
case .ratio16x9: "16:9"
case .ratio1x1: "1:1"
case .ratio4x3: "4:3"
case .ratio3x2: "3:2"
case .ratio2x3: "2:3"
case .ratio9x16: "9:16"
case .circle: "Circle"
}
}

var isCircle: Bool {
self == .circle
}

func heightToWidthRatio(webcamSize: CGSize) -> CGFloat {
switch self {
case .original: webcamSize.height / max(webcamSize.width, 1)
case .ratio16x9: 9.0 / 16.0
case .ratio1x1: 1.0
case .ratio4x3: 3.0 / 4.0
case .ratio3x2: 2.0 / 3.0
case .ratio2x3: 3.0 / 2.0
case .ratio9x16: 16.0 / 9.0
case .circle: 1.0
}
}

func cornerRadius(in rect: CGRect, percentage: CGFloat) -> CGFloat {
let minDimension = min(rect.width, rect.height)
return minDimension * ((isCircle ? 50 : percentage) / 100.0)
}
}

enum CameraFullscreenFillMode: String, Codable, Sendable, CaseIterable, Identifiable {
Expand Down
24 changes: 14 additions & 10 deletions Reframed/Editor/PropertiesPanel+CameraTab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,21 @@ extension PropertiesPanel {
.opacity(editorState.webcamEnabled ? 1 : 0.5)
}

var cameraAspectRatioSection: some View {
var cameraShapeSection: some View {
VStack(alignment: .leading, spacing: Layout.itemSpacing) {
SectionHeader(icon: "aspectratio", title: "Aspect Ratio")
SectionHeader(icon: "circle.square", title: "Camera Shape")

SegmentPicker(
items: CameraAspect.allCases,
label: { $0.label },
selection: $editorState.cameraAspect
)
.onChange(of: editorState.cameraAspect) { _, _ in
editorState.clampCameraPosition()
ForEach(CameraAspect.pickerRows, id: \.self) { row in
SegmentPicker(
items: row,
label: { $0.label },
selection: $editorState.cameraAspect
)
}
}
.onChange(of: editorState.cameraAspect) { _, _ in
editorState.clampCameraPosition()
}
.disabled(!editorState.webcamEnabled)
.opacity(editorState.webcamEnabled ? 1 : 0.5)
}
Expand All @@ -82,8 +84,10 @@ extension PropertiesPanel {
label: "Radius",
value: $editorState.cameraCornerRadius,
range: 0...50,
formattedValue: "\(Int(editorState.cameraCornerRadius))%"
formattedValue: editorState.cameraAspect.isCircle ? "Circle" : "\(Int(editorState.cameraCornerRadius))%"
)
.disabled(editorState.cameraAspect.isCircle)
.opacity(editorState.cameraAspect.isCircle ? 0.5 : 1)

SliderRow(
label: "Shadow",
Expand Down
2 changes: 1 addition & 1 deletion Reframed/Editor/PropertiesPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ struct PropertiesPanel: View {
backgroundSection
case .camera:
cameraSection
cameraShapeSection
cameraPositionSection
cameraAspectRatioSection
cameraStyleSection
cameraBackgroundSection
cameraFullscreenSection
Expand Down
2 changes: 1 addition & 1 deletion Reframed/Editor/VideoPreviewContainer+Interaction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ extension VideoPreviewContainer {
webcamWrapper.frame = CGRect(x: x, y: bounds.height - y - h, width: w, height: h)
if currentCameraShadow > 0 {
let minDim = min(w, h)
let scaledRadius = minDim * (currentCameraCornerRadius / 100.0)
let scaledRadius = currentCameraAspect.cornerRadius(in: webcamWrapper.frame, percentage: currentCameraCornerRadius)
let camBlur = minDim * currentCameraShadow / 2000.0
webcamWrapper.layer?.shadowRadius = camBlur
webcamWrapper.layer?.shadowOpacity = 0.6
Expand Down
13 changes: 5 additions & 8 deletions Reframed/Editor/VideoPreviewContainer+Layout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,7 @@ extension VideoPreviewContainer {
height: pipFrame.height + (fsTargetRect.height - pipFrame.height) * p
)

let pipMinDim = min(pipW, pipH)
let pipRadius = pipMinDim * (currentCameraCornerRadius / 100.0)
let pipRadius = currentCameraAspect.cornerRadius(in: pipFrame, percentage: currentCameraCornerRadius)
let interpRadius = pipRadius * (1.0 - p)
let pipBorder = currentCameraBorderWidth * min(scaleX, scaleY)
let interpBorder = pipBorder * (1.0 - p)
Expand Down Expand Up @@ -289,10 +288,8 @@ extension VideoPreviewContainer {
height: defFrame.height + (customFrame.height - defFrame.height) * p
)

let defMinDim = min(defW, defH)
let defRadius = defMinDim * (defaultPipCornerRadius / 100.0)
let customMinDim = min(w, h)
let customRadius = customMinDim * (currentCameraCornerRadius / 100.0)
let defRadius = defaultPipCameraAspect.cornerRadius(in: defFrame, percentage: defaultPipCornerRadius)
let customRadius = currentCameraAspect.cornerRadius(in: customFrame, percentage: currentCameraCornerRadius)
let interpRadius = defRadius + (customRadius - defRadius) * p

let defBorder = defaultPipBorderWidth * min(scaleX, scaleY)
Expand Down Expand Up @@ -342,11 +339,11 @@ extension VideoPreviewContainer {
return
}

let webcamFrame = CGRect(x: x, y: bounds.height - y - h, width: w, height: h)
let minDim = min(w, h)
let scaledRadius = minDim * (currentCameraCornerRadius / 100.0)
let scaledRadius = currentCameraAspect.cornerRadius(in: webcamFrame, percentage: currentCameraCornerRadius)
let scaledBorder = currentCameraBorderWidth * min(scaleX, scaleY)

let webcamFrame = CGRect(x: x, y: bounds.height - y - h, width: w, height: h)
webcamWrapper.frame = webcamFrame
webcamView.frame = webcamWrapper.bounds
webcamView.layer?.cornerRadius = scaledRadius
Expand Down
80 changes: 74 additions & 6 deletions Reframed/Recording/WebcamPreviewWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,39 @@ final class WebcamPreviewWindow {
nonisolated(unsafe) private var moveObserver: NSObjectProtocol?
private var appearanceObserver: NSKeyValueObservation?

private let videoWidth: CGFloat = 270
private let videoHeight: CGFloat = 202
private let cornerRadius: CGFloat = 60
private let baseVideoWidth: CGFloat = 270
private let cornerRadiusPercent: CGFloat = 30
private var videoWidth: CGFloat = 270
private var videoHeight: CGFloat = 202
private var cameraAspect: CameraAspect = .original
private var webcamSize: CGSize?

private var cornerRadius: CGFloat {
let rect = CGRect(origin: .zero, size: CGSize(width: videoWidth, height: videoHeight))
if cameraAspect.isCircle {
return cameraAspect.cornerRadius(in: rect, percentage: 50)
}
return min(videoWidth, videoHeight) * cornerRadiusPercent / 100
}

private var totalWidth: CGFloat { videoWidth }
private var totalHeight: CGFloat { videoHeight }

func showLoading() {
func showLoading(cameraAspect: CameraAspect = ConfigService.shared.cameraAspect, webcamSize: CGSize? = nil) {
configureStyle(cameraAspect: cameraAspect, webcamSize: webcamSize)

if panel == nil {
createPanel()
} else {
resizePanelForCurrentStyle()
}

previewLayer?.removeFromSuperlayer()
previewLayer = nil
loadingView?.removeFromSuperview()

guard let contentView = panel?.contentView else { return }
contentView.subviews.forEach { $0.removeFromSuperview() }

let container = NSView(frame: NSRect(origin: .zero, size: NSSize(width: videoWidth, height: videoHeight)))
container.wantsLayer = true
Expand Down Expand Up @@ -53,15 +69,26 @@ final class WebcamPreviewWindow {
panel?.orderFrontRegardless()
}

func show(captureSession: AVCaptureSession) {
func show(
captureSession: AVCaptureSession,
cameraAspect: CameraAspect = ConfigService.shared.cameraAspect,
webcamSize: CGSize? = nil
) {
configureStyle(cameraAspect: cameraAspect, webcamSize: webcamSize)

if panel == nil {
createPanel()
} else {
resizePanelForCurrentStyle()
}

previewLayer?.removeFromSuperlayer()
previewLayer = nil

guard let contentView = panel?.contentView else { return }
contentView.subviews
.filter { $0 !== loadingView }
.forEach { $0.removeFromSuperview() }

let videoView = NSView(frame: NSRect(origin: .zero, size: NSSize(width: videoWidth, height: videoHeight)))
videoView.wantsLayer = true
Expand All @@ -85,9 +112,13 @@ final class WebcamPreviewWindow {
}
}

func showError(_ message: String) {
func showError(_ message: String, cameraAspect: CameraAspect = ConfigService.shared.cameraAspect, webcamSize: CGSize? = nil) {
configureStyle(cameraAspect: cameraAspect, webcamSize: webcamSize)

if panel == nil {
createPanel()
} else {
resizePanelForCurrentStyle()
}

previewLayer?.removeFromSuperlayer()
Expand All @@ -96,6 +127,7 @@ final class WebcamPreviewWindow {
loadingView = nil

guard let contentView = panel?.contentView else { return }
contentView.subviews.forEach { $0.removeFromSuperview() }

let container = NSView(frame: NSRect(origin: .zero, size: NSSize(width: videoWidth, height: videoHeight)))
container.wantsLayer = true
Expand All @@ -122,6 +154,11 @@ final class WebcamPreviewWindow {
panel?.orderFrontRegardless()
}

func updateStyle(cameraAspect: CameraAspect, webcamSize: CGSize? = nil) {
configureStyle(cameraAspect: cameraAspect, webcamSize: webcamSize)
resizePanelForCurrentStyle()
}

func hide() {
panel?.orderOut(nil)
}
Expand Down Expand Up @@ -190,6 +227,37 @@ final class WebcamPreviewWindow {
}
}

private func configureStyle(cameraAspect: CameraAspect, webcamSize: CGSize?) {
self.cameraAspect = cameraAspect
if let webcamSize {
self.webcamSize = webcamSize
}

let sourceSize = self.webcamSize ?? CGSize(width: 4, height: 3)
let ratio = cameraAspect.heightToWidthRatio(webcamSize: sourceSize)
videoWidth = baseVideoWidth
videoHeight = round(videoWidth * ratio)
}

private func resizePanelForCurrentStyle() {
guard let panel else { return }
let newSize = NSSize(width: totalWidth, height: totalHeight)
let oldFrame = panel.frame
let origin = CGPoint(x: oldFrame.maxX - newSize.width, y: oldFrame.origin.y)
panel.setFrame(NSRect(origin: origin, size: newSize), display: true)

guard let contentView = panel.contentView else { return }
contentView.frame = NSRect(origin: .zero, size: newSize)
contentView.layer?.cornerRadius = cornerRadius

for subview in contentView.subviews {
subview.frame = contentView.bounds
subview.layer?.cornerRadius = cornerRadius
}

previewLayer?.frame = contentView.bounds
}

private func updateColors() {
if let container = loadingView {
container.layer?.backgroundColor = ReframedColors.backgroundNS.cgColor
Expand Down
6 changes: 6 additions & 0 deletions Reframed/State/ConfigService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ final class ConfigService {
set { data.cameraMaximumResolution = newValue; save() }
}

var cameraAspect: CameraAspect {
get { CameraAspect(rawValue: data.cameraAspect) ?? .original }
set { data.cameraAspect = newValue.rawValue; save() }
}

var projectFolder: String {
get { data.projectFolder }
set { data.projectFolder = newValue; save() }
Expand Down Expand Up @@ -174,6 +179,7 @@ private struct ConfigData: Codable {
var captureSystemAudio: Bool = false
var cameraDeviceId: String? = nil
var cameraMaximumResolution: String = "1080p"
var cameraAspect: String = CameraAspect.original.rawValue
var projectFolder: String = "~/Reframed"
var retinaCapture: Bool = false
var dimOuterArea: Bool = true
Expand Down
Loading