diff --git a/Signal/Images.xcassets/clock-components/Contents.json b/Signal/Images.xcassets/clock-components/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Signal/Images.xcassets/clock-components/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Signal/Images.xcassets/clock-components/clock-arabic-hour.imageset/Contents.json b/Signal/Images.xcassets/clock-components/clock-arabic-hour.imageset/Contents.json new file mode 100644 index 0000000000..b3b67d4859 --- /dev/null +++ b/Signal/Images.xcassets/clock-components/clock-arabic-hour.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "clock-arabic-hour.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Signal/Images.xcassets/clock-components/clock-arabic-hour.imageset/clock-arabic-hour.pdf b/Signal/Images.xcassets/clock-components/clock-arabic-hour.imageset/clock-arabic-hour.pdf new file mode 100644 index 0000000000..af5dd90670 Binary files /dev/null and b/Signal/Images.xcassets/clock-components/clock-arabic-hour.imageset/clock-arabic-hour.pdf differ diff --git a/Signal/Images.xcassets/clock-components/clock-arabic-minute.imageset/Contents.json b/Signal/Images.xcassets/clock-components/clock-arabic-minute.imageset/Contents.json new file mode 100644 index 0000000000..64e112e786 --- /dev/null +++ b/Signal/Images.xcassets/clock-components/clock-arabic-minute.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "clock-arabic-minute.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Signal/Images.xcassets/clock-components/clock-arabic-minute.imageset/clock-arabic-minute.pdf b/Signal/Images.xcassets/clock-components/clock-arabic-minute.imageset/clock-arabic-minute.pdf new file mode 100644 index 0000000000..1764dc928c Binary files /dev/null and b/Signal/Images.xcassets/clock-components/clock-arabic-minute.imageset/clock-arabic-minute.pdf differ diff --git a/Signal/Images.xcassets/clock-components/clock-arabic.imageset/Contents.json b/Signal/Images.xcassets/clock-components/clock-arabic.imageset/Contents.json new file mode 100644 index 0000000000..f40c5e8d4f --- /dev/null +++ b/Signal/Images.xcassets/clock-components/clock-arabic.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "clock-arabic.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Signal/Images.xcassets/clock-components/clock-arabic.imageset/clock-arabic.pdf b/Signal/Images.xcassets/clock-components/clock-arabic.imageset/clock-arabic.pdf new file mode 100644 index 0000000000..c77674d2e3 Binary files /dev/null and b/Signal/Images.xcassets/clock-components/clock-arabic.imageset/clock-arabic.pdf differ diff --git a/Signal/Images.xcassets/clock-components/clock-baton-hour.imageset/Contents.json b/Signal/Images.xcassets/clock-components/clock-baton-hour.imageset/Contents.json new file mode 100644 index 0000000000..0b601f31cf --- /dev/null +++ b/Signal/Images.xcassets/clock-components/clock-baton-hour.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "clock-baton-hour.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Signal/Images.xcassets/clock-components/clock-baton-hour.imageset/clock-baton-hour.pdf b/Signal/Images.xcassets/clock-components/clock-baton-hour.imageset/clock-baton-hour.pdf new file mode 100644 index 0000000000..b86335fefb Binary files /dev/null and b/Signal/Images.xcassets/clock-components/clock-baton-hour.imageset/clock-baton-hour.pdf differ diff --git a/Signal/Images.xcassets/clock-components/clock-baton-minute.imageset/Contents.json b/Signal/Images.xcassets/clock-components/clock-baton-minute.imageset/Contents.json new file mode 100644 index 0000000000..5108ddf7e3 --- /dev/null +++ b/Signal/Images.xcassets/clock-components/clock-baton-minute.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "clock-baton-minute.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Signal/Images.xcassets/clock-components/clock-baton-minute.imageset/clock-baton-minute.pdf b/Signal/Images.xcassets/clock-components/clock-baton-minute.imageset/clock-baton-minute.pdf new file mode 100644 index 0000000000..b618d67ca6 Binary files /dev/null and b/Signal/Images.xcassets/clock-components/clock-baton-minute.imageset/clock-baton-minute.pdf differ diff --git a/Signal/Images.xcassets/clock-components/clock-baton.imageset/Contents.json b/Signal/Images.xcassets/clock-components/clock-baton.imageset/Contents.json new file mode 100644 index 0000000000..0287e9f2e1 --- /dev/null +++ b/Signal/Images.xcassets/clock-components/clock-baton.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "clock-baton.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Signal/Images.xcassets/clock-components/clock-baton.imageset/clock-baton.pdf b/Signal/Images.xcassets/clock-components/clock-baton.imageset/clock-baton.pdf new file mode 100644 index 0000000000..252f384f06 Binary files /dev/null and b/Signal/Images.xcassets/clock-components/clock-baton.imageset/clock-baton.pdf differ diff --git a/Signal/Images.xcassets/clock-components/clock-diver-center.imageset/Contents.json b/Signal/Images.xcassets/clock-components/clock-diver-center.imageset/Contents.json new file mode 100644 index 0000000000..f85019696a --- /dev/null +++ b/Signal/Images.xcassets/clock-components/clock-diver-center.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "clock-diver-center.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Signal/Images.xcassets/clock-components/clock-diver-center.imageset/clock-diver-center.pdf b/Signal/Images.xcassets/clock-components/clock-diver-center.imageset/clock-diver-center.pdf new file mode 100644 index 0000000000..f6491406d1 Binary files /dev/null and b/Signal/Images.xcassets/clock-components/clock-diver-center.imageset/clock-diver-center.pdf differ diff --git a/Signal/Images.xcassets/clock-components/clock-diver-hour.imageset/Contents.json b/Signal/Images.xcassets/clock-components/clock-diver-hour.imageset/Contents.json new file mode 100644 index 0000000000..9b3584e831 --- /dev/null +++ b/Signal/Images.xcassets/clock-components/clock-diver-hour.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "clock-diver-hour.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Signal/Images.xcassets/clock-components/clock-diver-hour.imageset/clock-diver-hour.pdf b/Signal/Images.xcassets/clock-components/clock-diver-hour.imageset/clock-diver-hour.pdf new file mode 100644 index 0000000000..804f906e51 Binary files /dev/null and b/Signal/Images.xcassets/clock-components/clock-diver-hour.imageset/clock-diver-hour.pdf differ diff --git a/Signal/Images.xcassets/clock-components/clock-diver-minute.imageset/Contents.json b/Signal/Images.xcassets/clock-components/clock-diver-minute.imageset/Contents.json new file mode 100644 index 0000000000..98dc267cc8 --- /dev/null +++ b/Signal/Images.xcassets/clock-components/clock-diver-minute.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "clock-diver-minute.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Signal/Images.xcassets/clock-components/clock-diver-minute.imageset/clock-diver-minute.pdf b/Signal/Images.xcassets/clock-components/clock-diver-minute.imageset/clock-diver-minute.pdf new file mode 100644 index 0000000000..aaf2869d60 Binary files /dev/null and b/Signal/Images.xcassets/clock-components/clock-diver-minute.imageset/clock-diver-minute.pdf differ diff --git a/Signal/Images.xcassets/clock-components/clock-diver.imageset/Contents.json b/Signal/Images.xcassets/clock-components/clock-diver.imageset/Contents.json new file mode 100644 index 0000000000..8fe76b9c7b --- /dev/null +++ b/Signal/Images.xcassets/clock-components/clock-diver.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "clock-diver.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Signal/Images.xcassets/clock-components/clock-diver.imageset/clock-diver.pdf b/Signal/Images.xcassets/clock-components/clock-diver.imageset/clock-diver.pdf new file mode 100644 index 0000000000..96f109cc73 Binary files /dev/null and b/Signal/Images.xcassets/clock-components/clock-diver.imageset/clock-diver.pdf differ diff --git a/Signal/Images.xcassets/clock-components/clock-explorer-hour.imageset/Contents.json b/Signal/Images.xcassets/clock-components/clock-explorer-hour.imageset/Contents.json new file mode 100644 index 0000000000..4babb4e88e --- /dev/null +++ b/Signal/Images.xcassets/clock-components/clock-explorer-hour.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "clock-explorer-hour.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Signal/Images.xcassets/clock-components/clock-explorer-hour.imageset/clock-explorer-hour.pdf b/Signal/Images.xcassets/clock-components/clock-explorer-hour.imageset/clock-explorer-hour.pdf new file mode 100644 index 0000000000..bb0359aeff Binary files /dev/null and b/Signal/Images.xcassets/clock-components/clock-explorer-hour.imageset/clock-explorer-hour.pdf differ diff --git a/Signal/Images.xcassets/clock-components/clock-explorer-minute.imageset/Contents.json b/Signal/Images.xcassets/clock-components/clock-explorer-minute.imageset/Contents.json new file mode 100644 index 0000000000..1ee2bb792b --- /dev/null +++ b/Signal/Images.xcassets/clock-components/clock-explorer-minute.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "clock-explorer-minute.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Signal/Images.xcassets/clock-components/clock-explorer-minute.imageset/clock-explorer-minute.pdf b/Signal/Images.xcassets/clock-components/clock-explorer-minute.imageset/clock-explorer-minute.pdf new file mode 100644 index 0000000000..1218d5dc2c Binary files /dev/null and b/Signal/Images.xcassets/clock-components/clock-explorer-minute.imageset/clock-explorer-minute.pdf differ diff --git a/Signal/Images.xcassets/clock-components/clock-explorer.imageset/Contents.json b/Signal/Images.xcassets/clock-components/clock-explorer.imageset/Contents.json new file mode 100644 index 0000000000..394651682e --- /dev/null +++ b/Signal/Images.xcassets/clock-components/clock-explorer.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "clock-explorer.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Signal/Images.xcassets/clock-components/clock-explorer.imageset/clock-explorer.pdf b/Signal/Images.xcassets/clock-components/clock-explorer.imageset/clock-explorer.pdf new file mode 100644 index 0000000000..e88c6462ae Binary files /dev/null and b/Signal/Images.xcassets/clock-components/clock-explorer.imageset/clock-explorer.pdf differ diff --git a/SignalUI/ViewControllers/Stickers/EditorSticker.swift b/SignalUI/ViewControllers/Stickers/EditorSticker.swift index 8a0d58e427..7703374f40 100644 --- a/SignalUI/ViewControllers/Stickers/EditorSticker.swift +++ b/SignalUI/ViewControllers/Stickers/EditorSticker.swift @@ -6,6 +6,24 @@ import UIKit import SignalMessaging +private class LayerContainerView: UIView { + let contentLayer: CALayer + init(contentLayer: CALayer) { + self.contentLayer = contentLayer + super.init(frame: .zero) + layer.addSublayer(contentLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + contentLayer.frame = CGRect(origin: self.frame.origin, size: self.frame.size) + } +} + // MARK: - EditorSticker public enum EditorSticker { @@ -16,6 +34,7 @@ public enum EditorSticker { public enum StorySticker { case clockDigital(DigitalClockStyle) + case clockAnalog(AnalogClockStyle) func previewView() -> UIView { switch self { @@ -24,6 +43,9 @@ public enum EditorSticker { label.attributedText = digitalClockStyle.attributedString(date: Date()) label.adjustsFontSizeToFitWidth = true return label + case .clockAnalog(let clockStyle): + let clockLayer = clockStyle.drawClock(date: Date()) + return LayerContainerView(contentLayer: clockLayer) } } @@ -33,6 +55,7 @@ public enum EditorSticker { static var pickerStickers: [StorySticker] { [ .clockDigital(.white), + .clockAnalog(.arabic), ] } } @@ -123,3 +146,236 @@ extension EditorSticker.StorySticker { } } } + +// MARK: AnalogClockStyle + +extension EditorSticker.StorySticker { + public enum AnalogClockStyle: CaseIterable { + case arabic + case baton + case explorer + case diver + + var backgroundImage: UIImage { + switch self { + case .arabic: + return #imageLiteral(resourceName: "clock-arabic.pdf") + case .baton: + return #imageLiteral(resourceName: "clock-baton.pdf") + case .explorer: + return #imageLiteral(resourceName: "clock-explorer.pdf") + case .diver: + return #imageLiteral(resourceName: "clock-diver.pdf") + } + } + + func drawClock(date: Date) -> CALayer { + return AnalogClockLayer(style: self, date: date) + } + + var hourHandImage: UIImage { + switch self { + case .arabic: + return #imageLiteral(resourceName: "clock-arabic-hour.pdf") + case .baton: + return #imageLiteral(resourceName: "clock-baton-hour.pdf") + case .explorer: + return #imageLiteral(resourceName: "clock-explorer-hour.pdf") + case .diver: + return #imageLiteral(resourceName: "clock-diver-hour.pdf") + } + } + + var hourHandHeight: CGFloat { + switch self { + case .arabic: + return 1/3 + case .baton: + return 0.35 + case .explorer: + return 149/600 + case .diver: + return 139/600 + } + } + + var hourHandOffset: CGFloat { + switch self { + case .arabic: + return 0.72 + case .baton: + return 16/21 + case .explorer: + return 1 + case .diver: + return 141/139 + } + } + + var minuteHandImage: UIImage { + switch self { + case .arabic: + return #imageLiteral(resourceName: "clock-arabic-minute.pdf") + case .baton: + return #imageLiteral(resourceName: "clock-baton-minute.pdf") + case .explorer: + return #imageLiteral(resourceName: "clock-explorer-minute.pdf") + case .diver: + return #imageLiteral(resourceName: "clock-diver-minute.pdf") + } + } + + var minuteHandHeight: CGFloat { + switch self { + case .arabic: + return 280/600 + case .baton: + return 308/600 + case .explorer: + return 229/600 + case .diver: + return 268/600 + } + } + + var minuteHandOffset: CGFloat { + switch self { + case .arabic: + return 4/5 + case .baton: + return 129/154 + case .explorer: + return 1 + case .diver: + return 1 + } + } + + var centerImage: UIImage? { + switch self { + case .diver: + return #imageLiteral(resourceName: "clock-diver-center.pdf") + case .arabic, .baton, .explorer: + return nil + } + } + + func nextStyle() -> AnalogClockStyle { + switch self { + case .arabic: + return .baton + case .baton: + return .explorer + case .explorer: + return .diver + case .diver: + return .arabic + } + } + + func stickerWithNextStyle() -> EditorSticker { + return .story(.clockAnalog(self.nextStyle())) + } + } +} + +// MARK: - AnalogClockLayer + +private class AnalogClockLayer: CALayer { + typealias Style = EditorSticker.StorySticker.AnalogClockStyle + + private let clockStyle: Style + private let date: Date + private let background: CALayer + private let hourHand: CALayer + private let minuteHand: CALayer + private let center: CALayer? + + override var frame: CGRect { + didSet { + updateSublayerFrames() + } + } + + init(style: Style, date: Date) { + self.clockStyle = style + self.date = date + + background = UIImageView(image: style.backgroundImage).layer + + let hourHandImageView = UIImageView(image: style.hourHandImage) + hourHandImageView.contentMode = .scaleAspectFit + hourHand = hourHandImageView.layer + + let minuteHandImageView = UIImageView(image: style.minuteHandImage) + minuteHandImageView.contentMode = .scaleAspectFit + minuteHand = minuteHandImageView.layer + + center = style.centerImage.map(UIImageView.init(image:))?.layer + + super.init() + addSublayer(background) + addSublayer(hourHand) + addSublayer(minuteHand) + if let center { + addSublayer(center) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateSublayerFrames() { + let dateComponents = Calendar.current.dateComponents([.hour, .minute], from: date) + let minutes = CGFloat(dateComponents.minute ?? 0) + let hours = CGFloat(dateComponents.hour ?? 0) + minutes/60 +// let minutes = CGFloat.random(in: 0..<60) +// let hours = CGFloat.random(in: 0..<12) + + background.frame.size = self.frame.size + transfrom( + clockHandLayer: hourHand, + time: hours/12, + height: clockStyle.hourHandHeight, + offset: clockStyle.hourHandOffset + ) + transfrom( + clockHandLayer: minuteHand, + time: minutes/60, + height: clockStyle.minuteHandHeight, + offset: clockStyle.minuteHandOffset + ) + if let center { + let size: CGFloat = 42/600 * self.frame.height + center.frame = CGRect( + origin: .init( + x: self.frame.width/2 - size/2, + y: self.frame.height/2 - size/2 + ), + size: .square(size) + ) + } + } + + private func transfrom( + clockHandLayer hand: CALayer, + time: CGFloat, + height: CGFloat, + offset: CGFloat + ) { + hand.setAffineTransform(.identity) + hand.frame.size.height = self.frame.height * height + hand.frame.origin = .init( + x: self.frame.width/2 - hand.frame.size.width/2, + y: self.frame.height/2 - hand.frame.size.height/2 + ) + + hand.anchorPoint = .init(x: 0.5, y: offset) + hand.setAffineTransform( + .init(translationX: 0, y: -hand.frame.height * (offset - 0.5)) + .rotated(by: time * 2 * .pi) + ) + } + +} diff --git a/SignalUI/Views/ImageEditor/ImageEditorCanvasView.swift b/SignalUI/Views/ImageEditor/ImageEditorCanvasView.swift index a5fe928a97..7eb7e2811f 100644 --- a/SignalUI/Views/ImageEditor/ImageEditorCanvasView.swift +++ b/SignalUI/Views/ImageEditor/ImageEditorCanvasView.swift @@ -988,7 +988,7 @@ class ImageEditorCanvasView: UIView { model.stickerViewCache.setObject(ThreadSafeCacheHandle(stickerImage), forKey: item.itemId) image = stickerImage case (.none, .story(let storySticker)): - // Special sticker. Return early with special rendering + // Story sticker. Return early with special rendering return storyStickerLayer( for: storySticker, imageFrame: imageFrame, @@ -1002,8 +1002,27 @@ class ImageEditorCanvasView: UIView { let imageView = UIImageView(image: image) imageView.contentMode = .scaleAspectFit - let imageLayer = imageView.layer + return makeLayerTransformable( + imageView.layer, + imageFrame: imageFrame, + item: item, + model: model, + isFaded: isFaded, + transform: transform, + viewSize: viewSize + ) + } + + private class func makeLayerTransformable( + _ imageLayer: CALayer, + imageFrame: CGRect, + item: ImageEditorStickerItem, + model: ImageEditorModel, + isFaded: Bool, + transform: ImageEditorTransform, + viewSize: CGSize + ) -> CALayer { imageLayer.contentsScale = UIScreen.main.scale * item.scaling * transform.scaling let stickerSize = CGSize(square: 175 * imageFrame.size.width / item.referenceImageWidth) let centerInCanvas = item.unitCenter.fromUnitCoordinates(viewBounds: imageFrame) @@ -1059,6 +1078,18 @@ class ImageEditorCanvasView: UIView { transform: transform, viewSize: viewSize ) + case .clockAnalog(let clockStyle): + let clockLayer = clockStyle.drawClock(date: item.date) + + return makeLayerTransformable( + clockLayer, + imageFrame: imageFrame, + item: item, + model: model, + isFaded: isFaded, + transform: transform, + viewSize: viewSize + ) } } diff --git a/SignalUI/Views/ImageEditor/ImageEditorView.swift b/SignalUI/Views/ImageEditor/ImageEditorView.swift index 8ac0b8a557..c8679057d0 100644 --- a/SignalUI/Views/ImageEditor/ImageEditorView.swift +++ b/SignalUI/Views/ImageEditor/ImageEditorView.swift @@ -321,6 +321,10 @@ class ImageEditorView: UIView { let newSticker = clockStyle.stickerWithNextStyle() let newStickerItem = stickerItem.copy(sticker: newSticker) model.replace(item: newStickerItem) + case .clockAnalog(let clockStyle): + let newSticker = clockStyle.stickerWithNextStyle() + let newStickerItem = stickerItem.copy(sticker: newSticker) + model.replace(item: newStickerItem) } } }