123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158 |
- /******************************************************************************
- * Spine Runtimes License Agreement
- * Last updated April 5, 2025. Replaces all prior versions.
- *
- * Copyright (c) 2013-2025, Esoteric Software LLC
- *
- * Integration of the Spine Runtimes into software or otherwise creating
- * derivative works of the Spine Runtimes is permitted under the terms and
- * conditions of Section 2 of the Spine Editor License Agreement:
- * http://esotericsoftware.com/spine-editor-license
- *
- * Otherwise, it is permitted to integrate the Spine Runtimes into software
- * or otherwise create derivative works of the Spine Runtimes (collectively,
- * "Products"), provided that each user of the Products must obtain their own
- * Spine Editor license and redistribution of the Products in any form must
- * include this license and copyright notice.
- *
- * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
- * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
- * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *****************************************************************************/
- import SwiftUI
- import Spine
- import SpineCppLite
- struct DressUp: View {
-
- @StateObject
- var model = DressUpModel()
-
- var body: some View {
- HStack(spacing: 0) {
- List {
- ForEach(model.skinImages.keys.sorted(), id: \.self) { skinName in
- let rawImageData = model.skinImages[skinName]!
- Button(action: { model.toggleSkin(skinName: skinName) }) {
- Image(uiImage: UIImage(cgImage: rawImageData))
- .resizable()
- .scaledToFit()
- .frame(width: model.thumbnailSize.width, height: model.thumbnailSize.height)
- .grayscale(model.selectedSkins[skinName] == true ? 0.0 : 1.0)
- }
- }
- }
- .listStyle(.plain)
-
- Divider()
-
- if let drawable = model.drawable {
- SpineView(
- from: .drawable(drawable),
- controller: model.controller,
- boundsProvider: SkinAndAnimationBounds(skins: ["full-skins/girl"])
- )
- } else {
- Spacer()
- }
- }
- .navigationTitle("Dress Up")
- .navigationBarTitleDisplayMode(.inline)
- }
- }
- #Preview {
- DressUp()
- }
- final class DressUpModel: ObservableObject {
-
- let thumbnailSize = CGSize(width: 200, height: 200)
-
- @Published
- var controller: SpineController
-
- @Published
- var drawable: SkeletonDrawableWrapper?
-
- @Published
- var skinImages = [String: CGImage]()
-
- @Published
- var selectedSkins = [String: Bool]()
-
- private var customSkin: Skin?
-
- init() {
- controller = SpineController(
- onInitialized: { controller in
- controller.animationState.setAnimationByName(
- trackIndex: 0,
- animationName: "dance",
- loop: true
- )
- },
- disposeDrawableOnDeInit: false
- )
- Task.detached(priority: .high) {
- let drawable = try await SkeletonDrawableWrapper.fromBundle(
- atlasFileName: "mix-and-match-pma.atlas",
- skeletonFileName: "mix-and-match-pro.skel"
- )
- try await MainActor.run {
- for skin in drawable.skeletonData.skins {
- if skin.name == "default" { continue }
- let skeleton = drawable.skeleton
- skeleton.skin = skin
- skeleton.setToSetupPose()
- skeleton.update(delta: 0)
- skeleton.updateWorldTransform(physics: SPINE_PHYSICS_UPDATE)
- try skin.name.flatMap { skinName in
- self.skinImages[skinName] = try drawable.renderToImage(
- size: self.thumbnailSize,
- backgroundColor: .white,
- scaleFactor: UIScreen.main.scale
- )
- self.selectedSkins[skinName] = false
- }
- }
- self.toggleSkin(skinName: "full-skins/girl", drawable: drawable)
- self.drawable = drawable
- }
- }
- }
-
- deinit {
- drawable?.dispose()
- customSkin?.dispose()
- }
-
- func toggleSkin(skinName: String) {
- if let drawable {
- toggleSkin(skinName: skinName, drawable: drawable)
- }
- }
-
- func toggleSkin(skinName: String, drawable: SkeletonDrawableWrapper) {
- selectedSkins[skinName] = !(selectedSkins[skinName] ?? false)
- customSkin?.dispose()
- customSkin = Skin.create(name: "custom-skin")
- for skinName in selectedSkins.keys {
- if selectedSkins[skinName] == true {
- if let skin = drawable.skeletonData.findSkin(name: skinName) {
- customSkin?.addSkin(other: skin)
- }
- }
- }
- drawable.skeleton.skin = customSkin
- drawable.skeleton.setToSetupPose()
- }
- }
|