diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aaa9ad..36acc59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +## Turtle 0.2: + +### Turtles All The Way Down + +A turtle can now contain `.Turtles` +Which can contain `.Turtles` +Which can contain `.Turtles` +Which can contain `.Turtles`... + +* Turtles all the way down (#206) + * `Turtle.get/set_Turtles` (#207) + * `Turtle.get_SVG` supports children (#209) + * `Turtle.get_Canvas` rasterization improvement (#210) + * `Turtle.Towards()` multiple targets (#211) + * `Turtle.Distance()` multiple targets (#212) +* `Turtle.Morph` supports stepwise animation (#215) +* Small fixes + * `Turtle.Step()` uses Add (#213) + * `Turtle.set_Steps` initialization fix (#214) + * `Turtle.set_Duration` anytime (#216) + * `Turtle.get_SVG` empty viewbox support (#218) + * `Turtle.get/set_SVGAttribute` (#219) + * `Turtle.get/set_SVGAnimation` (#220) + * `Turtle.get/set_PathTransform` (#217) + * `Turtle.Forward()` removing rounding (#221) + +--- + ## Turtle 0.1.10: * Updated Methods diff --git a/Commands/Get-Turtle.ps1 b/Commands/Get-Turtle.ps1 index 0a8bb61..9accf6b 100644 --- a/Commands/Get-Turtle.ps1 +++ b/Commands/Get-Turtle.ps1 @@ -333,7 +333,23 @@ function Get-Turtle { .EXAMPLE turtle spirolateral 23 144 8 .EXAMPLE - turtle spirolateral 23 72 8 + turtle spirolateral 23 72 8 + .EXAMPLE + # Lets get practical. Turtle can easily make a bar graph. + turtle BarGraph 200 300 (1..10) + .EXAMPLE + # Want a vertical bar graph? Rotate first. + turtle rotate 90 BarGraph 200 300 (1..10) + .EXAMPLE + # Let's provide more random points: + turtle rotate 90 BarGraph 200 300 (1..20 | Get-Random -Count 20) + .EXAMPLE + # We can draw pretty pictures by connecting and rotating graphs + turtle @( + 'BarGraph', 200, 300, (1..10), + 'BarGraph', 200, 300, (10..1), + 'rotate',180 * 2 + ) .EXAMPLE # Turtle can draw a number of fractals turtle BoxFractal 42 4 @@ -370,6 +386,9 @@ function Get-Turtle { .EXAMPLE # We can use a Moore Curve to fill a space with a bit more density. turtle MooreCurve 42 4 + .EXAMPLE + # We can rotate and repeat moore curves, giving us even Moore. + turtle @('MooreCurve', 42, 4, 'Rotate', 90 * 4) .EXAMPLE # We can show a binary tree turtle BinaryTree 42 4 @@ -382,6 +401,9 @@ function Get-Turtle { .EXAMPLE # The SierpinskiTriangle is a Fractal classic turtle SierpinskiTriangle 42 4 + .EXAMPLE + # We can morph a SierpinskiTriangle to show it step by step + turtle SierpinskiTriangle 42 4 morph .EXAMPLE # Let's draw two reflected Sierpinski Triangles turtle @( diff --git a/Examples/BoxFractal.png b/Examples/BoxFractal.png index c35cc3c..00d7bf8 100644 Binary files a/Examples/BoxFractal.png and b/Examples/BoxFractal.png differ diff --git a/Examples/BoxFractal.svg b/Examples/BoxFractal.svg index a3771ef..f8299f2 100644 --- a/Examples/BoxFractal.svg +++ b/Examples/BoxFractal.svg @@ -1,7 +1,9 @@ - - + + + + \ No newline at end of file diff --git a/Examples/EndlessBoxFractal.svg b/Examples/EndlessBoxFractal.svg index 7134fff..8c9385f 100644 --- a/Examples/EndlessBoxFractal.svg +++ b/Examples/EndlessBoxFractal.svg @@ -6,7 +6,7 @@ - + diff --git a/Examples/EndlessHilbert.svg b/Examples/EndlessHilbert.svg index 6243f0d..46b0869 100644 --- a/Examples/EndlessHilbert.svg +++ b/Examples/EndlessHilbert.svg @@ -6,7 +6,7 @@ - + diff --git a/Examples/EndlessScissorPoly.svg b/Examples/EndlessScissorPoly.svg index e2cfff0..13d9c58 100644 --- a/Examples/EndlessScissorPoly.svg +++ b/Examples/EndlessScissorPoly.svg @@ -6,7 +6,7 @@ - + diff --git a/Examples/EndlessSierpinskiTrianglePattern.svg b/Examples/EndlessSierpinskiTrianglePattern.svg index cca5d4d..c7f09fa 100644 --- a/Examples/EndlessSierpinskiTrianglePattern.svg +++ b/Examples/EndlessSierpinskiTrianglePattern.svg @@ -6,7 +6,7 @@ - + diff --git a/Examples/EndlessSnowflake.svg b/Examples/EndlessSnowflake.svg index 8d48d65..c03eb0a 100644 --- a/Examples/EndlessSnowflake.svg +++ b/Examples/EndlessSnowflake.svg @@ -1,12 +1,12 @@ - + - + diff --git a/Examples/EndlessSpirolateral.svg b/Examples/EndlessSpirolateral.svg index 491d75c..9a2c580 100644 --- a/Examples/EndlessSpirolateral.svg +++ b/Examples/EndlessSpirolateral.svg @@ -6,7 +6,7 @@ - + diff --git a/Examples/EndlessStepSpiral.svg b/Examples/EndlessStepSpiral.svg index 2a30361..08ff440 100644 --- a/Examples/EndlessStepSpiral.svg +++ b/Examples/EndlessStepSpiral.svg @@ -1,10 +1,10 @@ - + - + diff --git a/Examples/FollowThatTurtle.svg b/Examples/FollowThatTurtle.svg new file mode 100644 index 0000000..de654bc --- /dev/null +++ b/Examples/FollowThatTurtle.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Examples/FollowThatTurtle.turtle.ps1 b/Examples/FollowThatTurtle.turtle.ps1 new file mode 100644 index 0000000..bf32883 --- /dev/null +++ b/Examples/FollowThatTurtle.turtle.ps1 @@ -0,0 +1,62 @@ +<# +.SYNOPSIS + Follow that Turtle! +.DESCRIPTION + Basic behavior modelling with Turtle. + + A series of turtles will follow the next turtle. +#> +param( +# The size of the square +[double] +$Size = 200, + +# The speed of each turtle +[double] +$Speed = 1, + +# The number of steps +[int] +$StepCount +) + +# If no steps were provided +if (-not $StepCount) { + # double the size and divide by speed + $StepCount = ($size * 2)/$speed +} + +# Set up our turtles. +$followThatTurtle = turtle stroke '#4488ff' square $Size turtles ([Ordered]@{ + t1 = turtle teleport 0 0 stroke '#4488ff' + t2 = turtle teleport $Size 0 stroke '#4488ff' + t3 = turtle teleport $Size $Size stroke '#4488ff' + t4 = turtle teleport 0 $Size stroke '#4488ff' +}) + +# For each step +foreach ($n in 1..([Math]::Abs($StepCount))) { + # Go to each turtle + for ($turtleNumber = 0; $turtleNumber -lt $followThatTurtle.Turtles.Count; $turtleNumber++) { + $thisTurtle = $followThatTurtle.Turtles[$turtleNumber] + # and find the next turtle + $nextTurtle = if ($turtleNumber -eq $followThatTurtle.Turtles.Count - 1) { + $followThatTurtle.Turtles[0] + } else { + $followThatTurtle.Turtles[$turtleNumber + 1] + } + # If we are more than 1 unit away + if ($thisTurtle.Distance($nextTurtle) -ge 1) { + # rotate towards it + $null = $thisTurtle.Rotate( + $thisTurtle.Towards($nextTurtle) + ).Forward($Speed) # and move forward. + } + } +} + + +$followThatTurtle | turtle save ./FollowThatTurtle.svg +$followThatTurtle.Stroke = 'transparent' +$followThatTurtle | Save-Turtle ./FollowThatTurtlePattern.svg Pattern + diff --git a/Examples/FollowThatTurtleHideAndSeek.svg b/Examples/FollowThatTurtleHideAndSeek.svg new file mode 100644 index 0000000..03891e7 --- /dev/null +++ b/Examples/FollowThatTurtleHideAndSeek.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Examples/FollowThatTurtleHideAndSeek.turtle.ps1 b/Examples/FollowThatTurtleHideAndSeek.turtle.ps1 new file mode 100644 index 0000000..f94892a --- /dev/null +++ b/Examples/FollowThatTurtleHideAndSeek.turtle.ps1 @@ -0,0 +1,88 @@ +<# +.SYNOPSIS + Hide and Seek +.DESCRIPTION + Simple behavior modelling with Turtle. +.NOTES + Imagine we have eight turtles playing hide and seek + + Four turtles are seeking. + + Four turtles are hiding. + + Each hiding turtle starts in the center. + + Each seeking turtle will chase a hiding turtle. + + Each hiding turtle will run away at an angle (by default 90 degrees). +#> +param( +[double] +$SquareSize = 200, +[double] +$HiderSpeed = 2, +[double] +$SeekerSpeedRatio = ((1 + [Math]::Sqrt(5))/2), +[double] +$EvadeAngle = 90 +) + +if ($PSScriptRoot) { Push-Location $PSScriptRoot} + +$midpoint = ($squareSize/2), ($squareSize/2) +$seekerSpeed = $HiderSpeed * $SeekerSpeedRatio # (1 + (Get-Random -Min 10 -Max 50)/50) # (Get-Random -Min 1 -Max 5) +$stepCount = $squareSize/2 * (1 + ([Math]::Abs($attackerSpeed - $evaderSpeed))) + +$hideAndSeek = turtle square $squareSize stroke '#4488ff' turtles ([Ordered]@{ + s1 = turtle teleport 0 0 stroke '#4488ff' # stroke 'red' pathclass 'red-stroke' fill red + s2 = turtle teleport $squareSize 0 stroke '#4488ff' # stroke 'yellow' pathclass 'yellow-stroke' fill yellow + s3 = turtle teleport $squareSize $squareSize stroke '#4488ff' # stroke 'green' pathclass 'green-stroke' fill green + s4 = turtle teleport 0 $squareSize stroke '#4488ff' # stroke 'blue' PathClass 'blue-stroke' fill blue + h1 = turtle teleport $midpoint stroke '#4488ff' # stroke 'red' fill 'red' + h2 = turtle teleport $midpoint stroke '#4488ff' # stroke 'yellow' fill 'yellow' + h3 = turtle teleport $midpoint stroke '#4488ff' # stroke 'green' fill 'green' + h4 = turtle teleport $midpoint stroke '#4488ff' # stroke 'blue' fill 'blue' +}) + + + +# Since all attackers and evaders start with equal distances, +# when we have caught one we have caught them all. +:caughtEm foreach ($n in 1..$stepCount) { + + # Get the seeker turtles + $seekers = $hideAndSeek.Turtles[@($hideAndSeek.Turtles.Keys -match '^s')] + # Get the hiding turtles + $hiders = $hideAndSeek.Turtles[@($hideAndSeek.Turtles.Keys -match '^h')] + + for ($hiderNumber = 0; $hiderNumber -lt $hiders.Length; $hiderNumber++) { + $thisTurtle = $hiders[$hiderNumber] + $runningAwayFrom = $seekers[$hiderNumber % $seekers.Length] + $null = $thisTurtle.Rotate( + $thisTurtle.Towards($runningAwayFrom) + $evadeAngle # (Get-Random -Minimum 80 -Maximum 100) + ).Forward($HiderSpeed) + } + + for ($seekerNumber = 0; $seekerNumber -lt $seekers.Length; $seekerNumber++) { + $thisTurtle = $seekers[$seekerNumber] + $runningTowards = $hiders[$seekerNumber % $hiders.Length] + $null = $thisTurtle.Rotate( + $thisTurtle.Towards($runningTowards) # + (Get-Random -Minimum -10 -Maximum 10) + ).Forward($seekerSpeed) + } + + for ($seekerNumber = 0; $seekerNumber -lt $seekers.Length; $seekerNumber++) { + $thisTurtle = $seekers[$seekerNumber] + $runningTowards = $hiders[$seekerNumber % $hiders.Length] + if ($thisTurtle.Distance($runningTowards) -le 1) { + break caughtEm + } + } +} + + +$hideAndSeek | turtle save ./FollowThatTurtleHideAndSeek.svg +$hideAndSeek.Stroke = 'transparent' +$hideAndSeek | Save-Turtle ./FollowThatTurtleHideAndSeekPattern.svg Pattern + +if ($PSScriptRoot) { Pop-Location} \ No newline at end of file diff --git a/Examples/FollowThatTurtleHideAndSeekPattern.svg b/Examples/FollowThatTurtleHideAndSeekPattern.svg new file mode 100644 index 0000000..12c820c --- /dev/null +++ b/Examples/FollowThatTurtleHideAndSeekPattern.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Examples/FollowThatTurtleNotTooClose.svg b/Examples/FollowThatTurtleNotTooClose.svg new file mode 100644 index 0000000..9fa55f5 --- /dev/null +++ b/Examples/FollowThatTurtleNotTooClose.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Examples/FollowThatTurtleNotTooClose.turtle.ps1 b/Examples/FollowThatTurtleNotTooClose.turtle.ps1 new file mode 100644 index 0000000..54b6065 --- /dev/null +++ b/Examples/FollowThatTurtleNotTooClose.turtle.ps1 @@ -0,0 +1,78 @@ +<# +.SYNOPSIS + Follow that Turtle! (but not too close) +.DESCRIPTION + Basic behavior modelling with Turtle. + + A series of turtles will follow the next turtle, until they get within a proximity. + + Then, the same turtles will avoid the turtle, until they are outside of the proximity. +#> +param( +# The size of the square +[double] +$Size = 200, + +# The speed of each turtle +[double] +$Speed = 1, + +# The number of steps +[int] +$StepCount, + +[double] +$Proximity +) + +# If no steps were provided +if (-not $StepCount) { + # double the size and divide by speed + $StepCount = ($size * 2)/$speed +} + +if (-not $Proximity) { + $Proximity = $size/2 +} + +# Set up our turtles. +$followThatTurtle = turtle square $Size stroke '#4488ff' turtles ([Ordered]@{ + t1 = turtle teleport 0 0 stroke '#4488ff' + t2 = turtle teleport $Size 0 stroke '#4488ff' + t3 = turtle teleport $Size $Size stroke '#4488ff' + t4 = turtle teleport 0 $Size stroke '#4488ff' +}) + +# For each step +foreach ($n in 1..([Math]::Abs($StepCount))) { + # Go to each turtle + for ($turtleNumber = 0; $turtleNumber -lt $followThatTurtle.Turtles.Count; $turtleNumber++) { + $thisTurtle = $followThatTurtle.Turtles[$turtleNumber] + # and find the next turtle + $nextTurtle = if ($turtleNumber -eq $followThatTurtle.Turtles.Count - 1) { + $followThatTurtle.Turtles[0] + } else { + $followThatTurtle.Turtles[$turtleNumber + 1] + } + # If we are more than the proximity away + if ($thisTurtle.Distance($nextTurtle) -ge $Proximity) { + # rotate towards it + $null = $thisTurtle.Rotate( + $thisTurtle.Towards($nextTurtle) + ).Forward($Speed) # and move forward. + } + # If we within the proxmity + else { + # rotate away + $null = $thisTurtle.Rotate( + $thisTurtle.Towards($nextTurtle) + 90 + ).Forward($Speed) # and move forward. + } + } +} + + +$followThatTurtle | turtle save ./FollowThatTurtleNotTooClose.svg +$followThatTurtle.Stroke = 'transparent' +$followThatTurtle | Save-Turtle ./FollowThatTurtleNotTooClosePattern.svg Pattern + diff --git a/Examples/FollowThatTurtleNotTooClosePattern.svg b/Examples/FollowThatTurtleNotTooClosePattern.svg new file mode 100644 index 0000000..8f03135 --- /dev/null +++ b/Examples/FollowThatTurtleNotTooClosePattern.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Examples/FollowThatTurtlePattern.svg b/Examples/FollowThatTurtlePattern.svg new file mode 100644 index 0000000..0871a03 --- /dev/null +++ b/Examples/FollowThatTurtlePattern.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Examples/InscribedCircle.svg b/Examples/InscribedCircle.svg new file mode 100644 index 0000000..e6088b8 --- /dev/null +++ b/Examples/InscribedCircle.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Examples/InscribedCircle.turtle.ps1 b/Examples/InscribedCircle.turtle.ps1 new file mode 100644 index 0000000..023a829 --- /dev/null +++ b/Examples/InscribedCircle.turtle.ps1 @@ -0,0 +1,14 @@ +<# +.SYNOPSIS + An inscribed circle +.DESCRIPTION + A simple example of turtles containing turtles +#> +$inscribedCircle = + turtle width 42 height 42 turtles ([Ordered]@{ + 'square' = turtle square 42 fill '#4488ff' stroke '#224488' + 'circle' = turtle circle 21 fill '#224488' stroke '#4488ff' + }) + +$inscribedCircle | Save-Turtle ./InscribedCircle.svg +$inscribedCircle | Save-Turtle ./InscribedCirclePattern.svg Pattern diff --git a/Examples/InscribedCirclePattern.svg b/Examples/InscribedCirclePattern.svg new file mode 100644 index 0000000..986095e --- /dev/null +++ b/Examples/InscribedCirclePattern.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Examples/SierpinskiTriangle.png b/Examples/SierpinskiTriangle.png index 5201700..98169fa 100644 Binary files a/Examples/SierpinskiTriangle.png and b/Examples/SierpinskiTriangle.png differ diff --git a/Examples/SierpinskiTriangle.svg b/Examples/SierpinskiTriangle.svg index 4000b31..0132c8f 100644 --- a/Examples/SierpinskiTriangle.svg +++ b/Examples/SierpinskiTriangle.svg @@ -1,7 +1,9 @@ - - + + + + \ No newline at end of file diff --git a/Examples/TurtlesOnATextPath-ATurtleCircle.svg b/Examples/TurtlesOnATextPath-ATurtleCircle.svg index 13991b8..3f02c65 100644 --- a/Examples/TurtlesOnATextPath-ATurtleCircle.svg +++ b/Examples/TurtlesOnATextPath-ATurtleCircle.svg @@ -1,10 +1,12 @@ - - - - a turtle circle - + + + + + a turtle circle + + \ No newline at end of file diff --git a/Examples/TurtlesOnATextPath-Morph.svg b/Examples/TurtlesOnATextPath-Morph.svg index b3887d5..215deb5 100644 --- a/Examples/TurtlesOnATextPath-Morph.svg +++ b/Examples/TurtlesOnATextPath-Morph.svg @@ -1,15 +1,17 @@ - - - - - turtles on a text path - - - - - + + + + + + turtles on a text path + + + + + + \ No newline at end of file diff --git a/Examples/TurtlesOnATextPath.svg b/Examples/TurtlesOnATextPath.svg index 819e67f..58553aa 100644 --- a/Examples/TurtlesOnATextPath.svg +++ b/Examples/TurtlesOnATextPath.svg @@ -1,10 +1,12 @@ - - - - turtles on a text path - + + + + + turtles on a text path + + \ No newline at end of file diff --git a/README.md b/README.md index 4bbd539..dd4fd14 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,6 @@ - - ## Turtles in a PowerShell [Turtle Graphics](https://en.wikipedia.org/wiki/Turtle_graphics) are a great way to learn programming and describe shapes. @@ -90,6 +88,9 @@ The turtle is represented as an object, and any number of commands can make or m Last but not least: Get-Turtle lets you run multiple steps of turtle, and is aliased to urtle. +Get-Turtle is the command we will use most often, and we will almost always just call it by the alias urtle. + +If you want to get a sense of all that Turtle can do, check out the [Get-Turtle examples](https://psturtle.com/Commands/Get-Turtle) ### Drawing Squares @@ -102,15 +103,10 @@ Let's start simple, by drawing a square with a series of commands. ~~~PowerShell -New-Turtle | - Move-Turtle Forward 10 | - Move-Turtle Rotate 90 | - Move-Turtle Forward 10 | - Move-Turtle Rotate 90 | - Move-Turtle Forward 10 | - Move-Turtle Rotate 90 | - Move-Turtle Forward 10 | - Move-Turtle Rotate 90 | +turtle Forward 10 Rotate 90 | + turtle Forward 10 Rotate 90 | + turtle Forward 10 Rotate 90 | + turtle Forward 10 Rotate 90 | Save-Turtle "./Square.svg" ~~~ @@ -198,7 +194,7 @@ Because this Turtle generates SVG, we can also use it to create patterns. ### Drawing Fractals -Turtle is often used to draw fractals. +Turtle is can be used to draw fractals. Many fractals can be described in something called a [L-System](https://en.wikipedia.org/wiki/L-system) (short for Lindenmayer system) @@ -264,7 +260,7 @@ For example, here is an example of a pattern comprised of Koch Snowflakes: ~~~
-Snowflake Pattern +Snowflake Pattern
We can also animate the pattern, for endless variety: @@ -288,7 +284,219 @@ $turtle | save-turtle -Path ./EndlessSnowflake.svg -Property Pattern Pop-Location ~~~
-Endless Snowflake Pattern +Endless Snowflake Pattern +
+### Turtles all the way down + +A turtle can contain turtles, which can contain turtles, which can contain turtles ... + +We call this 'Turtles All The Way Down', and it lets us do two very important sets of things: + +* It allows turtles to interact +* It allows us to model the behavior of multiple turtles + +Let's start with a few cool examples. + +At the most basic, let's make an inscribed circle and square: + +~~~PowerShell +.SYNOPSIS + An inscribed circle +.DESCRIPTION + A simple example of turtles containing turtles +#> +$inscribedCircle = + turtle width 42 height 42 turtles @{ + 'square' = turtle square 42 fill '#4488ff' stroke '#224488' + 'circle' = turtle circle 21 fill '#224488' stroke '#4488ff' + } + +$inscribedCircle | Save-Turtle ./InscribedCircle.svg +$inscribedCircle | Save-Turtle ./InscribedCirclePattern.svg Pattern +~~~ + +
+Inscribed Circle +
+Let's see it as a pattern: +
+Inscribed Circle Pattern +
+#### Behavior Modelling + +Imagine we are four turtles in a square, each trying to catch up with the next turtle. + +[What kind of shape do you think our paths will draw?](Get-Content ./Examples/FollowThatTurtle.turtle.ps1) + +~~~PowerShell +.SYNOPSIS + Follow that Turtle! +.DESCRIPTION + Basic behavior modelling with Turtle. + + A series of turtles will follow the next turtle. +#> +param( +# The size of the square +[double] +$Size = 200, + +# The speed of each turtle +[double] +$Speed = 1, + +# The number of steps +[int] +$StepCount +) + +# If no steps were provided +if (-not $StepCount) { + # double the size and divide by speed + $StepCount = ($size * 2)/$speed +} + +# Set up our turtles. +$followThatTurtle = turtle stroke '#4488ff' square $Size turtles ([Ordered]@{ + t1 = turtle teleport 0 0 stroke '#4488ff' + t2 = turtle teleport $Size 0 stroke '#4488ff' + t3 = turtle teleport $Size $Size stroke '#4488ff' + t4 = turtle teleport 0 $Size stroke '#4488ff' +}) + +# For each step +foreach ($n in 1..([Math]::Abs($StepCount))) { + # Go to each turtle + for ($turtleNumber = 0; $turtleNumber -lt $followThatTurtle.Turtles.Count; $turtleNumber++) { + $thisTurtle = $followThatTurtle.Turtles[$turtleNumber] + # and find the next turtle + $nextTurtle = if ($turtleNumber -eq $followThatTurtle.Turtles.Count - 1) { + $followThatTurtle.Turtles[0] + } else { + $followThatTurtle.Turtles[$turtleNumber + 1] + } + # If we are more than 1 unit away + if ($thisTurtle.Distance($nextTurtle) -ge 1) { + # rotate towards it + $null = $thisTurtle.Rotate( + $thisTurtle.Towards($nextTurtle) + ).Forward($Speed) # and move forward. + } + } +} + + +$followThatTurtle | turtle save ./FollowThatTurtle.svg +$followThatTurtle.Stroke = 'transparent' +$followThatTurtle | Save-Turtle ./FollowThatTurtlePattern.svg Pattern + +~~~ +
+Follow That Turtle +
+Let's see it as a pattern: +
+Follow That Turtle +
+ +Now let's imagine we have four turtles in the center, and they're trying to get away from the turtles in the corners. + +[What kind of shape will this produce?](./Examples/FollowThatTurtleHideAndSeek.turtle.ps1) +~~~PowerShell +.SYNOPSIS + Hide and Seek +.DESCRIPTION + Simple behavior modelling with Turtle. +.NOTES + Imagine we have eight turtles playing hide and seek + + Four turtles are seeking. + + Four turtles are hiding. + + Each hiding turtle starts in the center. + + Each seeking turtle will chase a hiding turtle. + + Each hiding turtle will run away at an angle (by default 90 degrees). +#> +param( +[double] +$SquareSize = 200, +[double] +$HiderSpeed = 2, +[double] +$SeekerSpeedRatio = ((1 + [Math]::Sqrt(5))/2), +[double] +$EvadeAngle = 90 +) + +if ($PSScriptRoot) { Push-Location $PSScriptRoot} + +$midpoint = ($squareSize/2), ($squareSize/2) +$seekerSpeed = $HiderSpeed * $SeekerSpeedRatio # (1 + (Get-Random -Min 10 -Max 50)/50) # (Get-Random -Min 1 -Max 5) +$stepCount = $squareSize/2 * (1 + ([Math]::Abs($attackerSpeed - $evaderSpeed))) + +$hideAndSeek = turtle square $squareSize stroke '#4488ff' turtles ([Ordered]@{ + s1 = turtle teleport 0 0 stroke '#4488ff' # stroke 'red' pathclass 'red-stroke' fill red + s2 = turtle teleport $squareSize 0 stroke '#4488ff' # stroke 'yellow' pathclass 'yellow-stroke' fill yellow + s3 = turtle teleport $squareSize $squareSize stroke '#4488ff' # stroke 'green' pathclass 'green-stroke' fill green + s4 = turtle teleport 0 $squareSize stroke '#4488ff' # stroke 'blue' PathClass 'blue-stroke' fill blue + h1 = turtle teleport $midpoint stroke '#4488ff' # stroke 'red' fill 'red' + h2 = turtle teleport $midpoint stroke '#4488ff' # stroke 'yellow' fill 'yellow' + h3 = turtle teleport $midpoint stroke '#4488ff' # stroke 'green' fill 'green' + h4 = turtle teleport $midpoint stroke '#4488ff' # stroke 'blue' fill 'blue' +}) + + + +# Since all attackers and evaders start with equal distances, +# when we have caught one we have caught them all. +:caughtEm foreach ($n in 1..$stepCount) { + + # Get the seeker turtles + $seekers = $hideAndSeek.Turtles[@($hideAndSeek.Turtles.Keys -match '^s')] + # Get the hiding turtles + $hiders = $hideAndSeek.Turtles[@($hideAndSeek.Turtles.Keys -match '^h')] + + for ($hiderNumber = 0; $hiderNumber -lt $hiders.Length; $hiderNumber++) { + $thisTurtle = $hiders[$hiderNumber] + $runningAwayFrom = $seekers[$hiderNumber % $seekers.Length] + $null = $thisTurtle.Rotate( + $thisTurtle.Towards($runningAwayFrom) + $evadeAngle # (Get-Random -Minimum 80 -Maximum 100) + ).Forward($HiderSpeed) + } + + for ($seekerNumber = 0; $seekerNumber -lt $seekers.Length; $seekerNumber++) { + $thisTurtle = $seekers[$seekerNumber] + $runningTowards = $hiders[$seekerNumber % $hiders.Length] + $null = $thisTurtle.Rotate( + $thisTurtle.Towards($runningTowards) # + (Get-Random -Minimum -10 -Maximum 10) + ).Forward($seekerSpeed) + } + + for ($seekerNumber = 0; $seekerNumber -lt $seekers.Length; $seekerNumber++) { + $thisTurtle = $seekers[$seekerNumber] + $runningTowards = $hiders[$seekerNumber % $hiders.Length] + if ($thisTurtle.Distance($runningTowards) -le 1) { + break caughtEm + } + } +} + + +$hideAndSeek | turtle save ./FollowThatTurtleHideAndSeek.svg +$hideAndSeek.Stroke = 'transparent' +$hideAndSeek | Save-Turtle ./FollowThatTurtleHideAndSeekPattern.svg Pattern + +if ($PSScriptRoot) { Pop-Location} +~~~ +
+Follow That Turtle Hide And Seek +
+Let's see it as a pattern: +
+Follow That Turtle Hide And Seek Pattern
### Turtles in HTML @@ -307,11 +515,16 @@ turtle SierpinskiTriangle | Anything we do with our turtle should work within a webpage. +To include a Turtle in a page, we can simply stringify it: + +~~~PowerShell +"$(turtle SierpinskiTriangle)" +~~~ + There are a few properties of the turtle that may be helpful: * .Canvas returns the turtle rendered in an HTML canvas * .OffsetPath returns the turtle as an offset path -* .ClipPath returns the turtle as a clip path ### Turtles in Raster diff --git a/README.md.ps1 b/README.md.ps1 index 86c252a..9dcb206 100644 --- a/README.md.ps1 +++ b/README.md.ps1 @@ -7,11 +7,9 @@ #requires -Module Turtle param() -#region Introduction - -@" -# Turtle +$imageHeader = @( +@'
SierpinskiTriangle
@@ -19,8 +17,15 @@ param()
+'@ +) + +#region Introduction +@" +# Turtle +$imageHeader ## Turtles in a PowerShell @@ -123,6 +128,9 @@ The turtle is represented as an object, and any number of commands can make or m Last but not least: `Get-Turtle` lets you run multiple steps of turtle, and is aliased to `turtle`. +Get-Turtle is the command we will use most often, and we will almost always just call it by the alias `turtle`. + +If you want to get a sense of all that Turtle can do, check out the [Get-Turtle examples](https://psturtle.com/Commands/Get-Turtle) "@ @" @@ -143,15 +151,10 @@ Let's start simple, by drawing a square with a series of commands. ~~~PowerShell $( $drawSquare1 = { -New-Turtle | - Move-Turtle Forward 10 | - Move-Turtle Rotate 90 | - Move-Turtle Forward 10 | - Move-Turtle Rotate 90 | - Move-Turtle Forward 10 | - Move-Turtle Rotate 90 | - Move-Turtle Forward 10 | - Move-Turtle Rotate 90 | +turtle Forward 10 Rotate 90 | + turtle Forward 10 Rotate 90 | + turtle Forward 10 Rotate 90 | + turtle Forward 10 Rotate 90 | Save-Turtle "./Square.svg" } $drawSquare1 @@ -296,7 +299,7 @@ $box3 = { ### Drawing Fractals -Turtle is often used to draw fractals. +Turtle is can be used to draw fractals. Many fractals can be described in something called a [L-System](https://en.wikipedia.org/wiki/L-system) (short for Lindenmayer system) @@ -371,7 +374,7 @@ $SnowFlakePattern = . $MakeSnowflakePattern @"
-Snowflake Pattern +Snowflake Pattern
"@ @@ -388,12 +391,115 @@ $( @"
-Endless Snowflake Pattern +Endless Snowflake Pattern
"@ #endregion LSystems +#region Turtles All The Way Down +@" +### Turtles all the way down + +A turtle can contain turtles, which can contain turtles, which can contain turtles ... + +We call this 'Turtles All The Way Down', and it lets us do two very important sets of things: + +* It allows turtles to interact +* It allows us to model the behavior of multiple turtles + +Let's start with a few cool examples. + +At the most basic, let's make an inscribed circle and square: + +~~~PowerShell +$( + @(Get-Content ./Examples/InscribedCircle.turtle.ps1 | + Select-Object -Skip 1) -join [Environment]::NewLine +) +~~~ + +"@ + +@" +
+Inscribed Circle +
+"@ + +@" +Let's see it as a pattern: +"@ + +@" +
+Inscribed Circle Pattern +
+"@ + + +@" +#### Behavior Modelling + +Imagine we are four turtles in a square, each trying to catch up with the next turtle. + +[What kind of shape do you think our paths will draw?](Get-Content ./Examples/FollowThatTurtle.turtle.ps1) + +~~~PowerShell +$( + @(Get-Content ./Examples/FollowThatTurtle.turtle.ps1 | + Select-Object -Skip 1) -join [Environment]::NewLine +) +~~~ +"@ + +@" +
+Follow That Turtle +
+"@ + +@" +Let's see it as a pattern: +"@ + +@" +
+Follow That Turtle +
+"@ + +@" + +Now let's imagine we have four turtles in the center, and they're trying to get away from the turtles in the corners. + +[What kind of shape will this produce?](./Examples/FollowThatTurtleHideAndSeek.turtle.ps1) +~~~PowerShell +$( + @(Get-Content ./Examples/FollowThatTurtleHideAndSeek.turtle.ps1 | + Select-Object -Skip 1) -join [Environment]::NewLine +) +~~~ +"@ + + +@" +
+Follow That Turtle Hide And Seek +
+"@ + +@" +Let's see it as a pattern: +"@ + +@" +
+Follow That Turtle Hide And Seek Pattern +
+"@ +#endregion Turtles All The Way Down + #region Turtles in HTML @" @@ -413,11 +519,16 @@ turtle SierpinskiTriangle | Anything we do with our turtle should work within a webpage. +To include a Turtle in a page, we can simply stringify it: + +~~~PowerShell +"`$(turtle SierpinskiTriangle)" +~~~ + There are a few properties of the turtle that may be helpful: * `.Canvas` returns the turtle rendered in an HTML canvas * `.OffsetPath` returns the turtle as an offset path -* `.ClipPath` returns the turtle as a clip path "@ #endregion Turtles in HTML diff --git a/Turtle.psd1 b/Turtle.psd1 index 903f643..eb328e7 100644 --- a/Turtle.psd1 +++ b/Turtle.psd1 @@ -1,6 +1,6 @@ @{ # Version number of this module. - ModuleVersion = '0.1.10' + ModuleVersion = '0.2.0' # Description of the module Description = "Turtles in a PowerShell" # Script module or binary module file associated with this manifest. @@ -37,24 +37,46 @@ # A URL to the license for this module. LicenseURI = 'https://github.com/PowerShellWeb/Turtle/blob/main/LICENSE' ReleaseNotes = @' -## Turtle 0.1.10: +## Turtle 0.2: + +### Turtles All The Way Down + +A turtle can now contain `.Turtles` +Which can contain `.Turtles` +Which can contain `.Turtles` +Which can contain `.Turtles`... + +* Turtles all the way down (#206) + * `Turtle.get/set_Turtles` (#207) + * `Turtle.get_SVG` supports children (#209) + * `Turtle.get_Canvas` rasterization improvement (#210) + * `Turtle.Towards()` multiple targets (#211) + * `Turtle.Distance()` multiple targets (#212) +* `Turtle.Morph` supports stepwise animation (#215) +* Small fixes + * `Turtle.Step()` uses Add (#213) + * `Turtle.set_Steps` initialization fix (#214) + * `Turtle.set_Duration` anytime (#216) + * `Turtle.get_SVG` empty viewbox support (#218) + * `Turtle.get/set_SVGAttribute` (#219) + * `Turtle.get/set_SVGAnimation` (#220) + * `Turtle.get/set_PathTransform` (#217) + * `Turtle.Forward()` removing rounding (#221) -* Updated Methods - * `Turtle.Star` even point fix (#190) - * `Turtle.Polygon` partial polygon support (#194) -* New Shapes - * `Turtle.Rectangle` (#192) - * `Turtle.StarFlower` (#191) - * `Turtle.GoldenFlower` (#193) - * `Turtle.HatMonotile` (#196) - * `Turtle.TurtleMonotile` (#195) - * `Turtle.BarGraph` (#173) -* Added Demos - * Intro To Turtles (#197) - --- Additional details available in the [CHANGELOG](https://github.com/PowerShellWeb/Turtle/blob/main/CHANGELOG.md) + +Please: + +* [Like](https://github.com/PowerShell/Turtle) +* [Share](https://psturtle.com/) +* Subscribe + * [psturtle.com](https://bsky.app/profile/psturtle.com) + * [mrpowershell.com](https://bsky.app/profile/mrpowershell.com) + * [StartAutomating](https://github.com/StartAutomating) + * [PowerShellWeb](https://github.com/PowerShellWeb) +* Sponsor [StartAutomating](https://github.com/sponsors/StartAutomating) '@ } } diff --git a/Turtle.tests.ps1 b/Turtle.tests.ps1 index 7eabdb5..2607e14 100644 --- a/Turtle.tests.ps1 +++ b/Turtle.tests.ps1 @@ -60,7 +60,7 @@ describe Turtle { [Math]::Round($turtle.Position.Y,10) | Should -be 1 $turtle = $turtle.Rotate($turtle.Towards(2,2)) $turtle = $turtle.Forward($turtle.Distance(2,2)) - $turtle.Heading | Should -be 45 + $turtle.Heading -as [float] | Should -be 45 [Math]::Round($turtle.Position.Y,10) | Should -be 2 [Math]::Round($turtle.Position.Y,10) | Should -be 2 } diff --git a/Turtle.types.ps1xml b/Turtle.types.ps1xml index 7fd0c7b..cd5361c 100644 --- a/Turtle.types.ps1xml +++ b/Turtle.types.ps1xml @@ -486,12 +486,23 @@ return $this Clear @@ -540,16 +551,36 @@ return $this.LSystem('F+F+F+F', [Ordered]@{ .DESCRIPTION Determines the distance from the turtle's current position to a point. #> -param( -# The X-coordinate -[double]$X = 0, -# The Y-coordinate -[double]$Y = 0 -) +param() + +$towards = $args | . { process { $_ } } + +$tx = 0.0 +$ty = 0.0 + +$nCount = 0 +foreach ($toward in $towards) { + if ($toward -is [double] -or $toward -is [float] -or $toward -is [int]) { + if (-not ($nCount % 2)) { + $tx = $toward + } else { + $ty = $toward + } + $nCount++ + } + elseif ($null -ne $toward.X -and $null -ne $toward.Y) { + $tx = $toward.x + $ty = $toward.y + $nCount+= 2 + } +} + +$tx/=($nCount/2) +$ty/=($nCount/2) # Determine the delta from the turtle's current position to the specified point -$deltaX = $X - $this.Position.X -$deltaY = $Y - $this.Position.Y +$deltaX = $tx - $this.Position.X +$deltaY = $ty - $this.Position.Y # Calculate the distance using the Pythagorean theorem return [Math]::Sqrt($deltaX*$deltaX + $deltaY*$deltaY) @@ -661,8 +692,9 @@ param( $Distance = 10 ) -$x = $Distance * ([math]::round([math]::cos($this.Heading * [Math]::PI / 180),15)) -$y = $Distance * ([math]::round([math]::sin($this.Heading * [Math]::PI / 180),15)) +$x = $Distance * ([math]::cos($this.Heading * [Math]::PI / 180)) +$y = $Distance * ([math]::sin($this.Heading * [Math]::PI / 180)) + return $this.Step($x, $y) @@ -1330,7 +1362,11 @@ return $this.LSystem( .DESCRIPTION Morphs a Turtle by animating its path. - Any two paths with the same number of points can be morphed into each other. + Any two paths with the same number of points can be morphed into each other smoothly. + + Any two paths with a different number of points will become a step-by-step animation. + + Since animations can include multiple complex paths, they can get quite large, and be quite beautiful. .EXAMPLE $sierpinskiTriangle = turtle SierpinskiTriangle 42 4 $SierpinskiTriangleFlipped = turtle rotate 180 SierpinskiTriangle 42 4 @@ -1360,6 +1396,12 @@ return $this.LSystem( $flowerPetals3, $flowerPetals ) | Save-Turtle ./flowerPetalMorph.svg Pattern +.EXAMPLE + turtle SierpinskiTriangle 42 4 morph | + Save-Turtle ./SierpinskiTriangleConstruction.svg +.EXAMPLE + turtle stroke '#224488' fill '#4488ff' backgroundColor '#112244' rotate 60 SierpinskiTriangle 42 4 SierpinskiTriangle -42 4 morph | + Save-Turtle ./SierpinskiTriangleReflectionConstructionAndFill.svg #> param( [Parameter(ValueFromRemainingArguments)] @@ -1367,30 +1409,50 @@ $Arguments ) $durationArgument = $null - +$hasPoints = $false +$segmentCount = 0 $newPaths = @(foreach ($arg in $Arguments) { if ($arg -is [string]) { if ($arg -match '^\s{0,}m') { $arg + $hasPoints = $true } } elseif ($arg.PathData) { $arg.PathData + $hasPoints = $true } elseif ($arg.D) { $arg.D + $hasPoints = $true } elseif ($arg -is [TimeSpan]) { $durationArgument = $arg } elseif ($arg -is [double] -or $arg -is [int]) { - $durationArgument = [TimeSpan]::FromSeconds($arg) + if (-not $hasPoints -and $arg -is [int]) { + $segmentCount = [Math]::Abs($arg) + } else { + $durationArgument = [TimeSpan]::FromSeconds($arg) + } } }) if (-not $newPaths) { - return $this - <#$pathSegments = @($this.PathData -split '(?=\p{L})') - $newPaths = @(for ($segmentNumber = 0; $segmentNumber -lt $pathSegments.Count; $segmentNumber++) { - $pathSegments[0..$segmentNumber] -join ' ' - }) -join ';'#> + if ($this.Steps.Count) { + $stepList = @($this.PathData -join ' ' -split '(?=\p{L})' -ne '') + if ($segmentCount) { + $newPaths = @( + for ($n = 1; $n -lt $stepList.Length; $n += ($stepList.Length/$segmentCount)) { + $stepList[0..$n] -join ' ' + } + ) + } else { + $newPaths = @(foreach ($n in 1..($stepList.Length)) { + $stepList[0..$n] -join ' ' + }) + } + + } else { + return $this + } } if ($this.PathAnimation) { @@ -1398,8 +1460,8 @@ if ($this.PathAnimation) { @(foreach ($animationXML in $this.PathAnimation -split '(?<=/>)') { $animationXML = $animationXML -as [xml] if (-not $animationXML) { continue } - if ($animationXML.attributeName -eq 'd') { - $animationXML.values = "$($newPaths -join ';')" + if ($animationXML.animate.attributeName -eq 'd') { + $animationXML.animate.values = "$($newPaths -join ';')" } $animationXML.OuterXml }) @@ -2234,9 +2296,9 @@ param( if ($DeltaX -or $DeltaY) { $this.Position = $DeltaX, $DeltaY if ($This.IsPenDown) { - $this.Steps += " l $DeltaX $DeltaY" + $this.Steps.Add(" l $DeltaX $DeltaY") } else { - $this.Steps += " m $DeltaX $DeltaY" + $this.Steps.Add(" m $DeltaX $DeltaY") } } @@ -2427,16 +2489,36 @@ return "$($this.SVG.OuterXml)" .DESCRIPTION Determines the angle from the turtle's current heading towards a point. #> -param( -# The X-coordinate -[double]$X = 0, -# The Y-coordinate -[double]$Y = 0 -) +param() + +$towards = $args | . { process { $_ } } + +$tx = 0.0 +$ty = 0.0 + +$nCount = 0 +foreach ($toward in $towards) { + if ($toward -is [double] -or $toward -is [float] -or $toward -is [int]) { + if (-not ($nCount % 2)) { + $tx = $toward + } else { + $ty = $toward + } + $nCount++ + } + elseif ($null -ne $toward.X -and $null -ne $toward.Y) { + $tx = $toward.x + $ty = $toward.y + $nCount+= 2 + } +} + +$tx/=($nCount/2) +$ty/=($nCount/2) # Determine the delta from the turtle's current position to the specified point -$deltaX = $X - $this.Position.X -$deltaY = $Y - $this.Position.Y +$deltaX = $tx - $this.Position.X +$deltaY = $ty - $this.Position.Y # Calculate the angle in radians and convert to degrees $angle = [Math]::Atan2($deltaY, $deltaX) * 180 / [Math]::PI # Return the angle minus the current heading (modulo 360) @@ -2690,32 +2772,33 @@ $this | Add-Member NoteProperty -Name '.BackgroundColor' -Value $value -Force .DESCRIPTION Gets a turtle a canvas element. #> - @( $viewBox = $this.ViewBox - $null, $null, $viewX, $viewY = $viewBox - "<style>canvas {max-width: 100%; height: 100%}</style>" - "<canvas id='$($this.ID)-canvas' width='$($viewX + 1)' height='$($viewY + 1)'></canvas>" - - "<script>" + $null, $null, $viewX, $viewY = $viewBox + "<canvas id='$($this.ID)-canvas'></canvas>" + "<script type='module'>" @" window.onload = async function() { - var canvas = document.getElementById('$($this.ID)-canvas'); - var ctx = canvas.getContext('2d'); - ctx.strokeStyle = '$($this.Stroke)' - ctx.lineWidth = '$( - if ($this.StrokeWidth -match '%') { - [Math]::Max($viewX, $viewY) * ($this.StrokeWidth -replace '%' -as [double])/100 - } else { - $this.StrokeWidth + const loadImage = async url => { + const newImage = document.createElement('img') + newImage.src = url + return new Promise((resolve, reject) => { + newImage.onload = () => resolve(newImage) + newImage.onerror = reject + }) } -)' - ctx.fillStyle = '$($this.Fill)' - var p = new Path2D("$($this.PathData)") - ctx.stroke(p) - ctx.fill(p) + const dataHeader = 'data:image/svg+xml;charset=utf-8' + const serializeAsXML = e => (new XMLSerializer()).serializeToString(e) + const encodeAsUTF8 = s => ```${dataHeader},`${encodeURIComponent(s)}`` - /*Insert-Post-Processing-Here*/ + const img = await loadImage('$($this.DataUrl)') + + var canvas = document.getElementById('$($this.ID)-canvas'); + canvas.width = $viewX + canvas.height = $viewY + var ctx = canvas.getContext('2d') + ctx.drawImage(img, 0, 0, $viewX, $viewY) + /*Insert-Post-Processing-Here*/ } "@ "</script>" @@ -2741,9 +2824,10 @@ window.onload = async function() { This can be used as an inline image in HTML, CSS, or Markdown. #> -$thisSymbol = $this.Symbol -$b64 = [Convert]::ToBase64String($OutputEncoding.GetBytes($thisSymbol.outerXml)) -"data:image/svg+xml;base64,$b64" +$thisSVG = $this.SVG +"data:image/svg+xml;base64,$( + [Convert]::ToBase64String($OutputEncoding.GetBytes($this.SVG.outerXml)) +)" @@ -2771,16 +2855,27 @@ $value ) foreach ($v in $value) { - if ($v -is [Timespan]) { - $this | Add-Member NoteProperty '.Duration' $v -Force - } elseif ($v -is [double] -or $v -is [int]) { + if ($v -is [double] -or $v -is [int]) { $this | Add-Member NoteProperty '.Duration' ([TimeSpan]::FromSeconds($v)) -Force + } elseif ($v -as [TimeSpan]) { + $this | Add-Member NoteProperty '.Duration' ($v -as [Timespan]) -Force } else { Write-Warning "'$Value' is not a number or timespan" } } - +if (($this.'.Duration' -is [TimeSpan]) -and $this.PathAnimation) { + $updatedAnimations = + @(foreach ($animationXML in $this.PathAnimation -split '(?<=/>)') { + $animationXML = $animationXML -as [xml] + if (-not $animationXML) { continue } + if ($animationXML.animate.attributeName -eq 'd') { + $animationXML.animate.dur = "$(($this.'.Duration').TotalSeconds)s" + } + $animationXML.OuterXml + }) + $this.PathAnimation = $updatedAnimations +} @@ -3230,16 +3325,38 @@ $this | Add-Member -MemberType NoteProperty -Force -Name '.PathClass' -Value @( PathElement - @( -"<path id='$($this.id)-path' d='$($this.PathData)' stroke='$( - if ($this.Stroke) { $this.Stroke } else { 'currentColor' } -)' stroke-width='$( - if ($this.StrokeWidth) { $this.StrokeWidth } else { '0.1%' } -)' fill='$($this.Fill)' class='$( - $this.PathClass -join ' ' -)' transform-origin='50% 50%' $( - foreach ($pathAttributeName in $this.PathAttribute.Keys) { - " $pathAttributeName='$($this.PathAttribute[$pathAttributeName])'" + <# +.SYNOPSIS + Gets the Turtle Path Element +.DESCRIPTION + Gets the Path Element of a Turtle. + + This contains the path of the Turtle's motion. +#> + +# Set our core attributes +$coreAttributes = [Ordered]@{ + id="$($this.id)-path" + d="$($this.PathData)" + stroke= + if ($this.Stroke) { $this.Stroke } + else { 'currentColor' } + 'stroke-width'= + if ($this.StrokeWidth) { $this.StrokeWidth } + else { '0.1%' } + fill="$($this.Fill)" + class=$($this.PathClass -join ' ') + 'transform-origin'='50% 50%' +} +# If someone decides to override any of these attributes, they are welcome to (at their own aesthetic risk) +foreach ($pathAttributeName in $this.PathAttribute.Keys) { + $coreAttributes[$pathAttributeName] = $($this.PathAttribute[$pathAttributeName]) +} + +@( +"<path$( + foreach ($attributeName in $coreAttributes.Keys) { + " $attributeName='$($coreAttributes[$attributeName])'" } )>" if ($this.PathAnimation) {$this.PathAnimation} @@ -3247,6 +3364,42 @@ if ($this.PathAnimation) {$this.PathAnimation} ) -as [xml] + + PathTransform + + <# +.SYNOPSIS + Gets any Path Transforms +.DESCRIPTION + Gets any transforms that will apply to this Turtle's path. +#> +return $this.PathAttribute['transform'] + + + <# +.SYNOPSIS + Sets Path Transforms +.DESCRIPTION + Sets any transforms that apply to the turtle path. +.EXAMPLE + turtle width 100 height 100 teleport 25 25 square 50 pathTransform @{skewX=45} save ./skewSquare.svg +#> +param($value) +$value = $value | . { process { $_ }} +$transformString = foreach ($v in $value) { + if ($v -is [Collections.IDictionary]) { + foreach ($k in $v.Keys) { + "$k($($v[$k]))" + } + } else { + "$v" + } +} + + +return $this.PathAttribute['transform'] = "$transformString" + + Pattern @@ -3264,10 +3417,9 @@ $null, $null, $viewX, $viewY = $viewBox }) -join ' ' ) + "'" } - )>" + )>" $(if ($this.PatternAnimation) { $this.PatternAnimation }) - $this.PathElement.OuterXml - $this.TextElement.OuterXml + $($this.SVG.SVG.InnerXML) "</pattern>" "</defs>" $( @@ -3503,12 +3655,9 @@ param( $Steps ) -if (-not $this.'.Steps') { - $this | Add-Member -MemberType NoteProperty -Force -Name '.Steps' -Value @( - [Collections.Generic.List[string]]::new($Steps) - ) -} else { - $this.'.Steps' = $steps +$currentSteps = $this.Steps +foreach ($step in $steps) { + $currentSteps.Add($step) } @@ -3547,15 +3696,164 @@ $this | Add-Member -MemberType NoteProperty -Force -Name '.StrokeWidth' -Value $ SVG - param() + <# +.SYNOPSIS + The Turtle's SVG +.DESCRIPTION + Gets this turtle and any nested turtles as a single Scalable Vector Graphic. +#> +param() @( -"<svg xmlns='http://www.w3.org/2000/svg' viewBox='$($this.ViewBox)' transform-origin='50% 50%' width='100%' height='100%'>" + +$svgAttributes = [Ordered]@{ + xmlns='http://www.w3.org/2000/svg' + viewBox="$($this.ViewBox)" + 'transform-origin'='50% 50%' + width='100%' + height='100%' +} + +# If the viewbox would have zero width or height +if ($this.ViewBox[-1] -eq 0 -or $this.ViewBox[-2] -eq 0) { + # It's not much of a viewbox at all, and we will omit the attribute. + $svgAttributes.Remove('viewBox') +} + +# Any explicitly provided attributes should override any automatic attributes. +foreach ($key in $this.SVGAttribute.Keys) { + $svgAttributes[$key] = $this.SVGAttribute[$key] +} + +"<svg $(@(foreach ($attributeName in $svgAttributes.Keys) { + " $attributeName='$($svgAttributes[$attributeName])'" +}) -join '')>" + # Declare any SVG animations + if ($this.SVGAnimation) {$this.SVGAnimation} + + # Output our own path $this.PathElement.OuterXml - $this.TextElement.OuterXml + # Followed by any text elements + $this.TextElement.OuterXml + + # If the turtle has children + $children = @(foreach ($turtleName in $this.Turtles.Keys) { + # make sure they're actually turtles + if ($this.Turtles[$turtleName].pstypenames -notcontains 'Turtle') { continue } + # and then set their IDs + $childTurtle = $this.Turtles[$turtleName] + $childTurtle.ID = "$($this.ID)-$turtleName" + $childTurtle + }) + # If we have any children + if ($children) { + # put them in a group containing their children + "<g id='$($this.ID)-children'>" + foreach ($child in $children) { + # and ask for this child's inner XML + # (which would contain any of its children) + # (and their children's children) + # and so on. + $child.SVG.SVG.InnerXML + } + "</g>" + } "</svg>" ) -join '' -as [xml] + + SVGAnimation + + if ($this.'.SVGAnimation') { + return $this.'.SVGAnimation' +} + + + + <# +.SYNOPSIS + Sets the Turtle SVG Animation +.DESCRIPTION + Sets an animation for the Turtle's SVG. +.EXAMPLE + turtle flower SVGAnimation ([Ordered]@{ + attributeName = 'fill' ; values = "#4488ff;#224488;#4488ff" ; repeatCount = 'indefinite'; dur = "4.2s" # ; additive = 'sum' + }, [Ordered]@{ + attributeName = 'stroke' ; values = "#224488;#4488ff;#224488" ; repeatCount = 'indefinite'; dur = "2.1s" # ; additive = 'sum' + }, [Ordered]@{ + type = 'rotate' ; values = 0, 360 ;repeatCount = 'indefinite'; dur = "41s" + }) save ./AnimatedFlower.svg +#> +param( +# The path animation object. +# This may be a string containing animation XML, XML, or a dictionary containing animation settings. +[PSObject] +$SVGAnimation +) + +$newAnimation = @(foreach ($animation in $SVGAnimation) { + if ($animation -is [Collections.IDictionary]) { + $animationCopy = [Ordered]@{} + $animation + if (-not $animationCopy['attributeType']) { + $animationCopy['attributeType'] = 'XML' + } + if (-not $animationCopy['attributeName']) { + $animationCopy['attributeName'] = 'transform' + } + if ($animationCopy.values -is [object[]]) { + $animationCopy['values'] = $animationCopy['values'] -join ';' + } + + $elementName = 'animate' + if ($animationCopy['attributeName'] -eq 'transform') { + $elementName = 'animateTransform' + } + + + if (-not $animationCopy['dur'] -and $this.Duration) { + $animationCopy['dur'] = "$($this.Duration.TotalSeconds)s" + } + + "<$elementName $( + @(foreach ($key in $animationCopy.Keys) { + " $key='$([Web.HttpUtility]::HtmlAttributeEncode($animationCopy[$key]))'" + }) -join '' + )/>" + } + if ($animation -is [string]) { + $animation + } + if ($animation.OuterXml) { + $animation.OuterXml + } +}) + +$this | Add-Member -MemberType NoteProperty -Force -Name '.SVGAnimation' -Value $newAnimation + + + + + SVGAttribute + + if (-not $this.'.SVGAttribute') { + $this | Add-Member NoteProperty '.SVGAttribute' ([Ordered]@{}) -Force +} +return $this.'.SVGAttribute' + + + param( +[Collections.IDictionary] +$SVGAttribute = [Ordered]@{} +) + +if (-not $this.'.SVGAttribute') { + $this | Add-Member -MemberType NoteProperty -Name '.SVGAttribute' -Value ([Ordered]@{}) -Force +} +foreach ($key in $SVGAttribute.Keys) { + $this.'.SVGAttribute'[$key] = $SVGAttribute[$key] +} + + Symbol @@ -3577,8 +3875,7 @@ param() @( "<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%' transform-origin='50% 50%'>" "<symbol id='$($this.ID)-symbol' viewBox='$($this.ViewBox)' transform-origin='50% 50%'>" - $this.PathElement.OuterXml - $this.TextElement.OuterXml + $($this.SVG.OuterXml) "</symbol>" $( if ($this.BackgroundColor) { @@ -3749,6 +4046,116 @@ if ($this.Text) { + + Turtles + + <# +.SYNOPSIS + Gets a Turtle's Turtles +.DESCRIPTION + Gets the Turtles contained within a Turtle object. + + These turtles may also contain turtles... + which may also contain turtles... + which may also contain turtles... + which may also contain turtles... + all the way down. +.EXAMPLE + turtle square 42 turtles @{ + circle = turtle circle 21 + } save ./InscribedSquare.svg + +.EXAMPLE + turtle square 42 turtles @{ + square = + turtle teleport 4 4 square 34 turtles @{ + square = turtle teleport 8 8 square 26 turtles @{ + square = turtle teleport 8 8 square 26 turtles @{ + square = turtle teleport 12 12 square 18 turtles @{ + square = turtle teleport 16 16 square 10 + } + } + } + } + } save ./SquaresWithinSquares.svg +#> +if ($this -and -not $this.'.Turtles') { + $this | Add-Member NoteProperty '.Turtles' ([Ordered]@{}) -Force +} + +return $this.'.Turtles' + + + <# +.SYNOPSIS + Sets a Turtle's Turtles +.DESCRIPTION + Sets the Turtles contained within a Turtle object. + + These turtles may also contain turtles... + which may also contain turtles... + which may also contain turtles... + which may also contain turtles... + all the way down. +.EXAMPLE + turtle square 42 turtles @{ + circle = turtle circle 21 + } save ./InscribedSquare.svg +.EXAMPLE + turtle square 42 turtles @{ + square = + turtle teleport 4 4 square 34 turtles @{ + square = turtle teleport 8 8 square 26 turtles @{ + square = turtle teleport 8 8 square 26 turtles @{ + square = turtle teleport 12 12 square 18 turtles @{ + square = turtle teleport 16 16 square 10 + } + } + } + } + } save ./SquaresWithinSquares.svg +#> +param( +[PSObject] +$Value +) + +# If we don't already have a turtles dictionary +if ($this -and -not $this.'.Turtles') { + # now is the time to create one. + $this | Add-Member NoteProperty '.Turtles' ([Ordered]@{}) -Force +} + +# Go over each value +foreach ($v in $value) { + # If the value was a dictionary + if ($v -is [Collections.IDictionary]) { + # merge it into our turtle dictionary + foreach ($key in $v.Keys) { + $this.'.Turtles'[$key] = $V[$key] + } + } elseif ($v.pstypenames -contains 'Turtle') { + # If it was a turtle, just add it + + # If the turtle had an ID, use it + if ($v.ID -ne 'Turtle') { + $this.'.Turtles'[$v.ID] = $v + } else { + # otherwise, provide it an auto incremented ID + $this.'.Turtles'["Turtle$($this.'.Turtles'.Count + 1)"] = $v + } + } elseif ($v -is [int]) { + # If the provided a number, let's create that many turtles. + # Note: the automatic placement of these turtles might be nice, and may be added in the future. + foreach ($n in 1..([Math]::Abs($value))) { + $this.'.Turtles'["Turtle$($this.'.Turtles'.Count + 1)"] = turtle + } + } +} + +return $this.'.Turtles' + + ViewBox diff --git a/Types/Turtle/Clear.ps1 b/Types/Turtle/Clear.ps1 index c88c6d5..2b932ad 100644 --- a/Types/Turtle/Clear.ps1 +++ b/Types/Turtle/Clear.ps1 @@ -1,7 +1,18 @@ +<# +.SYNOPSIS + Clears a Turtle +.DESCRIPTION + Clears the heading, steps, position, minimim, maximum, and any nested Turtles. +.EXAMPLE + turtle square 42 clear circle 21 +#> $this.Heading = 0 -$this.Steps = @() +if ($this.Steps.Clear) { + $this.Steps.Clear() +} $this | Add-Member -MemberType NoteProperty -Force -Name '.Position' -Value ([pscustomobject]@{ X = 0; Y = 0 }) $this | Add-Member -MemberType NoteProperty -Force -Name '.Minimum' -Value ([pscustomobject]@{ X = 0; Y = 0 }) $this | Add-Member -MemberType NoteProperty -Force -Name '.Maximum' -Value ([pscustomobject]@{ X = 0; Y = 0 }) $this.ViewBox = 0 +$this.Turtles.Clear() return $this \ No newline at end of file diff --git a/Types/Turtle/Distance.ps1 b/Types/Turtle/Distance.ps1 index 87c3b25..afda565 100644 --- a/Types/Turtle/Distance.ps1 +++ b/Types/Turtle/Distance.ps1 @@ -4,16 +4,36 @@ .DESCRIPTION Determines the distance from the turtle's current position to a point. #> -param( -# The X-coordinate -[double]$X = 0, -# The Y-coordinate -[double]$Y = 0 -) +param() + +$towards = $args | . { process { $_ } } + +$tx = 0.0 +$ty = 0.0 + +$nCount = 0 +foreach ($toward in $towards) { + if ($toward -is [double] -or $toward -is [float] -or $toward -is [int]) { + if (-not ($nCount % 2)) { + $tx = $toward + } else { + $ty = $toward + } + $nCount++ + } + elseif ($null -ne $toward.X -and $null -ne $toward.Y) { + $tx = $toward.x + $ty = $toward.y + $nCount+= 2 + } +} + +$tx/=($nCount/2) +$ty/=($nCount/2) # Determine the delta from the turtle's current position to the specified point -$deltaX = $X - $this.Position.X -$deltaY = $Y - $this.Position.Y +$deltaX = $tx - $this.Position.X +$deltaY = $ty - $this.Position.Y # Calculate the distance using the Pythagorean theorem return [Math]::Sqrt($deltaX*$deltaX + $deltaY*$deltaY) \ No newline at end of file diff --git a/Types/Turtle/Forward.ps1 b/Types/Turtle/Forward.ps1 index 6391d1f..9a2a4b2 100644 --- a/Types/Turtle/Forward.ps1 +++ b/Types/Turtle/Forward.ps1 @@ -12,6 +12,7 @@ param( $Distance = 10 ) -$x = $Distance * ([math]::round([math]::cos($this.Heading * [Math]::PI / 180),15)) -$y = $Distance * ([math]::round([math]::sin($this.Heading * [Math]::PI / 180),15)) +$x = $Distance * ([math]::cos($this.Heading * [Math]::PI / 180)) +$y = $Distance * ([math]::sin($this.Heading * [Math]::PI / 180)) + return $this.Step($x, $y) \ No newline at end of file diff --git a/Types/Turtle/Morph.ps1 b/Types/Turtle/Morph.ps1 index 7b3594e..bd5575f 100644 --- a/Types/Turtle/Morph.ps1 +++ b/Types/Turtle/Morph.ps1 @@ -4,7 +4,11 @@ .DESCRIPTION Morphs a Turtle by animating its path. - Any two paths with the same number of points can be morphed into each other. + Any two paths with the same number of points can be morphed into each other smoothly. + + Any two paths with a different number of points will become a step-by-step animation. + + Since animations can include multiple complex paths, they can get quite large, and be quite beautiful. .EXAMPLE $sierpinskiTriangle = turtle SierpinskiTriangle 42 4 $SierpinskiTriangleFlipped = turtle rotate 180 SierpinskiTriangle 42 4 @@ -34,6 +38,12 @@ $flowerPetals3, $flowerPetals ) | Save-Turtle ./flowerPetalMorph.svg Pattern +.EXAMPLE + turtle SierpinskiTriangle 42 4 morph | + Save-Turtle ./SierpinskiTriangleConstruction.svg +.EXAMPLE + turtle stroke '#224488' fill '#4488ff' backgroundColor '#112244' rotate 60 SierpinskiTriangle 42 4 SierpinskiTriangle -42 4 morph | + Save-Turtle ./SierpinskiTriangleReflectionConstructionAndFill.svg #> param( [Parameter(ValueFromRemainingArguments)] @@ -41,30 +51,50 @@ $Arguments ) $durationArgument = $null - +$hasPoints = $false +$segmentCount = 0 $newPaths = @(foreach ($arg in $Arguments) { if ($arg -is [string]) { if ($arg -match '^\s{0,}m') { $arg + $hasPoints = $true } } elseif ($arg.PathData) { $arg.PathData + $hasPoints = $true } elseif ($arg.D) { $arg.D + $hasPoints = $true } elseif ($arg -is [TimeSpan]) { $durationArgument = $arg } elseif ($arg -is [double] -or $arg -is [int]) { - $durationArgument = [TimeSpan]::FromSeconds($arg) + if (-not $hasPoints -and $arg -is [int]) { + $segmentCount = [Math]::Abs($arg) + } else { + $durationArgument = [TimeSpan]::FromSeconds($arg) + } } }) if (-not $newPaths) { - return $this - <#$pathSegments = @($this.PathData -split '(?=\p{L})') - $newPaths = @(for ($segmentNumber = 0; $segmentNumber -lt $pathSegments.Count; $segmentNumber++) { - $pathSegments[0..$segmentNumber] -join ' ' - }) -join ';'#> + if ($this.Steps.Count) { + $stepList = @($this.PathData -join ' ' -split '(?=\p{L})' -ne '') + if ($segmentCount) { + $newPaths = @( + for ($n = 1; $n -lt $stepList.Length; $n += ($stepList.Length/$segmentCount)) { + $stepList[0..$n] -join ' ' + } + ) + } else { + $newPaths = @(foreach ($n in 1..($stepList.Length)) { + $stepList[0..$n] -join ' ' + }) + } + + } else { + return $this + } } if ($this.PathAnimation) { @@ -72,8 +102,8 @@ if ($this.PathAnimation) { @(foreach ($animationXML in $this.PathAnimation -split '(?<=/>)') { $animationXML = $animationXML -as [xml] if (-not $animationXML) { continue } - if ($animationXML.attributeName -eq 'd') { - $animationXML.values = "$($newPaths -join ';')" + if ($animationXML.animate.attributeName -eq 'd') { + $animationXML.animate.values = "$($newPaths -join ';')" } $animationXML.OuterXml }) diff --git a/Types/Turtle/Step.ps1 b/Types/Turtle/Step.ps1 index 3988b7f..5908ca4 100644 --- a/Types/Turtle/Step.ps1 +++ b/Types/Turtle/Step.ps1 @@ -17,9 +17,9 @@ param( if ($DeltaX -or $DeltaY) { $this.Position = $DeltaX, $DeltaY if ($This.IsPenDown) { - $this.Steps += " l $DeltaX $DeltaY" + $this.Steps.Add(" l $DeltaX $DeltaY") } else { - $this.Steps += " m $DeltaX $DeltaY" + $this.Steps.Add(" m $DeltaX $DeltaY") } } diff --git a/Types/Turtle/Towards.ps1 b/Types/Turtle/Towards.ps1 index 35af8b5..e3460f4 100644 --- a/Types/Turtle/Towards.ps1 +++ b/Types/Turtle/Towards.ps1 @@ -4,16 +4,36 @@ .DESCRIPTION Determines the angle from the turtle's current heading towards a point. #> -param( -# The X-coordinate -[double]$X = 0, -# The Y-coordinate -[double]$Y = 0 -) +param() + +$towards = $args | . { process { $_ } } + +$tx = 0.0 +$ty = 0.0 + +$nCount = 0 +foreach ($toward in $towards) { + if ($toward -is [double] -or $toward -is [float] -or $toward -is [int]) { + if (-not ($nCount % 2)) { + $tx = $toward + } else { + $ty = $toward + } + $nCount++ + } + elseif ($null -ne $toward.X -and $null -ne $toward.Y) { + $tx = $toward.x + $ty = $toward.y + $nCount+= 2 + } +} + +$tx/=($nCount/2) +$ty/=($nCount/2) # Determine the delta from the turtle's current position to the specified point -$deltaX = $X - $this.Position.X -$deltaY = $Y - $this.Position.Y +$deltaX = $tx - $this.Position.X +$deltaY = $ty - $this.Position.Y # Calculate the angle in radians and convert to degrees $angle = [Math]::Atan2($deltaY, $deltaX) * 180 / [Math]::PI # Return the angle minus the current heading (modulo 360) diff --git a/Types/Turtle/get_Canvas.ps1 b/Types/Turtle/get_Canvas.ps1 index 0515567..4cc447f 100644 --- a/Types/Turtle/get_Canvas.ps1 +++ b/Types/Turtle/get_Canvas.ps1 @@ -4,32 +4,33 @@ .DESCRIPTION Gets a turtle a canvas element. #> - @( $viewBox = $this.ViewBox - $null, $null, $viewX, $viewY = $viewBox - "" - "" - - "" diff --git a/Types/Turtle/get_DataURL.ps1 b/Types/Turtle/get_DataURL.ps1 index 4f7eb4e..4d51eb7 100644 --- a/Types/Turtle/get_DataURL.ps1 +++ b/Types/Turtle/get_DataURL.ps1 @@ -6,6 +6,7 @@ This can be used as an inline image in HTML, CSS, or Markdown. #> -$thisSymbol = $this.Symbol -$b64 = [Convert]::ToBase64String($OutputEncoding.GetBytes($thisSymbol.outerXml)) -"data:image/svg+xml;base64,$b64" \ No newline at end of file +$thisSVG = $this.SVG +"data:image/svg+xml;base64,$( + [Convert]::ToBase64String($OutputEncoding.GetBytes($this.SVG.outerXml)) +)" \ No newline at end of file diff --git a/Types/Turtle/get_PathElement.ps1 b/Types/Turtle/get_PathElement.ps1 index 6e36b0e..1cd57f4 100644 --- a/Types/Turtle/get_PathElement.ps1 +++ b/Types/Turtle/get_PathElement.ps1 @@ -1,13 +1,35 @@ +<# +.SYNOPSIS + Gets the Turtle Path Element +.DESCRIPTION + Gets the Path Element of a Turtle. + + This contains the path of the Turtle's motion. +#> + +# Set our core attributes +$coreAttributes = [Ordered]@{ + id="$($this.id)-path" + d="$($this.PathData)" + stroke= + if ($this.Stroke) { $this.Stroke } + else { 'currentColor' } + 'stroke-width'= + if ($this.StrokeWidth) { $this.StrokeWidth } + else { '0.1%' } + fill="$($this.Fill)" + class=$($this.PathClass -join ' ') + 'transform-origin'='50% 50%' +} +# If someone decides to override any of these attributes, they are welcome to (at their own aesthetic risk) +foreach ($pathAttributeName in $this.PathAttribute.Keys) { + $coreAttributes[$pathAttributeName] = $($this.PathAttribute[$pathAttributeName]) +} + @( -"" if ($this.PathAnimation) {$this.PathAnimation} diff --git a/Types/Turtle/get_PathTransform.ps1 b/Types/Turtle/get_PathTransform.ps1 new file mode 100644 index 0000000..8bede1b --- /dev/null +++ b/Types/Turtle/get_PathTransform.ps1 @@ -0,0 +1,7 @@ +<# +.SYNOPSIS + Gets any Path Transforms +.DESCRIPTION + Gets any transforms that will apply to this Turtle's path. +#> +return $this.PathAttribute['transform'] \ No newline at end of file diff --git a/Types/Turtle/get_Pattern.ps1 b/Types/Turtle/get_Pattern.ps1 index 5a05e6e..d4c8454 100644 --- a/Types/Turtle/get_Pattern.ps1 +++ b/Types/Turtle/get_Pattern.ps1 @@ -12,10 +12,9 @@ $null, $null, $viewX, $viewY = $viewBox }) -join ' ' ) + "'" } - )>" + )>" $(if ($this.PatternAnimation) { $this.PatternAnimation }) - $this.PathElement.OuterXml - $this.TextElement.OuterXml + $($this.SVG.SVG.InnerXML) "" "" $( diff --git a/Types/Turtle/get_SVG.ps1 b/Types/Turtle/get_SVG.ps1 index 5dddfa2..9b90740 100644 --- a/Types/Turtle/get_SVG.ps1 +++ b/Types/Turtle/get_SVG.ps1 @@ -1,7 +1,63 @@ +<# +.SYNOPSIS + The Turtle's SVG +.DESCRIPTION + Gets this turtle and any nested turtles as a single Scalable Vector Graphic. +#> param() @( -"" + +$svgAttributes = [Ordered]@{ + xmlns='http://www.w3.org/2000/svg' + viewBox="$($this.ViewBox)" + 'transform-origin'='50% 50%' + width='100%' + height='100%' +} + +# If the viewbox would have zero width or height +if ($this.ViewBox[-1] -eq 0 -or $this.ViewBox[-2] -eq 0) { + # It's not much of a viewbox at all, and we will omit the attribute. + $svgAttributes.Remove('viewBox') +} + +# Any explicitly provided attributes should override any automatic attributes. +foreach ($key in $this.SVGAttribute.Keys) { + $svgAttributes[$key] = $this.SVGAttribute[$key] +} + +"" + # Declare any SVG animations + if ($this.SVGAnimation) {$this.SVGAnimation} + + # Output our own path $this.PathElement.OuterXml - $this.TextElement.OuterXml + # Followed by any text elements + $this.TextElement.OuterXml + + # If the turtle has children + $children = @(foreach ($turtleName in $this.Turtles.Keys) { + # make sure they're actually turtles + if ($this.Turtles[$turtleName].pstypenames -notcontains 'Turtle') { continue } + # and then set their IDs + $childTurtle = $this.Turtles[$turtleName] + $childTurtle.ID = "$($this.ID)-$turtleName" + $childTurtle + }) + # If we have any children + if ($children) { + # put them in a group containing their children + "" + foreach ($child in $children) { + # and ask for this child's inner XML + # (which would contain any of its children) + # (and their children's children) + # and so on. + $child.SVG.SVG.InnerXML + } + "" + } "" ) -join '' -as [xml] \ No newline at end of file diff --git a/Types/Turtle/get_SVGAnimation.ps1 b/Types/Turtle/get_SVGAnimation.ps1 new file mode 100644 index 0000000..ecc13d2 --- /dev/null +++ b/Types/Turtle/get_SVGAnimation.ps1 @@ -0,0 +1,3 @@ +if ($this.'.SVGAnimation') { + return $this.'.SVGAnimation' +} diff --git a/Types/Turtle/get_SVGAttribute.ps1 b/Types/Turtle/get_SVGAttribute.ps1 new file mode 100644 index 0000000..d490f2f --- /dev/null +++ b/Types/Turtle/get_SVGAttribute.ps1 @@ -0,0 +1,4 @@ +if (-not $this.'.SVGAttribute') { + $this | Add-Member NoteProperty '.SVGAttribute' ([Ordered]@{}) -Force +} +return $this.'.SVGAttribute' \ No newline at end of file diff --git a/Types/Turtle/get_Symbol.ps1 b/Types/Turtle/get_Symbol.ps1 index f29d10f..e028799 100644 --- a/Types/Turtle/get_Symbol.ps1 +++ b/Types/Turtle/get_Symbol.ps1 @@ -16,8 +16,7 @@ param() @( "" "" - $this.PathElement.OuterXml - $this.TextElement.OuterXml + $($this.SVG.OuterXml) "" $( if ($this.BackgroundColor) { diff --git a/Types/Turtle/get_Turtles.ps1 b/Types/Turtle/get_Turtles.ps1 new file mode 100644 index 0000000..a3e5342 --- /dev/null +++ b/Types/Turtle/get_Turtles.ps1 @@ -0,0 +1,35 @@ +<# +.SYNOPSIS + Gets a Turtle's Turtles +.DESCRIPTION + Gets the Turtles contained within a Turtle object. + + These turtles may also contain turtles... + which may also contain turtles... + which may also contain turtles... + which may also contain turtles... + all the way down. +.EXAMPLE + turtle square 42 turtles @{ + circle = turtle circle 21 + } save ./InscribedSquare.svg + +.EXAMPLE + turtle square 42 turtles @{ + square = + turtle teleport 4 4 square 34 turtles @{ + square = turtle teleport 8 8 square 26 turtles @{ + square = turtle teleport 8 8 square 26 turtles @{ + square = turtle teleport 12 12 square 18 turtles @{ + square = turtle teleport 16 16 square 10 + } + } + } + } + } save ./SquaresWithinSquares.svg +#> +if ($this -and -not $this.'.Turtles') { + $this | Add-Member NoteProperty '.Turtles' ([Ordered]@{}) -Force +} + +return $this.'.Turtles' \ No newline at end of file diff --git a/Types/Turtle/set_Duration.ps1 b/Types/Turtle/set_Duration.ps1 index 9fa5f1b..3cbf349 100644 --- a/Types/Turtle/set_Duration.ps1 +++ b/Types/Turtle/set_Duration.ps1 @@ -10,12 +10,24 @@ $value ) foreach ($v in $value) { - if ($v -is [Timespan]) { - $this | Add-Member NoteProperty '.Duration' $v -Force - } elseif ($v -is [double] -or $v -is [int]) { + if ($v -is [double] -or $v -is [int]) { $this | Add-Member NoteProperty '.Duration' ([TimeSpan]::FromSeconds($v)) -Force + } elseif ($v -as [TimeSpan]) { + $this | Add-Member NoteProperty '.Duration' ($v -as [Timespan]) -Force } else { Write-Warning "'$Value' is not a number or timespan" } } +if (($this.'.Duration' -is [TimeSpan]) -and $this.PathAnimation) { + $updatedAnimations = + @(foreach ($animationXML in $this.PathAnimation -split '(?<=/>)') { + $animationXML = $animationXML -as [xml] + if (-not $animationXML) { continue } + if ($animationXML.animate.attributeName -eq 'd') { + $animationXML.animate.dur = "$(($this.'.Duration').TotalSeconds)s" + } + $animationXML.OuterXml + }) + $this.PathAnimation = $updatedAnimations +} \ No newline at end of file diff --git a/Types/Turtle/set_PathTransform.ps1 b/Types/Turtle/set_PathTransform.ps1 new file mode 100644 index 0000000..9987156 --- /dev/null +++ b/Types/Turtle/set_PathTransform.ps1 @@ -0,0 +1,22 @@ +<# +.SYNOPSIS + Sets Path Transforms +.DESCRIPTION + Sets any transforms that apply to the turtle path. +.EXAMPLE + turtle width 100 height 100 teleport 25 25 square 50 pathTransform @{skewX=45} save ./skewSquare.svg +#> +param($value) +$value = $value | . { process { $_ }} +$transformString = foreach ($v in $value) { + if ($v -is [Collections.IDictionary]) { + foreach ($k in $v.Keys) { + "$k($($v[$k]))" + } + } else { + "$v" + } +} + + +return $this.PathAttribute['transform'] = "$transformString" \ No newline at end of file diff --git a/Types/Turtle/set_SVGAnimation.ps1 b/Types/Turtle/set_SVGAnimation.ps1 new file mode 100644 index 0000000..e7d0a84 --- /dev/null +++ b/Types/Turtle/set_SVGAnimation.ps1 @@ -0,0 +1,59 @@ +<# +.SYNOPSIS + Sets the Turtle SVG Animation +.DESCRIPTION + Sets an animation for the Turtle's SVG. +.EXAMPLE + turtle flower SVGAnimation ([Ordered]@{ + attributeName = 'fill' ; values = "#4488ff;#224488;#4488ff" ; repeatCount = 'indefinite'; dur = "4.2s" # ; additive = 'sum' + }, [Ordered]@{ + attributeName = 'stroke' ; values = "#224488;#4488ff;#224488" ; repeatCount = 'indefinite'; dur = "2.1s" # ; additive = 'sum' + }, [Ordered]@{ + type = 'rotate' ; values = 0, 360 ;repeatCount = 'indefinite'; dur = "41s" + }) save ./AnimatedFlower.svg +#> +param( +# The path animation object. +# This may be a string containing animation XML, XML, or a dictionary containing animation settings. +[PSObject] +$SVGAnimation +) + +$newAnimation = @(foreach ($animation in $SVGAnimation) { + if ($animation -is [Collections.IDictionary]) { + $animationCopy = [Ordered]@{} + $animation + if (-not $animationCopy['attributeType']) { + $animationCopy['attributeType'] = 'XML' + } + if (-not $animationCopy['attributeName']) { + $animationCopy['attributeName'] = 'transform' + } + if ($animationCopy.values -is [object[]]) { + $animationCopy['values'] = $animationCopy['values'] -join ';' + } + + $elementName = 'animate' + if ($animationCopy['attributeName'] -eq 'transform') { + $elementName = 'animateTransform' + } + + + if (-not $animationCopy['dur'] -and $this.Duration) { + $animationCopy['dur'] = "$($this.Duration.TotalSeconds)s" + } + + "<$elementName $( + @(foreach ($key in $animationCopy.Keys) { + " $key='$([Web.HttpUtility]::HtmlAttributeEncode($animationCopy[$key]))'" + }) -join '' + )/>" + } + if ($animation -is [string]) { + $animation + } + if ($animation.OuterXml) { + $animation.OuterXml + } +}) + +$this | Add-Member -MemberType NoteProperty -Force -Name '.SVGAnimation' -Value $newAnimation diff --git a/Types/Turtle/set_SVGAttribute.ps1 b/Types/Turtle/set_SVGAttribute.ps1 new file mode 100644 index 0000000..1e531ef --- /dev/null +++ b/Types/Turtle/set_SVGAttribute.ps1 @@ -0,0 +1,11 @@ +param( +[Collections.IDictionary] +$SVGAttribute = [Ordered]@{} +) + +if (-not $this.'.SVGAttribute') { + $this | Add-Member -MemberType NoteProperty -Name '.SVGAttribute' -Value ([Ordered]@{}) -Force +} +foreach ($key in $SVGAttribute.Keys) { + $this.'.SVGAttribute'[$key] = $SVGAttribute[$key] +} \ No newline at end of file diff --git a/Types/Turtle/set_Steps.ps1 b/Types/Turtle/set_Steps.ps1 index 4c7529e..85d69e9 100644 --- a/Types/Turtle/set_Steps.ps1 +++ b/Types/Turtle/set_Steps.ps1 @@ -11,11 +11,8 @@ param( $Steps ) -if (-not $this.'.Steps') { - $this | Add-Member -MemberType NoteProperty -Force -Name '.Steps' -Value @( - [Collections.Generic.List[string]]::new($Steps) - ) -} else { - $this.'.Steps' = $steps +$currentSteps = $this.Steps +foreach ($step in $steps) { + $currentSteps.Add($step) } diff --git a/Types/Turtle/set_Turtles.ps1 b/Types/Turtle/set_Turtles.ps1 new file mode 100644 index 0000000..89e8a48 --- /dev/null +++ b/Types/Turtle/set_Turtles.ps1 @@ -0,0 +1,68 @@ +<# +.SYNOPSIS + Sets a Turtle's Turtles +.DESCRIPTION + Sets the Turtles contained within a Turtle object. + + These turtles may also contain turtles... + which may also contain turtles... + which may also contain turtles... + which may also contain turtles... + all the way down. +.EXAMPLE + turtle square 42 turtles @{ + circle = turtle circle 21 + } save ./InscribedSquare.svg +.EXAMPLE + turtle square 42 turtles @{ + square = + turtle teleport 4 4 square 34 turtles @{ + square = turtle teleport 8 8 square 26 turtles @{ + square = turtle teleport 8 8 square 26 turtles @{ + square = turtle teleport 12 12 square 18 turtles @{ + square = turtle teleport 16 16 square 10 + } + } + } + } + } save ./SquaresWithinSquares.svg +#> +param( +[PSObject] +$Value +) + +# If we don't already have a turtles dictionary +if ($this -and -not $this.'.Turtles') { + # now is the time to create one. + $this | Add-Member NoteProperty '.Turtles' ([Ordered]@{}) -Force +} + +# Go over each value +foreach ($v in $value) { + # If the value was a dictionary + if ($v -is [Collections.IDictionary]) { + # merge it into our turtle dictionary + foreach ($key in $v.Keys) { + $this.'.Turtles'[$key] = $V[$key] + } + } elseif ($v.pstypenames -contains 'Turtle') { + # If it was a turtle, just add it + + # If the turtle had an ID, use it + if ($v.ID -ne 'Turtle') { + $this.'.Turtles'[$v.ID] = $v + } else { + # otherwise, provide it an auto incremented ID + $this.'.Turtles'["Turtle$($this.'.Turtles'.Count + 1)"] = $v + } + } elseif ($v -is [int]) { + # If the provided a number, let's create that many turtles. + # Note: the automatic placement of these turtles might be nice, and may be added in the future. + foreach ($n in 1..([Math]::Abs($value))) { + $this.'.Turtles'["Turtle$($this.'.Turtles'.Count + 1)"] = turtle + } + } +} + +return $this.'.Turtles' \ No newline at end of file