diff --git a/README.md b/README.md index cc7e567..e3c7b14 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# Roact-Flow +# React-Flow diff --git a/default.project.json b/default.project.json index 9d964fb..3ab210f 100644 --- a/default.project.json +++ b/default.project.json @@ -1,5 +1,5 @@ { - "name": "roact-flow", + "name": "react-flow", "tree": { "$path": "src" } diff --git a/src/Animation.lua b/src/Animation.lua deleted file mode 100644 index fbd85cf..0000000 --- a/src/Animation.lua +++ /dev/null @@ -1,78 +0,0 @@ -local Hooks = script.Parent.Hooks - -local useTween = require(Hooks.useTween) -local useSpring = require(Hooks.useSpring) - -export type SpringProps = { - start: number, - goal: number, - speed: number, - damper: number, -} - -export type TweenProps = { - start: number, - goal: number, - tweenInfo: TweenInfo, -} - -local function Animation(name: string, timeStep: number?, props: SpringProps | TweenProps) - local update - local value - - local play, playReverse, stop - - if name == "Tween" then - assert(props.start, "Tween requires a start value") - assert(props.goal, "Tween requires a goal value") - assert(props.tweenInfo, "Tween requires a tweenInfo") - - value, update, stop = useTween(props.start, props.tweenInfo) - play = function() - update({ - start = props.start, - goal = props.goal, - }) - end - - playReverse = function() - update({ - start = props.goal, - goal = props.start, - }) - end - elseif name == "Spring" then - assert(props.start, "Spring requires a start value") - assert(props.goal, "Spring requires a goal value") - - value, update, stop = useSpring(props.start, props.speed, props.damper) - play = function() - update({ - value = props.start, - goal = props.goal, - force = props.force, - }) - end - - playReverse = function() - update({ - value = props.goal, - goal = props.start, - force = props.force, - }) - end - else - assert(false, name .. " is not a valid animation type") - end - - return { - timeStep = timeStep, - start = props.start, - value = value, - playReverse = playReverse, - play = play, - stop = stop, - } -end - -return Animation diff --git a/src/Animations/Base.lua b/src/Animations/Base.lua new file mode 100644 index 0000000..43e06be --- /dev/null +++ b/src/Animations/Base.lua @@ -0,0 +1,14 @@ +local BaseAnimation = {} + +function BaseAnimation.new() + local self = {} + + self.listener = nil + self.SetListener = function(_, listener: (any) -> ()) + self.listener = listener + end + + return self +end + +return BaseAnimation diff --git a/src/Animations/Types/Spring.lua b/src/Animations/Types/Spring.lua new file mode 100644 index 0000000..288c475 --- /dev/null +++ b/src/Animations/Types/Spring.lua @@ -0,0 +1,78 @@ +local BaseAnimation = require(script.Parent.Parent.Base) +local Promise = require(script.Parent.Parent.Parent.Promise) +local SpringValue = require(script.Parent.Parent.Parent.Utility.SpringValue) + +local Spring = {} +Spring.__index = Spring + +export type Spring = typeof(Spring.new()) +export type SpringProperties = { + damper: number?, + speed: number?, + start: any, + target: any, +} + +function Spring.new(props: SpringProperties) + local self = setmetatable(BaseAnimation.new(), Spring) :: Spring + + self.props = props + self.player = nil + + return self +end + +function Spring:Play(from: any?) + if self.playing then + self:Stop() + end + + local baseFromValue = self.props.start or from :: any + local baseToValue = self.props.target :: any + local force = self.props.force :: any + + assert(baseFromValue, "No start value provided") + assert(baseToValue, "No target value provided") + + if baseFromValue == baseToValue and not force then + return Promise.resolve() + end + + local newSpring = SpringValue.new(baseFromValue, self.props.speed, self.props.damper) + newSpring:SetGoal(baseToValue) + + if self._oldSpring then + newSpring:Impulse(self._oldSpring:GetVelocity()) + end + + if force then + newSpring:Impulse(force) + end + + local animation = newSpring:Run(function() + if self.listener then + self.listener(newSpring:GetValue()) + end + end) + + self.playing = true + self.player = animation + self._oldSpring = newSpring + + return animation +end + +function Spring:Stop() + if not self.playing then + return + end + + if self.player then + self.player:cancel() + self.player = nil + end + + self.playing = false +end + +return Spring diff --git a/src/Animations/Types/Tween.lua b/src/Animations/Types/Tween.lua new file mode 100644 index 0000000..251a5e1 --- /dev/null +++ b/src/Animations/Types/Tween.lua @@ -0,0 +1,132 @@ +local TweenService = game:GetService("TweenService") + +local BaseAnimation = require(script.Parent.Parent.Base) +local Promise = require(script.Parent.Parent.Parent.Promise) +local LinearValue = require(script.Parent.Parent.Parent.Utility.LinearValue) + +local Tween = {} +Tween.__index = Tween + +export type Tween = typeof(Tween.new()) +export type TweenProperties = { + info: TweenInfo, + startImmediate: T?, + start: T, + target: T, + delay: number?, +} + +local function playTween(tweenInfo, callback: (number) -> nil, completed: () -> nil) + local numberValue = Instance.new("NumberValue") + numberValue.Value = 0 + numberValue:GetPropertyChangedSignal("Value"):Connect(function() + callback(numberValue.Value) + end) + + local tween = TweenService:Create(numberValue, tweenInfo, { + Value = 1, + }) + + tween.Completed:Connect(function() + numberValue:Destroy() + completed() + end) + + return function() + callback(0) + tween:Play() + end, function() + numberValue:Destroy() + tween:Cancel() + end +end + +function Tween.new(props: TweenProperties) + local self = setmetatable(BaseAnimation.new(), Tween) :: Tween + + self.props = props + self.player = nil + + return self +end + +function Tween:Play(from: any?) + if self.playing then + self:Stop() + end + + local tweenInfo = self.props.info :: TweenInfo + local baseFromValue = self.props.startImmediate or self.props.start or from :: any + local baseToValue = self.props.target :: any + + -- start immediately will start the tween but not update the listener + local startImmediately = self.props.startImmediate ~= nil + local delayTime = self.props.delay :: number + + if not delayTime then + assert(startImmediately == false, "Cannot start immediately without a delay") + else + if startImmediately then + assert(delayTime > 0, "DelayTime must be greater than zero") + end + end + + assert(baseFromValue, "No start value provided") + assert(baseToValue, "No target value provided") + + assert(tweenInfo, "No tween info provided") + assert(tweenInfo.RepeatCount == 0, "RepeatCount must be 0") + assert(tweenInfo.Reverses == false, "Reverses must be false") + assert(tweenInfo.DelayTime == 0, "DelayTime must be 0") + + if baseFromValue == baseToValue then + return Promise.resolve() + end + + local fromValue = LinearValue.fromValue(baseFromValue) + local toValue = LinearValue.fromValue(baseToValue) + + local animation = Promise.new(function(resolve, _, onCancel) + local play, cancel = playTween(tweenInfo, function(value) + local newValue = fromValue:Lerp(toValue, value):ToValue() + self.listener(newValue) + end, function() + self.playing = false + self.player = nil + resolve() + end) + + onCancel(cancel) + + if not delayTime then + play() + else + if startImmediately then + self.listener(baseFromValue) + end + + task.wait(delayTime) + play() + end + end) + + self.playing = true + self.player = animation + + return animation +end + +function Tween:Stop() + if not self.playing then + return + end + + if self.player then + self.player:cancel() + self.player = nil + end + + self.playing = false +end + +return Tween diff --git a/src/Animations/init.lua b/src/Animations/init.lua new file mode 100644 index 0000000..2430dae --- /dev/null +++ b/src/Animations/init.lua @@ -0,0 +1,4 @@ +return { + Spring = require(script.Types.Spring).new, + Tween = require(script.Types.Tween).new, +} diff --git a/src/Hooks/useAnimation.lua b/src/Hooks/useAnimation.lua new file mode 100644 index 0000000..cc2d46c --- /dev/null +++ b/src/Hooks/useAnimation.lua @@ -0,0 +1,90 @@ +local Promise = require(script.Parent.Parent.Promise) + +local React = require(script.Parent.Parent.React) +local useRef = React.useRef + +export type AnimationProps = { + [string]: any, +} + +local Animation = {} +Animation.__index = Animation + +function Animation.new(props: AnimationProps) + local self = setmetatable({}, Animation) + + self.playing = false + self.listener = nil + self.animation = props + + for name, animation in props do + animation:SetListener(function(value) + if self.listener then + self.listener(name, value) + end + end) + end + + return self +end + +function Animation:SetListener(listener: (string, any) -> ()) + self.listener = listener +end + +function Animation:Play(fromProps: AnimationProps) + if self.playing then + self:Stop() + end + + local animation = Promise.new(function(resolve, _, onCancel) + local promises = {} + + for name, animatable in self.animation do + local animationPromise = animatable:Play(fromProps[name]) + table.insert(promises, animationPromise) + end + + local awaiter = Promise.all(promises):andThen(resolve) + + onCancel(function() + awaiter:cancel() + + for _, promise in promises do + promise:cancel() + end + end) + end) + + self.playing = true + self.player = animation + + return animation +end + +function Animation:Stop() + if not self.playing then + return + end + + if self.player then + self.player:cancel() + self.player = nil + end + + self.playing = false +end + +local function useAnimation(props: AnimationProps) + local animation = useRef() + local current = animation.current + + if not current then + current = Animation.new(props) + animation.current = current + end + + return current +end + +return useAnimation diff --git a/src/Hooks/useBindings.lua b/src/Hooks/useBindings.lua new file mode 100644 index 0000000..e1b2950 --- /dev/null +++ b/src/Hooks/useBindings.lua @@ -0,0 +1,45 @@ +local React = require(script.Parent.Parent.React) +local useEffect = React.useEffect +-- TODO: Remove this when we have a better way to subscribe to bindings +local subscribeToBinding = React.__subscribeToBinding + +local function useBindings(callback: (any...) -> (), bindings: { any }, deps: { table }) + useEffect(function() + local disconnects = {} + local values = {} + + local running = true + + for i, binding in bindings do + values[i] = binding:getValue() + end + + for i, binding in bindings do + local idx = i + + disconnects[idx] = subscribeToBinding(binding, function(newValue) + if not running then + warn("Binding updated after unmount") + return + end + + values[idx] = newValue + callback(unpack(values)) + end) + end + + callback(unpack(values)) + + return function() + for _, disconnect in disconnects do + disconnect() + end + + running = false + table.clear(disconnects) + table.clear(values) + end + end, { unpack(bindings), unpack(deps or {}) }) +end + +return useBindings diff --git a/src/Hooks/useGroupAnimation.lua b/src/Hooks/useGroupAnimation.lua new file mode 100644 index 0000000..49c169f --- /dev/null +++ b/src/Hooks/useGroupAnimation.lua @@ -0,0 +1,109 @@ +local React = require(script.Parent.Parent.React) +local useBinding = React.useBinding +local useRef = React.useRef + +local GroupAnimationController = {} +GroupAnimationController.__index = GroupAnimationController + +type StateSetters = { + [string]: (any) -> nil, +} + +export type Animation = { + Play: (DefaultProperties) -> Animation, + Stop: () -> nil, +} + +export type GroupAnimation = { + [string]: Animation, +} + +export type DefaultProperties = { + [string]: any, +} + +function GroupAnimationController.new(props: GroupAnimation, default: DefaultProperties, setters: StateSetters) + local self = setmetatable({}, GroupAnimationController) + + self.currentState = "Default" + self.animations = props + self.state = default + + for state, animation in props do + animation:SetListener(function(name, value) + if self.currentState ~= state then + -- warn("IGNORED UPDATE:", state, name, value) + return + end + + -- warn("UPDATE:", state, name, value) + self.state[name] = value + setters[name](value) + end) + end + + return self +end + +function GroupAnimationController:Play(newState: string) + local animation = self.animations[newState] + assert(animation, `No animation found for state {newState}`) + + self.currentState = newState + + if self.currentAnimation then + self.currentAnimation:Stop() + end + + self.currentAnimation = animation + animation:Play(self.state) +end + +function GroupAnimationController:Stop() + if self.currentAnimation then + self.currentAnimation:Stop() + self.currentAnimation = nil + end +end + +local function getStateContainer(defaults: DefaultProperties) + local setters = {} + local values = {} + + for name, value in defaults do + local binding, updateBinding = useBinding(value) + + setters[name] = updateBinding + values[name] = binding + end + + return setters, values +end + +local function useGroupAnimation(props: GroupAnimation, default: DefaultProperties) + local controller = useRef() + local current = controller.current + local setters, values = getStateContainer(default) + + if not current then + local newController = GroupAnimationController.new(props, default, setters) + + current = { + play = function(newState: string) + assert(typeof(newState) == "string", "useGroupAnimation expects a string 'state'") + + newController:Play(newState) + end, + + stop = function() + newController:Stop() + end, + } + + controller.current = current + end + + return values, current.play, current.stop +end + +return useGroupAnimation diff --git a/src/Hooks/useLatest.lua b/src/Hooks/useLatest.lua deleted file mode 100644 index 4f2dfb4..0000000 --- a/src/Hooks/useLatest.lua +++ /dev/null @@ -1,18 +0,0 @@ -local Roact = require(script.Parent.Parent.Roact) -local useEffect = Roact.useEffect -local useState = Roact.useState - -local function useLatest(...: any...) - local initialValue = select(1, ...) - local state, setState = useState(initialValue) - - for _, value in { ... } do - useEffect(function() - setState(value) - end, { value }) - end - - return state -end - -return useLatest diff --git a/src/Hooks/useSequence.lua b/src/Hooks/useSequence.lua deleted file mode 100644 index 7f27ed0..0000000 --- a/src/Hooks/useSequence.lua +++ /dev/null @@ -1,55 +0,0 @@ -local Roact = require(script.Parent.Parent.Roact) -local useRef = Roact.useRef - -local Animation = require(script.Parent.Parent.Animation) -export type Animation = typeof(Animation) - -local function makeSequence(animations: { [string]: Animation }) - local currentSequence - - return { - play = function(data: { reverse: boolean }?) - local reverse = data and data.reverse - local nonce = tick() - currentSequence = nonce - - for _, animation in animations do - animation.useStart.current = true - - task.delay(animation.timeStep or 0, function() - if currentSequence == nonce then - animation.useStart.current = false - animation[reverse and "playReverse" or "play"]() - end - end) - end - end, - - stop = function() - currentSequence = nil - for _, animation in animations do - animation.stop() - end - end, - } -end - -local function useSequence(animations: { [string]: Animation }) - local sequence = {} - local sequenceRef = useRef() - local currentSequence = sequenceRef.current - - if not currentSequence then - currentSequence = makeSequence(animations) - sequenceRef.current = currentSequence - end - - for name, animation in animations do - animation.useStart = useRef(true) - sequence[name] = animation.useStart.current and animation.start or animation.value - end - - return sequence, currentSequence.play, currentSequence.stop -end - -return useSequence diff --git a/src/Hooks/useSequenceAnimation.lua b/src/Hooks/useSequenceAnimation.lua new file mode 100644 index 0000000..be257e4 --- /dev/null +++ b/src/Hooks/useSequenceAnimation.lua @@ -0,0 +1,128 @@ +local Promise = require(script.Parent.Parent.Promise) + +local React = require(script.Parent.Parent.React) +local useRef = React.useRef + +export type SequenceProps = { { timestamp: number } | any } + +local Sequence = {} +Sequence.__index = Sequence + +function Sequence.new(props: SequenceProps) + local self = setmetatable({}, Sequence) + + self.playing = false + self.listener = nil + self.animation = props + + table.sort(props, function(a, b) + return a.timestamp < b.timestamp + end) + + local lastTimestamp + + for _, animation in props do + local timestamp = animation.timestamp + if lastTimestamp == timestamp then + error("Duplicate timestamp found in sequence") + end + + lastTimestamp = timestamp + + for name, animatable in animation do + if name == "timestamp" then + continue + end + + animatable:SetListener(function(value) + if self.listener then + self.listener(name, value) + end + end) + end + end + + return self +end + +function Sequence:SetListener(listener: (string, any) -> ()) + self.listener = listener +end + +function Sequence:Play(fromProps: SequenceProps) + if self.playing then + self:Stop() + end + + local animation = Promise.new(function(resolve, _, onCancel) + local promises = {} + local playing = {} + + for _, sequenced in self.animation do + local animationPromise = Promise.delay(sequenced.timestamp):andThen(function() + local allAnimatables = {} + + for name, animatable in sequenced do + if name == "timestamp" then + continue + end + + if playing[name] then + playing[name]:cancel() + end + + local promise = animatable:Play(fromProps[name]) + playing[name] = promise + + table.insert(allAnimatables, promise) + end + + return Promise.all(allAnimatables) + end) + + table.insert(promises, animationPromise) + end + + local awaiter = Promise.all(promises):andThen(resolve) + + onCancel(function() + for _, promise in promises do + promise:cancel() + end + + awaiter:cancel() + end) + end) + + self.playing = true + self.player = animation + + return animation +end + +function Sequence:Stop() + if not self.playing then + return + end + + if self.player then + self.player:cancel() + self.player = nil + end + + self.playing = false +end + +local function useSequenceAnimation(props: SequenceProps) + local animation = useRef() + local current = animation.current + + if not current then + current = Sequence.new(props) + animation.current = current + end + + return current +end + +return useSequenceAnimation diff --git a/src/Hooks/useSpring.lua b/src/Hooks/useSpring.lua index e71df6c..20779f4 100644 --- a/src/Hooks/useSpring.lua +++ b/src/Hooks/useSpring.lua @@ -1,75 +1,65 @@ -local RunService = game:GetService("RunService") - -local Roact = require(script.Parent.Parent.Roact) -local useEffect = Roact.useEffect -local useState = Roact.useState -local useMemo = Roact.useMemo - -local Utility = script.Parent.Parent.Utility -local LinearValue = require(Utility.LinearValue) -local SpringValue = require(Utility.SpringValue) -local SpringValues = {} - -RunService:UnbindFromRenderStep("UPDATE_REACT_SPRING") -RunService:BindToRenderStep("UPDATE_REACT_SPRING", Enum.RenderPriority.First.Value, function(dt: number) - for spring, setValue in pairs(SpringValues) do - local didUpdate = spring:Update(dt) - local updatedValue = spring:GetValue() - - if not didUpdate then - SpringValues[spring] = nil - updatedValue = spring:GetGoal() - end - - setValue(updatedValue) - end -end) - -local function makeSpring( - updateState: (any) -> (), - initial: LinearValue.LinearValueType, - speed: number?, - dampening: number? -) - local springValue = SpringValue.new(initial, speed, dampening) - - return { - play = function(animation: { - goal: LinearValue.LinearValueType?, - value: LinearValue.LinearValueType?, - force: LinearValue.LinearValueType?, - }) - if animation.goal then - springValue:SetGoal(animation.goal) - end - - if animation.value then - springValue:SetValue(animation.value) - end - - if animation.force then - springValue:Impulse(animation.force) - end - - springValue:Run(updateState) - end, - - stop = function() - springValue:Stop() - end, - } -end +local React = require(script.Parent.Parent.React) +local useRef = React.useRef +local useBinding = React.useBinding + +local SpringValue = require(script.Parent.Parent.Utility.SpringValue) +local Spring = require(script.Parent.Parent.Animations.Types.Spring) + +local function useSpring(props: Spring.SpringProperties) + local controller = useRef() + local spring = controller.current + + local binding, update = useBinding(props.start) + + if not spring then + local newController = SpringValue.new(props.start, props.speed, props.damper) + + spring = { + controller = newController, + + start = function(subProps: Spring.SpringProperties) + assert(typeof(subProps) == "table", "useSpring expects a table of properties") -local function useSpring(initial: LinearValue.LinearValueType, speed: number?, dampening: number?) - speed = speed or 20 - dampening = dampening or 1 + if subProps.target then + newController:SetGoal(subProps.target) + end + + if subProps.start then + newController:SetValue(subProps.start) + end + + if subProps.force then + newController:Impulse(subProps.force) + end + + if subProps.damper then + newController:SetDamper(subProps.damper) + end + + if subProps.speed then + newController:SetSpeed(subProps.speed) + end + + if subProps.target or subProps.start or subProps.force then + if not newController:Playing() then + newController:Run() + end + end + end, + + stop = function() + if newController:Playing() then + newController:Stop() + end + end, + } + + controller.current = spring + end - local state, setState = useState(initial) - local spring = useMemo(function() - return makeSpring(setState, initial, speed, dampening) - end, { initial, speed, dampening }) + spring.controller:SetUpdater(update) - return state, spring.play, spring.stop + return binding, spring.start, spring.stop end return useSpring diff --git a/src/Hooks/useStorage.lua b/src/Hooks/useStorage.lua deleted file mode 100644 index e47f713..0000000 --- a/src/Hooks/useStorage.lua +++ /dev/null @@ -1,19 +0,0 @@ -local Roact = require(script.Parent.Parent.Roact) -local useRef = Roact.useRef - -local function useStorage(name: string?) - name = name or "global" - - local globalStack = useRef({}) - - local currenStack = globalStack.current - local stack = currenStack[name] - if not stack then - stack = {} - currenStack[name] = stack - end - - return stack -end - -return useStorage diff --git a/src/Hooks/useTween.lua b/src/Hooks/useTween.lua index 5728ea1..17ee60d 100644 --- a/src/Hooks/useTween.lua +++ b/src/Hooks/useTween.lua @@ -1,89 +1,43 @@ -local TweenService = game:GetService("TweenService") +local React = require(script.Parent.Parent.React) +local useRef = React.useRef +local useBinding = React.useBinding -local Roact = require(script.Parent.Parent.Roact) -local useRef = Roact.useRef -local useState = Roact.useState -local useEffect = Roact.useEffect +local Tween = require(script.Parent.Parent.Animations.Types.Tween) -local LinearValue = require(script.Parent.Parent.Utility.LinearValue) -type LinearValue = typeof(LinearValue) +local function useTween(props: Tween.TweenProperties) + local controller = useRef() + local tween = controller.current -local function makeTween(updateState: (any) -> (), initial: LinearValue.LinearValueType, tweenInfo: TweenInfo) - local start, goal, tween - local current = LinearValue.fromValue(initial) + local binding, update = useBinding(props.start) - local value = Instance.new("NumberValue") - value:GetPropertyChangedSignal("Value"):Connect(function() - current = start:Lerp(goal, value.Value) - updateState(current:ToValue()) - end) + if not tween then + local newController = Tween.new(props) - return { - destroy = function() - value:Destroy() - end, + tween = { + controller = newController, - play = function(animation: { - start: LinearValue.LinearValueType?, - goal: LinearValue.LinearValueType?, - }) - local startValue = animation.start - local endValue = animation.goal + start = function(subProps: Tween.TweenProperties) + assert(typeof(subProps) == "table", "useTween expects a table of properties") - local currentValue = current:ToValue() + newController.props.info = subProps.info or newController.props.info + newController.props.start = subProps.start or binding:getValue() + newController.props.target = subProps.target or newController.props.target + newController.props.immediate = subProps.immediate or newController.props.immediate + newController.props.delay = subProps.delay or newController.props.delay + newController:Play(subProps.start or binding:getValue()) + end, - if startValue == currentValue and endValue == currentValue then - return - end + stop = function() + newController:Stop() + end, + } - if not startValue then - startValue = currentValue - end - - if tween then - tween:Cancel() - end - - start = LinearValue.fromValue(startValue) - goal = LinearValue.fromValue(endValue) - - value.Value = 0 - - tween = TweenService:Create(value, tweenInfo, { Value = 1 }) - tween:Play() - end, - - stop = function() - if tween then - tween:Cancel() - tween = nil - end - end, - } -end - -local function useTween(initial: LinearValue.LinearValueType, tweenInfo: TweenInfo) - local current, setState = useState(initial) - - local tweenRef = useRef() - local currentTweenRef = tweenRef.current - - if not currentTweenRef then - currentTweenRef = makeTween(setState, initial, tweenInfo) - tweenRef.current = currentTweenRef + controller.current = tween end - useEffect(function() - return function() - local currentTween = tweenRef.current - if currentTween then - currentTween:destroy() - tweenRef.current = nil - end - end - end, {}) + tween.controller:SetListener(update) - return current, currentTweenRef.play, currentTweenRef.stop + return binding, tween.start, tween.stop end return useTween diff --git a/src/React.lua b/src/React.lua new file mode 100644 index 0000000..45ac990 --- /dev/null +++ b/src/React.lua @@ -0,0 +1 @@ +return require(script.Parent.Parent.React) diff --git a/src/Roact.lua b/src/Roact.lua deleted file mode 100644 index e189fb4..0000000 --- a/src/Roact.lua +++ /dev/null @@ -1 +0,0 @@ -return require(script.Parent.Parent.Roact) diff --git a/src/Utility/SpringValue.lua b/src/Utility/SpringValue.lua index 402fd7f..df78aef 100644 --- a/src/Utility/SpringValue.lua +++ b/src/Utility/SpringValue.lua @@ -18,6 +18,7 @@ function SpringValue.new(initial: LinearValue.LinearValueType, speed: number?, d _velocities = {}, _speed = speed or 1, _damper = damper or 1, + _updater = nil, }, SpringValue) end @@ -33,10 +34,30 @@ function SpringValue:Impulse(impulse: LinearValue.LinearValueType) end end +function SpringValue:GetVelocity() + return LinearValue.new(self._current._ccstr, unpack(self._velocities)):ToValue() +end + function SpringValue:SetGoal(goal: LinearValue.LinearValueType) self._goal = LinearValue.fromValue(goal) end +function SpringValue:SetSpeed(speed: number) + self._speed = speed +end + +function SpringValue:SetDamper(damper: number) + self._damper = damper +end + +function SpringValue:SetUpdater(updater: (any) -> ()) + self._updater = updater + + if self:Playing() and updater then + updater(self:GetValue()) + end +end + function SpringValue:GetGoal() return self._goal:ToValue() end @@ -71,21 +92,37 @@ function SpringValue:Update(dt: number) end self._current = LinearValue.new(self._current._ccstr, unpack(newValues)) + return updated end +function SpringValue:Playing() + return SpringValues[self] ~= nil +end + function SpringValue:Stop() local value = SpringValues[self] if value then SpringValues[self] = nil - value[2]() + value() end end -function SpringValue:Run(update: () -> ()) - return Promise.new(function(resolve) - update(self:GetValue()) - SpringValues[self] = { update, resolve } +function SpringValue:Run(update: () -> ()?) + if update then + self._updater = update + end + + return Promise.new(function(resolve, _, onCancel) + onCancel(function() + self:Stop() + end) + + if update then + update(self:GetValue()) + end + + SpringValues[self] = resolve end) end @@ -130,12 +167,13 @@ end RunService:UnbindFromRenderStep("UPDATE_SPRING_VALUES") RunService:BindToRenderStep("UPDATE_SPRING_VALUES", Enum.RenderPriority.First.Value, function(dt: number) - for spring, updates in pairs(SpringValues) do + for spring, resolve in pairs(SpringValues) do local didUpdate = spring:Update(dt) local value = spring:GetValue() - local update, resolve = unpack(updates) - update(value) + if spring._updater then + spring._updater(value) + end if not didUpdate then SpringValues[spring] = nil diff --git a/src/init.lua b/src/init.lua index bb303a8..c6f9575 100644 --- a/src/init.lua +++ b/src/init.lua @@ -1,8 +1,15 @@ +local Animations = require(script.Animations) + return { - Animation = require(script.Animation), + Tween = Animations.Tween, + Spring = Animations.Spring, + + useAnimation = require(script.Hooks.useAnimation), + useGroupAnimation = require(script.Hooks.useGroupAnimation), + useSequenceAnimation = require(script.Hooks.useSequenceAnimation), - useLatest = require(script.Hooks.useLatest), - useSequence = require(script.Hooks.useSequence), useSpring = require(script.Hooks.useSpring), useTween = require(script.Hooks.useTween), + + useBindings = require(script.Hooks.useBindings), } diff --git a/test.project.json b/test.project.json index da44991..7bcd5be 100644 --- a/test.project.json +++ b/test.project.json @@ -1,5 +1,5 @@ { - "name": "Roact-Flow Animation Test", + "name": "React-Flow Animation Test", "tree": { "$className": "DataModel", "ReplicatedStorage": { @@ -9,7 +9,7 @@ "$path": "Packages" }, - "RoactAnimation": { + "ReactAnimation": { "$path": "src" } }, diff --git a/test/Test.lua b/test/Test.lua index 28f0fa4..5bf5bb5 100644 --- a/test/Test.lua +++ b/test/Test.lua @@ -1,25 +1,33 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") local Packages = ReplicatedStorage:WaitForChild("Packages") -local Roact = require(Packages.Roact) -local useEffect = Roact.useEffect -local createElement = Roact.createElement +local React = require(Packages.React) +local useEffect = React.useEffect +local createElement = React.createElement -local ReactAnimation = require(Packages.RoactAnimation) -local Animation = ReactAnimation.Animation -local useTween = ReactAnimation.useTween +local ReactAnimation = require(Packages.ReactAnimation) +local useGroupAnimation = ReactAnimation.useGroupAnimation +local useSequenceAnimation = ReactAnimation.useSequenceAnimation local useSpring = ReactAnimation.useSpring -local useSequence = ReactAnimation.useSequence -local useLatest = ReactAnimation.useLatest +local useTween = ReactAnimation.useTween +local useBindings = ReactAnimation.useBindings +-- local useAnimation = ReactAnimation.useAnimation +-- local Spring = ReactAnimation.Spring +local Tween = ReactAnimation.Tween -- @helpers +local RNG = Random.new() + local function createFrame(props, children) props.Size = props.Size or UDim2.fromScale(1, 1) props.SizeConstraint = Enum.SizeConstraint.RelativeXX props.Text = props.Name + local parentSize = props.ParentSize or UDim2.fromScale(0.1, 0.25) + props.ParentSize = nil + return createElement("Frame", { - Size = UDim2.fromScale(0.1, 0.25), + Size = parentSize, AnchorPoint = Vector2.new(0.5, 0), BackgroundTransparency = 1, }, { @@ -30,91 +38,114 @@ end -- @tests -- @TestSequence local function TestSequence() - local sequence, play, stop = useSequence({ - position = Animation( - "Spring", - 0, - { start = UDim2.fromScale(-1, 0), goal = UDim2.fromScale(1, 0), speed = 20, damper = 0.7 } - ), - - size = Animation( - "Tween", - 0.2, - { start = UDim2.fromScale(0.5, 0.5), goal = UDim2.fromScale(1.2, 1.2), tweenInfo = TweenInfo.new(1) } - ), + -- local sequence, play, stop = useGroupAnimation({ + -- moveRight = useAnimation({ + -- -- position = Spring({ + -- -- target = UDim2.fromScale(0.8, 0), + -- -- speed = 5, + -- -- damper = 0.7, + -- -- }), + + -- position = Tween({ + -- target = UDim2.fromScale(0.8, 0), + -- info = TweenInfo.new(1, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut), + -- }), + -- }), + + -- moveLeft = useAnimation({ + -- -- position = Spring({ + -- -- target = UDim2.fromScale(0.3, 0), + -- -- speed = 5, + -- -- damper = 0.7, + -- -- }), + + -- position = Tween({ + -- target = UDim2.fromScale(0.3, 0), + -- info = TweenInfo.new(1, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut), + -- }), + -- }), + -- }, { + -- position = UDim2.fromScale(0.5, 0), + -- }) + + local sequence, play, stop = useGroupAnimation({ + yo = useSequenceAnimation({ + { + timestamp = 0, + size = Tween({ + target = UDim2.fromOffset(400, 400), + info = TweenInfo.new(1, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut), + }), + }, + { + timestamp = 1, + position = Tween({ + target = UDim2.fromScale(0.3, 0), + info = TweenInfo.new(1, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut), + }), + }, + { + timestamp = 2, + position = Tween({ + target = UDim2.fromScale(0.8, 0), + info = TweenInfo.new(1, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut), + }), + }, + }), + }, { + position = UDim2.fromScale(0.5, 0), + size = UDim2.fromOffset(200, 200), }) useEffect(function() - play() + local running = true - return stop + task.spawn(function() + while running do + play("yo") + task.wait(3) + end + end) + + return function() + running = false + stop() + end end, {}) return createFrame({ Name = "Sequence", Position = sequence.position, Size = sequence.size, + ParentSize = UDim2.fromScale(1, 0.25), }) end --- @TestPriority -local function TestPriority() - local sequence1, play1 = useSequence({ - position = Animation( - "Spring", - 0, - { start = UDim2.fromScale(-1, 0), goal = UDim2.fromScale(1, 0), speed = 20, damper = 0.7 } - ), - - size = Animation( - "Tween", - 1, - { start = UDim2.fromScale(0.5, 0.5), goal = UDim2.fromScale(1.2, 1.2), tweenInfo = TweenInfo.new(1) } - ), - }) - - local sequence2, play2 = useSequence({ - position = Animation( - "Spring", - 0.1, - { start = UDim2.fromScale(1, 0), goal = UDim2.fromScale(-1, 0), speed = 20, damper = 0.7 } - ), - - size = Animation( - "Tween", - 0.2, - { start = UDim2.fromScale(0.7, 0.7), goal = UDim2.fromScale(0.1, 0.1), tweenInfo = TweenInfo.new(1) } - ), +-- -- @TestTween +local function TestTween() + local value, update = useTween({ + info = TweenInfo.new(1), + start = UDim2.fromScale(0, 0), }) - useEffect(function() - play1() - play2() - end, {}) - - return createFrame({ - Name = "Priority", - Position = useLatest(sequence1.position, sequence2.position), - Size = useLatest(sequence1.size, sequence2.size), + local value2, update2 = useTween({ + info = TweenInfo.new(1), + start = Color3.new(), }) -end - --- @TestTween -local function TestTween() - local value, update = useTween(UDim2.fromScale(0, 0), TweenInfo.new(1)) - local value2, update2 = useTween(Color3.new(), TweenInfo.new(1)) - if value.X.Scale == 0 then - update({ goal = UDim2.fromScale(1, 0) }) - elseif (value :: UDim2).X.Scale == 1 then - update({ goal = UDim2.fromScale(0, 0) }) - end + useBindings(function(v, v2) + if v.X.Scale == 0 then + update({ target = UDim2.fromScale(1, 0) }) + elseif (v :: UDim2).X.Scale == 1 then + update({ target = UDim2.fromScale(0, 0) }) + end - if value2.R == 0 then - update2({ start = Color3.new(1, 0, 0), goal = Color3.new(1, 0, 0) }) - elseif value2.R == 1 then - update2({ goal = Color3.new(0, 0, 0) }) - end + if v2.R == 0 then + update2({ start = Color3.new(1, 0, 0), target = Color3.new(1, 0, 0) }) + elseif v2.R == 1 then + update2({ target = Color3.new(0, 0, 0) }) + end + end, { value, value2 }) return createFrame({ Name = "Tween", @@ -123,20 +154,24 @@ local function TestTween() }) end --- @TestSpring +-- -- @TestSpring local function TestSpring() - local value, update = useSpring(UDim2.fromScale(-1, 0), 10, 0.5) + local value, update = useSpring({ + start = UDim2.fromScale(0, 0), + speed = 5, + damper = 0.7, + }) useEffect(function() local running = true task.spawn(function() while running do - update({ goal = UDim2.fromScale(1, 0) }) + update({ target = UDim2.fromScale(1, 0) }) task.wait(3) if running then - update({ goal = UDim2.fromScale(-1, 0) }) + update({ target = UDim2.fromScale(-1, 0) }) task.wait(3) end end @@ -166,8 +201,7 @@ local function Test() tween = createElement(TestTween), spring = createElement(TestSpring), - sequence = createElement(TestSequence), - priority = createElement(TestPriority), + -- sequence = createElement(TestSequence), }) end diff --git a/test/init.client.lua b/test/init.client.lua index 13ceb54..73352c2 100644 --- a/test/init.client.lua +++ b/test/init.client.lua @@ -3,17 +3,18 @@ local Players = game:GetService("Players") local LocalPlayer = Players.LocalPlayer local Packages = ReplicatedStorage:WaitForChild("Packages") -ReplicatedStorage.RoactAnimation.Parent = Packages +ReplicatedStorage.ReactAnimation.Parent = Packages local Test = require(script.Test) -local Roact = require(Packages.Roact) -local createElement = Roact.createElement -local mount = Roact.mount +local React = require(Packages.React) +local ReactRoblox = require(Packages.ReactRoblox) +local createElement = React.createElement if not LocalPlayer.Character then LocalPlayer.CharacterAdded:Wait() task.wait(4) end -mount(createElement("ScreenGui", {}, { Test = createElement(Test) }), LocalPlayer:WaitForChild("PlayerGui")) +local root = ReactRoblox.createRoot(LocalPlayer:WaitForChild("PlayerGui")) +root:render(createElement("ScreenGui", {}, { Test = createElement(Test) })) diff --git a/test/init.story.lua b/test/init.story.lua index ccb5cde..bc1b30f 100644 --- a/test/init.story.lua +++ b/test/init.story.lua @@ -1,19 +1,20 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") local Packages = ReplicatedStorage.Packages -local Roact = require(Packages.Roact) -local createElement = Roact.createElement -local mount, unmount = Roact.mount, Roact.unmount +local React = require(Packages.React) +local ReactRoblox = require(Packages.ReactRoblox) +local createElement = React.createElement return function(container) - local roactAnimation = ReplicatedStorage.RoactAnimation - roactAnimation.Parent = Packages + local ReactAnimation = ReplicatedStorage.ReactAnimation + ReactAnimation.Parent = Packages local Test = require(script.Parent.Test) - local root = mount(createElement(Test), container) + local root = ReactRoblox.createRoot(container) + root:render(createElement(Test)) return function() - unmount(root) - roactAnimation.Parent = ReplicatedStorage + ReactAnimation.Parent = ReplicatedStorage + root:unmount() end end diff --git a/wally.toml b/wally.toml index 768f184..d549419 100644 --- a/wally.toml +++ b/wally.toml @@ -1,10 +1,12 @@ [package] -name = "nexure/roact-animation" +name = "nexure/react-animation" version = "0.1.1" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" [dependencies] -Roact = "core-packages/roact-compat@17.0.1-rc.16.1" + +React = "core-packages/react@17.0.1-rc.18" +ReactRoblox = "core-packages/react-roblox@17.0.1-rc.16" Promise = "evaera/promise@4.0.0"