Skip to content

Instantly share code, notes, and snippets.

@daisyUniverse
Created April 10, 2025 18:18
Show Gist options
  • Save daisyUniverse/c838379703a7cddac4316b7d71d70bd8 to your computer and use it in GitHub Desktop.
Save daisyUniverse/c838379703a7cddac4316b7d71d70bd8 to your computer and use it in GitHub Desktop.
Continued experiments in creating a Powershell Screenbuffer
# 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