Compare commits

...

1 Commits

Author SHA1 Message Date
Niels Laute
85bfc399f9 Animations everywhere 2026-03-31 21:06:58 +02:00
31 changed files with 4163 additions and 3 deletions

View File

@@ -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,
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -89,6 +89,8 @@ public record SettingsModel
public int BackdropOpacity { get; init; } = 100;
public AmbientEffectType AmbientEffect { get; init; }
// </Theme settings>
// END SETTINGS

View File

@@ -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();
}
}
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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,
};
}
}

View File

@@ -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();
}
}
}

View File

@@ -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),
};
}
}

View File

@@ -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),
};
}
}

View File

@@ -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);
}
}

View File

@@ -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 &amp; 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();
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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();

View File

@@ -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>

View File

@@ -134,6 +134,26 @@
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<controls:SettingsCard
x:Uid="Settings_Appearance_AmbientEffect_SettingsCard"
HeaderIcon="{ui:FontIcon Glyph=&#xE7F4;}">
<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=&#xE790;}"

View File

@@ -104,6 +104,24 @@
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="Settings_Appearance_AmbientEffect_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE7F4;}">
<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"

View File

@@ -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 &amp; 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 &amp; 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>