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 +[![Matrix](https://img.shields.io/powershellgallery/dt/Matrix)](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 ` +$( + foreach ($palette in $script:AllPalettes.psobject.Properties) { + $paletteName = $palette.name + $palette = $palette.value + $selectedPalette = if ($defaultPalette -and $defaultPalette -eq $paletteName) { " selected='true'"} else { '' } + "" + } +) + + + + +"@ + +$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 = @( + "
    " + "
    " + "" + "
    " + if ($ExecutionContext.SessionState.InvokeCommand.GetCommand("/$siteName.svg", 'Alias,Function')) { + "
    " + "" + "
    " + } + "

    $siteName

    " + "
    " + "
    " + "$(/_includes/FeatherIcon settings)" + "
    Palette$( + /_includes/Palette -DefaultPalette $PaletteName + )
    " + "
    " + + "
    " + "
    " + @($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" + "
    " + "
    +
    +
    +
    +
    +
    +
    +
    " + + "" + "
    +
    +
    +
    +
    +
    +
    +
    " + "
    " +"
    " + + + +$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" + "
    " + "
    " +} +"
    " + +$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" + "
    " + "
    " +} +"
    " + + + + + + +@( + + "
    " + "
    +
    +
    +
    +
    +
    " + "
    " + "
    " + "
    +
    +
    +
    +
    +
    " + "
    " +) * 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)" + } +} + +@" + +# 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