Created
April 10, 2025 18:18
-
-
Save daisyUniverse/c838379703a7cddac4316b7d71d70bd8 to your computer and use it in GitHub Desktop.
Continued experiments in creating a Powershell Screenbuffer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Screenbuffer Experiments | |
# More experiments in learning how to implement a fast powershell screenbuffer | |
# Robin Universe [D] | |
# 03 . 05 . 25 | |
param( | |
[switch]$stepthrough, | |
[switch]$cls, | |
[switch]$slow | |
) | |
# X IS SIDE TO SIDE ($pos[0]) | |
# Y IS UP AND DOWN ($pos[1]) | |
[Console]::SetCursorPosition(0,0) | |
[Console]::CursorVisible = $false | |
$Esc=[char]0x1b | |
if ($cls){Clear-Host} | |
function rgb( [int[]]$rgb=($null), [int[]]$bgrgb=($null), [string]$string=$null, [switch]$NoReset, [switch]$Code){ | |
if (!$NoReset) { $rst = "$Esc[0m" } else { $rst="" } # Reset control code | |
$bgc = "$Esc[48;2;" # Background control code | |
$fgc = "$Esc[38;2;" # Foreground control code | |
if ($null -ne $rgb ) { $out = ( $fgc + $rgb[0] + ";" + $rgb[1] + ";" + $rgb[2] + "m" ) } # if there is a fg rgb value, apply | |
if ($null -ne $bgrgb) { $out += ( $bgc + $bgrgb[0] + ";" + $bgrgb[1] + ";" + $bgrgb[2] + "m" ) } # if there is a bg rgb value, apply | |
if ($Code) { Return ( $out ) } else { Return ( $out + $string + $rst ) } | |
} | |
class hue { | |
[int]$speed | |
[int[]]$rgb | |
[bool[]]$up = ( $true, $true, $true ) | |
$scd = $this.speed | |
hue() { | |
$this.speed = 50 # Speed controller, higher = slower | |
$this.scd = $this.speed | |
$this.rgb = ( 100, 64, 253 ) # Initial RGB values | |
$this.up = ( $true, $true, $true ) # Indicator for if the values are rising or falling | |
} | |
hue([int]$speed, [int[]]$rgb){ | |
$this.speed = $speed | |
$this.rgb = $rgb | |
$this.scd = $speed | |
} | |
[string] increment(){ | |
$i=0; $this.scd-- # Init counters | |
if ($this.scd -lt 0){ # Repeat same values for as long as SCD (Speed Countdown) is above zero | |
foreach ($color in $this.rgb){ | |
$sup = $this.up[$i] # Grab direction | |
$scale = [Math]::Abs( $color - 128 ) # Find current distance to 128 | |
$normalize = ( $scale - -255 ) / ( 255 - -255 ) # Normalize scale for easing | |
if ( $sup ) { $color += [Math]::Floor( $normalize + 1) } # Add normalized scale going upwards | |
elseif ( $false -eq $sup ) { $color -= [Math]::Floor( $normalize + 1) } # Sub normalized scale going downwards | |
if ( $color -gt 253 ) { $sup = $false; $color = 253 } # Clamp value max | |
elseif ( $color -lt 55 ) { $sup = $true; $color = 55 } # Clamp value min | |
$this.scd = $this.speed # Apply loop changes | |
$this.rgb[$i] = $color | |
$this.up[$i] = $sup | |
$i++ | |
} | |
} | |
Return $this.rgb | |
} | |
} | |
function Get-Canvas-Size { | |
$window = [PSCustomObject]@{ | |
height = (Get-Host).UI.RawUI.MaxWindowSize.Height | |
width = (Get-Host).UI.RawUI.MaxWindowSize.Width | |
} | |
return $window | |
} | |
function BuildBuffer($window, $char="") { | |
$l = 0 | |
$buffer = [string[]]::new($window.height) | |
while ($l -lt ($window.height - 1)){ | |
$l++ | |
$buffer[$l] = ($char * ($window.width - 2) + "`n") | |
} | |
return $buffer | |
} | |
function Render($buffer){ | |
[Console]::SetCursorPosition(0,0) | |
foreach ($line in $buffer){ if ( $line.length -ge $window.width ) { $line = $line.Substring(0, ($window.width + 1) ) } } | |
[Console]::Write($buffer) | |
} | |
class FPSCounter { | |
[int]$FrameCount = 0 | |
[System.Diagnostics.Stopwatch]$Stopwatch | |
[double]$LastTime = 0 | |
[double]$FPS = 0 | |
FPSCounter() { | |
$this.Stopwatch = [System.Diagnostics.Stopwatch]::new() | |
$this.Stopwatch.Start() | |
$this.LastTime = 0 | |
} | |
[double] Update() { | |
$this.FrameCount++ | |
$currentTime = $this.Stopwatch.Elapsed.TotalSeconds | |
$elapsed = $currentTime - $this.LastTime | |
if ($elapsed -ge 1.0) { | |
$this.FPS = [Math]::Round($this.FrameCount / $elapsed, 2) | |
$this.FrameCount = 0 | |
$this.LastTime = $currentTime | |
} | |
return $this.FPS | |
} | |
} | |
class canvas{ | |
$window | |
$LastCursorPosition | |
canvas() { | |
$this.window = Get-Canvas-Size | |
$this.LastCursorPosition | |
} | |
# Issue: The positioning of the string is skewed by the length of the string being different with ansi control characters | |
# IE: "X" becomes "[38;2;255;255;255m[48;2;21;23;255mX[0m" when given an RGB ansi control code | |
# This causes the screenbuffer to have unwritable space, or go out of bounds when attempting to modify the buffer line | |
[int[]] GetLastCursorPosition(){ | |
if ($Null -ne $this.LastCursorPosition){ | |
Return $this.LastCursorPosition | |
} else { | |
Return (0,0) | |
} | |
} | |
[string] SetColor([string]$buffer, [int[]]$rgb=$null, [int[]]$bgrgb=$null, [int[]]$pos1, [int[]]$pos2=$null){ | |
# This implementation should allow for all colors to be added once per render loop, but it will mean that only one color can be set per line... | |
# POS[0] X (Character Position) (Side to Side) | |
# POS[1] Y (Line) ( Up and Down ) | |
# Lines[POS[1]] Selects a line | |
$lines = $buffer.split("`n") | |
$rst = ( [char]0x1b + "[0m" ) | |
$col = ( rgb -rgb $rgb -bgrgb $bgrgb -Code ) | |
if ($null -ne $pos2){ | |
# Second position argument added, fill area. Will need to do some math stuff to figure out how to connect the dots.... | |
if ($pos2[0] -ge $this.window.width ) { $pos2[0] = ( $this.window.width - 1) } # Cap position arguments to window size | |
if ($pos2[1] -ge $this.window.height ) { $pos2[1] = ( $this.window.height - 1) } | |
$c = $pos1[1] | |
while ($c -le $($pos2[1]) ) { # Loop through each line in target area | |
$lines[ $c ] = $lines[ $c ].Insert( ( $pos2[0] ), $rst ) # First, insert the reset character after the target character | |
$lines[ $c ] = $lines[ $c ].Insert( ( $pos1[0] ), $col ) # And then, insert the color code to that same coordinate. | |
$c++ | |
} | |
} else { | |
# Only one position argument given, only color single character | |
if ($pos1[0] -gt $this.window.width ) { $pos2[0] = $this.window.width } # Cap position arguments to window size | |
if ($pos1[1] -gt $this.window.height ) { $pos2[1] = $this.window.height } | |
$lines[ $pos1[1] ] = $lines[ $pos1[1] ].Insert( ( $pos1[0] + 1 ), $rst ) # First, insert the reset character after the target character | |
$lines[ $pos1[1] ] = $lines[ $pos1[1] ].Insert( ( $pos1[0] ), $col ) # And then, insert the color code to that same coordinate. | |
} | |
# $li = 0 | |
# foreach ($line in $lines){ | |
# if ($line.Length -gt $this.window.width ) { | |
# $lines[$li] = $line.substring(0, ( $this.window.width - 1 ) ) + $rst | |
# } | |
# $li++ | |
# } | |
$buffer = $lines -join "`n" | |
Return $buffer | |
} | |
[string] SetCharacter([int[]]$pos, [string]$char, [string]$buffer) { | |
$lines = $buffer.split("`n") # Splits original buffer into an array of lines | |
$l = $char.Length | |
if($Null -ne $($lines[$pos[1]]) ) { | |
try { $lines[$pos[1]] = $lines[$pos[1]].Remove($pos[0], $l).Insert($pos[0], $char ) } | |
catch { } # Replaces characters starting at specified coordinate | |
} | |
if ($lines[$pos[1]].Length -gt $this.window.width ) { # Truncate lines that go past the screenbuffer size | |
$lines[$pos[1]] = $lines[$pos[1]].substring(0, ( ( $this.window.width - 1 ) ) ) + ( [char]0x1b + "[0m" ) | |
} | |
$buffer = $lines -join "`n" # Stitches the buffer back together | |
$this.LastCursorPosition = (($pos[0] + $l), $pos[1]) | |
Return $buffer | |
} | |
[string] Get([int]$x, [int]$y) { | |
Return " " | |
} | |
} | |
function Get-NormalizeCoordinates([int[]]$pos){ | |
$window = Get-Canvas-Size | |
$normalizedX = ($pos[0] / $window.width * 2) -1 | |
$normalizedY = ($pos[1] / $window.height * 2) -1 | |
return ($normalizedX, $normalizedY) | |
} | |
function Set-NormalizedCoordinates([double[]]$pos){ | |
$window = Get-Canvas-Size | |
$screenX = [Math]::Round( ( ( $pos[0] + 1 ) / 2) * $window.width ) | |
$screenY = [Math]::Round( ( ( $pos[1] + 1 ) / 2) * $window.height ) | |
return [int[]]($screenX, $screenY) | |
} | |
$window = Get-Canvas-Size | |
$buffer = BuildBuffer $window "X" | |
$canvas = [canvas]::new() | |
$fps = [FPSCounter]::new() | |
$bghue = [hue]::new() | |
function rect($char = " ", [int[]]$size = (1,1) , [int[]]$pos = (0,0), $buffer, [int[]]$rgb, [int[]]$bgrgb){ | |
$x=0; $y=0 | |
$char = $char * $size[0] | |
while ($x -lt $size[1]){ | |
$buffer = $canvas.SetCharacter( ( $pos[1], ($pos[0] + $x) ), $char, $buffer, $rgb, $bgrgb ) | |
$x++ | |
} | |
return $buffer | |
} | |
if($stepthrough){ # Advance frame by frame | |
$pos = 0, 0 | |
while($true){ | |
if ([console]::KeyAvailable){ | |
$x = [System.Console]::ReadKey($true) | |
if ($x.KeyChar -eq 'w'){ | |
$pos[1] = $pos[1] - 1 | |
$buffer = $canvas.Set($pos[0],$pos[1],"O", $buffer) | |
Render $buffer | |
} | |
if ($x.KeyChar -eq 's'){ | |
$pos[1] = $pos[1] + 1 | |
$buffer = $canvas.Set($pos[0],$pos[1],"O", $buffer) | |
Render $buffer | |
} | |
if ($x.KeyChar -eq 'a'){ | |
$pos[0] = $pos[0] - 1 | |
$buffer = $canvas.Set($pos[0],$pos[1],"O", $buffer) | |
Render $buffer | |
} | |
if ($x.KeyChar -eq 'd'){ | |
$pos[0] = $pos[0] + 1 | |
$buffer = $canvas.Set($pos[0],$pos[1],"O", $buffer) | |
Render $buffer | |
} | |
} | |
} | |
}else{ # Render as fast as possible | |
while($true){ | |
$fpsc = [string]( $fps.Update() ) | |
$CS = Get-Canvas-Size | |
if (($window.height -ne $CS.height) -or ($window.width -ne $CS.width)){$window = $CS; $buffer = BuildBuffer $window "X"} # Change buffer size as window size changes | |
$normalizedCoordsLeftMargin = @(-0.9, -0.9) | |
$normalizedCoordsRightMargin = @(0.9, 0.9) | |
$lmargin = ( Set-NormalizedCoordinates $normalizedCoordsLeftMargin ) | |
$rmargin = ( Set-NormalizedCoordinates $normalizedCoordsRightMargin ) | |
$hueC = ($bghue.increment()).Split(" ") | |
$fg = (200, 60, 200) | |
$bg = ($hueC[0], $hueC[1], $hueC[2]) | |
$buffer = $canvas.SetCharacter((0,0), " [ FPS $fpsc ]", $buffer) | |
$buffer = $canvas.SetCharacter(($canvas.GetLastCursorPosition()[0], 0), " [ Y $($window.height) ]", $buffer) | |
$buffer = $canvas.SetCharacter(($canvas.GetLastCursorPosition()[0], 0), " [ X $($window.width) ]", $buffer) | |
$buffer = $canvas.SetCharacter(($canvas.GetLastCursorPosition()[0], 0), " [ HUE [R $($hueC[0])] [G $($hueC[1])] [B $($hueC[2])] ]", $buffer) | |
$rem = ( "_" * ($CS.width - $canvas.GetLastCursorPosition()[0] - 1 ) ) | |
$buffer = $canvas.SetCharacter(($canvas.GetLastCursorPosition()[0], 0), $rem , $buffer) | |
$renderbuffer = $canvas.SetColor($buffer, $fg, $bg, (0,0), ($window.width, 0)) | |
$blue = (150, 150, 255) | |
$pink = (255, 150, 150) | |
$white = (255, 255, 255) | |
$height = $lmargin[1] | |
$renderbuffer = $canvas.SetColor($renderbuffer, $blue, $blue, ($lmargin[0],$height), ($rmargin[0], ($height = $height + 5))) | |
$renderbuffer = $canvas.SetColor($renderbuffer, $pink, $pink, ($lmargin[0],$height), ($rmargin[0], ($height = $height + 5))) | |
$renderbuffer = $canvas.SetColor($renderbuffer, $white, $white, ($lmargin[0],$height), ($rmargin[0], ($height = $height + 5))) | |
$renderbuffer = $canvas.SetColor($renderbuffer, $pink, $pink, ($lmargin[0],$height), ($rmargin[0], ($height = $height + 5))) | |
$renderbuffer = $canvas.SetColor($renderbuffer, $blue, $blue, ($lmargin[0],$height), ($rmargin[0], ($height = $height + 5))) | |
if ($slow){Start-Sleep 1} | |
Render $renderbuffer | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment