diff --git a/README.md b/README.md index e5845cb..05144d8 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ 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 much better performance and bug fixes +* Use latest camera nuget with better performance and bug fixes +* Smooth filters menu ### Install diff --git a/dev/CameraApp-Refs.sln b/dev/CameraApp-Refs.sln index bad1d20..879dfbe 100644 --- a/dev/CameraApp-Refs.sln +++ b/dev/CameraApp-Refs.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.0.11201.2 d18.0 +VisualStudioVersion = 18.0.11201.2 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Refs", "Refs", "{5B1CDC4F-5ED6-4662-8EC6-3DE3FF0B05BE}" EndProject @@ -12,6 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ..\.gitignore = ..\.gitignore ..\README.md = ..\README.md ..\..\DrawnUi.Maui\src\Maui\Addons\DrawnUi.Maui.Camera\README.md = ..\..\DrawnUi.Maui\src\Maui\Addons\DrawnUi.Maui.Camera\README.md + ..\..\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}" @@ -24,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShadersCarouselDemo", "..\. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FastRepro", "..\..\DrawnUi.Maui\src\Maui\Samples\FastRepro\FastRepro.csproj", "{AF62CF62-A472-E87B-7225-8BD178AC3DF2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sandbox", "..\..\DrawnUi.Maui\src\Maui\Samples\Sandbox\Sandbox.csproj", "{9E8CD945-AB2A-2FEF-D962-CBDC4A7248EF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,6 +46,7 @@ Global {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 {A53734E6-1896-F775-5EEA-1A175FDA2B28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A53734E6-1896-F775-5EEA-1A175FDA2B28}.Debug|Any CPU.Build.0 = Debug|Any CPU {A53734E6-1896-F775-5EEA-1A175FDA2B28}.Debug|Any CPU.Deploy.0 = Debug|Any CPU @@ -54,6 +58,10 @@ Global {AF62CF62-A472-E87B-7225-8BD178AC3DF2}.Debug|Any CPU.Deploy.0 = Debug|Any CPU {AF62CF62-A472-E87B-7225-8BD178AC3DF2}.Release|Any CPU.ActiveCfg = Release|Any CPU {AF62CF62-A472-E87B-7225-8BD178AC3DF2}.Release|Any CPU.Build.0 = Release|Any CPU + {9E8CD945-AB2A-2FEF-D962-CBDC4A7248EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E8CD945-AB2A-2FEF-D962-CBDC4A7248EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9E8CD945-AB2A-2FEF-D962-CBDC4A7248EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9E8CD945-AB2A-2FEF-D962-CBDC4A7248EF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -64,6 +72,7 @@ Global {93E119B1-4378-87DF-2DD2-A818D1E6C2A2} = {5B1CDC4F-5ED6-4662-8EC6-3DE3FF0B05BE} {A53734E6-1896-F775-5EEA-1A175FDA2B28} = {5B1CDC4F-5ED6-4662-8EC6-3DE3FF0B05BE} {AF62CF62-A472-E87B-7225-8BD178AC3DF2} = {5B1CDC4F-5ED6-4662-8EC6-3DE3FF0B05BE} + {9E8CD945-AB2A-2FEF-D962-CBDC4A7248EF} = {5B1CDC4F-5ED6-4662-8EC6-3DE3FF0B05BE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {329E3D0C-A3F7-4A3E-B61C-6B2D1BD7F708} diff --git a/src/app/Helpers/UserSettings.cs b/src/app/Helpers/UserSettings.cs index 4172716..878d3fa 100644 --- a/src/app/Helpers/UserSettings.cs +++ b/src/app/Helpers/UserSettings.cs @@ -14,7 +14,7 @@ public UserSettings() Formats = new(); Fill = false; Lang = "en"; - Filter = "Movie"; + Filter = "Street Zoom"; } public string Lang { get; set; } diff --git a/src/app/Platforms/Android/AndroidManifest.xml b/src/app/Platforms/Android/AndroidManifest.xml index e4eb93a..5bcb200 100644 --- a/src/app/Platforms/Android/AndroidManifest.xml +++ b/src/app/Platforms/Android/AndroidManifest.xml @@ -1,5 +1,5 @@  - + diff --git a/src/app/Platforms/iOS/Info.plist b/src/app/Platforms/iOS/Info.plist index 7e41d2c..7a94a8c 100644 --- a/src/app/Platforms/iOS/Info.plist +++ b/src/app/Platforms/iOS/Info.plist @@ -3,7 +3,7 @@ CFBundleVersion - 104001 + 141002 LSRequiresIPhoneOS UIDeviceFamily @@ -25,6 +25,8 @@ arm64 + UIRequiresFullScreen + UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/src/app/Resources/Raw/Shaders/ripples.sksl b/src/app/Resources/Raw/Shaders/ripples.sksl new file mode 100644 index 0000000..98de1b3 --- /dev/null +++ b/src/app/Resources/Raw/Shaders/ripples.sksl @@ -0,0 +1,135 @@ +uniform float4 iMouse; // Mouse drag pos=.xy Click pos=.zw (pixels) +uniform float iTime; // Shader playback time (s) +uniform float2 iResolution; // Viewport resolution (pixels) +uniform float2 iImageResolution; // iImage1 resolution (pixels) +uniform shader iImage1; // Texture +uniform shader iImage2; // Texture for reflection +uniform float2 iOffset; // Top-left corner of DrawingRect +uniform float2 iOrigin; // Mouse drag started here + +uniform float2 origins[10]; // Array for multiple mouse positions +uniform float progresses[10]; // Array for multiple animation progresses (0.0 -> 1.0) + +/* +This shader is ported from the original Apple shader presented at WWDC 2024. +For more details, see the session here: https://developer.apple.com/videos/play/wwdc2024/10151/ +Credit to Apple for the original implementation. +Credits to Raouf Rahiche for the GLSL single ripple version that was used to create this SKSL. +SKSL port + multi-ripples + reflection by Nick Kovalsky +*/ + + +const float duration = 5.0; // This is the solved animation duration at speed 1 +const float amplitude = 0.015; // Default amplitude of the ripple +const float frequency = 15.0; // Default frequency of the ripple +const float decay = 2.0; // Default decay rate of the ripple +const float speed = 0.8; // Default speed of the ripple +const float rippleIntensity = 0.05; + +const float reflectionIntensity = 0.15; +const float minReflectionIntensity = 1.5; // minimum reflection visible + +const vec3 waterTint = vec3(0.1, 0.2, 0.5); + +// Define separate reflection angles to simulate 3D viewing perspective +const float reflectionAngleX = 5.0; // viewing perspective on X axis +const float reflectionAngleY = 5.0; // viewing perspective on Y axis +const float reflectionAngleZ = 1.0; // viewing perspective on Z axis + +half4 main(float2 fragCoord) +{ + // Precompute the scale factor + float2 renderingScale = iImageResolution.xy / iResolution.xy; + float2 inputCoord = (fragCoord - iOffset) * renderingScale; + vec2 uv = (fragCoord - iOffset) / iResolution.xy; + + // Sample the base color + half4 baseColor = iImage1.eval(inputCoord); + + // Initialize the combined displacement vector + vec2 combinedDisplacement = vec2(0.0, 0.0); + + // Loop through the ripples + for (int i = 0; i < 10; i++) + { + float progress = progresses[i]; + vec2 mouse = origins[i]; + + if (progress >= 0.0 && progress <= 1.0) + { + // Get the cursor position and normalize it + vec2 origin = mouse / iResolution.xy; + + // Calculate the distance and direction from the origin + vec2 direction = uv - origin; + float distance = length(direction); + + // Calculate the delay based on the distance + float delay = distance / speed; + + // Adapt the time for the delay and clamp to 0 + float time = max(0.0, progress * duration - delay); + + // Calculate the ripple amount + float rippleAmount = amplitude * sin(frequency * time) * exp(-decay * time); + + // Normalize the direction vector + vec2 n = direction / distance; + + // Accumulate the displacement caused by this ripple + combinedDisplacement += rippleAmount * n; + } + } + + // Calculate the final position by applying the combined displacement + vec2 finalPosition = uv + combinedDisplacement; + + // Sample the texture at the new combined position + vec3 finalColor = iImage1.eval(finalPosition * iResolution.xy * renderingScale).rgb; + + // Define viewing direction using reflection angles + vec2 viewingDirection = normalize(vec2(reflectionAngleX, reflectionAngleY)); + + // Calculate Fresnel effect + float fresnelEffect = pow(1.0 - dot(normalize(combinedDisplacement), viewingDirection), 3.0); + + // Adapt reflection intensity with Fresnel effect + float reflectionFactor = fresnelEffect * clamp(length(combinedDisplacement) / amplitude, 0.0, 1.0); + + // Ensure minimum reflection intensity + reflectionFactor = max(reflectionFactor, minReflectionIntensity); + + // Calculate dynamic perturbation factor based on combined displacement + float dynamicPerturbationFactor = length(combinedDisplacement) * 10000.0; // Adapt factor based on ripple strength + + // Calculate perturbation + vec2 perturbation = vec2(sin(finalPosition.x * dynamicPerturbationFactor), cos(finalPosition.y * dynamicPerturbationFactor)) * 0.05; + + // Calculate 3D angle-based offsets + vec2 angleOffsetX = vec2(reflectionAngleX * combinedDisplacement.y, reflectionAngleX * combinedDisplacement.x); + vec2 angleOffsetY = vec2(reflectionAngleY * combinedDisplacement.y, reflectionAngleY * combinedDisplacement.x); + vec2 angleOffsetZ = vec2(reflectionAngleZ * combinedDisplacement.y, reflectionAngleZ * combinedDisplacement.x); + + // Combine all 3D offsets + vec2 angleOffset = angleOffsetX + angleOffsetY + angleOffsetZ; + + // Apply angle offset and perturbation to get final distorted coordinates for reflection + vec2 distortedCoord = finalPosition + angleOffset + perturbation; + + // Sample the distorted reflection texture + vec3 reflectionColor = iImage2.eval(distortedCoord * iResolution.xy * renderingScale).rgb; + + // Tint the reflection color based on water color + vec3 tintedReflectionColor = mix(reflectionColor, reflectionColor * waterTint, 0.5); + + // Lighten or darken the color based on the combined ripple amount + finalColor += rippleIntensity * (length(combinedDisplacement) / amplitude); + + // Blend the reflection with the base color + finalColor = mix(finalColor, tintedReflectionColor, reflectionFactor * reflectionIntensity); + + // Set the fragment color + half4 fragColor = vec4(finalColor, 1.0); + + return fragColor; +} \ No newline at end of file diff --git a/src/app/ShadersCamera.csproj b/src/app/ShadersCamera.csproj index 44f70c9..57c44ee 100644 --- a/src/app/ShadersCamera.csproj +++ b/src/app/ShadersCamera.csproj @@ -3,7 +3,6 @@ - net9.0-android;net9.0-ios;net9.0-maccatalyst @@ -37,11 +36,6 @@ - - false - iPhone Developer - - True True @@ -55,6 +49,12 @@ True + + + false + iPhone Developer + + @@ -91,14 +91,12 @@ - - + diff --git a/src/app/Views/Controls/CameraWithEffects.cs b/src/app/Views/Controls/CameraWithEffects.cs index 298a589..118e888 100644 --- a/src/app/Views/Controls/CameraWithEffects.cs +++ b/src/app/Views/Controls/CameraWithEffects.cs @@ -65,7 +65,7 @@ static void InitializeAvailableShaders() } private SkiaShaderEffect _shader; - + private SkiaShaderEffect _shaderGlobal; public void ChangeShaderCode(string code) { if (Display == null || _shader==null) @@ -95,6 +95,18 @@ protected virtual void SetCustomShader(ShaderItem shader) return; } + //just having fun, add ripples to preview +/* + if (_shaderGlobal == null) + { + _shaderGlobal = new MultiRippleWithTouchEffect() + { + SecondarySource="Images/logo.png" + }; + VisualEffects.Add(_shaderGlobal); + } +*/ + // Remove existing shader if any if (_shader != null && VisualEffects.Contains(_shader)) { diff --git a/src/app/Views/Controls/ClippedShaderEffect.cs b/src/app/Views/Controls/ClippedShaderEffect.cs index f02f0f4..960a99a 100644 --- a/src/app/Views/Controls/ClippedShaderEffect.cs +++ b/src/app/Views/Controls/ClippedShaderEffect.cs @@ -1,3 +1,6 @@ +using AppoMobi.Maui.Gestures; +using System.Collections.Concurrent; + namespace ShadersCamera.Views.Controls; /// @@ -30,4 +33,4 @@ public override void Render(DrawingContext ctx) base.Render(ctx.WithDestination(clipped)); } } -} \ No newline at end of file +} diff --git a/src/app/Views/Controls/MultiRippleWithTouchEffect.cs b/src/app/Views/Controls/MultiRippleWithTouchEffect.cs new file mode 100644 index 0000000..74023b2 --- /dev/null +++ b/src/app/Views/Controls/MultiRippleWithTouchEffect.cs @@ -0,0 +1,143 @@ +using AppoMobi.Maui.Gestures; +using System.Collections.Concurrent; + +namespace ShadersCamera.Views.Controls; + +public class MultiRippleWithTouchEffect : ShaderDoubleTexturesEffect, + IStateEffect, ISkiaGestureProcessor +{ + public MultiRippleWithTouchEffect() + { + ShaderSource = "Shaders/ripples.sksl"; + } + + protected bool Initialized { get; set; } + + private PointF _mouse; + + #region IStateEffect + + /// + /// Will be invoked before actually painting but after gestures processing and other internal calculations. By SkiaControl.OnBeforeDrawing method. Beware if you call Update() inside will never stop updating. + /// + public virtual void UpdateState() + { + if (Parent != null && !Initialized && Parent.IsLayoutReady) + { + Initialized = true; + } + + } + + public override void Attach(SkiaControl parent) + { + base.Attach(parent); + + UpdateState(); + } + + #endregion + + protected override SKRuntimeEffectUniforms CreateUniforms(SKRect destination) + { + var uniforms = base.CreateUniforms(destination); + + var activeRipples = GetActiveRipples(); + + var mouseArray = new float[10 * 2]; + var progressArray = new float[10]; + + for (int i = 0; i < 10; i++) + { + if (i < activeRipples.Count) + { + var ripple = activeRipples[i]; + mouseArray[i * 2] = ripple.Origin.X; + mouseArray[i * 2 + 1] = ripple.Origin.Y; + progressArray[i] = (float)ripple.Progress; + } + else + { + mouseArray[i * 2] = 0; + mouseArray[i * 2 + 1] = 0; + progressArray[i] = -1f; // inactive + } + } + + uniforms["origins"] = mouseArray; + uniforms["progresses"] = progressArray; + + //was for just one ripple + //uniforms["progress"] = (float)Progress; + //uniforms["iMouse"] = new[] { _mouse.X, _mouse.Y, 0f, 0f }; + + return uniforms; + } + + #region RIPPLES + + public class Ripple + { + public Guid Uid { get; set; } + public PointF Origin { get; set; } + public long Time { get; set; } + public double Progress { get; set; } + } + + ConcurrentDictionary Ripples = new(); + + public Ripple CreateRipple(PointF origin) + { + var ripple = new Ripple + { + Uid = Guid.NewGuid(), + Origin = origin, + Time = Super.GetCurrentTimeNanos() + }; + Ripples[ripple.Uid] = ripple; + return ripple; + } + + public void RemoveRipple(Guid key) + { + Ripples.TryRemove(key, out _); + } + + + public List GetActiveRipples() + { + return Ripples.Values.OrderByDescending(x => x.Time).Take(10).ToList(); + } + + public virtual ISkiaGestureListener ProcessGestures( + SkiaGesturesParameters args, + GestureEventProcessingInfo apply) + { + _mouse = args.Event.Location; + + if (args.Type == TouchActionResult.Down && Initialized) + { + + var ripple = CreateRipple(_mouse); + + //run new animator for every Down + //we use this helper task so that every new rangeanimator is disposed properly at the end + Task.Run(async () => + { + await Parent.AnimateRangeAsync((v) => + { + ripple.Progress = v; + Update(); + }, 0, 1, 4500); + + RemoveRipple(ripple.Uid); + + }).ConfigureAwait(false); + + } + + return null; + } + + #endregion +} \ No newline at end of file diff --git a/src/app/Views/MainPageCameraFluent.Ui.cs b/src/app/Views/MainPageCameraFluent.Ui.cs index 22a3368..8dbc8ef 100644 --- a/src/app/Views/MainPageCameraFluent.Ui.cs +++ b/src/app/Views/MainPageCameraFluent.Ui.cs @@ -102,6 +102,7 @@ SkiaLayout CreateMainLayout() HorizontalOptions = LayoutOptions.Fill, HeightRequest = 100, IsOpen = false, + BlockGesturesBelow=true, IgnoreWrongDirection = true, ZIndex = 50, Content = new SkiaShape() @@ -121,64 +122,64 @@ SkiaLayout CreateMainLayout() Children = { new SkiaScroll() + { + //AutoCache = true, + BackgroundColor = Colors.WhiteSmoke, + Margin = new Thickness(0, 0, 20, 0), + Orientation = ScrollOrientation.Horizontal, + HorizontalOptions = LayoutOptions.Fill, + VerticalOptions = LayoutOptions.Fill, + Padding = new Thickness(8), + OrderedScrollIsAnimated = false, + SkipRenderingOutOfBounds = true, + Header = new SkiaLayout() { - //AutoCache = true, - BackgroundColor = Colors.WhiteSmoke, - Margin = new Thickness(0, 0, 20, 0), - Orientation = ScrollOrientation.Horizontal, - HorizontalOptions = LayoutOptions.Fill, VerticalOptions = LayoutOptions.Fill, - Padding = new Thickness(8), - OrderedScrollIsAnimated = false, - SkipRenderingOutOfBounds = true, - Header = new SkiaLayout() - { - VerticalOptions = LayoutOptions.Fill, - WidthRequest = 8 - }, - Footer = new SkiaLayout() + WidthRequest = 8 + }, + Footer = new SkiaLayout() + { + VerticalOptions = LayoutOptions.Fill, + WidthRequest = 8 + 41 //drawer header + }, + Content = new SkiaLayoutWithSelector() + { + UseCache = SkiaCacheType.Operations, + Type = LayoutType.Row, + VerticalOptions = LayoutOptions.Center, + Spacing = 8, + RecyclingTemplate = RecyclingTemplate.Disabled, + VirtualisationInflated = 50, + ItemTemplate = CreateShaderItemTemplate(), + Selector = new SkiaShape() { + UseCache = SkiaCacheType.Operations, + HorizontalOptions = LayoutOptions.Fill, VerticalOptions = LayoutOptions.Fill, - WidthRequest = 8 - }, - Content = new SkiaLayoutWithSelector() - { - //UseCache = SkiaCacheType.Image, - Type = LayoutType.Row, - VerticalOptions = LayoutOptions.Center, - Spacing = 8, - RecyclingTemplate = RecyclingTemplate.Disabled, - VirtualisationInflated = 50, - ItemTemplate = CreateShaderItemTemplate(), - Selector = new SkiaShape() - { - UseCache = SkiaCacheType.Operations, - HorizontalOptions = LayoutOptions.Fill, - VerticalOptions = LayoutOptions.Fill, - StrokeColor = Color.Parse("#CB230D"), - StrokeWidth = 3 - } - } - .ObserveProperty(()=>_vm, nameof(_vm.SelectedShaderIndex), me => - { - me.SelectedIndex = _vm.SelectedShaderIndex; - }) - .ObserveBindingContext((me, vm, prop) => - { - bool attached = prop == nameof(BindingContext); - if (attached || prop == nameof(vm.ShaderItems)) - { - me.ItemsSource = vm.ShaderItems; - } - }) + StrokeColor = Color.Parse("#CB230D"), + StrokeWidth = 5 + } } - .Assign(out MainScroll) - .ObserveProperty(()=>_vm, nameof(_vm.InitialIndex), me => + .ObserveProperty(()=>_vm, nameof(_vm.SelectedShaderIndex), me => { - me.OrderedScroll = _vm.InitialIndex; + me.SelectedIndex = _vm.SelectedShaderIndex; }) - .ObserveProperty(() => ShaderDrawer, nameof(ShaderDrawer.IsOpen), - me => { me.RespondsToGestures = ShaderDrawer.IsOpen; }), + .ObserveBindingContext((me, vm, prop) => + { + bool attached = prop == nameof(BindingContext); + if (attached || prop == nameof(vm.ShaderItems)) + { + me.ItemsSource = vm.ShaderItems; + } + }) + } + .Assign(out MainScroll) + .ObserveProperty(()=>_vm, nameof(_vm.InitialIndex), me => + { + me.OrderedScroll = _vm.InitialIndex; + }) + .ObserveProperty(() => ShaderDrawer, nameof(ShaderDrawer.IsOpen), + me => { me.RespondsToGestures = ShaderDrawer.IsOpen; }), CreateDrawerHeader() } @@ -616,7 +617,7 @@ DataTemplate CreateShaderItemTemplate() HeightRequest = 80, CornerRadius = new CornerRadius(8), BackgroundColor = Colors.White, - UseCache = SkiaCacheType.Image, + UseCache = SkiaCacheType.ImageDoubleBuffered, Children = { new SkiaLayout() @@ -635,7 +636,7 @@ DataTemplate CreateShaderItemTemplate() UseCache = SkiaCacheType.Image, VisualEffects = { - new SkiaShaderEffect() + new SkiaShaderEffect(), } } .ObserveBindingContext((me, item, prop) =>