#Scope

A scope owns the Visions you capture and the animation runtime that drives them. It is the only thing you construct directly.

local Scope = Vision.Scope()

One scope per screen, per feature, or per session is all reasonable - the runtime inside it costs nothing until something is animating.

#Capture

Scope:Capture(Declaration: Declaration) -> Vision

Reads a declaration tree and returns a Vision. Creates no instances. Capture parses the tree, registers values and names, and hands back a handle you can write to.

A captured Vision is not held by the scope. Until it is mounted the scope does not know about it, Scope:Update skips it, and dropping it lets it be collected - exactly the same state a Vision returns to after Cleanup. Mount is what hands ownership to the scope.

local Interface = Scope:Capture({
    ClassName = "Frame",
    mount(PlayerGui),
    event("Count", 0, function(self, _, Value)
        self.Name = `Count{Value}`
    end),
})

#LoadStyle

Scope:LoadStyle(Declaration: { name: string, [string]: any }) -> Style

Creates or updates a named style and returns the sheet. Declarations bind to its fields with the style keyword, and changing a field updates every instance bound to it.

const Dark = Scope:LoadStyle({
    name = "Dark",
    Surface = Color3.fromRGB(20, 20, 26),
    Ink = Color3.fromRGB(240, 240, 250),
})

Dark:Set("Surface", Accent)

Styles can also be animated, so a theme change can crossfade instead of snapping:

Scope:SpringStyle(TweenInfo.new(0.4), Dark, { Surface = Paper, Ink = Coal })
Scope:PhysicsStyle(0.5, 0.7, "Dark", { Radius = 20 })

See Style for the full page.

#Update

Scope:Update(EventName: string, Value: any)

Writes one value into every Vision the scope has captured that declares that name. Visions without it are skipped, so a scope holding a mix of trees is fine.

Scope:Update("Theme", Color3.fromRGB(88, 101, 242))

It reaches everything the scope currently owns: captured Visions that have not been mounted yet, and mounted ones. A staged Vision stores the write and applies it on its first mount.

Cleanup takes a Vision out of the scope, so a cleaned up Vision is not updated. Mounting it again puts it back and later updates reach it.

Interface:Cleanup()
Scope:Update("Theme", Red)     -- Interface is not touched

Interface:Mount()
Scope:Update("Theme", Blue)    -- now it is

A derived value is skipped unless its compute handles "set", the same as writing it directly.

Tip

The scope holds its Visions weakly here, so Update never keeps one alive. Drop your reference to a Vision and it is still collectable.

#SpringValue deprecated

Deprecated

Use SpringEvent instead. It does the same thing and takes any number of values in one call, so SpringEvent(Info, Target, { Fill = 0.8 }) replaces SpringValue(Info, Target, "Fill", 0.8) directly.

Scope:SpringValue(
    Info: TweenInfo,
    Target: Vision | { Vision },
    ValueName: string,
    TargetValue: any
) -> Linker

Animates a single named value. Prefer SpringEvent, which does the same thing for any number of values in one call.

Writes through the CapFunc each frame - so every callback bound to that value runs, and the instances update themselves.

Scope:SpringValue(
    TweenInfo.new(0.6, Enum.EasingStyle.Back, Enum.EasingDirection.Out),
    Interface,
    "Fill",
    0.8
)

Accepts a list of Visions to drive several at once.

#SpringEvent

Scope:SpringEvent(
    Info: TweenInfo,
    Target: Vision | { Vision },
    Values: { [string]: any }
) -> Linker

Animates several named values at once, as one link. The table maps value names to their targets.

Scope:SpringEvent(TweenInfo.new(0.6), Interface, {
    Fill = 0.8,
    Tint = Color3.fromRGB(88, 101, 242),
})

Pass a list of Visions and every one of them animates from the same call, so one link can drive a whole grid.

Scope:SpringEvent(TweenInfo.new(0.6), Tiles, { Tint = Accent })

Each value keeps its own start, and each Vision keeps its own, so they can be at different places when the call is made. Naming a value the Vision does not declare raises.

#SpringInstance

Scope:SpringInstance(
    Info: TweenInfo,
    Target: Instance | { Instance },
    Properties: { [string]: any }
) -> Linker

Animates properties on instances directly, without going through a Vision.

Scope:SpringInstance(
    TweenInfo.new(0.18),
    Button,
    { BackgroundColor3 = Hover }
)

#SpringFunction

Scope:SpringFunction(
    Info: TweenInfo,
    Callback: (NewValue: any, DeltaTime: number) -> (),
    InitialValue: any,
    TargetValue: any
) -> Linker

Animates between two values and hands each step to your callback, along with the frame delta. For anything that is neither a Vision value nor a plain property.

Scope:SpringFunction(TweenInfo.new(0.7), function(Value, DeltaTime)
    Accent.Rotation = Value
end, 0, 180)

#PhysicsInstance, PhysicsFunction

Scope:PhysicsInstance(Period: number, Damping: number, Target, Properties) -> Linker
Scope:PhysicsFunction(Period: number, Damping: number, Callback, Initial, Goal) -> Linker

The same three shapes as the Spring* methods, driven by a damped spring instead of a TweenInfo. There is no duration: Period is the undamped period in seconds, Damping is the ratio, and the motion ends when it settles.

Scope:PhysicsInstance(0.6, 0.7, Button, { BackgroundColor3 = Hover })

#PhysicsValue deprecated

Deprecated

Use PhysicsEvent instead. PhysicsEvent(Period, Damping, Target, { Fill = 0.8 }) replaces PhysicsValue(Period, Damping, Target, "Fill", 0.8) directly.

Scope:PhysicsValue(Period: number, Damping: number, Target, ValueName, Goal) -> Linker

Animates a single named value with a solver. Still works, still cancels against the same claims, but every call it can make PhysicsEvent can make too.

damping behaviour
1 critically damped, no overshoot
0.7 overshoots ~4.6%
0.59 overshoots ~10%, close to Back/Out
> 1 overdamped, slow approach

#Velocity survives interruption

This is the reason to reach for these over the tween ones. Retarget a running physics spring and it keeps its velocity, so the motion flows into the new goal instead of stopping dead.

Scope:PhysicsInstance(0.6, 1, Panel, { Position = A })
-- mid flight
Scope:PhysicsInstance(0.6, 1, Panel, { Position = B })  -- carries the speed

A TweenInfo spring cannot do this: it restarts from the current value at zero velocity.

#Supported types

number, UDim, UDim2, Vector2, Vector3, Color3, Rect and NumberRange. Anything else raises, because a solver needs numeric components to integrate. Use a tween for CFrame, strings, sequences and booleans.

#They share the claim system

A physics spring and a tween spring claim targets the same way, so starting either cancels whatever was driving that value or property.

#Release

Scope:Release()

Cleans up every Vision the scope captured - running cleanup callbacks, disconnecting connections and destroying instances - cancels every link, and disconnects the runtime.

#Conflicts

Every link claims what it drives: a Vision and value name, an instance and property name, or a callback. Starting a new spring on a claim that is already held cancels the old link first.

Scope:SpringInstance(Info, Button, { BackgroundColor3 = Hover })
Scope:SpringInstance(Info, Button, { BackgroundColor3 = Idle })
-- the first link is cancelled; only one tween ever drives that property

This is why a fast mouse cannot leave two colour tweens fighting. A partial overlap cancels the whole earlier link - { Rotation, Transparency } followed by { Rotation } cancels the first entirely.

#The runtime

Each scope drives its links from one connection: RenderStepped on the client, Heartbeat on the server. It connects when the first link starts and disconnects when the last one finishes, so an idle scope costs nothing.