Last active
July 7, 2025 10:24
-
-
Save michaelforrest/e6b8c6bf65f1ecd97d2c7ca625676eb5 to your computer and use it in GitHub Desktop.
What we need for RealtimeSwift in CueCam
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct MainCameraView: RealtimeView{ | |
var body: some RealtimeView { | |
RealtimeSourceView("Primary") | |
VideoPencilView(canvasID: canvasID) | |
} | |
} | |
struct LightboxView: RealtimeView{ | |
var active: Bool | |
var body: some RealtimeView{ | |
if active{ | |
CameraView(camera, mode: settings.pictureInPictureMode) | |
VideoPencilView(canvasID: canvasID) | |
} | |
} | |
} | |
struct VideoPencilCameraReference: RealtimeView{ | |
var body: some RealtimeView{ | |
RealtimeSourceView("Primary") | |
} | |
} | |
struct EditorWindowView: RealtimeView{ | |
var doc: Doc | |
var card: Card | |
var body: some RealtimeView{ | |
PrimaryView() | |
.environment(card) | |
.injectingRealtimeSources(doc: doc, card: card) | |
} | |
} | |
enum CameraMode{ | |
case fullScreen, aside3D | |
case pictureInPicture(Corner) | |
case chromaKeyOverlay(Edge) | |
} | |
enum ChromaKey{ | |
case none | |
case regular(ChromaKeySettings) | |
case virtual(VirtualChromaKeySettings) | |
} | |
// sent to Video Pencil and used in Main Camera | |
@RealtimeSubrenderer("Primary") PrimaryView: RealtimeView{ | |
var camera: Camera | |
var cameraMode: CameraMode | |
var card: Card? | |
var slide: Slide? | |
var body: some View{ | |
if mode == .aside3D{ | |
CameraView(camera.name, mode: .fullScreen) | |
} | |
Stack(axis: slide?.layout.axis, order: slide?.layout.order){ | |
SlideView(slide: slide) // can be empty if slide is full screen | |
if let slide.presentedItem{ | |
// picture in picture | |
slide.presentedItemView // could just be the slide if it's full screen | |
.overlay(CameraView(mode: camera.settings.overlayMode) | |
.overlay(VideoPencilView(canvasID: card.id)) // we'll need to cache each canvas' latest drawing on this side because Video Pencil only sends one. | |
.asideEffect(mode == .aside3D) | |
}else{ | |
// side by side | |
CameraView(camera.name, mode: .fullScreen) | |
} | |
} | |
} | |
} | |
extension Slide{ | |
@RealtimeViewBuilder var presentedItemView: some RealtimeView{ | |
if layout == .fullScreen{ | |
SlideView(slide: slide) | |
}else{ | |
RealtimeSourceView(sharedContentName) | |
} | |
} | |
} | |
struct SlideView: RealtimeView{ | |
var slide: Slide | |
var body: some RealtimeView{ | |
// Basically SlideSourceView but with fixes. | |
} | |
} | |
struct CameraView: RealtimeView{ | |
var sourceName: String | |
var settings: CameraSourceSettings | |
var mode: CameraMode | |
var body: some RealtimeView{ | |
switch mode{ | |
case .fullScreen, .aside3D: | |
if let cameraBackground = settings.background, settings.key != .none{ | |
chromaKeyedFeed | |
.background(cameraBackground) | |
}else{ | |
cameraFeed | |
} | |
case .pictureInPicture(let corner): | |
cameraFeed | |
.clipped(Circle()) | |
.frame(width: 100, height: 100, alignment: corner) | |
case .chromaKeyOverlay(let edge): | |
chromaKeyedFeed | |
.frame(width: 300, alignment: edge) | |
} | |
} | |
var cameraFeed: some RealtimeView{ | |
RealtimeSourceView(sourceName) | |
.lutFilter(settings.lut) // optional lut | |
} | |
var chromaKeyedFeed: some RealtimeView{ | |
switch settings.key{ | |
case none: cameraFeed | |
case regular(let keySettings): | |
RealtimeSourceView(sourceName) | |
.chromaKeyFilter(keySettings) // put the chroma key filter first | |
.lutFilter(settings.lut) | |
case virtual(let keySettings): | |
cameraFeed // include the lut | |
.virtualChromaKey(keySettings) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct SlideView: RealtimeView{ | |
var body: some View{ | |
if slide.layout.isHorizontal{ | |
lowerOrUpperThirdLayout | |
}else{ | |
if settings.outputDimensions.isVertical{ | |
VStack(spacing: 0){ | |
regularLayout | |
} | |
}else{ | |
HStack(spacing: 0) { | |
regularLayout | |
} | |
} | |
} | |
.frame(maxHeight: slide.layout.expandsVertically ? .infinity : nil) | |
.background{ | |
if design.backgroundStyle == .union{ | |
self.backgroundColor | |
} | |
} | |
.frame(width: outputSize.width * layoutScale, height: height, alignment: slide.layout.outerAlignment) | |
.foregroundColor(design.darkText ? .black : .white) | |
} | |
@RealtimeViewBuilder var regularLayout: some RealtimeView { | |
VStack( | |
alignment: slide.layout.innerAlignment( | |
hasImage: slide.hasInlineImages | |
), | |
spacing: elementSpacing | |
) { | |
if slide.layout == .center, !settings.outputDimensions.isVertical, slide.fragments.contains(where: {$0.fragmentType == .image}){ | |
// show images first, in a row in the center layout | |
HStack(alignment: .center, spacing: 20 * layoutScale){ | |
eachFragment(alignment: .center, centered: false) { $0.element.asSlideSyntax == .image} | |
} | |
eachFragment(alignment: .top, centered: slide.layout == .center) { $0.element.asSlideSyntax != .footnote && $0.element.asSlideSyntax != .image} | |
}else{ | |
eachFragment(alignment: .top, centered: slide.layout == .center) { $0.element.asSlideSyntax != .footnote} | |
} | |
} | |
.multilineTextAlignment(slide.layout.innerAlignment(hasImage: slide.hasInlineImages).textAlignment) | |
.padding(20 * layoutScale) | |
.frame(maxHeight: slide.layout == .fullScreenEphemeral ? nil : .infinity) | |
.frame(width: contentWidth) | |
.background { | |
self.backgroundColor | |
.captured(id: .contentBackgroundZone, group: .contentBackgroundZone, fragmentType: .zoneOfInterest) | |
.handleTaps(element: .contentBackgroundZone) | |
} | |
} | |
@RealtimeViewBuilder var lowerOrUpperThirdLayout: some RealtimeView{ | |
HStack(alignment: .center, spacing: 20 * layoutScale){ | |
// CENTER IT | |
Spacer() | |
// IMAGE ON LEFT | |
eachFragment(alignment: .center, centered: false) { $0.element.asSlideSyntax == .image} | |
.frame( | |
width: lowerThirdTextFrame == nil ? nil : max(180 * layoutScale,lowerThirdTextFrame!.height), | |
height: lowerThirdTextFrame == nil ? nil : max(180 * layoutScale, lowerThirdTextFrame!.height) | |
) | |
// ALL TEXT CONTENT VERTICALLY | |
VStack(alignment: slide.fragments.filter({$0.fragmentType == .image}).count == 0 ? .center : .leading, spacing: 0) { | |
eachFragment(alignment: .center, centered: false, textScale: 0.8, filter: {$0.element.asSlideSyntax != .image && $0.element.asSlideSyntax != .footnote }) | |
} | |
.frame(minHeight: 20 * layoutScale) | |
.trackingFrame(frame: $lowerThirdTextFrame, in: .global, mode: .height, label: "lowerOrUpperThirdLayout") | |
// CENTER IT | |
Spacer() | |
} | |
.padding(.vertical, 20 * layoutScale) | |
.frame(maxWidth: .infinity, minHeight: 40 * layoutScale) | |
.background { | |
self.backgroundColor | |
.captured(id: .contentBackgroundZone, group: .contentBackgroundZone, fragmentType: .zoneOfInterest) | |
.handleTaps(element: .contentBackgroundZone) | |
} | |
} | |
@RealtimeViewBuilder func eachFragment(fragments: [SlideFragment]?=nil, alignment: VerticalAlignment, centered: Bool, textScale: CGFloat=1.0, filter shouldInclude:((SlideFragment)->Bool)?=nil)->some View{ | |
let fragments:[SlideFragment] = (fragments ?? slide.fragments).filter({ (shouldInclude != nil) ? shouldInclude?($0) == true : true}) | |
ForEach(fragments){ fragment in | |
Group { | |
switch fragment.element { | |
case .title(let text, let level): | |
self.title(text, scale: SlideSyntax.titleScale(level) * textScale * slide.layout.textScale, centered: slide.layout == .center) | |
.layoutPriority(Double(level)) | |
case .bullet(text: let text, bulletCharacter: let bulletCharacter): | |
self.bullet(text, bulletCharacter: bulletCharacter, alignment: alignment, centered: centered, bulletScale: slide.bulletScale) | |
.multilineTextAlignment(centered ? .center : .leading).fixedSize(horizontal: false, vertical: true) | |
case .image(let filename): | |
if let nsImage = document?.image(named: filename){ | |
Image(nsImage: nsImage) | |
.resizable() | |
.aspectRatio(contentMode: .fit) | |
}else{ | |
let _ = print("Could not find image named \(filename)") | |
} | |
case .quotation(let quotation): | |
self.quotation(quotation, centered: centered, fragment: fragment) | |
case .footnote(let footnote, let label): | |
self.footnote(text: footnote, label: label ) | |
case .table(let rows): | |
self.table(rows: rows) | |
case .spacer: | |
Spacer() | |
} | |
} | |
.textCase(design.textCase) | |
.shadow(color: .black.opacity(design.darkText ? 0 : 0.95), radius: 0, x: shadowSize * layoutScale, y: shadowSize * layoutScale ) | |
// .padding(shadowSize * 2 * layoutScale) // make sure we grab those shadows | |
// .drawingGroup(opaque: false) | |
.background(Color.white.opacity(0.001)) | |
.padding(design.padding.multiplied(by: layoutScale)) | |
.captured(id: fragment.id, group: .fragment, index: fragment.index, fragmentType: fragment.fragmentType) | |
.handleTaps(element: CapturedElementIdentifier(id: fragment.id, group: .fragment, index: fragment.index, fragmentType: fragment.fragmentType)) | |
.background { | |
if slidePreview, let design = slide.design ?? document?.config.slideDesign, design != .clean, let backgroundColor = design.backgroundColor, fragment.fragmentType != .image{ | |
Color(nsColor: backgroundColor.nsColor) | |
} | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Stack(axis: axis, order: order){ | |
SlideView(slide: slide) | |
if let slide.presentedItem{ | |
slide.presentedItemView | |
.overlay(CameraView(pictureInPicture: .circle) | |
.overlay(VideoPencilView(canvasID: card.id)) | |
.asideEffect(mode == .aside3D) | |
}else{ | |
CameraView(camera.name, mode: .fullScreen) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment