diff --git a/CHANGELOG.md b/CHANGELOG.md index 36acc59..e9dd61e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,128 @@ +## Turtle 0.2.1: + +* New Documentation: + * Over 130 examples! + * A Brief History of Turtles (#249) +* Website improvements + * Copy Code Button! (#331) + * Improved layout and new backgrounds (#333) + * Improving build (#344) + * Defaulting palette selection (#346) +* Major improvements + * A turtle can now be any element! + * Support for CSS keyframes, styles, and variables! + * Vastly expanded SVG support, including bezier curves! + * CircleArcs and Pie Graphs! Improvements to circles. +* `Turtle` command improvements: + * `Get-Turtle` + * `Get-Turtle` help (#273) ( `turtle flower help` `turtle flower help examples`) + * `Get-Turtle` now tracks commands (#250) + * `Get-Turtle` now supports brackets (#255) and prebalances them (#262) + * `Get-Turtle -AsJob` (#268) + * `Get-Turtle` improved set errors (#252) + * `Save-Turtle` + * `Save-Turtle` saves as SVG by default (#259) + * `Save-Turtle` autosaves by name (#269) + * `Show-Turtle` will show the turtle (#257) +* New methods: + * `Turtle.a/Arc` (#231) + * `Turtle.b/BezierCurve` (#228) + * `Turtle.CircleArc` (#235) + * `Turtle.c/CubicBezierCurve` (#230) + * `Turtle.FractalShrub` (#332) + * `Turtle.Leg` (#288) + * `Turtle.Pie/PieGraph` (#239) + * `Turtle.q/QuadraticBezierCurve` (#229) + * `Turtle.Repeat` (#256) + * `Turtle.Spider` (#289) + * `Turtle.Spiderweb` (#290) + * `Turtle.Spokes` (#291) + * `Turtle.Sun` (#297) + * `Turtle.Show` (#258) +* New properties: + * `Turtle.get_ArgumentList` (#296) + * `Turtle.get/set_Attribute` (#247) + * `Turtle.get/set_Class` (#237) + * `Turtle.get_Commands` (#250) + * `Turtle.get_DataBlock` (#284) + * `Turtle.get/set_Element` (#248) + * `Turtle.get/set_Defines` (#243) + * `Turtle.get_ScriptBlock` (#253) + * `Turtle.get/set_Defines` (#243) + * `Turtle.get/set_Keyframe(s)` (#251) + * `Turtle.get_History` (#279) + * `Turtle.get/set_Link/Href` (#241) + * `Turtle.get/set_Locale` (#300) + * `Turtle.get_Marker` (#227) + * `Turtle.get/set_MarkerEnd` (#233) + * `Turtle.get/set_MarkerMiddle` (#234) + * `Turtle.get/set_MarkerStart` (#232) + * `Turtle.get/set_Opacity` (#293) + * `Turtle.get/set_Precision` (#225) + * `Turtle.ResizeViewBox` (#238) + * `Turtle.get/set_Start` (#245) + * `Turtle.get/set_Style` (#254) + * `Turtle.get/set_Variable` (#263) + * `Turtle.get/set_Title` (#285) +* New pseudo type: + * `Turtle.History` + * `Turtle.History.ToString()` (#282) + * `Turtle.History.DefaultDisplay` (#283) + * `Turtle.js` (experimental) + * Javascript version of turtle (#302) + * Initial Core Operations: + * `Turtle.js.heading` (#303) + * `Turtle.js.rotate` (#304) + * `Turtle.js.forward` (#305) (#337) (#338) + * `Turtle.js.isPenDown` (#306) + * `Turtle.js.goTo` (#307) + * `Turtle.js.step` (#308) + * `Turtle.js.teleport` (#309) (#334) + * `Turtle.js.steps` (#310) + * `Turtle.js.min` (#311) + * `Turtle.js.max` (#312) + * `Turtle.js.resize` (#313) + * `Turtle.js.x` (#314) + * `Turtle.js.y` (#315) + * `Turtle.js.width` (#316) + * `Turtle.js.height` (#317) + * `Turtle.js.pathData` (#318) (#339) + * `Turtle.js.polygon` (#319) (#336) (#338) + * `Turtle.js.penUp` (#322) + * `Turtle.js.penDown` (#323) + * `Turtle.js.parse` (#327) + * `Turtle.js.go` (#330) + * `Turtle.js.ToString.ps1()` (#320) + * `Turtle.js.get_JavaScript.ps1` (#324) + * Thanks @ninmonkey for early testing! +* Improved methods: + * `Turtle.ArcLeft/ArcRight` allows StepCount (#272) + * `Turtle.Circle` optimization (#287) + * `Turtle.FractalPlant` improvement (#271) + * `Turtle.HorizontalLine` is mapped to SVG `h` (#280) + * `Turtle.VerticalLine` is mapped to SVG `v` (#281) +* Improvemented Properties: + * Adding `[OutputType([xml])]` to properties that output XML (#266) + * `Turtle.get_Duration` defaults (#270) + * `Turtle.get_Mask/PatternMask` returns only the mask (#261) + * `Turtle.set_BackgroundColor` applies to SVG directly (#260) + * `Turtle.get_Maximum` is a vector (#275) + * `Turtle.get_Minimum` is a vector (#276) + * `Turtle.get_Position` is a vector (#274) + * `Turtle.set_Stroke` supports gradients (#295) + * `Turtle.set_Fill` supports gradients (#294) + * `Turtle.set_PathAnimation` will not overwrite a morph (#244) + * `Turtle.get/set_PatternAnimation` uses duration (#299) and improved docs (#298) + * `Turtle.get_TextElement` defaults to centered text (#265) + * `Turtle.get_TextElement` improved color support (#292) + * `Turtle.get_ViewBox` negative bounds (#286) +* More aliases: + * Added Internationalized Aliases (i.e. `Turtle.BackgroundColour`) (#236) + * SVG syntax aliases (#240) +* Fixed extra output in `Turtle.Pop` (#264) + +--- + ## Turtle 0.2: ### Turtles All The Way Down diff --git a/Commands/Get-Turtle.ps1 b/Commands/Get-Turtle.ps1 index 69afa81..0b9afc0 100644 --- a/Commands/Get-Turtle.ps1 +++ b/Commands/Get-Turtle.ps1 @@ -1,15 +1,15 @@ function Get-Turtle { <# .SYNOPSIS - Gets Turtles + Turtle Graphics in PowerShell .DESCRIPTION - Gets turtles in a PowerShell. + Turtle Graphics in PowerShell. Draw any image with turtles in a powershell. .NOTES Turtle Graphics are pretty groovy. They have been kicking it since 1966, and they are how computers first learned to draw. - They kicked off the first computer-aided design boom and inspired generations of artists, engineers, mathematicians, and physicists. + They kicked off the first computer-aided design boom and inspired generations. They are also _incredibly_ easy to build. @@ -27,32 +27,37 @@ function Get-Turtle { We can describe more complex moves by combining these steps. - Each argument can be the name of a move of the turtle object. + As a PowerShell turtle, we can take any pipeline of objects and turn them into turtles. - After a member name is encountered, subsequent arguments will be passed to the member as parameters. + Each argument can be the name of a method or property of the turtle object. - Any parameter that begins with whitespace will be split into multiple words. + After a member name is encountered, subsequent arguments will be passed to the member as parameters. + .EXAMPLE + # We can write shapes as a series of steps. + # Let's start with a simple diagonal line + turtle rotate 45 forward 42 .EXAMPLE - # We can write shapes as a series of steps - turtle " - rotate 120 - forward 42 - rotate 120 - forward 42 - rotate 120 - forward 42 - " + # Let's draw an equilateral triangle + turtle forward 42 rotate 120 forward 42 rotate 120 forward 42 .EXAMPLE - # We can also use a method. + # Typing that might get tedious. + # Instead, let's use a method. # Polygon will draw an an N-sided polygon. turtle polygon 10 5 .EXAMPLE - # A simple case of this is a square + # There's also a method for squares turtle square 42 .EXAMPLE # If we rotate 45 degrees first, our square becomes a rhombus turtle rotate 45 square 42 .EXAMPLE + # We can also draw a rectangle + turtle rectangle 42 4.2 + .EXAMPLE + # If we only provide the first parameter, we get a golden rectangle + turtle rectangle 42 + .EXAMPLE + #### Circles # We can draw a circle turtle circle 10 .EXAMPLE @@ -134,6 +139,7 @@ function Get-Turtle { # Let's do the same thing, but with a smaller angle turtle ('polygon', 23, 6, 'rotate', -40 * 9) .EXAMPLE + #### Flowers # A flower is a series of repeated polygons and rotations turtle Flower .EXAMPLE @@ -156,6 +162,7 @@ function Get-Turtle { turtle Flower 50 10 $sideCount 72 ) .EXAMPLE + ### Petals and Flowers # We can draw a pair of arcs and turn back after each one. # # We call this a 'petal'. @@ -194,6 +201,37 @@ function Get-Turtle { $flowerPetals ) .EXAMPLE + #### Arcs and Suns + # We can arc right or left + turtle arcRight 42 120 + turtle arcLeft 42 120 + .EXAMPLE + # We can arc right and then left to produce a ray or wave shape + turtle arcRight 42 120 arcLeft 42 120 + .EXAMPLE + # We can rotate and repeat that rays to make a sun + $Length = 42 + $Angle = 160 + $RayAngle = 90 + $StepCount = 9 + turtle sun $Length $Angle $RayAngle $StepCount + .EXAMPLE + # If we reverse the ray angle and morph, the sun really shines! + turtle Sun 100 135 60 8 morph @( + turtle Sun 100 135 60 8 + turtle Sun 100 135 -60 8 + turtle Sun 100 135 60 8 + ) + .EXAMPLE + # We can add multiple fixed colors to make a gradient + # Then the sun is truly bright. + turtle Sun 100 135 60 8 fill 'yellow' 'goldenrod' stroke 'goldenrod' 'yellow' morph @( + turtle Sun 100 135 60 8 + turtle Sun 100 135 -60 8 + turtle Sun 100 135 60 8 + ) + .EXAMPLE + #### Stars # We can create a Star with N points turtle star 42 5 @@ -212,6 +250,7 @@ function Get-Turtle { turtle @('star',42,8,'rotate',45 * 8) .EXAMPLE + #### Starflowers # When we do this, we call it a Star Flower turtle StarFlower 42 .EXAMPLE @@ -246,6 +285,7 @@ function Get-Turtle { turtle StarFlower 42 12 5 30 ) .EXAMPLE + #### Scissors # We can construct a 'scissor' by drawing two lines at an angle turtle Scissor 42 60 .EXAMPLE @@ -268,6 +308,7 @@ function Get-Turtle { Turtle ScissorPoly 16 $n $n } .EXAMPLE + #### Step Spirals # We can draw an outward spiral by growing a bit each step turtle StepSpiral .EXAMPLE @@ -325,6 +366,7 @@ function Get-Turtle { turtle @('StepSpiral',3, 120, 'rotate',60 * 6) ) .EXAMPLE + #### Spirolaterals turtle spirolateral .EXAMPLE turtle spirolateral 50 60 10 @@ -335,6 +377,80 @@ function Get-Turtle { .EXAMPLE turtle spirolateral 23 72 8 .EXAMPLE + #### Bezier Curves + # We can draw simple Bezier Curves. + # Imagine a string being tugged by a point + turtle bezierCurve 0 100 100 100 + .EXAMPLE + # A morph can help us understand bezier curve movement + turtle bezierCurve 0 100 100 100 morph @( + turtle bezierCurve 0 100 100 100 + turtle bezierCurve 100 0 100 100 + turtle bezierCurve 0 100 100 100 + ) + .EXAMPLE + # Lets make it more exaggerated + turtle viewbox 150 bezierCurve 0 100 100 100 morph @( + turtle bezierCurve 0 200 100 100 + turtle bezierCurve 200 0 100 100 + turtle bezierCurve 0 200 100 100 + ) + .EXAMPLE + # We use the shorthand 's' for a simple bezier curve + turtle s 100 0 100 100 + .EXAMPLE + # This helps make beautifully short moprhs + turtle s 100 0 100 100 morph @( + turtle s 100 0 100 100 + turtle s 0 100 100 100 + turtle s 100 0 100 100 + ) + .EXAMPLE + # We can also draw quadratic bezier curves + turtle quadraticbezierCurve 0 100 100 100 + .EXAMPLE + # We can use the alias q, and morph them, too. + turtle q 0 100 100 100 morph @( + turtle q 100 0 100 100 + turtle q 0 100 100 100 + turtle q 100 0 100 100 + ) + .EXAMPLE + # We can also draw cubic bezier curves + # For these, imaging a string being pulled by two other strings. + turtle cubicBezierCurve 0 100 100 0 100 100 + .EXAMPLE + # We can shorten this to `c`, and morph it in beautiful ways + turtle width 200 height 200 morph @( + turtle c 0 0 0 0 200 200 + turtle c 0 200 200 0 200 200 + turtle c 0 0 0 0 200 200 + turtle c 200 0 0 200 200 200 + turtle c 0 0 0 0 200 200 + ) + + turtle width 200 height 200 start 200 200 morph @( + turtle c 0 0 0 0 -200 200 + turtle c 0 200 -200 0 -200 200 + turtle c 0 0 0 0 -200 200 + turtle c -200 0 0 200 -200 200 + turtle c 0 0 0 0 -200 200 + ) + .EXAMPLE + # We can start at a given location, and morph along an axis. + turtle width 200 height 200 morph @( + turtle start 100 0 c 0 0 0 0 0 200 + turtle start 100 0 c -100 0 100 200 0 200 + turtle start 100 0 c 0 0 0 0 0 200 + ) + .EXAMPLE + turtle width 200 height 200 morph @( + turtle start 0 100 c 0 0 0 0 200 0 + turtle start 0 100 c 0 -100 200 100 200 0 + turtle start 0 100 c 0 0 0 0 200 0 + ) + .EXAMPLE + #### Bar Graphs # Lets get practical. Turtle can easily make a bar graph. turtle BarGraph 200 300 (1..10) .EXAMPLE @@ -351,7 +467,308 @@ function Get-Turtle { 'rotate',180 * 2 ) .EXAMPLE - # Turtle can draw a number of fractals + #### Pie Graphs + # Want a Piece of Pie? + Turtle Pie 100 4 + Turtle Pie 100 6 + Turtle Pie 100 8 + .EXAMPLE + # Want a quarter? + Turtle Pie 100 (1/4) + .EXAMPLE + # How about a range of slices? + Turtle Pie 100 (1..10) + .EXAMPLE + # What about some colorful slices? + Turtle Pie 100 @( + foreach ($color in 'red', 'green', 'blue') { + @{ + Value = 1 + PathClass = "$color-fill foreground-stroke" + Fill = $color + Title = $color + } + } + ) + .EXAMPLE + # What about some random colorful slices? + Turtle Pie 100 @( + foreach ($color in 'red', 'green', 'blue', 'yellow', 'magenta','cyan') { + @{ + Value = (Get-Random -Max 100) + PathClass = "$color-fill foreground-stroke" + Fill = $color + Title = $color + } + } + ) + .EXAMPLE + #### Circle Arcs + # Pie graphs are made out of circle arcs + Turtle id Quadrants @( + 'CircleArc',42, 90, + 'Rotate', 90 * 4 + ) + Turtle id Sextants @( + 'CircleArc',42, 60, + 'Rotate', 60 * 6 + ) + Turtle id Octants @( + 'CircleArc',42, 45, + 'Rotate', 45 * 8 + ) + .EXAMPLE + # We can alternate rotations and arcs to create radial stripes + Turtle 'Rotate', -22.5 @( + 'Rotate', 15, + 'CircleArc',42, 15, + 'Rotate', 15 * 24 + ) + + Turtle 'Rotate', -15 @( + 'Rotate', 30, + 'CircleArc',42, 30, + 'Rotate', 30 * 12 + ) + + Turtle @( + 'Rotate', 60, + 'CircleArc',42, 60, + 'Rotate', 60 * 6 + ) + .EXAMPLE + # We can draw negative circle arcs + Turtle CircleArc 42 -90 + .EXAMPLE + # Negative quadrants + Turtle @( + 'CircleArc',42, -90, + 'Rotate', 90 * 4 + ) + .EXAMPLE + # Negative sextants + Turtle @( + 'CircleArc',42, -60, + 'Rotate', 60 * 6 + ) + .EXAMPLE + # Negative octants + Turtle @( + 'CircleArc',42, -45, + 'Rotate', 45 * 8 + ) + .EXAMPLE + # We can combine arcs and movement to make a pinwheel + turtle @( + 'circlearc', 42, 60, + 'rotate',60, + 'forward',42 * 6 + ) + .EXAMPLE + # Lets morph positive quadrants into negative quadrants + $quadrants = Turtle @( + 'CircleArc',42, 90, + 'Rotate', 90 * 4 + ) + $quadrants | turtle morph @( + $quadrants + Turtle @( + 'CircleArc',42, -90, + 'Rotate', 90 * 4 + ) + $quadrants + ) + + .EXAMPLE + # Lets morph positive sextants into negative sextants + $sextants = Turtle id Sextants @( + 'CircleArc',42, 60, + 'Rotate', 60 * 6 + ) + $sextants | turtle morph @( + $sextants + Turtle @( + 'CircleArc',42, -60, + 'Rotate', 60 * 6 + ) + $sextants + ) + .EXAMPLE + # Lets morph positive octants into negative octants + $octants = Turtle id Octants @( + 'CircleArc',42, 45, + 'Rotate', 45 * 8 + ) + + $octants | turtle morph @( + $octants + Turtle @( + 'CircleArc',42, -45, + 'Rotate', 45 * 8 + ) + $octants + ) + .EXAMPLE + # We can overlap pinwheels to make even more exotic shapes + turtle @( + @( + 'circlearc', 21, -60, + 'rotate',60, + 'forward',42 * 6 + 'rotate', 30 + ) * 12 + ) + .EXAMPLE + # We can morph and spin these exotic shapes to create hypnotic animations + $exoticShape = turtle ( + @( + 'circlearc', 21, -60, + 'rotate',60, + 'forward',42 * 6 + + 'rotate', 30 + ) * 12 + ) + + $exoticShape | + turtle morph @( + $exoticShape + turtle ( + @( + 'circlearc', 21, 60, + 'rotate',60, + 'forward',42 * 6 + + 'rotate', 30 + ) * 12 + ) + $exoticShape + ) pathAnimation @{ + type = 'rotate' + values = 0, 360 + repeatCount = 'indefinite' + } + .EXAMPLE + #### Turtles all the way down + # Turtles can contain turtles. + # Let's make a circle inscribed into a square + turtle viewbox 42 turtles ([Ordered]@{ + 'square' = turtle square 42 + 'circle' = turtle circle 21 + }) + .EXAMPLE + # Each turtle can have a distinct color or CSS class + turtle viewbox 42 turtles ([Ordered]@{ + 'square' = turtle square 42 pathclass 'blue-fill foreground-stroke' + 'circle' = turtle circle 21 pathclass 'cyan-fill foreground-stroke' + }) + .EXAMPLE + # Lets make some colorful boxes + turtle viewbox 42 turtles @([Ordered]@{ + 'q1' = turtle start 0 0 square 21 pathclass 'red-fill foreground-stroke' + 'q2' = turtle start 21 0 square 21 pathclass 'green-fill foreground-stroke' + 'q3' = turtle start 21 21 square 21 pathclass 'yellow-fill foreground-stroke' + 'q4' = turtle start 0 21 square 21 pathclass 'blue-fill foreground-stroke' + }) + .EXAMPLE + # Nested turtles can morph! + # Let's move these squares around. + turtle viewbox 42 turtles @([Ordered]@{ + 'q1' = turtle viewbox 21 fill red pathclass 'red-fill foreground-stroke' morph @( + turtle start 0 0 square 21 + turtle start 21 0 square 21 + turtle start 21 21 square 21 + turtle start 0 21 square 21 + turtle start 0 0 square 21 + ) + 'q2' = turtle viewbox 21 fill green pathclass 'green-fill foreground-stroke' morph @( + turtle start 21 0 square 21 + turtle start 21 21 square 21 + turtle start 0 21 square 21 + turtle start 0 0 square 21 + turtle start 21 0 square 21 + ) + 'q3' = turtle viewbox 21 fill yellow pathclass 'yellow-fill foreground-stroke' morph @( + turtle start 21 21 square 21 + turtle start 0 21 square 21 + turtle start 0 0 square 21 + turtle start 21 0 square 21 + turtle start 21 21 square 21 + ) + 'q4' = turtle viewbox 21 fill blue pathclass 'blue-fill foreground-stroke' morph @( + turtle start 0 21 square 21 + turtle start 0 0 square 21 + turtle start 21 0 square 21 + turtle start 21 21 square 21 + turtle start 0 21 square 21 + ) + }) + .EXAMPLE + # Let's make a colorful cubic morph + $Colors = @('fill', '#4488ff','stroke','#224488','pathclass', 'brightBlue-fill','blue-stroke') + turtle width 200 height 200 turtles @( + turtle morph @( + turtle c 0 0 0 0 200 200 + turtle c 0 200 200 0 200 200 + turtle c 0 0 0 0 200 200 + turtle c 200 0 0 200 200 200 + turtle c 0 0 0 0 200 200 + ) @colors + turtle morph @( + turtle c 0 0 0 0 -200 200 + turtle c 0 200 -200 0 -200 200 + turtle c 0 0 0 0 -200 200 + turtle c -200 0 0 200 -200 200 + turtle c 0 0 0 0 -200 200 + ) @colors + turtle morph @( + turtle teleport 100 0 c 0 0 0 0 0 200 + turtle teleport 100 0 c -100 0 100 200 0 200 + turtle teleport 100 0 c 0 0 0 0 0 200 + ) @colors + turtle morph @( + turtle teleport 0 100 c 0 0 0 0 200 0 + turtle teleport 0 100 c 0 -100 200 100 200 0 + turtle teleport 0 100 c 0 0 0 0 200 0 + ) @colors + ) + .EXAMPLE + #### Webs + # Turtle can draw webs + Turtle Spiderweb + .EXAMPLE + # Turtle can draw spiderwebs with any number of spokes and rings + Turtle Spiderweb 7 13 + .EXAMPLE + Turtle Spiderweb 7 13 + .EXAMPLE + # We can draw random webs + $spokes = Get-Random -Min 3 -Max 13 + $rings = Get-Random -Min 3 -Max (13 * 3) + turtle web 42 $spokes $rings morph @( + turtle web 42 $spokes $rings + turtle rotate ( + Get-Random -Max 360 + ) web 42 $spokes $rings + turtle web 42 $spokes $rings + ) stroke 'goldenrod' pathclass 'yellow-stroke' + .EXAMPLE + # We can draw a web with color and class + Turtle Spiderweb 7 13 stroke goldenrod pathclass 'yellow-stroke' + .EXAMPLE + # We can draw a random web with color and class + $spokes = Get-Random -Min 5 -Max 13 + $rings = Get-Random -Min 3 -Max (13 * 3) + turtle web 42 $spokes $rings morph @( + turtle web 42 $spokes $rings + turtle rotate ( + Get-Random -Min 90 -Max 360 + ) web 42 $spokes $rings + turtle web 42 $spokes $rings + ) stroke goldenrod pathclass 'yellow-stroke' + .EXAMPLE + #### L-Systems + # Turtle can draw a number of fractals turtle BoxFractal 42 4 .EXAMPLE # We can make a Board Fractal @@ -376,7 +793,7 @@ function Get-Turtle { turtle KochCurve 42 .EXAMPLE # We can make a Koch Snowflake - turtle KochSnowflake 42 + turtle KochSnowflake 42 .EXAMPLE # We can draw the Levy Curve turtle LevyCurve 42 6 @@ -393,8 +810,11 @@ function Get-Turtle { # We can show a binary tree turtle BinaryTree 42 4 .EXAMPLE - # We can also mimic plant growth + # We can make fractal plants turtle FractalPlant 42 4 + .EXAMPLE + # We can also make fractal shrubs + turtle FractalShrub 42 4 .EXAMPLE # The SierpinskiArrowHead Curve is pretty turtle SierpinskiArrowheadCurve 42 4 @@ -404,9 +824,9 @@ function Get-Turtle { .EXAMPLE # We can morph with no parameters to try to draw step by step # - # This will result in large files. + # This will result in large files, and may not work in all browsers # - # This may not work in all browsers for all graphics. + # For best results, adjust the precision turtle SierpinskiTriangle 42 3 morph .EXAMPLE # Let's draw two reflected Sierpinski Triangles @@ -427,7 +847,7 @@ function Get-Turtle { .EXAMPLE # We can draw a 'Sierpinski Snowflake' with multiple Sierpinski Triangles. turtle @('rotate', 30, 'SierpinskiTriangle',42,4 * 12) - .EXAMPLE + .EXAMPLE turtle @('rotate', 45, 'SierpinskiTriangle',42,4 * 24) #> [CmdletBinding(PositionalBinding=$false)] @@ -456,40 +876,140 @@ function Get-Turtle { # If the input object is not a turtle object, it will be ignored and a new turtle object will be created. [Parameter(ValueFromPipeline)] [PSObject] - $InputObject + $InputObject, + + # If set, will run as a background job. + [switch] + $AsJob ) begin { # Get information about our turtle pseudo-type. - $turtleType = Get-TypeData -TypeName Turtle + $turtleType = Get-TypeData -TypeName Turtle + $turtleTypes = @( + $turtleType + # Real types would work to, and we may support them in the future + # [Math] + ) + # any member name is a potential command - $memberNames = $turtleType.Members.Keys + $memberNames = @( + foreach ($typeInfo in $turtleTypes) { + if ($typeInfo.Members -is [Collections.IDictionary]) { + $typeInfo.Members.Keys + } + + <#elseif ($typeInfo -is [Type]) { + $typeInfo | Get-Member -Static | Select-Object -ExpandProperty Name + }#> + } + ) + + $helpfulKeywords = @( + '?' + '--help' + 'help' + '/help' + '/?' + ) + + filter getScriptHelp { + $scriptBlock = $_ + $Name = $args -join '' + $ExecutionContext.SessionState.PSVariable.Set("function:$Name",$scriptBlock) + if ($switches -is [Collections.IDictionary]) { + if ($switches.Syntax) { + Get-Command $Name -Syntax + } else { + Get-Help $Name @switches + } + } else { + Get-Help $Name + } + $ExecutionContext.SessionState.PSVariable.Remove("function:$Name") + } # We want to sort the member names by length, in case we need them in a pattern or want to sort quickly. $memberNames = $memberNames | Sort-Object @{Expression={ $_.Length };Descending=$true}, name # Create a new turtle object in case we have no turtle input. $currentTurtle = [PSCustomObject]@{PSTypeName='Turtle'} - $invocationInfo = $MyInvocation + # Grab our invocation information + $invocationInfo = $myInv = $MyInvocation + # and attach a script property to access this point in command history $invocationInfo | - Add-Member ScriptProperty History {Get-History -Id $this.HistoryId} -Force + Add-Member ScriptProperty History {Get-History -Id $this.HistoryId} -Force + + # Peek at our callstack + $myCallstack = @(Get-PSCallStack) + # and try to get our most recent few callers + foreach ($possibleCaller in $myCallstack[-1..-3]) { + # If we can, find the CommandAst that called us. + # (this will have the arugment list in a more useful form, and will help us recreate a call) + if (-not $possibleCaller.InvocationInfo.MyCommand.ScriptBlock.Ast) { continue } + $myCommandAst = + $possibleCaller.InvocationInfo.MyCommand.ScriptBlock.Ast.FindAll({ + param($ast) + $ast.Extent.StartLineNumber -eq $myInv.ScriptLineNumber -and + $ast.Extent.StartColumnNumber -eq $myInv.OffsetInLine -and + $ast -is [Management.Automation.Language.CommandAst] + },$true) + if ($myCommandAst) { + break + } + } } - process { + process { + # If we were piped in a Turtle, if ($PSBoundParameters.InputObject -and $PSBoundParameters.InputObject.pstypenames -eq 'Turtle') { + # make it the current turtle $currentTurtle = $PSBoundParameters.InputObject } elseif ($PSBoundParameters.InputObject) { # If input was passed, and it was not a turtle, pass it through. return $PSBoundParameters.InputObject } + #region -AsJob + # If we wanted to run a background job + if ($PSBoundParameters.AsJob) { + # remove the -AsJob variable from our parameters + $null = $PSBoundParameters.Remove('AsJob') + + # and then start a thread job that will import the module and run the command. + return Start-ThreadJob -ScriptBlock { + param([Collections.IDictionary]$IO) + Import-Module -Name $io.ModulePath + $argList = @($IO.ArgumentList) + if ($IO.InputObject) { + $io.InputObject | & $io.CommandName @argList + } else { + & $io.CommandName @argList + } + } -ArgumentList ( + [Ordered]@{ + ModulePath = $MyInvocation.MyCommand.ScriptBlock.Module.Path -replace '\.psm1$', '.psd1' + CommandName = $MyInvocation.MyCommand.Name + } + $PSBoundParameters + ) + } + #endregion -AsJob + if (-not $currentTurtle.Invocations) { $currentTurtle | Add-Member NoteProperty Invocations -Force @(,$invocationInfo) } elseif ($currentTurtle.Invocations -is [object[]]) { $currentTurtle.Invocations += $invocationInfo } + if ($myCommandAst) { + if (-not $currentTurtle.Commands) { + $currentTurtle | Add-Member NoteProperty Commands -Force @(,$myCommandAst) + } elseif ($currentTurtle.Commands -is [object[]]) { + $currentTurtle.Commands += $myCommandAst + } + } + # First we want to split each argument into words. # This way, it is roughly the same if you say: @@ -497,20 +1017,67 @@ function Get-Turtle { # * `turtle forward 10` # * `turtle 'forward', 10` $wordsAndArguments = @(foreach ($arg in $ArgumentList) { - # If the argument is a string, and it starts with whitespace + # If the argument is a string, and it starts with whitespace if ($arg -is [string]) { if ($arg -match '^[\r\n\s]+') { $arg -split '\s{1,}' } else { $arg - } - } else { + } + } else { # otherwise, leave the argument alone. $arg } }) - # Now that we have a series of words, we can process them. + # If any brackets are used, we want to balance them all now, and error if they appear unbalanced. + # Since we want to know the exact index, we walk thru matches + $depth = 0 + # and keep track of when it became unbalanced. + $unbalancedAt = $null + foreach ($match in [Regex]::Matches( + ($wordsAndArguments -join ' ' ), '[\[\]]' + ) + ) { + # To do this, we increment or decrement depth for brackets `[]` + if ($match.Value -eq '[') { $depth++} + if ($match.Value -eq ']') { $depth--} + # and, if the depth is ever negative, we are unbalanced. + if ($depth -lt 0) { + $unbalancedAt = $match; break + } + } + + # If the depth is still positive when we are done, + # we are also unbalanced + if ($depth -gt 0) { + # and we can consider our last bracket the point that needs to be balanced + $unbalancedAt = $match + } + + # If we are unbalanced, + if ($unbalancedAt) { + # write an error + Write-Error -Message "Unbalanced at index $($match.Index) +$( + # (try to make it a nice error by pointing out the match) + $str = $match.Result('$_') + if ($match.Index -ge 1) { + $str.Substring(0, $match.Index - 1) + if ($match.Index -lt ($str.Length - 1)) { + '-->' + } + } + $match.Value + '<--' + if ($match.Index -lt ($str.Length - 1)) { + $str.Substring($match.Index + 1) + } +) - $depth brackets off" # and by letting people know the depth difference. + return + } + + # Now that we have a series of balanced words, we can process them. # We want to keep track of the current member, # and continue to the next word until we find a member name. $currentMember = $null @@ -522,44 +1089,123 @@ function Get-Turtle { for ($argIndex =0; $argIndex -lt $wordsAndArguments.Length; $argIndex++) { $arg = $wordsAndArguments[$argIndex] # If the argument is not in the member names list, we can complain about it. - if ($arg -notin $memberNames) { - if (-not $currentMember -and $arg -is [string] -and "$arg".Trim()) { - Write-Warning "Unknown command '$arg'." + if ($arg -notin $memberNames) { + if ( + # (we might not want to, if it starts with a bracket) + -not $currentMember -and $arg -is [string] -and + "$arg".Trim() -and $arg -notmatch '^\[' + ) { + Write-Warning "Unknown command '$arg'." } continue - } + } + # If we have a current member, we can invoke it or get it. $currentMember = $arg - # We can also begin looking for arguments + $memberInfo = $turtleType.Members[$currentMember] + + if (-not $memberInfo) { + $memberInfo = foreach ($typeInfo in $turtleTypes) { + if ($typeInfo.Members -is [Collections.IDictionary] -and $typeInfo.Members[$currentMember]) { + $typeInfo; break + } + if ($typeInfo::$currentMember) { + $typeInfo::$currentMember + break + } + } + } + + # If it's an alias + if ($memberInfo.ReferencedMemberName) { + # try to resolve it. + $currentMember = $memberInfo.ReferencedMemberName + $memberInfo = $turtleType.Members[$currentMember] + } + + # We can also begin looking for arguments, as long as they are not bracketed. + $bracketDepth = 0 for ( - # at the next index. + # Let's start at the next index. $methodArgIndex = $argIndex + 1; - # We will continue until we reach the end of the words and arguments, - $methodArgIndex -lt $wordsAndArguments.Length -and - $wordsAndArguments[$methodArgIndex] -notin $memberNames; - $methodArgIndex++) { + # and continue until we reach the end of the words and arguments, + $methodArgIndex -lt $wordsAndArguments.Length; + $methodArgIndex++ + ) { + # Count our brackets + if ($wordsAndArguments[$methodArgIndex] -is [string]) { + $brackets = $wordsAndArguments[$methodArgIndex] -replace '[^\[\]]' + foreach ($bracket in $brackets.ToCharArray()) { + if ("$bracket" -eq '[') { $bracketDepth++ } + if ("$bracket" -eq ']') { $bracketDepth-- } + } + } + # If the next word is a method name, and our brackets are balanced + if ($wordsAndArguments[$methodArgIndex] -in $memberNames -and -not $bracketDepth) { + # break out of the loop. + break + } + } - # Now we know how long it took to get to the next member name. + # Now we know how far we had to look to get to the next member name. # And we can determine if we have any parameters. # (it is important that we always force any parameters into an array) + $HelpWanted = $false + $switches = [Ordered]@{} + $argList = @(if ($methodArgIndex -ne ($argIndex + 1)) { - $wordsAndArguments[($argIndex + 1)..($methodArgIndex - 1)] + # We only want to remove one pair of brackets + $debracketCount = 0 + foreach ($word in $wordsAndArguments[($argIndex + 1)..($methodArgIndex - 1)]) { + if ($word -in $helpfulKeywords) { + $HelpWanted = $true + continue + } + if ($HelpWanted -and $word -in 'example', 'examples', 'parameter','parameters','online') { + if ($word -in 'example','examples') { + $switches['Examples'] = $true + } + if ($word -in 'parameter','parameters') { + $switches['Parameters'] = '*' + } + if ($word -eq 'online') { + $switches['Online'] = $true + } + continue + } + if ($word -match '^[-/]+?[\D-[\.]]') { + $switchInfo = $word -replace '^[-/]+' + $switchName, $switchValue = $switchInfo -split ':', 2 + if ($null -eq ($switchName -as [double])) { + $switches[$switchName] = + if ($switchValue) { + $switchValue + } else { + $true + } + continue + } + } + # If the word started with a bracket, and we haven't removed any + if ("$word".StartsWith('[') -and -not $debracketCount) { + $word = $word -replace '^\[' # remove it + $debracketCount++ # and increment our removal counter. + } + # If the word ended with a bracket, and we have debracketed once + if ("$word".EndsWith(']') -and $debracketCount -eq 1) { + # remove the closing bracket + $word = $word -replace '\]$' + # and increment our removal counter + $debracketCount++ + } + $word # output the word into the array. + } $argIndex = $methodArgIndex - 1 }) - - # Look up the member information for the current member. - $memberInfo = $turtleType.Members[$currentMember] - # If it's an alias - if ($memberInfo.ReferencedMemberName) { - # try to resolve it. - $currentMember = $memberInfo.ReferencedMemberName - $memberInfo = $turtleType.Members[$currentMember] - } - - + # Now we want to get the output from the step. $stepOutput = if ( @@ -569,41 +1215,82 @@ function Get-Turtle { ) { # If we have arguments, if ($argList) { - # and we have a script method + # and a script method if ($memberInfo -is [Management.Automation.Runspaces.ScriptMethodData]) { - # set this to the current turtle - $this = $currentTurtle - # and call the script, splatting positional parameters - # (this allows more complex binding, like ValueFromRemainingArguments) - . $currentTurtle.$currentMember.Script @argList - } else { - # Otherwise, we pass the parameters directly to the method + # Check to see if we want help. + if ($HelpWanted) { + # If we do, get some help. + $memberInfo.Script | getScriptHelp $memberInfo.Name + } else { + # Otherwise, set `$this` to the current turtle + $this = $currentTurtle + # and call the script, splatting positional parameters + # (this allows more complex binding, like ValueFromRemainingArguments). + . $currentTurtle.$currentMember.Script @argList + } + } + elseif ($currentTurtle.$currentMember.Invoke) { $currentTurtle.$currentMember.Invoke($argList) + } elseif ($memberInfo.Invoke) { + $memberInfo.Invoke($argList) + } elseif ($memberInfo -is [ValueType]) { + $memberInfo + } + } + # If we don't have any arguments, but are still dealing with a method + else { + # If we want help, + if ($HelpWanted -and $memberInfo.Script) { + # get some help. + $memberInfo.Script | getScriptHelp $memberInfo.Name + } else { + # otherwise, invoke the method with no parameters. + $currentTurtle.$currentMember.Invoke() } - - } else { - # otherwise, just invoke the method with no arguments. - $currentTurtle.$currentMember.Invoke() } } else { # If the member is a property, we can get it or set it. # If we have any arguments, if ($argList) { - # Check to see if they are strongly typed - if ($memberInfo -is [Management.Automation.Runspaces.ScriptPropertyData]) { - $desiredType = $memberInfo.SetScriptBlock.Ast.ParamBlock.Parameters.StaticType - if ($desiredType -is [Type] -and - $argList.Length -eq 1 -and - $argList[0] -as $desiredType) { - $argList = $argList[0] -as $desiredType + # and we want help + if ($HelpWanted -and $memberInfo.SetScriptBlock) { + # get help about the set. + $memberInfo.SetScriptBlock | getScriptHelp $memberInfo.Name + } else { + # Otherwise, check to see if the arguments are strongly typed. + if ($memberInfo -is [Management.Automation.Runspaces.ScriptPropertyData]) { + $desiredType = $memberInfo.SetScriptBlock.Ast.ParamBlock.Parameters.StaticType + if ($desiredType -is [Type] -and + $argList.Length -eq 1 -and + $argList[0] -as $desiredType) { + $argList = $argList[0] -as $desiredType + } + } + + # And try to set the property. + try { + $currentTurtle.$currentMember = $argList + } catch { + # If that fails, + $ex = $_ + # use .WriteError for a cleaner error. + $PSCmdlet.WriteError($ex) } } - # lets try to set it. - $currentTurtle.$currentMember = $argList + } else { # otherwise, lets get the property - $currentTurtle.$currentMember + # If we are getting a script and we want help + if ($memberInfo.GetScriptBlock -and $HelpWanted) { + # momentarily turn that script into a function + $memberInfo.GetScriptBlock | getScriptHelp $memberInfo.Name + } + elseif ($null -ne $currentTurtle.$currentMember) { + $currentTurtle.$currentMember + } elseif ($memberInfo -is [ValueType]) { + $memberInfo + } } } diff --git a/Commands/Save-Turtle.ps1 b/Commands/Save-Turtle.ps1 index c8e3523..921a03e 100644 --- a/Commands/Save-Turtle.ps1 +++ b/Commands/Save-Turtle.ps1 @@ -3,11 +3,18 @@ function Save-Turtle { .SYNOPSIS Saves a turtle. .DESCRIPTION - Saves a turtle graphics pattern to a file. + Saves Turtle graphics to a file. .EXAMPLE - New-Turtle | - Move-Turtle SierpinskiTriangle 20 3 | - Save-Turtle "./SierpinskiTriangle.svg" + turtle SierpinskiTriangle 42 4 | + Save-Turtle ./SierpinskiTriangle-42-4.svg + .EXAMPLE + # We can save a turtle as a pattern by using `-Property Pattern` + turtle Flower 42 | + Save-Turtle ./Flower-42.svg -Property Pattern + .EXAMPLE + # We can also save a turtle as a pattern by naming the file with `Pattern` in it. + turtle Flower 42 10 6 36 | + Save-Turtle ./HexFlowerPattern.svg .EXAMPLE Move-Turtle BoxFractal 15 5 | Set-Turtle Stroke '#4488ff' | @@ -37,7 +44,7 @@ function Save-Turtle { } })] [string] - $Property = 'Symbol', + $Property = 'SVG', # The turtle input object. [Parameter(ValueFromPipeline)] @@ -47,27 +54,59 @@ function Save-Turtle { ) process { + # If there is no input, return if (-not $inputObject) { return } - switch -regex ($FilePath) { - '\.png$' { if ($Property -eq 'Symbol') { $Property = 'PNG' } } - '\.jpe?g$' { if ($Property -eq 'Symbol') { $Property = 'JPEG' } } - '\.webp$' { if ($Property -eq 'Symbol') { $Property = 'WEBP' } } - } - $toExport = $inputObject.$Property + # Auto detect property names from file names + $defaultToProperty = + switch -regex ($FilePath) { + '\.png$' { 'PNG' } + '\.jpe?g$' { 'JPEG' } + '\.webp$' { 'WEBP' } + 'PatternMask' { 'PatternMask'; break } + 'Mask' { 'Mask'; break } + 'Pattern' { 'Pattern'; break } + 'Symbol' { 'Symbol'; break } + default { 'SVG' } + } + + # If we have not provided a property and we know of a viable default, use that + if ($defaultToProperty -and -not $PSBoundParameters['Property']) { + $property = $PSBoundParameters['Property'] = $defaultToProperty + } + + # Get the value of our property + $toExport = $inputObject.$Property + + # If there is nothing there, return if (-not $toExport) { return } + + # Find the file path $unresolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FilePath) + # and create a file $null = New-Item -ItemType File -Force -Path $unresolvedPath - if ($toExport -is [xml]) { + # If we are exporting XML + if ($toExport -is [xml]) + { + # save it to that path $toExport.Save("$unresolvedPath") } - elseif ($toExport -is [byte[]]) { - Set-Content -Path $unresolvedPath -Value $toExport -AsByteStream - } else { + # If we are outputting bytes + elseif ($toExport -is [byte[]]) + { + # write them to the file + [IO.File]::WriteAllBytes("$unresolvedPath", $toExport) + } + # If we are outputting anything else + else + { + # simply redirect to the file $toExport > $unresolvedPath } + # If the last command worked if ($?) { - Get-Item -Path $unresolvedPath + # return the file + return (Get-Item -Path $unresolvedPath) } } } diff --git a/Commands/Show-Turtle.ps1 b/Commands/Show-Turtle.ps1 new file mode 100644 index 0000000..fbef234 --- /dev/null +++ b/Commands/Show-Turtle.ps1 @@ -0,0 +1,73 @@ +function Show-Turtle +{ + <# + .SYNOPSIS + Shows a Turtle + .DESCRIPTION + Shows a Turtle by opening it with the default file association. + .NOTES + It is highly recommended that you show turtles in a browser; + not all drawing programs support the full capabilities of SVG. + + There are a few circumstances where the Turtle will refuse to show. + + * If the session is not interactive + * If $env:GITHUB_WORKFLOW is present + * If $env:TURTLE_BOT is present + #> + [CmdletBinding(PositionalBinding=$false)] + param( + # The input object to show. + # This should be a turtle, or a file a .svg, + [Parameter(ValueFromPipeline)] + [PSObject] + $InputObject + ) + + begin { + $validExtensions = @( + '.svg', + '.png', + '.webp', + '.jpe?g', + '.html?' + ) + $extensionPattern = "(?>$($validExtensions -replace '\.','\.' -join '|'))$" + } + process { + # If we are not running interactively, + # we obviously do not want to try to show something on the screen. + if (-not [Environment]::UserInteractive -or $env:GITHUB_WORKFLOW -or $env:TURTLE_BOT) { + # Instead, just pass thru our input. + return $InputObject + } + if ($InputObject -is [IO.FileInfo] -and $InputObject.Extension -match $extensionPattern) { + Invoke-Item $InputObject.Fullname + } elseif ($InputObject.pstypenames -contains 'Turtle') { + New-Item -ItemType File -Path "./$($InputObject.id).svg" -Value "$InputObject" -Force | + Invoke-Item + } + elseif ($InputObject -is [string] -and $InputObject -match $extensionPattern) { + $gotItem = Get-Item -Path $InputObject + if ($gotItem) { + Invoke-Item $gotItem.FullName + } + } + elseif ($InputObject -is [xml]) { + $fileName = if ($InputObject.svg) { + "./$($InputObject.id).svg" + } + elseif ($InputObject.html) { + "./$($InputObject.id).html" + } + + if ($fileName) { + New-Item -ItemType File -Path $fileName -Value "$($InputObject.OuterXml)" -Force | + Invoke-Item + } + } + else { + Write-Error "Nothing to see here" + } + } +} diff --git a/Examples/BoxFractal.svg b/Examples/BoxFractal.svg index f8299f2..615fbda 100644 --- a/Examples/BoxFractal.svg +++ b/Examples/BoxFractal.svg @@ -1,9 +1,4 @@ - - - - - - - - + + + \ No newline at end of file diff --git a/Examples/EndlessBoxFractal.svg b/Examples/EndlessBoxFractal.svg index 8c9385f..011b319 100644 --- a/Examples/EndlessBoxFractal.svg +++ b/Examples/EndlessBoxFractal.svg @@ -1,15 +1,15 @@ - + - + + - \ No newline at end of file diff --git a/Examples/EndlessHilbert.svg b/Examples/EndlessHilbert.svg index 46b0869..c1e5132 100644 --- a/Examples/EndlessHilbert.svg +++ b/Examples/EndlessHilbert.svg @@ -1,15 +1,15 @@ - + - + + - \ No newline at end of file diff --git a/Examples/EndlessScissorPoly.svg b/Examples/EndlessScissorPoly.svg index 13d9c58..27d82ae 100644 --- a/Examples/EndlessScissorPoly.svg +++ b/Examples/EndlessScissorPoly.svg @@ -1,12 +1,12 @@ - + - + diff --git a/Examples/EndlessSierpinskiTrianglePattern.svg b/Examples/EndlessSierpinskiTrianglePattern.svg index c7f09fa..ed2591b 100644 --- a/Examples/EndlessSierpinskiTrianglePattern.svg +++ b/Examples/EndlessSierpinskiTrianglePattern.svg @@ -1,12 +1,12 @@ - + - + diff --git a/Examples/EndlessSnowflake.svg b/Examples/EndlessSnowflake.svg index c03eb0a..5d3d9f9 100644 --- a/Examples/EndlessSnowflake.svg +++ b/Examples/EndlessSnowflake.svg @@ -1,12 +1,12 @@ - + - + diff --git a/Examples/EndlessSpirolateral.svg b/Examples/EndlessSpirolateral.svg index 9a2c580..0ec7490 100644 --- a/Examples/EndlessSpirolateral.svg +++ b/Examples/EndlessSpirolateral.svg @@ -1,12 +1,12 @@ - + - + diff --git a/Examples/EndlessStepSpiral.svg b/Examples/EndlessStepSpiral.svg index 08ff440..531c6f4 100644 --- a/Examples/EndlessStepSpiral.svg +++ b/Examples/EndlessStepSpiral.svg @@ -1,10 +1,10 @@ - + - + diff --git a/Examples/FollowThatTurtle.svg b/Examples/FollowThatTurtle.svg index de654bc..1d2618a 100644 --- a/Examples/FollowThatTurtle.svg +++ b/Examples/FollowThatTurtle.svg @@ -1,19 +1,14 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/Examples/FollowThatTurtleHideAndSeek.svg b/Examples/FollowThatTurtleHideAndSeek.svg index 03891e7..79d8259 100644 --- a/Examples/FollowThatTurtleHideAndSeek.svg +++ b/Examples/FollowThatTurtleHideAndSeek.svg @@ -1,27 +1,22 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Examples/FollowThatTurtleHideAndSeekPattern.svg b/Examples/FollowThatTurtleHideAndSeekPattern.svg index 12c820c..fa5f53c 100644 --- a/Examples/FollowThatTurtleHideAndSeekPattern.svg +++ b/Examples/FollowThatTurtleHideAndSeekPattern.svg @@ -1,24 +1,24 @@ - - + + - + - + - + - + - + - + - + - + diff --git a/Examples/FollowThatTurtleNotTooClose.svg b/Examples/FollowThatTurtleNotTooClose.svg index 9fa55f5..f835922 100644 --- a/Examples/FollowThatTurtleNotTooClose.svg +++ b/Examples/FollowThatTurtleNotTooClose.svg @@ -1,19 +1,14 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/Examples/FollowThatTurtleNotTooClosePattern.svg b/Examples/FollowThatTurtleNotTooClosePattern.svg index 8f03135..8661968 100644 --- a/Examples/FollowThatTurtleNotTooClosePattern.svg +++ b/Examples/FollowThatTurtleNotTooClosePattern.svg @@ -1,16 +1,16 @@ - - + + - + - + - + - + diff --git a/Examples/FollowThatTurtlePattern.svg b/Examples/FollowThatTurtlePattern.svg index 0871a03..d6e2f9e 100644 --- a/Examples/FollowThatTurtlePattern.svg +++ b/Examples/FollowThatTurtlePattern.svg @@ -1,16 +1,16 @@ - - + + - + - + - + - + diff --git a/Examples/InscribedCircle.svg b/Examples/InscribedCircle.svg index e6088b8..4dc3f7c 100644 --- a/Examples/InscribedCircle.svg +++ b/Examples/InscribedCircle.svg @@ -1,15 +1,10 @@ - - - - - - - - - - - - - - + + + + + + + + + \ No newline at end of file diff --git a/Examples/InscribedCirclePattern.svg b/Examples/InscribedCirclePattern.svg index 986095e..992b3df 100644 --- a/Examples/InscribedCirclePattern.svg +++ b/Examples/InscribedCirclePattern.svg @@ -1,12 +1,12 @@ - + - + - + diff --git a/Examples/Keyframes-Moving-Square.svg b/Examples/Keyframes-Moving-Square.svg new file mode 100644 index 0000000..de11090 --- /dev/null +++ b/Examples/Keyframes-Moving-Square.svg @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/Examples/Keyframes-Wiggle-Square.svg b/Examples/Keyframes-Wiggle-Square.svg new file mode 100644 index 0000000..8a163c2 --- /dev/null +++ b/Examples/Keyframes-Wiggle-Square.svg @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/Examples/Keyframes-Wow.svg b/Examples/Keyframes-Wow.svg new file mode 100644 index 0000000..2eff79d --- /dev/null +++ b/Examples/Keyframes-Wow.svg @@ -0,0 +1,35 @@ + + + + + + wow +wow + + \ No newline at end of file diff --git a/Examples/Keyframes.turtle.ps1 b/Examples/Keyframes.turtle.ps1 new file mode 100644 index 0000000..b01e467 --- /dev/null +++ b/Examples/Keyframes.turtle.ps1 @@ -0,0 +1,58 @@ +<# +.SYNOPSIS + Turtles can now use keyframes +.DESCRIPTION + Turtles can now use CSS keyframes. + + Here are a few examples. +#> + +turtle id wiggle-square square 42 fill '#4488ff' stroke '#224488' keyframe ([Ordered]@{ + 'wiggle3d' = [Ordered]@{ + '0%,100%' = [Ordered]@{ + transform = "rotateX(-3deg) rotateY(-3deg) rotateZ(-3deg)" + } + '50%' = [Ordered]@{ + transform = "rotateX(3deg) rotateY(3deg) rotateZ(3deg)" + } + } +}) pathclass wiggle3d save ./Keyframes-Wiggle-Square.svg + + +turtle viewbox 84 id moving-square square 42 fill '#4488ff' stroke '#224488' keyframe ([Ordered]@{ + 'moving-in-3d' = [Ordered]@{ + '0%,100%' = [Ordered]@{ + transform = "translate3d(0ch, 2ch, 5em) rotateY(-180deg)" + } + '50%' = [Ordered]@{ + transform = "translate3d(5ch, 1ch, 5em) rotateY(0deg)" + } + } +}) pathclass moving-in-3d save ./Keyframes-Moving-Square.svg + +turtle id "wow-wow-wow-wow-wow" keyframe @{ + 'bigger-font' = [Ordered]@{ + '0%' = @{ + 'font-size' = '1rem' + } + '16%' = @{ + 'font-size' = '2rem' + } + '32%' = @{ + 'font-size' = '5rem' + } + '48%' = @{ + 'font-size' = '10rem' + } + '64%' = @{ + 'font-size' = '15rem' + } + '100%' = @{ + 'font-size' = '20rem' + } + } +} duration '00:00:01.68' TextAttribute @{ + class='bigger-font' +} text ["wow"] save ./Keyframes-Wow.svg + + diff --git a/Examples/SierpinskiTriangle.svg b/Examples/SierpinskiTriangle.svg index 0132c8f..b91de4c 100644 --- a/Examples/SierpinskiTriangle.svg +++ b/Examples/SierpinskiTriangle.svg @@ -1,9 +1,4 @@ - - - - - - - - + + + \ No newline at end of file diff --git a/Examples/TurtlesOnATextPath-ATurtleCircle.svg b/Examples/TurtlesOnATextPath-ATurtleCircle.svg index 3f02c65..d0f3c51 100644 --- a/Examples/TurtlesOnATextPath-ATurtleCircle.svg +++ b/Examples/TurtlesOnATextPath-ATurtleCircle.svg @@ -1,12 +1,8 @@ - - - - - - - a turtle circle - - - - + + + + + a turtle circle + a turtle circle + \ No newline at end of file diff --git a/Examples/TurtlesOnATextPath-BendMorph.svg b/Examples/TurtlesOnATextPath-BendMorph.svg new file mode 100644 index 0000000..5576653 --- /dev/null +++ b/Examples/TurtlesOnATextPath-BendMorph.svg @@ -0,0 +1,9 @@ + + + + + + turtles on a text path + turtles on a text path + + \ No newline at end of file diff --git a/Examples/TurtlesOnATextPath-Morph.svg b/Examples/TurtlesOnATextPath-Morph.svg index 215deb5..281d1f9 100644 --- a/Examples/TurtlesOnATextPath-Morph.svg +++ b/Examples/TurtlesOnATextPath-Morph.svg @@ -1,17 +1,13 @@ - - - - - - - - turtles on a text path - - - - - - - - + + + + + + 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 58553aa..990820a 100644 --- a/Examples/TurtlesOnATextPath.svg +++ b/Examples/TurtlesOnATextPath.svg @@ -1,12 +1,8 @@ - - - - - - - turtles on a text path - - - - + + + + + turtles on a text path + turtles on a text path + \ No newline at end of file diff --git a/Examples/TurtlesOnATextPath.turtle.ps1 b/Examples/TurtlesOnATextPath.turtle.ps1 index 49f3008..4cb80ee 100644 --- a/Examples/TurtlesOnATextPath.turtle.ps1 +++ b/Examples/TurtlesOnATextPath.turtle.ps1 @@ -1,10 +1,14 @@ if ($PSScriptRoot) { Push-Location $PSScriptRoot} -$turtlesOnATextPath = turtle rotate 90 jump 50 rotate -90 ArcRight 50 60 text 'turtles on a text path' textattribute @{'font-size'=36} +$turtlesOnATextPath = turtle rotate 90 jump 50 rotate -90 ArcRight 500 60 text 'turtles on a text path' textattribute @{'font-size'=36} $turtlesOnATextPath | Save-Turtle ./TurtlesOnATextPath.svg +$turtlesOnATextPath = $turtlesOnATextPath.Morph(@( + $turtlesOnATextPath + turtle rotate 90 jump 50 rotate -90 ArcRight 500 -60 + $turtlesOnATextPath +)) - -$textPath2 = turtle rotate 90 jump 50 rotate -90 ArcRight 50 -60 +$turtlesOnATextPath | Save-Turtle ./TurtlesOnATextPath-BendMorph.svg $turtlesOnATextPath = turtle rotate 90 jump 50 rotate -90 rotate -30 forward 200 text 'turtles on a text path' morph @( diff --git a/README.md b/README.md index dd4fd14..c6daace 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,10 @@ We can easily keep a list of these steps in memory, and draw them with [SVG](htt We can make Turtle in any language. -This module makes Turtle in PowerShell. +This module gives you Turtle in a PowerShell. + +* [Amazing Examples](https://psturtle.com/Commands/Get-Turtle) +* [A Brief History of Turtles](https://psturtle.com/History) ### Installing and Importing We can install Turtle from the PowerShell Gallery: @@ -306,10 +309,10 @@ At the most basic, let's make an inscribed circle and square: A simple example of turtles containing turtles #> $inscribedCircle = - turtle width 42 height 42 turtles @{ + 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/README.md.ps1 b/README.md.ps1 index 9dcb206..8f0c276 100644 --- a/README.md.ps1 +++ b/README.md.ps1 @@ -50,7 +50,10 @@ We can easily keep a list of these steps in memory, and draw them with [SVG](htt We can make Turtle in any language. -This module makes Turtle in PowerShell. +This module gives you Turtle in a PowerShell. + +* [Amazing Examples](https://psturtle.com/Commands/Get-Turtle) +* [A Brief History of Turtles](https://psturtle.com/History) "@ #endregion Introduction diff --git a/Turtle.psd1 b/Turtle.psd1 index eb328e7..79875e2 100644 --- a/Turtle.psd1 +++ b/Turtle.psd1 @@ -1,8 +1,8 @@ @{ # Version number of this module. - ModuleVersion = '0.2.0' + ModuleVersion = '0.2.1' # Description of the module - Description = "Turtles in a PowerShell" + Description = "Turtle Graphics in PowerShell" # Script module or binary module file associated with this manifest. RootModule = 'Turtle.psm1' # ID used to uniquely identify this module @@ -21,8 +21,9 @@ 'Get-Turtle', 'Move-Turtle', 'New-Turtle', + 'Save-Turtle', 'Set-Turtle', - 'Save-Turtle' + 'Show-Turtle' # Format files (.ps1xml) to be loaded when importing this module # FormatsToProcess = @() # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. @@ -31,37 +32,134 @@ PrivateData = @{ PSData = @{ # Tags applied to this module. These help with module discovery in online galleries. - Tags = 'PowerShell', 'Turtle', 'SVG', 'Graphics', 'Drawing', 'L-System', 'Fractal' + Tags = 'PowerShell', 'Turtle', 'Graphics', 'TurtleGraphics', 'SVG', 'Drawing', 'L-System', 'Fractal' # A URL to the main website for this project. ProjectURI = 'https://github.com/PowerShellWeb/Turtle' # A URL to the license for this module. LicenseURI = 'https://github.com/PowerShellWeb/Turtle/blob/main/LICENSE' ReleaseNotes = @' -## Turtle 0.2: +## Turtle 0.2.1: -### 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) +* New Documentation: + * Over 130 examples! + * A Brief History of Turtles (#249) +* Website improvements + * Copy Code Button! (#331) + * Improved layout and new backgrounds (#333) + * Improving build (#344) + * Defaulting palette selection (#346) +* Major improvements + * A turtle can now be any element! + * Support for CSS keyframes, styles, and variables! + * Vastly expanded SVG support, including bezier curves! + * CircleArcs and Pie Graphs! Improvements to circles. +* `Turtle` command improvements: + * `Get-Turtle` + * `Get-Turtle` help (#273) ( `turtle flower help` `turtle flower help examples`) + * `Get-Turtle` now tracks commands (#250) + * `Get-Turtle` now supports brackets (#255) and prebalances them (#262) + * `Get-Turtle -AsJob` (#268) + * `Get-Turtle` improved set errors (#252) + * `Save-Turtle` + * `Save-Turtle` saves as SVG by default (#259) + * `Save-Turtle` autosaves by name (#269) + * `Show-Turtle` will show the turtle (#257) +* New methods: + * `Turtle.a/Arc` (#231) + * `Turtle.b/BezierCurve` (#228) + * `Turtle.CircleArc` (#235) + * `Turtle.c/CubicBezierCurve` (#230) + * `Turtle.FractalShrub` (#332) + * `Turtle.Leg` (#288) + * `Turtle.Pie/PieGraph` (#239) + * `Turtle.q/QuadraticBezierCurve` (#229) + * `Turtle.Repeat` (#256) + * `Turtle.Spider` (#289) + * `Turtle.Spiderweb` (#290) + * `Turtle.Spokes` (#291) + * `Turtle.Sun` (#297) + * `Turtle.Show` (#258) +* New properties: + * `Turtle.get_ArgumentList` (#296) + * `Turtle.get/set_Attribute` (#247) + * `Turtle.get/set_Class` (#237) + * `Turtle.get_Commands` (#250) + * `Turtle.get_DataBlock` (#284) + * `Turtle.get/set_Element` (#248) + * `Turtle.get/set_Defines` (#243) + * `Turtle.get_ScriptBlock` (#253) + * `Turtle.get/set_Defines` (#243) + * `Turtle.get/set_Keyframe(s)` (#251) + * `Turtle.get_History` (#279) + * `Turtle.get/set_Link/Href` (#241) + * `Turtle.get/set_Locale` (#300) + * `Turtle.get_Marker` (#227) + * `Turtle.get/set_MarkerEnd` (#233) + * `Turtle.get/set_MarkerMiddle` (#234) + * `Turtle.get/set_MarkerStart` (#232) + * `Turtle.get/set_Opacity` (#293) + * `Turtle.get/set_Precision` (#225) + * `Turtle.ResizeViewBox` (#238) + * `Turtle.get/set_Start` (#245) + * `Turtle.get/set_Style` (#254) + * `Turtle.get/set_Variable` (#263) + * `Turtle.get/set_Title` (#285) +* New pseudo type: + * `Turtle.History` + * `Turtle.History.ToString()` (#282) + * `Turtle.History.DefaultDisplay` (#283) + * `Turtle.js` (experimental) + * Javascript version of turtle (#302) + * Initial Core Operations: + * `Turtle.js.heading` (#303) + * `Turtle.js.rotate` (#304) + * `Turtle.js.forward` (#305) (#337) (#338) + * `Turtle.js.isPenDown` (#306) + * `Turtle.js.goTo` (#307) + * `Turtle.js.step` (#308) + * `Turtle.js.teleport` (#309) (#334) + * `Turtle.js.steps` (#310) + * `Turtle.js.min` (#311) + * `Turtle.js.max` (#312) + * `Turtle.js.resize` (#313) + * `Turtle.js.x` (#314) + * `Turtle.js.y` (#315) + * `Turtle.js.width` (#316) + * `Turtle.js.height` (#317) + * `Turtle.js.pathData` (#318) (#339) + * `Turtle.js.polygon` (#319) (#336) (#338) + * `Turtle.js.penUp` (#322) + * `Turtle.js.penDown` (#323) + * `Turtle.js.parse` (#327) + * `Turtle.js.go` (#330) + * `Turtle.js.ToString.ps1()` (#320) + * `Turtle.js.get_JavaScript.ps1` (#324) + * Thanks @ninmonkey for early testing! +* Improved methods: + * `Turtle.ArcLeft/ArcRight` allows StepCount (#272) + * `Turtle.Circle` optimization (#287) + * `Turtle.FractalPlant` improvement (#271) + * `Turtle.HorizontalLine` is mapped to SVG `h` (#280) + * `Turtle.VerticalLine` is mapped to SVG `v` (#281) +* Improvemented Properties: + * Adding `[OutputType([xml])]` to properties that output XML (#266) + * `Turtle.get_Duration` defaults (#270) + * `Turtle.get_Mask/PatternMask` returns only the mask (#261) + * `Turtle.set_BackgroundColor` applies to SVG directly (#260) + * `Turtle.get_Maximum` is a vector (#275) + * `Turtle.get_Minimum` is a vector (#276) + * `Turtle.get_Position` is a vector (#274) + * `Turtle.set_Stroke` supports gradients (#295) + * `Turtle.set_Fill` supports gradients (#294) + * `Turtle.set_PathAnimation` will not overwrite a morph (#244) + * `Turtle.get/set_PatternAnimation` uses duration (#299) and improved docs (#298) + * `Turtle.get_TextElement` defaults to centered text (#265) + * `Turtle.get_TextElement` improved color support (#292) + * `Turtle.get_ViewBox` negative bounds (#286) +* More aliases: + * Added Internationalized Aliases (i.e. `Turtle.BackgroundColour`) (#236) + * SVG syntax aliases (#240) +* Fixed extra output in `Turtle.Pop` (#264) --- diff --git a/Turtle.tests.ps1 b/Turtle.tests.ps1 index 2607e14..98f8c63 100644 --- a/Turtle.tests.ps1 +++ b/Turtle.tests.ps1 @@ -24,18 +24,21 @@ describe Turtle { $t = turtle ArcRight $Radius 360 $Heading = 180.0 [Math]::Round($t.Width,1) | Should -Be ($Radius * 2) + [Math]::Round($t.Height,1) | Should -Be ($Radius * 2) [Math]::Round($t.Heading,1) | Should -Be 360.0 $Radius = 1 $Heading = 180.0 $t = turtle ArcRight $Radius 180 - [Math]::Round($t.Width,1) | Should -Be ($Radius * 2) + [Math]::Round($t.Width,1) | Should -Be $Radius + [Math]::Round($t.Height,1) | Should -Be ($Radius * 2) [Math]::Round($t.Heading,1) | Should -Be $Heading $Radius = 1 $Heading = 90.0 $t = turtle ArcRight $Radius $Heading - [Math]::Round($t.Width,1) | Should -Be ($Radius * 4) + [Math]::Round($t.Width,1) | Should -Be $Radius + [Math]::Round($t.Height,1) | Should -Be $Radius [Math]::Round($t.Heading,1) | Should -Be $Heading } @@ -56,15 +59,33 @@ describe Turtle { $turtle = $turtle.Rotate($turtle.Towards(1,1)) $turtle = $turtle.Forward($turtle.Distance(1,1)) $turtle.Heading | Should -be 45 - [Math]::Round($turtle.Position.X,10) | Should -be 1 - [Math]::Round($turtle.Position.Y,10) | Should -be 1 + [Math]::Round($turtle.Position.X,$turtle.Precision) | Should -be 1 + [Math]::Round($turtle.Position.Y,$turtle.Precision) | Should -be 1 $turtle = $turtle.Rotate($turtle.Towards(2,2)) $turtle = $turtle.Forward($turtle.Distance(2,2)) $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 + [Math]::Round($turtle.Position.X,$turtle.Precision) | Should -be 2 + [Math]::Round($turtle.Position.Y,$turtle.Precision) | Should -be 2 } } - + context 'Turtle Security' { + it 'Can run in a data block' { + $dataBlockTurtle = data -supportedCommand turtle, Get-Random { + turtle rotate 45 forward (Get-Random -Min 21 -Max 42) + } + $dataBlockTurtle.Heading | Should -Be 45 + } + it 'Will not show a turtle in non-interactive mode' { + if ([Environment]::UserInteractive -and -not $env:GITHUB_WORKFLOW) { + Write-Warning "Cannot test non-iteractivity interactively" + } else { + $dataBlockTurtle = data -supportedCommand turtle, Get-Random { + turtle rotate 45 forward (Get-Random -Min 21 -Max 42) show + } + $dataBlockTurtle.Heading | Should -Be 45 + } + } + } } + diff --git a/Turtle.types.ps1xml b/Turtle.types.ps1xml index cd5361c..95eff6a 100644 --- a/Turtle.types.ps1xml +++ b/Turtle.types.ps1xml @@ -15,6 +15,10 @@ + + a + Arc + ArcL ArcLeft @@ -23,14 +27,54 @@ ArcR ArcRight + + Args + ArgumentList + + + Argument + ArgumentList + + + Arguments + ArgumentList + + + Arm + Leg + Back Backward + + BackgroundColour + BackgroundColor + + + BézierCurve + BezierCurve + bk Backward + + c + CubicBezierCurve + + + Cobweb + Spiderweb + + + CubicBézierCurve + CubicBezierCurve + + + Defs + Defines + down PenDown @@ -39,6 +83,10 @@ fd Forward + + FillColour + FillColor + FlowerGolden GoldenFlower @@ -47,13 +95,25 @@ FlowerStar StarFlower + + h + HorizontalLine + HLineBy HorizontalLine + + Href + Link + + + Keyframes + Keyframe + l - Left + Step LineTo @@ -63,6 +123,10 @@ lt Left + + MarkerMid + MarkerMiddle + MoveTo Teleport @@ -71,18 +135,38 @@ pd PenDown + + PenColour + PenColor + + + Pie + PieGraph + pu PenUp + + q + QuadraticBezierCurve + + + QuadraticBézierCurve + QuadraticBezierCurve + r - Right + Rotate rt Right + + s + BezierCurve + SetPos GoTo @@ -91,14 +175,54 @@ SetPosition GoTo + + SierpińskiArrowHeadCurve + SierpinskiArrowHeadCurve + + + SierpińskiCurve + SierpinskiCurve + + + SierpińskiSquareCurve + SierpinskiSquareCurve + + + SierpińskiTriangle + SierpinskiTriangle + + + Spoke + Spokes + + + Stick + Sticks + + + Sticks + Spokes + + + Styles + Style + up PenUp + + v + VerticalLine + VLineBy VerticalLine + + Web + Spiderweb + xPos xcor @@ -107,6 +231,74 @@ yPos ycor + + Arc + + ArcLeft @@ -312,22 +515,97 @@ return $this + + + + BezierCurve + BinaryTree + + + CircleArc + @@ -540,6 +1014,234 @@ return $this.LSystem('F+F+F+F', [Ordered]@{ 'F' = { $this.Forward($Size) } }) + + + + CubicBezierCurve + @@ -692,30 +1394,83 @@ param( $Distance = 10 ) -$x = $Distance * ([math]::cos($this.Heading * [Math]::PI / 180)) -$y = $Distance * ([math]::sin($this.Heading * [Math]::PI / 180)) - +$heading = $this.Heading +$x = $Distance * [math]::cos($heading * [Math]::PI / 180) +$y = $Distance * [math]::sin($heading * [Math]::PI / 180) return $this.Step($x, $y) + FractalPlant + + + FractalShrub + @@ -916,15 +1671,25 @@ return $this.Teleport(0,0) Draws a horizontal line. The heading will not be changed. +.EXAMPLE + turtle HorizontalLine 42 +.EXAMPLE + turtle HorizontalLine 42 pathdata #> param( [double] $Distance ) - -$this.GoTo($this.Position.X + $Distance, $this.Position.Y) - +$instruction = + if ($this.IsPenDown) { + "h $Distance" + } else { + "m $($this.Position.X + $Distance) 0" + } +$this.Position = $Distance,0 +$this.Steps.Add($instruction) +return $this @@ -1004,9 +1769,9 @@ return $this.LSystem('F', @{ $turtle.Pattern.Save("$pwd/KochIsland2.svg") #> param( - [double]$Size = 20, - [int]$Order = 3, - [double]$Angle = 90 + [double]$Size = 42, + [int]$Order = 4, + [double]$Angle = -90 ) return $this.LSystem('W', [Ordered]@{ @@ -1075,6 +1840,56 @@ param( $this.Rotate($Angle * -1) + + Leg + + LevyCurve + + + PieGraph + @@ -1674,32 +2804,118 @@ return $this.Rotate((360 / $SideCount) * $remainder).Forward($remainder * $Size) Pop Push + + + QuadraticBezierCurve + @@ -1736,6 +2952,167 @@ $this. Forward($height).Rotate(90) + + Repeat + + + + ResizeViewBox + + Right - SierpinskiArrowheadCurve + Show + + + SierpinskiArrowheadCurve + + + Spider + + + + Spiderweb + + Spirolateral + + Spokes + + Square + + + Sun + @@ -2383,7 +4207,7 @@ $X, $Y ) -$deltaX = $x - $this.X +$deltaX = $x - $this.X $deltaY = $y - $this.Y $penState = $this.IsPenDown $this.IsPenDown = $false @@ -2475,9 +4299,23 @@ return $this.LSystem('F+F+F+F', [Ordered]@{ ToString @@ -2679,14 +4517,26 @@ return $this.LSystem('FX+FX+', [Ordered]@{ Draws a vertical line. The heading will not be changed. +.EXAMPLE + turtle VerticalLine 42 +.EXAMPLE + turtle VerticalLine 42 pathdata #> param( +# The length of the line. [double] $Distance ) -$this.GoTo($this.Position.X, $this.Position.Y + $Distance) - +$instruction = + if ($this.IsPenDown) { + "v $Distance" + } else { + "m 0 $($this.Position.Y + $Distance)" + } +$this.Position = 0, $Distance +$this.Steps.Add($instruction) +return $this @@ -2704,13 +4554,26 @@ $this.GoTo($this.Position.X, $this.Position.Y + $Distance) AnimateMotion - @("<animateMotion dur='$( - if ($this.AnimateMotionDuration) { - $this.AnimateMotionDuration + <# +.SYNOPSIS + Gets a Turtle's animation motion +.DESCRIPTION + Gets a Turtle's path as an animation motion. + + This only provides the animation path of this turtle, not any turtles contained within this turtle. +#> +[OutputType([xml])] +param() + +[xml]@( +"<animateMotion dur='$( + if ($this.Duration -is [TimeSpan]) { + "$($this.Duration.TotalSeconds)s" } else { "$(($this.Points.Length / 2 / 10))s" } -)' repeatCount='indefinite' path='$($this.PathData)' />") -as [xml] +)' repeatCount='indefinite' path='$($this.PathData)' /> +") @@ -2743,6 +4606,99 @@ $this | Add-Member -MemberType NoteProperty -Force -Name '.AnimateMotionDuration + + ArgumentList + + <# +.SYNOPSIS + Gets the Turtle's arguments +.DESCRIPTION + Gets a list of the arguments passed to the Turtle. + + We can reuse these arguments to recreate the Turtle. +.NOTES + This will directly output each of the arguments, with the exception of `ArgumentList` + (and any aliases to ArgumentList) +.EXAMPLE + turtle rotate 45 forward 42 arguments +#> +if (-not $this.Invocations) { return } +foreach ($arg in $this.Invocations.BoundParameters['ArgumentList']) { + if ($arg -notin 'ArgumentList', 'Arguments', 'Args','Argument') { + $arg + } +} + + + + Attribute + + <# +.SYNOPSIS + Gets Turtle attributes +.DESCRIPTION + Gets attributes of the turtle. + + These attributes apply directly to the Turtle as an `.Element`. + + They can also be targeted to apply to an aspect of the turtle, such as it's Pattern, Path, Text, Mask, or Marker. + + To set an attribute that targets an aspect of the turtle, prefix it with the name followed by a slash. + + (for example `path/data-key` would set an attribute on the path) +.EXAMPLE + turtle attribute @{someKey='someValue'} attribute +#> +if (-not $this.'.Attributes') { + $this | Add-Member NoteProperty '.Attributes' ([Ordered]@{}) -Force +} +return $this.'.Attributes' + + + <# +.SYNOPSIS + Sets Turtle attributes +.DESCRIPTION + Sets arbitrary attributes for the current Turtle. + + Attributes generally apply to the topmost tag. + + If an attribute contains a slash, it will be targeted to tags of that type. +.EXAMPLE + turtle attribute @{foo='bar'} attribute +.EXAMPLE + turtle attribute 'foo=bar' attribute +#> +param( +[PSObject[]] +$Attribute = [Ordered]@{} +) + +$myAttributes = $this.Attribute +foreach ($attrSet in $Attribute) { + if ($attrSet -is [Collections.IDictionary]) { + foreach ($key in $attrSet.Keys) { + $myAttributes[$key] = $attrSet[$key] + } + } + elseif ($attrSet -is [string]) { + if ($attrSet -match '[:=]') { + $key, $value = $attrSet -split '[:=]', 2 + $myAttributes[$key] = $value + } else { + $myAttributes[$key] = '' + } + } + elseif ($attrSet -is [PSObject]) { + foreach ($key in $attrSet.psobject.properties.name) { + $myAttributes[$key] = $attrSet.$key + } + } + +} + + + BackgroundColor @@ -2808,20 +4764,83 @@ window.onload = async function() { - ClipPath - - "clip-path: path(`"$($this.PathData)`");" - - - - DataURL + Class <# .SYNOPSIS - Gets the turtle data URL. + Gets a Turtle's class .DESCRIPTION - Gets the turtle symbol as a data URL. - + Gets any CSS classes associated with the turtle +.EXAMPLE + turtle class foo bar baz bing class +#> +return $this.SVGAttribute["class"] -split '\s+' + + + + <# +.SYNOPSIS + Sets a Turtle's class +.DESCRIPTION + Sets any CSS classes associated with the turtle +.EXAMPLE + turtle class foo bar baz bing class +#> +param( +$Class +) + +$this.Attribute['class'] = $this.SVGAttribute['class'] = $this.PathAttribute['class'] = $Class -join ' ' + + + + + ClipPath + + "clip-path: path(`"$($this.PathData)`");" + + + + DataBlock + + <# +.SYNOPSIS + Gets a Turtle as data block +.DESCRIPTION + Gets our Turtle as a data block that will recreate our Turtle. + + The only commands that can be used in the data block are: `Turtle`, `Get-Turtle`, and `Get-Random` +.NOTES + PowerShell data blocks provide a much more limited syntax. + + They can only use simple expressions, cannot declare variables, use loops, declare script blocks, or use most types. + + They can also be declared with whitelist of Supported Commands. + + This property will return the current turtle inside of a data block, if possible. + + If any errors occur during conversion, they will be present in `$error`. +.LINK + https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_data_sections?wt.mc_id=MVP_321542 +#> +[OutputType([ScriptBlock])] +param() +[ScriptBlock]::Create("data -supportedCommand turtle, Get-Turtle, Get-Random { + $($this.ScriptBlock) +}") + + + + + + DataURL + + <# +.SYNOPSIS + Gets the turtle data URL. +.DESCRIPTION + Gets the turtle symbol as a data URL. + This can be used as an inline image in HTML, CSS, or Markdown. #> $thisSVG = $this.SVG @@ -2830,6 +4849,88 @@ $thisSVG = $this.SVG )" + + Defines + + if ($this.'.Defines') { + return $this.'.Defines' +} + + + + <# +.SYNOPSIS + Sets the Turtle Path Animation +.DESCRIPTION + Sets an animation for the Turtle path. +.EXAMPLE + $t = turtle defines @( + "<radialGradient id='gradient'> + <stop offset='33%' stop-color='red' /> + <stop offset='66%' stop-color='green' /> + <stop offset='100%' stop-color='blue' /> + </radialGradient>" + "<radialGradient id='gradient2'> + <stop offset='33%' stop-color='blue' /> + <stop offset='66%' stop-color='green' /> + <stop offset='100%' stop-color='red' /> + </radialGradient>" + + ) flower 42 fill 'url("#gradient")' stroke 'url("#gradient2")' + $t | turtle save ./gradient.svg +.EXAMPLE + $t = turtle defines @( + "<radialGradient id='gradient'> + <stop offset='33%' stop-color='red' /> + <stop offset='66%' stop-color='green' /> + <stop offset='100%' stop-color='blue' /> + </radialGradient>" + "<radialGradient id='gradient2'> + <stop offset='33%' stop-color='blue' /> + <stop offset='66%' stop-color='green' /> + <stop offset='100%' stop-color='red' /> + </radialGradient>" + ) width 100 height 100 teleport 50 50 StarFlower 42 14.4 6 25 fill 'url("#gradient")' stroke 'url("#gradient2")' fillrule evenodd morph @( + turtle teleport 50 50 StarFlower 42 12 5 30 + turtle teleport 50 50 StarFlower 42 14.4 6 25 + turtle teleport 50 50 StarFlower 42 12 5 30 + ) PathAnimation ( [Ordered]@{ + type = 'rotate' ; values = 0, 360 ;repeatCount = 'indefinite'; dur = "4.2s" + }) + $t | turtle save ./gradientrotate.svg +#> +param( +# The definition object. +# This may be a string, XML, a dictionary containing defines, or an element +[PSObject] +$Defines +) + +$newDefinition = @(foreach ($definition in $Defines) { + if ($definition -is [Collections.IDictionary]) { + $definitionCopy = [Ordered]@{} + $definition + "<$elementName $( + @(foreach ($key in $definitionCopy.Keys) { + if ($key -eq 'Children') { continue } + " $key='$([Web.HttpUtility]::HtmlAttributeEncode($definitionCopy[$key]))'" + }) -join '' + )$()>" + } + elseif ($definition -is [string]) { + $definition + } + elseif ($definition.OuterXml) { + $definition.OuterXml + } + else { + "$definition" + } +}) + +$this | Add-Member -MemberType NoteProperty -Force -Name '.Defines' -Value $newDefinition + + + Duration @@ -2838,9 +4939,14 @@ $thisSVG = $this.SVG Gets the duration .DESCRIPTION Gets the default duration of animations and morphs. + + By default, 4.2 seconds. #> -if ($this.'.Duration') { return $this.'.Duration'} -return +if ($null -eq $this.'.Duration') { + $this | Add-Member NoteProperty '.Duration' ([timespan]::FromSeconds(4.2)) -Force +} +return $this.'.Duration' + <# @@ -2878,19 +4984,350 @@ if (($this.'.Duration' -is [TimeSpan]) -and $this.PathAnimation) { } + + Element + + <# +.SYNOPSIS + Gets a Turtle as an Element +.DESCRIPTION + We can treat any Turtle as any arbitrary markup element. + + To do this, all we need to do is set an element name, and, optionally, add some attributes or children. +.EXAMPLE + # Any bareword can become the name of an element, as long as it is not a method name + turtle element div element +.EXAMPLE + # We can provide anything that will cast to XML as an element + turtle element '<div/>' element +.EXAMPLE + # We can provide an element and attributes + turtle element '<div class='myClass' />' element +.EXAMPLE + # We can put a turtle inside of an aribtrary element + turtle SpiderWeb element '<div />' +#> + +# If we have set an element name +if ($this.'.Element'.ElementName) { + + # make this little filter to recursively turn the element back into XML + filter toElement { + $in = $_ + # If the input was a dictionary with an element name + if ($in -is [Collections.IDictionary] -and $in.ElementName) { + # start the markup + "<$($in.ElementName)$( + # and pop in any element attributes + foreach ($attributeCollection in 'attr','attribute','attributes') { + if (-not $in.$attributeCollection) { continue } + if ($in.$attributeCollection -is [Collections.IDictionary]) { + foreach ($attributeName in $in.$attributeCollection.Keys) { + if ($attributeName -match '/') { continue } + ' ', $attributeName,"='",$in.$attributeCollection[$attributeName],"'" -join '' + } + } elseif ($in.$attributeCollection -is [string]) { + $in.$attributeCollection + } + break + } + )>$( + # Now include any child elements. + # First, if we have drawn anything in our turtle, include that + if ($this.Steps -or $this.Text -or $this.Turtles.Count) { + $this.SVG.OuterXml + } + @(foreach ($childCollection in 'child','ChildNodes','Children','Content') { + if (-not $in.$childCollection) { + continue + } + foreach ($child in $in.$childCollection) { + # strings are directly included + if ($child -is [string]) { + $child + } elseif ($child -is [xml] -or $child -is [xml.xmlElement]) { + # xml elements will embed themselves + $child.OuterXml + } elseif ($child -is [Collections.IDictionary] -and $child.ElementName) { + # and dictionaries with an element name will recurisvely call ourselves. + $child | & $MyInvocation.MyCommand.ScriptBlock + } else { + # Any other input will be stringified + "$child" + } + } + break + }) -join ([Environment]::NewLine) + )</$($in.ElementName)>" + } + if ($_ -is [string]) { + $_ + } + } + + $elementMarkup = $this.'.Element' | toElement + $elementXml = $elementMarkup -as [xml] + if ($elementXml) { + $elementXml + } else { + $elementMarkup + } + return +} +else { + return $this.SVG +} + +return + + + + <# +.SYNOPSIS + Sets the Turtle element +.DESCRIPTION + Sets the Turtle to an arbitrary element. + + This lets us write web pages and xml entirely in turtle. +.EXAMPLE + # Any bareword can become the name of an element, as long as it is not a method name + turtle element div element +.EXAMPLE + # We can provide anything that will cast to XML as an element + turtle element '<div/>' element +.EXAMPLE + # We can provide an element and attributes + turtle element '<div class='myClass' />' element +.EXAMPLE + # We can put a turtle inside of an aribtrary element + turtle SpiderWeb element '<div />' +#> + +param() + +if (-not $this.'.Element') { + $this | Add-Member NoteProperty '.Element' -Value ([Ordered]@{ + ElementName='' + Attribute=$this.Attribute + Children=@() + }) +} + +$unrolledArgs = $args |. {process { $_ }} + +foreach ($element in $unrolledArgs){ + if ($element -is [string] -and + (-not ($element -as [xml])) -and + $element -notmatch '\s' + ) { + $this.'.Element'.ElementName = $Element + continue + } + + if ($element -is [xml] -or $Element -as [xml]) { + if ($Element -isnot [xml]) { + $element = $Element -as [xml] + } + $this.'.Element'.ElementName = $Element.ChildNodes[0].LocalName + foreach ($attribute in $element.ChildNodes[0].Attributes) { + $this.'.Element'.Attribute[$attribute.Name] = $attribute.Value + } + foreach ($grandchild in $element.ChildNodes[0].ChildNodes) { + $this.'.Element'.Children += $grandchild + } + continue + } + + if ($element -is [Collections.IDictionary]) { + $elementKeys = 'ElementName','Name','E' + foreach ($potentialName in $elementKeys) { + if ($element.$potentialName) { + $this.'.Element'.ElementName = $element.$potentialName + break + } + } + $attributeKeys = 'Attribute', 'Attributes', 'A' + foreach ($potentialAttributeName in $attributeKeys) { + if ($element.$potentialAttributeName -is [Collections.IDictionary] -and + $element.$potentialAttributeName.Count) { + foreach ($attributeName in $element.$potentialAttributeName.Keys) { + $this.'.Element'.Attribute[$attributeName] = $element.$potentialAttributeName[$attributeName] + } + break + } + } + $childKeys = 'Child', 'Children', 'ChildNodes','Content', 'C' + + foreach ($potentialChildrenName in $childKeys) { + $children = $element.$potentialChildrenName + if (-not $children) { continue } + $this.'.Element'.Children += $children + break + } + + $specialKeys = @( + $elementKeys + $attributeKeys + $childKeys + ) + + foreach ($elementKey in $element.Keys) { + if ($elementKey -in $specialKeys) { continue } + $elementValue = $element[$elementKey] + if ($elementValue -is [ValueType] -or ( + $elementValue -is [string] -and $elementValue -notmatch '[\r\n]' + )) { + $this.'.Element'.Attribute[$elementKey] = $elementValue + } + } + continue + } + + if ($elementName) { + + } +} + + + + Fill - if ($this.'.Fill') { + <# +.SYNOPSIS + Gets a Turtle's fill color +.DESCRIPTION + Gets one or more colors used to fill the Turtle. + + By default, this is transparent. + + If more than one value is provided, the fill will be a gradient. +.EXAMPLE + # Draw a blue square + turtle square 42 fill blue +.EXAMPLE + # Draw a PowerShell blue square + turtle square 42 fill '#4488ff' +.EXAMPLE + # Draw a red, green, blue gradient + turtle square 42 fill red green blue show +.EXAMPLE + # Draw a red, green, blue linear gradient + turtle square 42 fill red green blue linear show +.EXAMPLE + turtle flower fill red green blue fillrule evenodd show +#> +if ($this.'.Fill') { return $this.'.Fill' } return 'transparent' - param( - [string]$Fill = 'transparent' + <# +.SYNOPSIS + Sets a Turtle's fill color +.DESCRIPTION + Sets one or more colors used to fill the Turtle. + + By default, this is transparent. + + If more than one value is provided, the fill will be a gradient. +.EXAMPLE + # Draw a blue square + turtle square 42 fill blue +.EXAMPLE + # Draw a PowerShell blue square + turtle square 42 fill '#4488ff' +.EXAMPLE + # Draw a red, green, blue gradient + turtle square 42 fill red green blue show +.EXAMPLE + # Draw a red, green, blue linear gradient + turtle square 42 fill red green blue linear show +.EXAMPLE + turtle flower fill red green blue fillrule evenodd show +#> +param( +[PSObject[]] +$Fill = 'transparent' ) +# If we have no fill information, return +if (-not $fill) { return } + +# If the fill count is greater than one, try to make a graidnet +if ($fill.Count -gt 1) { + + # Default to a radial gradient + $gradientTypeHint = 'radial' + # and create a collection for attributes + $gradientAttributes = [Ordered]@{ + # default our identifier to the current id plus `fill-gradient` + # (so we could have multiple gradients without a collision) + id="$($this.id)-fill-gradient" + } + + $fill = @(foreach ($color in $fill) { + # If the value matches `linear` or `radial` + if ($color -match '^(linear|radial)') { + # take the hint and make it the right type of gradient. + $gradientTypeHint = ($color -replace 'gradient').ToLower() + } + # If the color was `pad`, `reflect`, or `repeat` + elseif ($fillColor -in 'pad', 'reflect', 'repeat') { + # take the hint and set the spreadMethod + $gradientAttributes['spreadMethod'] = $color + } + # If the fill is a dictionary + elseif ($color -is [Collections.IDictionary]) { + # propagate the values into attributes. + foreach ($gradientAttributeKey in $color.Keys) { + $gradientAttributes[$gradientAttributeKey] = $color[$gradientAttributeKey] + } + } + # Otherwise output the color + else { + $color + } + }) + + # If we have no fill colors after filtering, return + if (-not $fill) { return } + + # If our count is one + if ($fill.Count -eq 1) { + # it's not really going to be a gradient, so just use the one color. + $this | Add-Member -MemberType NoteProperty -Name '.Fill' -Value $Fill -Force + return + } + + # Now we have at least two colors we want to be a gradient + # We need to make sure the offset starts at 0% an ends at 100% + # and so we actually need to divide by one less than our fill color, so we end at 100%. + $offsetStep = 1 / ($fill.Count - 1) + $Gradient = @( + # Construct our gradient element. + "<${gradientTypeHint}Gradient$( + # propagate our attributes + @(foreach ($gradientAttributeKey in $gradientAttributes.Keys) { + " $gradientAttributeKey='$($gradientAttributes[$gradientAttributeKey])'" + }) -join '' + )>" + @( + # and put in our stop colors + for ($fillNumber = 0; $fillNumber -lt $fill.Count; $fillNumber++) { + "<stop offset='$($offsetStep * $fillNumber * 100)%' stop-color='$($fill[$fillNumber])' />" + } + ) + "</${gradientTypeHint}Gradient>" + ) -join [Environment]::NewLine + + # add this gradient to our defines + $this.Defines += $Gradient + # and set fill to this gradient. + $fill = "url(`"#$($gradientAttributes.id)`")" +} if (-not $this.'.Fill') { $this | Add-Member -MemberType NoteProperty -Name '.Fill' -Value $Fill -Force } else { @@ -3002,6 +5439,225 @@ $this.ViewBox = $viewBox[0],$viewBox[1],$viewbox[-2], $height + + History + + <# +.SYNOPSIS + Gets a Turtle's history +.DESCRIPTION + Gets an annotated history of a turtle's movements. + + This is an SVG path translated into back into human readable text and coordinates. +#> +$currentPosition = [Numerics.Vector2]::new(0,0) +$historyList = [Collections.Generic.List[PSObject]]::new() +$startStack = [Collections.Stack]::new() +foreach ($pathStep in $this.PathData -join ' ' -split '(?=[\p{L}-[E]])' -ne '') { + $letter = $pathStep[0] + $isUpper = "$letter".ToLower() -cne $letter + $isLower = -not $isUpper + $toBy = if ($isUpper) { 'to'} else { 'by'} + $stepPoints = $pathStep -replace $letter -replace ',', ' ' -split '\s{1,}' -ne '' -as [float[]] + + $historyEntry = + switch ($letter) { + a { + for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex+=7) { + $sequence = $stepPoints[$stepIndex..($stepIndex + 6)] + $comment = "arc $toBy $sequence" + $delta = [Numerics.Vector2]::new.Invoke($sequence[-2,-1]) + if ($isUpper) { $delta -= $currentPosition } + [PSCustomObject]@{ + PSTypeName='Turtle.History' + Letter = "$letter" + Start = $currentPosition + End = $currentPosition + $delta + Delta = $delta + Instruction = "$Letter $sequence" + Comment = $comment + } + $currentPosition += $delta + } + } + c { + + for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex+=6) { + $sequence = $stepPoints[$stepIndex..($stepIndex + 5)] + $comment = "cubic curve $toBy $sequence" + $delta = [Numerics.Vector2]::new.Invoke($sequence[-2,-1]) + if ($isUpper) { $delta -= $currentPosition } + [PSCustomObject]@{ + PSTypeName='Turtle.History' + Letter = "$letter" + Start = $currentPosition + End = $currentPosition + $delta + Delta = $delta + Instruction = "$Letter $sequence" + Comment = $comment + } + $currentPosition += $delta + } + } + l { + # line segment + for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex+=2) { + $sequence = $stepPoints[$stepIndex..($stepIndex + 1)] + $comment = "line $toBy $sequence" + $delta = [Numerics.Vector2]::new.Invoke($sequence[-2,-1]) + if ($isUpper) { $delta -= $currentPosition } + [PSCustomObject]@{ + PSTypeName='Turtle.History' + Letter = "$letter" + Start = $currentPosition + End = $currentPosition + $delta + Delta = $delta + Instruction = "$Letter $sequence" + Comment = $comment + } + $currentPosition += $delta + } + } + m { + # movement + for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex+=2) { + $sequence = $stepPoints[$stepIndex..($stepIndex + 1)] + + $comment = "line $toBy $sequence" + + $delta = [Numerics.Vector2]::new.Invoke($sequence[-2,-1]) + + if ($isUpper) { $delta -= $currentPosition } + + if ($stepIndex -gt 0) { + if ($letter -eq 'm') { + if ($isUpper) { $letter = 'L' } + else { $letter = 'l'} + } + $comment = "line $toBy $sequence" + } else { + $comment = "move $toBy $sequence" + $startStack.Push($currentPosition + $delta) + } + + [PSCustomObject]@{ + PSTypeName='Turtle.History' + Letter = "$letter" + Start = $currentPosition + End = $currentPosition + $delta + Delta = $delta + Instruction = "$Letter $sequence" + Comment = $comment + } + $currentPosition += $delta + } + } + s { + # simple bezier curve + for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex+=4) { + $sequence = $stepPoints[$stepIndex..($stepIndex + 3)] + $comment = "simple bezier curve $toBy $sequence" + $delta = [Numerics.Vector2]::new.Invoke($sequence[-2,-1]) + if ($isUpper) { $delta -= $currentPosition } + [PSCustomObject]@{ + PSTypeName='Turtle.History' + Letter = "$letter" + Start = $currentPosition + End = $currentPosition + $delta + Delta = $delta + Instruction = "$Letter $sequence" + Comment = $comment + } + $currentPosition += $delta + } + } + t { + # continue simple bezier curve + for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex+=2) { + $sequence = $stepPoints[$stepIndex..($stepIndex + 1)] + $comment = "continue bezier curve $toBy $sequence" + $delta = [Numerics.Vector2]::new.Invoke($sequence[-2,-1]) + if ($isUpper) { $delta -= $currentPosition } + [PSCustomObject]@{ + PSTypeName='Turtle.History' + Letter = "$letter" + Start = $currentPosition + End = $currentPosition + $delta + Delta = $delta + Instruction = "$Letter $sequence" + Comment = $comment + } + $currentPosition += $delta + } + } + q { + for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex+=4) { + + $sequence = $stepPoints[$stepIndex..($stepIndex + 3)] + $comment = "quadratic bezier curve $toBy $sequence" + $delta = [Numerics.Vector2]::new.Invoke($sequence[-2,-1]) + if ($isUpper) { $delta -= $currentPosition } + [PSCustomObject]@{ + PSTypeName='Turtle.History' + Letter = "$letter" + Start = $currentPosition + End = $currentPosition + $delta + Delta = $delta + Instruction = "$Letter $sequence" + Comment = $comment + } + $currentPosition += $delta + } + } + { $_ -in 'h', 'v' } { + for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex++) { + $sequence = $stepPoints[$stepIndex..$stepIndex] + $comment = "$( + if ($letter -eq 'v') { 'vertical' } else {'horizontal'} + ) line $toBy $sequence" + $delta = + if ($letter -eq 'v') { + [Numerics.Vector2]::new(0, $sequence[0]) + } else { + [Numerics.Vector2]::new($sequence[0], 0) + } + if ($isUpper) { $delta -= $currentPosition } + [PSCustomObject]@{ + PSTypeName='Turtle.History' + Letter = "$letter" + Start = $currentPosition + End = $currentPosition + $delta + Delta = $delta + Instruction = "$Letter $sequence" + Comment = $comment + } + $currentPosition += $delta + } + } + z { + $closePosition = $startStack.Pop() + $delta = $closePosition - $currentPosition + [PSCustomObject]@{ + PSTypeName='Turtle.History' + Letter = "$letter" + Start = $currentPosition + End = $currentPosition + $delta + Delta = $delta + Instruction = "$Letter" + Comment = "close path" + } + $currentPosition += $delta + + } + } + + $historyList.Add($historyEntry) +} + + +return $historyList + + ID @@ -3037,68 +5693,499 @@ if ($null -eq $this.'.IsPenDown') { - JPEG + JPEG + + $chromiumNames = 'chromium','chrome' +foreach ($browserName in $chromiumNames) { + $chromiumCommand = + $ExecutionContext.SessionState.InvokeCommand.GetCommand($browserName,'Application') + if (-not $chromiumCommand) { + $chromiumCommand = + Get-Process -Name $browserName -ErrorAction Ignore | + Select-Object -First 1 -ExpandProperty Path + } + if ($chromiumCommand) { break } +} +if (-not $chromiumCommand) { + Write-Error "No Chromium-based browser found. Please install one of: $($chromiumNames -join ', ')" + return +} + +$pngRasterizer = $this.Canvas -replace '/\*Insert-Post-Processing-Here\*/', @' + const dataUrl = await canvas.toDataURL('image/jpeg') + console.log(dataUrl) + + const newImage = document.createElement('img') + newImage.src = dataUrl + document.body.appendChild(newImage) +'@ + + +$appDataRoot = [Environment]::GetFolderPath("ApplicationData") +$appDataPath = Join-Path $appDataRoot 'Turtle' +$filePath = Join-Path $appDataPath 'Turtle.raster.html' +$null = New-Item -ItemType File -Force -Path $filePath -Value ( + $pngRasterizer -join [Environment]::NewLine +) +# $pngRasterizer > $filePath + +$headlessArguments = @( + '--headless', # run in headless mode + '--dump-dom', # dump the DOM to stdout + '--disable-gpu', # disable GPU acceleration + '--no-sandbox' # disable the sandbox if running in CI/CD +) + +$chromeOutput = & $chromiumCommand @headlessArguments "$filePath" | Out-String +if ($chromeOutput -match '<img\ssrc="data:image/\w+;base64,(?<b64>[^"]+)') { + ,[Convert]::FromBase64String($matches.b64) +} + + + + + Keyframe + + <# +.SYNOPSIS + Gets Turtle keyframes +.DESCRIPTION + Gets CSS Keyframes animations associated with the Turtle. + + Keyframes are stored as a dictionary of dictionaries. + + Each key is the name of the keyframe. + + Each nested dictionary is the keyframe at a given percentage. +.EXAMPLE + turtle keyframe ([Ordered]@{ + 'slide-in' = [Ordered]@{ + from = [Ordered]@{ + translate = "-150vw 0" + scale = "200% 1" + } + to = [Ordered]@{ + translate = "0 0" + scale = "100% 1" + } + } + }) keyframe +.LINK + https://MrPowerShell.com/CSS/Keyframes +#> +if (-not $this.'.Keyframes') { + $this | Add-Member NoteProperty '.Keyframes' ([Ordered]@{}) -Force +} +return $this.'.Keyframes' + + + + <# +.SYNOPSIS + Sets Turtle Keyframes +.DESCRIPTION + Sets CSS Keyframes associated with a Turtle. +.EXAMPLE + turtle square 42 fill '#4488ff' stroke '#224488' keyframe ([Ordered]@{ + 'wiggle3d' = [Ordered]@{ + '0%,100%' = [Ordered]@{ + transform = "rotateX(-3deg) rotateY(-3deg) rotateZ(-3deg)" + } + '50%' = [Ordered]@{ + transform = "rotateX(3deg) rotateY(3deg) rotateZ(3deg)" + } + } + }) pathclass wiggle3d save ./wiggleSquare.svg +#> +param( +[PSObject] +$Keyframe +) + +$keyframes = $this.Keyframe +if ($Keyframe -is [Collections.IDictionary]) { + foreach ($key in $keyframe.Keys) { + $keyframes[$key] = $Keyframe[$key] + } +} + + + + + Link + + <# +.SYNOPSIS + Gets a Turtle's link +.DESCRIPTION + Gets a link reference (href) associated with the turtle. + + If one is present, this will nest the turtle inside of an <a> element +.EXAMPLE + turtle link https://psturtle.com/ link + +#> +$this.'.link' + + + + <# +.SYNOPSIS + Sets a Turtle's link +.DESCRIPTION + Sets a link reference (`href`) associated with the turtle. + + If one is present, this will nest the turtle inside of an anchor `<a>` element +.EXAMPLE + turtle link https://psturtle.com/ +#> +param( +[string] +$Link +) + +$this | Add-Member NoteProperty '.Link' $link -Force + + + + + Locale + + <# +.SYNOPSIS + Gets a Turtle's Locale +.DESCRIPTION + Gets the locale associated with a Turtle. + + This is usually nothing, as a picture speaks a thousand words in any language. + + If it is set, it can be used to render content invisible unless the systemLanguage attribute matches the current language preference. +#> +return $this.Attributes['systemLanguage'] + + + + <# +.SYNOPSIS + Gets a Turtle's Locale +.DESCRIPTION + Gets the locale associated with a Turtle. + + This is usually nothing, as a picture speaks a thousand words in any language. + + If it is set, it can be used to render content invisible unless the systemLanguage attribute matches the current language preference. +#> +$unrolledArgs = $args | . { process { $_ } } +$joinedArgs = $unrolledArgs -join ',' +if (-not $joinedArgs) { + $this.Attribute.Remove('systemLanguage') + $this.SVGAttribute.Remove('systemLanguage') +} else { + $this.Attribute['systemLanguage'] = $joinedArgs + $this.SVGAttribute['systemLanguage'] = $joinedArgs +} + + + + + Marker + + <# +.SYNOPSIS + Gets the turtle as a Marker +.DESCRIPTION + Gets the Turtle as a `<marker>`, which can mark points on another shape. +.EXAMPLE + turtle viewbox 200 teleport 0 100 forward 100 markerEnd ( + turtle viewbox 10 rotate -90 polygon 10 3 fill context-fill stroke context-stroke + ) strokewidth '3%' fill currentColor save ./marker.svg +.LINK + https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/marker +#> +[OutputType([xml])] +param() + +# The default settings for markers +$markerAttributes = [Ordered]@{ + id="$($this.id)-marker" + viewBox="$($this.ViewBox)" + orient='auto-start-reverse' + refX=$this.Width/2 + refY=$this.Height/2 + markerWidth=5 + marketHeight=5 +} +# Marker attributes can exist in .Attribute or .SVGAttribute +$prefix = [Regex]::new('^/?marker/', 'IgnoreCase') +foreach ($collection in $this.Attribute, $this.SVGAttribute) { + foreach ($key in $collection.Keys) { + if ($key -notmatch $prefix) { continue } + $markerAttributes[$key -replace $prefix] = $collection[$key] + } +} + +# Create a marker XML. +[xml]@( + "<marker$( + foreach ($key in $markerAttributes.Keys) { + " $key='$($markerAttributes[$key])'" + } +)>" + $this.SVG.SVG.InnerXML +"</marker>" +) + + + + + MarkerEnd + + <# +.SYNOPSIS + Gets a Turtle's end marker +.DESCRIPTION + Gets the end marker used on the line drawn by the turtle +.EXAMPLE + turtle viewbox 200 teleport 0 100 forward 100 markerEnd ( + turtle viewbox 10 rotate -90 polygon 10 3 # fill context-fill stroke context-stroke + ) fill '#4488ff' stroke '#224488' strokewidth '3%' save ./marker.svg +#> +return $this.PathAttribute['marker-end'] + + + + <# +.SYNOPSIS + Sets the end marker +.DESCRIPTION + Sets the end marker used on the line drawn by the turtle. + + If this is set to a string without spaces, it will be be treated as an identifier. +.EXAMPLE + turtle viewbox 200 teleport 0 100 forward 100 markerEnd ( + turtle viewbox 10 rotate -90 polygon 10 3 # fill context-fill stroke context-stroke + ) fill '#4488ff' stroke '#224488' strokewidth '3%' save ./marker.svg +.EXAMPLE + turtle viewbox 200 teleport 0 100 forward 100 markerEnd ( + turtle viewbox 20 rotate -90 polygon 10 3 morph @( + turtle rotate -90 polygon 20 3 + turtle rotate -90 polygon 20 3 + ) + ) strokewidth '3%' save ./marker.svg +#> +param($Value) + +$attributeValue = + if ($value -is [string]) { + if ($value -notmatch '\s' -and $value -notmatch '^url') { + $Value = "url(`"$Value`")" + } else { + $value + } + } + else { + if ($value.pstypenames -contains 'Turtle') { + $Value.id += "-end" + $this.Defines+=$Value.Marker.OuterXml + "url(#$($value.id)-marker)" + } + } + +$this.PathAttribute['marker-end'] = $attributeValue + + + + + + MarkerMiddle + + <# +.SYNOPSIS + Gets a Turtle's middle marker +.DESCRIPTION + Gets the middle marker used on the line drawn by the turtle. + + This marker will be drawn on all vertices that are not the start or the end. +.EXAMPLE + turtle viewbox 200 start 10 200 rotate -60 @( + 'forward',42,'rotate',30,'forward',42,'rotate',-30 * 4 + ) markerMiddle ( + turtle circle 10 fill red + ) strokewidth '3%' save ./marker.svg +#> +return $this.PathAttribute['marker-mid'] + + + + <# +.SYNOPSIS + Sets the middle marker +.DESCRIPTION + Sets the middle marker used on the line drawn by the turtle. + + If this is set to a string without spaces, it will be be treated as an identifier. +.EXAMPLE + turtle viewbox 200 start 10 200 rotate -60 @( + 'forward',42,'rotate',30,'forward',42,'rotate',-30 * 4 + ) markerMiddle ( + turtle circle 10 fill red + ) strokewidth '3%' save ./marker.svg +#> +param($Value) + +$attributeValue = + if ($value -is [string]) { + if ($value -notmatch '\s' -and $value -notmatch '^url') { + $Value = "url(`"$Value`")" + } else { + $value + } + } + else { + if ($value.pstypenames -contains 'Turtle') { + $Value.id += "-mid" + $this.Defines+=$Value.Marker.OuterXml + "url(#$($value.id)-marker)" + } + } + +$this.PathAttribute['marker-mid'] = $attributeValue + + + + + + MarkerStart - $chromiumNames = 'chromium','chrome' -foreach ($browserName in $chromiumNames) { - $chromiumCommand = - $ExecutionContext.SessionState.InvokeCommand.GetCommand($browserName,'Application') - if (-not $chromiumCommand) { - $chromiumCommand = - Get-Process -Name $browserName -ErrorAction Ignore | - Select-Object -First 1 -ExpandProperty Path - } - if ($chromiumCommand) { break } -} -if (-not $chromiumCommand) { - Write-Error "No Chromium-based browser found. Please install one of: $($chromiumNames -join ', ')" - return -} + <# +.SYNOPSIS + Gets a Turtle's start marker +.DESCRIPTION + Gets the start marker used on the line drawn by the turtle +.EXAMPLE + turtle viewbox 200 start 50 50 rotate 45 forward 100 markerStart ( + turtle rotate -90 turtleMonotile -42 fill context-fill stroke context-stroke + ) fill 'currentColor' strokewidth '3%' save ./marker.svg +.EXAMPLE + turtle viewbox 200 start 50 50 rotate 45 forward 100 markerStart ( + turtle rotate -90 polygon 42 3 fill context-fill stroke context-stroke + ) fill 'currentColor' strokewidth '3%' save ./marker.svg +#> +return $this.PathAttribute['marker-start'] -$pngRasterizer = $this.Canvas -replace '/\*Insert-Post-Processing-Here\*/', @' - const dataUrl = await canvas.toDataURL('image/jpeg') - console.log(dataUrl) - - const newImage = document.createElement('img') - newImage.src = dataUrl - document.body.appendChild(newImage) -'@ + + + <# +.SYNOPSIS + Sets the start marker +.DESCRIPTION + Sets the start marker used on the line drawn by the turtle. + If this is set to a string without spaces, it will be be treated as an identifier. +.EXAMPLE + turtle viewbox 200 start 50 100 rotate 45 forward 100 markerStart ( + turtle rotate -90 polygon 10 3 fill context-fill stroke context-stroke + ) fill '#4488ff' stroke '#224488' strokewidth '3%' save ./marker.svg +.EXAMPLE + turtle viewbox 200 teleport 50 100 forward 100 markerEnd ( + turtle viewbox 20 rotate -90 polygon 10 3 morph @( + turtle rotate -90 polygon 20 3 + turtle rotate -90 polygon 20 3 + ) + ) strokewidth '3%' save ./marker.svg +#> +param($Value) -$appDataRoot = [Environment]::GetFolderPath("ApplicationData") -$appDataPath = Join-Path $appDataRoot 'Turtle' -$filePath = Join-Path $appDataPath 'Turtle.raster.html' -$null = New-Item -ItemType File -Force -Path $filePath -Value ( - $pngRasterizer -join [Environment]::NewLine -) -# $pngRasterizer > $filePath +$attributeValue = + if ($value -is [string]) { + if ($value -notmatch '\s' -and $value -notmatch '^url') { + $Value = "url(`"$Value`")" + } else { + $value + } + } + else { + if ($value.pstypenames -contains 'Turtle') { + $Value.id += "-start" + $this.Defines+=$Value.Marker.OuterXml + "url(#$($value.id)-marker)" + } + } -$headlessArguments = @( - '--headless', # run in headless mode - '--dump-dom', # dump the DOM to stdout - '--disable-gpu', # disable GPU acceleration - '--no-sandbox' # disable the sandbox if running in CI/CD -) +$this.PathAttribute['marker-start'] = $attributeValue -$chromeOutput = & $chromiumCommand @headlessArguments "$filePath" | Out-String -if ($chromeOutput -match '<img\ssrc="data:image/\w+;base64,(?<b64>[^"]+)') { - ,[Convert]::FromBase64String($matches.b64) -} - + Mask - $segments = @( -"<svg xmlns='http://www.w3.org/2000/svg' width='0%' height='0%'>" - "<defs>" - "<mask id='$($this.Id)-mask'>" - $this.Symbol.OuterXml -replace '\<\?[^\>]+\>' - "</mask>" - "</defs>" -"</svg>" + <# +.SYNOPSIS + Gets a Turtle's mask +.DESCRIPTION + Gets a Turtle as an image mask. + + Everything under a white pixel will be visible. + + Everything under a black pixel will be invisible. +.EXAMPLE + # Masks will autoscale to the object bounding box by default + # Make sure to leave a hole. + turtle defines @( + turtle id smallsquare width 84 height 84 @( + ) teleport 21 21 square 42 @( + ) fill black backgroundcolor white mask + ) square 84 fill '#4488ff' stroke '#224488' pathattribute @{ + mask='url(#smallsquare-mask)' + } save ./square-mask.svg +.EXAMPLE + # Masks can contain morphing + turtle defines @( + turtle id smallsquare viewbox 84 @( + 'fill','black' + 'backgroundcolor','white' + ) morph @( + turtle teleport 21 21 square 42 + turtle teleport 41.5 41.5 square 1 + turtle teleport 21 21 square 42 + ) duration '00:00:01.68' mask + ) square 840 fill '#050506ff' stroke '#224488' pathattribute @{ + mask='url(#smallsquare-mask)' + } save ./square-mask-morph.svg +.LINK + https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/mask +#> +[OutputType([xml])] +param() + +$keyPattern = '^mask/' +$defaultId = "$($this.Id)-mask" +# Gather the mask attributes, and default the ID (the only attribute we actually need) +$maskAttributes = [Ordered]@{id=$defaultId} +# Attributes can exist in .SVGAttribute or .Attribute +foreach ($collectionName in 'SVGAttribute','Attribute') { + # as long as they start with mask/ + # (slashes are not valid attribute names, so this can denote a target name or type) + foreach ($key in $this.$collectionName.Keys -match $keyPattern) { + $maskAttributes[$key -replace $keyPattern] = $this.$collectionName[$key] + } +} + +# Create an attribute declaration +$maskAttributes = @(foreach ($attributeName in $maskAttributes.Keys) { + "$($attributeName)='$( + [Web.HttpUtility]::HtmlAttributeEncode($maskAttributes[$attributeName]) + )'" +}) -join ' ' + +# Declare the mask segments +$segments = @( +"<mask $maskAttributes>" + $this.SVG.OuterXml -replace '\<\?[^\>]+\>' +"</mask>" ) +# join them and cast to XML. [xml]($segments -join '') @@ -3107,25 +6194,40 @@ if ($chromeOutput -match '<img\ssrc="data:image/\w+;base64,(?<b64>[^"]+ <# .SYNOPSIS - Gets the turtle maximum point. + Gets the turtle's highest point. .DESCRIPTION - Gets the maximum point reached by the turtle. + Gets the maximum point vector visited by the turtle. - Keeping track of this as we go is far more efficient than calculating it from the path. + This would the highest point that the turtle has been. #> -if ($this.'.Maximum') { - return $this.'.Maximum' +[OutputType([Numerics.Vector2])] +param() +if (-not $this.'.Maximum') { + $this | Add-Member NoteProperty '.Maximum' ([Numerics.Vector2]::new(0,0)) -Force } -return ([pscustomobject]@{ X = 0; Y = 0 }) + +return $this.'.Maximum' + Minimum - if ($this.'.Minimum') { - return $this.'.Minimum' + <# +.SYNOPSIS + Gets a Turtle's lowest point +.DESCRIPTION + Gets the minimum vector for this turtle. + + This would the lowest point that the turtle has visted. +#> +[OutputType([Numerics.Vector2])] +param() +if (-not $this.'.Minimum') { + $this | Add-Member NoteProperty '.Minimum' ([Numerics.Vector2]::new(0,0)) -Force } -return ([pscustomobject]@{ X = 0; Y = 0 }) + +return $this.'.Minimum' @@ -3148,41 +6250,50 @@ param() <# .SYNOPSIS - Gets the turtle opacity + Gets a Turtle's opacity .DESCRIPTION - Gets the opacity of the turtle path. + Gets the opacity of a Turtle +.EXAMPLE + turtle opacity .5 #> -if (-not $this.'.PathAttribute') { - $this | Add-Member -MemberType NoteProperty -Name '.PathAttribute' -Value ([Ordered]@{}) -Force -} -if ($this.'.PathAttribute'.'opacity') { - return $this.'.PathAttribute'.'opacity' -} else { - return 1.0 -} +param() +return $this.'.Opacity' <# .SYNOPSIS - Sets the opacity + Sets a Turtle's opacity .DESCRIPTION - Sets the opacity of the path + Sets the opacity of a Turtle .EXAMPLE - turtle forward 100 opacity 0.5 save ./dimLine.svg + turtle opacity .5 opacity #> param( [double] -$Opacity = 'nonzero' +$Opacity ) - -$this.PathAttribute = [Ordered]@{'opacity' = $Opacity} +$this | Add-Member NoteProperty '.Opacity' $Opacity -Force PathAnimation - if ($this.'.PathAnimation') { + <# +.SYNOPSIS + Gets the Turtle's Path Animation +.DESCRIPTION + Gets any path animations associated with the current turtle. +.EXAMPLE + turtle flower PathAnimation ([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 +#> +if ($this.'.PathAnimation') { return $this.'.PathAnimation' } @@ -3246,8 +6357,15 @@ $newAnimation = @(foreach ($animation in $PathAnimation) { } }) +$pathAnimation = $this.PathAnimation +if ($pathAnimation) { + $newAnimation = @($pathAnimation) + $newAnimation +} $this | Add-Member -MemberType NoteProperty -Force -Name '.PathAnimation' -Value $newAnimation + + + @@ -3298,27 +6416,93 @@ $this | Add-Member -MemberType NoteProperty -Force -Name '.PathClass' -Value @( PathData - @( - @( - - if ($this.Start.X -and $this.Start.Y) { + <# +.SYNOPSIS + Gets our Turtle's path +.DESCRIPTION + Gets the path data of this Turtle's movements. + + This is the shape this turtle will draw. +.NOTES + Turtle Path data is represented as a + [SVG path](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorials/SVG_from_scratch/Paths). + + This format can also be used as a [Path2D](https://developer.mozilla.org/en-US/docs/Web/API/Path2D/Path2D) in a Canvas element. + + It can also be used in WPF, where it is simply called [Path Markup](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/graphics-multimedia/path-markup-syntax) +.LINK + https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorials/SVG_from_scratch/Paths +.LINK + https://developer.mozilla.org/en-US/docs/Web/API/Path2D/Path2D +.LINK + https://learn.microsoft.com/en-us/dotnet/desktop/wpf/graphics-multimedia/path-markup-syntax?wt.mc_id=MVP_321542 +.EXAMPLE + turtle square 42 pathdata +#> +@( + # Let's call this trick Schrödinger's rounding. + # We want to be able to render our shapes with a custom precision + # but we don't want to slow down in rounding or only be able to round once. + + # So we can round when we ask for the path data. + # This is a much less common request than moving the turtle forward. + $precision = $this.Precision + filter roundToPrecision { [Math]::Round($_, $precision)} + + if ($null -ne $this.Start.X -and $null -ne $this.Start.Y) { + if ($precision) { + "m $($this.Start.x | roundToPrecision) $($this.Start.y | roundToPrecision)" + } else { "m $($this.Start.x) $($this.Start.y)" } - else { - @("m" - if ($this.Minimum.X -lt 0) { - -1 * $this.Minimum.X - } else { - 0 + + } + else { + @("m" + # If the viewbox has been manually set + if ($this.'.ViewBox') { + 0, 0 # do not adjust our starting position + } else { + # otherwise, translate by the minimum point. + if ($this.Minimum.X -lt 0) { + if ($precision) { + -1 * $this.Minimum.X | roundToPrecision + } else { + -1 * $this.Minimum.X + } } + else { 0 } + if ($this.Minimum.Y -lt 0) { - -1 * $this.Minimum.Y - } else { - 0 - }) -join ' ' + if ($precision) { + -1 * $this.Minimum.Y | roundToPrecision + } else { + -1 * $this.Minimum.Y + } + + } + else { 0 } + }) -join ' ' + } + + # Walk over our steps + foreach ($step in + $this.Steps -join ' ' -replace ',',' ' -split '(?=[\p{L}-[E]])' -ne '' + ) { + # If our precision is zero or nothing, don't round + if (-not $precision) { + $step + } else { + # Otherwise, pick out the letter + $step.Substring(0,1) + # and get each digit + $digits = $step.Substring(1) -split '\s+' -ne '' -as [double[]] + # and round them. + foreach ($digit in $digits) { + [Math]::Round($digit, $precision) + } } - ) + $this.Steps - # @("m $($this.Start.x) $($this.Start.y) ") + $this.Steps + } ) -join ' ' @@ -3332,8 +6516,11 @@ $this | Add-Member -MemberType NoteProperty -Force -Name '.PathClass' -Value @( Gets the Path Element of a Turtle. This contains the path of the Turtle's motion. +.EXAMPLE + turtle forward 42 rotate 90 forward 42 pathElement #> - +[OutputType([xml])] +param() # Set our core attributes $coreAttributes = [Ordered]@{ id="$($this.id)-path" @@ -3353,15 +6540,26 @@ foreach ($pathAttributeName in $this.PathAttribute.Keys) { $coreAttributes[$pathAttributeName] = $($this.PathAttribute[$pathAttributeName]) } -@( +# Path attributes can be defined within .SVGAttribute or .Attribute +$prefix = [Regex]::new('^/?path/', 'IgnoreCase') +foreach ($collection in $this.SVGAttribute, $this.Attribute) { + if (-not $collection) { continue } + foreach ($key in $collection.Keys -match $prefix) { + $coreAttributes[$attributeName -replace $prefix] = $collection[$attributeName] + } +} + +# Create the elements in an array, and cast it to XML. +[xml]@( "<path$( foreach ($attributeName in $coreAttributes.Keys) { " $attributeName='$($coreAttributes[$attributeName])'" } )>" +if ($this.Title) { "<title>$([Security.SecurityElement]::Escape($this.Title))</title>" } if ($this.PathAnimation) {$this.PathAnimation} "</path>" -) -as [xml] +) @@ -3403,46 +6601,297 @@ return $this.PathAttribute['transform'] = "$transformString" Pattern - param() -$segments = @( + <# +.SYNOPSIS + Gets a Turtle Pattern +.DESCRIPTION + Gets the current turtle as a pattern that stretches off to infinity. +.EXAMPLE + turtle star 42 4 | Save-Turtle "./GridPattern.svg" +.EXAMPLE + turtle star 42 6 | Save-Turtle "./StarPattern6.svg" +.EXAMPLE + turtle star 42 8 | Save-Turtle "./StarPattern8.svg" +.EXAMPLE + turtle star 42 5 | Save-Turtle "./StarPattern5.svg" +.EXAMPLE + turtle star 42 7 | Save-Turtle "./Star7Pattern.svg" +.EXAMPLE + turtle viewbox 100 start 25 25 square 50 | Save-Turtle "./WindowPattern.svg" Pattern +.EXAMPLE + turtle star 100 4 morph @( + turtle star 100 4 + turtle rotate 90 star 100 4 + turtle rotate 180 star 100 4 + turtle rotate 270 star 100 4 + turtle star 100 4 + ) | Save-Turtle "./GridPatternMorph.svg" +.EXAMPLE + turtle star 100 3 morph @( + turtle star 100 3 + turtle rotate 90 star 100 3 + turtle rotate 180 star 100 3 + turtle rotate 270 star 100 3 + turtle star 100 3 + ) | Save-Turtle "./TriPatternMorph.svg" +.EXAMPLE + turtle star 100 6 morph @( + turtle star 100 6 + turtle rotate 90 star 100 6 + turtle rotate 180 star 100 6 + turtle rotate 270 star 100 6 + turtle star 100 6 + ) | Save-Turtle "./Star6PatternMorph.svg" | Show-Turtle +.EXAMPLE + # We can use a pattern transform to scale the pattern + turtle sierpinskiTriangle PatternTransform @{ + scale = 0.25 + rotate = 120 + } | + Save-Turtle "./SierpinskiTrianglePattern.svg" Pattern | + show-Turtle +.EXAMPLE + # We can use pattern animations to change the pattern + # Animations are relative to initial transforms + turtle sierpinskiTriangle PatternTransform @{ + scale = 0.25 + rotate = 120 + } PatternAnimation ([Ordered]@{ + type = 'scale' ; values = 1.33,0.66, 1.33 ; repeatCount = 'indefinite' ;dur = "23s"; additive = 'sum' + }) | + Save-Turtle "./SierpinskiTrianglePattern.svg" Pattern | + Show-Turtle +.LINK + https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorials/SVG_from_scratch/Patterns +.LINK + https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/pattern +#> +[OutputType([xml])] +param() + +# Get our viewbox $viewBox = $this.ViewBox -$null, $null, $viewX, $viewY = $viewBox +# and get the width and height +$viewX, $viewY, $viewWidth, $viewHeight = $viewBox + +# Initialize our core attributes. +# These may be overwritten by user request. +$coreAttributes = [Ordered]@{ + 'id' = "$($this.ID)-pattern" + 'patternUnits' = 'userSpaceOnUse' + 'x' = $viewX + 'y' = $viewY + 'width' = $viewWidth + 'height' = $viewHeight + 'transform-origin' = '50% 50%' +} + +# If we have specified any transforms +if ($this.PatternTransform) { + $coreAttributes."patternTransform" = + # Then generate a transform expression + @(foreach ($key in $this.PatternTransform.Keys) { + # transforms are a name, followed by parameters in paranthesis + "$key($($this.PatternTransform[$key]))" + }) -join ' ' +} + +# Pattern attributes can be defined within .SVGAttribute or .Attribute +# provided they have the appropriate prefix +$prefix = [Regex]::new('^/?pattern/', 'IgnoreCase') +# (slashes are invalid markup, and thus a fine way to target nested instances) + +foreach ($collection in $this.SVGAttribute, $this.Attribute) { + # If the connection does not exist, continue. + if (-not $collection) { continue } + # For each key that matches the prefix + foreach ($key in $collection.Keys -match $prefix) { + # add it to the attributes after stripping the prefix. + $coreAttributes[$attributeName -replace $prefix] = $collection[$attributeName] + } +} + +$segments = @( "<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%'>" "<defs>" - "<pattern id='$($this.ID)-pattern' patternUnits='userSpaceOnUse' width='$viewX' height='$viewY' transform-origin='50% 50%'$( - if ($this.PatternTransform) { - " patternTransform='" + ( - @(foreach ($key in $this.PatternTransform.Keys) { - "$key($($this.PatternTransform[$key]))" - }) -join ' ' - ) + "'" - } - )>" + "<pattern$( + foreach ($attributeName in $coreAttributes.Keys) { + " $attributeName='$($coreAttributes[$attributeName])'" + } +)>" $(if ($this.PatternAnimation) { $this.PatternAnimation }) $($this.SVG.SVG.InnerXML) "</pattern>" "</defs>" -$( - if ($this.BackgroundColor) { - "<rect width='10000%' height='10000%' x='-5000%' y='-5000%' fill='$($this.BackgroundColor)' transform-origin='50% 50%' />" - } -) "<rect width='10000%' height='10000%' x='-5000%' y='-5000%' fill='url(#$($this.ID)-pattern)' transform-origin='50% 50%' />" -"</svg>") +"</svg>" +) -$segments -join '' -as [xml] +[xml]$segments PatternAnimation - if ($this.'.PatternAnimation') { + <# +.SYNOPSIS + Gets pattern animations +.DESCRIPTION + Gets one or more animations that apply to our Turtle's pattern. + + These animations will transform the pattern, allowing for endless variation. +.EXAMPLE + turtle flower PatternAnimation ([Ordered]@{ + type = 'translate' + values = "0 0","0 420", "0 0" + repeatCount = 'indefinite' + # dur = "11s" # The duration will default to the Turtle's duration + additive = 'sum' + }) save ./FlowerPatternAnimation.svg +.EXAMPLE + # We can have multiple pattern animations, and need to use `additive=sum` to ensure they do not conflict + turtle SierpinskiTriangle duration '00:00:42' PatternAnimation ([Ordered]@{ + type = 'rotate' + values = "0","360" + repeatCount = 'indefinite' + additive = 'sum' + }, [Ordered]@{ + type = 'scale' + values = "1",".25", "1" + repeatCount = 'indefinite' + additive = 'sum' + }) save ./SierpinskiTrianglePatternAnimation.svg +.EXAMPLE + # Pattern Transforms set a starting state for animations + turtle SierpinskiTriangle duration '00:00:42' PatternTransform @{ + scale = .25 + } PatternAnimation ([Ordered]@{ + type = 'rotate' + values = "0","360" + repeatCount = 'indefinite' + additive = 'sum' + }, [Ordered]@{ + type = 'scale' + values = "1",".25", "1" + repeatCount = 'indefinite' + additive = 'sum' + }) save ./SierpinskiTrianglePatternAnimationSmaller.svg +.EXAMPLE + # We can use primes as pattern transform durations to ensure animations rarely overlap + # This example uses four primes under 100: + # It will repeat in `23 * 41 * 61 * 83` seconds + # (or just over 55 days) + turtle SierpinskiTriangle duration '00:00:42' PatternTransform @{ + scale = .25 + } PatternAnimation ([Ordered]@{ + type = 'rotate' + values = "0","360" + repeatCount = 'indefinite' + dur = '83s' + additive = 'sum' + }, [Ordered]@{ + type = 'scale' + values = "1",".25", "1" + repeatCount = 'indefinite' + dur = '23s' + additive = 'sum' + }, [Ordered]@{ + type = 'skewX' + values = "0","45", "0" + repeatCount = 'indefinite' + additive = 'sum' + dur = '41s' + }, [Ordered]@{ + type = 'skewX' + values = "0","-45", "0" + repeatCount = 'indefinite' + additive = 'sum' + dur = '61s' + }) save ./SierpinskiTrianglePatternAnimationEndless.svg +#> +if ($this.'.PatternAnimation') { return $this.'.PatternAnimation' } - param( + <# +.SYNOPSIS + Sets a pattern animation +.DESCRIPTION + Sets one or more animations to apply to our Turtle's pattern. + + These animations will transform the pattern, allowing for endless variation. +.EXAMPLE + turtle flower PatternAnimation ([Ordered]@{ + type = 'translate' + values = "0 0","0 420", "0 0" + repeatCount = 'indefinite' + # dur = "11s" # The duration will default to the Turtle's duration + additive = 'sum' + }) save ./FlowerPatternAnimation.svg +.EXAMPLE + # We can have multiple pattern animations, and need to use `additive=sum` to ensure they do not conflict + turtle SierpinskiTriangle duration '00:00:42' PatternAnimation ([Ordered]@{ + type = 'rotate' + values = "0","360" + repeatCount = 'indefinite' + additive = 'sum' + }, [Ordered]@{ + type = 'scale' + values = "1",".25", "1" + repeatCount = 'indefinite' + additive = 'sum' + }) save ./SierpinskiTrianglePatternAnimation.svg +.EXAMPLE + # Pattern Transforms set a starting state for animations + turtle SierpinskiTriangle duration '00:00:42' PatternTransform @{ + scale = .25 + } PatternAnimation ([Ordered]@{ + type = 'rotate' + values = "0","360" + repeatCount = 'indefinite' + additive = 'sum' + }, [Ordered]@{ + type = 'scale' + values = "1",".25", "1" + repeatCount = 'indefinite' + additive = 'sum' + }) save ./SierpinskiTrianglePatternAnimationSmaller.svg +.EXAMPLE + # We can use primes as pattern transform durations to ensure animations rarely overlap + # This example uses four primes under 100: + # It will repeat in `23 * 41 * 61 * 83` seconds + # (or just over 55 days) + turtle SierpinskiTriangle duration '00:00:42' PatternTransform @{ + scale = .25 + } PatternAnimation ([Ordered]@{ + type = 'rotate' + values = "0","360" + repeatCount = 'indefinite' + dur = '83s' + additive = 'sum' + }, [Ordered]@{ + type = 'scale' + values = "1",".25", "1" + repeatCount = 'indefinite' + dur = '23s' + additive = 'sum' + }, [Ordered]@{ + type = 'skewX' + values = "0","45", "0" + repeatCount = 'indefinite' + additive = 'sum' + dur = '41s' + }, [Ordered]@{ + type = 'skewX' + values = "0","-45", "0" + repeatCount = 'indefinite' + additive = 'sum' + dur = '61s' + }) save ./SierpinskiTrianglePatternAnimationEndless.svg +#> +param( [PSObject] $PatternAnimation ) @@ -3459,6 +6908,9 @@ $newAnimation = @(foreach ($animation in $PatternAnimation) { if ($animationCopy.values -is [object[]]) { $animationCopy['values'] = $animationCopy['values'] -join ';' } + if (-not $animationCopy['dur']) { + $animationCopy['dur'] = "$($this.Duration.TotalSeconds)s" + } "<animateTransform $( @(foreach ($key in $animationCopy.Keys) { @@ -3494,16 +6946,75 @@ $b64 = [Convert]::ToBase64String($OutputEncoding.GetBytes($thisPattern.outerXml) PatternMask - $segments = @( -"<svg xmlns='http://www.w3.org/2000/svg' width='0%' height='0%'>" - "<defs>" - "<mask id='$($this.ID)-mask'>" - $this.Pattern.OuterXml -replace '\<\?[^\>]+\>' - "</mask>" - "</defs>" -"</svg>" + <# +.SYNOPSIS + Gets a Turtle's Pattern Mask +.DESCRIPTION + Gets the current turtle as a pattern mask. + + Everything under a white pixel will be visible. + + Everything under a black pixel will be invisible. + + This will be a mask of the turtle's `.Pattern` property, and will repeat the turtle's `.SVG` multiple times. +.EXAMPLE + # Masks will autoscale to the object bounding box by default + # Make sure to leave a hole. + turtle defines @( + turtle id smallsquare viewbox 84 teleport 21 21 square 42 @( + 'fill','black' + 'backgroundcolor','white' + ) patternmask + ) square 840 fill '#4488ff' stroke '#224488' pathattribute @{ + mask='url(#smallsquare-pattern-mask)' + } save ./square-pattern-mask.svg +.EXAMPLE + # Masks can contain morphing + turtle defines @( + turtle id smallsquare viewbox 84 @( + 'fill','black' + 'backgroundcolor','white' + ) morph @( + turtle teleport 21 21 square 42 + turtle teleport 42 42 square 1 + turtle teleport 21 21 square 42 + ) duration '00:00:01.68' patternmask + ) square 840 fill '#4488ff' stroke '#224488' pathattribute @{ + mask='url(#smallsquare-pattern-mask)' + } save ./square-pattern-mask-morph.svg +.LINK + https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/mask +#> +[OutputType([xml])] +param() +$keyPattern = '^pattern-?mask/' +$defaultId = "$($this.Id)-pattern-mask" +# Gather the mask attributes, and default the ID (the only attribute we actually need) +$maskAttributes = [Ordered]@{id=$defaultId} +# Attributes can exist in .SVGAttribute or .Attribute +foreach ($collectionName in 'SVGAttribute','Attribute') { + # as long as they start with mask/ + # (slashes are not valid attribute names, so this can denote a target name or type) + foreach ($key in $this.$collectionName.Keys -match $keyPattern) { + $maskAttributes[$key -replace $keyPattern] = $this.$collectionName[$key] + } +} + +# Create an attribute declaration +$maskAttributes = @(foreach ($attributeName in $maskAttributes.Keys) { + "$($attributeName)='$( + [Web.HttpUtility]::HtmlAttributeEncode($maskAttributes[$attributeName]) + )'" +}) -join ' ' + +# Declare the mask segments +$segments = @( +"<mask $maskAttributes>" + $this.Pattern.OuterXml -replace '\<\?[^\>]+\>' +"</mask>" ) -[xml]($segments -join '') +# join them and cast to XML. +[xml]$segments @@ -3583,33 +7094,67 @@ if ($chromeOutput -match '<img\ssrc="data:image/png;base64,(?<b64>[^"]+ Position - if (-not $this.'.Position') { - $this | Add-Member -MemberType NoteProperty -Force -Name '.Position' -Value ([pscustomobject]@{ X = 0; Y = 0 }) + <# +.SYNOPSIS + Gets the Turtle's position +.DESCRIPTION + Gets the current position of the turtle as a vector. +#> +[OutputType([Numerics.Vector2])] +param() +if (-not $this.'.Position') { + $this | Add-Member -MemberType NoteProperty -Force -Name '.Position' -Value ( + [Numerics.Vector2]::new(0,0) + ) } return $this.'.Position' - param([double[]]$xy) + <# +.SYNOPSIS + Sets the Turtle's position +.DESCRIPTION + Sets the position of the Turtle and updates its minimum and maximum. + + This should really not be done directly - the position should be updated as the Turtle moves. +.NOTES + Changing the position outside of the turtle will probably not work how you would expect. +#> +param([double[]]$xy) +# break apart the components $x, $y = $xy +# and add a position if we do not have one. if (-not $this.'.Position') { - $this | Add-Member -MemberType NoteProperty -Force -Name '.Position' -Value ([pscustomobject]@{ X = 0; Y = 0 }) + $this | Add-Member -MemberType NoteProperty -Force -Name '.Position' -Value ([Numerics.Vector2]@{ X = 0; Y = 0 }) } + +# Modify the position without creating a new object $this.'.Position'.X += $x $this.'.Position'.Y += $y +# And readback our new position $posX, $posY = $this.'.Position'.X, $this.'.Position'.Y +# If we have no .Minimum if (-not $this.'.Minimum') { - $this | Add-Member -MemberType NoteProperty -Force -Name '.Minimum' -Value ([pscustomobject]@{ X = 0; Y = 0 }) -} -if (-not $this.'.Maximum') { - $this | Add-Member -MemberType NoteProperty -Force -Name '.Maximum' -Value ([pscustomobject]@{ X = 0; Y = 0 }) + # create one. + $this | Add-Member -MemberType NoteProperty -Force -Name '.Minimum' -Value ([Numerics.Vector2]@{ X = 0; Y = 0 }) } + +# Then check if we need to update our minimum point. if ($posX -lt $this.'.Minimum'.X) { $this.'.Minimum'.X = $posX } if ($posY -lt $this.'.Minimum'.Y) { $this.'.Minimum'.Y = $posY } + +# If we have no .Maximum +if (-not $this.'.Maximum') { + # create one. + $this | Add-Member -MemberType NoteProperty -Force -Name '.Maximum' -Value ([Numerics.Vector2]@{ X = 0; Y = 0 }) +} + +# Then update our maximum point if ($posX -gt $this.'.Maximum'.X) { $this.'.Maximum'.X = $posX } @@ -3618,6 +7163,80 @@ if ($posY -gt $this.'.Maximum'.Y) { } + + Precision + + <# +.SYNOPSIS + Gets Turtle Precision +.DESCRIPTION + Gets the rounding precision for the turtle. + + Any move the turtle makes will be rounded by this number of digits. + + Paths with more rounding may be more accurate at extremly high resolutions. + + They will have difficulty rendering stepwise animations and take up more file space per point. + + The default value for `Precision` is currently `6` +#> +if (-not $this.'.Precision') { + $this | Add-Member NoteProperty '.Precision' 6 -Force +} +return $this.'.Precision' + + + + <# +.SYNOPSIS + Sets the Turtle's Precision +.DESCRIPTION + Sets the level of precision a turtle should use for rounding. + + This is the number of digits a value will be rounded to. + + Lower precision will result in smaller filesizes, and a much better chance of stepwise animations working properly. + + Higher precision will result in large filesizes and will occassionally cause stepwise animations to get stuck. +#> +param( +# The number of decimal places used in rounding. +[ValidateRange(1,28)] +[int] +$Precision = 6 +) + +$this | Add-Member NoteProperty '.Precision' $Precision -Force + + + + + + + ScriptBlock + + <# +.SYNOPSIS + Get the Turtle's ScriptBlock +.DESCRIPTION + Gets the ScriptBlock used to create the turtle. + + All steps will become a fluent pipeline. +.EXAMPLE + turtle SierpinskiTriangle 42 4 scriptBlock +#> +[OutputType([ScriptBlock])] +param() +# Join all of our previous command extents into a fluent pipeline +$stringifiedScript = $this.Commands.Extent -join + (' |' + [Environment]::NewLine + ' ') -replace # and then replace any unescaped use of 'ScriptBlock' or 'DataBlock' + "(?<!\[)(?>$( + 'ScriptBlock', 'DataBlock' -join '|' + ))(?!\])\s{0,}" +[ScriptBlock]::Create($stringifiedScript) + + + Stack @@ -3628,6 +7247,57 @@ $this.'.Stack' + + Start + + <# +.SYNOPSIS + Gets the Start Vector a Turtle +.DESCRIPTION + Gets the starting vector for a Turtle. + + Setting this value avoids an automatic calculation of a starting position. +.EXAMPLE + turtle width 300 height 300 start 50 square 200 start +#> +return $this.'.Start' + + + + <# +.SYNOPSIS + Sets the Start Vector for a Turtle +.DESCRIPTION + Sets the starting vector for a Turtle. + + This avoids an automatic calculation of a starting position +.EXAMPLE + turtle width 300 height 300 start 50 square 200 +#> +param( +[PSObject] +$Value +) + + +$aNewStart = + if ($value -is [object[]] -and $value -as [float[]]) { + [Numerics.Vector2]::new($value -as [float[]]) + } elseif ($value.GetType -and $value.GetType().IsPrimitive) { + [Numerics.Vector2]::new($value,$value) + } elseif ($value.X -and $value.Y) { + [Numerics.Vector2]::new($value.X,$value.Y) + } + +if ($aNewStart) { + $this | Add-Member NoteProperty '.Start' $aNewStart -Force +} + + + + + + Steps @@ -3666,16 +7336,146 @@ foreach ($step in $steps) { Stroke - if ($this.'.Stroke') { + <# +.SYNOPSIS + Gets a Turtle's stroke color +.DESCRIPTION + Gets one or more colors used to stroke the Turtle. + + By default, this is transparent. + + If more than one value is provided, the stroke will be a gradient. +.EXAMPLE + # Draw a blue square + turtle square 42 stroke blue +.EXAMPLE + # Draw a PowerShell blue square + turtle square 42 stroke '#4488ff' +.EXAMPLE + # Draw a red, green, blue gradient + turtle square 42 stroke red green blue show +.EXAMPLE + # Draw a red, green, blue linear gradient + turtle square 42 stroke red green blue linear show +.EXAMPLE + turtle flower stroke red green blue strokerule evenodd show +#> +if ($this.'.Stroke') { return $this.'.Stroke' } else { return 'currentcolor' } - param([string]$value) + <# +.SYNOPSIS + Sets a Turtle's stroke color +.DESCRIPTION + Sets one or more colors used to stroke the Turtle. + + By default, this is transparent. + + If more than one value is provided, the stroke will be a gradient. +.EXAMPLE + # Draw a blue square + turtle square 42 stroke blue +.EXAMPLE + # Draw a PowerShell blue square + turtle square 42 stroke '#4488ff' +.EXAMPLE + # Draw a red, green, blue gradient + turtle square 42 stroke red green blue show +.EXAMPLE + # Draw a red, green, blue linear gradient + turtle square 42 stroke red green blue linear show +.EXAMPLE + turtle flower stroke red green blue strokerule evenodd show +#> +param( +[PSObject[]] +$stroke = 'transparent' +) + +# If we have no stroke information, return +if (-not $stroke) { return } + +# If the stroke count is greater than one, try to make a graidnet +if ($stroke.Count -gt 1) { + + # Default to a radial gradient + $gradientTypeHint = 'radial' + # and create a collection for attributes + $gradientAttributes = [Ordered]@{ + # default our identifier to the current id plus `stroke-gradient` + # (so we could have multiple gradients without a collision) + id="$($this.id)-stroke-gradient" + } + + $stroke = @(foreach ($color in $stroke) { + # If the value matches `linear` or `radial` + if ($color -match '^(linear|radial)') { + # take the hint and make it the right type of gradient. + $gradientTypeHint = ($color -replace 'gradient').ToLower() + } + # If the color was `pad`, `reflect`, or `repeat` + elseif ($strokeColor -in 'pad', 'reflect', 'repeat') { + # take the hint and set the spreadMethod + $gradientAttributes['spreadMethod'] = $color + } + # If the stroke is a dictionary + elseif ($color -is [Collections.IDictionary]) { + # propagate the values into attributes. + foreach ($gradientAttributeKey in $color.Keys) { + $gradientAttributes[$gradientAttributeKey] = $color[$gradientAttributeKey] + } + } + # Otherwise output the color + else { + $color + } + }) + + # If we have no stroke colors after filtering, return + if (-not $stroke) { return } + + # If our count is one + if ($stroke.Count -eq 1) { + # it's not really going to be a gradient, so just use the one color. + $this | Add-Member -MemberType NoteProperty -Name '.Stroke' -Value $stroke -Force + return + } -$this | Add-Member -MemberType NoteProperty -Force -Name '.Stroke' -Value $value + # Now we have at least two colors we want to be a gradient + # We need to make sure the offset starts at 0% an ends at 100% + # and so we actually need to divide by one less than our stroke color, so we end at 100%. + $offsetStep = 1 / ($stroke.Count - 1) + $Gradient = @( + # Construct our gradient element. + "<${gradientTypeHint}Gradient$( + # propagate our attributes + @(foreach ($gradientAttributeKey in $gradientAttributes.Keys) { + " $gradientAttributeKey='$($gradientAttributes[$gradientAttributeKey])'" + }) -join '' + )>" + @( + # and put in our stop colors + for ($strokeNumber = 0; $strokeNumber -lt $stroke.Count; $strokeNumber++) { + "<stop offset='$($offsetStep * $strokeNumber * 100)%' stop-color='$($stroke[$strokeNumber])' />" + } + ) + "</${gradientTypeHint}Gradient>" + ) -join [Environment]::NewLine + + # add this gradient to our defines + $this.Defines += $Gradient + # and set stroke to this gradient. + $stroke = "url(`"#$($gradientAttributes.id)`")" +} +if (-not $this.'.stroke') { + $this | Add-Member -MemberType NoteProperty -Name '.Stroke' -Value $stroke -Force +} else { + $this.'.stroke' = $stroke +} @@ -3693,6 +7493,175 @@ $this | Add-Member -MemberType NoteProperty -Force -Name '.Stroke' -Value $value $this | Add-Member -MemberType NoteProperty -Force -Name '.StrokeWidth' -Value $value + + Style + + <# +.SYNOPSIS + Gets a Turtle's Style +.DESCRIPTION + Gets any CSS styles associated with the Turtle. + + These styles will be declared in a `<style>` element, just beneath a Turtle's `<svg>` +.EXAMPLE + turtle style '.myClass { color: #4488ff}' style +#> +param() + +if (-not $this.'.style') { + $this | Add-Member NoteProperty '.style' @() -Force +} + +$keyframe = $this.Keyframe +$myVariables = $this.Variable +$cssTypePattern = '^(?<type>\<[\w-].+?\>)[\:\=]?' +$myCssVariables = foreach ($variableKey in $myVariables.Keys -match '^--') { + $variableValue = $myVariables[$variableKey] + if ($variableValue -match $cssTypePattern) { + $variableValue = $variableValue -replace $cssTypePattern + "@property $variableKey { syntax: '$( + [Security.SecurityElement]::Escape($matches.type) + )'; initial-value: $($variableValue -replace $cssTypePattern)}" + } + "$variableKey",':', $variableValue -join '' +} +$styleElementParts = @( +if ($myCssVariables) { + "#$($this.id)-path, #$($this.id)-text {" + ($myCssVariables -join (';' + [Environment]::NewLine + (' ' * 4))) + "}" +} +foreach ($keyframeName in $keyframe.Keys) { + $keyframeKeyframes = $keyframe[$keyframeName] + "@keyframes $keyframeName {" + foreach ($percent in $keyframeKeyframes.Keys) { + " $percent {" + $props = $keyframeKeyframes[$percent] + foreach ($prop in $props.Keys) { + $value = $props.$prop + " ${prop}: $value;" + } + " }" + } + "}" + ".$keyframeName {" + " animation-name: $keyframeName;" + " animation-duration: $($this.Duration.TotalSeconds)s;" + " animation-iteration-count: infinite;" + "}" +} +if ($this.'.Style') { + "$($this.'.Style' -join (';' + [Environment]::NewLine))" +} +) + +if ($styleElementParts) { + # Style elements are one of the only places where we can be reasonably certain there will not be child elements + try { + # so if we have an error with unescaped content + return [xml]@("<style>" + $styleElementParts -join [Environment]::NewLine + "</style>") + } catch { + # catch it and escape the content + return [xml]@( + "<style>" + [Security.SecurityElement]::Escape($styleElementParts -join [Environment]::NewLine) + "</style>" + ) + } +} else { + return '' +} + +return $this.'.style' + + + <# +.SYNOPSIS + Sets a Turtle's Style +.DESCRIPTION + Sets any CSS styles associated with the Turtle. + + These styles will be declared in a `<style>` element, just beneath a Turtle's `<svg>` +.EXAMPLE + turtle style '.myClass { color: #4488ff}' style +.EXAMPLE + turtle style abc +.EXAMPLE + turtle style "@import url('https://fonts.googleapis.com/css?family=Abel')" text 'Hello World' textattribute @{'font-family'='Abel';'font-size'='3em'} fill 'red' save ./t.png show +#> +param( +[PSObject[]] +$Style +) + +filter toCss { + # Capture the input, + $in = $_ + # know myself, + $mySelf = $MyInvocation.MyCommand.ScriptBlock + # and determine our depth + $depth = 0 + # (with a little callstack peeking). + foreach ($frame in Get-PSCallStack) { + if ($frame.InvocationInfo.MyCommand.ScriptBlock -eq $mySelf) { + $depth++ + } + } + # Always substract one so we don't indent the root. + $depth-- + + if ($in -is [string]) { + $in # Directly output strings + } elseif ($in -is [Collections.IDictionary]) { + # Join dictionaries by semicolons and indentation + ($in.GetEnumerator() | & $mySelf) -join ( + ';' + [Environment]::NewLine + (' ' * 2 * $depth) + ) + } elseif ($in.Key -and $in.Value) { + # Key/value pairs containing dictionaries + if ($in.Value -is [Collections.IDictionary]) { + # become `selector { rules }` + ( + "$($in.Key) {", ( + (' ' * 2) + ($in.Value | & $mySelf) + ) -join ( + [Environment]::NewLine + (' ' * 2 * $depth) + ) + ) + ( + [Environment]::NewLine + (' ' * 2 * ($depth - 1)) + ) + '}' + } + elseif ($in.Value -is [TimeSpan]) { + "$($in.Key):$($in.Value.TotalSeconds)s" + } + else { + # Other key/value pairs are placed inline + "$($in.Key):$($in.Value)" + } + } + elseif ($in -is [PSObject]) { + # turn non-dictionaries into dictionaries + $inDictionary = [Ordered]@{} + foreach ($property in $in.psobject.properties) { + $inDictionary[$property.Name] = $in.($property.Name) + } + if ($inDictionary.Count) { + # and recurse. + $inDictionary | & $mySelf + } + } +} + +if (-not $this.'.style') { + $this | Add-Member NoteProperty '.style' @() -Force +} +$this.'.style' += $style |toCss + + + + SVG @@ -3702,8 +7671,8 @@ $this | Add-Member -MemberType NoteProperty -Force -Name '.StrokeWidth' -Value $ .DESCRIPTION Gets this turtle and any nested turtles as a single Scalable Vector Graphic. #> +[OutputType([xml])] param() -@( $svgAttributes = [Ordered]@{ xmlns='http://www.w3.org/2000/svg' @@ -3713,6 +7682,11 @@ $svgAttributes = [Ordered]@{ height='100%' } +# If opacity is set, it should apply to the entire SVG. +if ($null -ne $this.opacity) { + $svgAttributes['opacity'] = $this.opacity +} + # 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. @@ -3720,20 +7694,49 @@ if ($this.ViewBox[-1] -eq 0 -or $this.ViewBox[-2] -eq 0) { } # Any explicitly provided attributes should override any automatic attributes. + +# These can come from .Attribute +foreach ($key in $this.Attribute.Keys) { + if ($key -match '^svg/') { # (as long as they start with `svg/`) + $svgAttributes[$key -replace '^svg/'] = $this.Attribute[$key] + } +} + +# They can also come from `.SVGAttribute` foreach ($key in $this.SVGAttribute.Keys) { $svgAttributes[$key] = $this.SVGAttribute[$key] } +$svgElement = @( "<svg $(@(foreach ($attributeName in $svgAttributes.Keys) { + if ($attributeName -match '/') { continue } " $attributeName='$($svgAttributes[$attributeName])'" }) -join '')>" + # Declare any definitions, like markers or gradients. + if ($this.Defines) { + "<defs>" + $this.Defines + "</defs>" + } + + $style = $this.Style + if ($style -is [xml]) { + $style.OuterXml + } + + # Declare any SVG animations if ($this.SVGAnimation) {$this.SVGAnimation} - + if ($this.BackgroundColor) { + "<rect width='10000%' height='10000%' x='-5000%' y='-5000%' fill='$($this.BackgroundColor)' transform-origin='50% 50%' />" + } + if ($this.Link) { + "<a href='$($this.Link)'>" + } # Output our own path $this.PathElement.OuterXml # Followed by any text elements - $this.TextElement.OuterXml + $this.TextElement.OuterXml # If the turtle has children $children = @(foreach ($turtleName in $this.Turtles.Keys) { @@ -3757,8 +7760,12 @@ foreach ($key in $this.SVGAttribute.Keys) { } "</g>" } + if ($this.Link) { + "</a>" + } "</svg>" -) -join '' -as [xml] +) +[xml]$svgElement @@ -3870,21 +7877,17 @@ foreach ($key in $SVGAttribute.Keys) { Move-Turtle Flower | Select-Object -ExpandProperty Symbol #> +[OutputType([xml])] 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.SVG.OuterXml) - "</symbol>" - $( - if ($this.BackgroundColor) { - "<rect width='10000%' height='10000%' x='-5000%' y='-5000%' fill='$($this.BackgroundColor)' transform-origin='50% 50%' />" - } - ) - "<use href='#$($this.ID)-symbol' width='100%' height='100%' transform-origin='50% 50%' />" - "</svg>" -) -join '' -as [xml] +[xml]@( +"<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.SVG.OuterXml) + "</symbol>" + "<use href='#$($this.ID)-symbol' width='100%' height='100%' transform-origin='50% 50%' />" +"</svg>" +) @@ -3984,67 +7987,183 @@ $newAnimation = @(foreach ($animation in $TextAnimation) { } }) -$this | Add-Member -MemberType NoteProperty -Force -Name '.TextAnimation' -Value $newAnimation +$this | Add-Member -MemberType NoteProperty -Force -Name '.TextAnimation' -Value $newAnimation + + + + + TextAttribute + + <# +.SYNOPSIS + Gets any Text Attributes +.DESCRIPTION + Gets any attributes associated with the Turtle text + +#> +if (-not $this.'.TextAttribute') { + $this | Add-Member NoteProperty '.TextAttribute' ([Ordered]@{}) -Force +} +return $this.'.TextAttribute' + + + <# +.SYNOPSIS + Sets text attributes +.DESCRIPTION + Sets any attributes associated with the turtle text. + + These will become the attributes on the `<text>` element. +#> +param( +# The text attributes. +[Collections.IDictionary] +$TextAttribute = [Ordered]@{} +) + +if (-not $this.'.TextAttribute') { + $this | Add-Member -MemberType NoteProperty -Name '.TextAttribute' -Value ([Ordered]@{}) -Force +} +foreach ($key in $TextAttribute.Keys) { + $this.'.TextAttribute'[$key] = $TextAttribute[$key] +} + + + + TextElement + + <# +.SYNOPSIS + Gets a Turtle's text element +.DESCRIPTION + Gets a Turtle's text as a SVG Text element. + + If the Turtle does not have any text, this will return nothing. + + If the Turtle has text, but no path, the text will be centered in the Turtle's viewbox. +.LINK + https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/text +.EXAMPLE + turtle text "hello world" textElement +.EXAMPLE + turtle text "hello world" title "Hi!" textElement +#> +[OutputType([xml])] +param() + +# If there is no text, there's no text element +if (-not $this.Text) { return } + +# Collect all of our text attributes +$textAttributes = [Ordered]@{ + id="$($this.ID)-text" +} + +# If there are no steps +if (-not $this.Steps) { + # default the text to the middle + $textAttributes['dominant-baseline'] = 'middle' + $textAttributes['text-anchor'] = 'middle' + $textAttributes['x'] = '50%' + $textAttributes['y'] = '50%' +} + +if ($this.fill -ne 'transparent') { + $textAttributes['stroke'] = $this.stroke + $textAttributes['fill'] = $this.fill +} else { + $textAttributes['fill'] = $this.stroke +} + + +# Text Attributes can exist in Attribute or SVGAttribute, as long as they are prefixed. +$prefix = '^/?text/' +foreach ($collection in 'Attribute','SVGAttribute') { + if (-not $this.$Collection.Count) { continue } + foreach ($key in $this.$collection.Keys) { + if ($key -match $prefix) { + $textAttributes[$key -replace $prefix] = $this.$collection[$key] + } + } +} + +# Explicit text attributes will be copied last, so they take precedent. +foreach ($key in $this.TextAttribute.Keys) { + $textAttributes[$key] = $this.TextAttribute[$key] +} + +# Return a constructed element +return [xml]@( +# Create the text element +"<text$( + foreach ($TextAttributeName in $TextAttributes.Keys) { + " $TextAttributeName='$($TextAttributes[$TextAttributeName])'" + } +)>" + +# If there is a title +if ($this.Title) { + # embed it here (so that the text is accessible). + "<title>$([Security.SecurityElement]::Escape($this.Title))</title>" +} else { + # otherwise, use the text as the title. + "<title>$([Security.SecurityElement]::Escape($this.Text))</title>" +} - +# If there are any text animations, include them here. +if ($this.TextAnimation) {$this.TextAnimation} + +# Escape our text +$escapedText = [Security.SecurityElement]::Escape($this.Text) +# If we have steps, +if ($this.Steps) { + # put the escaped text within a `<textPath>`. + "<textPath href='#$($this.id)-path'>$escapedText</textPath>" +} else { + # otherwise, include the escaped text as the content + $escapedText +} +# close the element and return our XML. +"</text>" +) + - TextAttribute + Title <# .SYNOPSIS - Gets any Text Attributes + Gets a Turtle's title .DESCRIPTION - Gets any attributes associated with the Turtle text + Gets the title assigned to a Turtle. + A title will provide alternate text for the image that should be visible on hover, and should be available to screen readers. +.EXAMPLE + turtle square 42 title "It's Hip To Be Square" #> -if (-not $this.'.TextAttribute') { - $this | Add-Member NoteProperty '.TextAttribute' ([Ordered]@{}) -Force -} -return $this.'.TextAttribute' +return $this.'.Title' + <# .SYNOPSIS - Sets text attributes + Sets a Turtle's title .DESCRIPTION - Sets any attributes associated with the turtle text. - - These will become the attributes on the `<text>` element. + Sets the title assigned to a Turtle. + + A title will provide alternate text for the image that should be visible on hover, and should be available to screen readers. +.EXAMPLE + turtle square 42 title "It's Hip To Be Square" #> param( -# The text attributes. -[Collections.IDictionary] -$TextAttribute = [Ordered]@{} +# The title +[string] +$Title ) -if (-not $this.'.TextAttribute') { - $this | Add-Member -MemberType NoteProperty -Name '.TextAttribute' -Value ([Ordered]@{}) -Force -} -foreach ($key in $TextAttribute.Keys) { - $this.'.TextAttribute'[$key] = $TextAttribute[$key] -} - - - - TextElement - - -if ($this.Text) { - return @( - "<text id='$($this.ID)-text' $( - foreach ($TextAttributeName in $this.TextAttribute.Keys) { - " $TextAttributeName='$($this.TextAttribute[$TextAttributeName])'" - } -)>" - "<textPath href='#$($this.id)-path'>$([Security.SecurityElement]::Escape($this.Text))</textPath>" - if ($this.TextAnimation) {$this.TextAnimation} - "</text>" - ) -as [xml] -} - +$this | Add-Member NoteProperty '.Title' $title -Force - + Turtles @@ -4156,51 +8275,178 @@ foreach ($v in $value) { return $this.'.Turtles' + + Variable + + <# +.SYNOPSIS + Gets Turtle Variables +.DESCRIPTION + Gets variables associated with the Turtle. + + Variables that start with -- will become CSS variables +#> +param() + +if (-not $this.'.Variables') { + $this | Add-Member NoteProperty '.Variables' ([Ordered]@{}) -Force +} + +return $this.'.Variables' + + + <# +.SYNOPSIS + Sets Turtle variables +.DESCRIPTION + Sets arbitrary variables for the current Turtle. + + Variables that begin with -- will become CSS variables. +.EXAMPLE + turtle variable @{ + '--red' = '#ff0000' + '--green' = '#00ff00' + '--blue' = '#0000ff' + } style +#> +param( +# Any variables to set +[Collections.IDictionary[]] +$Variable = [Ordered]@{} +) + +$myVariables = $this.Variable +foreach ($variableSet in $Variable) { + foreach ($key in $variableSet.Keys) { + $myVariables[$key] = $variableSet[$key] + } +} + + + ViewBox - if ($this.'.ViewBox') { return $this.'.ViewBox' } + <# +.SYNOPSIS + Gets the Turtle's viewbox +.DESCRIPTION + Gets the Turtle's current viewBox. -$viewX = $this.Maximum.X + ($this.Minimum.X * -1) -$viewY = $this.Maximum.Y + ($this.Minimum.Y * -1) + If this has not been set, it will be automatically calculated by the minimum and maximum +.NOTES + turtle square 42 viewbox +#> + +param() + +# If we have set a viewbox, return it. +if ($this.'.ViewBox') { return $this.'.ViewBox' } + +# Otherwise, subtract max from minimum to get a bounding box +$viewBox = ($this.Maximum - $this.Minimum) + +$precision = $this.Precision +filter roundToPrecision { [Math]::Round($_, $precision)} + + +$viewX = [Math]::Round($viewBox.X, 10) +$viewY = [Math]::Round($viewBox.Y, 10) + +if ($viewX -and -not $viewY) { + $viewY = $viewX +} +if ($viewY -and -not $viewX) { + $viewX = $viewY +} + + +# and return the viewbox +if ($precision) { + return 0, 0, $viewX, $viewY | roundToPrecision +} else { + return 0, 0, $viewX, $viewY +} -return 0, 0, $viewX, $viewY - param( + <# +.SYNOPSIS + Sets the Turtle's ViewBox +.DESCRIPTION + Sets the ViewBox for the Turtle. + + If not set, the viewbox will be automatically calculated. + + Once set, the viewbox will not be automatically calculated until it is set to four zeros. +.EXAMPLE + turtle viewbox +.LINK + https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/viewBox +#> +param( +# The ViewBox coordinates. [double[]] $viewBox ) -if ($viewBox.Length -gt 4) { - $viewBox = $viewBox[0..3] -} -if ($viewBox.Length -lt 4) { - if ($viewBox.Length -eq 3) { - $viewBox = $viewBox[0], $viewBox[1], $viewBox[2],$viewBox[2] +# We have to ensure the viewbox only contains four points +$viewBox = switch ($viewBox.Length) { + # If only one point was provided, + 1 { + # check if it was negative. + if ($viewBox[0] -lt 0) { + # If it was, create a square anchored at that coordinate. + $viewBox[0],$viewBox[0], [Math]::Abs($viewBox[0]), [Math]::Abs($viewBox[0]) + } else { + # If the only point was positive, make a square anchored at <0,0> + 0,0, $viewBox[0], $viewBox[0] + } + } + # If two points were provided, we are making a rectangle + 2 { + # If both points are negative, the rectangle is anchored at <-X,-Y> + if ($viewBox[0] -lt 0 -and $viewBox[1] -lt 0) { + $viewBox[0],$viewBox[1], [Math]::Abs($viewBox[0]), [Math]::Abs($viewBox[1]) + } + elseif ($viewBox[0] -lt 0) { + # If only the X coordinate is negative, the rectangle is anchored at <-X,0> + $viewBox[0], 0, [Math]::Abs($viewBox[0]), $viewBox[1] + } + elseif ($viewBox[1] -lt 0) { + # If only the y coordinate is negative, the rectangle is anchored at <0,-Y> + 0, $viewBox[1], 0, [Math]::Abs($viewBox[1]) + } + else { + # If neither point is negative, the rectangle is anchored at <0,0> + 0,0, $viewBox[0], $viewBox[1] + } } - if ($viewBox.Length -eq 2) { - $viewBox = 0,0, $viewBox[0], $viewBox[1] + 3 { + # If three points were provided, the first are anchors, and the third coordinate represents a square size + $viewBox[0], $viewBox[1], $viewBox[2],$viewBox[2] } - if ($viewBox.Length -eq 1) { - $viewBox = 0,0, $viewBox[0], $viewBox[0] + default { + # If four or more points were provided, take the first four + $viewBox[0..3] } } +# If all four coordinates are zero if ($viewBox[0] -eq 0 -and $viewBox[1] -eq 0 -and $viewBox[2] -eq 0 -and $viewBox[3] -eq 0 ) { - $viewX = $this.Maximum.X + ($this.Minimum.X * -1) - $viewY = $this.Maximum.Y + ($this.Minimum.Y * -1) + # remove the viewbox $this.psobject.Properties.Remove('.ViewBox') - return + return # and return } +# Otherwise, set the viewBox $this | Add-Member -MemberType NoteProperty -Force -Name '.ViewBox' -Value $viewBox @@ -4319,4 +8565,355 @@ Position + + Turtle.History + + + PSStandardMembers + + + DefaultDisplayPropertySet + + Start + End + Delta + Instruction + + + + + + ToString + + + + DefaultDisplay + Start +End +Delta +Instruction + + + + + + Turtle.js + + + ToString + + + + JavaScript + + <# +.SYNOPSIS + `Turtle.js` definition +.DESCRIPTION + Our JavaScipt turtle is actually contained in a PowerShell object first. + + This object has a number of properties ending with `.js`. + + These are portions of the class. + + To create our class, we simply join these properties together, and output a javascript object. +.NOTES + This is an experimental feature and is subject to change and improvement. +.EXAMPLE + $turtleJs = [PSCustomObject]@{PSTypeName='Turtle.js'} + $html = @( + "<html><body>" + "<svg id='output' width='100%' height='100%'>" + "<path id='outputPath' stroke='#4488ff' />" + "</svg>" + "<script>" + "const turtle = $turtleJS" + "turtle.go('ROTATE', 45,'forward', 42)" + "document.getElementById('outputPath').setAttribute('d',turtle.pathData)" + "document.getElementById('output').setAttribute('viewBox', ``0 0 `${turtle.width} `${turtle.height}`` )" + "</script>" + "</body></html>" + ) > ./TurtleTest.html +#> +param() + +$objectParts = + +foreach ($javaScriptProperty in $this.psobject.properties | Sort-Object Name) { + # We only want the .js properties + if ($javaScriptProperty.Name -notmatch '\.js$') { continue } + # If the property is a function, we need to handle it differently + if ($javaScriptProperty.value -match '^function.+?\(') { + # specificically, we need to remove the "function " prefix from the name + $predicate, $extra = $javaScriptProperty.value -split '\(', 2 + # and then we need to reassemble it as a javascript method + $functionName = $predicate -replace 'function\s{1,}' + if ($functionName -match '^[gs]et_') { + $getSet = $functionName -replace '_.+$' + $propertyName = $functionName -replace '^[gs]et_' + if ($getSet -eq 'get') { + $extra = $extra -replace '^.{0,}\)\s{0,}' + "$getSet ${propertyName}() $($extra)" + } else { + "$getSet ${propertyName}($($extra)" + } + + } else { + "${functionName}:function ($extra" + } + + } + else { + # Otherwise, include it inline. + $javaScriptProperty.value + } + +} +# Since we are building a javascript object, we need to wrap everything in curly braces +@("{ +" +# Indentation does not matter to most machines, but people tend to appreciate it. +" " +($objectParts -join (',' + [Environment]::Newline + ' ')) +"}") -join '' + + + + + forward.js + function forward(distance) { + return this.step( + distance * Math.cos(this.heading * Math.PI / 180), + distance * Math.sin(this.heading * Math.PI / 180) + ) +} + + + get_heading.js + function get_heading() { + const _ = this + if ( _['#heading'] === undefined ) { + _['#heading'] = 0.0 + } + return _['#heading'] +} + + + + get_pathData.js + function get_pathData() { + let startX = 0; + let startY = 0; + if (!this.min) { this.min = { x: 0.0, y: 0.0}} + if (!this.max) { this.max = { x: 0.0, y: 0.0}} + if (this.min.x < 0) { + startX = (this.min.x) * -1 + } + if (this.min.y < 0) { + startY = (this.min.y) * -1 + } + return `m ${startX} ${startY} ${this.steps?.join(' ')}` +} + + + go.js + function go() { + const parsedArgs = this.parse.call(this, ...arguments) + let $this = this + for (let parsed of parsedArgs) { + let result = $this[parsed.method](...parsed.arguments) + } +} + + + goto.js + function goto(x,y) { return this.step(x - this.x, y - this.y) } + + + height.js + height: 0.0 + + + isPenDown.js + isPenDown: true + + + max.js + max: ({x:0.0, y:0.0}) + + + min.js + min: ({x:0.0, y:0.0}) + + + parse.js + function parse() { + let _ = this + var memberNames = [] + var objectPrototype = Object.getPrototypeOf(this) + var propertyNames = Object.getOwnPropertyNames(this) + var memberNameLookup = {} + for (let memberName of propertyNames) { + if (memberName == "constructor") { continue } + memberNameLookup[memberName.toUpperCase(memberName)] = memberName + } + + const parsed = [] + + const unbound = [] + + nextArgument: for (let argNumber = 0 ; argNumber < arguments.length; argNumber++) { + let arg = arguments[argNumber] + + if (typeof(arg) == "string" && ! memberNameLookup[arg.toUpperCase()]) { + unbound.push(arg) + continue nextArgument + } + + if (typeof(arg) != "string") { + unbound.push(arg) + continue nextArgument + } + + let memberName = arg.toUpperCase() + let memberInfo = memberNameLookup[arg.toUpperCase()] + let memberArgs = [] + lookingForParameters: for ( + let memberArgIndex = argNumber + 1; + memberArgIndex < arguments.length; + memberArgIndex++ + ) { + let memberArg = arguments[memberArgIndex] + if (typeof(memberArg) == "string" && memberNameLookup[memberArg.toUpperCase()]) { + argNumber = memberArgIndex - 1 + break lookingForParameters + } + memberArgs.push(memberArg) + } + + parsed.push({ + method: memberNameLookup[memberName], + arguments: memberArgs + }) + } + + return parsed +} + + + penDown.js + function penDown() { + let $this = this + $this.isPenDown = false + return $this +} + + + penUp.js + function penUp() { + let $this = this + $this.isPenUp = false + return $this +} + + + polygon.js + function polygon(size = 42, sides = 6) { + let $this = this + for (let side = 0; side < sides; side++) { + $this = $this.forward(size).rotate(360/sides) + } + return $this +} + + + resize.js + function resize() { + if (this.x > this.max.x) { this.max.x = this.x } + if (this.y > this.max.y) { this.max.y = this.y } + if (this.x < this.min.x) { this.min.x = this.x } + if (this.y < this.min.y) { this.min.y = this.y } + this.width = this.max.x - this.min.x + this.height = this.max.y - this.min.y + return this +} + + + rotate.js + function rotate(angle) { this.heading += Number(angle); return this } + + + set_heading.js + function set_heading(value) { + const _ = this + try { + _['#heading'] = new Number(value) + } catch { + _['#heading'] = 0.0 + } + return _ +} + + + step.js + function step(dx,dy) { + if (this.isPenDown) { this.steps.push(` l ${dx} ${dy} `) } + else { this.steps.push(` m ${dx} ${dy} `) } + this.x += dx; this.y += dy ; this.resize() + return this +} + + + + + steps.js + steps: [] + + + teleport.js + function teleport(x,y) { + var penState = this.penDown + this.penDown = false + this.step(x - this.x, y - this.y) + this.penDown = penState + return this +} + + + width.js + width: 0.0 + + + x.js + x: 0.0 + + + y.js + y: 0.0 + + + \ No newline at end of file diff --git a/Types/Turtle.History/DefaultDisplay.txt b/Types/Turtle.History/DefaultDisplay.txt new file mode 100644 index 0000000..7843de9 --- /dev/null +++ b/Types/Turtle.History/DefaultDisplay.txt @@ -0,0 +1,4 @@ +Start +End +Delta +Instruction diff --git a/Types/Turtle.History/ToString.ps1 b/Types/Turtle.History/ToString.ps1 new file mode 100644 index 0000000..d4785b3 --- /dev/null +++ b/Types/Turtle.History/ToString.ps1 @@ -0,0 +1 @@ +$this.Instruction diff --git a/Types/Turtle.js/ToString.ps1 b/Types/Turtle.js/ToString.ps1 new file mode 100644 index 0000000..f74689f --- /dev/null +++ b/Types/Turtle.js/ToString.ps1 @@ -0,0 +1,18 @@ +<# +.SYNOPSIS + `Turtle.js` definition +.DESCRIPTION + Our JavaScipt turtle is actually contained in a PowerShell object first. + + This object has a number of properties ending with `.js`. + + These are portions of the class. + + To create our class, we simply join these properties together, and output a javascript object. +#> +param() + + + +$javaScript = "$($this.JavaScript)" +return $javaScript diff --git a/Types/Turtle.js/forward.js b/Types/Turtle.js/forward.js new file mode 100644 index 0000000..466412b --- /dev/null +++ b/Types/Turtle.js/forward.js @@ -0,0 +1,6 @@ +function forward(distance) { + return this.step( + distance * Math.cos(this.heading * Math.PI / 180), + distance * Math.sin(this.heading * Math.PI / 180) + ) +} \ No newline at end of file diff --git a/Types/Turtle.js/get_JavaScript.ps1 b/Types/Turtle.js/get_JavaScript.ps1 new file mode 100644 index 0000000..afa5514 --- /dev/null +++ b/Types/Turtle.js/get_JavaScript.ps1 @@ -0,0 +1,70 @@ +<# +.SYNOPSIS + `Turtle.js` definition +.DESCRIPTION + Our JavaScipt turtle is actually contained in a PowerShell object first. + + This object has a number of properties ending with `.js`. + + These are portions of the class. + + To create our class, we simply join these properties together, and output a javascript object. +.NOTES + This is an experimental feature and is subject to change and improvement. +.EXAMPLE + $turtleJs = [PSCustomObject]@{PSTypeName='Turtle.js'} + $html = @( + "" + "" + "" + "" + "" + "" + ) > ./TurtleTest.html +#> +param() + +$objectParts = + +foreach ($javaScriptProperty in $this.psobject.properties | Sort-Object Name) { + # We only want the .js properties + if ($javaScriptProperty.Name -notmatch '\.js$') { continue } + # If the property is a function, we need to handle it differently + if ($javaScriptProperty.value -match '^function.+?\(') { + # specificically, we need to remove the "function " prefix from the name + $predicate, $extra = $javaScriptProperty.value -split '\(', 2 + # and then we need to reassemble it as a javascript method + $functionName = $predicate -replace 'function\s{1,}' + if ($functionName -match '^[gs]et_') { + $getSet = $functionName -replace '_.+$' + $propertyName = $functionName -replace '^[gs]et_' + if ($getSet -eq 'get') { + $extra = $extra -replace '^.{0,}\)\s{0,}' + "$getSet ${propertyName}() $($extra)" + } else { + "$getSet ${propertyName}($($extra)" + } + + } else { + "${functionName}:function ($extra" + } + + } + else { + # Otherwise, include it inline. + $javaScriptProperty.value + } + +} +# Since we are building a javascript object, we need to wrap everything in curly braces +@("{ +" +# Indentation does not matter to most machines, but people tend to appreciate it. +" " +($objectParts -join (',' + [Environment]::Newline + ' ')) +"}") -join '' diff --git a/Types/Turtle.js/get_heading.js b/Types/Turtle.js/get_heading.js new file mode 100644 index 0000000..bf11f24 --- /dev/null +++ b/Types/Turtle.js/get_heading.js @@ -0,0 +1,7 @@ +function get_heading() { + const _ = this + if ( _['#heading'] === undefined ) { + _['#heading'] = 0.0 + } + return _['#heading'] +} diff --git a/Types/Turtle.js/get_pathData.js b/Types/Turtle.js/get_pathData.js new file mode 100644 index 0000000..8133b6c --- /dev/null +++ b/Types/Turtle.js/get_pathData.js @@ -0,0 +1,13 @@ +function get_pathData() { + let startX = 0; + let startY = 0; + if (!this.min) { this.min = { x: 0.0, y: 0.0}} + if (!this.max) { this.max = { x: 0.0, y: 0.0}} + if (this.min.x < 0) { + startX = (this.min.x) * -1 + } + if (this.min.y < 0) { + startY = (this.min.y) * -1 + } + return `m ${startX} ${startY} ${this.steps?.join(' ')}` +} \ No newline at end of file diff --git a/Types/Turtle.js/go.js b/Types/Turtle.js/go.js new file mode 100644 index 0000000..26e8a95 --- /dev/null +++ b/Types/Turtle.js/go.js @@ -0,0 +1,7 @@ +function go() { + const parsedArgs = this.parse.call(this, ...arguments) + let $this = this + for (let parsed of parsedArgs) { + let result = $this[parsed.method](...parsed.arguments) + } +} \ No newline at end of file diff --git a/Types/Turtle.js/goto.js b/Types/Turtle.js/goto.js new file mode 100644 index 0000000..55b2229 --- /dev/null +++ b/Types/Turtle.js/goto.js @@ -0,0 +1 @@ +function goto(x,y) { return this.step(x - this.x, y - this.y) } \ No newline at end of file diff --git a/Types/Turtle.js/height.js b/Types/Turtle.js/height.js new file mode 100644 index 0000000..28849cd --- /dev/null +++ b/Types/Turtle.js/height.js @@ -0,0 +1 @@ +height: 0.0 \ No newline at end of file diff --git a/Types/Turtle.js/isPenDown.js b/Types/Turtle.js/isPenDown.js new file mode 100644 index 0000000..909f710 --- /dev/null +++ b/Types/Turtle.js/isPenDown.js @@ -0,0 +1 @@ +isPenDown: true \ No newline at end of file diff --git a/Types/Turtle.js/max.js b/Types/Turtle.js/max.js new file mode 100644 index 0000000..3fa91eb --- /dev/null +++ b/Types/Turtle.js/max.js @@ -0,0 +1 @@ +max: ({x:0.0, y:0.0}) \ No newline at end of file diff --git a/Types/Turtle.js/min.js b/Types/Turtle.js/min.js new file mode 100644 index 0000000..65ecabb --- /dev/null +++ b/Types/Turtle.js/min.js @@ -0,0 +1 @@ +min: ({x:0.0, y:0.0}) \ No newline at end of file diff --git a/Types/Turtle.js/parse.js b/Types/Turtle.js/parse.js new file mode 100644 index 0000000..f9a2b6e --- /dev/null +++ b/Types/Turtle.js/parse.js @@ -0,0 +1,52 @@ +function parse() { + let _ = this + var memberNames = [] + var objectPrototype = Object.getPrototypeOf(this) + var propertyNames = Object.getOwnPropertyNames(this) + var memberNameLookup = {} + for (let memberName of propertyNames) { + if (memberName == "constructor") { continue } + memberNameLookup[memberName.toUpperCase(memberName)] = memberName + } + + const parsed = [] + + const unbound = [] + + nextArgument: for (let argNumber = 0 ; argNumber < arguments.length; argNumber++) { + let arg = arguments[argNumber] + + if (typeof(arg) == "string" && ! memberNameLookup[arg.toUpperCase()]) { + unbound.push(arg) + continue nextArgument + } + + if (typeof(arg) != "string") { + unbound.push(arg) + continue nextArgument + } + + let memberName = arg.toUpperCase() + let memberInfo = memberNameLookup[arg.toUpperCase()] + let memberArgs = [] + lookingForParameters: for ( + let memberArgIndex = argNumber + 1; + memberArgIndex < arguments.length; + memberArgIndex++ + ) { + let memberArg = arguments[memberArgIndex] + if (typeof(memberArg) == "string" && memberNameLookup[memberArg.toUpperCase()]) { + argNumber = memberArgIndex - 1 + break lookingForParameters + } + memberArgs.push(memberArg) + } + + parsed.push({ + method: memberNameLookup[memberName], + arguments: memberArgs + }) + } + + return parsed +} \ No newline at end of file diff --git a/Types/Turtle.js/penDown.js b/Types/Turtle.js/penDown.js new file mode 100644 index 0000000..393332c --- /dev/null +++ b/Types/Turtle.js/penDown.js @@ -0,0 +1,5 @@ +function penDown() { + let $this = this + $this.isPenDown = false + return $this +} \ No newline at end of file diff --git a/Types/Turtle.js/penUp.js b/Types/Turtle.js/penUp.js new file mode 100644 index 0000000..22543d9 --- /dev/null +++ b/Types/Turtle.js/penUp.js @@ -0,0 +1,5 @@ +function penUp() { + let $this = this + $this.isPenUp = false + return $this +} \ No newline at end of file diff --git a/Types/Turtle.js/polygon.js b/Types/Turtle.js/polygon.js new file mode 100644 index 0000000..58d6860 --- /dev/null +++ b/Types/Turtle.js/polygon.js @@ -0,0 +1,7 @@ +function polygon(size = 42, sides = 6) { + let $this = this + for (let side = 0; side < sides; side++) { + $this = $this.forward(size).rotate(360/sides) + } + return $this +} \ No newline at end of file diff --git a/Types/Turtle.js/resize.js b/Types/Turtle.js/resize.js new file mode 100644 index 0000000..3a07883 --- /dev/null +++ b/Types/Turtle.js/resize.js @@ -0,0 +1,9 @@ +function resize() { + if (this.x > this.max.x) { this.max.x = this.x } + if (this.y > this.max.y) { this.max.y = this.y } + if (this.x < this.min.x) { this.min.x = this.x } + if (this.y < this.min.y) { this.min.y = this.y } + this.width = this.max.x - this.min.x + this.height = this.max.y - this.min.y + return this +} \ No newline at end of file diff --git a/Types/Turtle.js/rotate.js b/Types/Turtle.js/rotate.js new file mode 100644 index 0000000..bc5ac17 --- /dev/null +++ b/Types/Turtle.js/rotate.js @@ -0,0 +1 @@ +function rotate(angle) { this.heading += Number(angle); return this } \ No newline at end of file diff --git a/Types/Turtle.js/set_heading.js b/Types/Turtle.js/set_heading.js new file mode 100644 index 0000000..12a2bcd --- /dev/null +++ b/Types/Turtle.js/set_heading.js @@ -0,0 +1,9 @@ +function set_heading(value) { + const _ = this + try { + _['#heading'] = new Number(value) + } catch { + _['#heading'] = 0.0 + } + return _ +} \ No newline at end of file diff --git a/Types/Turtle.js/step.js b/Types/Turtle.js/step.js new file mode 100644 index 0000000..3c41a52 --- /dev/null +++ b/Types/Turtle.js/step.js @@ -0,0 +1,7 @@ +function step(dx,dy) { + if (this.isPenDown) { this.steps.push(` l ${dx} ${dy} `) } + else { this.steps.push(` m ${dx} ${dy} `) } + this.x += dx; this.y += dy ; this.resize() + return this +} + diff --git a/Types/Turtle.js/steps.js b/Types/Turtle.js/steps.js new file mode 100644 index 0000000..108f2a6 --- /dev/null +++ b/Types/Turtle.js/steps.js @@ -0,0 +1 @@ +steps: [] \ No newline at end of file diff --git a/Types/Turtle.js/teleport.js b/Types/Turtle.js/teleport.js new file mode 100644 index 0000000..73a821c --- /dev/null +++ b/Types/Turtle.js/teleport.js @@ -0,0 +1,7 @@ +function teleport(x,y) { + var penState = this.penDown + this.penDown = false + this.step(x - this.x, y - this.y) + this.penDown = penState + return this +} \ No newline at end of file diff --git a/Types/Turtle.js/width.js b/Types/Turtle.js/width.js new file mode 100644 index 0000000..4131a21 --- /dev/null +++ b/Types/Turtle.js/width.js @@ -0,0 +1 @@ +width: 0.0 \ No newline at end of file diff --git a/Types/Turtle.js/x.js b/Types/Turtle.js/x.js new file mode 100644 index 0000000..c829203 --- /dev/null +++ b/Types/Turtle.js/x.js @@ -0,0 +1 @@ +x: 0.0 \ No newline at end of file diff --git a/Types/Turtle.js/y.js b/Types/Turtle.js/y.js new file mode 100644 index 0000000..3d0ac4a --- /dev/null +++ b/Types/Turtle.js/y.js @@ -0,0 +1 @@ +y: 0.0 \ No newline at end of file diff --git a/Types/Turtle/Alias.psd1 b/Types/Turtle/Alias.psd1 index 64785d2..cc8bdd2 100644 --- a/Types/Turtle/Alias.psd1 +++ b/Types/Turtle/Alias.psd1 @@ -1,25 +1,83 @@ @{ + # SVG Path Compatibility + # (these methods directly reflect the corresponding instruction) + a = 'Arc' + c = 'CubicBezierCurve' + l = 'Step' + h = 'HorizontalLine' + q = 'QuadraticBezierCurve' + s = 'BezierCurve' + v = 'VerticalLine' + + # Shorter forms: + Pie = 'PieGraph' + ArcR = 'ArcRight' + ArcL = 'ArcLeft' + + # Logo ('Original') Turtle Compatibility pd = 'PenDown' pu = 'PenUp' fd = 'Forward' - down = 'PenDown' - up = 'PenUp' - l = 'Left' lt = 'Left' rt = 'Right' - r = 'Right' + bk = 'Backward' + + # Python Turtle Compatibility + SetPos = 'GoTo' + SetPosition = 'GoTo' + Back = 'Backward' xPos = 'xcor' yPos = 'ycor' + + # Python Turtle Compatibility That Will be Revised if/when the Turtle goes to 3D + down = 'PenDown' + up = 'PenUp' + r = 'Rotate' + + # CSS shape pre-compatibility LineTo = 'GoTo' - SetPos = 'GoTo' - SetPosition = 'GoTo' MoveTo = 'Teleport' - Back = 'Backward' - bk = 'Backward' - ArcR = 'ArcRight' - ArcL = 'ArcLeft' HLineBy = 'HorizontalLine' VLineBy = 'VerticalLine' + + # Usability aliases + Arm = 'Leg' + Sticks = 'Spokes' + + # Synonyms + Cobweb = 'Spiderweb' + Web = 'Spiderweb' + + # Common transposition errors FlowerStar = 'StarFlower' FlowerGolden = 'GoldenFlower' + + # Technically accurate aliases to more helpful names + Href = 'Link' + Defs = 'Defines' + MarkerMid = 'MarkerMiddle' + + # Aliasing plurals + Arguments = 'ArgumentList' + Args = 'ArgumentList' + Argument = 'ArgumentList' + Keyframes = 'Keyframe' + Styles = 'Style' + Spoke = 'Spokes' + Stick = 'Sticks' + + # Anglican color property names + BackgroundColour = 'BackgroundColor' + FillColour = 'FillColor' + PenColour = 'PenColor' + + # Internationalized Method Names. + # These are technically more correct, but will not be easy to type on all keyboards. + BézierCurve = 'BezierCurve' + QuadraticBézierCurve = 'QuadraticBezierCurve' + CubicBézierCurve = 'CubicBezierCurve' + SierpińskiTriangle = 'SierpinskiTriangle' + SierpińskiArrowHeadCurve = 'SierpinskiArrowHeadCurve' + SierpińskiSquareCurve = 'SierpinskiSquareCurve' + SierpińskiCurve = 'SierpinskiCurve' } \ No newline at end of file diff --git a/Types/Turtle/Arc.ps1 b/Types/Turtle/Arc.ps1 new file mode 100644 index 0000000..a9a35c7 --- /dev/null +++ b/Types/Turtle/Arc.ps1 @@ -0,0 +1,62 @@ +<# +.SYNOPSIS + Draws an Arc +.DESCRIPTION + Draws an arc with the Turtle. +.NOTES + This method directly corresponds to the `a` instruction in an SVG Path. +.LINK + https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorials/SVG_from_scratch/Paths#arcs +#> +param( +# The X Radius of the arc. +[double] +$RadiusX, + +# The Y Radius of the arc. +[double] +$RadiusY, + +# The rotation along the x-axis. +[double] +$Rotation = 0, + +# If set to 1, will draw a large arc. +# If set to 0, will draw a small arc +[ValidateSet(0,1, "Large", "Small", $true, $false)] +[PSObject] +$IsLargeArc = 1, + +# By default, the arc will be drawn clockwise +# If this is set to 1, the arc will be drawn counterclockwise +# If set to 1, will draw an arc counterclockwise +[ValidateSet(0, 1, 'Clockwise','CounterclockWise', 'cw', 'ccw', $true, $false)] +[PSObject] +$IsCounterClockwise = 0, + +# The deltaX +[double] +$DeltaX, + +# The deltaY +[double] +$DeltaY +) + +if ($DeltaX -or $DeltaY) { + $this.Position = $DeltaX,$DeltaY + # If the pen is down + if ($this.IsPenDown) { + # draw the curve + $LargeArcFlag = ($IsLargeArc -in 1, 'Large',$true) -as [byte] + $SweepFlag = ($IsCounterClockwise -in 1, 'ccw','CounterClockwise', $true) -as [byte] + $this.Steps.Add("a $RadiusX $RadiusY $Rotation $LargeArcFlag $SweepFlag $DeltaX $DeltaY") + } else { + # otherwise, move to the deltaX/deltaY + $this.Steps.Add("m $deltaX $deltaY") + } +} + +return $this + + diff --git a/Types/Turtle/ArcRight.ps1 b/Types/Turtle/ArcRight.ps1 index 2acf455..3f5b93a 100644 --- a/Types/Turtle/ArcRight.ps1 +++ b/Types/Turtle/ArcRight.ps1 @@ -15,7 +15,11 @@ $Radius = 10, # The angle of the arc [double] -$Angle = 60 +$Angle = 60, + +# The number of steps. If not provided, will default to half of the angle. +[int] +$StepCount ) # Determine the absolute angle, for this operation @@ -26,23 +30,30 @@ if ($absAngle -eq 0) { return $this } # Determine the circumference of a circle of this radius $Circumference = ((2 * $Radius) * [Math]::PI) -# Clamp the angle, as arcs beyond 360 just continue to circle -$ClampedAngle = - if ($absAngle -gt 360) { 360 } - elseif ($absAngle -lt -360) { -360} - else { $absAngle } -# The circumference step is the circumference divided by our clamped angle -$CircumferenceStep = $Circumference / [Math]::Floor($ClampedAngle) +# The circumference step is the circumference times +# the number of revolutions +$revolutionCount = $angle/360 +# divided by the angle +$CircumferenceStep = ($Circumference * $revolutionCount) / $Angle + # The iteration is as close to one or negative one as possible $iteration = $angle / [Math]::Floor($absAngle) -# Start off at iteration 1 -$angleDelta = $iteration -# while we have not reached the angle -while ([Math]::Abs($angleDelta) -le $absAngle) { - # Rotate and move forward - $null = $this.Rotate($iteration).Forward($CircumferenceStep) - $angleDelta+=$iteration + +# If we have no step count +if (-not $StepCount) { + # default to half of the angle. + $StepCount = [Math]::Round($absAngle / 2) +} +# Turn this into a ratio (by default, this ratio would be `2`). +$stepSize = $absAngle / $StepCount + +# Starting at zero, keep turning until we have reached the number. +# Increase our angle by iteration * stepSize each time. +for ($angleDelta = 0; [Math]::Abs($angleDelta) -lt $absAngle; $angleDelta+=($iteration*$stepSize)) { + $this = $this. # In each step, + Forward($CircumferenceStep*$StepSize). # move forward a fraction of the circumference, + Rotate($iteration*$StepSize) # and rotate a fraction of the total angle. } -# Return this so we can keep the chain. +# When we are done, return $this so we never break the chain. return $this \ No newline at end of file diff --git a/Types/Turtle/BezierCurve.ps1 b/Types/Turtle/BezierCurve.ps1 new file mode 100644 index 0000000..f600d68 --- /dev/null +++ b/Types/Turtle/BezierCurve.ps1 @@ -0,0 +1,58 @@ +<# +.SYNOPSIS + Draws a Bezier Curve +.DESCRIPTION + Draws a simple Bezier curve. +.EXAMPLE + turtle BezierCurve 0 -100 100 -100 save ./b1.svg +.EXAMPLE + turtle BezierCurve 0 -100 100 -100 BezierCurve 100 100 100 100 save ./b2.svg +.EXAMPLE + turtle @( + 'BezierCurve', 0, -100, 100, -100 + 'BezierCurve', 100, 100, 100, 100 + 'BezierCurve', 0, 100, -100, 100 + ) save ./b3.svg +.EXAMPLE + turtle @( + 'BezierCurve', 0, -100, 100, -100 + 'BezierCurve', 100, 100, 100, 100 + 'BezierCurve', 0, 100, -100, 100 + 'BezierCurve', -100, -100, -100, -100 + ) save ./b4.svg +.LINK + https://en.wikipedia.org/wiki/B%C3%A9zier_curve +#> +param( +# The X control point +[double] +$ControlX, + +# The Y control point +[double] +$ControlY, + +# The delta X +[double] +$DeltaX, + +# The delta Y +[double] +$DeltaY +) + + +if ($DeltaX -or $DeltaY) { + $this.Position = $DeltaX,$DeltaY + # If the pen is down + if ($this.IsPenDown) { + # draw the curve + $this.Steps.Add("s $controlX $controlY $deltaX $deltaY") + } else { + # otherwise, move to the deltaX/deltaY + $this.Steps.Add("m $deltaX $deltaY") + } +} + +return $this + diff --git a/Types/Turtle/BinaryTree.ps1 b/Types/Turtle/BinaryTree.ps1 index 951917b..37ae9d8 100644 --- a/Types/Turtle/BinaryTree.ps1 +++ b/Types/Turtle/BinaryTree.ps1 @@ -1,14 +1,25 @@ +<# +.SYNOPSIS + Draws a binary tree +.DESCRIPTION + Draws a binary tree using an L-system. +.LINK + https://en.wikipedia.org/wiki/L-system#Example_2:_fractal_(binary)_tree +#> param( - [double]$Size = 20, - [int]$Order = 4, - [double]$Angle = 45 +# The size of each segment +[double]$Size = 42, +# The order of magnitude (the number of times the L-system is expanded) +[int]$Order = 4, +# The angle +[double]$Angle = 45 ) return $this.Rotate(-90).LSystem('0', [Ordered]@{ '1' = '11' '0' = '1[0]0' }, $Order, [Ordered]@{ '[01]' = { $this.Forward($Size) } - '\[' = { $this.Rotate($Angle * -1).Push() } + '\[' = { $this.Push().Rotate($Angle * -1) } '\]' = { $this.Pop().Rotate($Angle) } }) diff --git a/Types/Turtle/Circle.ps1 b/Types/Turtle/Circle.ps1 index 8b1c187..f018904 100644 --- a/Types/Turtle/Circle.ps1 +++ b/Types/Turtle/Circle.ps1 @@ -11,55 +11,164 @@ To draw a quarter circle, use an extent of 0.25. To draw a half hexagon, use an extent of 0.5 and step count of 6. +.EXAMPLE + turtle circle 42 | Save-Turtle ./Circle.svg +.EXAMPLE + turtle circle 42 .5 | Save-Turtle ./HalfCircle.svg +.EXAMPLE + turtle circle 42 .25 | Save-Turtle ./QuarterCircle.svg +.EXAMPLE + turtle circle 42 | Save-Turtle ./CirclePattern.svg +.EXAMPLE + turtle start 21 21 circle 42 morph @( + turtle start 21 21 circle 42 + turtle start 21 21 circle -42 + turtle start 21 21 circle 42 + ) | Save-Turtle ./CircleMorphPattern.svg .EXAMPLE $turtle = New-Turtle $turtle.Circle(10).Pattern.Save("$pwd/CirclePattern.svg") .EXAMPLE - Move-Turtle Circle 10 | - Save-Turtle "$pwd/CirclePattern.svg" -Property Pattern + turtle circle 42 1 90 morph | + Save-Turtle ./CircleConstructionMorph.svg .EXAMPLE - $turtle = New-Turtle | - Move-Turtle Forward 10 | - Move-Turtle Rotate -90 | - Move-Turtle Circle 5 | - Move-Turtle Circle 5 .5 | - Move-Turtle Rotate -90 | - Move-Turtle Forward 10 | Save-Turtle .\DashDotDash.svg + turtle forward 42 rotate -90 Circle 21 Circle 21 .5 rotate -90 forward 42 | + Save-Turtle ./DashDotDash.svg .EXAMPLE - $turtle = New-Turtle | - Move-Turtle Forward 20 | - Move-Turtle Circle 5 .75 | - Move-Turtle Forward 20 | - Move-Turtle Circle 5 .75 | - Move-Turtle Forward 20 | - Move-Turtle Circle 5 .75 | - Move-Turtle Forward 20 | - Move-Turtle Circle 5 .75 | - Save-Turtle .\CommandSymbol.svg + turtle @('forward', 40, 'Circle', 10, .75 * 4) | + Save-Turtle ./CommandSymbol.svg +.EXAMPLE + turtle @('forward', 40, 'Circle', 10, .75 * 4) morph | + Save-Turtle ./CommandSymbolStepMorph.svg +.EXAMPLE + turtle @('forward', 40, 'Circle', 10, .75 * 4) | + Save-Turtle ./CommandSymbolPattern.svg #> param( -[double]$Radius = 10, +# The radius of the circle +[double]$Radius = 42, +# The portion of the circle to draw. [double]$Extent = 1, -[int]$StepCount = 180 +# The number of steps. +# If this is not provided, steps will be automatically determined +# If the the extent is between `1` or `-1` and the angle is a multiple of 90, +# then the circle will be drawn in up to four steps. +# Otherwise, the step count will default to 180. +[int]$StepCount ) +if (-not $this) { return } +if ($extent -eq 0) { return $this } + +# If the step count was not specified, and the `-Extent` is `1` or `-1`, +# we want to draw an optimized path around the circle. +if ((-not $StepCount) -and ( + -not (($extent * 360) % 90) + ) -and + $extent -le 1 -and + $extent -ge -1 +) { + # First, we need to know what the center is. + # Luckily, the center is always a right triangle away + $headingToCenter = $this.Heading + 90 + $circleCenter = [Numerics.Vector2]::new( + $this.X + ($radius * [math]::cos($headingToCenter * [Math]::PI / 180)), + $this.Y + ($radius * [math]::sin($headingToCenter * [Math]::PI / 180)) + ) + + # Once we know the center, we can construct four vectors for each quadrant of the circle + $circleRight, $circleBottom, $circleLeft, $circleTop = foreach ($n in 0..3) { + $headingTo = $this.Heading + (90 * $n) + $circleCenter + [Numerics.Vector2]::new( + $radius * [math]::cos($headingTo * [Math]::PI / 180), + $radius * [math]::sin($headingTo * [Math]::PI / 180) + ) + } + # and then we can draw up to four relative arcs. + # (this ensures a pure circle is smoothly drawn and the bounding box is updated accordingly) + $updated = switch ($extent * 360) { + 90 { + $this. + Arc($radius, $radius, 0, $false, $true,$circleRight.X - $this.X, $circleRight.Y - $this.Y). + Rotate(90) + } + -90 { + $this. + Arc($radius, $radius, 0, $false, $false,$circleLeft.X - $this.X, $circleLeft.Y - $this.Y). + Rotate(-90) + } + 180 { + $this. + Arc($radius, $radius, 0, $false, $true,$circleRight.X - $this.X, $circleRight.Y - $this.Y). + Arc($radius, $radius, 0, $false, $true,$circleBottom.X - $this.X, $circleBottom.Y - $this.Y). + Rotate(180) + } + -180 { + $this. + Arc($radius, $radius, 0, $false, $false,$circleLeft.X - $this.X, $circleLeft.Y - $this.Y). + Arc($radius, $radius, 0, $false, $false,$circleBottom.X - $this.X, $circleBottom.Y - $this.Y). + Rotate(-180) + } + 270 { + $this. + Arc($radius, $radius, 0, $false, $true,$circleRight.X - $this.X, $circleRight.Y - $this.Y). + Arc($radius, $radius, 0, $false, $true,$circleBottom.X - $this.X, $circleBottom.Y - $this.Y). + Arc($radius, $radius, 0, $false, $true,$circleLeft.X - $this.X, $circleLeft.Y - $this.Y). + Rotate(270) + } + -270 { + $this. + Arc($radius, $radius, 0, $false, $false,$circleLeft.X - $this.X, $circleLeft.Y - $this.Y). + Arc($radius, $radius, 0, $false, $false,$circleBottom.X - $this.X, $circleBottom.Y - $this.Y). + Arc($radius, $radius, 0, $false, $false,$circleRight.X - $this.X, $circleRight.Y - $this.Y). + Rotate(-270) + } + 360 { + $this. + Arc($radius, $radius, 0, $false, $true,$circleRight.X - $this.X, $circleRight.Y - $this.Y). + Arc($radius, $radius, 0, $false, $true,$circleBottom.X - $this.X, $circleBottom.Y - $this.Y). + Arc($radius, $radius, 0, $false, $true,$circleLeft.X - $this.X, $circleLeft.Y - $this.Y). + Arc($radius, $radius, 0, $false, $true,$circleTop.X - $this.X, $circleTop.Y - $this.Y) + } + -360 { + $this. + Arc($radius, $radius, 0, $false, $false,$circleLeft.X - $this.X, $circleLeft.Y - $this.Y). + Arc($radius, $radius, 0, $false, $false,$circleBottom.X - $this.X, $circleBottom.Y - $this.Y). + Arc($radius, $radius, 0, $false, $false,$circleRight.X - $this.X, $circleRight.Y - $this.Y). + Arc($radius, $radius, 0, $false, $false,$circleTop.X - $this.X, $circleTop.Y - $this.Y) + } + } + + # If we drew our arcs + if ($updated) { + # return the updated turtle + return $updated + } +} + +# If no step count was specified, default to 180 +if (-not $StepCount) { $StepCount = 180 } + +# Determine the circumference of the circle $circumference = 2 * [math]::PI * $Radius +# and divide by the number of steps $circumferenceStep = $circumference / $StepCount -if ($extent -eq 0) { return $this} - +# Get a multiplier for our extent, so we turn in the right direction $extentMultiplier = if ($extent -gt 0) { 1 } else { -1 } $currentExtent = 0 $maxExtent = [math]::Abs($extent) - +# determine how much extent each step covers. $extentStep = 1/$StepCount +# Every step we take $null = foreach ($n in 1..$StepCount) { - + # we move forward by a portion of the circumference $this.Forward($circumferenceStep) $currentExtent += $extentStep + # and we rotate (as long as we would not exceed the extent). if ($n -le $StepCount -and $currentExtent -le $maxExtent) { $this.Rotate( (360 / $StepCount) * $extentMultiplier) } @@ -68,4 +177,5 @@ $null = foreach ($n in 1..$StepCount) { break } } +# Once we have taken all of the necessary steps, return this so we never break the chain. return $this \ No newline at end of file diff --git a/Types/Turtle/CircleArc.ps1 b/Types/Turtle/CircleArc.ps1 new file mode 100644 index 0000000..42ccf7b --- /dev/null +++ b/Types/Turtle/CircleArc.ps1 @@ -0,0 +1,80 @@ +<# +.SYNOPSIS + Draws a Circle Arc +.DESCRIPTION + Draws a Circular Arc. + + The Turtle heading will not change, and the Turtle will end up at it's original position. +.EXAMPLE + Turtle CircleArc +.EXAMPLE + Turtle @( + 'CircleArc',42, 90, + 'Rotate', 90 * 4 + ) save ./Quadrants.svg +.EXAMPLE + Turtle @( + 'CircleArc',42, 60, + 'Rotate', 60 * 6 + ) save ./Sextants.svg +.EXAMPLE + Turtle @( + 'CircleArc',42, 45, + 'Rotate', 45 * 8 + ) save ./Octants.svg +#> +param( +# The radius of the circle +[double] +$Radius = 42, + +# The angle of the arc +[double] +$Angle = 30 +) + +# If we wanted an angle that was a multiple of 360 +# we actually want to just draw a circle +if ([Math]::Round($Angle % 360, $this.Precision) -eq 0) { + # We start at the center + $centerX = $this.X + $centerY = $this.Y + + # Jump to an edge + $this = $this.Jump($Radius) + + # and track the delta + $DeltaX = $this.X - $centerX + $DeltaY = $this.Y - $centerY + + + return $this. + # Arcing to the opposite of that delta (*2) takes us to the far edge + Arc($Radius, $Radius, 0, $false, $false, $DeltaX * -2, $DeltaY * -2). + # And Arcing back takes us to our original position along the edge + Arc($Radius, $Radius, 0, $false, $false, $DeltaX * 2, $DeltaY * 2). + # Jump back and we are back in the center of the circle. + Jump(-$radius) +} + +# For a normal circular arc, start by pushing our location onto the stack +$this = $this.Push() +# Draw a line to the edge of the circle +$null = $this.Forward($Radius) +# This will be the wedge end +$WedgeEndX = $this.Position.X +$WedgeEndY = $this.Position.Y +# Go back to the center, rotate, and move forward by the radius. +$null = $this.Forward(-$radius) +$null = $this.Rotate($Angle).Forward($radius) +# now we can compute the distance to the end of the wedge +$DeltaX = $WedgeEndX - $this.Position.X +$DeltaY = $WedgeEndY - $this.Position.Y +# and draw an arc to this location +$this = $this.Arc($Radius, $Radius, 0, ($Angle -gt 180), $false, $DeltaX, $DeltaY) +# and then pop our position back +$null = $this.Pop() +# and return this +$null = $this.ResizeViewBox($Radius) +return $this + diff --git a/Types/Turtle/CubicBezierCurve.ps1 b/Types/Turtle/CubicBezierCurve.ps1 new file mode 100644 index 0000000..fd83a82 --- /dev/null +++ b/Types/Turtle/CubicBezierCurve.ps1 @@ -0,0 +1,222 @@ + +<# +.SYNOPSIS + Draws a Cubic Bezier Curve +.DESCRIPTION + Draws a Cubic Bezier curve. + + Cubic Bezier curves take three points: + + * A Start Control Point + * An End Control Point + * An End Point + + A line will be drawn from the current position to the end point, + curved towards both the start and control point. +.EXAMPLE + turtle @( + 'CubicBezierCurve', + 200,0, # Start Control Point + 0,200, # End Control Point + 200,200 # End Point + ) save ./cubic.svg +.EXAMPLE + turtle width 200 height 200 morph @( + turtle 'CubicBezierCurve', + 200,0, # Start Control Point + 0,200, # End Control Point + 200,200 # End Point + + turtle 'CubicBezierCurve', + 0,200, # Start Control Point + 200,0, # End Control Point + 200,200 # End Point + turtle 'CubicBezierCurve', + 200,0, # Start Control Point + 0,200, # End Control Point + 200,200 # End Point + ) save ./CubicMorph.svg +.EXAMPLE + turtle width 200 height 200 morph @( + turtle 'CubicBezierCurve', + 200,0, # Start Control Point + 0,200, # End Control Point + 200,200 # End Point + + turtle 'CubicBezierCurve', + 0,200, # Start Control Point + 200,0, # End Control Point + 200,200 # End Point + + turtle 'CubicBezierCurve', + 200,200, # Start Control Point + 200,0, # End Control Point + 200,200 # End Point + + turtle 'CubicBezierCurve', + 0,200, # Start Control Point + 0,200, # End Control Point + 200,200 # End Point + + turtle 'CubicBezierCurve', + 400,0, # Start Control Point + 0,200, # End Control Point + 200,200 # End Point + + turtle 'CubicBezierCurve', + 0,200, # Start Control Point + 200,0, # End Control Point + 200,200 # End Point + + turtle 'CubicBezierCurve', + 400,0, # Start Control Point + 0,400, # End Control Point + 200,200 # End Point + + turtle 'CubicBezierCurve', + 0,200, # Start Control Point + 200,0, # End Control Point + 200,200 # End Point + + turtle 'CubicBezierCurve', + 200,0, # Start Control Point + 0,200, # End Control Point + 200,200 # End Point + ) save ./MoreCubicMorphs.svg +.EXAMPLE + # Cubic Bezier Curves are Aliased to 'c' + turtle c 0 200 200 0 200 200 +.EXAMPLE + turtle c 0 200 200 0 200 200 morph @( + turtle c 0 0 0 0 200 200 + turtle c 0 200 200 0 200 200 + turtle c 0 0 0 0 200 200 + turtle c 200 0 0 200 200 200 + turtle c 0 0 0 0 200 200 + ) save ./cmorph.svg +.EXAMPLE + turtle teleport 200 0 c 0 0 0 0 -200 200 morph @( + turtle c 0 0 0 0 -200 200 + turtle c 0 200 -200 0 -200 200 + turtle c 0 0 0 0 -200 200 + turtle c -200 0 0 200 -200 200 + turtle c 0 0 0 0 -200 200 + ) save ./cmorph2.svg +.EXAMPLE + turtle backgroundcolor '#000000' width 200 height 200 turtles @( + turtle width 200 height 200 morph @( + turtle c 0 0 0 0 200 200 + turtle c 0 200 200 0 200 200 + turtle c 0 0 0 0 200 200 + turtle c 200 0 0 200 200 200 + turtle c 0 0 0 0 200 200 + ) fill '#4488ff' stroke '#224488' + turtle width 200 height 200 teleport 200 morph @( + turtle c 0 0 0 0 -200 200 + turtle c 0 200 -200 0 -200 200 + turtle c 0 0 0 0 -200 200 + turtle c -200 0 0 200 -200 200 + turtle c 0 0 0 0 -200 200 + ) stroke '#4488ff' fill '#224488' + ) save ./cmorph3.svg +.EXAMPLE + turtle backgroundcolor '#000000' width 200 height 200 turtles @( + turtle width 200 height 200 morph @( + turtle teleport 100 0 c 0 0 0 0 0 200 + turtle teleport 100 0 c -100 0 100 200 0 200 + turtle teleport 100 0 c 0 0 0 0 0 200 + ) fill '#4488ff' stroke '#224488' + turtle width 200 height 200 teleport 200 morph @( + turtle teleport 0 100 c 0 0 0 0 200 0 + turtle teleport 0 100 c 0 -100 200 100 200 0 + turtle teleport 0 100 c 0 0 0 0 200 0 + ) stroke '#4488ff' fill '#224488' + ) save ./cmorph4.svg +.EXAMPLE + turtle backgroundcolor '#000000' width 200 height 200 turtles @( + turtle width 200 height 200 morph @( + turtle c 0 0 0 0 200 200 + turtle c 0 200 200 0 200 200 + turtle c 0 0 0 0 200 200 + turtle c 200 0 0 200 200 200 + turtle c 0 0 0 0 200 200 + ) fill '#4488ff' stroke '#224488' + turtle width 200 height 200 teleport 200 morph @( + turtle c 0 0 0 0 -200 200 + turtle c 0 200 -200 0 -200 200 + turtle c 0 0 0 0 -200 200 + turtle c -200 0 0 200 -200 200 + turtle c 0 0 0 0 -200 200 + ) stroke '#4488ff' fill '#224488' + turtle width 200 height 200 morph @( + turtle teleport 100 0 c 0 0 0 0 0 200 + turtle teleport 100 0 c -100 0 100 200 0 200 + turtle teleport 100 0 c 0 0 0 0 0 200 + ) fill '#4488ff' stroke '#224488' + turtle width 200 height 200 teleport 200 morph @( + turtle teleport 0 100 c 0 0 0 0 200 0 + turtle teleport 0 100 c 0 -100 200 100 200 0 + turtle teleport 0 100 c 0 0 0 0 200 0 + ) stroke '#4488ff' fill '#224488' + ) save ./cmorph5.svg +.EXAMPLE + turtle backgroundcolor '#000000' width 200 height 200 turtles @( + $r = @(foreach ($n in 1..4) { Get-Random -Min 0 -Max 200}) + turtle width 200 height 200 morph @( + turtle teleport 100 0 c 0 0 0 0 0 200 + turtle teleport 100 0 c $r $r $r $r 0 200 + turtle teleport 100 0 c 0 0 0 0 0 200 + ) fill '#4488ff' stroke '#224488' + turtle width 200 height 200 teleport 200 morph @( + turtle teleport 0 100 c 0 0 0 0 200 0 + turtle teleport 0 100 c $r $r $r $r 200 0 + turtle teleport 0 100 c 0 0 0 0 200 0 + ) stroke '#4488ff' fill '#224488' + ) save ./cmorphrandom.svg +.NOTES + This corresponds to the `c` element in an SVG Path +.LINK + https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorials/SVG_from_scratch/Paths#b%C3%A9zier_curves +.LINK + https://en.wikipedia.org/wiki/B%C3%A9zier_curve +#> +param( +# The X control point +[double] +$ControlStartX, + +# The Y control point +[double] +$ControlStartY, + +# The X control point +[double] +$ControlEndX, + +# The Y control point +[double] +$ControlEndY, + +# The delta X +[double] +$DeltaX, + +# The delta Y +[double] +$DeltaY +) + +if ($DeltaX -or $DeltaY) { + $this.Position = $DeltaX, $DeltaY + # If the pen is down + if ($this.IsPenDown) { + # draw the curve + $this.Steps.Add("c $ControlStartX $ControlStartY $ControlEndX $ControlEndY $DeltaX $DeltaY") + } else { + # otherwise, move to the deltaX/deltaY + $this.Steps.Add("m $DeltaX $DeltaY") + } +} + +return $this + diff --git a/Types/Turtle/Forward.ps1 b/Types/Turtle/Forward.ps1 index 9a2a4b2..a3530e6 100644 --- a/Types/Turtle/Forward.ps1 +++ b/Types/Turtle/Forward.ps1 @@ -12,7 +12,7 @@ param( $Distance = 10 ) -$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 +$heading = $this.Heading +$x = $Distance * [math]::cos($heading * [Math]::PI / 180) +$y = $Distance * [math]::sin($heading * [Math]::PI / 180) +return $this.Step($x, $y) diff --git a/Types/Turtle/FractalPlant.ps1 b/Types/Turtle/FractalPlant.ps1 index cd780c0..5cacab5 100644 --- a/Types/Turtle/FractalPlant.ps1 +++ b/Types/Turtle/FractalPlant.ps1 @@ -1,14 +1,30 @@ +<# +.SYNOPSIS + Draws a Fractal Plant +.DESCRIPTION + Draws a Fractal Plant as an L-System +.LINK + https://en.wikipedia.org/wiki/L-system#Example_7:_fractal_plant +.EXAMPLE + turtle FractalPlant save ./FractalPlant.svg +.EXAMPLE + turtle FractalPlant morph save ./FractalPlantMorph.svg +#> param( - [double]$Size = 20, + # The size of each segment + [double]$Size = 42, + # The order of magnitude (the number of times the L-system is expanded) [int]$Order = 4, - [double]$Angle = 25 + # The angle of each segment + [double]$Angle = -25 ) return $this.Rotate(-90).LSystem('-X', [Ordered]@{ 'X' = 'F+[[X]-X]-F[-FX]+X' - 'F' = 'FF' + 'F' = 'FF' }, $Order, [Ordered]@{ - 'F' = { $this.Forward($Size) } - '\[' = { $this.Rotate($Angle * -1).Push() } - '\]' = { $this.Pop().Rotate($Angle) } -}) - + 'F' = { $this.Forward($Size) } + '\+' = { $this.Rotate($angle)} + '\-' = { $this.Rotate($angle * -1)} + '\[' = { $this.Push() } + '\]' = { $this.Pop() } +}) \ No newline at end of file diff --git a/Types/Turtle/FractalShrub.ps1 b/Types/Turtle/FractalShrub.ps1 new file mode 100644 index 0000000..e655941 --- /dev/null +++ b/Types/Turtle/FractalShrub.ps1 @@ -0,0 +1,32 @@ +<# +.SYNOPSIS + Draws a Fractal Shrub +.DESCRIPTION + Draws a Fractal Shrub using an an L-System. + + This is a modification of the fractal plant will less rotation +.LINK + https://en.wikipedia.org/wiki/L-system#Example_7:_fractal_plant +.EXAMPLE + turtle FractalShrub save ./FractalShrub.svg +.EXAMPLE + turtle FractalShrub morph save ./FractalShrubMorph.svg +#> +param( + # The size of each segment + [double]$Size = 42, + # The order of magnitude (the number of times the L-system is expanded) + [int]$Order = 4, + # The angle of each segment + [double]$Angle = -25 +) +return $this.Rotate(-90).LSystem('-X', [Ordered]@{ + 'X' = 'F[[X]X]F[FX]X' + 'F' = 'FF' +}, $Order, [Ordered]@{ + 'F' = { $this.Forward($Size) } + #'\+' = { $this.Rotate($angle)} + # '\-' = { $this.Rotate($angle * -1)} + '\[' = { $this.Push().Rotate($angle) } + '\]' = { $this.Pop().Rotate($angle * -1) } +}) \ No newline at end of file diff --git a/Types/Turtle/HorizontalLine.ps1 b/Types/Turtle/HorizontalLine.ps1 index 111600c..0257320 100644 --- a/Types/Turtle/HorizontalLine.ps1 +++ b/Types/Turtle/HorizontalLine.ps1 @@ -5,11 +5,22 @@ Draws a horizontal line. The heading will not be changed. +.EXAMPLE + turtle HorizontalLine 42 +.EXAMPLE + turtle HorizontalLine 42 pathdata #> param( [double] $Distance ) - -$this.GoTo($this.Position.X + $Distance, $this.Position.Y) +$instruction = + if ($this.IsPenDown) { + "h $Distance" + } else { + "m $($this.Position.X + $Distance) 0" + } +$this.Position = $Distance,0 +$this.Steps.Add($instruction) +return $this \ No newline at end of file diff --git a/Types/Turtle/KochIsland.ps1 b/Types/Turtle/KochIsland.ps1 index 0e1ebe1..bb3133a 100644 --- a/Types/Turtle/KochIsland.ps1 +++ b/Types/Turtle/KochIsland.ps1 @@ -21,9 +21,9 @@ $turtle.Pattern.Save("$pwd/KochIsland2.svg") #> param( - [double]$Size = 20, - [int]$Order = 3, - [double]$Angle = 90 + [double]$Size = 42, + [int]$Order = 4, + [double]$Angle = -90 ) return $this.LSystem('W', [Ordered]@{ diff --git a/Types/Turtle/Leg.ps1 b/Types/Turtle/Leg.ps1 new file mode 100644 index 0000000..b7243e5 --- /dev/null +++ b/Types/Turtle/Leg.ps1 @@ -0,0 +1,44 @@ +<# +.SYNOPSIS + Draws a arm or a leg +.DESCRIPTION + Instructs our Turtle to draw an arm or a leg. + + Each segment of the arm or leg can be considered a pair of vectors. + + Each pair will represent a leg length followed by a leg angle. +.EXAMPLE + turtle push leg 42 90 42 90 pop push rotate -15 leg 42 90 42 90 pop rotate -30 leg 42 90 42 90 +.EXAMPLE + turtle rotate 90 rotate -15 push leg 21 15 pop rotate 30 push leg 21 -15 pop show +#> +param() + +$pairs = @() +$pair = @() +foreach ($arg in $args) { + if ($arg -is [ValueType]) { + if ($arg -is [Numerics.Vector2] -or + $arg -is [Numerics.Vector3] -or + $arg -is [Numerics.Vector4] + ) { + $pair += $arg.X + $pair += $arg.Y + } else { + $pair += $arg + } + + } + elseif ($arg -as [double]) { + $pair += $arg + } + if ($pair.Count -eq 2) { + $pairs += ,$pair + $pair = @() + } +} + +foreach ($pair in $pairs) { + $this = $this.Forward($pair[0]).Rotate($pair[1]) +} +return $this diff --git a/Types/Turtle/PieGraph.ps1 b/Types/Turtle/PieGraph.ps1 new file mode 100644 index 0000000..5780c5e --- /dev/null +++ b/Types/Turtle/PieGraph.ps1 @@ -0,0 +1,310 @@ +<# +.SYNOPSIS + Draws a pie graph using turtle graphics. +.DESCRIPTION + This script uses turtle graphics to draw a pie graph based on the provided data. +.EXAMPLE + turtle PieGraph 400 80 20 save ./80-20.svg +.EXAMPLE + turtle PieGraph 400 5 10 15 20 15 10 5 | Save-Turtle ./PieGraph.svg +.EXAMPLE + turtle PieGraph 400 @{value=20;fill='red'} @{value=40;fill='blue'} save ./PieGraphColor.svg +.EXAMPLE + turtle PieGraph 400 @( + 5,10,15,20,15,10,5 | Sort-Object -Descending + ) | Save-Turtle ./PieGraphDescending.svg +.EXAMPLE + turtle rotate (Get-Random -Max 360) PieGraph 400 @( + 5,10,15,20,15,10,5 | Sort-Object -Descending + ) | Save-Turtle ./PieGraphDescendingRotated.svg +.EXAMPLE + turtle PieGraph 200 ( + @(1..50) | + Get-Random -Count (Get-Random -Minimum 5 -Maximum 20) + ) save ./RandomPieGraph.svg +.EXAMPLE + turtle rotate -90 piegraph 100 @( + $allTokens = Get-Module Turtle | + Split-Path | + Get-ChildItem -Filter *.ps1 | + Foreach-Object { + [Management.Automation.PSParser]::Tokenize( + (Get-Content -Path $_.FullName -Raw), [ref]$null + ) + } + $allTokens | + Group-Object Type -NoElement | + Sort-Object Count -Descending | + Add-Member ScriptProperty Fill { + "#{0:x6}" -f (Get-Random -Maximum 0xffffff) + } -Force -PassThru | + Add-Member ScriptProperty Link { + "https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.pstokentype?view=powershellsdk-7.4.0#system-management-automation-pstokentype-$($this.Name.ToLower())" + } -Force -PassThru + + ) save ./TurtlePSTokenCountPieGraph.svg +.EXAMPLE + turtle rotate -90 piegraph 100 @( + $allTokens = Get-Module Turtle | + Split-Path | + Get-ChildItem -Filter *.ps1 | + Foreach-Object { + [Management.Automation.PSParser]::Tokenize( + (Get-Content -Path $_.FullName -Raw), [ref]$null + ) + } + $allTokens | + Group-Object Type | + Select-Object Name, @{ + Name='TotalLength' + Expression = { + $total = 0 + foreach ($item in $_.Group) { + $total+=$item.Length + } + $total + } + } | + Sort-Object TotalLength -Descending | + Add-Member ScriptProperty Fill { + "#{0:x6}" -f (Get-Random -Maximum 0xffffff) + } -Force -PassThru | + Add-Member ScriptProperty Link { + "https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.pstokentype?view=powershellsdk-7.4.0#system-management-automation-pstokentype-$($this.Name.ToLower())" + } -Force -PassThru + + ) save ./TurtlePSTokenLengthPieGraph.svg +.EXAMPLE + turtle rotate -90 piegraph 100 @( + $allTypes = Get-Module Turtle | + Split-Path | + Get-ChildItem -Filter *.ps1 | + Get-Command -Name { $_.FullName } | + Foreach-Object { + $_.ScriptBlock.Ast.FindAll({ + param($ast) + $ast -is [Management.Automation.Language.TypeExpressionAst] + }, $true) + } | + Foreach-Object { + $_.Extent -replace '^\[' -replace '\]$' -as [type] + } + $allTypes | + Group-Object FullName | + Sort-Object Count -Descending | + Add-Member ScriptProperty Fill { + "#{0:x6}" -f (Get-Random -Maximum 0xffffff) + } -Force -PassThru | + Add-Member ScriptProperty Title { + $this.Name + } -Force -PassThru | + Add-Member ScriptProperty Link { + "https://learn.microsoft.com/en-us/dotnet/api/$($this.Name.ToLower())?wt.mc_id=MVP_321542" + } -Force -PassThru + ) save ./TurtleDotNetTypesPieGraph.svg +.EXAMPLE + $n = Get-Random -Min 5 -Max 10 + turtle width 200 height 200 morph @( + turtle PieGraph 200 200 @(1..50 | Get-Random -Count $n) + turtle PieGraph 200 200 @(1..50 | Get-Random -Count $n) + turtle PieGraph 200 200 @(1..50 | Get-Random -Count $n) + ) save ./RandomPieGraphMorph.svg +.EXAMPLE + turtle PieGraph 200 ( + @(1..50;-1..-50) | + Get-Random -Count (Get-Random -Minimum 5 -Maximum 20) + ) save ./RandomPieGraphWithNegative.svg +.EXAMPLE + $randomNegativePie = turtle PieGraph 200 ( + @(1..50;-1..-50) | + Get-Random -Count 10 + ) + turtle viewbox 200 morph @( + $randomNegativePie + turtle PieGraph 200 200 ( + @(1..50;-1..-50) | + Get-Random -Count 10 + ) + $randomNegativePie + ) save ./RandomPieGraphWithNegativeMorph.svg +.EXAMPLE + # Multiple pie graphs + turtle PieGraph 400 (1,2,4,8,4,2,1) jump 800 rotate 180 PieGraph 400 (1,2,4,8,4,2,1) save ./pg.svg +#> +param( +# The radius of the bar graph +[double]$Radius, + +# The points in the bar graph. +# Each point will be turned into a relative number and turned into an equal-width bar. +[Parameter(ValueFromRemainingArguments)] +[PSObject[]] +$GraphData +) + + +# If there were no points, we are drawing nothing, so return ourself. +if (-not $GraphData) { return $this} + +filter IsPrimitive {$_.GetType -and $_.GetType().IsPrimitive} + +# To make a pie graph we need to know the total, and thus we need to make a couple of passes +[double]$Total = 0.0 + +$sliceObjects = [Ordered]@{} +$richSlices = $false +$Slices = @( + $dataPointIndex = 0 + foreach ($dataPoint in $GraphData) + { + # If the data point is a number (or other primitive data) + if ($dataPoint | IsPrimitive) + { + $Total += $dataPoint # add it to the total + $dataPoint -as [double] # and output it + $sliceObjects["slice$($sliceObjects.Count)"] = $dataPoint + } + # Otherwise, if the data point has a value that is a number + elseif ($dataPoint.value | IsPrimitive) + { + $Total += $dataPoint.value # add it to the total + $dataPoint.value -as [double] # and output that + $richSlices = $true + $sliceObjects["slice$($sliceObjects.Count)"] = $dataPoint + } + elseif ($dataPoint -is [Collections.IDictionary]) { + foreach ($key in $dataPoint.Keys) { + if ($dataPoint[$key] | IsPrimitive) { + $Total += $dataPoint[$key] # add it to the total + $dataPoint[$key] -as [double] # and output that + $sliceObjects["slice$($sliceObjects.Count)"] = $dataPoint[$key] + } + } + + $richSlices = $true + } + elseif ($DataPoint -is 'Microsoft.PowerShell.Commands.GroupInfo') { + $total += $dataPoint.Count + $dataPoint.Count + $sliceObjects["slice$($sliceObjects.Count)"] = $dataPoint + $richSlices = $true + } + elseif ($dataPoint -isnot [string]) { + foreach ($prop in $dataPoint.psobject.properties) { + if ($dataPoint.($prop.Name) | IsPrimitive) { + $Total += $dataPoint.($prop.Name) # add it to the total + $dataPoint.($prop.Name) -as [double] # and output that + $sliceObjects["slice$($sliceObjects.Count)"] = $dataPoint + } + } + $richSlices = $true + } + } +) + +if ($Slices.Length -eq 1 -and -not $richSlices) { + + # If we provide a single number, we will auto-slice the pie + # If the number is between 0 and 1, we want to show a fraction + if ($slices[0] -ge 0 -and $slices[0] -le 1) { + # Set the total to one + $total = 1 + # and make two pie slices. + $slices = $slices[0], (1- $slices[0]) + } else { + # Otherwise, we want mostly equal pie slices + # (mostly is in case of a decimal value) + # Get the floor of our slice, + $floor = [Math]::Floor($slices[0]) + # and determine the remainder. + $remainder = $slices[0] - $floor + # Then create N equal slices. + $Slices = @(,1 * $floor) + # If there was a remainder + if ($remainder) { + # create a small slice. + $slices += $remainder + } + # Retotal our pie + $total = 0.0 + foreach ($slice in $slices) { + $total += $slice + } + } +} + +# Turn each numeric slice into a ratio +$relativeSlices = + foreach ($slice in $Slices) { $slice/ $total } + +# If we have no ratios, we have nothing to graph, and we are done here. +if (-not $relativeSlices) { return $this } + +# Next let's figure out the maximum delta x and delta y +$dx = $this.X + $Radius +$dy = $this.Y + $Radius +# and resize our viewbox with respect to our radius +$null = $this.ResizeViewBox($Radius) + +# If we are not rendering "rich" slices, we can draw the arcs as one path +if (-not $richSlices) { + # and we do not need to teleport + for ($sliceNumber =0 ; $sliceNumber -lt $Slices.Length; $sliceNumber++) { + # Turn each ratio into an angle + $Angle = $relativeSlices[$sliceNumber] * 360 + $this = $this. + # Draw an arc of that angle, + CircleArc($Radius, $Angle). + # then rotate by the angle. + Rotate($angle) + } +} +else { + # Otherwise, we are making multiple turtles + $nestedTurtles = [Ordered]@{} + # The idea is the same, but the implementation is more complicated + $heading = $this.Heading + if (-not $heading) { $heading = 0.0 } + # Calulate the midpoint of the circle + for ($sliceNumber =0 ; $sliceNumber -lt $Slices.Length; $sliceNumber++) { + $Angle = $relativeSlices[$sliceNumber] * 360 + $sliceName = "slice$sliceNumber" + # created a nested turtle at the midpoint + $nestedTurtles["slice$sliceNumber"] = turtle start $dx $dy + # with the current heading + $nestedTurtles["slice$sliceNumber"].Heading = $this.Heading + # and arc by the angle + $null = $nestedTurtles["slice$sliceNumber"].CircleArc($Radius, $Angle) + + # If the slice was of a dictionary + if ($sliceObjects[$sliceName] -is [Collections.IDictionary]) + { + # set any settable properties on the turtle + foreach ($key in $sliceObjects[$sliceName].Keys) { + # that exist in both the turtle and the dictionary + if ($nestedTurtles[$sliceName].psobject.properties[$key].SetterScript) { + $nestedTurtles[$sliceName].$key = $sliceObjects[$sliceName][$key] + } + } + } + # If the slice was not a string + elseif ($sliceObjects[$sliceName] -isnot [string]) + { + # Set any settable properties on the turlte + foreach ($key in $sliceObjects[$sliceName].psobject.properties.Name) { + # that exist in both the turtle and the slice object. + if ($nestedTurtles[$sliceName].psobject.properties[$key].SetterScript) { + $nestedTurtles[$sliceName].$key = $sliceObjects[$sliceName].$key + } + } + } + + # Now rotate our own heading, even though we are not drawing anything. + $null = $this.Rotate($angle) + } + # and set our nested turtles. + $this.Turtles = $nestedTurtles + # $null = $this.ResizeViewBox($Radius) +} + +return $this \ No newline at end of file diff --git a/Types/Turtle/Pop.ps1 b/Types/Turtle/Pop.ps1 index 326f8a3..8f43de3 100644 --- a/Types/Turtle/Pop.ps1 +++ b/Types/Turtle/Pop.ps1 @@ -1,12 +1,24 @@ -if ($this.'.Stack' -isnot [Collections.Stack]) { - return -} +<# +.SYNOPSIS + Pops the Turtle Stack +.DESCRIPTION + Pops the Turtle back to the last location and heading in the stack. -if ($this.'.Stack'.Count -eq 0) { - return -} + By pushing and popping, we can draw multiple branches. +.EXAMPLE + # Draws a T shape by pushing and popping + turtle rotate -90 forward 42 push rotate 90 forward 21 pop rotate -90 forward 21 show +#> +param() +# If the stack is not a stack, return ourself +if ($this.'.Stack' -isnot [Collections.Stack]) { return $this } +# If the stack is empty, return ourself +if ($this.'.Stack'.Count -eq 0) { return $this } +# Pop the stack $popped = $this.'.Stack'.Pop() -$this.PenUp().Goto($popped.Position.X, $popped.Position.Y).PenDown() -$this.Heading = $popped.Heading -return $this \ No newline at end of file + +$this. # Rotate by the differene in heading, + Rotate($popped.Heading - $this.Heading). + # then teleport to the popped location + Teleport($popped.Position.X, $popped.Position.Y) diff --git a/Types/Turtle/Push.ps1 b/Types/Turtle/Push.ps1 index 55b0f80..193b660 100644 --- a/Types/Turtle/Push.ps1 +++ b/Types/Turtle/Push.ps1 @@ -1,7 +1,16 @@ +<# +.SYNOPSIS + Pushes the Turtle Stack +.DESCRIPTION + Pushes the current state of this Turtle onto a stack. + + If this stack is popped, the Turtle will teleport back to the location where it was pushed. + + By pushing and popping, we can draw multiple branches. +#> if (-not $this.'.Stack') { $this | Add-Member NoteProperty '.Stack' ([Collections.Stack]::new()) -Force } - $this.'.Stack'.Push(@{ Position = [Ordered]@{X=$this.Position.X;Y=$this.Position.Y} Heading = $this.Heading diff --git a/Types/Turtle/QuadraticBezierCurve.ps1 b/Types/Turtle/QuadraticBezierCurve.ps1 new file mode 100644 index 0000000..8d93407 --- /dev/null +++ b/Types/Turtle/QuadraticBezierCurve.ps1 @@ -0,0 +1,58 @@ +<# +.SYNOPSIS + Draws a quadratic Bezier Curve +.DESCRIPTION + Draws a quadratic Bezier curve. +.EXAMPLE + turtle QuadraticBezierCurve 0 -100 100 -100 save ./q.svg +.EXAMPLE + turtle QuadraticBezierCurve 0 -100 100 -100 QuadraticBezierCurve 100 100 100 100 save ./q2.svg +.EXAMPLE + turtle @( + 'QuadraticBezierCurve', 0, -100, 100, -100 + 'QuadraticBezierCurve', 100, 100, 100, 100 + 'QuadraticBezierCurve', 0, 100, -100, 100 + ) save ./q3.svg +.EXAMPLE + turtle @( + 'QuadraticBezierCurve', 0, -100, 100, -100 + 'QuadraticBezierCurve', 100, 0, 100, 100 + 'QuadraticBezierCurve', 0, 100, -100, 100 + 'QuadraticBezierCurve', -100, 0, -100, -100 + ) save ./q4.svg +#> +param( +# The X control point +[double] +$ControlX, + +# The Y control point +[double] +$ControlY, + +# The delta X +[double] +$DeltaX, + +# The delta Y +[double] +$DeltaY +) + + + +if ($DeltaX -or $DeltaY) { + $this.Position = $DeltaX, $DeltaY + # If the pen is down + if ($this.IsPenDown) { + # draw the curve + $this.Steps.Add("q $ControlX $ControlY $DeltaX $DeltaY") + } else { + # otherwise, move to the deltaX/deltaY + $this.Steps.Add("m $DeltaX $DeltaY") + } +} + +return $this + + diff --git a/Types/Turtle/Repeat.ps1 b/Types/Turtle/Repeat.ps1 new file mode 100644 index 0000000..6393deb --- /dev/null +++ b/Types/Turtle/Repeat.ps1 @@ -0,0 +1,73 @@ +<# +.SYNOPSIS + Repeats Turtle Commands +.DESCRIPTION + Repeats Turtle Commands any number of times. + + Repeat is the original loop statement in Turtle graphics. +.NOTES + Repetition can be performed in many ways in PowerShell. + + Any example of repeat can also be written as an array with that series of steps, multiplied by the repeat count. + + ~~~PowerShell + turtle repeat 3 [rotate 120 forward 42] + # Produces the same shape as... + turtle 'rotate',120,'forward',42 * 3 + # Produces the same shape as... + turtle @( + foreach ($n in 1..3) { + 'rotate', 120, 'forward', 42 + } + ) + # Produces the same shape as... + turtle @( + foreach ($n in 1..3) { + 'rotate' + 120 + 'forward' + 42 + } + ) + ~~~ + + Because repeat parses each step each time, repeat is likely to be one of the slower ways to repeat. +.EXAMPLE + turtle repeat 3 [rotate (360/3) forward 42] save ./tri.svg +.EXAMPLE + turtle repeat 6 [rotate (360/6) forward 42] save ./hex.svg +.EXAMPLE + turtle repeat 7 [rotate (360/7) star 42 7] save ./starFlower.svg +.EXAMPLE + turtle repeat 4 [rotate (360/4) forward 42 repeat 3 [rotate 120 forward 42]] save ./r.svg +.EXAMPLE + turtle repeat 6 [rotate (360/6) forward 42 repeat 3 [rotate 120 forward 4.2]] save ./r2.svg +.EXAMPLE + turtle repeat 9 [rotate ( + 360/9 + ) forward 84 repeat 6 [rotate ( + 360/6 + ) forward 42 repeat 3 [rotate ( + 360/3 + ) forward 4.2]]] save ./r3.svg +#> +param( +# The repeat count. +# This will be rounded down to the nearest integer and converted into an absolute value. +[double] +$RepeatCount, + +# The steps to repeat. +[Parameter(ValueFromRemainingArguments)] +[PSObject[]] +$Command +) + +# If there was no repeat count, return this +if (-not $RepeatCount) { return $this } +$floorCount = [Math]::Abs([Math]::Floor($RepeatCount)) + +if ($floorCount -ge 1) { + $this = $this | turtle @($Command * $floorCount) +} +return $this \ No newline at end of file diff --git a/Types/Turtle/ResizeViewBox.ps1 b/Types/Turtle/ResizeViewBox.ps1 new file mode 100644 index 0000000..765eb73 --- /dev/null +++ b/Types/Turtle/ResizeViewBox.ps1 @@ -0,0 +1,78 @@ +<# +.SYNOPSIS + Resizes the Turtle ViewBox +.DESCRIPTION + Resizes the Turtle Viewbox to fit the current position + + (plus or minus a view rectangle) + + Any arguments that are primitive types will be considered a point. +#> +param() + +# Any argument that is a point can influence the bounding box +# (though, for the moment, we only care about the first four) +$boundingPoints = @(foreach ($arg in $args) { + if ($arg.GetType -and $arg.GetType().IsPrimitive) { + $arg + } +}) + +# If there were no points provided, we are resizing to fit nothing new +if (-not $boundingPoints) { $boundingPoints = @(0.0) } + +# Set our mins and maxes to zero +$minX, $minY, $maxX, $maxY = @(0.0) * 4 +# If there was one point provided +if ($boundingPoints.Length -eq 1) { + # we want to make sure a square of this size would fit + $minX, $minY, $maxX, $maxY = $boundingPoints * 4 + $minX *= -1 + $minY *= -1 +} +# If there were two points provided +elseif ($boundingPoints -eq 2) +{ + # We want to make sure a rectangle of this size would fit + $minX, $minY, $maxX, $maxY = $boundingPoints * 2 + $minX *= -1 + $minY *= -1 +} +# If there were four points +elseif ($boundingPoints -eq 4) { + # Consider those the bounds we want. + $minX, $minY, $maxX, $maxY = $boundingPoints +} + +# Make sure we have a place to store our position +if (-not $this.'.Position') { + $this | Add-Member -MemberType NoteProperty -Force -Name '.Position' -Value ([Numerics.Vector2]@{ X = 0; Y = 0 }) +} + +# and minimum +if (-not $this.'.Minimum') { + $this | Add-Member -MemberType NoteProperty -Force -Name '.Minimum' -Value ([Numerics.Vector2]@{ X = 0; Y = 0 }) +} +# and maximum +if (-not $this.'.Maximum') { + $this | Add-Member -MemberType NoteProperty -Force -Name '.Maximum' -Value ([Numerics.Vector2]@{ X = 0; Y = 0 }) +} + +# Resize our bounds as appropriate. +if ($this.'.Maximum'.X -lt ($this.Position.X + $maxX)) { + $this.'.Maximum'.X = $this.Position.X + $maxX +} + +if ($this.'.Minimum'.X -gt ($this.Position.X + $minX)) { + $this.'.Minimum'.X = $this.Position.X + $minX +} + +if ($this.'.Maximum'.Y -lt ($this.Position.Y + $maxY)) { + $this.'.Maximum'.Y = $this.Position.Y + $maxY +} + +if ($this.'.Minimum'.Y -gt ($this.Position.Y + $minY)) { + $this.'.Minimum'.Y = $this.Position.Y + $minY +} + +return $this \ No newline at end of file diff --git a/Types/Turtle/Show.ps1 b/Types/Turtle/Show.ps1 new file mode 100644 index 0000000..32089e1 --- /dev/null +++ b/Types/Turtle/Show.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Shows the turtle. +.DESCRIPTION + Shows the current turtle by opening it with the default file association +.LINK + Save-Turtle +#> +param() + +return $this | Show-Turtle diff --git a/Types/Turtle/Spider.ps1 b/Types/Turtle/Spider.ps1 new file mode 100644 index 0000000..cde2a4d --- /dev/null +++ b/Types/Turtle/Spider.ps1 @@ -0,0 +1,76 @@ +<# +.SYNOPSIS + Draws a Spider +.DESCRIPTION + Draws a Spider using a Turtle. +.NOTES + This example was adapted from the Apple II Logo manual +.EXAMPLE + turtle spider +.EXAMPLE + turtle spider morph @( + turtle spider 42 10 + turtle spider 42 15 + turtle spider 42 10 + ) show +.EXAMPLE + turtle rotate 90 forward 120 rotate -90 spider 42 +.EXAMPLE + turtle rotate 90 forward 120 rotate -90 spider 42 morph @( + turtle rotate 90 forward 1.2 rotate -90 spider 42 10 + turtle rotate 90 forward 120 rotate -90 spider 42 15 + turtle rotate 90 forward 1.2 rotate -90 spider 42 10 + ) | + Save-Turtle ./SpiderDescendingMorph.svg +.LINK + https://logothings.github.io/logothings/AppleLogo.html +#> +param( +# The size of each segment of the leg +[double] +$LegSize = 42, + +# The rotation between each leg segment +[double] +$LegRotation = 10, + +# The angle of the leg segment +[double] +$LegAngle = 90, + +# The length of the head. +# One quarter of this value will be the "neck" +# One half of this value will be the "head" +[double] +$HeadLength = 2.5 +) + +# Right legs +$this = $this.Push() +foreach ($legNumber in 1..4) { + $this = $this. + Push(). + Leg($LegSize, $LegAngle, $LegSize, $LegAngle). + Pop(). + Rotate(-$LegRotation) +} + +# Reset our stack and flip to the other side +$this = $this.Pop().Push().Rotate(180) + +# Left legs +foreach ($legNumber in 1..4) { + $this = $this. + Push(). + Leg($LegSize, -$LegAngle, $LegSize, -$LegAngle). + Pop(). + Rotate($LegRotation) +} + + +return $this.Pop().Push(). + Rotate(90). + Forward(-$HeadLength/4). + Rotate(-90). + Circle(-$HeadLength/2). + Pop() \ No newline at end of file diff --git a/Types/Turtle/Spiderweb.ps1 b/Types/Turtle/Spiderweb.ps1 new file mode 100644 index 0000000..33ea4ac --- /dev/null +++ b/Types/Turtle/Spiderweb.ps1 @@ -0,0 +1,159 @@ +<# +.SYNOPSIS + Draws a spiderweb with a Turtle +.DESCRIPTION + Tells our Turtle to draw a spiderweb. + + This will draw any number of spokes, and draw a polygon between each spoke at regular intervals +.EXAMPLE + # Draw a spiderweb + turtle spiderweb +.EXAMPLE + # Draw a spider web with a radius six + # containing six rings + # along six spokes + turtle spiderweb 6 6 6 show +.EXAMPLE + # Draw a random spiderweb + turtle rotate ( + Get-Random -Max 360 + ) web 42 ( + Get-Random -Min 3 -Max 13 + ) ( + Get-Random -Min 3 -Max 13 + ) save ./RandomWeb.svg show +.EXAMPLE + turtle rotate ( + Get-Random -Max 360 + ) web 42 ( + Get-Random -Min 3 -Max 13 + ) ( + Get-Random -Min 3 -Max (13 * 3) + ) pathAnimation ( + [Ordered]@{ + type = 'rotate' ; values = 0, 360 ;repeatCount = 'indefinite'; dur = "41s" + } + ) save ./RandomWebRotate.svg show +.EXAMPLE + turtle viewbox 1080 start (1080/2) (1080/2) web (1080/2) ( + Get-Random -Min 3 -Max 13 + ) ( + Get-Random -Min 3 -Max (13 * 3) + ) backgroundcolor 'black' stroke 'yellow' pathclass 'yellow-stroke' save ./RandomWebColor.svg save ./RandomWebColor.png +.EXAMPLE + turtle rotate ( + Get-Random -Max 360 + ) web 42 ( + Get-Random -Min 3 -Max 13 + ) ( + Get-Random -Min 3 -Max (13 * 3) + ) pathAnimation ( + [Ordered]@{ + type = 'rotate' ; values = 0, 360 ;repeatCount = 'indefinite'; dur = "41s" + } + ) backgroundcolor 'black' stroke 'yellow' pathclass 'yellow-stroke' save ./RandomWebRotateColor.svg show +.EXAMPLE + turtle rotate ( + Get-Random -Max 360 + ) web 42 ( + Get-Random -Min 3 -Max 13 + ) ( + Get-Random -Min 3 -Max (13 * 3) + ) morph save ./RandomWebStepMorph.svg +.EXAMPLE + $spokes = Get-Random -Min 3 -Max 13 + $rings = Get-Random -Min 3 -Max (13 * 3) + turtle web 42 $spokes $rings morph @( + turtle web 42 $spokes $rings + turtle rotate ( + Get-Random -Max 360 + ) web 42 $spokes $rings + turtle web 42 $spokes $rings + ) save ./RandomWebMorph.svg +.EXAMPLE + $spokes = Get-Random -Min 3 -Max 13 + $rings = Get-Random -Min 3 -Max (13 * 3) + turtle web 42 $spokes $rings morph @( + turtle web 42 $spokes $rings + turtle rotate ( + Get-Random -Max 360 + ) web 42 $spokes $rings + turtle web 42 $spokes $rings + ) backgroundcolor 'black' stroke 'yellow' pathclass 'yellow-stroke' save ./RandomWebMorphColor.svg + +#> +param( +# The radius of the web +[double] +$Radius = 42, + +# The number of spokes in the web. +[int] +$SpokeCount = 6, + +# The number of rings in the web. +[int] +$RingCount = 6 +) + + +# If there were no spokes or rings, return this +if ($spokeCount -eq 0 ) { return $this } +if ($RingCount -eq 0 ) { return $this } + + +# Determine the angle of the spokes +$spokeAngle = 360 / $SpokeCount + +# And draw each spoke. +foreach ($n in 1..$([Math]::Abs($SpokeCount))) { + $this = $this.Forward($radius).Backward($radius).Rotate($spokeAngle) +} + +# Now we have the structure of our web, and we are at the center. +$center = + [Numerics.Vector2]::new($this.X, $this.Y) + + +# Each ring we want to grow the in radius +$radiusStep = $radius / $RingCount +$inRadius = 0 + +# Starting from the center, we want to try to make a series of rings to each next point in the web +foreach ($ringNumber in 1..$([Math]::Abs($RingCount))) { + $inRadius+=$radiusStep + # First, move along our spoke + $null = $this.Forward($radiusStep) + + # Then get our bearings + $heading = $this.Heading + + # and imagine points around a circle, along each of our spokes + $webPoints = @( + foreach ($spokeNumber in 1..$SpokeCount) { + $heading += $spokeAngle + [Numerics.Vector2]::new( + $center.X + $inRadius * [math]::cos($heading * [Math]::PI / 180), + $center.Y + $inRadius * [math]::sin($heading * [Math]::PI / 180) + ) + } + ) + + # Now that we have the points, + foreach ($point in $webPoints) { + # our turtle spider can + $this = $this.Rotate( + # rotate towards the point + $this.Towards($point.X, $point.Y) + ).Forward( + # and close the distance. + $this.Distance($point.X, $point.Y) + ) + } + + # Reset our bearings and head up to the next ring. + $this.Heading = $heading +} + +# Now that we've drawn our web, return ourself. +return $this \ No newline at end of file diff --git a/Types/Turtle/Spokes.ps1 b/Types/Turtle/Spokes.ps1 new file mode 100644 index 0000000..3fe16b5 --- /dev/null +++ b/Types/Turtle/Spokes.ps1 @@ -0,0 +1,94 @@ +<# +.SYNOPSIS + Draws spokes of a wheel +.DESCRIPTION + Draws spokes of a wheel, or sticks around a point. +.NOTES + This was adapted from Cynthia Solomon's example on LogoThings +.LINK + https://logothings.github.io/logothings/logo/Sticks.html +.EXAMPLE + turtle spokes 42 4 +.EXAMPLE + turtle spokes 42 5 +.EXAMPLE + turtle spokes 42 6 +.EXAMPLE + turtle spokes 42 8 +.EXAMPLE + turtle spokes 42 6 morph @( + turtle spokes 42 6 + turtle rotate 90 spokes 42 6 + turtle rotate 180 spokes 42 6 + turtle rotate 270 spokes 42 6 + turtle spokes 42 6 + ) show +.EXAMPLE + turtle spokes pathAnimation ( + [Ordered]@{ + type = 'rotate' ; values = 0, 360 ;repeatCount = 'indefinite'; dur = "4.2s" + } + ) show +.EXAMPLE + turtle viewbox 84 turtles @( + turtle spokes 42 6 pathAnimation ( + [Ordered]@{ + type = 'rotate' ; values = 0, 360 ;repeatCount = 'indefinite'; dur = "4.2s" + } + ) + turtle spokes 42 6 pathAnimation ( + [Ordered]@{ + type = 'rotate' ; values = 360, 0 ;repeatCount = 'indefinite'; dur = "4.2s" + } + ) + ) show +.EXAMPLE + turtle viewbox 84 turtles @( + turtle start 42 42 stroke red spokes 42 6 pathAnimation ( + [Ordered]@{ + type = 'rotate' ; values = 0, 360 ;repeatCount = 'indefinite'; dur = "4.2s" + } + ) + turtle start 42 42 stroke red spokes 42 6 pathAnimation ( + [Ordered]@{ + type = 'rotate' ; values = 360, 0 ;repeatCount = 'indefinite'; dur = "4.2s" + } + ) + turtle start 42 42 stroke green spokes 42 8 pathAnimation ( + [Ordered]@{ + type = 'rotate' ; values = 0, 360 ;repeatCount = 'indefinite'; dur = "4.2s" + } + ) + turtle start 42 42 stroke green spokes 42 8 pathAnimation ( + [Ordered]@{ + type = 'rotate' ; values = 360, 0 ;repeatCount = 'indefinite'; dur = "4.2s" + } + ) + turtle start 42 42 stroke blue spokes 42 10 pathAnimation ( + [Ordered]@{ + type = 'rotate' ; values = 0, 360 ;repeatCount = 'indefinite'; dur = "4.2s" + } + ) + turtle start 42 42 stroke blue spokes 42 10 pathAnimation ( + [Ordered]@{ + type = 'rotate' ; values = 360, 0 ;repeatCount = 'indefinite'; dur = "4.2s" + } + ) + ) show +#> +param( +# The radius of the spokes +[double] +$Radius = 42, + +# The number of spokes or sticks to draw +[int] +$SpokeCount = 6 +) + +$spokeAngle = 360 / $SpokeCount + +foreach ($n in 1..$([Math]::Abs($SpokeCount))) { + $this = $this.Forward($radius).Backward($radius).Rotate($spokeAngle) +} +return $this \ No newline at end of file diff --git a/Types/Turtle/Step.ps1 b/Types/Turtle/Step.ps1 index 5908ca4..820d247 100644 --- a/Types/Turtle/Step.ps1 +++ b/Types/Turtle/Step.ps1 @@ -12,7 +12,6 @@ param( # The DeltaY [double]$DeltaY = 0 ) - # If both coordinates are empty, there is no step if ($DeltaX -or $DeltaY) { $this.Position = $DeltaX, $DeltaY diff --git a/Types/Turtle/Sun.ps1 b/Types/Turtle/Sun.ps1 new file mode 100644 index 0000000..006c59c --- /dev/null +++ b/Types/Turtle/Sun.ps1 @@ -0,0 +1,81 @@ +<# +.SYNOPSIS + Draws a Sun +.DESCRIPTION + Draws a Sun in Turtle. + + Suns are drawn by drawing a ArcRight and ArcLeft, followed by a rotation. +.EXAMPLE + turtle Sun save ./Sun.svg +.EXAMPLE + turtle Sun 100 90 90 4 save ./Sun-120-4.svg +.EXAMPLE + turtle Sun 100 (360/7) (7/360) 7 +.EXAMPLE + turtle Sun 100 135 90 8 save ./Sun-135-8.svg +.EXAMPLE + turtle Sun 100 135 90 8 fill 'yellow' 'goldenrod' save ./Sun-135-90-8-gradient.svg +.EXAMPLE + turtle Sun 100 135 60 8 fill 'yellow' 'goldenrod' stroke 'goldenrod' 'yellow' save ./Sun-135-90-8-gradient-mix.svg +.EXAMPLE + turtle Sun 100 135 60 8 fill 'yellow' 'goldenrod' stroke 'goldenrod' 'yellow' morph @( + turtle Sun 100 135 60 8 + turtle Sun 100 135 -60 8 + turtle Sun 100 135 60 8 + ) save ./Sun-135-60-8-gradient-mix.svg +.EXAMPLE + turtle Sun 100 160 90 18 fill 'yellow' 'goldenrod' stroke 'goldenrod' 'yellow' morph @( + turtle Sun 100 160 90 18 + turtle Sun 100 160 -90 18 + turtle Sun 100 160 90 18 + ) save ./Sun-160-90-18-gradient-mix.svg +.EXAMPLE + turtle Sun 100 150 -90 12 save ./Sun-150-12.svg +.EXAMPLE + turtle Sun 100 160 -90 9 save ./Sun2.svg +.EXAMPLE + turtle Sun 100 120 36 3 fill 'yellow' 'goldenrod' fillrule evenodd save ./Sun-230-36-EvenOdd.svg +.EXAMPLE + turtle Sun 100 230 36 fill '#4488ff' fillrule evenodd save ./Sun-230-36-EvenOdd.svg +.EXAMPLE + turtle Sun 100 160 -100 9 morph @( + turtle Sun 100 160 -100 9 + turtle Sun 100 160 100 9 + turtle Sun 100 160 -100 9 + ) +#> +param( +# The length of both arcs +[double] +$Length = 42, + +# The rotation after each step +[double] +$Angle = 160, + +# The angle of the rays of the sun +[double] +$RayAngle = 90, + +# The number of steps to draw. +# In order to close the shape, this multiplied by the angle should be a multiple of 360. +[int] +$StepCount = 9 +) + +# If there are no steps to draw, return this +if ($stepCount -eq 0) { return $this } + +# Every step we take +$null = foreach ($n in 1..([Math]::Abs($StepCount))) { + $this. + # arc right + ArcRight($length/2, $RayAngle). + # then arc left + ArcLeft($length/2, $RayAngle). + # then rotate. + Rotate($Angle) +} + +# Return this so we never break the chain. +return $this diff --git a/Types/Turtle/Teleport.ps1 b/Types/Turtle/Teleport.ps1 index 4149c4d..1f3b237 100644 --- a/Types/Turtle/Teleport.ps1 +++ b/Types/Turtle/Teleport.ps1 @@ -16,7 +16,7 @@ $X, $Y ) -$deltaX = $x - $this.X +$deltaX = $x - $this.X $deltaY = $y - $this.Y $penState = $this.IsPenDown $this.IsPenDown = $false diff --git a/Types/Turtle/ToString.ps1 b/Types/Turtle/ToString.ps1 index 6c32f26..24cdfe5 100644 --- a/Types/Turtle/ToString.ps1 +++ b/Types/Turtle/ToString.ps1 @@ -1,3 +1,17 @@ +<# +.SYNOPSIS + Gets the Turtle as a string +.DESCRIPTION + Stringifies the Turtle. + + This will return the turtle as an element, so it can be rendered within a web page. +#> param() -return "$($this.SVG.OuterXml)" \ No newline at end of file +$element = $this.Element +if ($element -is [xml]) { + $element.OuterXml +} else { + "$element" +} +return \ No newline at end of file diff --git a/Types/Turtle/VerticalLine.ps1 b/Types/Turtle/VerticalLine.ps1 index de0dfd3..e0591a7 100644 --- a/Types/Turtle/VerticalLine.ps1 +++ b/Types/Turtle/VerticalLine.ps1 @@ -5,10 +5,23 @@ Draws a vertical line. The heading will not be changed. +.EXAMPLE + turtle VerticalLine 42 +.EXAMPLE + turtle VerticalLine 42 pathdata #> param( +# The length of the line. [double] $Distance ) -$this.GoTo($this.Position.X, $this.Position.Y + $Distance) +$instruction = + if ($this.IsPenDown) { + "v $Distance" + } else { + "m 0 $($this.Position.Y + $Distance)" + } +$this.Position = 0, $Distance +$this.Steps.Add($instruction) +return $this \ No newline at end of file diff --git a/Types/Turtle/get_AnimateMotion.ps1 b/Types/Turtle/get_AnimateMotion.ps1 index c614938..6da9d33 100644 --- a/Types/Turtle/get_AnimateMotion.ps1 +++ b/Types/Turtle/get_AnimateMotion.ps1 @@ -1,7 +1,20 @@ -@(" +[OutputType([xml])] +param() + +[xml]@( +"") -as [xml] \ No newline at end of file +)' repeatCount='indefinite' path='$($this.PathData)' /> +") \ No newline at end of file diff --git a/Types/Turtle/get_ArgumentList.ps1 b/Types/Turtle/get_ArgumentList.ps1 new file mode 100644 index 0000000..6210cbb --- /dev/null +++ b/Types/Turtle/get_ArgumentList.ps1 @@ -0,0 +1,19 @@ +<# +.SYNOPSIS + Gets the Turtle's arguments +.DESCRIPTION + Gets a list of the arguments passed to the Turtle. + + We can reuse these arguments to recreate the Turtle. +.NOTES + This will directly output each of the arguments, with the exception of `ArgumentList` + (and any aliases to ArgumentList) +.EXAMPLE + turtle rotate 45 forward 42 arguments +#> +if (-not $this.Invocations) { return } +foreach ($arg in $this.Invocations.BoundParameters['ArgumentList']) { + if ($arg -notin 'ArgumentList', 'Arguments', 'Args','Argument') { + $arg + } +} \ No newline at end of file diff --git a/Types/Turtle/get_Attribute.ps1 b/Types/Turtle/get_Attribute.ps1 new file mode 100644 index 0000000..647d7c1 --- /dev/null +++ b/Types/Turtle/get_Attribute.ps1 @@ -0,0 +1,20 @@ +<# +.SYNOPSIS + Gets Turtle attributes +.DESCRIPTION + Gets attributes of the turtle. + + These attributes apply directly to the Turtle as an `.Element`. + + They can also be targeted to apply to an aspect of the turtle, such as it's Pattern, Path, Text, Mask, or Marker. + + To set an attribute that targets an aspect of the turtle, prefix it with the name followed by a slash. + + (for example `path/data-key` would set an attribute on the path) +.EXAMPLE + turtle attribute @{someKey='someValue'} attribute +#> +if (-not $this.'.Attributes') { + $this | Add-Member NoteProperty '.Attributes' ([Ordered]@{}) -Force +} +return $this.'.Attributes' \ No newline at end of file diff --git a/Types/Turtle/get_Class.ps1 b/Types/Turtle/get_Class.ps1 new file mode 100644 index 0000000..ecec098 --- /dev/null +++ b/Types/Turtle/get_Class.ps1 @@ -0,0 +1,9 @@ +<# +.SYNOPSIS + Gets a Turtle's class +.DESCRIPTION + Gets any CSS classes associated with the turtle +.EXAMPLE + turtle class foo bar baz bing class +#> +return $this.SVGAttribute["class"] -split '\s+' diff --git a/Types/Turtle/get_DataBlock.ps1 b/Types/Turtle/get_DataBlock.ps1 new file mode 100644 index 0000000..eea9508 --- /dev/null +++ b/Types/Turtle/get_DataBlock.ps1 @@ -0,0 +1,26 @@ +<# +.SYNOPSIS + Gets a Turtle as data block +.DESCRIPTION + Gets our Turtle as a data block that will recreate our Turtle. + + The only commands that can be used in the data block are: `Turtle`, `Get-Turtle`, and `Get-Random` +.NOTES + PowerShell data blocks provide a much more limited syntax. + + They can only use simple expressions, cannot declare variables, use loops, declare script blocks, or use most types. + + They can also be declared with whitelist of Supported Commands. + + This property will return the current turtle inside of a data block, if possible. + + If any errors occur during conversion, they will be present in `$error`. +.LINK + https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_data_sections?wt.mc_id=MVP_321542 +#> +[OutputType([ScriptBlock])] +param() +[ScriptBlock]::Create("data -supportedCommand turtle, Get-Turtle, Get-Random { + $($this.ScriptBlock) +}") + diff --git a/Types/Turtle/get_Defines.ps1 b/Types/Turtle/get_Defines.ps1 new file mode 100644 index 0000000..ea58b2f --- /dev/null +++ b/Types/Turtle/get_Defines.ps1 @@ -0,0 +1,3 @@ +if ($this.'.Defines') { + return $this.'.Defines' +} diff --git a/Types/Turtle/get_Duration.ps1 b/Types/Turtle/get_Duration.ps1 index 90df98a..cb4c941 100644 --- a/Types/Turtle/get_Duration.ps1 +++ b/Types/Turtle/get_Duration.ps1 @@ -3,6 +3,10 @@ Gets the duration .DESCRIPTION Gets the default duration of animations and morphs. + + By default, 4.2 seconds. #> -if ($this.'.Duration') { return $this.'.Duration'} -return \ No newline at end of file +if ($null -eq $this.'.Duration') { + $this | Add-Member NoteProperty '.Duration' ([timespan]::FromSeconds(4.2)) -Force +} +return $this.'.Duration' diff --git a/Types/Turtle/get_Element.ps1 b/Types/Turtle/get_Element.ps1 new file mode 100644 index 0000000..a9e17ce --- /dev/null +++ b/Types/Turtle/get_Element.ps1 @@ -0,0 +1,92 @@ +<# +.SYNOPSIS + Gets a Turtle as an Element +.DESCRIPTION + We can treat any Turtle as any arbitrary markup element. + + To do this, all we need to do is set an element name, and, optionally, add some attributes or children. +.EXAMPLE + # Any bareword can become the name of an element, as long as it is not a method name + turtle element div element +.EXAMPLE + # We can provide anything that will cast to XML as an element + turtle element '
' element +.EXAMPLE + # We can provide an element and attributes + turtle element '
' element +.EXAMPLE + # We can put a turtle inside of an aribtrary element + turtle SpiderWeb element '
' +#> + +# If we have set an element name +if ($this.'.Element'.ElementName) { + + # make this little filter to recursively turn the element back into XML + filter toElement { + $in = $_ + # If the input was a dictionary with an element name + if ($in -is [Collections.IDictionary] -and $in.ElementName) { + # start the markup + "<$($in.ElementName)$( + # and pop in any element attributes + foreach ($attributeCollection in 'attr','attribute','attributes') { + if (-not $in.$attributeCollection) { continue } + if ($in.$attributeCollection -is [Collections.IDictionary]) { + foreach ($attributeName in $in.$attributeCollection.Keys) { + if ($attributeName -match '/') { continue } + ' ', $attributeName,"='",$in.$attributeCollection[$attributeName],"'" -join '' + } + } elseif ($in.$attributeCollection -is [string]) { + $in.$attributeCollection + } + break + } + )>$( + # Now include any child elements. + # First, if we have drawn anything in our turtle, include that + if ($this.Steps -or $this.Text -or $this.Turtles.Count) { + $this.SVG.OuterXml + } + @(foreach ($childCollection in 'child','ChildNodes','Children','Content') { + if (-not $in.$childCollection) { + continue + } + foreach ($child in $in.$childCollection) { + # strings are directly included + if ($child -is [string]) { + $child + } elseif ($child -is [xml] -or $child -is [xml.xmlElement]) { + # xml elements will embed themselves + $child.OuterXml + } elseif ($child -is [Collections.IDictionary] -and $child.ElementName) { + # and dictionaries with an element name will recurisvely call ourselves. + $child | & $MyInvocation.MyCommand.ScriptBlock + } else { + # Any other input will be stringified + "$child" + } + } + break + }) -join ([Environment]::NewLine) + )" + } + if ($_ -is [string]) { + $_ + } + } + + $elementMarkup = $this.'.Element' | toElement + $elementXml = $elementMarkup -as [xml] + if ($elementXml) { + $elementXml + } else { + $elementMarkup + } + return +} +else { + return $this.SVG +} + +return diff --git a/Types/Turtle/get_Fill.ps1 b/Types/Turtle/get_Fill.ps1 index 0d456f4..dbe7057 100644 --- a/Types/Turtle/get_Fill.ps1 +++ b/Types/Turtle/get_Fill.ps1 @@ -1,3 +1,27 @@ +<# +.SYNOPSIS + Gets a Turtle's fill color +.DESCRIPTION + Gets one or more colors used to fill the Turtle. + + By default, this is transparent. + + If more than one value is provided, the fill will be a gradient. +.EXAMPLE + # Draw a blue square + turtle square 42 fill blue +.EXAMPLE + # Draw a PowerShell blue square + turtle square 42 fill '#4488ff' +.EXAMPLE + # Draw a red, green, blue gradient + turtle square 42 fill red green blue show +.EXAMPLE + # Draw a red, green, blue linear gradient + turtle square 42 fill red green blue linear show +.EXAMPLE + turtle flower fill red green blue fillrule evenodd show +#> if ($this.'.Fill') { return $this.'.Fill' } diff --git a/Types/Turtle/get_History.ps1 b/Types/Turtle/get_History.ps1 new file mode 100644 index 0000000..2160d26 --- /dev/null +++ b/Types/Turtle/get_History.ps1 @@ -0,0 +1,214 @@ +<# +.SYNOPSIS + Gets a Turtle's history +.DESCRIPTION + Gets an annotated history of a turtle's movements. + + This is an SVG path translated into back into human readable text and coordinates. +#> +$currentPosition = [Numerics.Vector2]::new(0,0) +$historyList = [Collections.Generic.List[PSObject]]::new() +$startStack = [Collections.Stack]::new() +foreach ($pathStep in $this.PathData -join ' ' -split '(?=[\p{L}-[E]])' -ne '') { + $letter = $pathStep[0] + $isUpper = "$letter".ToLower() -cne $letter + $isLower = -not $isUpper + $toBy = if ($isUpper) { 'to'} else { 'by'} + $stepPoints = $pathStep -replace $letter -replace ',', ' ' -split '\s{1,}' -ne '' -as [float[]] + + $historyEntry = + switch ($letter) { + a { + for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex+=7) { + $sequence = $stepPoints[$stepIndex..($stepIndex + 6)] + $comment = "arc $toBy $sequence" + $delta = [Numerics.Vector2]::new.Invoke($sequence[-2,-1]) + if ($isUpper) { $delta -= $currentPosition } + [PSCustomObject]@{ + PSTypeName='Turtle.History' + Letter = "$letter" + Start = $currentPosition + End = $currentPosition + $delta + Delta = $delta + Instruction = "$Letter $sequence" + Comment = $comment + } + $currentPosition += $delta + } + } + c { + + for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex+=6) { + $sequence = $stepPoints[$stepIndex..($stepIndex + 5)] + $comment = "cubic curve $toBy $sequence" + $delta = [Numerics.Vector2]::new.Invoke($sequence[-2,-1]) + if ($isUpper) { $delta -= $currentPosition } + [PSCustomObject]@{ + PSTypeName='Turtle.History' + Letter = "$letter" + Start = $currentPosition + End = $currentPosition + $delta + Delta = $delta + Instruction = "$Letter $sequence" + Comment = $comment + } + $currentPosition += $delta + } + } + l { + # line segment + for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex+=2) { + $sequence = $stepPoints[$stepIndex..($stepIndex + 1)] + $comment = "line $toBy $sequence" + $delta = [Numerics.Vector2]::new.Invoke($sequence[-2,-1]) + if ($isUpper) { $delta -= $currentPosition } + [PSCustomObject]@{ + PSTypeName='Turtle.History' + Letter = "$letter" + Start = $currentPosition + End = $currentPosition + $delta + Delta = $delta + Instruction = "$Letter $sequence" + Comment = $comment + } + $currentPosition += $delta + } + } + m { + # movement + for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex+=2) { + $sequence = $stepPoints[$stepIndex..($stepIndex + 1)] + + $comment = "line $toBy $sequence" + + $delta = [Numerics.Vector2]::new.Invoke($sequence[-2,-1]) + + if ($isUpper) { $delta -= $currentPosition } + + if ($stepIndex -gt 0) { + if ($letter -eq 'm') { + if ($isUpper) { $letter = 'L' } + else { $letter = 'l'} + } + $comment = "line $toBy $sequence" + } else { + $comment = "move $toBy $sequence" + $startStack.Push($currentPosition + $delta) + } + + [PSCustomObject]@{ + PSTypeName='Turtle.History' + Letter = "$letter" + Start = $currentPosition + End = $currentPosition + $delta + Delta = $delta + Instruction = "$Letter $sequence" + Comment = $comment + } + $currentPosition += $delta + } + } + s { + # simple bezier curve + for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex+=4) { + $sequence = $stepPoints[$stepIndex..($stepIndex + 3)] + $comment = "simple bezier curve $toBy $sequence" + $delta = [Numerics.Vector2]::new.Invoke($sequence[-2,-1]) + if ($isUpper) { $delta -= $currentPosition } + [PSCustomObject]@{ + PSTypeName='Turtle.History' + Letter = "$letter" + Start = $currentPosition + End = $currentPosition + $delta + Delta = $delta + Instruction = "$Letter $sequence" + Comment = $comment + } + $currentPosition += $delta + } + } + t { + # continue simple bezier curve + for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex+=2) { + $sequence = $stepPoints[$stepIndex..($stepIndex + 1)] + $comment = "continue bezier curve $toBy $sequence" + $delta = [Numerics.Vector2]::new.Invoke($sequence[-2,-1]) + if ($isUpper) { $delta -= $currentPosition } + [PSCustomObject]@{ + PSTypeName='Turtle.History' + Letter = "$letter" + Start = $currentPosition + End = $currentPosition + $delta + Delta = $delta + Instruction = "$Letter $sequence" + Comment = $comment + } + $currentPosition += $delta + } + } + q { + for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex+=4) { + + $sequence = $stepPoints[$stepIndex..($stepIndex + 3)] + $comment = "quadratic bezier curve $toBy $sequence" + $delta = [Numerics.Vector2]::new.Invoke($sequence[-2,-1]) + if ($isUpper) { $delta -= $currentPosition } + [PSCustomObject]@{ + PSTypeName='Turtle.History' + Letter = "$letter" + Start = $currentPosition + End = $currentPosition + $delta + Delta = $delta + Instruction = "$Letter $sequence" + Comment = $comment + } + $currentPosition += $delta + } + } + { $_ -in 'h', 'v' } { + for ($stepIndex = 0; $stepIndex -lt $stepPoints.Length; $stepIndex++) { + $sequence = $stepPoints[$stepIndex..$stepIndex] + $comment = "$( + if ($letter -eq 'v') { 'vertical' } else {'horizontal'} + ) line $toBy $sequence" + $delta = + if ($letter -eq 'v') { + [Numerics.Vector2]::new(0, $sequence[0]) + } else { + [Numerics.Vector2]::new($sequence[0], 0) + } + if ($isUpper) { $delta -= $currentPosition } + [PSCustomObject]@{ + PSTypeName='Turtle.History' + Letter = "$letter" + Start = $currentPosition + End = $currentPosition + $delta + Delta = $delta + Instruction = "$Letter $sequence" + Comment = $comment + } + $currentPosition += $delta + } + } + z { + $closePosition = $startStack.Pop() + $delta = $closePosition - $currentPosition + [PSCustomObject]@{ + PSTypeName='Turtle.History' + Letter = "$letter" + Start = $currentPosition + End = $currentPosition + $delta + Delta = $delta + Instruction = "$Letter" + Comment = "close path" + } + $currentPosition += $delta + + } + } + + $historyList.Add($historyEntry) +} + + +return $historyList \ No newline at end of file diff --git a/Types/Turtle/get_Keyframe.ps1 b/Types/Turtle/get_Keyframe.ps1 new file mode 100644 index 0000000..c55cc57 --- /dev/null +++ b/Types/Turtle/get_Keyframe.ps1 @@ -0,0 +1,31 @@ +<# +.SYNOPSIS + Gets Turtle keyframes +.DESCRIPTION + Gets CSS Keyframes animations associated with the Turtle. + + Keyframes are stored as a dictionary of dictionaries. + + Each key is the name of the keyframe. + + Each nested dictionary is the keyframe at a given percentage. +.EXAMPLE + turtle keyframe ([Ordered]@{ + 'slide-in' = [Ordered]@{ + from = [Ordered]@{ + translate = "-150vw 0" + scale = "200% 1" + } + to = [Ordered]@{ + translate = "0 0" + scale = "100% 1" + } + } + }) keyframe +.LINK + https://MrPowerShell.com/CSS/Keyframes +#> +if (-not $this.'.Keyframes') { + $this | Add-Member NoteProperty '.Keyframes' ([Ordered]@{}) -Force +} +return $this.'.Keyframes' diff --git a/Types/Turtle/get_Link.ps1 b/Types/Turtle/get_Link.ps1 new file mode 100644 index 0000000..12793a8 --- /dev/null +++ b/Types/Turtle/get_Link.ps1 @@ -0,0 +1,12 @@ +<# +.SYNOPSIS + Gets a Turtle's link +.DESCRIPTION + Gets a link reference (href) associated with the turtle. + + If one is present, this will nest the turtle inside of an element +.EXAMPLE + turtle link https://psturtle.com/ link + +#> +$this.'.link' diff --git a/Types/Turtle/get_Locale.ps1 b/Types/Turtle/get_Locale.ps1 new file mode 100644 index 0000000..fb0ae49 --- /dev/null +++ b/Types/Turtle/get_Locale.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Gets a Turtle's Locale +.DESCRIPTION + Gets the locale associated with a Turtle. + + This is usually nothing, as a picture speaks a thousand words in any language. + + If it is set, it can be used to render content invisible unless the systemLanguage attribute matches the current language preference. +#> +return $this.Attributes['systemLanguage'] diff --git a/Types/Turtle/get_Marker.ps1 b/Types/Turtle/get_Marker.ps1 new file mode 100644 index 0000000..db4b461 --- /dev/null +++ b/Types/Turtle/get_Marker.ps1 @@ -0,0 +1,44 @@ +<# +.SYNOPSIS + Gets the turtle as a Marker +.DESCRIPTION + Gets the Turtle as a ``, which can mark points on another shape. +.EXAMPLE + turtle viewbox 200 teleport 0 100 forward 100 markerEnd ( + turtle viewbox 10 rotate -90 polygon 10 3 fill context-fill stroke context-stroke + ) strokewidth '3%' fill currentColor save ./marker.svg +.LINK + https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/marker +#> +[OutputType([xml])] +param() + +# The default settings for markers +$markerAttributes = [Ordered]@{ + id="$($this.id)-marker" + viewBox="$($this.ViewBox)" + orient='auto-start-reverse' + refX=$this.Width/2 + refY=$this.Height/2 + markerWidth=5 + marketHeight=5 +} +# Marker attributes can exist in .Attribute or .SVGAttribute +$prefix = [Regex]::new('^/?marker/', 'IgnoreCase') +foreach ($collection in $this.Attribute, $this.SVGAttribute) { + foreach ($key in $collection.Keys) { + if ($key -notmatch $prefix) { continue } + $markerAttributes[$key -replace $prefix] = $collection[$key] + } +} + +# Create a marker XML. +[xml]@( + "" + $this.SVG.SVG.InnerXML +"" +) diff --git a/Types/Turtle/get_MarkerEnd.ps1 b/Types/Turtle/get_MarkerEnd.ps1 new file mode 100644 index 0000000..0b40e87 --- /dev/null +++ b/Types/Turtle/get_MarkerEnd.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Gets a Turtle's end marker +.DESCRIPTION + Gets the end marker used on the line drawn by the turtle +.EXAMPLE + turtle viewbox 200 teleport 0 100 forward 100 markerEnd ( + turtle viewbox 10 rotate -90 polygon 10 3 # fill context-fill stroke context-stroke + ) fill '#4488ff' stroke '#224488' strokewidth '3%' save ./marker.svg +#> +return $this.PathAttribute['marker-end'] diff --git a/Types/Turtle/get_MarkerMiddle.ps1 b/Types/Turtle/get_MarkerMiddle.ps1 new file mode 100644 index 0000000..078d4f3 --- /dev/null +++ b/Types/Turtle/get_MarkerMiddle.ps1 @@ -0,0 +1,15 @@ +<# +.SYNOPSIS + Gets a Turtle's middle marker +.DESCRIPTION + Gets the middle marker used on the line drawn by the turtle. + + This marker will be drawn on all vertices that are not the start or the end. +.EXAMPLE + turtle viewbox 200 start 10 200 rotate -60 @( + 'forward',42,'rotate',30,'forward',42,'rotate',-30 * 4 + ) markerMiddle ( + turtle circle 10 fill red + ) strokewidth '3%' save ./marker.svg +#> +return $this.PathAttribute['marker-mid'] diff --git a/Types/Turtle/get_MarkerStart.ps1 b/Types/Turtle/get_MarkerStart.ps1 new file mode 100644 index 0000000..e99c666 --- /dev/null +++ b/Types/Turtle/get_MarkerStart.ps1 @@ -0,0 +1,15 @@ +<# +.SYNOPSIS + Gets a Turtle's start marker +.DESCRIPTION + Gets the start marker used on the line drawn by the turtle +.EXAMPLE + turtle viewbox 200 start 50 50 rotate 45 forward 100 markerStart ( + turtle rotate -90 turtleMonotile -42 fill context-fill stroke context-stroke + ) fill 'currentColor' strokewidth '3%' save ./marker.svg +.EXAMPLE + turtle viewbox 200 start 50 50 rotate 45 forward 100 markerStart ( + turtle rotate -90 polygon 42 3 fill context-fill stroke context-stroke + ) fill 'currentColor' strokewidth '3%' save ./marker.svg +#> +return $this.PathAttribute['marker-start'] diff --git a/Types/Turtle/get_Mask.ps1 b/Types/Turtle/get_Mask.ps1 index cc64782..b38eb94 100644 --- a/Types/Turtle/get_Mask.ps1 +++ b/Types/Turtle/get_Mask.ps1 @@ -1,10 +1,67 @@ +<# +.SYNOPSIS + Gets a Turtle's mask +.DESCRIPTION + Gets a Turtle as an image mask. + + Everything under a white pixel will be visible. + + Everything under a black pixel will be invisible. +.EXAMPLE + # Masks will autoscale to the object bounding box by default + # Make sure to leave a hole. + turtle defines @( + turtle id smallsquare width 84 height 84 @( + ) teleport 21 21 square 42 @( + ) fill black backgroundcolor white mask + ) square 84 fill '#4488ff' stroke '#224488' pathattribute @{ + mask='url(#smallsquare-mask)' + } save ./square-mask.svg +.EXAMPLE + # Masks can contain morphing + turtle defines @( + turtle id smallsquare viewbox 84 @( + 'fill','black' + 'backgroundcolor','white' + ) morph @( + turtle teleport 21 21 square 42 + turtle teleport 41.5 41.5 square 1 + turtle teleport 21 21 square 42 + ) duration '00:00:01.68' mask + ) square 840 fill '#050506ff' stroke '#224488' pathattribute @{ + mask='url(#smallsquare-mask)' + } save ./square-mask-morph.svg +.LINK + https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/mask +#> +[OutputType([xml])] +param() + +$keyPattern = '^mask/' +$defaultId = "$($this.Id)-mask" +# Gather the mask attributes, and default the ID (the only attribute we actually need) +$maskAttributes = [Ordered]@{id=$defaultId} +# Attributes can exist in .SVGAttribute or .Attribute +foreach ($collectionName in 'SVGAttribute','Attribute') { + # as long as they start with mask/ + # (slashes are not valid attribute names, so this can denote a target name or type) + foreach ($key in $this.$collectionName.Keys -match $keyPattern) { + $maskAttributes[$key -replace $keyPattern] = $this.$collectionName[$key] + } +} + +# Create an attribute declaration +$maskAttributes = @(foreach ($attributeName in $maskAttributes.Keys) { + "$($attributeName)='$( + [Web.HttpUtility]::HtmlAttributeEncode($maskAttributes[$attributeName]) + )'" +}) -join ' ' + +# Declare the mask segments $segments = @( -"" - "" - "" - $this.Symbol.OuterXml -replace '\<\?[^\>]+\>' - "" - "" -"" +"" + $this.SVG.OuterXml -replace '\<\?[^\>]+\>' +"" ) +# join them and cast to XML. [xml]($segments -join '') \ No newline at end of file diff --git a/Types/Turtle/get_Maximum.ps1 b/Types/Turtle/get_Maximum.ps1 index ac2c4fd..0289aa8 100644 --- a/Types/Turtle/get_Maximum.ps1 +++ b/Types/Turtle/get_Maximum.ps1 @@ -1,12 +1,15 @@ <# .SYNOPSIS - Gets the turtle maximum point. + Gets the turtle's highest point. .DESCRIPTION - Gets the maximum point reached by the turtle. + Gets the maximum point vector visited by the turtle. - Keeping track of this as we go is far more efficient than calculating it from the path. + This would the highest point that the turtle has been. #> -if ($this.'.Maximum') { - return $this.'.Maximum' +[OutputType([Numerics.Vector2])] +param() +if (-not $this.'.Maximum') { + $this | Add-Member NoteProperty '.Maximum' ([Numerics.Vector2]::new(0,0)) -Force } -return ([pscustomobject]@{ X = 0; Y = 0 }) \ No newline at end of file + +return $this.'.Maximum' diff --git a/Types/Turtle/get_Minimum.ps1 b/Types/Turtle/get_Minimum.ps1 index fe607d7..e7446e2 100644 --- a/Types/Turtle/get_Minimum.ps1 +++ b/Types/Turtle/get_Minimum.ps1 @@ -1,4 +1,15 @@ -if ($this.'.Minimum') { - return $this.'.Minimum' +<# +.SYNOPSIS + Gets a Turtle's lowest point +.DESCRIPTION + Gets the minimum vector for this turtle. + + This would the lowest point that the turtle has visted. +#> +[OutputType([Numerics.Vector2])] +param() +if (-not $this.'.Minimum') { + $this | Add-Member NoteProperty '.Minimum' ([Numerics.Vector2]::new(0,0)) -Force } -return ([pscustomobject]@{ X = 0; Y = 0 }) \ No newline at end of file + +return $this.'.Minimum' \ No newline at end of file diff --git a/Types/Turtle/get_Opacity.ps1 b/Types/Turtle/get_Opacity.ps1 index 97b47e1..cd0d6ce 100644 --- a/Types/Turtle/get_Opacity.ps1 +++ b/Types/Turtle/get_Opacity.ps1 @@ -1,14 +1,10 @@ <# .SYNOPSIS - Gets the turtle opacity + Gets a Turtle's opacity .DESCRIPTION - Gets the opacity of the turtle path. + Gets the opacity of a Turtle +.EXAMPLE + turtle opacity .5 #> -if (-not $this.'.PathAttribute') { - $this | Add-Member -MemberType NoteProperty -Name '.PathAttribute' -Value ([Ordered]@{}) -Force -} -if ($this.'.PathAttribute'.'opacity') { - return $this.'.PathAttribute'.'opacity' -} else { - return 1.0 -} \ No newline at end of file +param() +return $this.'.Opacity' \ No newline at end of file diff --git a/Types/Turtle/get_PathAnimation.ps1 b/Types/Turtle/get_PathAnimation.ps1 index b1aef2e..de3b148 100644 --- a/Types/Turtle/get_PathAnimation.ps1 +++ b/Types/Turtle/get_PathAnimation.ps1 @@ -1,3 +1,17 @@ +<# +.SYNOPSIS + Gets the Turtle's Path Animation +.DESCRIPTION + Gets any path animations associated with the current turtle. +.EXAMPLE + turtle flower PathAnimation ([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 +#> if ($this.'.PathAnimation') { return $this.'.PathAnimation' } diff --git a/Types/Turtle/get_PathData.ps1 b/Types/Turtle/get_PathData.ps1 index e4c0097..d446a44 100644 --- a/Types/Turtle/get_PathData.ps1 +++ b/Types/Turtle/get_PathData.ps1 @@ -1,22 +1,88 @@ +<# +.SYNOPSIS + Gets our Turtle's path +.DESCRIPTION + Gets the path data of this Turtle's movements. + + This is the shape this turtle will draw. +.NOTES + Turtle Path data is represented as a + [SVG path](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorials/SVG_from_scratch/Paths). + + This format can also be used as a [Path2D](https://developer.mozilla.org/en-US/docs/Web/API/Path2D/Path2D) in a Canvas element. + + It can also be used in WPF, where it is simply called [Path Markup](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/graphics-multimedia/path-markup-syntax) +.LINK + https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorials/SVG_from_scratch/Paths +.LINK + https://developer.mozilla.org/en-US/docs/Web/API/Path2D/Path2D +.LINK + https://learn.microsoft.com/en-us/dotnet/desktop/wpf/graphics-multimedia/path-markup-syntax?wt.mc_id=MVP_321542 +.EXAMPLE + turtle square 42 pathdata +#> @( - @( - - if ($this.Start.X -and $this.Start.Y) { + # Let's call this trick Schrödinger's rounding. + # We want to be able to render our shapes with a custom precision + # but we don't want to slow down in rounding or only be able to round once. + + # So we can round when we ask for the path data. + # This is a much less common request than moving the turtle forward. + $precision = $this.Precision + filter roundToPrecision { [Math]::Round($_, $precision)} + + if ($null -ne $this.Start.X -and $null -ne $this.Start.Y) { + if ($precision) { + "m $($this.Start.x | roundToPrecision) $($this.Start.y | roundToPrecision)" + } else { "m $($this.Start.x) $($this.Start.y)" } - else { - @("m" - if ($this.Minimum.X -lt 0) { - -1 * $this.Minimum.X - } else { - 0 + + } + else { + @("m" + # If the viewbox has been manually set + if ($this.'.ViewBox') { + 0, 0 # do not adjust our starting position + } else { + # otherwise, translate by the minimum point. + if ($this.Minimum.X -lt 0) { + if ($precision) { + -1 * $this.Minimum.X | roundToPrecision + } else { + -1 * $this.Minimum.X + } } + else { 0 } + if ($this.Minimum.Y -lt 0) { - -1 * $this.Minimum.Y - } else { - 0 - }) -join ' ' + if ($precision) { + -1 * $this.Minimum.Y | roundToPrecision + } else { + -1 * $this.Minimum.Y + } + + } + else { 0 } + }) -join ' ' + } + + # Walk over our steps + foreach ($step in + $this.Steps -join ' ' -replace ',',' ' -split '(?=[\p{L}-[E]])' -ne '' + ) { + # If our precision is zero or nothing, don't round + if (-not $precision) { + $step + } else { + # Otherwise, pick out the letter + $step.Substring(0,1) + # and get each digit + $digits = $step.Substring(1) -split '\s+' -ne '' -as [double[]] + # and round them. + foreach ($digit in $digits) { + [Math]::Round($digit, $precision) + } } - ) + $this.Steps - # @("m $($this.Start.x) $($this.Start.y) ") + $this.Steps + } ) -join ' ' \ No newline at end of file diff --git a/Types/Turtle/get_PathElement.ps1 b/Types/Turtle/get_PathElement.ps1 index 1cd57f4..d2ba954 100644 --- a/Types/Turtle/get_PathElement.ps1 +++ b/Types/Turtle/get_PathElement.ps1 @@ -5,8 +5,11 @@ Gets the Path Element of a Turtle. This contains the path of the Turtle's motion. +.EXAMPLE + turtle forward 42 rotate 90 forward 42 pathElement #> - +[OutputType([xml])] +param() # Set our core attributes $coreAttributes = [Ordered]@{ id="$($this.id)-path" @@ -26,12 +29,23 @@ foreach ($pathAttributeName in $this.PathAttribute.Keys) { $coreAttributes[$pathAttributeName] = $($this.PathAttribute[$pathAttributeName]) } -@( +# Path attributes can be defined within .SVGAttribute or .Attribute +$prefix = [Regex]::new('^/?path/', 'IgnoreCase') +foreach ($collection in $this.SVGAttribute, $this.Attribute) { + if (-not $collection) { continue } + foreach ($key in $collection.Keys -match $prefix) { + $coreAttributes[$attributeName -replace $prefix] = $collection[$attributeName] + } +} + +# Create the elements in an array, and cast it to XML. +[xml]@( "" +if ($this.Title) { "$([Security.SecurityElement]::Escape($this.Title))" } if ($this.PathAnimation) {$this.PathAnimation} "" -) -as [xml] \ No newline at end of file +) \ No newline at end of file diff --git a/Types/Turtle/get_Pattern.ps1 b/Types/Turtle/get_Pattern.ps1 index d4c8454..8639417 100644 --- a/Types/Turtle/get_Pattern.ps1 +++ b/Types/Turtle/get_Pattern.ps1 @@ -1,28 +1,127 @@ +<# +.SYNOPSIS + Gets a Turtle Pattern +.DESCRIPTION + Gets the current turtle as a pattern that stretches off to infinity. +.EXAMPLE + turtle star 42 4 | Save-Turtle "./GridPattern.svg" +.EXAMPLE + turtle star 42 6 | Save-Turtle "./StarPattern6.svg" +.EXAMPLE + turtle star 42 8 | Save-Turtle "./StarPattern8.svg" +.EXAMPLE + turtle star 42 5 | Save-Turtle "./StarPattern5.svg" +.EXAMPLE + turtle star 42 7 | Save-Turtle "./Star7Pattern.svg" +.EXAMPLE + turtle viewbox 100 start 25 25 square 50 | Save-Turtle "./WindowPattern.svg" Pattern +.EXAMPLE + turtle star 100 4 morph @( + turtle star 100 4 + turtle rotate 90 star 100 4 + turtle rotate 180 star 100 4 + turtle rotate 270 star 100 4 + turtle star 100 4 + ) | Save-Turtle "./GridPatternMorph.svg" +.EXAMPLE + turtle star 100 3 morph @( + turtle star 100 3 + turtle rotate 90 star 100 3 + turtle rotate 180 star 100 3 + turtle rotate 270 star 100 3 + turtle star 100 3 + ) | Save-Turtle "./TriPatternMorph.svg" +.EXAMPLE + turtle star 100 6 morph @( + turtle star 100 6 + turtle rotate 90 star 100 6 + turtle rotate 180 star 100 6 + turtle rotate 270 star 100 6 + turtle star 100 6 + ) | Save-Turtle "./Star6PatternMorph.svg" | Show-Turtle +.EXAMPLE + # We can use a pattern transform to scale the pattern + turtle sierpinskiTriangle PatternTransform @{ + scale = 0.25 + rotate = 120 + } | + Save-Turtle "./SierpinskiTrianglePattern.svg" Pattern | + show-Turtle +.EXAMPLE + # We can use pattern animations to change the pattern + # Animations are relative to initial transforms + turtle sierpinskiTriangle PatternTransform @{ + scale = 0.25 + rotate = 120 + } PatternAnimation ([Ordered]@{ + type = 'scale' ; values = 1.33,0.66, 1.33 ; repeatCount = 'indefinite' ;dur = "23s"; additive = 'sum' + }) | + Save-Turtle "./SierpinskiTrianglePattern.svg" Pattern | + Show-Turtle +.LINK + https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorials/SVG_from_scratch/Patterns +.LINK + https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/pattern +#> +[OutputType([xml])] param() -$segments = @( + +# Get our viewbox $viewBox = $this.ViewBox -$null, $null, $viewX, $viewY = $viewBox +# and get the width and height +$viewX, $viewY, $viewWidth, $viewHeight = $viewBox + +# Initialize our core attributes. +# These may be overwritten by user request. +$coreAttributes = [Ordered]@{ + 'id' = "$($this.ID)-pattern" + 'patternUnits' = 'userSpaceOnUse' + 'x' = $viewX + 'y' = $viewY + 'width' = $viewWidth + 'height' = $viewHeight + 'transform-origin' = '50% 50%' +} + +# If we have specified any transforms +if ($this.PatternTransform) { + $coreAttributes."patternTransform" = + # Then generate a transform expression + @(foreach ($key in $this.PatternTransform.Keys) { + # transforms are a name, followed by parameters in paranthesis + "$key($($this.PatternTransform[$key]))" + }) -join ' ' +} + +# Pattern attributes can be defined within .SVGAttribute or .Attribute +# provided they have the appropriate prefix +$prefix = [Regex]::new('^/?pattern/', 'IgnoreCase') +# (slashes are invalid markup, and thus a fine way to target nested instances) + +foreach ($collection in $this.SVGAttribute, $this.Attribute) { + # If the connection does not exist, continue. + if (-not $collection) { continue } + # For each key that matches the prefix + foreach ($key in $collection.Keys -match $prefix) { + # add it to the attributes after stripping the prefix. + $coreAttributes[$attributeName -replace $prefix] = $collection[$attributeName] + } +} + +$segments = @( "" "" - "" + "" $(if ($this.PatternAnimation) { $this.PatternAnimation }) $($this.SVG.SVG.InnerXML) "" "" -$( - if ($this.BackgroundColor) { - "" - } -) "" -"") +"" +) -$segments -join '' -as [xml] \ No newline at end of file +[xml]$segments \ No newline at end of file diff --git a/Types/Turtle/get_PatternAnimation.ps1 b/Types/Turtle/get_PatternAnimation.ps1 index b0a25b7..753df82 100644 --- a/Types/Turtle/get_PatternAnimation.ps1 +++ b/Types/Turtle/get_PatternAnimation.ps1 @@ -1,3 +1,79 @@ +<# +.SYNOPSIS + Gets pattern animations +.DESCRIPTION + Gets one or more animations that apply to our Turtle's pattern. + + These animations will transform the pattern, allowing for endless variation. +.EXAMPLE + turtle flower PatternAnimation ([Ordered]@{ + type = 'translate' + values = "0 0","0 420", "0 0" + repeatCount = 'indefinite' + # dur = "11s" # The duration will default to the Turtle's duration + additive = 'sum' + }) save ./FlowerPatternAnimation.svg +.EXAMPLE + # We can have multiple pattern animations, and need to use `additive=sum` to ensure they do not conflict + turtle SierpinskiTriangle duration '00:00:42' PatternAnimation ([Ordered]@{ + type = 'rotate' + values = "0","360" + repeatCount = 'indefinite' + additive = 'sum' + }, [Ordered]@{ + type = 'scale' + values = "1",".25", "1" + repeatCount = 'indefinite' + additive = 'sum' + }) save ./SierpinskiTrianglePatternAnimation.svg +.EXAMPLE + # Pattern Transforms set a starting state for animations + turtle SierpinskiTriangle duration '00:00:42' PatternTransform @{ + scale = .25 + } PatternAnimation ([Ordered]@{ + type = 'rotate' + values = "0","360" + repeatCount = 'indefinite' + additive = 'sum' + }, [Ordered]@{ + type = 'scale' + values = "1",".25", "1" + repeatCount = 'indefinite' + additive = 'sum' + }) save ./SierpinskiTrianglePatternAnimationSmaller.svg +.EXAMPLE + # We can use primes as pattern transform durations to ensure animations rarely overlap + # This example uses four primes under 100: + # It will repeat in `23 * 41 * 61 * 83` seconds + # (or just over 55 days) + turtle SierpinskiTriangle duration '00:00:42' PatternTransform @{ + scale = .25 + } PatternAnimation ([Ordered]@{ + type = 'rotate' + values = "0","360" + repeatCount = 'indefinite' + dur = '83s' + additive = 'sum' + }, [Ordered]@{ + type = 'scale' + values = "1",".25", "1" + repeatCount = 'indefinite' + dur = '23s' + additive = 'sum' + }, [Ordered]@{ + type = 'skewX' + values = "0","45", "0" + repeatCount = 'indefinite' + additive = 'sum' + dur = '41s' + }, [Ordered]@{ + type = 'skewX' + values = "0","-45", "0" + repeatCount = 'indefinite' + additive = 'sum' + dur = '61s' + }) save ./SierpinskiTrianglePatternAnimationEndless.svg +#> if ($this.'.PatternAnimation') { return $this.'.PatternAnimation' } diff --git a/Types/Turtle/get_PatternMask.ps1 b/Types/Turtle/get_PatternMask.ps1 index 21a3072..1a2e04f 100644 --- a/Types/Turtle/get_PatternMask.ps1 +++ b/Types/Turtle/get_PatternMask.ps1 @@ -1,10 +1,69 @@ +<# +.SYNOPSIS + Gets a Turtle's Pattern Mask +.DESCRIPTION + Gets the current turtle as a pattern mask. + + Everything under a white pixel will be visible. + + Everything under a black pixel will be invisible. + + This will be a mask of the turtle's `.Pattern` property, and will repeat the turtle's `.SVG` multiple times. +.EXAMPLE + # Masks will autoscale to the object bounding box by default + # Make sure to leave a hole. + turtle defines @( + turtle id smallsquare viewbox 84 teleport 21 21 square 42 @( + 'fill','black' + 'backgroundcolor','white' + ) patternmask + ) square 840 fill '#4488ff' stroke '#224488' pathattribute @{ + mask='url(#smallsquare-pattern-mask)' + } save ./square-pattern-mask.svg +.EXAMPLE + # Masks can contain morphing + turtle defines @( + turtle id smallsquare viewbox 84 @( + 'fill','black' + 'backgroundcolor','white' + ) morph @( + turtle teleport 21 21 square 42 + turtle teleport 42 42 square 1 + turtle teleport 21 21 square 42 + ) duration '00:00:01.68' patternmask + ) square 840 fill '#4488ff' stroke '#224488' pathattribute @{ + mask='url(#smallsquare-pattern-mask)' + } save ./square-pattern-mask-morph.svg +.LINK + https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/mask +#> +[OutputType([xml])] +param() +$keyPattern = '^pattern-?mask/' +$defaultId = "$($this.Id)-pattern-mask" +# Gather the mask attributes, and default the ID (the only attribute we actually need) +$maskAttributes = [Ordered]@{id=$defaultId} +# Attributes can exist in .SVGAttribute or .Attribute +foreach ($collectionName in 'SVGAttribute','Attribute') { + # as long as they start with mask/ + # (slashes are not valid attribute names, so this can denote a target name or type) + foreach ($key in $this.$collectionName.Keys -match $keyPattern) { + $maskAttributes[$key -replace $keyPattern] = $this.$collectionName[$key] + } +} + +# Create an attribute declaration +$maskAttributes = @(foreach ($attributeName in $maskAttributes.Keys) { + "$($attributeName)='$( + [Web.HttpUtility]::HtmlAttributeEncode($maskAttributes[$attributeName]) + )'" +}) -join ' ' + +# Declare the mask segments $segments = @( -"" - "" - "" - $this.Pattern.OuterXml -replace '\<\?[^\>]+\>' - "" - "" -"" +"" + $this.Pattern.OuterXml -replace '\<\?[^\>]+\>' +"" ) -[xml]($segments -join '') \ No newline at end of file +# join them and cast to XML. +[xml]$segments \ No newline at end of file diff --git a/Types/Turtle/get_Position.ps1 b/Types/Turtle/get_Position.ps1 index becc406..35f0f6f 100644 --- a/Types/Turtle/get_Position.ps1 +++ b/Types/Turtle/get_Position.ps1 @@ -1,4 +1,14 @@ +<# +.SYNOPSIS + Gets the Turtle's position +.DESCRIPTION + Gets the current position of the turtle as a vector. +#> +[OutputType([Numerics.Vector2])] +param() if (-not $this.'.Position') { - $this | Add-Member -MemberType NoteProperty -Force -Name '.Position' -Value ([pscustomobject]@{ X = 0; Y = 0 }) + $this | Add-Member -MemberType NoteProperty -Force -Name '.Position' -Value ( + [Numerics.Vector2]::new(0,0) + ) } return $this.'.Position' diff --git a/Types/Turtle/get_Precision.ps1 b/Types/Turtle/get_Precision.ps1 new file mode 100644 index 0000000..82406fe --- /dev/null +++ b/Types/Turtle/get_Precision.ps1 @@ -0,0 +1,18 @@ +<# +.SYNOPSIS + Gets Turtle Precision +.DESCRIPTION + Gets the rounding precision for the turtle. + + Any move the turtle makes will be rounded by this number of digits. + + Paths with more rounding may be more accurate at extremly high resolutions. + + They will have difficulty rendering stepwise animations and take up more file space per point. + + The default value for `Precision` is currently `6` +#> +if (-not $this.'.Precision') { + $this | Add-Member NoteProperty '.Precision' 6 -Force +} +return $this.'.Precision' diff --git a/Types/Turtle/get_SVG.ps1 b/Types/Turtle/get_SVG.ps1 index 9b90740..22f4d86 100644 --- a/Types/Turtle/get_SVG.ps1 +++ b/Types/Turtle/get_SVG.ps1 @@ -4,8 +4,8 @@ .DESCRIPTION Gets this turtle and any nested turtles as a single Scalable Vector Graphic. #> +[OutputType([xml])] param() -@( $svgAttributes = [Ordered]@{ xmlns='http://www.w3.org/2000/svg' @@ -15,6 +15,11 @@ $svgAttributes = [Ordered]@{ height='100%' } +# If opacity is set, it should apply to the entire SVG. +if ($null -ne $this.opacity) { + $svgAttributes['opacity'] = $this.opacity +} + # 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. @@ -22,20 +27,49 @@ if ($this.ViewBox[-1] -eq 0 -or $this.ViewBox[-2] -eq 0) { } # Any explicitly provided attributes should override any automatic attributes. + +# These can come from .Attribute +foreach ($key in $this.Attribute.Keys) { + if ($key -match '^svg/') { # (as long as they start with `svg/`) + $svgAttributes[$key -replace '^svg/'] = $this.Attribute[$key] + } +} + +# They can also come from `.SVGAttribute` foreach ($key in $this.SVGAttribute.Keys) { $svgAttributes[$key] = $this.SVGAttribute[$key] } +$svgElement = @( "" + # Declare any definitions, like markers or gradients. + if ($this.Defines) { + "" + $this.Defines + "" + } + + $style = $this.Style + if ($style -is [xml]) { + $style.OuterXml + } + + # Declare any SVG animations if ($this.SVGAnimation) {$this.SVGAnimation} - + if ($this.BackgroundColor) { + "" + } + if ($this.Link) { + "" + } # Output our own path $this.PathElement.OuterXml # Followed by any text elements - $this.TextElement.OuterXml + $this.TextElement.OuterXml # If the turtle has children $children = @(foreach ($turtleName in $this.Turtles.Keys) { @@ -59,5 +93,9 @@ foreach ($key in $this.SVGAttribute.Keys) { } "" } + if ($this.Link) { + "" + } "" -) -join '' -as [xml] \ No newline at end of file +) +[xml]$svgElement \ No newline at end of file diff --git a/Types/Turtle/get_ScriptBlock.ps1 b/Types/Turtle/get_ScriptBlock.ps1 new file mode 100644 index 0000000..a27c31d --- /dev/null +++ b/Types/Turtle/get_ScriptBlock.ps1 @@ -0,0 +1,19 @@ +<# +.SYNOPSIS + Get the Turtle's ScriptBlock +.DESCRIPTION + Gets the ScriptBlock used to create the turtle. + + All steps will become a fluent pipeline. +.EXAMPLE + turtle SierpinskiTriangle 42 4 scriptBlock +#> +[OutputType([ScriptBlock])] +param() +# Join all of our previous command extents into a fluent pipeline +$stringifiedScript = $this.Commands.Extent -join + (' |' + [Environment]::NewLine + ' ') -replace # and then replace any unescaped use of 'ScriptBlock' or 'DataBlock' + "(?$( + 'ScriptBlock', 'DataBlock' -join '|' + ))(?!\])\s{0,}" +[ScriptBlock]::Create($stringifiedScript) diff --git a/Types/Turtle/get_Start.ps1 b/Types/Turtle/get_Start.ps1 new file mode 100644 index 0000000..7c59b9b --- /dev/null +++ b/Types/Turtle/get_Start.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Gets the Start Vector a Turtle +.DESCRIPTION + Gets the starting vector for a Turtle. + + Setting this value avoids an automatic calculation of a starting position. +.EXAMPLE + turtle width 300 height 300 start 50 square 200 start +#> +return $this.'.Start' diff --git a/Types/Turtle/get_Stroke.ps1 b/Types/Turtle/get_Stroke.ps1 index 5b193d3..64d4fae 100644 --- a/Types/Turtle/get_Stroke.ps1 +++ b/Types/Turtle/get_Stroke.ps1 @@ -1,3 +1,27 @@ +<# +.SYNOPSIS + Gets a Turtle's stroke color +.DESCRIPTION + Gets one or more colors used to stroke the Turtle. + + By default, this is transparent. + + If more than one value is provided, the stroke will be a gradient. +.EXAMPLE + # Draw a blue square + turtle square 42 stroke blue +.EXAMPLE + # Draw a PowerShell blue square + turtle square 42 stroke '#4488ff' +.EXAMPLE + # Draw a red, green, blue gradient + turtle square 42 stroke red green blue show +.EXAMPLE + # Draw a red, green, blue linear gradient + turtle square 42 stroke red green blue linear show +.EXAMPLE + turtle flower stroke red green blue strokerule evenodd show +#> if ($this.'.Stroke') { return $this.'.Stroke' } else { diff --git a/Types/Turtle/get_Style.ps1 b/Types/Turtle/get_Style.ps1 new file mode 100644 index 0000000..bfcf9e0 --- /dev/null +++ b/Types/Turtle/get_Style.ps1 @@ -0,0 +1,79 @@ +<# +.SYNOPSIS + Gets a Turtle's Style +.DESCRIPTION + Gets any CSS styles associated with the Turtle. + + These styles will be declared in a `") + } catch { + # catch it and escape the content + return [xml]@( + "" + ) + } +} else { + return '' +} + +return $this.'.style' \ No newline at end of file diff --git a/Types/Turtle/get_Symbol.ps1 b/Types/Turtle/get_Symbol.ps1 index e028799..a71152a 100644 --- a/Types/Turtle/get_Symbol.ps1 +++ b/Types/Turtle/get_Symbol.ps1 @@ -11,18 +11,14 @@ Move-Turtle Flower | Select-Object -ExpandProperty Symbol #> +[OutputType([xml])] param() -@( - "" - "" - $($this.SVG.OuterXml) - "" - $( - if ($this.BackgroundColor) { - "" - } - ) - "" - "" -) -join '' -as [xml] \ No newline at end of file +[xml]@( +"" + "" + $($this.SVG.OuterXml) + "" + "" +"" +) \ No newline at end of file diff --git a/Types/Turtle/get_TextElement.ps1 b/Types/Turtle/get_TextElement.ps1 index 58d33e2..d344080 100644 --- a/Types/Turtle/get_TextElement.ps1 +++ b/Types/Turtle/get_TextElement.ps1 @@ -1,14 +1,94 @@ +<# +.SYNOPSIS + Gets a Turtle's text element +.DESCRIPTION + Gets a Turtle's text as a SVG Text element. -if ($this.Text) { - return @( - " +[OutputType([xml])] +param() + +# If there is no text, there's no text element +if (-not $this.Text) { return } + +# Collect all of our text attributes +$textAttributes = [Ordered]@{ + id="$($this.ID)-text" +} + +# If there are no steps +if (-not $this.Steps) { + # default the text to the middle + $textAttributes['dominant-baseline'] = 'middle' + $textAttributes['text-anchor'] = 'middle' + $textAttributes['x'] = '50%' + $textAttributes['y'] = '50%' +} + +if ($this.fill -ne 'transparent') { + $textAttributes['stroke'] = $this.stroke + $textAttributes['fill'] = $this.fill +} else { + $textAttributes['fill'] = $this.stroke +} + + +# Text Attributes can exist in Attribute or SVGAttribute, as long as they are prefixed. +$prefix = '^/?text/' +foreach ($collection in 'Attribute','SVGAttribute') { + if (-not $this.$Collection.Count) { continue } + foreach ($key in $this.$collection.Keys) { + if ($key -match $prefix) { + $textAttributes[$key -replace $prefix] = $this.$collection[$key] + } + } +} + +# Explicit text attributes will be copied last, so they take precedent. +foreach ($key in $this.TextAttribute.Keys) { + $textAttributes[$key] = $this.TextAttribute[$key] +} + +# Return a constructed element +return [xml]@( +# Create the text element +"" - "$([Security.SecurityElement]::Escape($this.Text))" - if ($this.TextAnimation) {$this.TextAnimation} - "" - ) -as [xml] + +# If there is a title +if ($this.Title) { + # embed it here (so that the text is accessible). + "$([Security.SecurityElement]::Escape($this.Title))" +} else { + # otherwise, use the text as the title. + "$([Security.SecurityElement]::Escape($this.Text))" } +# If there are any text animations, include them here. +if ($this.TextAnimation) {$this.TextAnimation} + +# Escape our text +$escapedText = [Security.SecurityElement]::Escape($this.Text) +# If we have steps, +if ($this.Steps) { + # put the escaped text within a ``. + "$escapedText" +} else { + # otherwise, include the escaped text as the content + $escapedText +} +# close the element and return our XML. +"" +) \ No newline at end of file diff --git a/Types/Turtle/get_Title.ps1 b/Types/Turtle/get_Title.ps1 new file mode 100644 index 0000000..4d071fc --- /dev/null +++ b/Types/Turtle/get_Title.ps1 @@ -0,0 +1,11 @@ +<# +.SYNOPSIS + Gets a Turtle's title +.DESCRIPTION + Gets the title assigned to a Turtle. + + A title will provide alternate text for the image that should be visible on hover, and should be available to screen readers. +.EXAMPLE + turtle square 42 title "It's Hip To Be Square" +#> +return $this.'.Title' diff --git a/Types/Turtle/get_Variable.ps1 b/Types/Turtle/get_Variable.ps1 new file mode 100644 index 0000000..9725c4e --- /dev/null +++ b/Types/Turtle/get_Variable.ps1 @@ -0,0 +1,15 @@ +<# +.SYNOPSIS + Gets Turtle Variables +.DESCRIPTION + Gets variables associated with the Turtle. + + Variables that start with -- will become CSS variables +#> +param() + +if (-not $this.'.Variables') { + $this | Add-Member NoteProperty '.Variables' ([Ordered]@{}) -Force +} + +return $this.'.Variables' \ No newline at end of file diff --git a/Types/Turtle/get_ViewBox.ps1 b/Types/Turtle/get_ViewBox.ps1 index f508524..0b7cd33 100644 --- a/Types/Turtle/get_ViewBox.ps1 +++ b/Types/Turtle/get_ViewBox.ps1 @@ -1,8 +1,43 @@ +<# +.SYNOPSIS + Gets the Turtle's viewbox +.DESCRIPTION + Gets the Turtle's current viewBox. + + If this has not been set, it will be automatically calculated by the minimum and maximum +.NOTES + turtle square 42 viewbox +#> + +param() + +# If we have set a viewbox, return it. if ($this.'.ViewBox') { return $this.'.ViewBox' } -$viewX = $this.Maximum.X + ($this.Minimum.X * -1) -$viewY = $this.Maximum.Y + ($this.Minimum.Y * -1) +# Otherwise, subtract max from minimum to get a bounding box +$viewBox = ($this.Maximum - $this.Minimum) + +$precision = $this.Precision +filter roundToPrecision { [Math]::Round($_, $precision)} + + +$viewX = [Math]::Round($viewBox.X, 10) +$viewY = [Math]::Round($viewBox.Y, 10) + +if ($viewX -and -not $viewY) { + $viewY = $viewX +} +if ($viewY -and -not $viewX) { + $viewX = $viewY +} + + +# and return the viewbox +if ($precision) { + return 0, 0, $viewX, $viewY | roundToPrecision +} else { + return 0, 0, $viewX, $viewY +} -return 0, 0, $viewX, $viewY diff --git a/Types/Turtle/set_Attribute.ps1 b/Types/Turtle/set_Attribute.ps1 new file mode 100644 index 0000000..8fd14a6 --- /dev/null +++ b/Types/Turtle/set_Attribute.ps1 @@ -0,0 +1,41 @@ +<# +.SYNOPSIS + Sets Turtle attributes +.DESCRIPTION + Sets arbitrary attributes for the current Turtle. + + Attributes generally apply to the topmost tag. + + If an attribute contains a slash, it will be targeted to tags of that type. +.EXAMPLE + turtle attribute @{foo='bar'} attribute +.EXAMPLE + turtle attribute 'foo=bar' attribute +#> +param( +[PSObject[]] +$Attribute = [Ordered]@{} +) + +$myAttributes = $this.Attribute +foreach ($attrSet in $Attribute) { + if ($attrSet -is [Collections.IDictionary]) { + foreach ($key in $attrSet.Keys) { + $myAttributes[$key] = $attrSet[$key] + } + } + elseif ($attrSet -is [string]) { + if ($attrSet -match '[:=]') { + $key, $value = $attrSet -split '[:=]', 2 + $myAttributes[$key] = $value + } else { + $myAttributes[$key] = '' + } + } + elseif ($attrSet -is [PSObject]) { + foreach ($key in $attrSet.psobject.properties.name) { + $myAttributes[$key] = $attrSet.$key + } + } + +} diff --git a/Types/Turtle/set_Class.ps1 b/Types/Turtle/set_Class.ps1 new file mode 100644 index 0000000..0a97b2a --- /dev/null +++ b/Types/Turtle/set_Class.ps1 @@ -0,0 +1,13 @@ +<# +.SYNOPSIS + Sets a Turtle's class +.DESCRIPTION + Sets any CSS classes associated with the turtle +.EXAMPLE + turtle class foo bar baz bing class +#> +param( +$Class +) + +$this.Attribute['class'] = $this.SVGAttribute['class'] = $this.PathAttribute['class'] = $Class -join ' ' diff --git a/Types/Turtle/set_Defines.ps1 b/Types/Turtle/set_Defines.ps1 new file mode 100644 index 0000000..cb048b4 --- /dev/null +++ b/Types/Turtle/set_Defines.ps1 @@ -0,0 +1,70 @@ +<# +.SYNOPSIS + Sets the Turtle Path Animation +.DESCRIPTION + Sets an animation for the Turtle path. +.EXAMPLE + $t = turtle defines @( + " + + + + " + " + + + + " + + ) flower 42 fill 'url("#gradient")' stroke 'url("#gradient2")' + $t | turtle save ./gradient.svg +.EXAMPLE + $t = turtle defines @( + " + + + + " + " + + + + " + ) width 100 height 100 teleport 50 50 StarFlower 42 14.4 6 25 fill 'url("#gradient")' stroke 'url("#gradient2")' fillrule evenodd morph @( + turtle teleport 50 50 StarFlower 42 12 5 30 + turtle teleport 50 50 StarFlower 42 14.4 6 25 + turtle teleport 50 50 StarFlower 42 12 5 30 + ) PathAnimation ( [Ordered]@{ + type = 'rotate' ; values = 0, 360 ;repeatCount = 'indefinite'; dur = "4.2s" + }) + $t | turtle save ./gradientrotate.svg +#> +param( +# The definition object. +# This may be a string, XML, a dictionary containing defines, or an element +[PSObject] +$Defines +) + +$newDefinition = @(foreach ($definition in $Defines) { + if ($definition -is [Collections.IDictionary]) { + $definitionCopy = [Ordered]@{} + $definition + "<$elementName $( + @(foreach ($key in $definitionCopy.Keys) { + if ($key -eq 'Children') { continue } + " $key='$([Web.HttpUtility]::HtmlAttributeEncode($definitionCopy[$key]))'" + }) -join '' + )$()>" + } + elseif ($definition -is [string]) { + $definition + } + elseif ($definition.OuterXml) { + $definition.OuterXml + } + else { + "$definition" + } +}) + +$this | Add-Member -MemberType NoteProperty -Force -Name '.Defines' -Value $newDefinition diff --git a/Types/Turtle/set_Element.ps1 b/Types/Turtle/set_Element.ps1 new file mode 100644 index 0000000..729f970 --- /dev/null +++ b/Types/Turtle/set_Element.ps1 @@ -0,0 +1,106 @@ +<# +.SYNOPSIS + Sets the Turtle element +.DESCRIPTION + Sets the Turtle to an arbitrary element. + + This lets us write web pages and xml entirely in turtle. +.EXAMPLE + # Any bareword can become the name of an element, as long as it is not a method name + turtle element div element +.EXAMPLE + # We can provide anything that will cast to XML as an element + turtle element '
' element +.EXAMPLE + # We can provide an element and attributes + turtle element '
' element +.EXAMPLE + # We can put a turtle inside of an aribtrary element + turtle SpiderWeb element '
' +#> + +param() + +if (-not $this.'.Element') { + $this | Add-Member NoteProperty '.Element' -Value ([Ordered]@{ + ElementName='' + Attribute=$this.Attribute + Children=@() + }) +} + +$unrolledArgs = $args |. {process { $_ }} + +foreach ($element in $unrolledArgs){ + if ($element -is [string] -and + (-not ($element -as [xml])) -and + $element -notmatch '\s' + ) { + $this.'.Element'.ElementName = $Element + continue + } + + if ($element -is [xml] -or $Element -as [xml]) { + if ($Element -isnot [xml]) { + $element = $Element -as [xml] + } + $this.'.Element'.ElementName = $Element.ChildNodes[0].LocalName + foreach ($attribute in $element.ChildNodes[0].Attributes) { + $this.'.Element'.Attribute[$attribute.Name] = $attribute.Value + } + foreach ($grandchild in $element.ChildNodes[0].ChildNodes) { + $this.'.Element'.Children += $grandchild + } + continue + } + + if ($element -is [Collections.IDictionary]) { + $elementKeys = 'ElementName','Name','E' + foreach ($potentialName in $elementKeys) { + if ($element.$potentialName) { + $this.'.Element'.ElementName = $element.$potentialName + break + } + } + $attributeKeys = 'Attribute', 'Attributes', 'A' + foreach ($potentialAttributeName in $attributeKeys) { + if ($element.$potentialAttributeName -is [Collections.IDictionary] -and + $element.$potentialAttributeName.Count) { + foreach ($attributeName in $element.$potentialAttributeName.Keys) { + $this.'.Element'.Attribute[$attributeName] = $element.$potentialAttributeName[$attributeName] + } + break + } + } + $childKeys = 'Child', 'Children', 'ChildNodes','Content', 'C' + + foreach ($potentialChildrenName in $childKeys) { + $children = $element.$potentialChildrenName + if (-not $children) { continue } + $this.'.Element'.Children += $children + break + } + + $specialKeys = @( + $elementKeys + $attributeKeys + $childKeys + ) + + foreach ($elementKey in $element.Keys) { + if ($elementKey -in $specialKeys) { continue } + $elementValue = $element[$elementKey] + if ($elementValue -is [ValueType] -or ( + $elementValue -is [string] -and $elementValue -notmatch '[\r\n]' + )) { + $this.'.Element'.Attribute[$elementKey] = $elementValue + } + } + continue + } + + if ($elementName) { + + } +} + diff --git a/Types/Turtle/set_Fill.ps1 b/Types/Turtle/set_Fill.ps1 index 9d8588c..3ad74a5 100644 --- a/Types/Turtle/set_Fill.ps1 +++ b/Types/Turtle/set_Fill.ps1 @@ -1,7 +1,107 @@ +<# +.SYNOPSIS + Sets a Turtle's fill color +.DESCRIPTION + Sets one or more colors used to fill the Turtle. + + By default, this is transparent. + + If more than one value is provided, the fill will be a gradient. +.EXAMPLE + # Draw a blue square + turtle square 42 fill blue +.EXAMPLE + # Draw a PowerShell blue square + turtle square 42 fill '#4488ff' +.EXAMPLE + # Draw a red, green, blue gradient + turtle square 42 fill red green blue show +.EXAMPLE + # Draw a red, green, blue linear gradient + turtle square 42 fill red green blue linear show +.EXAMPLE + turtle flower fill red green blue fillrule evenodd show +#> param( - [string]$Fill = 'transparent' +[PSObject[]] +$Fill = 'transparent' ) +# If we have no fill information, return +if (-not $fill) { return } + +# If the fill count is greater than one, try to make a graidnet +if ($fill.Count -gt 1) { + + # Default to a radial gradient + $gradientTypeHint = 'radial' + # and create a collection for attributes + $gradientAttributes = [Ordered]@{ + # default our identifier to the current id plus `fill-gradient` + # (so we could have multiple gradients without a collision) + id="$($this.id)-fill-gradient" + } + + $fill = @(foreach ($color in $fill) { + # If the value matches `linear` or `radial` + if ($color -match '^(linear|radial)') { + # take the hint and make it the right type of gradient. + $gradientTypeHint = ($color -replace 'gradient').ToLower() + } + # If the color was `pad`, `reflect`, or `repeat` + elseif ($fillColor -in 'pad', 'reflect', 'repeat') { + # take the hint and set the spreadMethod + $gradientAttributes['spreadMethod'] = $color + } + # If the fill is a dictionary + elseif ($color -is [Collections.IDictionary]) { + # propagate the values into attributes. + foreach ($gradientAttributeKey in $color.Keys) { + $gradientAttributes[$gradientAttributeKey] = $color[$gradientAttributeKey] + } + } + # Otherwise output the color + else { + $color + } + }) + + # If we have no fill colors after filtering, return + if (-not $fill) { return } + + # If our count is one + if ($fill.Count -eq 1) { + # it's not really going to be a gradient, so just use the one color. + $this | Add-Member -MemberType NoteProperty -Name '.Fill' -Value $Fill -Force + return + } + + # Now we have at least two colors we want to be a gradient + # We need to make sure the offset starts at 0% an ends at 100% + # and so we actually need to divide by one less than our fill color, so we end at 100%. + $offsetStep = 1 / ($fill.Count - 1) + $Gradient = @( + # Construct our gradient element. + "<${gradientTypeHint}Gradient$( + # propagate our attributes + @(foreach ($gradientAttributeKey in $gradientAttributes.Keys) { + " $gradientAttributeKey='$($gradientAttributes[$gradientAttributeKey])'" + }) -join '' + )>" + @( + # and put in our stop colors + for ($fillNumber = 0; $fillNumber -lt $fill.Count; $fillNumber++) { + "" + } + ) + "" + ) -join [Environment]::NewLine + + # add this gradient to our defines + $this.Defines += $Gradient + # and set fill to this gradient. + $fill = "url(`"#$($gradientAttributes.id)`")" +} if (-not $this.'.Fill') { $this | Add-Member -MemberType NoteProperty -Name '.Fill' -Value $Fill -Force } else { diff --git a/Types/Turtle/set_Keyframe.ps1 b/Types/Turtle/set_Keyframe.ps1 new file mode 100644 index 0000000..796b1e9 --- /dev/null +++ b/Types/Turtle/set_Keyframe.ps1 @@ -0,0 +1,28 @@ +<# +.SYNOPSIS + Sets Turtle Keyframes +.DESCRIPTION + Sets CSS Keyframes associated with a Turtle. +.EXAMPLE + turtle square 42 fill '#4488ff' stroke '#224488' keyframe ([Ordered]@{ + 'wiggle3d' = [Ordered]@{ + '0%,100%' = [Ordered]@{ + transform = "rotateX(-3deg) rotateY(-3deg) rotateZ(-3deg)" + } + '50%' = [Ordered]@{ + transform = "rotateX(3deg) rotateY(3deg) rotateZ(3deg)" + } + } + }) pathclass wiggle3d save ./wiggleSquare.svg +#> +param( +[PSObject] +$Keyframe +) + +$keyframes = $this.Keyframe +if ($Keyframe -is [Collections.IDictionary]) { + foreach ($key in $keyframe.Keys) { + $keyframes[$key] = $Keyframe[$key] + } +} diff --git a/Types/Turtle/set_Link.ps1 b/Types/Turtle/set_Link.ps1 new file mode 100644 index 0000000..24f779e --- /dev/null +++ b/Types/Turtle/set_Link.ps1 @@ -0,0 +1,16 @@ +<# +.SYNOPSIS + Sets a Turtle's link +.DESCRIPTION + Sets a link reference (`href`) associated with the turtle. + + If one is present, this will nest the turtle inside of an anchor `` element +.EXAMPLE + turtle link https://psturtle.com/ +#> +param( +[string] +$Link +) + +$this | Add-Member NoteProperty '.Link' $link -Force diff --git a/Types/Turtle/set_Locale.ps1 b/Types/Turtle/set_Locale.ps1 new file mode 100644 index 0000000..4b31078 --- /dev/null +++ b/Types/Turtle/set_Locale.ps1 @@ -0,0 +1,19 @@ +<# +.SYNOPSIS + Gets a Turtle's Locale +.DESCRIPTION + Gets the locale associated with a Turtle. + + This is usually nothing, as a picture speaks a thousand words in any language. + + If it is set, it can be used to render content invisible unless the systemLanguage attribute matches the current language preference. +#> +$unrolledArgs = $args | . { process { $_ } } +$joinedArgs = $unrolledArgs -join ',' +if (-not $joinedArgs) { + $this.Attribute.Remove('systemLanguage') + $this.SVGAttribute.Remove('systemLanguage') +} else { + $this.Attribute['systemLanguage'] = $joinedArgs + $this.SVGAttribute['systemLanguage'] = $joinedArgs +} diff --git a/Types/Turtle/set_MarkerEnd.ps1 b/Types/Turtle/set_MarkerEnd.ps1 new file mode 100644 index 0000000..cb64bcc --- /dev/null +++ b/Types/Turtle/set_MarkerEnd.ps1 @@ -0,0 +1,39 @@ +<# +.SYNOPSIS + Sets the end marker +.DESCRIPTION + Sets the end marker used on the line drawn by the turtle. + + If this is set to a string without spaces, it will be be treated as an identifier. +.EXAMPLE + turtle viewbox 200 teleport 0 100 forward 100 markerEnd ( + turtle viewbox 10 rotate -90 polygon 10 3 # fill context-fill stroke context-stroke + ) fill '#4488ff' stroke '#224488' strokewidth '3%' save ./marker.svg +.EXAMPLE + turtle viewbox 200 teleport 0 100 forward 100 markerEnd ( + turtle viewbox 20 rotate -90 polygon 10 3 morph @( + turtle rotate -90 polygon 20 3 + turtle rotate -90 polygon 20 3 + ) + ) strokewidth '3%' save ./marker.svg +#> +param($Value) + +$attributeValue = + if ($value -is [string]) { + if ($value -notmatch '\s' -and $value -notmatch '^url') { + $Value = "url(`"$Value`")" + } else { + $value + } + } + else { + if ($value.pstypenames -contains 'Turtle') { + $Value.id += "-end" + $this.Defines+=$Value.Marker.OuterXml + "url(#$($value.id)-marker)" + } + } + +$this.PathAttribute['marker-end'] = $attributeValue + diff --git a/Types/Turtle/set_MarkerMiddle.ps1 b/Types/Turtle/set_MarkerMiddle.ps1 new file mode 100644 index 0000000..8c6b617 --- /dev/null +++ b/Types/Turtle/set_MarkerMiddle.ps1 @@ -0,0 +1,34 @@ +<# +.SYNOPSIS + Sets the middle marker +.DESCRIPTION + Sets the middle marker used on the line drawn by the turtle. + + If this is set to a string without spaces, it will be be treated as an identifier. +.EXAMPLE + turtle viewbox 200 start 10 200 rotate -60 @( + 'forward',42,'rotate',30,'forward',42,'rotate',-30 * 4 + ) markerMiddle ( + turtle circle 10 fill red + ) strokewidth '3%' save ./marker.svg +#> +param($Value) + +$attributeValue = + if ($value -is [string]) { + if ($value -notmatch '\s' -and $value -notmatch '^url') { + $Value = "url(`"$Value`")" + } else { + $value + } + } + else { + if ($value.pstypenames -contains 'Turtle') { + $Value.id += "-mid" + $this.Defines+=$Value.Marker.OuterXml + "url(#$($value.id)-marker)" + } + } + +$this.PathAttribute['marker-mid'] = $attributeValue + diff --git a/Types/Turtle/set_MarkerStart.ps1 b/Types/Turtle/set_MarkerStart.ps1 new file mode 100644 index 0000000..456aae0 --- /dev/null +++ b/Types/Turtle/set_MarkerStart.ps1 @@ -0,0 +1,39 @@ +<# +.SYNOPSIS + Sets the start marker +.DESCRIPTION + Sets the start marker used on the line drawn by the turtle. + + If this is set to a string without spaces, it will be be treated as an identifier. +.EXAMPLE + turtle viewbox 200 start 50 100 rotate 45 forward 100 markerStart ( + turtle rotate -90 polygon 10 3 fill context-fill stroke context-stroke + ) fill '#4488ff' stroke '#224488' strokewidth '3%' save ./marker.svg +.EXAMPLE + turtle viewbox 200 teleport 50 100 forward 100 markerEnd ( + turtle viewbox 20 rotate -90 polygon 10 3 morph @( + turtle rotate -90 polygon 20 3 + turtle rotate -90 polygon 20 3 + ) + ) strokewidth '3%' save ./marker.svg +#> +param($Value) + +$attributeValue = + if ($value -is [string]) { + if ($value -notmatch '\s' -and $value -notmatch '^url') { + $Value = "url(`"$Value`")" + } else { + $value + } + } + else { + if ($value.pstypenames -contains 'Turtle') { + $Value.id += "-start" + $this.Defines+=$Value.Marker.OuterXml + "url(#$($value.id)-marker)" + } + } + +$this.PathAttribute['marker-start'] = $attributeValue + diff --git a/Types/Turtle/set_Opacity.ps1 b/Types/Turtle/set_Opacity.ps1 index dc17539..f39ce0f 100644 --- a/Types/Turtle/set_Opacity.ps1 +++ b/Types/Turtle/set_Opacity.ps1 @@ -1,14 +1,13 @@ <# .SYNOPSIS - Sets the opacity + Sets a Turtle's opacity .DESCRIPTION - Sets the opacity of the path + Sets the opacity of a Turtle .EXAMPLE - turtle forward 100 opacity 0.5 save ./dimLine.svg + turtle opacity .5 opacity #> param( [double] -$Opacity = 'nonzero' +$Opacity ) - -$this.PathAttribute = [Ordered]@{'opacity' = $Opacity} +$this | Add-Member NoteProperty '.Opacity' $Opacity -Force diff --git a/Types/Turtle/set_PathAnimation.ps1 b/Types/Turtle/set_PathAnimation.ps1 index 034d00a..97f96cf 100644 --- a/Types/Turtle/set_PathAnimation.ps1 +++ b/Types/Turtle/set_PathAnimation.ps1 @@ -56,4 +56,11 @@ $newAnimation = @(foreach ($animation in $PathAnimation) { } }) +$pathAnimation = $this.PathAnimation +if ($pathAnimation) { + $newAnimation = @($pathAnimation) + $newAnimation +} $this | Add-Member -MemberType NoteProperty -Force -Name '.PathAnimation' -Value $newAnimation + + + diff --git a/Types/Turtle/set_PatternAnimation.ps1 b/Types/Turtle/set_PatternAnimation.ps1 index 21d3420..e316483 100644 --- a/Types/Turtle/set_PatternAnimation.ps1 +++ b/Types/Turtle/set_PatternAnimation.ps1 @@ -1,3 +1,79 @@ +<# +.SYNOPSIS + Sets a pattern animation +.DESCRIPTION + Sets one or more animations to apply to our Turtle's pattern. + + These animations will transform the pattern, allowing for endless variation. +.EXAMPLE + turtle flower PatternAnimation ([Ordered]@{ + type = 'translate' + values = "0 0","0 420", "0 0" + repeatCount = 'indefinite' + # dur = "11s" # The duration will default to the Turtle's duration + additive = 'sum' + }) save ./FlowerPatternAnimation.svg +.EXAMPLE + # We can have multiple pattern animations, and need to use `additive=sum` to ensure they do not conflict + turtle SierpinskiTriangle duration '00:00:42' PatternAnimation ([Ordered]@{ + type = 'rotate' + values = "0","360" + repeatCount = 'indefinite' + additive = 'sum' + }, [Ordered]@{ + type = 'scale' + values = "1",".25", "1" + repeatCount = 'indefinite' + additive = 'sum' + }) save ./SierpinskiTrianglePatternAnimation.svg +.EXAMPLE + # Pattern Transforms set a starting state for animations + turtle SierpinskiTriangle duration '00:00:42' PatternTransform @{ + scale = .25 + } PatternAnimation ([Ordered]@{ + type = 'rotate' + values = "0","360" + repeatCount = 'indefinite' + additive = 'sum' + }, [Ordered]@{ + type = 'scale' + values = "1",".25", "1" + repeatCount = 'indefinite' + additive = 'sum' + }) save ./SierpinskiTrianglePatternAnimationSmaller.svg +.EXAMPLE + # We can use primes as pattern transform durations to ensure animations rarely overlap + # This example uses four primes under 100: + # It will repeat in `23 * 41 * 61 * 83` seconds + # (or just over 55 days) + turtle SierpinskiTriangle duration '00:00:42' PatternTransform @{ + scale = .25 + } PatternAnimation ([Ordered]@{ + type = 'rotate' + values = "0","360" + repeatCount = 'indefinite' + dur = '83s' + additive = 'sum' + }, [Ordered]@{ + type = 'scale' + values = "1",".25", "1" + repeatCount = 'indefinite' + dur = '23s' + additive = 'sum' + }, [Ordered]@{ + type = 'skewX' + values = "0","45", "0" + repeatCount = 'indefinite' + additive = 'sum' + dur = '41s' + }, [Ordered]@{ + type = 'skewX' + values = "0","-45", "0" + repeatCount = 'indefinite' + additive = 'sum' + dur = '61s' + }) save ./SierpinskiTrianglePatternAnimationEndless.svg +#> param( [PSObject] $PatternAnimation @@ -15,6 +91,9 @@ $newAnimation = @(foreach ($animation in $PatternAnimation) { if ($animationCopy.values -is [object[]]) { $animationCopy['values'] = $animationCopy['values'] -join ';' } + if (-not $animationCopy['dur']) { + $animationCopy['dur'] = "$($this.Duration.TotalSeconds)s" + } " param([double[]]$xy) +# break apart the components $x, $y = $xy +# and add a position if we do not have one. if (-not $this.'.Position') { - $this | Add-Member -MemberType NoteProperty -Force -Name '.Position' -Value ([pscustomobject]@{ X = 0; Y = 0 }) + $this | Add-Member -MemberType NoteProperty -Force -Name '.Position' -Value ([Numerics.Vector2]@{ X = 0; Y = 0 }) } + +# Modify the position without creating a new object $this.'.Position'.X += $x $this.'.Position'.Y += $y +# And readback our new position $posX, $posY = $this.'.Position'.X, $this.'.Position'.Y +# If we have no .Minimum if (-not $this.'.Minimum') { - $this | Add-Member -MemberType NoteProperty -Force -Name '.Minimum' -Value ([pscustomobject]@{ X = 0; Y = 0 }) -} -if (-not $this.'.Maximum') { - $this | Add-Member -MemberType NoteProperty -Force -Name '.Maximum' -Value ([pscustomobject]@{ X = 0; Y = 0 }) + # create one. + $this | Add-Member -MemberType NoteProperty -Force -Name '.Minimum' -Value ([Numerics.Vector2]@{ X = 0; Y = 0 }) } + +# Then check if we need to update our minimum point. if ($posX -lt $this.'.Minimum'.X) { $this.'.Minimum'.X = $posX } if ($posY -lt $this.'.Minimum'.Y) { $this.'.Minimum'.Y = $posY } + +# If we have no .Maximum +if (-not $this.'.Maximum') { + # create one. + $this | Add-Member -MemberType NoteProperty -Force -Name '.Maximum' -Value ([Numerics.Vector2]@{ X = 0; Y = 0 }) +} + +# Then update our maximum point if ($posX -gt $this.'.Maximum'.X) { $this.'.Maximum'.X = $posX } diff --git a/Types/Turtle/set_Precision.ps1 b/Types/Turtle/set_Precision.ps1 new file mode 100644 index 0000000..6adbdcf --- /dev/null +++ b/Types/Turtle/set_Precision.ps1 @@ -0,0 +1,22 @@ +<# +.SYNOPSIS + Sets the Turtle's Precision +.DESCRIPTION + Sets the level of precision a turtle should use for rounding. + + This is the number of digits a value will be rounded to. + + Lower precision will result in smaller filesizes, and a much better chance of stepwise animations working properly. + + Higher precision will result in large filesizes and will occassionally cause stepwise animations to get stuck. +#> +param( +# The number of decimal places used in rounding. +[ValidateRange(1,28)] +[int] +$Precision = 6 +) + +$this | Add-Member NoteProperty '.Precision' $Precision -Force + + diff --git a/Types/Turtle/set_Start.ps1 b/Types/Turtle/set_Start.ps1 new file mode 100644 index 0000000..2040637 --- /dev/null +++ b/Types/Turtle/set_Start.ps1 @@ -0,0 +1,31 @@ +<# +.SYNOPSIS + Sets the Start Vector for a Turtle +.DESCRIPTION + Sets the starting vector for a Turtle. + + This avoids an automatic calculation of a starting position +.EXAMPLE + turtle width 300 height 300 start 50 square 200 +#> +param( +[PSObject] +$Value +) + + +$aNewStart = + if ($value -is [object[]] -and $value -as [float[]]) { + [Numerics.Vector2]::new($value -as [float[]]) + } elseif ($value.GetType -and $value.GetType().IsPrimitive) { + [Numerics.Vector2]::new($value,$value) + } elseif ($value.X -and $value.Y) { + [Numerics.Vector2]::new($value.X,$value.Y) + } + +if ($aNewStart) { + $this | Add-Member NoteProperty '.Start' $aNewStart -Force +} + + + diff --git a/Types/Turtle/set_Stroke.ps1 b/Types/Turtle/set_Stroke.ps1 index 7a6b9d9..c09cf74 100644 --- a/Types/Turtle/set_Stroke.ps1 +++ b/Types/Turtle/set_Stroke.ps1 @@ -1,3 +1,109 @@ -param([string]$value) +<# +.SYNOPSIS + Sets a Turtle's stroke color +.DESCRIPTION + Sets one or more colors used to stroke the Turtle. -$this | Add-Member -MemberType NoteProperty -Force -Name '.Stroke' -Value $value \ No newline at end of file + By default, this is transparent. + + If more than one value is provided, the stroke will be a gradient. +.EXAMPLE + # Draw a blue square + turtle square 42 stroke blue +.EXAMPLE + # Draw a PowerShell blue square + turtle square 42 stroke '#4488ff' +.EXAMPLE + # Draw a red, green, blue gradient + turtle square 42 stroke red green blue show +.EXAMPLE + # Draw a red, green, blue linear gradient + turtle square 42 stroke red green blue linear show +.EXAMPLE + turtle flower stroke red green blue strokerule evenodd show +#> +param( +[PSObject[]] +$stroke = 'transparent' +) + +# If we have no stroke information, return +if (-not $stroke) { return } + +# If the stroke count is greater than one, try to make a graidnet +if ($stroke.Count -gt 1) { + + # Default to a radial gradient + $gradientTypeHint = 'radial' + # and create a collection for attributes + $gradientAttributes = [Ordered]@{ + # default our identifier to the current id plus `stroke-gradient` + # (so we could have multiple gradients without a collision) + id="$($this.id)-stroke-gradient" + } + + $stroke = @(foreach ($color in $stroke) { + # If the value matches `linear` or `radial` + if ($color -match '^(linear|radial)') { + # take the hint and make it the right type of gradient. + $gradientTypeHint = ($color -replace 'gradient').ToLower() + } + # If the color was `pad`, `reflect`, or `repeat` + elseif ($strokeColor -in 'pad', 'reflect', 'repeat') { + # take the hint and set the spreadMethod + $gradientAttributes['spreadMethod'] = $color + } + # If the stroke is a dictionary + elseif ($color -is [Collections.IDictionary]) { + # propagate the values into attributes. + foreach ($gradientAttributeKey in $color.Keys) { + $gradientAttributes[$gradientAttributeKey] = $color[$gradientAttributeKey] + } + } + # Otherwise output the color + else { + $color + } + }) + + # If we have no stroke colors after filtering, return + if (-not $stroke) { return } + + # If our count is one + if ($stroke.Count -eq 1) { + # it's not really going to be a gradient, so just use the one color. + $this | Add-Member -MemberType NoteProperty -Name '.Stroke' -Value $stroke -Force + return + } + + # Now we have at least two colors we want to be a gradient + # We need to make sure the offset starts at 0% an ends at 100% + # and so we actually need to divide by one less than our stroke color, so we end at 100%. + $offsetStep = 1 / ($stroke.Count - 1) + $Gradient = @( + # Construct our gradient element. + "<${gradientTypeHint}Gradient$( + # propagate our attributes + @(foreach ($gradientAttributeKey in $gradientAttributes.Keys) { + " $gradientAttributeKey='$($gradientAttributes[$gradientAttributeKey])'" + }) -join '' + )>" + @( + # and put in our stop colors + for ($strokeNumber = 0; $strokeNumber -lt $stroke.Count; $strokeNumber++) { + "" + } + ) + "" + ) -join [Environment]::NewLine + + # add this gradient to our defines + $this.Defines += $Gradient + # and set stroke to this gradient. + $stroke = "url(`"#$($gradientAttributes.id)`")" +} +if (-not $this.'.stroke') { + $this | Add-Member -MemberType NoteProperty -Name '.Stroke' -Value $stroke -Force +} else { + $this.'.stroke' = $stroke +} \ No newline at end of file diff --git a/Types/Turtle/set_Style.ps1 b/Types/Turtle/set_Style.ps1 new file mode 100644 index 0000000..a2de0ed --- /dev/null +++ b/Types/Turtle/set_Style.ps1 @@ -0,0 +1,82 @@ +<# +.SYNOPSIS + Sets a Turtle's Style +.DESCRIPTION + Sets any CSS styles associated with the Turtle. + + These styles will be declared in a `" #endregion Grid Styles +$exampleCount = @($CommandHelp.examples.example).Length +$progress = [Ordered]@{id=Get-Random} +$progress.Activity = "$($command) examples" # Create a grid for examples "
" $exampleNumber = 0 # Walk over each example -foreach ($example in $CommandHelp.examples.example) { +foreach ($example in $CommandHelp.examples.example) { $exampleNumber++ + # Combine the code and remarks $exampleLines = @( @@ -107,6 +157,11 @@ foreach ($example in $CommandHelp.examples.example) { # Anything until the first non-comment line is a markdown predicate to the example $nonCommentLine = $false $markdownLines = @() + + $progress.PercentComplete = $exampleNumber * 100 / $exampleCount + $progress.Activity = "$($command) examples $exampleNumber" + $progress.Status = "$($exampleLines[0])" + Write-Progress @progress # Go thru each line in the example as part of a loop $codeBlock = @(foreach ($exampleLine in $exampleLines) { @@ -129,7 +184,7 @@ foreach ($example in $CommandHelp.examples.example) { (ConvertFrom-Markdown -InputObject $Markdown).Html } # followed by our sample code - "
" + "
" "
"
                 "" + 
                     [Web.HttpUtility]::HtmlEncode($codeBlock) + 
@@ -142,8 +197,7 @@ foreach ($example in $CommandHelp.examples.example) {
             "
" "
" continue - } - + } # Otherwise, try to make our example a script block $exampleCode = try { @@ -153,8 +207,14 @@ foreach ($example in $CommandHelp.examples.example) { continue } + if (-not $global:ExampleOutputCache) { + $global:ExampleOutputCache = [Ordered]@{} + } + if (-not $global:ExampleOutputCache[$codeBlock]) { + $global:ExampleOutputCache[$codeBlock] = @(. $exampleCode) + } # then run it and capture the output - $exampleOutputs = @(. $exampleCode) + $exampleOutputs = $global:ExampleOutputCache[$codeBlock] # Keep track of our example output count $exampleOutputNumber = 0 @@ -195,4 +255,20 @@ foreach ($example in $CommandHelp.examples.example) { "
" "
" } -"
" \ No newline at end of file +"
" + +$progress.Remove('PercentComplete') +$progress['Completed'] = $true +Write-Progress @progress + +@" + +"@ \ No newline at end of file diff --git a/psturtle.com/_includes/SelectPalette.ps1 b/psturtle.com/_includes/SelectPalette.ps1 index 9e7ac58..4aec382 100644 --- a/psturtle.com/_includes/SelectPalette.ps1 +++ b/psturtle.com/_includes/SelectPalette.ps1 @@ -18,7 +18,19 @@ $SelectPaletteId = 'SelectPalette', # The identifier for the stylesheet. By default, palette. [string] -$PaletteId = 'palette' +$PaletteId = 'palette', + +[string] +$DefaultPalette = $( + if ($page.Palette) { + $page.Palette + } elseif ($site.Palette) { + $site.Palette + } + else { + '' + } +) ) @@ -43,7 +55,8 @@ $( $script:PaletteList = Invoke-RestMethod $PaletteListSource } foreach ($paletteName in $script:PaletteList) { - "" + $selectedPalette = if ($defaultPalette -and $defaultPalette -eq $paletteName) { " selected='true'"} else { '' } + "" } ) diff --git a/psturtle.com/build.ps1 b/psturtle.com/build.ps1 index 8804abb..79c6fa1 100644 --- a/psturtle.com/build.ps1 +++ b/psturtle.com/build.ps1 @@ -28,15 +28,49 @@ $Site.Files = else { Get-ChildItem -Recurse -File } $Site.PSScriptRoot = "$PSScriptRoot" +foreach ($underbarDirectory in Get-ChildItem -Path $site.PSScriptRoot -Filter _* -Directory) { + $Site[$underbarDirectory.Name -replace '^_'] = $Site[$underbarDirectory.Name] = [Ordered]@{} + foreach ($underbarFile in Get-ChildItem -Path $underbarDirectory -Recurse) { + $relativePath = $underbarFile.FullName.Substring($underbarDirectory.FullName.Length + 1) + $pointer = $site + $hierarchy = @($relativePath -split '[\\/]') + for ($index = 0; $index -lt ($hierarchy.Length - 1); $index++) { + $subdirectory = $hierarchy[$index] -replace '_' + if (-not $pointer[$subdirectory]) { + $pointer[$subdirectory] = [Ordered]@{} + } + $pointer = $pointer[$subdirectory] + } + + $propertyName = $hierarchy[-1] -replace '_' + $getFile = @{LiteralPath=$underbarFile.FullName} + $fileData = + switch -regex ($underbarFile.Extension) { + '\.ps1$' { $ExecutionContext.SessionState.InvokeCommand.GetCommand($underbarFile.FullName, 'ExternalScript') } + '\.(css|html|txt)$' { Get-Content @getFile } + '\.json$' { Get-Content @getFile | ConvertFrom-Json } + '\.jsonl$' { Get-Content @getFile | ConvertFrom-Json } + '\.psd1$' { Get-Content @getFile -Raw | ConvertFrom-StringData } + '\.(?>ps1xml|xml|svg)$' { (Get-Content @getFile -Raw) -as [xml] } + '\.(?>yaml|toml)$' { Get-Content @getFile -Raw } + '\.csv$' { Import-Csv @getFile } + '\.tsv$' { Import-Csv @getFile -Delimiter "`t" } + } + if (-not $fileData) { continue } + $pointer[$relativePath -replace '\.ps1$'] = $fileData + } +} #region Common Functions and Filters +# Any functions or filter file at the site root should be loaded. $functionFileNames = 'functions', 'function', 'filters', 'filter' $functionPattern = "(?>$($functionFileNames -join '|'))\.ps1$" $functionFiles = Get-ChildItem -Path $Site.PSScriptRoot | Where-Object Name -Match $functionPattern foreach ($file in $functionFiles) { - # If we have a file with the name function or functions, we'll use it to set the site configuration. + # If we have a file with the name function or functions, + # we'll dot source it now so we can use the functions in the config . $file.FullName } #endregion Common Functions and Filters @@ -44,8 +78,9 @@ foreach ($file in $functionFiles) { # Set an alias to buildPage.ps1 Set-Alias BuildPage ./buildPage.ps1 -# If we have an event path, -$gitHubEvent = +# If we have a github event, +# save it to a variable and to the `$site` +$site.GitHubEvent = $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { # all we need to do to serve it is copy it. Copy-Item $env:GITHUB_EVENT_PATH .\gitHubEvent.json @@ -59,12 +94,15 @@ if (Test-Path 'CNAME') { $Site.CNAME = $CNAME = (Get-Content -Path 'CNAME' -Raw).Trim() $Site.RootUrl = "https://$CNAME/" } elseif ( + # otherwise, if we are in a directory that could be a domain ($site.PSScriptRoot | Split-Path -Leaf) -like '*.*' ) { + # assume it _is_ the domain. $site.CNAME = $CNAME = ($site.PSScriptRoot | Split-Path -Leaf) $site.RootUrl = "https://$CNAME/" } +#region config # If we have a config.json file, it can be used to set the site configuration. if (Test-Path 'config.json') { $siteConfig = Get-Content -Path 'config.json' -Raw | ConvertFrom-Json @@ -98,6 +136,7 @@ if (Test-Path 'config.ps1') { # run it, and let it configure anything it chooses to. . $configScript } +#endregion config # Start the clock $site['LastBuildTime'] = $lastBuildTime = [DateTime]::Now @@ -105,10 +144,12 @@ $site['LastBuildTime'] = $lastBuildTime = [DateTime]::Now # Start the clock on the build process $buildStart = [DateTime]::Now +Write-Host "Started Building Pages @ $buildStart" # pipe every file we find to buildFile $Site.Files | . buildPage # and stop the clock $buildEnd = [DateTime]::Now +Write-Host "Finished Building Pages @ $buildEnd ($($buildEnd - $buildStart))" #endregion Build Files diff --git a/psturtle.com/buildPage.ps1 b/psturtle.com/buildPage.ps1 index 8d3c3d4..e45126b 100644 --- a/psturtle.com/buildPage.ps1 +++ b/psturtle.com/buildPage.ps1 @@ -28,7 +28,12 @@ if (-not $site.PagesByUrl) { } $pagesByUrl = $site.PagesByUrl -:nextFile foreach ($file in $allFiles) { +$fileQueue = [Collections.Queue]::new() +foreach ($file in $allFiles) { $fileQueue.Enqueue($file) } + +:nextFile while ($fileQueue.Count) { + $file = $fileQueue.Dequeue() + if ($file.FullName -match '/_[^\.]') { continue } if ($Site -and $Site.Exclude) { $included = $false :exclude do { @@ -50,6 +55,7 @@ $pagesByUrl = $site.PagesByUrl } $included = $true } until ($included) + if (-not $included) { continue } } $fileRoot = $file.Directory.FullName Push-Location $fileRoot @@ -83,15 +89,15 @@ $pagesByUrl = $site.PagesByUrl $gitDates = try { # we can use `git log --follow --format=%ci` to get the dates in order - (& $gitCommand log --follow --format=%ci --date default $file.FullName *>&1) -as [datetime[]] + (& $gitCommand log --follow --format=%ci --date default $file.FullName *>&1) -as [datetime] } catch { $null } # Because the file might not be in git, we want to always set the `$LASTEXITCODE` to 0 $LASTEXITCODE = 0 - # Set the date to the last date we find. + # Set the date to the first date we find. if ($gitDates) { - $page.Date = $gitDates[-1] + $page.Date = $gitDates[0] } } } @@ -186,11 +192,37 @@ $pagesByUrl = $site.PagesByUrl } } + default { + Pop-Location + continue nextFile + } } #endregion Get Page Content + + # We want to filter out files from the rest of output + $outputFiles = @() + $otherOutput = @(foreach ($out in $output) { + if ($out -is [IO.FileInfo]) { + $outputFiles += $out + } else { + $out + } + }) + + # If there were any files output + if ($outputFiles) { + # queue them + foreach ($outputFile in $outputFiles) { + $fileQueue.Enqueue($outputFile) + $totalFiles++ + } + } + + # Set out output to any non-file output. + $output = $otherOutput # If we don't have output, - if ($null -eq $Output) { + if ($null -eq $OtherOutput) { Pop-Location continue nextFile # continue to the next file. } @@ -330,6 +362,7 @@ $pagesByUrl = $site.PagesByUrl if ($?) { $page.OutputFile = Get-Item -Path $outFile $page.OutputFile + Pop-Location continue nextFile } } @@ -338,6 +371,7 @@ $pagesByUrl = $site.PagesByUrl if ($?) { $page.OutputFile = Get-Item -Path $outFile $page.OutputFile + Pop-Location continue nextFile } } @@ -346,6 +380,7 @@ $pagesByUrl = $site.PagesByUrl if ($?) { $page.OutputFile = Get-Item -Path $outFile $page.OutputFile + Pop-Location continue nextFile } } @@ -379,21 +414,26 @@ $pagesByUrl = $site.PagesByUrl # just output them directly. $outputFiles } else { - # otherwise, we'll save output to a file. - - # If the file does not exists - if (-not (Test-Path -Path $outFile)) { - # create an empty file. - $null = New-Item -Path $outFile -ItemType File -Force - } - - $output > $outFile - # and if that worked, - if ($?) { - # output the file. - $page.OutputFile = Get-Item -Path $outFile - $page.OutputFile - } + # otherwise, we'll save output to a file (assuming we have output). + if ("$output") { + # If the file does not exists + if (-not (Test-Path -Path $outFile)) { + # create an empty file. + $null = New-Item -Path $outFile -ItemType File -Force + } + if ($outFile -like '*.svg') { + $null = $null + } + $output > $outFile + # and if that worked, + if ($?) { + # output the file. + $page.OutputFile = Get-Item -Path $outFile + $page.OutputFile + } + } else { + $null = $null + } } #endregion Output diff --git a/psturtle.com/config.ps1 b/psturtle.com/config.ps1 index 45d3d26..5ec7a77 100644 --- a/psturtle.com/config.ps1 +++ b/psturtle.com/config.ps1 @@ -33,6 +33,14 @@ if ($psScriptRoot -and -not $site.PSScriptRoot) { } #endregion Core +#region Exclude +if ($site.Exclude -isnot [Collections.IDictionary] ) { + $site.Exclude = [Ordered]@{} +} +$site.Exclude.Wildcards = '*.functions.ps1', '*.function.ps1', '*.filters.ps1`', '*.filter.ps1', '*.turtle.ps1' +$site.Exclude.Patterns = 'build\\.[^\\.]+\\.ps1$' +#endregion Exclude + #region _ if ($site.PSScriptRoot) { $underbarItems = @@ -120,6 +128,10 @@ $Site.Logo = { turtle SierpinskiTriangle 42 4 } + + { + turtle rotate -90 TurtleMonotile 42 + } ) $site.Logo = . ($site.Logo | Get-Random) @@ -133,24 +145,25 @@ $site.Taskbar = [Ordered]@{ 'BlueSky' = 'https://bsky.app/profile/psturtle.com' 'GitHub' = 'https://github.com/PowerShellWeb/Turtle' 'RSS' = 'https://psturtle.com/RSS/index.rss' - 'Help' = @( -if ($site.Module) { - "

Installing

" - "
Install-Module $($site.Module)
" - "

Updating

" - "
Install-Module $($site.Module) -Force
" - "

Importing

" - "
Import-Module $($site.Module)
" - "

Basics

" - "
turtle polygon 42 6
" - "$(turtle polygon 42 6)" - "

More Examples

" + 'Help' = '/Commands/Get-Turtle' } - ) -join [Environment]::NewLine - 'Settings' = @( - . $site.includes.SelectPalette - . $site.includes.GetRandomPalette - ) + +$env:TURTLE_BOT = $true + +$Site.Palette = "Andromeda" + +$site.Footer = @( + . $site.includes.SelectPalette + . $site.includes.GetRandomPalette +) + +$Site.FixFooter = [Ordered]@{ + 'width' = '90vw' + 'margin-left' = '5vw' + 'margin-right' = '5vw' + 'height' = '5vh' + 'top' = '92.5vh' + 'z-index' = 100 } <#$site.HeaderMenu = [Ordered]@{ @@ -317,6 +330,55 @@ $sitebackgrounds = @( { turtle FlowerPetal } + + { + turtle ( + @( + 'circlearc', 21, -60, + 'rotate',60, + 'forward',42 * 6 + + 'rotate', 30 + ) * 12 + ) + } + + { + turtle StarFlower + } + + { + $spokes = Get-Random -Min 3 -Max 13 + $rings = Get-Random -Min 3 -Max (13 * 3) + turtle web 42 $spokes $rings morph @( + turtle web 42 $spokes $rings + turtle rotate ( + Get-Random -Max 360 + ) web 42 $spokes $rings + turtle web 42 $spokes $rings + ) + } + + { + Turtle @( + 'CircleArc',42, -90, + 'Rotate', 90 * 4 + ) + } + + { + Turtle @( + 'CircleArc',42, -60, + 'Rotate', 60 * 6 + ) + } + + { + Turtle @( + 'CircleArc',42, -45, + 'Rotate', 45 * 8 + ) + } ) $siteBackground = $sitebackgrounds | Get-Random diff --git a/psturtle.com/layout.ps1 b/psturtle.com/layout.ps1 index ade3c55..7e617b9 100644 --- a/psturtle.com/layout.ps1 +++ b/psturtle.com/layout.ps1 @@ -16,7 +16,7 @@ param( [string] $PaletteName = $( if ($Site -and $Site['PaletteName']) { $Site['PaletteName'] } - else { 'Konsolas' } + else { 'Konsolas' } ), # The Google Font name @@ -61,14 +61,14 @@ param( ), # The footer menu. - [Collections.IDictionary] - $FooterMenu = $( - if ($page -and $page.'FooterMenu' -is [Collections.IDictionary]) { - $page.'FooterMenu' - } elseif ($Site -and $site.'FooterMenu' -is [Collections.IDictionary]) { - $site.'FooterMenu' + [PSObject[]] + $Footer = @( + if ($page.Footer) { + $page.Footer + } elseif ($Site -and $site.Footer) { + $site.Footer } else { - [Ordered]@{} + '' } ) ) @@ -145,32 +145,18 @@ $( if ($HeaderMenu) { # If the device is in landscape mode, use larger padding and gaps "@media (orientation: landscape) {" - ".header-menu { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 0.5em }" + ".header-menu { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 0.5em; position: sticky; top: 0px}" ".header-menu-item { text-align: center; padding: 0.5em; }" "}" # If the device is in portrait mode, use smaller padding and gaps "@media (orientation: portrait) {" - ".header-menu { display: grid; grid-template-columns: repeat(auto-fit, minmax(66px, 1fr)); gap: 0.25em }" + ".header-menu { display: grid; grid-template-columns: repeat(auto-fit, minmax(66px, 1fr)); gap: 0.25em; position: sticky; top: 0px }" ".header-menu-item { text-align: center; padding: 0.25em; }" "}" } ) -$( - if ($FooterMenu) { - "@media (orientation: landscape) {" - ".footer-menu { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 0.5em }" - ".footer-menu-item { text-align: center; padding: 0.5em; }" - "}" - - "@media (orientation: portrait) {" - ".footer-menu { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 0.25em }" - ".footer-menu-item { text-align: center; padding: 0.25em; }" - "}" - } -) - .logo { display: inline; height: 4.2rem; @@ -215,9 +201,8 @@ a:hover, a:focus { }) } -.taskbar { - float: right; - position: absolute; +.taskbar { + position: sticky; top: 0; right: 0; z-index: 10; display: flex; flex-direction: row-reverse; align-content: right; align-items: flex-start; @@ -315,10 +300,7 @@ $headerElements = @( # Now we declare the body elements $bodyElements = @( - # * The background layers - - - + # * The background layers "" if ($page.Background -is [xml]) { $page.Background.OuterXml @@ -326,7 +308,7 @@ $bodyElements = @( elseif ($site.Background -is [xml]) { $site.Background.OuterXml } - "" + "" "" if ($taskbar) { # * Our taskbar @@ -346,15 +328,16 @@ $bodyElements = @( } } else { $taskbarItem.Key } - if ($taskbarItem.Value -match '[<>]') { + $taskBarContent = $taskbarItem.Value + if ($taskBarContent -match '[<>]') { "
" "" $itemIconAndOrName - "" + "" $taskbarItem.Value "
" } else { - "" + "" $itemIconAndOrName "" } @@ -406,27 +389,26 @@ $bodyElements = @( "" # * The main content + "
$outputHtml
" - # * The footer - "