spine-runtimes/spine-ios/Sources/Spine/SkeletonDrawableWrapper.swift
Denis Andrašec 0d5c3e3b18
Spine iOS (#2504)
* Add `spine-iOS` SPM package & example app (#1)

* Basic Mesh Rendering (#2)

* Spine C++ Swift Wrapper (#3)

* Load `Atlas` & `SkeletonData` (#4)

Load & dispose `Atlas` & `SkeletonData` from bundled files.

* Generate Swift classes from `spine-cpp-lite.h` (#5)

* Draw `SkeletonData` render commands (#6)

- Use `SkeletonData` render commands in the renderer
- Simple loop for animation support

* Add `BoundsProvider` (#7)

- Implement & support `BoundsProvider` classes
- Introduce alignment and content mode
- Update c to swift script to return optional for find prefixed methods

* Support `SpineController` & `Event` callbacks (#8)

- Support SpineController callbacks
- Support Event callbacks
- Apply tint color in renderer

* Support `DressUp` sample (#9)

- Add `DressUp` sample
- Move SpineViewController to SpineUIView
- Implement SpineUIView export to image

* Remove unused file

* Add `Physics` sample (#10)

- Add `Physics` sample
- Fix offsets in `IKFollowing` sample
- Fix `SpineView` background color

* Add `DebugRendering` sample (#11)

- Add `DebugRendering` sample
- Make `SpineUIView` transparent

* Move remaining files to SPM package (#12)

- Move remaining files to SPM package
- Rename `SpineWrapper` to `SpineCppLite`

* Load assets from different sources (#13)

- Load from bundle, file, http & drawable
- Apply correct blend mode & pma in renderer

* Add `Obj-C` + `UIKit` sample (#14)

- Add `Obj-C` + `UIKit` sample
- Update `Spine` to be usable in Obj-C code base

* Support CocoaPods (#15)

* Metal Best Practices (#16)

- Tripple Buffering
- Buffer Bindings
- Shared Objects

* Annotate functions that should return optional (#17)

* Add option to disable drawing when out of viewport (#18)

- Add option to disable drawing when out of viewport
- Move update clock to controller so multiple views can share it

* Add docs for public Spine classes/methods (#19)

* Fix various regressions (#20)

- Fix retain `SpineController` retain cycle
- Fix issue wehre images were not rendered
2024-06-18 10:02:25 +02:00

146 lines
6.5 KiB
Swift

import Foundation
import Spine
import SpineCppLite
import CoreGraphics
import UIKit
/// A ``SkeletonDrawableWrapper`` with ``SkeletonDrawable`` bundle loading, updating, and rendering an ``Atlas``, ``Skeleton``, and ``AnimationState``
/// into a single easy to use class.
///
/// Use the ``SkeletonDrawableWrapper/fromBundle(atlasFileName:skeletonFileName:bundle:)``, ``SkeletonDrawableWrapper/fromFile(atlasFile:skeletonFile:)``, or ``SkeletonDrawableWrapper/fromHttp(atlasURL:skeletonURL:)`` methods to construct a ``SkeletonDrawableWrapper``. To have
/// multiple skeleton drawable wrapper instances share the same ``Atlas`` and ``SkeletonData``, use the constructor.
///
/// You can then directly access the `skeletonDrawable` and with it `atlas`, `skeletonData`, `skeleton`, `animationStateData`, `animationState` and `animationStateWrapper`.
/// to query and animate the skeleton. Use the ``AnimationStateWrapper`` to queue animations on one or more tracks
/// via ``AnimationState/setAnimation(trackIndex:animation:loop:)`` or ``AnimationState/addAnimation(trackIndex:animation:loop:delay:)``.
///
/// To update the ``AnimationState`` and apply it to the ``Skeleton`` call the ``AnimationStateWrapper/update`` function, providing it
/// a delta time in seconds to advance the animations.
///
/// To render the current pose of the ``Skeleton`` as a `CGImage`, use ``SkeletonDrawableWrapper/renderToImage(size:backgroundColor:scaleFactor:)``.
///
/// When the skeleton drawable is no longer needed, call the ``SkeletonDrawableWrapper/dispose()`` method to release its resources. If
/// the skeleton drawable was constructed from a shared ``Atlas`` and ``SkeletonData``, make sure to dispose the
/// atlas and skeleton data as well, if no skeleton drawable references them anymore.
@objc(SpineSkeletonDrawableWrapper)
@objcMembers
public final class SkeletonDrawableWrapper: NSObject {
public let atlas: Atlas
public let atlasPages: [UIImage]
public let skeletonData: SkeletonData
public let skeletonDrawable: SkeletonDrawable
public let skeleton: Skeleton
public let animationStateData: AnimationStateData
public let animationState: AnimationState
public let animationStateWrapper: AnimationStateWrapper
internal var disposed = false
/// Constructs a new skeleton drawable from the `atlasFileName` and `skeletonFileName` from the `main` bundle
/// or the optionally provided `bundle`.
///
/// Throws an `Error` in case the data could not be loaded.
public static func fromBundle(atlasFileName: String, skeletonFileName: String, bundle: Bundle = .main) async throws -> SkeletonDrawableWrapper {
let atlasAndPages = try await Atlas.fromBundle(atlasFileName, bundle: bundle)
let skeletonData = try await SkeletonData.fromBundle(
atlas: atlasAndPages.0,
skeletonFileName: skeletonFileName,
bundle: bundle
)
return try SkeletonDrawableWrapper(
atlas: atlasAndPages.0,
atlasPages: atlasAndPages.1,
skeletonData: skeletonData
)
}
/// Constructs a new skeleton drawable from the `atlasFile` and `skeletonFile`.
///
/// Throws an `Error` in case the data could not be loaded.
public static func fromFile(atlasFile: URL, skeletonFile: URL) async throws -> SkeletonDrawableWrapper {
let atlasAndPages = try await Atlas.fromFile(atlasFile)
let skeletonData = try await SkeletonData.fromFile(
atlas: atlasAndPages.0,
skeletonFile: skeletonFile
)
return try SkeletonDrawableWrapper(
atlas: atlasAndPages.0,
atlasPages: atlasAndPages.1,
skeletonData: skeletonData
)
}
/// Constructs a new skeleton drawable wrapper from the http `atlasUrl` and `skeletonUrl`.
///
/// Throws an `Error` in case the data could not be loaded.
public static func fromHttp(atlasURL: URL, skeletonURL: URL) async throws -> SkeletonDrawableWrapper {
let atlasAndPages = try await Atlas.fromHttp(atlasURL)
let skeletonData = try await SkeletonData.fromHttp(
atlas: atlasAndPages.0,
skeletonURL: skeletonURL
)
return try SkeletonDrawableWrapper(
atlas: atlasAndPages.0,
atlasPages: atlasAndPages.1,
skeletonData: skeletonData
)
}
public init(atlas: Atlas, atlasPages: [UIImage], skeletonData: SkeletonData) throws {
self.atlas = atlas
self.atlasPages = atlasPages
self.skeletonData = skeletonData
guard let nativeSkeletonDrawable = spine_skeleton_drawable_create(skeletonData.wrappee) else {
throw "Could not load native skeleton drawable"
}
skeletonDrawable = SkeletonDrawable(nativeSkeletonDrawable)
guard let nativeSkeleton = spine_skeleton_drawable_get_skeleton(skeletonDrawable.wrappee) else {
throw "Could not load native skeleton"
}
skeleton = Skeleton(nativeSkeleton)
guard let nativeAnimationStateData = spine_skeleton_drawable_get_animation_state_data(skeletonDrawable.wrappee) else {
throw "Could not load native animation state data"
}
animationStateData = AnimationStateData(nativeAnimationStateData)
guard let nativeAnimationState = spine_skeleton_drawable_get_animation_state(skeletonDrawable.wrappee) else {
throw "Could not load native animation state"
}
animationState = AnimationState(nativeAnimationState)
animationStateWrapper = AnimationStateWrapper(
animationState: animationState,
aninationStateEvents: skeletonDrawable.animationStateEvents
)
skeleton.updateWorldTransform(physics: SPINE_PHYSICS_NONE)
super.init()
}
/// Updates the ``AnimationState`` using the `delta` time given in seconds, applies the
/// animation state to the ``Skeleton`` and updates the world transforms of the skeleton
/// to calculate its current pose.
public func update(delta: Float) {
if disposed { return }
animationStateWrapper.update(delta: delta)
animationState.apply(skeleton: skeleton)
skeleton.update(delta: delta)
skeleton.updateWorldTransform(physics: SPINE_PHYSICS_UPDATE)
}
public func dispose() {
if disposed { return }
disposed = true
atlas.dispose()
skeletonData.dispose()
skeletonDrawable.dispose()
}
}