mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Fix LottieAnimations
This commit is contained in:
parent
419398ecb2
commit
2657fffc41
5 changed files with 493 additions and 382 deletions
|
|
@ -25,6 +25,7 @@
|
||||||
7E8840D72E9A346700C5CB8C /* Dog waiting.json in Resources */ = {isa = PBXBuildFile; fileRef = 7E8840D62E9A346700C5CB8C /* Dog waiting.json */; };
|
7E8840D72E9A346700C5CB8C /* Dog waiting.json in Resources */ = {isa = PBXBuildFile; fileRef = 7E8840D62E9A346700C5CB8C /* Dog waiting.json */; };
|
||||||
7E8840D92E9A347000C5CB8C /* Moody Dog.json in Resources */ = {isa = PBXBuildFile; fileRef = 7E8840D82E9A347000C5CB8C /* Moody Dog.json */; };
|
7E8840D92E9A347000C5CB8C /* Moody Dog.json in Resources */ = {isa = PBXBuildFile; fileRef = 7E8840D82E9A347000C5CB8C /* Moody Dog.json */; };
|
||||||
7E8840DB2E9A347800C5CB8C /* Orange Cat Peeping.json in Resources */ = {isa = PBXBuildFile; fileRef = 7E8840DA2E9A347800C5CB8C /* Orange Cat Peeping.json */; };
|
7E8840DB2E9A347800C5CB8C /* Orange Cat Peeping.json in Resources */ = {isa = PBXBuildFile; fileRef = 7E8840DA2E9A347800C5CB8C /* Orange Cat Peeping.json */; };
|
||||||
|
7E9911B02EFD7D1B002EDFD5 /* Reindeer.json in Resources */ = {isa = PBXBuildFile; fileRef = 7E9911AF2EFD7D1B002EDFD5 /* Reindeer.json */; };
|
||||||
7EE1D73D2E6B1E0B00997219 /* ApplicationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7EE1D73C2E6B1E0B00997219 /* ApplicationServices.framework */; };
|
7EE1D73D2E6B1E0B00997219 /* ApplicationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7EE1D73C2E6B1E0B00997219 /* ApplicationServices.framework */; };
|
||||||
7EE1D73F2E6B1E1200997219 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7EE1D73E2E6B1E1200997219 /* IOKit.framework */; };
|
7EE1D73F2E6B1E1200997219 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7EE1D73E2E6B1E1200997219 /* IOKit.framework */; };
|
||||||
BA90505C953246D0AB206451 /* MediaRemoteAdapter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4C1DD09EC134302938E84F6 /* MediaRemoteAdapter.framework */; };
|
BA90505C953246D0AB206451 /* MediaRemoteAdapter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4C1DD09EC134302938E84F6 /* MediaRemoteAdapter.framework */; };
|
||||||
|
|
@ -55,6 +56,7 @@
|
||||||
7E8840D62E9A346700C5CB8C /* Dog waiting.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "Dog waiting.json"; sourceTree = "<group>"; };
|
7E8840D62E9A346700C5CB8C /* Dog waiting.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "Dog waiting.json"; sourceTree = "<group>"; };
|
||||||
7E8840D82E9A347000C5CB8C /* Moody Dog.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "Moody Dog.json"; sourceTree = "<group>"; };
|
7E8840D82E9A347000C5CB8C /* Moody Dog.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "Moody Dog.json"; sourceTree = "<group>"; };
|
||||||
7E8840DA2E9A347800C5CB8C /* Orange Cat Peeping.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "Orange Cat Peeping.json"; sourceTree = "<group>"; };
|
7E8840DA2E9A347800C5CB8C /* Orange Cat Peeping.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "Orange Cat Peeping.json"; sourceTree = "<group>"; };
|
||||||
|
7E9911AF2EFD7D1B002EDFD5 /* Reindeer.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Reindeer.json; sourceTree = "<group>"; };
|
||||||
7EE1D73C2E6B1E0B00997219 /* ApplicationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ApplicationServices.framework; path = System/Library/Frameworks/ApplicationServices.framework; sourceTree = SDKROOT; };
|
7EE1D73C2E6B1E0B00997219 /* ApplicationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ApplicationServices.framework; path = System/Library/Frameworks/ApplicationServices.framework; sourceTree = SDKROOT; };
|
||||||
7EE1D73E2E6B1E1200997219 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; };
|
7EE1D73E2E6B1E1200997219 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; };
|
||||||
F4C1DD09EC134302938E84F6 /* MediaRemoteAdapter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaRemoteAdapter.framework; path = Frameworks/MediaRemoteAdapter.framework; sourceTree = SOURCE_ROOT; };
|
F4C1DD09EC134302938E84F6 /* MediaRemoteAdapter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaRemoteAdapter.framework; path = Frameworks/MediaRemoteAdapter.framework; sourceTree = SOURCE_ROOT; };
|
||||||
|
|
@ -122,6 +124,7 @@
|
||||||
children = (
|
children = (
|
||||||
7E6856472EA963FD00D261F5 /* Lock.json */,
|
7E6856472EA963FD00D261F5 /* Lock.json */,
|
||||||
7E8840D62E9A346700C5CB8C /* Dog waiting.json */,
|
7E8840D62E9A346700C5CB8C /* Dog waiting.json */,
|
||||||
|
7E9911AF2EFD7D1B002EDFD5 /* Reindeer.json */,
|
||||||
7E8840D82E9A347000C5CB8C /* Moody Dog.json */,
|
7E8840D82E9A347000C5CB8C /* Moody Dog.json */,
|
||||||
7E8840DA2E9A347800C5CB8C /* Orange Cat Peeping.json */,
|
7E8840DA2E9A347800C5CB8C /* Orange Cat Peeping.json */,
|
||||||
);
|
);
|
||||||
|
|
@ -241,6 +244,7 @@
|
||||||
7E8130412E3FB40500E08F3F /* NowPlayingTestClient in Resources */,
|
7E8130412E3FB40500E08F3F /* NowPlayingTestClient in Resources */,
|
||||||
7E6856482EA963FD00D261F5 /* Lock.json in Resources */,
|
7E6856482EA963FD00D261F5 /* Lock.json in Resources */,
|
||||||
7E8840D92E9A347000C5CB8C /* Moody Dog.json in Resources */,
|
7E8840D92E9A347000C5CB8C /* Moody Dog.json in Resources */,
|
||||||
|
7E9911B02EFD7D1B002EDFD5 /* Reindeer.json in Resources */,
|
||||||
7E8840D72E9A346700C5CB8C /* Dog waiting.json in Resources */,
|
7E8840D72E9A346700C5CB8C /* Dog waiting.json in Resources */,
|
||||||
7E8840DB2E9A347800C5CB8C /* Orange Cat Peeping.json in Resources */,
|
7E8840DB2E9A347800C5CB8C /* Orange Cat Peeping.json in Resources */,
|
||||||
);
|
);
|
||||||
|
|
@ -429,7 +433,7 @@
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
||||||
MARKETING_VERSION = "1.2.1-beta";
|
MARKETING_VERSION = "1.2.2-beta";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.Ebullioscopic.Atoll.dev;
|
PRODUCT_BUNDLE_IDENTIFIER = com.Ebullioscopic.Atoll.dev;
|
||||||
PRODUCT_NAME = Atoll;
|
PRODUCT_NAME = Atoll;
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|
@ -500,7 +504,7 @@
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
||||||
MARKETING_VERSION = "1.2.1-beta";
|
MARKETING_VERSION = "1.2.2-beta";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.Ebullioscopic.Atoll;
|
PRODUCT_BUNDLE_IDENTIFIER = com.Ebullioscopic.Atoll;
|
||||||
PRODUCT_NAME = Atoll;
|
PRODUCT_NAME = Atoll;
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|
|
||||||
|
|
@ -1391,6 +1391,7 @@
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"1x" : {
|
"1x" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -1471,6 +1472,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"20" : {
|
"20" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -1491,6 +1493,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"30" : {
|
"30" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -3050,6 +3053,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Animation" : {
|
"Animation" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -3992,6 +3996,7 @@
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Blue dashed box shows actual size in Atoll" : {
|
"Blue dashed box shows actual size in Atoll" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -4001,6 +4006,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Blue outline matches the notch canvas" : {
|
||||||
|
"comment" : "A tooltip explaining that the blue outline in the animation preview matches the notch on a device.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Bluetooth Audio Devices" : {
|
"Bluetooth Audio Devices" : {
|
||||||
"comment" : "A section header that lists Bluetooth audio devices.",
|
"comment" : "A section header that lists Bluetooth audio devices.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
|
|
@ -4140,9 +4149,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"Bottom Pad:" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Breakdown" : {
|
"Breakdown" : {
|
||||||
"comment" : "A label displayed above the breakdown of memory usage.",
|
"comment" : "A label displayed above the breakdown of memory usage.",
|
||||||
|
|
@ -4616,6 +4622,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Center" : {
|
"Center" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -6004,6 +6011,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Current Transform" : {
|
||||||
|
"comment" : "A label displayed above the current transform values in the preview stats view.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Currently active" : {
|
"Currently active" : {
|
||||||
"comment" : "A text indicating whether the camera is \"Currently active\" or \"Not active\".",
|
"comment" : "A text indicating whether the camera is \"Currently active\" or \"Not active\".",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
|
|
@ -6601,6 +6612,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Dash outline equals the exact notch bounds." : {
|
||||||
|
"comment" : "A description under the live preview section of the animation editor.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Default" : {
|
"Default" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ar" : {
|
"ar" : {
|
||||||
|
|
@ -7110,6 +7125,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Display Options" : {
|
"Display Options" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -8234,6 +8250,9 @@
|
||||||
},
|
},
|
||||||
"Enable timer live activity" : {
|
"Enable timer live activity" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Enable when scaling beyond 30x20px so the Atoll opens gracefully." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Engine Utilization" : {
|
"Engine Utilization" : {
|
||||||
"comment" : "A label describing the utilization of the GPU's engine components.",
|
"comment" : "A label describing the utilization of the GPU's engine components.",
|
||||||
|
|
@ -8678,7 +8697,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Expand Notch with Animation" : {
|
"Expand notch to follow animation width" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Expanded drag detection area" : {
|
"Expanded drag detection area" : {
|
||||||
|
|
@ -9686,6 +9705,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Height:" : {
|
"Height:" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -10041,6 +10061,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Horizontal:" : {
|
"Horizontal:" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -11057,9 +11078,6 @@
|
||||||
},
|
},
|
||||||
"Loop Mode" : {
|
"Loop Mode" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Loop Mode:" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Lottie JSON URL" : {
|
"Lottie JSON URL" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -14952,9 +14970,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"Opacity:" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Open Google AI Studio" : {
|
"Open Google AI Studio" : {
|
||||||
|
|
||||||
|
|
@ -15202,8 +15217,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Preview Zoom:" : {
|
"Preview Zoom" : {
|
||||||
|
"comment" : "A label describing the zoom control for the animation preview.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Privacy Indicators" : {
|
"Privacy Indicators" : {
|
||||||
"comment" : "A heading for the section that lists privacy indicators.",
|
"comment" : "A heading for the section that lists privacy indicators.",
|
||||||
|
|
@ -15336,9 +15352,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"Quick Presets" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Quick Share" : {
|
"Quick Share" : {
|
||||||
"comment" : "A section header that says \"Quick Share\".",
|
"comment" : "A section header that says \"Quick Share\".",
|
||||||
|
|
@ -16041,6 +16054,7 @@
|
||||||
|
|
||||||
},
|
},
|
||||||
"Rotation:" : {
|
"Rotation:" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -16205,6 +16219,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Scale:" : {
|
"Scale:" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -17614,6 +17629,7 @@
|
||||||
|
|
||||||
},
|
},
|
||||||
"Size & Scale" : {
|
"Size & Scale" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -18149,6 +18165,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Speed:" : {
|
"Speed:" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -19237,6 +19254,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Transform" : {
|
"Transform" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -20194,6 +20212,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Vertical:" : {
|
"Vertical:" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -20563,14 +20582,12 @@
|
||||||
},
|
},
|
||||||
"When enabled, stats monitoring stops a few seconds after the notch closes." : {
|
"When enabled, stats monitoring stops a few seconds after the notch closes." : {
|
||||||
|
|
||||||
},
|
|
||||||
"When enabled, the Atoll will expand horizontally to match the animation size" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery." : {
|
"When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Width:" : {
|
"Width:" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ struct AnimationEditorView: View {
|
||||||
@State private var isImporting = false
|
@State private var isImporting = false
|
||||||
|
|
||||||
// Preview state
|
// Preview state
|
||||||
@State private var previewScale: CGFloat = 1.0
|
@State private var previewScale: CGFloat = 10.0
|
||||||
|
|
||||||
init(sourceURL: URL, isRemoteURL: Bool, animation: Binding<CustomIdleAnimation?>, existingAnimation: CustomIdleAnimation? = nil) {
|
init(sourceURL: URL, isRemoteURL: Bool, animation: Binding<CustomIdleAnimation?>, existingAnimation: CustomIdleAnimation? = nil) {
|
||||||
self.sourceURL = sourceURL
|
self.sourceURL = sourceURL
|
||||||
|
|
@ -69,398 +69,19 @@ struct AnimationEditorView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// Header
|
header
|
||||||
HStack {
|
|
||||||
Text(existingAnimation != nil ? "Edit Animation" : "Customize Animation")
|
|
||||||
.font(.title2)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button("Cancel") {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
.keyboardShortcut(.cancelAction)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
HStack(spacing: 0) {
|
||||||
HSplitView {
|
previewPanel
|
||||||
// Preview Panel
|
.frame(minWidth: 320, maxWidth: 360)
|
||||||
VStack {
|
|
||||||
Text("Preview")
|
|
||||||
.font(.headline)
|
|
||||||
.padding(.top)
|
|
||||||
|
|
||||||
ZStack {
|
|
||||||
// Background to show bounds
|
|
||||||
RoundedRectangle(cornerRadius: 8)
|
|
||||||
.fill(Color(nsColor: .controlBackgroundColor))
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 8)
|
|
||||||
.strokeBorder(Color.gray.opacity(0.3), lineWidth: 1)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Target size indicator (30x20 - actual size in notch)
|
|
||||||
Rectangle()
|
|
||||||
.strokeBorder(Color.blue.opacity(0.5), style: StrokeStyle(lineWidth: 2, dash: [5, 3]))
|
|
||||||
.frame(width: 30 * previewScale, height: 20 * previewScale)
|
|
||||||
|
|
||||||
// Animation preview with transformations
|
|
||||||
if let existing = existingAnimation, case .builtInFace = existing.source {
|
|
||||||
// Show built-in face
|
|
||||||
MinimalFaceFeatures(height: 20, width: 30)
|
|
||||||
.scaleEffect(previewScale)
|
|
||||||
.frame(width: cropWidth * scale * previewScale, height: cropHeight * scale * previewScale)
|
|
||||||
.offset(x: offsetX * previewScale, y: offsetY * previewScale)
|
|
||||||
.rotationEffect(.degrees(rotation))
|
|
||||||
.opacity(opacity)
|
|
||||||
.padding(.bottom, paddingBottom * previewScale)
|
|
||||||
.clipped()
|
|
||||||
} else {
|
|
||||||
// Show Lottie animation
|
|
||||||
LottieView(state: LUStateData(
|
|
||||||
type: .loadedFrom(sourceURL),
|
|
||||||
speed: speed,
|
|
||||||
loopMode: loopMode.lottieLoopMode
|
|
||||||
))
|
|
||||||
.frame(width: cropWidth * scale * previewScale, height: cropHeight * scale * previewScale)
|
|
||||||
.offset(x: offsetX * previewScale, y: offsetY * previewScale)
|
|
||||||
.rotationEffect(.degrees(rotation))
|
|
||||||
.opacity(opacity)
|
|
||||||
.padding(.bottom, paddingBottom * previewScale)
|
|
||||||
.clipped()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
.padding()
|
|
||||||
|
|
||||||
// Preview Scale Control
|
|
||||||
HStack {
|
|
||||||
Text("Preview Zoom:")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Slider(value: $previewScale, in: 1...10)
|
|
||||||
.frame(width: 150)
|
|
||||||
Text("\(Int(previewScale))x")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.frame(width: 30)
|
|
||||||
}
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.bottom)
|
|
||||||
}
|
|
||||||
.frame(minWidth: 300)
|
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
controlsPanel
|
||||||
// Controls Panel
|
|
||||||
ScrollView {
|
|
||||||
VStack(alignment: .leading, spacing: 20) {
|
|
||||||
// Name
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
Text("Animation Name")
|
|
||||||
.font(.headline)
|
|
||||||
TextField("Name", text: $name)
|
|
||||||
.textFieldStyle(.roundedBorder)
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
// Size Controls
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
Text("Size & Scale")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
// Scale
|
|
||||||
HStack {
|
|
||||||
Text("Scale:")
|
|
||||||
.frame(width: 80, alignment: .leading)
|
|
||||||
Slider(value: $scale, in: 0.1...5.0)
|
|
||||||
Text(String(format: "%.2f", scale))
|
|
||||||
.frame(width: 50, alignment: .trailing)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Button("Reset") {
|
|
||||||
withAnimation { scale = 1.0 }
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.controlSize(.small)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Crop Width
|
|
||||||
HStack {
|
|
||||||
Text("Width:")
|
|
||||||
.frame(width: 80, alignment: .leading)
|
|
||||||
Slider(value: $cropWidth, in: 5...100)
|
|
||||||
Text(String(format: "%.0f", cropWidth))
|
|
||||||
.frame(width: 50, alignment: .trailing)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Button("30") {
|
|
||||||
withAnimation { cropWidth = 30 }
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.controlSize(.small)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Crop Height
|
|
||||||
HStack {
|
|
||||||
Text("Height:")
|
|
||||||
.frame(width: 80, alignment: .leading)
|
|
||||||
Slider(value: $cropHeight, in: 5...100)
|
|
||||||
Text(String(format: "%.0f", cropHeight))
|
|
||||||
.frame(width: 50, alignment: .trailing)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Button("20") {
|
|
||||||
withAnimation { cropHeight = 20 }
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.controlSize(.small)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
// Position Controls
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
Text("Position")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
// Offset X
|
|
||||||
HStack {
|
|
||||||
Text("Horizontal:")
|
|
||||||
.frame(width: 80, alignment: .leading)
|
|
||||||
Slider(value: $offsetX, in: -50...50)
|
|
||||||
Text(String(format: "%.1f", offsetX))
|
|
||||||
.frame(width: 50, alignment: .trailing)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Button("Center") {
|
|
||||||
withAnimation { offsetX = 0 }
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.controlSize(.small)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Offset Y
|
|
||||||
HStack {
|
|
||||||
Text("Vertical:")
|
|
||||||
.frame(width: 80, alignment: .leading)
|
|
||||||
Slider(value: $offsetY, in: -50...50)
|
|
||||||
Text(String(format: "%.1f", offsetY))
|
|
||||||
.frame(width: 50, alignment: .trailing)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Button("Center") {
|
|
||||||
withAnimation { offsetY = 0 }
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.controlSize(.small)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Padding Bottom
|
|
||||||
HStack {
|
|
||||||
Text("Bottom Pad:")
|
|
||||||
.frame(width: 80, alignment: .leading)
|
|
||||||
Slider(value: $paddingBottom, in: -20...20)
|
|
||||||
Text(String(format: "%.1f", paddingBottom))
|
|
||||||
.frame(width: 50, alignment: .trailing)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Button("Reset") {
|
|
||||||
withAnimation { paddingBottom = 0 }
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.controlSize(.small)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
// Rotation & Opacity
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
Text("Transform")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
// Rotation
|
|
||||||
HStack {
|
|
||||||
Text("Rotation:")
|
|
||||||
.frame(width: 80, alignment: .leading)
|
|
||||||
Slider(value: $rotation, in: -180...180)
|
|
||||||
Text(String(format: "%.0f°", rotation))
|
|
||||||
.frame(width: 50, alignment: .trailing)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Button("Reset") {
|
|
||||||
withAnimation { rotation = 0 }
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.controlSize(.small)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Opacity
|
|
||||||
HStack {
|
|
||||||
Text("Opacity:")
|
|
||||||
.frame(width: 80, alignment: .leading)
|
|
||||||
Slider(value: $opacity, in: 0...1)
|
|
||||||
Text(String(format: "%.0f%%", opacity * 100))
|
|
||||||
.frame(width: 50, alignment: .trailing)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Button("100%") {
|
|
||||||
withAnimation { opacity = 1.0 }
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.controlSize(.small)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
// Animation Speed
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
Text("Animation")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text("Speed:")
|
|
||||||
.frame(width: 80, alignment: .leading)
|
|
||||||
Slider(value: $speed, in: 0.1...3.0)
|
|
||||||
Text(String(format: "%.2fx", speed))
|
|
||||||
.frame(width: 50, alignment: .trailing)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Button("1x") {
|
|
||||||
withAnimation { speed = 1.0 }
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.controlSize(.small)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop Mode
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
Text("Loop Mode:")
|
|
||||||
.font(.subheadline)
|
|
||||||
Picker("Loop Mode", selection: $loopMode) {
|
|
||||||
ForEach(AnimationLoopMode.allCases, id: \.self) { mode in
|
|
||||||
Text(mode.rawValue).tag(mode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pickerStyle(.segmented)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
// Display Options
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
Text("Display Options")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
Toggle(isOn: $expandWithAnimation) {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text("Expand Notch with Animation")
|
|
||||||
.font(.body)
|
|
||||||
Text("When enabled, the Atoll will expand horizontally to match the animation size")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
// Presets
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
Text("Quick Presets")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 8) {
|
|
||||||
Button("Fit to Notch") {
|
|
||||||
withAnimation {
|
|
||||||
scale = 1.0
|
|
||||||
cropWidth = 30
|
|
||||||
cropHeight = 20
|
|
||||||
offsetX = 0
|
|
||||||
offsetY = 0
|
|
||||||
rotation = 0
|
|
||||||
paddingBottom = 0
|
|
||||||
expandWithAnimation = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
|
|
||||||
Button("Fill Notch") {
|
|
||||||
withAnimation {
|
|
||||||
scale = 1.5
|
|
||||||
cropWidth = 30
|
|
||||||
cropHeight = 20
|
|
||||||
offsetX = 0
|
|
||||||
offsetY = 0
|
|
||||||
paddingBottom = 0
|
|
||||||
expandWithAnimation = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
|
|
||||||
Button("Scale Up 2x") {
|
|
||||||
withAnimation {
|
|
||||||
scale = 2.0
|
|
||||||
expandWithAnimation = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
|
|
||||||
Button("Scale Down") {
|
|
||||||
withAnimation {
|
|
||||||
scale = 0.75
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
|
|
||||||
Button("Reset All") {
|
|
||||||
withAnimation {
|
|
||||||
scale = 1.0
|
|
||||||
cropWidth = 30
|
|
||||||
cropHeight = 20
|
|
||||||
offsetX = 0
|
|
||||||
offsetY = 0
|
|
||||||
rotation = 0
|
|
||||||
opacity = 1.0
|
|
||||||
speed = 1.0
|
|
||||||
paddingBottom = 0
|
|
||||||
expandWithAnimation = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.tint(.red)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
.frame(minWidth: 350, maxWidth: 450)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
footer
|
||||||
// Footer with actions
|
|
||||||
HStack {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Image(systemName: "info.circle")
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Text("Blue dashed box shows actual size in Atoll")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button(existingAnimation != nil ? "Save Changes" : "Import") {
|
|
||||||
importAnimation()
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
.keyboardShortcut(.defaultAction)
|
|
||||||
.disabled(name.isEmpty || isImporting)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
}
|
||||||
.frame(idealWidth: 900, idealHeight: 650)
|
.frame(idealWidth: 980, idealHeight: 640)
|
||||||
.frame(minWidth: 800, minHeight: 600)
|
.frame(minWidth: 860, minHeight: 620)
|
||||||
.fixedSize()
|
.fixedSize()
|
||||||
.alert("Import Error", isPresented: $showingError) {
|
.alert("Import Error", isPresented: $showingError) {
|
||||||
Button("OK") { }
|
Button("OK") { }
|
||||||
|
|
@ -469,6 +90,363 @@ struct AnimationEditorView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Layout Sections
|
||||||
|
|
||||||
|
private var header: some View {
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(existingAnimation != nil ? "Edit Animation" : "Customize Animation")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
Text(sourceDescription)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
Button("Cancel") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
.keyboardShortcut(.cancelAction)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var footer: some View {
|
||||||
|
HStack {
|
||||||
|
Label("Blue outline matches the notch canvas", systemImage: "info.circle")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Spacer()
|
||||||
|
Button(existingAnimation != nil ? "Save Changes" : "Import") {
|
||||||
|
importAnimation()
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.keyboardShortcut(.defaultAction)
|
||||||
|
.disabled(name.isEmpty || isImporting)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var previewPanel: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Live Preview")
|
||||||
|
.font(.headline)
|
||||||
|
Text("Dash outline equals the exact notch bounds.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
previewCanvas
|
||||||
|
previewZoomControls
|
||||||
|
previewStats
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
.padding(20)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var previewCanvas: some View {
|
||||||
|
ZStack {
|
||||||
|
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
||||||
|
.fill(Color(nsColor: .controlBackgroundColor))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
||||||
|
.stroke(Color.white.opacity(0.08), lineWidth: 1)
|
||||||
|
)
|
||||||
|
.shadow(color: .black.opacity(0.15), radius: 16, x: 0, y: 8)
|
||||||
|
|
||||||
|
Rectangle()
|
||||||
|
.strokeBorder(Color.blue.opacity(0.55), style: StrokeStyle(lineWidth: 2, dash: [6, 3]))
|
||||||
|
.frame(width: 30 * previewScale, height: 20 * previewScale)
|
||||||
|
|
||||||
|
Group {
|
||||||
|
if let existing = existingAnimation, case .builtInFace = existing.source {
|
||||||
|
MinimalFaceFeatures(height: 20, width: 30)
|
||||||
|
.scaleEffect(previewScale)
|
||||||
|
} else {
|
||||||
|
LottieView(state: LUStateData(
|
||||||
|
type: .loadedFrom(sourceURL),
|
||||||
|
speed: speed,
|
||||||
|
loopMode: loopMode.lottieLoopMode
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: cropWidth * scale * previewScale, height: cropHeight * scale * previewScale)
|
||||||
|
.offset(x: offsetX * previewScale, y: offsetY * previewScale)
|
||||||
|
.rotationEffect(.degrees(rotation))
|
||||||
|
.opacity(opacity)
|
||||||
|
.padding(.bottom, paddingBottom * previewScale)
|
||||||
|
.clipped()
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(height: 280)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var previewZoomControls: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack {
|
||||||
|
Label("Preview Zoom", systemImage: "magnifyingglass")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Spacer()
|
||||||
|
Text("\(Int(previewScale))x")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.monospacedDigit()
|
||||||
|
}
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
Button {
|
||||||
|
withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) {
|
||||||
|
previewScale = max(1, previewScale - 1)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "minus")
|
||||||
|
.frame(width: 28, height: 28)
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.controlSize(.mini)
|
||||||
|
|
||||||
|
Slider(value: $previewScale, in: 1...10)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) {
|
||||||
|
previewScale = min(10, previewScale + 1)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "plus")
|
||||||
|
.frame(width: 28, height: 28)
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.controlSize(.mini)
|
||||||
|
|
||||||
|
Button("Reset") {
|
||||||
|
withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) {
|
||||||
|
previewScale = 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.controlSize(.mini)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var previewStats: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Current Transform")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
PreviewStatChip(title: "Scale", value: String(format: "%.2fx", scale))
|
||||||
|
PreviewStatChip(title: "Output", value: "\(Int(cropWidth * scale))x\(Int(cropHeight * scale)) px")
|
||||||
|
PreviewStatChip(title: "Offset", value: String(format: "%.1f / %.1f", offsetX, offsetY))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var controlsPanel: some View {
|
||||||
|
ScrollView {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
EditorSectionCard(title: "Animation Details", subtitle: "Give it a friendly name and control playback speed.") {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
Text("Animation Name")
|
||||||
|
.font(.subheadline)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
TextField("Name", text: $name)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
}
|
||||||
|
ParameterSliderRow(
|
||||||
|
title: "Playback Speed",
|
||||||
|
value: $speed,
|
||||||
|
range: 0.1...3.0,
|
||||||
|
formatter: { String(format: "%.2fx", $0) },
|
||||||
|
resetLabel: "1x",
|
||||||
|
resetValue: 1.0,
|
||||||
|
step: 0.05
|
||||||
|
)
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
Text("Loop Mode")
|
||||||
|
.font(.subheadline)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
Picker("Loop Mode", selection: $loopMode) {
|
||||||
|
ForEach(AnimationLoopMode.allCases, id: \.self) { mode in
|
||||||
|
Text(mode.rawValue).tag(mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorSectionCard(title: "Size & Crop", subtitle: "Match the notch footprint or intentionally exceed it.") {
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
ParameterSliderRow(
|
||||||
|
title: "Scale",
|
||||||
|
value: $scale,
|
||||||
|
range: 0.1...5.0,
|
||||||
|
formatter: { String(format: "%.2fx", $0) },
|
||||||
|
resetLabel: "Reset",
|
||||||
|
resetValue: 1.0
|
||||||
|
)
|
||||||
|
ParameterSliderRow(
|
||||||
|
title: "Visible Width",
|
||||||
|
value: $cropWidth,
|
||||||
|
range: 5...100,
|
||||||
|
formatter: { "\(Int($0)) px" },
|
||||||
|
resetLabel: "30",
|
||||||
|
resetValue: 30
|
||||||
|
)
|
||||||
|
ParameterSliderRow(
|
||||||
|
title: "Visible Height",
|
||||||
|
value: $cropHeight,
|
||||||
|
range: 5...100,
|
||||||
|
formatter: { "\(Int($0)) px" },
|
||||||
|
resetLabel: "20",
|
||||||
|
resetValue: 20
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorSectionCard(title: "Position & Padding", subtitle: "Nudge the animation inside the notch.") {
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
ParameterSliderRow(
|
||||||
|
title: "Horizontal Offset",
|
||||||
|
value: $offsetX,
|
||||||
|
range: -50...50,
|
||||||
|
formatter: { String(format: "%.1f px", $0) },
|
||||||
|
resetLabel: "Center",
|
||||||
|
resetValue: 0
|
||||||
|
)
|
||||||
|
ParameterSliderRow(
|
||||||
|
title: "Vertical Offset",
|
||||||
|
value: $offsetY,
|
||||||
|
range: -50...50,
|
||||||
|
formatter: { String(format: "%.1f px", $0) },
|
||||||
|
resetLabel: "Center",
|
||||||
|
resetValue: 0
|
||||||
|
)
|
||||||
|
ParameterSliderRow(
|
||||||
|
title: "Bottom Padding",
|
||||||
|
value: $paddingBottom,
|
||||||
|
range: -20...20,
|
||||||
|
formatter: { String(format: "%.1f px", $0) },
|
||||||
|
resetLabel: "Reset",
|
||||||
|
resetValue: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorSectionCard(title: "Transform & Display", subtitle: "Dial in the final polish.") {
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
ParameterSliderRow(
|
||||||
|
title: "Rotation",
|
||||||
|
value: $rotation,
|
||||||
|
range: -180...180,
|
||||||
|
formatter: { String(format: "%.0f°", $0) },
|
||||||
|
resetLabel: "Reset",
|
||||||
|
resetValue: 0,
|
||||||
|
step: 1
|
||||||
|
)
|
||||||
|
ParameterSliderRow(
|
||||||
|
title: "Opacity",
|
||||||
|
value: $opacity,
|
||||||
|
range: 0...1,
|
||||||
|
formatter: { "\(Int($0 * 100))%" },
|
||||||
|
resetLabel: "100%",
|
||||||
|
resetValue: 1,
|
||||||
|
step: 0.01
|
||||||
|
)
|
||||||
|
Toggle(isOn: $expandWithAnimation) {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Expand notch to follow animation width")
|
||||||
|
.fontWeight(.medium)
|
||||||
|
Text("Enable when scaling beyond 30x20px so the Atoll opens gracefully.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorSectionCard(title: "Quick Presets", subtitle: "Start from a popular layout and tweak from there.") {
|
||||||
|
let columns = [GridItem(.adaptive(minimum: 160), spacing: 12)]
|
||||||
|
LazyVGrid(columns: columns, spacing: 12) {
|
||||||
|
Button("Fit to Notch") {
|
||||||
|
withAnimation {
|
||||||
|
scale = 1.0
|
||||||
|
cropWidth = 30
|
||||||
|
cropHeight = 20
|
||||||
|
offsetX = 0
|
||||||
|
offsetY = 0
|
||||||
|
rotation = 0
|
||||||
|
paddingBottom = 0
|
||||||
|
expandWithAnimation = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
|
||||||
|
Button("Fill Notch") {
|
||||||
|
withAnimation {
|
||||||
|
scale = 1.5
|
||||||
|
cropWidth = 30
|
||||||
|
cropHeight = 20
|
||||||
|
offsetX = 0
|
||||||
|
offsetY = 0
|
||||||
|
paddingBottom = 0
|
||||||
|
expandWithAnimation = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
|
||||||
|
Button("Scale Up 2x") {
|
||||||
|
withAnimation {
|
||||||
|
scale = 2.0
|
||||||
|
expandWithAnimation = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
|
||||||
|
Button("Scale Down") {
|
||||||
|
withAnimation {
|
||||||
|
scale = 0.75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
|
||||||
|
Button("Reset All") {
|
||||||
|
withAnimation {
|
||||||
|
scale = 1.0
|
||||||
|
cropWidth = 30
|
||||||
|
cropHeight = 20
|
||||||
|
offsetX = 0
|
||||||
|
offsetY = 0
|
||||||
|
rotation = 0
|
||||||
|
opacity = 1.0
|
||||||
|
speed = 1.0
|
||||||
|
paddingBottom = 0
|
||||||
|
expandWithAnimation = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.tint(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(20)
|
||||||
|
}
|
||||||
|
.frame(minWidth: 500, maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sourceDescription: String {
|
||||||
|
if let existing = existingAnimation {
|
||||||
|
return existing.name
|
||||||
|
}
|
||||||
|
if isRemoteURL {
|
||||||
|
return sourceURL.absoluteString
|
||||||
|
}
|
||||||
|
return sourceURL.deletingPathExtension().lastPathComponent
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Import Logic
|
// MARK: - Import Logic
|
||||||
|
|
||||||
private func importAnimation() {
|
private func importAnimation() {
|
||||||
|
|
@ -534,6 +512,117 @@ struct AnimationEditorView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Supporting Views
|
||||||
|
|
||||||
|
private struct EditorSectionCard<Content: View>: View {
|
||||||
|
let title: String
|
||||||
|
let subtitle: String?
|
||||||
|
let content: Content
|
||||||
|
|
||||||
|
init(title: String, subtitle: String? = nil, @ViewBuilder content: () -> Content) {
|
||||||
|
self.title = title
|
||||||
|
self.subtitle = subtitle
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(title)
|
||||||
|
.font(.headline)
|
||||||
|
if let subtitle, !subtitle.isEmpty {
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content
|
||||||
|
}
|
||||||
|
.padding(16)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
||||||
|
.fill(Color(nsColor: .controlBackgroundColor))
|
||||||
|
)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
||||||
|
.stroke(Color.white.opacity(0.05), lineWidth: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ParameterSliderRow: View {
|
||||||
|
let title: String
|
||||||
|
@Binding var value: CGFloat
|
||||||
|
let range: ClosedRange<CGFloat>
|
||||||
|
let formatter: (CGFloat) -> String
|
||||||
|
var resetLabel: String? = nil
|
||||||
|
var resetValue: CGFloat? = nil
|
||||||
|
var step: CGFloat? = nil
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
HStack {
|
||||||
|
Text(title)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
Spacer()
|
||||||
|
Text(formatter(value))
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.monospacedDigit()
|
||||||
|
}
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
slider
|
||||||
|
if let resetLabel, let resetValue {
|
||||||
|
Button(resetLabel) {
|
||||||
|
withAnimation(.spring(response: 0.3, dampingFraction: 0.85)) {
|
||||||
|
value = resetValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.controlSize(.mini)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var slider: some View {
|
||||||
|
let binding = Binding<Double>(
|
||||||
|
get: { Double(value) },
|
||||||
|
set: { value = CGFloat($0) }
|
||||||
|
)
|
||||||
|
let bounds = Double(range.lowerBound)...Double(range.upperBound)
|
||||||
|
if let step {
|
||||||
|
Slider(value: binding, in: bounds, step: Double(step))
|
||||||
|
} else {
|
||||||
|
Slider(value: binding, in: bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PreviewStatChip: View {
|
||||||
|
let title: String
|
||||||
|
let value: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(title)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Text(value)
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.monospacedDigit()
|
||||||
|
}
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||||
|
.fill(Color.white.opacity(0.06))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Preview
|
// MARK: - Preview
|
||||||
#Preview {
|
#Preview {
|
||||||
AnimationEditorView(
|
AnimationEditorView(
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ class IdleAnimationManager {
|
||||||
print("📦 [IdleAnimationManager] Loading bundled animations...")
|
print("📦 [IdleAnimationManager] Loading bundled animations...")
|
||||||
|
|
||||||
// The JSON files are added as individual resources, not in a folder
|
// The JSON files are added as individual resources, not in a folder
|
||||||
let bundledFiles = ["Dog waiting", "Moody Dog", "Orange Cat Peeping"]
|
let bundledFiles = ["Dog waiting", "Moody Dog", "Orange Cat Peeping", "Reindeer"]
|
||||||
var animations: [CustomIdleAnimation] = []
|
var animations: [CustomIdleAnimation] = []
|
||||||
|
|
||||||
for filename in bundledFiles {
|
for filename in bundledFiles {
|
||||||
|
|
|
||||||
1
LottieAnimations/Reindeer.json
Normal file
1
LottieAnimations/Reindeer.json
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue