diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..c53b233
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,496 @@
+
+name: Build Module
+on:
+ push:
+ pull_request:
+ workflow_dispatch:
+jobs:
+ TestPowerShellOnLinux:
+ runs-on: ubuntu-latest
+ steps:
+ - name: InstallPester
+ id: InstallPester
+ shell: pwsh
+ run: |
+ $Parameters = @{}
+ $Parameters.PesterMaxVersion = ${env:PesterMaxVersion}
+ foreach ($k in @($parameters.Keys)) {
+ if ([String]::IsNullOrEmpty($parameters[$k])) {
+ $parameters.Remove($k)
+ }
+ }
+ Write-Host "::debug:: InstallPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')"
+ & {<#
+ .Synopsis
+ Installs Pester
+ .Description
+ Installs Pester
+ #>
+ param(
+ # The maximum pester version. Defaults to 4.99.99.
+ [string]
+ $PesterMaxVersion = '4.99.99'
+ )
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+ Install-Module -Name Pester -Repository PSGallery -Force -Scope CurrentUser -MaximumVersion $PesterMaxVersion -SkipPublisherCheck -AllowClobber
+ Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion} @Parameters
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: RunPester
+ id: RunPester
+ shell: pwsh
+ run: |
+ $Parameters = @{}
+ $Parameters.ModulePath = ${env:ModulePath}
+ $Parameters.PesterMaxVersion = ${env:PesterMaxVersion}
+ $Parameters.NoCoverage = ${env:NoCoverage}
+ $Parameters.NoCoverage = $parameters.NoCoverage -match 'true';
+ foreach ($k in @($parameters.Keys)) {
+ if ([String]::IsNullOrEmpty($parameters[$k])) {
+ $parameters.Remove($k)
+ }
+ }
+ Write-Host "::debug:: RunPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')"
+ & {<#
+ .Synopsis
+ Runs Pester
+ .Description
+ Runs Pester tests after importing a PowerShell module
+ #>
+ param(
+ # The module path. If not provided, will default to the second half of the repository ID.
+ [string]
+ $ModulePath,
+ # The Pester max version. By default, this is pinned to 4.99.99.
+ [string]
+ $PesterMaxVersion = '4.99.99',
+
+ # If set, will not collect code coverage.
+ [switch]
+ $NoCoverage
+ )
+
+ $global:ErrorActionPreference = 'continue'
+ $global:ProgressPreference = 'silentlycontinue'
+
+ $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/"
+ if (-not $ModulePath) { $ModulePath = ".\$moduleName.psd1" }
+ $importedPester = Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion
+ $importedModule = Import-Module $ModulePath -Force -PassThru
+ $importedPester, $importedModule | Out-Host
+
+ $codeCoverageParameters = @{
+ CodeCoverage = "$($importedModule | Split-Path)\*-*.ps1"
+ CodeCoverageOutputFile = ".\$moduleName.Coverage.xml"
+ }
+
+ if ($NoCoverage) {
+ $codeCoverageParameters = @{}
+ }
+
+
+ $result =
+ Invoke-Pester -PassThru -Verbose -OutputFile ".\$moduleName.TestResults.xml" -OutputFormat NUnitXml @codeCoverageParameters
+
+ if ($result.FailedCount -gt 0) {
+ "::debug:: $($result.FailedCount) tests failed"
+ foreach ($r in $result.TestResult) {
+ if (-not $r.Passed) {
+ "::error::$($r.describe, $r.context, $r.name -join ' ') $($r.FailureMessage)"
+ }
+ }
+ throw "::error:: $($result.FailedCount) tests failed"
+ }
+ } @Parameters
+ - name: PublishTestResults
+ uses: actions/upload-artifact@main
+ with:
+ name: PesterResults
+ path: '**.TestResults.xml'
+ if: ${{always()}}
+ TagReleaseAndPublish:
+ runs-on: ubuntu-latest
+ if: ${{ success() }}
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v2
+ - name: TagModuleVersion
+ id: TagModuleVersion
+ shell: pwsh
+ run: |
+ $Parameters = @{}
+ $Parameters.ModulePath = ${env:ModulePath}
+ $Parameters.UserEmail = ${env:UserEmail}
+ $Parameters.UserName = ${env:UserName}
+ $Parameters.TagVersionFormat = ${env:TagVersionFormat}
+ $Parameters.TagAnnotationFormat = ${env:TagAnnotationFormat}
+ foreach ($k in @($parameters.Keys)) {
+ if ([String]::IsNullOrEmpty($parameters[$k])) {
+ $parameters.Remove($k)
+ }
+ }
+ Write-Host "::debug:: TagModuleVersion $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')"
+ & {param(
+ [string]
+ $ModulePath,
+
+ # The user email associated with a git commit.
+ [string]
+ $UserEmail,
+
+ # The user name associated with a git commit.
+ [string]
+ $UserName,
+
+ # The tag version format (default value: 'v$(imported.Version)')
+ # This can expand variables. $imported will contain the imported module.
+ [string]
+ $TagVersionFormat = 'v$($imported.Version)',
+
+ # The tag version format (default value: '$($imported.Name) $(imported.Version)')
+ # This can expand variables. $imported will contain the imported module.
+ [string]
+ $TagAnnotationFormat = '$($imported.Name) $($imported.Version)'
+ )
+
+
+ $gitHubEvent = if ($env:GITHUB_EVENT_PATH) {
+ [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json
+ } else { $null }
+
+
+ @"
+ ::group::GitHubEvent
+ $($gitHubEvent | ConvertTo-Json -Depth 100)
+ ::endgroup::
+ "@ | Out-Host
+
+ if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and
+ (-not $gitHubEvent.psobject.properties['inputs'])) {
+ "::warning::Pull Request has not merged, skipping Tagging" | Out-Host
+ return
+ }
+
+
+
+ $imported =
+ if (-not $ModulePath) {
+ $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/"
+ Import-Module ".\$moduleName.psd1" -Force -PassThru -Global
+ } else {
+ Import-Module $modulePath -Force -PassThru -Global
+ }
+
+ if (-not $imported) { return }
+
+ $targetVersion =$ExecutionContext.InvokeCommand.ExpandString($TagVersionFormat)
+ $existingTags = git tag --list
+
+ @"
+ Target Version: $targetVersion
+
+ Existing Tags:
+ $($existingTags -join [Environment]::NewLine)
+ "@ | Out-Host
+
+ $versionTagExists = $existingTags | Where-Object { $_ -match $targetVersion }
+
+ if ($versionTagExists) {
+ "::warning::Version $($versionTagExists)"
+ return
+ }
+
+ if (-not $UserName) { $UserName = $env:GITHUB_ACTOR }
+ if (-not $UserEmail) { $UserEmail = "$UserName@github.com" }
+ git config --global user.email $UserEmail
+ git config --global user.name $UserName
+
+ git tag -a $targetVersion -m $ExecutionContext.InvokeCommand.ExpandString($TagAnnotationFormat)
+ git push origin --tags
+
+ if ($env:GITHUB_ACTOR) {
+ exit 0
+ }} @Parameters
+ - name: ReleaseModule
+ id: ReleaseModule
+ shell: pwsh
+ run: |
+ $Parameters = @{}
+ $Parameters.ModulePath = ${env:ModulePath}
+ $Parameters.UserEmail = ${env:UserEmail}
+ $Parameters.UserName = ${env:UserName}
+ $Parameters.TagVersionFormat = ${env:TagVersionFormat}
+ $Parameters.ReleaseNameFormat = ${env:ReleaseNameFormat}
+ $Parameters.ReleaseAsset = ${env:ReleaseAsset}
+ $Parameters.ReleaseAsset = $parameters.ReleaseAsset -split ';' -replace '^[''"]' -replace '[''"]$'
+ foreach ($k in @($parameters.Keys)) {
+ if ([String]::IsNullOrEmpty($parameters[$k])) {
+ $parameters.Remove($k)
+ }
+ }
+ Write-Host "::debug:: ReleaseModule $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')"
+ & {param(
+ [string]
+ $ModulePath,
+
+ # The user email associated with a git commit.
+ [string]
+ $UserEmail,
+
+ # The user name associated with a git commit.
+ [string]
+ $UserName,
+
+ # The tag version format (default value: 'v$(imported.Version)')
+ # This can expand variables. $imported will contain the imported module.
+ [string]
+ $TagVersionFormat = 'v$($imported.Version)',
+
+ # The release name format (default value: '$($imported.Name) $($imported.Version)')
+ [string]
+ $ReleaseNameFormat = '$($imported.Name) $($imported.Version)',
+
+ # Any assets to attach to the release. Can be a wildcard or file name.
+ [string[]]
+ $ReleaseAsset
+ )
+
+
+ $gitHubEvent = if ($env:GITHUB_EVENT_PATH) {
+ [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json
+ } else { $null }
+
+
+ @"
+ ::group::GitHubEvent
+ $($gitHubEvent | ConvertTo-Json -Depth 100)
+ ::endgroup::
+ "@ | Out-Host
+
+ if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and
+ (-not $gitHubEvent.psobject.properties['inputs'])) {
+ "::warning::Pull Request has not merged, skipping GitHub release" | Out-Host
+ return
+ }
+
+
+
+ $imported =
+ if (-not $ModulePath) {
+ $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/"
+ Import-Module ".\$moduleName.psd1" -Force -PassThru -Global
+ } else {
+ Import-Module $modulePath -Force -PassThru -Global
+ }
+
+ if (-not $imported) { return }
+
+ $targetVersion =$ExecutionContext.InvokeCommand.ExpandString($TagVersionFormat)
+ $targetReleaseName = $targetVersion
+ $releasesURL = 'https://api.github.com/repos/${{github.repository}}/releases'
+ "Release URL: $releasesURL" | Out-Host
+ $listOfReleases = Invoke-RestMethod -Uri $releasesURL -Method Get -Headers @{
+ "Accept" = "application/vnd.github.v3+json"
+ "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}'
+ }
+
+ $releaseExists = $listOfReleases | Where-Object tag_name -eq $targetVersion
+
+ if ($releaseExists) {
+ "::warning::Release '$($releaseExists.Name )' Already Exists" | Out-Host
+ $releasedIt = $releaseExists
+ } else {
+ $releasedIt = Invoke-RestMethod -Uri $releasesURL -Method Post -Body (
+ [Ordered]@{
+ owner = '${{github.owner}}'
+ repo = '${{github.repository}}'
+ tag_name = $targetVersion
+ name = $ExecutionContext.InvokeCommand.ExpandString($ReleaseNameFormat)
+ body =
+ if ($env:RELEASENOTES) {
+ $env:RELEASENOTES
+ } elseif ($imported.PrivateData.PSData.ReleaseNotes) {
+ $imported.PrivateData.PSData.ReleaseNotes
+ } else {
+ "$($imported.Name) $targetVersion"
+ }
+ draft = if ($env:RELEASEISDRAFT) { [bool]::Parse($env:RELEASEISDRAFT) } else { $false }
+ prerelease = if ($env:PRERELEASE) { [bool]::Parse($env:PRERELEASE) } else { $false }
+ } | ConvertTo-Json
+ ) -Headers @{
+ "Accept" = "application/vnd.github.v3+json"
+ "Content-type" = "application/json"
+ "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}'
+ }
+ }
+
+
+
+
+
+ if (-not $releasedIt) {
+ throw "Release failed"
+ } else {
+ $releasedIt | Out-Host
+ }
+
+ $releaseUploadUrl = $releasedIt.upload_url -replace '\{.+$'
+
+ if ($ReleaseAsset) {
+ $fileList = Get-ChildItem -Recurse
+ $filesToRelease =
+ @(:nextFile foreach ($file in $fileList) {
+ foreach ($relAsset in $ReleaseAsset) {
+ if ($relAsset -match '[\*\?]') {
+ if ($file.Name -like $relAsset) {
+ $file; continue nextFile
+ }
+ } elseif ($file.Name -eq $relAsset -or $file.FullName -eq $relAsset) {
+ $file; continue nextFile
+ }
+ }
+ })
+
+ $releasedFiles = @{}
+ foreach ($file in $filesToRelease) {
+ if ($releasedFiles[$file.Name]) {
+ Write-Warning "Already attached file $($file.Name)"
+ continue
+ } else {
+ $fileBytes = [IO.File]::ReadAllBytes($file.FullName)
+ $releasedFiles[$file.Name] =
+ Invoke-RestMethod -Uri "${releaseUploadUrl}?name=$($file.Name)" -Headers @{
+ "Accept" = "application/vnd.github+json"
+ "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}'
+ } -Body $fileBytes -ContentType Application/octet-stream
+ $releasedFiles[$file.Name]
+ }
+ }
+
+ "Attached $($releasedFiles.Count) file(s) to release" | Out-Host
+ }
+
+
+
+ } @Parameters
+ - name: PublishPowerShellGallery
+ id: PublishPowerShellGallery
+ shell: pwsh
+ run: |
+ $Parameters = @{}
+ $Parameters.ModulePath = ${env:ModulePath}
+ $Parameters.Exclude = ${env:Exclude}
+ $Parameters.Exclude = $parameters.Exclude -split ';' -replace '^[''"]' -replace '[''"]$'
+ foreach ($k in @($parameters.Keys)) {
+ if ([String]::IsNullOrEmpty($parameters[$k])) {
+ $parameters.Remove($k)
+ }
+ }
+ Write-Host "::debug:: PublishPowerShellGallery $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')"
+ & {param(
+ [string]
+ $ModulePath,
+
+ [string[]]
+ $Exclude = @('*.png', '*.mp4', '*.jpg','*.jpeg', '*.gif', 'docs[/\]*')
+ )
+
+ $gitHubEvent = if ($env:GITHUB_EVENT_PATH) {
+ [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json
+ } else { $null }
+
+ if (-not $Exclude) {
+ $Exclude = @('*.png', '*.mp4', '*.jpg','*.jpeg', '*.gif','docs[/\]*')
+ }
+
+
+ @"
+ ::group::GitHubEvent
+ $($gitHubEvent | ConvertTo-Json -Depth 100)
+ ::endgroup::
+ "@ | Out-Host
+
+ @"
+ ::group::PSBoundParameters
+ $($PSBoundParameters | ConvertTo-Json -Depth 100)
+ ::endgroup::
+ "@ | Out-Host
+
+ if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and
+ (-not $gitHubEvent.psobject.properties['inputs'])) {
+ "::warning::Pull Request has not merged, skipping Gallery Publish" | Out-Host
+ return
+ }
+
+
+ $imported =
+ if (-not $ModulePath) {
+ $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/"
+ Import-Module ".\$moduleName.psd1" -Force -PassThru -Global
+ } else {
+ Import-Module $modulePath -Force -PassThru -Global
+ }
+
+ if (-not $imported) { return }
+
+ $foundModule = try { Find-Module -Name $imported.Name -ErrorAction SilentlyContinue} catch {}
+
+ if ($foundModule -and (([Version]$foundModule.Version) -ge ([Version]$imported.Version))) {
+ "::warning::Gallery Version of $moduleName is more recent ($($foundModule.Version) >= $($imported.Version))" | Out-Host
+ } else {
+
+ $gk = '${{secrets.GALLERYKEY}}'
+
+ $rn = Get-Random
+ $moduleTempFolder = Join-Path $pwd "$rn"
+ $moduleTempPath = Join-Path $moduleTempFolder $moduleName
+ New-Item -ItemType Directory -Path $moduleTempPath -Force | Out-Host
+
+ Write-Host "Staging Directory: $ModuleTempPath"
+
+ $imported | Split-Path |
+ Get-ChildItem -Force |
+ Where-Object Name -NE $rn |
+ Copy-Item -Destination $moduleTempPath -Recurse
+
+ $moduleGitPath = Join-Path $moduleTempPath '.git'
+ Write-Host "Removing .git directory"
+ if (Test-Path $moduleGitPath) {
+ Remove-Item -Recurse -Force $moduleGitPath
+ }
+
+ if ($Exclude) {
+ "::notice::Attempting to Exlcude $exclude" | Out-Host
+ Get-ChildItem $moduleTempPath -Recurse |
+ Where-Object {
+ foreach ($ex in $exclude) {
+ if ($_.FullName -like $ex) {
+ "::notice::Excluding $($_.FullName)" | Out-Host
+ return $true
+ }
+ }
+ } |
+ Remove-Item
+ }
+
+ Write-Host "Module Files:"
+ Get-ChildItem $moduleTempPath -Recurse
+ Write-Host "Publishing $moduleName [$($imported.Version)] to Gallery"
+ Publish-Module -Path $moduleTempPath -NuGetApiKey $gk
+ if ($?) {
+ Write-Host "Published to Gallery"
+ } else {
+ Write-Host "Gallery Publish Failed"
+ exit 1
+ }
+ }
+ } @Parameters
+ BuildMatrix:
+ runs-on: ubuntu-latest
+ if: ${{ success() }}
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@main
+ - name: UseEZOut
+ uses: StartAutomating/EZOut@master
+
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..99e9fb4
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,61 @@
+# Simple workflow for deploying static content to GitHub Pages
+name: deploy
+
+on:
+ # Runs on pushes targeting the default branch
+ push:
+ branches: [$default-branch]
+ # or manually from the Actions tab
+ workflow_dispatch:
+
+# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# Allow only one concurrent deployment, and cancel any in-progress deployments if a new one is triggered
+concurrency:
+ group: "pages"
+ cancel-in-progress: true
+
+jobs:
+ # GitHub Pages use a single job, named deploy.
+ deploy:
+ # By using an environment, we avoid locking
+ environment:
+ name: github-pages
+ # and we can control where it is deployed.
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ steps:
+ # Check out our repository
+ - name: Checkout
+ uses: actions/checkout@main
+ with:
+ # Using fetch-depth: 0 to ensure we get the full history of the repository
+ fetch-depth: 0
+ # Setup GitHub Pages
+ - name: Setup Pages
+ uses: actions/configure-pages@main
+ # To Build Pages in PowerShell, we just call a script
+ - name: Build Pages
+ # set the shell to pwsh
+ shell: pwsh
+ # and then call any script we would like to build the page.
+ # By default, this can use the same name as the workflow
+ # (in this case, just deploy.ps1)
+ run: . "./deploy.ps1"
+ # This approach makes it easier to logically organize workflow scripts.
+ # We will also map the page_url to an environment variable, so our scripts can access it.
+ env:
+ page_url: ${{ steps.deployment.outputs.page_url }}
+ analytics_id: ${{vars.ANALYTICSID}}
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@main
+ with:
+ # Upload the contents to the GitHub Pages artifact
+ path: './'
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@main
\ No newline at end of file
diff --git a/Build/GitHub/Jobs/BuildMatrix.psd1 b/Build/GitHub/Jobs/BuildMatrix.psd1
new file mode 100644
index 0000000..e39282f
--- /dev/null
+++ b/Build/GitHub/Jobs/BuildMatrix.psd1
@@ -0,0 +1,11 @@
+@{
+ "runs-on" = "ubuntu-latest"
+ if = '${{ success() }}'
+ steps = @(
+ @{
+ name = 'Check out repository'
+ uses = 'actions/checkout@main'
+ }
+ 'RunEZOut'
+ )
+}
\ No newline at end of file
diff --git a/Build/GitHub/Steps/PublishTestResults.psd1 b/Build/GitHub/Steps/PublishTestResults.psd1
new file mode 100644
index 0000000..e8111e8
--- /dev/null
+++ b/Build/GitHub/Steps/PublishTestResults.psd1
@@ -0,0 +1,10 @@
+@{
+ name = 'PublishTestResults'
+ uses = 'actions/upload-artifact@main'
+ with = @{
+ name = 'PesterResults'
+ path = '**.TestResults.xml'
+ }
+ if = '${{always()}}'
+}
+
diff --git a/Build/Matrix.GitHubWorkflow.PSDevOps.ps1 b/Build/Matrix.GitHubWorkflow.PSDevOps.ps1
new file mode 100644
index 0000000..4e700d0
--- /dev/null
+++ b/Build/Matrix.GitHubWorkflow.PSDevOps.ps1
@@ -0,0 +1,18 @@
+#requires -Module PSDevOps
+param(
+ $moduleName = $(
+ $PSScriptRoot | Split-Path | Split-Path -Leaf
+ )
+)
+Import-BuildStep -SourcePath (
+ Join-Path $PSScriptRoot 'GitHub'
+) -BuildSystem GitHubWorkflow
+
+
+Push-Location ($PSScriptRoot | Split-Path)
+New-GitHubWorkflow -Name "Build Module" -On Push,
+ PullRequest,
+ Demand -Job TestPowerShellOnLinux,
+ TagReleaseAndPublish, "Build$moduleName" -OutputPath .\.github\workflows\build.yml
+
+Pop-Location
\ No newline at end of file
diff --git a/Build/Matrix.ezout.ps1 b/Build/Matrix.ezout.ps1
new file mode 100644
index 0000000..6b9d2fa
--- /dev/null
+++ b/Build/Matrix.ezout.ps1
@@ -0,0 +1,39 @@
+#requires -Module EZOut
+# Install-Module EZOut or https://github.com/StartAutomating/EZOut
+$myFile = $MyInvocation.MyCommand.ScriptBlock.File
+$myModuleName = $MyInvocation.MyCommand.Name -replace '\.ezout.ps1$'
+$myRoot = $myFile | Split-Path | Split-Path
+Push-Location $myRoot
+$formatting = @(
+ # Add your own Write-FormatView here,
+ # or put them in a Formatting or Views directory
+ foreach ($potentialDirectory in 'Formatting','Views','Types') {
+ Join-Path $myRoot $potentialDirectory |
+ Get-ChildItem -ea ignore |
+ Import-FormatView -FilePath {$_.Fullname}
+ }
+)
+
+$destinationRoot = $myRoot
+
+if ($formatting) {
+ $myFormatFilePath = Join-Path $destinationRoot "$myModuleName.format.ps1xml"
+ # You can also output to multiple paths by passing a hashtable to -OutputPath.
+ $formatting | Out-FormatData -Module $MyModuleName -OutputPath $myFormatFilePath
+}
+
+$types = @(
+ # Add your own Write-TypeView statements here
+ # or declare them in the 'Types' directory
+ Join-Path $myRoot Types |
+ Get-Item -ea ignore |
+ Import-TypeView
+
+)
+
+if ($types) {
+ $myTypesFilePath = Join-Path $destinationRoot "$myModuleName.types.ps1xml"
+ # You can also output to multiple paths by passing a hashtable to -OutputPath.
+ $types | Out-TypeData -OutputPath $myTypesFilePath
+}
+Pop-Location
diff --git a/CHANGELOG.html.ps1 b/CHANGELOG.html.ps1
new file mode 100644
index 0000000..99924d1
--- /dev/null
+++ b/CHANGELOG.html.ps1
@@ -0,0 +1,37 @@
+<#
+.SYNOPSIS
+ CHANGELOG
+.DESCRIPTION
+ Matrix CHANGELOG
+.NOTES
+ Renders the CHANGLOG as html, with links to the repo.
+#>
+param(
+[uri]
+$RepositoryUrl = $(
+ if ($env:GITHUB_REPOSITORY) {
+ "https://github.com/$env:GITHUB_REPOSITORY"
+ } else {
+ "https://github.com/PoshWeb/Matrix"
+ }
+)
+)
+
+$changelogPath = Join-Path $PSScriptRoot 'CHANGELOG.md'
+
+$changeLog = Get-Content $changelogPath -Raw
+
+[Regex]::Replace(
+ $changeLog,
+ '#(?\d+)',
+ {
+ param($match)
+ "[$match]($(
+ "$repositoryUrl" + '/issues/' + $match.Groups['n'].Value
+ ))"
+ }
+) |
+ ConvertFrom-Markdown |
+ Select-Object -ExpandProperty Html
+
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..54ccce4
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,44 @@
+# Matrix
+
+## Matrix 0.1
+
+* Initial Matrix Module (#1)
+* `Matrix.ps1` makes matrices (#2)
+* `Matrix` has a build (#3)
+* `Matrix` extended types
+ * `[Numerics.Matrix3x2]`
+ * `.Points` contains the points in the matrix (#4)
+ * `.CSS` renders the matrix as CSS (#5)
+ * `.MathML` renders the matrix as MathML (#33)
+ * `.Html` renders the matrix as MathML in HTML (#36)
+ * `DefaultDisplay` limits the number of displayed properties (#34)
+ * `[Numerics.Matrix4x4]`
+ * `.Points` contains the points in the matrix (#6)
+ * `.CSS` renders the matrix as CSS (#7)
+ * `.MathML` renders the matrix as MathML (#31)
+ * `.Html` renders the matrix as MathML in HTML (#35)
+ * `DefaultDisplay` limits the number of displayed properties (#32)
+ * `[Numerics.Quaternion]`
+ * `.CSS` renders the matrix as CSS (#12)
+ * `.MathML` renders a quaternion as MathML (#37)
+ * `.Html` renders the matrix as MathML in HTML (#38)
+ * `DefaultDisplay` limits the number of displayed properties (#39)
+* `Matrix` aliases provide CSS Compatibility (#8)
+* `Matrix` has an autogenerated `README.md.ps1` (#10)
+* `Matrix` website demonstrates matrix
+ * `layout.ps1` lays out pages (#16)
+ * `deploy.ps1` deploys the site (#22)
+ * `config.ps1` configures the site (#23)
+ * `/_includes/`
+ * `/_includes/FeatherIcon` includes feather icons (#24)
+ * `/_includes/Palette` includes a palette (#25)
+ * `/_includes/CopyCode` includes a "copy code" button (#26)
+ * `/_includes/Sitemap` includes a sitemap (#27)
+ * `/_includes/Menu` includes a menu (#29)
+ * `/Matrix.html.ps1` is the site root (#14)
+ * `/matrix/css`
+ * `/matrix/css/compatible` shows side by side comparisons (#17)
+ * `/matrix/css/transform` shows some matrix transform examples (#18)
+ * `/matrix/dotnet` explains matrices in DotNet (#19)
+ * `/matrix/svg` shows matrices in SVG (#28)
+ * `/matrix/powershell` shows matrices in PowerShell (#20)
diff --git a/Matrix.css.ps1 b/Matrix.css.ps1
new file mode 100644
index 0000000..a70eca8
--- /dev/null
+++ b/Matrix.css.ps1
@@ -0,0 +1,262 @@
+param(
+[string]$PaletteName = 'cyberpunk',
+
+# The Google Font name
+[Alias('FontName')][string]$Font = 'Roboto',
+
+# The Google Code Font name
+[string]$CodeFont = 'CodeFont'
+)
+
+# Know thyself
+$mySelf = $MyInvocation.MyCommand
+
+$body = @"
+body {
+ max-width:100%;
+ height:100vh;
+ font-family:'$Font', sans-serif
+}
+"@
+
+$header = @"
+header {
+ display:grid;
+ position:sticky;
+ grid-area:header;
+ grid-template-areas:"main-menu title options";
+ grid-template-columns:1fr 3fr 1fr;
+ transform-style:preserve-3d;
+ top:0;
+ left:0;
+ max-width:100%;
+ height:auto;
+ z-index:10;
+ margin:1rem;
+ gap:0.5rem;
+ background:color-mix(in srgb, var(--background) 25%, transparent)
+}
+
+
+.title > svg {
+ display:block;
+ text-align:center
+}
+
+.social {
+ display:flex;
+ flex-direction: column;
+ grid-area:social
+}
+.title {
+ grid-area:title;
+ place-self:center;
+ place-items:center;
+ text-align:center
+}
+.options-menu {
+ grid-area:options;
+ text-align: right;
+}
+.options-menu > summary {
+ list-style-type: none
+}
+.logo {
+ display:block;
+ width: 4.2rem;
+ height:4.2rem;
+}
+.logo-text {
+ text-align: center;
+}
+.main-menu {
+ grid-area: main-menu;
+ list-style-type: none;
+ display: flex;
+ flex-direction: column;
+}
+.main-menu > summary {
+ list-style-type: none
+}
+"@
+
+$footer = @"
+footer {
+ display:grid;
+ grid-area:footer;
+ position:sticky;
+ grid-template-rows:auto auto;
+ width:100%;
+ height:1vh;
+ bottom:0;
+ z-index:100
+}
+"@
+
+$article = @"
+article {
+ background:color-mix(in srgb, var(--background) 50%, transparent)
+}
+"@
+
+$popOver = @"
+@keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+[popover] {
+ opacity: 0;
+ animation: fadeIn 0.5s ease-in;
+}
+[popover]:popover-open {
+ opacity: 1;
+}
+
+[popover] {
+ color: var(--foreground);
+ background-color: var(--background);
+ a {
+ color: var(--foreground)
+ }
+}
+"@
+
+$anchors = @"
+a, a:visited { text-decoration:none }
+a:hover, a:focus { text-decoration:underline }
+"@
+
+$portrait = @"
+@media (orientation: portrait) {
+ .row-or-column { flex-direction:column }
+ .logo { height:2.3rem }
+ .page-title, .site-title {
+ font-size:0.84rem;
+ line-height:0.66rem
+ }
+}
+"@
+
+$landscape = @"
+@media (orientation: landscape) {
+ .row-or-column { flex-direction:row }
+ .logo { height:4.2rem }
+ .site-title, .page-title {
+ font-size:1.23rem;
+ line-height:0.75rem
+ }
+}
+"@
+
+$viewAskew = @"
+@keyframes view-askew {
+ 0%,100% { transform: $(
+ (Scale 0.125 0.125).CSS
+ )
+ }
+ 50% { transform: $(
+ (Scale 1 1).CSS
+ ) }
+
+}
+
+.viewAskew {
+ animation-name: view-askew;
+ animation-iteration-count:
+ infinite; animation-duration: 7s;
+ transform-origin: 50% 50%
+}
+"@
+
+$tables = @"
+table { width: 100% }
+"@
+
+@"
+$body
+
+$anchors
+
+$header
+
+$footer
+
+$article
+
+$popover
+
+$tables
+
+$portrait
+
+$landscape
+
+$viewAskew
+
+fieldset {
+ border: 1px solid var(--foreground);
+ display: grid;
+ place-items: center;
+}
+
+.foreground {
+ display:grid;
+ grid-template-rows:auto 1fr auto;
+ grid-template-areas:"header" "main" "footer"
+}
+
+.main {
+ grid-area:main;
+ max-width:90%;
+ margin-top:10rem;
+ padding-left:5%;
+ padding-right:5%;
+ font-size:1.23em;
+ line-height:1.5rem
+}
+
+pre, code {
+ font-family:'$CodeFont', monospace
+}
+
+.row-or-column {
+ flex-direction:row
+}
+"@
+
+$scrollProgress = @"
+@keyframes grow-progress {
+ from { transform:scaleX(0) scaleY(0.5) }
+ to { transform:scaleX(1) scaleY(1) }
+}
+.scroll-progress {
+ width:100%;
+ height:1rem;
+ margin-top:auto;
+ margin-bottom:auto;
+ transform-origin:0 50%;
+ background:linear-gradient(to right, transparent, var(--foreground));
+ animation:grow-progress auto linear;
+ animation-timeline:scroll()
+}
+"@
+
+$scrollProgress
+
+
+$highlightJSColors = @"
+.hljs {
+ background:color-mix(in srgb, var(--background) 75%, transparent);
+ color:var(--foreground)
+}
+.hljs-number { color:var(--cyan) }
+.hljs-type { color:var(--purple) }
+.hljs-string { color:var(--brightWhite) }
+.hljs-built_in { color:var(--brightBlue); font-weight:demibold }
+.hljs-variable { color:var(--green); font-weight:demibold }
+.hljs-comment { color:var(--brightGreen); font-weight:demibold }
+.hljs-literal { color:var(--brightWhite) }
+"@
+
+$highlightJSColors
\ No newline at end of file
diff --git a/Matrix.html.ps1 b/Matrix.html.ps1
new file mode 100644
index 0000000..d84ccaa
--- /dev/null
+++ b/Matrix.html.ps1
@@ -0,0 +1,15 @@
+<#
+.SYNOPSIS
+ Matrix
+.DESCRIPTION
+ Matrix Transforms
+.NOTES
+ Currently just replicating the README within the layout
+#>
+[OutputType('text/html')]
+param()
+
+$Title = 'Matrix'
+
+ConvertFrom-Markdown -Path ./README.md -ErrorAction Ignore |
+ Select-Object -ExpandProperty Html
diff --git a/Matrix.ps1 b/Matrix.ps1
new file mode 100644
index 0000000..dd1817d
--- /dev/null
+++ b/Matrix.ps1
@@ -0,0 +1,530 @@
+<#
+.SYNOPSIS
+ Matrix
+.DESCRIPTION
+ Makes and Manipulates Matrix Transformations.
+
+ Matrix Transformations move objects in space.
+
+ Matrix makes matrixes in PowerShell.
+
+ This can transform objects in 2D, 3D, and 4D
+
+ We can use matrix to make CSS or transform Vectors.
+.NOTES
+ Matrix math is hard, and this module lets us avoid having to do it.
+
+ Instead, we can pipe objects into this module and transform them.
+
+ A Matrix in .NET is the same as a Matrix in CSS.
+
+ |css function|.NET type|
+ |-|-|
+ |`matrix()`|`[Numerics.Matrix3x2]`|
+ |`matrix3d()`|`[Numerics.Matrix4x4]`|
+
+ This means we can do every transformation that CSS can do.
+
+ Any object piped with a `Transform` static method will be transformed.
+
+ Other objects will be passed thru.
+.EXAMPLE
+ # Get the identity matrix.
+ # This is the object, untransformed, in 2D
+ Matrix Identity
+.EXAMPLE
+ # Gets a 3d identity matrix
+ # This is the object, untransformed, in 3d.
+ Matrix3D Identity
+.EXAMPLE
+ # Scale a point in 2d space by directly calling `::CreateScale`
+ [Numerics.Vector2]::new(1,1) |
+ Matrix 2 -Member CreateScale 1 2
+.EXAMPLE
+ # Scale a point in 2d space by using `scale`
+ [Numerics.Vector2]::new(1,1) |
+ Scale 1 2
+.EXAMPLE
+ # Skew a point
+ [Numerics.Vector2]::new(1,1) |
+ Skew 30deg 10deg
+.EXAMPLE
+ # Skew a point along X, then along Y
+ [Numerics.Vector2]::new(1,1) |
+ SkewX 30deg |
+ SkewY 10deg
+.EXAMPLE
+ [Numerics.Vector2]::new(1,1) |
+ ScaleZ 1
+.EXAMPLE
+ # Scale X in 2D
+ [Numerics.Vector2]::new(1,1) |
+ ScaleX 2
+.EXAMPLE
+ # Scale X in 3D
+ [Numerics.Vector3]::new(1,1,1) |
+ ScaleX 2
+.EXAMPLE
+ # Scale Y in 3D
+ [Numerics.Vector2]::new(1,1) |
+ ScaleY 2
+.EXAMPLE
+ # Scale Z in 3D
+ [Numerics.Vector3]::new(1,1,1) |
+ ScaleZ 3
+.EXAMPLE
+ # Move a point in 3d
+ [Numerics.Vector3]::new(1,1,1) |
+ TranslateX 3 |
+ TranslateY 3 |
+ TranslateZ 3
+.EXAMPLE
+ # Move and scale a point in 3d
+ [Numerics.Vector3]::new(1,1,1) |
+ Translate3d 1 2 5 |
+ Scale3d 3 2 1
+.EXAMPLE
+ [Numerics.Vector3]::new(1,1,1) |
+ Translate3d 1 2 5 |
+ Scale3d 3 2 1
+.EXAMPLE
+ # Rotate3d
+ [Numerics.Vector3]::new(1,1,1) |
+ Rotate3d 1 1 1 30deg
+.EXAMPLE
+ # rotate3d as a matrix3d, as CSS
+ (Rotate3d 1 1 1 30deg).css
+.EXAMPLE
+ # Constructing a cube using translation
+
+ # Make a corner point
+ $corner = [Numerics.Vector3]::new(1,1,1)
+
+ # Make a square by translating along X and Y
+ $square = @(
+ $corner
+ $corner | TranslateX 1
+ $corner | TranslateY 1
+ $corner | TranslateX 1 | TranslateY 1
+ )
+
+ # Make a cube by translating the square along Z.
+ $cube = @(
+ $square
+ $square |
+ TranslateZ 1
+ )
+
+ $cube
+.LINK
+ https://github.com/PoshWeb/Matrix
+.LINK
+ https://learn.microsoft.com/en-us/dotnet/api/system.numerics.matrix4x4?wt.mc_id=MVP_321542
+.LINK
+ https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/transform-function/matrix3D
+.LINK
+ https://learn.microsoft.com/en-us/dotnet/api/system.numerics.matrix3x2?wt.mc_id=MVP_321542
+.LINK
+ https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/transform-function/matrix
+#>
+[Alias(
+ 'Matrix4x4',
+ 'Matrix3x2',
+ 'Matrix2d',
+ 'Matrix3d',
+ 'Quaternion',
+ 'Skew',
+ 'SkewX',
+ 'SkewY',
+ 'Scale',
+ 'ScaleX',
+ 'ScaleY',
+ 'ScaleZ',
+ 'Scale3d',
+ 'Rotate',
+ 'RotateX',
+ 'RotateY',
+ 'RotateZ',
+ 'Rotate3d',
+ 'Translate',
+ 'Translate3d',
+ 'TranslateX',
+ 'TranslateY',
+ 'TranslateZ'
+)]
+[CmdletBinding(PositionalBinding=$false)]
+param(
+# Any arguments for the transform function
+# Arguments can include numbers of CSS units.
+# `deg` and `turn` are converted into radians
+# `%` becomes a value between 0 and 1
+[Parameter(ValueFromRemainingArguments)]
+[Alias('Arguments','Argument','Args')]
+[object[]]
+$ArgumentList,
+
+# Any input objects.
+# If the input object has a `Transform` static method,
+# it will be transformed.
+# If it does not, it will be passed thru.
+[Parameter(ValueFromPipeline)]
+[Alias('Input')]
+[PSObject[]]
+$InputObject,
+
+# The name of the method or property of a matrix transform.
+# If this is provided, this method will be called instead.
+# Many aliases, such as `Skew` or `Rotate`,
+# will use a custom member and will ignore this parameter.
+[ArgumentCompleter({
+ param(
+ $CommandName, $parameterName, $wordToComplete,
+ $commandAst, $fakeBoundParameters
+ )
+
+ $firstElement = @($commandAst.CommandElements)[0]
+ $members =
+ if ($firstElement -match '(?>3d|4x4)') {
+ [Numerics.Matrix4x4].GetMembers('Static,Public').Name -notmatch '_'
+ }
+ elseif ($firstElement -match 'Quaternion') {
+ [Numerics.Quaternion].GetMembers('Static,Public').Name -notmatch '_'
+ }
+ else {
+ [Numerics.Matrix3x2].GetMembers('Static,Public').Name -notmatch '_'
+ }
+
+ if ($wordToComplete) {
+ $members -match "$([regex]::Escape($wordToComplete))"
+ } else {
+ $members
+ }
+})]
+[string]
+$Member = 'Create'
+)
+
+# Get our invocation name
+$myName = $MyInvocation.InvocationName
+
+# and all of our input
+$allInput = @($input)
+
+# If input was not piped
+if (-not $allInput.Length) {
+ # bind it to any unpiped input object
+ $allInput += $InputObject
+}
+
+# Attempt to determine the matrix type.
+$matrixType =
+ # If the name contains 3d or 4x4,
+ if ($myName -match '(?>3d|4x4)') {
+ [Numerics.Matrix4x4] # treat it as a 3d matrix.
+ }
+ elseif ($myName -match '(?>Quaternion|Versor)') {
+ [Numerics.Quaternion]
+ }
+ else { # Otherwise
+ [Numerics.Matrix3x2] # treat it as a 2d matrix.
+ }
+
+# Declare a quick little filter to convert css units into numbers.
+filter cssunit {
+ $arg = $_
+ if ($arg -isnot [string]) {
+ return $arg
+ }
+ switch -regex ($arg) {
+ 'turn$' {
+ # Each turn of the circle is 360 radians
+ ($_ -replace 'turn$' -as [single]) * ([Math]::PI/180 * 360)
+ continue
+ }
+ 'deg$' {
+ ($_ -replace 'deg$' -as [single]) * ([Math]::PI/180)
+ continue
+ }
+ '%$' {
+ ($_ -replace '%$' -as [single])
+ continue
+ }
+ '[\-\.\d]+\p{L}+' {
+ $_ -replace '\p{L}+$' -as [single]
+ continue
+ }
+ default {
+ $_
+ }
+ }
+}
+
+# Then convert all of our arguments into units
+$ArgumentList = @(
+ $ArgumentList | cssunit
+) -ne $null
+
+# Next we will be determine the right matrix transform
+# Switch based off the name.
+# Any matching CSS transforms should be their literal equivalent.
+# These are _mostly_ self explanatory, with one _very_ annoying outlier
+switch ($myName) {
+ Rotate {
+ # Rotate becomes `[Numerics.Matrix3x2]::CreateRotation`
+ $MatrixType = [Numerics.Matrix3x2]
+ $Member = 'CreateRotation'
+ $ArgumentList = $ArgumentList[0]
+ }
+ RotateX {
+ # RotateX becomes `[Numerics.Matrix4x4]::CreateRotationX`
+ $MatrixType = [Numerics.Matrix4x4]
+ $Member = 'CreateRotationX'
+ $ArgumentList = $ArgumentList[0]
+ }
+ RotateY {
+ # RotateY becomes `[Numerics.Matrix4x4]::CreateRotationY`
+ $MatrixType = [Numerics.Matrix4x4]
+ $Member = 'CreateRotationY'
+ $ArgumentList = $ArgumentList[0]
+ }
+ RotateZ {
+ # RotateZ becomes `[Numerics.Matrix4x4]::CreateRotationZ`
+ $MatrixType = [Numerics.Matrix4x4]
+ $Member = 'CreateRotationZ'
+ $ArgumentList = $ArgumentList[0]
+ }
+ Rotate3d {
+ # Rotate3d is the complicated one.
+ # It took some digging, but the CSS working defines rotate3d in matrix form
+ # https://drafts.csswg.org/css-transforms-2/#Rotate3dDefined
+
+ # It's a 4x4 matrix
+ $MatrixType = [Numerics.Matrix4x4]
+ $Member = 'Create'
+ # that requires normalized vectors
+ $x, $y, $z, $null = $ArgumentList
+ # Sadly, this Normalize is different than .NET's Normalize
+ $normalize = [Numerics.Vector3]::new($x, $y, $z)
+ # We need to get the sum of all squares (the length)
+ $magnitude = $normalize.Length()
+ # If that sum was zero, make it the sqrt root of 3
+ if ($magnitude -eq 0) { $magnitude = [Math]::Sqrt(3) }
+ # Normalize x y z with the magnitude
+ $x, $y, $z = ($x/$magnitude), ($y/$magnitude), ($z/$magnitude)
+ # The formula requires each value squared, so do that now
+ $x2, $y2, $z2 = [Math]::Pow($x, 2), [Math]::Pow($y,2), [Math]::Pow($z, 2)
+ # It also defines the angle as `alpha`
+ # Sadly, it does not denote the direction,
+ # and testing indicates that it flips the angle.
+ $alpha = ($ArgumentList[3] -as [single]) * -1
+
+ # Some light trig gives us `$sc`,
+ # which is used throughout the following matrix
+ $sc = [Math]::Sin($alpha/2) * [Math]::Cos($alpha/2)
+ # `$sq` is the square of the sine of this angle,
+ # and is also used throughout the matrix.
+ $sq = [Math]::Pow([Math]::Sin($alpha/2), 2)
+
+ # This next bit of complexity is translated directly from the reference.
+ # With spacing and docs added for clarity.
+ $ArgumentList = @(
+ #M 1 1 (ScaleX)
+ 1 - (2 * ($y2 + $z2) * $sq)
+ # M 1 2
+ 2 * (($x * $y * $sq) - ($z * $sc))
+ # M 1 3 (Rotation Y)
+ 2 * (($x * $z * $sq) + ($y * $sc))
+ # M 1 4
+ 0
+
+ # M 2 1 ( Rotation Z )
+ 2 * (($x * $y * $sq) + ($z * $sc))
+
+ # M 2 2 ( Scale Y)
+ 1 - (2 * ($x2 + $z2) * $sq)
+
+ # M 2 3
+ 2 * (($y * $z * $sq) - ($x * $sc))
+
+ # m 2 4
+ 0
+
+ # M 3 1
+ 2 * (($x * $z * $sq) - ($y * $sc))
+
+ # M 3 2 ( Rotation X )
+ 2 * (($y * $z * $sq) + ($x * $sc))
+
+ # M 3 3 (Scale Z)
+ 1.0 - (2 * ($x2 + $y2) * $sq)
+
+ # M 3 4
+ 0
+
+ # M 4 1 ( Translate X )
+ 0
+ # M 4 2 ( Translate Y )
+ 0
+ # M 4 3 ( Translate Z )
+ 0
+ # M 4 4
+ 1
+ )
+
+ # Every other CSS transform can be done in a few lines of PowerShell
+ # This particular transform is the (quite painful) outlier.
+ }
+ Scale {
+ # Scale becomes `[Numerics.Matrix3x2]::CreateScale`
+ $MatrixType = [Numerics.Matrix3x2]
+ $Member = 'CreateScale'
+ }
+ ScaleX {
+ # `ScaleX` becomes `[Numerics.Matrix4x4]::CreateScale`
+ $MatrixType = [Numerics.Matrix4x4]
+ $Member = 'CreateScale'
+ $ArgumentList = $ArgumentList[0], 1, 1
+ }
+ ScaleY {
+ # `ScaleY` becomes `[Numerics.Matrix4x4]::CreateScale`
+ $MatrixType = [Numerics.Matrix4x4]
+ $Member = 'CreateScale'
+ $ArgumentList = 1, $ArgumentList[0], 1
+ }
+ ScaleZ {
+ # `ScaleZ` becomes `[Numerics.Matrix4x4]::CreateScale`
+ $MatrixType = [Numerics.Matrix4x4]
+ $Member = 'CreateScale'
+ $ArgumentList = 1, 1, $ArgumentList[0]
+ }
+ Scale3d {
+ # `Scale3d` becomes `[Numerics.Matrix4x4]::CreateScale`
+ $MatrixType = [Numerics.Matrix4x4]
+ $Member = 'CreateScale'
+ }
+ Skew {
+ # `Skew` becomes `[Numerics.Matrix3x2]::CreateSkew`
+ $MatrixType = [Numerics.Matrix3x2]
+ $Member = 'CreateSkew'
+ if ($ArgumentList.Length -eq 1) {
+ $ArgumentList *= 2
+ }
+ }
+ SkewX {
+ # `SkewX` becomes `[Numerics.Matrix3x2]::CreateSkew`
+ $MatrixType = [Numerics.Matrix3x2]
+ $Member = 'CreateSkew'
+ if ($ArgumentList.Length -eq 1) {
+ $ArgumentList = $ArgumentList[0], 0
+ }
+ }
+ SkewY {
+ # `SkewY` becomes `[Numerics.Matrix3x2]::CreateSkew`
+ $MatrixType = [Numerics.Matrix3x2]
+ $Member = 'CreateSkew'
+ if ($ArgumentList.Length -eq 1) {
+ $ArgumentList = 0, $ArgumentList[0]
+ }
+ }
+ Translate {
+ # `Translate` becomes `[Numerics.Matrix3x2]::CreateTranslation`
+ $MatrixType = [Numerics.Matrix3x2]
+ $Member = 'CreateTranslation'
+ }
+ Translate3d {
+ # `Translate3d` becomes `[Numerics.Matrix4x4]::CreateTranslation`
+ $MatrixType = [Numerics.Matrix4x4]
+ $Member = 'CreateTranslation'
+ }
+ TranslateX {
+ # `TranslateX` becomes `[Numerics.Matrix4x4]::CreateTranslation`
+ $MatrixType = [Numerics.Matrix4x4]
+ $Member = 'CreateTranslation'
+ $ArgumentList = @($ArgumentList[0], 0, 0)
+ }
+ TranslateY {
+ # `TranslateY` becomes `[Numerics.Matrix4x4]::CreateTranslation`
+ $MatrixType = [Numerics.Matrix4x4]
+ $Member = 'CreateTranslation'
+ $ArgumentList = @(0, $ArgumentList[0], 0)
+ }
+ TranslateZ {
+ # `TranslateZ` becomes `[Numerics.Matrix4x4]::CreateTranslation`
+ $MatrixType = [Numerics.Matrix4x4]
+ $Member = 'CreateTranslation'
+ $ArgumentList = @(0, 0, $ArgumentList[0])
+ }
+}
+
+# If we do not have a matching member
+if ($null -eq $matrixType::$Member) {
+ # error out
+ Write-Error "$Member does not exist on $MatrixType"
+ return
+}
+
+# If we have no arguments
+if (-not $ArgumentList.Length) {
+ # and no member, return the matrix type.
+ if (-not $PSBoundParameters.Member) {return $matrixType }
+ # If we have no arguments and a member, return the member.
+ else { return $matrixType::$member }
+}
+
+# If the first argument is a static member
+if ($ArgumentList.Length -eq 1 -and
+ $matrixType::($ArgumentList[0])
+) {
+ # use that as the member
+ $Member = $ArgumentList[0]
+}
+
+
+# .Net does not provide a `Create` method for either matrix that accepts a matrix
+if ($Member -eq 'Create' -and $ArgumentList[0] -is $matrixType) {
+ # So take all of Matrix properties and make them arguments.
+ $argumentList = foreach ($property in $ArgumentList[0].psobject.properties) {
+ if ($property.Name -match '^M\d{2}') {
+ $property.Value
+ }
+ }
+}
+
+# Create the matrix by invoking the member
+# (or just returning the property)
+$matrix =
+ if ($matrixType::$member.Invoke) {
+ $matrixType::$Member.Invoke($ArgumentList)
+ } else {
+ $matrixType::$Member
+ }
+
+# If we have any input
+if ($allInput.Length -and ($null -ne $allInput[0])) {
+ # Make a 3d variation of our matrix
+ $3dMatrix =
+ if ($matrix -is [Numerics.Matrix3x2]) {
+ [Numerics.Matrix4x4]::Create($matrix)
+ }
+ elseif ($matrix -is [Numerics.Quaternion]) {
+ [Numerics.Matrix4x4]::CreateFromQuaternion($matrix)
+ }
+ else {
+ $matrix
+ }
+
+ # Walk over all of our input
+ foreach ($in in $allInput) {
+ # If the input is transformable
+ if ($in::Transform.Invoke) {
+ # transform the input.
+ $in::Transform($in, $3dMatrix)
+ } else {
+ # Otherwise, pass the input thru.
+ $in
+ }
+ }
+} else {
+ # If we had no input, output the matrix.
+ $matrix
+}
\ No newline at end of file
diff --git a/Matrix.psd1 b/Matrix.psd1
new file mode 100644
index 0000000..19b5bd7
--- /dev/null
+++ b/Matrix.psd1
@@ -0,0 +1,220 @@
+#
+# Module manifest for module 'Matrix'
+#
+# Generated by: James Brundage
+#
+# Generated on: 8/10/2026
+#
+
+@{
+
+# Script module or binary module file associated with this manifest.
+RootModule = 'Matrix.psm1'
+
+# Version number of this module.
+ModuleVersion = '0.1'
+
+# Supported PSEditions
+# CompatiblePSEditions = @()
+
+# ID used to uniquely identify this module
+GUID = '1ea87d5e-babe-42d9-97fa-68775e5335af'
+
+# Author of this module
+Author = 'James Brundage'
+
+# Company or vendor of this module
+CompanyName = 'Start-Automating'
+
+# Copyright statement for this module
+Copyright = '2026'
+
+# Description of the functionality provided by this module
+Description = 'Matrix Transforms with PowerShell'
+
+# Minimum version of the PowerShell engine required by this module
+# PowerShellVersion = ''
+
+# Name of the PowerShell host required by this module
+# PowerShellHostName = ''
+
+# Minimum version of the PowerShell host required by this module
+# PowerShellHostVersion = ''
+
+# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
+# DotNetFrameworkVersion = ''
+
+# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
+# ClrVersion = ''
+
+# Processor architecture (None, X86, Amd64) required by this module
+# ProcessorArchitecture = ''
+
+# Modules that must be imported into the global environment prior to importing this module
+# RequiredModules = @()
+
+# Assemblies that must be loaded prior to importing this module
+# RequiredAssemblies = @()
+
+# Script files (.ps1) that are run in the caller's environment prior to importing this module.
+# ScriptsToProcess = @()
+
+# Type files (.ps1xml) to be loaded when importing this module
+TypesToProcess = 'Matrix.types.ps1xml'
+
+# Format files (.ps1xml) to be loaded when importing this module
+# FormatsToProcess = @()
+
+# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
+# NestedModules = @()
+
+# 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.
+FunctionsToExport = 'Matrix', 'Get-Matrix'
+
+# Cmdlets 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 cmdlets to export.
+CmdletsToExport = @()
+
+# Variables to export from this module
+VariablesToExport = 'Matrix'
+
+# Aliases 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 aliases to export.
+AliasesToExport =
+ @(
+ 'Matrix4x4', 'Matrix3x2', 'Matrix2d', 'Matrix3d','Quaternion'
+ 'Scale',
+ 'ScaleX',
+ 'ScaleY',
+ 'ScaleZ',
+ 'Scale3D',
+ 'Skew',
+ 'SkewX',
+ 'SkewY',
+ 'Rotate',
+ 'RotateX',
+ 'RotateY',
+ 'RotateZ',
+ 'Rotate3D',
+ 'Translate',
+ 'Translate3d',
+ 'TranslateX',
+ 'TranslateY',
+ 'TranslateZ'
+ )
+
+
+# DSC resources to export from this module
+# DscResourcesToExport = @()
+
+# List of all modules packaged with this module
+# ModuleList = @()
+
+# List of all files packaged with this module
+# FileList = @()
+
+# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
+PrivateData = @{
+
+ PSData = @{
+
+ # Tags applied to this module. These help with module discovery in online galleries.
+ # Tags = @()
+
+ # A URL to the license for this module.
+ LicenseUri = 'https://github.com/PoshWeb/Matrix/blob/main/LICENSE'
+
+ # A URL to the main website for this project.
+ ProjectUri = 'https://github.com/PoshWeb/Matrix/'
+
+ # A URL to an icon representing this module.
+ # IconUri = ''
+
+ # ReleaseNotes of this module
+ ReleaseNotes = @'
+## Matrix 0.1
+
+* Initial Matrix Module (#1)
+* `Matrix.ps1` makes matrices (#2)
+* `Matrix` has a build (#3)
+* `Matrix` extended types
+ * `[Numerics.Matrix3x2]`
+ * `.Points` contains the points in the matrix (#4)
+ * `.CSS` renders the matrix as CSS (#5)
+ * `.MathML` renders the matrix as MathML (#33)
+ * `.Html` renders the matrix as MathML in HTML (#36)
+ * `DefaultDisplay` limits the number of displayed properties (#34)
+ * `[Numerics.Matrix4x4]`
+ * `.Points` contains the points in the matrix (#6)
+ * `.CSS` renders the matrix as CSS (#7)
+ * `.MathML` renders the matrix as MathML (#31)
+ * `.Html` renders the matrix as MathML in HTML (#35)
+ * `DefaultDisplay` limits the number of displayed properties (#32)
+ * `[Numerics.Quaternion]`
+ * `.CSS` renders the matrix as CSS (#12)
+ * `.MathML` renders a quaternion as MathML (#37)
+ * `.Html` renders the matrix as MathML in HTML (#38)
+ * `DefaultDisplay` limits the number of displayed properties (#39)
+* `Matrix` aliases provide CSS Compatibility (#8)
+* `Matrix` has an autogenerated `README.md.ps1` (#10)
+* `Matrix` website demonstrates matrix
+ * `layout.ps1` lays out pages (#16)
+ * `deploy.ps1` deploys the site (#22)
+ * `config.ps1` configures the site (#23)
+ * `/_includes/`
+ * `/_includes/FeatherIcon` includes feather icons (#24)
+ * `/_includes/Palette` includes a palette (#25)
+ * `/_includes/CopyCode` includes a "copy code" button (#26)
+ * `/_includes/Sitemap` includes a sitemap (#27)
+ * `/_includes/Menu` includes a menu (#29)
+ * `/Matrix.html.ps1` is the site root (#14)
+ * `/matrix/css`
+ * `/matrix/css/compatible` shows side by side comparisons (#17)
+ * `/matrix/css/transform` shows some matrix transform examples (#18)
+ * `/matrix/dotnet` explains matrices in DotNet (#19)
+ * `/matrix/svg` shows matrices in SVG (#28)
+ * `/matrix/powershell` shows matrices in PowerShell (#20)
+
+'@
+
+
+ PSIntro = @'
+Matrix math is tedious. This module lets us avoid having to do it.
+
+We can represent changes in space using a matrix.
+
+In CSS, these are called [Transform Functions](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/transform-function)
+
+|Dimension|Transform Function|.NET type|
+|:-:|:-:|:-:|
+|`2D`|`matrix()`|`[Numerics.Matrix3x2]`|
+|`3D`|`matrix3d()`|`[Numerics.Matrix4x4]`|
+
+The module allows you to make, modify, and use matrix transforms.
+
+It supports almost identical syntax to the CSS.
+
+We can use Matrix to make CSS transforms,
+and we can manipulate objects in 2D or 3D the same way a webpage would.
+'@
+ # Prerelease string of this module
+ # Prerelease = ''
+
+ # Flag to indicate whether the module requires explicit user acceptance for install/update/save
+ # RequireLicenseAcceptance = $false
+
+ # External dependent modules of this module
+ # ExternalModuleDependencies = @()
+
+ } # End of PSData hashtable
+
+ Recommends = 'Vector'
+
+} # End of PrivateData hashtable
+
+# HelpInfo URI of this module
+# HelpInfoURI = ''
+
+# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
+# DefaultCommandPrefix = ''
+
+}
+
diff --git a/Matrix.psm1 b/Matrix.psm1
new file mode 100644
index 0000000..86a7438
--- /dev/null
+++ b/Matrix.psm1
@@ -0,0 +1,99 @@
+#region Eponym
+
+# Functions and scripts are interchangeable in PowerShell
+# So we can make a small module using an eponym file.
+# First we need to identify the module name
+$moduleName = $MyInvocation.MyCommand.Name -replace '\.psm1$'
+
+# Once we have done this, we can look for an eponymous script:
+$eponym =
+ $ExecutionContext.SessionState.InvokeCommand.GetCommand((
+ Join-Path $PSScriptRoot "$moduleName.ps1"
+ ), 'ExternalScript')
+
+# If we did not find one,
+if (-not $eponym) {
+ # warn and return.
+ Write-Warning "Missing ./$moduleName.ps1"
+ return
+}
+
+# We want to define two functions from this script
+
+# One is the name of the script
+# The other is the "verb" form of the script.
+
+# Collect our list of verbs
+$verbs = Get-Verb |
+ Sort-Object { $_.Verb.Length }, {$_.Verb } -Descending |
+ Select-Object -ExpandProperty Verb
+
+# and craft a regex to see if we start with the verb.
+$startsWithVerb = "^(?>$(
+ $verbs -join '|'
+))"
+
+# Our Exports are:
+$exports =
+ $moduleName, # * The Eponym
+ $(
+ # The `Verb-Noun` form
+ if ($moduleName -match $startsWithVerb) {
+ "$($matches.0)-$($moduleName -replace "$startsWithVerb\p{P}?")"
+ } else {
+ "Get-$($ModuleName -replace '\p{P}')"
+ }
+ )
+
+# We can use the function provider to create functions in this scope.
+foreach ($functionName in $exports) {
+ # This allows us to dynamically set each export to by the eponym
+ $ExecutionContext.SessionState.PSVariable.Set(
+ "function:$functionName",
+ $eponym.ScriptBlock
+ )
+}
+
+# We also want to export any aliases
+# and add support for argument completers.
+$argumentCompleter = $null
+$aliasExports = @(
+ # walk over all of our attributes
+ foreach ($attribute in $eponym.ScriptBlock.Attributes) {
+ # and keep track of any argument completers we find.
+ if ($attribute -is [ArgumentCompleter]) {
+ $argumentCompleter = $attribute
+ }
+ # Then make our aliases
+ foreach ($alias in $attribute.aliasNames) {
+ # (unless the alias is already exported as a function)
+ if ($alias -in $exports) { continue }
+ $ExecutionContext.SessionState.PSVariable.Set(
+ "alias:$alias", $moduleName
+ )
+ $alias
+ }
+ }
+)
+
+# If we had an argument completer
+if ($argumentCompleter.ScriptBlock) {
+ # now is the time to register it.
+
+ # Argument completers need to be registered for each function
+ foreach ($functionExport in $exports) {
+ Register-ArgumentCompleter -CommandName $functionExport -ScriptBlock $argumentCompleter.ScriptBlock
+ }
+
+ # and alias
+ foreach ($aliasExport in $aliasExports) {
+ Register-ArgumentCompleter -CommandName $aliasExport -ScriptBlock $argumentCompleter.ScriptBlock
+ }
+}
+
+# We will also be exporting our eponym as a variable
+$ExecutionContext.SessionState.PSVariable.Set($moduleName, $eponym)
+
+# All that's left to do is explicitly export just these functions.
+Export-ModuleMember -Function $exports -Alias $aliasExports -Variable $moduleName
+#endregion Eponym
\ No newline at end of file
diff --git a/Matrix.svg.ps1 b/Matrix.svg.ps1
new file mode 100644
index 0000000..5f4041f
--- /dev/null
+++ b/Matrix.svg.ps1
@@ -0,0 +1,51 @@
+<#
+.SYNOPSIS
+ Matrix Logo
+.DESCRIPTION
+ Logo for `Matrix`
+.LINK
+ https://github.com/PoshWeb/Matrix
+#>
+param()
+
+$psChevron = '
+
+ '
+
+@"
+
+
+ $psChevron
+ $(
+ $scale = 1
+ for ($scale = 1; $scale -lt 4; $scale += 0.5) {
+ $scaleFactor = 1/[Math]::Pow(2,($scale - 1))
+ $upscale = 1/[Math]::Pow(2,($scale))
+ $transform = (Scale $scaleFactor).CSS
+
+
+ " "
+
+ " "
+
+ " "
+
+ " "
+
+
+ <#" "#>
+ }
+ )
+
+
+"@
diff --git a/Matrix.tests.ps1 b/Matrix.tests.ps1
new file mode 100644
index 0000000..2d5e8c4
--- /dev/null
+++ b/Matrix.tests.ps1
@@ -0,0 +1,79 @@
+describe Matrix {
+ it 'Transforms' {
+ $transformed = [Numerics.Vector2]::new(1,1) |
+ scale 2 1
+ $transformed.X | Should -be 2
+
+
+ $transformed = [Numerics.Vector2]::new(1,1) |
+ scale 1 2
+ $transformed.Y | Should -be 2
+ }
+
+ context 2d {
+ it 'Can Scale' {
+ [Numerics.Vector2]::new(1,1) |
+ scale 2 1 |
+ Select-Object -ExpandProperty X |
+ Should -Be 2
+ }
+
+ it 'Can Translate' {
+ $translated = [Numerics.Vector2]::new(1,1) |
+ translate 1 2
+
+ $translated.X | Should -Be 2
+ $translated.Y | Should -Be 3
+
+ }
+
+ it 'Can skew' {
+ $skewed = [Numerics.Vector2]::new(1,1) |
+ skew 45deg 0
+ $skewed.X | Should -Be 2
+ $skewed.Y | Should -Be 1
+ }
+
+ it 'Can rotate' {
+ $rotated = [Numerics.Vector2]::new(1,1) |
+ rotate 90deg
+
+ $rotated.X | Should -Be -1
+ $rotated.Y | Should -Be 1
+ }
+ }
+
+ context 3d {
+ it 'Can Translate' {
+ $translated = [Numerics.Vector3]::new(1,1,1) |
+ translate3d 1 2 3
+
+ $translated.X | Should -Be 2
+ $translated.Y | Should -Be 3
+ $translated.Z | Should -Be 4
+ }
+
+ it 'Can Scale' {
+ $scaled = [Numerics.Vector3]::new(1,1,1) |
+ scale3d 1 2 3
+
+ $scaled.X | Should -Be 1
+ $scaled.Y | Should -Be 2
+ $scaled.Z | Should -Be 3
+ }
+
+ it 'Can Rotate' {
+ $rotated = [Numerics.Vector3]::new(1,1,1) |
+ rotate 90deg
+
+ $rotated.X | Should -Be -1
+ $rotated.Y | Should -Be 1
+ $rotated.Z | Should -Be 1
+ }
+
+ it 'Can Rotated3d' {
+ (Rotate3d 1 1 1 30deg).CSS |
+ Should -Be 'matrix3d(0.9106836, 0.3333333, -0.2440169, 0, -0.2440169, 0.9106836, 0.3333333, 0, 0.3333333, -0.2440169, 0.9106836, 0, 0, 0, 0, 1)'
+ }
+ }
+}
diff --git a/Matrix.types.ps1xml b/Matrix.types.ps1xml
new file mode 100644
index 0000000..3f8d8c8
--- /dev/null
+++ b/Matrix.types.ps1xml
@@ -0,0 +1,395 @@
+
+
+
+ System.Numerics.Matrix3x2
+
+
+ PSStandardMembers
+
+
+ DefaultDisplayPropertySet
+
+ X
+ Y
+ Z
+
+
+
+
+
+ CSS
+
+ return "matrix($(
+@(
+$this.M11
+$this.M12
+$this.M21
+$this.M22
+$this.M31
+$this.M32
+) -join ', '
+))"
+
+
+
+ Html
+
+ <#
+.SYNOPSIS
+ Gets Matrix Html
+.DESCRIPTION
+ Gets an HTML representation of the Matrix.
+
+ If there is an attached Content property, it will be rendered using the matrix.
+
+ Otherwise, the Matrix's MathML representation will be rendered twice:
+
+ * Once untransformed
+ * Once transformed using itself
+#>
+param()
+
+# If we have content
+if ($this.Content) {
+ # put it in the matrix.
+ "<section style='transform:$($this.CSS)'>"
+ if ($this.Content.OuterXml) {
+ $this.Content.OuterXml
+ } elseif ($this.Content.Html) {
+ $this.Content.Html
+ } else {
+ "$($this.Content)"
+ }
+ "</section>"
+} else {
+ # Otherwise,
+ $mathML = $this.MathML
+ $mathML.OuterXML # show the MathML
+ # and show it again, transformed.
+ $mathML.math.setAttribute('style', "transform:$($this.CSS)")
+ $mathML.OuterXML
+}
+
+
+
+ MathML
+
+ <#
+.SYNOPSIS
+ Matrix MathML
+.DESCRIPTION
+ Gets the Matrix as a MathML representation of itself.
+.NOTES
+ Also shows the equivalent `[Numerics.Matrtix4x4]`
+#>
+[xml]@"
+<math display='block'>
+ <mo>[</mo>
+ <mtable>
+ $(
+ foreach ($row in 1..3) {
+ "<mtr>"
+ foreach ($col in 1..2) {
+ "<mtd><mn>$($this."M${row}${col}")</mn></mtd>"
+ }
+ "</mtr>"
+ }
+ )
+ </mtable>
+ <mo>]</mo>
+ <mo>=</mo>$(
+ [Numerics.Matrix4x4]::Create($this).MathML.math.innerXml
+ )
+</math>
+"@
+
+
+
+
+ Points
+
+ $this.M11
+$this.M12
+$this.M21
+$this.M22
+$this.M31
+$this.M32
+
+
+
+
+ DefaultDisplay
+ X
+Y
+Z
+
+
+
+
+ System.Numerics.Matrix4x4
+
+
+ PSStandardMembers
+
+
+ DefaultDisplayPropertySet
+
+ X
+ Y
+ Z
+ W
+
+
+
+
+
+ CSS
+
+ return "matrix3d($(
+@(
+$this.M11
+$this.M12
+$this.M13
+$this.M14
+$this.M21
+$this.M22
+$this.M23
+$this.M24
+$this.M31
+$this.M32
+$this.M33
+$this.M34
+$this.M41
+$this.M42
+$this.M43
+$this.M44
+) -join ', '
+))"
+
+
+
+ Html
+
+ <#
+.SYNOPSIS
+ Gets Matrix Html
+.DESCRIPTION
+ Gets an HTML representation of the Matrix.
+
+ If there is an attached Content property, it will be rendered using the matrix.
+
+ Otherwise, the Matrix's MathML representation will be rendered twice:
+
+ * Once untransformed
+ * Once transformed using itself
+#>
+param()
+
+# If we have content
+if ($this.Content) {
+ # put it in the matrix.
+ "<section style='transform:$($this.CSS)'>"
+ if ($this.Content.OuterXml) {
+ $this.Content.OuterXml
+ } elseif ($this.Content.Html) {
+ $this.Content.Html
+ } else {
+ "$($this.Content)"
+ }
+ "</section>"
+} else {
+ # Otherwise,
+ $mathML = $this.MathML
+ $mathML.OuterXML # show the MathML
+ # and show it again, transformed.
+ $mathML.math.setAttribute('style', "transform:$($this.CSS)")
+ $mathML.OuterXML
+}
+
+
+
+ MathML
+
+ [xml]@"
+<math display='block'>
+<mo>[</mo>
+<mtable>
+$(
+ foreach ($row in 1..4) {
+ "<mtr>"
+ foreach ($col in 1..4) {
+ "<mtd><mn>$($this."M${row}${col}")</mn></mtd>"
+ }
+ "</mtr>"
+ }
+)
+</mtable>
+<mo>]</mo>
+</math>
+"@
+
+
+
+
+
+ Points
+
+ $this.M11
+$this.M12
+$this.M13
+$this.M14
+$this.M21
+$this.M22
+$this.M23
+$this.M24
+$this.M31
+$this.M32
+$this.M33
+$this.M34
+$this.M41
+$this.M42
+$this.M43
+$this.M44
+
+
+
+
+ DefaultDisplay
+ X
+Y
+Z
+W
+
+
+
+
+ System.Numerics.Quaternion
+
+
+ PSStandardMembers
+
+
+ DefaultDisplayPropertySet
+
+ X
+ Y
+ Z
+ W
+
+
+
+
+
+ CSS
+
+ $matrix = [Numerics.Matrix4x4]::CreateFromQuaternion($this)
+return "matrix3d($(
+@(
+$matrix.M11
+$matrix.M12
+$matrix.M13
+$matrix.M14
+$matrix.M21
+$matrix.M22
+$matrix.M23
+$matrix.M24
+$matrix.M31
+$matrix.M32
+$matrix.M33
+$matrix.M34
+$matrix.M41
+$matrix.M42
+$matrix.M43
+$matrix.M44
+) -join ', '
+))"
+
+
+
+ Html
+
+ <#
+.SYNOPSIS
+ Gets Matrix Html
+.DESCRIPTION
+ Gets an HTML representation of the Matrix.
+
+ If there is an attached Content property, it will be rendered using the matrix.
+
+ Otherwise, the Matrix's MathML representation will be rendered twice:
+
+ * Once untransformed
+ * Once transformed using itself
+#>
+param()
+
+# If we have content
+if ($this.Content) {
+ # put it in the matrix.
+ "<section style='transform:$($this.CSS)'>"
+ if ($this.Content.OuterXml) {
+ $this.Content.OuterXml
+ } elseif ($this.Content.Html) {
+ $this.Content.Html
+ } else {
+ "$($this.Content)"
+ }
+ "</section>"
+} else {
+ # Otherwise,
+ $mathML = $this.MathML
+ $mathML.OuterXML # show the MathML
+ # and show it again, transformed.
+ $mathML.math.setAttribute('style', "transform:$($this.CSS)")
+ $mathML.OuterXML
+}
+
+
+
+ MathML
+
+ <#
+.SYNOPSIS
+ Matrix MathML
+.DESCRIPTION
+ Gets the Matrix as a MathML representation of itself.
+.NOTES
+ Also shows the equivalent `[Numerics.Matrtix4x4]`
+#>
+param()
+[xml]@"
+<math display='block'>
+<mo>[</mo>
+<mtable>
+$(
+ "<mtr>"
+ foreach ($var in 'X','Y','Z','W') {
+ "<mtd>"
+ "<mi>$var</mi>"
+ "<mo>:</mo>"
+ "<mn>$($this.$var)</mn>"
+ "</mtd>"
+ }
+ "</mtr>"
+)
+</mtable>
+<mo>]</mo>
+<mo>=</mo>$(
+ [Numerics.Matrix4x4]::CreateFromQuaternion($this).MathML.math.innerXml
+)
+</math>
+"@
+
+
+
+
+
+ DefaultDisplay
+ X
+Y
+Z
+W
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 4bc0bbd..5847cab 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,306 @@
# Matrix
-Matrix Transforms with PowerShell
+[](https://www.powershellgallery.com/packages/Matrix/)
+## Matrix Transforms with PowerShell
+Matrix math is tedious. This module lets us avoid having to do it.
+
+We can represent changes in space using a matrix.
+
+In CSS, these are called [Transform Functions](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/transform-function)
+
+|Dimension|Transform Function|.NET type|
+|-|-|-|
+|`2D`|`matrix()`|`[Numerics.Matrix3x2]`|
+|`3D`|`matrix3d()`|`[Numerics.Matrix4x4]`|
+
+
+
+The module allows you to make, modify, and use matrix transforms.
+
+It supports almost identical syntax to the CSS.
+
+We can use Matrix to make CSS transforms,
+and we can manipulate objects in 2D or 3D the same way a webpage would.
+
+## Installing and Importing
+
+You can install Matrix from the [PowerShell gallery](https://powershellgallery.com/)
+
+~~~PowerShell
+Install-Module Matrix -Scope CurrentUser -Force
+~~~
+
+Once installed, you can import the module with:
+
+~~~PowerShell
+Import-Module Matrix -PassThru
+~~~
+
+
+You can also clone the repo and import the module locally:
+
+~~~PowerShell
+git clone https://github.com/PoshWeb/Matrix/
+cd ./Matrix
+Import-Module ./ -PassThru
+~~~
+
+## Functions
+Matrix has 1 function
+### Get-Matrix
+#### Matrix
+
+Makes and Manipulates Matrix Transformations.
+
+Matrix Transformations move objects in space.
+
+Matrix makes matrixes in PowerShell.
+
+This can transform objects in 2D, 3D, and 4D
+
+We can use matrix to make CSS or transform Vectors.
+
+
+Notes
+
+Matrix math is hard, and this module lets us avoid having to do it.
+
+Instead, we can pipe objects into this module and transform them.
+
+A Matrix in .NET is the same as a Matrix in CSS.
+
+|css function|.NET type|
+|-|-|
+|`matrix()`|`[Numerics.Matrix3x2]`|
+|`matrix3d()`|`[Numerics.Matrix4x4]`|
+
+This means we can do every transformation that CSS can do.
+
+Any object piped with a `Transform` static method will be transformed.
+
+Other objects will be passed thru.
+
+
+
+
+Aliases
+
+- Matrix2d
+- Matrix3d
+- Matrix3x2
+- Matrix4x4
+- Rotate
+- Rotate3d
+- RotateX
+- RotateY
+- RotateZ
+- Scale
+- Scale3d
+- ScaleX
+- ScaleY
+- ScaleZ
+- Skew
+- SkewX
+- SkewY
+- Translate
+- Translate3d
+- TranslateX
+- TranslateY
+- TranslateZ
+
+
+
+
+Examples
+
+#### Example 1
+
+Get the identity matrix.
+This is the object, untransformed, in 2D
+~~~PowerShell
+Matrix Identity
+~~~
+
+
+#### Example 2
+
+Gets a 3d identity matrix
+This is the object, untransformed, in 3d.
+~~~PowerShell
+Matrix3D Identity
+~~~
+
+
+#### Example 3
+
+Scale a point in 2d space by directly calling `::CreateScale`
+~~~PowerShell
+[Numerics.Vector2]::new(1,1) |
+ Matrix 2 -Member CreateScale 1 2
+~~~
+
+
+#### Example 4
+
+Scale a point in 2d space by using `scale`
+~~~PowerShell
+[Numerics.Vector2]::new(1,1) |
+ Scale 1 2
+~~~
+
+
+#### Example 5
+
+Skew a point
+~~~PowerShell
+[Numerics.Vector2]::new(1,1) |
+ Skew 30deg 10deg
+~~~
+
+
+#### Example 6
+
+Skew a point along X, then along Y
+~~~PowerShell
+[Numerics.Vector2]::new(1,1) |
+ SkewX 30deg |
+ SkewY 10deg
+~~~
+
+
+#### Example 7
+
+~~~PowerShell
+[Numerics.Vector2]::new(1,1) |
+ ScaleZ 1
+~~~
+
+
+#### Example 8
+
+Scale X in 2D
+~~~PowerShell
+[Numerics.Vector2]::new(1,1) |
+ ScaleX 2
+~~~
+
+
+#### Example 9
+
+Scale X in 3D
+~~~PowerShell
+[Numerics.Vector3]::new(1,1,1) |
+ ScaleX 2
+~~~
+
+
+#### Example 10
+
+Scale Y in 3D
+~~~PowerShell
+[Numerics.Vector2]::new(1,1) |
+ ScaleY 2
+~~~
+
+
+#### Example 11
+
+Scale Z in 3D
+~~~PowerShell
+[Numerics.Vector3]::new(1,1,1) |
+ ScaleZ 3
+~~~
+
+
+#### Example 12
+
+Move a point in 3d
+~~~PowerShell
+[Numerics.Vector3]::new(1,1,1) |
+ TranslateX 3 |
+ TranslateY 3 |
+ TranslateZ 3
+~~~
+
+
+#### Example 13
+
+Move and scale a point in 3d
+~~~PowerShell
+[Numerics.Vector3]::new(1,1,1) |
+ Translate3d 1 2 5 |
+ Scale3d 3 2 1
+~~~
+
+
+#### Example 14
+
+~~~PowerShell
+[Numerics.Vector3]::new(1,1,1) |
+ Translate3d 1 2 5 |
+ Scale3d 3 2 1
+~~~
+
+
+#### Example 15
+
+Rotate3d
+~~~PowerShell
+[Numerics.Vector3]::new(1,1,1) |
+ Rotate3d 1 1 1 30deg
+~~~
+
+
+#### Example 16
+
+rotate3d as a matrix3d, as CSS
+~~~PowerShell
+(Rotate3d 1 1 1 30deg).css
+~~~
+
+
+#### Example 17
+
+Constructing a cube using translation
+Make a corner point
+~~~PowerShell
+$corner = [Numerics.Vector3]::new(1,1,1)
+
+# Make a square by translating along X and Y
+$square = @(
+ $corner
+ $corner | TranslateX 1
+ $corner | TranslateY 1
+ $corner | TranslateX 1 | TranslateY 1
+)
+
+# Make a cube by translating the square along Z.
+$cube = @(
+ $square
+ $square |
+ TranslateZ 1
+)
+
+$cube
+~~~
+
+
+
+Parameters
+
+|Name|Type|Description|
+|-|-|-|
+|ArgumentList|Object[]|Any arguments for the transform function Arguments can include numbers of CSS units. `deg` and `turn` are converted into radians `%` becomes a value between 0 and 1|
+|InputObject|PSObject[]|Any input objects. If the input object has a `Transform` static method, it will be transformed. If it does not, it will be passed thru.|
+|Member|String|The name of the method or property of a matrix transform. If this is provided, this method will be called instead. Many aliases, such as `Skew` or `Rotate`, will use a custom member and will ignore this parameter.|
+
+
+
+Links
+
+* [ PoshWeb Matrix (GitHub)](https://github.com/PoshWeb/Matrix)
+* [system.numerics.matrix4x4 (Learn DotNet)](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.matrix4x4?wt.mc_id=MVP_321542)
+* [CSS matrix3D (MDN)](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/transform-function/matrix3D)
+* [system.numerics.matrix3x2 (Learn DotNet)](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.matrix3x2?wt.mc_id=MVP_321542)
+* [CSS matrix (MDN)](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/transform-function/matrix)
+
+
diff --git a/README.md.ps1 b/README.md.ps1
new file mode 100644
index 0000000..33b54d4
--- /dev/null
+++ b/README.md.ps1
@@ -0,0 +1,305 @@
+<#
+.SYNOPSIS
+ README.md.ps1
+.DESCRIPTION
+ README.md.ps1 makes README.md
+
+ This is a simple and helpful scripting convention for writing READMEs.
+
+ `./README.md.ps1 > ./README.md`
+
+ Feel free to copy and paste this code.
+
+ Please document your parameters, and add NOTES.
+.NOTES
+ This README.md.ps1 is used to generate help for a module.
+
+ It:
+
+ * Outputs the name and description
+ * Provides installation instructions
+ * Lists commands
+ * Includes aliases
+ * Includes notes
+ * Lists parameters
+ * Lists examples
+ * Lists links
+.EXAMPLE
+ ./README.md.ps1 > ./README.md
+.EXAMPLE
+ Get-Help ./README.md.ps1
+#>
+param(
+# The name of the module
+[string]$ModuleName = $($PSScriptRoot | Split-Path -Leaf),
+
+# The domains that serve git repositories.
+# If the project uri links to this domain,
+# installation instructions will show how to import the module locally.
+[string[]]
+$GitDomains = @(
+ 'github.com', 'tangled.org', 'tangled.sh', 'codeberg.org'
+),
+
+# A list of types the module exposes
+[Alias('ModuleTypeNames','ModuleTypes')]
+[string[]]
+$ModuleTypeName = @(),
+
+# If set, we don't need no badges.
+[switch]
+$NoBadge,
+
+# If set, will not display gallery instructions or badges
+[switch]
+$NotOnGallery
+)
+
+Push-Location $PSScriptRoot
+
+# Import the module
+$module = Import-Module "./$ModuleName.psd1" -PassThru
+
+# And output a header
+"# $module"
+
+if (-not $NoBadge) {
+ # If it is on the gallery, show the downloads badge.
+ if (-not $NotOnGallery) {
+ @(
+ "[!"
+ "[$ModuleName](https://img.shields.io/powershellgallery/dt/$ModuleName)"
+ "](https://www.powershellgallery.com/packages/$ModuleName/)"
+ ) -join ''
+ }
+}
+
+# Show the module description
+"## $($module.Description)"
+
+# Show any intro section defined in the manifest
+$module.PrivateData.PSData.PSIntro
+
+#region Boilerplate installation instructions
+if (-not $NotOnGallery) {
+@"
+
+## Installing and Importing
+
+You can install $ModuleName from the [PowerShell gallery](https://powershellgallery.com/)
+
+~~~PowerShell
+Install-Module $($ModuleName) -Scope CurrentUser -Force
+~~~
+
+Once installed, you can import the module with:
+
+~~~PowerShell
+Import-Module $ModuleName -PassThru
+~~~
+
+"@
+}
+#endregion Gallery installation instructions
+
+#region Git installation instructions
+$projectUri = $module.PrivateData.PSData.ProjectURI -as [uri]
+
+if ($projectUri.DnsSafeHost -in $GitDomains) {
+@"
+
+You can also clone the repo and import the module locally:
+
+~~~PowerShell
+git clone $projectUri
+cd ./$ModuleName
+Import-Module ./ -PassThru
+~~~
+
+"@
+}
+#endregion Git installation instructions
+
+#region Exported Functions
+$exportedFunctions = $module.ExportedFunctions
+
+$uniqueFunctions = @()
+$uniqueNames = @(foreach ($function in $exportedFunctions.GetEnumerator()) {
+ if ($uniqueFunctions -contains $function.Value.ScriptBlock) {
+ continue
+ } else {
+ $uniqueFunctions += $function.Value.ScriptBlock
+ }
+ $function.Key
+})
+
+
+if ($uniqueNames) {
+
+ "## Functions"
+
+ "$($ModuleName) has $($uniqueNames.Count) function$(
+ if ($uniqueNames.Count -gt 1) { "s"}
+ )"
+
+ $duplicate = @()
+ foreach ($export in $uniqueNames) {
+ $exportedFunction = $exportedFunctions[$export]
+ if ($duplicate -contains $exportedFunction.ScriptBlock) {
+ continue
+ } else {
+ $duplicate += $exportedFunction.ScriptBlock
+ }
+ # Get help if it there is help to get
+ $help = Get-Help $export
+ # If the help is a string,
+ if ($help -is [string]) {
+ # make it preformatted text
+ "~~~"
+ "$export"
+ "~~~"
+ } else {
+ # Otherwise, add list the export
+ "### $($export)"
+
+ # And make it's synopsis a header
+ "#### $($help.SYNOPSIS)"
+
+ ""
+ # put the description below that
+ "$($help.Description.text -join [Environment]::NewLine)"
+ ""
+
+ $commandAliases = foreach ($aliasName in $module.ExportedAliases.Keys) {
+ if ($module.ExportedAliases[$aliasName].ResolvedCommand.ScriptBlock -eq
+ $exportedFunction.ScriptBlock) {
+ $aliasName
+ }
+ }
+
+ $notes = $help.alertSet.alert.text
+
+ if ($notes) {
+ ""
+ "Notes "
+ ""
+ $notes -join [Environment]::NewLine
+ ""
+ " "
+ }
+
+ if ($commandAliases) {
+ ""
+ ""
+ "Aliases "
+ ""
+ foreach ($commandAlias in $commandAliases) {
+ "- $commandAlias"
+ }
+ " "
+ ""
+ ""
+
+ }
+
+ # Show our examples
+ if ($help.examples.example) {
+ ""
+ "Examples "
+
+ $exampleNumber = 0
+ foreach ($example in $help.examples.example) {
+ $markdownLines = @()
+ $exampleNumber++
+ $nonCommentLine = $false
+ ""
+ "#### Example $exampleNumber"
+ ""
+
+ # Combine the code and remarks
+ $exampleLines =
+ @(
+ $example.Code
+ foreach ($remark in $example.Remarks.text) {
+ if (-not $remark) { continue }
+ $remark
+ }
+ ) -join ([Environment]::NewLine) -split '(?>\r\n|\n)' # and split into lines
+
+ # Go thru each line in the example as part of a loop
+ $codeBlock = @(foreach ($exampleLine in $exampleLines) {
+ # Any comments until the first uncommentedLine are markdown
+ if ($exampleLine -match '^\#' -and -not $nonCommentLine) {
+ $markdownLines += $exampleLine -replace '^\#\s{0,1}'
+ } else {
+ $nonCommentLine = $true
+ $exampleLine
+ }
+ }) -join [Environment]::NewLine
+
+ $markdownLines
+ "~~~PowerShell"
+ $CodeBlock
+ "~~~"
+ ""
+ }
+ " "
+ }
+
+ # Make a table of parameters
+ if ($help.parameters.parameter) {
+ ""
+
+ "Parameters "
+
+ ""
+
+ "|Name|Type|Description|"
+ "|-|:-:|-|"
+ foreach ($parameter in $help.Parameters.Parameter) {
+ "|$($parameter.Name)|$($parameter.type.name)|$(
+ $parameter.description.text -replace '(?>\r\n|\n)', ' '
+ )|"
+ }
+
+ " "
+ ""
+ }
+
+ $relatedUris = foreach ($link in $help.relatedLinks.navigationLink) {
+ if ($link.uri) {
+ $link.uri
+ }
+ }
+ if ($relatedUris) {
+ ""
+ "Links "
+ ""
+ foreach ($related in $relatedUris) {
+ $relatedUri = $related -as [uri]
+ if ($relatedUri.DnsSafeHost -eq 'learn.microsoft.com' -and
+ $relatedUri.LocalPath -match '/dotnet/api') {
+ "* [$($relatedUri.segments[-1] -replace '/') (Learn DotNet)]($related)"
+ }
+ elseif ($relatedUri.DnsSafeHost -eq 'developer.mozilla.org' -and
+ $relatedUri.LocalPath -match '/(?[[^/]+)/reference') {
+ "* [$($matches.ref) $($relatedUri.segments[-1] -replace '/') (MDN)]($related)"
+ }
+ elseif ($relatedUri.DnsSafeHost -eq 'github.com') {
+ "* [$($relatedUri.Segments -replace '/', ' ') (GitHub)]($related)"
+ }
+ elseif ($relatedUri.DnsSafeHost) {
+ "* [$($relatedUri.DnsSafeHost)$($relatedUri.LocalPath)]($related)"
+ } else {
+ "* [$related]($related)"
+ }
+ }
+ ""
+ "] "
+ }
+ }
+ }
+}
+#endregion Exported Functions
+
+Pop-Location
\ No newline at end of file
diff --git a/Types/Matrix3x2/DefaultDisplay.txt b/Types/Matrix3x2/DefaultDisplay.txt
new file mode 100644
index 0000000..1770757
--- /dev/null
+++ b/Types/Matrix3x2/DefaultDisplay.txt
@@ -0,0 +1,3 @@
+X
+Y
+Z
\ No newline at end of file
diff --git a/Types/Matrix3x2/PSTypeName.txt b/Types/Matrix3x2/PSTypeName.txt
new file mode 100644
index 0000000..7c3f671
--- /dev/null
+++ b/Types/Matrix3x2/PSTypeName.txt
@@ -0,0 +1 @@
+System.Numerics.Matrix3x2
\ No newline at end of file
diff --git a/Types/Matrix3x2/get_CSS.ps1 b/Types/Matrix3x2/get_CSS.ps1
new file mode 100644
index 0000000..30a7273
--- /dev/null
+++ b/Types/Matrix3x2/get_CSS.ps1
@@ -0,0 +1,10 @@
+return "matrix($(
+@(
+$this.M11
+$this.M12
+$this.M21
+$this.M22
+$this.M31
+$this.M32
+) -join ', '
+))"
\ No newline at end of file
diff --git a/Types/Matrix3x2/get_Html.ps1 b/Types/Matrix3x2/get_Html.ps1
new file mode 100644
index 0000000..3f645c7
--- /dev/null
+++ b/Types/Matrix3x2/get_Html.ps1
@@ -0,0 +1,35 @@
+<#
+.SYNOPSIS
+ Gets Matrix Html
+.DESCRIPTION
+ Gets an HTML representation of the Matrix.
+
+ If there is an attached Content property, it will be rendered using the matrix.
+
+ Otherwise, the Matrix's MathML representation will be rendered twice:
+
+ * Once untransformed
+ * Once transformed using itself
+#>
+param()
+
+# If we have content
+if ($this.Content) {
+ # put it in the matrix.
+ ""
+ if ($this.Content.OuterXml) {
+ $this.Content.OuterXml
+ } elseif ($this.Content.Html) {
+ $this.Content.Html
+ } else {
+ "$($this.Content)"
+ }
+ " "
+} else {
+ # Otherwise,
+ $mathML = $this.MathML
+ $mathML.OuterXML # show the MathML
+ # and show it again, transformed.
+ $mathML.math.setAttribute('style', "transform:$($this.CSS)")
+ $mathML.OuterXML
+}
\ No newline at end of file
diff --git a/Types/Matrix3x2/get_MathML.ps1 b/Types/Matrix3x2/get_MathML.ps1
new file mode 100644
index 0000000..1ab2d52
--- /dev/null
+++ b/Types/Matrix3x2/get_MathML.ps1
@@ -0,0 +1,28 @@
+<#
+.SYNOPSIS
+ Matrix MathML
+.DESCRIPTION
+ Gets the Matrix as a MathML representation of itself.
+.NOTES
+ Also shows the equivalent `[Numerics.Matrtix4x4]`
+#>
+[xml]@"
+
+ [
+
+ $(
+ foreach ($row in 1..3) {
+ ""
+ foreach ($col in 1..2) {
+ "$($this."M${row}${col}") "
+ }
+ " "
+ }
+ )
+
+ ]
+ = $(
+ [Numerics.Matrix4x4]::Create($this).MathML.math.innerXml
+ )
+
+"@
diff --git a/Types/Matrix3x2/get_Points.ps1 b/Types/Matrix3x2/get_Points.ps1
new file mode 100644
index 0000000..3a0654c
--- /dev/null
+++ b/Types/Matrix3x2/get_Points.ps1
@@ -0,0 +1,6 @@
+$this.M11
+$this.M12
+$this.M21
+$this.M22
+$this.M31
+$this.M32
diff --git a/Types/Matrix4x4/DefaultDisplay.txt b/Types/Matrix4x4/DefaultDisplay.txt
new file mode 100644
index 0000000..8874cd0
--- /dev/null
+++ b/Types/Matrix4x4/DefaultDisplay.txt
@@ -0,0 +1,4 @@
+X
+Y
+Z
+W
\ No newline at end of file
diff --git a/Types/Matrix4x4/PSTypeName.txt b/Types/Matrix4x4/PSTypeName.txt
new file mode 100644
index 0000000..06bc61b
--- /dev/null
+++ b/Types/Matrix4x4/PSTypeName.txt
@@ -0,0 +1 @@
+System.Numerics.Matrix4x4
\ No newline at end of file
diff --git a/Types/Matrix4x4/get_CSS.ps1 b/Types/Matrix4x4/get_CSS.ps1
new file mode 100644
index 0000000..9654be3
--- /dev/null
+++ b/Types/Matrix4x4/get_CSS.ps1
@@ -0,0 +1,20 @@
+return "matrix3d($(
+@(
+$this.M11
+$this.M12
+$this.M13
+$this.M14
+$this.M21
+$this.M22
+$this.M23
+$this.M24
+$this.M31
+$this.M32
+$this.M33
+$this.M34
+$this.M41
+$this.M42
+$this.M43
+$this.M44
+) -join ', '
+))"
\ No newline at end of file
diff --git a/Types/Matrix4x4/get_Html.ps1 b/Types/Matrix4x4/get_Html.ps1
new file mode 100644
index 0000000..3f645c7
--- /dev/null
+++ b/Types/Matrix4x4/get_Html.ps1
@@ -0,0 +1,35 @@
+<#
+.SYNOPSIS
+ Gets Matrix Html
+.DESCRIPTION
+ Gets an HTML representation of the Matrix.
+
+ If there is an attached Content property, it will be rendered using the matrix.
+
+ Otherwise, the Matrix's MathML representation will be rendered twice:
+
+ * Once untransformed
+ * Once transformed using itself
+#>
+param()
+
+# If we have content
+if ($this.Content) {
+ # put it in the matrix.
+ ""
+ if ($this.Content.OuterXml) {
+ $this.Content.OuterXml
+ } elseif ($this.Content.Html) {
+ $this.Content.Html
+ } else {
+ "$($this.Content)"
+ }
+ " "
+} else {
+ # Otherwise,
+ $mathML = $this.MathML
+ $mathML.OuterXML # show the MathML
+ # and show it again, transformed.
+ $mathML.math.setAttribute('style', "transform:$($this.CSS)")
+ $mathML.OuterXML
+}
\ No newline at end of file
diff --git a/Types/Matrix4x4/get_MathML.ps1 b/Types/Matrix4x4/get_MathML.ps1
new file mode 100644
index 0000000..d5ec8ad
--- /dev/null
+++ b/Types/Matrix4x4/get_MathML.ps1
@@ -0,0 +1,18 @@
+[xml]@"
+
+[
+
+$(
+ foreach ($row in 1..4) {
+ ""
+ foreach ($col in 1..4) {
+ "$($this."M${row}${col}") "
+ }
+ " "
+ }
+)
+
+]
+
+"@
+
diff --git a/Types/Matrix4x4/get_Points.ps1 b/Types/Matrix4x4/get_Points.ps1
new file mode 100644
index 0000000..c1e7864
--- /dev/null
+++ b/Types/Matrix4x4/get_Points.ps1
@@ -0,0 +1,16 @@
+$this.M11
+$this.M12
+$this.M13
+$this.M14
+$this.M21
+$this.M22
+$this.M23
+$this.M24
+$this.M31
+$this.M32
+$this.M33
+$this.M34
+$this.M41
+$this.M42
+$this.M43
+$this.M44
diff --git a/Types/Quaternion/DefaultDisplay.txt b/Types/Quaternion/DefaultDisplay.txt
new file mode 100644
index 0000000..8874cd0
--- /dev/null
+++ b/Types/Quaternion/DefaultDisplay.txt
@@ -0,0 +1,4 @@
+X
+Y
+Z
+W
\ No newline at end of file
diff --git a/Types/Quaternion/PSTypeName.txt b/Types/Quaternion/PSTypeName.txt
new file mode 100644
index 0000000..91fe4e6
--- /dev/null
+++ b/Types/Quaternion/PSTypeName.txt
@@ -0,0 +1 @@
+System.Numerics.Quaternion
\ No newline at end of file
diff --git a/Types/Quaternion/get_CSS.ps1 b/Types/Quaternion/get_CSS.ps1
new file mode 100644
index 0000000..5df9ac4
--- /dev/null
+++ b/Types/Quaternion/get_CSS.ps1
@@ -0,0 +1,21 @@
+$matrix = [Numerics.Matrix4x4]::CreateFromQuaternion($this)
+return "matrix3d($(
+@(
+$matrix.M11
+$matrix.M12
+$matrix.M13
+$matrix.M14
+$matrix.M21
+$matrix.M22
+$matrix.M23
+$matrix.M24
+$matrix.M31
+$matrix.M32
+$matrix.M33
+$matrix.M34
+$matrix.M41
+$matrix.M42
+$matrix.M43
+$matrix.M44
+) -join ', '
+))"
\ No newline at end of file
diff --git a/Types/Quaternion/get_Html.ps1 b/Types/Quaternion/get_Html.ps1
new file mode 100644
index 0000000..3f645c7
--- /dev/null
+++ b/Types/Quaternion/get_Html.ps1
@@ -0,0 +1,35 @@
+<#
+.SYNOPSIS
+ Gets Matrix Html
+.DESCRIPTION
+ Gets an HTML representation of the Matrix.
+
+ If there is an attached Content property, it will be rendered using the matrix.
+
+ Otherwise, the Matrix's MathML representation will be rendered twice:
+
+ * Once untransformed
+ * Once transformed using itself
+#>
+param()
+
+# If we have content
+if ($this.Content) {
+ # put it in the matrix.
+ ""
+ if ($this.Content.OuterXml) {
+ $this.Content.OuterXml
+ } elseif ($this.Content.Html) {
+ $this.Content.Html
+ } else {
+ "$($this.Content)"
+ }
+ " "
+} else {
+ # Otherwise,
+ $mathML = $this.MathML
+ $mathML.OuterXML # show the MathML
+ # and show it again, transformed.
+ $mathML.math.setAttribute('style', "transform:$($this.CSS)")
+ $mathML.OuterXML
+}
\ No newline at end of file
diff --git a/Types/Quaternion/get_MathML.ps1 b/Types/Quaternion/get_MathML.ps1
new file mode 100644
index 0000000..96621d0
--- /dev/null
+++ b/Types/Quaternion/get_MathML.ps1
@@ -0,0 +1,32 @@
+<#
+.SYNOPSIS
+ Matrix MathML
+.DESCRIPTION
+ Gets the Matrix as a MathML representation of itself.
+.NOTES
+ Also shows the equivalent `[Numerics.Matrtix4x4]`
+#>
+param()
+[xml]@"
+
+[
+
+$(
+ ""
+ foreach ($var in 'X','Y','Z','W') {
+ ""
+ "$var "
+ ": "
+ "$($this.$var) "
+ " "
+ }
+ " "
+)
+
+]
+= $(
+ [Numerics.Matrix4x4]::CreateFromQuaternion($this).MathML.math.innerXml
+)
+
+"@
+
diff --git a/_includes/CopyCode.ps1 b/_includes/CopyCode.ps1
new file mode 100644
index 0000000..3440e22
--- /dev/null
+++ b/_includes/CopyCode.ps1
@@ -0,0 +1,22 @@
+<#
+.SYNOPSIS
+ Includes a CopyCode button
+.DESCRIPTION
+ Includes a copy-to-clipboard near all code blocks.
+#>
+[OutputType('text/html')]
+param()
+""
+"
+
+"
\ No newline at end of file
diff --git a/_includes/FeatherIcon.ps1 b/_includes/FeatherIcon.ps1
new file mode 100644
index 0000000..3444055
--- /dev/null
+++ b/_includes/FeatherIcon.ps1
@@ -0,0 +1,44 @@
+<#
+.SYNOPSIS
+ Includes Feather Icons
+.DESCRIPTION
+ Includes a feather icon in the site.
+.NOTES
+ Icons will be cached in memory to avoid repeated CDN requests.
+.EXAMPLE
+ /_includes/FeatherIcon clipboard
+.LINK
+ https://feathericons.com/
+#>
+[OutputType('image/svg+xml')]
+param(
+# The feather icon name
+[string]
+$Icon = 'terminal',
+
+[uri]
+$FeatherCDN = "https://cdn.jsdelivr.net/gh/feathericons/feather@latest/icons/"
+)
+
+if (-not $script:FeatherIconCache) {
+ $script:FeatherIconCache = [Ordered]@{}
+}
+
+$iconUri =
+ (
+ $FeatherCDN -replace '^https?://' -replace '^',
+ 'https://' -replace '/$'
+ ), (
+ $icon.ToLower() -replace '\.svg$' -replace '^/' -replace '$' -replace '\s',
+ '-' -replace '$', '.svg'
+ ) -join '/'
+
+if (-not $script:FeatherIconCache[$iconUri]) {
+ $script:FeatherIconCache[$iconUri] = try {
+ Invoke-RestMethod $iconUri
+ } catch {
+ Write-Warning "Could not get $iconUri : $_"
+ }
+}
+
+$script:FeatherIconCache[$iconUri].OuterXml
diff --git a/_includes/Menu.ps1 b/_includes/Menu.ps1
new file mode 100644
index 0000000..77ca04a
--- /dev/null
+++ b/_includes/Menu.ps1
@@ -0,0 +1,56 @@
+<#
+.SYNOPSIS
+ Includes a Menu
+.DESCRIPTION
+
+
+#>
+param(
+[PSObject]
+$Menu,
+
+[string]
+$Name
+)
+
+
+filter menuToHtml {
+
+ $menu = $_
+
+ foreach ($property in $Menu.psobject.properties) {
+ ""
+ if ($property.Value -is [string] -and
+ $property.Value -match '^/' -or $property.Value -is [uri]) {
+ "$(
+ if ($property.Name -notmatch '^<') {
+ [Web.HttpUtility]::HtmlEncode($property.Name)
+ } else {
+ $property.Name
+ }
+ ) "
+ } else {
+ ""
+ "$(
+ if ($property.Name -notmatch '^<') {
+ [Web.HttpUtility]::HtmlEncode($property.Name)
+ } else {
+ $property.Name
+ }
+ ) "
+ ""
+ $property.Value | menuToHtml
+ " "
+ " "
+ }
+ " "
+ }
+}
+
+""
+ " "
+ $menu | menuToHtml
+ " "
+""
\ No newline at end of file
diff --git a/_includes/Palette.ps1 b/_includes/Palette.ps1
new file mode 100644
index 0000000..407dc64
--- /dev/null
+++ b/_includes/Palette.ps1
@@ -0,0 +1,147 @@
+<#
+.SYNOPSIS
+ Includes a palette selector
+.DESCRIPTION
+ Includes a palette selector and randomized palette switcher.
+
+ This allows the page to use multiple color palettes.
+#>
+param(
+# The source for all palette information
+[uri]
+$AllPalettesSource = 'https://4bitcss.com/Palettes.json',
+
+# The Palette CDN. This is the root URL of all palettes.
+[uri]
+$PaletteCDN = 'https://cdn.jsdelivr.net/gh/2bitdesigns/4bitcss@latest/css/',
+
+# The identifier for the palette ``.
+[string]
+$SelectPaletteId = 'SelectPalette',
+
+# The identifier for the stylesheet. By default, palette.
+[string]
+$PaletteId = 'palette',
+
+[string]
+$DefaultPalette = $(
+ if ($page.Palette) {
+ $page.Palette
+ } elseif ($site.Palette) {
+ $site.Palette
+ }
+ else {
+ ''
+ }
+)
+)
+
+
+$JavaScript = @"
+function SetPalette(event) {
+ var palette = document.getElementById('$PaletteId')
+ if (! palette) {
+ palette = document.createElement('link')
+ palette.rel = 'stylesheet'
+ palette.id = 'palette'
+ document.head.appendChild(palette)
+ }
+ var selectedPalette = event.target.value
+ palette.href = '$PaletteCDN' + selectedPalette + '.css'
+}
+"@
+
+if (-not $script:AllPalettes) {
+ $script:AllPalettes = Invoke-RestMethod $AllPalettesSource
+}
+
+if (-not $script:Cache) {
+ $script:Cache = [Ordered]@{}
+}
+
+
+$paletteSquare = "
+
+ $(
+ $colors =
+ 'black', 'red', 'green', 'blue', 'yellow', 'cyan', 'purple', 'white'
+
+ $colors = @($colors) + @(
+ foreach ($color in $colors) {
+ "bright$(
+ $color.Substring(0,1).ToUpper()
+ )$($color.Substring(1))"
+ }
+ )
+ for ($colorNumber =0;$colorNumber -lt $colors.Count; $colorNumber++) {
+ $row = [Math]::Floor($colorNumber / 4)
+ $column = $colorNumber % 4
+ $color = $colors[$colorNumber]
+ " "
+ }
+ )
+
+"
+
+$paletteSelector = @"
+
+
+
+
+$(
+ foreach ($palette in $script:AllPalettes.psobject.Properties) {
+ $paletteName = $palette.name
+ $palette = $palette.value
+ $selectedPalette = if ($defaultPalette -and $defaultPalette -eq $paletteName) { " selected='true'"} else { '' }
+ "$([Web.HttpUtility]::HtmlEncode($paletteName)) "
+ }
+)
+
+$(
+ /_includes/FeatherIcon -Icon refresh-cw
+ $paletteSquare
+)
+
+
+"@
+
+$HTML = @"
+
+$PaletteSelector
+"@
+
+$HTML
\ No newline at end of file
diff --git a/_includes/Sitemap.ps1 b/_includes/Sitemap.ps1
new file mode 100644
index 0000000..04ddbc7
--- /dev/null
+++ b/_includes/Sitemap.ps1
@@ -0,0 +1,86 @@
+<#
+.SYNOPSIS
+ Includes a Sitemap
+.DESCRIPTION
+ Includes a sitemap
+.LINK
+ https://en.wikipedia.org/wiki/Sitemaps
+#>
+[OutputType('application/xml')]
+param(
+# A root url for the website
+[uri]
+$Url,
+
+# A collection of all pages
+# The keys should be the urls.
+[Alias('Pages')]
+[Collections.IDictionary]
+$PagesByUrl,
+
+# An optional list of items to disallow
+[SupportsWildcards()]
+[Alias('Hide','Hidden','NoIndex','NoSitemap')]
+[string[]]
+$Disallow
+)
+
+# If there were no pages, there is no sitemap
+if (-not $PagesByUrl.Count) {
+ return
+}
+
+
+# A sitemap is just a bit of XML
+$sitemap = @(
+ ''
+
+ :nextPage foreach ($key in $PagesByUrl.Keys) {
+ $keyUri = $key -as [Uri]
+ $page = $PagesByUrl[$key]
+
+ # Skip any page that is not html
+ if ($page.Extension -ne 'html') { continue }
+
+ # Skip any explicitly disallowed pages
+ if ($Disallow) {
+ foreach ($disallowed in $Disallow) {
+ if ($keyUri.LocalPath -like "*$disallowed*") { continue nextPage }
+ if ($keyUri.AbsoluteUri -like "*$disallowed*") { continue nextPage }
+ }
+ }
+
+ # If the page does not want to be indexed or sitemapped
+ # (or is explicitly hidden)
+ if ($page.NoIndex -or
+ $page.NoSitemap -or
+ $page.Hidden -or
+ $page.Hide
+ ) { continue } # continue.
+
+ # Otherwise, it's in the sitemap
+ ""
+
+ # If the url was already absolute
+ if ($keyUri.IsAbsoluteUri) {
+ "$key " # use it
+ } else {
+ # Otherwise, use our site url.
+ "$($url -replace '/$')/$($key -replace '^/') "
+ }
+
+ # If the page has a date
+ if ($PagesByUrl[$key].Date -is [DateTime]) {
+ # that will be it's last modified
+ "$($PagesByUrl[$key].Date.ToString('yyyy-MM-dd')) "
+ }
+
+ " "
+ }
+ ' '
+)
+
+# Cast our sitemap to XML.
+# This will return the sitemap, or throw an exception if the XML is invalid.
+[xml]$sitemap
+
diff --git a/assets/dodge-matrix.gif b/assets/dodge-matrix.gif
new file mode 100644
index 0000000..be4e4cf
Binary files /dev/null and b/assets/dodge-matrix.gif differ
diff --git a/config.ps1 b/config.ps1
new file mode 100644
index 0000000..9cebda9
--- /dev/null
+++ b/config.ps1
@@ -0,0 +1,14 @@
+# Aliasing /includes.
+Set-Alias /_includes/CopyCode ./_includes/CopyCode.ps1
+Set-Alias /_includes/Palette ./_includes/Palette.ps1
+Set-Alias /_includes/FeatherIcon ./_includes/FeatherIcon.ps1
+Set-Alias /_includes/Sitemap ./_includes/Sitemap.ps1
+Set-Alias /_includes/Menu ./_includes/Menu.ps1
+
+# Alias files we will use inline
+Set-Alias Matrix.css ./Matrix.css.ps1
+Set-Alias Matrix.svg ./Matrix.svg.ps1
+Set-Alias /Matrix.css ./Matrix.css.ps1
+Set-Alias /Matrix.svg ./Matrix.svg.ps1
+
+$env:PaletteName = 'cyberpunk', 'Neon' | Get-Random
\ No newline at end of file
diff --git a/deploy.ps1 b/deploy.ps1
new file mode 100644
index 0000000..5e4972c
--- /dev/null
+++ b/deploy.ps1
@@ -0,0 +1,474 @@
+<#
+.SYNOPSIS
+ `deploy.ps1`
+.DESCRIPTION
+ `deploy.ps1` deploys a website.
+
+ This is a simple and helpful scripting convention for site deployment.
+
+ Just run `./deploy.ps1`
+
+ `./deploy.ps1` can deploy any way we want.
+
+ This `./deploy.ps1` dynamically generates a static site.
+
+ Feel free to copy and paste this code.
+
+ Please document your parameters, and add NOTES.
+.NOTES
+ This deploys a static site by running any `*.*.ps1`.
+
+ The output from each file will go into a corresponding file.
+
+ For example: `a.css.ps1` will output `a.css`
+
+ * `*.html.ps1` files will be piped into `layout`
+ * `*.json.ps1` files will be converted to `json`
+
+ `html`, `json`, and `xml` files will be made into `index` files
+ (unless `-Ugly` links are preferred)
+#>
+[CmdletBinding(SupportsShouldProcess)]
+param(
+# An anlytics ID
+[string]$AnalyticsID,
+
+# The list of acceptable extensions
+[string[]]
+$AcceptableExtensions = @(
+ 'css',
+ 'md',
+ 'html',
+ 'js',
+ 'json',
+ 'svg',
+ 'xml'
+),
+
+# If set, will make "ugly" page links
+# This will make `a.html.ps1` generate `a.html`, instead of `/a/index.html`
+[switch]
+$Ugly,
+
+[string[]]
+$IndexExtension = @('html','xml','json'),
+
+# The root page url
+[uri]
+$PageUrl,
+
+# The site root
+[string]
+$SiteRoot
+)
+
+# Know thyself
+$mySelf = $MyInvocation.MyCommand
+
+if (-not $SiteRoot) {
+ $SiteRoot = $PSScriptRoot
+}
+
+# If there is no script root,
+if (-not $SiteRoot) {
+ # error out.
+ Write-Error "`$psScriptRoot is empty, Will not deploy"
+ return
+}
+
+# Push into $psScriptRoot so everything is root relative.
+Push-Location $PSScriptRoot
+
+#region Map Parameters from Environment
+
+# Look at our environment
+foreach ($env in Get-ChildItem env:) {
+ # See if any environment variables map to parameter
+ # (after we remove any punctuation from the name)
+ $envName = $env.Name -replace '\p{P}'
+ if (
+ # If they map
+ $mySelf.Parameters[$envName] -and
+ # and are not already bound
+ (-not $PSBoundParameters.ContainsKey($envName))
+ ) {
+ # set them
+ $ExecutionContext.SessionState.PSVariable.Set(
+ $envName,
+ $(
+ # If the value looks like json
+ if ($env.Value -match '^\s{0,}[\[\{]') {
+ # convert it before we set the value.
+ ConvertFrom-Json -InputObject $env.Value
+ }
+ # If the value is a boolean string
+ elseif ($env.Value -match '^true|false$') {
+ # convert it to a boolean
+ $evn.Value -match 'true'
+ }
+ # Otherwise, directly map it.
+ else {
+ $env.Value
+ }
+ )
+ )
+ # After we set the variable, map it into `$psBoundParameters`
+ $PSBoundParameters[$envName] =
+ $ExecutionContext.SessionState.PSVariable.Get(
+ $envName
+ ).Value
+ }
+}
+#endregion Map Parameters from Environment
+
+# Configs, Layouts, and Pages all may require modules
+# Make a little filter to import and install any module a script `#requires`.
+filter requireModule {
+ # Our input should be a file.
+ $in = $_
+
+ # Get the script at this location
+ $command = Get-Command $in.FullName -CommandType ExternalScript
+ # If that somehow failed, return
+ if (-not $command) { return }
+
+ # If the script has requirements
+ foreach ($requirement in
+ $command.ScriptBlock.Ast.ScriptRequirements.RequiredModules
+ ) {
+ # Check if they are loaded
+ $requiredModule = Get-Module -ErrorAction Ignore -Name $requirement.Name
+
+ # If they are not,
+ if (-not $requiredModule) {
+ # try to load the requirement
+ "Importing Requirement $($requirement.Name) for $($in.FullName)" |
+ Out-Host
+
+ $requiredModule = Import-Module $requiredModule -Force -PassThru -ErrorAction Ignore
+
+ # If that did not work,
+ if (-not $requiredModule) {
+ "Installing Requirement $($requirement.Name)" | Out-Host
+ Install-Module $requirement.Name -Scope CurrentUser -Force
+ Import-Module $requirement.Name -Global -Force -PassThru |
+ Out-Host
+ }
+ }
+ }
+}
+
+# Get whatever local module exists
+$psd1Path =
+ Get-ChildItem -Filter *.psd1 |
+ Select-String 'ModuleVersion' |
+ Select-Object -ExpandProperty Path -First 1
+
+# If one was found
+if ($psd1Path) {
+ # import it
+ "Importing $psd1Path" | Out-Host
+ Import-Module $psd1Path -Force -PassThru |
+ Out-Host
+}
+
+# Check for a layout script.
+$layout = Get-Command ./layout.ps1 -ErrorAction Ignore
+if ($layout) {
+ # Import any requirements it might have
+ if ($layout.ScriptBlock.Ast.ScriptRequirements) {
+ [IO.FileInfo]$layout.Source | . requireModule
+ }
+ # And alias it to `layout`.
+ Set-Alias layout $layout.Source
+}
+
+# Any `$site` metadata can be stored in a dictionary
+# Any of our parameters should be site wide metadata
+$site = [Ordered]@{} + $PSBoundParameters
+$Pages = [Ordered]@{}
+$PagesByUrl = [Ordered]@{}
+
+# A `config.ps1` file may do anything it wants to configure the site.
+
+# If there is a `.config.ps1`
+$configPs1 = Get-Command ./config.ps1 -ErrorAction Ignore
+if ($configPs1) {
+ # Import any requirements it might have
+ if ($configPs1.ScriptBlock.Ast.ScriptRequirements) {
+ [IO.FileInfo]$configPs1.Source | . requireModule
+ }
+ . ./config.ps1 # and run it.
+}
+
+# We will be run any `*.*.ps1`(with an acceptable extension).
+
+# The output from each script will go to the corresponding file.
+
+# Declare some patterns we will use:
+# * `$matchExtension` will tell us which extension
+$matchExtension = '\.(?[^\.]+)\.ps1$'
+# * `$acceptablePattern` will ensure we only build file types we accept
+$acceptablePattern = "\.(?>$($acceptableExtensions -join '|'))\.ps1$"
+# * `$indexPattern` indicates which file types will become indeces.
+$indexPattern = "\.(?>$($IndexExtension -join '|'))\.ps1$"
+
+# Get all files that match our pattern
+$files = @(
+ Get-ChildItem -Path *.ps1 -File -Recurse |
+ Where-Object Name -match $acceptablePattern
+) |
+ Sort-Object @{
+ # We want to generate Markdown files first
+ # because html files may include their result inline.
+ Expression = {
+ # So sort on any md files first
+ $_.Name -match '\.md'
+ }
+ Descending = $true # in descending order
+ # (so they are at the top of the pile)
+ }, Fullname
+
+# Prepare our progress bars
+$progress = @{ID=Get-Random;Activity="Building Pages"}
+$total = @($files).Length
+$counter = 0
+
+# Walk over each of our files
+foreach ($in in $files) {
+ $progress.PercentComplete = ++$counter * 100 / $total
+ $progress.Status = $in.Name
+ Write-Progress @progress
+
+ # import any requirements
+ $in | . requireModule
+
+ $inScript =
+ $ExecutionContext.SessionState.InvokeCommand.GetCommand($in.FullName, 'ExternalScript')
+
+ # Make sure we can map the output extension
+ $outputExtension =
+ if ($in -match $matchExtension) {
+ $matches.x
+ } else {
+ # otherwise, warn and continue.
+ Write-Warning "Will not output $in without an extension"
+ continue
+ }
+
+ # Default the title to the file name
+ $FileName = $in.Name -replace $matchExtension
+ $title = $FileName -replace '-', ' '
+ $help = Get-Help $in.FullName -ErrorAction Ignore
+
+ $meta = [Ordered]@{}
+ foreach ($attr in $inScript.ScriptBlock.Attributes) {
+ if ($attr.Key -and $attr.Value) {
+ $meta[$attr.Key] = $attr.Value
+ }
+ }
+
+ # and initialize any page metadata
+ $page = [Ordered]@{
+ Title = $title
+ Command = $inScript
+ File = $in
+ FileName = $FileName
+ Source = $inScript.ScriptBlock
+ Help = $help
+ Meta = $meta
+ }
+
+ $description = $help.description.text -join [Environment]::NewLine
+
+ # Generate a file date by:
+ $fileDate = $fileName -replace
+ # * Remove any non-digit (except colon, dash, and underscore, and Z)
+ '[^\d:-_Z]' -replace
+ # * Trim leading punctuation, and trailing punctuation (and Z),
+ '^\p{P}+' -replace '[-Z]+$' -replace
+ # * replace underscores with colons, and try to cast to `[DateTime]`
+ '_',':' -as [DateTime]
+
+ # If we have a file date,
+ if ($fileDate) {
+ $page.Date = $fileDate # set the `$Page.Date`
+ } else {
+ # otherwise, we'll try to get the date from git.
+ $gitCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('git', 'Application')
+ if ($gitCommand) {
+ $gitDates =
+ try {
+ # we can use `git log --follow --format=%ci` to get the dates in order
+ (& $gitCommand log --follow --format=%ci --date default $in.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.
+ if ($gitDates) {
+ $page.Date = $gitDates[-1]
+ }
+ }
+ }
+
+ # If we map the output path _before_ we run our script
+ # we can know what URL we will be publishing to.
+
+ # Our output file path starts by replacing the .ps1
+ $outputPath = $in.FullName -replace '\.ps1$'
+
+ # If we do not care about "pretty" url format
+ # or are not an extension that we can make into an index,
+ # we do not need to change the file path
+ if ((-not $Ugly) -and ($in.Name -match $indexPattern)) {
+ # If we do, things get a little more complicated
+ # We will want to special case some extensions we want to use as an index.
+
+ # If the name of the file matches the name of the directory
+ if ($in.Name -match
+ "^$([Regex]::Escape($in.Directory.Name))$matchExtension"
+ ) {
+ # it will become an index of that directory.
+ $outputPath = Join-Path $in.Directory "index.$outputExtension"
+ } else {
+ # otherwise, make it an index of it's own directory
+ $outputPath =
+ ($in.FullName -replace $matchExtension) +
+ "/index.$outputExtension" -replace
+ 'index/index', 'index'
+ }
+ }
+
+ # Now that we know our output path,
+ # we can predict our url
+ $page.Url = $outputPath -replace "^$(
+ [Regex]::Escape($siteRoot) # just remove the site root
+ )" -replace # replace any index with a slash
+ '[\\/]index\.[^\.]+?$','/' -replace
+ '[\\/]', '/' # and fix any slashes.
+
+ $page.OutputPath = $outputPath
+
+ $page.Extension = $outputExtension
+ # Get our page output
+ $output = @(. $in.FullName)
+
+ if ($title) {
+ $page.Title = $title
+ }
+
+ if ($description) {
+ $page.Description = $description
+ }
+
+
+ # Store our output in the page
+ $page.Output = $output
+
+ # And put our page in two collections
+
+ # One by output path (we will use this output)
+ $Pages[$outputPath] = $page
+ # and one by url
+ $PagesByUrl[$page.Url] = $page
+}
+
+$progress.Activity = "Deploying Pages"
+$total = $pages.Count
+$counter = 0
+
+foreach ($outputPath in $pages.Keys) {
+
+ $progress.PercentComplete = ++$counter * 100 / $total
+ $progress.Status = "$($page.Title) "
+ Write-Progress @progress
+
+ $page = $pages[$outputPath]
+ $title = $page.Title
+ $description = $page.Description
+ $outputExtension = $page.Extension
+ $output = $page.Output
+ $outputFiles = @()
+ $output = @(foreach ($out in $output) {
+ if ($out -is [IO.FileInfo]) {
+ $outputFiles += $out
+ } else {
+ $out
+ }
+ })
+
+ # If the output was a list of files
+ if ($outputFiles -and -not $output) {
+ # then do not write anything to disk
+ $outputFiles
+ continue
+ }
+
+ # If we have a layout file and are outputting html
+ if ($layout -and (
+ $page.File.FullName -replace '\.ps1$' -match '\.html$'
+ )) {
+ # Pipe our output to the layout script.
+ # By doing this in a second pass,
+ # our layout script can have much more context.
+ $output = $output | layout
+ }
+ # Otherwise, if our output extension is xml
+ # and we have only that one xml
+ elseif (($outputExtension -eq 'xml') -and
+ ($output.Length -eq 1) -and
+ ($output[0] -is [xml])
+ ) {
+ # set our output to the outerXML.
+ $output = $output.OuterXml
+ }
+ # Otherwise, if our output extension is json
+ # and the output is not already a string
+ elseif (($outputExtension -eq 'json') -and (
+ $output[0] -isnot [string]
+ )) {
+ # convert it into json.
+ # If there is one more than one item,
+ if ($output.Length -gt 1) {
+ # make it a list
+ $output = $output | ConvertTo-Json -Depth (
+ $FormatEnumerationLimit
+ )
+ } else {
+ # If there is only one item, make it an object.
+ $output = ConvertTo-Json -InputObject $output -Depth (
+ $FormatEnumerationLimit
+ )
+ }
+ }
+
+ $outputFile = [Ordered]@{
+ ItemType = 'File';Force = $true
+ Value = $output -join [Environment]::NewLine
+ Path = $outputPath
+ }
+
+ if ($WhatIfPreference) {
+ $outputFile
+ } elseif ($PSCmdlet.ShouldProcess("Output $($outputPath)")) {
+ New-Item @outputFile
+ }
+}
+
+if ($PageUrl) {
+ $sitemap = /_includes/sitemap -Url $pageUrl -Pages $PagesByUrl
+ if ($sitemap) {
+ $sitemap.Save((
+ Join-Path $SiteRoot "sitemap.xml"
+ ))
+ Get-Item (Join-Path $SiteRoot "sitemap.xml")
+ }
+}
+
+$progress.Remove('PercentComplete')
+$progress.Completed = $true
+Write-Progress @progress
+
+Pop-Location
\ No newline at end of file
diff --git a/layout.ps1 b/layout.ps1
new file mode 100644
index 0000000..aa17ca2
--- /dev/null
+++ b/layout.ps1
@@ -0,0 +1,273 @@
+<#
+.SYNOPSIS
+ `layout.ps1`
+.DESCRIPTION
+ `layout.ps1` lays out a page.
+
+ This is a simple and helpful scripting convention for web development.
+
+ Just pipe to `./layout.ps1`
+
+ A layout gives pages in a site a consistent way to render content
+
+ `./layout.ps1` can layout content any way we want.
+
+ This `./layout.ps1` places content within a standard frame.
+
+ Feel free to copy and paste this code.
+
+ Please document your parameters, and add NOTES.
+.NOTES
+ This layout makes use of a few includes and a custom CSS function.
+
+ Includes are commands that directly output content, often in `html` or `svg`.
+
+ * `/_includes/CopyCode' includes a copy code link
+ * `/_includes/FeatherIcon` includes a feather icon
+ * `/_includes/Palette` includes a color palette selector
+
+
+ These should be initialized prior to use.
+#>
+param(
+[uri]
+$RepositoryUrl = $(
+ if ($env:GITHUB_REPOSITORY) {
+ "https://github.com/$env:GITHUB_REPOSITORY"
+ } else {
+ "https://github.com/PoshWeb/Matrix"
+ }
+),
+
+[string]
+$AnalyticsId,
+
+[string]
+$PaletteName = 'cyberpunk',
+
+# The Google Font name
+[Alias('FontName')]
+[string]
+$Font = 'Roboto',
+
+# The Google Code Font name
+[string]
+$CodeFont = 'CodeFont',
+
+# If set, will not include highlight js
+[switch]
+$NoHighlight,
+
+[psobject]
+$SiteMenu = $(
+ [PSCustomObject]@{
+ Matrix = [PSCustomObject]@{
+ CSS = [PSCustomObject]@{
+ "CSS Matrix" = '/matrix/css/'
+ Compatibility = '/matrix/css/compatible/'
+ Transforms = '/matrix/css/transform/'
+ }
+ HTML = [PSCustomObject]@{
+ "HTML Matrix" = '/matrix/html/'
+ "MathML Matrix" = '/matrix/mathml/'
+ "SVG Matrix" = '/matrix/svg/'
+ }
+ PowerShell = [PSCustomObject]@{
+ DotNet = '/matrix/dotnet/'
+ PowerShell = '/matrix/powershell/'
+ }
+ }
+ }
+),
+
+[string]
+$SiteName = $(
+ if ($env:GITHUB_REPOSITORY) {
+ @($env:GITHUB_REPOSITORY -split '/', 2)[-1]
+ } else {
+ 'Matrix'
+ }
+),
+
+# The locale for the page.
+# By default, the Current UI Culture.
+[Alias('Locale')]
+[cultureinfo]
+$Culture = [CultureInfo]::CurrentUICulture,
+
+[uri]
+$PageUrl
+)
+
+# Gather all input using `$input` for maximum efficiency.
+$allInput = @($input)
+
+# Declare a filter to turn things into HTML.
+filter toHtml {
+ $in = $_
+
+ # XML lives rent-free in HTML
+ if ($in.OuterXml) {return $in.OuterXml}
+
+ # Any object with an .html property
+ $inHtml = $in.Html
+ if ($inHtml) {
+ # should render that property
+ return $inHtml
+ }
+
+ "$in"
+}
+
+
+# Know thyself
+$mySelf = $MyInvocation.MyCommand
+
+# Any environment variables
+foreach ($env in Get-ChildItem env:) {
+ # See if any environment variables map to parameter
+ # (after we remove any punctuation from the name)
+ $envName = $env.Name -replace '\p{P}'
+ if (
+ # If they map
+ $mySelf.Parameters[$envName] -and
+ # and are not already bound
+ (-not $PSBoundParameters.ContainsKey($envName))
+ ) {
+ # set them
+ $ExecutionContext.SessionState.PSVariable.Set(
+ $envName,
+ $(
+ # If the value looks like json
+ if ($env.Value -match '^\s{0,}[\[\{]') {
+ # convert it before we set the value.
+ ConvertFrom-Json -InputObject $env.Value
+ }
+ # If the value is a boolean string
+ elseif ($env.Value -match '^true|false$') {
+ # convert it to a boolean
+ $evn.Value -match 'true'
+ }
+ # Otherwise, directly map it.
+ else {
+ $env.Value
+ }
+ )
+ )
+ # After we set the variable, map it into `$psBoundParameters`
+ $PSBoundParameters[$envName] =
+ $ExecutionContext.SessionState.PSVariable.Get(
+ $envName
+ ).Value
+ }
+}
+
+
+$head = @(
+ # Analytics first (per recommendations)
+ if ($AnalyticsId) {
+ "
+
+ "
+ }
+ # Basic viewport
+ " "
+ " "
+
+ # If a title was set
+ if ($title) {
+ # use it
+ "$(
+ [Web.HttpUtility]::HtmlEncode($title)
+ ) "
+ }
+
+ if ($description) {
+ " "
+ }
+
+ # If a palette name was provided
+ if ($PaletteName) {
+ # link to the stylesheet
+ " "
+ }
+
+ if (-not $NoHighlight) {
+ " "
+ ''
+ foreach ($language in 'css', 'svg', 'html','powershell') {
+ ""
+ }
+ }
+
+ if ($ExecutionContext.SessionState.InvokeCommand.GetCommand("/$SiteName.css", 'Alias,Function')) {
+ if ($PageUrl) {
+ " "
+ } else {
+ ""
+ }
+ }
+)
+
+$body = @(
+ ""
+ ""
+ ""
+ @($allInput | toHtml) -join [Environment]::NewLine
+ " "
+ ""
+ " "
+ /_includes/CopyCode
+)
+
+
+""
+""
+ "$(
+ $head -join [Environment]::NewLine
+ )"
+ "$(
+ $body -join [Environment]::NewLine
+ )"
+""
\ No newline at end of file
diff --git a/matrix/css/compatible.html.ps1 b/matrix/css/compatible.html.ps1
new file mode 100644
index 0000000..c402474
--- /dev/null
+++ b/matrix/css/compatible.html.ps1
@@ -0,0 +1,246 @@
+<#
+.SYNOPSIS
+ CSS Compatibility
+.DESCRIPTION
+ Matrix CSS Compatibility
+.COMPONENT
+ /matrix/css/
+#>
+[Reflection.AssemblyMetadata(
+ 'og:description', 'Matrix CSS Compatibility'
+)]
+param()
+
+$Title = 'CSS Compatibility'
+
+ConvertFrom-Markdown -InputObject @"
+
+# Matrix CSS
+
+## Matrix CSS Compatibility
+
+Matrix is mostly compatible with CSS.
+
+Each CSS transform function can be represented in a 4x4 or 3x2 matrix.
+
+Matrix extends every `[Numerics.Matrix4x4]` and `[Numerics.Matrix3x2]` matrix with a .CSS property.
+
+We can embed matrices in any css file or html file by simply outputting that property:
+
+~~~PowerShell
+(rotateX 45deg).CSS
+~~~
+
+This capability is quite useful.
+
+It means we can transform objects in 2D or 3D the exact same way we would in CSS.
+
+The rest of this page proves the point.
+
+It shows a side-by-side comparison of pure CSS and the matrix operation.
+
+Each side should be identical.
+
+"@ |
+ Select-Object -ExpandProperty Html
+
+
+""
+
+
+""
+"Cube "
+ ""
+ ""
+
+ ""
+ "Cube"
+ " "
+ ""
+ " "
+" "
+
+
+
+$2d = [Ordered]@{
+ "rotate(30deg)" = "rotate(30deg)", (Rotate 30deg).CSS
+ "skewX(30deg)" = "skewX(30deg)", (SkewX 30deg).CSS
+ "skewY(30deg)" = "skewY(30deg)", (SkewY 30deg).CSS
+ "skew(30deg, 15deg)" = "skew(30deg, 15deg)", (Skew 30deg 15deg).CSS
+ "skew(15deg, 30deg)" = "skew(15deg, 30deg)", (Skew 15deg 30deg).CSS
+ "scale(1,1.5)" = "scale(1,1.5)", (Scale 1 1.5).CSS
+ "scale(1.5,1)" = "scale(1.5,1)", (Scale 1.5 1).CSS
+ "translate(-5px, 10px)" = "translate(-5px,10px)", (Translate -5px 10px).CSS
+}
+
+
+""
+"2D "
+foreach ($key in $2d.Keys) {
+ ""
+ ""
+ "$key"
+ " "
+ ""
+ "$key"
+ " "
+ ""
+ "$key"
+ " "
+ " "
+}
+" "
+
+$3d = [Ordered]@{
+ "rotateX(45deg)" = "rotateX(45deg)", (RotateX 45deg).CSS
+ "rotateY(45deg)" = "rotateY(45deg)", (RotateY 45deg).CSS
+ "rotateZ(45deg)" = "rotateZ(45deg)", (RotateZ 45deg).CSS
+ "rotate3d(1,1,1, 45deg)" = "rotate3d(1,1,1, 45deg)", (rotate3d 1 1 1 45deg).CSS
+ "scaleX(1.5)" = "scaleX(1.5)", (ScaleX 1.5).CSS
+ "scaleY(1.5)" = "scaleY(1.5)", (ScaleY 1.5).CSS
+ "scaleZ(1.5)" = "scaleZ(1.5)", (ScaleZ 1.5).CSS
+ "scale3D(1.5, 0.75, 0.5)" = "scale3D(1.5, 0.75, 0.5)", (scale3D 1.5, 0.75, 0.5).CSS
+ "translateX(10px)" = "translateX(10px)", (TranslateX 10px).CSS
+ "translateY(10px)" = "translateY(10px)", (TranslateY 10px).CSS
+ "translateZ(-20px)" = "translateZ(-20px)", (TranslateZ -20px).CSS
+ "translate3d(10px, 20px, 30px)" = "translate3d(10px, 20px, 30px)", (translate3d 10px, 20px, 30px).CSS
+}
+
+""
+"3D "
+foreach ($key in $3d.Keys) {
+ ""
+ ""
+ "$key"
+ " "
+ ""
+ "$key"
+ " "
+ ""
+ "$key"
+ " "
+ " "
+}
+" "
+
+
+
+
+
+
+@(
+
+ ""
+ ""
+) * 4
+
+
+return
diff --git a/matrix/css/css.html.ps1 b/matrix/css/css.html.ps1
new file mode 100644
index 0000000..1f04d36
--- /dev/null
+++ b/matrix/css/css.html.ps1
@@ -0,0 +1,33 @@
+<#
+.SYNOPSIS
+ CSS Matrix
+.DESCRIPTION
+ Matrix in CSS
+.LINK
+ https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Matrix_math_for_the_web
+.COMPONENT
+ /matrix/css/
+#>
+param()
+
+@'
+
+# CSS Matrix
+
+CSS loves matrix transformations!
+
+Whenever we `rotate`, `scale`, or `translate`, we are using a matrix transform.
+
+All those nice 3D effects? Matrix transforms.
+
+The `Matrix` module helps give us greater mastery of [matrix transformations](/matrix/css/transform/),
+and gives us a way to make [css compatible](/matrix/css/compatible) transforms in PowerShell.
+
+Matrix maps [dotnet](/matrix/dotnet/) to [css](/matrix/css/).
+
+* `Matrix3x2` is a [CSS matrix()](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/transform-function/matrix)
+* `Matrix4x4` is a [CSS matrix3d()](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/transform-function/matrix)
+
+'@ |
+ ConvertFrom-Markdown |
+ Select-Object -ExpandProperty Html
\ No newline at end of file
diff --git a/matrix/css/transform.html.ps1 b/matrix/css/transform.html.ps1
new file mode 100644
index 0000000..e9897d7
--- /dev/null
+++ b/matrix/css/transform.html.ps1
@@ -0,0 +1,219 @@
+<#
+.SYNOPSIS
+ CSS Matrix
+.DESCRIPTION
+ CSS Transformation Matrices
+.COMPONENT
+ /matrix/css/
+#>
+param(
+# The sample image
+[string]$SampleImage = '/assets/dodge-matrix.gif',
+
+# The duration of the sample image.
+# This will be used as a base time for animations.
+[TimeSpan]$SampleImageDuration = '00:00:01.8'
+)
+
+function markdown {
+ @(
+ if ($args) {
+ $args
+ }
+ $allInput = @($input)
+ if ($allInput.Length) {
+ $allInput
+ }
+ ) -join [Environment]::NewLine |
+ ConvertFrom-Markdown |
+ Select-Object -ExpandProperty Html
+}
+
+markdown @"
+
+# Matrix Transforms
+
+## How to use Matrix transforms.
+
+We can use Matrix transforms in two major ways:
+
+* We can manipulate points using a Matrix
+* We can use a matrix as a transform in CSS
+
+This page demonstrates various animation transforms using Matrix.
+"@
+
+
+""
+
+markdown @"
+
+### Mirroring
+
+We can mirror two images by using a negative scale.
+
+This can be done in CSS with the `scale()` function, which becomes a `matrix()`.
+
+To mirror points along X, we can use:
+
+~~~PowerShell
+scale -1 1
+~~~
+
+"@
+
+@"
+
+
+
+
+
+
+"@
+
+"This works horizontally and vertically
"
+
+@"
+
+
+
+
+
+
+"@
+
+
+$QuadTopLeft = ".quad-top-left { transform: $((Scale -1 1).Css)}"
+$QuadTopRight = ".quad-top-right { transform: $((Scale 1 1).Css)}"
+$QuadBottomLeft = ".quad-bottom-left { transform: $((Scale -1 -1).Css)}"
+$QuadBottomRight = ".quad-bottom-right { transform: $((Scale 1 -1).Css)}"
+
+
+@"
+### Quad Mirror
+
+We can create a quad mirror effect by making four copies of an image.
+
+~~~css
+$QuadTopLeft
+$QuadTopRight
+$QuadBottomLeft
+$QuadBottomRight
+~~~
+"@ |
+ ConvertFrom-Markdown |
+ Select-Object -ExpandProperty Html
+
+
+""
+""
+ ""
+ ""
+ " "
+ " "
+ " "
+ ""
+ " "
+ " "
+ " "
+ " "
+" "
+
+""
+
+
+$flipX = @"
+@keyframes flip-x {
+ from {
+ transform: $((Scale 1 1).CSS)
+ }
+ to {
+ transform: $((Scale -1 1).CSS)
+ }
+}
+.flip-x {
+ animation-name: flip-x;
+ animation-duration: 3.6s;
+ animation-iteration-count: infinite;
+}
+"@
+
+$backFlipX = @"
+@keyframes back-flip-x {
+ from {
+ transform: $((Scale -1 1).CSS)
+ }
+ to {
+ transform: $((Scale 1 1).CSS)
+ }
+}
+.back-flip-x {
+ animation-name: back-flip-x;
+ animation-duration: $($SampleImageDuration.TotalSeconds * 2)s;
+ animation-iteration-count: infinite;
+}
+"@
+
+@"
+### Transform animations
+
+We can use transforms in CSS animations.
+
+Just use @keyframes
+
+~~~css
+$flipX
+$backFlipX
+~~~
+"@ |
+ ConvertFrom-Markdown |
+ Select-Object -ExpandProperty Html
+
+
+""
+
+""
+
+ ""
+ " "
+ " "
+ " "
+
+" "
+
+return
diff --git a/matrix/dotnet.html.ps1 b/matrix/dotnet.html.ps1
new file mode 100644
index 0000000..ad21d96
--- /dev/null
+++ b/matrix/dotnet.html.ps1
@@ -0,0 +1,70 @@
+<#
+.SYNOPSIS
+ DotNet Matrix
+.DESCRIPTION
+ Matrix in DotNet
+#>
+param()
+
+$matrixLinks = @{
+ "Matrix3x2" =
+ '[[Numerics.Matrix3x2]](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.matrix3x2?wt.mc_id=MVP_321542)'
+ "Matrix3x2 Source" =
+ '[[Numerics.Matrix3x2] source](https://github.com/microsoft/referencesource/blob/main/System.Numerics/System/Numerics/Matrix3x2.cs)'
+ "Matrix4x4" =
+ '[[Numerics.Matrix4x4]](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.matrix4x4?wt.mc_id=MVP_321542)'
+ "Matrix4x4 Source" =
+ '[[Numerics.Matrix4x4] source](https://github.com/microsoft/referencesource/blob/main/System.Numerics/System/Numerics/Matrix4x4.cs)'
+ "Quaternion" =
+ '[[Numerics.Quaternion]](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.quaternion?wt.mc_id=MVP_321542)'
+ "Quaternion Source" =
+ '[[Numerics.Quaternion] source](https://github.com/microsoft/referencesource/blob/main/System.Numerics/System/Numerics/Quaternion.cs)'
+}
+
+ConvertFrom-Markdown -InputObject @"
+
+# DotNet Matrix
+
+This module would not be possible without the .NET framework.
+
+.NET makes matrix math easy.
+
+Let's learn a bit about the dotnet matrix
+
+## The DotNet Matrix
+
+.NET provides three built-in types we can use to construct matrices:
+
+* $($matrixLinks.'Matrix3x2')
+* $($matrixLinks.'Matrix4x4')
+* $($matrixLinks.'Quaternion')
+
+Two of these map directly to CSS:
+
+* `Matrix3x2` to a [CSS matrix()](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/transform-function/matrix)
+* `Matrix4x4` to a [CSS matrix3d()](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/transform-function/matrix)
+
+A `Quaternion` can be converted to a `Matrix4x4`, and thus can become a `matrix3d`
+
+### The DotNet Matrix is Open Source
+
+The DotNet core framework is open source, and so are the Matrix classes we use in this module.
+
+They are well documented and fairly complete:
+
+|Class|Source|
+|-|-|
+|$($matrixLinks.'Matrix3x2')|$($($matrixLinks.'Matrix3x2 Source'))|
+|$($matrixLinks.'Matrix4x4')|$($($matrixLinks.'Matrix4x4 Source'))|
+|$($matrixLinks.'Quaternion')|$($($matrixLinks.'Quaternion Source'))|
+
+We can add, subtract, multiply and divide our matrices.
+We can also use them to transform any `Vector2`, `Vector3`, or `Vector4`.
+
+The Matrix module allows us to apply these transformations to an object pipeline of points in [PowerShell](/powershell/),
+using [css compatible](/css/compatible/) transforms.
+
+"@ |
+ Select-Object -ExpandProperty Html
+
+return
diff --git a/matrix/html.html.ps1 b/matrix/html.html.ps1
new file mode 100644
index 0000000..c2aa0b2
--- /dev/null
+++ b/matrix/html.html.ps1
@@ -0,0 +1,281 @@
+<#
+.SYNOPSIS
+ HTML Matrix
+.DESCRIPTION
+ Matrix in HTML.
+#>
+param()
+
+@'
+# HTML Matrix
+
+Transformation Matrixes are broadly supported in HTML.
+
+We can use Matrix to transform any element using it's style property.
+
+We can use Matrix to write [CSS Compatible](/matrix/css/compatible) transforms.
+
+There are numerous things we can do with SVG transforms.
+
+For example, we can use a scale transform to give object apparent depth.
+
+If we want an object to appear twice as far away, we can scale it down by 0.5.
+
+If we want an object to appear four times as far away, we can scale it by 0.25.
+
+The general formula for apparent distance scaling is:
+
+~~~PowerShell
+1/[Math]::Pow(2, `$scale - 1)
+~~~
+
+We can make any object look like it is Z away by applying a scale of that formula.
+
+This page show some experiments with the technique.
+
+To get things to overlap property, we need to put things into the same frame of reference.
+
+We can do this with a little bit of css
+
+~~~css
+/* Make an overlapping grid */
+.overlap {
+ place-items: center; display: grid; grid-template-rows: 1fr; grid-template-columns: 1fr;
+}
+/* Make every item in the grid be in row 1, column 1 */
+.overlap * {
+ grid-row: 1;grid-column: 1;
+}
+~~~
+
+The inner element's box helps determine how much space the grid will occupy.
+
+Use an inline element, like ````, if we want the grid to occupy only one line of space.
+
+'@ | ConvertFrom-Markdown |
+ Select-Object -ExpandProperty Html
+
+@"
+
+
+ $(
+ $scale = 1
+ foreach ($scale in 1..8) {
+ $scaleFactor = 1/[Math]::Pow(2,($scale - 1))
+ "$scale "
+ }
+ )
+
+"@
+
+
+"If we want the overlapping grid to block off more space, Use a block element, like ````" |
+ ConvertFrom-Markdown |
+ Select-Object -ExpandProperty html
+
+@"
+
+ $(
+ $scale = 1
+ foreach ($scale in 1..8) {
+ $scaleFactor = 1/[Math]::Pow(2,($scale - 1))
+ "
$scale "
+ }
+ )
+
+"@
+
+
+$lookAround = @"
+@keyframes look-around {
+ 0%, 100% {
+ transform-origin: 50% 50%;
+ }
+ 20% {
+ transform-origin: 0% 0%;
+ }
+ 40% {
+ transform-origin: 100% 0%;
+ }
+ 60% {
+ transform-origin: 0% 100%;
+ }
+ 80% {
+ transform-origin: 100% 100%;
+ }
+}
+.look-around {
+ animation-name: look-around;
+ animation-duration: 8.4s;
+ animation-iteration-count: infinite;
+}
+"@
+
+
+@"
+
+## Looking Around
+
+We can change where the scaling is centered by changing ``transform-origin``.
+
+This means we can perform the same animated look around we can in [svg](/svg).
+
+~~~css
+$lookAround
+~~~
+"@ | ConvertFrom-Markdown |
+ Select-Object -ExpandProperty Html
+
+
+""
+
+@"
+
+
+ $(
+ $scale = 1
+ foreach ($scale in 1..8) {
+ $scaleFactor = 1/[Math]::Pow(2,($scale - 1))
+ "
$scale "
+ }
+ )
+
+
+"@
+
+
+$zProp = "
+@property --z {
+ syntax: '';
+ inherits: true;
+ initial-value: 1;
+}
+"
+
+$zVar = @"
+calc(
+ 1 / pow( 2,
+ calc(
+ var(--z) - 1
+ )
+ )
+)
+"@
+
+
+@"
+
+## z trick
+
+Since there is a uniform method of scaling, we can simplify it in a CSS class or two.
+
+If only there was some way for elements to let us know where they are in z space 🤔.
+
+`z-index` will work, but it has some cannonical drawbacks:
+
+1. `z-index` is an integer, not a number
+2. A larger `z-index` is more visible, not less.
+
+I believe a better approach is using a CSS `` property to repesent z
+
+~~~css
+$zProp
+~~~
+
+With this property in hand, we can easily calculate a dynamic scale content.
+
+~~~css
+$zVar
+~~~
+
+Now we just need a class we can apply.
+
+Let us call it `z`
+
+~~~css
+.z {
+ transform: scale($zVar);
+ opacity: $zVar
+}
+~~~
+
+Putting it all together:
+
+~~~css
+$zProp
+.z {
+ transform: scale($zVar);
+ opacity: $zVar
+}
+~~~
+
+Let's see z trick in action:
+"@ |
+ ConvertFrom-Markdown |
+ Select-Object -ExpandProperty Html
+
+$zTrick = @"
+$zProp
+.z {
+ transform: scale($zVar);
+ opacity: $zVar
+}
+
+
+@keyframes z-move-down {
+ from {
+ transform-origin: 50% 50%;
+ }
+ to {
+ transform-origin: 500% 500%;
+ }
+}
+
+.z-move {
+ animation-name: z-move-down;
+ animation-iteration-count: infinite;
+ animation-duration: 4.2s;
+}
+
+$(
+ foreach ($n in 1..8) {
+ ".z-$n {--z: $n}"
+ }
+ foreach ($n in 1..8) {
+ foreach ($percent in 25, 50, 75) {
+ ".z-${n}-$percent {--z: $($n + $percent/100)}"
+ }
+ }
+)
+
+"@
+
+""
+""
+"
1 "
+"2 "
+"3 "
+"4 "
+""
+return
\ No newline at end of file
diff --git a/matrix/mathml.html.ps1 b/matrix/mathml.html.ps1
new file mode 100644
index 0000000..f50b3cf
--- /dev/null
+++ b/matrix/mathml.html.ps1
@@ -0,0 +1,124 @@
+<#
+.SYNOPSIS
+ MathML Matrix
+.DESCRIPTION
+ Matrix and MathML
+#>
+[OutputType('text/html')]
+param()
+
+
+@'
+
+## MathML Matrix
+
+A Matrix is just Math.
+
+MathML can show math inside of HTML.
+
+To show a matrix as a MathML matrix, we just need to access the `.MathML` property.
+
+~~~PowerShell
+(Scale3d 3 2 1).MathML
+~~~
+
+'@ |
+ ConvertFrom-Markdown |
+ Select-Object -ExpandProperty Html
+
+(Scale3d 3 2 1).MathML
+
+
+@'
+
+### Matrix3x2
+
+A 2D Matrix (`Matrix3x2`) can be mapped to a 3D Matrix (`Matrix4x4`).
+
+When we show a 2D Matrix's MathML, we show both the `matrix` and and it's equivalent `matrix3d`
+
+~~~PowerShell
+(Scale 1 2).MathML
+~~~
+'@ |
+ ConvertFrom-Markdown |
+ Select-Object -ExpandProperty Html
+
+(Scale 1 2).MathML
+
+@'
+
+### Quaternion
+
+A Quaternion (`[Numerics.Quaternion]`) can be mapped to a 3D Matrix (`[Numerics.Matrix4x4]`).
+
+When we show a Quaternion's MathML, we show both the `[Numerics.Quaternion]` and and it's equivalent `matrix3d`
+
+~~~PowerShell
+(Quaternion Identity).MathML
+~~~
+'@ |
+ ConvertFrom-Markdown |
+ Select-Object -ExpandProperty Html
+
+(Quaternion Identity).MathML
+
+@'
+
+### MathML in HTML
+
+MathML obviously works fine in html.
+
+We can also preview the matrix by accessing another property: `.html`
+
+~~~PowerShell
+(Scale3d 3 2 1).Html
+~~~
+
+By default, this will show the matrix twice.
+
+Once in it's original form, and once transformed by itself.
+
+'@ |
+ ConvertFrom-Markdown |
+ Select-Object -ExpandProperty Html
+
+(Scale3d 3 2 1).Html
+
+
+@'
+
+A Matrix3x2 and a Quaternion can both be converted to a Matrix4x4
+
+We show the equivalent matrix side by side
+
+~~~PowerShell
+(Scale 1 0.5).MathML
+(Quaternion 0.5 0.5 0.5 1).MathML
+~~~
+'@ |
+ ConvertFrom-Markdown |
+ Select-Object -ExpandProperty Html
+
+
+(Scale 1 0.5).MathML
+
+(Quaternion 0 0 0 1).MathML
+
+@'
+
+We can also attach our own .Content property to the matrix:
+
+~~~PowerShell
+Scale -1 1 |
+ Add-Member NoteProperty Content "Backwards " -Force -PassThru |
+ Select-Object -ExpandProperty Html
+~~~
+'@ |
+ ConvertFrom-Markdown |
+ Select-Object -ExpandProperty Html
+
+
+Scale -1 1 |
+ Add-Member NoteProperty Content "Backwards " -Force -PassThru |
+ Select-Object -ExpandProperty Html
\ No newline at end of file
diff --git a/matrix/matrix.html.ps1 b/matrix/matrix.html.ps1
new file mode 100644
index 0000000..e395246
--- /dev/null
+++ b/matrix/matrix.html.ps1
@@ -0,0 +1,15 @@
+<#
+.SYNOPSIS
+ Matrix
+.DESCRIPTION
+ Matrix Transforms
+.NOTES
+ Currently just replicating the README within the layout
+#>
+[OutputType('text/html')]
+param()
+
+ConvertFrom-Markdown -Path (
+ $PSScriptRoot | Split-Path | Join-Path -ChildPath 'README.md'
+) -ErrorAction Ignore |
+ Select-Object -ExpandProperty Html
diff --git a/matrix/powershell.html.ps1 b/matrix/powershell.html.ps1
new file mode 100644
index 0000000..90b8c5b
--- /dev/null
+++ b/matrix/powershell.html.ps1
@@ -0,0 +1,55 @@
+<#
+.SYNOPSIS
+ PowerShell Matrix
+.DESCRIPTION
+ Using Matrix in PowerShell
+#>
+$cubeTranslationExample = {
+ # Constructing a cube using translation
+
+ # Make a corner point
+ $corner = [Numerics.Vector3]::new(1,1,1)
+
+ # Make a square by translating along X and Y
+ $square = @(
+ $corner
+ $corner | TranslateX 1
+ $corner | TranslateX 1 | TranslateY 1
+ $corner | TranslateY 1
+ )
+
+ # Make a cube by translating the square along Z.
+ $cube = @(
+ $square
+ $square |
+ TranslateZ 1
+ )
+
+ $cube
+}
+
+@"
+
+
+The Matrix module allows us to apply these transformations to a pipeline of points, using [css compatible](/css/compatible/) transforms.
+
+For example, let's construct the points in a cube
+
+~~~PowerShell
+$($cubeTranslationExample)
+~~~
+
+This produces:
+~~~
+$(. $cubeTranslationExample | Out-String)
+~~~
+
+The capability can be very powerful.
+
+It lets us take an object pipeline full of points and transform them any way we see fit.
+
+It also allows us to calculate complex information without having to do the math.
+
+"@ |
+ ConvertFrom-Markdown |
+ Select-Object -ExpandProperty Html
diff --git a/matrix/svg.html.ps1 b/matrix/svg.html.ps1
new file mode 100644
index 0000000..a6b783b
--- /dev/null
+++ b/matrix/svg.html.ps1
@@ -0,0 +1,595 @@
+<#
+.SYNOPSIS
+ SVG Matrix
+.DESCRIPTION
+ Using Matrix in SVG.
+#>
+param()
+function copyz {
+ param(
+ [int]
+ $StepCount = 4,
+
+ [double]
+ $InitialScale = 1,
+
+ [double]
+ $FinalScale = 9,
+
+ [string]
+ $Element = 'rect',
+
+ [string[]]
+ $Attribute,
+
+ [string[]]
+ $Children
+ )
+
+ if (-not $StepCount) {
+ $stepCount = 1
+ }
+
+ $attribute += @(
+ "fill='transparent'"
+ "stroke='currentColor' class='foreground-stroke'"
+ "x='0%' y='0%'"
+ "width='100%' height='100%'"
+ "transform-origin='50% 50%'"
+ )
+
+ $scaleStep = ($FinalScale - $InitialScale)/$StepCount
+ for ($scale = $InitialScale; [Math]::Abs($scale) -lt [Math]::Abs($FinalScale); $scale += $scaleStep) {
+ $scaleFactor = 1/[Math]::Pow(2,($scale - 1))
+
+ "<$element$(
+ if ($Attribute) {
+ " $attribute"
+ }
+ )$(
+ " transform='$((Scale $scaleFactor).CSS)'"
+ )>$($Children -join [Environment]::Newline)$element>"
+ }
+}
+
+@"
+
+# SVG Matrix
+
+SVG is a web native standard for scalable vector graphics.
+
+We can use Matrix to make transforms embedded in an SVG.
+
+SVG transforms can be applied a few ways:
+
+* [Transform Attribute](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/transform)
+* [GradientTransform Attribute](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/gradientTransform)
+* [PatternTransform Attribute](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/patternTransform)
+
+These transforms tend to be limited to a 3x2 matrix (though technically should support both).
+
+We can always set use the [style attribute](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/style) to provide a custom style,
+or use the [class](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/style) attribute to style our SVG using CSS.
+
+There are numerous things we can do with SVG transforms.
+
+For example, we can use a scale transform to give object apparent depth.
+
+If we want an object to appear twice as far away, we can scale it down by 0.5.
+
+If we want an object to appear four times as far away, we can scale it by 0.25.
+
+The general formula for apparent distance scaling is:
+
+~~~PowerShell
+1/[Math]::Pow(2, `$scale - 1)
+~~~
+
+We can think of this a step function.
+
+~~~PowerShell
+function copyz {$(
+ (Get-Command copyz).ScriptBlock
+)}
+~~~
+
+"@ | ConvertFrom-Markdown |
+ Select-Object -ExpandProperty Html
+
+""
+
+""
+
+"4 steps "
+@"
+
+ $(copyz 4)
+
+"@
+
+"8 steps "
+@"
+
+ $(copyz 8)
+
+"@
+
+"16 steps "
+
+@"
+
+ $(copyz 16)
+
+"@
+
+
+"32 steps "
+
+@"
+
+ $(copyz 32)
+
+"@
+
+"64 steps "
+
+@"
+
+ $(copyz 64)
+
+"@
+
+"128 steps "
+
+$128Steps = @"
+
+ $(copyz 128)
+
+"@
+
+$128Steps
+
+
+"As the steps increase, the object seems deeper "
+
+"Drawing diagonals might help understand what is happenning
"
+
+"The center of the X is the center of our perpsective
"
+
+@"
+
+
+
+ $(copyz 64)
+
+"@
+
+"transform-origin "
+
+"Changing our
transform-origin changes the center of the effect "
+
+foreach ($origin in '0% 0%', '100% 0%', '0% 100%', '100% 100%') {
+@"
+
+
transform-origin:$origin
+
+
+$(if ($origin -eq '50% 50%') {
+ "
+ "
+})
+
+ $(
+ $scale = 1
+ foreach ($scale in 1..8) {
+ $scaleFactor = 1/[Math]::Pow(2,($scale - 1))
+ " "
+ }
+ )
+
+"@
+}
+
+"Transform Origin Animation "
+
+"We can animate our transform origin with CSS keyframes
"
+
+$lookUpAndDown = @"
+@keyframes look-up-and-down {
+ 0%, 100% {
+ transform-origin: 50% 50%;
+ }
+ 33% {
+ transform-origin: 50% 25%;
+ }
+ 66% {
+ transform-origin: 50% 75%;
+ }
+}
+.look-up-and-down {
+ animation-name: look-up-and-down;
+ animation-duration: 4.2s;
+ animation-iteration-count: infinite;
+}
+"@
+
+"
+$lookUpAnddown
+ "
+
+@"
+
+"@
+
+@"
+
+ $(
+ copyz -Attribute "class='look-up-and-down'" -StepCount 64
+ )
+
+"@
+
+"We can look left and right
"
+
+$lookLeftAndRight = @"
+@keyframes look-left-and-right {
+ 0%, 100% {
+ transform-origin: 50% 50%;
+ }
+ 33% {
+ transform-origin: 25% 50%;
+ }
+ 66% {
+ transform-origin: 75% 50%;
+ }
+}
+.look-left-and-right {
+ animation-name: look-left-and-right;
+ animation-duration: 4.2s;
+ animation-iteration-count: infinite;
+}
+"@
+
+"
+$lookLeftAndRight
+ "
+
+@"
+
+ $(
+ copyz -Attribute "class='look-left-and-right'" -StepCount 64
+ )
+
+"@
+
+"We can look around
"
+
+$lookAround = @"
+@keyframes look-around {
+ 0%, 100% {
+ transform-origin: 50% 50%;
+ }
+ $([Math]::Round(1/5,4) * 100)% {
+ transform-origin: 25% 25%;
+ }
+ $([Math]::Round(2/5,4) * 100)% {
+ transform-origin: 75% 25%;
+ }
+ $([Math]::Round(3/5,4) * 100)% {
+ transform-origin: 25% 75%;
+ }
+ $([Math]::Round(4/5,4) * 100)% {
+ transform-origin: 75% 75%;
+ }
+}
+.look-around {
+ animation-name: look-around;
+ animation-duration: 8.4s;
+ animation-iteration-count: infinite;
+}
+"@
+
+""
+
+"
+$lookAround
+ "
+
+@"
+
+ $(
+ copyz -Attribute "class='look-around'" -StepCount 64
+ )
+
+"@
+
+
+"Motion "
+
+"We can animate the transform to give us motion
"
+
+@"
+
+ $(
+ $scale = 1
+ for ($scale = 1; $scale -lt 9; $scale += 0.125) {
+ $scaleFactor = 1/[Math]::Pow(2,($scale - 1))
+ ""
+ " "
+ " "
+ }
+ )
+
+"@
+
+"If we scale from our factor to 1, the object seems to get closer
"
+
+@"
+
+ $(
+ $scale = 1
+ for ($scale = 1; $scale -lt 9; $scale += 0.125) {
+ $scaleFactor = 1/[Math]::Pow(2,($scale - 1))
+ ""
+ " "
+ " "
+ }
+ )
+
+"@
+
+"If we scale from 1 to our factor, the object seems to get farther away
"
+
+@"
+
+ $(
+ $scale = 1
+ for ($scale = 1; $scale -lt 9; $scale += 0.125) {
+ $scaleFactor = 1/[Math]::Pow(2,($scale - 1))
+ ""
+ " "
+ " "
+ }
+ )
+
+"@
+
+"We can also transform the element itself, with all of our inner transforms intact
"
+
+
+""
+@"
+
+ $(
+ $scale = 1
+ for ($scale = 1; $scale -lt 9; $scale += 0.125) {
+ $scaleFactor = 1/[Math]::Pow(2,($scale - 1))
+ ""
+ " "
+ " "
+ }
+ )
+
+"@
+
+"We can animate this, too
"
+
+""
+
+@"
+
+ $(
+ $scale = 1
+ for ($scale = 1; $scale -lt 9; $scale += 0.125) {
+ $scaleFactor = 1/[Math]::Pow(2,($scale - 1))
+ ""
+ " "
+ " "
+ }
+ )
+
+"@
+
+"We can also animate our transform-origin as we animate our transform
"
+
+"Let's revisit our lookaround examples, with some added motion
"
+
+@"
+
+ $(
+ $scale = 1
+ for ($scale = 1; $scale -lt 9; $scale += 0.125) {
+ $scaleFactor = 1/[Math]::Pow(2,($scale - 1))
+ ""
+ " "
+ " "
+ }
+ )
+
+"@
+
+@"
+
+ $(
+ $scale = 1
+ for ($scale = 1; $scale -lt 9; $scale += 0.125) {
+ $scaleFactor = 1/[Math]::Pow(2,($scale - 1))
+ ""
+ " "
+ " "
+ }
+ )
+
+"@
+
+@"
+
+ $(
+ $scale = 1
+ for ($scale = 1; $scale -lt 9; $scale += 0.125) {
+ $scaleFactor = 1/[Math]::Pow(2,($scale - 1))
+ ""
+ " "
+ " "
+ }
+ )
+
+"@
+
+
+@"
+### Circles
+
+We can perform this magic trick with any shape.
+
+Using `` instead of `` produces some interesting results.
+"@ |
+ ConvertFrom-Markdown |
+ Select-Object -ExpandProperty Html
+
+
+
+@"
+
+ $(
+ copyz -Attribute @(
+ "cx='50%'"
+ "cy='50%'"
+ "r='25%'"
+ ) -StepCount 32 -Element circle
+ )
+
+"@
+
+"We can also look up and down
"
+
+@"
+
+ $(
+ copyz -Attribute @(
+ "class='look-up-and-down'"
+ "cx='50%'"
+ "cy='50%'"
+ "r='25%'"
+ ) -StepCount 64 -Element circle
+ )
+
+"@
+
+"Or look left and right
"
+
+@"
+
+
+ $(
+ copyz -Attribute @(
+ "class='look-left-and-right'"
+ "cx='50%'"
+ "cy='50%'"
+ "r='25%'"
+ ) -StepCount 64 -Element circle
+ )
+
+
+"@
+
+
+$lookFarLeftAndRight = @"
+@keyframes look-far-left-and-right {
+ 0%, 100% {
+ transform-origin: 50% 50%;
+ }
+ 33% {
+ transform-origin: -25% 50%;
+ }
+ 66% {
+ transform-origin: 125% 50%;
+ }
+}
+.look-far-left-and-right {
+ animation-name: look-far-left-and-right;
+ animation-duration: 4.2s;
+ animation-iteration-count: infinite;
+}
+"@
+
+
+@"
+
+We can use a transform-origin that exceeds 100%.
+
+This will make things appear outside of their original bounds.
+
+When animated, this can look like we are moving outside the original object.
+
+~~~css
+$lookFarLeftAndRight
+~~~
+
+"@ |
+ ConvertFrom-Markdown |
+ Select-Object -ExpandProperty Html
+
+@"
+
+
+ $(
+ copyz -Attribute @(
+ "class='look-far-left-and-right'"
+ "cx='50%'"
+ "cy='50%'"
+ "r='25%'"
+ ) -StepCount 64 -Element circle
+ )
+
+"@
+
+""
+
+return
\ No newline at end of file