Types/OBS.GetSceneItemList.Response/Animate.ps1

<#
.SYNOPSIS
    Animates scene items
.DESCRIPTION
    Animates the motion of scene items within a frame.
.EXAMPLE
    $stars = Add-OBSBrowserSource -URI https://pssvg.start-automating.com/Examples/Stars.svg
    $stars.FitToScreen()
    $stars.Animate(@{
        scale = 0.1
    },"00:00:01")
#>

param(
)

filter ToPosition {
    param(
    [switch]
    $Width,

    [switch]
    $Height
    )

    
    if ($_ -is [string] -and $_ -match '%$') {
        $_ = $_ -replace '%$' -as [double]
        if (-not $script:CachedOBSVideoSettings) {
            $script:CachedOBSVideoSettings = Get-OBSVideoSettings
        }
        $videoSettings = $script:CachedOBSVideoSettings
        if ($Width) {
            $_/100 * $videoSettings.baseWidth
        }
        if ($Height) {
            $_/100 * $videoSettings.baseHeight
        }
        
    }
    elseif ($_ -is [double] -or $_ -is [int]) {
        if ($_ -is [double] -and $_ -ge 0 -and $_ -lt 1) {
            if (-not $script:CachedOBSVideoSettings) {
                $script:CachedOBSVideoSettings = Get-OBSVideoSettings
            }
            $videoSettings = $script:CachedOBSVideoSettings
            if ($Width) {
                $_ * $videoSettings.baseWidth
            }
            if ($Height) {
                $_ * $videoSettings.baseHeight
            }
            
        } else {
            [int]$_
        }
    }
}

filter ToScale {
    if ($_ -is [string] -and $_ -match '%$') {
        $_ = $_ -replace '%$' -as [double]
        $_/100
    }
    elseif ($_ -is [double] -or $_ -is [int]) {
        if ($_ -is [double] -and $_ -ge 0 -and $_ -le 1) {
            $_
        } else {
            [double]$_/100
        }
    }
}

$nextTimeSpan = [timespan]0

$keyNames = 'positionX', 'positionY', 'scaleX','scaleY', 'cropBottom', 'cropLeft', 'cropRight', 'cropTop', 'rotation'
$keyAliases = [Ordered]@{'Rotate'='rotation'}
$duplicatedKeyAliases = [Ordered]@{
    'position' = 'positionX', 'positionY'
    'scale' = 'scaleX', 'scaleY'
    'crop' = 'cropBottom', 'cropLeft', 'cropRight', 'cropTop'
}
foreach ($position in 'X','Y') {
    $keyAliases[$position] = "position$position"
}
foreach ($cropType in 'Bottom','Left', 'Right', 'Top') {
    $keyAliases[$cropType] = "crop$cropType"
}
   
$allSteps = @()


$originalTransform = $this | Get-OBSSceneItemTransform
$lastFrom = 
    if ($originalTransform -is [Collections.IDictionary]) {
        [Ordered]@{} + $from
    } else {
        $newFrom = [Ordered]@{}
        foreach ($property in $originalTransform.psobject.properties) {
            $newFrom[$property.Name] = $property.Value
        }
        $newFrom
    }

$totalTimeSpan = [timespan]0

# We want to walk over every argument and turn them into a series of animations

$PassThru   = $false

$AllArgs = @($args)

$allSteps = @(
:NextArgument for ($argIndex = 0 ; $argIndex -lt $allArgs.Length; $argIndex++) {
    $arg = $allArgs[$argIndex]
    # If the arg is a timespan, we want to track this
    if ($arg -as [timespan]) {
        $nextTimeSpan = $arg -as [timespan]
    } elseif ($arg -is [double] -or $arg -is [int]) {
        $nextTimeSpan = [timespan]::fromSeconds($arg)
    }
    elseif ($arg -is [bool]) {
        if ($arg) {
            $PassThru = $true
        }
    }
    else {
        $currentTo = 
            if ($arg -isnot [Collections.IDictionary]) {
                $newTo = [Ordered]@{}
                foreach ($property in $arg.psobject.properties) {
                    $newTo[$property.Name] = $property.Value
                }
                $newTo
            } else {
                [Ordered]@{} + $arg
            }

        $badKey = 
            @(foreach ($k in @($currentTo.Keys)) {
                if ($k -notin $keyNames) {
                    if ($keyAliases[$k]) {
                        $currentTo[$keyAliases[$k]] = $currentTo[$k]
                        $currentTo.Remove($k)
                    } 
                    elseif ($duplicatedKeyAliases[$k]) {
                        foreach ($duplicateKey in $duplicatedKeyAliases[$k]) {
                            $currentTo[$duplicateKey] = $currentTo[$k]
                        }
                        
                        $currentTo.Remove($k)
                    }
                    else {
                        $k
                    }
                }
            })

        foreach ($checkKeyValue in @($currentTo.GetEnumerator())) {
            
            $newValue = 
                switch ($checkKeyValue.Key) {
                    positionX { $checkKeyValue.Value | ToPosition -Width }
                    positionY { $checkKeyValue.Value | ToPosition -Height }
                    cropLeft { $checkKeyValue.Value | ToPosition -Width }
                    cropRight { $checkKeyValue.Value | ToPosition -Width }
                    cropTop { $checkKeyValue.Value | ToPosition -Height }
                    cropBottom { $checkKeyValue.Value | ToPosition -Height }
                    scaleX { $checkKeyValue.Value | ToScale  }
                    scaleY { $checkKeyValue.Value | ToScale  }
                }

            if ($null -ne $newValue) {
                $currentTo[$checkKeyValue.Key] = $newValue
            }
            
        }
        
        if ($badKey) {
            throw "Cannot animate '$($badKey -join "','")' : Can only animate $($keyNames -join ',')"
        }

        if (-not $nextTimeSpan.TotalMilliseconds -and 
            ($argIndex -lt ($AllArgs.Length - 1)) -and 
            $AllArgs[$argIndex + 1] -as [timespan]
        ) {
            $nextTimeSpan = $AllArgs[$argIndex + 1] -as [timespan]
            $argIndex++
        }

        if ($lastFrom -and $nextTimeSpan) {
            $StepCount = [Math]::Ceiling($NextTimeSpan.TotalMilliseconds / ([timespan]::fromSeconds(1/30).TotalMilliseconds)) * 2            
            if (-not $StepCount) {
                $this | Set-OBSSceneItemTransform -SceneItemTransform $currentTo -PassThru
                $newLastFrom = [Ordered]@{} + $currentTo
                foreach ($kv in $lastFrom.GetEnumerator()) {
                    if ($currentTo.Contains($kv.Key)) { continue }
                    $newLastFrom[$kv.Key] = $kv.Value
                }
                $lastFrom = $newLastFrom # May need to join this with the remaining properties in from

                $totalTimeSpan += $nextTimeSpan
                $nextTimeSpan = [timespan]0
                continue NextArgument
            }

            $stepMilliseconds  = $nextTimeSpan.TotalMilliseconds / $StepCount

            
            # Compare the two sets of keys to determine the base data object
            $BaseObject = [Ordered]@{}
            foreach ($key in $currentTo.Keys) {
                if (-not $BaseObject[$key]) {
                    $BaseObject[$key] = 
                        if ($null -ne $lastFrom[$key]) {
                            $lastFrom[$key]
                        } else {
                            $currentTo[$key]
                        }
                }
            }                    

            # Determine the animation change per step.
            $eachStepValue = [Ordered]@{}
            foreach ($key in $baseObject.Keys) {
                $distance = try { $currentTo[$key] - $baseObject[$key] } catch { $null }
                if ($null -ne $distance) {
                    $eachStepValue[$key] = [float]$distance / $StepCount
                }
            }

            
            foreach ($stepNumber in 1..($stepCount)) {
                $stepObject = [Ordered]@{}
                foreach ($key in $BaseObject.Keys) {
                    $stepObject[$key] = $BaseObject[$key] + ($eachStepValue[$key] * $stepNumber)
                }
                $this | Set-OBSSceneItemTransform -SceneItemTransform $stepObject -PassThru
                Send-OBSSleep -SleepMillis $stepMilliseconds -PassThru
            }

        } else {
            $this | Set-OBSSceneItemTransform -SceneItemTransform $currentTo -PassThru
        }
        $newLastFrom = [Ordered]@{} + $currentTo
        foreach ($kv in $lastFrom.GetEnumerator()) {
            if ($currentTo.Contains($kv.Key)) { continue }
            $newLastFrom[$kv.Key] = $kv.Value
        }
        $lastFrom = $newLastFrom # May need to join this with the remaining properties in from

        $totalTimeSpan += $nextTimeSpan
        $nextTimeSpan = [timespan]0

        if (($argIndex -lt ($AllArgs.Length - 1)) -and 
            $AllArgs[$argIndex + 1] -as [timespan]
        ) {
            Send-OBSSleep -SleepMillis ($AllArgs[$argIndex + 1] -as [timespan]).TotalMilliseconds -PassThru
        }
    }

    $IsFirstArg = $false
}
)

if ($allSteps) {
    # If any boolean true was in the arguments, we're passing thru
    if ($PassThru) {
        $allSteps
    } else {
        # Send all of the steps to OBS.
        $allSteps | Send-OBS 
    }    
}