diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml new file mode 100644 index 0000000..5b8cae9 --- /dev/null +++ b/.github/workflows/android-release.yml @@ -0,0 +1,143 @@ +name: Android Release Build + +on: + workflow_dispatch: + inputs: + package_format: + description: Android package output to build + required: true + type: choice + default: both + options: + - both + - aab + - apk + +jobs: + build-android: + runs-on: windows-latest + + env: + PROJECT_PATH: src/app/ShadersCamera.csproj + TARGET_FRAMEWORK: net10.0-android + OUTPUT_DIR: artifacts/android + MANIFEST_PATH: src/app/Platforms/Android/AndroidManifest.xml + KEYSTORE_FILE_NAME: release.keystore + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET from global.json + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + + - name: Install MAUI workload + shell: pwsh + run: dotnet workload install maui + + - name: Decode Android keystore + shell: pwsh + run: | + $keystorePath = Join-Path $pwd $env:KEYSTORE_FILE_NAME + [IO.File]::WriteAllBytes( + $keystorePath, + [Convert]::FromBase64String("${{ secrets.ANDROID_KEYSTORE }}") + ) + "KEYSTORE_PATH=$keystorePath" >> $env:GITHUB_ENV + + - name: Set version variables + shell: pwsh + run: | + [xml]$project = Get-Content $env:PROJECT_PATH + [xml]$manifest = Get-Content $env:MANIFEST_PATH + $androidNs = "http://schemas.android.com/apk/res/android" + + $projectNode = $project.Project.PropertyGroup | Where-Object { $_.ApplicationDisplayVersion } | Select-Object -First 1 + $displayBase = if ($projectNode -and $projectNode.ApplicationDisplayVersion) { "$($projectNode.ApplicationDisplayVersion)" } else { "1.0" } + + $manifestVersionCode = [int]$manifest.manifest.GetAttribute("versionCode", $androidNs) + $runNumber = [int]"${{ github.run_number }}" + $versionCode = $manifestVersionCode + $runNumber + $versionDisplay = "$displayBase.$runNumber" + + "VERSION_DISPLAY=$versionDisplay" >> $env:GITHUB_ENV + "VERSION_CODE=$versionCode" >> $env:GITHUB_ENV + + - name: Resolve package formats + shell: pwsh + run: | + $packageFormat = "${{ inputs.package_format }}" + "PACKAGE_FORMAT=$packageFormat" >> $env:GITHUB_ENV + + - name: Restore NuGet packages + shell: pwsh + run: dotnet restore "$env:PROJECT_PATH" + + - name: Publish signed Android packages + shell: pwsh + run: | + function Publish-AndroidPackage([string]$packageFormat) { + $publishArgs = @( + 'publish' + $env:PROJECT_PATH + '-f' + $env:TARGET_FRAMEWORK + '-c' + 'Release' + '-p:AndroidKeyStore=true' + "-p:AndroidPackageFormats=$packageFormat" + "-p:AndroidVersionCode=$env:VERSION_CODE" + "-p:ApplicationVersion=$env:VERSION_CODE" + "-p:ApplicationDisplayVersion=$env:VERSION_DISPLAY" + "-p:AndroidSigningKeyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" + "-p:AndroidSigningKeyPass=${{ secrets.ANDROID_KEY_PASSWORD }}" + "-p:AndroidSigningStorePass=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" + "-p:AndroidSigningKeyStore=$env:KEYSTORE_PATH" + ) + + & dotnet @publishArgs + if ($LASTEXITCODE -ne 0) { + throw "dotnet publish failed for package format '$packageFormat'." + } + } + + if ($env:PACKAGE_FORMAT -eq 'both') { + Publish-AndroidPackage 'aab' + Publish-AndroidPackage 'apk' + } + else { + Publish-AndroidPackage $env:PACKAGE_FORMAT + } + + - name: Collect Android artifacts + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path "$env:OUTPUT_DIR" | Out-Null + $files = Get-ChildItem -Path (Split-Path $env:PROJECT_PATH -Parent) -Recurse -File | + Where-Object { + $_.Extension -in '.aab', '.apk' -and + $_.BaseName -like '*-Signed' + } + + if (-not $files) { + throw "No signed Android artifacts were produced." + } + + foreach ($file in $files) { + Copy-Item $file.FullName -Destination "$env:OUTPUT_DIR" -Force + } + + - name: Upload Android artifacts + uses: actions/upload-artifact@v4 + with: + name: android-${{ inputs.package_format }}-release + path: artifacts/android + if-no-files-found: error \ No newline at end of file diff --git a/.github/workflows/dotnet-windows.yml b/.github/workflows/dotnet-windows.yml index 3153274..9f39338 100644 --- a/.github/workflows/dotnet-windows.yml +++ b/.github/workflows/dotnet-windows.yml @@ -5,23 +5,27 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] + workflow_dispatch: jobs: build: - runs-on: windows-latest steps: - - uses: actions/checkout@v3 - - name: Setup .NET - uses: actions/setup-dotnet@v2 - with: - dotnet-version: 9.0.x - - - name: Install MAUI workload - run: dotnet workload install maui - - - name: Build - run: dotnet build src/app/ShadersCamera.csproj -c Release -f:net9.0-windows10.0.19041.0 + - uses: actions/checkout@v4 + + - name: Setup .NET from global.json + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Install MAUI workload + run: dotnet workload install maui + + - name: Restore + run: dotnet restore src/app/ShadersCamera.csproj + + - name: Build + run: dotnet build src/app/ShadersCamera.csproj -c Release -f:net10.0-windows10.0.19041.0 --no-restore diff --git a/.github/workflows/ios-release.yml b/.github/workflows/ios-release.yml new file mode 100644 index 0000000..1cb56e8 --- /dev/null +++ b/.github/workflows/ios-release.yml @@ -0,0 +1,197 @@ +name: iOS IPA Build + +on: + workflow_dispatch: + +jobs: + validate-signing: + runs-on: macos-15 + + env: + PROJECT_PATH: src/app/ShadersCamera.csproj + CERTIFICATE_PATH: ${{ github.workspace }}/ios-signing.p12 + PROFILE_PATH: ${{ github.workspace }}/ios-signing.mobileprovision + PROFILE_PLIST_PATH: ${{ github.workspace }}/ios-signing-profile.plist + KEYCHAIN_PATH: ${{ github.workspace }}/ios-signing.keychain-db + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Decode signing assets + shell: bash + run: | + echo "${{ secrets.IOS_P12_BASE64 }}" | base64 --decode > "$CERTIFICATE_PATH" + echo "${{ secrets.IOS_MOBILEPROVISION_BASE64 }}" | base64 --decode > "$PROFILE_PATH" + + - name: Validate signing materials + shell: bash + run: | + KEYCHAIN_PASSWORD="$(uuidgen)" + + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security import "$CERTIFICATE_PATH" -P "${{ secrets.IOS_P12_PASSWORD }}" -f pkcs12 -k "$KEYCHAIN_PATH" -T /usr/bin/codesign -T /usr/bin/security + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + if ! security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep -F "${{ secrets.IOS_CODESIGN_KEY }}" >/dev/null; then + echo "Configured IOS_CODESIGN_KEY was not found in the imported certificate." + exit 1 + fi + + security cms -D -i "$PROFILE_PATH" > "$PROFILE_PLIST_PATH" + + PROFILE_NAME="$(/usr/libexec/PlistBuddy -c 'Print :Name' "$PROFILE_PLIST_PATH")" + PROFILE_APP_ID="$(/usr/libexec/PlistBuddy -c 'Print :Entitlements:application-identifier' "$PROFILE_PLIST_PATH")" + APP_BUNDLE_ID="$(python3 -c 'import os, xml.etree.ElementTree as ET; root = ET.parse(os.environ["PROJECT_PATH"]).getroot(); print(next((node.text.strip() for group in root.findall("PropertyGroup") for node in [group.find("ApplicationId")] if node is not None and node.text), ""))')" + + PROFILE_BUNDLE_PATTERN="${PROFILE_APP_ID#*.}" + + if [[ "$PROFILE_BUNDLE_PATTERN" == '*' ]]; then + : + elif [[ "$PROFILE_BUNDLE_PATTERN" == *'.*' ]]; then + PROFILE_PREFIX="${PROFILE_BUNDLE_PATTERN%.*}" + if [[ "$APP_BUNDLE_ID" != "$PROFILE_PREFIX".* ]]; then + echo "Provisioning profile wildcard '$PROFILE_APP_ID' does not match bundle id '$APP_BUNDLE_ID'." + exit 1 + fi + elif [[ "$PROFILE_BUNDLE_PATTERN" != "$APP_BUNDLE_ID" ]]; then + echo "Provisioning profile app identifier '$PROFILE_APP_ID' does not match bundle id '$APP_BUNDLE_ID'." + exit 1 + fi + + echo "Validated signing identity and provisioning profile: $PROFILE_NAME" + + - name: Cleanup validation assets + if: always() + shell: bash + run: | + security delete-keychain "$KEYCHAIN_PATH" || true + rm -f "$CERTIFICATE_PATH" "$PROFILE_PATH" "$PROFILE_PLIST_PATH" + + build-ios: + needs: validate-signing + runs-on: macos-15 + + env: + PROJECT_PATH: src/app/ShadersCamera.csproj + TARGET_FRAMEWORK: net10.0-ios + OUTPUT_DIR: artifacts/ios + CERTIFICATE_PATH: ${{ github.workspace }}/ios-signing.p12 + PROFILE_PATH: ${{ github.workspace }}/ios-signing.mobileprovision + PROFILE_PLIST_PATH: ${{ github.workspace }}/ios-signing-profile.plist + KEYCHAIN_PATH: ${{ github.workspace }}/ios-signing.keychain-db + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET from global.json + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Select Xcode 16.4 + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '16.4' + + - name: Show Xcode version + shell: bash + run: xcodebuild -version + + - name: Install MAUI workload + shell: bash + run: dotnet workload install maui + + - name: Decode signing assets + shell: bash + run: | + echo "${{ secrets.IOS_P12_BASE64 }}" | base64 --decode > "$CERTIFICATE_PATH" + echo "${{ secrets.IOS_MOBILEPROVISION_BASE64 }}" | base64 --decode > "$PROFILE_PATH" + + - name: Import certificate and provisioning profile + shell: bash + run: | + KEYCHAIN_PASSWORD="$(uuidgen)" + echo "KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> "$GITHUB_ENV" + + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security import "$CERTIFICATE_PATH" -P "${{ secrets.IOS_P12_PASSWORD }}" -f pkcs12 -k "$KEYCHAIN_PATH" -T /usr/bin/codesign -T /usr/bin/security + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security default-keychain -d user -s "$KEYCHAIN_PATH" + security list-keychain -d user -s "$KEYCHAIN_PATH" login.keychain-db + + mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles" + + security cms -D -i "$PROFILE_PATH" > "$PROFILE_PLIST_PATH" + + PROFILE_UUID="$(/usr/libexec/PlistBuddy -c 'Print :UUID' "$PROFILE_PLIST_PATH")" + PROFILE_NAME="$(/usr/libexec/PlistBuddy -c 'Print :Name' "$PROFILE_PLIST_PATH")" + + cp "$PROFILE_PATH" "$HOME/Library/MobileDevice/Provisioning Profiles/$PROFILE_UUID.mobileprovision" + + echo "IOS_PROFILE_UUID=$PROFILE_UUID" >> "$GITHUB_ENV" + echo "IOS_PROFILE_NAME=$PROFILE_NAME" >> "$GITHUB_ENV" + + - name: Set version variables + shell: bash + run: | + DISPLAY_BASE="$(python3 -c 'import os, xml.etree.ElementTree as ET; root = ET.parse(os.environ["PROJECT_PATH"]).getroot(); print(next((node.text.strip() for group in root.findall("PropertyGroup") for node in [group.find("ApplicationDisplayVersion")] if node is not None and node.text), "1.0"))')" + + BUILD_NUMBER="${{ github.run_number }}" + echo "VERSION_DISPLAY=$DISPLAY_BASE.$BUILD_NUMBER" >> "$GITHUB_ENV" + echo "VERSION_BUILD=$BUILD_NUMBER" >> "$GITHUB_ENV" + + - name: Restore NuGet packages + shell: bash + run: | + dotnet restore "$PROJECT_PATH" \ + -p:RuntimeIdentifier=ios-arm64 + + - name: Publish signed IPA + shell: bash + run: | + dotnet publish "$PROJECT_PATH" \ + -f "$TARGET_FRAMEWORK" \ + -c Release \ + -p:RuntimeIdentifier=ios-arm64 \ + -p:ArchiveOnBuild=true \ + -p:BuildIpa=true \ + -p:ProvisioningType=manual \ + -p:CodesignKey="${{ secrets.IOS_CODESIGN_KEY }}" \ + -p:CodesignProvision="$IOS_PROFILE_NAME" \ + -p:ApplicationDisplayVersion="$VERSION_DISPLAY" \ + -p:ApplicationVersion="$VERSION_BUILD" + + - name: Collect IPA artifact + shell: bash + run: | + mkdir -p "$OUTPUT_DIR" + PROJECT_DIR="$(dirname "$PROJECT_PATH")" + IPA_PATH="$(find "$PROJECT_DIR/bin/Release/net10.0-ios" -type f -name "*.ipa" | head -n 1)" + + if [[ -z "$IPA_PATH" ]]; then + echo "No .ipa file was produced." + exit 1 + fi + + cp "$IPA_PATH" "$OUTPUT_DIR/" + + - name: Upload iOS IPA artifact + uses: actions/upload-artifact@v4 + with: + name: ios-ipa-release + path: artifacts/ios + if-no-files-found: error + + - name: Cleanup signing assets + if: always() + shell: bash + run: | + security delete-keychain "$KEYCHAIN_PATH" || true + rm -f "$CERTIFICATE_PATH" "$PROFILE_PATH" "$PROFILE_PLIST_PATH" + rm -f "$HOME/Library/MobileDevice/Provisioning Profiles/$IOS_PROFILE_UUID.mobileprovision" || true \ No newline at end of file diff --git a/README.md b/README.md index 05144d8..13eb370 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,11 @@ Read [the blog article](https://taublast.github.io/posts/FiltersCamera/) 👈 ### Latest Changes -* Fixed camera album creation/permission on iOS 26+ -* Use latest camera nuget with better performance and bug fixes -* Smooth filters menu +* New draw-style shaders: Print, Geisha +* Enabled shaders for Android due to performance gain: Poster, Cartoon +* Save geolocation to EXIF if permissions granted +* Save filter name to EXIF Software +* Now uses .NET 10 and updated nugets ### Install @@ -58,7 +60,6 @@ style="margin-top: 16px;" /> * Rotate saved photo on iOS if taken while rotated even if rotation turned off for app * Rotate previews in menu when phone is rotated to landscape -* Save filter name to EXIF (what field, Software (0x0131)?) * Add selection indicator for previews, scroll to selected at startup * Pass rendering scale as uniform for all shaders for full consistency between preview and large capture * Localization and change language in settings @@ -68,7 +69,6 @@ style="margin-top: 16px;" /> * Create presets (BW, For Kids etc..) * Crop manual/presets * Combine with lens shaders -* Save geolocation to EXIF * Shaders editor for mobile version * ML Z-axis detection and apply smart bokeh @@ -82,6 +82,8 @@ style="margin-top: 16px;" /> Contributing to repository is very welcome. Many other nifty shaders could be added, the current UI is also not something fixed. +CI/CD documentation is in `docs/github-actions-cicd.md`. + ### Credits * **App Screenshots** - created with [Hotpot](https://hotpot.ai/) diff --git a/dev/CameraApp-Refs.sln b/dev/CameraApp-Refs.sln index cec0e71..4f34d07 100644 --- a/dev/CameraApp-Refs.sln +++ b/dev/CameraApp-Refs.sln @@ -5,8 +5,6 @@ VisualStudioVersion = 18.0.11201.2 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Refs", "Refs", "{5B1CDC4F-5ED6-4662-8EC6-3DE3FF0B05BE}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DrawnUi.Maui.Camera", "..\..\DrawnUi.Maui\src\Maui\Addons\DrawnUi.Maui.Camera\DrawnUi.Maui.Camera.csproj", "{DD2D491D-7046-41D2-A00E-FE65CBADE85E}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{089100B1-113F-4E66-888A-E83F3999EAFD}" ProjectSection(SolutionItems) = preProject ..\.gitignore = ..\.gitignore @@ -15,13 +13,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ..\..\ShadersCam.targets = ..\..\ShadersCam.targets EndProjectSection EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "DrawnUi.Shared", "..\..\DrawnUi.Maui\src\Shared\DrawnUi.Shared.shproj", "{83974207-9636-48DD-BDB3-98EDECBB1107}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShadersCamera", "..\src\app\ShadersCamera.csproj", "{9F71E169-6D24-B132-9826-8A6DA9FFFBC8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrawnUi.Maui", "..\..\DrawnUi.Maui\src\Maui\DrawnUi\DrawnUi.Maui.csproj", "{93E119B1-4378-87DF-2DD2-A818D1E6C2A2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrawnUi.Maui.Camera", "..\..\DrawnUi.Maui.Camera\src\Lib\DrawnUi.Maui.Camera.csproj", "{B908CF8F-86DA-57A3-E68C-E59E36316522}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShadersCamera", "..\src\app\ShadersCamera.csproj", "{9F71E169-6D24-B132-9826-8A6DA9FFFBC8}" +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "DrawnUi.Shared", "..\..\DrawnUi\src\Shared\DrawnUi.Shared.shproj", "{83974207-9636-48DD-BDB3-98EDECBB1107}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CameraTests", "..\..\DrawnUi.Maui\src\Maui\Samples\Camera\CameraTests.csproj", "{2C274321-F41E-090D-C929-79400D88D71E}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrawnUi.Maui", "..\..\DrawnUi\src\Maui\DrawnUi\DrawnUi.Maui.csproj", "{315B4382-048D-46A6-E86A-B0684C70741B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -29,39 +27,34 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {DD2D491D-7046-41D2-A00E-FE65CBADE85E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DD2D491D-7046-41D2-A00E-FE65CBADE85E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DD2D491D-7046-41D2-A00E-FE65CBADE85E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DD2D491D-7046-41D2-A00E-FE65CBADE85E}.Release|Any CPU.Build.0 = Release|Any CPU - {93E119B1-4378-87DF-2DD2-A818D1E6C2A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {93E119B1-4378-87DF-2DD2-A818D1E6C2A2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {93E119B1-4378-87DF-2DD2-A818D1E6C2A2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {93E119B1-4378-87DF-2DD2-A818D1E6C2A2}.Release|Any CPU.Build.0 = Release|Any CPU {9F71E169-6D24-B132-9826-8A6DA9FFFBC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9F71E169-6D24-B132-9826-8A6DA9FFFBC8}.Debug|Any CPU.Build.0 = Debug|Any CPU {9F71E169-6D24-B132-9826-8A6DA9FFFBC8}.Debug|Any CPU.Deploy.0 = Debug|Any CPU {9F71E169-6D24-B132-9826-8A6DA9FFFBC8}.Release|Any CPU.ActiveCfg = Release|Any CPU {9F71E169-6D24-B132-9826-8A6DA9FFFBC8}.Release|Any CPU.Build.0 = Release|Any CPU {9F71E169-6D24-B132-9826-8A6DA9FFFBC8}.Release|Any CPU.Deploy.0 = Release|Any CPU - {2C274321-F41E-090D-C929-79400D88D71E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2C274321-F41E-090D-C929-79400D88D71E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2C274321-F41E-090D-C929-79400D88D71E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2C274321-F41E-090D-C929-79400D88D71E}.Release|Any CPU.Build.0 = Release|Any CPU + {B908CF8F-86DA-57A3-E68C-E59E36316522}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B908CF8F-86DA-57A3-E68C-E59E36316522}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B908CF8F-86DA-57A3-E68C-E59E36316522}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B908CF8F-86DA-57A3-E68C-E59E36316522}.Release|Any CPU.Build.0 = Release|Any CPU + {315B4382-048D-46A6-E86A-B0684C70741B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {315B4382-048D-46A6-E86A-B0684C70741B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {315B4382-048D-46A6-E86A-B0684C70741B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {315B4382-048D-46A6-E86A-B0684C70741B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {DD2D491D-7046-41D2-A00E-FE65CBADE85E} = {5B1CDC4F-5ED6-4662-8EC6-3DE3FF0B05BE} + {B908CF8F-86DA-57A3-E68C-E59E36316522} = {5B1CDC4F-5ED6-4662-8EC6-3DE3FF0B05BE} {83974207-9636-48DD-BDB3-98EDECBB1107} = {5B1CDC4F-5ED6-4662-8EC6-3DE3FF0B05BE} - {93E119B1-4378-87DF-2DD2-A818D1E6C2A2} = {5B1CDC4F-5ED6-4662-8EC6-3DE3FF0B05BE} - {2C274321-F41E-090D-C929-79400D88D71E} = {5B1CDC4F-5ED6-4662-8EC6-3DE3FF0B05BE} + {315B4382-048D-46A6-E86A-B0684C70741B} = {5B1CDC4F-5ED6-4662-8EC6-3DE3FF0B05BE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {329E3D0C-A3F7-4A3E-B61C-6B2D1BD7F708} EndGlobalSection GlobalSection(SharedMSBuildProjectFiles) = preSolution - ..\..\DrawnUi.Maui\src\Shared\Shared.projitems*{83974207-9636-48dd-bdb3-98edecbb1107}*SharedItemsImports = 13 - ..\..\DrawnUi.Maui\src\Shared\Shared.projitems*{93e119b1-4378-87df-2dd2-a818d1e6c2a2}*SharedItemsImports = 5 + ..\..\DrawnUi\src\Shared\Shared.projitems*{315b4382-048d-46a6-e86a-b0684c70741b}*SharedItemsImports = 5 + ..\..\DrawnUi\src\Shared\Shared.projitems*{83974207-9636-48dd-bdb3-98edecbb1107}*SharedItemsImports = 13 EndGlobalSection EndGlobal diff --git a/docs/github-actions-cicd.md b/docs/github-actions-cicd.md new file mode 100644 index 0000000..eea1929 --- /dev/null +++ b/docs/github-actions-cicd.md @@ -0,0 +1,120 @@ +# GitHub Actions CI/CD + +This repository includes GitHub Actions workflows for MAUI build/release automation: + +* `.github/workflows/dotnet-windows.yml` + * Triggers: push and pull request on `main`, plus manual dispatch + * Purpose: restore and build Windows target (`net10.0-windows10.0.19041.0`) + +* `.github/workflows/android-release.yml` + * Trigger: manual (`workflow_dispatch`) + * Input: `package_format` (`both`, `aab`, `apk`) + * Purpose: signed Android release publish and artifact upload + * Artifacts: signed outputs only (`*-Signed.aab`, `*-Signed.apk`) + +* `.github/workflows/ios-release.yml` + * Trigger: manual (`workflow_dispatch`) + * Purpose: validate iOS signing, build signed IPA, and upload artifact + * Artifacts: built `.ipa` + +All repository secrets (single list): + +```dos +ANDROID_KEYSTORE= +ANDROID_KEY_ALIAS= +ANDROID_KEY_PASSWORD= +ANDROID_KEYSTORE_PASSWORD= + +IOS_CODESIGN_KEY= +IOS_P12_BASE64= +IOS_P12_PASSWORD= +IOS_MOBILEPROVISION_BASE64= + +APPSTORE_USERNAME= +APPSTORE_APP_PASSWORD= +APPSTORE_PROVIDER_PUBLIC_ID= +``` + +Secret meaning and how to create values: + +* `IOS_CODESIGN_KEY` + * What it is: the exact certificate identity name used for code signing. + * Example value: `Apple Distribution: Your Company Name (TEAMID1234)` + * How to find it on macOS: + * Run: `security find-identity -v -p codesigning` + * Copy the full identity string exactly. + +* `IOS_P12_BASE64` + * What it is: base64 of the exported signing certificate `.p12` file. + * How to create `.p12`: + * Open Keychain Access -> My Certificates. + * Export your Apple Distribution certificate as `.p12`. + * Set an export password (this becomes `IOS_P12_PASSWORD`). + * How to produce base64 (macOS): +```xml +base64 -i ios_dist.p12 | pbcopy +``` + * How to produce base64 (Windows PowerShell): +```xml +$b64 = [Convert]::ToBase64String( + [IO.File]::ReadAllBytes("C:\path\ios_dist.p12") +) +$b64 | Set-Clipboard +Write-Output $b64 +``` + +* `IOS_P12_PASSWORD` + * What it is: the password you entered when exporting the `.p12` certificate. + * Important: this is not your Apple ID password. + +* `IOS_MOBILEPROVISION_BASE64` + * What it is: base64 of the provisioning profile `.mobileprovision` used for this app id. + * How to get profile: + * Apple Developer Portal -> Profiles -> create/download App Store profile for `com.appomobi.drawnui.shaderscam`. + * How to produce base64 (macOS): +```xml +base64 -i ShadersCam.mobileprovision | pbcopy +``` + * How to produce base64 (Windows PowerShell): +```xml +$b64 = [Convert]::ToBase64String( + [IO.File]::ReadAllBytes("C:\path\ShadersCam.mobileprovision") +) +$b64 | Set-Clipboard +Write-Output $b64 +``` + +App Store upload secrets: + +* `APPSTORE_USERNAME` + * What it is: Apple ID email used for App Store Connect uploads. + * Example: `your.name@company.com` + +* `APPSTORE_APP_PASSWORD` + * What it is: Apple app-specific password (not your Apple ID login password). + * Where to create it: + * Go to https://account.apple.com + * Sign in with the Apple ID from `APPSTORE_USERNAME` + * Open Sign-In and Security -> App-Specific Passwords + * Create a new app-specific password and copy it immediately + * Store that generated value in this secret. + +* `APPSTORE_PROVIDER_PUBLIC_ID` (optional) + * What it is: provider public id used only if the Apple ID belongs to multiple providers/teams. + * Leave empty unless upload fails with provider ambiguity. + +Recommended combined flow in one workflow file: + +* Validate signing assets +* Build signed IPA +* Upload IPA to App Store Connect + +Minimal upload example in workflow step: + +* `xcrun altool --upload-app --type ios --file --username "$APPSTORE_USERNAME" --password "$APPSTORE_APP_PASSWORD"` + +Notes: + +* SDK is pinned via `global.json`. +* Android version/build numbers are derived from manifest/project values plus GitHub run number. +* iOS profile name is parsed from the provisioning profile during the workflow. \ No newline at end of file diff --git a/global.json b/global.json new file mode 100644 index 0000000..9e2ef80 --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "10.0.104" + } +} \ No newline at end of file diff --git a/src/app/Platforms/Android/AndroidManifest.xml b/src/app/Platforms/Android/AndroidManifest.xml index 9ac1420..3c8ae5e 100644 --- a/src/app/Platforms/Android/AndroidManifest.xml +++ b/src/app/Platforms/Android/AndroidManifest.xml @@ -1,5 +1,5 @@  - + @@ -11,5 +11,7 @@ + + \ No newline at end of file diff --git a/src/app/Resources/Raw/Shaders/Camera/cartoon.sksl b/src/app/Resources/Raw/Shaders/Camera/cartoon.sksl new file mode 100644 index 0000000..5299dee --- /dev/null +++ b/src/app/Resources/Raw/Shaders/Camera/cartoon.sksl @@ -0,0 +1,270 @@ +uniform float4 iMouse; +uniform float iTime; +uniform float2 iResolution; +uniform float2 iImageResolution; +uniform shader iImage1; +uniform float2 iOffset; +uniform float2 iOrigin; + +// Debug switches +const int DEBUG_MODE = 2; + +// Sketch mixing parameters +const float SKETCH_THRESHOLD = 0.94; +const int BLEND_MODE = 3; + +// Sketch parameters +const float iLineWidth = 4.5; +const float eraseNoise = 1.045; +const float contrast = 9.0; + +// === KAWAII COLOR PARAMETERS === +const float COLOR_LEVELS = 12.0; +const float iColorAlpha = 0.82; +const float iHueShift = 0.02; +const float iSaturation = 2.25; +const float iLightness = 1.28; +const float iShadows = -0.08; +const float iHighlights = 0.42; + +const float SMOOTH_RADIUS = 5.0; + +// Color dodge +half3 colorDodge(in half3 src, in half3 dst) { + return step(0.0, dst) * mix(min(half3(1.0), dst / (1.0 - src)), half3(1.0), step(1.0, src)); +} + +// Grayscale +float greyScale(in half3 col) { + return dot(col, half3(0.3, 0.59, 0.11)); +} + +// Gaussian blur +half3 gaussianBlur(float2 inputCoord, float scaleFactor) { + half3 result = half3(0.0); + float totalWeight = 0.0; + + float radius = SMOOTH_RADIUS * scaleFactor; + + for (int i = -2; i <= 2; i++) { + for (int j = -2; j <= 2; j++) { + float2 sampleCoord = inputCoord + float2(float(i), float(j)) * radius; + half3 sampleColor = iImage1.eval(sampleCoord).rgb; + + float distance = float(i*i + j*j); + float weight = exp(-distance * 0.15); + + result += sampleColor * weight; + totalWeight += weight; + } + } + + return result / totalWeight; +} + +// Posterize +half3 smoothPosterize(float2 inputCoord, float scaleFactor) { + half3 blurred = gaussianBlur(inputCoord, scaleFactor); + return floor(blurred * COLOR_LEVELS + 0.5) / COLOR_LEVELS; +} + +// RGB to HSL +half3 rgbToHsl(half3 rgb) { + float maxVal = max(max(rgb.r, rgb.g), rgb.b); + float minVal = min(min(rgb.r, rgb.g), rgb.b); + float delta = maxVal - minVal; + + float l = (maxVal + minVal) * 0.5; + + if (delta == 0.0) { + return half3(0.0, 0.0, l); + } + + float s = l > 0.5 ? delta / (2.0 - maxVal - minVal) : delta / (maxVal + minVal); + + float h; + if (maxVal == rgb.r) { + h = (rgb.g - rgb.b) / delta + (rgb.g < rgb.b ? 6.0 : 0.0); + } else if (maxVal == rgb.g) { + h = (rgb.b - rgb.r) / delta + 2.0; + } else { + h = (rgb.r - rgb.g) / delta + 4.0; + } + h /= 6.0; + + return half3(h, s, l); +} + +// HSL to RGB +half3 hslToRgb(half3 hsl) { + float h = hsl.x; + float s = hsl.y; + float l = hsl.z; + + if (s == 0.0) { + return half3(l, l, l); + } + + float q = l < 0.5 ? l * (1.0 + s) : l + s - l * s; + float p = 2.0 * l - q; + + float tr = h + 1.0/3.0; + float tg = h; + float tb = h - 1.0/3.0; + + if (tr < 0.0) tr += 1.0; + if (tr > 1.0) tr -= 1.0; + if (tb < 0.0) tb += 1.0; + if (tb > 1.0) tb -= 1.0; + + float r, g, b; + + if (tr < 1.0/6.0) r = p + (q - p) * 6.0 * tr; + else if (tr < 1.0/2.0) r = q; + else if (tr < 2.0/3.0) r = p + (q - p) * (2.0/3.0 - tr) * 6.0; + else r = p; + + if (tg < 1.0/6.0) g = p + (q - p) * 6.0 * tg; + else if (tg < 1.0/2.0) g = q; + else if (tg < 2.0/3.0) g = p + (q - p) * (2.0/3.0 - tg) * 6.0; + else g = p; + + if (tb < 1.0/6.0) b = p + (q - p) * 6.0 * tb; + else if (tb < 1.0/2.0) b = q; + else if (tb < 2.0/3.0) b = p + (q - p) * (2.0/3.0 - tb) * 6.0; + else b = p; + + return half3(r, g, b); +} + +// HSL adjustment +half3 adjustHSL(half3 rgb, float hueShift, float satMult, float lightMult) { + half3 hsl = rgbToHsl(rgb); + hsl.x = fract(hsl.x + hueShift); + hsl.y = clamp(hsl.y * satMult, 0.0, 1.0); + hsl.z = clamp(hsl.z * lightMult, 0.0, 1.0); + return hslToRgb(hsl); +} + +// Shadows & Highlights (softened for kawaii) +half3 adjustShadowsHighlights(half3 rgb, float shadowsAdj, float highlightsAdj) { + float luminance = dot(rgb, half3(0.299, 0.587, 0.114)); + float shadowMask = 1.0 - smoothstep(0.0, 0.55, luminance); + float highlightMask = smoothstep(0.45, 1.0, luminance); + half3 adjustedRgb = rgb + half3(shadowsAdj * shadowMask + highlightsAdj * highlightMask); + return clamp(adjustedRgb, 0.0, 1.0); +} + +// Blend functions +half3 blendMultiply(half3 base, half3 blend) { + return base * blend; +} + +half3 blendDarken(half3 base, half3 blend) { + return min(base, blend); +} + +half3 blendColorBurn(half3 base, half3 blend) { + return half3(1.0) - (half3(1.0) - base) / blend; +} + +half3 blendOverlay(half3 base, half3 blend) { + return mix( + 2.0 * base * blend, + half3(1.0) - 2.0 * (half3(1.0) - base) * (half3(1.0) - blend), + step(0.5, base) + ); +} + +// Line blending +half3 applyLineBlending(half3 colors, float sketchIntensity) { + if (sketchIntensity >= SKETCH_THRESHOLD) { + return colors; + } + + float lineAlpha = 1.0 - (sketchIntensity / SKETCH_THRESHOLD); + lineAlpha = clamp(lineAlpha, 0.0, 1.0); + + if (BLEND_MODE == 3) { + half3 lineCol = max(half3(sketchIntensity / SKETCH_THRESHOLD), half3(0.001)); + return blendColorBurn(colors, lineCol); + } else if (BLEND_MODE == 0) { + return mix(colors, half3(0.0), lineAlpha); + } else if (BLEND_MODE == 1) { + return blendMultiply(colors, half3(sketchIntensity / SKETCH_THRESHOLD)); + } else if (BLEND_MODE == 2) { + return blendDarken(colors, half3(sketchIntensity / SKETCH_THRESHOLD)); + } else if (BLEND_MODE == 4) { + return blendOverlay(colors, half3(sketchIntensity / SKETCH_THRESHOLD)); + } + + return colors; +} + +// Kawaii color processing +half3 processCartoonColors(float2 inputCoord, float scaleFactor) { + half3 col = iImage1.eval(inputCoord).rgb; + + half3 quantizedColor = smoothPosterize(inputCoord, scaleFactor); + + half3 adjustedCartoonColor = adjustHSL(quantizedColor, iHueShift, iSaturation, iLightness); + adjustedCartoonColor = adjustShadowsHighlights(adjustedCartoonColor, iShadows, iHighlights); + + half3 adjustedOriginalColor = adjustHSL(col, iHueShift, iSaturation, iLightness); + adjustedOriginalColor = adjustShadowsHighlights(adjustedOriginalColor, iShadows, iHighlights); + + return mix(adjustedOriginalColor, adjustedCartoonColor, iColorAlpha); +} + +// Original sketch lines (unchanged) +float processSketchLines(float2 inputCoord, float scaleFactor) { + half3 col = iImage1.eval(inputCoord).rgb; + + half3 blurred = half3(0.0); + float totalWeight = 0.0; + + for (int i = -2; i <= 2; i++) { + for (int j = -2; j <= 2; j++) { + float weight = exp(-float(i*i + j*j) * 0.2); + float2 sampleCoord = inputCoord + float2(float(i), float(j)) * 1.5 * scaleFactor * iLineWidth; + blurred += iImage1.eval(sampleCoord).rgb * weight; + totalWeight += weight; + } + } + blurred = blurred / totalWeight; + + float2 texelSize = (1.0 / iImageResolution.xy) * scaleFactor * iLineWidth; + float gradX = greyScale(iImage1.eval(inputCoord + float2(texelSize.x, 0.0)).rgb) - + greyScale(iImage1.eval(inputCoord - float2(texelSize.x, 0.0)).rgb); + float gradY = greyScale(iImage1.eval(inputCoord + float2(0.0, texelSize.y)).rgb) - + greyScale(iImage1.eval(inputCoord - float2(0.0, texelSize.y)).rgb); + + half3 inv = half3(1.0) - blurred; + half3 lighten = colorDodge(col, inv) * eraseNoise; + half3 res = half3(greyScale(lighten)); + res = half3(pow(res.x, contrast)); + + return res.x; +} + +half4 main(float2 fragCoord) { + float REFERENCE_SIZE = 1000.0; + + float2 renderingScale = iImageResolution.xy / iResolution.xy; + float2 inputCoord = (fragCoord - iOffset) * renderingScale; + + float imageSize = min(iImageResolution.x, iImageResolution.y); + float scaleFactor = imageSize / REFERENCE_SIZE; + + half3 cartoonColors = processCartoonColors(inputCoord, scaleFactor); + float sketchIntensity = processSketchLines(inputCoord, scaleFactor); + + if (DEBUG_MODE == 0) { + return half4(half3(sketchIntensity), 1.0); + } else if (DEBUG_MODE == 1) { + return half4(cartoonColors, 1.0); + } else { + half3 finalColor = applyLineBlending(cartoonColors, sketchIntensity); + return half4(clamp(finalColor, 0.0, 1.0), 1.0); + } +} \ No newline at end of file diff --git a/src/app/Resources/Raw/Shaders/Camera/geisha.sksl b/src/app/Resources/Raw/Shaders/Camera/geisha.sksl new file mode 100644 index 0000000..327d7cf --- /dev/null +++ b/src/app/Resources/Raw/Shaders/Camera/geisha.sksl @@ -0,0 +1,206 @@ +uniform float4 iMouse; +uniform float iTime; +uniform float2 iResolution; +uniform float2 iImageResolution; +uniform shader iImage1; +uniform float2 iOffset; + +const int DEBUG_MODE = 2; + +const float SKETCH_THRESHOLD = 0.93; +const int BLEND_MODE = 3; + +const float iLineWidth = 5.0; +const float eraseNoise = 1.05; +const float contrast = 8.0; + +// Fast Kawaii Parameters +const float COLOR_LEVELS = 22.0; +const float iColorAlpha = 0.68; +const float iHueShift = 0.01; +const float iSaturation = 1.90; +const float iLightness = 1.24; +const float iShadows = -0.05; +const float iHighlights = 0.36; + +const float SMOOTH_RADIUS = 1.8; + +// ======================== +// Fast helpers +float greyScale(half3 col) { + return dot(col, half3(0.299, 0.587, 0.114)); +} + +half3 colorDodge(half3 src, half3 dst) { + return step(0.0, dst) * mix(min(half3(1.0), dst / (1.0 - src)), half3(1.0), step(1.0, src)); +} + +// Cheap 3x3 smoothing (big performance win) +half3 fastSmooth(float2 coord, float2 texel) { + half3 sum = iImage1.eval(coord).rgb * 4.0; + sum += iImage1.eval(coord + float2(-texel.x, 0.0)).rgb; + sum += iImage1.eval(coord + float2( texel.x, 0.0)).rgb; + sum += iImage1.eval(coord + float2(0.0, -texel.y)).rgb; + sum += iImage1.eval(coord + float2(0.0, texel.y)).rgb; + return sum / 8.0; +} + +// Fast posterize +half3 fastPosterize(float2 inputCoord, float2 texel) { + half3 smoothed = fastSmooth(inputCoord, texel * SMOOTH_RADIUS); + return floor(smoothed * COLOR_LEVELS + 0.5) / COLOR_LEVELS; +} + +// RGB to HSL +half3 rgbToHsl(half3 rgb) { + float maxVal = max(max(rgb.r, rgb.g), rgb.b); + float minVal = min(min(rgb.r, rgb.g), rgb.b); + float delta = maxVal - minVal; + + float l = (maxVal + minVal) * 0.5; + + if (delta == 0.0) { + return half3(0.0, 0.0, l); + } + + float s = l > 0.5 ? delta / (2.0 - maxVal - minVal) : delta / (maxVal + minVal); + + float h; + if (maxVal == rgb.r) { + h = (rgb.g - rgb.b) / delta + (rgb.g < rgb.b ? 6.0 : 0.0); + } else if (maxVal == rgb.g) { + h = (rgb.b - rgb.r) / delta + 2.0; + } else { + h = (rgb.r - rgb.g) / delta + 4.0; + } + h /= 6.0; + + return half3(h, s, l); +} + +// HSL to RGB +half3 hslToRgb(half3 hsl) { + float h = hsl.x; + float s = hsl.y; + float l = hsl.z; + + if (s == 0.0) { + return half3(l, l, l); + } + + float q = l < 0.5 ? l * (1.0 + s) : l + s - l * s; + float p = 2.0 * l - q; + + float tr = h + 1.0/3.0; + float tg = h; + float tb = h - 1.0/3.0; + + if (tr < 0.0) tr += 1.0; if (tr > 1.0) tr -= 1.0; + if (tb < 0.0) tb += 1.0; if (tb > 1.0) tb -= 1.0; + + float r, g, b; + + if (tr < 1.0/6.0) r = p + (q - p) * 6.0 * tr; + else if (tr < 1.0/2.0) r = q; + else if (tr < 2.0/3.0) r = p + (q - p) * (2.0/3.0 - tr) * 6.0; + else r = p; + + if (tg < 1.0/6.0) g = p + (q - p) * 6.0 * tg; + else if (tg < 1.0/2.0) g = q; + else if (tg < 2.0/3.0) g = p + (q - p) * (2.0/3.0 - tg) * 6.0; + else g = p; + + if (tb < 1.0/6.0) b = p + (q - p) * 6.0 * tb; + else if (tb < 1.0/2.0) b = q; + else if (tb < 2.0/3.0) b = p + (q - p) * (2.0/3.0 - tb) * 6.0; + else b = p; + + return half3(r, g, b); +} + +// HSL Adjustment +half3 adjustHSL(half3 rgb, float hueShift, float satMult, float lightMult) { + half3 hsl = rgbToHsl(rgb); + hsl.x = fract(hsl.x + hueShift); + hsl.y = clamp(hsl.y * satMult, 0.0, 1.0); + hsl.z = clamp(hsl.z * lightMult, 0.0, 1.0); + return hslToRgb(hsl); +} + +// Shadows & Highlights +half3 adjustShadowsHighlights(half3 rgb, float shadowsAdj, float highlightsAdj) { + float lum = greyScale(rgb); + float shadowMask = 1.0 - smoothstep(0.0, 0.65, lum); + float highlightMask = smoothstep(0.35, 1.0, lum); + return clamp(rgb + half3(shadowsAdj * shadowMask + highlightsAdj * highlightMask), 0.0, 1.0); +} + +// Blend +half3 blendColorBurn(half3 base, half3 blend) { + return half3(1.0) - (half3(1.0) - base) / blend; +} + +// Fast sketch lines +float fastSketchLines(float2 inputCoord, float scaleFactor) { + half3 col = iImage1.eval(inputCoord).rgb; + + // Cheap blur + half3 blurred = col * 0.6; + float2 offset = 1.4 * scaleFactor * iLineWidth / iImageResolution.xy; + blurred += iImage1.eval(inputCoord + float2(-offset.x, 0.0)).rgb * 0.1; + blurred += iImage1.eval(inputCoord + float2( offset.x, 0.0)).rgb * 0.1; + blurred += iImage1.eval(inputCoord + float2(0.0, -offset.y)).rgb * 0.1; + blurred += iImage1.eval(inputCoord + float2(0.0, offset.y)).rgb * 0.1; + + // Simple fast edge + float2 texel = 1.8 * scaleFactor * iLineWidth / iImageResolution.xy; + float edge = abs(greyScale(iImage1.eval(inputCoord + float2(texel.x, 0.0)).rgb) - + greyScale(iImage1.eval(inputCoord - float2(texel.x, 0.0)).rgb)) + + abs(greyScale(iImage1.eval(inputCoord + float2(0.0, texel.y)).rgb) - + greyScale(iImage1.eval(inputCoord - float2(0.0, texel.y)).rgb)); + + half3 inv = half3(1.0) - blurred; + float sketch = pow(greyScale(colorDodge(col, inv) * eraseNoise), contrast); + + return clamp(sketch * (1.0 + edge * 1.2), 0.0, 1.0); +} + +// Fast color processing +half3 processKawaiiColors(float2 inputCoord, float2 texel) { + half3 col = iImage1.eval(inputCoord).rgb; + + half3 quantized = fastPosterize(inputCoord, texel); + + half3 cartoon = adjustHSL(quantized, iHueShift, iSaturation, iLightness); + cartoon = adjustShadowsHighlights(cartoon, iShadows, iHighlights); + + half3 originalAdj = adjustHSL(col, iHueShift, iSaturation, iLightness); + originalAdj = adjustShadowsHighlights(originalAdj, iShadows, iHighlights); + + return mix(originalAdj, cartoon, iColorAlpha); +} + +// Line blending +half3 applyLineBlending(half3 colors, float sketchIntensity) { + if (sketchIntensity >= SKETCH_THRESHOLD) return colors; + + float alpha = clamp(1.0 - (sketchIntensity / SKETCH_THRESHOLD), 0.0, 1.0) * 1.6; + return blendColorBurn(colors, max(half3(sketchIntensity / SKETCH_THRESHOLD), half3(0.001))); +} + +half4 main(float2 fragCoord) { + float2 renderingScale = iImageResolution.xy / iResolution.xy; + float2 inputCoord = (fragCoord - iOffset) * renderingScale; + + float2 texel = 1.0 / iImageResolution.xy; + float scaleFactor = min(iImageResolution.x, iImageResolution.y) / 1000.0; + + half3 colors = processKawaiiColors(inputCoord, texel); + float lines = fastSketchLines(inputCoord, scaleFactor); + + if (DEBUG_MODE == 0) return half4(half3(lines), 1.0); + if (DEBUG_MODE == 1) return half4(colors, 1.0); + + half3 final = applyLineBlending(colors, lines); + return half4(clamp(final, 0.0, 1.0), 1.0); +} \ No newline at end of file diff --git a/src/app/Resources/Raw/Shaders/Camera/print.sksl b/src/app/Resources/Raw/Shaders/Camera/print.sksl new file mode 100644 index 0000000..9d7cfa5 --- /dev/null +++ b/src/app/Resources/Raw/Shaders/Camera/print.sksl @@ -0,0 +1,210 @@ +uniform float4 iMouse; +uniform float iTime; +uniform float2 iResolution; +uniform float2 iImageResolution; +uniform shader iImage1; +uniform float2 iOffset; + +const int DEBUG_MODE = 2; + +const float SKETCH_THRESHOLD = 0.94; +const int BLEND_MODE = 3; + +const float iLineWidth = 5.2; +const float eraseNoise = 1.05; +const float contrast = 8.5; + +// KAWAII COLOR PARAMETERS +const float COLOR_LEVELS = 20.0; +const float iColorAlpha = 0.78; +const float iHueShift = 0.015; +const float iSaturation = 1.95; +const float iLightness = 1.22; +const float iShadows = -0.06; +const float iHighlights = 0.35; + +const float SMOOTH_RADIUS = 2.6; // Increased a bit for softer look + +// Helpers +half3 colorDodge(in half3 src, in half3 dst) { + return step(0.0, dst) * mix(min(half3(1.0), dst / (1.0 - src)), half3(1.0), step(1.0, src)); +} + +float greyScale(in half3 col) { + return dot(col, half3(0.3, 0.59, 0.11)); +} + +// Softer 3x3 blur for colors (better quality than previous fast version) +half3 softColorBlur(float2 inputCoord, float scaleFactor) { + half3 result = half3(0.0); + float totalWeight = 0.0; + float radius = SMOOTH_RADIUS * scaleFactor; + + for (int i = -1; i <= 1; i++) { + for (int j = -1; j <= 1; j++) { + float weight = exp(-float(i*i + j*j) * 0.5); // Gaussian-like weight + float2 sampleCoord = inputCoord + float2(float(i), float(j)) * radius; + result += iImage1.eval(sampleCoord).rgb * weight; + totalWeight += weight; + } + } + return result / totalWeight; +} + +// Posterize with softer blur +half3 smoothPosterize(float2 inputCoord, float scaleFactor) { + half3 blurred = softColorBlur(inputCoord, scaleFactor); + return floor(blurred * COLOR_LEVELS + 0.5) / COLOR_LEVELS; +} + +// Your original HSL functions (unchanged) +half3 rgbToHsl(half3 rgb) { + float maxVal = max(max(rgb.r, rgb.g), rgb.b); + float minVal = min(min(rgb.r, rgb.g), rgb.b); + float delta = maxVal - minVal; + float l = (maxVal + minVal) * 0.5; + + if (delta == 0.0) return half3(0.0, 0.0, l); + + float s = l > 0.5 ? delta / (2.0 - maxVal - minVal) : delta / (maxVal + minVal); + + float h; + if (maxVal == rgb.r) h = (rgb.g - rgb.b) / delta + (rgb.g < rgb.b ? 6.0 : 0.0); + else if (maxVal == rgb.g) h = (rgb.b - rgb.r) / delta + 2.0; + else h = (rgb.r - rgb.g) / delta + 4.0; + h /= 6.0; + + return half3(h, s, l); +} + +half3 hslToRgb(half3 hsl) { + float h = hsl.x; float s = hsl.y; float l = hsl.z; + if (s == 0.0) return half3(l, l, l); + + float q = l < 0.5 ? l * (1.0 + s) : l + s - l * s; + float p = 2.0 * l - q; + + float tr = h + 1.0/3.0; + float tg = h; + float tb = h - 1.0/3.0; + + if (tr < 0.0) tr += 1.0; if (tr > 1.0) tr -= 1.0; + if (tb < 0.0) tb += 1.0; if (tb > 1.0) tb -= 1.0; + + float r, g, b; + if (tr < 1.0/6.0) r = p + (q - p) * 6.0 * tr; + else if (tr < 1.0/2.0) r = q; + else if (tr < 2.0/3.0) r = p + (q - p) * (2.0/3.0 - tr) * 6.0; + else r = p; + + if (tg < 1.0/6.0) g = p + (q - p) * 6.0 * tg; + else if (tg < 1.0/2.0) g = q; + else if (tg < 2.0/3.0) g = p + (q - p) * (2.0/3.0 - tg) * 6.0; + else g = p; + + if (tb < 1.0/6.0) b = p + (q - p) * 6.0 * tb; + else if (tb < 1.0/2.0) b = q; + else if (tb < 2.0/3.0) b = p + (q - p) * (2.0/3.0 - tb) * 6.0; + else b = p; + + return half3(r, g, b); +} + +half3 adjustHSL(half3 rgb, float hueShift, float satMult, float lightMult) { + half3 hsl = rgbToHsl(rgb); + hsl.x = fract(hsl.x + hueShift); + hsl.y = clamp(hsl.y * satMult, 0.0, 1.0); + hsl.z = clamp(hsl.z * lightMult, 0.0, 1.0); + return hslToRgb(hsl); +} + +half3 adjustShadowsHighlights(half3 rgb, float shadowsAdj, float highlightsAdj) { + float luminance = dot(rgb, half3(0.299, 0.587, 0.114)); + float shadowMask = 1.0 - smoothstep(0.0, 0.6, luminance); + float highlightMask = smoothstep(0.4, 1.0, luminance); + return clamp(rgb + half3(shadowsAdj * shadowMask + highlightsAdj * highlightMask), 0.0, 1.0); +} + +half3 blendColorBurn(half3 base, half3 blend) { + return half3(1.0) - (half3(1.0) - base) / blend; +} + +// Color processing +half3 processCartoonColors(float2 inputCoord, float scaleFactor) { + half3 col = iImage1.eval(inputCoord).rgb; + half3 quantizedColor = smoothPosterize(inputCoord, scaleFactor); + + half3 adjustedCartoonColor = adjustHSL(quantizedColor, iHueShift, iSaturation, iLightness); + adjustedCartoonColor = adjustShadowsHighlights(adjustedCartoonColor, iShadows, iHighlights); + + half3 adjustedOriginalColor = adjustHSL(col, iHueShift, iSaturation, iLightness); + adjustedOriginalColor = adjustShadowsHighlights(adjustedOriginalColor, iShadows, iHighlights); + + return mix(adjustedOriginalColor, adjustedCartoonColor, iColorAlpha); +} + +// Keep your original sketch lines (best for line quality) +float processSketchLines(float2 inputCoord, float scaleFactor) { + half3 col = iImage1.eval(inputCoord).rgb; + + half3 blurred = half3(0.0); + float totalWeight = 0.0; + + for (int i = -2; i <= 2; i++) { + for (int j = -2; j <= 2; j++) { + float weight = exp(-float(i*i + j*j) * 0.2); + float2 sampleCoord = inputCoord + float2(float(i), float(j)) * 1.5 * scaleFactor * iLineWidth; + blurred += iImage1.eval(sampleCoord).rgb * weight; + totalWeight += weight; + } + } + blurred = blurred / totalWeight; + + float2 texelSize = (1.0 / iImageResolution.xy) * scaleFactor * iLineWidth; + float gradX = greyScale(iImage1.eval(inputCoord + float2(texelSize.x, 0.0)).rgb) - + greyScale(iImage1.eval(inputCoord - float2(texelSize.x, 0.0)).rgb); + float gradY = greyScale(iImage1.eval(inputCoord + float2(0.0, texelSize.y)).rgb) - + greyScale(iImage1.eval(inputCoord - float2(0.0, texelSize.y)).rgb); + + half3 inv = half3(1.0) - blurred; + half3 lighten = colorDodge(col, inv) * eraseNoise; + half3 res = half3(greyScale(lighten)); + res = half3(pow(res.x, contrast)); + + return res.x; +} + +// Line blending +half3 applyLineBlending(half3 colors, float sketchIntensity) { + if (sketchIntensity >= SKETCH_THRESHOLD) { + return colors; + } + + float lineAlpha = 1.0 - (sketchIntensity / SKETCH_THRESHOLD); + lineAlpha = clamp(lineAlpha, 0.0, 1.0); + + if (BLEND_MODE == 3) { + half3 lineCol = max(half3(sketchIntensity / SKETCH_THRESHOLD), half3(0.001)); + return blendColorBurn(colors, lineCol); + } + return mix(colors, half3(0.0), lineAlpha); +} + +half4 main(float2 fragCoord) { + float REFERENCE_SIZE = 1000.0; + + float2 renderingScale = iImageResolution.xy / iResolution.xy; + float2 inputCoord = (fragCoord - iOffset) * renderingScale; + + float imageSize = min(iImageResolution.x, iImageResolution.y); + float scaleFactor = imageSize / REFERENCE_SIZE; + + half3 cartoonColors = processCartoonColors(inputCoord, scaleFactor); + float sketchIntensity = processSketchLines(inputCoord, scaleFactor); + + if (DEBUG_MODE == 0) return half4(half3(sketchIntensity), 1.0); + if (DEBUG_MODE == 1) return half4(cartoonColors, 1.0); + + half3 finalColor = applyLineBlending(cartoonColors, sketchIntensity); + return half4(clamp(finalColor, 0.0, 1.0), 1.0); +} \ No newline at end of file diff --git a/src/app/ShadersCamera.csproj b/src/app/ShadersCamera.csproj index e0d1d8e..3ec84c0 100644 --- a/src/app/ShadersCamera.csproj +++ b/src/app/ShadersCamera.csproj @@ -2,11 +2,11 @@ - - + + - net9.0-android;net9.0-ios;net9.0-maccatalyst - $(TargetFrameworks);net9.0-windows10.0.19041.0 + net10.0-android;net10.0-ios;net10.0-maccatalyst + $(TargetFrameworks);net10.0-windows10.0.19041.0 Exe ShadersCamera @@ -20,8 +20,22 @@ com.appomobi.drawnui.shaderscam 340e07b0-ebc2-4fde-9ac9-074d5c3269b4 + false + + + + + + + + + + + + + None 15.0 @@ -32,9 +46,6 @@ $(MSBuildProjectName) - - - True @@ -55,20 +66,12 @@ iPhone Developer - - - - manual - Apple Distribution: NIKOLAY KOVALSKIY (F5H2D34D9G) - ShadersCam AppStore - - @@ -91,20 +94,11 @@ - - - - - - - - - - - + + + + @@ -155,7 +149,7 @@ - + True diff --git a/src/app/ViewModels/CameraViewModel.cs b/src/app/ViewModels/CameraViewModel.cs index 0a69b9a..bdf4994 100644 --- a/src/app/ViewModels/CameraViewModel.cs +++ b/src/app/ViewModels/CameraViewModel.cs @@ -62,10 +62,13 @@ public CameraViewModel() new ShaderItem { Title = "Sketch", Filename = "Shaders/Camera/sketch.sksl" }, new ShaderItem { Title = "Paint", Filename = "Shaders/Camera/sketchcolors.sksl" }, + new ShaderItem { Title = "Print", Filename = "Shaders/Camera/print.sksl" }, + new ShaderItem { Title = "Geisha", Filename = "Shaders/Camera/geisha.sksl" }, -#if !ANDROID + #region Low FPS on Android new ShaderItem { Title = "Poster", Filename = "Shaders/Camera/sketchcomics4.sksl" }, -#endif + new ShaderItem { Title = "Cartoon", Filename = "Shaders/Camera/cartoon.sksl" }, + #endregion new ShaderItem { Title = "Mars", Filename = "Shaders/Camera/hell.sksl" }, new ShaderItem { Title = "Invert", Filename = "Shaders/Camera/invert.sksl" }, @@ -316,7 +319,7 @@ public void AttachCamera(SkiaCamera camera) Camera.CaptureSuccess += OnCaptureSuccess; Camera.StateChanged += OnCameraStateChanged; Camera.NewPreviewSet += OnNewPreviewSet; - Camera.IsRecordingVideoChanged += OnIsRecordingVideoChanged; + Camera.IsRecordingChanged += OnIsRecordingVideoChanged; Camera.RecordingProgress += OnVideoRecordingProgress; } } @@ -328,7 +331,7 @@ public override void OnDisposing() Camera.CaptureSuccess -= OnCaptureSuccess; Camera.StateChanged -= OnCameraStateChanged; Camera.NewPreviewSet -= OnNewPreviewSet; - Camera.IsRecordingVideoChanged -= OnIsRecordingVideoChanged; + Camera.IsRecordingChanged -= OnIsRecordingVideoChanged; Camera.RecordingProgress -= OnVideoRecordingProgress; Camera = null; } @@ -378,26 +381,27 @@ private void OnCameraStateChanged(object sender, HardwareState state) private async void OnCaptureSuccess(object sender, CapturedImage captured) { - captured.SolveExifOrientation(); - - var imageWithOverlay = await Camera.RenderCapturedPhotoAsync(captured, null, image => + var imageWithEffect = await Camera.RenderCapturedPhotoAsync(captured, null, image => { if (SelectedShader != null) { var shaderEffect = new SkiaShaderEffect() { ShaderSource = SelectedShader.Filename, - TileMode = SKShaderTileMode.Mirror }; image.VisualEffects.Add(shaderEffect); } }, true); captured.Image.Dispose(); - captured.Image = imageWithOverlay; + captured.Image = imageWithEffect; captured.Meta.Vendor = MauiProgram.ExifCameraVendor; captured.Meta.Model = MauiProgram.ExifCameraModel; + if (SelectedShader != null) + { + captured.Meta.Software = SelectedShader.Title; + } await Camera.SaveToGalleryAsync(captured); diff --git a/src/app/Views/Controls/CameraWithEffects.cs b/src/app/Views/Controls/AppCamera.cs similarity index 61% rename from src/app/Views/Controls/CameraWithEffects.cs rename to src/app/Views/Controls/AppCamera.cs index 2e0a3a8..6e85a46 100644 --- a/src/app/Views/Controls/CameraWithEffects.cs +++ b/src/app/Views/Controls/AppCamera.cs @@ -1,17 +1,39 @@ +using AppoMobi.Specials; using DrawnUi.Camera; using DrawnUi.Infrastructure; using ShadersCamera.Models; using System.Windows.Input; -using AppoMobi.Specials; +using static Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.VisualElement; namespace ShadersCamera.Views.Controls { - public class CameraWithEffects : SkiaCamera + public class AppCamera : SkiaCamera { - public CameraWithEffects() + public AppCamera() { - //NeedPermissionsSet = NeedPermissions.Camera | NeedPermissions.Gallery; + NeedPermissionsSet = NeedPermissions.Camera | NeedPermissions.Gallery; //we don't add location because it will be requested by OS when we + + UseRealtimeVideoProcessing = true; + + //GPS metadata + this.InjectGpsLocation = true; + +#if DEBUG + VideoDiagnosticsOn = true; +#endif + } + + protected override void RenderPreviewForProcessing(SKCanvas canvas, SKImage frame) + { + var shader = GetEffectShader(); + if (shader == null) + { + base.RenderPreviewForProcessing(canvas, frame); + return; + } + + shader.DrawImage(canvas, frame, 0, 0); } protected override void OnDisplayReady() @@ -23,13 +45,7 @@ protected override void OnDisplayReady() //do not block startup by this InitializeEffects(); }); - } - - protected override void Paint(DrawingContext ctx) - { - base.Paint(ctx); - FrameAquired = false; } /// @@ -60,20 +76,19 @@ static void InitializeAvailableShaders() if (_shaders == null) { _shaders = Files.ListAssets(path); - } } - private SkiaShaderEffect _shader; + //private SkiaShaderEffect _shader; private SkiaShaderEffect _shaderGlobal; public void ChangeShaderCode(string code) { - if (Display == null || _shader==null) + if (_effectShader == null) { return; } - _shader.ShaderCode = code; + _effectShader.CompileFromCode(code, null, false, RaiseShaderError); } public void SetEffect(SkiaImageEffect effect) @@ -87,59 +102,80 @@ public void SetEffect(SkiaImageEffect effect) SetCustomShader(ShaderSource); } - - protected virtual void SetCustomShader(ShaderItem shader) + private SkiaShader GetEffectShader() { - if (Display == null) + var effect = VideoEffect; + if (effect == null) { - return; + ReleaseEffectShader(); + return null; } - //just having fun, add ripples to preview -/* - if (_shaderGlobal == null) + if (_effectShader != null && _loadedEffect == effect) { - _shaderGlobal = new MultiRippleWithTouchEffect() - { - SecondarySource="Images/logo.png" - }; - VisualEffects.Add(_shaderGlobal); + return _effectShader; } -*/ - // Remove existing shader if any - if (_shader != null && VisualEffects.Contains(_shader)) + ReleaseEffectShader(); + + var filename = effect.Filename;// ShaderEffectHelper.GetFilename(effect); + if (string.IsNullOrWhiteSpace(filename)) { - _shader.OnCompilationError -= OnShaderError; - VisualEffects.Remove(_shader); + return null; } - if (Effect == SkiaImageEffect.Custom && shader != null) - { + _effectShader = SkiaShader.FromResource(filename, true, RaiseShaderError); + _loadedEffect = effect; - // Create new shader with the specified filename - _shader = new ClippedShaderEffect(Display) - { - ShaderSource = shader.Filename, - //FilterMode = SKFilterMode.Linear <== it's default - }; + return _effectShader; + } - // Add the new shader - if (_shader != null && !VisualEffects.Contains(_shader)) - { - _shader.OnCompilationError += OnShaderError; - VisualEffects.Add(_shader); - } - } + private void ReleaseEffectShader() + { + _effectShader?.Dispose(); + _effectShader = null; + _loadedEffect = null; + } + + private SkiaShader _effectShader; + private ShaderItem _loadedEffect; + + public static readonly BindableProperty VideoEffectProperty = BindableProperty.Create( + nameof(VideoEffect), + typeof(ShaderItem), + typeof(AppCamera), + null); + + public ShaderItem VideoEffect + { + get => (ShaderItem)GetValue(VideoEffectProperty); + set => SetValue(VideoEffectProperty, value); + } + + public override void OnWillDisposeWithChildren() + { + ReleaseEffectShader(); + + base.OnWillDisposeWithChildren(); + } + + protected virtual void SetCustomShader(ShaderItem shader) + { + VideoEffect = shader; } private ShaderEditorPage _editor; - private void OnShaderError(object sender, string error) + private void RaiseShaderError(string error) { _editor?.ReportCompilationError(error); } + private void OnShaderError(object sender, string error) + { + RaiseShaderError(error); + } + public ICommand CommandEditShader { get @@ -147,9 +183,9 @@ public ICommand CommandEditShader return new Command(async (context) => { //just change currently running shader code, no matter what exactly we longpressed - if (_shader != null) + if (_effectShader != null) { - var code = _shader.LoadedCode; + var code = _effectShader.Code; MainThread.BeginInvokeOnMainThread(() => { _editor = new ShaderEditorPage(code, CallBackSetSelectedShaderCode); @@ -192,7 +228,7 @@ public static void OpenPageInNewWindow(ContentPage page, private static void NeedChangeShader(BindableObject bindable, object oldValue, object newValue) { - if (bindable is CameraWithEffects control) + if (bindable is AppCamera control) { control.SetCustomShader(control.ShaderSource); } @@ -200,7 +236,7 @@ private static void NeedChangeShader(BindableObject bindable, object oldValue, o public static readonly BindableProperty ShaderSourceProperty = BindableProperty.Create(nameof(ShaderSource), typeof(ShaderItem), - typeof(CameraWithEffects), + typeof(AppCamera), null, propertyChanged: NeedChangeShader); public ShaderItem ShaderSource diff --git a/src/app/Views/MainPageCameraFluent.Ui.cs b/src/app/Views/MainPageCameraFluent.Ui.cs index 5eb7168..29e448e 100644 --- a/src/app/Views/MainPageCameraFluent.Ui.cs +++ b/src/app/Views/MainPageCameraFluent.Ui.cs @@ -14,7 +14,7 @@ namespace ShadersCamera.Views public partial class MainCameraPageFluent : BasePageReloadable, IPageWIthCamera { Canvas Canvas; - CameraWithEffects CameraControl; + AppCamera CameraControl; //static for Hot Preview public static SkiaViewSwitcher? ViewsContainer; @@ -58,48 +58,43 @@ public override void Build() CreateMainLayout() } }.Assign(out ViewsContainer), -#if xDEBUG - new SkiaLabelFps() - { - Margin = new(0, 0, 4, 24), - VerticalOptions = LayoutOptions.End, - HorizontalOptions = LayoutOptions.End, - Rotation = -45, - FontSize = 11, - BackgroundColor = Colors.DarkRed, - TextColor = Colors.White, - ZIndex = 110, - } +#if DEBUG + CreateDebugFps() #endif } }.Fill() }; this.Content = - new Grid() + new Grid() //for safe insets due to MAUI specifics { Children = { Canvas } }; - + Subscribe(true); } SkiaLayout CreateMainLayout() { + var headerSize = 38; + return new SkiaLayout() { HorizontalOptions = LayoutOptions.Fill, VerticalOptions = LayoutOptions.Fill, Children = { - CreateCameraLayer(), + CreateCameraControl(), + + CreateControlsPanel(), + + CreateGestureCatcher(), new SkiaDrawer() { - AutoCache = false, - UseCache = SkiaCacheType.Operations, + UseCache = SkiaCacheType.GPU, Margin = new Thickness(0, 0, 0, 100), - HeaderSize = 40, + HeaderSize = headerSize, Direction = DrawerDirection.FromLeft, VerticalOptions = LayoutOptions.End, HorizontalOptions = LayoutOptions.Fill, @@ -143,11 +138,12 @@ SkiaLayout CreateMainLayout() Footer = new SkiaLayout() { VerticalOptions = LayoutOptions.Fill, - WidthRequest = 8 + 41 //drawer header + WidthRequest = 9 + headerSize //drawer header }, Content = new SkiaLayoutWithSelector() { Type = LayoutType.Row, + UseCache = SkiaCacheType.Operations, VerticalOptions = LayoutOptions.Center, Spacing = 8, RecyclingTemplate = RecyclingTemplate.Disabled, @@ -190,28 +186,18 @@ SkiaLayout CreateMainLayout() } } .Assign(out ShaderDrawer), -#if DEBUG - CreateDebugFps() -#endif } - }; - } - - SkiaLayer CreateCameraLayer() - { - return new SkiaLayer() + }.OnTapped(me => { - HorizontalOptions = LayoutOptions.Fill, - VerticalOptions = LayoutOptions.Fill, - Children = - { - CreateCameraControl(), - CreateRecordingBadge(), - CreateControlsLayer(), - CreateZoomHotspot() - } - } - .OnTapped(me => { TriggerUpdateSmallPreview = true; }); + if (!CameraControl.IsOn) + { + CameraControl.IsOn = true; + } + else + { + TriggerUpdateSmallPreview = true; + } + }); ; } SkiaShape CreateRecordingBadge() @@ -266,9 +252,9 @@ SkiaShape CreateRecordingBadge() }); } - CameraWithEffects CreateCameraControl() + AppCamera CreateCameraControl() { - return new CameraWithEffects() + return new AppCamera() { BackgroundColor = Colors.Black, PhotoQuality = CaptureQuality.Medium, @@ -282,7 +268,7 @@ CameraWithEffects CreateCameraControl() Tag = "Camera" } .Assign(out CameraControl) - .ObserveBindingContext((me, vm, prop) => + .ObserveBindingContext((me, vm, prop) => { bool attached = prop == nameof(BindingContext); if (attached || prop == nameof(vm.SelectedShader)) @@ -292,21 +278,7 @@ CameraWithEffects CreateCameraControl() }); } - SkiaLayer CreateControlsLayer() - { - return new SkiaLayer() - { - UseCache = SkiaCacheType.Operations, - VerticalOptions = LayoutOptions.Fill, - Children = - { - //CreateCaptureModeLabel(), //todo in next version for video - CreateControlsPanel(), - CreateResumeHotspot() - } - }; - } - + SkiaShape CreateCaptureModeLabel() { return new SkiaShape() @@ -355,6 +327,7 @@ SkiaShape CreateControlsPanel() { return new SkiaShape() { + UseCache = SkiaCacheType.GPU, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.End, Margin = new Thickness(0, 0, 0, 24), @@ -700,35 +673,17 @@ SkiaShape CreateCaptureButton() }); } - SkiaHotspot CreateResumeHotspot() - { - return new SkiaHotspot() - { - HorizontalOptions = LayoutOptions.Center, - LockRatio = 1, - VerticalOptions = LayoutOptions.Center, - WidthRequest = 290, - ZIndex = 110 - } - .OnTapped(me => TappedResume()) - .ObserveBindingContext((me, vm, prop) => - { - bool attached = prop == nameof(BindingContext); - if (attached || prop == nameof(vm.ShowResume)) - { - me.IsVisible = vm.ShowResume; - } - }); - } - - SkiaHotspotZoom CreateZoomHotspot() + SkiaHotspotZoom CreateGestureCatcher() { return new SkiaHotspotZoom() { ZoomMax = 3, ZoomMin = 1 } - .Initialize(hotspot => { hotspot.Zoomed += OnZoomed; }); + .Initialize(hotspot => + { + hotspot.Zoomed += OnZoomed; + }); } DataTemplate CreateShaderItemTemplate() @@ -873,6 +828,7 @@ SkiaLabelFps CreateDebugFps() return new SkiaLabelFps() { Margin = new Thickness(0, 0, 4, 24), + UseCache = SkiaCacheType.GPU, BackgroundColor = Colors.Black, ForceRefresh = false, HorizontalOptions = LayoutOptions.End, @@ -926,8 +882,6 @@ void SyncUi() public void OpenHelp() { - return; - MainThread.BeginInvokeOnMainThread(() => { var popup = new HelpPopup(); diff --git a/src/app/Views/MainPageCameraFluent.cs b/src/app/Views/MainPageCameraFluent.cs index e4acdd2..b4a1cbf 100644 --- a/src/app/Views/MainPageCameraFluent.cs +++ b/src/app/Views/MainPageCameraFluent.cs @@ -33,6 +33,17 @@ public MainCameraPageFluent() } } + void RefreshGpsLocationIfNeeded() + { + if (CameraControl.InjectGpsLocation) + { + MainThread.BeginInvokeOnMainThread(() => + { + _ = CameraControl.RefreshGpsLocation(); + }); + } + } + void Subscribe(bool subscribe) { if (subscribe) @@ -42,7 +53,6 @@ void Subscribe(bool subscribe) if (CameraControl != null) { CameraControl.CaptureFlashMode = (CaptureFlashMode)UserSettings.Current.Flash; - CameraControl.PropertyChanged += OnContextPropertyChanged; } } else @@ -52,11 +62,6 @@ void Subscribe(bool subscribe) Canvas.ViewDisposing -= CanvasWillDispose; Canvas.WillFirstTimeDraw -= WillFirstTimeDraw; } - - if (CameraControl != null) - { - CameraControl.PropertyChanged -= OnContextPropertyChanged; - } } } @@ -110,6 +115,20 @@ private void OnCameraStateChanged(object sender, HardwareState state) { CameraControl.PhotoQuality = CaptureQuality.Medium; } + + if (CameraControl.Display != null) + { + CameraControl.Display.Blur = 0; + } + + RefreshGpsLocationIfNeeded(); + } + else + { + if (CameraControl.Display != null) + { + CameraControl.Display.Blur = 10; + } } } @@ -248,34 +267,9 @@ private void TappedTurnCamera() } } - - private async void TappedTakePicture(object sender, SkiaGesturesParameters skiaGesturesParameters) - { - if (CameraControl.State == HardwareState.On && !CameraControl.IsBusy) - { - CameraControl.FlashScreen(Color.Parse("#EEFFFFFF")); - await CameraControl.TakePicture().ConfigureAwait(false); - } - } - - private void TappedResume() - { - CameraControl.IsOn = true; - } - float step = 0.2f; private bool _flashOn; - private void Tapped_ZoomOut(object sender, SkiaGesturesParameters skiaGesturesParameters) - { - CameraControl.Zoom -= step; - } - - private void Tapped_ZoomIn(object sender, SkiaGesturesParameters skiaGesturesParameters) - { - CameraControl.Zoom += step; - } - private void OnZoomed(object sender, ZoomEventArgs e) { CameraControl.Zoom = e.Value; @@ -297,11 +291,6 @@ private void TappedFlash() SyncUi(); } - private void TappedBackground(object sender, ControlTappedEventArgs e) - { - TriggerUpdateSmallPreview = true; - } - private void WillFirstTimeDraw(object sender, SkiaDrawingContext e) { @@ -320,24 +309,6 @@ private void TappedDrawerHeader() } - /// - /// Observing SkiaCamera props - /// - /// - /// - private void OnContextPropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(SkiaCamera.IsBusy)) - { - if (_vm.IsRecording) - return; - - ButtonCapture.BackgroundColor = CameraControl.IsBusy - ? Colors.DarkRed - : Color.Parse("#CECECE"); - } - } - #region SELECT FORMAT