1. 可以从另一个 app 中拖入图片或图片链接作为背景 2. 可以从 palette 中拖入 emoji 3. 可以拖动和双击缩放背景图 Signed-off-by: Ching <loooching@gmail.com>
172 lines
6.1 KiB
Swift
172 lines
6.1 KiB
Swift
//
|
|
// EmojiArtDocumentView.swift
|
|
// EmojiArt
|
|
//
|
|
// Created by ching on 2023/2/19.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct EmojiArtDocumentView: View {
|
|
let testEmojis = "😀😃😄😁😆😅😂🤣🥲🥹☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛😝😜🤪🤨🧐🤓😎🥸🤩"
|
|
|
|
@ObservedObject var document: EmojiArtDocument
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
documentBody
|
|
palette
|
|
}
|
|
}
|
|
|
|
var documentBody: some View {
|
|
GeometryReader { geometry in
|
|
ZStack {
|
|
Color.yellow.overlay(
|
|
OptionalImage(uiImage: document.backgroundImage)
|
|
.scaleEffect(zoomScale)
|
|
.position(convertFromEmojiCoordinates((0, 0), in: geometry))
|
|
)
|
|
.gesture(doubleTapToZoom(in: geometry.size))
|
|
if document.backgroundImageFetchStatus == .fetching {
|
|
ProgressView().scaleEffect(2)
|
|
} else {
|
|
ForEach(document.emojis) { emoji in
|
|
Text(emoji.text)
|
|
.font(.system(size: fontSize(for: emoji)))
|
|
.scaleEffect(zoomScale)
|
|
.position(position(for: emoji, in: geometry))
|
|
}
|
|
}
|
|
}
|
|
.clipped()
|
|
.onDrop(of: [.plainText, .url, .image], isTargeted: nil) { providers, location in
|
|
drop(providers: providers, at: location, in: geometry)
|
|
}
|
|
.gesture(panGesture().simultaneously(with: zoomGesture()))
|
|
}
|
|
}
|
|
|
|
private func drop(providers: [NSItemProvider], at location: CGPoint, in geometry: GeometryProxy) -> Bool {
|
|
var found = providers.loadObjects(ofType: URL.self) { url in
|
|
document.setBackground(EmojiArtModel.Background.url(url.imageURL))
|
|
}
|
|
if !found {
|
|
found = providers.loadObjects(ofType: UIImage.self) { image in
|
|
if let data = image.jpegData(compressionQuality: 1.0) {
|
|
document.setBackground(.imageData(data))
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
found = providers.loadObjects(ofType: String.self) { string in
|
|
if let emoji = string.first, emoji.isEmoji {
|
|
document.addEmoji(
|
|
String(emoji),
|
|
at: convertToEmojiCoordinates(location, in: geometry),
|
|
size: const.defaultEmojiFontSize / zoomScale)
|
|
}
|
|
}
|
|
}
|
|
return found
|
|
}
|
|
|
|
private func fontSize(for emoji: EmojiArtModel.Emoji) -> CGFloat {
|
|
CGFloat(emoji.size)
|
|
}
|
|
|
|
private func position(for emoji: EmojiArtModel.Emoji, in geometry: GeometryProxy) -> CGPoint {
|
|
convertFromEmojiCoordinates((emoji.x, emoji.y), in: geometry)
|
|
}
|
|
|
|
private func convertToEmojiCoordinates(_ location: CGPoint, in geometry: GeometryProxy) -> (x: Int, y: Int) {
|
|
let center = geometry.frame(in: .local).center
|
|
let location = CGPoint(
|
|
x: (location.x - center.x - panOffset.width) / zoomScale,
|
|
y: (location.y - center.y - panOffset.height) / zoomScale)
|
|
return (Int(location.x), Int(location.y))
|
|
}
|
|
|
|
private func convertFromEmojiCoordinates(_ location: (x: Int, y: Int), in geometry: GeometryProxy) -> CGPoint {
|
|
let center = geometry.frame(in: .local).center
|
|
return CGPoint(
|
|
x: center.x + CGFloat(location.x) * zoomScale + panOffset.width,
|
|
y: center.y + CGFloat(location.y) * zoomScale + panOffset.height)
|
|
}
|
|
|
|
@State private var steadyStatePanOffset: CGSize = .zero
|
|
@GestureState private var gesturePanOffset: CGSize = .zero
|
|
private var panOffset: CGSize {
|
|
(steadyStatePanOffset + gesturePanOffset) * zoomScale
|
|
}
|
|
|
|
@State private var steadyStateZoomScale: CGFloat = 1
|
|
@GestureState private var gestureZoomScale: CGFloat = 1
|
|
private var zoomScale: CGFloat {
|
|
steadyStateZoomScale * gestureZoomScale
|
|
}
|
|
|
|
private func panGesture() -> some Gesture {
|
|
DragGesture()
|
|
.updating($gesturePanOffset) { latestDragGestureValue, gesturePanOffset, _ in
|
|
gesturePanOffset = latestDragGestureValue.translation / zoomScale
|
|
}
|
|
.onEnded { finalDragGestureValue in
|
|
steadyStatePanOffset = steadyStatePanOffset + (finalDragGestureValue.translation / zoomScale)
|
|
}
|
|
}
|
|
|
|
private func zoomGesture() -> some Gesture {
|
|
MagnificationGesture()
|
|
.updating($gestureZoomScale) { latestGestureScale, gestureZoomScale, _ in
|
|
gestureZoomScale = latestGestureScale
|
|
}
|
|
.onEnded { gestureScaleAtEnd in
|
|
steadyStateZoomScale *= gestureScaleAtEnd
|
|
}
|
|
}
|
|
|
|
private func doubleTapToZoom(in size: CGSize) -> some Gesture {
|
|
TapGesture(count: 2)
|
|
.onEnded {
|
|
withAnimation {
|
|
zoomToFit(document.backgroundImage, in: size)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func zoomToFit(_ image: UIImage?, in size: CGSize) {
|
|
if let image = image, image.size.width > 0, image.size.height > 0, size.width > 0, size.height > 0 {
|
|
let hZoom = size.width / image.size.width
|
|
let vZoom = size.height / image.size.height
|
|
steadyStatePanOffset = .zero
|
|
steadyStateZoomScale = min(hZoom, vZoom)
|
|
}
|
|
}
|
|
|
|
var palette: some View {
|
|
SrcollingEmojiView(emojis: testEmojis)
|
|
.font(.system(size: const.defaultEmojiFontSize))
|
|
}
|
|
}
|
|
|
|
struct SrcollingEmojiView: View {
|
|
let emojis: String
|
|
var body: some View {
|
|
ScrollView(.horizontal) {
|
|
HStack {
|
|
ForEach(emojis.map { String($0) }, id: \.self) { emoji in
|
|
Text(emoji)
|
|
.onDrag { NSItemProvider(object: emoji as NSString) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ContentView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
EmojiArtDocumentView(document: EmojiArtDocument())
|
|
}
|
|
}
|