Skip to content

Commit 5c1be5b

Browse files
committed
In this update
Some file name changed Sticker media will show inside resizableView on rendererView Text position, size, rotation applied in export compositions
1 parent 4a4d222 commit 5c1be5b

22 files changed

Lines changed: 882 additions & 215 deletions

VideoEditor.xcodeproj/project.pbxproj

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@
141141
F2TXTAL112FAA400100E04DD1 /* TextOverlayTextAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2TXTAL122FAA400100E04DD1 /* TextOverlayTextAlignment.swift */; };
142142
F2TXTCI312FAA500100E04DD1 /* TextOverlayPreviewCIRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2TXTCI322FAA500100E04DD1 /* TextOverlayPreviewCIRenderer.swift */; };
143143
F2TXTCV112FAA400100E04DD1 /* EditorCanvasTextOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2TXTCV122FAA400100E04DD1 /* EditorCanvasTextOverlayView.swift */; };
144+
F2IMGOV112FAA500100E04DD1 /* EditorCanvasImageOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2IMGOV122FAA500100E04DD1 /* EditorCanvasImageOverlayView.swift */; };
145+
F2OVCBND12F90402000E04DD1 /* EditorCanvasOverlayBindings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2OVCBND22F90402000E04DD1 /* EditorCanvasOverlayBindings.swift */; };
146+
F2OVCLIP12F90402000E04DD1 /* OverlayCanvasClip.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2OVCLIP22F90402000E04DD1 /* OverlayCanvasClip.swift */; };
147+
F2OVUCAS12F90402000E04DD1 /* BuildEditorCanvasOverlayState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2OVUCAS22F90402000E04DD1 /* BuildEditorCanvasOverlayState.swift */; };
144148
/* End PBXBuildFile section */
145149

146150
/* Begin PBXContainerItemProxy section */
@@ -289,6 +293,10 @@
289293
F2TXTAL122FAA400100E04DD1 /* TextOverlayTextAlignment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextOverlayTextAlignment.swift; sourceTree = "<group>"; };
290294
F2TXTCI322FAA500100E04DD1 /* TextOverlayPreviewCIRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextOverlayPreviewCIRenderer.swift; sourceTree = "<group>"; };
291295
F2TXTCV122FAA400100E04DD1 /* EditorCanvasTextOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorCanvasTextOverlayView.swift; sourceTree = "<group>"; };
296+
F2IMGOV122FAA500100E04DD1 /* EditorCanvasImageOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorCanvasImageOverlayView.swift; sourceTree = "<group>"; };
297+
F2OVCBND22F90402000E04DD1 /* EditorCanvasOverlayBindings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorCanvasOverlayBindings.swift; sourceTree = "<group>"; };
298+
F2OVCLIP22F90402000E04DD1 /* OverlayCanvasClip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayCanvasClip.swift; sourceTree = "<group>"; };
299+
F2OVUCAS22F90402000E04DD1 /* BuildEditorCanvasOverlayState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildEditorCanvasOverlayState.swift; sourceTree = "<group>"; };
292300
/* End PBXFileReference section */
293301

294302
/* Begin PBXFrameworksBuildPhase section */
@@ -440,6 +448,7 @@
440448
F25D1C462F740F4D00B82DC2 /* AssetIdentifier.swift */,
441449
F2TEXT0122FAA200100E04DD1 /* TextOverlayDescriptor.swift */,
442450
F2TOVIT322FAA600100E04DD1 /* TextOverlayItem.swift */,
451+
F2OVCLIP22F90402000E04DD1 /* OverlayCanvasClip.swift */,
443452
F2TXTAL122FAA400100E04DD1 /* TextOverlayTextAlignment.swift */,
444453
F25D1C482F740F5700B82DC2 /* ClipTransition.swift */,
445454
F25D1C522F740FAB00B82DC2 /* MediaClip.swift */,
@@ -488,7 +497,6 @@
488497
F25D1C2E2F73F65800B82DC2 /* Data */ = {
489498
isa = PBXGroup;
490499
children = (
491-
F25D1C3B2F73F74800B82DC2 /* Repositories */,
492500
F25D1C3A2F73F73700B82DC2 /* Persistence */,
493501
D4322426CB7412D483397FC6 /* Services */,
494502
);
@@ -528,12 +536,30 @@
528536
F25D1C322F73F69200B82DC2 /* Editor */ = {
529537
isa = PBXGroup;
530538
children = (
539+
F2EDMODL2F90402000E04DD0 /* Models */,
540+
F2EDUCAS2F90402000E04DD0 /* UseCases */,
531541
F29334452F75153500E04DD0 /* Views */,
532542
A1E0GR012F90000000E04DD0 /* ViewModel */,
533543
);
534544
path = Editor;
535545
sourceTree = "<group>";
536546
};
547+
F2EDMODL2F90402000E04DD0 /* Models */ = {
548+
isa = PBXGroup;
549+
children = (
550+
F2OVCBND22F90402000E04DD1 /* EditorCanvasOverlayBindings.swift */,
551+
);
552+
path = Models;
553+
sourceTree = "<group>";
554+
};
555+
F2EDUCAS2F90402000E04DD0 /* UseCases */ = {
556+
isa = PBXGroup;
557+
children = (
558+
F2OVUCAS22F90402000E04DD1 /* BuildEditorCanvasOverlayState.swift */,
559+
);
560+
path = UseCases;
561+
sourceTree = "<group>";
562+
};
537563
F25D1C332F73F69B00B82DC2 /* Export */ = {
538564
isa = PBXGroup;
539565
children = (
@@ -617,13 +643,6 @@
617643
path = Persistence;
618644
sourceTree = "<group>";
619645
};
620-
F25D1C3B2F73F74800B82DC2 /* Repositories */ = {
621-
isa = PBXGroup;
622-
children = (
623-
);
624-
path = Repositories;
625-
sourceTree = "<group>";
626-
};
627646
F25D1C3C2F73F79C00B82DC2 /* Splash */ = {
628647
isa = PBXGroup;
629648
children = (
@@ -678,6 +697,7 @@
678697
F29334462F75155500E04DD0 /* EditorNavigationBar.swift */,
679698
F29334482F751AB900E04DD0 /* EditorRenderView.swift */,
680699
F2TXTCV122FAA400100E04DD1 /* EditorCanvasTextOverlayView.swift */,
700+
F2IMGOV122FAA500100E04DD1 /* EditorCanvasImageOverlayView.swift */,
681701
F293344A2F75282300E04DD0 /* EditorToolbarView.swift */,
682702
F29334552F754FD700E04DD0 /* EditorFeaturesView.swift */,
683703
F291A8732F77530800F5839A /* AudioBottomSheetView */,
@@ -884,6 +904,7 @@
884904
F25D1C532F740FAB00B82DC2 /* MediaClip.swift in Sources */,
885905
F29334492F751AB900E04DD0 /* EditorRenderView.swift in Sources */,
886906
F2TXTCV112FAA400100E04DD1 /* EditorCanvasTextOverlayView.swift in Sources */,
907+
F2IMGOV112FAA500100E04DD1 /* EditorCanvasImageOverlayView.swift in Sources */,
887908
F207CEBB2F767F9600A74075 /* TimelineConfiguration.swift in Sources */,
888909
A1E0TL012F90000000E04DD1 /* TimelineLayoutProvider.swift in Sources */,
889910
F291A8832F77E45A00F5839A /* VideoThumbnailPipeline.swift in Sources */,
@@ -994,6 +1015,9 @@
9941015
F2TEXT0132FAA200100E04DD1 /* OverlayGenerating.swift in Sources */,
9951016
F291A8912F78163200F5839A /* UIColor+Extension.swift in Sources */,
9961017
F2TEXT0152FAA200100E04DD1 /* TextOverlayRenderingService.swift in Sources */,
1018+
F2OVCLIP12F90402000E04DD1 /* OverlayCanvasClip.swift in Sources */,
1019+
F2OVCBND12F90402000E04DD1 /* EditorCanvasOverlayBindings.swift in Sources */,
1020+
F2OVUCAS12F90402000E04DD1 /* BuildEditorCanvasOverlayState.swift in Sources */,
9971021
A1E0PB052F90000000E04DD0 /* EditorViewModel.swift in Sources */,
9981022
A1E0TA012F90000000E04DD2 /* TimelineArranging.swift in Sources */,
9991023
A1E0TA032F90000000E04DD2 /* MasterTrackTimelineArranger.swift in Sources */,

VideoEditor.xcodeproj/xcshareddata/xcschemes/VideoEditor.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
parallelizable = "YES">
3636
<BuildableReference
3737
BuildableIdentifier = "primary"
38-
BlueprintIdentifier = "F25D1BEE2F73F58000B82DC2"
38+
BlueprintIdentifier = "F21B09762F91EEB800B83482"
3939
BuildableName = "VideoEditorTests.xctest"
4040
BlueprintName = "VideoEditorTests"
4141
ReferencedContainer = "container:VideoEditor.xcodeproj">

VideoEditor/Domain/Models/Effects/TransformEffect.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ nonisolated struct TransformEffect: Codable, Equatable, Hashable, Sendable {
3030
rotationAngle: 0
3131
)
3232

33+
/// Default placement for raster stickers on the overlay track (square box; user can resize on canvas).
34+
static let overlayStickerDefault = TransformEffect(
35+
normalizedCenter: CGPoint(x: 0.5, y: 0.5),
36+
normalizedSize: CGSize(width: 0.32, height: 0.32),
37+
normalizedScale: 1.0,
38+
rotationAngle: 0
39+
)
40+
3341
// MARK: - Codable
3442

3543
enum CodingKeys: String, CodingKey {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//
2+
// OverlayCanvasClip
3+
// VideoEditor
4+
//
5+
// Domain DTO: one overlay-lane clip shown on the editor preview canvas (UIKit), not AVComposition layout.
6+
7+
import CoreMedia
8+
import Foundation
9+
10+
nonisolated enum OverlayCanvasClipKind: Equatable, Sendable {
11+
case text(TextOverlayDescriptor)
12+
case sticker(URL)
13+
}
14+
15+
nonisolated struct OverlayCanvasClip: Equatable, Sendable {
16+
let id: UUID
17+
var timelineRange: ClipTimeRange
18+
var transform: TransformEffect
19+
var opacity: Float
20+
var kind: OverlayCanvasClipKind
21+
22+
func containsPlayhead(_ time: CMTime) -> Bool {
23+
guard time.isValid, !time.isIndefinite else { return false }
24+
let s = time.seconds
25+
guard s.isFinite else { return false }
26+
return s >= timelineRange.startSeconds && s < timelineRange.endSeconds
27+
}
28+
}

VideoEditor/Domain/Protocols/CompositionBuilding.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,15 @@ nonisolated struct CompositionBuildOptions: Sendable {
1616
/// Preview keeps this `false` so the editor can draw live text in UIKit.
1717
var includeTextOverlaysInVideoComposition: Bool = false
1818

19+
/// When `true`, file-backed `.image` clips on overlay tracks are baked into the composition (export).
20+
/// Preview keeps this `false` so stickers are positioned with the same UIKit transform handles as text.
21+
var includeRasterStickerOverlaysInVideoComposition: Bool = false
22+
1923
static let previewDefault = CompositionBuildOptions()
20-
static let exportWithText = CompositionBuildOptions(includeTextOverlaysInVideoComposition: true)
24+
static let exportWithText = CompositionBuildOptions(
25+
includeTextOverlaysInVideoComposition: true,
26+
includeRasterStickerOverlaysInVideoComposition: true
27+
)
2128
}
2229

2330
/// Player item, composition, and video composition; all builders share this output shape.

VideoEditor/Engine/PreviewTimelineCompositionBuilder.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ final class PreviewTimelineCompositionBuilder: CompositionBuilding {
6464
let leadIn = Self.compositionLeadIn(forClipAt: idx, in: oClips)
6565
// Text uses `PreviewTextOverlaySpec` when `options.includeTextOverlaysInVideoComposition` is set.
6666
if case .text = clip.asset { continue }
67+
if !options.includeRasterStickerOverlaysInVideoComposition, case .image = clip.asset {
68+
continue
69+
}
6770
if let item = try await makePreviewClip(from: clip, compositionLeadIn: leadIn, donor: donor) {
6871
overlayChannel.append(item)
6972
}

VideoEditor/Engine/TextOverlayPreviewCIRenderer.swift

Lines changed: 51 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,16 @@ struct PreviewTextOverlaySpec: Sendable {
2626
// MARK: - Renderer + cache
2727

2828
final class TextOverlayPreviewCIRenderer: @unchecked Sendable {
29-
29+
3030
static let shared = TextOverlayPreviewCIRenderer()
31-
31+
3232
private let cache = NSCache<NSString, CIImage>()
3333
private static let referenceCompositionHeight: CGFloat = 1080
34-
34+
3535
private init() {
3636
cache.countLimit = 48
3737
}
38-
38+
3939
func ciImageFullFrame(for spec: PreviewTextOverlaySpec, renderSize: CGSize) -> CIImage? {
4040
guard renderSize.width > 1, renderSize.height > 1 else { return nil }
4141
let key = Self.cacheKey(for: spec, renderSize: renderSize) as NSString
@@ -46,19 +46,25 @@ final class TextOverlayPreviewCIRenderer: @unchecked Sendable {
4646
cache.setObject(image, forKey: key)
4747
return image
4848
}
49-
49+
5050
func invalidateAllCaches() {
5151
cache.removeAllObjects()
5252
}
53-
53+
5454
// MARK: - Raster (background thread safe; no UIKit)
55-
55+
5656
private static func rasterize(spec: PreviewTextOverlaySpec, renderSize: CGSize) -> CIImage? {
5757
let w = Int(max(1, renderSize.width.rounded(.down)))
5858
let h = Int(max(1, renderSize.height.rounded(.down)))
59-
let box = layoutBox(transform: spec.transform, renderSize: CGSize(width: w, height: h))
59+
60+
// 1. EN ÖNEMLİ ADIM: UI (Top-Left) koordinatını CoreGraphics (Bottom-Left) koordinatına çeviriyoruz.
61+
var correctedTransform = spec.transform
62+
correctedTransform.normalizedCenter.y = 1.0 - correctedTransform.normalizedCenter.y
63+
64+
// Kutu pozisyonunu düzeltilmiş transform ile hesaplıyoruz
65+
let box = layoutBox(transform: correctedTransform, renderSize: CGSize(width: w, height: h))
6066
guard box.width >= 1, box.height >= 1 else { return nil }
61-
67+
6268
let colorSpace = CGColorSpace(name: CGColorSpace.sRGB) ?? CGColorSpaceCreateDeviceRGB()
6369
guard let ctx = CGContext(
6470
data: nil,
@@ -69,53 +75,59 @@ final class TextOverlayPreviewCIRenderer: @unchecked Sendable {
6975
space: colorSpace,
7076
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
7177
) else { return nil }
72-
73-
ctx.translateBy(x: 0, y: CGFloat(h))
74-
ctx.scaleBy(x: 1, y: -1)
75-
78+
79+
// DİKKAT: Eski kodundaki ctx.translateBy(x: 0, y: CGFloat(h)) ve ctx.scaleBy(x: 1, y: -1)
80+
// satırlarını SİLDİK. CoreText'in yazıyı düz (upright) çizebilmesi için context'in
81+
// orijinal Bottom-Left yapısında kalması gerekiyor.
82+
7683
ctx.saveGState()
84+
85+
// Rotasyon Merkezini ayarlıyoruz
7786
ctx.translateBy(x: box.midX, y: box.midY)
78-
ctx.rotate(by: spec.transform.rotationAngle)
87+
// UIKit'te rotasyon saat yönündedir, Bottom-Left sistemde tam tersidir.
88+
// Bu yüzden eksi (-) işareti koyuyoruz.
89+
ctx.rotate(by: -spec.transform.rotationAngle)
7990
ctx.translateBy(x: -box.midX, y: -box.midY)
80-
91+
92+
// Arkaplan Rengi (varsa)
8193
if let bgHex = spec.descriptor.backgroundColorHex,
8294
let bg = rgbColor(fromHex: bgHex) {
8395
ctx.setFillColor(bg)
8496
ctx.fill(box)
8597
}
86-
98+
8799
let fontSize = scaledPointSize(from: spec.descriptor, compositionHeight: CGFloat(h))
88100
let ctFont = CTFontCreateWithName(spec.descriptor.fontName as CFString, fontSize, nil)
89-
90101
let textColor = rgbColor(fromHex: spec.descriptor.textColorHex) ?? CGColor(gray: 1, alpha: 1)
91-
92102
let paragraph = Self.makeParagraphStyle(alignment: spec.descriptor.alignmentMode.ctAlignment)
93-
103+
94104
let attrs: [NSAttributedString.Key: Any] = [
95105
kCTFontAttributeName as NSAttributedString.Key: ctFont,
96106
kCTForegroundColorAttributeName as NSAttributedString.Key: textColor,
97107
kCTParagraphStyleAttributeName as NSAttributedString.Key: paragraph,
98108
]
109+
99110
let attrString = NSAttributedString(string: spec.descriptor.text, attributes: attrs)
100111
let framesetter = CTFramesetterCreateWithAttributedString(attrString as CFAttributedString)
101112
let path = CGPath(rect: box.insetBy(dx: 6, dy: 4), transform: nil)
102113
let frame = CTFramesetterCreateFrame(framesetter, CFRange(location: 0, length: attrString.length), path, nil)
114+
103115
CTFrameDraw(frame, ctx)
104-
116+
105117
ctx.restoreGState()
106-
118+
107119
guard let cgImage = ctx.makeImage() else { return nil }
108120
return CIImage(cgImage: cgImage)
109121
}
110-
122+
111123
private static func layoutBox(transform: TransformEffect, renderSize: CGSize) -> CGRect {
112124
let w = max(transform.normalizedSize.width * renderSize.width * transform.normalizedScale, 44)
113125
let h = max(transform.normalizedSize.height * renderSize.height * transform.normalizedScale, 1)
114126
let cx = transform.normalizedCenter.x * renderSize.width
115127
let cy = transform.normalizedCenter.y * renderSize.height
116128
return CGRect(x: cx - w / 2, y: cy - h / 2, width: w, height: h)
117129
}
118-
130+
119131
private static func makeParagraphStyle(alignment: CTTextAlignment) -> CTParagraphStyle {
120132
var align = alignment
121133
return withUnsafePointer(to: &align) { ptr in
@@ -127,12 +139,12 @@ final class TextOverlayPreviewCIRenderer: @unchecked Sendable {
127139
return CTParagraphStyleCreate([setting], 1)
128140
}
129141
}
130-
142+
131143
private static func scaledPointSize(from descriptor: TextOverlayDescriptor, compositionHeight: CGFloat) -> CGFloat {
132144
let scale = compositionHeight / referenceCompositionHeight
133145
return max(1, descriptor.fontSize * scale)
134146
}
135-
147+
136148
private static func cacheKey(for spec: PreviewTextOverlaySpec, renderSize: CGSize) -> String {
137149
let d = spec.descriptor
138150
let t = spec.transform
@@ -145,21 +157,21 @@ final class TextOverlayPreviewCIRenderer: @unchecked Sendable {
145157
\(t.normalizedScale)|\(t.rotationAngle)|\(spec.opacity)|\(w)x\(h)
146158
"""
147159
}
148-
160+
149161
private static func rgbColor(fromHex hexString: String) -> CGColor? {
150162
let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
151163
var int = UInt64()
152164
guard Scanner(string: hex).scanHexInt64(&int) else { return nil }
153165
let a, r, g, b: UInt64
154166
switch hex.count {
155-
case 3:
156-
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
157-
case 6:
158-
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
159-
case 8:
160-
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
161-
default:
162-
return nil
167+
case 3:
168+
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
169+
case 6:
170+
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
171+
case 8:
172+
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
173+
default:
174+
return nil
163175
}
164176
return CGColor(
165177
red: CGFloat(r) / 255,
@@ -175,18 +187,18 @@ final class TextOverlayPreviewCIRenderer: @unchecked Sendable {
175187
private extension TextOverlayTextAlignment {
176188
var ctAlignment: CTTextAlignment {
177189
switch self {
178-
case .natural: return .natural
179-
case .left: return .left
180-
case .center: return .center
181-
case .right: return .right
190+
case .natural: return .natural
191+
case .left: return .left
192+
case .center: return .center
193+
case .right: return .right
182194
}
183195
}
184196
}
185197

186198
// MARK: - Compositor helper
187199

188200
enum PreviewTextOverlayCompositor {
189-
201+
190202
static func composite(
191203
specs: [PreviewTextOverlaySpec],
192204
onto base: CIImage?,

0 commit comments

Comments
 (0)