mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-02 08:28:55 +02:00
Compare commits
1 Commits
powerscrip
...
niels9001/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85bfc399f9 |
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the GPU-accelerated ambient background effect rendered behind content.
|
||||
/// </summary>
|
||||
public enum AmbientEffectType
|
||||
{
|
||||
/// <summary>
|
||||
/// No ambient effect (default).
|
||||
/// </summary>
|
||||
Off,
|
||||
|
||||
/// <summary>
|
||||
/// Slowly drifting radial gradient blobs with soft merge blur.
|
||||
/// </summary>
|
||||
LavaLamp,
|
||||
|
||||
/// <summary>
|
||||
/// Red light sweeping back and forth (Knight Rider style).
|
||||
/// </summary>
|
||||
KittScanner,
|
||||
|
||||
/// <summary>
|
||||
/// Northern-lights-style flowing color bands.
|
||||
/// </summary>
|
||||
Aurora,
|
||||
|
||||
/// <summary>
|
||||
/// Gentle breathing radial glow.
|
||||
/// </summary>
|
||||
PulseGlow,
|
||||
|
||||
/// <summary>
|
||||
/// Soft roaming spotlight tracing a Lissajous curve.
|
||||
/// </summary>
|
||||
Spotlight,
|
||||
|
||||
/// <summary>
|
||||
/// WMP-inspired bouncing equalizer bars along the bottom edge.
|
||||
/// </summary>
|
||||
Bars,
|
||||
|
||||
/// <summary>
|
||||
/// WMP "Alchemy" inspired rotating starburst rays from center.
|
||||
/// </summary>
|
||||
Alchemy,
|
||||
|
||||
/// <summary>
|
||||
/// WMP "Geiss" inspired swirling psychedelic plasma field.
|
||||
/// </summary>
|
||||
Plasma,
|
||||
|
||||
/// <summary>
|
||||
/// Retro 80s synthwave neon grid with scanlines and glowing horizon.
|
||||
/// </summary>
|
||||
RetroGrid,
|
||||
|
||||
/// <summary>
|
||||
/// Audio-reactive equalizer bars driven by real-time system audio via WASAPI loopback.
|
||||
/// </summary>
|
||||
BarsLive,
|
||||
|
||||
/// <summary>
|
||||
/// Multi-color audio-reactive glow along the top edge — each zone reacts to a frequency band.
|
||||
/// </summary>
|
||||
AudioGlow,
|
||||
|
||||
/// <summary>
|
||||
/// WMP "Ambience" inspired — dreamy flowing organic shapes with audio-reactive morphing and slow color cycling.
|
||||
/// </summary>
|
||||
Ambience,
|
||||
}
|
||||
@@ -346,6 +346,20 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
public bool IsBackdropOpacityVisible =>
|
||||
BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsOpacity;
|
||||
|
||||
public int AmbientEffectIndex
|
||||
{
|
||||
get => (int)_settingsService.Settings.AmbientEffect;
|
||||
set
|
||||
{
|
||||
var newEffect = (AmbientEffectType)value;
|
||||
if (_settingsService.Settings.AmbientEffect != newEffect)
|
||||
{
|
||||
_settingsService.UpdateSettings(s => s with { AmbientEffect = newEffect });
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the backdrop description (for styles without options) should be visible.
|
||||
/// </summary>
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
public partial class DockWindowViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly DispatcherQueue _uiDispatcherQueue = DispatcherQueue.GetForCurrentThread()!;
|
||||
|
||||
[ObservableProperty]
|
||||
@@ -49,11 +50,31 @@ public partial class DockWindowViewModel : ObservableObject, IDisposable
|
||||
[ObservableProperty]
|
||||
public partial double ColorizationOpacity { get; private set; }
|
||||
|
||||
public DockWindowViewModel(IThemeService themeService)
|
||||
[ObservableProperty]
|
||||
public partial AmbientEffectType DockAmbientEffect { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial DockSide DockSide { get; private set; } = DockSide.Top;
|
||||
|
||||
public DockWindowViewModel(IThemeService themeService, ISettingsService settingsService)
|
||||
{
|
||||
_themeService = themeService;
|
||||
_themeService.ThemeChanged += ThemeService_ThemeChanged;
|
||||
_settingsService = settingsService;
|
||||
_settingsService.SettingsChanged += SettingsService_SettingsChanged;
|
||||
UpdateFromThemeSnapshot();
|
||||
UpdateFromSettings(_settingsService.Settings);
|
||||
}
|
||||
|
||||
private void SettingsService_SettingsChanged(ISettingsService sender, SettingsModel args)
|
||||
{
|
||||
_uiDispatcherQueue.TryEnqueue(() => UpdateFromSettings(args));
|
||||
}
|
||||
|
||||
private void UpdateFromSettings(SettingsModel settings)
|
||||
{
|
||||
DockAmbientEffect = settings.DockSettings.AmbientEffect;
|
||||
DockSide = settings.DockSettings.Side;
|
||||
}
|
||||
|
||||
private void ThemeService_ThemeChanged(object? sender, ThemeChangedEventArgs e)
|
||||
@@ -85,6 +106,7 @@ public partial class DockWindowViewModel : ObservableObject, IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
_themeService.ThemeChanged -= ThemeService_ThemeChanged;
|
||||
_settingsService.SettingsChanged -= SettingsService_SettingsChanged;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,20 @@ public sealed partial class DockAppearanceSettingsViewModel : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
public int AmbientEffectIndex
|
||||
{
|
||||
get => (int)_settingsService.Settings.DockSettings.AmbientEffect;
|
||||
set
|
||||
{
|
||||
var newEffect = (AmbientEffectType)value;
|
||||
if (_settingsService.Settings.DockSettings.AmbientEffect != newEffect)
|
||||
{
|
||||
_settingsService.UpdateSettings(s => s with { DockSettings = s.DockSettings with { AmbientEffect = newEffect } });
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ColorizationMode ColorizationMode
|
||||
{
|
||||
get => _settingsService.Settings.DockSettings.ColorizationMode;
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
public partial class MainWindowViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly DispatcherQueue _uiDispatcherQueue = DispatcherQueue.GetForCurrentThread()!;
|
||||
|
||||
[ObservableProperty]
|
||||
@@ -50,6 +51,9 @@ public partial class MainWindowViewModel : ObservableObject, IDisposable
|
||||
[NotifyPropertyChangedFor(nameof(EffectiveImageOpacity))]
|
||||
public partial float BackdropOpacity { get; private set; } = 1.0f;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial AmbientEffectType AmbientEffect { get; private set; }
|
||||
|
||||
// Returns null when no transparency needed (BlurImageControl uses this to decide source type)
|
||||
public BackdropStyle? EffectiveBackdropStyle =>
|
||||
BackdropStyle == BackdropStyle.Clear ||
|
||||
@@ -64,10 +68,21 @@ public partial class MainWindowViewModel : ObservableObject, IDisposable
|
||||
? BackgroundImageOpacity * Math.Sqrt(BackdropOpacity)
|
||||
: BackgroundImageOpacity;
|
||||
|
||||
public MainWindowViewModel(IThemeService themeService)
|
||||
public MainWindowViewModel(IThemeService themeService, ISettingsService settingsService)
|
||||
{
|
||||
_themeService = themeService;
|
||||
_themeService.ThemeChanged += ThemeService_ThemeChanged;
|
||||
_settingsService = settingsService;
|
||||
_settingsService.SettingsChanged += SettingsService_SettingsChanged;
|
||||
AmbientEffect = _settingsService.Settings.AmbientEffect;
|
||||
}
|
||||
|
||||
private void SettingsService_SettingsChanged(ISettingsService sender, SettingsModel args)
|
||||
{
|
||||
_uiDispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
AmbientEffect = args.AmbientEffect;
|
||||
});
|
||||
}
|
||||
|
||||
private void ThemeService_ThemeChanged(object? sender, ThemeChangedEventArgs e)
|
||||
@@ -93,6 +108,7 @@ public partial class MainWindowViewModel : ObservableObject, IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
_themeService.ThemeChanged -= ThemeService_ThemeChanged;
|
||||
_settingsService.SettingsChanged -= SettingsService_SettingsChanged;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ public record DockSettings
|
||||
|
||||
public string? BackgroundImagePath { get; init; }
|
||||
|
||||
public AmbientEffectType AmbientEffect { get; init; }
|
||||
|
||||
// </Theme settings>
|
||||
public ImmutableList<DockBandSettings> StartBands { get; init; } = ImmutableList.Create(
|
||||
new DockBandSettings
|
||||
|
||||
@@ -89,6 +89,8 @@ public record SettingsModel
|
||||
|
||||
public int BackdropOpacity { get; init; } = 100;
|
||||
|
||||
public AmbientEffectType AmbientEffect { get; init; }
|
||||
|
||||
// </Theme settings>
|
||||
|
||||
// END SETTINGS
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Numerics;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Hosting;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls.AmbientEffects;
|
||||
|
||||
/// <summary>
|
||||
/// A single XAML control that renders GPU-accelerated ambient background effects
|
||||
/// via the Composition API. Drop it into any layout as a non-interactive overlay.
|
||||
/// </summary>
|
||||
internal sealed partial class AmbientEffectControl : Control
|
||||
{
|
||||
public static readonly DependencyProperty EffectTypeProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(EffectType),
|
||||
typeof(AmbientEffectType),
|
||||
typeof(AmbientEffectControl),
|
||||
new PropertyMetadata(AmbientEffectType.Off, OnEffectTypeChanged));
|
||||
|
||||
public static readonly DependencyProperty DockSideProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(DockSide),
|
||||
typeof(DockSide),
|
||||
typeof(AmbientEffectControl),
|
||||
new PropertyMetadata(DockSide.Top, OnEffectTypeChanged));
|
||||
|
||||
public static readonly DependencyProperty IsDockModeProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(IsDockMode),
|
||||
typeof(bool),
|
||||
typeof(AmbientEffectControl),
|
||||
new PropertyMetadata(false, OnEffectTypeChanged));
|
||||
|
||||
private Compositor? _compositor;
|
||||
private ContainerVisual? _rootVisual;
|
||||
private IBackgroundEffect? _currentEffect;
|
||||
private bool _isVisible;
|
||||
|
||||
public AmbientEffectType EffectType
|
||||
{
|
||||
get => (AmbientEffectType)GetValue(EffectTypeProperty);
|
||||
set => SetValue(EffectTypeProperty, value);
|
||||
}
|
||||
|
||||
public DockSide DockSide
|
||||
{
|
||||
get => (DockSide)GetValue(DockSideProperty);
|
||||
set => SetValue(DockSideProperty, value);
|
||||
}
|
||||
|
||||
public bool IsDockMode
|
||||
{
|
||||
get => (bool)GetValue(IsDockModeProperty);
|
||||
set => SetValue(IsDockModeProperty, value);
|
||||
}
|
||||
|
||||
public AmbientEffectControl()
|
||||
{
|
||||
DefaultStyleKey = typeof(AmbientEffectControl);
|
||||
Loaded += OnLoaded;
|
||||
Unloaded += OnUnloaded;
|
||||
SizeChanged += OnSizeChanged;
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_isVisible = true;
|
||||
EnsureCompositor();
|
||||
ApplyEffect();
|
||||
}
|
||||
|
||||
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_isVisible = false;
|
||||
CleanupEffect();
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
var newSize = new Vector2((float)e.NewSize.Width, (float)e.NewSize.Height);
|
||||
if (_rootVisual != null)
|
||||
{
|
||||
_rootVisual.Size = newSize;
|
||||
}
|
||||
|
||||
_currentEffect?.Resize(newSize);
|
||||
}
|
||||
|
||||
private static void OnEffectTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is AmbientEffectControl control && control._compositor != null)
|
||||
{
|
||||
control.ApplyEffect();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureCompositor()
|
||||
{
|
||||
if (_compositor != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var elementVisual = ElementCompositionPreview.GetElementVisual(this);
|
||||
_compositor = elementVisual.Compositor;
|
||||
_rootVisual = _compositor.CreateContainerVisual();
|
||||
_rootVisual.Size = new Vector2((float)ActualWidth, (float)ActualHeight);
|
||||
_rootVisual.Clip = _compositor.CreateInsetClip();
|
||||
ElementCompositionPreview.SetElementChildVisual(this, _rootVisual);
|
||||
}
|
||||
|
||||
private void ApplyEffect()
|
||||
{
|
||||
CleanupEffect();
|
||||
|
||||
if (_compositor == null || _rootVisual == null || EffectType == AmbientEffectType.Off)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var size = new Vector2((float)ActualWidth, (float)ActualHeight);
|
||||
_currentEffect = BackgroundEffectFactory.Create(EffectType, IsDockMode ? DockSide : null);
|
||||
|
||||
if (_currentEffect != null)
|
||||
{
|
||||
_currentEffect.Initialize(_compositor, _rootVisual, size);
|
||||
if (_isVisible)
|
||||
{
|
||||
_currentEffect.Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanupEffect()
|
||||
{
|
||||
_currentEffect?.Stop();
|
||||
_currentEffect?.Dispose();
|
||||
_currentEffect = null;
|
||||
|
||||
// Clear the container visual's children
|
||||
if (_rootVisual != null)
|
||||
{
|
||||
_rootVisual.Children.RemoveAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls.AmbientEffects.Audio;
|
||||
|
||||
#pragma warning disable SA1401 // Fields should be private
|
||||
#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter
|
||||
#pragma warning disable SA1310 // Field names should not contain underscore
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
#pragma warning disable SA1649 // File name should match first type name
|
||||
|
||||
internal static class AudioConstants
|
||||
{
|
||||
public const int AUDCLNT_STREAMFLAGS_LOOPBACK = 0x00020000;
|
||||
public const int AUDCLNT_BUFFERFLAGS_SILENT = 0x2;
|
||||
public const uint CLSCTX_ALL = 0x17;
|
||||
public const int STGM_READ = 0;
|
||||
public const int EDataFlow_eRender = 0;
|
||||
public const int ERole_eConsole = 0;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct WaveFormatEx
|
||||
{
|
||||
public ushort wFormatTag;
|
||||
public ushort nChannels;
|
||||
public uint nSamplesPerSec;
|
||||
public uint nAvgBytesPerSec;
|
||||
public ushort nBlockAlign;
|
||||
public ushort wBitsPerSample;
|
||||
public ushort cbSize;
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
|
||||
internal class MMDeviceEnumeratorClass
|
||||
{
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IMMDeviceEnumerator
|
||||
{
|
||||
[PreserveSig]
|
||||
int EnumAudioEndpoints(int dataFlow, uint stateMask, out IntPtr devices);
|
||||
|
||||
[PreserveSig]
|
||||
int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice device);
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[Guid("D666063F-1587-4E43-81F1-B948E807363F")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IMMDevice
|
||||
{
|
||||
[PreserveSig]
|
||||
int Activate(ref Guid iid, uint clsCtx, IntPtr activationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[Guid("1CB9AD4C-DBFA-4c32-B178-C2F568A703B2")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IAudioClient
|
||||
{
|
||||
[PreserveSig]
|
||||
int Initialize(int shareMode, int streamFlags, long bufferDuration, long periodicity, IntPtr pFormat, IntPtr audioSessionGuid);
|
||||
|
||||
[PreserveSig]
|
||||
int GetBufferSize(out uint bufferFrameCount);
|
||||
|
||||
[PreserveSig]
|
||||
int GetStreamLatency(out long latency);
|
||||
|
||||
[PreserveSig]
|
||||
int GetCurrentPadding(out uint numPaddingFrames);
|
||||
|
||||
[PreserveSig]
|
||||
int IsFormatSupported(int shareMode, IntPtr pFormat, out IntPtr closestMatch);
|
||||
|
||||
[PreserveSig]
|
||||
int GetMixFormat(out IntPtr pFormat);
|
||||
|
||||
[PreserveSig]
|
||||
int GetDevicePeriod(out long defaultDevicePeriod, out long minimumDevicePeriod);
|
||||
|
||||
[PreserveSig]
|
||||
int Start();
|
||||
|
||||
[PreserveSig]
|
||||
int Stop();
|
||||
|
||||
[PreserveSig]
|
||||
int Reset();
|
||||
|
||||
[PreserveSig]
|
||||
int SetEventHandle(IntPtr eventHandle);
|
||||
|
||||
[PreserveSig]
|
||||
int GetService(ref Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppv);
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[Guid("C8ADBD64-E71E-48a0-A4DE-185C395CD317")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IAudioCaptureClient
|
||||
{
|
||||
[PreserveSig]
|
||||
int GetBuffer(out IntPtr dataPtr, out uint numFramesAvailable, out int flags, out long devicePosition, out long qpcPosition);
|
||||
|
||||
[PreserveSig]
|
||||
int ReleaseBuffer(uint numFramesRead);
|
||||
|
||||
[PreserveSig]
|
||||
int GetNextPacketSize(out uint numFramesInNextPacket);
|
||||
}
|
||||
|
||||
#pragma warning restore SA1310
|
||||
#pragma warning restore SA1307
|
||||
#pragma warning restore SA1401
|
||||
#pragma warning restore SA1402
|
||||
#pragma warning restore SA1649
|
||||
@@ -0,0 +1,328 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls.AmbientEffects.Audio;
|
||||
|
||||
/// <summary>
|
||||
/// Captures system audio via WASAPI loopback, runs FFT, and exposes
|
||||
/// real-time frequency band levels. Runs on a dedicated background thread.
|
||||
/// </summary>
|
||||
internal sealed class AudioLoopbackService : IDisposable
|
||||
{
|
||||
private const int FFTSize = 1024;
|
||||
private const int HalfFFT = FFTSize / 2;
|
||||
private const float AttackCoeff = 0.8f;
|
||||
private const float DecayCoeff = 0.12f;
|
||||
|
||||
private static readonly Guid AudioClientGuid = new("1CB9AD4C-DBFA-4c32-B178-C2F568A703B2");
|
||||
private static readonly Guid AudioCaptureClientGuid = new("C8ADBD64-E71E-48a0-A4DE-185C395CD317");
|
||||
|
||||
private readonly object _lock = new();
|
||||
private readonly float[] _smoothedBands;
|
||||
private readonly int _bandCount;
|
||||
private readonly int[] _bandBinEdges;
|
||||
|
||||
private IAudioClient? _audioClient;
|
||||
private IAudioCaptureClient? _captureClient;
|
||||
private Thread? _captureThread;
|
||||
private volatile bool _running;
|
||||
private uint _sampleRate;
|
||||
private ushort _channels;
|
||||
private bool _isFloat;
|
||||
|
||||
public bool IsCapturing => _running;
|
||||
|
||||
public AudioLoopbackService(int bandCount = 48)
|
||||
{
|
||||
_bandCount = bandCount;
|
||||
_smoothedBands = new float[bandCount];
|
||||
_bandBinEdges = ComputeLogBandEdges(bandCount, HalfFFT, 20f, 20000f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes WASAPI loopback capture and starts the background thread.
|
||||
/// Returns false if the audio device cannot be opened.
|
||||
/// </summary>
|
||||
public bool Start()
|
||||
{
|
||||
try
|
||||
{
|
||||
var enumerator = (IMMDeviceEnumerator)new MMDeviceEnumeratorClass();
|
||||
var hr = enumerator.GetDefaultAudioEndpoint(
|
||||
AudioConstants.EDataFlow_eRender,
|
||||
AudioConstants.ERole_eConsole,
|
||||
out var device);
|
||||
|
||||
if (hr != 0 || device == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var audioClientGuid = AudioClientGuid;
|
||||
hr = device.Activate(ref audioClientGuid, AudioConstants.CLSCTX_ALL, IntPtr.Zero, out var clientObj);
|
||||
if (hr != 0 || clientObj == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_audioClient = (IAudioClient)clientObj;
|
||||
|
||||
hr = _audioClient.GetMixFormat(out var formatPtr);
|
||||
if (hr != 0 || formatPtr == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var format = Marshal.PtrToStructure<WaveFormatEx>(formatPtr);
|
||||
_sampleRate = format.nSamplesPerSec;
|
||||
_channels = format.nChannels;
|
||||
_isFloat = format.wFormatTag == 3 || (format.wFormatTag == 0xFFFE && format.wBitsPerSample == 32);
|
||||
|
||||
// Initialize in loopback mode with 100ms buffer
|
||||
hr = _audioClient.Initialize(
|
||||
0, // AUDCLNT_SHAREMODE_SHARED
|
||||
AudioConstants.AUDCLNT_STREAMFLAGS_LOOPBACK,
|
||||
1000000, // 100ms in 100-ns units
|
||||
0,
|
||||
formatPtr,
|
||||
IntPtr.Zero);
|
||||
|
||||
Marshal.FreeCoTaskMem(formatPtr);
|
||||
|
||||
if (hr != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var captureGuid = AudioCaptureClientGuid;
|
||||
hr = _audioClient.GetService(ref captureGuid, out var captureObj);
|
||||
if (hr != 0 || captureObj == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_captureClient = (IAudioCaptureClient)captureObj;
|
||||
|
||||
_audioClient.Start();
|
||||
_running = true;
|
||||
_captureThread = new Thread(CaptureLoop) { IsBackground = true, Name = "AudioLoopback" };
|
||||
_captureThread.Start();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to initialize audio loopback", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current smoothed band levels. Thread-safe.
|
||||
/// </summary>
|
||||
public void GetBandLevels(float[] output)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var count = Math.Min(output.Length, _smoothedBands.Length);
|
||||
Array.Copy(_smoothedBands, output, count);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_running = false;
|
||||
_captureThread?.Join(500);
|
||||
_captureThread = null;
|
||||
|
||||
try
|
||||
{
|
||||
_audioClient?.Stop();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort cleanup
|
||||
}
|
||||
|
||||
if (_captureClient is IDisposable captureDisp)
|
||||
{
|
||||
captureDisp.Dispose();
|
||||
}
|
||||
|
||||
if (_audioClient is IDisposable clientDisp)
|
||||
{
|
||||
clientDisp.Dispose();
|
||||
}
|
||||
|
||||
_captureClient = null;
|
||||
_audioClient = null;
|
||||
}
|
||||
|
||||
private void CaptureLoop()
|
||||
{
|
||||
var fftBuffer = new float[FFTSize * 2]; // Interleaved real/imag
|
||||
var magnitudes = new float[HalfFFT];
|
||||
var sampleAccumulator = new float[FFTSize];
|
||||
var accumulatedCount = 0;
|
||||
|
||||
while (_running)
|
||||
{
|
||||
Thread.Sleep(30); // ~30fps
|
||||
|
||||
if (_captureClient == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
DrainSamples(sampleAccumulator, ref accumulatedCount);
|
||||
|
||||
if (accumulatedCount >= FFTSize)
|
||||
{
|
||||
ProcessFFT(sampleAccumulator, fftBuffer, magnitudes);
|
||||
UpdateBands(magnitudes);
|
||||
accumulatedCount = 0;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Audio device may have been removed
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrainSamples(float[] accumulator, ref int count)
|
||||
{
|
||||
if (_captureClient == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
var hr = _captureClient.GetNextPacketSize(out var packetSize);
|
||||
if (hr != 0 || packetSize == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
hr = _captureClient.GetBuffer(out var dataPtr, out var numFrames, out var flags, out _, out _);
|
||||
if (hr != 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var isSilent = (flags & AudioConstants.AUDCLNT_BUFFERFLAGS_SILENT) != 0;
|
||||
var channels = Math.Max(_channels, (ushort)1);
|
||||
|
||||
for (uint f = 0; f < numFrames && count < FFTSize; f++)
|
||||
{
|
||||
if (isSilent)
|
||||
{
|
||||
accumulator[count++] = 0f;
|
||||
}
|
||||
else if (_isFloat)
|
||||
{
|
||||
// Mix channels to mono
|
||||
var sum = 0f;
|
||||
for (var ch = 0; ch < channels; ch++)
|
||||
{
|
||||
sum += Marshal.PtrToStructure<float>(dataPtr + ((((int)f * channels) + ch) * sizeof(float)));
|
||||
}
|
||||
|
||||
accumulator[count++] = sum / channels;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 16-bit PCM fallback
|
||||
var sum = 0f;
|
||||
for (var ch = 0; ch < channels; ch++)
|
||||
{
|
||||
var sample = Marshal.PtrToStructure<short>(dataPtr + ((((int)f * channels) + ch) * sizeof(short)));
|
||||
sum += sample / 32768f;
|
||||
}
|
||||
|
||||
accumulator[count++] = sum / channels;
|
||||
}
|
||||
}
|
||||
|
||||
_captureClient.ReleaseBuffer(numFrames);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessFFT(float[] samples, float[] fftBuffer, float[] magnitudes)
|
||||
{
|
||||
SimpleFFT.ApplyHanningWindow(samples, FFTSize);
|
||||
|
||||
// Copy into interleaved format (real, 0, real, 0, ...)
|
||||
Array.Clear(fftBuffer);
|
||||
for (var i = 0; i < FFTSize; i++)
|
||||
{
|
||||
fftBuffer[2 * i] = samples[i];
|
||||
}
|
||||
|
||||
SimpleFFT.ComputeFFT(fftBuffer, FFTSize);
|
||||
SimpleFFT.GetMagnitudes(fftBuffer, magnitudes, FFTSize);
|
||||
}
|
||||
|
||||
private void UpdateBands(float[] magnitudes)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
for (var b = 0; b < _bandCount; b++)
|
||||
{
|
||||
var startBin = _bandBinEdges[b];
|
||||
var endBin = _bandBinEdges[b + 1];
|
||||
endBin = Math.Max(endBin, startBin + 1);
|
||||
|
||||
var sum = 0f;
|
||||
for (var i = startBin; i < endBin && i < magnitudes.Length; i++)
|
||||
{
|
||||
sum += magnitudes[i];
|
||||
}
|
||||
|
||||
// Average and scale to 0-1 range (with a boost for visibility)
|
||||
var raw = sum / (endBin - startBin) * 25f;
|
||||
raw = Math.Clamp(raw, 0f, 1f);
|
||||
|
||||
// Smooth: fast attack, slow decay
|
||||
if (raw > _smoothedBands[b])
|
||||
{
|
||||
_smoothedBands[b] += (raw - _smoothedBands[b]) * AttackCoeff;
|
||||
}
|
||||
else
|
||||
{
|
||||
_smoothedBands[b] += (raw - _smoothedBands[b]) * DecayCoeff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes logarithmically spaced bin edges for frequency bands.
|
||||
/// Human hearing is logarithmic, so bass frequencies get more visual space.
|
||||
/// </summary>
|
||||
private static int[] ComputeLogBandEdges(int bandCount, int totalBins, float minFreq, float maxFreq)
|
||||
{
|
||||
var edges = new int[bandCount + 1];
|
||||
var logMin = MathF.Log10(minFreq);
|
||||
var logMax = MathF.Log10(maxFreq);
|
||||
|
||||
for (var i = 0; i <= bandCount; i++)
|
||||
{
|
||||
var logFreq = logMin + ((logMax - logMin) * i / bandCount);
|
||||
var freq = MathF.Pow(10, logFreq);
|
||||
|
||||
// Map frequency to bin index (assuming sampleRate ~48kHz)
|
||||
var bin = (int)(freq / 20000f * totalBins);
|
||||
edges[i] = Math.Clamp(bin, 0, totalBins);
|
||||
}
|
||||
|
||||
return edges;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls.AmbientEffects.Audio;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal in-place Cooley-Tukey radix-2 FFT.
|
||||
/// Operates on interleaved real/imaginary float arrays.
|
||||
/// </summary>
|
||||
internal static class SimpleFFT
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies a Hanning window to the real-valued samples in place.
|
||||
/// </summary>
|
||||
public static void ApplyHanningWindow(float[] samples, int count)
|
||||
{
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var multiplier = 0.5f * (1f - MathF.Cos((2f * MathF.PI * i) / (count - 1)));
|
||||
samples[i] *= multiplier;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes an in-place FFT on interleaved real/imaginary data.
|
||||
/// <paramref name="data"/> must have length = 2 * N where N is a power of 2.
|
||||
/// data[2*k] = real part, data[2*k+1] = imaginary part.
|
||||
/// </summary>
|
||||
public static void ComputeFFT(float[] data, int n)
|
||||
{
|
||||
// Bit-reversal permutation
|
||||
var j = 0;
|
||||
for (var i = 0; i < n - 1; i++)
|
||||
{
|
||||
if (i < j)
|
||||
{
|
||||
(data[2 * i], data[2 * j]) = (data[2 * j], data[2 * i]);
|
||||
(data[(2 * i) + 1], data[(2 * j) + 1]) = (data[(2 * j) + 1], data[(2 * i) + 1]);
|
||||
}
|
||||
|
||||
var m = n >> 1;
|
||||
while (m >= 1 && j >= m)
|
||||
{
|
||||
j -= m;
|
||||
m >>= 1;
|
||||
}
|
||||
|
||||
j += m;
|
||||
}
|
||||
|
||||
// Cooley-Tukey butterfly
|
||||
for (var step = 1; step < n; step <<= 1)
|
||||
{
|
||||
var angleStep = -MathF.PI / step;
|
||||
var wR = MathF.Cos(angleStep);
|
||||
var wI = MathF.Sin(angleStep);
|
||||
|
||||
for (var group = 0; group < n; group += step << 1)
|
||||
{
|
||||
var twR = 1f;
|
||||
var twI = 0f;
|
||||
|
||||
for (var pair = 0; pair < step; pair++)
|
||||
{
|
||||
var even = group + pair;
|
||||
var odd = even + step;
|
||||
|
||||
var oddR = data[2 * odd];
|
||||
var oddI = data[(2 * odd) + 1];
|
||||
|
||||
var tR = (twR * oddR) - (twI * oddI);
|
||||
var tI = (twR * oddI) + (twI * oddR);
|
||||
|
||||
data[2 * odd] = data[2 * even] - tR;
|
||||
data[(2 * odd) + 1] = data[(2 * even) + 1] - tI;
|
||||
data[2 * even] += tR;
|
||||
data[(2 * even) + 1] += tI;
|
||||
|
||||
var newTwR = (twR * wR) - (twI * wI);
|
||||
twI = (twR * wI) + (twI * wR);
|
||||
twR = newTwR;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts magnitude spectrum from interleaved FFT result.
|
||||
/// Returns N/2 magnitudes (only positive frequencies).
|
||||
/// </summary>
|
||||
public static void GetMagnitudes(float[] fftData, float[] magnitudes, int n)
|
||||
{
|
||||
for (var i = 0; i < n / 2; i++)
|
||||
{
|
||||
var re = fftData[2 * i];
|
||||
var im = fftData[(2 * i) + 1];
|
||||
magnitudes[i] = MathF.Sqrt((re * re) + (im * im)) / n;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI.Controls.AmbientEffects.Effects;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls.AmbientEffects;
|
||||
|
||||
internal static class BackgroundEffectFactory
|
||||
{
|
||||
public static IBackgroundEffect? Create(AmbientEffectType type, DockSide? dockSide = null)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
AmbientEffectType.LavaLamp => new LavaLampEffect(),
|
||||
AmbientEffectType.KittScanner => new KittScannerEffect(dockSide),
|
||||
AmbientEffectType.Aurora => new AuroraEffect(),
|
||||
AmbientEffectType.PulseGlow => new PulseGlowEffect(),
|
||||
AmbientEffectType.Spotlight => new SpotlightEffect(),
|
||||
AmbientEffectType.Bars => new BarsEffect(),
|
||||
AmbientEffectType.Alchemy => new AlchemyEffect(),
|
||||
AmbientEffectType.Plasma => new PlasmaEffect(),
|
||||
AmbientEffectType.RetroGrid => new RetroGridEffect(),
|
||||
AmbientEffectType.BarsLive => new LiveBarsEffect(),
|
||||
AmbientEffectType.AudioGlow => new AudioGlowEffect(),
|
||||
AmbientEffectType.Ambience => new AmbienceEffect(),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Numerics;
|
||||
using Microsoft.CmdPal.UI.Controls.AmbientEffects.Audio;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls.AmbientEffects.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// WMP "Alchemy" visualization — concentric geometric starburst patterns
|
||||
/// with an inner and outer ring of rays rotating in opposite directions,
|
||||
/// pulsing scale/opacity, and a bright morphing center orb. Authentic
|
||||
/// spirographic kaleidoscope feel.
|
||||
/// </summary>
|
||||
internal sealed class AlchemyEffect : IBackgroundEffect
|
||||
{
|
||||
private const int OuterRayCount = 24;
|
||||
private const int InnerRayCount = 14;
|
||||
|
||||
private static readonly Color OuterColor = Color.FromArgb(180, 100, 180, 255);
|
||||
private static readonly Color InnerColor = Color.FromArgb(200, 255, 100, 220);
|
||||
private static readonly Color AccentColor = Color.FromArgb(180, 100, 255, 180);
|
||||
|
||||
private Compositor? _compositor;
|
||||
private SpriteVisual[]? _outerRays;
|
||||
private SpriteVisual[]? _innerRays;
|
||||
private SpriteVisual? _centerOrb;
|
||||
private SpriteVisual? _outerRing;
|
||||
private Vector2 _size;
|
||||
|
||||
private AudioLoopbackService? _audioService;
|
||||
private DispatcherQueueTimer? _updateTimer;
|
||||
private float[]? _levelBuffer;
|
||||
private bool _audioAvailable;
|
||||
|
||||
public void Initialize(Compositor compositor, ContainerVisual rootVisual, Vector2 size)
|
||||
{
|
||||
_compositor = compositor;
|
||||
_size = size;
|
||||
|
||||
var cx = size.X * 0.5f;
|
||||
var cy = size.Y * 0.5f;
|
||||
|
||||
// Use smaller dimension so it fits in dock's thin aspect ratio
|
||||
var span = Math.Min(size.X, size.Y);
|
||||
var outerLen = span * 0.45f;
|
||||
var innerLen = outerLen * 0.5f;
|
||||
|
||||
// If we're in a wide/thin layout (dock), extend rays further
|
||||
if (size.X > size.Y * 2.5f)
|
||||
{
|
||||
outerLen = size.Y * 0.8f;
|
||||
innerLen = outerLen * 0.5f;
|
||||
}
|
||||
|
||||
// Expanding/contracting outer ring
|
||||
_outerRing = compositor.CreateSpriteVisual();
|
||||
var ringDiam = outerLen * 2.5f;
|
||||
_outerRing.Size = new Vector2(ringDiam, ringDiam);
|
||||
_outerRing.AnchorPoint = new Vector2(0.5f, 0.5f);
|
||||
_outerRing.Offset = new Vector3(cx, cy, 0);
|
||||
_outerRing.Opacity = 0.35f;
|
||||
|
||||
var ringBrush = compositor.CreateRadialGradientBrush();
|
||||
ringBrush.EllipseCenter = new Vector2(0.5f, 0.5f);
|
||||
ringBrush.EllipseRadius = new Vector2(0.5f, 0.5f);
|
||||
ringBrush.ColorStops.Add(compositor.CreateColorGradientStop(0f, Color.FromArgb(0, OuterColor.R, OuterColor.G, OuterColor.B)));
|
||||
ringBrush.ColorStops.Add(compositor.CreateColorGradientStop(0.6f, Color.FromArgb(0, OuterColor.R, OuterColor.G, OuterColor.B)));
|
||||
ringBrush.ColorStops.Add(compositor.CreateColorGradientStop(0.8f, Color.FromArgb(100, OuterColor.R, OuterColor.G, OuterColor.B)));
|
||||
ringBrush.ColorStops.Add(compositor.CreateColorGradientStop(0.92f, Color.FromArgb(50, OuterColor.R, OuterColor.G, OuterColor.B)));
|
||||
ringBrush.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, OuterColor.R, OuterColor.G, OuterColor.B)));
|
||||
_outerRing.Brush = ringBrush;
|
||||
rootVisual.Children.InsertAtTop(_outerRing);
|
||||
|
||||
// Outer rays — long, thin, rotate clockwise
|
||||
_outerRays = new SpriteVisual[OuterRayCount];
|
||||
for (var i = 0; i < OuterRayCount; i++)
|
||||
{
|
||||
var ray = CreateRay(compositor, cx, cy, 5f, outerLen, i, OuterRayCount, OuterColor, AccentColor);
|
||||
_outerRays[i] = ray;
|
||||
rootVisual.Children.InsertAtTop(ray);
|
||||
}
|
||||
|
||||
// Inner rays — shorter, slightly wider, rotate counter-clockwise
|
||||
_innerRays = new SpriteVisual[InnerRayCount];
|
||||
for (var i = 0; i < InnerRayCount; i++)
|
||||
{
|
||||
var ray = CreateRay(compositor, cx, cy, 6f, innerLen, i, InnerRayCount, InnerColor, OuterColor);
|
||||
_innerRays[i] = ray;
|
||||
rootVisual.Children.InsertAtTop(ray);
|
||||
}
|
||||
|
||||
// Bright center orb — large and prominent
|
||||
_centerOrb = compositor.CreateSpriteVisual();
|
||||
var orbSize = Math.Min(size.X, size.Y) * 0.4f;
|
||||
_centerOrb.Size = new Vector2(orbSize, orbSize);
|
||||
_centerOrb.AnchorPoint = new Vector2(0.5f, 0.5f);
|
||||
_centerOrb.Offset = new Vector3(cx, cy, 0);
|
||||
_centerOrb.Opacity = 0.8f;
|
||||
|
||||
var orbBrush = compositor.CreateRadialGradientBrush();
|
||||
orbBrush.EllipseCenter = new Vector2(0.5f, 0.5f);
|
||||
orbBrush.EllipseRadius = new Vector2(0.5f, 0.5f);
|
||||
orbBrush.ColorStops.Add(compositor.CreateColorGradientStop(0f, Color.FromArgb(200, 255, 255, 255)));
|
||||
orbBrush.ColorStops.Add(compositor.CreateColorGradientStop(0.15f, Color.FromArgb(160, 220, 200, 255)));
|
||||
orbBrush.ColorStops.Add(compositor.CreateColorGradientStop(0.4f, Color.FromArgb(80, 160, 120, 255)));
|
||||
orbBrush.ColorStops.Add(compositor.CreateColorGradientStop(0.7f, Color.FromArgb(30, 100, 80, 220)));
|
||||
orbBrush.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, 60, 40, 180)));
|
||||
_centerOrb.Brush = orbBrush;
|
||||
rootVisual.Children.InsertAtTop(_centerOrb);
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (_compositor == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var smooth = _compositor.CreateCubicBezierEasingFunction(new Vector2(0.4f, 0f), new Vector2(0.6f, 1f));
|
||||
var linear = _compositor.CreateLinearEasingFunction();
|
||||
|
||||
// Outer rays: slow clockwise, staggered pulse
|
||||
AnimateRayGroup(_outerRays, OuterRayCount, 30.0, true, smooth, linear);
|
||||
|
||||
// Inner rays: faster counter-clockwise
|
||||
AnimateRayGroup(_innerRays, InnerRayCount, 18.0, false, smooth, linear);
|
||||
|
||||
// Outer ring breathe
|
||||
if (_outerRing != null)
|
||||
{
|
||||
var ringScale = _compositor.CreateVector2KeyFrameAnimation();
|
||||
ringScale.Duration = TimeSpan.FromSeconds(6);
|
||||
ringScale.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
ringScale.InsertKeyFrame(0f, new Vector2(0.85f, 0.85f), smooth);
|
||||
ringScale.InsertKeyFrame(0.5f, new Vector2(1.15f, 1.15f), smooth);
|
||||
ringScale.InsertKeyFrame(1f, new Vector2(0.85f, 0.85f), smooth);
|
||||
_outerRing.StartAnimation("Scale.XY", ringScale);
|
||||
|
||||
var ringOp = _compositor.CreateScalarKeyFrameAnimation();
|
||||
ringOp.Duration = TimeSpan.FromSeconds(6);
|
||||
ringOp.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
ringOp.InsertKeyFrame(0f, 0.15f, smooth);
|
||||
ringOp.InsertKeyFrame(0.5f, 0.35f, smooth);
|
||||
ringOp.InsertKeyFrame(1f, 0.15f, smooth);
|
||||
_outerRing.StartAnimation("Opacity", ringOp);
|
||||
}
|
||||
|
||||
// Center orb pulse
|
||||
if (_centerOrb != null)
|
||||
{
|
||||
var orbScale = _compositor.CreateVector2KeyFrameAnimation();
|
||||
orbScale.Duration = TimeSpan.FromSeconds(2.5);
|
||||
orbScale.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
orbScale.InsertKeyFrame(0f, new Vector2(0.7f, 0.7f), smooth);
|
||||
orbScale.InsertKeyFrame(0.5f, new Vector2(1.4f, 1.4f), smooth);
|
||||
orbScale.InsertKeyFrame(1f, new Vector2(0.7f, 0.7f), smooth);
|
||||
_centerOrb.StartAnimation("Scale.XY", orbScale);
|
||||
|
||||
var orbOp = _compositor.CreateScalarKeyFrameAnimation();
|
||||
orbOp.Duration = TimeSpan.FromSeconds(2.5);
|
||||
orbOp.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
orbOp.InsertKeyFrame(0f, 0.5f, smooth);
|
||||
orbOp.InsertKeyFrame(0.5f, 1f, smooth);
|
||||
orbOp.InsertKeyFrame(1f, 0.5f, smooth);
|
||||
_centerOrb.StartAnimation("Opacity", orbOp);
|
||||
}
|
||||
|
||||
// Start audio reactivity
|
||||
var totalRays = OuterRayCount + InnerRayCount;
|
||||
_levelBuffer = new float[totalRays];
|
||||
_audioService = new AudioLoopbackService(totalRays);
|
||||
_audioAvailable = _audioService.Start();
|
||||
|
||||
if (!_audioAvailable)
|
||||
{
|
||||
_audioService.Dispose();
|
||||
_audioService = null;
|
||||
}
|
||||
|
||||
var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
if (dispatcherQueue != null)
|
||||
{
|
||||
_updateTimer = dispatcherQueue.CreateTimer();
|
||||
_updateTimer.Interval = TimeSpan.FromMilliseconds(33);
|
||||
_updateTimer.Tick += OnAudioTick;
|
||||
_updateTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_updateTimer?.Stop();
|
||||
if (_updateTimer != null)
|
||||
{
|
||||
_updateTimer.Tick -= OnAudioTick;
|
||||
}
|
||||
|
||||
_updateTimer = null;
|
||||
_audioService?.Dispose();
|
||||
_audioService = null;
|
||||
|
||||
StopRays(_outerRays);
|
||||
StopRays(_innerRays);
|
||||
_outerRing?.StopAnimation("Scale.XY");
|
||||
_outerRing?.StopAnimation("Opacity");
|
||||
_centerOrb?.StopAnimation("Scale.XY");
|
||||
_centerOrb?.StopAnimation("Opacity");
|
||||
}
|
||||
|
||||
public void Resize(Vector2 newSize)
|
||||
{
|
||||
_size = newSize;
|
||||
var cx = newSize.X * 0.5f;
|
||||
var cy = newSize.Y * 0.5f;
|
||||
|
||||
var span = Math.Min(newSize.X, newSize.Y);
|
||||
var outerLen = span * 0.45f;
|
||||
var innerLen = outerLen * 0.5f;
|
||||
|
||||
if (newSize.X > newSize.Y * 2.5f)
|
||||
{
|
||||
outerLen = newSize.Y * 0.8f;
|
||||
innerLen = outerLen * 0.5f;
|
||||
}
|
||||
|
||||
ResizeRays(_outerRays, cx, cy, outerLen);
|
||||
ResizeRays(_innerRays, cx, cy, innerLen);
|
||||
|
||||
if (_outerRing != null)
|
||||
{
|
||||
var ringDiam = outerLen * 2.5f;
|
||||
_outerRing.Size = new Vector2(ringDiam, ringDiam);
|
||||
_outerRing.Offset = new Vector3(cx, cy, 0);
|
||||
}
|
||||
|
||||
if (_centerOrb != null)
|
||||
{
|
||||
var orbSize = Math.Min(newSize.X, newSize.Y) * 0.4f;
|
||||
_centerOrb.Size = new Vector2(orbSize, orbSize);
|
||||
_centerOrb.Offset = new Vector3(cx, cy, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
DisposeArray(_outerRays);
|
||||
DisposeArray(_innerRays);
|
||||
_outerRing?.Dispose();
|
||||
_centerOrb?.Dispose();
|
||||
_outerRays = null;
|
||||
_innerRays = null;
|
||||
_outerRing = null;
|
||||
_centerOrb = null;
|
||||
}
|
||||
|
||||
private void OnAudioTick(DispatcherQueueTimer sender, object args)
|
||||
{
|
||||
if (_levelBuffer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_audioAvailable && _audioService != null)
|
||||
{
|
||||
_audioService.GetBandLevels(_levelBuffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback pulse
|
||||
var time = (float)(Environment.TickCount64 / 1000.0);
|
||||
for (var i = 0; i < _levelBuffer.Length; i++)
|
||||
{
|
||||
_levelBuffer[i] = 0.3f + (0.15f * MathF.Sin((time * 2f) + (i * 0.3f)));
|
||||
}
|
||||
}
|
||||
|
||||
// Modulate outer rays — 0 to full blast
|
||||
if (_outerRays != null)
|
||||
{
|
||||
for (var i = 0; i < _outerRays.Length && i < _levelBuffer.Length; i++)
|
||||
{
|
||||
var level = _levelBuffer[i];
|
||||
_outerRays[i].Opacity = level;
|
||||
_outerRays[i].Scale = new Vector3(
|
||||
0.2f + (4.0f * level),
|
||||
level * 3.0f,
|
||||
1f);
|
||||
}
|
||||
}
|
||||
|
||||
// Modulate inner rays — even more explosive
|
||||
if (_innerRays != null)
|
||||
{
|
||||
for (var i = 0; i < _innerRays.Length; i++)
|
||||
{
|
||||
var bandIdx = OuterRayCount + i;
|
||||
var level = (bandIdx < _levelBuffer.Length) ? _levelBuffer[bandIdx] : 0.3f;
|
||||
_innerRays[i].Opacity = level;
|
||||
_innerRays[i].Scale = new Vector3(
|
||||
0.15f + (5.0f * level),
|
||||
level * 4.0f,
|
||||
1f);
|
||||
}
|
||||
}
|
||||
|
||||
// Center orb reacts hard to bass — BIG pulsing flash
|
||||
if (_centerOrb != null && _levelBuffer.Length > 4)
|
||||
{
|
||||
var bass = (_levelBuffer[0] + _levelBuffer[1] + _levelBuffer[2] + _levelBuffer[3]) * 0.25f;
|
||||
var orbScale = 0.2f + (3.5f * bass);
|
||||
_centerOrb.Scale = new Vector3(orbScale, orbScale, 1f);
|
||||
_centerOrb.Opacity = 0.3f + (0.7f * bass);
|
||||
}
|
||||
|
||||
// Outer ring reacts to overall energy
|
||||
if (_outerRing != null)
|
||||
{
|
||||
var overall = 0f;
|
||||
for (var i = 0; i < _levelBuffer.Length; i++)
|
||||
{
|
||||
overall += _levelBuffer[i];
|
||||
}
|
||||
|
||||
overall /= _levelBuffer.Length;
|
||||
_outerRing.Opacity = 0.1f + (0.6f * overall);
|
||||
var ringScale = 0.85f + (0.4f * overall);
|
||||
_outerRing.Scale = new Vector3(ringScale, ringScale, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
private static SpriteVisual CreateRay(
|
||||
Compositor compositor,
|
||||
float cx,
|
||||
float cy,
|
||||
float width,
|
||||
float length,
|
||||
int index,
|
||||
int total,
|
||||
Color primary,
|
||||
Color secondary)
|
||||
{
|
||||
var ray = compositor.CreateSpriteVisual();
|
||||
ray.Size = new Vector2(width, length);
|
||||
ray.AnchorPoint = new Vector2(0.5f, 1f);
|
||||
ray.Offset = new Vector3(cx, cy, 0);
|
||||
ray.RotationAngleInDegrees = index * (360f / total);
|
||||
ray.Opacity = 0.7f;
|
||||
|
||||
var brush = compositor.CreateLinearGradientBrush();
|
||||
brush.StartPoint = new Vector2(0.5f, 1f);
|
||||
brush.EndPoint = new Vector2(0.5f, 0f);
|
||||
|
||||
// Soft, diffuse glow — rays bleed into each other
|
||||
var color = (index % 2 == 0) ? primary : secondary;
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0f, Color.FromArgb(80, 255, 255, 255)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0.05f, color));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0.2f, Color.FromArgb((byte)(color.A * 0.7), color.R, color.G, color.B)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0.5f, Color.FromArgb((byte)(color.A * 0.3), color.R, color.G, color.B)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, color.R, color.G, color.B)));
|
||||
|
||||
ray.Brush = brush;
|
||||
return ray;
|
||||
}
|
||||
|
||||
private void AnimateRayGroup(
|
||||
SpriteVisual[]? rays,
|
||||
int count,
|
||||
double rotPeriod,
|
||||
bool clockwise,
|
||||
CompositionEasingFunction smooth,
|
||||
CompositionEasingFunction linear)
|
||||
{
|
||||
if (rays == null || _compositor == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < rays.Length; i++)
|
||||
{
|
||||
var baseDeg = i * (360f / count);
|
||||
var dir = clockwise ? 360f : -360f;
|
||||
|
||||
// Rotation — all rays in group share period but start at different angles
|
||||
var rotAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
rotAnim.Duration = TimeSpan.FromSeconds(rotPeriod);
|
||||
rotAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
rotAnim.InsertKeyFrame(0f, baseDeg, linear);
|
||||
rotAnim.InsertKeyFrame(1f, baseDeg + dir, linear);
|
||||
rays[i].StartAnimation("RotationAngleInDegrees", rotAnim);
|
||||
|
||||
// Length pulse — staggered so rays "breathe" in a wave pattern
|
||||
var pulsePeriod = 3.0 + (i * 0.4);
|
||||
var scaleAnim = _compositor.CreateVector2KeyFrameAnimation();
|
||||
scaleAnim.Duration = TimeSpan.FromSeconds(pulsePeriod);
|
||||
scaleAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
scaleAnim.InsertKeyFrame(0f, new Vector2(1f, 0.7f), smooth);
|
||||
scaleAnim.InsertKeyFrame(0.5f, new Vector2(1.3f, 1.2f), smooth);
|
||||
scaleAnim.InsertKeyFrame(1f, new Vector2(1f, 0.7f), smooth);
|
||||
rays[i].StartAnimation("Scale.XY", scaleAnim);
|
||||
|
||||
// Opacity wave
|
||||
var opAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
opAnim.Duration = TimeSpan.FromSeconds(pulsePeriod * 0.8);
|
||||
opAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
opAnim.InsertKeyFrame(0f, 0.25f, smooth);
|
||||
opAnim.InsertKeyFrame(0.5f, 0.7f, smooth);
|
||||
opAnim.InsertKeyFrame(1f, 0.25f, smooth);
|
||||
rays[i].StartAnimation("Opacity", opAnim);
|
||||
}
|
||||
}
|
||||
|
||||
private static void StopRays(SpriteVisual[]? rays)
|
||||
{
|
||||
if (rays == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var r in rays)
|
||||
{
|
||||
r.StopAnimation("RotationAngleInDegrees");
|
||||
r.StopAnimation("Scale.XY");
|
||||
r.StopAnimation("Opacity");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResizeRays(SpriteVisual[]? rays, float cx, float cy, float length)
|
||||
{
|
||||
if (rays == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var r in rays)
|
||||
{
|
||||
r.Size = new Vector2(r.Size.X, length);
|
||||
r.Offset = new Vector3(cx, cy, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DisposeArray(SpriteVisual[]? arr)
|
||||
{
|
||||
if (arr == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var v in arr)
|
||||
{
|
||||
v.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Numerics;
|
||||
using Microsoft.CmdPal.UI.Controls.AmbientEffects.Audio;
|
||||
using Microsoft.Graphics.Canvas.Effects;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls.AmbientEffects.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// WMP "Ambience" inspired visualization — dreamy flowing organic shapes that
|
||||
/// morph and blend with smooth color transitions. Multiple soft radial blobs
|
||||
/// drift in slow, fluid orbits. Heavy gaussian blur creates a liquid,
|
||||
/// hallucinogenic look. Audio drives blob scale, opacity, and movement speed.
|
||||
/// Colors cycle slowly through a warm-to-cool palette.
|
||||
/// </summary>
|
||||
internal sealed class AmbienceEffect : IBackgroundEffect
|
||||
{
|
||||
private const int BlobCount = 7;
|
||||
private const float BlurAmount = 100f;
|
||||
private const float HueSpeed = 0.003f;
|
||||
|
||||
private Compositor? _compositor;
|
||||
private SpriteVisual? _blurHost;
|
||||
private ContainerVisual? _blobContainer;
|
||||
private SpriteVisual[]? _blobs;
|
||||
private CompositionRadialGradientBrush[]? _blobBrushes;
|
||||
private Vector2 _size;
|
||||
|
||||
private AudioLoopbackService? _audioService;
|
||||
private DispatcherQueueTimer? _updateTimer;
|
||||
private float[]? _levelBuffer;
|
||||
private bool _audioAvailable;
|
||||
private float _hueOffset;
|
||||
private float _time;
|
||||
|
||||
public void Initialize(Compositor compositor, ContainerVisual rootVisual, Vector2 size)
|
||||
{
|
||||
_compositor = compositor;
|
||||
_size = size;
|
||||
|
||||
_blobContainer = compositor.CreateContainerVisual();
|
||||
_blobContainer.Size = size;
|
||||
|
||||
_blobs = new SpriteVisual[BlobCount];
|
||||
_blobBrushes = new CompositionRadialGradientBrush[BlobCount];
|
||||
|
||||
for (var i = 0; i < BlobCount; i++)
|
||||
{
|
||||
var blob = compositor.CreateSpriteVisual();
|
||||
var scale = 0.5f + (0.5f * ((i % 3) / 2f));
|
||||
var diameter = Math.Max(size.X, size.Y) * 0.6f * scale;
|
||||
blob.Size = new Vector2(diameter, diameter);
|
||||
blob.AnchorPoint = new Vector2(0.5f, 0.5f);
|
||||
|
||||
// Spread in a circular pattern
|
||||
var angle = (i * MathF.PI * 2f) / BlobCount;
|
||||
blob.Offset = new Vector3(
|
||||
(size.X * 0.5f) + (size.X * 0.2f * MathF.Cos(angle)),
|
||||
(size.Y * 0.5f) + (size.Y * 0.2f * MathF.Sin(angle)),
|
||||
0);
|
||||
blob.Opacity = 0.8f;
|
||||
|
||||
var brush = compositor.CreateRadialGradientBrush();
|
||||
brush.EllipseCenter = new Vector2(0.5f, 0.5f);
|
||||
brush.EllipseRadius = new Vector2(0.5f, 0.5f);
|
||||
|
||||
var color = HueToColor((float)i / BlobCount);
|
||||
SetBlobColor(compositor, brush, color);
|
||||
|
||||
blob.Brush = brush;
|
||||
_blobs[i] = blob;
|
||||
_blobBrushes[i] = brush;
|
||||
_blobContainer.Children.InsertAtTop(blob);
|
||||
}
|
||||
|
||||
// Heavy blur for the dreamy liquid look
|
||||
var blurEffect = new GaussianBlurEffect
|
||||
{
|
||||
Name = "Blur",
|
||||
BlurAmount = BlurAmount,
|
||||
BorderMode = EffectBorderMode.Soft,
|
||||
Source = new CompositionEffectSourceParameter("Source"),
|
||||
};
|
||||
|
||||
var factory = compositor.CreateEffectFactory(blurEffect);
|
||||
var effectBrush = factory.CreateBrush();
|
||||
|
||||
var surface = compositor.CreateVisualSurface();
|
||||
surface.SourceVisual = _blobContainer;
|
||||
surface.SourceSize = size;
|
||||
|
||||
var surfaceBrush = compositor.CreateSurfaceBrush(surface);
|
||||
surfaceBrush.Stretch = CompositionStretch.Fill;
|
||||
effectBrush.SetSourceParameter("Source", surfaceBrush);
|
||||
|
||||
_blurHost = compositor.CreateSpriteVisual();
|
||||
_blurHost.Size = size;
|
||||
_blurHost.Brush = effectBrush;
|
||||
_blurHost.Opacity = 0.8f;
|
||||
|
||||
rootVisual.Children.InsertAtTop(_blurHost);
|
||||
|
||||
_levelBuffer = new float[BlobCount];
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_audioService = new AudioLoopbackService(BlobCount);
|
||||
_audioAvailable = _audioService.Start();
|
||||
|
||||
if (!_audioAvailable)
|
||||
{
|
||||
_audioService.Dispose();
|
||||
_audioService = null;
|
||||
}
|
||||
|
||||
var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
if (dispatcherQueue != null)
|
||||
{
|
||||
_updateTimer = dispatcherQueue.CreateTimer();
|
||||
_updateTimer.Interval = TimeSpan.FromMilliseconds(33);
|
||||
_updateTimer.Tick += OnTick;
|
||||
_updateTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_updateTimer?.Stop();
|
||||
if (_updateTimer != null)
|
||||
{
|
||||
_updateTimer.Tick -= OnTick;
|
||||
}
|
||||
|
||||
_updateTimer = null;
|
||||
_audioService?.Dispose();
|
||||
_audioService = null;
|
||||
}
|
||||
|
||||
public void Resize(Vector2 newSize)
|
||||
{
|
||||
_size = newSize;
|
||||
if (_blobContainer != null)
|
||||
{
|
||||
_blobContainer.Size = newSize;
|
||||
}
|
||||
|
||||
if (_blurHost != null)
|
||||
{
|
||||
_blurHost.Size = newSize;
|
||||
if (_blurHost.Brush is CompositionEffectBrush effectBrush && _compositor != null && _blobContainer != null)
|
||||
{
|
||||
var surface = _compositor.CreateVisualSurface();
|
||||
surface.SourceVisual = _blobContainer;
|
||||
surface.SourceSize = newSize;
|
||||
var surfaceBrush = _compositor.CreateSurfaceBrush(surface);
|
||||
surfaceBrush.Stretch = CompositionStretch.Fill;
|
||||
effectBrush.SetSourceParameter("Source", surfaceBrush);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
_blurHost?.Dispose();
|
||||
if (_blobs != null)
|
||||
{
|
||||
foreach (var b in _blobs)
|
||||
{
|
||||
b.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_blobContainer?.Dispose();
|
||||
_blurHost = null;
|
||||
_blobs = null;
|
||||
_blobBrushes = null;
|
||||
_blobContainer = null;
|
||||
}
|
||||
|
||||
private void OnTick(DispatcherQueueTimer sender, object args)
|
||||
{
|
||||
if (_blobs == null || _blobBrushes == null || _levelBuffer == null || _compositor == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_time += 0.033f;
|
||||
|
||||
if (_audioAvailable && _audioService != null)
|
||||
{
|
||||
_audioService.GetBandLevels(_levelBuffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < _levelBuffer.Length; i++)
|
||||
{
|
||||
_levelBuffer[i] = 0.3f + (0.15f * MathF.Sin((_time * 1.5f) + (i * 0.8f)));
|
||||
}
|
||||
}
|
||||
|
||||
// Slow hue drift
|
||||
_hueOffset += HueSpeed;
|
||||
if (_hueOffset >= 1f)
|
||||
{
|
||||
_hueOffset -= 1f;
|
||||
}
|
||||
|
||||
var cx = _size.X * 0.5f;
|
||||
var cy = _size.Y * 0.5f;
|
||||
|
||||
for (var i = 0; i < BlobCount; i++)
|
||||
{
|
||||
var level = _levelBuffer[i];
|
||||
var blob = _blobs[i];
|
||||
|
||||
// Fluid orbital motion — speed increases with audio level
|
||||
var speedMultiplier = 0.5f + (1.5f * level);
|
||||
var baseAngle = (i * MathF.PI * 2f) / BlobCount;
|
||||
var xPeriod = 8f + (i * 2.3f);
|
||||
var yPeriod = xPeriod * 1.4f;
|
||||
var orbitRadius = Math.Min(_size.X, _size.Y) * (0.15f + (0.2f * level));
|
||||
|
||||
var x = cx + (orbitRadius * MathF.Cos((_time * speedMultiplier / xPeriod * MathF.PI * 2f) + baseAngle));
|
||||
var y = cy + (orbitRadius * MathF.Sin((_time * speedMultiplier / yPeriod * MathF.PI * 2f) + baseAngle));
|
||||
blob.Offset = new Vector3(x, y, 0);
|
||||
|
||||
// Scale breathes with audio — blobs grow on beats
|
||||
var scale = 0.6f + (1.5f * level);
|
||||
blob.Scale = new Vector3(scale, scale, 1f);
|
||||
blob.Opacity = 0.4f + (0.6f * level);
|
||||
|
||||
// Update color with slow hue rotation
|
||||
var hue = (((float)i / BlobCount) + _hueOffset) % 1f;
|
||||
var color = HueToColor(hue);
|
||||
SetBlobColor(_compositor, _blobBrushes[i], color);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetBlobColor(Compositor compositor, CompositionRadialGradientBrush brush, Color c)
|
||||
{
|
||||
brush.ColorStops.Clear();
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0f, Color.FromArgb(255, c.R, c.G, c.B)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0.35f, Color.FromArgb(180, c.R, c.G, c.B)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0.65f, Color.FromArgb(60, c.R, c.G, c.B)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, c.R, c.G, c.B)));
|
||||
}
|
||||
|
||||
private static Color HueToColor(float hue)
|
||||
{
|
||||
hue = ((hue % 1f) + 1f) % 1f;
|
||||
var h = hue * 6f;
|
||||
var sector = (int)h;
|
||||
var frac = h - sector;
|
||||
|
||||
byte full = 255;
|
||||
var rising = (byte)(255 * frac);
|
||||
var falling = (byte)(255 * (1f - frac));
|
||||
|
||||
return sector switch
|
||||
{
|
||||
0 => Color.FromArgb(full, full, rising, 0),
|
||||
1 => Color.FromArgb(full, falling, full, 0),
|
||||
2 => Color.FromArgb(full, 0, full, rising),
|
||||
3 => Color.FromArgb(full, 0, falling, full),
|
||||
4 => Color.FromArgb(full, rising, 0, full),
|
||||
_ => Color.FromArgb(full, full, 0, falling),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Numerics;
|
||||
using Microsoft.CmdPal.UI.Controls.AmbientEffects.Audio;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls.AmbientEffects.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// Audio-reactive full-width glow along the top edge. A single wide glow
|
||||
/// band spans the entire width and pulses downward with overall audio energy.
|
||||
/// The color continuously morphs through the spectrum over time.
|
||||
/// Multiple overlapping layers create a rich, blended look.
|
||||
/// </summary>
|
||||
internal sealed class AudioGlowEffect : IBackgroundEffect
|
||||
{
|
||||
private const int LayerCount = 5;
|
||||
private const float GlowHeight = 500f;
|
||||
private const float HueRotationSpeed = 0.0033f;
|
||||
|
||||
private Compositor? _compositor;
|
||||
private SpriteVisual[]? _layers;
|
||||
private CompositionRadialGradientBrush[]? _layerBrushes;
|
||||
private SpriteVisual? _fullWidthPulse;
|
||||
private CompositionLinearGradientBrush? _pulseBrush;
|
||||
private Vector2 _size;
|
||||
|
||||
private AudioLoopbackService? _audioService;
|
||||
private DispatcherQueueTimer? _updateTimer;
|
||||
private float[]? _levelBuffer;
|
||||
private bool _audioAvailable;
|
||||
private float _hueOffset;
|
||||
|
||||
public void Initialize(Compositor compositor, ContainerVisual rootVisual, Vector2 size)
|
||||
{
|
||||
_compositor = compositor;
|
||||
_size = size;
|
||||
|
||||
var glowH = Math.Min(GlowHeight, size.Y);
|
||||
|
||||
// Full-width pulse layer — reacts to overall energy, spans entire dock
|
||||
_fullWidthPulse = compositor.CreateSpriteVisual();
|
||||
_fullWidthPulse.Size = new Vector2(size.X, glowH);
|
||||
_fullWidthPulse.Offset = new Vector3(0, 0, 0);
|
||||
_fullWidthPulse.Opacity = 0f;
|
||||
|
||||
_pulseBrush = compositor.CreateLinearGradientBrush();
|
||||
_pulseBrush.StartPoint = new Vector2(0.5f, 0f);
|
||||
_pulseBrush.EndPoint = new Vector2(0.5f, 1f);
|
||||
UpdatePulseBrushColors(compositor, _pulseBrush, 0f);
|
||||
_fullWidthPulse.Brush = _pulseBrush;
|
||||
rootVisual.Children.InsertAtTop(_fullWidthPulse);
|
||||
|
||||
// Overlapping glow layers — each responds to a frequency range
|
||||
// They span the full width but are offset horizontally so the
|
||||
// color bleeds across the entire band
|
||||
_layers = new SpriteVisual[LayerCount];
|
||||
_layerBrushes = new CompositionRadialGradientBrush[LayerCount];
|
||||
|
||||
for (var i = 0; i < LayerCount; i++)
|
||||
{
|
||||
var layer = compositor.CreateSpriteVisual();
|
||||
layer.Size = new Vector2(size.X, glowH);
|
||||
|
||||
// Spread layers across the width with heavy overlap
|
||||
var xCenter = size.X * ((i + 0.5f) / LayerCount);
|
||||
layer.Offset = new Vector3(0, 0, 0);
|
||||
layer.Opacity = 0f;
|
||||
|
||||
var brush = compositor.CreateRadialGradientBrush();
|
||||
brush.EllipseCenter = new Vector2(0.5f, 0f);
|
||||
brush.EllipseRadius = new Vector2(0.5f, 0.9f);
|
||||
|
||||
var color = HueToColor((float)i / LayerCount);
|
||||
SetGlowBrushColor(compositor, brush, color);
|
||||
|
||||
layer.Brush = brush;
|
||||
_layers[i] = layer;
|
||||
_layerBrushes[i] = brush;
|
||||
rootVisual.Children.InsertAtTop(layer);
|
||||
}
|
||||
|
||||
_levelBuffer = new float[LayerCount + 1];
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_audioService = new AudioLoopbackService(_levelBuffer?.Length ?? LayerCount + 1);
|
||||
_audioAvailable = _audioService.Start();
|
||||
|
||||
if (!_audioAvailable)
|
||||
{
|
||||
_audioService.Dispose();
|
||||
_audioService = null;
|
||||
}
|
||||
|
||||
var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
if (dispatcherQueue != null)
|
||||
{
|
||||
_updateTimer = dispatcherQueue.CreateTimer();
|
||||
_updateTimer.Interval = TimeSpan.FromMilliseconds(33);
|
||||
_updateTimer.Tick += OnAudioTick;
|
||||
_updateTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_updateTimer?.Stop();
|
||||
if (_updateTimer != null)
|
||||
{
|
||||
_updateTimer.Tick -= OnAudioTick;
|
||||
}
|
||||
|
||||
_updateTimer = null;
|
||||
_audioService?.Dispose();
|
||||
_audioService = null;
|
||||
}
|
||||
|
||||
public void Resize(Vector2 newSize)
|
||||
{
|
||||
_size = newSize;
|
||||
var glowH = Math.Min(GlowHeight, newSize.Y);
|
||||
|
||||
if (_fullWidthPulse != null)
|
||||
{
|
||||
_fullWidthPulse.Size = new Vector2(newSize.X, glowH);
|
||||
}
|
||||
|
||||
if (_layers != null)
|
||||
{
|
||||
for (var i = 0; i < _layers.Length; i++)
|
||||
{
|
||||
_layers[i].Size = new Vector2(newSize.X, glowH);
|
||||
_layers[i].Offset = new Vector3(0, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
_fullWidthPulse?.Dispose();
|
||||
if (_layers != null)
|
||||
{
|
||||
foreach (var l in _layers)
|
||||
{
|
||||
l.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_fullWidthPulse = null;
|
||||
_layers = null;
|
||||
_layerBrushes = null;
|
||||
}
|
||||
|
||||
private void OnAudioTick(DispatcherQueueTimer sender, object args)
|
||||
{
|
||||
if (_levelBuffer == null || _layers == null || _layerBrushes == null || _compositor == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_audioAvailable && _audioService != null)
|
||||
{
|
||||
_audioService.GetBandLevels(_levelBuffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
var time = (float)(Environment.TickCount64 / 1000.0);
|
||||
for (var i = 0; i < _levelBuffer.Length; i++)
|
||||
{
|
||||
_levelBuffer[i] = 0.15f + (0.1f * MathF.Sin((time * 2f) + (i * 0.5f)));
|
||||
}
|
||||
}
|
||||
|
||||
// Advance hue — continuous color morphing
|
||||
_hueOffset += HueRotationSpeed;
|
||||
if (_hueOffset >= 1f)
|
||||
{
|
||||
_hueOffset -= 1f;
|
||||
}
|
||||
|
||||
// Compute overall energy for the full-width pulse
|
||||
var overall = 0f;
|
||||
for (var i = 0; i < _levelBuffer.Length; i++)
|
||||
{
|
||||
overall += _levelBuffer[i];
|
||||
}
|
||||
|
||||
overall /= _levelBuffer.Length;
|
||||
|
||||
// Full-width pulse reacts to overall energy — the whole dock glows
|
||||
if (_fullWidthPulse != null && _pulseBrush != null)
|
||||
{
|
||||
_fullWidthPulse.Opacity = 0.1f + (0.8f * overall);
|
||||
var scaleY = 0.3f + (2.0f * overall);
|
||||
_fullWidthPulse.Scale = new Vector3(1f, scaleY, 1f);
|
||||
UpdatePulseBrushColors(_compositor, _pulseBrush, _hueOffset);
|
||||
}
|
||||
|
||||
// Individual layers add frequency-specific color highlights on top
|
||||
for (var i = 0; i < LayerCount && i < _layers.Length; i++)
|
||||
{
|
||||
var level = (i < _levelBuffer.Length) ? _levelBuffer[i] : overall;
|
||||
|
||||
_layers[i].Opacity = 0.05f + (0.9f * level);
|
||||
var scaleY = 0.2f + (2.5f * level);
|
||||
_layers[i].Scale = new Vector3(1f, scaleY, 1f);
|
||||
|
||||
// Update color with hue rotation
|
||||
var hue = (((float)i / LayerCount) + _hueOffset) % 1f;
|
||||
var color = HueToColor(hue);
|
||||
SetGlowBrushColor(_compositor, _layerBrushes[i], color);
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdatePulseBrushColors(Compositor compositor, CompositionLinearGradientBrush brush, float hueOffset)
|
||||
{
|
||||
var c = HueToColor(hueOffset);
|
||||
brush.ColorStops.Clear();
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0f, Color.FromArgb(200, c.R, c.G, c.B)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0.2f, Color.FromArgb(120, c.R, c.G, c.B)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0.5f, Color.FromArgb(40, c.R, c.G, c.B)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, c.R, c.G, c.B)));
|
||||
}
|
||||
|
||||
private static void SetGlowBrushColor(Compositor compositor, CompositionRadialGradientBrush brush, Color color)
|
||||
{
|
||||
brush.ColorStops.Clear();
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0f, Color.FromArgb(255, color.R, color.G, color.B)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0.15f, Color.FromArgb(200, color.R, color.G, color.B)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0.4f, Color.FromArgb(80, color.R, color.G, color.B)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, color.R, color.G, color.B)));
|
||||
}
|
||||
|
||||
private static Color HueToColor(float hue)
|
||||
{
|
||||
hue = ((hue % 1f) + 1f) % 1f;
|
||||
var h = hue * 6f;
|
||||
var sector = (int)h;
|
||||
var frac = h - sector;
|
||||
|
||||
byte full = 255;
|
||||
var rising = (byte)(255 * frac);
|
||||
var falling = (byte)(255 * (1f - frac));
|
||||
|
||||
return sector switch
|
||||
{
|
||||
0 => Color.FromArgb(full, full, rising, 0),
|
||||
1 => Color.FromArgb(full, falling, full, 0),
|
||||
2 => Color.FromArgb(full, 0, full, rising),
|
||||
3 => Color.FromArgb(full, 0, falling, full),
|
||||
4 => Color.FromArgb(full, rising, 0, full),
|
||||
_ => Color.FromArgb(full, full, 0, falling),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Numerics;
|
||||
using Microsoft.UI.Composition;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls.AmbientEffects.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// Aurora / Northern Lights effect: multiple overlapping vertical gradient
|
||||
/// bands slowly drift horizontally and shift colors, creating a flowing
|
||||
/// curtain of light reminiscent of the aurora borealis.
|
||||
/// </summary>
|
||||
internal sealed class AuroraEffect : IBackgroundEffect
|
||||
{
|
||||
private const int LayerCount = 5;
|
||||
|
||||
private static readonly Color[][] LayerPalettes =
|
||||
[
|
||||
[
|
||||
Color.FromArgb(120, 0, 255, 140),
|
||||
Color.FromArgb(80, 0, 200, 180),
|
||||
Color.FromArgb(0, 0, 140, 160),
|
||||
],
|
||||
[
|
||||
Color.FromArgb(100, 0, 200, 220),
|
||||
Color.FromArgb(70, 20, 120, 255),
|
||||
Color.FromArgb(0, 10, 60, 200),
|
||||
],
|
||||
[
|
||||
Color.FromArgb(90, 140, 50, 240),
|
||||
Color.FromArgb(60, 200, 80, 180),
|
||||
Color.FromArgb(0, 160, 30, 120),
|
||||
],
|
||||
[
|
||||
Color.FromArgb(80, 0, 240, 200),
|
||||
Color.FromArgb(50, 0, 180, 240),
|
||||
Color.FromArgb(0, 0, 100, 180),
|
||||
],
|
||||
[
|
||||
Color.FromArgb(70, 100, 0, 255),
|
||||
Color.FromArgb(40, 180, 40, 200),
|
||||
Color.FromArgb(0, 120, 20, 140),
|
||||
],
|
||||
];
|
||||
|
||||
private Compositor? _compositor;
|
||||
private ContainerVisual? _root;
|
||||
private SpriteVisual[]? _layers;
|
||||
private Vector2 _size;
|
||||
|
||||
public void Initialize(Compositor compositor, ContainerVisual rootVisual, Vector2 size)
|
||||
{
|
||||
_compositor = compositor;
|
||||
_root = rootVisual;
|
||||
_size = size;
|
||||
|
||||
_layers = new SpriteVisual[LayerCount];
|
||||
|
||||
for (var i = 0; i < LayerCount; i++)
|
||||
{
|
||||
var layer = compositor.CreateSpriteVisual();
|
||||
|
||||
// Each layer is wider than the viewport for seamless scrolling
|
||||
layer.Size = new Vector2(size.X * 2.5f, size.Y);
|
||||
layer.Offset = new Vector3(-size.X * 0.4f * i, 0, 0);
|
||||
layer.Opacity = 0.45f;
|
||||
layer.RotationAngleInDegrees = -3f + (i * 1.5f);
|
||||
|
||||
var gradientBrush = compositor.CreateLinearGradientBrush();
|
||||
gradientBrush.StartPoint = new Vector2(0, 0);
|
||||
gradientBrush.EndPoint = new Vector2(0, 1);
|
||||
|
||||
var palette = LayerPalettes[i % LayerPalettes.Length];
|
||||
gradientBrush.ColorStops.Add(compositor.CreateColorGradientStop(0f, palette[0]));
|
||||
gradientBrush.ColorStops.Add(compositor.CreateColorGradientStop(0.5f, palette[1]));
|
||||
gradientBrush.ColorStops.Add(compositor.CreateColorGradientStop(1f, palette[2]));
|
||||
|
||||
layer.Brush = gradientBrush;
|
||||
_layers[i] = layer;
|
||||
rootVisual.Children.InsertAtTop(layer);
|
||||
}
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (_compositor == null || _layers == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < _layers.Length; i++)
|
||||
{
|
||||
AnimateLayer(_layers[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (_layers == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var layer in _layers)
|
||||
{
|
||||
layer.StopAnimation("Offset.X");
|
||||
layer.StopAnimation("Opacity");
|
||||
}
|
||||
}
|
||||
|
||||
public void Resize(Vector2 newSize)
|
||||
{
|
||||
_size = newSize;
|
||||
if (_layers == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < _layers.Length; i++)
|
||||
{
|
||||
_layers[i].Size = new Vector2(newSize.X * 2.5f, newSize.Y);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
if (_layers != null)
|
||||
{
|
||||
foreach (var layer in _layers)
|
||||
{
|
||||
layer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_layers = null;
|
||||
}
|
||||
|
||||
private void AnimateLayer(SpriteVisual layer, int index)
|
||||
{
|
||||
if (_compositor == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var period = 12.0 + (index * 5.0);
|
||||
var easing = _compositor.CreateCubicBezierEasingFunction(
|
||||
new Vector2(0.35f, 0f), new Vector2(0.65f, 1f));
|
||||
|
||||
// Horizontal drift: layer slides and undulates
|
||||
var xRange = _size.X * 0.6f;
|
||||
var startX = -xRange * 0.3f;
|
||||
var xAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
xAnim.Duration = TimeSpan.FromSeconds(period);
|
||||
xAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
xAnim.InsertKeyFrame(0f, startX - (xRange * 0.4f), easing);
|
||||
xAnim.InsertKeyFrame(0.33f, startX + (xRange * 0.3f), easing);
|
||||
xAnim.InsertKeyFrame(0.66f, startX - (xRange * 0.2f), easing);
|
||||
xAnim.InsertKeyFrame(1f, startX - (xRange * 0.4f), easing);
|
||||
|
||||
layer.StartAnimation("Offset.X", xAnim);
|
||||
|
||||
// Vertical shimmy — layers gently bob up and down
|
||||
var yAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
yAnim.Duration = TimeSpan.FromSeconds(period * 0.7);
|
||||
yAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
var yRange = _size.Y * 0.06f;
|
||||
yAnim.InsertKeyFrame(0f, -yRange, easing);
|
||||
yAnim.InsertKeyFrame(0.5f, yRange, easing);
|
||||
yAnim.InsertKeyFrame(1f, -yRange, easing);
|
||||
|
||||
layer.StartAnimation("Offset.Y", yAnim);
|
||||
|
||||
// Opacity shimmer
|
||||
var opacityAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
opacityAnim.Duration = TimeSpan.FromSeconds(period * 0.5);
|
||||
opacityAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
opacityAnim.InsertKeyFrame(0f, 0.3f, easing);
|
||||
opacityAnim.InsertKeyFrame(0.5f, 0.6f, easing);
|
||||
opacityAnim.InsertKeyFrame(1f, 0.3f, easing);
|
||||
|
||||
layer.StartAnimation("Opacity", opacityAnim);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Numerics;
|
||||
using Microsoft.UI.Composition;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls.AmbientEffects.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// WMP "Bars & Waves" visualization — classic VU-meter equalizer bars with
|
||||
/// green→yellow→red gradient, snappy rise, gravity-like fall, and peak hold
|
||||
/// indicators. Bottom-aligned with subtle transparency.
|
||||
/// </summary>
|
||||
internal sealed class BarsEffect : IBackgroundEffect
|
||||
{
|
||||
private const int BarCount = 48;
|
||||
private const float BarGap = 1.5f;
|
||||
private const float MaxBarHeightRatio = 0.7f;
|
||||
private const float BarOpacity = 0.55f;
|
||||
|
||||
private Compositor? _compositor;
|
||||
private SpriteVisual[]? _bars;
|
||||
private SpriteVisual[]? _peaks;
|
||||
private Vector2 _size;
|
||||
|
||||
public void Initialize(Compositor compositor, ContainerVisual rootVisual, Vector2 size)
|
||||
{
|
||||
_compositor = compositor;
|
||||
_size = size;
|
||||
|
||||
_bars = new SpriteVisual[BarCount];
|
||||
_peaks = new SpriteVisual[BarCount];
|
||||
var barWidth = (size.X - ((BarCount - 1) * BarGap)) / BarCount;
|
||||
barWidth = Math.Max(barWidth, 2f);
|
||||
|
||||
for (var i = 0; i < BarCount; i++)
|
||||
{
|
||||
var xPos = i * (barWidth + BarGap);
|
||||
var initialH = size.Y * 0.02f;
|
||||
|
||||
// Main bar — grows upward from the bottom
|
||||
var bar = compositor.CreateSpriteVisual();
|
||||
bar.Size = new Vector2(barWidth, initialH);
|
||||
bar.Offset = new Vector3(xPos, size.Y - initialH, 0);
|
||||
bar.Opacity = BarOpacity;
|
||||
bar.Brush = CreateVuMeterBrush(compositor);
|
||||
_bars[i] = bar;
|
||||
rootVisual.Children.InsertAtTop(bar);
|
||||
|
||||
// Peak indicator — thin bright bar that lingers at the top
|
||||
var peak = compositor.CreateSpriteVisual();
|
||||
peak.Size = new Vector2(barWidth, 2f);
|
||||
peak.Offset = new Vector3(xPos, size.Y - initialH - 3f, 0);
|
||||
peak.Opacity = BarOpacity * 0.8f;
|
||||
peak.Brush = compositor.CreateColorBrush(Color.FromArgb(200, 255, 255, 180));
|
||||
_peaks[i] = peak;
|
||||
rootVisual.Children.InsertAtTop(peak);
|
||||
}
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (_compositor == null || _bars == null || _peaks == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < BarCount; i++)
|
||||
{
|
||||
AnimateBar(i);
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
StopArray(_bars, "Size.Y", "Offset.Y");
|
||||
StopArray(_peaks, "Offset.Y");
|
||||
}
|
||||
|
||||
public void Resize(Vector2 newSize)
|
||||
{
|
||||
_size = newSize;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
DisposeArray(_bars);
|
||||
DisposeArray(_peaks);
|
||||
_bars = null;
|
||||
_peaks = null;
|
||||
}
|
||||
|
||||
private void AnimateBar(int index)
|
||||
{
|
||||
if (_compositor == null || _bars == null || _peaks == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bar = _bars[index];
|
||||
var peak = _peaks[index];
|
||||
var maxH = _size.Y * MaxBarHeightRatio;
|
||||
|
||||
// Snappy rise, slow gravity fall
|
||||
var riseEase = _compositor.CreateCubicBezierEasingFunction(
|
||||
new Vector2(0.1f, 0.8f), new Vector2(0.2f, 1f));
|
||||
var fallEase = _compositor.CreateCubicBezierEasingFunction(
|
||||
new Vector2(0.6f, 0f), new Vector2(0.9f, 0.4f));
|
||||
|
||||
// Golden-ratio-based pseudo-random seed per bar
|
||||
var golden = 1.618033988749f;
|
||||
var seed = ((index * golden) % 1f) + 0.3f;
|
||||
var period = 0.6 + (seed * 1.2);
|
||||
|
||||
// 8 keyframes simulating a VU meter bouncing to a beat
|
||||
var kf = new float[8];
|
||||
for (var k = 0; k < 8; k++)
|
||||
{
|
||||
var t = (((index * 13) + (k * 37)) % 97) / 97f;
|
||||
kf[k] = maxH * (0.08f + (0.92f * t * t));
|
||||
}
|
||||
|
||||
// Height animation
|
||||
var heightAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
heightAnim.Duration = TimeSpan.FromSeconds(period * 4);
|
||||
heightAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
|
||||
for (var k = 0; k < 8; k++)
|
||||
{
|
||||
var frac = k / 8f;
|
||||
var ease = (k % 2 == 0) ? riseEase : fallEase;
|
||||
heightAnim.InsertKeyFrame(frac, kf[k], ease);
|
||||
}
|
||||
|
||||
heightAnim.InsertKeyFrame(1f, kf[0], fallEase);
|
||||
bar.StartAnimation("Size.Y", heightAnim);
|
||||
|
||||
// Offset.Y — bars grow upward from the bottom edge
|
||||
var offsetAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
offsetAnim.Duration = TimeSpan.FromSeconds(period * 4);
|
||||
offsetAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
|
||||
for (var k = 0; k < 8; k++)
|
||||
{
|
||||
var frac = k / 8f;
|
||||
var ease = (k % 2 == 0) ? riseEase : fallEase;
|
||||
offsetAnim.InsertKeyFrame(frac, _size.Y - kf[k], ease);
|
||||
}
|
||||
|
||||
offsetAnim.InsertKeyFrame(1f, _size.Y - kf[0], fallEase);
|
||||
bar.StartAnimation("Offset.Y", offsetAnim);
|
||||
|
||||
// Peak tracks highest point with slower fall
|
||||
var peakAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
peakAnim.Duration = TimeSpan.FromSeconds(period * 4);
|
||||
peakAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
|
||||
for (var k = 0; k < 8; k++)
|
||||
{
|
||||
var frac = k / 8f;
|
||||
peakAnim.InsertKeyFrame(frac, _size.Y - kf[k] - 3f, fallEase);
|
||||
}
|
||||
|
||||
peakAnim.InsertKeyFrame(1f, _size.Y - kf[0] - 3f, fallEase);
|
||||
peak.StartAnimation("Offset.Y", peakAnim);
|
||||
}
|
||||
|
||||
private static CompositionLinearGradientBrush CreateVuMeterBrush(Compositor compositor)
|
||||
{
|
||||
var brush = compositor.CreateLinearGradientBrush();
|
||||
brush.StartPoint = new Vector2(0, 1);
|
||||
brush.EndPoint = new Vector2(0, 0);
|
||||
|
||||
// Classic VU: green at base → yellow → orange → red at top
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0f, Color.FromArgb(240, 0, 200, 40)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0.5f, Color.FromArgb(240, 180, 220, 0)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0.75f, Color.FromArgb(240, 255, 160, 0)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0.9f, Color.FromArgb(240, 255, 40, 0)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(255, 255, 0, 0)));
|
||||
|
||||
return brush;
|
||||
}
|
||||
|
||||
private static void StopArray(SpriteVisual[]? arr, params string[] props)
|
||||
{
|
||||
if (arr == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var v in arr)
|
||||
{
|
||||
foreach (var p in props)
|
||||
{
|
||||
v.StopAnimation(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DisposeArray(SpriteVisual[]? arr)
|
||||
{
|
||||
if (arr == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var v in arr)
|
||||
{
|
||||
v.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Numerics;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.UI.Composition;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls.AmbientEffects.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// Knight Rider / KITT scanner effect.
|
||||
/// A persistent ambient red glow spans the entire bottom edge. A brighter
|
||||
/// "hot spot" sweeps left↔right rapidly, intensifying the existing glow
|
||||
/// rather than appearing as a separate dot.
|
||||
/// </summary>
|
||||
internal sealed class KittScannerEffect : IBackgroundEffect
|
||||
{
|
||||
private const float HotSpotWidthRatio = 0.25f;
|
||||
|
||||
private readonly DockSide? _dockSide;
|
||||
private Compositor? _compositor;
|
||||
private SpriteVisual? _ambientGlow;
|
||||
private SpriteVisual? _hotSpot;
|
||||
private SpriteVisual? _hotSpotTrail;
|
||||
private Vector2 _size;
|
||||
|
||||
private CompositionScopedBatch? _batch;
|
||||
private bool _isRunning;
|
||||
private bool _movingForward = true;
|
||||
|
||||
public KittScannerEffect(DockSide? dockSide = null)
|
||||
{
|
||||
_dockSide = dockSide;
|
||||
}
|
||||
|
||||
public void Initialize(Compositor compositor, ContainerVisual rootVisual, Vector2 size)
|
||||
{
|
||||
_compositor = compositor;
|
||||
_size = size;
|
||||
|
||||
// Layer 1: persistent ambient red glow across the top edge
|
||||
_ambientGlow = compositor.CreateSpriteVisual();
|
||||
_ambientGlow.Size = new Vector2(size.X, Math.Min(120f, size.Y));
|
||||
_ambientGlow.Offset = new Vector3(0, 0, 0);
|
||||
_ambientGlow.Opacity = 1f;
|
||||
|
||||
var ambientBrush = compositor.CreateLinearGradientBrush();
|
||||
ambientBrush.StartPoint = new Vector2(0.5f, 0f);
|
||||
ambientBrush.EndPoint = new Vector2(0.5f, 1f);
|
||||
ambientBrush.ColorStops.Add(compositor.CreateColorGradientStop(0f, Color.FromArgb(90, 220, 20, 0)));
|
||||
ambientBrush.ColorStops.Add(compositor.CreateColorGradientStop(0.2f, Color.FromArgb(60, 200, 10, 0)));
|
||||
ambientBrush.ColorStops.Add(compositor.CreateColorGradientStop(0.5f, Color.FromArgb(30, 180, 0, 0)));
|
||||
ambientBrush.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, 160, 0, 0)));
|
||||
_ambientGlow.Brush = ambientBrush;
|
||||
|
||||
rootVisual.Children.InsertAtTop(_ambientGlow);
|
||||
|
||||
// Layer 2: wider trailing intensification
|
||||
var hotW = size.X * HotSpotWidthRatio;
|
||||
var hotH = Math.Min(140f, size.Y);
|
||||
|
||||
_hotSpotTrail = compositor.CreateSpriteVisual();
|
||||
_hotSpotTrail.Size = new Vector2(hotW * 1.8f, hotH);
|
||||
_hotSpotTrail.Offset = new Vector3(0, 0, 0);
|
||||
_hotSpotTrail.Opacity = 0.5f;
|
||||
|
||||
var trailBrush = compositor.CreateRadialGradientBrush();
|
||||
trailBrush.EllipseCenter = new Vector2(0.5f, 0f);
|
||||
trailBrush.EllipseRadius = new Vector2(0.5f, 0.85f);
|
||||
trailBrush.ColorStops.Add(compositor.CreateColorGradientStop(0f, Color.FromArgb(140, 255, 20, 0)));
|
||||
trailBrush.ColorStops.Add(compositor.CreateColorGradientStop(0.4f, Color.FromArgb(60, 200, 0, 0)));
|
||||
trailBrush.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, 120, 0, 0)));
|
||||
_hotSpotTrail.Brush = trailBrush;
|
||||
|
||||
rootVisual.Children.InsertAtTop(_hotSpotTrail);
|
||||
|
||||
// Layer 3: bright hot spot — the "scanner beam" that sweeps
|
||||
_hotSpot = compositor.CreateSpriteVisual();
|
||||
_hotSpot.Size = new Vector2(hotW, hotH);
|
||||
_hotSpot.Offset = new Vector3(0, 0, 0);
|
||||
_hotSpot.Opacity = 0.9f;
|
||||
|
||||
var hotBrush = compositor.CreateRadialGradientBrush();
|
||||
hotBrush.EllipseCenter = new Vector2(0.5f, 0f);
|
||||
hotBrush.EllipseRadius = new Vector2(0.45f, 0.8f);
|
||||
hotBrush.ColorStops.Add(compositor.CreateColorGradientStop(0f, Color.FromArgb(255, 255, 220, 200)));
|
||||
hotBrush.ColorStops.Add(compositor.CreateColorGradientStop(0.1f, Color.FromArgb(255, 255, 60, 30)));
|
||||
hotBrush.ColorStops.Add(compositor.CreateColorGradientStop(0.35f, Color.FromArgb(200, 240, 10, 0)));
|
||||
hotBrush.ColorStops.Add(compositor.CreateColorGradientStop(0.6f, Color.FromArgb(80, 180, 0, 0)));
|
||||
hotBrush.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, 100, 0, 0)));
|
||||
_hotSpot.Brush = hotBrush;
|
||||
|
||||
rootVisual.Children.InsertAtTop(_hotSpot);
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_isRunning = true;
|
||||
_movingForward = true;
|
||||
|
||||
// Pulse the ambient glow
|
||||
if (_compositor != null && _ambientGlow != null)
|
||||
{
|
||||
var easing = _compositor.CreateCubicBezierEasingFunction(
|
||||
new Vector2(0.35f, 0f), new Vector2(0.65f, 1f));
|
||||
var pulse = _compositor.CreateScalarKeyFrameAnimation();
|
||||
pulse.Duration = TimeSpan.FromSeconds(1.8);
|
||||
pulse.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
pulse.InsertKeyFrame(0f, 0.7f, easing);
|
||||
pulse.InsertKeyFrame(0.5f, 1f, easing);
|
||||
pulse.InsertKeyFrame(1f, 0.7f, easing);
|
||||
_ambientGlow.StartAnimation("Opacity", pulse);
|
||||
}
|
||||
|
||||
AnimateSweep();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_isRunning = false;
|
||||
_ambientGlow?.StopAnimation("Opacity");
|
||||
if (_batch != null)
|
||||
{
|
||||
_batch.Completed -= OnBatchCompleted;
|
||||
_batch = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Resize(Vector2 newSize)
|
||||
{
|
||||
_size = newSize;
|
||||
|
||||
if (_ambientGlow != null)
|
||||
{
|
||||
_ambientGlow.Size = new Vector2(newSize.X, Math.Min(120f, newSize.Y));
|
||||
_ambientGlow.Offset = new Vector3(0, 0, 0);
|
||||
}
|
||||
|
||||
var hotW = newSize.X * HotSpotWidthRatio;
|
||||
var hotH = Math.Min(140f, newSize.Y);
|
||||
|
||||
if (_hotSpot != null)
|
||||
{
|
||||
_hotSpot.Size = new Vector2(hotW, hotH);
|
||||
_hotSpot.Offset = new Vector3(_hotSpot.Offset.X, 0, 0);
|
||||
}
|
||||
|
||||
if (_hotSpotTrail != null)
|
||||
{
|
||||
_hotSpotTrail.Size = new Vector2(hotW * 1.8f, hotH);
|
||||
_hotSpotTrail.Offset = new Vector3(_hotSpotTrail.Offset.X, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
_ambientGlow?.Dispose();
|
||||
_hotSpot?.Dispose();
|
||||
_hotSpotTrail?.Dispose();
|
||||
_ambientGlow = null;
|
||||
_hotSpot = null;
|
||||
_hotSpotTrail = null;
|
||||
}
|
||||
|
||||
private void AnimateSweep()
|
||||
{
|
||||
if (!_isRunning || _compositor == null || _hotSpot == null || _hotSpotTrail == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var hotW = _hotSpot.Size.X;
|
||||
var startPos = -hotW * 0.2f;
|
||||
var endPos = _size.X - (hotW * 0.8f);
|
||||
var from = _movingForward ? startPos : endPos;
|
||||
var to = _movingForward ? endPos : startPos;
|
||||
|
||||
// Fast sweep — 1 second per pass
|
||||
var duration = TimeSpan.FromSeconds(1.0);
|
||||
var easing = _compositor.CreateCubicBezierEasingFunction(
|
||||
new Vector2(0.4f, 0f), new Vector2(0.6f, 1f));
|
||||
|
||||
_batch = _compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
|
||||
_batch.Completed += OnBatchCompleted;
|
||||
|
||||
// Hot spot sweep
|
||||
var hotAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
hotAnim.Duration = duration;
|
||||
hotAnim.InsertKeyFrame(0f, from, easing);
|
||||
hotAnim.InsertKeyFrame(1f, to, easing);
|
||||
_hotSpot.StartAnimation("Offset.X", hotAnim);
|
||||
|
||||
// Trail follows slightly behind
|
||||
var trailAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
trailAnim.Duration = duration;
|
||||
trailAnim.DelayTime = TimeSpan.FromMilliseconds(60);
|
||||
trailAnim.InsertKeyFrame(0f, from, easing);
|
||||
trailAnim.InsertKeyFrame(1f, to, easing);
|
||||
_hotSpotTrail.StartAnimation("Offset.X", trailAnim);
|
||||
|
||||
_batch.End();
|
||||
}
|
||||
|
||||
private void OnBatchCompleted(object sender, CompositionBatchCompletedEventArgs args)
|
||||
{
|
||||
if (_batch != null)
|
||||
{
|
||||
_batch.Completed -= OnBatchCompleted;
|
||||
_batch = null;
|
||||
}
|
||||
|
||||
_movingForward = !_movingForward;
|
||||
AnimateSweep();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Numerics;
|
||||
using Microsoft.Graphics.Canvas.Effects;
|
||||
using Microsoft.UI.Composition;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls.AmbientEffects.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// Lava lamp effect: multiple large, softly glowing radial gradient blobs drift
|
||||
/// around the background with layered sinusoidal motion. A gaussian blur
|
||||
/// softens the edges so overlapping blobs appear to merge organically.
|
||||
/// </summary>
|
||||
internal sealed class LavaLampEffect : IBackgroundEffect
|
||||
{
|
||||
private const int BlobCount = 5;
|
||||
private const float BlurAmount = 80f;
|
||||
|
||||
private static readonly Color[] BlobColors =
|
||||
[
|
||||
Color.FromArgb(220, 255, 40, 100),
|
||||
Color.FromArgb(200, 180, 20, 240),
|
||||
Color.FromArgb(210, 255, 140, 20),
|
||||
Color.FromArgb(190, 20, 200, 200),
|
||||
Color.FromArgb(200, 255, 60, 200),
|
||||
];
|
||||
|
||||
private Compositor? _compositor;
|
||||
private ContainerVisual? _root;
|
||||
private SpriteVisual? _blurHost;
|
||||
private ContainerVisual? _blobContainer;
|
||||
private SpriteVisual[]? _blobs;
|
||||
private Vector2 _size;
|
||||
|
||||
public void Initialize(Compositor compositor, ContainerVisual rootVisual, Vector2 size)
|
||||
{
|
||||
_compositor = compositor;
|
||||
_root = rootVisual;
|
||||
_size = size;
|
||||
|
||||
// Create a container for the blobs, which will be blurred as a group
|
||||
_blobContainer = compositor.CreateContainerVisual();
|
||||
_blobContainer.Size = size;
|
||||
|
||||
_blobs = new SpriteVisual[BlobCount];
|
||||
var blobDiameter = Math.Max(size.X, size.Y) * 0.5f;
|
||||
|
||||
for (var i = 0; i < BlobCount; i++)
|
||||
{
|
||||
var blob = compositor.CreateSpriteVisual();
|
||||
var scale = 0.7f + (0.6f * (i % 3) / 2f);
|
||||
blob.Size = new Vector2(blobDiameter * scale, blobDiameter * scale);
|
||||
blob.AnchorPoint = new Vector2(0.5f, 0.5f);
|
||||
|
||||
// Spread blobs in a pentagon-like pattern
|
||||
var angle = (i * MathF.PI * 2) / BlobCount;
|
||||
blob.Offset = new Vector3(
|
||||
(size.X * 0.5f) + (size.X * 0.25f * MathF.Cos(angle)),
|
||||
(size.Y * 0.5f) + (size.Y * 0.25f * MathF.Sin(angle)),
|
||||
0);
|
||||
|
||||
var brush = compositor.CreateRadialGradientBrush();
|
||||
brush.EllipseCenter = new Vector2(0.5f, 0.5f);
|
||||
brush.EllipseRadius = new Vector2(0.5f, 0.5f);
|
||||
|
||||
var color = BlobColors[i % BlobColors.Length];
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0f, color));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0.5f, Color.FromArgb((byte)(color.A / 2), color.R, color.G, color.B)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, color.R, color.G, color.B)));
|
||||
|
||||
blob.Brush = brush;
|
||||
_blobs[i] = blob;
|
||||
_blobContainer.Children.InsertAtTop(blob);
|
||||
}
|
||||
|
||||
// Apply gaussian blur to the entire blob container using an effect brush
|
||||
var blurEffect = new GaussianBlurEffect
|
||||
{
|
||||
Name = "Blur",
|
||||
BlurAmount = BlurAmount,
|
||||
BorderMode = EffectBorderMode.Soft,
|
||||
Source = new CompositionEffectSourceParameter("Source"),
|
||||
};
|
||||
|
||||
var effectFactory = compositor.CreateEffectFactory(blurEffect);
|
||||
var effectBrush = effectFactory.CreateBrush();
|
||||
|
||||
// Use a VisualSurface to capture the blob container
|
||||
var surface = compositor.CreateVisualSurface();
|
||||
surface.SourceVisual = _blobContainer;
|
||||
surface.SourceSize = size;
|
||||
|
||||
var surfaceBrush = compositor.CreateSurfaceBrush(surface);
|
||||
surfaceBrush.Stretch = CompositionStretch.Fill;
|
||||
effectBrush.SetSourceParameter("Source", surfaceBrush);
|
||||
|
||||
_blurHost = compositor.CreateSpriteVisual();
|
||||
_blurHost.Size = size;
|
||||
_blurHost.Brush = effectBrush;
|
||||
_blurHost.Opacity = 0.7f;
|
||||
|
||||
rootVisual.Children.InsertAtTop(_blurHost);
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (_compositor == null || _blobs == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < _blobs.Length; i++)
|
||||
{
|
||||
AnimateBlob(_blobs[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (_blobs == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var blob in _blobs)
|
||||
{
|
||||
blob.StopAnimation("Offset.X");
|
||||
blob.StopAnimation("Offset.Y");
|
||||
blob.StopAnimation("Scale.XY");
|
||||
}
|
||||
}
|
||||
|
||||
public void Resize(Vector2 newSize)
|
||||
{
|
||||
_size = newSize;
|
||||
if (_blobContainer != null)
|
||||
{
|
||||
_blobContainer.Size = newSize;
|
||||
}
|
||||
|
||||
if (_blurHost != null)
|
||||
{
|
||||
_blurHost.Size = newSize;
|
||||
|
||||
// Update the visual surface source size
|
||||
if (_blurHost.Brush is CompositionEffectBrush effectBrush)
|
||||
{
|
||||
// Recreate the surface for the new size
|
||||
if (_compositor != null && _blobContainer != null)
|
||||
{
|
||||
var surface = _compositor.CreateVisualSurface();
|
||||
surface.SourceVisual = _blobContainer;
|
||||
surface.SourceSize = newSize;
|
||||
|
||||
var surfaceBrush = _compositor.CreateSurfaceBrush(surface);
|
||||
surfaceBrush.Stretch = CompositionStretch.Fill;
|
||||
effectBrush.SetSourceParameter("Source", surfaceBrush);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_blobs != null)
|
||||
{
|
||||
var blobDiameter = Math.Max(newSize.X, newSize.Y) * 0.55f;
|
||||
foreach (var blob in _blobs)
|
||||
{
|
||||
blob.Size = new Vector2(blobDiameter, blobDiameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
_blurHost?.Dispose();
|
||||
if (_blobs != null)
|
||||
{
|
||||
foreach (var blob in _blobs)
|
||||
{
|
||||
blob.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_blobContainer?.Dispose();
|
||||
_blurHost = null;
|
||||
_blobs = null;
|
||||
_blobContainer = null;
|
||||
}
|
||||
|
||||
private void AnimateBlob(SpriteVisual blob, int index)
|
||||
{
|
||||
if (_compositor == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Each blob has a unique period and path so they move organically
|
||||
var basePeriod = 10.0 + (index * 3.7);
|
||||
var easing = _compositor.CreateCubicBezierEasingFunction(
|
||||
new Vector2(0.35f, 0f), new Vector2(0.65f, 1f));
|
||||
|
||||
// X oscillation — wide lazy arcs
|
||||
var xAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
xAnim.Duration = TimeSpan.FromSeconds(basePeriod);
|
||||
xAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
|
||||
var xCenter = _size.X * 0.5f;
|
||||
var xRange = _size.X * 0.4f;
|
||||
var xPhase = index * 0.2f;
|
||||
xAnim.InsertKeyFrame(0f, xCenter + (xRange * MathF.Cos(MathF.PI * 2 * xPhase)), easing);
|
||||
xAnim.InsertKeyFrame(0.25f, xCenter + (xRange * MathF.Cos(MathF.PI * 2 * (xPhase + 0.25f))), easing);
|
||||
xAnim.InsertKeyFrame(0.5f, xCenter + (xRange * MathF.Cos(MathF.PI * 2 * (xPhase + 0.5f))), easing);
|
||||
xAnim.InsertKeyFrame(0.75f, xCenter + (xRange * MathF.Cos(MathF.PI * 2 * (xPhase + 0.75f))), easing);
|
||||
xAnim.InsertKeyFrame(1f, xCenter + (xRange * MathF.Cos(MathF.PI * 2 * xPhase)), easing);
|
||||
|
||||
blob.StartAnimation("Offset.X", xAnim);
|
||||
|
||||
// Y oscillation — slightly different ratio for figure-8-like paths
|
||||
var yPeriod = basePeriod * 1.4;
|
||||
var yAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
yAnim.Duration = TimeSpan.FromSeconds(yPeriod);
|
||||
yAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
|
||||
var yCenter = _size.Y * 0.5f;
|
||||
var yRange = _size.Y * 0.38f;
|
||||
var yPhase = index * 0.33f;
|
||||
yAnim.InsertKeyFrame(0f, yCenter + (yRange * MathF.Sin(MathF.PI * 2 * yPhase)), easing);
|
||||
yAnim.InsertKeyFrame(0.25f, yCenter + (yRange * MathF.Sin(MathF.PI * 2 * (yPhase + 0.25f))), easing);
|
||||
yAnim.InsertKeyFrame(0.5f, yCenter + (yRange * MathF.Sin(MathF.PI * 2 * (yPhase + 0.5f))), easing);
|
||||
yAnim.InsertKeyFrame(0.75f, yCenter + (yRange * MathF.Sin(MathF.PI * 2 * (yPhase + 0.75f))), easing);
|
||||
yAnim.InsertKeyFrame(1f, yCenter + (yRange * MathF.Sin(MathF.PI * 2 * yPhase)), easing);
|
||||
|
||||
blob.StartAnimation("Offset.Y", yAnim);
|
||||
|
||||
// Organic scale pulsing — each blob breathes independently
|
||||
var scaleAnim = _compositor.CreateVector2KeyFrameAnimation();
|
||||
scaleAnim.Duration = TimeSpan.FromSeconds(basePeriod * 0.6);
|
||||
scaleAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
scaleAnim.InsertKeyFrame(0f, new Vector2(0.85f, 0.95f), easing);
|
||||
scaleAnim.InsertKeyFrame(0.33f, new Vector2(1.2f, 1.05f), easing);
|
||||
scaleAnim.InsertKeyFrame(0.66f, new Vector2(0.95f, 1.2f), easing);
|
||||
scaleAnim.InsertKeyFrame(1f, new Vector2(0.85f, 0.95f), easing);
|
||||
|
||||
blob.StartAnimation("Scale.XY", scaleAnim);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Numerics;
|
||||
using Microsoft.CmdPal.UI.Controls.AmbientEffects.Audio;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Windows.UI;
|
||||
using Windows.UI.ViewManagement;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls.AmbientEffects.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// Audio-reactive equalizer bars that visualize system audio in real-time.
|
||||
/// Bars glow using the system accent color with a soft radial gradient.
|
||||
/// Uses WASAPI loopback → FFT → CompositionPropertySet → ExpressionAnimation.
|
||||
/// </summary>
|
||||
internal sealed class LiveBarsEffect : IBackgroundEffect
|
||||
{
|
||||
private const int BarCount = 48;
|
||||
private const float BarGap = 2f;
|
||||
private const float MaxBarHeightRatio = 0.8f;
|
||||
private const float BarOpacity = 0.7f;
|
||||
|
||||
private Compositor? _compositor;
|
||||
private SpriteVisual[]? _bars;
|
||||
private SpriteVisual[]? _glows;
|
||||
private SpriteVisual[]? _peaks;
|
||||
private CompositionPropertySet? _propertySet;
|
||||
private Vector2 _size;
|
||||
|
||||
private AudioLoopbackService? _audioService;
|
||||
private DispatcherQueueTimer? _updateTimer;
|
||||
private float[]? _levelBuffer;
|
||||
private float[]? _peakLevels;
|
||||
private bool _audioAvailable;
|
||||
|
||||
public void Initialize(Compositor compositor, ContainerVisual rootVisual, Vector2 size)
|
||||
{
|
||||
_compositor = compositor;
|
||||
_size = size;
|
||||
|
||||
var accentColor = GetAccentColor();
|
||||
|
||||
_propertySet = compositor.CreatePropertySet();
|
||||
for (var i = 0; i < BarCount; i++)
|
||||
{
|
||||
_propertySet.InsertScalar($"B{i}", 0f);
|
||||
}
|
||||
|
||||
_bars = new SpriteVisual[BarCount];
|
||||
_glows = new SpriteVisual[BarCount];
|
||||
_peaks = new SpriteVisual[BarCount];
|
||||
_levelBuffer = new float[BarCount];
|
||||
_peakLevels = new float[BarCount];
|
||||
|
||||
var barWidth = (size.X - ((BarCount - 1) * BarGap)) / BarCount;
|
||||
barWidth = Math.Max(barWidth, 2f);
|
||||
var maxH = size.Y * MaxBarHeightRatio;
|
||||
|
||||
for (var i = 0; i < BarCount; i++)
|
||||
{
|
||||
var xPos = i * (barWidth + BarGap);
|
||||
|
||||
// Glow behind each bar — wider, softer radial gradient
|
||||
var glow = compositor.CreateSpriteVisual();
|
||||
glow.Size = new Vector2(barWidth * 3f, 0);
|
||||
glow.Offset = new Vector3(xPos - barWidth, size.Y, 0);
|
||||
glow.Opacity = 0.35f;
|
||||
glow.Brush = CreateGlowBrush(compositor, accentColor);
|
||||
_glows[i] = glow;
|
||||
rootVisual.Children.InsertAtTop(glow);
|
||||
|
||||
// Main bar
|
||||
var bar = compositor.CreateSpriteVisual();
|
||||
bar.Size = new Vector2(barWidth, 0);
|
||||
bar.Offset = new Vector3(xPos, size.Y, 0);
|
||||
bar.Opacity = BarOpacity;
|
||||
bar.Brush = CreateBarBrush(compositor, accentColor);
|
||||
_bars[i] = bar;
|
||||
rootVisual.Children.InsertAtTop(bar);
|
||||
|
||||
// Expression animations driven by property set
|
||||
BindBarExpressions(compositor, bar, glow, i, maxH, size.Y);
|
||||
|
||||
// Peak indicator — accent colored
|
||||
var peak = compositor.CreateSpriteVisual();
|
||||
peak.Size = new Vector2(barWidth + 2f, 2f);
|
||||
peak.Offset = new Vector3(xPos - 1f, size.Y, 0);
|
||||
peak.Opacity = 0.9f;
|
||||
peak.Brush = compositor.CreateColorBrush(accentColor);
|
||||
_peaks[i] = peak;
|
||||
rootVisual.Children.InsertAtTop(peak);
|
||||
}
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_audioService = new AudioLoopbackService(BarCount);
|
||||
_audioAvailable = _audioService.Start();
|
||||
|
||||
if (!_audioAvailable)
|
||||
{
|
||||
_audioService.Dispose();
|
||||
_audioService = null;
|
||||
}
|
||||
|
||||
var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
if (dispatcherQueue != null)
|
||||
{
|
||||
_updateTimer = dispatcherQueue.CreateTimer();
|
||||
_updateTimer.Interval = TimeSpan.FromMilliseconds(33);
|
||||
_updateTimer.Tick += OnUpdateTick;
|
||||
_updateTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_updateTimer?.Stop();
|
||||
if (_updateTimer != null)
|
||||
{
|
||||
_updateTimer.Tick -= OnUpdateTick;
|
||||
}
|
||||
|
||||
_updateTimer = null;
|
||||
_audioService?.Dispose();
|
||||
_audioService = null;
|
||||
}
|
||||
|
||||
public void Resize(Vector2 newSize)
|
||||
{
|
||||
_size = newSize;
|
||||
if (_bars == null || _glows == null || _compositor == null || _propertySet == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var barWidth = (newSize.X - ((BarCount - 1) * BarGap)) / BarCount;
|
||||
barWidth = Math.Max(barWidth, 2f);
|
||||
var maxH = newSize.Y * MaxBarHeightRatio;
|
||||
|
||||
for (var i = 0; i < BarCount; i++)
|
||||
{
|
||||
var xPos = i * (barWidth + BarGap);
|
||||
_bars[i].Size = new Vector2(barWidth, _bars[i].Size.Y);
|
||||
_glows[i].Size = new Vector2(barWidth * 3f, _glows[i].Size.Y);
|
||||
BindBarExpressions(_compositor, _bars[i], _glows[i], i, maxH, newSize.Y);
|
||||
|
||||
if (_peaks != null)
|
||||
{
|
||||
_peaks[i].Size = new Vector2(barWidth + 2f, 2f);
|
||||
_peaks[i].Offset = new Vector3(xPos - 1f, _peaks[i].Offset.Y, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
DisposeArray(_bars);
|
||||
DisposeArray(_glows);
|
||||
DisposeArray(_peaks);
|
||||
_propertySet?.Dispose();
|
||||
_bars = null;
|
||||
_glows = null;
|
||||
_peaks = null;
|
||||
_propertySet = null;
|
||||
}
|
||||
|
||||
private void BindBarExpressions(Compositor compositor, SpriteVisual bar, SpriteVisual glow, int index, float maxH, float totalH)
|
||||
{
|
||||
if (_propertySet == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var heightExpr = compositor.CreateExpressionAnimation($"props.B{index} * maxH");
|
||||
heightExpr.SetReferenceParameter("props", _propertySet);
|
||||
heightExpr.SetScalarParameter("maxH", maxH);
|
||||
bar.StartAnimation("Size.Y", heightExpr);
|
||||
|
||||
var offsetExpr = compositor.CreateExpressionAnimation($"totalH - (props.B{index} * maxH)");
|
||||
offsetExpr.SetReferenceParameter("props", _propertySet);
|
||||
offsetExpr.SetScalarParameter("totalH", totalH);
|
||||
offsetExpr.SetScalarParameter("maxH", maxH);
|
||||
bar.StartAnimation("Offset.Y", offsetExpr);
|
||||
|
||||
// Glow follows the bar
|
||||
var glowHExpr = compositor.CreateExpressionAnimation($"props.B{index} * maxH * 1.3");
|
||||
glowHExpr.SetReferenceParameter("props", _propertySet);
|
||||
glowHExpr.SetScalarParameter("maxH", maxH);
|
||||
glow.StartAnimation("Size.Y", glowHExpr);
|
||||
|
||||
var glowOffExpr = compositor.CreateExpressionAnimation($"totalH - (props.B{index} * maxH * 1.3)");
|
||||
glowOffExpr.SetReferenceParameter("props", _propertySet);
|
||||
glowOffExpr.SetScalarParameter("totalH", totalH);
|
||||
glowOffExpr.SetScalarParameter("maxH", maxH);
|
||||
glow.StartAnimation("Offset.Y", glowOffExpr);
|
||||
}
|
||||
|
||||
private void OnUpdateTick(DispatcherQueueTimer sender, object args)
|
||||
{
|
||||
if (_propertySet == null || _levelBuffer == null || _peakLevels == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_audioAvailable && _audioService != null)
|
||||
{
|
||||
_audioService.GetBandLevels(_levelBuffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
GenerateFallbackLevels(_levelBuffer);
|
||||
}
|
||||
|
||||
for (var i = 0; i < BarCount; i++)
|
||||
{
|
||||
_propertySet.InsertScalar($"B{i}", _levelBuffer[i]);
|
||||
|
||||
if (_levelBuffer[i] > _peakLevels[i])
|
||||
{
|
||||
_peakLevels[i] = _levelBuffer[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
_peakLevels[i] = Math.Max(0f, _peakLevels[i] - 0.008f);
|
||||
}
|
||||
|
||||
if (_peaks != null)
|
||||
{
|
||||
var maxH = _size.Y * MaxBarHeightRatio;
|
||||
_peaks[i].Offset = new Vector3(
|
||||
_peaks[i].Offset.X,
|
||||
_size.Y - (_peakLevels[i] * maxH) - 3f,
|
||||
0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateFallbackLevels(float[] levels)
|
||||
{
|
||||
var time = (float)(Environment.TickCount64 / 1000.0);
|
||||
for (var i = 0; i < levels.Length; i++)
|
||||
{
|
||||
var phase = i * 0.15f;
|
||||
var val = 0.05f + (0.03f * MathF.Sin((time * 1.5f) + phase));
|
||||
val += 0.02f * MathF.Sin((time * 2.7f) + (phase * 1.3f));
|
||||
levels[i] = Math.Clamp(val, 0f, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
private static Color GetAccentColor()
|
||||
{
|
||||
try
|
||||
{
|
||||
var uiSettings = new UISettings();
|
||||
return uiSettings.GetColorValue(UIColorType.Accent);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Color.FromArgb(255, 0, 120, 215);
|
||||
}
|
||||
}
|
||||
|
||||
private static CompositionLinearGradientBrush CreateBarBrush(Compositor compositor, Color accent)
|
||||
{
|
||||
var brush = compositor.CreateLinearGradientBrush();
|
||||
brush.StartPoint = new Vector2(0, 1);
|
||||
brush.EndPoint = new Vector2(0, 0);
|
||||
|
||||
// Accent color bar: solid at base, brighter/whiter at tip
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0f, accent));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0.7f, accent));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0.9f, Color.FromArgb(
|
||||
accent.A,
|
||||
(byte)Math.Min(255, accent.R + 80),
|
||||
(byte)Math.Min(255, accent.G + 80),
|
||||
(byte)Math.Min(255, accent.B + 80))));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(240, 255, 255, 255)));
|
||||
|
||||
return brush;
|
||||
}
|
||||
|
||||
private static CompositionRadialGradientBrush CreateGlowBrush(Compositor compositor, Color accent)
|
||||
{
|
||||
var brush = compositor.CreateRadialGradientBrush();
|
||||
brush.EllipseCenter = new Vector2(0.5f, 1f);
|
||||
brush.EllipseRadius = new Vector2(0.5f, 0.8f);
|
||||
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0f, Color.FromArgb(120, accent.R, accent.G, accent.B)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0.5f, Color.FromArgb(40, accent.R, accent.G, accent.B)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, accent.R, accent.G, accent.B)));
|
||||
|
||||
return brush;
|
||||
}
|
||||
|
||||
private static void DisposeArray(SpriteVisual[]? arr)
|
||||
{
|
||||
if (arr == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var v in arr)
|
||||
{
|
||||
v.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Numerics;
|
||||
using Microsoft.Graphics.Canvas.Effects;
|
||||
using Microsoft.UI.Composition;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls.AmbientEffects.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// WMP "Geiss" / plasma inspired effect — multiple large overlapping radial
|
||||
/// gradient blobs with vivid psychedelic colors drift, rotate and scale
|
||||
/// continuously. A heavy gaussian blur merges them into swirling plasma.
|
||||
/// </summary>
|
||||
internal sealed class PlasmaEffect : IBackgroundEffect
|
||||
{
|
||||
private const int BlobCount = 6;
|
||||
private const float BlurAmount = 60f;
|
||||
|
||||
private static readonly Color[] PlasmaColors =
|
||||
[
|
||||
Color.FromArgb(230, 255, 0, 80),
|
||||
Color.FromArgb(230, 0, 80, 255),
|
||||
Color.FromArgb(220, 220, 0, 255),
|
||||
Color.FromArgb(210, 0, 255, 120),
|
||||
Color.FromArgb(220, 255, 200, 0),
|
||||
Color.FromArgb(200, 255, 0, 200),
|
||||
];
|
||||
|
||||
private Compositor? _compositor;
|
||||
private SpriteVisual? _blurHost;
|
||||
private ContainerVisual? _blobContainer;
|
||||
private SpriteVisual[]? _blobs;
|
||||
private Vector2 _size;
|
||||
|
||||
public void Initialize(Compositor compositor, ContainerVisual rootVisual, Vector2 size)
|
||||
{
|
||||
_compositor = compositor;
|
||||
_size = size;
|
||||
|
||||
_blobContainer = compositor.CreateContainerVisual();
|
||||
_blobContainer.Size = size;
|
||||
|
||||
_blobs = new SpriteVisual[BlobCount];
|
||||
var blobDiam = Math.Max(size.X, size.Y) * 0.7f;
|
||||
|
||||
for (var i = 0; i < BlobCount; i++)
|
||||
{
|
||||
var blob = compositor.CreateSpriteVisual();
|
||||
blob.Size = new Vector2(blobDiam, blobDiam);
|
||||
blob.AnchorPoint = new Vector2(0.5f, 0.5f);
|
||||
blob.Offset = new Vector3(
|
||||
size.X * (0.2f + (0.6f * (((i * 3) % BlobCount) / (float)BlobCount))),
|
||||
size.Y * (0.2f + (0.6f * (((i * 7) % BlobCount) / (float)BlobCount))),
|
||||
0);
|
||||
blob.Opacity = 0.8f;
|
||||
|
||||
var brush = compositor.CreateRadialGradientBrush();
|
||||
brush.EllipseCenter = new Vector2(0.5f, 0.5f);
|
||||
brush.EllipseRadius = new Vector2(0.5f, 0.5f);
|
||||
|
||||
var c = PlasmaColors[i % PlasmaColors.Length];
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0f, c));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0.45f, Color.FromArgb((byte)(c.A / 2), c.R, c.G, c.B)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, c.R, c.G, c.B)));
|
||||
|
||||
blob.Brush = brush;
|
||||
_blobs[i] = blob;
|
||||
_blobContainer.Children.InsertAtTop(blob);
|
||||
}
|
||||
|
||||
// Blur the entire blob container into swirling plasma
|
||||
var blurEffect = new GaussianBlurEffect
|
||||
{
|
||||
Name = "Blur",
|
||||
BlurAmount = BlurAmount,
|
||||
BorderMode = EffectBorderMode.Soft,
|
||||
Source = new CompositionEffectSourceParameter("Source"),
|
||||
};
|
||||
|
||||
var factory = compositor.CreateEffectFactory(blurEffect);
|
||||
var effectBrush = factory.CreateBrush();
|
||||
|
||||
var surface = compositor.CreateVisualSurface();
|
||||
surface.SourceVisual = _blobContainer;
|
||||
surface.SourceSize = size;
|
||||
|
||||
var surfaceBrush = compositor.CreateSurfaceBrush(surface);
|
||||
surfaceBrush.Stretch = CompositionStretch.Fill;
|
||||
effectBrush.SetSourceParameter("Source", surfaceBrush);
|
||||
|
||||
_blurHost = compositor.CreateSpriteVisual();
|
||||
_blurHost.Size = size;
|
||||
_blurHost.Brush = effectBrush;
|
||||
_blurHost.Opacity = 0.75f;
|
||||
|
||||
rootVisual.Children.InsertAtTop(_blurHost);
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (_compositor == null || _blobs == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var smoothEasing = _compositor.CreateCubicBezierEasingFunction(
|
||||
new Vector2(0.45f, 0.05f), new Vector2(0.55f, 0.95f));
|
||||
var linearEasing = _compositor.CreateLinearEasingFunction();
|
||||
|
||||
for (var i = 0; i < _blobs.Length; i++)
|
||||
{
|
||||
var blob = _blobs[i];
|
||||
|
||||
// Lissajous-style X motion — faster
|
||||
var xPeriod = 5.0 + (i * 2.5);
|
||||
var xRange = _size.X * 0.45f;
|
||||
var cx = _size.X * 0.5f;
|
||||
var xPhase = i * 0.2f;
|
||||
|
||||
var xAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
xAnim.Duration = TimeSpan.FromSeconds(xPeriod);
|
||||
xAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
xAnim.InsertKeyFrame(0f, cx + (xRange * MathF.Cos(MathF.PI * 2 * xPhase)), smoothEasing);
|
||||
xAnim.InsertKeyFrame(0.25f, cx + (xRange * MathF.Cos(MathF.PI * 2 * (xPhase + 0.25f))), smoothEasing);
|
||||
xAnim.InsertKeyFrame(0.5f, cx + (xRange * MathF.Cos(MathF.PI * 2 * (xPhase + 0.5f))), smoothEasing);
|
||||
xAnim.InsertKeyFrame(0.75f, cx + (xRange * MathF.Cos(MathF.PI * 2 * (xPhase + 0.75f))), smoothEasing);
|
||||
xAnim.InsertKeyFrame(1f, cx + (xRange * MathF.Cos(MathF.PI * 2 * xPhase)), smoothEasing);
|
||||
blob.StartAnimation("Offset.X", xAnim);
|
||||
|
||||
// Y motion — offset ratio for tighter swirl patterns
|
||||
var yPeriod = xPeriod * 1.3;
|
||||
var yRange = _size.Y * 0.45f;
|
||||
var cy = _size.Y * 0.5f;
|
||||
var yPhase = i * 0.35f;
|
||||
|
||||
var yAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
yAnim.Duration = TimeSpan.FromSeconds(yPeriod);
|
||||
yAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
yAnim.InsertKeyFrame(0f, cy + (yRange * MathF.Sin(MathF.PI * 2 * yPhase)), smoothEasing);
|
||||
yAnim.InsertKeyFrame(0.25f, cy + (yRange * MathF.Sin(MathF.PI * 2 * (yPhase + 0.25f))), smoothEasing);
|
||||
yAnim.InsertKeyFrame(0.5f, cy + (yRange * MathF.Sin(MathF.PI * 2 * (yPhase + 0.5f))), smoothEasing);
|
||||
yAnim.InsertKeyFrame(0.75f, cy + (yRange * MathF.Sin(MathF.PI * 2 * (yPhase + 0.75f))), smoothEasing);
|
||||
yAnim.InsertKeyFrame(1f, cy + (yRange * MathF.Sin(MathF.PI * 2 * yPhase)), smoothEasing);
|
||||
blob.StartAnimation("Offset.Y", yAnim);
|
||||
|
||||
// Faster rotation for psychedelic turbulence
|
||||
var rotAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
rotAnim.Duration = TimeSpan.FromSeconds(xPeriod * 1.5);
|
||||
rotAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
rotAnim.InsertKeyFrame(0f, 0f, linearEasing);
|
||||
rotAnim.InsertKeyFrame(1f, (i % 2 == 0) ? 360f : -360f, linearEasing);
|
||||
blob.StartAnimation("RotationAngleInDegrees", rotAnim);
|
||||
|
||||
// Dramatic scale pulse
|
||||
var scaleAnim = _compositor.CreateVector2KeyFrameAnimation();
|
||||
scaleAnim.Duration = TimeSpan.FromSeconds(4 + (i * 1.5));
|
||||
scaleAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
scaleAnim.InsertKeyFrame(0f, new Vector2(0.7f, 0.7f), smoothEasing);
|
||||
scaleAnim.InsertKeyFrame(0.5f, new Vector2(1.4f, 1.4f), smoothEasing);
|
||||
scaleAnim.InsertKeyFrame(1f, new Vector2(0.7f, 0.7f), smoothEasing);
|
||||
blob.StartAnimation("Scale.XY", scaleAnim);
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (_blobs == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var blob in _blobs)
|
||||
{
|
||||
blob.StopAnimation("Offset.X");
|
||||
blob.StopAnimation("Offset.Y");
|
||||
blob.StopAnimation("RotationAngleInDegrees");
|
||||
blob.StopAnimation("Scale.XY");
|
||||
}
|
||||
}
|
||||
|
||||
public void Resize(Vector2 newSize)
|
||||
{
|
||||
_size = newSize;
|
||||
if (_blobContainer != null)
|
||||
{
|
||||
_blobContainer.Size = newSize;
|
||||
}
|
||||
|
||||
if (_blurHost != null)
|
||||
{
|
||||
_blurHost.Size = newSize;
|
||||
if (_blurHost.Brush is CompositionEffectBrush effectBrush && _compositor != null && _blobContainer != null)
|
||||
{
|
||||
var surface = _compositor.CreateVisualSurface();
|
||||
surface.SourceVisual = _blobContainer;
|
||||
surface.SourceSize = newSize;
|
||||
var surfaceBrush = _compositor.CreateSurfaceBrush(surface);
|
||||
surfaceBrush.Stretch = CompositionStretch.Fill;
|
||||
effectBrush.SetSourceParameter("Source", surfaceBrush);
|
||||
}
|
||||
}
|
||||
|
||||
if (_blobs != null)
|
||||
{
|
||||
var blobDiam = Math.Max(newSize.X, newSize.Y) * 0.7f;
|
||||
foreach (var blob in _blobs)
|
||||
{
|
||||
blob.Size = new Vector2(blobDiam, blobDiam);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
_blurHost?.Dispose();
|
||||
if (_blobs != null)
|
||||
{
|
||||
foreach (var blob in _blobs)
|
||||
{
|
||||
blob.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_blobContainer?.Dispose();
|
||||
_blurHost = null;
|
||||
_blobs = null;
|
||||
_blobContainer = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Numerics;
|
||||
using Microsoft.UI.Composition;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls.AmbientEffects.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// A gentle breathing/pulsing glow effect with concentric color rings.
|
||||
/// Multiple radial gradients at different scales create a layered
|
||||
/// heartbeat-like pulse with slowly shifting colors.
|
||||
/// </summary>
|
||||
internal sealed class PulseGlowEffect : IBackgroundEffect
|
||||
{
|
||||
private const int RingCount = 3;
|
||||
|
||||
private Compositor? _compositor;
|
||||
private ContainerVisual? _root;
|
||||
private SpriteVisual[]? _rings;
|
||||
private Vector2 _size;
|
||||
|
||||
public void Initialize(Compositor compositor, ContainerVisual rootVisual, Vector2 size)
|
||||
{
|
||||
_compositor = compositor;
|
||||
_root = rootVisual;
|
||||
_size = size;
|
||||
|
||||
_rings = new SpriteVisual[RingCount];
|
||||
|
||||
var colors = new[]
|
||||
{
|
||||
(inner: Color.FromArgb(140, 0, 180, 240), outer: Color.FromArgb(0, 0, 80, 180)),
|
||||
(inner: Color.FromArgb(100, 120, 0, 220), outer: Color.FromArgb(0, 60, 0, 140)),
|
||||
(inner: Color.FromArgb(80, 0, 200, 160), outer: Color.FromArgb(0, 0, 100, 100)),
|
||||
};
|
||||
|
||||
for (var i = 0; i < RingCount; i++)
|
||||
{
|
||||
var ring = compositor.CreateSpriteVisual();
|
||||
ring.AnchorPoint = new Vector2(0.5f, 0.5f);
|
||||
ring.Offset = new Vector3(size.X * 0.5f, size.Y * 0.5f, 0);
|
||||
|
||||
var scale = 0.6f + (i * 0.25f);
|
||||
var diam = Math.Max(size.X, size.Y) * scale;
|
||||
ring.Size = new Vector2(diam, diam);
|
||||
ring.Opacity = 0.5f;
|
||||
|
||||
var brush = compositor.CreateRadialGradientBrush();
|
||||
brush.EllipseCenter = new Vector2(0.5f, 0.5f);
|
||||
brush.EllipseRadius = new Vector2(0.5f, 0.5f);
|
||||
|
||||
var c = colors[i];
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0f, c.inner));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0.5f, Color.FromArgb((byte)(c.inner.A / 3), c.inner.R, c.inner.G, c.inner.B)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(1f, c.outer));
|
||||
|
||||
ring.Brush = brush;
|
||||
_rings[i] = ring;
|
||||
rootVisual.Children.InsertAtTop(ring);
|
||||
}
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (_compositor == null || _rings == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var breatheEasing = _compositor.CreateCubicBezierEasingFunction(
|
||||
new Vector2(0.35f, 0f), new Vector2(0.65f, 1f));
|
||||
|
||||
for (var i = 0; i < _rings.Length; i++)
|
||||
{
|
||||
var ring = _rings[i];
|
||||
var period = 3.5 + (i * 1.2);
|
||||
|
||||
// Staggered opacity breathe
|
||||
var opAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
opAnim.Duration = TimeSpan.FromSeconds(period);
|
||||
opAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
opAnim.InsertKeyFrame(0f, 0.2f, breatheEasing);
|
||||
opAnim.InsertKeyFrame(0.5f, 0.65f, breatheEasing);
|
||||
opAnim.InsertKeyFrame(1f, 0.2f, breatheEasing);
|
||||
ring.StartAnimation("Opacity", opAnim);
|
||||
|
||||
// Scale breathe — inner ring pumps more than outer
|
||||
var scaleMin = 0.8f + (i * 0.05f);
|
||||
var scaleMax = 1.15f - (i * 0.05f);
|
||||
var scAnim = _compositor.CreateVector2KeyFrameAnimation();
|
||||
scAnim.Duration = TimeSpan.FromSeconds(period);
|
||||
scAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
scAnim.InsertKeyFrame(0f, new Vector2(scaleMin, scaleMin), breatheEasing);
|
||||
scAnim.InsertKeyFrame(0.5f, new Vector2(scaleMax, scaleMax), breatheEasing);
|
||||
scAnim.InsertKeyFrame(1f, new Vector2(scaleMin, scaleMin), breatheEasing);
|
||||
ring.StartAnimation("Scale.XY", scAnim);
|
||||
|
||||
// Slow rotation for visual interest
|
||||
var rotAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
rotAnim.Duration = TimeSpan.FromSeconds(period * 8);
|
||||
rotAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
var dir = (i % 2 == 0) ? 360f : -360f;
|
||||
rotAnim.InsertKeyFrame(0f, 0f, _compositor.CreateLinearEasingFunction());
|
||||
rotAnim.InsertKeyFrame(1f, dir, _compositor.CreateLinearEasingFunction());
|
||||
ring.StartAnimation("RotationAngleInDegrees", rotAnim);
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (_rings == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var ring in _rings)
|
||||
{
|
||||
ring.StopAnimation("Opacity");
|
||||
ring.StopAnimation("Scale.XY");
|
||||
ring.StopAnimation("RotationAngleInDegrees");
|
||||
}
|
||||
}
|
||||
|
||||
public void Resize(Vector2 newSize)
|
||||
{
|
||||
_size = newSize;
|
||||
if (_rings == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < _rings.Length; i++)
|
||||
{
|
||||
_rings[i].Offset = new Vector3(newSize.X * 0.5f, newSize.Y * 0.5f, 0);
|
||||
var scale = 0.6f + (i * 0.25f);
|
||||
var diam = Math.Max(newSize.X, newSize.Y) * scale;
|
||||
_rings[i].Size = new Vector2(diam, diam);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
if (_rings != null)
|
||||
{
|
||||
foreach (var ring in _rings)
|
||||
{
|
||||
ring.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_rings = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Numerics;
|
||||
using Microsoft.CmdPal.UI.Controls.AmbientEffects.Audio;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls.AmbientEffects.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// Retro 80s / Tron / synthwave inspired effect. A neon grid recedes towards
|
||||
/// a glowing horizon, with horizontal scanlines sweeping upward and pulsing
|
||||
/// neon accent bars at the top and bottom edges. Full synthwave vibes.
|
||||
/// </summary>
|
||||
internal sealed class RetroGridEffect : IBackgroundEffect
|
||||
{
|
||||
private const int HorizontalLineCount = 8;
|
||||
private const int VerticalLineCount = 12;
|
||||
private const int ScanlineCount = 3;
|
||||
|
||||
private static readonly Color NeonPink = Color.FromArgb(200, 255, 0, 200);
|
||||
private static readonly Color NeonCyan = Color.FromArgb(180, 0, 240, 255);
|
||||
private static readonly Color NeonPurple = Color.FromArgb(160, 180, 0, 255);
|
||||
private static readonly Color DarkBase = Color.FromArgb(60, 20, 0, 40);
|
||||
|
||||
private Compositor? _compositor;
|
||||
private SpriteVisual? _horizonGlow;
|
||||
private SpriteVisual? _topEdgeGlow;
|
||||
private SpriteVisual? _bottomEdgeGlow;
|
||||
private SpriteVisual[]? _hLines;
|
||||
private SpriteVisual[]? _vLines;
|
||||
private SpriteVisual[]? _scanlines;
|
||||
private Vector2 _size;
|
||||
|
||||
private AudioLoopbackService? _audioService;
|
||||
private DispatcherQueueTimer? _updateTimer;
|
||||
private float[]? _levelBuffer;
|
||||
private bool _audioAvailable;
|
||||
|
||||
public void Initialize(Compositor compositor, ContainerVisual rootVisual, Vector2 size)
|
||||
{
|
||||
_compositor = compositor;
|
||||
_size = size;
|
||||
|
||||
// Dark purple/black base tint
|
||||
var baseTint = compositor.CreateSpriteVisual();
|
||||
baseTint.Size = size;
|
||||
baseTint.Brush = compositor.CreateColorBrush(DarkBase);
|
||||
rootVisual.Children.InsertAtTop(baseTint);
|
||||
|
||||
// Horizon glow — a wide gradient band at ~60% height
|
||||
var horizonY = size.Y * 0.55f;
|
||||
_horizonGlow = compositor.CreateSpriteVisual();
|
||||
_horizonGlow.Size = new Vector2(size.X * 1.5f, size.Y * 0.35f);
|
||||
_horizonGlow.AnchorPoint = new Vector2(0.5f, 0.5f);
|
||||
_horizonGlow.Offset = new Vector3(size.X * 0.5f, horizonY, 0);
|
||||
|
||||
var horizonBrush = compositor.CreateRadialGradientBrush();
|
||||
horizonBrush.EllipseCenter = new Vector2(0.5f, 0.5f);
|
||||
horizonBrush.EllipseRadius = new Vector2(0.5f, 0.5f);
|
||||
horizonBrush.ColorStops.Add(compositor.CreateColorGradientStop(0f, Color.FromArgb(140, 255, 80, 200)));
|
||||
horizonBrush.ColorStops.Add(compositor.CreateColorGradientStop(0.4f, Color.FromArgb(60, 200, 0, 180)));
|
||||
horizonBrush.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, 100, 0, 80)));
|
||||
_horizonGlow.Brush = horizonBrush;
|
||||
rootVisual.Children.InsertAtTop(_horizonGlow);
|
||||
|
||||
// Horizontal grid lines — converge towards horizon using perspective
|
||||
_hLines = new SpriteVisual[HorizontalLineCount];
|
||||
for (var i = 0; i < HorizontalLineCount; i++)
|
||||
{
|
||||
var line = compositor.CreateSpriteVisual();
|
||||
var t = (i + 1f) / HorizontalLineCount;
|
||||
var yPos = horizonY + ((size.Y - horizonY) * t * t);
|
||||
var thickness = 1f + (t * 2f);
|
||||
line.Size = new Vector2(size.X, thickness);
|
||||
line.Offset = new Vector3(0, yPos, 0);
|
||||
line.Opacity = 0.3f + (0.5f * t);
|
||||
|
||||
var lineBrush = compositor.CreateLinearGradientBrush();
|
||||
lineBrush.StartPoint = new Vector2(0, 0.5f);
|
||||
lineBrush.EndPoint = new Vector2(1, 0.5f);
|
||||
lineBrush.ColorStops.Add(compositor.CreateColorGradientStop(0f, Color.FromArgb(0, NeonCyan.R, NeonCyan.G, NeonCyan.B)));
|
||||
lineBrush.ColorStops.Add(compositor.CreateColorGradientStop(0.2f, NeonCyan));
|
||||
lineBrush.ColorStops.Add(compositor.CreateColorGradientStop(0.8f, NeonCyan));
|
||||
lineBrush.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, NeonCyan.R, NeonCyan.G, NeonCyan.B)));
|
||||
|
||||
line.Brush = lineBrush;
|
||||
_hLines[i] = line;
|
||||
rootVisual.Children.InsertAtTop(line);
|
||||
}
|
||||
|
||||
// Vertical grid lines — converge towards center at the horizon
|
||||
_vLines = new SpriteVisual[VerticalLineCount];
|
||||
var gridBottom = size.Y;
|
||||
for (var i = 0; i < VerticalLineCount; i++)
|
||||
{
|
||||
var line = compositor.CreateSpriteVisual();
|
||||
var fraction = (i - (VerticalLineCount / 2f)) / (VerticalLineCount / 2f);
|
||||
var topX = (size.X * 0.5f) + (size.X * 0.08f * fraction);
|
||||
var bottomX = (size.X * 0.5f) + (size.X * 0.55f * fraction);
|
||||
|
||||
// Approximate a perspective line with a thin tall rect + rotation
|
||||
var dx = bottomX - topX;
|
||||
var dy = gridBottom - horizonY;
|
||||
var length = MathF.Sqrt((dx * dx) + (dy * dy));
|
||||
var angle = MathF.Atan2(dx, dy) * (180f / MathF.PI);
|
||||
|
||||
line.Size = new Vector2(1.5f, length);
|
||||
line.AnchorPoint = new Vector2(0.5f, 0f);
|
||||
line.Offset = new Vector3(topX, horizonY, 0);
|
||||
line.RotationAngleInDegrees = -angle;
|
||||
line.Opacity = 0.2f + (0.3f * (1f - MathF.Abs(fraction)));
|
||||
|
||||
line.Brush = compositor.CreateColorBrush(NeonPurple);
|
||||
_vLines[i] = line;
|
||||
rootVisual.Children.InsertAtTop(line);
|
||||
}
|
||||
|
||||
// Sweeping scanlines
|
||||
_scanlines = new SpriteVisual[ScanlineCount];
|
||||
for (var i = 0; i < ScanlineCount; i++)
|
||||
{
|
||||
var scanline = compositor.CreateSpriteVisual();
|
||||
scanline.Size = new Vector2(size.X, 2f);
|
||||
scanline.Offset = new Vector3(0, -10f, 0);
|
||||
scanline.Opacity = 0f;
|
||||
|
||||
var scanBrush = compositor.CreateLinearGradientBrush();
|
||||
scanBrush.StartPoint = new Vector2(0, 0.5f);
|
||||
scanBrush.EndPoint = new Vector2(1, 0.5f);
|
||||
scanBrush.ColorStops.Add(compositor.CreateColorGradientStop(0f, Color.FromArgb(0, NeonPink.R, NeonPink.G, NeonPink.B)));
|
||||
scanBrush.ColorStops.Add(compositor.CreateColorGradientStop(0.3f, NeonPink));
|
||||
scanBrush.ColorStops.Add(compositor.CreateColorGradientStop(0.7f, NeonPink));
|
||||
scanBrush.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, NeonPink.R, NeonPink.G, NeonPink.B)));
|
||||
|
||||
scanline.Brush = scanBrush;
|
||||
_scanlines[i] = scanline;
|
||||
rootVisual.Children.InsertAtTop(scanline);
|
||||
}
|
||||
|
||||
// Top neon edge glow
|
||||
_topEdgeGlow = CreateEdgeGlow(compositor, size, true);
|
||||
rootVisual.Children.InsertAtTop(_topEdgeGlow);
|
||||
|
||||
// Bottom neon edge glow
|
||||
_bottomEdgeGlow = CreateEdgeGlow(compositor, size, false);
|
||||
rootVisual.Children.InsertAtTop(_bottomEdgeGlow);
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (_compositor == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var easing = _compositor.CreateCubicBezierEasingFunction(
|
||||
new Vector2(0.3f, 0f), new Vector2(0.7f, 1f));
|
||||
var linearEasing = _compositor.CreateLinearEasingFunction();
|
||||
|
||||
// Horizon glow pulse
|
||||
if (_horizonGlow != null)
|
||||
{
|
||||
var pulseAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
pulseAnim.Duration = TimeSpan.FromSeconds(4);
|
||||
pulseAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
pulseAnim.InsertKeyFrame(0f, 0.6f, easing);
|
||||
pulseAnim.InsertKeyFrame(0.5f, 1f, easing);
|
||||
pulseAnim.InsertKeyFrame(1f, 0.6f, easing);
|
||||
_horizonGlow.StartAnimation("Opacity", pulseAnim);
|
||||
}
|
||||
|
||||
// Grid line pulse
|
||||
if (_hLines != null)
|
||||
{
|
||||
for (var i = 0; i < _hLines.Length; i++)
|
||||
{
|
||||
var lineAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
lineAnim.Duration = TimeSpan.FromSeconds(2.5 + (i * 0.3));
|
||||
lineAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
var baseOp = _hLines[i].Opacity;
|
||||
lineAnim.InsertKeyFrame(0f, baseOp * 0.6f, easing);
|
||||
lineAnim.InsertKeyFrame(0.5f, Math.Min(baseOp * 1.3f, 1f), easing);
|
||||
lineAnim.InsertKeyFrame(1f, baseOp * 0.6f, easing);
|
||||
_hLines[i].StartAnimation("Opacity", lineAnim);
|
||||
}
|
||||
}
|
||||
|
||||
// Scanline sweep — each scanline sweeps up at staggered intervals
|
||||
if (_scanlines != null)
|
||||
{
|
||||
for (var i = 0; i < _scanlines.Length; i++)
|
||||
{
|
||||
AnimateScanline(_scanlines[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
// Edge glow pulse
|
||||
AnimateEdgeGlow(_topEdgeGlow, 3.0);
|
||||
AnimateEdgeGlow(_bottomEdgeGlow, 3.5);
|
||||
|
||||
// Start audio reactivity
|
||||
_levelBuffer = new float[HorizontalLineCount + 4];
|
||||
_audioService = new AudioLoopbackService(_levelBuffer.Length);
|
||||
_audioAvailable = _audioService.Start();
|
||||
|
||||
if (!_audioAvailable)
|
||||
{
|
||||
_audioService.Dispose();
|
||||
_audioService = null;
|
||||
}
|
||||
|
||||
var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
if (dispatcherQueue != null)
|
||||
{
|
||||
_updateTimer = dispatcherQueue.CreateTimer();
|
||||
_updateTimer.Interval = TimeSpan.FromMilliseconds(33);
|
||||
_updateTimer.Tick += OnAudioTick;
|
||||
_updateTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_updateTimer?.Stop();
|
||||
if (_updateTimer != null)
|
||||
{
|
||||
_updateTimer.Tick -= OnAudioTick;
|
||||
}
|
||||
|
||||
_updateTimer = null;
|
||||
_audioService?.Dispose();
|
||||
_audioService = null;
|
||||
|
||||
_horizonGlow?.StopAnimation("Opacity");
|
||||
|
||||
if (_hLines != null)
|
||||
{
|
||||
foreach (var line in _hLines)
|
||||
{
|
||||
line.StopAnimation("Opacity");
|
||||
}
|
||||
}
|
||||
|
||||
if (_scanlines != null)
|
||||
{
|
||||
foreach (var scanline in _scanlines)
|
||||
{
|
||||
scanline.StopAnimation("Offset.Y");
|
||||
scanline.StopAnimation("Opacity");
|
||||
}
|
||||
}
|
||||
|
||||
_topEdgeGlow?.StopAnimation("Opacity");
|
||||
_bottomEdgeGlow?.StopAnimation("Opacity");
|
||||
}
|
||||
|
||||
public void Resize(Vector2 newSize)
|
||||
{
|
||||
_size = newSize;
|
||||
|
||||
// Full re-layout would be complex; the key visuals will still render
|
||||
if (_horizonGlow != null)
|
||||
{
|
||||
_horizonGlow.Size = new Vector2(newSize.X * 1.5f, newSize.Y * 0.35f);
|
||||
_horizonGlow.Offset = new Vector3(newSize.X * 0.5f, newSize.Y * 0.55f, 0);
|
||||
}
|
||||
|
||||
if (_topEdgeGlow != null)
|
||||
{
|
||||
_topEdgeGlow.Size = new Vector2(newSize.X, 6f);
|
||||
}
|
||||
|
||||
if (_bottomEdgeGlow != null)
|
||||
{
|
||||
_bottomEdgeGlow.Size = new Vector2(newSize.X, 6f);
|
||||
_bottomEdgeGlow.Offset = new Vector3(0, newSize.Y - 6f, 0);
|
||||
}
|
||||
|
||||
if (_scanlines != null)
|
||||
{
|
||||
foreach (var scanline in _scanlines)
|
||||
{
|
||||
scanline.Size = new Vector2(newSize.X, 2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
_horizonGlow?.Dispose();
|
||||
_topEdgeGlow?.Dispose();
|
||||
_bottomEdgeGlow?.Dispose();
|
||||
|
||||
DisposeArray(_hLines);
|
||||
DisposeArray(_vLines);
|
||||
DisposeArray(_scanlines);
|
||||
|
||||
_hLines = null;
|
||||
_vLines = null;
|
||||
_scanlines = null;
|
||||
_horizonGlow = null;
|
||||
_topEdgeGlow = null;
|
||||
_bottomEdgeGlow = null;
|
||||
}
|
||||
|
||||
private void OnAudioTick(DispatcherQueueTimer sender, object args)
|
||||
{
|
||||
if (_levelBuffer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_audioAvailable && _audioService != null)
|
||||
{
|
||||
_audioService.GetBandLevels(_levelBuffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
var time = (float)(Environment.TickCount64 / 1000.0);
|
||||
for (var i = 0; i < _levelBuffer.Length; i++)
|
||||
{
|
||||
_levelBuffer[i] = 0.3f + (0.1f * MathF.Sin((time * 2f) + (i * 0.5f)));
|
||||
}
|
||||
}
|
||||
|
||||
// Bass drives horizon glow intensity
|
||||
var bass = (_levelBuffer.Length > 2) ? (_levelBuffer[0] + _levelBuffer[1]) * 0.5f : 0.3f;
|
||||
if (_horizonGlow != null)
|
||||
{
|
||||
_horizonGlow.Opacity = 0.4f + (0.6f * bass);
|
||||
var scale = 0.9f + (0.3f * bass);
|
||||
_horizonGlow.Scale = new Vector3(scale, scale, 1f);
|
||||
}
|
||||
|
||||
// Each horizontal grid line reacts to a frequency band
|
||||
if (_hLines != null)
|
||||
{
|
||||
for (var i = 0; i < _hLines.Length && i < _levelBuffer.Length; i++)
|
||||
{
|
||||
_hLines[i].Opacity = 0.2f + (0.8f * _levelBuffer[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Edge glows react to bass
|
||||
if (_topEdgeGlow != null)
|
||||
{
|
||||
_topEdgeGlow.Opacity = 0.3f + (0.7f * bass);
|
||||
}
|
||||
|
||||
if (_bottomEdgeGlow != null)
|
||||
{
|
||||
_bottomEdgeGlow.Opacity = 0.3f + (0.7f * bass);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DisposeArray(SpriteVisual[]? visuals)
|
||||
{
|
||||
if (visuals == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var v in visuals)
|
||||
{
|
||||
v.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static SpriteVisual CreateEdgeGlow(Compositor compositor, Vector2 size, bool isTop)
|
||||
{
|
||||
var glow = compositor.CreateSpriteVisual();
|
||||
glow.Size = new Vector2(size.X, 6f);
|
||||
glow.Offset = isTop ? new Vector3(0, 0, 0) : new Vector3(0, size.Y - 6f, 0);
|
||||
glow.Opacity = 0.7f;
|
||||
|
||||
var brush = compositor.CreateLinearGradientBrush();
|
||||
brush.StartPoint = new Vector2(0, 0.5f);
|
||||
brush.EndPoint = new Vector2(1, 0.5f);
|
||||
|
||||
var color = isTop ? NeonCyan : NeonPink;
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0f, Color.FromArgb(0, color.R, color.G, color.B)));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0.15f, color));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(0.85f, color));
|
||||
brush.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, color.R, color.G, color.B)));
|
||||
|
||||
glow.Brush = brush;
|
||||
return glow;
|
||||
}
|
||||
|
||||
private void AnimateScanline(SpriteVisual scanline, int index)
|
||||
{
|
||||
if (_compositor == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var easing = _compositor.CreateCubicBezierEasingFunction(
|
||||
new Vector2(0.4f, 0f), new Vector2(0.6f, 1f));
|
||||
|
||||
var period = 3.0 + (index * 2.0);
|
||||
|
||||
// Scanlines only sweep within the grid area: bottom → horizon
|
||||
// They travel "into" the grid, fading out as they approach the vanishing point
|
||||
var horizonY = _size.Y * 0.55f;
|
||||
var bottomY = _size.Y;
|
||||
|
||||
var yAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
yAnim.Duration = TimeSpan.FromSeconds(period);
|
||||
yAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
yAnim.InsertKeyFrame(0f, bottomY, easing);
|
||||
yAnim.InsertKeyFrame(1f, horizonY, easing);
|
||||
scanline.StartAnimation("Offset.Y", yAnim);
|
||||
|
||||
// Bright at the bottom (near us), fades to invisible at the horizon
|
||||
var opAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
opAnim.Duration = TimeSpan.FromSeconds(period);
|
||||
opAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
opAnim.InsertKeyFrame(0f, 0f, easing);
|
||||
opAnim.InsertKeyFrame(0.1f, 0.6f, easing);
|
||||
opAnim.InsertKeyFrame(0.7f, 0.3f, easing);
|
||||
opAnim.InsertKeyFrame(1f, 0f, easing);
|
||||
scanline.StartAnimation("Opacity", opAnim);
|
||||
}
|
||||
|
||||
private void AnimateEdgeGlow(SpriteVisual? glow, double period)
|
||||
{
|
||||
if (_compositor == null || glow == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var easing = _compositor.CreateCubicBezierEasingFunction(
|
||||
new Vector2(0.35f, 0f), new Vector2(0.65f, 1f));
|
||||
|
||||
var anim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
anim.Duration = TimeSpan.FromSeconds(period);
|
||||
anim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
anim.InsertKeyFrame(0f, 0.4f, easing);
|
||||
anim.InsertKeyFrame(0.5f, 0.9f, easing);
|
||||
anim.InsertKeyFrame(1f, 0.4f, easing);
|
||||
glow.StartAnimation("Opacity", anim);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Numerics;
|
||||
using Microsoft.UI.Composition;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls.AmbientEffects.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// Spotlight effect: a large, soft radial gradient light that slowly roams
|
||||
/// around the background tracing a smooth Lissajous curve. Pairs well with
|
||||
/// background images as a subtle animated highlight.
|
||||
/// </summary>
|
||||
internal sealed class SpotlightEffect : IBackgroundEffect
|
||||
{
|
||||
private Compositor? _compositor;
|
||||
private ContainerVisual? _root;
|
||||
private SpriteVisual? _spotVisual;
|
||||
private Vector2 _size;
|
||||
|
||||
public void Initialize(Compositor compositor, ContainerVisual rootVisual, Vector2 size)
|
||||
{
|
||||
_compositor = compositor;
|
||||
_root = rootVisual;
|
||||
_size = size;
|
||||
|
||||
var spotDiameter = Math.Max(size.X, size.Y) * 0.9f;
|
||||
_spotVisual = compositor.CreateSpriteVisual();
|
||||
_spotVisual.Size = new Vector2(spotDiameter, spotDiameter);
|
||||
_spotVisual.AnchorPoint = new Vector2(0.5f, 0.5f);
|
||||
_spotVisual.Offset = new Vector3(size.X * 0.5f, size.Y * 0.5f, 0);
|
||||
_spotVisual.Opacity = 0.3f;
|
||||
|
||||
var gradientBrush = compositor.CreateRadialGradientBrush();
|
||||
gradientBrush.EllipseCenter = new Vector2(0.5f, 0.5f);
|
||||
gradientBrush.EllipseRadius = new Vector2(0.5f, 0.5f);
|
||||
|
||||
// Warm white/soft gold light
|
||||
gradientBrush.ColorStops.Add(compositor.CreateColorGradientStop(0f, Color.FromArgb(160, 255, 245, 220)));
|
||||
gradientBrush.ColorStops.Add(compositor.CreateColorGradientStop(0.35f, Color.FromArgb(80, 255, 230, 190)));
|
||||
gradientBrush.ColorStops.Add(compositor.CreateColorGradientStop(0.7f, Color.FromArgb(20, 200, 180, 150)));
|
||||
gradientBrush.ColorStops.Add(compositor.CreateColorGradientStop(1f, Color.FromArgb(0, 150, 130, 100)));
|
||||
|
||||
_spotVisual.Brush = gradientBrush;
|
||||
rootVisual.Children.InsertAtTop(_spotVisual);
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (_compositor == null || _spotVisual == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var easing = _compositor.CreateCubicBezierEasingFunction(
|
||||
new Vector2(0.45f, 0.05f), new Vector2(0.55f, 0.95f));
|
||||
|
||||
// Lissajous X motion: period ~18s
|
||||
var xAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
xAnim.Duration = TimeSpan.FromSeconds(18);
|
||||
xAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
var cx = _size.X * 0.5f;
|
||||
var rx = _size.X * 0.4f;
|
||||
xAnim.InsertKeyFrame(0f, cx + rx, easing);
|
||||
xAnim.InsertKeyFrame(0.25f, cx, easing);
|
||||
xAnim.InsertKeyFrame(0.5f, cx - rx, easing);
|
||||
xAnim.InsertKeyFrame(0.75f, cx, easing);
|
||||
xAnim.InsertKeyFrame(1f, cx + rx, easing);
|
||||
_spotVisual.StartAnimation("Offset.X", xAnim);
|
||||
|
||||
// Lissajous Y motion: period ~24s (3:4 ratio with X for variety)
|
||||
var yAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
yAnim.Duration = TimeSpan.FromSeconds(24);
|
||||
yAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
var cy = _size.Y * 0.5f;
|
||||
var ry = _size.Y * 0.35f;
|
||||
yAnim.InsertKeyFrame(0f, cy, easing);
|
||||
yAnim.InsertKeyFrame(0.25f, cy + ry, easing);
|
||||
yAnim.InsertKeyFrame(0.5f, cy, easing);
|
||||
yAnim.InsertKeyFrame(0.75f, cy - ry, easing);
|
||||
yAnim.InsertKeyFrame(1f, cy, easing);
|
||||
_spotVisual.StartAnimation("Offset.Y", yAnim);
|
||||
|
||||
// Gentle opacity pulse
|
||||
var opacityAnim = _compositor.CreateScalarKeyFrameAnimation();
|
||||
opacityAnim.Duration = TimeSpan.FromSeconds(10);
|
||||
opacityAnim.IterationBehavior = AnimationIterationBehavior.Forever;
|
||||
opacityAnim.InsertKeyFrame(0f, 0.25f, easing);
|
||||
opacityAnim.InsertKeyFrame(0.5f, 0.4f, easing);
|
||||
opacityAnim.InsertKeyFrame(1f, 0.25f, easing);
|
||||
_spotVisual.StartAnimation("Opacity", opacityAnim);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_spotVisual?.StopAnimation("Offset.X");
|
||||
_spotVisual?.StopAnimation("Offset.Y");
|
||||
_spotVisual?.StopAnimation("Opacity");
|
||||
}
|
||||
|
||||
public void Resize(Vector2 newSize)
|
||||
{
|
||||
_size = newSize;
|
||||
if (_spotVisual == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var spotDiameter = Math.Max(newSize.X, newSize.Y) * 0.9f;
|
||||
_spotVisual.Size = new Vector2(spotDiameter, spotDiameter);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
_spotVisual?.Dispose();
|
||||
_spotVisual = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Numerics;
|
||||
using Microsoft.UI.Composition;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls.AmbientEffects;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a GPU-accelerated background visual effect rendered via the Composition API.
|
||||
/// Each implementation builds its own composition tree inside the provided <see cref="ContainerVisual"/>.
|
||||
/// </summary>
|
||||
internal interface IBackgroundEffect : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Called once to set up the composition visual tree.
|
||||
/// </summary>
|
||||
/// <param name="compositor">The compositor to use for creating visuals and animations.</param>
|
||||
/// <param name="rootVisual">The container visual to attach child visuals to.</param>
|
||||
/// <param name="size">The initial size of the effect area.</param>
|
||||
void Initialize(Compositor compositor, ContainerVisual rootVisual, Vector2 size);
|
||||
|
||||
/// <summary>
|
||||
/// Starts or resumes all animations.
|
||||
/// </summary>
|
||||
void Start();
|
||||
|
||||
/// <summary>
|
||||
/// Stops all animations (e.g. when the window is hidden).
|
||||
/// </summary>
|
||||
void Stop();
|
||||
|
||||
/// <summary>
|
||||
/// Called when the host element is resized.
|
||||
/// </summary>
|
||||
void Resize(Vector2 newSize);
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:ambienteffects="using:Microsoft.CmdPal.UI.Controls.AmbientEffects"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Dock"
|
||||
@@ -45,5 +46,13 @@
|
||||
TintColor="{x:Bind WindowViewModel.BackgroundImageTint, Mode=OneWay}"
|
||||
TintIntensity="{x:Bind WindowViewModel.BackgroundImageTintIntensity, Mode=OneWay}"
|
||||
Visibility="{x:Bind WindowViewModel.ShowBackgroundImage, Mode=OneWay}" />
|
||||
|
||||
<ambienteffects:AmbientEffectControl
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
DockSide="{x:Bind WindowViewModel.DockSide, Mode=OneWay}"
|
||||
EffectType="{x:Bind WindowViewModel.DockAmbientEffect, Mode=OneWay}"
|
||||
IsDockMode="True"
|
||||
IsHitTestVisible="False" />
|
||||
</Grid>
|
||||
</winuiex:WindowEx>
|
||||
|
||||
@@ -83,7 +83,7 @@ public sealed partial class DockWindow : WindowEx,
|
||||
_themeService = serviceProvider.GetRequiredService<IThemeService>();
|
||||
_themeService.ThemeChanged += ThemeService_ThemeChanged;
|
||||
InitializeBackdropSupport();
|
||||
_windowViewModel = new DockWindowViewModel(_themeService);
|
||||
_windowViewModel = new DockWindowViewModel(_themeService, _settingsService);
|
||||
_dock = new DockControl(viewModel);
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:ambienteffects="using:Microsoft.CmdPal.UI.Controls.AmbientEffects"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:pages="using:Microsoft.CmdPal.UI.Pages"
|
||||
@@ -31,6 +32,12 @@
|
||||
TintIntensity="{x:Bind ViewModel.BackgroundImageTintIntensity, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.ShowBackgroundImage, Mode=OneWay}" />
|
||||
|
||||
<ambienteffects:AmbientEffectControl
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
EffectType="{x:Bind ViewModel.AmbientEffect, Mode=OneWay}"
|
||||
IsHitTestVisible="False" />
|
||||
|
||||
<pages:ShellPage HostWindow="{x:Bind}" />
|
||||
</Grid>
|
||||
</winuiex:WindowEx>
|
||||
|
||||
@@ -134,6 +134,26 @@
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
|
||||
<controls:SettingsCard
|
||||
x:Uid="Settings_Appearance_AmbientEffect_SettingsCard"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.Appearance.AmbientEffectIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="AmbientEffect_Off" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_LavaLamp" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_KittScanner" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_Aurora" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_PulseGlow" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_Spotlight" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_Bars" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_Alchemy" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_Plasma" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_RetroGrid" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_BarsLive" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_AudioGlow" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_Ambience" />
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsExpander
|
||||
x:Uid="Settings_GeneralPage_Background_SettingsExpander"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
|
||||
@@ -104,6 +104,24 @@
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard x:Uid="Settings_Appearance_AmbientEffect_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.DockAppearance.AmbientEffectIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="AmbientEffect_Off" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_LavaLamp" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_KittScanner" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_Aurora" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_PulseGlow" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_Spotlight" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_Bars" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_Alchemy" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_Plasma" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_RetroGrid" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_BarsLive" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_AudioGlow" />
|
||||
<ComboBoxItem x:Uid="AmbientEffect_Ambience" />
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Background / Colorization Section -->
|
||||
<controls:SettingsExpander
|
||||
x:Uid="DockAppearance_Background_SettingsExpander"
|
||||
|
||||
@@ -1158,4 +1158,49 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<value>Right</value>
|
||||
<comment>Right section label in pin to dock dialog (code access, horizontal end)</comment>
|
||||
</data>
|
||||
<data name="Settings_Appearance_AmbientEffect_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Ambient effect</value>
|
||||
</data>
|
||||
<data name="Settings_Appearance_AmbientEffect_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Add an animated background visual effect</value>
|
||||
</data>
|
||||
<data name="AmbientEffect_Off.Content" xml:space="preserve">
|
||||
<value>Off</value>
|
||||
</data>
|
||||
<data name="AmbientEffect_LavaLamp.Content" xml:space="preserve">
|
||||
<value>Lava Lamp</value>
|
||||
</data>
|
||||
<data name="AmbientEffect_KittScanner.Content" xml:space="preserve">
|
||||
<value>KITT Scanner</value>
|
||||
</data>
|
||||
<data name="AmbientEffect_Aurora.Content" xml:space="preserve">
|
||||
<value>Aurora</value>
|
||||
</data>
|
||||
<data name="AmbientEffect_PulseGlow.Content" xml:space="preserve">
|
||||
<value>Pulse Glow</value>
|
||||
</data>
|
||||
<data name="AmbientEffect_Spotlight.Content" xml:space="preserve">
|
||||
<value>Spotlight</value>
|
||||
</data>
|
||||
<data name="AmbientEffect_Bars.Content" xml:space="preserve">
|
||||
<value>Bars & Waves</value>
|
||||
</data>
|
||||
<data name="AmbientEffect_Alchemy.Content" xml:space="preserve">
|
||||
<value>Alchemy</value>
|
||||
</data>
|
||||
<data name="AmbientEffect_Plasma.Content" xml:space="preserve">
|
||||
<value>Plasma</value>
|
||||
</data>
|
||||
<data name="AmbientEffect_RetroGrid.Content" xml:space="preserve">
|
||||
<value>Retro Grid</value>
|
||||
</data>
|
||||
<data name="AmbientEffect_BarsLive.Content" xml:space="preserve">
|
||||
<value>Bars & Waves (Live Audio)</value>
|
||||
</data>
|
||||
<data name="AmbientEffect_AudioGlow.Content" xml:space="preserve">
|
||||
<value>Audio Glow</value>
|
||||
</data>
|
||||
<data name="AmbientEffect_Ambience.Content" xml:space="preserve">
|
||||
<value>Ambience</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
Reference in New Issue
Block a user