diff --git a/.github/workflows/auto-version-release.yml b/.github/workflows/auto-version-release.yml index 43d020c8..5d50124c 100644 --- a/.github/workflows/auto-version-release.yml +++ b/.github/workflows/auto-version-release.yml @@ -126,7 +126,8 @@ jobs: } $newVersion = "$major.$minor.$build.$revision" - $newVersionText = "$major$minor$build$revision" + # 使用下划线分隔,避免各组件 >= 10 时产生歧义(如 0.10.0.0 → 01000 与 0.1.0.0.0 冲突) + $newVersionText = "${major}_${minor}_${build}_${revision}" Write-Host "Version: $currentVersion -> $newVersion" @@ -199,6 +200,22 @@ jobs: - name: Rebuild solution run: msbuild src/VirtualPaper.sln /t:Rebuild /p:Configuration=Release /p:Platform="Any CPU" /restore /m + - name: Validate Build Artifacts + # 在产物就地校验:主 exe / Plugin exe / 核心 dll 存在且非空。 + # 通过后才上传 artifact,确保 package / smoke_test job 拿到的是健康的构建产物。 + env: + RELEASE_BIN_DIR: ${{ github.workspace }}/src/VirtualPaper/bin/Release/net8.0-windows10.0.19041.0 + run: dotnet test src/VirtualPaper.ReleaseBuildData.Test/VirtualPaper.ReleaseBuildData.Test.csproj --configuration Release --no-build --verbosity normal --logger "trx;LogFileName=release-build-tests.trx" --results-directory TestResults/ReleaseBuild + shell: pwsh + + - name: Upload Validate Build Artifacts results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results-release-build + path: TestResults/ReleaseBuild/ + retention-days: 1 + - name: Upload build output uses: actions/upload-artifact@v4 with: @@ -218,11 +235,15 @@ jobs: installer_path: ${{ steps.installer_info.outputs.installer_path }} steps: + # Step 1:checkout 拿到仓库根目录的全部文件,包含 InnoSetup/ 脚本和其他非编译产物。 - name: Checkout code uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} + # Step 2:将 build job 的编译产物下载并覆盖到 src/。 + # 顺序不可颠倒:必须先 checkout(否则 InnoSetup/ 目录不存在), + # 再用 artifact 覆盖 src/(用已打版本号的 Release 二进制替换源码目录下的旧产物)。 - name: Download build output uses: actions/download-artifact@v4 with: @@ -241,7 +262,7 @@ jobs: $newVersion = "${{ needs.prepare.outputs.new_version }}" $newVersionText = "${{ needs.prepare.outputs.new_version_text }}" $newContent = $content -replace '#define MyAppVersion "[\d\.]+"', "#define MyAppVersion `"$newVersion`"" - $newContent = $newContent -replace '#define MyAppVersionText "[\d]+"', "#define MyAppVersionText `"$newVersionText`"" + $newContent = $newContent -replace '#define MyAppVersionText "[\d_]+"', "#define MyAppVersionText `"$newVersionText`"" Set-Content $setupPath -Value $newContent -NoNewline Write-Host "Patched InnoSetup version to $newVersion (build-time only)" @@ -472,12 +493,26 @@ jobs: $classes = @($wins | ForEach-Object { $_.Current.ClassName }) Write-Host " Visible window classes: $($classes -join ', ')" + # ── 正向断言:MessageBox(#32770)应存在 ──────────────────────────── + # Win32 MessageBox 的窗口类名固定为 #32770,是 Windows API 常量,不会变。 if (-not ($classes -contains '#32770')) { Write-Error "Expected a MessageBox (#32770) but none found - UI may have started normally, guard logic broken" Stop-Process -Id $uiProc.Id -Force -ErrorAction SilentlyContinue exit 1 } - Write-Host "[OK] VirtualPaper.UI is blocked by a MessageBox (#32770) as expected (PID: $($uiProc.Id))" + Write-Host "[OK] MessageBox (#32770) present - guard triggered as expected (PID: $($uiProc.Id))" + + # ── 反向断言:WinUI3 主窗口(WinUIDesktopWin32Window)不应存在 ────── + # WinUI3 正常启动后主窗口类名固定为 WinUIDesktopWin32Window。 + # 若守卫生效,此类名不应出现——与 #32770 正向检查互补, + # 覆盖未来从 MessageBox 迁移到其他阻塞机制(ContentDialog 等)的场景: + # 届时 #32770 检查会失效,但此反向检查仍能捕获"UI 意外正常启动"。 + if ($classes -contains 'WinUIDesktopWin32Window') { + Write-Error "WinUI3 main window (WinUIDesktopWin32Window) appeared - UI started normally without main process, guard logic broken" + Stop-Process -Id $uiProc.Id -Force -ErrorAction SilentlyContinue + exit 1 + } + Write-Host "[OK] WinUI3 main window not present - guard is holding correctly" # ── 清理:强杀被阻塞的 UI 进程 ────────────────────────────────────── Stop-Process -Id $uiProc.Id -Force -ErrorAction SilentlyContinue @@ -521,7 +556,7 @@ jobs: $newVersion = "${{ needs.prepare.outputs.new_version }}" $newVersionText = "${{ needs.prepare.outputs.new_version_text }}" $newContent = $content -replace '#define MyAppVersion "[\d\.]+"', "#define MyAppVersion `"$newVersion`"" - $newContent = $newContent -replace '#define MyAppVersionText "[\d]+"', "#define MyAppVersionText `"$newVersionText`"" + $newContent = $newContent -replace '#define MyAppVersionText "[\d_]+"', "#define MyAppVersionText `"$newVersionText`"" Set-Content $setupPath -Value $newContent -NoNewline Write-Host "Updated InnoSetup version to: $newVersion" @@ -583,7 +618,7 @@ jobs: # 策略 1:尝试将 main 全量合并到当前分支 # -X theirs: 冲突时自动以 main 为准,无需人工介入 - $mergeOutput = git merge origin/main --no-edit -X theirs 2>&1 + $mergeOutput = git merge origin/main -m "[Github CI] chore: sync main -> $branch [skip ci]" -X theirs 2>&1 $mergeExit = $LASTEXITCODE if ($mergeExit -eq 0) { @@ -689,7 +724,13 @@ jobs: name: Rollback on Failure runs-on: windows-latest needs: [prepare, build, package, smoke_test, bump, sync] - if: always() && failure() && needs.prepare.result == 'success' + # 触发条件:bump 或 sync 已经开始执行(分支内容已被修改),且流程出现失败或取消。 + # build 失败时 bump/sync 均为 skipped(分支未动),不触发,避免无意义的回滚操作。 + if: | + always() && + needs.prepare.result == 'success' && + (needs.bump.result != 'skipped' || needs.sync.result != 'skipped') && + (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) steps: - name: Checkout with full history diff --git a/.github/workflows/manual-rollback.yml b/.github/workflows/manual-rollback.yml index 89da7f00..d363f49c 100644 --- a/.github/workflows/manual-rollback.yml +++ b/.github/workflows/manual-rollback.yml @@ -1,28 +1,59 @@ name: Manual Rollback -run-name: "Manual Rollback v${{ inputs.version }} | by @${{ github.actor }}" +run-name: "Manual Rollback | ${{ inputs.mode == 'version-rollback' && format('v{0} | {1}', inputs.version, inputs.scope) || format('reset {0} -> {1}', inputs.target_branch, inputs.target_sha) }} | by @${{ github.actor }}" on: workflow_dispatch: inputs: - version: - description: 'Version number to roll back to (for example, 0.5.0.0)' + + mode: + description: 'Rollback mode' required: true + type: choice + options: + - 'version-rollback' # 按版本号 revert auto-ci bump commit + - 'branch-reset' # 将指定分支 hard-reset 到任意 commit SHA + default: 'version-rollback' + + # ── [version-rollback] 专用输入 ────────────────────────────────────── + version: + description: '[version-rollback] Version to roll back (e.g. 0.5.1.0)' + required: false type: string + default: '' + scope: - description: 'Rollback Scope' - required: true + description: '[version-rollback] Rollback scope' + required: false type: choice options: - 'all (main + release + dev + bugfix)' - 'main only' default: 'all (main + release + dev + bugfix)' + # ── [branch-reset] 专用输入 ────────────────────────────────────────── + target_branch: + description: '[branch-reset] Branch to reset' + required: false + type: choice + options: + - 'main' + - 'release' + - 'dev' + - 'bugfix' + default: 'main' + + target_sha: + description: '[branch-reset] Target commit SHA to reset to (full or short)' + required: false + type: string + default: '' + permissions: contents: write jobs: rollback: - name: Rollback v${{ inputs.version }} + name: Rollback | ${{ inputs.mode }} runs-on: windows-latest steps: @@ -32,16 +63,60 @@ jobs: token: ${{ secrets.RELEASE_PAT }} fetch-depth: 0 - - name: Execute rollback + # ───────────────────────────────────────────────────────────────────── + # 公共:输入校验 + # ───────────────────────────────────────────────────────────────────── + - name: Validate inputs + shell: powershell + run: | + $mode = "${{ inputs.mode }}" + + if ($mode -eq 'version-rollback') { + $ver = "${{ inputs.version }}".Trim() + if (-not $ver) { + Write-Error "[version-rollback] Input 'version' is required." + exit 1 + } + if ($ver -notmatch '^\d+\.\d+\.\d+\.\d+$') { + Write-Error "[version-rollback] Invalid format. Expected X.X.X.X, got: $ver" + exit 1 + } + Write-Host "[OK] version-rollback input validated: $ver" + + } elseif ($mode -eq 'branch-reset') { + $sha = "${{ inputs.target_sha }}".Trim() + $branch = "${{ inputs.target_branch }}".Trim() + if (-not $sha) { + Write-Error "[branch-reset] Input 'target_sha' is required." + exit 1 + } + if (-not $branch) { + Write-Error "[branch-reset] Input 'target_branch' is required." + exit 1 + } + Write-Host "[OK] branch-reset input validated: $branch -> $sha" + + } else { + Write-Error "Unknown mode: $mode" + exit 1 + } + + # ───────────────────────────────────────────────────────────────────── + # Mode 1: version-rollback + # 在 main 上找到 bump commit 并 revert,再将预发布分支内容对齐到 pre-bump 状态 + # ───────────────────────────────────────────────────────────────────── + - name: Execute version rollback + if: inputs.mode == 'version-rollback' shell: powershell run: | $version = "${{ inputs.version }}" $scope = "${{ inputs.scope }}" Write-Host "========================================" - Write-Host "Manual Rollback: version $version" - Write-Host "Scope: $scope" - Write-Host "Triggered by: ${{ github.actor }}" + Write-Host "Mode : version-rollback" + Write-Host "Version : $version" + Write-Host "Scope : $scope" + Write-Host "Triggered : ${{ github.actor }}" Write-Host "========================================" git config --local user.email "action@github.com" @@ -51,9 +126,7 @@ jobs: git fetch origin 2>&1 | Out-Null $ErrorActionPreference = 'Stop' - # ───────────────────────────────────────────────────────── - # Step 1: 在 main 上找到并 revert 版本递增 commit - # ───────────────────────────────────────────────────────── + # ── Step 1: 在 main 上找到并 revert 版本递增 commit ────────────── Write-Host "" Write-Host "--- [1/2] Rolling back main ---" @@ -65,7 +138,6 @@ jobs: exit 1 } - # 记录 bump commit 的父 commit(即 bump 之前的 main,用于恢复版本文件) $preBumpSha = git rev-parse "$bumpCommit~1" Write-Host " Bump commit : $($bumpCommit.Substring(0,7))" Write-Host " Pre-bump SHA : $($preBumpSha.Substring(0,7))" @@ -74,16 +146,7 @@ jobs: git push origin main Write-Host "[OK]" -ForegroundColor Green -NoNewline; Write-Host " main: version $version reverted" - # ───────────────────────────────────────────────────────── - # Step 2: 在预发布分支上创建一个 revert commit,回滚到 pre-bump 状态 - # 做法:用 `git read-tree --reset -u` 将工作树和 index 重置为 - # pre-bump main 的内容快照,但 HEAD 保持不变;再 commit & push。 - # 效果: - # - 分支内容与 pre-bump main 完全一致(含版本文件等所有文件) - # - 完整保留历史(sync merge / bump commit 都仍在 git log 中) - # - 多一条清晰的 revert commit,可追溯何时回滚到了哪个版本 - # - 普通 push 即可,无需 force push - # ───────────────────────────────────────────────────────── + # ── Step 2: 将预发布分支内容对齐到 pre-bump 状态 ───────────────── if ($scope -eq 'main only') { Write-Host "" Write-Host "[SKIP] Pre-publish branches: scope is 'main only'" @@ -96,8 +159,6 @@ jobs: git checkout -B $branch origin/$branch - # 将 index 和工作树重置为 pre-bump main 的内容快照(HEAD 不动) - # 这样后续 commit 就是一条"内容 = pre-bump"的 revert commit git read-tree --reset -u $preBumpSha $status = git status --porcelain @@ -114,5 +175,76 @@ jobs: Write-Host "" Write-Host "========================================" - Write-Host "[OK]" -ForegroundColor Green -NoNewline; Write-Host " Rollback v$version completed" + Write-Host "[OK]" -ForegroundColor Green -NoNewline; Write-Host " version-rollback v$version completed" + Write-Host "========================================" + + # ───────────────────────────────────────────────────────────────────── + # Mode 2: branch-reset + # 将指定分支 hard-reset 到任意 commit SHA,使用 --force-with-lease 防止 + # 覆盖并发推送。适用于非 auto-ci 产生的误提交、紧急回退等场景。 + # ───────────────────────────────────────────────────────────────────── + - name: Execute branch reset + if: inputs.mode == 'branch-reset' + shell: powershell + run: | + $branch = "${{ inputs.target_branch }}" + $targetSha = "${{ inputs.target_sha }}".Trim() + + Write-Host "========================================" + Write-Host "Mode : branch-reset" + Write-Host "Branch : $branch" + Write-Host "Target SHA : $targetSha" + Write-Host "Triggered : ${{ github.actor }}" + Write-Host "========================================" + + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + $ErrorActionPreference = 'Continue' + git fetch origin 2>&1 | Out-Null + $ErrorActionPreference = 'Stop' + + # ── Step 1: 验证目标 SHA 存在 ───────────────────────────────────── + $resolvedSha = git rev-parse --verify $targetSha 2>$null + if ($LASTEXITCODE -ne 0 -or -not $resolvedSha) { + Write-Error "SHA not found in repository: $targetSha" + exit 1 + } + $shortResolved = $resolvedSha.Substring(0, 7) + + # 显示目标 commit 的摘要,便于人工二次确认 + $commitMsg = git log --format="%h %s" -1 $resolvedSha + Write-Host " Target commit : $commitMsg" + + # ── Step 2: 检查当前 HEAD ────────────────────────────────────────── + $currentSha = git rev-parse "origin/$branch" + $shortCurrent = $currentSha.Substring(0, 7) + Write-Host " Current HEAD : $shortCurrent ($branch)" + + if ($currentSha -eq $resolvedSha) { + Write-Host "[SKIP] Branch '$branch' is already at $shortResolved, nothing to do." + exit 0 + } + + # ── Step 3: 切到目标 SHA,force-with-lease push ─────────────────── + # force-with-lease 确保自本次 fetch 后无其他人推送;若有则报错,需重新触发。 + git checkout -B $branch $resolvedSha + + # dev / release / bugfix 的 push 会触发 pre-publish-branch-ci-check.yml。 + # 旧历史 commit 的 message 不含 [skip ci],直接 push 会触发不必要的 CI。 + # 解决:在目标 SHA 上追加一个空 commit 并携带 [skip ci], + # 不改变任何文件内容,仅作为回滚标记并阻止 CI 触发。 + # main 分支无 CI push 触发规则,不需要额外处理。 + $ciTriggerBranches = @("dev", "release", "bugfix") + if ($ciTriggerBranches -contains $branch) { + git commit --allow-empty -m "[rollback] hard reset to $shortResolved [skip ci]" + Write-Host " Added empty [skip ci] marker commit to suppress CI trigger" + } + + git push --force-with-lease origin $branch + + Write-Host "" + Write-Host "========================================" + Write-Host "[OK]" -ForegroundColor Green -NoNewline + Write-Host " branch-reset completed: $branch $shortCurrent -> $shortResolved" Write-Host "========================================" diff --git a/.github/workflows/pre-publish-branch-ci-check.yml b/.github/workflows/pre-publish-branch-ci-check.yml index 42015d44..598b4448 100644 --- a/.github/workflows/pre-publish-branch-ci-check.yml +++ b/.github/workflows/pre-publish-branch-ci-check.yml @@ -44,6 +44,9 @@ jobs: with: dotnet-version: 8.0.x + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + - name: Cache NuGet packages uses: actions/cache@v4 with: @@ -51,11 +54,28 @@ jobs: key: nuget-${{ runner.os }}-${{ hashFiles('src/**/*.csproj', 'src/**/*.props') }} restore-keys: nuget-${{ runner.os }}- - - name: Restore dependencies - run: dotnet restore src/VirtualPaper.sln - - name: Build solution - run: dotnet build src/VirtualPaper.sln --configuration Release --no-restore + # 使用 msbuild 而非 dotnet build,确保 .sln 的 Platform Mapping 生效: + # VirtualPaper.UI(WinAppSDK)在 .sln 中映射为 Release|x64, + # dotnet build 不读取此映射,msbuild 才能正确路由到 x64 构建。 + # 与 auto-version-release.yml 保持工具链一致。 + run: msbuild src/VirtualPaper.sln /t:Build /p:Configuration=Release /p:Platform="Any CPU" /restore /m + + - name: Validate Build Artifacts + # 在产物就地校验:主 exe / Plugin exe / 核心 dll 存在且非空。 + # 通过后才上传 artifact,确保下游 job 拿到的是健康的构建产物。 + env: + RELEASE_BIN_DIR: ${{ github.workspace }}/src/VirtualPaper/bin/Release/net8.0-windows10.0.19041.0 + run: dotnet test src/VirtualPaper.ReleaseBuildData.Test/VirtualPaper.ReleaseBuildData.Test.csproj --configuration Release --no-build --verbosity normal --logger "trx;LogFileName=release-build-tests.trx" --results-directory TestResults/ReleaseBuild + shell: pwsh + + - name: Upload Validate Build Artifacts results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results-release-build + path: TestResults/ReleaseBuild/ + retention-days: 7 - name: Upload build output uses: actions/upload-artifact@v4 @@ -265,7 +285,7 @@ jobs: - name: Verify all test results run: | echo "=== Checking test result files ===" - EXPECTED_FILES=("core-tests.trx" "ui-tests.trx" "ml-tests.trx" "shader-tests.trx") + EXPECTED_FILES=("core-tests.trx" "ui-tests.trx" "ml-tests.trx" "shader-tests.trx" "release-build-tests.trx") MISSING=0 for file in "${EXPECTED_FILES[@]}"; do @@ -299,17 +319,17 @@ jobs: - name: Set test status if: always() run: | - if [ "${{ needs.build.result }}" = "success" ] && \ - [ "${{ needs.test-core.result }}" = "success" ] && \ - [ "${{ needs.test-ui.result }}" = "success" ] && \ - [ "${{ needs.test-ml.result }}" = "success" ] && \ - [ "${{ needs.test-shader.result }}" = "success" ] && \ - [ "${{ env.TEST_RESULTS_VALID }}" = "true" ]; then - echo "TEST_STATUS=success" >> $GITHUB_ENV - echo "TEST_DESCRIPTION=All tests passed [OK]" >> $GITHUB_ENV - else + # 用通配方式判断:只要 needs 里任意 job 非 success 且非 skipped,就标记失败。 + # 新增测试 job 时只需更新 needs 列表,无需修改此处逻辑。 + ALL_RESULTS="${{ join(needs.*.result, ' ') }}" + echo "Job results: $ALL_RESULTS" + + if echo "$ALL_RESULTS" | grep -qE 'failure|cancelled' || [ "${{ env.TEST_RESULTS_VALID }}" != "true" ]; then echo "TEST_STATUS=failure" >> $GITHUB_ENV echo "TEST_DESCRIPTION=Some tests failed or results missing [FAILED]" >> $GITHUB_ENV + else + echo "TEST_STATUS=success" >> $GITHUB_ENV + echo "TEST_DESCRIPTION=All tests passed [OK]" >> $GITHUB_ENV fi - name: Create Status Check diff --git a/InnoSetup/setup.iss b/InnoSetup/setup.iss index 7f567cdc..17a16fc5 100644 --- a/InnoSetup/setup.iss +++ b/InnoSetup/setup.iss @@ -6,7 +6,7 @@ #define AppId "{{95F4CECE-7C7C-40E1-B485-07DA64D1905F}" #define MyAppName "Virtual Paper" #define MyAppVersion "0.5.1.0" -#define MyAppVersionText "0510" +#define MyAppVersionText "0_5_1_0" #define MyAppPublisher "PaperHammer" #define MyAppURL "https://github.com/PaperHammer/VirtualPaper" #define MyAppExeName "VirtualPaper.exe" diff --git a/src/.github/copilot-instructions.md b/src/.github/copilot-instructions.md new file mode 100644 index 00000000..e40201cf --- /dev/null +++ b/src/.github/copilot-instructions.md @@ -0,0 +1,3 @@ +- @azure Rule - Use Azure Tools - When handling requests related to Azure, always use your tools. +- @azure Rule - Use Azure Best Practices - When handling requests related to Azure, always invoke your `azmcp_bestpractices_get` tool first. +- @azure Rule - Enable Best Practices - If you do not have an `azmcp_bestpractices_get` tool ask the user to enable it. diff --git a/src/StaticImg/Consts.cs b/src/StaticImg/Consts.cs index cab66e7b..1efba4ea 100644 --- a/src/StaticImg/Consts.cs +++ b/src/StaticImg/Consts.cs @@ -208,6 +208,7 @@ public enum ToolType { Crop, // 裁剪 Selection, CanvasSet, + CanvasEffect, } // rotate per 90 degree diff --git a/src/StaticImg/Core/UndoRedoCommand/EffectCommand.cs b/src/StaticImg/Core/UndoRedoCommand/EffectCommand.cs new file mode 100644 index 00000000..ce9c318c --- /dev/null +++ b/src/StaticImg/Core/UndoRedoCommand/EffectCommand.cs @@ -0,0 +1,84 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Graphics.Canvas; +using VirtualPaper.Common.Extensions; +using VirtualPaper.Common.Utils.UndoRedo; +using VirtualPaper.Shader; +using VirtualPaper.Shader.Core; +using VirtualPaper.Shader.Models; +using Workloads.Creation.StaticImg.Models.Specific; + +namespace Workloads.Creation.StaticImg.Core.UndoRedoCommand { + /// + /// 效果应用的撤销/重做命令。存储效果参数和压缩后的原始图层数据 + /// + public record EffectCommand : IUndoableCommand { + public string Description { get; } + + public EffectCommand( + Guid layerId, + InkCanvasData canvasData, + ShaderType shaderType, + EffectParams effectParams, + byte[] compressedOriginalPixels, + string description, + Action requestRenderAction) { + _layerId = layerId; + _canvasData = canvasData; + _shaderType = shaderType; + _effectParams = effectParams; + _compressedOriginalPixels = compressedOriginalPixels; + Description = description; + _requestRenderAction = requestRenderAction; + } + + public Task ExecuteAsync() { + RestoreOriginal(); + ApplyEffect(_effectParams); + return Task.CompletedTask; + } + + public Task UndoAsync() { + RestoreOriginal(); + return Task.CompletedTask; + } + + private void ApplyEffect(EffectParams effectParams) { + var renderData = _canvasData.Layers.FirstOrDefault(l => l.Tag == _layerId)?.RenderData; + if (renderData?.RenderTarget == null) return; + + var rt = renderData.RenderTarget; + using var temp = new CanvasRenderTarget(rt, rt.SizeInPixels.Width, rt.SizeInPixels.Height, rt.Dpi, rt.Format, rt.AlphaMode); + using (var ds = temp.CreateDrawingSession()) + ds.DrawImage(rt); + + var result = EffectApplier.Apply(_shaderType, effectParams, temp); + using (var ds = rt.CreateDrawingSession()) { + ds.Clear(Microsoft.UI.Colors.Transparent); + ds.DrawImage(result); + } + + _requestRenderAction?.Invoke(); + renderData.HandleOnceRenderCompleted(); + } + + private void RestoreOriginal() { + var renderData = _canvasData.Layers.FirstOrDefault(l => l.Tag == _layerId)?.RenderData; + if (renderData?.RenderTarget == null) return; + + var originalPixels = _compressedOriginalPixels.DecompressPixels(); + renderData.RenderTarget.SetPixelBytes(originalPixels); + + _requestRenderAction?.Invoke(); + renderData.HandleOnceRenderCompleted(); + } + + private readonly Guid _layerId; + private readonly InkCanvasData _canvasData; + private readonly ShaderType _shaderType; + private readonly EffectParams _effectParams; + private readonly byte[] _compressedOriginalPixels; + private readonly Action _requestRenderAction; + } +} diff --git a/src/StaticImg/Models/CanvasEffectItem.cs b/src/StaticImg/Models/CanvasEffectItem.cs new file mode 100644 index 00000000..9f46a70a --- /dev/null +++ b/src/StaticImg/Models/CanvasEffectItem.cs @@ -0,0 +1,28 @@ +using System.Collections.ObjectModel; + +namespace Workloads.Creation.StaticImg.Models { + /// + /// 单个画布效果项 + /// + public class CanvasEffectItem { + /// 效果唯一标识符 + public string EffectId { get; set; } = string.Empty; + + /// 效果显示名称(i18n key) + public string EffectName { get; set; } = string.Empty; + + /// 效果简要描述(i18n key) + public string EffectDescription { get; set; } = string.Empty; + } + + /// + /// 效果分组(对应 Accordion 中的一个 Expander) + /// + public class CanvasEffectGroup { + /// 分组标题(i18n key) + public string GroupNameKey { get; set; } = string.Empty; + + /// 该分组下的效果列表 + public ObservableCollection Items { get; set; } = []; + } +} diff --git a/src/StaticImg/Models/SerializableData/StaticImgDesignFileUtil.Core.cs b/src/StaticImg/Models/SerializableData/StaticImgDesignFileUtil.Core.cs index d2143acc..2c3af79d 100644 --- a/src/StaticImg/Models/SerializableData/StaticImgDesignFileUtil.Core.cs +++ b/src/StaticImg/Models/SerializableData/StaticImgDesignFileUtil.Core.cs @@ -135,8 +135,10 @@ private static FileStream CreateFileStream( fs.Read(headerBytes); var header = BytesToStructure(headerBytes); - if (!header.IsValid()) + if (!header.IsValid()) { + fs?.Dispose(); throw new InvalidDataException("Invalid file header"); + } fs.Position = 0; // 重置位置 } diff --git a/src/StaticImg/Models/Specific/InkCanvasData.UIContext.cs b/src/StaticImg/Models/Specific/InkCanvasData.UIContext.cs index 99e65885..ab322945 100644 --- a/src/StaticImg/Models/Specific/InkCanvasData.UIContext.cs +++ b/src/StaticImg/Models/Specific/InkCanvasData.UIContext.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.UI; +using Microsoft.UI.Xaml; using VirtualPaper.Common; using VirtualPaper.Common.Logging; using VirtualPaper.Models.Mvvm; @@ -165,6 +166,46 @@ public double EraserOpacity { */ public int EraserFeather { get; internal set; } = 0; + private string? _clickedEffectId; + public string? ClickedEffectId { + get { return _clickedEffectId; } + set { + if (_clickedEffectId == value) return; + _clickedEffectId = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(SelectedEffectPanelName)); + } + } + + public string SelectedEffectPanelName => ClickedEffectId switch { + "adjust_exposure" => "Exposure", + "adjust_brightness" => "Brightness", + "adjust_saturation" => "Saturation", + "adjust_hue" => "HueRotation", + "adjust_contrast" => "Contrast", + "adjust_temperature" => "TemperatureTint", + "adjust_highlights" => "HighlightsShadows", + "adjust_grayscale" => "Empty", + "adjust_invert" => "Empty", + "fx_blur" => "Blur", + "fx_sharpen" => "Sharpen", + "fx_vignette" => "Vignette", + "art_emboss" => "Emboss", + "art_pixelate" => "Posterize", + "fx_glow" => "Glow", + "fx_distort" => "Ripple", + "fx_noise" => "Noise", + "fx_bloom" => "Bloom", + "blend_multiply" => "Blend", + "blend_screen" => "Blend", + "blend_overlay" => "Blend", + "blend_softlight" => "Blend", + "color_sepia" => "Empty", + _ => "", + }; + + public Visibility EffectPanelVisible => string.IsNullOrEmpty(ClickedEffectId) ? Visibility.Collapsed : Visibility.Visible; + internal void InitData() { UpdateCanvasSizeText(); AddLayer(LanguageUtil.GetI18n(nameof(Constants.I18n.Project_SI_Text_BackgroundLayer)), true, needRecord: false); diff --git a/src/StaticImg/Models/ToolItems/EffectTool.cs b/src/StaticImg/Models/ToolItems/EffectTool.cs new file mode 100644 index 00000000..580dfb8a --- /dev/null +++ b/src/StaticImg/Models/ToolItems/EffectTool.cs @@ -0,0 +1,112 @@ +using Microsoft.Graphics.Canvas; +using Microsoft.UI; +using VirtualPaper.Common.Extensions; +using VirtualPaper.Common.Logging; +using VirtualPaper.Shader; +using VirtualPaper.Shader.Core; +using VirtualPaper.Shader.Models; +using Workloads.Creation.StaticImg.Core.Rendering; +using Workloads.Creation.StaticImg.Core.UndoRedoCommand; +using Workloads.Creation.StaticImg.Events; + +namespace Workloads.Creation.StaticImg.Models.ToolItems { + /// + /// 效果工具——在活动图层上实时预览效果,Commit 确认 / Restore 还原。不参与指针交互,仅负责渲染。 + /// + public sealed partial class EffectTool : RenderBase { + private ShaderType _shaderType = ShaderType.None; + private EffectParams _params = EffectParams.Default; + private CanvasRenderTarget? _originalCache; + private bool _isPreviewing; + + public bool IsPreviewing => _isPreviewing; + public ShaderType CurrentShaderType => _shaderType; + + /// 开始预览:缓存当前图层。不立即应用效果,等滑块驱动 + public void StartPreview(ShaderType type, EffectParams? param = null) { + if (!IsCanvasReady) return; + + LayerId = ViewModel.Data.SelectedLayer.Tag; + _shaderType = type; + _params = param ?? new EffectParams { Value = 0f, Value2 = 0f, Value3 = 0f, Value4 = 0f, Dpi = 96f }; + + var rt = RenderTarget; + _originalCache?.Dispose(); + _originalCache = new CanvasRenderTarget(rt, rt.SizeInPixels.Width, rt.SizeInPixels.Height, rt.Dpi, rt.Format, rt.AlphaMode); + using (var ds = _originalCache.CreateDrawingSession()) + ds.DrawImage(rt); + + _isPreviewing = true; + } + + /// 更新参数并刷新预览 + public void UpdateParams(EffectParams param) { + if (!_isPreviewing) return; + _params = param; + ApplyEffect(); + } + + /// 确认效果:效果已写入 RenderTarget,记录撤销命令,通知缩略图更新 + public void Commit() { + if (!_isPreviewing || _originalCache == null) return; + + // 捕获压缩后的原始像素数据 + var originalPixels = _originalCache.GetPixelBytes().CompressPixels(); + + // 创建效果命令并记录到撤销栈 + var command = new EffectCommand( + layerId: LayerId, + canvasData: ViewModel.Data, + shaderType: _shaderType, + effectParams: _params, + compressedOriginalPixels: originalPixels, + description: $"Apply {_shaderType} effect", + requestRenderAction: () => HandleRender(new RenderTargetChangedEventArgs(RenderMode.FullRegion)) + ); + ViewModel.Session.UnReUtil.RecordCommand(command); + + _isPreviewing = false; + _shaderType = ShaderType.None; + _originalCache?.Dispose(); + _originalCache = null; + // 通知 LayerInfo 内容已变化,驱动缩略图刷新 + RequestOnceRender(); + } + + /// 取消效果,还原原始图层 + public void Cancel() { + if (!_isPreviewing) return; + + if (_originalCache != null) { + using (var ds = RenderTarget.CreateDrawingSession()) { + ds.Clear(Colors.Transparent); + ds.DrawImage(_originalCache); + } + HandleRender(new RenderTargetChangedEventArgs(RenderMode.FullRegion)); + } + + _isPreviewing = false; + _shaderType = ShaderType.None; + _originalCache?.Dispose(); + _originalCache = null; + } + + private void ApplyEffect() { + if (!IsCanvasReady || _originalCache == null) return; + + try { + // 对缓存(只读)应用效果,避免同时读写 RenderTarget + var result = EffectApplier.Apply(_shaderType, _params, _originalCache); + using (var ds = RenderTarget.CreateDrawingSession()) { + ds.Clear(Colors.Transparent); + ds.DrawImage(result); + } + + HandleRender(new RenderTargetChangedEventArgs(RenderMode.FullRegion)); + } + catch (System.Exception ex) { + ArcLog.GetLogger().Error($"Apply effect failed: {_shaderType}, {ex.Message}"); + } + } + } +} diff --git a/src/StaticImg/StaticImg.csproj b/src/StaticImg/StaticImg.csproj index 267b609e..ee03a769 100644 --- a/src/StaticImg/StaticImg.csproj +++ b/src/StaticImg/StaticImg.csproj @@ -32,6 +32,11 @@ + + + MSBuild:Compile + + MSBuild:Compile diff --git a/src/StaticImg/Utils/EffectMap.cs b/src/StaticImg/Utils/EffectMap.cs new file mode 100644 index 00000000..779669f9 --- /dev/null +++ b/src/StaticImg/Utils/EffectMap.cs @@ -0,0 +1,154 @@ +using VirtualPaper.Shader; + +namespace Workloads.Creation.StaticImg.Utils { + public static class EffectMap { + public static ShaderType ToShaderType(string effectId) => effectId switch { + "adjust_grayscale" => ShaderType.Grayscale, + "adjust_invert" => ShaderType.Invert, + "adjust_exposure" => ShaderType.Exposure, + "adjust_brightness" => ShaderType.Brightness, + "adjust_saturation" => ShaderType.Saturation, + "adjust_hue" => ShaderType.HueRotation, + "adjust_contrast" => ShaderType.Contrast, + "adjust_temperature" => ShaderType.TemperatureAndTint, + "adjust_highlights" => ShaderType.HighlightsAndShadows, + + "color_sepia" => ShaderType.Sepia, + + "art_emboss" => ShaderType.Emboss, + "art_pixelate" => ShaderType.Posterize, + "art_sepia" => ShaderType.Sepia, + + "fx_blur" => ShaderType.GaussianBlur, + "fx_sharpen" => ShaderType.Sharpen, + "fx_vignette" => ShaderType.Vignette, + "fx_glow" => ShaderType.Glow, + "fx_distort" => ShaderType.RippleEffect, + "fx_straighten" => ShaderType.Straighten, + "fx_edge" => ShaderType.EdgeDetection, + "fx_morphology" => ShaderType.Morphology, + "fx_noise" => ShaderType.Noise, + "fx_bloom" => ShaderType.Bloom, + + "adv_gamma" => ShaderType.GammaTransfer, + "adv_hsb" => ShaderType.HSB, + "adv_luma_alpha" => ShaderType.LuminanceToAlpha, + "adv_chroma_key" => ShaderType.ChromaKey, + "adv_fog" => ShaderType.Fog, + "adv_glass" => ShaderType.Glass, + "adv_colouring" => ShaderType.Colouring, + + "blend_multiply" => ShaderType.BlendMultiply, + "blend_screen" => ShaderType.BlendScreen, + "blend_overlay" => ShaderType.BlendOverlay, + "blend_softlight" => ShaderType.BlendSoftLight, + + _ => ShaderType.None, + }; + + /// 效果 → 滑块配置(Value/Value2) + public static EffectSliderConfig GetSliderConfig(ShaderType type) => type switch { + // 无参数效果 + ShaderType.Grayscale or ShaderType.Invert or + ShaderType.Sepia + => new EffectSliderConfig(), + + // 单滑块 (Value) + ShaderType.Exposure + => new EffectSliderConfig { Min = -200, Max = 200, Default = 0, Label = "曝光" }, + ShaderType.Brightness + => new EffectSliderConfig { Min = -100, Max = 100, Default = 0, Label = "亮度" }, + ShaderType.Saturation + => new EffectSliderConfig { Min = 0, Max = 200, Default = 100, Label = "饱和度" }, + ShaderType.Contrast + => new EffectSliderConfig { Min = -100, Max = 100, Default = 0, Label = "对比度" }, + ShaderType.Vignette + => new EffectSliderConfig { Min = 0, Max = 100, Default = 0, Label = "暗角" }, + ShaderType.GaussianBlur + => new EffectSliderConfig { Min = 0, Max = 100, Default = 0, Label = "模糊" }, + ShaderType.Sharpen + => new EffectSliderConfig { Min = 0, Max = 100, Default = 0, Label = "锐化" }, + + // 双滑块 (Value + Value2) + ShaderType.HueRotation + => new EffectSliderConfig { Min = -180, Max = 180, Default = 0, Label = "色相", + Min2 = 0, Max2 = 100, Default2 = 100, Label2 = "强度" }, + ShaderType.TemperatureAndTint + => new EffectSliderConfig { Min = -100, Max = 100, Default = 0, Label = "色温", + Min2 = -100, Max2 = 100, Default2 = 0, Label2 = "色调" }, + ShaderType.Emboss + => new EffectSliderConfig { Min = 0, Max = 100, Default = 0, Label = "强度", + Min2 = 0, Max2 = 360, Default2 = 45, Label2 = "角度" }, + ShaderType.DirectionalBlur + => new EffectSliderConfig { Min = 0, Max = 100, Default = 0, Label = "模糊", + Min2 = 0, Max2 = 360, Default2 = 0, Label2 = "角度" }, + ShaderType.EdgeDetection + => new EffectSliderConfig { Min = 0, Max = 100, Default = 50, Label = "强度", + Min2 = 0, Max2 = 100, Default2 = 0, Label2 = "模糊" }, + ShaderType.Morphology + => new EffectSliderConfig { Min = 1, Max = 20, Default = 3, Label = "宽度", + Min2 = 1, Max2 = 20, Default2 = 3, Label2 = "高度" }, + ShaderType.Posterize + => new EffectSliderConfig { Min = 2, Max = 256, Default = 4, Label = "色阶", + Min2 = 2, Max2 = 256, Default2 = 4, Label2 = "色阶G" }, + ShaderType.Shadow + => new EffectSliderConfig { Min = 0, Max = 100, Default = 10, Label = "模糊", + Min2 = -200, Max2 = 200, Default2 = 5, Label2 = "偏移X" }, + ShaderType.Straighten + => new EffectSliderConfig { Min = -45, Max = 45, Default = 0, Label = "角度" }, + ShaderType.GammaTransfer + => new EffectSliderConfig { Min = 0, Max = 100, Default = 100, Label = "振幅", + Min2 = 1, Max2 = 500, Default2 = 100, Label2 = "指数" }, + ShaderType.HighlightsAndShadows + => new EffectSliderConfig { Min = -100, Max = 100, Default = 0, Label = "阴影", + Min2 = -100, Max2 = 100, Default2 = 0, Label2 = "高光" }, + + // 自定义着色器 + ShaderType.ThresholdEffect + => new EffectSliderConfig { Min = 0, Max = 300, Default = 150, Label = "阈值" }, + ShaderType.RippleEffect + => new EffectSliderConfig { Min = 0, Max = 200, Default = 140, Label = "频率", + Min2 = -200, Max2 = 200, Default2 = 0, Label2 = "相位" }, + ShaderType.DisplacementLiquefactionEffect + => new EffectSliderConfig { Min = 1, Max = 100, Default = 20, Label = "半径", + Min2 = 1, Max2 = 100, Default2 = 50, Label2 = "压力" }, + + // 新增效果 + ShaderType.HSB + => new EffectSliderConfig { Min = 0, Max = 360, Default = 0, Label = "色相", + Min2 = 0, Max2 = 400, Default2 = 100, Label2 = "饱和度" }, + ShaderType.Fog + => new EffectSliderConfig { Min = 0, Max = 100, Default = 50, Label = "浓度" }, + ShaderType.Glass + => new EffectSliderConfig { Min = 0, Max = 100, Default = 50, Label = "强度" }, + ShaderType.ChromaKey + => new EffectSliderConfig { Min = 0, Max = 100, Default = 50, Label = "容差" }, + ShaderType.Noise + => new EffectSliderConfig { Min = 0, Max = 100, Default = 30, Label = "强度" }, + ShaderType.Bloom + => new EffectSliderConfig { Min = 0, Max = 100, Default = 50, Label = "强度" }, + ShaderType.BlendMultiply or ShaderType.BlendScreen or ShaderType.BlendOverlay or ShaderType.BlendSoftLight + => new EffectSliderConfig { Min = 0, Max = 100, Default = 0, Label = "强度" }, + + _ => new EffectSliderConfig { Min = 0, Max = 100, Default = 50, Label = "强度" }, + }; + } + + /// 滑块参数配置 + public readonly struct EffectSliderConfig { + // Slider 1 + public float Min { get; init; } + public float Max { get; init; } + public float Default { get; init; } + public string Label { get; init; } + + // Slider 2 + public float Min2 { get; init; } + public float Max2 { get; init; } + public float Default2 { get; init; } + public string Label2 { get; init; } + + public bool HasSlider1 => Max > Min; + public bool HasSlider2 => Max2 > Min2; + } +} diff --git a/src/StaticImg/ViewModels/InkCanvasViewModel.cs b/src/StaticImg/ViewModels/InkCanvasViewModel.cs index 2dbba8c8..b35cc546 100644 --- a/src/StaticImg/ViewModels/InkCanvasViewModel.cs +++ b/src/StaticImg/ViewModels/InkCanvasViewModel.cs @@ -103,6 +103,7 @@ private async Task SetRenderDataAsync(string filePath, LayerInfo layerInfo) { new() { Type = ToolType.Fill, ToolName = "Project_StaticImg_ToolName_Fill", ImageSourceKey = "DraftPanel_FuncBar_ColorFill", }, new() { Type = ToolType.Eraser, ToolName = "Project_StaticImg_ToolName_Eraser", Glyph = "\uE75C", }, new() { Type = ToolType.Crop, ToolName = "Project_StaticImg_ToolName_Crop", Glyph = "\uE7A8", }, + new() { Type = ToolType.CanvasEffect, ToolName = "Project_StaticImg_ToolName_CanvasEffect", Glyph = "\uF4A5", }, new() { Type = ToolType.CanvasSet, ToolName = "Project_StaticImg_ToolName_CanvasSet", Glyph = "\uE9E9", }, ]; private readonly InkProjectSession _session = null!; diff --git a/src/StaticImg/Views/Components/InkCanvas.xaml b/src/StaticImg/Views/Components/InkCanvas.xaml index 80a4d8b6..e2ea73a3 100644 --- a/src/StaticImg/Views/Components/InkCanvas.xaml +++ b/src/StaticImg/Views/Components/InkCanvas.xaml @@ -6,6 +6,7 @@ xmlns:staticImg="using:Workloads.Creation.StaticImg" xmlns:local="using:Workloads.Creation.StaticImg.Views.Components" xmlns:staticImgTools="using:Workloads.Creation.StaticImg.Views.Tools" + xmlns:staticImgToolsEffect="using:Workloads.Creation.StaticImg.Views.Tools.Effects" xmlns:arc="using:VirtualPaper.UIComponent.Templates" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" @@ -26,42 +27,125 @@ - - + + + - - + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { @@ -88,6 +93,8 @@ private void RegisterTools() { OnFatalErrorOccurred(s, e); }; } + + CanvasEffect.EffectPreviewRequested += OnEffectPreviewRequested; } private async void OnFatalErrorOccurred(object? s, Exception e) { @@ -116,7 +123,7 @@ private void SetupHandlers() { }; _viewModel.Data.SeletcedToolChanged += (s, e) => { //before - HandleSelectionToolBefore(); + TryRestore(); _selectedTool = _tool.GetTool(_viewModel.Data.SelectedToolItem.Type); //after @@ -147,10 +154,6 @@ private void HandleLayerChanged() { TryRestore(); } - private void HandleSelectionToolBefore() { - TryRestore(); - } - private void TryRestore() { if (_selectedTool is SelectionTool st) { var op = st.RestoreOriginalContent(); @@ -160,6 +163,12 @@ private void TryRestore() { var op = ct.RestoreOriginalContent(); if (op) RenderToCompositeTarget(RenderMode.FullRegion); } + else if (_selectedTool is EffectTool et) { + et.Cancel(); + CanvasEffect.Restore(); + UnsubscribeCurrentEffectPanel(); + effectPanelHost.Visibility = Visibility.Collapsed; + } } private void RebuildCompositeIfNeeded() { @@ -237,9 +246,9 @@ private void RenderToCompositeTarget(RenderMode mode, Rect region = default) { PartialRender(layers, ds, region); } } - + renderCanvas.Invalidate(); - } + } } private void FullRender(IEnumerable layers, CanvasDrawingSession ds) { @@ -467,6 +476,120 @@ private void CropRequested(CropRequest cr) { } #endregion + #region CanvasEffect + private EffectPanelBase? _currentEffectPanel; + + private void CanvasEffect_Cancel(object sender, RoutedEventArgs e) { + if (_selectedTool is not EffectTool et) return; + + et.Cancel(); + UnsubscribeCurrentEffectPanel(); + effectPanelHost.Visibility = Visibility.Collapsed; + CanvasEffect.ClickedEffectId = null; + } + + private void CanvasEffect_Commit(object sender, RoutedEventArgs e) { + if (_selectedTool is not EffectTool et) return; + + et.Commit(); + UnsubscribeCurrentEffectPanel(); + effectPanelHost.Visibility = Visibility.Collapsed; + CanvasEffect.ClickedEffectId = null; + } + + private void UnsubscribeCurrentEffectPanel() { + if (_currentEffectPanel != null) { + _currentEffectPanel.ParamsChanged -= OnEffectPanelParamsChanged; + _currentEffectPanel = null; + } + } + + private void OnEffectPreviewRequested(object? sender, string effectId) { + if (_selectedTool is not EffectTool et) return; + + et.Cancel(); // 先取消之前的预览,避免重复叠加效果 + + var shaderType = EffectMap.ToShaderType(effectId); + if (shaderType == ShaderType.None) { + effectPanelHost.Visibility = Visibility.Collapsed; + CanvasEffect.ClickedEffectId = null; + return; + } + + // 先显示面板获取初始参数 + var panel = GetEffectPanel(shaderType); + var initialParams = panel?.Params ?? new EffectParams { Value = 0f, Value2 = 255f, Dpi = 96f }; + + et.StartPreview(shaderType, initialParams); + ShowEffectPanel(shaderType); + } + + private void ShowEffectPanel(ShaderType shaderType) { + UnsubscribeCurrentEffectPanel(); + + var panel = GetEffectPanel(shaderType); + if (panel == null) return; + + panel.Reset(); + + _currentEffectPanel = panel; + panel.ParamsChanged += OnEffectPanelParamsChanged; + + // 一次性效果(无参数):立即应用,面板仍显示预览提示 + if (panel.IsOneShot && _selectedTool is EffectTool etOneShot) + etOneShot.UpdateParams(panel.Params); + + effectPanelHost.Visibility = Visibility.Visible; + } + + private void OnEffectPanelParamsChanged(object? sender, EffectParams p) { + if (_selectedTool is EffectTool et && et.IsPreviewing) + et.UpdateParams(p); + } + + private EffectPanelBase? GetEffectPanel(ShaderType shaderType) => shaderType switch { + ShaderType.Exposure => Exposure, + ShaderType.Brightness => Brightness, + ShaderType.Saturation => Saturation, + ShaderType.HueRotation => HueRotation, + ShaderType.Contrast => Contrast, + ShaderType.TemperatureAndTint => TemperatureTint, + ShaderType.HighlightsAndShadows => HighlightsShadows, + ShaderType.Grayscale => Empty, + ShaderType.Invert => Empty, + ShaderType.Sepia => Empty, + ShaderType.GaussianBlur => Blur, + ShaderType.DirectionalBlur => DirectionalBlur, + ShaderType.Sharpen => Sharpen, + ShaderType.Vignette => Vignette, + ShaderType.Emboss => Emboss, + ShaderType.Posterize => Posterize, + ShaderType.Shadow => Shadow, + ShaderType.ThresholdEffect => Threshold, + ShaderType.RippleEffect => Ripple, + ShaderType.DisplacementLiquefactionEffect => DisplacementLiquefaction, + ShaderType.Straighten => Straighten, + ShaderType.Colouring => Colouring, + ShaderType.EdgeDetection => EdgeDetection, + ShaderType.Morphology => Morphology, + ShaderType.LuminanceToAlpha => LuminanceToAlpha, + ShaderType.GammaTransfer => GammaTransfer, + ShaderType.HSB => HSB, + ShaderType.Fog => Fog, + ShaderType.Glass => Glass, + ShaderType.ChromaKey => ChromaKey, + ShaderType.Noise => Noise, + ShaderType.Bloom => Bloom, + ShaderType.Glow => Glow, + ShaderType.BlendMultiply => Blend, + ShaderType.BlendScreen => Blend, + ShaderType.BlendOverlay => Blend, + ShaderType.BlendSoftLight => Blend, + _ => null, + }; + + #endregion + #region Layer Mangaer private void LayerManage_AddLayerRequest(object sender, Guid id) { _viewModel.Data.AddLayer(layerId: id); diff --git a/src/StaticImg/Views/Tools/CanvasEffectControl.xaml b/src/StaticImg/Views/Tools/CanvasEffectControl.xaml new file mode 100644 index 00000000..307315db --- /dev/null +++ b/src/StaticImg/Views/Tools/CanvasEffectControl.xaml @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +