Skip to content

Tween

Bindings for @solid-primitives/tween.

type Easing = float -> float

let createTween(target: Accessor<'T>, ?duration: int, ?easing: Easing): Accessor<'T>

A derived signal that moves smoothly from the target's previous value to its new one whenever it changes. The target can be any accessor; to tween a component prop, pass fun () -> props.value. duration is in ms and defaults to 100. easing maps progress from 0. to 1., to speed up or slow down parts of the transition, and defaults to linear. Updates run on requestAnimationFrame, and stop once the value reaches the target.

Easings

The binding ships these easing functions, written in F# with no dependencies:

InOutIn and out
easeInSineeaseOutSineeaseInOutSine
easeInQuadeaseOutQuadeaseInOutQuad
easeInCubiceaseOutCubiceaseInOutCubic
easeInQuarteaseOutQuarteaseInOutQuart
easeInQuinteaseOutQuinteaseInOutQuint
easeInExpoeaseOutExpoeaseInOutExpo
easeInCirceaseOutCirceaseInOutCirc
easeInBackeaseOutBackeaseInOutBack
easeInElasticeaseOutElasticeaseInOutElastic
easeInBounceeaseOutBounceeaseInOutBounce

On Solid 2 today

createTween is an effect that watches the target and steps a signal towards it once per frame. Written out in plain Partas.Solid:

[<Global>]
let requestAnimationFrame (callback: float -> unit) : int = jsNative

[<Global>]
let cancelAnimationFrame (handle: int) : unit = jsNative

let tween (target: unit -> float) (duration: float) (easing: float -> float) =
    let value, setValue = createSignal (target ())
    createEffect (
        (fun (_: float option) -> target ()),
        fun (goal: float) ->
            let from = value ()
            let mutable start = -1.
            let mutable handle = 0
            let rec step (now: float) =
                if start < 0. then start <- now
                let t = min 1. ((now - start) / duration)
                setValue (from + (goal - from) * easing t)
                if t < 1. then handle <- requestAnimationFrame step
            handle <- requestAnimationFrame step
            fun () -> cancelAnimationFrame handle
    )
    value

let linear (t: float) = t
let easeOutCubic (t: float) = 1. - (1. - t) ** 3.
let easeOutBack (t: float) = 1. + 2.70158 * (t - 1.) ** 3. + 1.70158 * (t - 1.) ** 2.

Each row below tweens the same target over 800 ms with a different easing. Press Move:

[<SolidComponent>]
let TweenRow (name: string) (easing: float -> float) (target: unit -> float) =
    let x = tween target 800. easing
    div (style = "display: grid; grid-template-columns: 7.5rem 1fr; align-items: center; gap: .75rem") {
        code () { name }
        div (style = "position: relative; height: 1rem; border-radius: 999px; background: var(--nacara-bg-subtle)") {
            div (
                style =
                    "position: absolute; top: 0; width: 1rem; height: 1rem; border-radius: 999px; background: var(--nacara-primary); left: calc((100% - 1rem) * "
                    + string (x () / 100.)
                    + ")"
            )
        }
    }

[<SolidComponent>]
let TweenDemo () =
    let target, setTarget = createSignal 0.
    div (style = "display: grid; gap: .6rem; width: 100%") {
        TweenRow "linear" linear target
        TweenRow "easeOutCubic" easeOutCubic target
        TweenRow "easeOutBack" easeOutBack target
        button (
            class' = "p-btn p-btn--secondary",
            style = "justify-self: start",
            onClick = fun _ -> setTarget (if target () = 0. then 100. else 0.)
        ) { "Move" }
    }
export function TweenRow(name, easing, target) {
    const x = tween(target, 800, easing);
    return <div style="display: grid; grid-template-columns: 7.5rem 1fr; align-items: center; gap: .75rem">
        <code>
            {name}
        </code>
        <div style="position: relative; height: 1rem; border-radius: 999px; background: var(--nacara-bg-subtle)">
            <div style={("position: absolute; top: 0; width: 1rem; height: 1rem; border-radius: 999px; background: var(--nacara-primary); left: calc((100% - 1rem) * " + (x() / 100).toString()) + ")"} />
        </div>
    </div>;
}

export function TweenDemo() {
    const patternInput = createSignal(0);
    const target = patternInput[0];
    return <div style="display: grid; gap: .6rem; width: 100%">
        {untrack(TweenRow.bind(undefined, "linear", (linear), target))}
        {untrack(TweenRow.bind(undefined, "easeOutCubic", (easeOutCubic), target))}
        {untrack(TweenRow.bind(undefined, "easeOutBack", (easeOutBack), target))}
        <button class="p-btn p-btn--secondary"
            style="justify-self: start"
            onClick={(_arg) => {
                patternInput[1]((target() === 0) ? 100 : 0);
            }}>
            Move
        </button>
    </div>;
}
Edit this page