From 7a8a564d351751cb6c1959af1cd13357d544c657 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 31 Oct 2026 14:23:11 +0800 Subject: [PATCH 1/3] feat: add transition descriptors --- .../Animation/ORBTransition.swift | 186 +++++++++++++++ .../Animation/ORBTransition.m | 213 ++++++++++++++++++ .../include/OpenRenderBox/ORBTransition.h | 61 +++++ .../Animation/ORBTransition.h | 50 ++++ 4 files changed, 510 insertions(+) create mode 100644 Sources/OpenRenderBox/Animation/ORBTransition.swift create mode 100644 Sources/OpenRenderBoxCxx/Animation/ORBTransition.m create mode 100644 Sources/OpenRenderBoxCxx/include/OpenRenderBox/ORBTransition.h create mode 100644 Sources/OpenRenderBoxCxx/include/OpenRenderBoxObjC/Animation/ORBTransition.h diff --git a/Sources/OpenRenderBox/Animation/ORBTransition.swift b/Sources/OpenRenderBox/Animation/ORBTransition.swift new file mode 100644 index 0000000..5273247 --- /dev/null +++ b/Sources/OpenRenderBox/Animation/ORBTransition.swift @@ -0,0 +1,186 @@ +// +// ORBTransition.swift +// OpenRenderBox + +#if !canImport(ObjectiveC) +public import Foundation +public import OpenRenderBoxCxx + +private func transitionTime(_ value: Float) -> UInt8 { + guard !value.isNaN else { return 0 } + return UInt8(Float(0.5).addingProduct(min(max(value, 0), 1), 255)) +} + +public class ORBTransitionEffect: NSObject { + private var bits: UInt16 = 0x301 + private var begin: UInt8 = 0 + private var length: UInt8 = 255 + private var arguments: [UInt32] = [0, 0] + + public var type: ORBTransitionEffectType { + get { .init(rawValue: UInt32(bits & 127))! } + set { bits = (bits & ~127) | UInt16(newValue.rawValue & 127) } + } + public var flags: ORBTransitionEffectFlags { + get { .init(rawValue: UInt32(bits >> 14)) } + set { bits = (bits & 0x3fff) | UInt16((newValue.rawValue & 3) << 14) } + } + public var events: ORBTransitionEvents { + get { .init(rawValue: UInt32((bits >> 8) & 63)) } + set { bits = (bits & 0xc0ff) | UInt16((newValue.rawValue & 63) << 8) } + } + public var beginTime: Float { + get { bits & 128 != 0 ? 0 : Float(begin) * (1 / 255) } + set { + if bits & 128 != 0 { + bits &= ~128 + length = 255 + } + begin = transitionTime(newValue) + } + } + public var duration: Float { + get { bits & 128 != 0 ? 1 : Float(length) * (1 / 255) } + set { + if bits & 128 != 0 { + bits &= ~128 + begin = 0 + } + length = transitionTime(newValue) + } + } + public var animationIndex: UInt { + get { bits & 128 != 0 && begin == length ? UInt(begin) : 0 } + set { + insertAnimationIndex = newValue + removeAnimationIndex = newValue + } + } + public var insertAnimationIndex: UInt { + get { bits & 128 != 0 ? UInt(begin) : 0 } + set { + if bits & 128 == 0 { + bits |= 128 + length = 0 + } + begin = UInt8(truncatingIfNeeded: newValue) + } + } + public var removeAnimationIndex: UInt { + get { bits & 128 != 0 ? UInt(length) : 0 } + set { + if bits & 128 == 0 { + bits |= 128 + begin = 0 + } + length = UInt8(truncatingIfNeeded: newValue) + } + } + + public func argumentValue(at index: UInt) -> Float { + Float(bitPattern: integerArgumentValue(at: index)) + } + public func setArgumentValue(_ value: Float, at index: UInt) { + setIntegerArgumentValue(value.bitPattern, at: index) + } + public func integerArgumentValue(at index: UInt) -> UInt32 { + index < 2 ? arguments[Int(index)] : 0 + } + public func setIntegerArgumentValue(_ value: UInt32, at index: UInt) { + if index < 2 { arguments[Int(index)] = value } + } + + fileprivate func snapshot() -> ORBTransitionEffect { + let copy = ORBTransitionEffect() + copy.bits = bits + copy.begin = begin + copy.length = length + let counts = [0, 0, 1, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0] + let type = Int(type.rawValue) + let count = type < counts.count ? counts[type] : 2 + for index in 0.. Bool { + guard let other = object as? ORBTransitionEffect else { return false } + return bits == other.bits && begin == other.begin && length == other.length && arguments == other.arguments + } + public override var hash: Int { + var hash = UInt(bits & 0x3fff) | UInt((bits >> 2) & 0x3000) | (UInt(begin) << 16) | (UInt(length) << 24) + hash = hash &* 16_777_619 ^ UInt(arguments[0]) + hash = hash &* 16_777_619 ^ UInt(arguments[1]) + return Int(bitPattern: hash) + } +} + +public class ORBTransition: NSObject, NSCopying { + public static let `default`: ORBTransition = { + let transition = ORBTransition() + transition.isDefault = true + return transition + }() + + private var isDefault = false + private var storedMethod: ORBTransitionMethod = .empty + private var storedAnimation: ORBAnimation? + private var storedEffects: [ORBTransitionEffect] = [] + private var storedDuration: UInt8 = 32 + + public var method: ORBTransitionMethod { + get { storedMethod } + set { + let method: ORBTransitionMethod = newValue.rawValue <= 8 ? newValue : .empty + if method != storedMethod { + precondition(!isDefault, "The default transition cannot be modified.") + storedMethod = method + } + } + } + public var animation: ORBAnimation? { + get { storedAnimation?.isEmpty == false ? storedAnimation?.copy() as? ORBAnimation : nil } + set { + precondition(!isDefault, "The default transition cannot be modified.") + storedAnimation = newValue?.copy() as? ORBAnimation + } + } + public var maxChanges: UInt32 = .max + public var isReplaceable: Bool = false + public var effects: [ORBTransitionEffect] { storedEffects.map { $0.snapshot() } } + public var addRemoveDuration: Float { + get { Float(storedDuration) * (1 / 255) } + set { storedDuration = transitionTime(newValue) } + } + + public func addEffect(_ effect: ORBTransitionEffect?) { + precondition(!isDefault, "The default transition cannot be modified.") + if let effect { storedEffects.append(effect.snapshot()) } + } + public func copy(with zone: NSZone? = nil) -> Any { + let copy = ORBTransition() + copy.storedMethod = storedMethod + copy.storedAnimation = storedAnimation?.copy() as? ORBAnimation + copy.storedEffects = storedEffects + copy.maxChanges = maxChanges + copy.isReplaceable = isReplaceable + copy.storedDuration = storedDuration + return copy + } + public override func isEqual(_ object: Any?) -> Bool { + guard let other = object as? ORBTransition else { return false } + return method == other.method && maxChanges == other.maxChanges + && isReplaceable == other.isReplaceable && storedDuration == other.storedDuration + && storedEffects == other.storedEffects && animation == other.animation + } + public override var hash: Int { + var hasher = Hasher() + hasher.combine(method.rawValue) + hasher.combine(maxChanges) + hasher.combine(isReplaceable) + hasher.combine(storedDuration) + hasher.combine(storedEffects) + hasher.combine(animation) + return hasher.finalize() + } +} +#endif diff --git a/Sources/OpenRenderBoxCxx/Animation/ORBTransition.m b/Sources/OpenRenderBoxCxx/Animation/ORBTransition.m new file mode 100644 index 0000000..09d18e3 --- /dev/null +++ b/Sources/OpenRenderBoxCxx/Animation/ORBTransition.m @@ -0,0 +1,213 @@ +// +// ORBTransition.m +// OpenRenderBox + +#include + +#if ORB_OBJC_FOUNDATION + +#include +#include +#include + +static uint8_t ORBTransitionTime(float value) { + if (isnan(value)) return 0; + return (uint8_t)fmaf(fminf(fmaxf(value, 0), 1), 255, 0.5f); +} + +@interface ORBTransitionEffect () { + uint16_t _bits; + uint8_t _begin; + uint8_t _duration; + uint32_t _arguments[2]; +} +- (ORBTransitionEffect *)orb_snapshot; +@end + +@implementation ORBTransitionEffect + +- (instancetype)init { + if ((self = [super init])) { + _bits = 0x301; + _duration = 255; + } + return self; +} + +- (ORBTransitionEffectType)type { return _bits & 127; } +- (void)setType:(ORBTransitionEffectType)value { _bits = (_bits & ~127) | (value & 127); } +- (ORBTransitionEffectFlags)flags { return _bits >> 14; } +- (void)setFlags:(ORBTransitionEffectFlags)value { _bits = (_bits & 0x3fff) | (value << 14); } +- (ORBTransitionEvents)events { return (_bits >> 8) & 63; } +- (void)setEvents:(ORBTransitionEvents)value { _bits = (_bits & 0xc0ff) | ((value & 63) << 8); } + +- (float)beginTime { return (_bits & 128) ? 0 : _begin * (1.0f / 255); } +- (void)setBeginTime:(float)value { + if (_bits & 128) { + _bits &= ~128; + _duration = 255; + } + _begin = ORBTransitionTime(value); +} + +- (float)duration { return (_bits & 128) ? 1 : _duration * (1.0f / 255); } +- (void)setDuration:(float)value { + if (_bits & 128) { + _bits &= ~128; + _begin = 0; + } + _duration = ORBTransitionTime(value); +} + +- (NSUInteger)animationIndex { return (_bits & 128) && _begin == _duration ? _begin : 0; } +- (void)setAnimationIndex:(NSUInteger)value { + self.insertAnimationIndex = value; + self.removeAnimationIndex = value; +} +- (NSUInteger)insertAnimationIndex { return (_bits & 128) ? _begin : 0; } +- (void)setInsertAnimationIndex:(NSUInteger)value { + if (!(_bits & 128)) { + _bits |= 128; + _duration = 0; + } + _begin = value; +} +- (NSUInteger)removeAnimationIndex { return (_bits & 128) ? _duration : 0; } +- (void)setRemoveAnimationIndex:(NSUInteger)value { + if (!(_bits & 128)) { + _bits |= 128; + _begin = 0; + } + _duration = value; +} + +- (float)argumentValueAtIndex:(NSUInteger)index { + float value = 0; + if (index < 2) memcpy(&value, &_arguments[index], sizeof(value)); + return value; +} +- (void)setArgumentValue:(float)value atIndex:(NSUInteger)index { + if (index < 2) memcpy(&_arguments[index], &value, sizeof(value)); +} +- (uint32_t)integerArgumentValueAtIndex:(NSUInteger)index { return index < 2 ? _arguments[index] : 0; } +- (void)setIntegerArgumentValue:(uint32_t)value atIndex:(NSUInteger)index { + if (index < 2) _arguments[index] = value; +} + +- (ORBTransitionEffect *)orb_snapshot { + ORBTransitionEffect *copy = [ORBTransitionEffect new]; + copy->_bits = _bits; + copy->_begin = _begin; + copy->_duration = _duration; + static const uint8_t argumentCounts[] = {0, 0, 1, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0}; + NSUInteger type = self.type; + NSUInteger count = type < sizeof(argumentCounts) ? argumentCounts[type] : 2; + memcpy(copy->_arguments, _arguments, count * sizeof(uint32_t)); + return copy; +} + +- (BOOL)isEqual:(id)object { + if (![object isKindOfClass:[ORBTransitionEffect class]]) return NO; + ORBTransitionEffect *other = object; + return _bits == other->_bits && _begin == other->_begin && _duration == other->_duration + && _arguments[0] == other->_arguments[0] && _arguments[1] == other->_arguments[1]; +} + +- (NSUInteger)hash { + NSUInteger hash = (_bits & 0x3fff) | ((_bits >> 2) & 0x3000) | ((NSUInteger)_begin << 16) | ((NSUInteger)_duration << 24); + hash = hash * 16777619 ^ _arguments[0]; + return hash * 16777619 ^ _arguments[1]; +} + +@end + +@interface ORBAnimation (TransitionStorage) +- (BOOL)orb_isEmpty; +@end + +@interface ORBTransition () { + ORBTransitionMethod _method; + ORBAnimation *_animation; + NSMutableArray *_effects; + uint8_t _addRemoveDuration; + BOOL _isDefault; +} +@end + +@implementation ORBTransition + ++ (ORBTransition *)defaultTransition { + static ORBTransition *transition; + static dispatch_once_t once; + dispatch_once(&once, ^{ + transition = [ORBTransition new]; + transition->_isDefault = YES; + }); + return transition; +} + +- (instancetype)init { + if ((self = [super init])) { + _maxChanges = UINT32_MAX; + _addRemoveDuration = 32; + _effects = [NSMutableArray new]; + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + ORBTransition *copy = [ORBTransition new]; + copy->_method = _method; + copy->_animation = [_animation copy]; + copy->_effects = [_effects mutableCopy]; + copy->_maxChanges = _maxChanges; + copy->_replaceable = _replaceable; + copy->_addRemoveDuration = _addRemoveDuration; + return copy; +} + +- (ORBTransitionMethod)method { return _method; } +- (void)setMethod:(ORBTransitionMethod)value { + value = value <= ORBTransitionMethodPrefixAndSuffix ? value : ORBTransitionMethodEmpty; + if (_method != value) { + NSAssert(!_isDefault, @"The default transition cannot be modified."); + _method = value; + } +} +- (ORBAnimation *)animation { return [_animation orb_isEmpty] ? nil : [_animation copy]; } +- (void)setAnimation:(ORBAnimation *)value { + NSAssert(!_isDefault, @"The default transition cannot be modified."); + _animation = [value copy]; +} +- (NSArray *)effects { + NSMutableArray *effects = [NSMutableArray arrayWithCapacity:_effects.count]; + for (ORBTransitionEffect *effect in _effects) [effects addObject:[effect orb_snapshot]]; + return effects; +} +- (void)addEffect:(ORBTransitionEffect *)effect { + NSAssert(!_isDefault, @"The default transition cannot be modified."); + if (effect) [_effects addObject:[effect orb_snapshot]]; +} +- (float)addRemoveDuration { return _addRemoveDuration * (1.0f / 255); } +- (void)setAddRemoveDuration:(float)value { _addRemoveDuration = ORBTransitionTime(value); } + +- (BOOL)isEqual:(id)object { + if (![object isKindOfClass:[ORBTransition class]]) return NO; + ORBTransition *other = object; + ORBAnimation *animation = self.animation; + ORBAnimation *otherAnimation = other.animation; + return _method == other->_method && _maxChanges == other->_maxChanges + && _replaceable == other->_replaceable && _addRemoveDuration == other->_addRemoveDuration + && [_effects isEqual:other->_effects] + && (animation == otherAnimation || [animation isEqual:otherAnimation]); +} +- (NSUInteger)hash { + NSUInteger hash = _method ^ _maxChanges ^ _replaceable ^ _addRemoveDuration; + hash = hash * 33 ^ self.animation.hash; + for (ORBTransitionEffect *effect in _effects) hash = hash * 33 ^ effect.hash; + return hash; +} + +@end + +#endif /* ORB_OBJC_FOUNDATION */ diff --git a/Sources/OpenRenderBoxCxx/include/OpenRenderBox/ORBTransition.h b/Sources/OpenRenderBoxCxx/include/OpenRenderBox/ORBTransition.h new file mode 100644 index 0000000..bd57342 --- /dev/null +++ b/Sources/OpenRenderBoxCxx/include/OpenRenderBox/ORBTransition.h @@ -0,0 +1,61 @@ +// +// ORBTransition.h +// OpenRenderBox + +#pragma once + +#include + +ORB_ASSUME_NONNULL_BEGIN + +ORB_EXTERN_C_BEGIN + +typedef ORB_ENUM(uint32_t, ORBTransitionMethod) { + ORBTransitionMethodEmpty = 0, + ORBTransitionMethodDiff = 1, + ORBTransitionMethodForwards = 2, + ORBTransitionMethodBackwards = 3, + ORBTransitionMethodPrefix = 4, + ORBTransitionMethodSuffix = 5, + ORBTransitionMethodBinary = 6, + ORBTransitionMethodNone = 7, + ORBTransitionMethodPrefixAndSuffix = 8, +}; + +typedef ORB_OPTIONS(uint32_t, ORBTransitionEvents) { + ORBTransitionEventsAdd = 1 << 0, + ORBTransitionEventsRemove = 1 << 1, + ORBTransitionEventsAddRemove = ORBTransitionEventsAdd | ORBTransitionEventsRemove, +}; + +typedef ORB_OPTIONS(uint32_t, ORBTransitionEffectFlags) { + ORBTransitionEffectFlagsRemoveInverts = 1 << 0, +}; + +typedef ORB_ENUM(uint32_t, ORBTransitionEffectType) { + ORBTransitionEffectTypeNone = 0, + ORBTransitionEffectTypeAlpha = 1, + ORBTransitionEffectTypeScaleCenter = 2, + ORBTransitionEffectTypeTranslation = 3, + ORBTransitionEffectTypeBlur = 4, + ORBTransitionEffectTypeMatchMove = 5, + ORBTransitionEffectTypeScaleOrigin = 6, + ORBTransitionEffectTypeAnchorLeft = 7, + ORBTransitionEffectTypeAnchorRight = 8, + ORBTransitionEffectTypeAnchorUp = 9, + ORBTransitionEffectTypeAnchorDown = 10, + ORBTransitionEffectTypeSequenceLeft = 11, + ORBTransitionEffectTypeSequenceRight = 12, + ORBTransitionEffectTypeSequenceUp = 13, + ORBTransitionEffectTypeSequenceDown = 14, + ORBTransitionEffectTypeRelativeTranslation = 15, + ORBTransitionEffectTypeRelativeBlur = 16, + ORBTransitionEffectTypeAppearDisappear = 17, + ORBTransitionEffectTypeReplace = 18, + ORBTransitionEffectTypeSequenceForwards = 19, + ORBTransitionEffectTypeSequenceBackwards = 20, +}; + +ORB_EXTERN_C_END + +ORB_ASSUME_NONNULL_END diff --git a/Sources/OpenRenderBoxCxx/include/OpenRenderBoxObjC/Animation/ORBTransition.h b/Sources/OpenRenderBoxCxx/include/OpenRenderBoxObjC/Animation/ORBTransition.h new file mode 100644 index 0000000..a0918ff --- /dev/null +++ b/Sources/OpenRenderBoxCxx/include/OpenRenderBoxObjC/Animation/ORBTransition.h @@ -0,0 +1,50 @@ +// +// ORBTransition.h +// OpenRenderBox + +#pragma once + +#include + +#if ORB_OBJC_FOUNDATION + +#include +#include + +ORB_ASSUME_NONNULL_BEGIN + +@interface ORBTransitionEffect : NSObject + +@property (nonatomic) ORBTransitionEffectType type; +@property (nonatomic) ORBTransitionEffectFlags flags; +@property (nonatomic) ORBTransitionEvents events; +@property (nonatomic) float beginTime; +@property (nonatomic) float duration; +@property (nonatomic) NSUInteger animationIndex; +@property (nonatomic) NSUInteger insertAnimationIndex; +@property (nonatomic) NSUInteger removeAnimationIndex; + +- (float)argumentValueAtIndex:(NSUInteger)index; +- (void)setArgumentValue:(float)value atIndex:(NSUInteger)index; +- (uint32_t)integerArgumentValueAtIndex:(NSUInteger)index; +- (void)setIntegerArgumentValue:(uint32_t)value atIndex:(NSUInteger)index; + +@end + +@interface ORBTransition : NSObject + +@property (class, readonly, nonatomic) ORBTransition *defaultTransition; +@property (nonatomic) ORBTransitionMethod method; +@property (nonatomic, strong, nullable) ORBAnimation *animation; +@property (nonatomic) uint32_t maxChanges; +@property (nonatomic, getter=isReplaceable) BOOL replaceable; +@property (readonly, nonatomic, copy) NSArray *effects; +@property (nonatomic) float addRemoveDuration; + +- (void)addEffect:(nullable ORBTransitionEffect *)effect; + +@end + +ORB_ASSUME_NONNULL_END + +#endif /* ORB_OBJC_FOUNDATION */ From 6653bec71ac599988b53033b6f77e8155fb55cfc Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 31 Oct 2026 15:20:29 +0800 Subject: [PATCH 2/3] feat(animation): implement transition storage and animation descriptors --- .../Animation/ORBAnimation.swift | 31 ++- .../OpenRenderBoxCxx/Animation/Animation.cpp | 55 +++++ .../OpenRenderBoxCxx/Animation/ORBAnimation.m | 15 -- .../Animation/ORBAnimation.mm | 45 ++++ .../Animation/ORBAnimation_Private.h | 16 ++ .../Animation/ORBTransition.m | 213 ----------------- .../Animation/ORBTransition.mm | 221 ++++++++++++++++++ .../OpenRenderBoxCxx/Animation/Transition.cpp | 101 ++++++++ .../include/OpenRenderBox/OpenRenderBox.h | 1 + .../OpenRenderBoxCxx/Animation/Animation.hpp | 42 ++++ .../OpenRenderBoxCxx/Animation/Transition.hpp | 110 +++++++++ .../OpenRenderBoxCxx/Util/refcounted_ptr.hpp | 56 +++++ .../OpenRenderBoxCxx/Vector/vector.tpp | 2 +- .../OpenRenderBoxCxx/include/module.modulemap | 6 + Sources/OpenRenderBoxShims/ORBShims.swift | 6 + .../ORBShims.swift | 6 + .../TransitionTests.swift | 138 +++++++++++ .../Animation/TransitionStorageTests.swift | 74 ++++++ 18 files changed, 905 insertions(+), 233 deletions(-) create mode 100644 Sources/OpenRenderBoxCxx/Animation/Animation.cpp delete mode 100644 Sources/OpenRenderBoxCxx/Animation/ORBAnimation.m create mode 100644 Sources/OpenRenderBoxCxx/Animation/ORBAnimation.mm create mode 100644 Sources/OpenRenderBoxCxx/Animation/ORBAnimation_Private.h delete mode 100644 Sources/OpenRenderBoxCxx/Animation/ORBTransition.m create mode 100644 Sources/OpenRenderBoxCxx/Animation/ORBTransition.mm create mode 100644 Sources/OpenRenderBoxCxx/Animation/Transition.cpp create mode 100644 Sources/OpenRenderBoxCxx/include/OpenRenderBoxCxx/Animation/Animation.hpp create mode 100644 Sources/OpenRenderBoxCxx/include/OpenRenderBoxCxx/Animation/Transition.hpp create mode 100644 Sources/OpenRenderBoxCxx/include/OpenRenderBoxCxx/Util/refcounted_ptr.hpp create mode 100644 Tests/OpenRenderBoxCompatibilityTests/TransitionTests.swift create mode 100644 Tests/OpenRenderBoxTests/Animation/TransitionStorageTests.swift diff --git a/Sources/OpenRenderBox/Animation/ORBAnimation.swift b/Sources/OpenRenderBox/Animation/ORBAnimation.swift index a7635bd..bdfa548 100644 --- a/Sources/OpenRenderBox/Animation/ORBAnimation.swift +++ b/Sources/OpenRenderBox/Animation/ORBAnimation.swift @@ -6,15 +6,36 @@ #if !canImport(ObjectiveC) public import Foundation -public class ORBAnimation: NSObject { - public private(set) var activeDuration: Double = 0 +public class ORBAnimation: NSObject, NSCopying { + private var springTerms: [UInt32] = [] + + var isEmpty: Bool { springTerms.isEmpty } + + public var activeDuration: Double { + isEmpty ? 0 : Double(Float(bitPattern: springTerms[1])) + } public override init() { super.init() } public func removeAll() { - _openRenderBoxUnimplementedFailure() + springTerms.removeAll() + } + + public func copy(with zone: NSZone? = nil) -> Any { + let copy = ORBAnimation() + copy.springTerms = springTerms + return copy + } + + public override func isEqual(_ object: Any?) -> Bool { + guard let other = object as? ORBAnimation else { return false } + return springTerms == other.springTerms + } + + public override var hash: Int { + springTerms.hashValue } public func evaluate(atTime time: Double) -> Float { @@ -46,7 +67,9 @@ public class ORBAnimation: NSObject { } public func addSpringDuration(_ duration: Double, mass: Double, stiffness: Double, damping: Double, initialVelocity velocity: Double) { - _openRenderBoxUnimplementedFailure() + springTerms.append(velocity == 0 ? 9 : 10) + springTerms.append(contentsOf: [duration, mass, stiffness, damping].map { Float($0).bitPattern }) + if velocity != 0 { springTerms.append(Float(velocity).bitPattern) } } } #endif diff --git a/Sources/OpenRenderBoxCxx/Animation/Animation.cpp b/Sources/OpenRenderBoxCxx/Animation/Animation.cpp new file mode 100644 index 0000000..2c29925 --- /dev/null +++ b/Sources/OpenRenderBoxCxx/Animation/Animation.cpp @@ -0,0 +1,55 @@ +// +// Animation.cpp +// OpenRenderBox + +#include +#include + +namespace ORB { + +Animation::Animation(const Animation &other) { + *this = other; +} + +Animation &Animation::operator=(const Animation &other) { + if (this != &other) { + _terms.reserve(other._terms.size()); + _terms.clear(); + for (uint32_t i = 0; i < other._terms.size(); ++i) { + _terms.push_back(other._terms[i]); + } + } + return *this; +} + +float Animation::active_duration() const { + return _terms.empty() ? 0 : _terms[1].argument; +} + +void Animation::add_spring(double duration, double mass, double stiffness, double damping, double velocity) { + bool has_velocity = velocity != 0; + _terms.reserve(_terms.size() + (has_velocity ? 6 : 5)); + _terms.push_back(TermOrArg(uint32_t(has_velocity ? 10 : 9))); + _terms.push_back(TermOrArg(float(duration))); + _terms.push_back(TermOrArg(float(mass))); + _terms.push_back(TermOrArg(float(stiffness))); + _terms.push_back(TermOrArg(float(damping))); + if (has_velocity) _terms.push_back(TermOrArg(float(velocity))); +} + +bool Animation::operator==(const Animation &other) const { + return _terms.size() == other._terms.size() + && (_terms.empty() || std::memcmp(_terms.data(), other._terms.data(), _terms.size() * sizeof(TermOrArg)) == 0); +} + +size_t Animation::hash() const { + size_t hash = 2166136261; + for (uint32_t i = 0; i < _terms.size(); ++i) { + uint32_t word; + std::memcpy(&word, &_terms[i], sizeof(word)); + hash = hash * 16777619 ^ word; + } + return hash; +} + +} /* ORB */ diff --git a/Sources/OpenRenderBoxCxx/Animation/ORBAnimation.m b/Sources/OpenRenderBoxCxx/Animation/ORBAnimation.m deleted file mode 100644 index b8b8fbf..0000000 --- a/Sources/OpenRenderBoxCxx/Animation/ORBAnimation.m +++ /dev/null @@ -1,15 +0,0 @@ -// -// ORBAnimation.m -// OpenRenderBox - -#include - -#if ORB_OBJC_FOUNDATION - -@implementation ORBAnimation - -// TODO - -@end - -#endif /* ORB_OBJC_FOUNDATION */ diff --git a/Sources/OpenRenderBoxCxx/Animation/ORBAnimation.mm b/Sources/OpenRenderBoxCxx/Animation/ORBAnimation.mm new file mode 100644 index 0000000..61e3c7b --- /dev/null +++ b/Sources/OpenRenderBoxCxx/Animation/ORBAnimation.mm @@ -0,0 +1,45 @@ +// +// ORBAnimation.mm +// OpenRenderBox + +#include + +#if ORB_OBJC_FOUNDATION + +#include "ORBAnimation_Private.h" + +@implementation ORBAnimation { + ORB::Animation _animation; +} + +- (id)copyWithZone:(NSZone *)zone { + ORBAnimation *copy = [ORBAnimation new]; + copy->_animation = _animation; + return copy; +} + +- (BOOL)isEqual:(id)object { + return [object isKindOfClass:[ORBAnimation class]] + && _animation == ((ORBAnimation *)object)->_animation; +} + +- (NSUInteger)hash { return _animation.hash(); } +- (BOOL)orb_isEmpty { return _animation.empty(); } +- (void)removeAll { _animation.clear(); } + +- (double)activeDuration { + return _animation.active_duration(); +} + +- (void)addSpringDuration:(double)duration mass:(double)mass stiffness:(double)stiffness damping:(double)damping initialVelocity:(double)velocity { + _animation.add_spring(duration, mass, stiffness, damping, velocity); +} + +- (const ORB::Animation &)orb_animation { return _animation; } +- (void)setORBAnimation:(const ORB::Animation &)animation { _animation = animation; } + +// Other animation terms and evaluation require the animation engine. + +@end + +#endif /* ORB_OBJC_FOUNDATION */ diff --git a/Sources/OpenRenderBoxCxx/Animation/ORBAnimation_Private.h b/Sources/OpenRenderBoxCxx/Animation/ORBAnimation_Private.h new file mode 100644 index 0000000..a8b340c --- /dev/null +++ b/Sources/OpenRenderBoxCxx/Animation/ORBAnimation_Private.h @@ -0,0 +1,16 @@ +// +// ORBAnimation_Private.h +// OpenRenderBox + +#pragma once + +#include + +#if ORB_OBJC_FOUNDATION && defined(__cplusplus) +#include + +@interface ORBAnimation () +- (const ORB::Animation &)orb_animation __attribute__((objc_direct)); +- (void)setORBAnimation:(const ORB::Animation &)animation __attribute__((objc_direct)); +@end +#endif diff --git a/Sources/OpenRenderBoxCxx/Animation/ORBTransition.m b/Sources/OpenRenderBoxCxx/Animation/ORBTransition.m deleted file mode 100644 index 09d18e3..0000000 --- a/Sources/OpenRenderBoxCxx/Animation/ORBTransition.m +++ /dev/null @@ -1,213 +0,0 @@ -// -// ORBTransition.m -// OpenRenderBox - -#include - -#if ORB_OBJC_FOUNDATION - -#include -#include -#include - -static uint8_t ORBTransitionTime(float value) { - if (isnan(value)) return 0; - return (uint8_t)fmaf(fminf(fmaxf(value, 0), 1), 255, 0.5f); -} - -@interface ORBTransitionEffect () { - uint16_t _bits; - uint8_t _begin; - uint8_t _duration; - uint32_t _arguments[2]; -} -- (ORBTransitionEffect *)orb_snapshot; -@end - -@implementation ORBTransitionEffect - -- (instancetype)init { - if ((self = [super init])) { - _bits = 0x301; - _duration = 255; - } - return self; -} - -- (ORBTransitionEffectType)type { return _bits & 127; } -- (void)setType:(ORBTransitionEffectType)value { _bits = (_bits & ~127) | (value & 127); } -- (ORBTransitionEffectFlags)flags { return _bits >> 14; } -- (void)setFlags:(ORBTransitionEffectFlags)value { _bits = (_bits & 0x3fff) | (value << 14); } -- (ORBTransitionEvents)events { return (_bits >> 8) & 63; } -- (void)setEvents:(ORBTransitionEvents)value { _bits = (_bits & 0xc0ff) | ((value & 63) << 8); } - -- (float)beginTime { return (_bits & 128) ? 0 : _begin * (1.0f / 255); } -- (void)setBeginTime:(float)value { - if (_bits & 128) { - _bits &= ~128; - _duration = 255; - } - _begin = ORBTransitionTime(value); -} - -- (float)duration { return (_bits & 128) ? 1 : _duration * (1.0f / 255); } -- (void)setDuration:(float)value { - if (_bits & 128) { - _bits &= ~128; - _begin = 0; - } - _duration = ORBTransitionTime(value); -} - -- (NSUInteger)animationIndex { return (_bits & 128) && _begin == _duration ? _begin : 0; } -- (void)setAnimationIndex:(NSUInteger)value { - self.insertAnimationIndex = value; - self.removeAnimationIndex = value; -} -- (NSUInteger)insertAnimationIndex { return (_bits & 128) ? _begin : 0; } -- (void)setInsertAnimationIndex:(NSUInteger)value { - if (!(_bits & 128)) { - _bits |= 128; - _duration = 0; - } - _begin = value; -} -- (NSUInteger)removeAnimationIndex { return (_bits & 128) ? _duration : 0; } -- (void)setRemoveAnimationIndex:(NSUInteger)value { - if (!(_bits & 128)) { - _bits |= 128; - _begin = 0; - } - _duration = value; -} - -- (float)argumentValueAtIndex:(NSUInteger)index { - float value = 0; - if (index < 2) memcpy(&value, &_arguments[index], sizeof(value)); - return value; -} -- (void)setArgumentValue:(float)value atIndex:(NSUInteger)index { - if (index < 2) memcpy(&_arguments[index], &value, sizeof(value)); -} -- (uint32_t)integerArgumentValueAtIndex:(NSUInteger)index { return index < 2 ? _arguments[index] : 0; } -- (void)setIntegerArgumentValue:(uint32_t)value atIndex:(NSUInteger)index { - if (index < 2) _arguments[index] = value; -} - -- (ORBTransitionEffect *)orb_snapshot { - ORBTransitionEffect *copy = [ORBTransitionEffect new]; - copy->_bits = _bits; - copy->_begin = _begin; - copy->_duration = _duration; - static const uint8_t argumentCounts[] = {0, 0, 1, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0}; - NSUInteger type = self.type; - NSUInteger count = type < sizeof(argumentCounts) ? argumentCounts[type] : 2; - memcpy(copy->_arguments, _arguments, count * sizeof(uint32_t)); - return copy; -} - -- (BOOL)isEqual:(id)object { - if (![object isKindOfClass:[ORBTransitionEffect class]]) return NO; - ORBTransitionEffect *other = object; - return _bits == other->_bits && _begin == other->_begin && _duration == other->_duration - && _arguments[0] == other->_arguments[0] && _arguments[1] == other->_arguments[1]; -} - -- (NSUInteger)hash { - NSUInteger hash = (_bits & 0x3fff) | ((_bits >> 2) & 0x3000) | ((NSUInteger)_begin << 16) | ((NSUInteger)_duration << 24); - hash = hash * 16777619 ^ _arguments[0]; - return hash * 16777619 ^ _arguments[1]; -} - -@end - -@interface ORBAnimation (TransitionStorage) -- (BOOL)orb_isEmpty; -@end - -@interface ORBTransition () { - ORBTransitionMethod _method; - ORBAnimation *_animation; - NSMutableArray *_effects; - uint8_t _addRemoveDuration; - BOOL _isDefault; -} -@end - -@implementation ORBTransition - -+ (ORBTransition *)defaultTransition { - static ORBTransition *transition; - static dispatch_once_t once; - dispatch_once(&once, ^{ - transition = [ORBTransition new]; - transition->_isDefault = YES; - }); - return transition; -} - -- (instancetype)init { - if ((self = [super init])) { - _maxChanges = UINT32_MAX; - _addRemoveDuration = 32; - _effects = [NSMutableArray new]; - } - return self; -} - -- (id)copyWithZone:(NSZone *)zone { - ORBTransition *copy = [ORBTransition new]; - copy->_method = _method; - copy->_animation = [_animation copy]; - copy->_effects = [_effects mutableCopy]; - copy->_maxChanges = _maxChanges; - copy->_replaceable = _replaceable; - copy->_addRemoveDuration = _addRemoveDuration; - return copy; -} - -- (ORBTransitionMethod)method { return _method; } -- (void)setMethod:(ORBTransitionMethod)value { - value = value <= ORBTransitionMethodPrefixAndSuffix ? value : ORBTransitionMethodEmpty; - if (_method != value) { - NSAssert(!_isDefault, @"The default transition cannot be modified."); - _method = value; - } -} -- (ORBAnimation *)animation { return [_animation orb_isEmpty] ? nil : [_animation copy]; } -- (void)setAnimation:(ORBAnimation *)value { - NSAssert(!_isDefault, @"The default transition cannot be modified."); - _animation = [value copy]; -} -- (NSArray *)effects { - NSMutableArray *effects = [NSMutableArray arrayWithCapacity:_effects.count]; - for (ORBTransitionEffect *effect in _effects) [effects addObject:[effect orb_snapshot]]; - return effects; -} -- (void)addEffect:(ORBTransitionEffect *)effect { - NSAssert(!_isDefault, @"The default transition cannot be modified."); - if (effect) [_effects addObject:[effect orb_snapshot]]; -} -- (float)addRemoveDuration { return _addRemoveDuration * (1.0f / 255); } -- (void)setAddRemoveDuration:(float)value { _addRemoveDuration = ORBTransitionTime(value); } - -- (BOOL)isEqual:(id)object { - if (![object isKindOfClass:[ORBTransition class]]) return NO; - ORBTransition *other = object; - ORBAnimation *animation = self.animation; - ORBAnimation *otherAnimation = other.animation; - return _method == other->_method && _maxChanges == other->_maxChanges - && _replaceable == other->_replaceable && _addRemoveDuration == other->_addRemoveDuration - && [_effects isEqual:other->_effects] - && (animation == otherAnimation || [animation isEqual:otherAnimation]); -} -- (NSUInteger)hash { - NSUInteger hash = _method ^ _maxChanges ^ _replaceable ^ _addRemoveDuration; - hash = hash * 33 ^ self.animation.hash; - for (ORBTransitionEffect *effect in _effects) hash = hash * 33 ^ effect.hash; - return hash; -} - -@end - -#endif /* ORB_OBJC_FOUNDATION */ diff --git a/Sources/OpenRenderBoxCxx/Animation/ORBTransition.mm b/Sources/OpenRenderBoxCxx/Animation/ORBTransition.mm new file mode 100644 index 0000000..0c6ad8f --- /dev/null +++ b/Sources/OpenRenderBoxCxx/Animation/ORBTransition.mm @@ -0,0 +1,221 @@ +// +// ORBTransition.mm +// OpenRenderBox + +#include + +#if ORB_OBJC_FOUNDATION + +#include +#include +#include +#include "ORBAnimation_Private.h" +#include + +@interface ORBTransitionEffect () { +@package + ORB::Transition::Effect _effect; + uint32_t _args[2]; +} +@end + +@implementation ORBTransitionEffect + +- (ORBTransitionEffectType)type { return static_cast(_effect.type()); } +- (void)setType:(ORBTransitionEffectType)value { _effect.set_type(value); } +- (ORBTransitionEffectFlags)flags { return _effect.flags(); } +- (void)setFlags:(ORBTransitionEffectFlags)value { _effect.set_flags(value); } +- (ORBTransitionEvents)events { return _effect.events(); } +- (void)setEvents:(ORBTransitionEvents)value { _effect.set_events(value); } + +- (float)beginTime { return _effect.begin_time(); } +- (void)setBeginTime:(float)value { _effect.set_begin_time(value); } +- (float)duration { return _effect.duration_value(); } +- (void)setDuration:(float)value { _effect.set_duration(value); } + +- (NSUInteger)animationIndex { return _effect.animation_index(); } +- (void)setAnimationIndex:(NSUInteger)value { _effect.set_animation_index(value); } +- (NSUInteger)insertAnimationIndex { return _effect.insert_animation(); } +- (void)setInsertAnimationIndex:(NSUInteger)value { _effect.set_insert_animation(value); } +- (NSUInteger)removeAnimationIndex { return _effect.remove_animation(); } +- (void)setRemoveAnimationIndex:(NSUInteger)value { _effect.set_remove_animation(value); } + +- (float)argumentValueAtIndex:(NSUInteger)index { + float value = 0; + if (index < 2) std::memcpy(&value, &_args[index], sizeof(value)); + return value; +} +- (void)setArgumentValue:(float)value atIndex:(NSUInteger)index { + if (index < 2) std::memcpy(&_args[index], &value, sizeof(value)); +} +- (uint32_t)integerArgumentValueAtIndex:(NSUInteger)index { return index < 2 ? _args[index] : 0; } +- (void)setIntegerArgumentValue:(uint32_t)value atIndex:(NSUInteger)index { + if (index < 2) _args[index] = value; +} + +- (BOOL)isEqual:(id)object { + if (![object isKindOfClass:[ORBTransitionEffect class]]) return NO; + ORBTransitionEffect *other = object; + return _effect.bits == other->_effect.bits && _effect.begin == other->_effect.begin + && _effect.duration == other->_effect.duration && _args[0] == other->_args[0] && _args[1] == other->_args[1]; +} + +- (NSUInteger)hash { + NSUInteger hash = _effect.hash() * 16777619 ^ _args[0]; + return hash * 16777619 ^ _args[1]; +} + +@end + +static ORB::Transition::Method ORBTransitionMethodValue(ORBTransitionMethod method) { + using Method = ORB::Transition::Method; + switch (method) { + case ORBTransitionMethodDiff: return Method::Diff; + case ORBTransitionMethodForwards: return Method::Forwards; + case ORBTransitionMethodBackwards: return Method::Backwards; + case ORBTransitionMethodPrefixAndSuffix: return Method::PrefixSuffix; + case ORBTransitionMethodPrefix: return Method::Prefix; + case ORBTransitionMethodSuffix: return Method::Suffix; + case ORBTransitionMethodBinary: return Method::Binary; + case ORBTransitionMethodNone: return Method::None; + default: return Method::Default; + } +} + +static ORBTransitionMethod ORBTransitionMethodFromValue(ORB::Transition::Method method) { + using Method = ORB::Transition::Method; + switch (method) { + case Method::Diff: return ORBTransitionMethodDiff; + case Method::Forwards: return ORBTransitionMethodForwards; + case Method::Backwards: return ORBTransitionMethodBackwards; + case Method::PrefixSuffix: return ORBTransitionMethodPrefixAndSuffix; + case Method::Prefix: return ORBTransitionMethodPrefix; + case Method::Suffix: return ORBTransitionMethodSuffix; + case Method::Binary: return ORBTransitionMethodBinary; + case Method::None: return ORBTransitionMethodNone; + default: return ORBTransitionMethodEmpty; + } +} + +@interface ORBTransition () { + ORB::refcounted_ptr _transition; + BOOL _is_default; +} +- (void)_copyOnWrite __attribute__((objc_direct)); +- (ORB::Transition *)orb_transition __attribute__((objc_direct)); +- (void)setTransition:(ORB::Transition *)transition __attribute__((objc_direct)); +@end + +@implementation ORBTransition + ++ (ORBTransition *)defaultTransition { + static ORBTransition *default_transition = [] { + ORBTransition *transition = [ORBTransition new]; + transition->_is_default = YES; + return transition; + }(); + return default_transition; +} + +- (instancetype)init { + if ((self = [super init])) { + _transition = ORB::refcounted_ptr(new ORB::Transition); + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + ORBTransition *copy = [ORBTransition new]; + copy->_transition = _transition; + return copy; +} + +- (void)_copyOnWrite { + if (self && !_transition->is_unique()) { + _transition = ORB::refcounted_ptr(new ORB::Transition(*_transition)); + } +} + +- (ORBTransitionMethod)method { return ORBTransitionMethodFromValue(_transition->method()); } +- (void)setMethod:(ORBTransitionMethod)value { + ORB::Transition::Method method = ORBTransitionMethodValue(value); + if (_transition->method() != method) { + if (_is_default) ORB::precondition_failure("modifying default transition"); + [self _copyOnWrite]; + _transition->set_method(method); + } +} + +- (ORBAnimation *)animation { + if (_transition->animation().empty()) return nil; + ORBAnimation *animation = [ORBAnimation new]; + [animation setORBAnimation:_transition->animation()]; + return animation; +} +- (void)setAnimation:(ORBAnimation *)value { + if (_is_default) ORB::precondition_failure("modifying default transition"); + [self _copyOnWrite]; + if (value) _transition->set_animation([value orb_animation]); + else _transition->clear_animation(); +} + +- (uint32_t)maxChanges { return _transition->max_changes(); } +- (void)setMaxChanges:(uint32_t)value { + if (_transition->max_changes() != value) { + [self _copyOnWrite]; + _transition->set_max_changes(value); + } +} +- (BOOL)isReplaceable { return _transition->replaceable(); } +- (void)setReplaceable:(BOOL)value { + if (_transition->replaceable() != value) { + [self _copyOnWrite]; + _transition->set_replaceable(value); + } +} +- (float)addRemoveDuration { return _transition->add_remove_duration(); } +- (void)setAddRemoveDuration:(float)value { + [self _copyOnWrite]; + _transition->set_add_remove_duration(value); +} + +- (NSArray *)effects { + const auto &values = _transition->effects(); + if (values.empty()) return @[]; + NSMutableArray *effects = [NSMutableArray array]; + for (uint32_t index = 0; index < values.size();) { + ORBTransitionEffect *effect = [ORBTransitionEffect new]; + effect->_effect = values[index++].effect; + uint8_t count = effect->_effect.argument_count(); + for (uint8_t i = 0; i < count; ++i) effect->_args[i] = values[index++].argument; + [effects addObject:effect]; + } + return effects; +} +- (void)addEffect:(ORBTransitionEffect *)effect { + if (_is_default) ORB::precondition_failure("modifying default transition"); + if (effect) { + [self _copyOnWrite]; + _transition->add_effect(effect->_effect, effect->_args); + } +} + +- (ORB::Transition *)orb_transition { return _transition.get(); } +- (void)setTransition:(ORB::Transition *)transition { + if (!self) return; + if (_is_default) ORB::precondition_failure("modifying default transition"); + if (_transition.get() != transition) { + transition->retain(); + _transition = ORB::refcounted_ptr(transition); + } +} + +- (BOOL)isEqual:(id)object { + return [object isKindOfClass:[ORBTransition class]] + && *_transition == *((ORBTransition *)object)->_transition; +} +- (NSUInteger)hash { return _transition->hash(); } + +@end + +#endif /* ORB_OBJC_FOUNDATION */ diff --git a/Sources/OpenRenderBoxCxx/Animation/Transition.cpp b/Sources/OpenRenderBoxCxx/Animation/Transition.cpp new file mode 100644 index 0000000..66ebf5c --- /dev/null +++ b/Sources/OpenRenderBoxCxx/Animation/Transition.cpp @@ -0,0 +1,101 @@ +// +// Transition.cpp +// OpenRenderBox + +#include +#include +#include + +namespace ORB { + +static uint8_t transition_time(float value) { + if (std::isnan(value)) return 0; + return uint8_t(std::fma(std::fmin(std::fmax(value, 0.0f), 1.0f), 255.0f, 0.5f)); +} + +Transition::Transition(const Transition &other) + : _method(other._method), _replaceable(other._replaceable), + _add_remove_duration(other._add_remove_duration), _max_changes(other._max_changes), + _animation(other._animation) { + _effects.reserve(other._effects.size()); + for (uint32_t i = 0; i < other._effects.size(); ++i) { + _effects.push_back(other._effects[i]); + } +} + +Transition::~Transition() = default; + +void Transition::set_add_remove_duration(float value) { + _add_remove_duration = transition_time(value); +} + +void Transition::add_effect(Effect effect, const uint32_t *arguments) { + uint8_t count = effect.argument_count(); + _effects.reserve(_effects.size() + 1 + count); + _effects.push_back(EffectOrArg(effect)); + for (uint8_t i = 0; i < count; ++i) { + _effects.push_back(EffectOrArg(arguments[i])); + } +} + +bool Transition::operator==(const Transition &other) const { + return _method == other._method && _replaceable == other._replaceable + && _add_remove_duration == other._add_remove_duration && _max_changes == other._max_changes + && _animation == other._animation && _effects.size() == other._effects.size() + && (_effects.empty() || std::memcmp(_effects.data(), other._effects.data(), _effects.size() * sizeof(EffectOrArg)) == 0); +} + +size_t Transition::hash() const { + size_t hash = (size_t(_method) ^ (size_t(0) - size_t(_replaceable))) * 33 ^ _animation.hash(); + for (uint32_t i = 0; i < _effects.size();) { + const Effect &effect = _effects[i++].effect; + hash = hash * 33 ^ effect.hash(); + for (uint8_t count = effect.argument_count(); count != 0; --count) { + hash = hash * 33 ^ _effects[i++].argument; + } + } + return hash; +} + +uint8_t Transition::Effect::argument_count() const { + static constexpr uint8_t counts[] = {0, 0, 1, 2, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0}; + return type() < sizeof(counts) ? counts[type()] : 2; +} + +void Transition::Effect::set_begin_time(float value) { + if (uses_animation_indices()) { + bits &= ~animation_indices_mask; + duration = 255; + } + begin = transition_time(value); +} + +void Transition::Effect::set_duration(float value) { + if (uses_animation_indices()) { + bits &= ~animation_indices_mask; + begin = 0; + } + duration = transition_time(value); +} + +void Transition::Effect::set_insert_animation(uint8_t value) { + if (!uses_animation_indices()) { + bits |= animation_indices_mask; + duration = 0; + } + begin = value; +} + +void Transition::Effect::set_remove_animation(uint8_t value) { + if (!uses_animation_indices()) { + bits |= animation_indices_mask; + begin = 0; + } + duration = value; +} + +size_t Transition::Effect::hash() const { + return (bits & 0x3fff) | ((bits >> 2) & 0x3000) | (size_t(begin) << 16) | (size_t(duration) << 24); +} + +} /* ORB */ diff --git a/Sources/OpenRenderBoxCxx/include/OpenRenderBox/OpenRenderBox.h b/Sources/OpenRenderBoxCxx/include/OpenRenderBox/OpenRenderBox.h index d5e7cb7..ad56eb4 100644 --- a/Sources/OpenRenderBoxCxx/include/OpenRenderBox/OpenRenderBox.h +++ b/Sources/OpenRenderBoxCxx/include/OpenRenderBox/OpenRenderBox.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include diff --git a/Sources/OpenRenderBoxCxx/include/OpenRenderBoxCxx/Animation/Animation.hpp b/Sources/OpenRenderBoxCxx/include/OpenRenderBoxCxx/Animation/Animation.hpp new file mode 100644 index 0000000..8a830fd --- /dev/null +++ b/Sources/OpenRenderBoxCxx/include/OpenRenderBoxCxx/Animation/Animation.hpp @@ -0,0 +1,42 @@ +// +// Animation.hpp +// OpenRenderBox + +#pragma once + +#include + +namespace ORB { + +class Animation { +public: + union TermOrArg { + uint32_t type; + float argument; + + TermOrArg() {} + explicit TermOrArg(uint32_t value) : type(value) {} + explicit TermOrArg(float value) : argument(value) {} + }; + + Animation() = default; + Animation(const Animation &other); + Animation &operator=(const Animation &other); + + bool empty() const { return _terms.empty(); } + void clear() { _terms.clear(); } + float active_duration() const; + void add_spring(double duration, double mass, double stiffness, double damping, double velocity); + bool operator==(const Animation &other) const; + size_t hash() const; + +private: + vector _terms; +}; + +static_assert(sizeof(Animation::TermOrArg) == 4); +#if ORB_TARGET_RT_64_BIT +static_assert(sizeof(Animation) == 0x28); +#endif + +} /* ORB */ diff --git a/Sources/OpenRenderBoxCxx/include/OpenRenderBoxCxx/Animation/Transition.hpp b/Sources/OpenRenderBoxCxx/include/OpenRenderBoxCxx/Animation/Transition.hpp new file mode 100644 index 0000000..7638f0a --- /dev/null +++ b/Sources/OpenRenderBoxCxx/include/OpenRenderBoxCxx/Animation/Transition.hpp @@ -0,0 +1,110 @@ +// +// Transition.hpp +// OpenRenderBox + +#pragma once + +#include +#include + +namespace ORB { + +class Transition { +public: + enum class Method : uint8_t { + Default, Diff, Forwards, Backwards, PrefixSuffix, Prefix, Suffix, Binary, None, + }; + + struct Effect { + uint16_t bits = 0x301; + uint8_t begin = 0; + uint8_t duration = 255; + + uint8_t type() const { return bits & type_mask; } + void set_type(uint8_t value) { bits = (bits & ~type_mask) | (value & type_mask); } + uint8_t flags() const { return bits >> 14; } + void set_flags(uint8_t value) { bits = (bits & ~flags_mask) | (value << 14); } + uint8_t events() const { return (bits & events_mask) >> 8; } + void set_events(uint8_t value) { bits = (bits & ~events_mask) | ((value << 8) & events_mask); } + uint8_t argument_count() const; + bool uses_animation_indices() const { return (bits & animation_indices_mask) != 0; } + float begin_time() const { return uses_animation_indices() ? 0 : begin * (1.0f / 255); } + float duration_value() const { return uses_animation_indices() ? 1 : duration * (1.0f / 255); } + void set_begin_time(float value); + void set_duration(float value); + uint8_t animation_index() const { return uses_animation_indices() && begin == duration ? begin : 0; } + void set_animation_index(uint8_t value) { + set_insert_animation(value); + set_remove_animation(value); + } + uint8_t insert_animation() const { return uses_animation_indices() ? begin : 0; } + void set_insert_animation(uint8_t value); + uint8_t remove_animation() const { return uses_animation_indices() ? duration : 0; } + void set_remove_animation(uint8_t value); + size_t hash() const; + + private: + static constexpr uint16_t type_mask = 0x007f; + static constexpr uint16_t animation_indices_mask = 0x0080; + static constexpr uint16_t events_mask = 0x3f00; + static constexpr uint16_t flags_mask = 0xc000; + }; + + union EffectOrArg { + Effect effect; + uint32_t argument; + + EffectOrArg() {} + explicit EffectOrArg(Effect value) : effect(value) {} + explicit EffectOrArg(uint32_t value) : argument(value) {} + }; + + Transition() = default; + Transition(const Transition &other); + Transition &operator=(const Transition &) = delete; + virtual ~Transition(); + + void retain() { _ref_count.fetch_add(1, std::memory_order_relaxed); } + void release() { + if (_ref_count.fetch_sub(1, std::memory_order_acq_rel) == 1) delete this; + } + bool is_unique() const { + if (_ref_count.load(std::memory_order_relaxed) != 1) return false; + std::atomic_thread_fence(std::memory_order_acquire); + return true; + } + + Method method() const { return _method; } + void set_method(Method value) { _method = value; } + bool replaceable() const { return _replaceable; } + void set_replaceable(bool value) { _replaceable = value; } + uint32_t max_changes() const { return _max_changes; } + void set_max_changes(uint32_t value) { _max_changes = value; } + float add_remove_duration() const { return _add_remove_duration * (1.0f / 255); } + void set_add_remove_duration(float value); + + const vector &effects() const { return _effects; } + void add_effect(Effect effect, const uint32_t *arguments); + const Animation &animation() const { return _animation; } + void set_animation(const Animation &value) { _animation = value; } + void clear_animation() { _animation.clear(); } + bool operator==(const Transition &other) const; + size_t hash() const; + +private: + std::atomic _ref_count{1}; + Method _method = Method::Default; + bool _replaceable = false; + uint8_t _add_remove_duration = 32; + uint32_t _max_changes = UINT32_MAX; + vector _effects; + Animation _animation; +}; + +static_assert(sizeof(Transition::Effect) == 4); +static_assert(sizeof(Transition::EffectOrArg) == 4); +#if ORB_TARGET_RT_64_BIT +static_assert(sizeof(Transition) == 0x60); +#endif + +} /* ORB */ diff --git a/Sources/OpenRenderBoxCxx/include/OpenRenderBoxCxx/Util/refcounted_ptr.hpp b/Sources/OpenRenderBoxCxx/include/OpenRenderBoxCxx/Util/refcounted_ptr.hpp new file mode 100644 index 0000000..49833f0 --- /dev/null +++ b/Sources/OpenRenderBoxCxx/include/OpenRenderBoxCxx/Util/refcounted_ptr.hpp @@ -0,0 +1,56 @@ +// +// refcounted_ptr.hpp +// OpenRenderBox + +#pragma once + +#include + +namespace ORB { + +template +class refcounted_ptr { +public: + refcounted_ptr() noexcept = default; + + // Adopt the reference held by a newly created value. + explicit refcounted_ptr(T *value) noexcept : _p(value) {} + + refcounted_ptr(const refcounted_ptr &other) noexcept : _p(other._p) { + if (_p) _p->retain(); + } + + refcounted_ptr(refcounted_ptr &&other) noexcept + : _p(std::exchange(other._p, nullptr)) {} + + ~refcounted_ptr() { + if (_p) _p->release(); + } + + refcounted_ptr &operator=(const refcounted_ptr &other) noexcept { + if (_p != other._p) { + T *value = other._p; + if (value) value->retain(); + if (_p) _p->release(); + _p = value; + } + return *this; + } + + refcounted_ptr &operator=(refcounted_ptr &&other) noexcept { + if (this != &other) { + if (_p) _p->release(); + _p = std::exchange(other._p, nullptr); + } + return *this; + } + + T *get() const noexcept { return _p; } + T *operator->() const noexcept { return _p; } + T &operator*() const noexcept { return *_p; } + +private: + T *_p = nullptr; +}; + +} /* ORB */ diff --git a/Sources/OpenRenderBoxCxx/include/OpenRenderBoxCxx/Vector/vector.tpp b/Sources/OpenRenderBoxCxx/include/OpenRenderBoxCxx/Vector/vector.tpp index 1a878c2..0d989de 100644 --- a/Sources/OpenRenderBoxCxx/include/OpenRenderBoxCxx/Vector/vector.tpp +++ b/Sources/OpenRenderBoxCxx/include/OpenRenderBoxCxx/Vector/vector.tpp @@ -40,7 +40,7 @@ void *realloc_vector(void *buffer, void *stack_buffer, size_type stack_size, siz #if ORB_TARGET_OS_DARWIN size_t new_size_bytes = malloc_good_size(preferred_new_size * element_size_bytes); #else - size_t new_size_bytes = malloc_good_size(preferred_new_size * element_size_bytes); + size_t new_size_bytes = preferred_new_size * element_size_bytes; #endif size_type new_size = new_size_bytes / element_size_bytes; if (new_size == *size) { diff --git a/Sources/OpenRenderBoxCxx/include/module.modulemap b/Sources/OpenRenderBoxCxx/include/module.modulemap index f5fac40..70a6662 100644 --- a/Sources/OpenRenderBoxCxx/include/module.modulemap +++ b/Sources/OpenRenderBoxCxx/include/module.modulemap @@ -13,6 +13,12 @@ module OpenRenderBoxCxx.Color { export * } +module OpenRenderBoxCxx.Animation { + requires cplusplus + umbrella "OpenRenderBoxCxx/Animation" + export * +} + module OpenRenderBoxCxx.Path { requires cplusplus umbrella "OpenRenderBoxCxx/Path" diff --git a/Sources/OpenRenderBoxShims/ORBShims.swift b/Sources/OpenRenderBoxShims/ORBShims.swift index 51ae8b7..230c10d 100644 --- a/Sources/OpenRenderBoxShims/ORBShims.swift +++ b/Sources/OpenRenderBoxShims/ORBShims.swift @@ -40,6 +40,12 @@ public typealias ORBLayerDelegate = RBLayerDelegate public typealias ORBPath = RBPath public typealias ORBPathApplyCallback = RBPathApplyCallback public typealias ORBUUID = RBUUID +public typealias ORBTransition = RBTransition +public typealias ORBTransitionEffect = RBTransitionEffect +public typealias ORBTransitionMethod = RBTransitionMethod +public typealias ORBTransitionEvents = RBTransitionEvents +public typealias ORBTransitionEffectFlags = RBTransitionEffectFlags +public typealias ORBTransitionEffectType = RBTransitionEffectType public typealias ORBSymbolAnimation = RBSymbolAnimation public typealias ORBSymbolScaleLevel = RBSymbolScaleLevel public typealias ORBSymbolAnimator = RBSymbolAnimator diff --git a/Tests/OpenRenderBoxCompatibilityTests/ORBShims.swift b/Tests/OpenRenderBoxCompatibilityTests/ORBShims.swift index 7001808..4080c01 100644 --- a/Tests/OpenRenderBoxCompatibilityTests/ORBShims.swift +++ b/Tests/OpenRenderBoxCompatibilityTests/ORBShims.swift @@ -16,6 +16,12 @@ public typealias ORBLayerDelegate = RBLayerDelegate public typealias ORBPath = RBPath public typealias ORBPathApplyCallback = RBPathApplyCallback public typealias ORBUUID = RBUUID +public typealias ORBTransition = RBTransition +public typealias ORBTransitionEffect = RBTransitionEffect +public typealias ORBTransitionMethod = RBTransitionMethod +public typealias ORBTransitionEffectType = RBTransitionEffectType +public typealias ORBTransitionEvents = RBTransitionEvents +public typealias ORBTransitionEffectFlags = RBTransitionEffectFlags public typealias ORBSymbolAnimation = RBSymbolAnimation public typealias ORBSymbolScaleLevel = RBSymbolScaleLevel public typealias ORBSymbolAnimator = RBSymbolAnimator diff --git a/Tests/OpenRenderBoxCompatibilityTests/TransitionTests.swift b/Tests/OpenRenderBoxCompatibilityTests/TransitionTests.swift new file mode 100644 index 0000000..b0618a7 --- /dev/null +++ b/Tests/OpenRenderBoxCompatibilityTests/TransitionTests.swift @@ -0,0 +1,138 @@ +// +// TransitionTests.swift +// OpenRenderBoxCompatibilityTests + +import Foundation +import Testing + +struct TransitionTests { + @Test + func defaults() { + let effect = ORBTransitionEffect() + #expect(effect.type == .alpha) + #expect(effect.events == .addRemove) + #expect(effect.flags.rawValue == 0) + #expect(effect.beginTime == 0) + #expect(effect.duration == 1) + let transition = ORBTransition() + #expect(transition.method == .empty) + #expect(transition.maxChanges == .max) + #expect(!transition.isReplaceable) + #expect(transition.animation == nil) + #expect(transition.effects.isEmpty) + #expect(transition.addRemoveDuration == Float(32) * (1 / 255)) + #expect(ORBTransition.default === ORBTransition.default) + #expect(ORBTransition.default.isEqual(transition)) + } + + @Test(arguments: [ + (Float(-1), Float(0)), (0, 0), (0.15, Float(38) * (1 / 255)), + (0.5, Float(128) * (1 / 255)), (2, 1), (.nan, 0), + ] as [(Float, Float)]) + func timeQuantization(input: Float, expected: Float) { + let effect = ORBTransitionEffect() + effect.beginTime = input + effect.duration = input + #expect(effect.beginTime == expected) + #expect(effect.duration == expected) + let transition = ORBTransition() + transition.addRemoveDuration = input + #expect(transition.addRemoveDuration == expected) + } + + @Test + func animationIndicesReplaceTimeline() { + let effect = ORBTransitionEffect() + effect.beginTime = 0.5 + effect.duration = 0.25 + effect.insertAnimationIndex = 258 + #expect(effect.insertAnimationIndex == 2) + #expect(effect.removeAnimationIndex == 0) + #expect(effect.animationIndex == 0) + #expect(effect.beginTime == 0) + #expect(effect.duration == 1) + effect.removeAnimationIndex = 2 + #expect(effect.animationIndex == 2) + effect.beginTime = 0.25 + #expect(effect.animationIndex == 0) + #expect(effect.duration == 1) + effect.animationIndex = 7 + effect.duration = 0.5 + #expect(effect.insertAnimationIndex == 0) + #expect(effect.removeAnimationIndex == 0) + #expect(effect.beginTime == 0) + #expect(effect.duration == Float(128) * (1 / 255)) + } + + @Test + func argumentsShareBitsAndIgnoreInvalidIndices() { + let effect = ORBTransitionEffect() + effect.setIntegerArgumentValue(0x3f800000, at: 0) + #expect(effect.argumentValue(at: 0) == 1) + effect.setArgumentValue(-0.0, at: 1) + #expect(effect.integerArgumentValue(at: 1) == 0x80000000) + effect.setArgumentValue(4, at: 2) + #expect(effect.argumentValue(at: 2) == 0) + #expect(effect.integerArgumentValue(at: .max) == 0) + effect.type = ORBTransitionEffectType(rawValue: 129)! + effect.flags = ORBTransitionEffectFlags(rawValue: 3) + effect.events = ORBTransitionEvents(rawValue: .max) + #expect(effect.type == .alpha) + #expect(effect.flags.rawValue == 3) + #expect(effect.events.rawValue == 63) + if !compatibilityTestEnabled { + // The 2024 descriptor stores two flag bits; newer system versions add a third bit. + effect.flags = ORBTransitionEffectFlags(rawValue: .max) + #expect(effect.flags.rawValue == 3) + } + } + + @Test + func transitionCopiesEffectsAndDropsUnusedArguments() throws { + let effect = ORBTransitionEffect() + effect.type = .scaleCenter + effect.setArgumentValue(0.5, at: 0) + effect.setArgumentValue(0.75, at: 1) + let transition = ORBTransition() + transition.addEffect(effect) + effect.setArgumentValue(1, at: 0) + let stored = try #require(transition.effects.first) + #expect(stored.argumentValue(at: 0) == 0.5) + #expect(stored.argumentValue(at: 1) == 0) + stored.setArgumentValue(2, at: 0) + #expect(transition.effects[0].argumentValue(at: 0) == 0.5) + let copy = try #require(transition.copy() as? ORBTransition) + #expect(copy.isEqual(transition)) + #expect(copy.hash == transition.hash) + copy.method = .diff + copy.isReplaceable = true + copy.maxChanges = 10 + copy.addEffect(effect) + #expect(transition.method == .empty) + #expect(!transition.isReplaceable) + #expect(transition.maxChanges == .max) + #expect(transition.effects.count == 1) + #expect(!copy.isEqual(transition)) + } + + @Test + func animationIsStoredByValue() throws { + let animation = ORBAnimation() + animation.addSpringDuration(0.5, mass: 1, stiffness: 344, damping: 37, initialVelocity: 0) + animation.addSpringDuration(0.8, mass: 2, stiffness: 470, damping: 34, initialVelocity: 0) + let transition = ORBTransition() + transition.animation = animation + let stored = try #require(transition.animation) + #expect(stored !== animation) + #expect(stored.isEqual(animation)) + #expect(stored.activeDuration == 0.5) + animation.removeAll() + stored.removeAll() + #expect(transition.animation?.activeDuration == 0.5) + let copy = try #require(transition.copy() as? ORBTransition) + #expect(copy.isEqual(transition)) + copy.animation = animation + #expect(copy.animation == nil) + #expect(transition.animation != nil) + } +} diff --git a/Tests/OpenRenderBoxTests/Animation/TransitionStorageTests.swift b/Tests/OpenRenderBoxTests/Animation/TransitionStorageTests.swift new file mode 100644 index 0000000..8befe93 --- /dev/null +++ b/Tests/OpenRenderBoxTests/Animation/TransitionStorageTests.swift @@ -0,0 +1,74 @@ +// +// TransitionStorageTests.swift +// OpenRenderBoxTests + +#if canImport(ObjectiveC) +import Foundation +import ObjectiveC +import OpenRenderBox +import Testing + +struct TransitionStorageTests { + @Test + func objectiveCStorageLayout() throws { + var count: UInt32 = 0 + let ivars = try #require(class_copyIvarList(ORBTransition.self, &count)) + defer { free(ivars) } + let fields = (0.. Date: Sat, 31 Oct 2026 15:36:01 +0800 Subject: [PATCH 3/3] build(deps): update DarwinPrivateFrameworks for transition APIs --- Package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.resolved b/Package.resolved index 8d831db..f44abb3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -7,7 +7,7 @@ "location" : "https://github.com/OpenSwiftUIProject/DarwinPrivateFrameworks.git", "state" : { "branch" : "main", - "revision" : "3690622baedc4daf3cf9f9e32b38ede607fc7f11" + "revision" : "bc86728a45d2d6dc04bea9ad248971b24c36f019" } }, {