DressUp.swift 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. /******************************************************************************
  2. * Spine Runtimes License Agreement
  3. * Last updated April 5, 2025. Replaces all prior versions.
  4. *
  5. * Copyright (c) 2013-2025, Esoteric Software LLC
  6. *
  7. * Integration of the Spine Runtimes into software or otherwise creating
  8. * derivative works of the Spine Runtimes is permitted under the terms and
  9. * conditions of Section 2 of the Spine Editor License Agreement:
  10. * http://esotericsoftware.com/spine-editor-license
  11. *
  12. * Otherwise, it is permitted to integrate the Spine Runtimes into software
  13. * or otherwise create derivative works of the Spine Runtimes (collectively,
  14. * "Products"), provided that each user of the Products must obtain their own
  15. * Spine Editor license and redistribution of the Products in any form must
  16. * include this license and copyright notice.
  17. *
  18. * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
  19. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  20. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
  22. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  23. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
  24. * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
  25. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  27. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. *****************************************************************************/
  29. import SwiftUI
  30. import Spine
  31. import SpineCppLite
  32. struct DressUp: View {
  33. @StateObject
  34. var model = DressUpModel()
  35. var body: some View {
  36. HStack(spacing: 0) {
  37. List {
  38. ForEach(model.skinImages.keys.sorted(), id: \.self) { skinName in
  39. let rawImageData = model.skinImages[skinName]!
  40. Button(action: { model.toggleSkin(skinName: skinName) }) {
  41. Image(uiImage: UIImage(cgImage: rawImageData))
  42. .resizable()
  43. .scaledToFit()
  44. .frame(width: model.thumbnailSize.width, height: model.thumbnailSize.height)
  45. .grayscale(model.selectedSkins[skinName] == true ? 0.0 : 1.0)
  46. }
  47. }
  48. }
  49. .listStyle(.plain)
  50. Divider()
  51. if let drawable = model.drawable {
  52. SpineView(
  53. from: .drawable(drawable),
  54. controller: model.controller,
  55. boundsProvider: SkinAndAnimationBounds(skins: ["full-skins/girl"])
  56. )
  57. } else {
  58. Spacer()
  59. }
  60. }
  61. .navigationTitle("Dress Up")
  62. .navigationBarTitleDisplayMode(.inline)
  63. }
  64. }
  65. #Preview {
  66. DressUp()
  67. }
  68. final class DressUpModel: ObservableObject {
  69. let thumbnailSize = CGSize(width: 200, height: 200)
  70. @Published
  71. var controller: SpineController
  72. @Published
  73. var drawable: SkeletonDrawableWrapper?
  74. @Published
  75. var skinImages = [String: CGImage]()
  76. @Published
  77. var selectedSkins = [String: Bool]()
  78. private var customSkin: Skin?
  79. init() {
  80. controller = SpineController(
  81. onInitialized: { controller in
  82. controller.animationState.setAnimationByName(
  83. trackIndex: 0,
  84. animationName: "dance",
  85. loop: true
  86. )
  87. },
  88. disposeDrawableOnDeInit: false
  89. )
  90. Task.detached(priority: .high) {
  91. let drawable = try await SkeletonDrawableWrapper.fromBundle(
  92. atlasFileName: "mix-and-match-pma.atlas",
  93. skeletonFileName: "mix-and-match-pro.skel"
  94. )
  95. try await MainActor.run {
  96. for skin in drawable.skeletonData.skins {
  97. if skin.name == "default" { continue }
  98. let skeleton = drawable.skeleton
  99. skeleton.skin = skin
  100. skeleton.setToSetupPose()
  101. skeleton.update(delta: 0)
  102. skeleton.updateWorldTransform(physics: SPINE_PHYSICS_UPDATE)
  103. try skin.name.flatMap { skinName in
  104. self.skinImages[skinName] = try drawable.renderToImage(
  105. size: self.thumbnailSize,
  106. backgroundColor: .white,
  107. scaleFactor: UIScreen.main.scale
  108. )
  109. self.selectedSkins[skinName] = false
  110. }
  111. }
  112. self.toggleSkin(skinName: "full-skins/girl", drawable: drawable)
  113. self.drawable = drawable
  114. }
  115. }
  116. }
  117. deinit {
  118. drawable?.dispose()
  119. customSkin?.dispose()
  120. }
  121. func toggleSkin(skinName: String) {
  122. if let drawable {
  123. toggleSkin(skinName: skinName, drawable: drawable)
  124. }
  125. }
  126. func toggleSkin(skinName: String, drawable: SkeletonDrawableWrapper) {
  127. selectedSkins[skinName] = !(selectedSkins[skinName] ?? false)
  128. customSkin?.dispose()
  129. customSkin = Skin.create(name: "custom-skin")
  130. for skinName in selectedSkins.keys {
  131. if selectedSkins[skinName] == true {
  132. if let skin = drawable.skeletonData.findSkin(name: skinName) {
  133. customSkin?.addSkin(other: skin)
  134. }
  135. }
  136. }
  137. drawable.skeleton.skin = customSkin
  138. drawable.skeleton.setToSetupPose()
  139. }
  140. }