Files
QuestPDF/QuestPDF.Previewer/InteractiveCanvas.cs
T
2022-03-21 01:02:53 +01:00

169 lines
4.8 KiB
C#

using System.Diagnostics;
using Avalonia;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Skia;
using QuestPDF.Helpers;
using SkiaSharp;
namespace QuestPDF.Previewer;
class InteractiveCanvas : ICustomDrawOperation
{
public Rect Bounds { get; set; }
public ICollection<RenderedPageInfo> Pages { get; set; }
private float Width => (float)Bounds.Width;
private float Height => (float)Bounds.Height;
public float Scale { get; private set; } = 1;
public float TranslateX { get; private set; }
public float TranslateY { get; private set; }
private const float MinScale = 0.1f;
private const float MaxScale = 10f;
private const float PageSpacing = 50f;
private const float SafeZone = 50f;
private void LimitScale()
{
Scale = Math.Max(Scale, MinScale);
Scale = Math.Min(Scale, MaxScale);
}
private void LimitTranslate()
{
var totalHeight = Pages.Sum(x => x.Size.Height) + (Pages.Count - 1) * PageSpacing;
var maxWidth = Pages.Max(x => x.Size.Width);
var maxTranslateY = SafeZone / Scale;
var minTranslateY = (Height - SafeZone) / Scale - totalHeight;
TranslateY = Math.Min(TranslateY, maxTranslateY);
TranslateY = Math.Max(TranslateY, minTranslateY);
if (maxWidth < Width / Scale)
{
TranslateX = 0;
}
else
{
var maxTranslateX = (Width / 2 - SafeZone) / Scale - maxWidth / 2;
TranslateX = Math.Min(TranslateX, -maxTranslateX);
TranslateX = Math.Max(TranslateX, maxTranslateX);
}
}
public void TranslateWithCurrentScale(float x, float y)
{
TranslateX += x / Scale;
TranslateY += y / Scale;
LimitTranslate();
}
public void ZoomToPoint(float x, float y, float factor)
{
var oldScale = Scale;
Scale *= factor;
LimitScale();
factor = Scale / oldScale;
TranslateX -= x / (oldScale * factor) - x / oldScale;
TranslateY -= y / (oldScale * factor) - y / oldScale;
LimitTranslate();
}
public void Render(IDrawingContextImpl context)
{
if (Pages.Count <= 0)
return;
LimitScale();
LimitTranslate();
var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas;
if (canvas == null)
throw new InvalidOperationException($"Context needs to be ISkiaDrawingContextImpl but got {nameof(context)}");
var originalMatrix = canvas.TotalMatrix;
canvas.Translate(Width / 2, Height / 2);
canvas.Scale(Scale);
canvas.Translate(TranslateX, TranslateY);
foreach (var page in Pages)
{
canvas.Translate(-page.Size.Width / 2f, 0);
DrawPageShadow(canvas, page.Size);
canvas.DrawPicture(page.Picture);
canvas.Translate(page.Size.Width / 2f, page.Size.Height + PageSpacing);
}
canvas.SetMatrix(originalMatrix);
if (TranslateY < 0)
DrawInnerGradient(canvas);
}
public void Dispose() { }
public bool Equals(ICustomDrawOperation? other) => false;
public bool HitTest(Point p) => true;
#region page shadow
private static readonly SKImageFilter PageShadow1 = SKImageFilter.CreateDropShadowOnly(0, 6, 6, 6, SKColors.Black.WithAlpha(64));
private static readonly SKImageFilter PageShadow2 = SKImageFilter.CreateDropShadowOnly(0, 10, 14, 14, SKColors.Black.WithAlpha(32));
private static SKPaint PageShadowPaint = new SKPaint
{
ImageFilter = SKImageFilter.CreateBlendMode(SKBlendMode.Overlay, PageShadow1, PageShadow2)
};
private void DrawPageShadow(SKCanvas canvas, QuestPDF.Infrastructure.Size size)
{
canvas.DrawRect(0, 0, size.Width, size.Height, PageShadowPaint);
}
#endregion
#region inner viewport gradient
private const float InnerGradientSize = 24f;
private static readonly SKColor InnerGradientColor = SKColor.Parse("#666");
private void DrawInnerGradient(SKCanvas canvas)
{
// gamma correction
var colors = Enumerable
.Range(0, 17)
.Select(x => 1f - x / 16f)
.Select(x => Math.Pow(x, 2f))
.Select(x => (byte)(x * 255))
.Select(x => InnerGradientColor.WithAlpha(x))
.ToArray();
using var fogPaint = new SKPaint
{
Shader = SKShader.CreateLinearGradient(
new SKPoint(0, 0),
new SKPoint(0, InnerGradientSize),
colors,
SKShaderTileMode.Clamp)
};
canvas.DrawRect(0, 0, Width, InnerGradientSize, fogPaint);
}
#endregion
}