implement gradient control

This commit is contained in:
DismissedLight
2022-07-17 22:55:20 +08:00
parent f232e072a9
commit f36f555f70
16 changed files with 518 additions and 58 deletions

View File

@@ -0,0 +1,64 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Context.FileSystem.Location;
using Windows.Storage;
namespace Snap.Hutao.Context.FileSystem;
/// <summary>
/// 缓存目录上下文
/// </summary>
[Injection(InjectAs.Transient)]
internal class CacheContext : FileSystemContext
{
/// <summary>
/// 构造一个新的缓存目录上下文
/// </summary>
/// <param name="cache">缓存位置</param>
public CacheContext(Cache cache)
: base(cache)
{
}
/// <summary>
/// 获取缓存文件夹
/// </summary>
public static StorageFolder Folder
{
get => ApplicationData.Current.TemporaryFolder;
}
/// <summary>
/// 获取缓存文件的名称
/// </summary>
/// <param name="uri">uri</param>
/// <returns>缓存文件的名称</returns>
public static string GetCacheFileName(Uri uri)
{
return CreateHash64(uri.ToString()).ToString();
}
/// <summary>
/// 获取缓存文件的名称
/// </summary>
/// <param name="url">url</param>
/// <returns>缓存文件的名称</returns>
public static string GetCacheFileName(string url)
{
return CreateHash64(url).ToString();
}
private static ulong CreateHash64(string str)
{
byte[] utf8 = System.Text.Encoding.UTF8.GetBytes(str);
ulong value = (ulong)utf8.Length;
for (int n = 0; n < utf8.Length; n++)
{
value += (ulong)utf8[n] << ((n * 5) % 56);
}
return value;
}
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Context.FileSystem.Location;
/// <summary>
/// 缓存位置
/// </summary>
[Injection(InjectAs.Transient)]
internal class Cache : IFileSystemLocation
{
private string? path;
/// <inheritdoc/>
public string GetPath()
{
if (string.IsNullOrEmpty(path))
{
path = Windows.Storage.ApplicationData.Current.TemporaryFolder.Path;
}
return path;
}
}

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Context.FileSystem.Location;
/// 我的文档位置
/// </summary>
[Injection(InjectAs.Transient)]
public class Metadata : IFileSystemLocation
internal class Metadata : IFileSystemLocation
{
private string? path;

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Context.FileSystem.Location;
/// 我的文档位置
/// </summary>
[Injection(InjectAs.Transient)]
public class MyDocument : IFileSystemLocation
internal class MyDocument : IFileSystemLocation
{
private string? path;

View File

@@ -16,4 +16,4 @@ internal class MetadataContext : FileSystemContext
: base(metadata)
{
}
}
}

View File

@@ -0,0 +1,140 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Helpers;
using CommunityToolkit.WinUI.UI.Animations;
using Microsoft.UI;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Animation;
using Snap.Hutao.Context.FileSystem;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Extension;
using System.Numerics;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
namespace Snap.Hutao.Control.Image;
/// <summary>
/// 支持渐变的图像
/// </summary>
public class Gradient : Microsoft.UI.Xaml.Controls.Control
{
private static readonly DependencyProperty SourceProperty = Property<Gradient>.Depend(nameof(Source), string.Empty, OnSourceChanged);
private static readonly ConcurrentCancellationTokenSource<Gradient> ImageLoading = new();
private SpriteVisual? spriteVisual;
private double imageAspectRatio;
/// <summary>
/// 构造一个新的渐变图像
/// </summary>
public Gradient()
{
SizeChanged += OnSizeChanged;
}
/// <summary>
/// 源
/// </summary>
public string Source
{
get => (string)GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
private static async Task<StorageFile> GetCachedFileAsync(string url, CancellationToken token)
{
string fileName = CacheContext.GetCacheFileName(url);
CacheContext cacheContext = Ioc.Default.GetRequiredService<CacheContext>();
StorageFile storageFile;
if (!cacheContext.FileExists(fileName))
{
storageFile = await CacheContext.Folder.CreateFileAsync(fileName).AsTask(token);
await StreamHelper.GetHttpStreamToStorageFileAsync(new(url), storageFile);
}
else
{
storageFile = await CacheContext.Folder.GetFileAsync(fileName).AsTask(token);
}
return storageFile;
}
private static void OnSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs arg)
{
Gradient gradient = (Gradient)sender;
string url = (string)arg.NewValue;
gradient.ApplyImageAsync(url, ImageLoading.Register(gradient)).SafeForget();
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
if (e.NewSize != e.PreviousSize && spriteVisual is not null)
{
UpdateVisual(spriteVisual);
}
}
private void UpdateVisual(SpriteVisual spriteVisual)
{
if (spriteVisual is not null)
{
double width = ActualWidth;
double height = Math.Clamp(width * imageAspectRatio, 0, MaxHeight);
spriteVisual.Size = new Vector2((float)width, (float)height);
Height = height;
}
}
private async Task ApplyImageAsync(string url, CancellationToken token)
{
await AnimationBuilder
.Create()
.Opacity(0, 1)
.StartAsync(this, token);
StorageFile storageFile = await GetCachedFileAsync(url, token);
Compositor compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;
LoadedImageSurface imageSurface = await LoadImageSurfaceAsync(storageFile, token);
CompositionSurfaceBrush imageSurfaceBrush = compositor.CompositeSurfaceBrush(imageSurface, stretch: CompositionStretch.UniformToFill, vRatio: 0f);
CompositionLinearGradientBrush backgroundBrush = compositor.CompositeLinearGradientBrush(new(1f, 0), Vector2.UnitY, new(0, Colors.White), new(1, Colors.Black));
CompositionLinearGradientBrush foregroundBrush = compositor.CompositeLinearGradientBrush(Vector2.Zero, Vector2.UnitY, new(0, Colors.White), new(0.95f, Colors.Black));
CompositionEffectBrush gradientEffectBrush = compositor.CompositeBlendEffectBrush(backgroundBrush, foregroundBrush);
CompositionEffectBrush opacityMaskEffectBrush = compositor.CompositeLuminanceToAlphaEffectBrush(gradientEffectBrush);
CompositionEffectBrush alphaMaskEffectBrush = compositor.CompositeAlphaMaskEffectBrush(imageSurfaceBrush, opacityMaskEffectBrush);
spriteVisual = compositor.CompositeSpriteVisual(alphaMaskEffectBrush);
UpdateVisual(spriteVisual);
ElementCompositionPreview.SetElementChildVisual(this, spriteVisual);
await AnimationBuilder
.Create()
.Opacity(1, 0)
.StartAsync(this, token);
}
private async Task<LoadedImageSurface> LoadImageSurfaceAsync(StorageFile storageFile, CancellationToken token)
{
using (IRandomAccessStream imageStream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask(token))
{
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(imageStream).AsTask(token);
imageAspectRatio = (double)decoder.PixelHeight / decoder.PixelWidth;
return LoadedImageSurface.StartLoadFromStream(imageStream);
}
}
}

View File

@@ -22,4 +22,9 @@ internal static class EventIds
/// Forget任务执行异常
/// </summary>
public static readonly EventId TaskException = new(100002, nameof(TaskException));
/// <summary>
/// Forget任务执行异常
/// </summary>
public static readonly EventId AsyncCommandException = new(100003, nameof(AsyncCommandException));
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Collections.Concurrent;
namespace Snap.Hutao.Core.Threading;
/// <summary>
/// 并发<see cref="CancellationTokenSource"/>
/// </summary>
/// <typeparam name="TItem">项类型</typeparam>
internal class ConcurrentCancellationTokenSource<TItem>
where TItem : notnull
{
private readonly ConcurrentDictionary<TItem, CancellationTokenSource> waitingItems = new();
/// <summary>
/// 未某个项注册取消令牌
/// </summary>
/// <param name="item">项</param>
/// <returns>取消令牌</returns>
public CancellationToken Register(TItem item)
{
if (waitingItems.TryRemove(item, out CancellationTokenSource? prevSource))
{
prevSource.Cancel();
}
CancellationTokenSource current = waitingItems.GetOrAdd(item, new CancellationTokenSource());
return current.Token;
}
}

View File

@@ -0,0 +1,154 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.UI.Composition;
using System.Numerics;
namespace Snap.Hutao.Extension;
/// <summary>
/// 合成扩展
/// </summary>
internal static class CompositionExtensions
{
/// <summary>
/// 创建拼合图视觉对象
/// </summary>
/// <param name="compositor">合成器</param>
/// <param name="brush">画刷</param>
/// <returns>拼合图视觉对象</returns>
public static SpriteVisual CompositeSpriteVisual(this Compositor compositor, CompositionBrush brush)
{
SpriteVisual spriteVisual = compositor.CreateSpriteVisual();
spriteVisual.Brush = brush;
return spriteVisual;
}
/// <summary>
/// 创建混合效果画刷
/// </summary>
/// <param name="compositor">合成器</param>
/// <param name="background">前景</param>
/// <param name="foreground">背景</param>
/// <param name="blendEffectMode">混合模式</param>
/// <returns>合成效果画刷</returns>
public static CompositionEffectBrush CompositeBlendEffectBrush(
this Compositor compositor,
CompositionBrush background,
CompositionBrush foreground,
BlendEffectMode blendEffectMode = BlendEffectMode.Multiply)
{
BlendEffect effect = new()
{
Background = new CompositionEffectSourceParameter("Background"),
Foreground = new CompositionEffectSourceParameter("Foreground"),
Mode = blendEffectMode,
};
CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush();
brush.SetSourceParameter("Background", background);
brush.SetSourceParameter("Foreground", foreground);
return brush;
}
/// <summary>
/// 创建亮度转不透明度效果画刷
/// </summary>
/// <param name="compositor">合成器</param>
/// <param name="sourceBrush">源</param>
/// <returns>合成效果画刷</returns>
public static CompositionEffectBrush CompositeLuminanceToAlphaEffectBrush(this Compositor compositor, CompositionBrush sourceBrush)
{
LuminanceToAlphaEffect effect = new()
{
Source = new CompositionEffectSourceParameter("Source"),
};
CompositionEffectBrush brush = compositor.CreateEffectFactory(effect).CreateBrush();
brush.SetSourceParameter("Source", sourceBrush);
return brush;
}
/// <summary>
/// 创建不透明度蒙版效果画刷
/// </summary>
/// <param name="compositor">合成器</param>
/// <param name="sourceBrush">源</param>
/// <param name="alphaMask">不透明度蒙版</param>
/// <returns>合成效果画刷</returns>
public static CompositionEffectBrush CompositeAlphaMaskEffectBrush(
this Compositor compositor,
CompositionBrush sourceBrush,
CompositionBrush alphaMask)
{
AlphaMaskEffect maskEffect = new()
{
AlphaMask = new CompositionEffectSourceParameter("AlphaMask"),
Source = new CompositionEffectSourceParameter("Source"),
};
CompositionEffectBrush brush = compositor.CreateEffectFactory(maskEffect).CreateBrush();
brush.SetSourceParameter("AlphaMask", alphaMask);
brush.SetSourceParameter("Source", sourceBrush);
return brush;
}
/// <summary>
/// 创建一个表面画刷
/// </summary>
/// <param name="compositor">合成器</param>
/// <param name="surface">合成表面</param>
/// <param name="stretch">拉伸方法</param>
/// <param name="hRatio">水平对齐比</param>
/// <param name="vRatio">垂直对齐比</param>
/// <returns>合成表面画刷</returns>
public static CompositionSurfaceBrush CompositeSurfaceBrush(
this Compositor compositor,
ICompositionSurface surface,
CompositionStretch stretch = CompositionStretch.None,
float hRatio = 0.5f,
float vRatio = 0.5f)
{
CompositionSurfaceBrush brush = compositor.CreateSurfaceBrush(surface);
brush.Stretch = stretch;
brush.VerticalAlignmentRatio = vRatio;
brush.HorizontalAlignmentRatio = hRatio;
return brush;
}
/// <summary>
/// 创建一个线性渐变画刷
/// </summary>
/// <param name="compositor">合成器</param>
/// <param name="start">起点</param>
/// <param name="end">终点</param>
/// <param name="stops">锚点</param>
/// <returns>线性渐变画刷</returns>
public static CompositionLinearGradientBrush CompositeLinearGradientBrush(
this Compositor compositor,
Vector2 start,
Vector2 end,
params GradientStop[] stops)
{
CompositionLinearGradientBrush brush = compositor.CreateLinearGradientBrush();
brush.StartPoint = start;
brush.EndPoint = end;
foreach (GradientStop stop in stops)
{
brush.ColorStops.Add(compositor.CreateColorGradientStop(stop.Offset, stop.Color));
}
return brush;
}
public record struct GradientStop(float Offset, Windows.UI.Color Color);
}

View File

@@ -24,6 +24,9 @@ public static class TaskExtensions
{
await task.ConfigureAwait(continueOnCapturedContext);
}
catch (TaskCanceledException)
{
}
catch (Exception e)
{
logger?.LogError(EventIds.TaskException, e, "{caller}:{exception}", nameof(SafeForget), e.GetBaseException());

View File

@@ -2,7 +2,7 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Input;
using Microsoft.AppCenter.Crashes;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Factory.Abstraction;
namespace Snap.Hutao.Factory;
@@ -93,8 +93,7 @@ internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
if (asyncRelayCommand.ExecutionTask?.Exception is AggregateException exception)
{
Exception baseException = exception.GetBaseException();
logger.LogError(baseException, "异步命令发生了错误");
Crashes.TrackError(baseException);
logger.LogError(EventIds.AsyncCommandException, baseException, "异步命令发生了错误");
}
}
}

View File

@@ -31,10 +31,10 @@ internal static class IocHttpClientConfiguration
services.AddHttpClient<MetadataService>(DefaultConfiguration);
// normal clients
services.AddHttpClient<HutaoClient>(DefaultConfiguration);
services.AddHttpClient<AnnouncementClient>(DefaultConfiguration);
services.AddHttpClient<UserGameRoleClient>(DefaultConfiguration);
services.AddHttpClient<EnkaClient>(DefaultConfiguration);
services.AddHttpClient<HutaoClient>(DefaultConfiguration);
services.AddHttpClient<UserGameRoleClient>(DefaultConfiguration);
// x-rpc clients
services.AddHttpClient<GameRecordClient>(XRpcConfiguration);

View File

@@ -0,0 +1,33 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 角色名片转换器
/// </summary>
internal class AvatarNameCardPicConverter : IValueConverter
{
private const string BaseUrl = "https://static.snapgenshin.com/NameCardPic/UI_NameCardPic_{0}_P.png";
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value == null)
{
return null!;
}
Avatar.Avatar avatar = (Avatar.Avatar)value;
string avatarName = avatar.Icon[14..];
return new Uri(string.Format(BaseUrl, avatarName));
}
/// <inheritdoc/>
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw Must.NeverHappen();
}
}

View File

@@ -9,7 +9,7 @@
<Identity
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
Publisher="CN=DGP Studio"
Version="1.0.11.0" />
Version="1.0.13.0" />
<Properties>
<DisplayName>胡桃</DisplayName>

View File

@@ -71,10 +71,9 @@
<PackageReference Include="CommunityToolkit.WinUI.UI.Behaviors" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Media" Version="7.1.2" />
<PackageReference Include="Microsoft.AppCenter.Analytics" Version="4.5.1" />
<PackageReference Include="Microsoft.AppCenter.Crashes" Version="4.5.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.7" />
<!-- The PrivateAssets & IncludeAssets of Microsoft.EntityFrameworkCore.Tools should be remove to prevent multiple deps file-->
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.7" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />

View File

@@ -5,7 +5,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:cwuc="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:cwum="using:CommunityToolkit.WinUI.UI.Media"
xmlns:mxic="using:Microsoft.Xaml.Interactions.Core"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:shc="using:Snap.Hutao.Control"
@@ -27,6 +26,7 @@
<shmmc:AvatarSideIconConverter x:Key="AvatarSideIconConverter"/>
<shmmc:ElementNameIconConverter x:Key="ElementNameIconConverter"/>
<shmmc:WeaponTypeIconConverter x:Key="WeaponTypeIconConverter"/>
<shmmc:AvatarNameCardPicConverter x:Key="AvatarNameCardPicConverter"/>
<shmmc:FightPropertyConverter x:Key="FightPropertyConverter"/>
<shmmc:FightPropertyValueFormatter x:Key="FightPropertyValueFormatter"/>
@@ -144,44 +144,48 @@
</Expander>
</DataTemplate>
</Page.Resources>
<Grid>
<SplitView
IsPaneOpen="True"
DisplayMode="Inline"
OpenPaneLength="200">
<SplitView.PaneBackground>
<SolidColorBrush Color="{StaticResource CardBackgroundFillColorSecondary}"/>
</SplitView.PaneBackground>
<SplitView.Pane>
<ListView
SelectionMode="Single"
ItemsSource="{Binding Avatars}"
SelectedItem="{Binding Selected,Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<shci:CachedImage
Grid.Column="0"
Width="48"
Height="48"
Margin="0,0,12,12"
Source="{Binding SideIcon,Converter={StaticResource AvatarSideIconConverter},Mode=OneWay}"/>
<TextBlock
VerticalAlignment="Center"
Grid.Column="1"
Margin="12,0,0,0"
Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</SplitView.Pane>
<SplitView.Content>
<SplitView
IsPaneOpen="True"
DisplayMode="Inline"
OpenPaneLength="200">
<SplitView.PaneBackground>
<SolidColorBrush Color="{StaticResource CardBackgroundFillColorSecondary}"/>
</SplitView.PaneBackground>
<SplitView.Pane>
<ListView
SelectionMode="Single"
ItemsSource="{Binding Avatars}"
SelectedItem="{Binding Selected,Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<shci:CachedImage
Grid.Column="0"
Width="48"
Height="48"
Margin="0,0,12,12"
Source="{Binding SideIcon,Converter={StaticResource AvatarSideIconConverter},Mode=OneWay}"/>
<TextBlock
VerticalAlignment="Center"
Grid.Column="1"
Margin="12,0,0,0"
Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</SplitView.Pane>
<SplitView.Content>
<Grid>
<shci:Gradient
VerticalAlignment="Top"
Source="{Binding Selected,Converter={StaticResource AvatarNameCardPicConverter}}">
</shci:Gradient>
<ScrollViewer>
<StackPanel Margin="0,0,16,16">
<!--简介-->
@@ -391,7 +395,7 @@
</ItemsControl>
</Grid>
</ScrollViewer>
</Expander>
<TextBlock Text="天赋" Style="{StaticResource BaseTextBlockStyle}" Margin="16,16,0,0"/>
<ItemsControl
@@ -407,9 +411,9 @@
<ItemsControl
ItemsSource="{Binding Selected.SkillDepot.Inherents}"
ItemTemplate="{StaticResource InherentDataTemplate}"/>
<TextBlock Text="命之座" Style="{StaticResource BaseTextBlockStyle}" Margin="16,16,0,0"/>
<ItemsControl
ItemsSource="{Binding Selected.SkillDepot.Talents}"
ItemTemplate="{StaticResource InherentDataTemplate}"/>
@@ -494,7 +498,7 @@
</Expander>
</StackPanel>
</ScrollViewer>
</SplitView.Content>
</SplitView>
</Grid>
</Grid>
</SplitView.Content>
</SplitView>
</Page>