Skip to content

Instantly share code, notes, and snippets.

@Jaykul
Last active December 26, 2023 11:41
Show Gist options
  • Save Jaykul/df0b8310ca04f0c6a0f56a2ae0a4ad57 to your computer and use it in GitHub Desktop.
Save Jaykul/df0b8310ca04f0c6a0f56a2ae0a4ad57 to your computer and use it in GitHub Desktop.
ASCII Mandelbrots in the PowerShell console

A slight tweak to the ConEmu install script, plus my own settings file.

To Install:

iex (irm https://gist.githubusercontent.com/Jaykul/6deda247c677d425862d7098d3ee34ae/raw/Install.ps1)

Sorry, I'll shorten that URL when I move this to a real repo 😉

using namespace System.Numerics
class Mandelbrot : System.Collections.IEnumerator {
# From the constructor?
# Sadly, PowerShell doesn't support optional parameters
[double]$HorizontalViewOffset = -0.5
[double]$VerticalViewOffset = 0
$Columns = 120
$Rows = 28
$ZoomViewDistance = 6.75
# calculated during initialization
hidden [double[]]$x_range
hidden [double[]]$y_range
hidden [double]$xscale
hidden [double]$yscale
hidden [int]$iterations
hidden [Complex]$z0
# These are the things which implement IEnumerator
hidden [int]$_ix = 0
hidden [int]$_iy = 0
hidden [int[]]$Actual = $null
hidden [int]$SkipLines = -1
[void] Init() {
$aspect_ratio = 1/3
# lowering the distance will zoom in
$factor = $this.ZoomViewDistance / $this.Columns
$this.xscale = $factor * $aspect_ratio
$this.yscale = $factor
$this.iterations = 170
$this.z0 = [Complex]::Zero
$this.SkipLines = -1
$xmin = $this.HorizontalViewOffset - (($this.xscale * $this.Columns) / 2)
$ymin = $this.VerticalViewOffset - (($this.yscale * $this.Rows) / 2)
$this.x_range = foreach($ix in 0..($this.Columns - 1)){ $xmin + ($this.xscale * $ix) }
$this.y_range = foreach($iy in 0..($this.Rows - 1)){ $ymin + ($this.yscale * $iy) }
}
[object] get_Current() {
return $this.Actual
}
[void] Reset() {
if($this.iterations -eq 0) { $this.Init() }
$this._ix = 0
$this._iy = 0
}
[bool] MoveNext() {
if($this.iterations -eq 0) { $this.Init() }
if($this._ix -ge $this.Columns) {
$this._ix = 0
$this._iy++
}
if($this._iy -ge $this.Rows) {
return $false
}
$c = [Complex]::new($this.x_range[$this._ix], $this.y_range[$this._iy])
$z = $this.z0
$color = 0
$mind = 2
foreach ($i in (0..($this.iterations - 1))) {
$z = $z * $z + $c
$d = [Complex]::Abs($z)
if ($d -ge 2) {
$color = [Math]::Min([int]($mind / 0.007), 254) + 1
break
} else {
$mind = [Math]::Min($d, $mind)
}
}
# yield return ...
$this.Actual = @( $this._ix, $this._iy, $color )
# obscure feature which would allow skipping lines, if you were using this as a "working" animation ...
# when you need to write output, you might skip as many lines of the mandelbrot as you had output lines
if ($this.SkipLines -ge 0) {
$this._iy += $this.SkipLines
$do_break = $this.SkipLines -gt 0
$this.SkipLines = -1
$this._ix = 0
# NOTE: I still don't understand this
if ($do_break) { return $false }
} else {
$this._ix++
}
return $true
}
}
$script:palette = @(
$Null
[ConsoleColor]::DarkBlue
[ConsoleColor]::DarkMagenta
[ConsoleColor]::DarkCyan
[ConsoleColor]::DarkRed
[ConsoleColor]::DarkYellow
[ConsoleColor]::DarkGreen
[ConsoleColor]::Gray
)
# These are some "favorite" locations
$script:MandelbrotFavoriteViews = @(
# x, y, "distance", max color range
[PSCustomObject]@{HorizontalViewOffset=-0.5; VerticalViewOffset= 0; ZoomViewDistance =6.75; ColorViewLimit=255}
[PSCustomObject]@{HorizontalViewOffset=0.37865401; VerticalViewOffset= 0.669227668; ZoomViewDistance =0.04; ColorViewLimit=111}
[PSCustomObject]@{HorizontalViewOffset=-1.2693; VerticalViewOffset= -0.4145; ZoomViewDistance =0.2; ColorViewLimit=105}
[PSCustomObject]@{HorizontalViewOffset=-1.2693; VerticalViewOffset= -0.4145; ZoomViewDistance =0.05; ColorViewLimit=97}
[PSCustomObject]@{HorizontalViewOffset=-1.2642; VerticalViewOffset= -0.4185; ZoomViewDistance =0.01; ColorViewLimit=95}
[PSCustomObject]@{HorizontalViewOffset=-1.15; VerticalViewOffset= -0.28; ZoomViewDistance =0.9; ColorViewLimit=94}
[PSCustomObject]@{HorizontalViewOffset=-1.15; VerticalViewOffset= -0.28; ZoomViewDistance =0.3; ColorViewLimit=58}
[PSCustomObject]@{HorizontalViewOffset=-1.15; VerticalViewOffset= -0.28; ZoomViewDistance =0.05; ColorViewLimit=26}
)
function Get-ParameterValues {
<#
.Synopsis
Get the actual values of parameters which have manually set (non-null) default values or values passed in the call
.Description
Unlike $PSBoundParameters, the hashtable returned from Get-ParameterValues includes non-empty default parameter values.
NOTE: Default values that are the same as the implied values are ignored (e.g.: empty strings, zero numbers, nulls).
.Example
function Test-Parameters {
[CmdletBinding()]
param(
$Name = $Env:UserName,
$Age
)
$Parameters = . Get-ParameterValues
# This WILL ALWAYS have a value...
Write-Host $Parameters["Name"]
# But this will NOT always have a value...
Write-Host $PSBoundParameters["Name"]
}
#>
[CmdletBinding()]
param(
# The $MyInvocation for the caller -- DO NOT pass this (dot-source Get-ParameterValues instead)
$Invocation = $MyInvocation,
# The $PSBoundParameters for the caller -- DO NOT pass this (dot-source Get-ParameterValues instead)
$BoundParameters = $PSBoundParameters
)
if($MyInvocation.Line[($MyInvocation.OffsetInLine - 1)] -ne '.') {
throw "Get-ParameterValues must be dot-sourced, like this: . Get-ParameterValues"
}
if($PSBoundParameters.Count -gt 0) {
throw "You should not pass parameters to Get-ParameterValues, just dot-source it like this: . Get-ParameterValues"
}
$ParameterValues = @{}
foreach($parameter in $Invocation.MyCommand.Parameters.GetEnumerator()) {
# gm -in $parameter.Value | Out-Default
try {
$key = $parameter.Key
if($null -ne ($value = Get-Variable -Name $key -ValueOnly -ErrorAction Ignore)) {
if($value -ne ($null -as $parameter.Value.ParameterType)) {
$ParameterValues[$key] = $value
}
}
if($BoundParameters.ContainsKey($key)) {
$ParameterValues[$key] = $BoundParameters[$key]
}
} finally {}
}
$ParameterValues
}
function Write-Pixel {
[CmdletBinding()]
param($color, $char, $ColorViewLimit = 256, [Switch]$invert)
$chars = @([char]183, "-", "+", "*", "%", "#")
$idx = {param($chars) [Math]::Abs(($color+1) * ($chars.Count - 1) / $ColorViewLimit) }
if(!$invert) {
$idx2 = $idx
$idx = {param($chars,$idx2=$idx2) $chars.Count - 1 - (&$idx2 $chars)}
}
if(!$char) {
$char = $chars[(&$idx $chars)]
}
$ansi_color = $script:palette[(&$idx $script:palette)]
if($null -eq $ansi_color) {
Write-Host $char -NoNewLine
} else {
Write-Host $char -Foreground $ansi_color -NoNewLine
}
if($DebugPreference -gt "SilentlyContinue") {
# When testing new locations, it's useful to be able to see the range of colors we *should* have used
if($null -eq $script:color_range) {
$script:color_range = $color, $color
}
else {
$old_color_range = $script:color_range
$script:color_range = @([Math]::Min($script:color_range[0], $color), [Math]::Max($script:color_range[1], $color))
if (($old_color_range[0] - $script:color) -gt 3 -or ($script:color - $old_color_range[1]) -gt 3) {
return $true
}
}
}
return $false
}
$Script:LastFavorite = 0
function New-Mandelbrot {
<#
.Synopsis
Generates an ASCII Mandelbrot in the console
.Description
Generates a Mandelbrot with the specified parameters using Write-Host to output it.
When called without parameters, cycles through a series of favorite views of the mandelbrot
.Example
New-Mandelbrot -Favorite 0 -Zoom 12
Generates a default full-console mandelbrot, zoomed out so there's a border all around
.Example
New-Mandelbrot -Favorite 3
Generates a highly zoomed in view from the favorites list
#>
[CmdletBinding(DefaultParameterSetName="Favorites")]
param(
# Select from a pre-defined set of good View values
# Combine with -Verbose if you want to see what the parameter values are
[ValidateScript({if($_ -ge 0 -and $_ -lt $MandelbrotFavoriteViews.Count) {$true}else { throw "FavoriteView out of range, there are only $($MandelbrotFavoriteViews.Count) favorites"}})]
[Parameter(ParameterSetName="Favorites", Position=0)]
$FavoriteView = $Script:LastFavorite,
# Set the width of the output Mandelbrot in console columns
$Columns = $(if($w=$host.UI.RawUI.WindowSize.Width){$W}else{80}),
# Set the height of the output Mandelbrot in console rows
$Rows = $(if($h=$host.UI.RawUI.WindowSize.Height){$h}else{30}),
# Controls the horizontal offset for the view (a good default would be -0.5)
[Parameter(ParameterSetName="ManualLocation")]
[double]$HorizontalViewOffset = $MandelbrotFavoriteViews[$FavoriteView].HorizontalViewOffset,
# Controls the vertical offset for the view (a good default would be 0)
[Parameter(ParameterSetName="ManualLocation")]
[double]$VerticalViewOffset = $MandelbrotFavoriteViews[$FavoriteView].VerticalViewOffset,
# Controls the zoom distance for the view (a good default would be 6.75)
# Lower values zoom in closer
[Parameter(ParameterSetName="ManualLocation")]
$ZoomViewDistance = $MandelbrotFavoriteViews[$FavoriteView].ZoomViewDistance,
# Limit the color display (a good default would be 255)
[Parameter(ParameterSetName="ManualLocation")]
$ColorViewLimit = $MandelbrotFavoriteViews[$FavoriteView].ColorViewLimit,
# If set, inverts the colors of the mandelbrot
[Switch]$invert
)
begin {
$ErrorActionPreference = "Stop"
$InterestingCoordinates = @()
$script:color_range = $Null
$mandelbrot = [Mandelbrot]::new()
$Parameters = . Get-ParameterValues
$null = $Parameters.Remove("InputObject")
$null = $Parameters.Remove("Invert")
Write-Verbose (($Parameters | Out-String -stream) -join "`n")
# Copy over whatever we can
foreach($key in (Get-Member -InputObject $mandelbrot -Name @($Parameters.Keys)).ForEach("Name")) {
$mandelbrot.$key = $Parameters[$key]
}
$null = $mandelbrot.Init()
}
process {
$x = 0
$c = 0
while($mandelbrot.MoveNext()) {
$x, $y, $c = $mandelbrot.Current
if(Write-Pixel $c -ColorViewLimit:$ColorViewLimit -Invert:$invert) {
$InterestingCoordinates += [PSCustomObject]@{x=@($x, $mandelbrot.x_range[$x]); y=@($y, $mandelbrot.y_range[$y])}
}
if($x -eq $Columns - 1) {
$x = 0
Write-Host
}
}
}
end {
if(($DebugPreference -gt "SilentlyContinue") -and $InterestingCoordinates) {
Write-Debug "Interesting coordinates: $(($InterestingCoordinates | Out-String -Stream) -join "`n")"
$InterestingCoordinates = @()
}
$Script:LastFavorite = ($FavoriteView + 1) % $MandelbrotFavoriteViews.Count
$fav = $MandelbrotFavoriteViews[$Script:LastFavorite]
$Mandelbrot.HorizontalViewOffset = $fav.HorizontalViewOffset
$Mandelbrot.VerticalViewOffset = $fav.VerticalViewOffset
$Mandelbrot.ZoomViewDistance = $fav.ZoomViewDistance
$ColorViewLimit = $fav.ColorViewLimit
if($DebugPreference -gt "SilentlyContinue") {
# Only used for debugging new locations:
Write-Debug "color range: $color_range"
}
$color_range = $null
return
}
}
Export-ModuleMember New-Mandelbrot -Variable MandelbrotFavoriteViews
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment