private/Controls/New-UiLoadingSpinner.ps1
|
function New-UiLoadingSpinner { <# .SYNOPSIS Spinning animation for loading states, handed back as its host Grid so Visibility drives it. #> [CmdletBinding()] [OutputType([System.Windows.Controls.Grid])] param( [int]$Size = 16, [string]$Color = '#FFFFFF', # Takes a theme key instead of a fixed color, for a spinner that lives past a theme switch. [string]$BrushKey ) $thickness = [Math]::Max(1.5, $Size / 8) $centre = $Size / 2 $brush = ConvertTo-UiBrush $Color $stroke = [System.Windows.Shapes.Shape]::StrokeProperty # Inset by half a stroke or the host clips the fat end of the spiral $radius = ($Size - $thickness) / 2 # The slices depend only on Size, and a button builds a spinner on every click, so they get cut once per size if (!$script:_spinnerSlices) { $script:_spinnerSlices = @{} } if ($script:_spinnerSlices.Count -ge 8) { $script:_spinnerSlices.Clear() } if (!$script:_spinnerSlices.ContainsKey($Size)) { $cut = [System.Collections.Generic.List[object]]::new() $slices = 48 $step = 265 / $slices # WPF has no gradient for conic, so the fade is slices stepping down in opacity and width, each running 0.6 degrees into the next because antialiasing opens a hairline at every join. # The fade is squared off, or the far tail stays bright enough to close the ring up again. for ($i = 0; $i -lt $slices; $i++) { $fromRad = (($i * $step) - 90) * [Math]::PI / 180 $toRad = (($i * $step) + $step + 0.6 - 90) * [Math]::PI / 180 $figure = [System.Windows.Media.PathFigure]::new() $figure.StartPoint = [System.Windows.Point]::new( $centre + $radius * [Math]::Cos($fromRad), $centre + $radius * [Math]::Sin($fromRad)) $arc = [System.Windows.Media.ArcSegment]::new() $arc.Point = [System.Windows.Point]::new( $centre + $radius * [Math]::Cos($toRad), $centre + $radius * [Math]::Sin($toRad)) $arc.Size = [System.Windows.Size]::new($radius, $radius) $arc.SweepDirection = [System.Windows.Media.SweepDirection]::Clockwise $arc.IsLargeArc = ($step + 0.6) -gt 180 [void]$figure.Segments.Add($arc) $geometry = [System.Windows.Media.PathGeometry]::new() [void]$geometry.Figures.Add($figure) $geometry.Freeze() $toward = ($i + 1) / $slices $cut.Add([pscustomobject]@{ Geometry = $geometry Width = $thickness * (0.45 + 0.55 * $toward) Fade = [Math]::Pow($toward, 1.7) IsHead = $i -eq ($slices - 1) }) } $script:_spinnerSlices[$Size] = $cut } # The fixed ring behind the comet gives the eye something still to track the motion against. Flat caps on the slices so they butt together without a bump, and only the head is round. $track = [System.Windows.Shapes.Ellipse]@{ Width = $radius * 2 Height = $radius * 2 StrokeThickness = $thickness * 0.5 Opacity = 0.1 HorizontalAlignment = 'Center' VerticalAlignment = 'Center' } if ($BrushKey) { $track.SetResourceReference($stroke, $BrushKey) } else { $track.Stroke = $brush } # Canvas, so the arcs keep the coordinates they were built in, where a Grid would recenter each one on its own bounds. $rotor = [System.Windows.Controls.Canvas]@{ Width = $Size; Height = $Size } $rotor.CacheMode = [System.Windows.Media.BitmapCache]@{ RenderAtScale = 3 } foreach ($slice in $script:_spinnerSlices[$Size]) { $endCap = if ($slice.IsHead) { [System.Windows.Media.PenLineCap]::Round } else { [System.Windows.Media.PenLineCap]::Flat } $path = [System.Windows.Shapes.Path]@{ Data = $slice.Geometry StrokeThickness = $slice.Width StrokeEndLineCap = $endCap Opacity = $slice.Fade } if ($BrushKey) { $path.SetResourceReference($stroke, $BrushKey) } else { $path.Stroke = $brush } [void]$rotor.Children.Add($path) } # CenterX and CenterY, as RenderTransformOrigin is a fraction of whatever size layout decides on and the comet wobbles when that isn't square $spin = [System.Windows.Media.RotateTransform]::new() $spin.CenterX = $centre $spin.CenterY = $centre $rotor.RenderTransform = $spin $animation = [System.Windows.Media.Animation.DoubleAnimation]@{ From = 0 To = 360 Duration = [System.Windows.Duration]::new([TimeSpan]::FromSeconds(1.1)) RepeatBehavior = [System.Windows.Media.Animation.RepeatBehavior]::Forever } [System.Windows.Media.Animation.Timeline]::SetDesiredFrameRate($animation, 60) $spin.BeginAnimation([System.Windows.Media.RotateTransform]::AngleProperty, $animation) $spinner = [System.Windows.Controls.Grid]@{ Width = $Size Height = $Size HorizontalAlignment = 'Center' VerticalAlignment = 'Center' } [void]$spinner.Children.Add($track) [void]$spinner.Children.Add($rotor) return $spinner } |