first commit
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Processor barrier recorded in a drawing backend timeline.
|
||||
/// </summary>
|
||||
internal sealed class ApplyBarrier
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ApplyBarrier"/> class.
|
||||
/// </summary>
|
||||
/// <param name="path">The closed path defining the processed region.</param>
|
||||
/// <param name="options">The drawing options captured when the barrier was recorded.</param>
|
||||
/// <param name="clipPaths">The active clip paths captured when the barrier was recorded.</param>
|
||||
/// <param name="canvasBounds">The canvas-local bounds captured when the barrier was recorded.</param>
|
||||
/// <param name="targetBounds">The absolute target bounds captured when the barrier was recorded.</param>
|
||||
/// <param name="destinationOffset">The absolute destination offset captured when the barrier was recorded.</param>
|
||||
/// <param name="isInsideLayer">Indicates whether the barrier was recorded inside a layer.</param>
|
||||
/// <param name="operation">The processor operation to run against the replay-time snapshot.</param>
|
||||
internal ApplyBarrier(
|
||||
IPath path,
|
||||
DrawingOptions options,
|
||||
IReadOnlyList<IPath> clipPaths,
|
||||
Rectangle canvasBounds,
|
||||
Rectangle targetBounds,
|
||||
Point destinationOffset,
|
||||
bool isInsideLayer,
|
||||
Action<IImageProcessingContext> operation)
|
||||
{
|
||||
this.Path = path;
|
||||
this.Options = options;
|
||||
this.ClipPaths = clipPaths;
|
||||
this.CanvasBounds = canvasBounds;
|
||||
this.TargetBounds = targetBounds;
|
||||
this.DestinationOffset = destinationOffset;
|
||||
this.IsInsideLayer = isInsideLayer;
|
||||
this.Operation = operation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the closed path defining the processed region.
|
||||
/// </summary>
|
||||
public IPath Path { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the drawing options captured when the barrier was recorded.
|
||||
/// </summary>
|
||||
public DrawingOptions Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active clip paths captured when the barrier was recorded.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IPath> ClipPaths { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the canvas-local bounds captured when the barrier was recorded.
|
||||
/// </summary>
|
||||
public Rectangle CanvasBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute target bounds captured when the barrier was recorded.
|
||||
/// </summary>
|
||||
public Rectangle TargetBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute destination offset captured when the barrier was recorded.
|
||||
/// </summary>
|
||||
public Point DestinationOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the barrier was recorded inside a layer.
|
||||
/// </summary>
|
||||
public bool IsInsideLayer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the processor operation to run against the replay-time snapshot.
|
||||
/// </summary>
|
||||
public Action<IImageProcessingContext> Operation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates the transient image-brush draw command that writes this barrier's processed snapshot back to the target.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="backend">The backend used to read the replay-time target pixels.</param>
|
||||
/// <param name="target">The target frame.</param>
|
||||
/// <param name="ownedResource">The image resource that must stay alive while the returned command batch is rendered.</param>
|
||||
/// <returns>The transient write-back command batch, or <see langword="null"/> when the barrier has no target coverage.</returns>
|
||||
public DrawingCommandBatch? CreateWriteBackBatch<TPixel>(
|
||||
Configuration configuration,
|
||||
IDrawingBackend backend,
|
||||
ICanvasFrame<TPixel> target,
|
||||
out IDisposable? ownedResource)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
RectangleF rawBounds = RectangleF.Transform(this.Path.Bounds, this.Options.Transform);
|
||||
Rectangle sourceRect = ToConservativeBounds(rawBounds);
|
||||
sourceRect = Rectangle.Intersect(this.CanvasBounds, sourceRect);
|
||||
|
||||
if (sourceRect.Width <= 0 || sourceRect.Height <= 0)
|
||||
{
|
||||
ownedResource = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
Image<TPixel> sourceImage = new(configuration, sourceRect.Width, sourceRect.Height);
|
||||
try
|
||||
{
|
||||
backend.ReadRegion(
|
||||
configuration,
|
||||
target,
|
||||
sourceRect,
|
||||
sourceImage.Frames.RootFrame.PixelBuffer.GetRegion());
|
||||
|
||||
sourceImage.Mutate(this.Operation);
|
||||
|
||||
Point brushOffset = new(
|
||||
sourceRect.X - (int)MathF.Floor(rawBounds.Left),
|
||||
sourceRect.Y - (int)MathF.Floor(rawBounds.Top));
|
||||
|
||||
ImageBrush<TPixel> brush = new(sourceImage, sourceImage.Bounds, brushOffset);
|
||||
GraphicsOptions graphicsOptions = this.Options.GraphicsOptions;
|
||||
RasterizationMode rasterizationMode = graphicsOptions.Antialias
|
||||
? RasterizationMode.Antialiased
|
||||
: RasterizationMode.Aliased;
|
||||
|
||||
RectangleF pathBounds = this.Path.Bounds;
|
||||
Rectangle interest = Rectangle.FromLTRB(
|
||||
(int)MathF.Floor(pathBounds.Left),
|
||||
(int)MathF.Floor(pathBounds.Top),
|
||||
(int)MathF.Ceiling(pathBounds.Right),
|
||||
(int)MathF.Ceiling(pathBounds.Bottom));
|
||||
|
||||
RasterizerOptions rasterizerOptions = new(
|
||||
interest,
|
||||
this.Options.ShapeOptions.IntersectionRule,
|
||||
rasterizationMode,
|
||||
RasterizerSamplingOrigin.PixelBoundary,
|
||||
graphicsOptions.AntialiasThreshold);
|
||||
|
||||
CompositionCommand command = CompositionCommand.Create(
|
||||
this.Path,
|
||||
brush,
|
||||
this.Options,
|
||||
in rasterizerOptions,
|
||||
this.TargetBounds,
|
||||
this.DestinationOffset,
|
||||
this.ClipPaths,
|
||||
this.IsInsideLayer);
|
||||
|
||||
ownedResource = sourceImage;
|
||||
CompositionSceneCommand[] commands = [new PathCompositionSceneCommand(command)];
|
||||
return new DrawingCommandBatch(commands, hasLayers: false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
sourceImage.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static Rectangle ToConservativeBounds(RectangleF bounds)
|
||||
=> Rectangle.FromLTRB(
|
||||
(int)MathF.Floor(bounds.Left),
|
||||
(int)MathF.Floor(bounds.Top),
|
||||
(int)MathF.Ceiling(bounds.Right),
|
||||
(int)MathF.Ceiling(bounds.Bottom));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Frame adapter that exposes a clipped subregion of another frame.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
internal sealed class CanvasRegionFrame<TPixel> : ICanvasFrame<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly ICanvasFrame<TPixel> parent;
|
||||
private readonly Rectangle region;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CanvasRegionFrame{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="parent">The parent frame that owns the target pixels.</param>
|
||||
/// <param name="region">The child region in parent-local coordinates.</param>
|
||||
public CanvasRegionFrame(ICanvasFrame<TPixel> parent, Rectangle region)
|
||||
{
|
||||
Guard.NotNull(parent, nameof(parent));
|
||||
Guard.MustBeGreaterThanOrEqualTo(region.Width, 0, nameof(region));
|
||||
Guard.MustBeGreaterThanOrEqualTo(region.Height, 0, nameof(region));
|
||||
|
||||
this.parent = parent;
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Rectangle Bounds => new(
|
||||
this.parent.Bounds.X + this.region.X,
|
||||
this.parent.Bounds.Y + this.region.Y,
|
||||
this.region.Width,
|
||||
this.region.Height);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetCpuRegion(out Buffer2DRegion<TPixel> region)
|
||||
{
|
||||
if (!this.parent.TryGetCpuRegion(out Buffer2DRegion<TPixel> parentRegion))
|
||||
{
|
||||
region = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
region = parentRegion.GetSubRegion(this.region);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface)
|
||||
=> this.parent.TryGetNativeSurface(out surface);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Identifies the flush-time role carried by a <see cref="CompositionCommand"/>.
|
||||
/// </summary>
|
||||
public enum CompositionCommandKind : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// A fill-path command.
|
||||
/// </summary>
|
||||
FillLayer = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Starts an isolated compositing layer.
|
||||
/// </summary>
|
||||
BeginLayer = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Ends the most recently opened layer.
|
||||
/// </summary>
|
||||
EndLayer = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One normalized fill-path or layer-based composition command queued for backend execution.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This type carries fill-path commands plus inline layer boundaries.
|
||||
/// </remarks>
|
||||
public readonly struct CompositionCommand
|
||||
{
|
||||
private readonly IPath? sourcePath;
|
||||
private readonly Brush? brush;
|
||||
private readonly DrawingOptions? drawingOptions;
|
||||
private readonly GraphicsOptions? layerGraphicsOptions;
|
||||
private readonly IReadOnlyList<IPath>? clipPaths;
|
||||
|
||||
private CompositionCommand(
|
||||
CompositionCommandKind kind,
|
||||
IPath? sourcePath,
|
||||
Brush? brush,
|
||||
DrawingOptions? drawingOptions,
|
||||
GraphicsOptions? layerGraphicsOptions,
|
||||
in RasterizerOptions rasterizerOptions,
|
||||
Rectangle targetBounds,
|
||||
Rectangle layerBounds,
|
||||
Point destinationOffset,
|
||||
IReadOnlyList<IPath>? clipPaths,
|
||||
bool isInsideLayer)
|
||||
{
|
||||
this.Kind = kind;
|
||||
this.sourcePath = sourcePath;
|
||||
this.brush = brush;
|
||||
this.drawingOptions = drawingOptions;
|
||||
this.layerGraphicsOptions = layerGraphicsOptions;
|
||||
this.RasterizerOptions = rasterizerOptions;
|
||||
this.TargetBounds = targetBounds;
|
||||
this.LayerBounds = layerBounds;
|
||||
this.DestinationOffset = destinationOffset;
|
||||
this.clipPaths = clipPaths;
|
||||
this.IsInsideLayer = isInsideLayer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command kind.
|
||||
/// </summary>
|
||||
public CompositionCommandKind Kind { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute bounds of the logical target for this command.
|
||||
/// </summary>
|
||||
public Rectangle TargetBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute bounds of the layer opened by this command.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only meaningful for <see cref="CompositionCommandKind.BeginLayer"/> and
|
||||
/// <see cref="CompositionCommandKind.EndLayer"/>.
|
||||
/// </remarks>
|
||||
public Rectangle LayerBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brush used during composition.
|
||||
/// </summary>
|
||||
public Brush Brush => this.brush ?? throw new InvalidOperationException("Layer commands do not carry a brush.");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the drawing options carried by the command.
|
||||
/// </summary>
|
||||
public DrawingOptions DrawingOptions => this.drawingOptions ?? throw new InvalidOperationException("Layer commands do not carry drawing options.");
|
||||
|
||||
/// <summary>
|
||||
/// Gets graphics options used for composition or layer compositing.
|
||||
/// </summary>
|
||||
public GraphicsOptions GraphicsOptions => this.drawingOptions?.GraphicsOptions ?? this.layerGraphicsOptions!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets rasterizer options used to generate coverage.
|
||||
/// </summary>
|
||||
public RasterizerOptions RasterizerOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute destination offset where the local coverage should be composited.
|
||||
/// </summary>
|
||||
public Point DestinationOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source path carried by the command.
|
||||
/// </summary>
|
||||
public IPath SourcePath => this.sourcePath ?? throw new InvalidOperationException("Layer commands do not carry path geometry.");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command transform.
|
||||
/// </summary>
|
||||
public Matrix4x4 Transform => this.drawingOptions?.Transform ?? Matrix4x4.Identity;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the clip paths carried by the command.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IPath>? ClipPaths => this.clipPaths;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shape options carried by the command.
|
||||
/// </summary>
|
||||
public ShapeOptions ShapeOptions => this.drawingOptions?.ShapeOptions ?? throw new InvalidOperationException("Layer commands do not carry shape options.");
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the command was recorded inside a layer.
|
||||
/// </summary>
|
||||
public bool IsInsideLayer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a fill-path composition command.
|
||||
/// </summary>
|
||||
/// <param name="path">Path in target-local coordinates.</param>
|
||||
/// <param name="brush">Brush used during composition.</param>
|
||||
/// <param name="drawingOptions">Drawing options (graphics, shape, transform) used during composition.</param>
|
||||
/// <param name="rasterizerOptions">Rasterizer options used to generate coverage.</param>
|
||||
/// <param name="targetBounds">The absolute bounds of the logical target for this command.</param>
|
||||
/// <param name="destinationOffset">Absolute destination offset where coverage is composited.</param>
|
||||
/// <param name="clipPaths">Optional clip paths supplied with the command.</param>
|
||||
/// <param name="isInsideLayer">True if the command was recorded inside a layer.</param>
|
||||
/// <returns>The composition command.</returns>
|
||||
public static CompositionCommand Create(
|
||||
IPath path,
|
||||
Brush brush,
|
||||
DrawingOptions drawingOptions,
|
||||
in RasterizerOptions rasterizerOptions,
|
||||
Rectangle targetBounds,
|
||||
Point destinationOffset,
|
||||
IReadOnlyList<IPath>? clipPaths,
|
||||
bool isInsideLayer)
|
||||
=> new(
|
||||
CompositionCommandKind.FillLayer,
|
||||
path,
|
||||
brush,
|
||||
drawingOptions,
|
||||
null,
|
||||
in rasterizerOptions,
|
||||
targetBounds,
|
||||
default,
|
||||
destinationOffset,
|
||||
clipPaths,
|
||||
isInsideLayer);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a begin-layer composition command. <see cref="IsInsideLayer"/> is false on the
|
||||
/// BeginLayer marker itself; the flag is only meaningful for fills/strokes that follow it.
|
||||
/// </summary>
|
||||
/// <param name="layerBounds">The absolute bounds of the layer.</param>
|
||||
/// <param name="graphicsOptions">The compositing options used when the layer closes.</param>
|
||||
/// <returns>The begin-layer command.</returns>
|
||||
public static CompositionCommand CreateBeginLayer(Rectangle layerBounds, GraphicsOptions graphicsOptions)
|
||||
=> new(
|
||||
CompositionCommandKind.BeginLayer,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
graphicsOptions,
|
||||
default,
|
||||
layerBounds,
|
||||
layerBounds,
|
||||
default,
|
||||
null,
|
||||
false);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an end-layer composition command. <see cref="IsInsideLayer"/> is false on the
|
||||
/// EndLayer marker itself; the flag is only meaningful for fills/strokes that preceded it.
|
||||
/// </summary>
|
||||
/// <param name="layerBounds">The absolute bounds of the layer being closed.</param>
|
||||
/// <param name="graphicsOptions">The compositing options used by the layer.</param>
|
||||
/// <returns>The end-layer command.</returns>
|
||||
public static CompositionCommand CreateEndLayer(Rectangle layerBounds, GraphicsOptions graphicsOptions)
|
||||
=> new(
|
||||
CompositionCommandKind.EndLayer,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
graphicsOptions,
|
||||
default,
|
||||
layerBounds,
|
||||
layerBounds,
|
||||
default,
|
||||
null,
|
||||
false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
#pragma warning disable SA1649 // Scene command types are grouped together in one file.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Visitor contract for one flush-scoped composition scene command.
|
||||
/// </summary>
|
||||
public interface ICompositionSceneCommandVisitor
|
||||
{
|
||||
/// <summary>
|
||||
/// Visits one fill-path or layer-based composition command.
|
||||
/// </summary>
|
||||
/// <param name="command">The command being visited.</param>
|
||||
public void Visit(PathCompositionSceneCommand command);
|
||||
|
||||
/// <summary>
|
||||
/// Visits one stroked path command.
|
||||
/// </summary>
|
||||
/// <param name="command">The command being visited.</param>
|
||||
public void Visit(StrokePathCompositionSceneCommand command);
|
||||
|
||||
/// <summary>
|
||||
/// Visits one explicit stroked line-segment command.
|
||||
/// </summary>
|
||||
/// <param name="command">The command being visited.</param>
|
||||
public void Visit(LineSegmentCompositionSceneCommand command);
|
||||
|
||||
/// <summary>
|
||||
/// Visits one explicit stroked polyline command.
|
||||
/// </summary>
|
||||
/// <param name="command">The command being visited.</param>
|
||||
public void Visit(PolylineCompositionSceneCommand command);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base type for one draw-order command in a flush-scoped scene stream.
|
||||
/// </summary>
|
||||
public abstract class CompositionSceneCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Dispatches the command to a visitor without a per-item kind switch at the call site.
|
||||
/// </summary>
|
||||
/// <param name="visitor">The visitor receiving the command.</param>
|
||||
public abstract void Accept(ICompositionSceneCommandVisitor visitor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scene command wrapper for fill-path and layer-based composition commands.
|
||||
/// </summary>
|
||||
public sealed class PathCompositionSceneCommand : CompositionSceneCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PathCompositionSceneCommand"/> class.
|
||||
/// </summary>
|
||||
/// <param name="command">The wrapped composition command.</param>
|
||||
public PathCompositionSceneCommand(in CompositionCommand command)
|
||||
=> this.Command = command;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the wrapped composition command.
|
||||
/// </summary>
|
||||
public CompositionCommand Command { get; internal set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(ICompositionSceneCommandVisitor visitor) => visitor.Visit(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scene command wrapper for stroked path commands.
|
||||
/// </summary>
|
||||
public sealed class StrokePathCompositionSceneCommand : CompositionSceneCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StrokePathCompositionSceneCommand"/> class.
|
||||
/// </summary>
|
||||
/// <param name="command">The wrapped stroke path command.</param>
|
||||
public StrokePathCompositionSceneCommand(in StrokePathCommand command)
|
||||
=> this.Command = command;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the wrapped stroke path command.
|
||||
/// </summary>
|
||||
public StrokePathCommand Command { get; internal set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(ICompositionSceneCommandVisitor visitor) => visitor.Visit(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scene command wrapper for explicit stroked line-segment commands.
|
||||
/// </summary>
|
||||
public sealed class LineSegmentCompositionSceneCommand : CompositionSceneCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LineSegmentCompositionSceneCommand"/> class.
|
||||
/// </summary>
|
||||
/// <param name="command">The wrapped stroke line-segment command.</param>
|
||||
public LineSegmentCompositionSceneCommand(in StrokeLineSegmentCommand command)
|
||||
=> this.Command = command;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the wrapped stroke line-segment command.
|
||||
/// </summary>
|
||||
public StrokeLineSegmentCommand Command { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(ICompositionSceneCommandVisitor visitor) => visitor.Visit(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scene command wrapper for explicit stroked polyline commands.
|
||||
/// </summary>
|
||||
public sealed class PolylineCompositionSceneCommand : CompositionSceneCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PolylineCompositionSceneCommand"/> class.
|
||||
/// </summary>
|
||||
/// <param name="command">The wrapped stroke polyline command.</param>
|
||||
public PolylineCompositionSceneCommand(in StrokePolylineCommand command)
|
||||
=> this.Command = command;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the wrapped stroke polyline command.
|
||||
/// </summary>
|
||||
public StrokePolylineCommand Command { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(ICompositionSceneCommandVisitor visitor) => visitor.Visit(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
# DefaultDrawingBackend
|
||||
|
||||
`DefaultDrawingBackend` is the CPU execution backend for ImageSharp.Drawing. It creates retained CPU scenes from prepared drawing command batches, executes those scenes with reusable scratch, and writes the result into a CPU destination buffer.
|
||||
|
||||
This document explains the backend as a system rather than as a list of methods. The goal is to help a newcomer understand:
|
||||
|
||||
- where the CPU backend fits in the canvas/backend selection model
|
||||
- what problem the CPU backend is solving
|
||||
- why the backend is organized around a retained row-oriented execution plan
|
||||
- what `FlushScene` means in this architecture
|
||||
- how rasterization, brush application, and layer composition fit together
|
||||
|
||||
## Where The CPU Backend Fits
|
||||
|
||||
`DefaultDrawingBackend` is the standard CPU execution path behind `DrawingCanvas`.
|
||||
|
||||
The canvas architecture reaches this backend in two common ways:
|
||||
|
||||
- ordinary typed canvas construction resolves `IDrawingBackend` from `Configuration`
|
||||
- specialized infrastructure can construct a canvas with an explicit backend instance
|
||||
|
||||
The CPU path usually uses the first route. The WebGPU helpers use the second route when they need a canvas that targets a native surface through `WebGPUDrawingBackend`.
|
||||
|
||||
That means the CPU backend is one backend implementation within the shared canvas architecture, not a separate public drawing model. It executes against any frame that exposes a writable CPU region, whether that frame is pure memory or a hybrid frame that also carries a native surface.
|
||||
|
||||
## The Main Problem
|
||||
|
||||
By the time work reaches `DefaultDrawingBackend`, the public drawing API has already been normalized into prepared commands. That is helpful, but it does not make CPU execution trivial.
|
||||
|
||||
The backend still has to solve a hard scheduling problem.
|
||||
|
||||
It needs to answer questions such as:
|
||||
|
||||
- which destination rows each command touches
|
||||
- how to preserve draw order while running work in parallel
|
||||
- how to avoid re-deriving geometry information in the hot loop
|
||||
- where temporary memory should live and when it should be reused
|
||||
|
||||
If the CPU backend executed commands directly from the incoming scene, each worker would repeatedly rediscover which rows matter, which parts of the geometry matter in those rows, and how much scratch is needed. That would push expensive planning work into the hottest part of the pipeline.
|
||||
|
||||
So the backend takes a different approach:
|
||||
|
||||
it turns the whole command batch into a row-oriented execution plan first, then executes that plan.
|
||||
|
||||
That decision explains most of the backend architecture.
|
||||
|
||||
## The Core Idea
|
||||
|
||||
The CPU backend is a flush executor, not a command-at-a-time painter.
|
||||
|
||||
Its central idea is:
|
||||
|
||||
> convert a command batch into row-local raster work once, then execute rows directly with reusable worker-local scratch
|
||||
|
||||
That is why the backend is built around `FlushScene`.
|
||||
|
||||
`FlushScene` is a retained execution plan. In non-retained rendering it is short-lived and disposed after one replay entry; in retained rendering it can live with the returned `DefaultDrawingBackendScene`. Its job is to take a prepared command stream and reorganize it into a form that is cheap for the row executor to consume.
|
||||
|
||||
If that idea is clear, most of the important types fall into place.
|
||||
|
||||
## The Most Important Terms
|
||||
|
||||
### Backend
|
||||
|
||||
`DefaultDrawingBackend` is the top-level CPU executor. It owns backend policy and orchestration:
|
||||
|
||||
- acquiring a writable CPU destination
|
||||
- creating the retained execution plan
|
||||
- executing that plan
|
||||
- handling CPU layer composition
|
||||
|
||||
It does not own every detail of geometry planning or scan conversion.
|
||||
|
||||
It also does not own backend selection. By the time `CreateScene(...)` or `RenderScene(...)` is called, the typed canvas implementation has already chosen the backend instance that will receive the prepared work.
|
||||
|
||||
### Scene
|
||||
|
||||
In the canvas architecture, the backend receives a `DrawingCommandBatch`. That batch already contains prepared commands and explicit layer boundaries for one contiguous command range.
|
||||
|
||||
For the CPU backend, that incoming batch is the starting point, not the final execution form.
|
||||
|
||||
### Flush Scene
|
||||
|
||||
`FlushScene` is the most important supporting type in the CPU backend.
|
||||
|
||||
In this codebase, `FlushScene` means:
|
||||
|
||||
"the retained, row-oriented execution plan for one CPU command batch"
|
||||
|
||||
It owns the retained information needed to make execution cheap:
|
||||
|
||||
- the visible prepared commands
|
||||
- retained rasterizable geometry
|
||||
- row membership
|
||||
- row-local execution items
|
||||
- scratch size requirements for the flush
|
||||
|
||||
### Rasterizer
|
||||
|
||||
`DefaultRasterizer` is the geometry-to-coverage engine.
|
||||
|
||||
It is responsible for:
|
||||
|
||||
- fixed-point scan conversion
|
||||
- fill-rule handling
|
||||
- coverage accumulation
|
||||
- emitting row coverage spans
|
||||
|
||||
It is not responsible for deciding which commands should run in which rows, and it does not write final pixels directly.
|
||||
|
||||
### Brush Renderer
|
||||
|
||||
`BrushRenderer<TPixel>` is the coverage-to-color engine for one prepared drawing command.
|
||||
|
||||
It receives:
|
||||
|
||||
- a destination row slice
|
||||
- coverage data
|
||||
- destination position
|
||||
- reusable workspace
|
||||
|
||||
and updates pixels accordingly.
|
||||
|
||||
The important separation is:
|
||||
|
||||
- the rasterizer decides coverage
|
||||
- the brush renderer decides color
|
||||
- the backend executor binds the two together
|
||||
|
||||
### Worker State
|
||||
|
||||
`WorkerState<TPixel>` is the reusable per-worker execution state.
|
||||
|
||||
It owns worker-local scratch such as:
|
||||
|
||||
- raster scratch
|
||||
- brush workspace
|
||||
- the coverage row handler state
|
||||
|
||||
This is how the backend avoids allocating fresh buffers for every row item during the hot parallel pass.
|
||||
|
||||
## The Big Picture Flow
|
||||
|
||||
The easiest way to understand the backend is to follow one command batch from scene creation to execution.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[DrawingCanvas disposal replay] --> B[DefaultDrawingBackend.CreateScene]
|
||||
B --> C[FlushScene.Create]
|
||||
C --> D[Prepare visible items]
|
||||
D --> E[Build row-local execution plan]
|
||||
E --> F[DefaultDrawingBackend.RenderScene]
|
||||
F --> G[Acquire CPU destination]
|
||||
G --> H[Execute rows in parallel]
|
||||
H --> I[DefaultRasterizer emits coverage]
|
||||
I --> J[BrushRenderer shades pixels]
|
||||
J --> K[Destination frame updated]
|
||||
```
|
||||
|
||||
There are three major stages in that flow:
|
||||
|
||||
1. build the retained execution plan
|
||||
2. establish the destination frame
|
||||
3. execute rows using that plan
|
||||
|
||||
## What `DefaultDrawingBackend` Owns
|
||||
|
||||
`DefaultDrawingBackend` is intentionally smaller than its supporting types. It owns orchestration, not every low-level detail.
|
||||
|
||||
Its responsibilities are:
|
||||
|
||||
- create a `FlushScene`
|
||||
- acquire a writable CPU region from the target frame
|
||||
- execute that scene
|
||||
- provide CPU layer composition services
|
||||
- manage frame usage for CPU-backed targets
|
||||
|
||||
The expensive work is delegated:
|
||||
|
||||
- `FlushScene` owns retained row planning
|
||||
- `DefaultRasterizer` owns scan conversion
|
||||
- `BrushRenderer<TPixel>` owns brush-specific shading
|
||||
|
||||
That split keeps each type focused on one class of problem.
|
||||
|
||||
The canvas layer above that split is also important:
|
||||
|
||||
- `DrawingCanvas` records public drawing intent
|
||||
- `DrawingCanvasBatcher<TPixel>` prepares commands and constructs `DrawingCommandBatch` values
|
||||
- `DefaultDrawingBackend` executes the retained scene on a CPU destination
|
||||
|
||||
## Building The Flush Scene
|
||||
|
||||
`FlushScene.Create(...)` turns the prepared command stream into an execution plan in several phases. Each phase changes the data into a form that is cheaper for the next phase to consume.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Prepared commands] --> B[Filter and compact visible work]
|
||||
B --> C[Create retained raster geometry]
|
||||
C --> D[Build row membership]
|
||||
D --> E[Build row-local execution items]
|
||||
E --> F[FlushScene]
|
||||
```
|
||||
|
||||
### 1. Filter and compact visible work
|
||||
|
||||
The scene builder begins from the incoming command stream and keeps only the work that is visible and relevant to the flush. The later phases should not pay repeatedly for invisible commands through sparse scans or conditional branching.
|
||||
|
||||
### 2. Create retained raster geometry
|
||||
|
||||
For each visible item, the builder decomposes the command's drawing matrix into an X/Y scale and the rotation-shear-translation-perspective residual, asks the path for its scale-baked `LinearGeometry` via `ToLinearGeometry(Vector2 scale)`, and hands both the geometry and the residual to `DefaultRasterizer` to create the retained rasterizable payload. Curve subdivision therefore happens once per (path, scale) pair — cached on the `IPath` — and any per-frame rotation or translation rides into the rasterizer as the residual without forcing the path to re-flatten.
|
||||
|
||||
This step matters because it moves expensive geometry preparation out of the hot row loop and out of every frame of workloads like text or panning that drift only in their residual.
|
||||
|
||||
### 3. Build row membership
|
||||
|
||||
Once retained geometry exists, the scene builder determines which scene rows each item touches. That produces row-local membership information while preserving original submission order within every row.
|
||||
|
||||
That detail is critical. Parallel execution is allowed, but draw order must remain deterministic within each row.
|
||||
|
||||
### 4. Build row-local execution items
|
||||
|
||||
The scene then materializes the payload that the row executor will visit. Each row item points into flush-owned retained storage and carries just enough metadata to reconstruct a cheap `RasterizableBand` view when execution reaches that row.
|
||||
|
||||
At that point the scene is execution-ready.
|
||||
|
||||
## Why The Backend Is Row-First
|
||||
|
||||
The CPU backend executes rows, not commands.
|
||||
|
||||
This is one of the most important architectural choices in the whole path.
|
||||
|
||||
Why it helps:
|
||||
|
||||
- each worker naturally touches localized destination memory
|
||||
- scratch can be reused across many row items
|
||||
- draw order is straightforward inside a row
|
||||
- geometry planning stays out of the hottest loop
|
||||
|
||||
A row-first executor fits the actual shape of CPU rendering much better than a command-first executor would.
|
||||
|
||||
## The Execution Pass
|
||||
|
||||
When `FlushScene.Execute(...)` runs, the backend prepares brush renderers and then executes scene rows in parallel.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Exec as FlushScene.Execute
|
||||
participant Worker as WorkerState
|
||||
participant Raster as DefaultRasterizer
|
||||
participant Brush as BrushRenderer
|
||||
|
||||
Exec->>Brush: create one renderer per visible item
|
||||
Exec->>Worker: start parallel row pass
|
||||
Worker->>Exec: enumerate row items in order
|
||||
Worker->>Raster: ExecuteRasterizableBand(...)
|
||||
Raster-->>Exec: coverage rows
|
||||
Exec->>Brush: Apply(...)
|
||||
```
|
||||
|
||||
There are two important ownership patterns in that pass:
|
||||
|
||||
- renderers are created once per visible item before the hot row loop
|
||||
- scratch and workspace are reused per worker during the row loop
|
||||
|
||||
That is one of the backend's main performance properties.
|
||||
|
||||
## How Rasterization and Shading Stay Separate
|
||||
|
||||
The rasterizer and the backend solve different problems.
|
||||
|
||||
`DefaultRasterizer` is responsible for geometry and coverage.
|
||||
|
||||
`DefaultDrawingBackend` and `FlushScene` are responsible for:
|
||||
|
||||
- which items execute
|
||||
- when they execute
|
||||
- where their coverage belongs in the destination
|
||||
- which brush renderer should consume that coverage
|
||||
|
||||
That separation is intentional. It lets the rasterizer stay geometry-focused while the backend handles composition and destination layout.
|
||||
|
||||
## Coverage Routing
|
||||
|
||||
The rasterizer does not write destination pixels directly. Instead it emits row coverage through a handler supplied by the backend.
|
||||
|
||||
The backend-side row handler:
|
||||
|
||||
- receives emitted coverage
|
||||
- maps band-local coordinates back into destination coordinates
|
||||
- slices the correct destination row
|
||||
- invokes the correct `BrushRenderer<TPixel>`
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Rasterizer coverage row] --> B[Row handler]
|
||||
B --> C[Map to destination slice]
|
||||
C --> D[BrushRenderer.Apply]
|
||||
D --> E[Pixels updated]
|
||||
```
|
||||
|
||||
This is why the brush renderer can stay target-unbound. It receives the destination row slice and coverage data at execution time rather than owning the destination frame itself.
|
||||
|
||||
## Layer Composition
|
||||
|
||||
CPU layer composition is a separate concern from path rasterization.
|
||||
|
||||
`ComposeLayer<TPixel>()` composites one CPU frame into another using `PixelBlender<TPixel>`. That path exists because compositing an already-rasterized layer is a different problem from scanning geometry into coverage.
|
||||
|
||||
Keeping those paths separate makes the backend easier to reason about.
|
||||
|
||||
## Frame And Memory Lifetime
|
||||
|
||||
The backend aligns ownership with the actual execution lifetime.
|
||||
|
||||
### Flush-owned
|
||||
|
||||
Owned by `FlushScene`:
|
||||
|
||||
- visible item arrays
|
||||
- row structures
|
||||
- retained raster data
|
||||
- start-cover storage
|
||||
|
||||
Disposed when the flush ends.
|
||||
|
||||
### Worker-owned
|
||||
|
||||
Owned by `WorkerState<TPixel>` during execution:
|
||||
|
||||
- raster scratch
|
||||
- brush workspace
|
||||
|
||||
Disposed when the worker completes.
|
||||
|
||||
### Item-owned
|
||||
|
||||
Created once per visible item during execution:
|
||||
|
||||
- `BrushRenderer<TPixel>`
|
||||
|
||||
Retained for the duration of the row pass and then released with the flush-owned scene item state.
|
||||
|
||||
That ownership model keeps allocation and disposal aligned with real work lifetime.
|
||||
|
||||
## Reading Guide
|
||||
|
||||
If you are new to this backend, read the code in this order:
|
||||
|
||||
1. `DrawingCanvas.cs`
|
||||
2. `DrawingCanvas{TPixel}.cs`
|
||||
3. `DrawingCanvasBatcher{TPixel}.cs`
|
||||
4. `DefaultDrawingBackend.cs`
|
||||
5. `FlushScene.cs`
|
||||
6. `FlushScene.RetainedTypes.cs`
|
||||
7. `DefaultDrawingBackend.Helpers.cs`
|
||||
8. `DefaultRasterizer.cs`
|
||||
|
||||
That order mirrors the runtime flow:
|
||||
|
||||
canvas and backend selection -> backend orchestration -> retained row planning -> row execution structures -> worker helpers -> scan conversion
|
||||
|
||||
## The Mental Model To Keep
|
||||
|
||||
The easiest way to keep this backend straight is to remember that it is not a command-at-a-time painter. It is a flush executor that converts visible commands into row-local retained raster work and then executes that work with reusable scratch.
|
||||
|
||||
If that model is clear, the major types fall into place:
|
||||
|
||||
- `DrawingCanvas` records intent, and the typed implementation selects the backend
|
||||
- `DefaultDrawingBackend` orchestrates
|
||||
- `FlushScene` plans
|
||||
- `DefaultRasterizer` converts geometry to coverage
|
||||
- `BrushRenderer<TPixel>` converts coverage to color
|
||||
@@ -0,0 +1,458 @@
|
||||
# DefaultRasterizer
|
||||
|
||||
`DefaultRasterizer` is the CPU polygon scanner used by the retained fill path in ImageSharp.Drawing. Its job is narrow but central: take already-prepared geometry, convert that geometry into fixed-point edge contributions, and emit coverage rows that the CPU backend can turn into pixels.
|
||||
|
||||
This rasterizer is based on ideas and implementation techniques from the Blaze project:
|
||||
|
||||
- https://github.com/aurimasg/blaze
|
||||
|
||||
This document explains the rasterizer as a newcomer needs to understand it:
|
||||
|
||||
- where the rasterizer fits relative to `DrawingCanvas` and `DefaultDrawingBackend`
|
||||
- what problem the rasterizer is solving inside the CPU backend
|
||||
- why the rasterizer is split into retained geometry building and band execution
|
||||
- what retained geometry, bands, and coverage mean in this architecture
|
||||
- how scan conversion stays separate from brush shading and frame ownership
|
||||
|
||||
## Where The Rasterizer Fits
|
||||
|
||||
`DefaultRasterizer` sits below `DrawingCanvas`, the typed canvas implementation, and `DefaultDrawingBackend`.
|
||||
|
||||
The canvas records commands, the batcher prepares them into `DrawingCommandBatch` ranges, and `DefaultDrawingBackend` chooses the row-oriented execution plan for each retained CPU scene. `DefaultRasterizer` then handles the narrower geometry-to-coverage problem inside that CPU execution path.
|
||||
|
||||
That means the rasterizer does not select the backend, own the destination frame, or interpret the public drawing API directly. It receives already-prepared geometry through the CPU backend pipeline, and the backend later routes its coverage into whichever frame exposes the CPU region for the flush.
|
||||
|
||||
## The Main Problem
|
||||
|
||||
The CPU backend does not want to rediscover shape geometry every time it touches a destination row.
|
||||
|
||||
If row execution had to start from raw prepared paths every time, the backend would repeatedly need to:
|
||||
|
||||
- walk contours
|
||||
- split segments against row-band boundaries
|
||||
- compute left-of-band winding influence
|
||||
- rebuild scan-conversion state for the same shape over and over
|
||||
|
||||
That would push expensive geometry work into the hottest part of CPU rendering.
|
||||
|
||||
So the rasterizer solves a different problem:
|
||||
|
||||
it builds retained rasterizable geometry once, then executes compact band-local scanning work many times, cheaply.
|
||||
|
||||
That two-phase design is the core idea behind `DefaultRasterizer`.
|
||||
|
||||
## The Core Idea
|
||||
|
||||
The rasterizer is a retained fixed-point polygon scanner.
|
||||
|
||||
Its central idea is:
|
||||
|
||||
> build band-local retained line data once, then execute fixed-point scan conversion from that retained data
|
||||
|
||||
This is why the rasterizer has two very different modes of work:
|
||||
|
||||
1. retained geometry building
|
||||
2. band execution
|
||||
|
||||
The first phase is a preparation phase. The second is the hot execution phase.
|
||||
|
||||
If that distinction is clear, the code becomes much easier to follow.
|
||||
|
||||
## The Most Important Terms
|
||||
|
||||
### Rasterizer
|
||||
|
||||
`DefaultRasterizer` is the geometry-to-coverage engine.
|
||||
|
||||
It is responsible for:
|
||||
|
||||
- converting prepared geometry into retained scan-conversion data
|
||||
- rasterizing retained band data with fixed-point arithmetic
|
||||
- emitting coverage rows
|
||||
|
||||
It is not responsible for:
|
||||
|
||||
- brush color generation
|
||||
- destination frame ownership
|
||||
- layer composition
|
||||
- deciding which scene items should execute
|
||||
|
||||
Those problems belong to the CPU backend and `FlushScene`.
|
||||
|
||||
### Retained Geometry
|
||||
|
||||
Retained geometry is the rasterizer's prepared execution payload.
|
||||
|
||||
In this codebase, retained geometry means:
|
||||
|
||||
"the fixed-point, band-local line data and start-cover seeds needed to rasterize one prepared shape later without revisiting its original contour data"
|
||||
|
||||
That retained form is stored in `RasterizableGeometry`.
|
||||
|
||||
### Band
|
||||
|
||||
A band is one small vertical slice of a shape's retained geometry.
|
||||
|
||||
The rasterizer does not keep one giant scene-wide edge table. It stores data in row bands so execution can stay local and bounded.
|
||||
|
||||
### Rasterizable Geometry
|
||||
|
||||
`RasterizableGeometry` is the retained representation of one prepared shape.
|
||||
|
||||
It stores:
|
||||
|
||||
- clipped local bounds
|
||||
- band-local metadata
|
||||
- retained line arrays
|
||||
- optional start-cover seeds for bands that need carry-in winding
|
||||
|
||||
This is the retained object that the CPU backend keeps in `FlushScene`.
|
||||
|
||||
### Rasterizable Band
|
||||
|
||||
A `RasterizableBand` is the execution-time view over one retained band of one retained shape.
|
||||
|
||||
It is the immediate input to `ExecuteRasterizableBand(...)`.
|
||||
|
||||
### Context
|
||||
|
||||
`DefaultRasterizer.Context` is the mutable fixed-point scanning state used during band execution.
|
||||
|
||||
It is a `ref struct` because it is tied directly to worker-owned scratch spans and should not escape the execution scope.
|
||||
|
||||
### Coverage
|
||||
|
||||
Coverage is the rasterizer's output.
|
||||
|
||||
The rasterizer does not decide final pixel colors. It decides how much geometric coverage each pixel receives. The backend later passes that coverage to a `BrushRenderer<TPixel>`, which decides how the destination pixels should be shaded.
|
||||
|
||||
## Pipeline Placement
|
||||
|
||||
The rasterizer sits in the middle of the CPU backend pipeline.
|
||||
|
||||
Upstream:
|
||||
|
||||
- `CompositionCommand` preparation produces prepared geometry
|
||||
- the typed canvas implementation and `DrawingCanvasBatcher<TPixel>` have already selected and called the CPU backend
|
||||
- `FlushScene` decides which items are visible and when they execute
|
||||
|
||||
Downstream:
|
||||
|
||||
- the rasterizer emits row coverage
|
||||
- `DefaultDrawingBackend` routes that coverage into `BrushRenderer<TPixel>.Apply(...)`
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Prepared geometry] --> B[DefaultRasterizer.CreateRasterizableGeometry]
|
||||
B --> C[RasterizableGeometry]
|
||||
C --> D[Build RasterizableBand view]
|
||||
D --> E[ExecuteRasterizableBand]
|
||||
E --> F[Coverage rows]
|
||||
F --> G[Brush renderer]
|
||||
```
|
||||
|
||||
That placement is important. The rasterizer is neither the public drawing model nor the final shading model. It is the geometry-to-coverage step between them.
|
||||
|
||||
## Why The Rasterizer Has Two Phases
|
||||
|
||||
The rasterizer separates:
|
||||
|
||||
1. building retained geometry
|
||||
2. executing retained geometry
|
||||
|
||||
### Phase 1: retained geometry building
|
||||
|
||||
`CreateRasterizableGeometry(...)` converts prepared geometry into a retained representation that is cheap to execute later.
|
||||
|
||||
This phase:
|
||||
|
||||
- walks prepared contours
|
||||
- converts coordinates into fixed-point
|
||||
- clips or splits segments as needed for band boundaries
|
||||
- records visible line pieces into retained line storage
|
||||
- records left-of-band winding influence into start-cover tables
|
||||
|
||||
The output is `RasterizableGeometry`.
|
||||
|
||||
### Phase 2: band execution
|
||||
|
||||
`ExecuteRasterizableBand(...)` is the hot execution entry point.
|
||||
|
||||
It does not revisit the original contour data. It receives a `RasterizableBand` view over retained data and performs the minimum work needed to emit coverage rows for that band.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Exec as ExecuteRasterizableBand
|
||||
participant Ctx as Context
|
||||
participant Emit as Coverage Row Handler
|
||||
|
||||
Exec->>Ctx: Reconfigure(...)
|
||||
Exec->>Ctx: SeedStartCovers(...)
|
||||
Exec->>Ctx: Rasterize retained lines
|
||||
Exec->>Ctx: EmitCoverageRows(...)
|
||||
Ctx-->>Emit: coverage rows
|
||||
Exec->>Ctx: ResetTouchedRows()
|
||||
```
|
||||
|
||||
That separation is one of the key reasons the retained fill path performs well. Expensive geometry work happens once; execution consumes compact band-local data.
|
||||
|
||||
## Fixed-Point Precision
|
||||
|
||||
The rasterizer works in 24.8 fixed-point coordinates.
|
||||
|
||||
That means:
|
||||
|
||||
- `1` pixel = `256` fixed-point units
|
||||
- `FixedShift = 8`
|
||||
- `FixedOne = 256`
|
||||
|
||||
This gives the scanner subpixel precision while keeping the hot execution path integer-based. Geometry may begin as floating-point path data, but once a retained line reaches the scan-conversion core it is treated as fixed-point state.
|
||||
|
||||
Coverage is converted back into normalized `float` values only at the emission boundary.
|
||||
|
||||
## Why Bands Exist
|
||||
|
||||
The rasterizer does not retain one monolithic edge table. It retains geometry in vertical row bands.
|
||||
|
||||
That matters because it keeps execution local and bounded.
|
||||
|
||||
When a segment crosses multiple bands, the linearizer splits it so each band receives only the portion it must scan. If a segment influences winding inside the visible band from the left side, that influence is folded into a start-cover seed rather than keeping an invisible off-screen line around forever.
|
||||
|
||||
This gives the backend several important properties:
|
||||
|
||||
- execution only touches the band it is currently composing
|
||||
- left-of-band winding can be precomputed
|
||||
- scratch requirements stay bounded
|
||||
- row-oriented execution consumes compact band-local payloads
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Contour segment] --> B{Touches one band?}
|
||||
B -- Yes --> C[Store visible line in that band]
|
||||
B -- No --> D[Split across band boundaries]
|
||||
D --> E[Store band-local visible pieces]
|
||||
D --> F[Accumulate start-cover seeds where needed]
|
||||
```
|
||||
|
||||
## Retained Geometry: What Gets Stored
|
||||
|
||||
`RasterizableGeometry` stores the retained data needed to rasterize a prepared shape later.
|
||||
|
||||
That includes:
|
||||
|
||||
- the local bounds of the prepared shape
|
||||
- band count and band-local metadata
|
||||
- retained line arrays for each band
|
||||
- optional start-cover arrays for bands that need carry-in winding
|
||||
|
||||
The retained line arrays use specialized storage formats such as:
|
||||
|
||||
- `LineArrayX16Y16`
|
||||
- `LineArrayX32Y16`
|
||||
|
||||
These are storage-oriented types. They exist to retain compact fixed-point line segments so execution does not need to revisit contour data.
|
||||
|
||||
## The Linearizer
|
||||
|
||||
The linearizer is the retained-geometry builder. It is generic over line-array storage, but the conceptual work is the same across variants.
|
||||
|
||||
Its responsibilities are:
|
||||
|
||||
- traverse prepared contours
|
||||
- apply the residual transform per-point as contours are read
|
||||
- clip work to retained bounds
|
||||
- convert coordinates into fixed-point
|
||||
- decide whether a segment is contained or must be split
|
||||
- store visible line pieces
|
||||
- accumulate start covers for left-of-band influence
|
||||
|
||||
For a newcomer, the most important thing to understand is that the linearizer is not the hot coverage emitter. It is the preparation step that turns arbitrary contour geometry into a stable retained scanning payload.
|
||||
|
||||
### Residual transform application
|
||||
|
||||
The prepared `LinearGeometry` passed to `CreateRasterizableGeometry(...)` carries scale-baked points — the effective X/Y scale of the drawing matrix has already been absorbed into the flattened contour, so curve subdivision happens at device-scale precision. The remaining rotation, shear, translation, and perspective is handed to the rasterizer as a separate `Matrix4x4 residual`, which the linearizer applies per-point where the contour is read: at segment emission time in `ProcessContained` / `ProcessUncontained` for fills, and at bounds / closure / contour-segment construction sites in the stroke linearizer.
|
||||
|
||||
This split keeps the scale-baked geometry cacheable across frames (text and panning workloads reuse the same bake at a fixed zoom) while letting per-frame rotation or translation ride through the rasterizer without re-subdividing curves.
|
||||
|
||||
### Contained lines
|
||||
|
||||
A contained line is one whose fixed-point endpoints already fit the assumptions of the current retained band representation. Those lines can be pushed directly into retained storage after the required fixed-point and band-boundary handling.
|
||||
|
||||
### Split lines
|
||||
|
||||
When a line crosses band boundaries, the linearizer splits it so each band receives only the contribution it needs to scan.
|
||||
|
||||
### Start-cover seeding
|
||||
|
||||
When a line contributes winding inside the visible band but lies partially to the left of the visible X range, the retained geometry stores that influence in a start-cover array instead of retaining an off-screen line.
|
||||
|
||||
This is one of the most important ideas in the retained design:
|
||||
|
||||
- visible geometry becomes retained lines
|
||||
- invisible left-of-band winding becomes retained start-cover seeds
|
||||
|
||||
## The Execution Context
|
||||
|
||||
`DefaultRasterizer.Context` is the mutable fixed-point scanning state used during band execution.
|
||||
|
||||
It owns per-band mutable state such as:
|
||||
|
||||
- `bitVectors`
|
||||
- `coverArea`
|
||||
- `startCover`
|
||||
- `rowMinTouchedColumn`
|
||||
- `rowMaxTouchedColumn`
|
||||
- `rowHasBits`
|
||||
- `rowTouched`
|
||||
- `touchedRows`
|
||||
|
||||
This state is reused across bands by reconfiguration, not by reallocation.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[WorkerScratch] --> B[Context]
|
||||
B --> C[Rasterize retained lines]
|
||||
C --> D[Mutate coverArea and bit vectors]
|
||||
D --> E[Emit coverage rows]
|
||||
E --> F[Reset touched rows]
|
||||
```
|
||||
|
||||
The `Context` bridges retained geometry and emitted coverage.
|
||||
|
||||
## How Coverage Accumulation Works
|
||||
|
||||
The rasterizer uses the classic area-and-cover formulation.
|
||||
|
||||
When a fixed-point line is rasterized, it is broken into cell contributions. Those contributions eventually reach `AddCell(...)`, which updates:
|
||||
|
||||
- delta cover
|
||||
- delta area
|
||||
|
||||
Rows also track sparse touched-column information through bit vectors, so the emitter can avoid scanning the full width of empty rows.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Rasterize fixed-point line] --> B[Decompose into touched cells]
|
||||
B --> C["AddCell(row, column, deltaCover, deltaArea)"]
|
||||
C --> D[Update coverArea]
|
||||
C --> E[Mark bitVectors]
|
||||
C --> F[Track touched rows and bounds]
|
||||
C --> G{column < 0?}
|
||||
G -- Yes --> H[Fold into startCover]
|
||||
G -- No --> I[Keep visible cell contribution]
|
||||
```
|
||||
|
||||
This is why the rasterizer can honor fill rules later. It accumulates signed contributions first and applies the fill rule during coverage emission.
|
||||
|
||||
## Coverage Emission
|
||||
|
||||
`EmitCoverageRows(...)` converts the accumulated fixed-point state into row spans.
|
||||
|
||||
For each touched row, the emitter:
|
||||
|
||||
1. starts from the seeded `startCover`
|
||||
2. walks the row's touched columns using the bit vectors
|
||||
3. updates the running cover from `deltaCover`
|
||||
4. combines running cover and `deltaArea` into signed area
|
||||
5. converts signed area into normalized coverage using the selected fill rule
|
||||
6. coalesces equal-coverage spans
|
||||
7. writes only non-zero spans into the reusable scanline buffer
|
||||
8. invokes the row callback
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Touched row] --> B[Walk set bits]
|
||||
B --> C[Reconstruct cover and area]
|
||||
C --> D[Apply fill rule]
|
||||
D --> E[Coalesce equal coverage]
|
||||
E --> F[Write compact scanline spans]
|
||||
F --> G[Invoke row handler]
|
||||
```
|
||||
|
||||
The rasterizer therefore emits only rows that actually received contributions and only the non-zero spans within those rows.
|
||||
|
||||
## Fill Rules
|
||||
|
||||
The rasterizer supports both `NonZero` and `EvenOdd`.
|
||||
|
||||
### NonZero
|
||||
|
||||
The accumulated signed area is treated as winding magnitude. Coverage is the clamped absolute value of that area.
|
||||
|
||||
### EvenOdd
|
||||
|
||||
The accumulated area is wrapped into the even-odd domain before coverage is produced. This gives parity-based behavior without changing the earlier scan-conversion logic.
|
||||
|
||||
The fill rule is therefore an emission-time decision, not a geometry-preprocessing decision.
|
||||
|
||||
## Antialiased And Aliased Modes
|
||||
|
||||
The rasterizer can emit either continuous or thresholded coverage.
|
||||
|
||||
- `Antialiased` mode keeps the continuous coverage produced by the area-and-cover math
|
||||
- `Aliased` mode thresholds that continuous coverage using `AntialiasThreshold`
|
||||
|
||||
The scan-conversion core stays the same in both modes. Only the final conversion from area to emitted coverage changes.
|
||||
|
||||
## Why Self-Intersections Work
|
||||
|
||||
The rasterizer can handle self-intersections because it does not require geometric boolean normalization before rasterization. It accumulates signed contributions and then applies the selected fill rule during emission.
|
||||
|
||||
That means overlapping or self-crossing contours are resolved by:
|
||||
|
||||
- area-and-cover integration
|
||||
- winding or parity mapping
|
||||
|
||||
instead of by an earlier polygon-boolean pass.
|
||||
|
||||
## How The Rasterizer Stays Separate From The Backend
|
||||
|
||||
The rasterizer and the backend solve different problems.
|
||||
|
||||
The rasterizer decides:
|
||||
|
||||
- how geometry contributes coverage
|
||||
- which rows and columns within a band are touched
|
||||
- how much coverage each emitted span has
|
||||
|
||||
The backend decides:
|
||||
|
||||
- which scene items execute
|
||||
- which retained band is being scanned
|
||||
- which destination slice receives the coverage
|
||||
- which brush renderer consumes the emitted spans
|
||||
|
||||
That separation is one of the main architectural advantages of the current CPU path.
|
||||
|
||||
## Reading Guide
|
||||
|
||||
If you are new to this part of the library, read the rasterizer in this order:
|
||||
|
||||
1. `DrawingCanvas.cs`
|
||||
2. `DrawingCanvas{TPixel}.cs`
|
||||
3. `DrawingCanvasBatcher{TPixel}.cs`
|
||||
4. `DefaultDrawingBackend.cs`
|
||||
5. `FlushScene.cs`
|
||||
6. `CreateRasterizableGeometry(...)` in `DefaultRasterizer.cs`
|
||||
7. `Linearizer<TL>` and the concrete linearizers in `DefaultRasterizer.Linearizer.cs`
|
||||
8. retained line types in `DefaultRasterizer.RetainedTypes.cs`
|
||||
9. `ExecuteRasterizableBand(...)` in `DefaultRasterizer.cs`
|
||||
10. `Context` in `DefaultRasterizer.cs`
|
||||
|
||||
That order mirrors the data lifecycle:
|
||||
|
||||
canvas intent -> prepared geometry -> retained storage -> band execution -> coverage emission
|
||||
|
||||
## The Mental Model To Keep
|
||||
|
||||
The easiest way to reason about `DefaultRasterizer` is this:
|
||||
|
||||
it is a retained fixed-point polygon scanner that transforms prepared geometry into compact band-local line payloads, then turns those payloads into row coverage spans.
|
||||
|
||||
If that model stays clear, the rest of the code becomes easier to read:
|
||||
|
||||
- the canvas and backend docs explain how execution reaches the CPU path
|
||||
- the linearizer explains where retained line data comes from
|
||||
- `RasterizableGeometry` explains what is stored
|
||||
- the `Context` explains how retained data becomes coverage
|
||||
- the backend explains how coverage becomes pixels
|
||||
@@ -0,0 +1,202 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// CPU backend that executes path coverage rasterization and brush composition directly against a CPU region.
|
||||
/// </summary>
|
||||
public sealed partial class DefaultDrawingBackend
|
||||
{
|
||||
/// <summary>
|
||||
/// Adapts rasterizer coverage callbacks into brush application against the active band target.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
private readonly struct FillCoverageRowHandler<TPixel> : IRasterizerCoverageRowHandler
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly BrushRenderer<TPixel> renderer;
|
||||
private readonly BandTarget<TPixel> target;
|
||||
private readonly BrushWorkspace<TPixel> brushWorkspace;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FillCoverageRowHandler{TPixel}"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="renderer">The brush renderer that will consume emitted coverage spans.</param>
|
||||
/// <param name="target">The active band target being rendered.</param>
|
||||
/// <param name="brushWorkspace">The worker-local brush workspace.</param>
|
||||
public FillCoverageRowHandler(
|
||||
BrushRenderer<TPixel> renderer,
|
||||
BandTarget<TPixel> target,
|
||||
BrushWorkspace<TPixel> brushWorkspace)
|
||||
{
|
||||
this.renderer = renderer;
|
||||
this.target = target;
|
||||
this.brushWorkspace = brushWorkspace;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies one emitted coverage span to the active destination band.
|
||||
/// </summary>
|
||||
/// <param name="y">The absolute destination row.</param>
|
||||
/// <param name="startX">The absolute start column of the coverage span.</param>
|
||||
/// <param name="coverage">The emitted coverage values.</param>
|
||||
public void Handle(int y, int startX, Span<float> coverage)
|
||||
{
|
||||
int localY = y - this.target.AbsoluteTop;
|
||||
if ((uint)localY >= (uint)this.target.Region.Height)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int clipStartX = Math.Max(startX, this.target.AbsoluteLeft);
|
||||
int clipEndX = Math.Min(startX + coverage.Length, this.target.AbsoluteLeft + this.target.Region.Width);
|
||||
if (clipEndX <= clipStartX)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// The rasterizer emits absolute coordinates; clip them once here so the brush
|
||||
// renderer can operate against a tight destination span with no extra bounds work.
|
||||
int coverageOffset = clipStartX - startX;
|
||||
int clippedLength = clipEndX - clipStartX;
|
||||
Span<TPixel> destinationRow = this.target.Region
|
||||
.DangerousGetRowSpan(localY)
|
||||
.Slice(clipStartX - this.target.AbsoluteLeft, clippedLength);
|
||||
this.renderer.Apply(destinationRow, coverage.Slice(coverageOffset, clippedLength), clipStartX, y, this.brushWorkspace);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents one active composition target for a retained row.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
private sealed class BandTarget<TPixel> : IDisposable
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly Buffer2D<TPixel>? owner;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BandTarget{TPixel}"/> class over an existing region.
|
||||
/// </summary>
|
||||
/// <param name="region">The destination region.</param>
|
||||
/// <param name="absoluteLeft">The absolute X origin of the region.</param>
|
||||
/// <param name="absoluteTop">The absolute Y origin of the region.</param>
|
||||
/// <param name="graphicsOptions">The graphics options used when this target is later composited.</param>
|
||||
public BandTarget(Buffer2DRegion<TPixel> region, int absoluteLeft, int absoluteTop, GraphicsOptions? graphicsOptions)
|
||||
{
|
||||
this.Region = region;
|
||||
this.AbsoluteLeft = absoluteLeft;
|
||||
this.AbsoluteTop = absoluteTop;
|
||||
this.GraphicsOptions = graphicsOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BandTarget{TPixel}"/> class over an owned temporary buffer.
|
||||
/// </summary>
|
||||
/// <param name="owner">The owned buffer backing the target.</param>
|
||||
/// <param name="bounds">The absolute bounds represented by the target.</param>
|
||||
/// <param name="graphicsOptions">The graphics options used when this target is later composited.</param>
|
||||
public BandTarget(Buffer2D<TPixel> owner, Rectangle bounds, GraphicsOptions? graphicsOptions)
|
||||
{
|
||||
this.owner = owner;
|
||||
this.Region = owner.GetRegion();
|
||||
this.AbsoluteLeft = bounds.X;
|
||||
this.AbsoluteTop = bounds.Y;
|
||||
this.GraphicsOptions = graphicsOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the writable pixel region for the target.
|
||||
/// </summary>
|
||||
public Buffer2DRegion<TPixel> Region { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute X origin of <see cref="Region"/>.
|
||||
/// </summary>
|
||||
public int AbsoluteLeft { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute Y origin of <see cref="Region"/>.
|
||||
/// </summary>
|
||||
public int AbsoluteTop { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the graphics options associated with the target when it is used as a layer.
|
||||
/// </summary>
|
||||
public GraphicsOptions? GraphicsOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Releases the owned temporary buffer when the target represents a layer.
|
||||
/// </summary>
|
||||
public void Dispose() => this.owner?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds the reusable worker-local scratch used while executing retained scene rows.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
private sealed class WorkerState<TPixel> : IDisposable
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly MemoryAllocator allocator;
|
||||
private DefaultRasterizer.WorkerScratch? scratch;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WorkerState{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="allocator">The memory allocator used for scratch growth.</param>
|
||||
/// <param name="destinationWidth">The destination width used to size the brush workspace.</param>
|
||||
/// <param name="layerDepth">The maximum retained layer depth required by the scene.</param>
|
||||
public WorkerState(
|
||||
MemoryAllocator allocator,
|
||||
int destinationWidth,
|
||||
int layerDepth)
|
||||
{
|
||||
this.allocator = allocator;
|
||||
this.BrushWorkspace = new BrushWorkspace<TPixel>(allocator, destinationWidth);
|
||||
this.TargetStack = new BandTarget<TPixel>[layerDepth];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reusable brush workspace for the worker.
|
||||
/// </summary>
|
||||
public BrushWorkspace<TPixel> BrushWorkspace { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reusable composition target stack for the worker.
|
||||
/// </summary>
|
||||
public BandTarget<TPixel>[] TargetStack { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a reusable raster scratch instance sized for the requested width.
|
||||
/// </summary>
|
||||
/// <param name="requiredWidth">The minimum scanline width required by the current row.</param>
|
||||
/// <returns>A scratch instance that can execute the row.</returns>
|
||||
public DefaultRasterizer.WorkerScratch GetOrCreateScratch(int requiredWidth)
|
||||
{
|
||||
DefaultRasterizer.WorkerScratch? current = this.scratch;
|
||||
if (current is not null && current.CanReuse(requiredWidth))
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
current?.Dispose();
|
||||
this.scratch = DefaultRasterizer.CreateWorkerScratch(this.allocator, requiredWidth);
|
||||
return this.scratch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the worker-local scratch and brush workspace.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
this.scratch?.Dispose();
|
||||
this.BrushWorkspace.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// CPU backend that executes path coverage rasterization and brush composition directly against a CPU region.
|
||||
/// </summary>
|
||||
public sealed partial class DefaultDrawingBackend : IDrawingBackend
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default backend instance.
|
||||
/// </summary>
|
||||
public static DefaultDrawingBackend Instance { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public DrawingBackendScene CreateScene(
|
||||
Configuration configuration,
|
||||
Rectangle targetBounds,
|
||||
DrawingCommandBatch commandBatch,
|
||||
IReadOnlyList<IDisposable>? ownedResources = null)
|
||||
{
|
||||
FlushScene scene = FlushScene.Create(
|
||||
commandBatch,
|
||||
targetBounds,
|
||||
configuration.MemoryAllocator,
|
||||
configuration.MaxDegreeOfParallelism);
|
||||
|
||||
return new DefaultDrawingBackendScene(scene, targetBounds, ownedResources);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RenderScene<TPixel>(
|
||||
Configuration configuration,
|
||||
ICanvasFrame<TPixel> target,
|
||||
DrawingBackendScene scene)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
if (scene is not DefaultDrawingBackendScene cpuScene)
|
||||
{
|
||||
throw new InvalidOperationException("The retained scene is not a CPU drawing backend scene.");
|
||||
}
|
||||
|
||||
if (!target.TryGetCpuRegion(out Buffer2DRegion<TPixel> destinationFrame))
|
||||
{
|
||||
throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible frame targets.");
|
||||
}
|
||||
|
||||
if (target.Bounds != cpuScene.Bounds)
|
||||
{
|
||||
throw new InvalidOperationException("The target bounds do not match the retained CPU scene bounds.");
|
||||
}
|
||||
|
||||
if (cpuScene.Scene is FlushScene flushScene && flushScene.RowCount != 0)
|
||||
{
|
||||
ExecuteScene(configuration, destinationFrame, flushScene);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes one retained flush scene against a CPU destination frame.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="destinationFrame">The destination CPU region.</param>
|
||||
/// <param name="scene">The retained scene to execute.</param>
|
||||
private static void ExecuteScene<TPixel>(
|
||||
Configuration configuration,
|
||||
Buffer2DRegion<TPixel> destinationFrame,
|
||||
FlushScene scene)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
// Warm the cached renderers before the row loop so the hot execution path only
|
||||
// performs retained-scene work and brush application.
|
||||
if (scene.FillItemCount > 0)
|
||||
{
|
||||
for (int i = 0; i < scene.FillItems.Length; i++)
|
||||
{
|
||||
if (scene.FillItems[i] is FlushScene.FillSceneItem item)
|
||||
{
|
||||
_ = item.GetRenderer<TPixel>(configuration, destinationFrame.Width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (scene.StrokeItemCount > 0)
|
||||
{
|
||||
for (int i = 0; i < scene.StrokeItems.Length; i++)
|
||||
{
|
||||
if (scene.StrokeItems[i] is FlushScene.StrokeSceneItem item)
|
||||
{
|
||||
_ = item.GetRenderer<TPixel>(configuration, destinationFrame.Width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int requestedParallelism = configuration.MaxDegreeOfParallelism;
|
||||
_ = Parallel.For(
|
||||
fromInclusive: 0,
|
||||
toExclusive: scene.RowCount,
|
||||
parallelOptions: ParallelExecutionHelper.CreateParallelOptions(requestedParallelism, scene.RowCount),
|
||||
localInit: () => new WorkerState<TPixel>(configuration.MemoryAllocator, destinationFrame.Width, scene.MaxLayerDepth + 1),
|
||||
body: (rowIndex, _, state) =>
|
||||
{
|
||||
ExecuteSceneRow(
|
||||
configuration,
|
||||
destinationFrame,
|
||||
scene,
|
||||
scene.Rows[rowIndex],
|
||||
state);
|
||||
|
||||
return state;
|
||||
},
|
||||
localFinally: static state => state.Dispose());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes one retained scene row against the destination band it overlaps.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="destinationFrame">The destination CPU region.</param>
|
||||
/// <param name="scene">The retained flush scene.</param>
|
||||
/// <param name="row">The retained scene row to execute.</param>
|
||||
/// <param name="state">The worker-local scratch and compositing state.</param>
|
||||
private static void ExecuteSceneRow<TPixel>(
|
||||
Configuration configuration,
|
||||
Buffer2DRegion<TPixel> destinationFrame,
|
||||
FlushScene scene,
|
||||
in FlushScene.SceneRow row,
|
||||
WorkerState<TPixel> state)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
int bandTop = row.RowBandIndex * DefaultRasterizer.DefaultTileHeight;
|
||||
int localBandTop = bandTop - destinationFrame.Bounds.Y;
|
||||
int bandHeight = Math.Min(DefaultRasterizer.DefaultTileHeight, destinationFrame.Height - localBandTop);
|
||||
if (bandHeight <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Buffer2DRegion<TPixel> destinationBand = destinationFrame.GetSubRegion(0, localBandTop, destinationFrame.Width, bandHeight);
|
||||
BandTarget<TPixel>[] targetStack = state.TargetStack;
|
||||
int targetCount = 1;
|
||||
targetStack[0] = new BandTarget<TPixel>(destinationBand, destinationFrame.Bounds.X, bandTop, null);
|
||||
int scratchWidth = GetRowScratchWidth(scene, row, destinationFrame.Width);
|
||||
DefaultRasterizer.WorkerScratch scratch = state.GetOrCreateScratch(scratchWidth);
|
||||
|
||||
try
|
||||
{
|
||||
for (FlushScene.SceneOperationBlock? block = row.FirstBlock; block is not null; block = block.Next)
|
||||
{
|
||||
foreach (FlushScene.SceneOperation operation in block.Items)
|
||||
{
|
||||
// Each retained row contains a compact mix of layer control operations and
|
||||
// draw operations in original command order, so the executor can replay the
|
||||
// row without re-walking the full scene description.
|
||||
switch (operation.Kind)
|
||||
{
|
||||
case FlushScene.SceneOperationKind.BeginLayer:
|
||||
GraphicsOptions? layerOptions = scene.LayerOptions[operation.ItemIndex];
|
||||
|
||||
targetStack[targetCount++] =
|
||||
new BandTarget<TPixel>(
|
||||
configuration.MemoryAllocator.Allocate2D<TPixel>(operation.LayerBounds.Width, operation.LayerBounds.Height, AllocationOptions.Clean),
|
||||
operation.LayerBounds,
|
||||
layerOptions);
|
||||
break;
|
||||
|
||||
case FlushScene.SceneOperationKind.EndLayer:
|
||||
BandTarget<TPixel> source = targetStack[--targetCount];
|
||||
BandTarget<TPixel> destination = targetStack[targetCount - 1];
|
||||
CompositeLayerBand(configuration, source, destination, state.BrushWorkspace);
|
||||
source.Dispose();
|
||||
break;
|
||||
|
||||
case FlushScene.SceneOperationKind.FillItem:
|
||||
BandTarget<TPixel> target = targetStack[targetCount - 1];
|
||||
FlushScene.FillSceneItem sceneItem = scene.FillItems[operation.ItemIndex]!;
|
||||
ExecuteFillOperation(
|
||||
sceneItem.GetRenderer<TPixel>(configuration, destinationFrame.Width),
|
||||
new DefaultRasterizer.RasterizableItem(sceneItem.Rasterizable, operation.LocalRowIndex),
|
||||
target,
|
||||
scratch,
|
||||
state);
|
||||
break;
|
||||
|
||||
case FlushScene.SceneOperationKind.StrokeItem:
|
||||
BandTarget<TPixel> strokeTarget = targetStack[targetCount - 1];
|
||||
FlushScene.StrokeSceneItem strokeSceneItem = scene.StrokeItems[operation.ItemIndex]!;
|
||||
ExecuteStrokeOperation(
|
||||
strokeSceneItem.GetRenderer<TPixel>(configuration, destinationFrame.Width),
|
||||
new DefaultRasterizer.StrokeRasterizableItem(strokeSceneItem.Rasterizable, operation.LocalRowIndex),
|
||||
strokeTarget,
|
||||
scratch,
|
||||
state);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
for (int i = 1; i < targetCount; i++)
|
||||
{
|
||||
targetStack[i].Dispose();
|
||||
targetStack[i] = null!;
|
||||
}
|
||||
|
||||
targetStack[0] = null!;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the minimum reusable scratch width needed to execute one retained scene row.
|
||||
/// </summary>
|
||||
/// <param name="scene">The retained flush scene.</param>
|
||||
/// <param name="row">The retained scene row.</param>
|
||||
/// <param name="minimumWidth">The baseline width taken from the destination band.</param>
|
||||
/// <returns>The scratch width required by the row.</returns>
|
||||
private static int GetRowScratchWidth(
|
||||
FlushScene scene,
|
||||
in FlushScene.SceneRow row,
|
||||
int minimumWidth)
|
||||
{
|
||||
int width = minimumWidth;
|
||||
for (FlushScene.SceneOperationBlock? block = row.FirstBlock; block is not null; block = block.Next)
|
||||
{
|
||||
foreach (FlushScene.SceneOperation operation in block.Items)
|
||||
{
|
||||
if (operation.Kind is FlushScene.SceneOperationKind.BeginLayer or FlushScene.SceneOperationKind.EndLayer)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int itemWidth = operation.Kind == FlushScene.SceneOperationKind.FillItem
|
||||
? scene.FillItems[operation.ItemIndex]!.Rasterizable.Width
|
||||
: scene.StrokeItems[operation.ItemIndex]!.Rasterizable.Width;
|
||||
if (itemWidth > width)
|
||||
{
|
||||
width = itemWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes one retained fill operation through the rasterizer and brush renderer.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="renderer">The memoized brush renderer for the scene item.</param>
|
||||
/// <param name="item">The retained rasterizable row item to execute.</param>
|
||||
/// <param name="target">The active composition target for the row.</param>
|
||||
/// <param name="scratch">The worker-local raster scratch.</param>
|
||||
/// <param name="state">The worker-local execution state.</param>
|
||||
private static void ExecuteFillOperation<TPixel>(
|
||||
BrushRenderer<TPixel> renderer,
|
||||
DefaultRasterizer.RasterizableItem item,
|
||||
BandTarget<TPixel> target,
|
||||
DefaultRasterizer.WorkerScratch scratch,
|
||||
WorkerState<TPixel> state)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
DefaultRasterizer.RasterizableBandInfo bandInfo = item.Rasterizable.GetBandInfo(item.LocalRowIndex);
|
||||
DefaultRasterizer.Context context = scratch.CreateContext(
|
||||
bandInfo.IntersectionRule,
|
||||
bandInfo.RasterizationMode,
|
||||
bandInfo.AntialiasThreshold);
|
||||
FillCoverageRowHandler<TPixel> rowHandler = new(renderer, target, state.BrushWorkspace);
|
||||
DefaultRasterizer.ExecuteRasterizableItem(
|
||||
ref context,
|
||||
in item,
|
||||
in bandInfo,
|
||||
scratch.Scanline,
|
||||
ref rowHandler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes one retained stroke operation through the rasterizer and brush renderer.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="renderer">The memoized brush renderer for the scene item.</param>
|
||||
/// <param name="item">The retained stroke rasterizable row item to execute.</param>
|
||||
/// <param name="target">The active composition target for the row.</param>
|
||||
/// <param name="scratch">The worker-local raster scratch.</param>
|
||||
/// <param name="state">The worker-local execution state.</param>
|
||||
private static void ExecuteStrokeOperation<TPixel>(
|
||||
BrushRenderer<TPixel> renderer,
|
||||
DefaultRasterizer.StrokeRasterizableItem item,
|
||||
BandTarget<TPixel> target,
|
||||
DefaultRasterizer.WorkerScratch scratch,
|
||||
WorkerState<TPixel> state)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
DefaultRasterizer.RasterizableBandInfo bandInfo = item.Rasterizable.GetBandInfo(item.LocalRowIndex);
|
||||
DefaultRasterizer.Context context = scratch.CreateContext(
|
||||
bandInfo.IntersectionRule,
|
||||
bandInfo.RasterizationMode,
|
||||
bandInfo.AntialiasThreshold);
|
||||
FillCoverageRowHandler<TPixel> rowHandler = new(renderer, target, state.BrushWorkspace);
|
||||
Span<float> strokeBandCoverage = item.Rasterizable.RequiresBandCoverage ? scratch.StrokeBandCoverage : [];
|
||||
DefaultRasterizer.ExecuteStrokeRasterizableItem(
|
||||
ref context,
|
||||
in item,
|
||||
in bandInfo,
|
||||
scratch.Scanline,
|
||||
strokeBandCoverage,
|
||||
ref rowHandler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Composites one temporary layer band back into its destination band.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="source">The source layer band.</param>
|
||||
/// <param name="destination">The destination band to blend into.</param>
|
||||
/// <param name="brushWorkspace">The worker-local amount buffer workspace.</param>
|
||||
private static void CompositeLayerBand<TPixel>(
|
||||
Configuration configuration,
|
||||
BandTarget<TPixel> source,
|
||||
BandTarget<TPixel> destination,
|
||||
BrushWorkspace<TPixel> brushWorkspace)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
int width = source.Region.Width;
|
||||
if (width == 0 || source.Region.Height == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Rectangle overlap = Rectangle.Intersect(
|
||||
new Rectangle(source.AbsoluteLeft, source.AbsoluteTop, source.Region.Width, source.Region.Height),
|
||||
new Rectangle(destination.AbsoluteLeft, destination.AbsoluteTop, destination.Region.Width, destination.Region.Height));
|
||||
|
||||
if (overlap.Width <= 0 || overlap.Height <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (source.GraphicsOptions is not GraphicsOptions graphicsOptions)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(graphicsOptions);
|
||||
Span<float> amounts = brushWorkspace.GetAmounts(overlap.Width);
|
||||
amounts[..overlap.Width].Fill(graphicsOptions.BlendPercentage);
|
||||
|
||||
int sourceOffsetX = overlap.X - source.AbsoluteLeft;
|
||||
int sourceOffsetY = overlap.Y - source.AbsoluteTop;
|
||||
int destinationOffsetX = overlap.X - destination.AbsoluteLeft;
|
||||
int destinationOffsetY = overlap.Y - destination.AbsoluteTop;
|
||||
|
||||
// Blend the overlapping rows only; the retained scene has already clipped the layer
|
||||
// bounds so there is no need for extra per-pixel bounds logic here.
|
||||
for (int y = 0; y < overlap.Height; y++)
|
||||
{
|
||||
Span<TPixel> sourceRow = source.Region.DangerousGetRowSpan(sourceOffsetY + y).Slice(sourceOffsetX, overlap.Width);
|
||||
Span<TPixel> destinationRow = destination.Region.DangerousGetRowSpan(destinationOffsetY + y).Slice(destinationOffsetX, overlap.Width);
|
||||
blender.Blend(
|
||||
configuration,
|
||||
destinationRow,
|
||||
destinationRow,
|
||||
sourceRow,
|
||||
amounts[..overlap.Width],
|
||||
brushWorkspace.GetBlendScratch(overlap.Width, 3));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Composites one CPU-backed frame onto another using the supplied graphics options.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="source">The source frame.</param>
|
||||
/// <param name="destination">The destination frame.</param>
|
||||
/// <param name="destinationOffset">The destination offset relative to <paramref name="destination"/>.</param>
|
||||
/// <param name="options">The graphics options controlling composition.</param>
|
||||
public static void ComposeLayer<TPixel>(
|
||||
Configuration configuration,
|
||||
ICanvasFrame<TPixel> source,
|
||||
ICanvasFrame<TPixel> destination,
|
||||
Point destinationOffset,
|
||||
GraphicsOptions options)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
Guard.NotNull(configuration, nameof(configuration));
|
||||
|
||||
if (!source.TryGetCpuRegion(out Buffer2DRegion<TPixel> sourceRegion))
|
||||
{
|
||||
throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible source frames.");
|
||||
}
|
||||
|
||||
if (!destination.TryGetCpuRegion(out Buffer2DRegion<TPixel> destinationRegion))
|
||||
{
|
||||
throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible destination frames.");
|
||||
}
|
||||
|
||||
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(options);
|
||||
float blendPercentage = options.BlendPercentage;
|
||||
|
||||
int srcWidth = sourceRegion.Width;
|
||||
int srcHeight = sourceRegion.Height;
|
||||
int dstWidth = destinationRegion.Width;
|
||||
int dstHeight = destinationRegion.Height;
|
||||
|
||||
// Clamp the compositing region to both source and destination bounds.
|
||||
int startX = Math.Max(0, -destinationOffset.X);
|
||||
int startY = Math.Max(0, -destinationOffset.Y);
|
||||
int endX = Math.Min(srcWidth, dstWidth - destinationOffset.X);
|
||||
int endY = Math.Min(srcHeight, dstHeight - destinationOffset.Y);
|
||||
|
||||
if (endX <= startX || endY <= startY)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int width = endX - startX;
|
||||
|
||||
// Allocate a reusable per-row amount buffer from the memory pool.
|
||||
using IMemoryOwner<float> amountsOwner = configuration.MemoryAllocator.Allocate<float>(width);
|
||||
Span<float> amounts = amountsOwner.Memory.Span;
|
||||
amounts.Fill(blendPercentage);
|
||||
|
||||
for (int y = startY; y < endY; y++)
|
||||
{
|
||||
Span<TPixel> srcRow = sourceRegion.DangerousGetRowSpan(y).Slice(startX, width);
|
||||
int dstX = destinationOffset.X + startX;
|
||||
int dstY = destinationOffset.Y + y;
|
||||
Span<TPixel> dstRow = destinationRegion.DangerousGetRowSpan(dstY).Slice(dstX, width);
|
||||
|
||||
blender.Blend(configuration, dstRow, dstRow, srcRow, amounts);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ReadRegion<TPixel>(
|
||||
Configuration configuration,
|
||||
ICanvasFrame<TPixel> target,
|
||||
Rectangle sourceRectangle,
|
||||
Buffer2DRegion<TPixel> destination)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
Guard.NotNull(configuration, nameof(configuration));
|
||||
Guard.NotNull(destination.Buffer, nameof(destination));
|
||||
|
||||
// CPU backend readback is available only when the target exposes CPU pixels.
|
||||
if (!target.TryGetCpuRegion(out Buffer2DRegion<TPixel> sourceRegion))
|
||||
{
|
||||
throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible frame targets for readback.");
|
||||
}
|
||||
|
||||
// Clamp the request to the target region to avoid out-of-range row slicing.
|
||||
Rectangle clipped = Rectangle.Intersect(
|
||||
new Rectangle(0, 0, sourceRegion.Width, sourceRegion.Height),
|
||||
sourceRectangle);
|
||||
|
||||
if (clipped.Width <= 0 || clipped.Height <= 0)
|
||||
{
|
||||
throw new ArgumentException("The requested readback rectangle does not intersect the target bounds.", nameof(sourceRectangle));
|
||||
}
|
||||
|
||||
int copyWidth = Math.Min(clipped.Width, destination.Width);
|
||||
int copyHeight = Math.Min(clipped.Height, destination.Height);
|
||||
|
||||
for (int y = 0; y < copyHeight; y++)
|
||||
{
|
||||
sourceRegion.DangerousGetRowSpan(clipped.Y + y)
|
||||
.Slice(clipped.X, copyWidth)
|
||||
.CopyTo(destination.DangerousGetRowSpan(y));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Retained scene created by the CPU drawing backend.
|
||||
/// </summary>
|
||||
public sealed class DefaultDrawingBackendScene : DrawingBackendScene
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultDrawingBackendScene"/> class.
|
||||
/// </summary>
|
||||
/// <param name="scene">The retained CPU flush scene.</param>
|
||||
/// <param name="bounds">The target bounds used to create the scene.</param>
|
||||
/// <param name="ownedResources">Resources that must stay alive for the retained scene.</param>
|
||||
internal DefaultDrawingBackendScene(
|
||||
FlushScene scene,
|
||||
Rectangle bounds,
|
||||
IReadOnlyList<IDisposable>? ownedResources)
|
||||
: base(bounds, ownedResources)
|
||||
=> this.Scene = scene;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained CPU flush scene when this is a leaf scene.
|
||||
/// </summary>
|
||||
internal FlushScene? Scene { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void DisposeCore()
|
||||
=> this.Scene?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Buffers;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
internal static partial class DefaultRasterizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Contract implemented by retained line-block payloads.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSelf">The concrete retained line-block type.</typeparam>
|
||||
internal interface ILineBlock<TSelf>
|
||||
where TSelf : class, ILineBlock<TSelf>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the number of lines stored in a full block.
|
||||
/// </summary>
|
||||
public static abstract int LineCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next block in the retained chain.
|
||||
/// </summary>
|
||||
public TSelf? Next { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Rasterizes the leading <paramref name="count"/> lines from this block.
|
||||
/// </summary>
|
||||
/// <param name="count">The number of leading lines to rasterize from this block.</param>
|
||||
/// <param name="context">The mutable scan-conversion context to write into.</param>
|
||||
public void Rasterize(int count, ref Context context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retained tile-space bounds for one linearized geometry payload.
|
||||
/// </summary>
|
||||
internal readonly struct TileBounds
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TileBounds"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="x">The tile-space left coordinate.</param>
|
||||
/// <param name="y">The tile-space top coordinate.</param>
|
||||
/// <param name="columnCount">The tile-space column count.</param>
|
||||
/// <param name="rowCount">The tile-space row count.</param>
|
||||
public TileBounds(int x, int y, int columnCount, int rowCount)
|
||||
{
|
||||
this.X = x;
|
||||
this.Y = y;
|
||||
this.ColumnCount = columnCount;
|
||||
this.RowCount = rowCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tile-space left coordinate.
|
||||
/// </summary>
|
||||
public int X { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tile-space top coordinate.
|
||||
/// </summary>
|
||||
public int Y { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tile-space column count.
|
||||
/// </summary>
|
||||
public int ColumnCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tile-space row count.
|
||||
/// </summary>
|
||||
public int RowCount { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds the finalized retained raster payload for one line-block encoding.
|
||||
/// </summary>
|
||||
/// <typeparam name="TLineBlock">The concrete retained line-block type.</typeparam>
|
||||
internal sealed class LinearizedRasterData<TLineBlock>
|
||||
where TLineBlock : class, ILineBlock<TLineBlock>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinearizedRasterData{TLineBlock}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="geometry">The source linear geometry.</param>
|
||||
/// <param name="bounds">The retained tile-space bounds.</param>
|
||||
/// <param name="lines">The retained line-block chain for each row band.</param>
|
||||
/// <param name="firstBlockLineCounts">The valid line count in each row's front block.</param>
|
||||
/// <param name="startCoverTable">The retained start-cover seeds for each row band.</param>
|
||||
public LinearizedRasterData(
|
||||
LinearGeometry geometry,
|
||||
TileBounds bounds,
|
||||
TLineBlock?[] lines,
|
||||
int[] firstBlockLineCounts,
|
||||
IMemoryOwner<int>?[] startCoverTable)
|
||||
{
|
||||
this.Geometry = geometry;
|
||||
this.Bounds = bounds;
|
||||
this.Lines = lines;
|
||||
this.FirstBlockLineCounts = firstBlockLineCounts;
|
||||
this.StartCoverTable = startCoverTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source linear geometry.
|
||||
/// </summary>
|
||||
public LinearGeometry Geometry { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained tile-space bounds.
|
||||
/// </summary>
|
||||
public TileBounds Bounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained line-block chain for each row band.
|
||||
/// </summary>
|
||||
public TLineBlock?[] Lines { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the valid front-block line count for each row band.
|
||||
/// </summary>
|
||||
public int[] FirstBlockLineCounts { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained start-cover seeds for each row band.
|
||||
/// </summary>
|
||||
public IMemoryOwner<int>?[] StartCoverTable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Iterates the retained line blocks for one row band.
|
||||
/// </summary>
|
||||
/// <param name="rowIndex">The row band index to iterate.</param>
|
||||
/// <param name="context">The mutable scan-conversion context.</param>
|
||||
public void Iterate(int rowIndex, ref Context context)
|
||||
{
|
||||
int count = this.FirstBlockLineCounts[rowIndex];
|
||||
TLineBlock? lineBlock = this.Lines[rowIndex];
|
||||
while (lineBlock is not null)
|
||||
{
|
||||
lineBlock.Rasterize(count, ref context);
|
||||
lineBlock = lineBlock.Next;
|
||||
count = TLineBlock.LineCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,920 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Numerics;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
internal static partial class DefaultRasterizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class that lowers translated geometry into retained per-row line storage.
|
||||
/// </summary>
|
||||
/// <typeparam name="TL">The mutable per-row line collector type.</typeparam>
|
||||
private abstract class Linearizer<TL>
|
||||
where TL : class
|
||||
{
|
||||
private bool hasAnyCoverage;
|
||||
|
||||
protected Linearizer(
|
||||
LinearGeometry geometry,
|
||||
Matrix4x4 residual,
|
||||
int translateX,
|
||||
int translateY,
|
||||
int minX,
|
||||
int minY,
|
||||
int width,
|
||||
int height,
|
||||
int firstBandIndex,
|
||||
int rowBandCount,
|
||||
float samplingOffsetX,
|
||||
float samplingOffsetY,
|
||||
MemoryAllocator allocator)
|
||||
{
|
||||
this.Geometry = geometry;
|
||||
this.Residual = residual;
|
||||
this.HasResidual = !residual.IsIdentity;
|
||||
this.TranslateX = translateX;
|
||||
this.TranslateY = translateY;
|
||||
this.MinX = minX;
|
||||
this.MinY = minY;
|
||||
this.Width = width;
|
||||
this.Height = height;
|
||||
this.FirstBandIndex = firstBandIndex;
|
||||
this.RowBandCount = rowBandCount;
|
||||
this.SamplingOffsetX = samplingOffsetX;
|
||||
this.SamplingOffsetY = samplingOffsetY;
|
||||
this.Allocator = allocator;
|
||||
this.BandTopStart = (firstBandIndex * PreferredRowHeight) - minY;
|
||||
this.FirstBlockLineCounts = new int[rowBandCount];
|
||||
this.LineCounts = new int[rowBandCount];
|
||||
this.StartCoverTable = new IMemoryOwner<int>?[rowBandCount];
|
||||
this.LineArrays = new TL?[rowBandCount];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source geometry being lowered.
|
||||
/// </summary>
|
||||
protected LinearGeometry Geometry { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the residual transform applied to each source point during emission.
|
||||
/// </summary>
|
||||
protected Matrix4x4 Residual { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether <see cref="Residual"/> is non-identity.
|
||||
/// </summary>
|
||||
protected bool HasResidual { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the translated X offset applied to the geometry.
|
||||
/// </summary>
|
||||
protected int TranslateX { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the translated Y offset applied to the geometry.
|
||||
/// </summary>
|
||||
protected int TranslateY { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the minimum destination X bound after clipping.
|
||||
/// </summary>
|
||||
protected int MinX { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the minimum destination Y bound after clipping.
|
||||
/// </summary>
|
||||
protected int MinY { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the visible destination width in pixels.
|
||||
/// </summary>
|
||||
protected int Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the visible destination height in pixels.
|
||||
/// </summary>
|
||||
protected int Height { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first retained row-band index touched by the geometry.
|
||||
/// </summary>
|
||||
protected int FirstBandIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of retained row bands owned by the geometry.
|
||||
/// </summary>
|
||||
protected int RowBandCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the horizontal sampling offset applied before fixed-point conversion.
|
||||
/// </summary>
|
||||
protected float SamplingOffsetX { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the vertical sampling offset applied before fixed-point conversion.
|
||||
/// </summary>
|
||||
protected float SamplingOffsetY { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the allocator used for retained start-cover storage.
|
||||
/// </summary>
|
||||
protected MemoryAllocator Allocator { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the top offset, in whole pixels, of the first retained row band.
|
||||
/// </summary>
|
||||
protected int BandTopStart { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mutable per-row line collectors used during lowering.
|
||||
/// </summary>
|
||||
protected TL?[] LineArrays { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the valid front-block line count for each retained row band.
|
||||
/// </summary>
|
||||
protected int[] FirstBlockLineCounts { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total retained line count for each row band.
|
||||
/// </summary>
|
||||
protected int[] LineCounts { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained start-cover storage for each row band.
|
||||
/// </summary>
|
||||
protected IMemoryOwner<int>?[] StartCoverTable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether any retained payload was produced.
|
||||
/// </summary>
|
||||
protected ref bool HasAnyCoverage => ref this.hasAnyCoverage;
|
||||
|
||||
/// <summary>
|
||||
/// Executes the linearization pass and finalizes the retained row payloads.
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> when any retained coverage was produced; otherwise <see langword="false"/>.</returns>
|
||||
protected virtual bool ProcessCore()
|
||||
{
|
||||
RectangleF translatedBounds = this.HasResidual
|
||||
? RectangleF.Transform(this.Geometry.Info.Bounds, this.Residual)
|
||||
: this.Geometry.Info.Bounds;
|
||||
translatedBounds.Offset(this.TranslateX + this.SamplingOffsetX - this.MinX, this.TranslateY + this.SamplingOffsetY - this.MinY);
|
||||
|
||||
bool contains =
|
||||
translatedBounds.Left >= 0F &&
|
||||
translatedBounds.Top >= 0F &&
|
||||
translatedBounds.Right <= this.Width &&
|
||||
translatedBounds.Bottom <= this.Height;
|
||||
|
||||
// Contained geometry can skip clipping and go straight to the fixed-point band splitter.
|
||||
if (contains)
|
||||
{
|
||||
this.ProcessContained();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Geometry that touches the interest edges needs clipping so start covers and line
|
||||
// segments still match the destination bounds seen by the rasterizer.
|
||||
this.ProcessUncontained();
|
||||
}
|
||||
|
||||
if (!this.hasAnyCoverage)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
this.FinalizeLines();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Linearizes geometry that is fully contained inside the destination interest.
|
||||
/// </summary>
|
||||
protected void ProcessContained()
|
||||
{
|
||||
SegmentEnumerator enumerator = this.Geometry.GetSegments();
|
||||
Matrix4x4 residual = this.Residual;
|
||||
bool hasResidual = this.HasResidual;
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
LinearSegment segment = enumerator.Current;
|
||||
PointF p0 = segment.Start;
|
||||
PointF p1 = segment.End;
|
||||
if (hasResidual)
|
||||
{
|
||||
p0 = PointF.Transform(p0, residual);
|
||||
p1 = PointF.Transform(p1, residual);
|
||||
}
|
||||
|
||||
this.AddContainedLineF24Dot8(
|
||||
FloatToFixed24Dot8(((p0.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX),
|
||||
FloatToFixed24Dot8(((p0.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY),
|
||||
FloatToFixed24Dot8(((p1.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX),
|
||||
FloatToFixed24Dot8(((p1.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Linearizes geometry that intersects the destination interest bounds and requires clipping.
|
||||
/// </summary>
|
||||
protected void ProcessUncontained()
|
||||
{
|
||||
SegmentEnumerator enumerator = this.Geometry.GetSegments();
|
||||
Matrix4x4 residual = this.Residual;
|
||||
bool hasResidual = this.HasResidual;
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
LinearSegment segment = enumerator.Current;
|
||||
PointF p0 = segment.Start;
|
||||
PointF p1 = segment.End;
|
||||
if (hasResidual)
|
||||
{
|
||||
p0 = PointF.Transform(p0, residual);
|
||||
p1 = PointF.Transform(p1, residual);
|
||||
}
|
||||
|
||||
this.AddUncontainedLine(
|
||||
((p0.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX,
|
||||
((p0.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY,
|
||||
((p1.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX,
|
||||
((p1.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clips one geometry line against the destination interest and adds the retained result.
|
||||
/// </summary>
|
||||
/// <param name="x0">The starting X coordinate in translated float space.</param>
|
||||
/// <param name="y0">The starting Y coordinate in translated float space.</param>
|
||||
/// <param name="x1">The ending X coordinate in translated float space.</param>
|
||||
/// <param name="y1">The ending Y coordinate in translated float space.</param>
|
||||
protected void AddUncontainedLine(float x0, float y0, float x1, float y1)
|
||||
{
|
||||
if (y0 == y1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (y0 <= 0F && y1 <= 0F)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (y0 >= this.Height && y1 >= this.Height)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (x0 >= this.Width && x1 >= this.Width)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (x0 == x1)
|
||||
{
|
||||
int x0c = Math.Clamp(FloatToFixed24Dot8(x0), 0, this.Width * FixedOne);
|
||||
int p0y = Math.Clamp(FloatToFixed24Dot8(y0), 0, this.Height * FixedOne);
|
||||
int p1y = Math.Clamp(FloatToFixed24Dot8(y1), 0, this.Height * FixedOne);
|
||||
|
||||
if (x0c == 0)
|
||||
{
|
||||
// Segments clipped fully to the left edge do not produce a visible line, but they
|
||||
// still change winding for rows they cross. Retain that effect as start covers.
|
||||
this.UpdateStartCoversClipped(p0y, p1y);
|
||||
this.hasAnyCoverage = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.AddContainedLineF24Dot8(x0c, p0y, x0c, p1y);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
double deltayV = Math.Abs(y1 - y0);
|
||||
double deltaxV = x1 - x0;
|
||||
double rx0 = x0;
|
||||
double ry0 = y0;
|
||||
double rx1 = x1;
|
||||
double ry1 = y1;
|
||||
|
||||
if (y1 > y0)
|
||||
{
|
||||
if (y0 < 0F)
|
||||
{
|
||||
double t = -y0 / deltayV;
|
||||
rx0 = x0 + (deltaxV * t);
|
||||
ry0 = 0D;
|
||||
}
|
||||
|
||||
if (y1 > this.Height)
|
||||
{
|
||||
double t = (this.Height - y0) / deltayV;
|
||||
rx1 = x0 + (deltaxV * t);
|
||||
ry1 = this.Height;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (y0 > this.Height)
|
||||
{
|
||||
double t = (y0 - this.Height) / deltayV;
|
||||
rx0 = x0 + (deltaxV * t);
|
||||
ry0 = this.Height;
|
||||
}
|
||||
|
||||
if (y1 < 0F)
|
||||
{
|
||||
double t = y0 / deltayV;
|
||||
rx1 = x0 + (deltaxV * t);
|
||||
ry1 = 0D;
|
||||
}
|
||||
}
|
||||
|
||||
if (rx0 >= this.Width && rx1 >= this.Width)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (rx0 > 0D && rx1 > 0D && rx0 < this.Width && rx1 < this.Width)
|
||||
{
|
||||
this.AddContainedLineF24Dot8(
|
||||
Math.Clamp(FloatToFixed24Dot8((float)rx0), 0, this.Width * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)rx1), 0, this.Width * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne));
|
||||
return;
|
||||
}
|
||||
|
||||
if (rx0 <= 0D && rx1 <= 0D)
|
||||
{
|
||||
// A segment that stays left of the visible band contributes winding only.
|
||||
this.UpdateStartCoversClipped(
|
||||
Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne));
|
||||
this.hasAnyCoverage = true;
|
||||
return;
|
||||
}
|
||||
|
||||
double deltayH = ry1 - ry0;
|
||||
double deltaxH = Math.Abs(rx1 - rx0);
|
||||
|
||||
if (rx1 > rx0)
|
||||
{
|
||||
double bx1 = rx1;
|
||||
double by1 = ry1;
|
||||
|
||||
if (rx1 > this.Width)
|
||||
{
|
||||
double t = (this.Width - rx0) / deltaxH;
|
||||
by1 = ry0 + (deltayH * t);
|
||||
bx1 = this.Width;
|
||||
}
|
||||
|
||||
if (rx0 < 0D)
|
||||
{
|
||||
double t = -rx0 / deltaxH;
|
||||
int a = Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne);
|
||||
int by = Math.Clamp(FloatToFixed24Dot8((float)(ry0 + (deltayH * t))), 0, this.Height * FixedOne);
|
||||
int cx = Math.Clamp(FloatToFixed24Dot8((float)bx1), 0, this.Width * FixedOne);
|
||||
int cy = Math.Clamp(FloatToFixed24Dot8((float)by1), 0, this.Height * FixedOne);
|
||||
|
||||
this.UpdateStartCoversClipped(a, by);
|
||||
this.hasAnyCoverage = true;
|
||||
|
||||
// The visible portion begins exactly at x == 0 after the left-edge clip.
|
||||
this.AddContainedLineF24Dot8(0, by, cx, cy);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.AddContainedLineF24Dot8(
|
||||
Math.Clamp(FloatToFixed24Dot8((float)rx0), 0, this.Width * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)ry0), 0, this.Height * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)bx1), 0, this.Width * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)by1), 0, this.Height * FixedOne));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
double bx0 = rx0;
|
||||
double by0 = ry0;
|
||||
|
||||
if (rx0 > this.Width)
|
||||
{
|
||||
double t = (rx0 - this.Width) / deltaxH;
|
||||
by0 = ry0 + (deltayH * t);
|
||||
bx0 = this.Width;
|
||||
}
|
||||
|
||||
if (rx1 < 0D)
|
||||
{
|
||||
double t = rx0 / deltaxH;
|
||||
int ax = Math.Clamp(FloatToFixed24Dot8((float)bx0), 0, this.Width * FixedOne);
|
||||
int ay = Math.Clamp(FloatToFixed24Dot8((float)by0), 0, this.Height * FixedOne);
|
||||
int by = Math.Clamp(FloatToFixed24Dot8((float)(ry0 + (deltayH * t))), 0, this.Height * FixedOne);
|
||||
int c = Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne);
|
||||
|
||||
// The right-to-left case mirrors the left-edge handling above: emit the
|
||||
// visible portion first, then retain the winding-only tail as start covers.
|
||||
this.AddContainedLineF24Dot8(ax, ay, 0, by);
|
||||
this.UpdateStartCoversClipped(by, c);
|
||||
this.hasAnyCoverage = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.AddContainedLineF24Dot8(
|
||||
Math.Clamp(FloatToFixed24Dot8((float)bx0), 0, this.Width * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)by0), 0, this.Height * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)rx1), 0, this.Width * FixedOne),
|
||||
Math.Clamp(FloatToFixed24Dot8((float)ry1), 0, this.Height * FixedOne));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds one fully-contained line segment in 24.8 fixed-point coordinates.
|
||||
/// </summary>
|
||||
/// <param name="x0">The starting X coordinate.</param>
|
||||
/// <param name="y0">The starting Y coordinate.</param>
|
||||
/// <param name="x1">The ending X coordinate.</param>
|
||||
/// <param name="y1">The ending Y coordinate.</param>
|
||||
protected void AddContainedLineF24Dot8(int x0, int y0, int x1, int y1)
|
||||
{
|
||||
if (y0 == y1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (x0 == x1)
|
||||
{
|
||||
if (y0 < y1)
|
||||
{
|
||||
this.VerticalDown(x0, y0, y1);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.VerticalUp(x0, y0, y1);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
int dx = Math.Abs(x1 - x0);
|
||||
int dy = Math.Abs(y1 - y0);
|
||||
if (dx > MaximumDelta || dy > MaximumDelta)
|
||||
{
|
||||
int mx = (x0 + x1) >> 1;
|
||||
int my = (y0 + y1) >> 1;
|
||||
this.AddContainedLineF24Dot8(x0, y0, mx, my);
|
||||
this.AddContainedLineF24Dot8(mx, my, x1, y1);
|
||||
return;
|
||||
}
|
||||
|
||||
int rowIndex0;
|
||||
int rowIndex1;
|
||||
int bandTopStart = this.BandTopStart * FixedOne;
|
||||
int bandHeight = PreferredRowHeight * FixedOne;
|
||||
if (y0 < y1)
|
||||
{
|
||||
rowIndex0 = (y0 - bandTopStart) / bandHeight;
|
||||
rowIndex1 = ((y1 - 1) - bandTopStart) / bandHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
rowIndex0 = ((y0 - 1) - bandTopStart) / bandHeight;
|
||||
rowIndex1 = (y1 - bandTopStart) / bandHeight;
|
||||
}
|
||||
|
||||
if ((uint)rowIndex0 >= (uint)this.RowBandCount || (uint)rowIndex1 >= (uint)this.RowBandCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (rowIndex0 == rowIndex1)
|
||||
{
|
||||
int rowTop = bandTopStart + (rowIndex0 * bandHeight);
|
||||
this.AppendLine(rowIndex0, x0, y0 - rowTop, x1, y1 - rowTop);
|
||||
this.LineCounts[rowIndex0]++;
|
||||
this.hasAnyCoverage = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.SplitAcrossBands(x0, y0, x1, y1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the mutable line collector used for one row band.
|
||||
/// </summary>
|
||||
/// <returns>The mutable line collector.</returns>
|
||||
protected abstract TL CreateLineArray();
|
||||
|
||||
/// <summary>
|
||||
/// Appends one line segment into the retained row-band collector.
|
||||
/// </summary>
|
||||
/// <param name="rowIndex">The local row-band index.</param>
|
||||
/// <param name="x0">The starting X coordinate relative to the row band.</param>
|
||||
/// <param name="y0">The starting Y coordinate relative to the row band.</param>
|
||||
/// <param name="x1">The ending X coordinate relative to the row band.</param>
|
||||
/// <param name="y1">The ending Y coordinate relative to the row band.</param>
|
||||
protected abstract void AppendLine(int rowIndex, int x0, int y0, int x1, int y1);
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes the mutable collectors into the retained line-block representation.
|
||||
/// </summary>
|
||||
protected abstract void FinalizeLines();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mutable line collector for a row band, creating it on first use.
|
||||
/// </summary>
|
||||
/// <param name="rowIndex">The local row-band index.</param>
|
||||
/// <returns>The mutable line collector.</returns>
|
||||
protected TL GetOrCreateLineArray(int rowIndex)
|
||||
{
|
||||
TL? lineArray = this.LineArrays[rowIndex];
|
||||
if (lineArray is not null)
|
||||
{
|
||||
return lineArray;
|
||||
}
|
||||
|
||||
lineArray = this.CreateLineArray();
|
||||
this.LineArrays[rowIndex] = lineArray;
|
||||
return lineArray;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a downward vertical segment by delegating to the shared band-splitting path.
|
||||
/// </summary>
|
||||
/// <param name="x">The fixed-point X coordinate.</param>
|
||||
/// <param name="y0">The starting fixed-point Y coordinate.</param>
|
||||
/// <param name="y1">The ending fixed-point Y coordinate.</param>
|
||||
private void VerticalDown(int x, int y0, int y1) => this.SplitAcrossBands(x, y0, x, y1);
|
||||
|
||||
/// <summary>
|
||||
/// Adds an upward vertical segment by delegating to the shared band-splitting path.
|
||||
/// </summary>
|
||||
/// <param name="x">The fixed-point X coordinate.</param>
|
||||
/// <param name="y0">The starting fixed-point Y coordinate.</param>
|
||||
/// <param name="y1">The ending fixed-point Y coordinate.</param>
|
||||
private void VerticalUp(int x, int y0, int y1) => this.SplitAcrossBands(x, y0, x, y1);
|
||||
|
||||
/// <summary>
|
||||
/// Splits a contained line segment at row-band boundaries and appends each retained piece.
|
||||
/// </summary>
|
||||
/// <param name="x0">The starting X coordinate.</param>
|
||||
/// <param name="y0">The starting Y coordinate.</param>
|
||||
/// <param name="x1">The ending X coordinate.</param>
|
||||
/// <param name="y1">The ending Y coordinate.</param>
|
||||
private void SplitAcrossBands(int x0, int y0, int x1, int y1)
|
||||
{
|
||||
int dy = y1 - y0;
|
||||
int dx = x1 - x0;
|
||||
int bandTopStart = this.BandTopStart * FixedOne;
|
||||
int bandHeight = PreferredRowHeight * FixedOne;
|
||||
int startBand = dy > 0 ? (y0 - bandTopStart) / bandHeight : ((y0 - 1) - bandTopStart) / bandHeight;
|
||||
int endBand = dy > 0 ? ((y1 - 1) - bandTopStart) / bandHeight : (y1 - bandTopStart) / bandHeight;
|
||||
int step = dy > 0 ? 1 : -1;
|
||||
int currentBand = startBand;
|
||||
int currentX = x0;
|
||||
int currentY = y0;
|
||||
|
||||
while (currentBand != endBand)
|
||||
{
|
||||
int bandBoundaryY = dy > 0 ? bandTopStart + ((currentBand + 1) * bandHeight) : bandTopStart + (currentBand * bandHeight);
|
||||
int deltaY = bandBoundaryY - currentY;
|
||||
int nextX = currentX + (int)(((long)dx * deltaY) / dy);
|
||||
int rowTop = bandTopStart + (currentBand * bandHeight);
|
||||
|
||||
// Each retained segment is stored in the local coordinate space of its owning band.
|
||||
this.AppendLine(currentBand, currentX, currentY - rowTop, nextX, bandBoundaryY - rowTop);
|
||||
this.LineCounts[currentBand]++;
|
||||
this.hasAnyCoverage = true;
|
||||
currentX = nextX;
|
||||
currentY = bandBoundaryY;
|
||||
currentBand += step;
|
||||
|
||||
if ((uint)currentBand >= (uint)this.RowBandCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int finalRowTop = bandTopStart + (endBand * bandHeight);
|
||||
this.AppendLine(endBand, currentX, currentY - finalRowTop, x1, y1 - finalRowTop);
|
||||
this.LineCounts[endBand]++;
|
||||
this.hasAnyCoverage = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates retained start-cover rows for a line that has been clipped against the visible band.
|
||||
/// </summary>
|
||||
/// <param name="y0">The clipped starting Y coordinate.</param>
|
||||
/// <param name="y1">The clipped ending Y coordinate.</param>
|
||||
private void UpdateStartCoversClipped(int y0, int y1)
|
||||
{
|
||||
if (y0 == y1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (y0 < y1)
|
||||
{
|
||||
int bandTopStart = this.BandTopStart * FixedOne;
|
||||
int bandHeight = PreferredRowHeight * FixedOne;
|
||||
int rowIndex0 = (y0 - bandTopStart) / bandHeight;
|
||||
int rowIndex1 = ((y1 - 1) - bandTopStart) / bandHeight;
|
||||
rowIndex0 = Math.Clamp(rowIndex0, 0, this.RowBandCount - 1);
|
||||
rowIndex1 = Math.Clamp(rowIndex1, 0, this.RowBandCount - 1);
|
||||
int fy0 = y0 - (bandTopStart + (rowIndex0 * bandHeight));
|
||||
int fy1 = y1 - (bandTopStart + (rowIndex1 * bandHeight));
|
||||
this.UpdateStartCovers(rowIndex0, fy0, rowIndex0 == rowIndex1 ? fy1 : bandHeight);
|
||||
for (int i = rowIndex0 + 1; i < rowIndex1; i++)
|
||||
{
|
||||
// Full interior bands receive a constant winding contribution.
|
||||
this.FillStartCovers(i, -FixedOne);
|
||||
}
|
||||
|
||||
if (rowIndex0 != rowIndex1)
|
||||
{
|
||||
this.UpdateStartCovers(rowIndex1, 0, fy1);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int bandTopStart = this.BandTopStart * FixedOne;
|
||||
int bandHeight = PreferredRowHeight * FixedOne;
|
||||
int rowIndex0 = ((y0 - 1) - bandTopStart) / bandHeight;
|
||||
int rowIndex1 = (y1 - bandTopStart) / bandHeight;
|
||||
rowIndex0 = Math.Clamp(rowIndex0, 0, this.RowBandCount - 1);
|
||||
rowIndex1 = Math.Clamp(rowIndex1, 0, this.RowBandCount - 1);
|
||||
int fy0 = y0 - (bandTopStart + (rowIndex0 * bandHeight));
|
||||
int fy1 = y1 - (bandTopStart + (rowIndex1 * bandHeight));
|
||||
this.UpdateStartCovers(rowIndex0, fy0, rowIndex0 == rowIndex1 ? fy1 : 0);
|
||||
for (int i = rowIndex0 - 1; i > rowIndex1; i--)
|
||||
{
|
||||
// Full interior bands receive a constant winding contribution.
|
||||
this.FillStartCovers(i, FixedOne);
|
||||
}
|
||||
|
||||
if (rowIndex0 != rowIndex1)
|
||||
{
|
||||
this.UpdateStartCovers(rowIndex1, bandHeight, fy1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills an entire retained start-cover row with a constant winding value.
|
||||
/// </summary>
|
||||
/// <param name="localBandIndex">The local row-band index.</param>
|
||||
/// <param name="value">The constant winding value to add.</param>
|
||||
private void FillStartCovers(int localBandIndex, int value)
|
||||
{
|
||||
IMemoryOwner<int>? owner = this.StartCoverTable[localBandIndex];
|
||||
if (owner is null)
|
||||
{
|
||||
owner = this.Allocator.Allocate<int>(PreferredRowHeight, AllocationOptions.Clean);
|
||||
this.StartCoverTable[localBandIndex] = owner;
|
||||
owner.Memory.Span[..PreferredRowHeight].Fill(value);
|
||||
return;
|
||||
}
|
||||
|
||||
Span<int> covers = owner.Memory.Span[..PreferredRowHeight];
|
||||
for (int i = 0; i < PreferredRowHeight; i++)
|
||||
{
|
||||
covers[i] += value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a retained start-cover row for one clipped vertical interval.
|
||||
/// </summary>
|
||||
/// <param name="localBandIndex">The local row-band index.</param>
|
||||
/// <param name="y0">The starting Y coordinate relative to the row band.</param>
|
||||
/// <param name="y1">The ending Y coordinate relative to the row band.</param>
|
||||
private void UpdateStartCovers(int localBandIndex, int y0, int y1)
|
||||
{
|
||||
IMemoryOwner<int>? owner = this.StartCoverTable[localBandIndex];
|
||||
if (owner is null)
|
||||
{
|
||||
owner = this.Allocator.Allocate<int>(PreferredRowHeight, AllocationOptions.Clean);
|
||||
this.StartCoverTable[localBandIndex] = owner;
|
||||
}
|
||||
|
||||
Span<int> covers = owner.Memory.Span[..PreferredRowHeight];
|
||||
if (y0 < y1)
|
||||
{
|
||||
UpdateCoverTableDown(covers, y0, y1);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateCoverTableUp(covers, y0, y1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a downward winding contribution to one retained start-cover table.
|
||||
/// </summary>
|
||||
/// <param name="covers">The retained start-cover rows.</param>
|
||||
/// <param name="y0">The starting Y coordinate relative to the row band.</param>
|
||||
/// <param name="y1">The ending Y coordinate relative to the row band.</param>
|
||||
private static void UpdateCoverTableDown(Span<int> covers, int y0, int y1)
|
||||
{
|
||||
int rowIndex0 = y0 >> FixedShift;
|
||||
int rowIndex1 = (y1 - 1) >> FixedShift;
|
||||
int fy0 = y0 - (rowIndex0 << FixedShift);
|
||||
int fy1 = y1 - (rowIndex1 << FixedShift);
|
||||
|
||||
if (rowIndex0 == rowIndex1)
|
||||
{
|
||||
covers[rowIndex0] -= fy1 - fy0;
|
||||
return;
|
||||
}
|
||||
|
||||
covers[rowIndex0] -= FixedOne - fy0;
|
||||
for (int i = rowIndex0 + 1; i < rowIndex1; i++)
|
||||
{
|
||||
covers[i] -= FixedOne;
|
||||
}
|
||||
|
||||
covers[rowIndex1] -= fy1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies an upward winding contribution to one retained start-cover table.
|
||||
/// </summary>
|
||||
/// <param name="covers">The retained start-cover rows.</param>
|
||||
/// <param name="y0">The starting Y coordinate relative to the row band.</param>
|
||||
/// <param name="y1">The ending Y coordinate relative to the row band.</param>
|
||||
private static void UpdateCoverTableUp(Span<int> covers, int y0, int y1)
|
||||
{
|
||||
int rowIndex0 = (y0 - 1) >> FixedShift;
|
||||
int rowIndex1 = y1 >> FixedShift;
|
||||
int fy0 = y0 - (rowIndex0 << FixedShift);
|
||||
int fy1 = y1 - (rowIndex1 << FixedShift);
|
||||
|
||||
if (rowIndex0 == rowIndex1)
|
||||
{
|
||||
covers[rowIndex0] += fy0 - fy1;
|
||||
return;
|
||||
}
|
||||
|
||||
covers[rowIndex0] += fy0;
|
||||
for (int i = rowIndex0 - 1; i > rowIndex1; i--)
|
||||
{
|
||||
covers[i] += FixedOne;
|
||||
}
|
||||
|
||||
covers[rowIndex1] += FixedOne - fy1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Linearizer that finalizes retained lines into the 32-bit-X encoding.
|
||||
/// </summary>
|
||||
private sealed class LinearizerX32Y16 : Linearizer<LineArrayX32Y16>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinearizerX32Y16"/> class.
|
||||
/// </summary>
|
||||
public LinearizerX32Y16(
|
||||
LinearGeometry geometry,
|
||||
Matrix4x4 residual,
|
||||
int translateX,
|
||||
int translateY,
|
||||
int minX,
|
||||
int minY,
|
||||
int width,
|
||||
int height,
|
||||
int firstBandIndex,
|
||||
int rowBandCount,
|
||||
float samplingOffsetX,
|
||||
float samplingOffsetY,
|
||||
MemoryAllocator allocator)
|
||||
: base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, samplingOffsetX, samplingOffsetY, allocator)
|
||||
=> this.FinalLines = new LineArrayX32Y16Block?[rowBandCount];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the finalized retained line blocks for each row band.
|
||||
/// </summary>
|
||||
public LineArrayX32Y16Block?[] FinalLines { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override LineArrayX32Y16 CreateLineArray() => new();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1)
|
||||
=> this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void FinalizeLines()
|
||||
{
|
||||
for (int i = 0; i < this.RowBandCount; i++)
|
||||
{
|
||||
LineArrayX32Y16? lineArray = this.LineArrays[i];
|
||||
this.FinalLines[i] = lineArray?.GetFrontBlock();
|
||||
this.FirstBlockLineCounts[i] = lineArray?.GetFrontBlockLineCount() ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the 32-bit-X linearization pass and returns the retained result.
|
||||
/// </summary>
|
||||
/// <param name="result">The finalized retained raster data.</param>
|
||||
/// <returns><see langword="true"/> when retained coverage was produced; otherwise <see langword="false"/>.</returns>
|
||||
internal bool TryProcess(out LinearizedRasterData<LineArrayX32Y16Block> result)
|
||||
{
|
||||
if (!this.ProcessCore())
|
||||
{
|
||||
result = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = new LinearizedRasterData<LineArrayX32Y16Block>(
|
||||
this.Geometry,
|
||||
new TileBounds(this.MinX, this.FirstBandIndex, this.Width, this.RowBandCount),
|
||||
this.FinalLines,
|
||||
this.FirstBlockLineCounts,
|
||||
this.StartCoverTable);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Linearizer that finalizes retained lines into the packed 16-bit-X encoding.
|
||||
/// </summary>
|
||||
private sealed class LinearizerX16Y16 : Linearizer<LineArrayX16Y16>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinearizerX16Y16"/> class.
|
||||
/// </summary>
|
||||
public LinearizerX16Y16(
|
||||
LinearGeometry geometry,
|
||||
Matrix4x4 residual,
|
||||
int translateX,
|
||||
int translateY,
|
||||
int minX,
|
||||
int minY,
|
||||
int width,
|
||||
int height,
|
||||
int firstBandIndex,
|
||||
int rowBandCount,
|
||||
float samplingOffsetX,
|
||||
float samplingOffsetY,
|
||||
MemoryAllocator allocator)
|
||||
: base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, samplingOffsetX, samplingOffsetY, allocator)
|
||||
=> this.FinalLines = new LineArrayX16Y16Block?[rowBandCount];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the finalized retained line blocks for each row band.
|
||||
/// </summary>
|
||||
public LineArrayX16Y16Block?[] FinalLines { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override LineArrayX16Y16 CreateLineArray() => new();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void AppendLine(int rowIndex, int x0, int y0, int x1, int y1)
|
||||
=> this.GetOrCreateLineArray(rowIndex).AppendLine(x0, y0, x1, y1);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void FinalizeLines()
|
||||
{
|
||||
for (int i = 0; i < this.RowBandCount; i++)
|
||||
{
|
||||
LineArrayX16Y16? lineArray = this.LineArrays[i];
|
||||
this.FinalLines[i] = lineArray?.GetFrontBlock();
|
||||
this.FirstBlockLineCounts[i] = lineArray?.GetFrontBlockLineCount() ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the 16-bit-X linearization pass and returns the retained result.
|
||||
/// </summary>
|
||||
/// <param name="result">The finalized retained raster data.</param>
|
||||
/// <returns><see langword="true"/> when retained coverage was produced; otherwise <see langword="false"/>.</returns>
|
||||
internal bool TryProcess(out LinearizedRasterData<LineArrayX16Y16Block> result)
|
||||
{
|
||||
if (!this.ProcessCore())
|
||||
{
|
||||
result = null!;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = new LinearizedRasterData<LineArrayX16Y16Block>(
|
||||
this.Geometry,
|
||||
new TileBounds(this.MinX, this.FirstBandIndex, this.Width, this.RowBandCount),
|
||||
this.FinalLines,
|
||||
this.FirstBlockLineCounts,
|
||||
this.StartCoverTable);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
internal static partial class DefaultRasterizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Flush-scoped retained row-local raster payload for one prepared fill geometry.
|
||||
/// </summary>
|
||||
internal sealed class RasterizableGeometry : IDisposable
|
||||
{
|
||||
private readonly RasterizableBandInfo[] bandInfos;
|
||||
private readonly LineArrayX16Y16Block?[]? linesX16;
|
||||
private readonly LineArrayX32Y16Block?[]? linesX32;
|
||||
private readonly int[] firstBlockLineCounts;
|
||||
private readonly IMemoryOwner<int>?[] startCoverTable;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RasterizableGeometry"/> class.
|
||||
/// </summary>
|
||||
/// <param name="firstRowBandIndex">The first absolute row-band index touched by the geometry.</param>
|
||||
/// <param name="rowBandCount">The number of retained local row bands owned by the geometry.</param>
|
||||
/// <param name="width">The geometry-local visible band width in pixels.</param>
|
||||
/// <param name="wordsPerRow">The bit-vector width in machine words required by the geometry.</param>
|
||||
/// <param name="coverStride">The scanner cover/area stride required by the geometry.</param>
|
||||
/// <param name="bandHeight">The retained row-band height in pixels.</param>
|
||||
/// <param name="isX16">Indicates whether the geometry uses the narrow X16Y16 line encoding.</param>
|
||||
/// <param name="bandInfos">The retained metadata for each local row band.</param>
|
||||
/// <param name="linesX16">The retained narrow line chains for each local row band.</param>
|
||||
/// <param name="linesX32">The retained wide line chains for each local row band.</param>
|
||||
/// <param name="firstBlockLineCounts">The valid line count in each front retained block.</param>
|
||||
/// <param name="startCoverTable">The retained start-cover table for each local row band.</param>
|
||||
public RasterizableGeometry(
|
||||
int firstRowBandIndex,
|
||||
int rowBandCount,
|
||||
int width,
|
||||
int wordsPerRow,
|
||||
int coverStride,
|
||||
int bandHeight,
|
||||
bool isX16,
|
||||
RasterizableBandInfo[] bandInfos,
|
||||
LineArrayX16Y16Block?[]? linesX16,
|
||||
LineArrayX32Y16Block?[]? linesX32,
|
||||
int[] firstBlockLineCounts,
|
||||
IMemoryOwner<int>?[] startCoverTable)
|
||||
{
|
||||
this.FirstRowBandIndex = firstRowBandIndex;
|
||||
this.RowBandCount = rowBandCount;
|
||||
this.Width = width;
|
||||
this.WordsPerRow = wordsPerRow;
|
||||
this.CoverStride = coverStride;
|
||||
this.BandHeight = bandHeight;
|
||||
this.IsX16 = isX16;
|
||||
this.bandInfos = bandInfos;
|
||||
this.linesX16 = linesX16;
|
||||
this.linesX32 = linesX32;
|
||||
this.firstBlockLineCounts = firstBlockLineCounts;
|
||||
this.startCoverTable = startCoverTable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first absolute row-band index touched by this geometry.
|
||||
/// </summary>
|
||||
public int FirstRowBandIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of retained local row bands owned by this geometry.
|
||||
/// </summary>
|
||||
public int RowBandCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the geometry-local visible band width in pixels.
|
||||
/// </summary>
|
||||
public int Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bit-vector width in machine words required by this geometry.
|
||||
/// </summary>
|
||||
public int WordsPerRow { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the scanner cover/area stride required by this geometry.
|
||||
/// </summary>
|
||||
public int CoverStride { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained row-band height in pixels.
|
||||
/// </summary>
|
||||
public int BandHeight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this geometry uses Blaze's narrow X16Y16 line arrays.
|
||||
/// </summary>
|
||||
public bool IsX16 { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns <see langword="true"/> when the given local row band has retained coverage payload.
|
||||
/// </summary>
|
||||
/// <param name="localRowIndex">The local row band index.</param>
|
||||
/// <returns><see langword="true"/> when the row band has retained coverage; otherwise <see langword="false"/>.</returns>
|
||||
public bool HasCoverage(int localRowIndex) => this.bandInfos[localRowIndex].HasCoverage;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained narrow line block chain for one local row.
|
||||
/// </summary>
|
||||
/// <param name="localRowIndex">The local row band index.</param>
|
||||
/// <returns>The retained narrow line chain for the row.</returns>
|
||||
public LineArrayX16Y16Block? GetLinesX16ForRow(int localRowIndex) => this.linesX16![localRowIndex];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained wide line block chain for one local row.
|
||||
/// </summary>
|
||||
/// <param name="localRowIndex">The local row band index.</param>
|
||||
/// <returns>The retained wide line chain for the row.</returns>
|
||||
public LineArrayX32Y16Block? GetLinesX32ForRow(int localRowIndex) => this.linesX32![localRowIndex];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of valid lines in the first retained block for a local row.
|
||||
/// </summary>
|
||||
/// <param name="localRowIndex">The local row band index.</param>
|
||||
/// <returns>The valid line count in the front retained block.</returns>
|
||||
public int GetFirstBlockLineCountForRow(int localRowIndex) => this.firstBlockLineCounts[localRowIndex];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained start-cover table entry for a local row, if one exists.
|
||||
/// </summary>
|
||||
/// <param name="localRowIndex">The local row band index.</param>
|
||||
/// <returns>The retained start-cover span for the row.</returns>
|
||||
public ReadOnlySpan<int> GetCoversForRow(int localRowIndex)
|
||||
{
|
||||
IMemoryOwner<int>? covers = this.startCoverTable[localRowIndex];
|
||||
return covers is null ? ReadOnlySpan<int>.Empty : covers.Memory.Span[..this.BandHeight];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained start-cover row payload without further interpretation, matching Blaze naming.
|
||||
/// </summary>
|
||||
/// <param name="localRowIndex">The local row band index.</param>
|
||||
/// <returns>The retained start-cover span for the row.</returns>
|
||||
public ReadOnlySpan<int> GetActualCoversForRow(int localRowIndex) => this.GetCoversForRow(localRowIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Gets retained metadata for one local row band.
|
||||
/// </summary>
|
||||
/// <param name="localRowIndex">The local row band index.</param>
|
||||
/// <returns>The retained band metadata.</returns>
|
||||
public RasterizableBandInfo GetBandInfo(int localRowIndex) => this.bandInfos[localRowIndex];
|
||||
|
||||
/// <summary>
|
||||
/// Releases the retained line blocks and start-cover storage.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (this.linesX16 is not null)
|
||||
{
|
||||
Array.Clear(this.linesX16);
|
||||
}
|
||||
|
||||
if (this.linesX32 is not null)
|
||||
{
|
||||
Array.Clear(this.linesX32);
|
||||
}
|
||||
|
||||
for (int i = 0; i < this.startCoverTable.Length; i++)
|
||||
{
|
||||
this.startCoverTable[i]?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
internal static partial class DefaultRasterizer
|
||||
{
|
||||
/// <summary>
|
||||
/// References one retained rasterizable geometry row inside a prepared scene item.
|
||||
/// </summary>
|
||||
internal readonly struct RasterizableItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RasterizableItem"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="rasterizable">The retained rasterizable geometry.</param>
|
||||
/// <param name="localRowIndex">The local row index within <paramref name="rasterizable"/>.</param>
|
||||
public RasterizableItem(RasterizableGeometry rasterizable, int localRowIndex)
|
||||
{
|
||||
this.Rasterizable = rasterizable;
|
||||
this.LocalRowIndex = localRowIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained rasterizable geometry.
|
||||
/// </summary>
|
||||
public RasterizableGeometry Rasterizable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the local row index within <see cref="Rasterizable"/>.
|
||||
/// </summary>
|
||||
public int LocalRowIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of lines stored in the first retained block for this row.
|
||||
/// </summary>
|
||||
/// <returns>The number of valid lines in the leading block.</returns>
|
||||
public int GetFirstBlockLineCount() => this.Rasterizable.GetFirstBlockLineCountForRow(this.LocalRowIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the 16-bit X retained line block for the row when the geometry uses the compact encoding.
|
||||
/// </summary>
|
||||
/// <returns>The retained block chain, or <see langword="null"/> when the row uses the 32-bit encoding.</returns>
|
||||
public LineArrayX16Y16Block? GetLineArrayX16() => this.Rasterizable.GetLinesX16ForRow(this.LocalRowIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the 32-bit X retained line block for the row when the geometry uses the wide encoding.
|
||||
/// </summary>
|
||||
/// <returns>The retained block chain, or <see langword="null"/> when the row uses the 16-bit encoding.</returns>
|
||||
public LineArrayX32Y16Block? GetLineArrayX32() => this.Rasterizable.GetLinesX32ForRow(this.LocalRowIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained start-cover seeds for the row.
|
||||
/// </summary>
|
||||
/// <returns>The retained start-cover span.</returns>
|
||||
public ReadOnlySpan<int> GetActualCovers() => this.Rasterizable.GetActualCoversForRow(this.LocalRowIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// References one retained stroke row inside a prepared scene item.
|
||||
/// </summary>
|
||||
internal readonly struct StrokeRasterizableItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StrokeRasterizableItem"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="rasterizable">The retained stroke rasterizable geometry.</param>
|
||||
/// <param name="localRowIndex">The local row index within <paramref name="rasterizable"/>.</param>
|
||||
public StrokeRasterizableItem(StrokeRasterizableGeometry rasterizable, int localRowIndex)
|
||||
{
|
||||
this.Rasterizable = rasterizable;
|
||||
this.LocalRowIndex = localRowIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained stroke rasterizable geometry.
|
||||
/// </summary>
|
||||
public StrokeRasterizableGeometry Rasterizable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the local row index within <see cref="Rasterizable"/>.
|
||||
/// </summary>
|
||||
public int LocalRowIndex { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metadata that describes one prepared rasterizable band.
|
||||
/// </summary>
|
||||
internal readonly struct RasterizableBandInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RasterizableBandInfo"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="lineCount">The number of retained visible lines in the band.</param>
|
||||
/// <param name="bandHeight">The band height in pixels.</param>
|
||||
/// <param name="width">The visible band width in pixels.</param>
|
||||
/// <param name="wordsPerRow">The bit-vector width in machine words.</param>
|
||||
/// <param name="coverStride">The scanner cover/area stride.</param>
|
||||
/// <param name="destinationLeft">The absolute destination X coordinate of the band's left column.</param>
|
||||
/// <param name="destinationTop">The absolute destination Y coordinate of the band's top row.</param>
|
||||
/// <param name="intersectionRule">The fill rule used when resolving accumulated winding.</param>
|
||||
/// <param name="rasterizationMode">The rasterization mode used by the band.</param>
|
||||
/// <param name="antialiasThreshold">The aliased threshold used when the band runs in aliased mode.</param>
|
||||
/// <param name="hasStartCovers">Indicates whether the band has non-zero start-cover seeds.</param>
|
||||
public RasterizableBandInfo(
|
||||
int lineCount,
|
||||
int bandHeight,
|
||||
int width,
|
||||
int wordsPerRow,
|
||||
int coverStride,
|
||||
int destinationLeft,
|
||||
int destinationTop,
|
||||
IntersectionRule intersectionRule,
|
||||
RasterizationMode rasterizationMode,
|
||||
float antialiasThreshold,
|
||||
bool hasStartCovers)
|
||||
{
|
||||
this.LineCount = lineCount;
|
||||
this.BandHeight = bandHeight;
|
||||
this.Width = width;
|
||||
this.WordsPerRow = wordsPerRow;
|
||||
this.CoverStride = coverStride;
|
||||
this.DestinationLeft = destinationLeft;
|
||||
this.DestinationTop = destinationTop;
|
||||
this.IntersectionRule = intersectionRule;
|
||||
this.RasterizationMode = rasterizationMode;
|
||||
this.AntialiasThreshold = antialiasThreshold;
|
||||
this.HasStartCovers = hasStartCovers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of visible raster lines stored for the band.
|
||||
/// </summary>
|
||||
public int LineCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the band height in pixels.
|
||||
/// </summary>
|
||||
public int BandHeight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the visible band width in pixels.
|
||||
/// </summary>
|
||||
public int Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bit-vector width in machine words.
|
||||
/// </summary>
|
||||
public int WordsPerRow { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the scanner cover/area stride.
|
||||
/// </summary>
|
||||
public int CoverStride { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute destination X coordinate of the band's left column.
|
||||
/// </summary>
|
||||
public int DestinationLeft { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute destination Y coordinate of the band's top row.
|
||||
/// </summary>
|
||||
public int DestinationTop { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the fill rule used when resolving accumulated winding.
|
||||
/// </summary>
|
||||
public IntersectionRule IntersectionRule { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the coverage mode used by the band.
|
||||
/// </summary>
|
||||
public RasterizationMode RasterizationMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the aliased threshold used when the band runs in aliased mode.
|
||||
/// </summary>
|
||||
public float AntialiasThreshold { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the band has non-zero start-cover seeds.
|
||||
/// </summary>
|
||||
public bool HasStartCovers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the band would emit any coverage.
|
||||
/// </summary>
|
||||
public bool HasCoverage => this.LineCount > 0 || this.HasStartCovers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collects retained line segments whose X coordinates require 32-bit storage.
|
||||
/// </summary>
|
||||
internal sealed class LineArrayX32Y16
|
||||
{
|
||||
private LineArrayX32Y16Block? current;
|
||||
private int count = LineArrayX32Y16Block.LineCount;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the front block in the retained line chain.
|
||||
/// </summary>
|
||||
/// <returns>The front retained block, or <see langword="null"/> when no lines were appended.</returns>
|
||||
public LineArrayX32Y16Block? GetFrontBlock() => this.current;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of valid lines in the front retained block.
|
||||
/// </summary>
|
||||
/// <returns>The number of valid front-block lines.</returns>
|
||||
public int GetFrontBlockLineCount() => this.current is null ? 0 : this.count;
|
||||
|
||||
/// <summary>
|
||||
/// Appends one retained line to the chain.
|
||||
/// </summary>
|
||||
/// <param name="x0">The starting X coordinate in 24.8 fixed-point.</param>
|
||||
/// <param name="y0">The starting Y coordinate in 24.8 fixed-point.</param>
|
||||
/// <param name="x1">The ending X coordinate in 24.8 fixed-point.</param>
|
||||
/// <param name="y1">The ending Y coordinate in 24.8 fixed-point.</param>
|
||||
public void AppendLine(int x0, int y0, int x1, int y1)
|
||||
{
|
||||
if (y0 == y1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int packedY0Y1 = Pack(y0, y1);
|
||||
LineArrayX32Y16Block? block = this.current;
|
||||
int currentCount = this.count;
|
||||
if (currentCount < LineArrayX32Y16Block.LineCount)
|
||||
{
|
||||
block!.Set(currentCount, packedY0Y1, x0, x1);
|
||||
this.count = currentCount + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
LineArrayX32Y16Block next = new(block);
|
||||
next.Set(0, packedY0Y1, x0, x1);
|
||||
this.current = next;
|
||||
this.count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Packs two signed 16-bit fixed-point values into one 32-bit integer.
|
||||
/// </summary>
|
||||
/// <param name="lo">The low 16-bit value.</param>
|
||||
/// <param name="hi">The high 16-bit value.</param>
|
||||
/// <returns>The packed value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int Pack(int lo, int hi) => (lo & 0xFFFF) | (hi << 16);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents one retained 32-bit-X line block.
|
||||
/// </summary>
|
||||
internal sealed class LineArrayX32Y16Block : ILineBlock<LineArrayX32Y16Block>
|
||||
{
|
||||
private const int BlockLineCount = 32;
|
||||
private PackedLineX32Y16Buffer lines;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LineArrayX32Y16Block"/> class.
|
||||
/// </summary>
|
||||
/// <param name="next">The next block in the retained chain.</param>
|
||||
public LineArrayX32Y16Block(LineArrayX32Y16Block? next) => this.Next = next;
|
||||
|
||||
/// <inheritdoc />
|
||||
public static int LineCount => BlockLineCount;
|
||||
|
||||
/// <inheritdoc />
|
||||
public LineArrayX32Y16Block? Next { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores one retained line into the block.
|
||||
/// </summary>
|
||||
/// <param name="index">The block-local line index.</param>
|
||||
/// <param name="packedY0Y1">The packed 16-bit Y endpoints.</param>
|
||||
/// <param name="x0">The starting X coordinate in 24.8 fixed-point.</param>
|
||||
/// <param name="x1">The ending X coordinate in 24.8 fixed-point.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Set(int index, int packedY0Y1, int x0, int x1)
|
||||
{
|
||||
ref PackedLineX32Y16 line = ref this.lines[index];
|
||||
line.PackedY0Y1 = packedY0Y1;
|
||||
line.X0 = x0;
|
||||
line.X1 = x1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Rasterize(int count, ref Context context)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
PackedLineX32Y16 line = this.lines[i];
|
||||
context.RasterizeLineSegment(line.X0, UnpackLo(line.PackedY0Y1), line.X1, UnpackHi(line.PackedY0Y1));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates the retained block chain and rasterizes each block in sequence.
|
||||
/// </summary>
|
||||
/// <param name="firstBlockLineCount">The number of valid lines stored in the front block.</param>
|
||||
/// <param name="context">The mutable scan-conversion context.</param>
|
||||
public void Iterate(int firstBlockLineCount, ref Context context)
|
||||
{
|
||||
int count = firstBlockLineCount;
|
||||
LineArrayX32Y16Block? lineBlock = this;
|
||||
while (lineBlock is not null)
|
||||
{
|
||||
lineBlock.Rasterize(count, ref context);
|
||||
lineBlock = lineBlock.Next;
|
||||
count = LineCount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unpacks the low signed 16-bit value from a packed endpoint pair.
|
||||
/// </summary>
|
||||
/// <param name="packed">The packed endpoint pair.</param>
|
||||
/// <returns>The unpacked low value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int UnpackLo(int packed) => (short)(packed & 0xFFFF);
|
||||
|
||||
/// <summary>
|
||||
/// Unpacks the high signed 16-bit value from a packed endpoint pair.
|
||||
/// </summary>
|
||||
/// <param name="packed">The packed endpoint pair.</param>
|
||||
/// <returns>The unpacked high value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int UnpackHi(int packed) => packed >> 16;
|
||||
|
||||
/// <summary>
|
||||
/// Holds one retained 32-bit-X line record in block-local storage.
|
||||
/// </summary>
|
||||
private struct PackedLineX32Y16
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the packed Y endpoints.
|
||||
/// </summary>
|
||||
public int PackedY0Y1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the starting X coordinate.
|
||||
/// </summary>
|
||||
public int X0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ending X coordinate.
|
||||
/// </summary>
|
||||
public int X1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds the fixed-capacity retained line payload inline with the block object.
|
||||
/// </summary>
|
||||
[InlineArray(BlockLineCount)]
|
||||
private struct PackedLineX32Y16Buffer
|
||||
{
|
||||
private PackedLineX32Y16 element0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collects retained line segments whose X coordinates fit in packed 16-bit storage.
|
||||
/// </summary>
|
||||
internal sealed class LineArrayX16Y16
|
||||
{
|
||||
private LineArrayX16Y16Block? current;
|
||||
private int count = LineArrayX16Y16Block.LineCount;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the front block in the retained line chain.
|
||||
/// </summary>
|
||||
/// <returns>The front retained block, or <see langword="null"/> when no lines were appended.</returns>
|
||||
public LineArrayX16Y16Block? GetFrontBlock() => this.current;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of valid lines in the front retained block.
|
||||
/// </summary>
|
||||
/// <returns>The number of valid front-block lines.</returns>
|
||||
public int GetFrontBlockLineCount() => this.current is null ? 0 : this.count;
|
||||
|
||||
/// <summary>
|
||||
/// Appends one retained line to the chain.
|
||||
/// </summary>
|
||||
/// <param name="x0">The starting X coordinate in 24.8 fixed-point.</param>
|
||||
/// <param name="y0">The starting Y coordinate in 24.8 fixed-point.</param>
|
||||
/// <param name="x1">The ending X coordinate in 24.8 fixed-point.</param>
|
||||
/// <param name="y1">The ending Y coordinate in 24.8 fixed-point.</param>
|
||||
public void AppendLine(int x0, int y0, int x1, int y1)
|
||||
{
|
||||
if (y0 == y1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int packedY0Y1 = Pack(y0, y1);
|
||||
int packedX0X1 = Pack(x0, x1);
|
||||
LineArrayX16Y16Block? block = this.current;
|
||||
int currentCount = this.count;
|
||||
if (currentCount < LineArrayX16Y16Block.LineCount)
|
||||
{
|
||||
block!.Set(currentCount, packedY0Y1, packedX0X1);
|
||||
this.count = currentCount + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
LineArrayX16Y16Block next = new(block);
|
||||
next.Set(0, packedY0Y1, packedX0X1);
|
||||
this.current = next;
|
||||
this.count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Packs two signed 16-bit fixed-point values into one 32-bit integer.
|
||||
/// </summary>
|
||||
/// <param name="lo">The low 16-bit value.</param>
|
||||
/// <param name="hi">The high 16-bit value.</param>
|
||||
/// <returns>The packed value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int Pack(int lo, int hi) => (lo & 0xFFFF) | (hi << 16);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents one retained 16-bit-X line block.
|
||||
/// </summary>
|
||||
internal sealed class LineArrayX16Y16Block : ILineBlock<LineArrayX16Y16Block>
|
||||
{
|
||||
private const int BlockLineCount = 32;
|
||||
private PackedLineX16Y16Buffer lines;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LineArrayX16Y16Block"/> class.
|
||||
/// </summary>
|
||||
/// <param name="next">The next block in the retained chain.</param>
|
||||
public LineArrayX16Y16Block(LineArrayX16Y16Block? next) => this.Next = next;
|
||||
|
||||
/// <inheritdoc />
|
||||
public static int LineCount => BlockLineCount;
|
||||
|
||||
/// <inheritdoc />
|
||||
public LineArrayX16Y16Block? Next { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Stores one retained line into the block.
|
||||
/// </summary>
|
||||
/// <param name="index">The block-local line index.</param>
|
||||
/// <param name="packedY0Y1">The packed 16-bit Y endpoints.</param>
|
||||
/// <param name="packedX0X1">The packed 16-bit X endpoints.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Set(int index, int packedY0Y1, int packedX0X1)
|
||||
{
|
||||
ref PackedLineX16Y16 line = ref this.lines[index];
|
||||
line.PackedY0Y1 = packedY0Y1;
|
||||
line.PackedX0X1 = packedX0X1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Rasterize(int count, ref Context context)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
PackedLineX16Y16 line = this.lines[i];
|
||||
context.RasterizeLineSegment(
|
||||
UnpackLo(line.PackedX0X1),
|
||||
UnpackLo(line.PackedY0Y1),
|
||||
UnpackHi(line.PackedX0X1),
|
||||
UnpackHi(line.PackedY0Y1));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Iterates the retained block chain and rasterizes each block in sequence.
|
||||
/// </summary>
|
||||
/// <param name="firstBlockLineCount">The number of valid lines stored in the front block.</param>
|
||||
/// <param name="context">The mutable scan-conversion context.</param>
|
||||
public void Iterate(int firstBlockLineCount, ref Context context)
|
||||
{
|
||||
int count = firstBlockLineCount;
|
||||
LineArrayX16Y16Block? lineBlock = this;
|
||||
while (lineBlock is not null)
|
||||
{
|
||||
lineBlock.Rasterize(count, ref context);
|
||||
lineBlock = lineBlock.Next;
|
||||
count = LineCount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unpacks the low signed 16-bit value from a packed endpoint pair.
|
||||
/// </summary>
|
||||
/// <param name="packed">The packed endpoint pair.</param>
|
||||
/// <returns>The unpacked low value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int UnpackLo(int packed) => (short)(packed & 0xFFFF);
|
||||
|
||||
/// <summary>
|
||||
/// Unpacks the high signed 16-bit value from a packed endpoint pair.
|
||||
/// </summary>
|
||||
/// <param name="packed">The packed endpoint pair.</param>
|
||||
/// <returns>The unpacked high value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int UnpackHi(int packed) => packed >> 16;
|
||||
|
||||
/// <summary>
|
||||
/// Holds one retained 16-bit-X line record in block-local storage.
|
||||
/// </summary>
|
||||
private struct PackedLineX16Y16
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the packed Y endpoints.
|
||||
/// </summary>
|
||||
public int PackedY0Y1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the packed X endpoints.
|
||||
/// </summary>
|
||||
public int PackedX0X1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds the fixed-capacity retained line payload inline with the block object.
|
||||
/// </summary>
|
||||
[InlineArray(BlockLineCount)]
|
||||
private struct PackedLineX16Y16Buffer
|
||||
{
|
||||
private PackedLineX16Y16 element0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Base type for retained drawing backend scenes.
|
||||
/// </summary>
|
||||
public abstract class DrawingBackendScene : IDisposable
|
||||
{
|
||||
private readonly IReadOnlyList<IDisposable>? ownedResources;
|
||||
private bool isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DrawingBackendScene"/> class.
|
||||
/// </summary>
|
||||
/// <param name="bounds">The target bounds used to create the scene.</param>
|
||||
/// <param name="ownedResources">Resources that must stay alive for the retained scene.</param>
|
||||
protected DrawingBackendScene(
|
||||
Rectangle bounds,
|
||||
IReadOnlyList<IDisposable>? ownedResources)
|
||||
{
|
||||
this.Bounds = bounds;
|
||||
this.ownedResources = ownedResources;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target bounds used to create the scene.
|
||||
/// </summary>
|
||||
public Rectangle Bounds { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (this.isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.DisposeCore();
|
||||
this.DisposeOwnedResources();
|
||||
this.isDisposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes backend-specific resources retained by this scene.
|
||||
/// </summary>
|
||||
protected virtual void DisposeCore()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes resources retained for image-brush commands in this scene.
|
||||
/// </summary>
|
||||
private void DisposeOwnedResources()
|
||||
{
|
||||
if (this.ownedResources is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < this.ownedResources.Count; i++)
|
||||
{
|
||||
this.ownedResources[i].Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// One prepared draw-order command batch consumed by a drawing backend.
|
||||
/// </summary>
|
||||
public readonly struct DrawingCommandBatch
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DrawingCommandBatch"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="commands">The draw-order scene commands.</param>
|
||||
/// <param name="hasLayers">Indicates whether the command stream contains layer boundaries.</param>
|
||||
public DrawingCommandBatch(
|
||||
IReadOnlyList<CompositionSceneCommand> commands,
|
||||
bool hasLayers)
|
||||
{
|
||||
this.Commands = commands;
|
||||
this.HasLayers = hasLayers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DrawingCommandBatch"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="commands">The backing command buffer.</param>
|
||||
/// <param name="commandCount">The number of commands in the prepared batch.</param>
|
||||
/// <param name="hasLayers">Indicates whether the command stream contains layer boundaries.</param>
|
||||
internal DrawingCommandBatch(
|
||||
CompositionSceneCommand[] commands,
|
||||
int commandCount,
|
||||
bool hasLayers)
|
||||
: this(new ArraySegment<CompositionSceneCommand>(commands, 0, commandCount), hasLayers)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DrawingCommandBatch"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="commands">The backing command buffer.</param>
|
||||
/// <param name="startIndex">The first command index.</param>
|
||||
/// <param name="commandCount">The number of commands in the prepared batch.</param>
|
||||
/// <param name="hasLayers">Indicates whether the command stream contains layer boundaries.</param>
|
||||
internal DrawingCommandBatch(
|
||||
CompositionSceneCommand[] commands,
|
||||
int startIndex,
|
||||
int commandCount,
|
||||
bool hasLayers)
|
||||
: this(new ArraySegment<CompositionSceneCommand>(commands, startIndex, commandCount), hasLayers)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the draw-order scene commands.
|
||||
/// </summary>
|
||||
public IReadOnlyList<CompositionSceneCommand> Commands { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total number of draw-order commands in the scene.
|
||||
/// </summary>
|
||||
public int CommandCount => this.Commands.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this scene contains inline layer commands.
|
||||
/// </summary>
|
||||
public bool HasLayers { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Represents a flush-ready CPU scene built from retained row-local raster payload.
|
||||
/// </summary>
|
||||
internal sealed partial class FlushScene
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies the retained row operation carried by a <see cref="SceneOperation"/>.
|
||||
/// </summary>
|
||||
internal enum SceneOperationKind : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// A retained fill item.
|
||||
/// </summary>
|
||||
FillItem = 0,
|
||||
|
||||
/// <summary>
|
||||
/// A retained stroke item.
|
||||
/// </summary>
|
||||
StrokeItem = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Starts an isolated compositing layer.
|
||||
/// </summary>
|
||||
BeginLayer = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Ends the most recently opened layer.
|
||||
/// </summary>
|
||||
EndLayer = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds one retained row operation.
|
||||
/// </summary>
|
||||
internal readonly struct SceneOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SceneOperation"/> struct for a draw item.
|
||||
/// </summary>
|
||||
/// <param name="kind">The retained draw operation kind.</param>
|
||||
/// <param name="itemIndex">The retained scene item index.</param>
|
||||
/// <param name="localRowIndex">The retained rasterizable row index.</param>
|
||||
public SceneOperation(SceneOperationKind kind, int itemIndex, int localRowIndex)
|
||||
{
|
||||
this.Kind = kind;
|
||||
this.ItemIndex = itemIndex;
|
||||
this.LocalRowIndex = localRowIndex;
|
||||
this.LayerBounds = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SceneOperation"/> struct for a layer control operation.
|
||||
/// </summary>
|
||||
/// <param name="kind">The layer operation kind.</param>
|
||||
/// <param name="layerBounds">The retained row-local layer bounds.</param>
|
||||
/// <param name="itemIndex">The retained layer-options index for begin-layer operations.</param>
|
||||
public SceneOperation(CompositionCommandKind kind, Rectangle layerBounds, int itemIndex)
|
||||
{
|
||||
this.Kind = kind == CompositionCommandKind.BeginLayer ? SceneOperationKind.BeginLayer : SceneOperationKind.EndLayer;
|
||||
this.ItemIndex = itemIndex;
|
||||
this.LocalRowIndex = -1;
|
||||
this.LayerBounds = layerBounds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the operation kind.
|
||||
/// </summary>
|
||||
public SceneOperationKind Kind { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained scene item index for fill operations.
|
||||
/// </summary>
|
||||
public int ItemIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained rasterizable row index for fill operations.
|
||||
/// </summary>
|
||||
public int LocalRowIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained row-local layer bounds for layer operations.
|
||||
/// </summary>
|
||||
public Rectangle LayerBounds { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds one retained scene row.
|
||||
/// </summary>
|
||||
internal readonly struct SceneRow : IDisposable
|
||||
{
|
||||
private readonly SceneOperationBlock? firstBlock;
|
||||
private readonly SceneOperationBlock? lastBlock;
|
||||
private readonly int rowBandIndex;
|
||||
private readonly int count;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SceneRow"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="firstBlock">The first retained row-item block.</param>
|
||||
/// <param name="lastBlock">The last retained row-item block.</param>
|
||||
/// <param name="rowBandIndex">The absolute row-band index represented by the row.</param>
|
||||
/// <param name="count">The number of retained operations in the row.</param>
|
||||
public SceneRow(SceneOperationBlock? firstBlock, SceneOperationBlock? lastBlock, int rowBandIndex, int count)
|
||||
{
|
||||
this.firstBlock = firstBlock;
|
||||
this.lastBlock = lastBlock;
|
||||
this.rowBandIndex = rowBandIndex;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute row-band index represented by this scene row.
|
||||
/// </summary>
|
||||
public int RowBandIndex => this.rowBandIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of row items in this scene row.
|
||||
/// </summary>
|
||||
public int Count => this.count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first retained row-item block.
|
||||
/// </summary>
|
||||
public SceneOperationBlock? FirstBlock => this.firstBlock;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last retained row-item block.
|
||||
/// </summary>
|
||||
public SceneOperationBlock? LastBlock => this.lastBlock;
|
||||
|
||||
/// <summary>
|
||||
/// Releases the row storage.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
SceneOperationBlock? block = this.firstBlock;
|
||||
while (block is not null)
|
||||
{
|
||||
SceneOperationBlock? next = block.Next;
|
||||
block.Dispose();
|
||||
block = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends row items directly into allocator-backed row storage.
|
||||
/// </summary>
|
||||
private struct RowBuilder : IDisposable
|
||||
{
|
||||
private readonly MemoryAllocator allocator;
|
||||
private SceneOperationBlock? firstBlock;
|
||||
private SceneOperationBlock? lastBlock;
|
||||
private int count;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RowBuilder"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="allocator">The allocator used for row-block storage.</param>
|
||||
public RowBuilder(MemoryAllocator allocator)
|
||||
{
|
||||
this.allocator = allocator;
|
||||
this.firstBlock = null;
|
||||
this.lastBlock = null;
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the builder has been initialized.
|
||||
/// </summary>
|
||||
public readonly bool IsInitialized => this.allocator is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of operations appended to this builder.
|
||||
/// </summary>
|
||||
public readonly int Count => this.count;
|
||||
|
||||
/// <summary>
|
||||
/// Appends a row item.
|
||||
/// </summary>
|
||||
/// <param name="operation">The retained operation to append.</param>
|
||||
public void Append(SceneOperation operation)
|
||||
{
|
||||
if (this.lastBlock is null)
|
||||
{
|
||||
SceneOperationBlock block = new(this.allocator);
|
||||
block.Append(operation);
|
||||
this.firstBlock = block;
|
||||
this.lastBlock = block;
|
||||
this.count++;
|
||||
return;
|
||||
}
|
||||
|
||||
SceneOperationBlock current = this.lastBlock;
|
||||
if (current.Count < SceneOperationBlock.ItemsPerBlock)
|
||||
{
|
||||
current.Append(operation);
|
||||
this.count++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Once a row block fills, link a fresh fixed-capacity block instead of reallocating
|
||||
// and copying existing operations. This keeps the retained row builder append-only.
|
||||
SceneOperationBlock next = new(this.allocator);
|
||||
next.Append(operation);
|
||||
current.Next = next;
|
||||
next.Previous = current;
|
||||
this.lastBlock = next;
|
||||
this.count++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the retained blocks owned by <paramref name="source"/> to <paramref name="destination"/>
|
||||
/// without copying individual operations.
|
||||
/// </summary>
|
||||
/// <param name="destination">The builder receiving the appended blocks.</param>
|
||||
/// <param name="source">The builder supplying the appended blocks.</param>
|
||||
public static void AppendBuilder(ref RowBuilder destination, ref RowBuilder source)
|
||||
{
|
||||
if (source.firstBlock is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (destination.firstBlock is null)
|
||||
{
|
||||
destination = source;
|
||||
source = default;
|
||||
return;
|
||||
}
|
||||
|
||||
destination.lastBlock!.Next = source.firstBlock;
|
||||
source.firstBlock.Previous = destination.lastBlock;
|
||||
destination.lastBlock = source.lastBlock;
|
||||
destination.count += source.count;
|
||||
source = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes the builder into retained scene storage.
|
||||
/// </summary>
|
||||
/// <param name="rowBandIndex">The absolute row-band index represented by the row.</param>
|
||||
/// <returns>The finalized retained row.</returns>
|
||||
public readonly SceneRow Finalize(int rowBandIndex) => new(this.firstBlock, this.lastBlock, rowBandIndex, this.count);
|
||||
|
||||
/// <summary>
|
||||
/// Disposes unfinalized storage.
|
||||
/// </summary>
|
||||
public readonly void Dispose()
|
||||
{
|
||||
SceneOperationBlock? block = this.firstBlock;
|
||||
while (block is not null)
|
||||
{
|
||||
SceneOperationBlock? next = block.Next;
|
||||
block.Dispose();
|
||||
block = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents one fixed-capacity row-item block.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This mirrors Blaze's <c>RowItemList<T>::Block</c> shape: append into the current block,
|
||||
/// allocate a fresh block only when that block fills, and never reallocate or copy existing blocks.
|
||||
/// </remarks>
|
||||
internal sealed class SceneOperationBlock : IDisposable
|
||||
{
|
||||
private readonly IMemoryOwner<SceneOperation> owner;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SceneOperationBlock"/> class.
|
||||
/// </summary>
|
||||
/// <param name="allocator">The allocator used for block storage.</param>
|
||||
public SceneOperationBlock(MemoryAllocator allocator)
|
||||
=> this.owner = allocator.Allocate<SceneOperation>(ItemsPerBlock);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the fixed item capacity per block.
|
||||
/// </summary>
|
||||
public static int ItemsPerBlock => 32;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the previous block in the row list.
|
||||
/// </summary>
|
||||
public SceneOperationBlock? Previous { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the next block in the row list.
|
||||
/// </summary>
|
||||
public SceneOperationBlock? Next { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of items written into this block.
|
||||
/// </summary>
|
||||
public int Count { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the items written into this block.
|
||||
/// </summary>
|
||||
public Span<SceneOperation> Items => this.owner.Memory.Span[..this.Count];
|
||||
|
||||
/// <summary>
|
||||
/// Appends an item into this block.
|
||||
/// </summary>
|
||||
/// <param name="operation">The retained operation to append.</param>
|
||||
public void Append(SceneOperation operation) => this.owner.Memory.Span[this.Count++] = operation;
|
||||
|
||||
/// <summary>
|
||||
/// Releases the block storage.
|
||||
/// </summary>
|
||||
public void Dispose() => this.owner.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds one retained fill scene item.
|
||||
/// </summary>
|
||||
internal sealed class FillSceneItem : IDisposable
|
||||
{
|
||||
private object? renderer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FillSceneItem"/> class.
|
||||
/// </summary>
|
||||
/// <param name="brush">The brush used by the fill item.</param>
|
||||
/// <param name="graphicsOptions">The graphics options used by the fill item.</param>
|
||||
/// <param name="brushBounds">The brush bounds used for applicator creation.</param>
|
||||
/// <param name="rasterizable">The retained rasterizable geometry.</param>
|
||||
public FillSceneItem(
|
||||
Brush brush,
|
||||
GraphicsOptions graphicsOptions,
|
||||
Rectangle brushBounds,
|
||||
DefaultRasterizer.RasterizableGeometry rasterizable)
|
||||
{
|
||||
this.Brush = brush;
|
||||
this.GraphicsOptions = graphicsOptions;
|
||||
this.BrushBounds = brushBounds;
|
||||
this.Rasterizable = rasterizable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brush used by the fill item.
|
||||
/// </summary>
|
||||
public Brush Brush { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the graphics options used by the fill item.
|
||||
/// </summary>
|
||||
public GraphicsOptions GraphicsOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brush bounds used for applicator creation.
|
||||
/// </summary>
|
||||
public Rectangle BrushBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained rasterizable geometry.
|
||||
/// </summary>
|
||||
public DefaultRasterizer.RasterizableGeometry Rasterizable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the memoized renderer for this scene item, creating it on first use.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="canvasWidth">The destination canvas width.</param>
|
||||
/// <returns>The memoized renderer for the scene item.</returns>
|
||||
public BrushRenderer<TPixel> GetRenderer<TPixel>(Configuration configuration, int canvasWidth)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
if (this.renderer is BrushRenderer<TPixel> typed)
|
||||
{
|
||||
return typed;
|
||||
}
|
||||
|
||||
typed = this.Brush.CreateRenderer<TPixel>(
|
||||
configuration,
|
||||
this.GraphicsOptions,
|
||||
canvasWidth,
|
||||
this.BrushBounds);
|
||||
|
||||
this.renderer = typed;
|
||||
return typed;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => this.Rasterizable.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds one retained stroke scene item.
|
||||
/// </summary>
|
||||
internal sealed class StrokeSceneItem : IDisposable
|
||||
{
|
||||
private object? renderer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StrokeSceneItem"/> class.
|
||||
/// </summary>
|
||||
/// <param name="brush">The prepared brush for the stroke item.</param>
|
||||
/// <param name="graphicsOptions">The graphics options for the stroke item.</param>
|
||||
/// <param name="brushBounds">The prepared brush bounds.</param>
|
||||
/// <param name="rasterizable">The retained stroke rasterizable geometry.</param>
|
||||
public StrokeSceneItem(
|
||||
Brush brush,
|
||||
GraphicsOptions graphicsOptions,
|
||||
Rectangle brushBounds,
|
||||
DefaultRasterizer.StrokeRasterizableGeometry rasterizable)
|
||||
{
|
||||
this.Brush = brush;
|
||||
this.GraphicsOptions = graphicsOptions;
|
||||
this.BrushBounds = brushBounds;
|
||||
this.Rasterizable = rasterizable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the prepared brush for the stroke item.
|
||||
/// </summary>
|
||||
public Brush Brush { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the graphics options for the stroke item.
|
||||
/// </summary>
|
||||
public GraphicsOptions GraphicsOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the prepared brush bounds for the stroke item.
|
||||
/// </summary>
|
||||
public Rectangle BrushBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the retained stroke rasterizable geometry.
|
||||
/// </summary>
|
||||
public DefaultRasterizer.StrokeRasterizableGeometry Rasterizable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the memoized renderer for this scene item, creating it on first use.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="canvasWidth">The destination canvas width.</param>
|
||||
/// <returns>The memoized renderer for the scene item.</returns>
|
||||
public BrushRenderer<TPixel> GetRenderer<TPixel>(Configuration configuration, int canvasWidth)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
if (this.renderer is BrushRenderer<TPixel> typed)
|
||||
{
|
||||
return typed;
|
||||
}
|
||||
|
||||
typed = this.Brush.CreateRenderer<TPixel>(
|
||||
configuration,
|
||||
this.GraphicsOptions,
|
||||
canvasWidth,
|
||||
this.BrushBounds);
|
||||
|
||||
this.renderer = typed;
|
||||
return typed;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => this.Rasterizable.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Per-frame destination for <see cref="DrawingCanvas{TPixel}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
public interface ICanvasFrame<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the frame bounds in root target coordinates.
|
||||
/// </summary>
|
||||
public Rectangle Bounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to get a CPU-accessible destination region.
|
||||
/// </summary>
|
||||
/// <param name="region">The CPU region when available.</param>
|
||||
/// <returns><see langword="true"/> when a CPU region is available.</returns>
|
||||
public bool TryGetCpuRegion(out Buffer2DRegion<TPixel> region);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to get an opaque native destination surface.
|
||||
/// </summary>
|
||||
/// <param name="surface">The native surface when available.</param>
|
||||
/// <returns><see langword="true"/> when a native surface is available.</returns>
|
||||
public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Defines the contract for creating and rendering retained drawing scenes for canvas targets.
|
||||
/// </summary>
|
||||
public interface IDrawingBackend
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a retained backend scene from a prepared command batch.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="targetBounds">The target bounds used for target-dependent scene data.</param>
|
||||
/// <param name="commandBatch">The scene commands in submission order.</param>
|
||||
/// <param name="ownedResources">The resources that must stay alive for the returned scene.</param>
|
||||
/// <returns>A retained backend scene.</returns>
|
||||
public DrawingBackendScene CreateScene(
|
||||
Configuration configuration,
|
||||
Rectangle targetBounds,
|
||||
DrawingCommandBatch commandBatch,
|
||||
IReadOnlyList<IDisposable>? ownedResources = null);
|
||||
|
||||
/// <summary>
|
||||
/// Renders a retained backend scene into the target.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="target">The target frame.</param>
|
||||
/// <param name="scene">The retained backend scene to render.</param>
|
||||
public void RenderScene<TPixel>(
|
||||
Configuration configuration,
|
||||
ICanvasFrame<TPixel> target,
|
||||
DrawingBackendScene scene)
|
||||
where TPixel : unmanaged, IPixel<TPixel>;
|
||||
|
||||
/// <summary>
|
||||
/// Reads source pixels from the target into the destination region.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="configuration">The active processing configuration.</param>
|
||||
/// <param name="target">The target frame.</param>
|
||||
/// <param name="sourceRectangle">The source rectangle in target-local coordinates.</param>
|
||||
/// <param name="destination">The destination region that receives the copied pixels.</param>
|
||||
public void ReadRegion<TPixel>(
|
||||
Configuration configuration,
|
||||
ICanvasFrame<TPixel> target,
|
||||
Rectangle sourceRectangle,
|
||||
Buffer2DRegion<TPixel> destination)
|
||||
where TPixel : unmanaged, IPixel<TPixel>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Receives one emitted non-zero coverage span from the rasterizer.
|
||||
/// </summary>
|
||||
internal interface IRasterizerCoverageRowHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles one emitted non-zero coverage span.
|
||||
/// </summary>
|
||||
/// <param name="y">The destination y coordinate.</param>
|
||||
/// <param name="startX">The first x coordinate represented by <paramref name="coverage"/>.</param>
|
||||
/// <param name="coverage">Non-zero coverage values starting at <paramref name="startX"/>.</param>
|
||||
public void Handle(int y, int startX, Span<float> coverage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Canvas frame backed by a <see cref="Buffer2DRegion{T}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
public sealed class MemoryCanvasFrame<TPixel> : ICanvasFrame<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly Buffer2DRegion<TPixel> region;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MemoryCanvasFrame{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="region">The pixel buffer region backing this frame.</param>
|
||||
public MemoryCanvasFrame(Buffer2DRegion<TPixel> region)
|
||||
{
|
||||
Guard.NotNull(region.Buffer, nameof(region));
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Rectangle Bounds => this.region.Bounds;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetCpuRegion(out Buffer2DRegion<TPixel> region)
|
||||
{
|
||||
region = this.region;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface)
|
||||
{
|
||||
surface = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Canvas frame backed by a <see cref="NativeSurface"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
public sealed class NativeCanvasFrame<TPixel> : ICanvasFrame<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly NativeSurface surface;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NativeCanvasFrame{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="bounds">The frame bounds.</param>
|
||||
/// <param name="surface">The native surface backing this frame.</param>
|
||||
public NativeCanvasFrame(Rectangle bounds, NativeSurface surface)
|
||||
{
|
||||
Guard.NotNull(surface, nameof(surface));
|
||||
Guard.MustBeGreaterThan(bounds.Width, 0, nameof(bounds));
|
||||
Guard.MustBeGreaterThan(bounds.Height, 0, nameof(bounds));
|
||||
|
||||
this.Bounds = bounds;
|
||||
this.surface = surface;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Rectangle Bounds { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetCpuRegion(out Buffer2DRegion<TPixel> region)
|
||||
{
|
||||
region = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface)
|
||||
{
|
||||
surface = this.surface;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Base type for backend-specific native drawing targets.
|
||||
/// </summary>
|
||||
public abstract class NativeSurface
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NativeSurface"/> class.
|
||||
/// </summary>
|
||||
protected NativeSurface()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Centralizes the conversion from configuration parallelism settings to partition counts and
|
||||
/// <see cref="ParallelOptions"/> instances used by retained-scene CPU execution paths.
|
||||
/// </summary>
|
||||
internal static class ParallelExecutionHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes the number of partitions to schedule for work constrained by a single work-item limit.
|
||||
/// </summary>
|
||||
/// <param name="maxDegreeOfParallelism">
|
||||
/// The configured maximum degree of parallelism. A value of <c>-1</c> leaves the runtime
|
||||
/// parallelism cap unbounded, but partition planning remains capped to
|
||||
/// <see cref="Environment.ProcessorCount"/> to avoid excessive fan-out.
|
||||
/// </param>
|
||||
/// <param name="workItemCount">The total number of work items available for partitioning.</param>
|
||||
/// <returns>The number of partitions to schedule.</returns>
|
||||
public static int GetPartitionCount(int maxDegreeOfParallelism, int workItemCount)
|
||||
=> Math.Min(GetPartitionLimit(maxDegreeOfParallelism), workItemCount);
|
||||
|
||||
/// <summary>
|
||||
/// Computes the number of partitions to schedule for work constrained by two independent limits.
|
||||
/// </summary>
|
||||
/// <param name="maxDegreeOfParallelism">
|
||||
/// The configured maximum degree of parallelism. A value of <c>-1</c> leaves the runtime
|
||||
/// parallelism cap unbounded, but partition planning remains capped to
|
||||
/// <see cref="Environment.ProcessorCount"/> to avoid excessive fan-out.
|
||||
/// </param>
|
||||
/// <param name="workItemCount">The total number of work items available for partitioning.</param>
|
||||
/// <param name="secondaryLimit">An additional caller-specific upper bound on useful partitions.</param>
|
||||
/// <returns>The number of partitions to schedule.</returns>
|
||||
public static int GetPartitionCount(int maxDegreeOfParallelism, int workItemCount, int secondaryLimit)
|
||||
=> Math.Min(GetPartitionLimit(maxDegreeOfParallelism), Math.Min(workItemCount, secondaryLimit));
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="ParallelOptions"/> for a partitioned operation.
|
||||
/// </summary>
|
||||
/// <param name="maxDegreeOfParallelism">
|
||||
/// The configured maximum degree of parallelism. A value of <c>-1</c> retains the runtime's
|
||||
/// unbounded sentinel because <paramref name="partitionCount"/> is always positive; positive
|
||||
/// values are capped to the smaller of the configured limit and the useful partition count.
|
||||
/// </param>
|
||||
/// <param name="partitionCount">The computed positive number of useful partitions for the operation.</param>
|
||||
/// <returns>The <see cref="ParallelOptions"/> instance for the operation.</returns>
|
||||
public static ParallelOptions CreateParallelOptions(int maxDegreeOfParallelism, int partitionCount)
|
||||
=> new() { MaxDegreeOfParallelism = Math.Min(maxDegreeOfParallelism, partitionCount) };
|
||||
|
||||
/// <summary>
|
||||
/// Computes the internal partition-planning cap for the configured parallelism setting.
|
||||
/// </summary>
|
||||
/// <param name="maxDegreeOfParallelism">
|
||||
/// The configured maximum degree of parallelism. A value of <c>-1</c> keeps the runtime
|
||||
/// parallelism setting unbounded, but partition planning is capped to
|
||||
/// <see cref="Environment.ProcessorCount"/>.
|
||||
/// </param>
|
||||
/// <returns>The maximum number of partitions to plan for.</returns>
|
||||
private static int GetPartitionLimit(int maxDegreeOfParallelism)
|
||||
=> maxDegreeOfParallelism == -1 ? Environment.ProcessorCount : maxDegreeOfParallelism;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// Describes whether rasterizers should emit continuous coverage or binary aliased coverage.
|
||||
/// </summary>
|
||||
public enum RasterizationMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Emit continuous coverage in the range [0, 1].
|
||||
/// </summary>
|
||||
Antialiased = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Emit binary coverage values (0 or 1).
|
||||
/// </summary>
|
||||
Aliased = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes where sample coverage is aligned relative to destination pixels.
|
||||
/// </summary>
|
||||
public enum RasterizerSamplingOrigin
|
||||
{
|
||||
/// <summary>
|
||||
/// Samples are aligned to pixel boundaries.
|
||||
/// </summary>
|
||||
PixelBoundary = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Samples are aligned to pixel centers.
|
||||
/// </summary>
|
||||
PixelCenter = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immutable options used by rasterizers when scan-converting vector geometry.
|
||||
/// </summary>
|
||||
public readonly struct RasterizerOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RasterizerOptions"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="interest">Destination bounds to rasterize into.</param>
|
||||
/// <param name="intersectionRule">Polygon intersection rule.</param>
|
||||
/// <param name="rasterizationMode">Rasterization coverage mode.</param>
|
||||
/// <param name="samplingOrigin">Sampling origin alignment.</param>
|
||||
/// <param name="antialiasThreshold">Coverage threshold for aliased mode (0 to 1).</param>
|
||||
public RasterizerOptions(
|
||||
Rectangle interest,
|
||||
IntersectionRule intersectionRule,
|
||||
RasterizationMode rasterizationMode,
|
||||
RasterizerSamplingOrigin samplingOrigin,
|
||||
float antialiasThreshold)
|
||||
{
|
||||
this.Interest = interest;
|
||||
this.IntersectionRule = intersectionRule;
|
||||
this.RasterizationMode = rasterizationMode;
|
||||
this.SamplingOrigin = samplingOrigin;
|
||||
this.AntialiasThreshold = antialiasThreshold;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets destination bounds to rasterize into.
|
||||
/// </summary>
|
||||
public Rectangle Interest { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the polygon intersection rule.
|
||||
/// </summary>
|
||||
public IntersectionRule IntersectionRule { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the rasterization coverage mode.
|
||||
/// </summary>
|
||||
public RasterizationMode RasterizationMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sampling origin alignment.
|
||||
/// </summary>
|
||||
public RasterizerSamplingOrigin SamplingOrigin { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the coverage threshold used when <see cref="RasterizationMode"/> is <see cref="RasterizationMode.Aliased"/>.
|
||||
/// Pixels with coverage above this value are rendered as fully opaque; pixels below are discarded.
|
||||
/// </summary>
|
||||
public float AntialiasThreshold { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copy of the current options with a different interest rectangle.
|
||||
/// </summary>
|
||||
/// <param name="interest">The replacement interest rectangle.</param>
|
||||
/// <returns>A new <see cref="RasterizerOptions"/> value.</returns>
|
||||
public RasterizerOptions WithInterest(Rectangle interest)
|
||||
=> new(interest, this.IntersectionRule, this.RasterizationMode, this.SamplingOrigin, this.AntialiasThreshold);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// One explicit stroked two-point line-segment command queued by the canvas batcher.
|
||||
/// </summary>
|
||||
public readonly struct StrokeLineSegmentCommand
|
||||
{
|
||||
private readonly PointF sourceStart;
|
||||
private readonly PointF sourceEnd;
|
||||
private readonly DrawingOptions drawingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StrokeLineSegmentCommand"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="sourceStart">The source line start point.</param>
|
||||
/// <param name="sourceEnd">The source line end point.</param>
|
||||
/// <param name="brush">The brush used to shade the stroke.</param>
|
||||
/// <param name="drawingOptions">The drawing options (graphics, shape, transform) used during composition.</param>
|
||||
/// <param name="rasterizerOptions">The rasterizer options used to generate coverage.</param>
|
||||
/// <param name="targetBounds">The absolute bounds of the logical target.</param>
|
||||
/// <param name="destinationOffset">The absolute destination offset of the command.</param>
|
||||
/// <param name="pen">The stroke metadata.</param>
|
||||
/// <param name="isInsideLayer">True if the command was recorded inside a layer.</param>
|
||||
public StrokeLineSegmentCommand(
|
||||
PointF sourceStart,
|
||||
PointF sourceEnd,
|
||||
Brush brush,
|
||||
DrawingOptions drawingOptions,
|
||||
in RasterizerOptions rasterizerOptions,
|
||||
Rectangle targetBounds,
|
||||
Point destinationOffset,
|
||||
Pen pen,
|
||||
bool isInsideLayer)
|
||||
{
|
||||
this.sourceStart = sourceStart;
|
||||
this.sourceEnd = sourceEnd;
|
||||
this.drawingOptions = drawingOptions;
|
||||
this.Brush = brush;
|
||||
this.RasterizerOptions = rasterizerOptions;
|
||||
this.TargetBounds = targetBounds;
|
||||
this.DestinationOffset = destinationOffset;
|
||||
this.Pen = pen;
|
||||
this.IsInsideLayer = isInsideLayer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brush used during composition.
|
||||
/// </summary>
|
||||
public Brush Brush { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the drawing options carried by the command.
|
||||
/// </summary>
|
||||
public DrawingOptions DrawingOptions => this.drawingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the graphics options used during composition.
|
||||
/// </summary>
|
||||
public GraphicsOptions GraphicsOptions => this.drawingOptions.GraphicsOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the rasterizer options used to generate coverage.
|
||||
/// </summary>
|
||||
public RasterizerOptions RasterizerOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute bounds of the logical target for this command.
|
||||
/// </summary>
|
||||
public Rectangle TargetBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute destination offset where the local coverage should be composited.
|
||||
/// </summary>
|
||||
public Point DestinationOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stroke metadata for this command.
|
||||
/// </summary>
|
||||
public Pen Pen { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source line start point.
|
||||
/// </summary>
|
||||
public PointF SourceStart => this.sourceStart;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source line end point.
|
||||
/// </summary>
|
||||
public PointF SourceEnd => this.sourceEnd;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command transform.
|
||||
/// </summary>
|
||||
public Matrix4x4 Transform => this.drawingOptions.Transform;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the command was recorded inside a layer.
|
||||
/// </summary>
|
||||
public bool IsInsideLayer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Computes the conservative stroked bounds of one two-point line segment.
|
||||
/// </summary>
|
||||
/// <param name="start">The line start point.</param>
|
||||
/// <param name="end">The line end point.</param>
|
||||
/// <param name="pen">The stroke metadata.</param>
|
||||
/// <returns>The conservative stroked bounds.</returns>
|
||||
public static RectangleF GetConservativeBounds(PointF start, PointF end, Pen pen)
|
||||
{
|
||||
float left = MathF.Min(start.X, end.X);
|
||||
float top = MathF.Min(start.Y, end.Y);
|
||||
float right = MathF.Max(start.X, end.X);
|
||||
float bottom = MathF.Max(start.Y, end.Y);
|
||||
RectangleF bounds = RectangleF.FromLTRB(left, top, right, bottom);
|
||||
return InflateBounds(bounds, pen);
|
||||
}
|
||||
|
||||
private static RectangleF InflateBounds(RectangleF bounds, Pen pen)
|
||||
{
|
||||
float halfWidth = pen.StrokeWidth * 0.5F;
|
||||
float inflate = pen.StrokeOptions.LineJoin switch
|
||||
{
|
||||
LineJoin.Miter or LineJoin.MiterRevert or LineJoin.MiterRound => (float)(halfWidth * Math.Max(pen.StrokeOptions.MiterLimit, 1D)),
|
||||
_ => halfWidth
|
||||
};
|
||||
|
||||
bounds.Inflate(new SizeF(inflate, inflate));
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// One stroked path command queued by the canvas batcher.
|
||||
/// </summary>
|
||||
public readonly struct StrokePathCommand
|
||||
{
|
||||
private readonly IPath sourcePath;
|
||||
private readonly DrawingOptions drawingOptions;
|
||||
private readonly IReadOnlyList<IPath>? clipPaths;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StrokePathCommand"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="sourcePath">The source stroke path.</param>
|
||||
/// <param name="brush">The brush used to shade the stroke.</param>
|
||||
/// <param name="drawingOptions">The drawing options (graphics, shape, transform) used during composition.</param>
|
||||
/// <param name="rasterizerOptions">The rasterizer options used to generate coverage.</param>
|
||||
/// <param name="targetBounds">The absolute bounds of the logical target.</param>
|
||||
/// <param name="destinationOffset">The absolute destination offset of the command.</param>
|
||||
/// <param name="pen">The stroke metadata.</param>
|
||||
/// <param name="clipPaths">Optional clip paths supplied with the command.</param>
|
||||
/// <param name="isInsideLayer">True if the command was recorded inside a layer.</param>
|
||||
public StrokePathCommand(
|
||||
IPath sourcePath,
|
||||
Brush brush,
|
||||
DrawingOptions drawingOptions,
|
||||
in RasterizerOptions rasterizerOptions,
|
||||
Rectangle targetBounds,
|
||||
Point destinationOffset,
|
||||
Pen pen,
|
||||
IReadOnlyList<IPath>? clipPaths,
|
||||
bool isInsideLayer)
|
||||
{
|
||||
this.sourcePath = sourcePath;
|
||||
this.drawingOptions = drawingOptions;
|
||||
this.clipPaths = clipPaths;
|
||||
this.Brush = brush;
|
||||
this.RasterizerOptions = rasterizerOptions;
|
||||
this.TargetBounds = targetBounds;
|
||||
this.DestinationOffset = destinationOffset;
|
||||
this.Pen = pen;
|
||||
this.IsInsideLayer = isInsideLayer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brush used during composition.
|
||||
/// </summary>
|
||||
public Brush Brush { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the drawing options carried by the command.
|
||||
/// </summary>
|
||||
public DrawingOptions DrawingOptions => this.drawingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the graphics options used during composition.
|
||||
/// </summary>
|
||||
public GraphicsOptions GraphicsOptions => this.drawingOptions.GraphicsOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the rasterizer options used to generate coverage.
|
||||
/// </summary>
|
||||
public RasterizerOptions RasterizerOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute bounds of the logical target for this command.
|
||||
/// </summary>
|
||||
public Rectangle TargetBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute destination offset where the local coverage should be composited.
|
||||
/// </summary>
|
||||
public Point DestinationOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stroke metadata for this command.
|
||||
/// </summary>
|
||||
public Pen Pen { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source stroke path.
|
||||
/// </summary>
|
||||
public IPath SourcePath => this.sourcePath;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the drawing transform.
|
||||
/// </summary>
|
||||
public Matrix4x4 Transform => this.drawingOptions.Transform;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional clip paths carried by the command.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IPath>? ClipPaths => this.clipPaths;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shape options carried by the command.
|
||||
/// </summary>
|
||||
public ShapeOptions ShapeOptions => this.drawingOptions.ShapeOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the command was recorded inside a layer.
|
||||
/// </summary>
|
||||
public bool IsInsideLayer { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Backends {
|
||||
/// <summary>
|
||||
/// One explicit stroked open polyline command queued by the canvas batcher.
|
||||
/// </summary>
|
||||
public readonly struct StrokePolylineCommand
|
||||
{
|
||||
private readonly PointF[] sourcePoints;
|
||||
private readonly DrawingOptions drawingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StrokePolylineCommand"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="sourcePoints">The source polyline points.</param>
|
||||
/// <param name="brush">The brush used to shade the stroke.</param>
|
||||
/// <param name="drawingOptions">The drawing options (graphics, shape, transform) used during composition.</param>
|
||||
/// <param name="rasterizerOptions">The rasterizer options used to generate coverage.</param>
|
||||
/// <param name="targetBounds">The absolute bounds of the logical target.</param>
|
||||
/// <param name="destinationOffset">The absolute destination offset of the command.</param>
|
||||
/// <param name="pen">The stroke metadata.</param>
|
||||
/// <param name="isInsideLayer">True if the command was recorded inside a layer.</param>
|
||||
public StrokePolylineCommand(
|
||||
PointF[] sourcePoints,
|
||||
Brush brush,
|
||||
DrawingOptions drawingOptions,
|
||||
in RasterizerOptions rasterizerOptions,
|
||||
Rectangle targetBounds,
|
||||
Point destinationOffset,
|
||||
Pen pen,
|
||||
bool isInsideLayer)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(sourcePoints);
|
||||
if (sourcePoints.Length < 2)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(sourcePoints), "Open stroke polylines require at least two points.");
|
||||
}
|
||||
|
||||
this.sourcePoints = sourcePoints;
|
||||
this.drawingOptions = drawingOptions;
|
||||
this.Brush = brush;
|
||||
this.RasterizerOptions = rasterizerOptions;
|
||||
this.TargetBounds = targetBounds;
|
||||
this.DestinationOffset = destinationOffset;
|
||||
this.Pen = pen;
|
||||
this.IsInsideLayer = isInsideLayer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brush used during composition.
|
||||
/// </summary>
|
||||
public Brush Brush { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the drawing options carried by the command.
|
||||
/// </summary>
|
||||
public DrawingOptions DrawingOptions => this.drawingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the graphics options used during composition.
|
||||
/// </summary>
|
||||
public GraphicsOptions GraphicsOptions => this.drawingOptions.GraphicsOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the rasterizer options used to generate coverage.
|
||||
/// </summary>
|
||||
public RasterizerOptions RasterizerOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute bounds of the logical target for this command.
|
||||
/// </summary>
|
||||
public Rectangle TargetBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute destination offset where the local coverage should be composited.
|
||||
/// </summary>
|
||||
public Point DestinationOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stroke metadata for this command.
|
||||
/// </summary>
|
||||
public Pen Pen { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source polyline points.
|
||||
/// </summary>
|
||||
public PointF[] SourcePoints => this.sourcePoints;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command transform.
|
||||
/// </summary>
|
||||
public Matrix4x4 Transform => this.drawingOptions.Transform;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the command was recorded inside a layer.
|
||||
/// </summary>
|
||||
public bool IsInsideLayer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Computes the conservative stroked bounds of one open polyline.
|
||||
/// </summary>
|
||||
/// <param name="points">The polyline points.</param>
|
||||
/// <param name="pen">The stroke metadata.</param>
|
||||
/// <returns>The conservative stroked bounds.</returns>
|
||||
public static RectangleF GetConservativeBounds(PointF[] points, Pen pen)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(points);
|
||||
if (points.Length == 0)
|
||||
{
|
||||
return RectangleF.Empty;
|
||||
}
|
||||
|
||||
float minX = points[0].X;
|
||||
float minY = points[0].Y;
|
||||
float maxX = minX;
|
||||
float maxY = minY;
|
||||
|
||||
for (int i = 1; i < points.Length; i++)
|
||||
{
|
||||
PointF point = points[i];
|
||||
minX = MathF.Min(minX, point.X);
|
||||
minY = MathF.Min(minY, point.Y);
|
||||
maxX = MathF.Max(maxX, point.X);
|
||||
maxY = MathF.Max(maxY, point.Y);
|
||||
}
|
||||
|
||||
RectangleF bounds = RectangleF.FromLTRB(minX, minY, maxX, maxY);
|
||||
return InflateBounds(bounds, pen);
|
||||
}
|
||||
|
||||
private static RectangleF InflateBounds(RectangleF bounds, Pen pen)
|
||||
{
|
||||
float halfWidth = pen.StrokeWidth * 0.5F;
|
||||
float inflate = pen.StrokeOptions.LineJoin switch
|
||||
{
|
||||
LineJoin.Miter or LineJoin.MiterRevert or LineJoin.MiterRound => (float)(halfWidth * Math.Max(pen.StrokeOptions.MiterLimit, 1D)),
|
||||
_ => halfWidth
|
||||
};
|
||||
|
||||
bounds.Inflate(new SizeF(inflate, inflate));
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Represents a logical configuration of a brush which can be used to source pixel colors.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A brush creates a <see cref="BrushRenderer{TPixel}"/> that performs the logic for retrieving
|
||||
/// pixel values for specific locations.
|
||||
/// </remarks>
|
||||
public abstract class Brush : IEquatable<Brush>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the prepared execution object for this brush.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel type.</typeparam>
|
||||
/// <param name="configuration">The configuration instance to use when performing operations.</param>
|
||||
/// <param name="options">The graphic options.</param>
|
||||
/// <param name="canvasWidth">The canvas width for the current render pass.</param>
|
||||
/// <param name="region">The region the brush will be applied to.</param>
|
||||
/// <returns>
|
||||
/// The <see cref="BrushRenderer{TPixel}"/> for this brush.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// The <paramref name="region" /> when being applied to things like shapes would usually be the
|
||||
/// bounding box of the shape not necessarily the bounds of the whole image.
|
||||
/// </remarks>
|
||||
public abstract BrushRenderer<TPixel> CreateRenderer<TPixel>(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
RectangleF region)
|
||||
where TPixel : unmanaged, IPixel<TPixel>;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new brush with its defining geometry transformed by the given matrix.
|
||||
/// </summary>
|
||||
/// <param name="matrix">The transformation matrix to apply.</param>
|
||||
/// <returns>A transformed brush, or <c>this</c> if the brush has no spatial parameters.</returns>
|
||||
public virtual Brush Transform(Matrix4x4 matrix) => this;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract bool Equals(Brush? other);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) => this.Equals(obj as Brush);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract override int GetHashCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Renders a <see cref="Brush"/> against individual coverage scanlines.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
public abstract class BrushRenderer<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BrushRenderer{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration instance to use when performing operations.</param>
|
||||
/// <param name="options">The graphics options.</param>
|
||||
/// <param name="canvasWidth">The canvas width for the current render pass.</param>
|
||||
protected BrushRenderer(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth)
|
||||
{
|
||||
this.Configuration = configuration;
|
||||
this.Options = options;
|
||||
this.CanvasWidth = canvasWidth;
|
||||
this.Blender = PixelOperations<TPixel>.Instance.GetPixelBlender(options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration instance to use when performing operations.
|
||||
/// </summary>
|
||||
protected Configuration Configuration { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pixel blender.
|
||||
/// </summary>
|
||||
internal PixelBlender<TPixel> Blender { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the graphics options.
|
||||
/// </summary>
|
||||
protected GraphicsOptions Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the canvas width for the current render pass.
|
||||
/// </summary>
|
||||
protected int CanvasWidth { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Applies the opacity weighting for each pixel in a scanline to the target based on the
|
||||
/// pattern contained in the brush.
|
||||
/// </summary>
|
||||
/// <param name="destinationRow">The destination row slice to shade.</param>
|
||||
/// <param name="scanline">The coverage values for the current destination scanline.</param>
|
||||
/// <param name="x">The x-position in the target pixel space that the start of the scanline data corresponds to.</param>
|
||||
/// <param name="y">The y-position in the target pixel space that the scanline corresponds to.</param>
|
||||
/// <param name="workspace">The worker-local scratch workspace for temporary blending buffers.</param>
|
||||
public abstract void Apply(
|
||||
Span<TPixel> destinationRow,
|
||||
ReadOnlySpan<float> scanline,
|
||||
int x,
|
||||
int y,
|
||||
BrushWorkspace<TPixel> workspace);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Numerics;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Worker-local scratch workspace used by prepared brushes during row composition.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The target pixel format.</typeparam>
|
||||
public sealed class BrushWorkspace<TPixel> : IDisposable
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly IMemoryOwner<float> amountsOwner;
|
||||
private readonly IMemoryOwner<TPixel> overlaysOwner;
|
||||
private readonly IMemoryOwner<Vector4> blendScratchOwner;
|
||||
|
||||
internal BrushWorkspace(MemoryAllocator allocator, int rowWidth)
|
||||
{
|
||||
int capacity = Math.Max(1, rowWidth);
|
||||
this.amountsOwner = allocator.Allocate<float>(capacity);
|
||||
this.overlaysOwner = allocator.Allocate<TPixel>(capacity);
|
||||
this.blendScratchOwner = allocator.Allocate<Vector4>(capacity * 3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shared amount buffer for the requested length.
|
||||
/// </summary>
|
||||
/// <param name="length">The number of elements required.</param>
|
||||
/// <returns>A slice of the worker-local pooled amount buffer.</returns>
|
||||
public Span<float> GetAmounts(int length)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(length);
|
||||
return this.amountsOwner.Memory.Span[..length];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shared overlay buffer for the requested length.
|
||||
/// </summary>
|
||||
/// <param name="length">The number of elements required.</param>
|
||||
/// <returns>A slice of the worker-local pooled overlay buffer.</returns>
|
||||
public Span<TPixel> GetOverlays(int length)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(length);
|
||||
return this.overlaysOwner.Memory.Span[..length];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shared vector scratch for the requested row length and vector row count.
|
||||
/// </summary>
|
||||
/// <param name="length">The number of pixels in the row.</param>
|
||||
/// <param name="vectorRows">The number of temporary vector rows required.</param>
|
||||
/// <returns>A slice of the worker-local pooled vector scratch buffer.</returns>
|
||||
public Span<Vector4> GetBlendScratch(int length, int vectorRows)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(length);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(vectorRows, 1);
|
||||
return this.blendScratchOwner.Memory.Span[..(length * vectorRows)];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
this.amountsOwner.Dispose();
|
||||
this.overlaysOwner.Dispose();
|
||||
this.blendScratchOwner.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,648 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <content>
|
||||
/// Provides additional hatch pattern brush factories.
|
||||
/// </content>
|
||||
public static partial class Brushes
|
||||
{
|
||||
// These hatch arrays were derived using the GDI+ pixel extraction technique described at
|
||||
// https://web.archive.org/web/20221228174326/https://www.codeproject.com/Articles/5350583/Recreating-Gdiplus-hatches-with-SkiaSharp.
|
||||
private static readonly bool[,] HorizontalPattern =
|
||||
{
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] VerticalPattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] ForwardDiagonalPattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, true, false, false, false, false, false, false, },
|
||||
{ false, false, true, false, false, false, false, false, },
|
||||
{ false, false, false, true, false, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, false, true, false, false, },
|
||||
{ false, false, false, false, false, false, true, false, },
|
||||
{ false, false, false, false, false, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] BackwardDiagonalPattern =
|
||||
{
|
||||
{ false, false, false, false, false, false, false, true, },
|
||||
{ false, false, false, false, false, false, true, false, },
|
||||
{ false, false, false, false, false, true, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, true, false, false, false, false, },
|
||||
{ false, false, true, false, false, false, false, false, },
|
||||
{ false, true, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] CrossPattern =
|
||||
{
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DiagonalCrossPattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, false, true, },
|
||||
{ false, true, false, false, false, false, true, false, },
|
||||
{ false, false, true, false, false, true, false, false, },
|
||||
{ false, false, false, true, true, false, false, false, },
|
||||
{ false, false, false, true, true, false, false, false, },
|
||||
{ false, false, true, false, false, true, false, false, },
|
||||
{ false, true, false, false, false, false, true, false, },
|
||||
{ true, false, false, false, false, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent05Pattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent10Pattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent20Pattern =
|
||||
{
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent25Pattern =
|
||||
{
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent30Pattern =
|
||||
{
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, false, false, true, false, false, },
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, false, false, true, false, false, false, true, },
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, false, false, true, false, false, },
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, false, false, true, false, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent40Pattern =
|
||||
{
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, true, false, false, false, true, },
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, false, false, true, false, true, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent50Pattern =
|
||||
{
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent60Pattern =
|
||||
{
|
||||
{ true, true, true, false, true, true, true, false, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ true, false, true, true, true, false, true, true, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ true, true, true, false, true, true, true, false, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ true, false, true, true, true, false, true, true, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent70Pattern =
|
||||
{
|
||||
{ false, true, true, true, false, true, true, true, },
|
||||
{ true, true, false, true, true, true, false, true, },
|
||||
{ false, true, true, true, false, true, true, true, },
|
||||
{ true, true, false, true, true, true, false, true, },
|
||||
{ false, true, true, true, false, true, true, true, },
|
||||
{ true, true, false, true, true, true, false, true, },
|
||||
{ false, true, true, true, false, true, true, true, },
|
||||
{ true, true, false, true, true, true, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent75Pattern =
|
||||
{
|
||||
{ false, true, true, true, false, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, false, true, true, true, false, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, true, true, true, false, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, false, true, true, true, false, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent80Pattern =
|
||||
{
|
||||
{ true, true, true, false, true, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, true, false, true, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] Percent90Pattern =
|
||||
{
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, true, true, false, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, true, true, true, true, true, true, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] LightDownwardDiagonalPattern =
|
||||
{
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, true, false, false, false, true, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, false, false, true, false, false, false, true, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, true, false, false, false, true, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, false, false, true, false, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] LightUpwardDiagonalPattern =
|
||||
{
|
||||
{ false, false, false, true, false, false, false, true, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, true, false, false, false, true, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, true, false, false, false, true, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, true, false, false, false, true, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DarkDownwardDiagonalPattern =
|
||||
{
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ false, true, true, false, false, true, true, false, },
|
||||
{ false, false, true, true, false, false, true, true, },
|
||||
{ true, false, false, true, true, false, false, true, },
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ false, true, true, false, false, true, true, false, },
|
||||
{ false, false, true, true, false, false, true, true, },
|
||||
{ true, false, false, true, true, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DarkUpwardDiagonalPattern =
|
||||
{
|
||||
{ false, false, true, true, false, false, true, true, },
|
||||
{ false, true, true, false, false, true, true, false, },
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ true, false, false, true, true, false, false, true, },
|
||||
{ false, false, true, true, false, false, true, true, },
|
||||
{ false, true, true, false, false, true, true, false, },
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ true, false, false, true, true, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] WideDownwardDiagonalPattern =
|
||||
{
|
||||
{ true, true, false, false, false, false, false, true, },
|
||||
{ true, true, true, false, false, false, false, false, },
|
||||
{ false, true, true, true, false, false, false, false, },
|
||||
{ false, false, true, true, true, false, false, false, },
|
||||
{ false, false, false, true, true, true, false, false, },
|
||||
{ false, false, false, false, true, true, true, false, },
|
||||
{ false, false, false, false, false, true, true, true, },
|
||||
{ true, false, false, false, false, false, true, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] WideUpwardDiagonalPattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, true, true, },
|
||||
{ false, false, false, false, false, true, true, true, },
|
||||
{ false, false, false, false, true, true, true, false, },
|
||||
{ false, false, false, true, true, true, false, false, },
|
||||
{ false, false, true, true, true, false, false, false, },
|
||||
{ false, true, true, true, false, false, false, false, },
|
||||
{ true, true, true, false, false, false, false, false, },
|
||||
{ true, true, false, false, false, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] LightVerticalPattern =
|
||||
{
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] LightHorizontalPattern =
|
||||
{
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] NarrowVerticalPattern =
|
||||
{
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] NarrowHorizontalPattern =
|
||||
{
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DarkVerticalPattern =
|
||||
{
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
{ true, true, false, false, true, true, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DarkHorizontalPattern =
|
||||
{
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DashedDownwardDiagonalPattern =
|
||||
{
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, true, false, false, false, true, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, false, false, true, false, false, false, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DashedUpwardDiagonalPattern =
|
||||
{
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, true, false, false, false, true, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, true, false, false, false, true, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DashedHorizontalPattern =
|
||||
{
|
||||
{ true, true, true, true, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, true, true, true, true, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DashedVerticalPattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] SmallConfettiPattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, true, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, true, false, },
|
||||
{ false, false, false, true, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, true, },
|
||||
{ false, false, true, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, true, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] LargeConfettiPattern =
|
||||
{
|
||||
{ true, false, true, true, false, false, false, true, },
|
||||
{ false, false, true, true, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, true, true, },
|
||||
{ false, false, false, true, true, false, true, true, },
|
||||
{ true, true, false, true, true, false, false, false, },
|
||||
{ true, true, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, true, true, false, false, },
|
||||
{ true, false, false, false, true, true, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] ZigZagPattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, false, true, },
|
||||
{ false, true, false, false, false, false, true, false, },
|
||||
{ false, false, true, false, false, true, false, false, },
|
||||
{ false, false, false, true, true, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, true, },
|
||||
{ false, true, false, false, false, false, true, false, },
|
||||
{ false, false, true, false, false, true, false, false, },
|
||||
{ false, false, false, true, true, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] WavePattern =
|
||||
{
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, true, true, false, false, false, },
|
||||
{ false, false, true, false, false, true, false, true, },
|
||||
{ true, true, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, true, true, false, false, false, },
|
||||
{ false, false, true, false, false, true, false, true, },
|
||||
{ true, true, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DiagonalBrickPattern =
|
||||
{
|
||||
{ false, false, false, false, false, false, false, true, },
|
||||
{ false, false, false, false, false, false, true, false, },
|
||||
{ false, false, false, false, false, true, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, true, true, false, false, false, },
|
||||
{ false, false, true, false, false, true, false, false, },
|
||||
{ false, true, false, false, false, false, true, false, },
|
||||
{ true, false, false, false, false, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] HorizontalBrickPattern =
|
||||
{
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] WeavePattern =
|
||||
{
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, true, false, true, false, true, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, true, false, false, false, true, false, true, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, true, false, true, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, true, false, true, false, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] PlaidPattern =
|
||||
{
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, true, false, true, false, true, false, true, },
|
||||
{ true, true, true, true, false, false, false, false, },
|
||||
{ true, true, true, true, false, false, false, false, },
|
||||
{ true, true, true, true, false, false, false, false, },
|
||||
{ true, true, true, true, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DivotPattern =
|
||||
{
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, true, false, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, true, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, true, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DottedGridPattern =
|
||||
{
|
||||
{ true, false, true, false, true, false, true, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] DottedDiamondPattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, false, false, true, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
{ false, false, true, false, false, false, true, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] ShinglePattern =
|
||||
{
|
||||
{ false, false, false, false, false, false, true, true, },
|
||||
{ true, false, false, false, false, true, false, false, },
|
||||
{ false, true, false, false, true, false, false, false, },
|
||||
{ false, false, true, true, false, false, false, false, },
|
||||
{ false, false, false, false, true, true, false, false, },
|
||||
{ false, false, false, false, false, false, true, false, },
|
||||
{ false, false, false, false, false, false, false, true, },
|
||||
{ false, false, false, false, false, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] TrellisPattern =
|
||||
{
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, true, true, false, false, true, true, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, false, false, true, true, false, false, true, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ false, true, true, false, false, true, true, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, false, false, true, true, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] SpherePattern =
|
||||
{
|
||||
{ false, true, true, true, false, true, true, true, },
|
||||
{ true, false, false, false, true, false, false, true, },
|
||||
{ true, false, false, false, true, true, true, true, },
|
||||
{ true, false, false, false, true, true, true, true, },
|
||||
{ false, true, true, true, false, true, true, true, },
|
||||
{ true, false, false, true, true, false, false, false, },
|
||||
{ true, true, true, true, true, false, false, false, },
|
||||
{ true, true, true, true, true, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] SmallGridPattern =
|
||||
{
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, true, true, true, true, true, true, true, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
{ true, false, false, false, true, false, false, false, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] SmallCheckerBoardPattern =
|
||||
{
|
||||
{ true, false, false, true, true, false, false, true, },
|
||||
{ false, true, true, false, false, true, true, false, },
|
||||
{ false, true, true, false, false, true, true, false, },
|
||||
{ true, false, false, true, true, false, false, true, },
|
||||
{ true, false, false, true, true, false, false, true, },
|
||||
{ false, true, true, false, false, true, true, false, },
|
||||
{ false, true, true, false, false, true, true, false, },
|
||||
{ true, false, false, true, true, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] LargeCheckerBoardPattern =
|
||||
{
|
||||
{ true, true, true, true, false, false, false, false, },
|
||||
{ true, true, true, true, false, false, false, false, },
|
||||
{ true, true, true, true, false, false, false, false, },
|
||||
{ true, true, true, true, false, false, false, false, },
|
||||
{ false, false, false, false, true, true, true, true, },
|
||||
{ false, false, false, false, true, true, true, true, },
|
||||
{ false, false, false, false, true, true, true, true, },
|
||||
{ false, false, false, false, true, true, true, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] OutlinedDiamondPattern =
|
||||
{
|
||||
{ true, false, false, false, false, false, true, false, },
|
||||
{ false, true, false, false, false, true, false, false, },
|
||||
{ false, false, true, false, true, false, false, false, },
|
||||
{ false, false, false, true, false, false, false, false, },
|
||||
{ false, false, true, false, true, false, false, false, },
|
||||
{ false, true, false, false, false, true, false, false, },
|
||||
{ true, false, false, false, false, false, true, false, },
|
||||
{ false, false, false, false, false, false, false, true, },
|
||||
};
|
||||
|
||||
private static readonly bool[,] SolidDiamondPattern =
|
||||
{
|
||||
{ false, false, false, true, false, false, false, false, },
|
||||
{ false, false, true, true, true, false, false, false, },
|
||||
{ false, true, true, true, true, true, false, false, },
|
||||
{ true, true, true, true, true, true, true, false, },
|
||||
{ false, true, true, true, true, true, false, false, },
|
||||
{ false, false, true, true, true, false, false, false, },
|
||||
{ false, false, false, true, false, false, false, false, },
|
||||
{ false, false, false, false, false, false, false, false, },
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,841 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// A collection of methods for creating generic brushes.
|
||||
/// </summary>
|
||||
public static partial class Brushes
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a solid color.
|
||||
/// </summary>
|
||||
/// <param name="color">The brush color.</param>
|
||||
/// <returns>A new <see cref="SolidBrush"/>.</returns>
|
||||
public static SolidBrush Solid(Color color) => new(color);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints horizontal line hatching using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Horizontal(Color foreColor)
|
||||
=> new(foreColor, Color.Transparent, HorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints horizontal line hatching using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Horizontal(Color foreColor, Color backColor)
|
||||
=> new(foreColor, backColor, HorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints horizontal line hatching for the minimum hatch style using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Min(Color foreColor)
|
||||
=> new(foreColor, Color.Transparent, HorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints horizontal line hatching for the minimum hatch style using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Min(Color foreColor, Color backColor)
|
||||
=> new(foreColor, backColor, HorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints vertical line hatching using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Vertical(Color foreColor)
|
||||
=> new(foreColor, Color.Transparent, VerticalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints vertical line hatching using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Vertical(Color foreColor, Color backColor)
|
||||
=> new(foreColor, backColor, VerticalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints diagonal line hatching from upper left to lower right using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush ForwardDiagonal(Color foreColor)
|
||||
=> new(foreColor, Color.Transparent, ForwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints diagonal line hatching from upper left to lower right using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush ForwardDiagonal(Color foreColor, Color backColor)
|
||||
=> new(foreColor, backColor, ForwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints diagonal line hatching from upper right to lower left using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush BackwardDiagonal(Color foreColor)
|
||||
=> new(foreColor, Color.Transparent, BackwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints diagonal line hatching from upper right to lower left using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush BackwardDiagonal(Color foreColor, Color backColor)
|
||||
=> new(foreColor, backColor, BackwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints intersecting horizontal and vertical line hatching using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Cross(Color foreColor) => new(foreColor, Color.Transparent, CrossPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints intersecting horizontal and vertical line hatching using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Cross(Color foreColor, Color backColor) => new(foreColor, backColor, CrossPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints intersecting forward and backward diagonal line hatching using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DiagonalCross(Color foreColor) => new(foreColor, Color.Transparent, DiagonalCrossPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints intersecting forward and backward diagonal line hatching using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DiagonalCross(Color foreColor, Color backColor) => new(foreColor, backColor, DiagonalCrossPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 5-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 5:95.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent05(Color foreColor) => new(foreColor, Color.Transparent, Percent05Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 5-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 5:95.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent05(Color foreColor, Color backColor) => new(foreColor, backColor, Percent05Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 10-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 10:90.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent10(Color foreColor)
|
||||
=> new(foreColor, Color.Transparent, Percent10Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 10-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 10:90.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent10(Color foreColor, Color backColor)
|
||||
=> new(foreColor, backColor, Percent10Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 20-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 20:80.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent20(Color foreColor)
|
||||
=> new(foreColor, Color.Transparent, Percent20Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 20-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 20:80.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent20(Color foreColor, Color backColor)
|
||||
=> new(foreColor, backColor, Percent20Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 25-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 25:75.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent25(Color foreColor) => new(foreColor, Color.Transparent, Percent25Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 25-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 25:75.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent25(Color foreColor, Color backColor) => new(foreColor, backColor, Percent25Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 30-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 30:70.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent30(Color foreColor) => new(foreColor, Color.Transparent, Percent30Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 30-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 30:70.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent30(Color foreColor, Color backColor) => new(foreColor, backColor, Percent30Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 40-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 40:60.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent40(Color foreColor) => new(foreColor, Color.Transparent, Percent40Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 40-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 40:60.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent40(Color foreColor, Color backColor) => new(foreColor, backColor, Percent40Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 50-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 50:50.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent50(Color foreColor) => new(foreColor, Color.Transparent, Percent50Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 50-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 50:50.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent50(Color foreColor, Color backColor) => new(foreColor, backColor, Percent50Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 60-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 60:40.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent60(Color foreColor) => new(foreColor, Color.Transparent, Percent60Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 60-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 60:40.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent60(Color foreColor, Color backColor) => new(foreColor, backColor, Percent60Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 70-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 70:30.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent70(Color foreColor) => new(foreColor, Color.Transparent, Percent70Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 70-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 70:30.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent70(Color foreColor, Color backColor) => new(foreColor, backColor, Percent70Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 75-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 75:25.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent75(Color foreColor) => new(foreColor, Color.Transparent, Percent75Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 75-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 75:25.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent75(Color foreColor, Color backColor) => new(foreColor, backColor, Percent75Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints an 80-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 80:20.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent80(Color foreColor) => new(foreColor, Color.Transparent, Percent80Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints an 80-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 80:20.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent80(Color foreColor, Color backColor) => new(foreColor, backColor, Percent80Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 90-percent hatch using the foreground color on a transparent background; the foreground-to-background color ratio is 90:10.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent90(Color foreColor) => new(foreColor, Color.Transparent, Percent90Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a 90-percent hatch using the specified foreground and background colors; the foreground-to-background color ratio is 90:10.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Percent90(Color foreColor, Color backColor) => new(foreColor, backColor, Percent90Pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints downward diagonal lines spaced more closely than ForwardDiagonal using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LightDownwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, LightDownwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints downward diagonal lines spaced more closely than ForwardDiagonal using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LightDownwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, LightDownwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints upward diagonal lines spaced more closely than BackwardDiagonal using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LightUpwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, LightUpwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints upward diagonal lines spaced more closely than BackwardDiagonal using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LightUpwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, LightUpwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints thicker downward diagonal lines spaced more closely than ForwardDiagonal using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DarkDownwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, DarkDownwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints thicker downward diagonal lines spaced more closely than ForwardDiagonal using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DarkDownwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, DarkDownwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints thicker upward diagonal lines spaced more closely than BackwardDiagonal using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DarkUpwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, DarkUpwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints thicker upward diagonal lines spaced more closely than BackwardDiagonal using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DarkUpwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, DarkUpwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints wide downward diagonal lines with ForwardDiagonal spacing using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush WideDownwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, WideDownwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints wide downward diagonal lines with ForwardDiagonal spacing using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush WideDownwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, WideDownwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints wide upward diagonal lines with BackwardDiagonal spacing using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush WideUpwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, WideUpwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints wide upward diagonal lines with BackwardDiagonal spacing using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush WideUpwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, WideUpwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints vertical lines spaced more closely than Vertical using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LightVertical(Color foreColor) => new(foreColor, Color.Transparent, LightVerticalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints vertical lines spaced more closely than Vertical using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LightVertical(Color foreColor, Color backColor) => new(foreColor, backColor, LightVerticalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints horizontal lines spaced more closely than Horizontal using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LightHorizontal(Color foreColor) => new(foreColor, Color.Transparent, LightHorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints horizontal lines spaced more closely than Horizontal using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LightHorizontal(Color foreColor, Color backColor) => new(foreColor, backColor, LightHorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints narrow vertical lines spaced more closely than LightVertical using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush NarrowVertical(Color foreColor) => new(foreColor, Color.Transparent, NarrowVerticalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints narrow vertical lines spaced more closely than LightVertical using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush NarrowVertical(Color foreColor, Color backColor) => new(foreColor, backColor, NarrowVerticalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints narrow horizontal lines spaced more closely than LightHorizontal using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush NarrowHorizontal(Color foreColor) => new(foreColor, Color.Transparent, NarrowHorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints narrow horizontal lines spaced more closely than LightHorizontal using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush NarrowHorizontal(Color foreColor, Color backColor) => new(foreColor, backColor, NarrowHorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints thicker vertical lines spaced more closely than Vertical using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DarkVertical(Color foreColor) => new(foreColor, Color.Transparent, DarkVerticalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints thicker vertical lines spaced more closely than Vertical using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DarkVertical(Color foreColor, Color backColor) => new(foreColor, backColor, DarkVerticalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints thicker horizontal lines spaced more closely than Horizontal using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DarkHorizontal(Color foreColor) => new(foreColor, Color.Transparent, DarkHorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints thicker horizontal lines spaced more closely than Horizontal using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DarkHorizontal(Color foreColor, Color backColor) => new(foreColor, backColor, DarkHorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints dashed diagonal lines from upper left to lower right using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DashedDownwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, DashedDownwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints dashed diagonal lines from upper left to lower right using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DashedDownwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, DashedDownwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints dashed diagonal lines from upper right to lower left using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DashedUpwardDiagonal(Color foreColor) => new(foreColor, Color.Transparent, DashedUpwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints dashed diagonal lines from upper right to lower left using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DashedUpwardDiagonal(Color foreColor, Color backColor) => new(foreColor, backColor, DashedUpwardDiagonalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints dashed horizontal lines using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DashedHorizontal(Color foreColor) => new(foreColor, Color.Transparent, DashedHorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints dashed horizontal lines using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DashedHorizontal(Color foreColor, Color backColor) => new(foreColor, backColor, DashedHorizontalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints dashed vertical lines using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DashedVertical(Color foreColor) => new(foreColor, Color.Transparent, DashedVerticalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints dashed vertical lines using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DashedVertical(Color foreColor, Color backColor) => new(foreColor, backColor, DashedVerticalPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a small confetti-style hatch using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush SmallConfetti(Color foreColor) => new(foreColor, Color.Transparent, SmallConfettiPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a small confetti-style hatch using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush SmallConfetti(Color foreColor, Color backColor) => new(foreColor, backColor, SmallConfettiPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a confetti-style hatch with larger pieces than SmallConfetti using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LargeConfetti(Color foreColor) => new(foreColor, Color.Transparent, LargeConfettiPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a confetti-style hatch with larger pieces than SmallConfetti using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LargeConfetti(Color foreColor, Color backColor) => new(foreColor, backColor, LargeConfettiPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints horizontal lines formed from zigzags using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush ZigZag(Color foreColor) => new(foreColor, Color.Transparent, ZigZagPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints horizontal lines formed from zigzags using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush ZigZag(Color foreColor, Color backColor) => new(foreColor, backColor, ZigZagPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints horizontal lines formed from wave shapes using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Wave(Color foreColor) => new(foreColor, Color.Transparent, WavePattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints horizontal lines formed from wave shapes using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Wave(Color foreColor, Color backColor) => new(foreColor, backColor, WavePattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints staggered brick shapes running diagonally upward using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DiagonalBrick(Color foreColor) => new(foreColor, Color.Transparent, DiagonalBrickPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints staggered brick shapes running diagonally upward using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DiagonalBrick(Color foreColor, Color backColor) => new(foreColor, backColor, DiagonalBrickPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints staggered brick shapes arranged horizontally using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush HorizontalBrick(Color foreColor) => new(foreColor, Color.Transparent, HorizontalBrickPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints staggered brick shapes arranged horizontally using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush HorizontalBrick(Color foreColor, Color backColor) => new(foreColor, backColor, HorizontalBrickPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a woven-material hatch using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Weave(Color foreColor) => new(foreColor, Color.Transparent, WeavePattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a woven-material hatch using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Weave(Color foreColor, Color backColor) => new(foreColor, backColor, WeavePattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a plaid-material hatch using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Plaid(Color foreColor) => new(foreColor, Color.Transparent, PlaidPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a plaid-material hatch using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Plaid(Color foreColor, Color backColor) => new(foreColor, backColor, PlaidPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a divot-style hatch using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Divot(Color foreColor) => new(foreColor, Color.Transparent, DivotPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a divot-style hatch using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Divot(Color foreColor, Color backColor) => new(foreColor, backColor, DivotPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints intersecting horizontal and vertical dotted lines using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DottedGrid(Color foreColor) => new(foreColor, Color.Transparent, DottedGridPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints intersecting horizontal and vertical dotted lines using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DottedGrid(Color foreColor, Color backColor) => new(foreColor, backColor, DottedGridPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints intersecting forward and backward diagonal dotted lines using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DottedDiamond(Color foreColor) => new(foreColor, Color.Transparent, DottedDiamondPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints intersecting forward and backward diagonal dotted lines using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush DottedDiamond(Color foreColor, Color backColor) => new(foreColor, backColor, DottedDiamondPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints layered shingle shapes running diagonally downward using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Shingle(Color foreColor) => new(foreColor, Color.Transparent, ShinglePattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints layered shingle shapes running diagonally downward using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Shingle(Color foreColor, Color backColor) => new(foreColor, backColor, ShinglePattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a trellis-style hatch using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Trellis(Color foreColor) => new(foreColor, Color.Transparent, TrellisPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a trellis-style hatch using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Trellis(Color foreColor, Color backColor) => new(foreColor, backColor, TrellisPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints adjacent sphere-like shapes using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Sphere(Color foreColor) => new(foreColor, Color.Transparent, SpherePattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints adjacent sphere-like shapes using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush Sphere(Color foreColor, Color backColor) => new(foreColor, backColor, SpherePattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints intersecting horizontal and vertical lines spaced more closely than Cross using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush SmallGrid(Color foreColor) => new(foreColor, Color.Transparent, SmallGridPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints intersecting horizontal and vertical lines spaced more closely than Cross using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush SmallGrid(Color foreColor, Color backColor) => new(foreColor, backColor, SmallGridPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a small checkerboard hatch using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush SmallCheckerBoard(Color foreColor) => new(foreColor, Color.Transparent, SmallCheckerBoardPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a small checkerboard hatch using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush SmallCheckerBoard(Color foreColor, Color backColor) => new(foreColor, backColor, SmallCheckerBoardPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a checkerboard hatch with larger squares than SmallCheckerBoard using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LargeCheckerBoard(Color foreColor) => new(foreColor, Color.Transparent, LargeCheckerBoardPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a checkerboard hatch with larger squares than SmallCheckerBoard using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush LargeCheckerBoard(Color foreColor, Color backColor) => new(foreColor, backColor, LargeCheckerBoardPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints outlined diamond shapes formed by crossing diagonal lines using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush OutlinedDiamond(Color foreColor) => new(foreColor, Color.Transparent, OutlinedDiamondPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints outlined diamond shapes formed by crossing diagonal lines using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush OutlinedDiamond(Color foreColor, Color backColor) => new(foreColor, backColor, OutlinedDiamondPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a filled diamond checkerboard hatch using the foreground color on a transparent background.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush SolidDiamond(Color foreColor) => new(foreColor, Color.Transparent, SolidDiamondPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a brush that paints a filled diamond checkerboard hatch using the specified foreground and background colors.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">The foreground color.</param>
|
||||
/// <param name="backColor">The background color.</param>
|
||||
/// <returns>A new <see cref="PatternBrush"/>.</returns>
|
||||
public static PatternBrush SolidDiamond(Color foreColor, Color backColor) => new(foreColor, backColor, SolidDiamondPattern);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// A struct that defines a single color stop.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("ColorStop({Ratio} -> {Color}")]
|
||||
public readonly struct ColorStop
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ColorStop" /> struct.
|
||||
/// </summary>
|
||||
/// <param name="ratio">Where should it be? 0 is at the start, 1 at the end of the Gradient.</param>
|
||||
/// <param name="color">What color should be used at that point?</param>
|
||||
public ColorStop(float ratio, in Color color)
|
||||
{
|
||||
this.Ratio = ratio;
|
||||
this.Color = color;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the point along the defined gradient axis.
|
||||
/// </summary>
|
||||
public float Ratio { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the color to be used.
|
||||
/// </summary>
|
||||
public Color Color { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
# DrawingCanvas
|
||||
|
||||
`DrawingCanvas` is the high-level drawing surface used by ImageSharp.Drawing. It lets the library expose one drawing model while supporting very different execution targets:
|
||||
|
||||
- CPU rasterization into memory
|
||||
- GPU execution through native surfaces
|
||||
- backends that prefer their own internal representation, such as vector export
|
||||
|
||||
That unification is the hard part. The public API wants to feel immediate and simple: fill a path, draw text, save state, restore state, draw into a region, maybe draw into a layer. The backends, however, do not all want the same kind of work. A CPU rasterizer wants rows, spans, and direct pixel access. A GPU backend wants compact command data, stable batching, and a single handoff point. A vector exporter would want semantic geometry rather than already-rasterized pixels.
|
||||
|
||||
The architecture around `DrawingCanvas` and its typed implementation exists to absorb that mismatch.
|
||||
|
||||
This document explains that architecture from the outside in. The goal is to help a newcomer understand what problem each piece solves before diving into methods and types.
|
||||
|
||||
## The Main Problem
|
||||
|
||||
If the canvas executed every public call immediately, each backend would have to implement the entire public drawing model directly:
|
||||
|
||||
- save and restore state
|
||||
- clip stacking
|
||||
- layers and isolated composition
|
||||
- text drawing
|
||||
- image drawing with transforms
|
||||
- region drawing
|
||||
- brush and pen handling
|
||||
- transform handling
|
||||
|
||||
That sounds straightforward until the differences between backends become obvious.
|
||||
|
||||
The CPU backend can cheaply mutate a memory buffer row by row. The GPU backend wants a larger batch of work so it can amortize setup, upload, and dispatch costs. A vector-style backend would ideally preserve geometry and draw intent for as long as possible. If each backend solved all of that from scratch, they would drift apart quickly and correctness bugs would multiply.
|
||||
|
||||
So the architecture chooses a different approach:
|
||||
|
||||
`DrawingCanvas` records drawing intent first, normalizes that intent when replaying or creating a retained scene, and only then hands the work to the backend.
|
||||
|
||||
That one decision explains most of the surrounding design.
|
||||
|
||||
## The Core Idea
|
||||
|
||||
The canvas is a deferred renderer.
|
||||
|
||||
Drawing calls do not rasterize immediately. They create `CompositionCommand` records and queue them into `DrawingCanvasBatcher<TPixel>`. The expensive normalization work happens later, during replay, when the batcher prepares those commands and hands `DrawingCommandBatch` ranges to the backend.
|
||||
|
||||
That gives the architecture three important benefits.
|
||||
|
||||
First, the public API stays backend-agnostic. A fill is a fill, whether the target is CPU memory or a GPU surface.
|
||||
|
||||
Second, the expensive shared command work can happen once, in one shared place. Transform application, stroke expansion, clip application, and dash expansion are not reimplemented independently by every backend.
|
||||
|
||||
Third, the backend receives a much more stable handoff. Instead of reacting to a long stream of public API calls, it receives prepared command batches with consistent semantics.
|
||||
|
||||
## The Most Important Terms
|
||||
|
||||
Before looking at the flow, it helps to define the major terms in the sense used by this codebase.
|
||||
|
||||
### Canvas
|
||||
|
||||
`DrawingCanvas` is the public drawing facade. It owns the current drawing state, accepts commands, and decides when to flush.
|
||||
|
||||
It is not the rasterizer. It is the object that makes the public drawing model coherent.
|
||||
|
||||
Callers usually reach that model through `IImageProcessingContext.Paint(...)`.
|
||||
|
||||
When callers already have an `ImageFrame` or `ImageFrame<TPixel>`, the public `CreateCanvas(...)` frame extensions create a canvas directly over that frame. The caller owns the returned canvas and must dispose it to replay recorded work into the frame.
|
||||
|
||||
`DrawingCanvas<TPixel>` is the typed implementation that carries the target pixel format for brush normalization,
|
||||
readback, and backend execution. Factory methods return `DrawingCanvas` so CPU and GPU entry points expose the same
|
||||
canvas-facing API while still constructing the typed implementation internally.
|
||||
|
||||
### Batcher
|
||||
|
||||
`DrawingCanvasBatcher<TPixel>` is the deferred command queue. It stores pending `CompositionCommand` values, records the canvas replay timeline, prepares commands during replay, and creates `DrawingCommandBatch` values for command-range entries.
|
||||
|
||||
It is the bridge between the immediate-looking public API and the deferred backend handoff.
|
||||
|
||||
### Command
|
||||
|
||||
`CompositionCommand` is the recorded unit of drawing intent. In the common case it means "fill this path with this brush under this state". The command stream also carries explicit layer boundaries through `BeginLayer` and `EndLayer`.
|
||||
|
||||
The command remains relatively close to the original user request. It may hold the original path, pen, brush, transform, and clip paths.
|
||||
|
||||
### Preparation
|
||||
|
||||
Preparation is the normalization step that turns recorded intent into backend-ready commands.
|
||||
|
||||
`DrawingCanvasBatcher<TPixel>.PrepareCommands(...)` runs only when needed. It applies command transforms, expands strokes to fill paths, applies clip paths so clipped commands reach the backend as ordinary fills, and expands dashed strokes when a stroke pattern is present.
|
||||
|
||||
Preparation stops at `DrawingCommandBatch`. Backend-specific lowering happens after that, inside `IDrawingBackend.CreateScene(...)`.
|
||||
|
||||
### Command Batch
|
||||
|
||||
`DrawingCommandBatch` is the prepared command range handed to the backend. It contains the command stream for one contiguous range and scene-level facts such as whether that range contains layer boundaries.
|
||||
|
||||
It is the backend handoff boundary.
|
||||
|
||||
### Backend
|
||||
|
||||
`IDrawingBackend` is the execution engine behind the canvas. The important implementations are:
|
||||
|
||||
- `DefaultDrawingBackend` for CPU rendering
|
||||
- `WebGPUDrawingBackend` for GPU rendering through native surfaces
|
||||
|
||||
The backend creates retained scenes from command batches and renders retained scenes into typed target frames.
|
||||
|
||||
There are two backend-selection paths in the architecture:
|
||||
|
||||
- direct `DrawingCanvas<TPixel>` construction resolves the backend from `Configuration`
|
||||
- specialized infrastructure can construct a canvas with an explicit backend
|
||||
|
||||
The ordinary CPU entry point is `Paint(...)` on `IImageProcessingContext`, which routes into the typed
|
||||
implementation internally. Public `ImageFrame` canvas extensions provide the lower-level frame entry point for callers that want to own the canvas lifetime directly.
|
||||
|
||||
That explicit-backend path matters for the WebGPU helpers. `WebGPUWindow`, `WebGPUExternalSurface`, and `WebGPURenderTarget` create canvases that point directly at their owned `WebGPUDrawingBackend` instance instead of storing that backend on the caller's `Configuration`.
|
||||
|
||||
### Frame
|
||||
|
||||
`ICanvasFrame<TPixel>` is the target abstraction that the backend renders into.
|
||||
|
||||
This is one of the terms that can be ambiguous without context, so it is worth being explicit. In this architecture, a canvas frame is not "a UI frame" or "one animation frame". It means "the destination surface for one canvas instance".
|
||||
|
||||
The important properties of a frame are:
|
||||
|
||||
- `Bounds`
|
||||
- whether it exposes a CPU region through `TryGetCpuRegion(...)`
|
||||
- whether it exposes a native surface through `TryGetNativeSurface(...)`
|
||||
|
||||
That abstraction lets the same canvas target:
|
||||
|
||||
- pure CPU memory with `MemoryCanvasFrame<TPixel>`
|
||||
- a native or GPU surface with `NativeCanvasFrame<TPixel>`
|
||||
- a combined CPU plus native target
|
||||
- a clipped view over another frame with `CanvasRegionFrame<TPixel>`
|
||||
|
||||
The point is not to hide all differences. The point is to express the minimum target contract the backends need.
|
||||
|
||||
### Layer
|
||||
|
||||
A layer is isolated group rendering. In public API terms, it is created with `SaveLayer(...)` and later closed by `Restore()` or `RestoreTo(...)`.
|
||||
|
||||
In this architecture, a layer is recorded inline in the command stream as:
|
||||
|
||||
- `BeginLayer`
|
||||
- commands inside the layer
|
||||
- `EndLayer`
|
||||
|
||||
The backend is responsible for lowering those layer boundaries into the execution model it needs.
|
||||
|
||||
Layer semantics stay in the shared command model so every backend receives the same layer structure at the handoff boundary.
|
||||
|
||||
## The Big Picture Flow
|
||||
|
||||
The easiest way to understand the system is to follow one normal draw call all the way through.
|
||||
|
||||
### Step 1: The canvas records intent
|
||||
|
||||
A public method such as `Fill(...)`, `Draw(...)`, or `DrawText(...)` resolves the active state and creates one or more `CompositionCommand` values.
|
||||
|
||||
At this point the canvas is mostly recording:
|
||||
|
||||
- geometry references
|
||||
- brushes or pens
|
||||
- active transform
|
||||
- clip paths
|
||||
- graphics options
|
||||
- target bounds relevant to this command
|
||||
|
||||
The canvas does not try to fully rasterize anything here.
|
||||
|
||||
### Step 2: The batcher owns the pending work
|
||||
|
||||
Commands go into `DrawingCanvasBatcher<TPixel>`.
|
||||
|
||||
The batcher exists so the canvas does not need to talk to the backend for every single API call. It accumulates work until a timeline boundary is reached.
|
||||
|
||||
The replay boundary usually comes from:
|
||||
|
||||
- explicit `Flush()`, which seals the current command range
|
||||
- `Apply(...)`, which needs read-modify-write behavior
|
||||
- `RenderScene(...)`, which inserts an existing retained scene into the timeline
|
||||
- disposal of the owning canvas
|
||||
|
||||
### Step 3: The batcher prepares commands
|
||||
|
||||
When the root canvas is disposed, or when the caller creates a retained scene, the batcher seals any pending commands and prepares the command buffer. This is where the architecture does the heavy shared work that would otherwise be duplicated across backends.
|
||||
|
||||
For a typical path-based command, canvas preparation does the following in concept:
|
||||
|
||||
1. transform the source path into its final geometry space
|
||||
2. if a pen is present, expand the stroke to fill geometry
|
||||
3. apply clip paths
|
||||
4. transform the brush into the same command space
|
||||
5. leave backend-specific retained geometry construction to `CreateScene(...)`
|
||||
|
||||
This is the architectural center of gravity. It is the shared normalization stage that makes the backends simpler.
|
||||
|
||||
### Step 4: The backend creates and renders scenes
|
||||
|
||||
After preparation, disposal replay walks the canvas timeline in order. Command-range entries become short-lived retained scenes through `backend.CreateScene(...)`, and those scenes are then rendered through `backend.RenderScene(...)`.
|
||||
|
||||
From that point the CPU and GPU paths diverge.
|
||||
|
||||
The CPU backend lowers each command batch into a row-oriented retained representation through `FlushScene` during `CreateScene(...)` and then composites into memory during `RenderScene(...)`.
|
||||
|
||||
The WebGPU backend encodes each command batch into its retained GPU representation during `CreateScene(...)`, then uploads render-scoped resources and dispatches GPU work during `RenderScene(...)`.
|
||||
|
||||
The architecture is successful if both backends can differ dramatically here without needing the public canvas model itself to fork.
|
||||
|
||||
## Why State Is Snapshotted
|
||||
|
||||
Drawing APIs look stateful because they are stateful. The active transform, clips, graphics options, and layer information all affect future commands.
|
||||
|
||||
`DrawingCanvasState` exists so that state changes are cheap to reason about and cheap to attach to commands.
|
||||
|
||||
The state snapshot contains the active options and target information for subsequent commands, including:
|
||||
|
||||
- `Options`
|
||||
- `ClipPaths`
|
||||
- `IsLayer`
|
||||
- layer-related graphics options and bounds
|
||||
- current target bounds
|
||||
|
||||
The canvas treats this state as immutable snapshots on a stack. `Save()` pushes a copy. `Restore()` pops one. Drawing calls always read the current top-of-stack state.
|
||||
|
||||
That makes save and restore semantics predictable and backend-independent.
|
||||
|
||||
## How Layers Work In This Architecture
|
||||
|
||||
Layer terminology often causes confusion because different systems use it differently. In this codebase, the most useful mental model is:
|
||||
|
||||
"A layer is a nested composition scope recorded inline in the command stream."
|
||||
|
||||
When `SaveLayer(...)` is called, the canvas:
|
||||
|
||||
1. clamps the requested layer bounds to the canvas
|
||||
2. converts them into absolute target bounds
|
||||
3. records `BeginLayer`
|
||||
4. pushes a state snapshot that marks the new layer scope
|
||||
|
||||
The layer bounds are expressed in the active local coordinate system, so the canvas
|
||||
transform in effect at `SaveLayer(...)` time is applied when resolving the layer's
|
||||
absolute target bounds. The resolved bounds limit isolation, allocation, and final
|
||||
composition. They do not shift the canvas coordinate system; draw commands inside a
|
||||
bounded layer still use the same local coordinates as the parent canvas.
|
||||
|
||||
When the layer is later closed through `Restore()` or `RestoreTo(...)`, the canvas records `EndLayer`.
|
||||
|
||||
The actual isolation is implemented later by the backend.
|
||||
|
||||
On the CPU backend, layer boundaries become temporary backing buffers during scene execution.
|
||||
|
||||
On the WebGPU backend, layer boundaries become explicit staged-scene operations inside the GPU-oriented pipeline.
|
||||
|
||||
The key architectural point is that the public canvas records one shared layer model and lets the backend lower it.
|
||||
|
||||
## Why Frames Exist
|
||||
|
||||
The frame abstraction solves another unification problem.
|
||||
|
||||
The canvas should be able to target a plain in-memory image, but that should not force the GPU backend to pretend everything is CPU memory. Likewise, GPU-native targets should not force the CPU path to know about native surfaces directly.
|
||||
|
||||
`ICanvasFrame<TPixel>` is the contract that keeps those concerns separated.
|
||||
|
||||
In this architecture, a frame means "the destination surface and its capabilities". That is why the interface exposes both:
|
||||
|
||||
- geometric bounds
|
||||
- optional CPU access
|
||||
- optional native-surface access
|
||||
|
||||
This lets the same canvas code target different kinds of surfaces without rewriting the command model.
|
||||
|
||||
`CanvasRegionFrame<TPixel>` extends that idea one step further by saying "treat this clipped rectangle inside another frame as the target". That is how region canvases can share the same backend and batcher model while still drawing into a sub-rectangle.
|
||||
|
||||
## What `CreateRegion(...)` Really Means
|
||||
|
||||
`CreateRegion(...)` does not create a new independent rendering universe. It creates a child canvas that views a clipped sub-region of the parent target.
|
||||
|
||||
The child:
|
||||
|
||||
- wraps the parent target in `CanvasRegionFrame<TPixel>`
|
||||
- keeps using the same backend
|
||||
- keeps using the same shared batcher
|
||||
- keeps participating in the same deferred replay model
|
||||
|
||||
The child canvas has local coordinates starting at `(0, 0)`, but its frame bounds resolve to the correct absolute position inside the parent target.
|
||||
|
||||
That distinction matters. It means the region API is a coordinate-system convenience, not a request to fork rendering into a totally separate backend pipeline.
|
||||
|
||||
## Why `DrawImage(...)` Is Special
|
||||
|
||||
Most draw calls record intent and defer the heavy work.
|
||||
|
||||
`DrawImage(...)` is the notable exception.
|
||||
|
||||
Images behave differently from paths because the canvas cannot simply attach a transform and let the backend "figure it out later" in the same way. The code performs eager image work before the final command is queued.
|
||||
|
||||
The rough flow is:
|
||||
|
||||
1. crop and scale the source image if needed
|
||||
2. if a canvas transform is active, bake that transform into the image pixels
|
||||
3. align the transformed bitmap to integer canvas bounds
|
||||
4. create an `ImageBrush`
|
||||
5. queue the final fill command using that brush
|
||||
|
||||
This design avoids applying the canvas transform twice and keeps the later command model consistent with brush-based filling.
|
||||
|
||||
That is why `DrawImage(...)` should be understood as "prepare an image-backed brush, then queue a normal fill", not as a completely separate rasterization pipeline.
|
||||
|
||||
## What The CPU Backend Receives
|
||||
|
||||
Once a command batch reaches `DefaultDrawingBackend.CreateScene(...)`, the public drawing model is already normalized.
|
||||
|
||||
The CPU backend does not need to understand every public API call individually. It works with:
|
||||
|
||||
- prepared commands
|
||||
- layer boundaries
|
||||
- target bounds during `CreateScene(...)`
|
||||
- the destination frame during `RenderScene<TPixel>(...)`
|
||||
|
||||
It lowers each command batch into a retained row-oriented structure through `FlushScene`. Later, `RenderScene<TPixel>(...)` acquires the CPU destination frame, allocates temporary backing buffers for layers when needed, and composites the final result into the target frame.
|
||||
|
||||
That is the payoff of the architecture: the CPU backend is solving a rendering problem, not a public-API interpretation problem.
|
||||
|
||||
## What The WebGPU Backend Receives
|
||||
|
||||
The WebGPU backend receives the same command batch shape, but it splits retained scene creation from render-scoped GPU work.
|
||||
|
||||
`CreateScene(...)` handles:
|
||||
|
||||
- encoding prepared command data
|
||||
|
||||
`RenderScene<TPixel>(...)` handles:
|
||||
|
||||
- creating render-scoped native resources
|
||||
- planning dispatches
|
||||
- executing the GPU pipeline
|
||||
|
||||
It benefits from the same canvas-level decisions:
|
||||
|
||||
- commands are already normalized
|
||||
- layers already exist as explicit boundaries
|
||||
- the frame already describes whether a native surface is available
|
||||
|
||||
The WebGPU public helpers reach this point in a target-first way:
|
||||
|
||||
- `WebGPUWindow` acquires a presentable native target per frame
|
||||
- `WebGPURenderTarget` owns an offscreen native target for GPU drawing and readback
|
||||
- `WebGPUExternalSurface` attaches WebGPU drawing to a caller-owned native host
|
||||
|
||||
Those helpers all create typed canvas instances with an explicit `WebGPUDrawingBackend`, so GPU execution stays attached to the WebGPU object that owns the native target and backend lifetime while callers work through `DrawingCanvas`.
|
||||
|
||||
The backend is free to choose a very different execution model because the canvas has already solved the shared semantics problem.
|
||||
|
||||
## The Practical Mental Model
|
||||
|
||||
If you are new to this code, the most useful mental model is:
|
||||
|
||||
`DrawingCanvas` is the stateful front end that records drawing intent, `DrawingCanvas<TPixel>` is the typed implementation, `DrawingCanvasBatcher<TPixel>` is the deferred handoff boundary, and the backend creates and renders retained scenes from prepared command batches.
|
||||
|
||||
Everything else serves that flow.
|
||||
|
||||
State snapshots exist so save and restore are precise.
|
||||
|
||||
Commands exist so public API calls can be deferred.
|
||||
|
||||
Preparation exists so backend-agnostic normalization happens once.
|
||||
|
||||
Frames exist so the same canvas can target memory, native surfaces, or sub-regions.
|
||||
|
||||
Layers exist as inline composition scopes in the command stream.
|
||||
|
||||
Once those ideas are clear, the code stops looking like a random collection of types and starts looking like one system with a clear division of responsibility.
|
||||
|
||||
## Reading Guide
|
||||
|
||||
If you want to move from the architecture into the code, this is the best order.
|
||||
|
||||
1. `DrawingCanvas.cs`
|
||||
2. `DrawingCanvas{TPixel}.cs`
|
||||
3. `DrawingCanvasFactoryExtensions.cs` and `DrawingCanvas.Shapes.cs`
|
||||
4. `DrawingCanvasBatcher{TPixel}.cs`
|
||||
5. `CompositionCommand.cs`
|
||||
6. `DefaultDrawingBackend.cs`
|
||||
7. `FlushScene.cs`
|
||||
8. `WebGPUEnvironment.cs`
|
||||
9. `WebGPUWindow.cs`, `WebGPUExternalSurface.cs`, and `WebGPURenderTarget.cs`
|
||||
10. `WebGPUDrawingBackend` and its scene/dispatch types
|
||||
|
||||
That path follows the real runtime flow:
|
||||
|
||||
public API -> recorded command -> prepared command batch -> backend scene creation -> backend scene rendering
|
||||
|
||||
Following the code in that order is much easier than starting from the backend internals first.
|
||||
@@ -0,0 +1,225 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <content>
|
||||
/// Convenience shape helpers that forward to the core <see cref="DrawingCanvas"/> primitives.
|
||||
/// </content>
|
||||
public abstract partial class DrawingCanvas
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves the current drawing state and begins an isolated compositing layer over the whole canvas.
|
||||
/// </summary>
|
||||
/// <returns>The save count after the layer state has been pushed.</returns>
|
||||
public int SaveLayer()
|
||||
=> this.SaveLayer(new GraphicsOptions(), this.Bounds);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current drawing state and begins an isolated compositing layer over the whole canvas.
|
||||
/// </summary>
|
||||
/// <param name="layerOptions">Graphics options controlling how the layer is composited on restore.</param>
|
||||
/// <returns>The save count after the layer state has been pushed.</returns>
|
||||
public int SaveLayer(GraphicsOptions layerOptions)
|
||||
=> this.SaveLayer(layerOptions, this.Bounds);
|
||||
|
||||
/// <summary>
|
||||
/// Fills the whole canvas using the given brush.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade destination pixels.</param>
|
||||
public void Fill(Brush brush)
|
||||
{
|
||||
Rectangle bounds = this.Bounds;
|
||||
|
||||
this.Fill(brush, new RectanglePolygon(bounds));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills a local region using the given brush.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade destination pixels.</param>
|
||||
/// <param name="region">Region to fill in local coordinates.</param>
|
||||
public void Fill(Brush brush, Rectangle region)
|
||||
=> this.Fill(brush, new RectanglePolygon(region));
|
||||
|
||||
/// <summary>
|
||||
/// Clears the whole canvas using the given brush and clear-style composition options.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade destination pixels during clear.</param>
|
||||
public void Clear(Brush brush)
|
||||
{
|
||||
Rectangle bounds = this.Bounds;
|
||||
|
||||
this.Clear(brush, new RectanglePolygon(bounds));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears a local region using the given brush and clear-style composition options.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade destination pixels during clear.</param>
|
||||
/// <param name="region">Region to clear in local coordinates.</param>
|
||||
public void Clear(Brush brush, Rectangle region)
|
||||
=> this.Clear(brush, new RectanglePolygon(region));
|
||||
|
||||
/// <summary>
|
||||
/// Fills all paths in a collection using the given brush.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade covered pixels.</param>
|
||||
/// <param name="paths">Path collection to fill.</param>
|
||||
public void Fill(Brush brush, IPathCollection paths)
|
||||
{
|
||||
Guard.NotNull(paths, nameof(paths));
|
||||
|
||||
foreach (IPath path in paths)
|
||||
{
|
||||
this.Fill(brush, path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills a path built by the provided builder using the given brush.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade covered pixels.</param>
|
||||
/// <param name="pathBuilder">The path builder describing the fill region.</param>
|
||||
public void Fill(Brush brush, PathBuilder pathBuilder)
|
||||
{
|
||||
Guard.NotNull(pathBuilder, nameof(pathBuilder));
|
||||
|
||||
this.Fill(brush, pathBuilder.Build());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills an ellipse using the provided brush.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade covered pixels.</param>
|
||||
/// <param name="center">Ellipse center point in local coordinates.</param>
|
||||
/// <param name="size">Ellipse width and height in local coordinates.</param>
|
||||
public void FillEllipse(Brush brush, PointF center, SizeF size)
|
||||
=> this.Fill(brush, new EllipsePolygon(center, size));
|
||||
|
||||
/// <summary>
|
||||
/// Fills the closed arc shape produced by joining the arc endpoints with a straight line.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade covered pixels.</param>
|
||||
/// <param name="center">Arc center point in local coordinates.</param>
|
||||
/// <param name="radius">Arc radii in local coordinates.</param>
|
||||
/// <param name="rotation">Ellipse rotation in degrees.</param>
|
||||
/// <param name="startAngle">Arc start angle in degrees.</param>
|
||||
/// <param name="sweepAngle">Arc sweep angle in degrees.</param>
|
||||
public void FillArc(Brush brush, PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle)
|
||||
=> this.Fill(brush, new Path(new ArcLineSegment(center, radius, rotation, startAngle, sweepAngle)));
|
||||
|
||||
/// <summary>
|
||||
/// Fills a pie sector using the provided brush.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade covered pixels.</param>
|
||||
/// <param name="center">The center point of the pie sector in local coordinates.</param>
|
||||
/// <param name="radius">The x and y radii of the pie sector in local coordinates.</param>
|
||||
/// <param name="rotation">Ellipse rotation in degrees.</param>
|
||||
/// <param name="startAngle">The start angle of the pie sector in degrees.</param>
|
||||
/// <param name="sweepAngle">The sweep angle of the pie sector in degrees.</param>
|
||||
public void FillPie(Brush brush, PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle)
|
||||
=> this.Fill(brush, new PiePolygon(center, radius, rotation, startAngle, sweepAngle));
|
||||
|
||||
/// <summary>
|
||||
/// Fills a pie sector using the provided brush.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade covered pixels.</param>
|
||||
/// <param name="center">The center point of the pie sector in local coordinates.</param>
|
||||
/// <param name="radius">The x and y radii of the pie sector in local coordinates.</param>
|
||||
/// <param name="startAngle">The start angle of the pie sector in degrees.</param>
|
||||
/// <param name="sweepAngle">The sweep angle of the pie sector in degrees.</param>
|
||||
public void FillPie(Brush brush, PointF center, SizeF radius, float startAngle, float sweepAngle)
|
||||
=> this.Fill(brush, new PiePolygon(center, radius, startAngle, sweepAngle));
|
||||
|
||||
/// <summary>
|
||||
/// Draws an arc outline using the provided pen.
|
||||
/// </summary>
|
||||
/// <param name="pen">Pen used to generate the arc outline.</param>
|
||||
/// <param name="center">Arc center point in local coordinates.</param>
|
||||
/// <param name="radius">Arc radii in local coordinates.</param>
|
||||
/// <param name="rotation">Ellipse rotation in degrees.</param>
|
||||
/// <param name="startAngle">Arc start angle in degrees.</param>
|
||||
/// <param name="sweepAngle">Arc sweep angle in degrees.</param>
|
||||
public void DrawArc(Pen pen, PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle)
|
||||
=> this.Draw(pen, new Path(new ArcLineSegment(center, radius, rotation, startAngle, sweepAngle)));
|
||||
|
||||
/// <summary>
|
||||
/// Draws a cubic bezier outline using the provided pen.
|
||||
/// </summary>
|
||||
/// <param name="pen">Pen used to generate the bezier outline.</param>
|
||||
/// <param name="points">Bezier control points.</param>
|
||||
public void DrawBezier(Pen pen, params PointF[] points)
|
||||
{
|
||||
Guard.NotNull(points, nameof(points));
|
||||
|
||||
this.Draw(pen, new Path(new CubicBezierLineSegment(points)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws an ellipse outline using the provided pen.
|
||||
/// </summary>
|
||||
/// <param name="pen">Pen used to generate the ellipse outline.</param>
|
||||
/// <param name="center">Ellipse center point in local coordinates.</param>
|
||||
/// <param name="size">Ellipse width and height in local coordinates.</param>
|
||||
public void DrawEllipse(Pen pen, PointF center, SizeF size)
|
||||
=> this.Draw(pen, new EllipsePolygon(center, size));
|
||||
|
||||
/// <summary>
|
||||
/// Draws a pie sector outline using the provided pen.
|
||||
/// </summary>
|
||||
/// <param name="pen">Pen used to generate the pie outline.</param>
|
||||
/// <param name="center">The center point of the pie sector in local coordinates.</param>
|
||||
/// <param name="radius">The x and y radii of the pie sector in local coordinates.</param>
|
||||
/// <param name="rotation">Ellipse rotation in degrees.</param>
|
||||
/// <param name="startAngle">The start angle of the pie sector in degrees.</param>
|
||||
/// <param name="sweepAngle">The sweep angle of the pie sector in degrees.</param>
|
||||
public void DrawPie(Pen pen, PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle)
|
||||
=> this.Draw(pen, new PiePolygon(center, radius, rotation, startAngle, sweepAngle));
|
||||
|
||||
/// <summary>
|
||||
/// Draws a pie sector outline using the provided pen.
|
||||
/// </summary>
|
||||
/// <param name="pen">Pen used to generate the pie outline.</param>
|
||||
/// <param name="center">The center point of the pie sector in local coordinates.</param>
|
||||
/// <param name="radius">The x and y radii of the pie sector in local coordinates.</param>
|
||||
/// <param name="startAngle">The start angle of the pie sector in degrees.</param>
|
||||
/// <param name="sweepAngle">The sweep angle of the pie sector in degrees.</param>
|
||||
public void DrawPie(Pen pen, PointF center, SizeF radius, float startAngle, float sweepAngle)
|
||||
=> this.Draw(pen, new PiePolygon(center, radius, startAngle, sweepAngle));
|
||||
|
||||
/// <summary>
|
||||
/// Draws a rectangular outline using the provided pen.
|
||||
/// </summary>
|
||||
/// <param name="pen">Pen used to generate the rectangle outline.</param>
|
||||
/// <param name="region">Rectangle region to stroke.</param>
|
||||
public void Draw(Pen pen, Rectangle region)
|
||||
=> this.Draw(pen, new RectanglePolygon(region));
|
||||
|
||||
/// <summary>
|
||||
/// Draws all paths in a collection using the provided pen.
|
||||
/// </summary>
|
||||
/// <param name="pen">Pen used to generate outlines.</param>
|
||||
/// <param name="paths">Path collection to stroke.</param>
|
||||
public void Draw(Pen pen, IPathCollection paths)
|
||||
{
|
||||
Guard.NotNull(paths, nameof(paths));
|
||||
|
||||
foreach (IPath path in paths)
|
||||
{
|
||||
this.Draw(pen, path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws a path outline built by the provided builder using the given pen.
|
||||
/// </summary>
|
||||
/// <param name="pen">Pen used to generate the outline fill path.</param>
|
||||
/// <param name="pathBuilder">The path builder describing the path to stroke.</param>
|
||||
public void Draw(Pen pen, PathBuilder pathBuilder)
|
||||
{
|
||||
Guard.NotNull(pathBuilder, nameof(pathBuilder));
|
||||
|
||||
this.Draw(pen, pathBuilder.Build());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.Fonts;
|
||||
using SixLabors.ImageSharp.Drawing.Processing.Backends;
|
||||
using SixLabors.ImageSharp.Drawing.Text;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using SixLabors.ImageSharp.Processing.Processors.Transforms;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Represents a drawing canvas over a frame target.
|
||||
/// </summary>
|
||||
public abstract partial class DrawingCanvas : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the local bounds of this canvas.
|
||||
/// </summary>
|
||||
public abstract Rectangle Bounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of saved states currently on the canvas stack.
|
||||
/// </summary>
|
||||
public abstract int SaveCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current drawing state on the state stack.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This operation stores the current canvas state by reference.
|
||||
/// If the same <see cref="DrawingOptions"/> instance is mutated after
|
||||
/// <see cref="Save()"/>, those mutations are visible when restoring.
|
||||
/// </remarks>
|
||||
/// <returns>The save count after the state has been pushed.</returns>
|
||||
public abstract int Save();
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current drawing state and replaces the active state with the provided options and clip paths.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The provided <paramref name="options"/> instance is stored by reference.
|
||||
/// Mutating it after this call mutates the active/restored state behavior.
|
||||
/// </remarks>
|
||||
/// <param name="options">Drawing options for the new active state.</param>
|
||||
/// <param name="clipPaths">Clip paths for the new active state.</param>
|
||||
/// <returns>The save count after the previous state has been pushed.</returns>
|
||||
public abstract int Save(DrawingOptions options, params IPath[] clipPaths);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current drawing state and begins an isolated compositing layer
|
||||
/// bounded to a subregion. Subsequent draw commands are recorded into that isolated
|
||||
/// logical layer. When <see cref="Restore"/> closes the layer, it is recorded into the
|
||||
/// canvas timeline and later composed during <see cref="IDisposable.Dispose"/> using the specified
|
||||
/// <paramref name="layerOptions"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The layer bounds are expressed in the current local coordinate system and are
|
||||
/// transformed with the active drawing transform when the layer is created. They
|
||||
/// limit allocation and compositing only; they do not change the canvas coordinate
|
||||
/// system used by commands recorded inside the layer.
|
||||
/// </remarks>
|
||||
/// <param name="layerOptions">
|
||||
/// Graphics options controlling how the closed layer is composited against the parent canvas
|
||||
/// when the canvas timeline is rendered during <see cref="IDisposable.Dispose"/>.
|
||||
/// </param>
|
||||
/// <param name="bounds">
|
||||
/// The local bounds of the layer. Only this region is allocated and composited.
|
||||
/// </param>
|
||||
/// <returns>The save count after the layer state has been pushed.</returns>
|
||||
public abstract int SaveLayer(GraphicsOptions layerOptions, Rectangle bounds);
|
||||
|
||||
/// <summary>
|
||||
/// Restores the most recently saved state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the most recently saved state was created by a <c>SaveLayer</c> overload,
|
||||
/// the layer is closed in the recorded timeline. Actual composition happens during
|
||||
/// <see cref="IDisposable.Dispose"/>.
|
||||
/// </remarks>
|
||||
public abstract void Restore();
|
||||
|
||||
/// <summary>
|
||||
/// Restores to a specific save count.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// State frames above <paramref name="saveCount"/> are discarded,
|
||||
/// and the last discarded frame becomes the current state.
|
||||
/// If any discarded state was created by a <c>SaveLayer</c> overload,
|
||||
/// those layers are closed in the recorded timeline and composed during
|
||||
/// <see cref="IDisposable.Dispose"/>.
|
||||
/// </remarks>
|
||||
/// <param name="saveCount">The save count to restore to.</param>
|
||||
public abstract void RestoreTo(int saveCount);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a child canvas over a subregion in local coordinates.
|
||||
/// </summary>
|
||||
/// <param name="region">The child region in local coordinates.</param>
|
||||
/// <returns>A child canvas with local origin at (0,0).</returns>
|
||||
public abstract DrawingCanvas CreateRegion(Rectangle region);
|
||||
|
||||
/// <summary>
|
||||
/// Clears a path region using the given brush and clear-style composition options.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade destination pixels during clear.</param>
|
||||
/// <param name="path">The path region to clear.</param>
|
||||
public abstract void Clear(Brush brush, IPath path);
|
||||
|
||||
/// <summary>
|
||||
/// Fills a path in local coordinates using the given brush.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to shade covered pixels.</param>
|
||||
/// <param name="path">The path to fill.</param>
|
||||
public abstract void Fill(Brush brush, IPath path);
|
||||
|
||||
/// <summary>
|
||||
/// Applies an image-processing operation to a local region.
|
||||
/// </summary>
|
||||
/// <param name="region">The local region to process.</param>
|
||||
/// <param name="operation">The image-processing operation to apply to the region.</param>
|
||||
public abstract void Apply(Rectangle region, Action<IImageProcessingContext> operation);
|
||||
|
||||
/// <summary>
|
||||
/// Applies an image-processing operation to a region described by a path builder.
|
||||
/// </summary>
|
||||
/// <param name="pathBuilder">The path builder describing the region to process.</param>
|
||||
/// <param name="operation">The image-processing operation to apply to the region.</param>
|
||||
public abstract void Apply(PathBuilder pathBuilder, Action<IImageProcessingContext> operation);
|
||||
|
||||
/// <summary>
|
||||
/// Applies an image-processing operation to a path region.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The operation affects only pixels covered by the supplied path.
|
||||
/// </remarks>
|
||||
/// <param name="path">The path region to process.</param>
|
||||
/// <param name="operation">The image-processing operation to apply to the region.</param>
|
||||
public abstract void Apply(IPath path, Action<IImageProcessingContext> operation);
|
||||
|
||||
/// <summary>
|
||||
/// Draws a polyline outline using the provided pen and drawing options.
|
||||
/// </summary>
|
||||
/// <param name="pen">Pen used to generate the line outline.</param>
|
||||
/// <param name="points">Polyline points.</param>
|
||||
public abstract void DrawLine(Pen pen, params PointF[] points);
|
||||
|
||||
/// <summary>
|
||||
/// Draws a path outline in local coordinates using the given pen.
|
||||
/// </summary>
|
||||
/// <param name="pen">Pen used to generate the outline fill path.</param>
|
||||
/// <param name="path">The path to stroke.</param>
|
||||
public abstract void Draw(Pen pen, IPath path);
|
||||
|
||||
/// <summary>
|
||||
/// Draws text onto this canvas.
|
||||
/// </summary>
|
||||
/// <param name="textOptions">The text rendering options.</param>
|
||||
/// <param name="text">The text to draw.</param>
|
||||
/// <param name="brush">Optional brush used to fill glyphs.</param>
|
||||
/// <param name="pen">Optional pen used to outline glyphs.</param>
|
||||
public abstract void DrawText(
|
||||
RichTextOptions textOptions,
|
||||
ReadOnlySpan<char> text,
|
||||
Brush? brush,
|
||||
Pen? pen);
|
||||
|
||||
/// <summary>
|
||||
/// Draws text along a path baseline onto this canvas.
|
||||
/// </summary>
|
||||
/// <param name="textOptions">The text rendering options.</param>
|
||||
/// <param name="text">The text to draw.</param>
|
||||
/// <param name="path">The path used as the text baseline in local canvas coordinates.</param>
|
||||
/// <param name="brush">Optional brush used to fill glyphs.</param>
|
||||
/// <param name="pen">Optional pen used to outline glyphs.</param>
|
||||
public abstract void DrawText(
|
||||
RichTextOptions textOptions,
|
||||
ReadOnlySpan<char> text,
|
||||
IPath path,
|
||||
Brush? brush,
|
||||
Pen? pen);
|
||||
|
||||
/// <summary>
|
||||
/// Draws a prepared text block onto this canvas.
|
||||
/// </summary>
|
||||
/// <param name="textBlock">The prepared text block to draw.</param>
|
||||
/// <param name="location">The drawing location in local canvas coordinates.</param>
|
||||
/// <param name="wrappingLength">The wrapping length in pixels. Use <c>-1</c> to disable wrapping.</param>
|
||||
/// <param name="brush">Optional brush used to fill glyphs.</param>
|
||||
/// <param name="pen">Optional pen used to outline glyphs.</param>
|
||||
public abstract void DrawText(
|
||||
TextBlock textBlock,
|
||||
PointF location,
|
||||
float wrappingLength,
|
||||
Brush? brush,
|
||||
Pen? pen);
|
||||
|
||||
/// <summary>
|
||||
/// Draws a prepared text block along a path baseline onto this canvas.
|
||||
/// </summary>
|
||||
/// <param name="textBlock">The prepared text block to draw.</param>
|
||||
/// <param name="path">The path used as the text baseline in local canvas coordinates.</param>
|
||||
/// <param name="wrappingLength">The wrapping length in pixels. Use <c>-1</c> to disable wrapping.</param>
|
||||
/// <param name="brush">Optional brush used to fill glyphs.</param>
|
||||
/// <param name="pen">Optional pen used to outline glyphs.</param>
|
||||
public abstract void DrawText(
|
||||
TextBlock textBlock,
|
||||
IPath path,
|
||||
float wrappingLength,
|
||||
Brush? brush,
|
||||
Pen? pen);
|
||||
|
||||
/// <summary>
|
||||
/// Draws one prepared line layout onto this canvas.
|
||||
/// </summary>
|
||||
/// <param name="lineLayout">The prepared line layout to draw.</param>
|
||||
/// <param name="location">The drawing location in local canvas coordinates.</param>
|
||||
/// <param name="brush">Optional brush used to fill glyphs.</param>
|
||||
/// <param name="pen">Optional pen used to outline glyphs.</param>
|
||||
public abstract void DrawText(
|
||||
LineLayout lineLayout,
|
||||
PointF location,
|
||||
Brush? brush,
|
||||
Pen? pen);
|
||||
|
||||
/// <summary>
|
||||
/// Draws one prepared line layout along a path baseline onto this canvas.
|
||||
/// </summary>
|
||||
/// <param name="lineLayout">The prepared line layout to draw.</param>
|
||||
/// <param name="path">The path used as the text baseline in local canvas coordinates.</param>
|
||||
/// <param name="brush">Optional brush used to fill glyphs.</param>
|
||||
/// <param name="pen">Optional pen used to outline glyphs.</param>
|
||||
public abstract void DrawText(
|
||||
LineLayout lineLayout,
|
||||
IPath path,
|
||||
Brush? brush,
|
||||
Pen? pen);
|
||||
|
||||
/// <summary>
|
||||
/// Draws layered glyph geometry.
|
||||
/// </summary>
|
||||
/// <param name="brush">Brush used to fill glyph layers.</param>
|
||||
/// <param name="pen">Pen used to outline dominant painted layers.</param>
|
||||
/// <param name="glyphs">Layered glyph geometry to draw.</param>
|
||||
public abstract void DrawGlyphs(
|
||||
Brush brush,
|
||||
Pen pen,
|
||||
IEnumerable<GlyphPathCollection> glyphs);
|
||||
|
||||
/// <summary>
|
||||
/// Measures the full set of layout metrics for the supplied text.
|
||||
/// </summary>
|
||||
/// <param name="textOptions">The text shaping and layout options.</param>
|
||||
/// <param name="text">The text to measure.</param>
|
||||
/// <returns>A <see cref="TextMetrics"/> value containing the metrics for the laid-out text.</returns>
|
||||
public abstract TextMetrics MeasureText(RichTextOptions textOptions, ReadOnlySpan<char> text);
|
||||
|
||||
/// <summary>
|
||||
/// Draws an image source region into a destination rectangle.
|
||||
/// </summary>
|
||||
/// <param name="image">The source image.</param>
|
||||
/// <param name="sourceRect">The source rectangle within <paramref name="image"/>.</param>
|
||||
/// <param name="destinationRect">The destination rectangle in local canvas coordinates.</param>
|
||||
/// <param name="sampler">
|
||||
/// Optional resampler used when scaling or transforming the image. Defaults to <see cref="KnownResamplers.Bicubic"/>.
|
||||
/// </param>
|
||||
public abstract void DrawImage(
|
||||
Image image,
|
||||
Rectangle sourceRect,
|
||||
RectangleF destinationRect,
|
||||
IResampler? sampler = null);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a retained backend scene from the drawing commands currently queued on this canvas.
|
||||
/// </summary>
|
||||
/// <returns>A retained backend scene.</returns>
|
||||
public abstract DrawingBackendScene CreateScene();
|
||||
|
||||
/// <summary>
|
||||
/// Renders a retained backend scene into this canvas target.
|
||||
/// </summary>
|
||||
/// <param name="scene">The retained backend scene to render.</param>
|
||||
public abstract void RenderScene(DrawingBackendScene scene);
|
||||
|
||||
/// <summary>
|
||||
/// Seals queued drawing commands into the canvas timeline.
|
||||
/// </summary>
|
||||
public abstract void Flush();
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract void Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
using SixLabors.ImageSharp.Drawing.Processing.Backends;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Queues normalized composition commands emitted by <see cref="DrawingCanvas{TPixel}"/>
|
||||
/// and prepares them in deterministic draw order.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The batcher owns command buffering and replay ordering only; it does not rasterize or composite.
|
||||
/// Draw commands are stored in the command buffer until a timeline command-range entry references
|
||||
/// them. Existing retained scenes passed through <see cref="DrawingCanvas.RenderScene"/> are stored
|
||||
/// separately and referenced by timeline entry index. During disposal replay, command ranges are
|
||||
/// lowered to short-lived backend scenes at the position where the canvas recorded the range.
|
||||
/// </remarks>
|
||||
internal sealed class DrawingCanvasBatcher<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly Configuration configuration;
|
||||
|
||||
// Draw commands stay in this buffer until replay lowers referenced command ranges
|
||||
// into backend scenes at their recorded timeline position.
|
||||
private CompositionSceneCommand[] commands;
|
||||
private int commandCount;
|
||||
private int sealedCommandCount;
|
||||
|
||||
// Layer metadata is range-sensitive, so sealing advances this alongside command
|
||||
// sealing instead of letting layer state leak across later command ranges.
|
||||
private int layerCommandCount;
|
||||
private int sealedLayerCommandCount;
|
||||
|
||||
// Clip and dash flags gate whole-buffer command preparation; prepared commands
|
||||
// remain in the same command buffer until replay consumes it.
|
||||
private bool hasClips;
|
||||
private bool hasDashes;
|
||||
|
||||
// Timeline entries keep compact indexes into the command, barrier, and retained
|
||||
// scene buffers while preserving the order recorded by the canvas.
|
||||
private DrawingCanvasTimelineEntry[] entries;
|
||||
|
||||
// Apply barriers carry replay-time target read/process/write operations.
|
||||
private ApplyBarrier[] applyBarriers;
|
||||
private int applyBarrierCount;
|
||||
|
||||
// These are existing retained scenes recorded through RenderScene, not scenes
|
||||
// produced later from this batcher's own command ranges.
|
||||
private DrawingBackendScene[] insertedScenes;
|
||||
private int insertedSceneCount;
|
||||
|
||||
internal DrawingCanvasBatcher(Configuration configuration)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
this.commands = [];
|
||||
this.entries = [];
|
||||
this.applyBarriers = [];
|
||||
this.insertedScenes = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether there are queued commands or timeline entries.
|
||||
/// </summary>
|
||||
public bool HasRecordedWork => this.commandCount > 0 || this.TimelineEntryCount > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of ordered replay items recorded in the canvas timeline.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is not a draw-command count. A single entry can represent a contiguous command range,
|
||||
/// an apply barrier, or an inserted retained scene.
|
||||
/// </remarks>
|
||||
public int TimelineEntryCount { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Appends one normalized composition command to the pending queue.
|
||||
/// </summary>
|
||||
/// <param name="composition">The command to queue.</param>
|
||||
public void AddComposition(in CompositionCommand composition)
|
||||
{
|
||||
this.EnsureCommandCapacity(this.commandCount + 1);
|
||||
this.commands[this.commandCount++] = new PathCompositionSceneCommand(composition);
|
||||
|
||||
if (composition.Kind is not CompositionCommandKind.FillLayer)
|
||||
{
|
||||
this.layerCommandCount++;
|
||||
}
|
||||
|
||||
this.hasClips |= composition.ClipPaths is not null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends one stroked path command to the pending queue.
|
||||
/// </summary>
|
||||
/// <param name="command">The command to queue.</param>
|
||||
public void AddStrokePath(in StrokePathCommand command)
|
||||
{
|
||||
this.EnsureCommandCapacity(this.commandCount + 1);
|
||||
this.commands[this.commandCount++] = new StrokePathCompositionSceneCommand(command);
|
||||
this.hasClips |= command.ClipPaths is not null;
|
||||
this.hasDashes |= command.Pen.StrokePattern.Length >= 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends one explicit stroked line-segment command to the pending queue.
|
||||
/// </summary>
|
||||
/// <param name="command">The command to queue.</param>
|
||||
public void AddStrokeLineSegment(in StrokeLineSegmentCommand command)
|
||||
{
|
||||
this.EnsureCommandCapacity(this.commandCount + 1);
|
||||
this.commands[this.commandCount++] = new LineSegmentCompositionSceneCommand(command);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends one explicit stroked polyline command to the pending queue.
|
||||
/// </summary>
|
||||
/// <param name="command">The command to queue.</param>
|
||||
public void AddStrokePolyline(in StrokePolylineCommand command)
|
||||
{
|
||||
this.EnsureCommandCapacity(this.commandCount + 1);
|
||||
this.commands[this.commandCount++] = new PolylineCompositionSceneCommand(command);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seals currently queued commands into the replay timeline.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This records a command range only. Backend scenes are created later by the replay path
|
||||
/// from the referenced command range, so sealing does not render or allocate backend scene state.
|
||||
/// </remarks>
|
||||
public void SealCommands()
|
||||
{
|
||||
int count = this.commandCount - this.sealedCommandCount;
|
||||
if (count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.EnsureEntryCapacity(this.TimelineEntryCount + 1);
|
||||
this.entries[this.TimelineEntryCount++] = DrawingCanvasTimelineEntry.CreateCommandRange(
|
||||
this.sealedCommandCount,
|
||||
count,
|
||||
this.layerCommandCount != this.sealedLayerCommandCount);
|
||||
|
||||
this.sealedCommandCount = this.commandCount;
|
||||
this.sealedLayerCommandCount = this.layerCommandCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends an apply barrier to the replay timeline after sealing queued commands.
|
||||
/// </summary>
|
||||
/// <param name="barrier">The apply barrier to append.</param>
|
||||
internal void AddApplyBarrier(ApplyBarrier barrier)
|
||||
{
|
||||
this.SealCommands();
|
||||
this.EnsureApplyBarrierCapacity(this.applyBarrierCount + 1);
|
||||
|
||||
int barrierIndex = this.applyBarrierCount;
|
||||
this.applyBarriers[this.applyBarrierCount++] = barrier;
|
||||
this.EnsureEntryCapacity(this.TimelineEntryCount + 1);
|
||||
this.entries[this.TimelineEntryCount++] = DrawingCanvasTimelineEntry.CreateApplyBarrier(barrierIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records an existing retained scene in the replay timeline after sealing queued commands.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This stores only scenes passed to <see cref="DrawingCanvas.RenderScene"/>. Scenes produced
|
||||
/// from this canvas's own command ranges are created later by the backend from command batches.
|
||||
/// </remarks>
|
||||
/// <param name="scene">The retained scene to render at this point in the timeline.</param>
|
||||
public void AddScene(DrawingBackendScene scene)
|
||||
{
|
||||
this.SealCommands();
|
||||
this.EnsureInsertedSceneCapacity(this.insertedSceneCount + 1);
|
||||
|
||||
int sceneIndex = this.insertedSceneCount;
|
||||
this.insertedScenes[this.insertedSceneCount++] = scene;
|
||||
this.EnsureEntryCapacity(this.TimelineEntryCount + 1);
|
||||
this.entries[this.TimelineEntryCount++] = DrawingCanvasTimelineEntry.CreateScene(sceneIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a retained backend scene from the recorded timeline.
|
||||
/// </summary>
|
||||
/// <param name="backend">The backend used to create the retained scene.</param>
|
||||
/// <param name="targetBounds">The target bounds used for target-dependent scene creation.</param>
|
||||
/// <param name="ownedResources">The resources that must stay alive for the returned scene.</param>
|
||||
/// <returns>The retained backend scene.</returns>
|
||||
public DrawingBackendScene CreateScene(
|
||||
IDrawingBackend backend,
|
||||
Rectangle targetBounds,
|
||||
IReadOnlyList<IDisposable>? ownedResources)
|
||||
{
|
||||
if (!this.HasRecordedWork)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot create a retained scene from an empty canvas.");
|
||||
}
|
||||
|
||||
this.SealAndPrepareCommands();
|
||||
|
||||
return backend.CreateScene(
|
||||
this.configuration,
|
||||
targetBounds,
|
||||
new DrawingCommandBatch(this.commands, this.commandCount, this.layerCommandCount > 0),
|
||||
ownedResources);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seals any pending commands and prepares queued command data for backend scene creation.
|
||||
/// </summary>
|
||||
public void SealAndPrepareCommands()
|
||||
{
|
||||
this.SealCommands();
|
||||
|
||||
this.PrepareCommands();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a command batch over one recorded command-range timeline entry.
|
||||
/// </summary>
|
||||
/// <param name="entry">The command-range timeline entry.</param>
|
||||
/// <returns>The command batch.</returns>
|
||||
public DrawingCommandBatch CreateCommandBatch(DrawingCanvasTimelineEntry entry)
|
||||
=> new(this.commands, entry.Index, entry.Count, entry.HasLayers);
|
||||
|
||||
/// <summary>
|
||||
/// Gets one recorded timeline entry.
|
||||
/// </summary>
|
||||
/// <param name="index">The entry index.</param>
|
||||
/// <returns>The recorded timeline entry.</returns>
|
||||
public DrawingCanvasTimelineEntry GetEntry(int index)
|
||||
=> this.entries[index];
|
||||
|
||||
/// <summary>
|
||||
/// Gets one recorded apply barrier.
|
||||
/// </summary>
|
||||
/// <param name="index">The apply-barrier index.</param>
|
||||
/// <returns>The recorded apply barrier.</returns>
|
||||
internal ApplyBarrier GetApplyBarrier(int index)
|
||||
=> this.applyBarriers[index];
|
||||
|
||||
/// <summary>
|
||||
/// Gets one retained scene reference recorded through <see cref="DrawingCanvas.RenderScene"/>.
|
||||
/// </summary>
|
||||
/// <param name="index">The retained-scene reference index.</param>
|
||||
/// <returns>The retained scene to render at the timeline entry.</returns>
|
||||
public DrawingBackendScene GetInsertedScene(int index)
|
||||
=> this.insertedScenes[index];
|
||||
|
||||
/// <summary>
|
||||
/// Clears command references after a prepared batch has been consumed.
|
||||
/// </summary>
|
||||
public void ClearCommandBatch()
|
||||
{
|
||||
Array.Clear(this.commands, 0, this.commandCount);
|
||||
Array.Clear(this.entries, 0, this.TimelineEntryCount);
|
||||
Array.Clear(this.applyBarriers, 0, this.applyBarrierCount);
|
||||
Array.Clear(this.insertedScenes, 0, this.insertedSceneCount);
|
||||
this.commandCount = 0;
|
||||
this.sealedCommandCount = 0;
|
||||
this.layerCommandCount = 0;
|
||||
this.sealedLayerCommandCount = 0;
|
||||
this.TimelineEntryCount = 0;
|
||||
this.applyBarrierCount = 0;
|
||||
this.insertedSceneCount = 0;
|
||||
this.hasClips = false;
|
||||
this.hasDashes = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the command buffer can store the requested command count without reallocating.
|
||||
/// </summary>
|
||||
/// <param name="requiredCapacity">The required command capacity.</param>
|
||||
private void EnsureCommandCapacity(int requiredCapacity)
|
||||
{
|
||||
if (requiredCapacity <= this.commands.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int nextCapacity = this.commands.Length == 0 ? 16 : this.commands.Length * 2;
|
||||
if (nextCapacity < requiredCapacity)
|
||||
{
|
||||
nextCapacity = requiredCapacity;
|
||||
}
|
||||
|
||||
Array.Resize(ref this.commands, nextCapacity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the timeline entry buffer can store the requested entry count without reallocating.
|
||||
/// </summary>
|
||||
/// <param name="requiredCapacity">The required entry capacity.</param>
|
||||
private void EnsureEntryCapacity(int requiredCapacity)
|
||||
{
|
||||
if (requiredCapacity <= this.entries.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int nextCapacity = this.entries.Length == 0 ? 4 : this.entries.Length * 2;
|
||||
if (nextCapacity < requiredCapacity)
|
||||
{
|
||||
nextCapacity = requiredCapacity;
|
||||
}
|
||||
|
||||
Array.Resize(ref this.entries, nextCapacity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the apply-barrier buffer can store the requested barrier count without reallocating.
|
||||
/// </summary>
|
||||
/// <param name="requiredCapacity">The required barrier capacity.</param>
|
||||
private void EnsureApplyBarrierCapacity(int requiredCapacity)
|
||||
{
|
||||
if (requiredCapacity <= this.applyBarriers.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int nextCapacity = this.applyBarriers.Length == 0 ? 2 : this.applyBarriers.Length * 2;
|
||||
if (nextCapacity < requiredCapacity)
|
||||
{
|
||||
nextCapacity = requiredCapacity;
|
||||
}
|
||||
|
||||
Array.Resize(ref this.applyBarriers, nextCapacity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the inserted-scene buffer can store the requested scene count without reallocating.
|
||||
/// </summary>
|
||||
/// <param name="requiredCapacity">The required scene capacity.</param>
|
||||
private void EnsureInsertedSceneCapacity(int requiredCapacity)
|
||||
{
|
||||
if (requiredCapacity <= this.insertedScenes.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int nextCapacity = this.insertedScenes.Length == 0 ? 2 : this.insertedScenes.Length * 2;
|
||||
if (nextCapacity < requiredCapacity)
|
||||
{
|
||||
nextCapacity = requiredCapacity;
|
||||
}
|
||||
|
||||
Array.Resize(ref this.insertedScenes, nextCapacity);
|
||||
}
|
||||
|
||||
private void PrepareCommands()
|
||||
{
|
||||
if (!this.hasClips && !this.hasDashes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If clipping is present we need to apply that now before handing the command
|
||||
// to the backend. This avoids complicating the backend with clipping logic
|
||||
// and allows us to reuse the same optimized backend code for clipped and unclipped paths.
|
||||
int requestedParallelism = this.configuration.MaxDegreeOfParallelism;
|
||||
int partitionCount = ParallelExecutionHelper.GetPartitionCount(requestedParallelism, this.commandCount);
|
||||
|
||||
if (partitionCount <= 1)
|
||||
{
|
||||
for (int i = 0; i < this.commandCount; i++)
|
||||
{
|
||||
PrepareCommand(ref this.commands[i]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_ = Parallel.For(
|
||||
0,
|
||||
partitionCount,
|
||||
ParallelExecutionHelper.CreateParallelOptions(requestedParallelism, partitionCount),
|
||||
partitionIndex =>
|
||||
{
|
||||
// Integer division splits the commands into contiguous half-open ranges,
|
||||
// keeping the partitions balanced while assigning each command exactly once.
|
||||
int commandStart = (partitionIndex * this.commandCount) / partitionCount;
|
||||
int commandEnd = ((partitionIndex + 1) * this.commandCount) / partitionCount;
|
||||
|
||||
for (int i = commandStart; i < commandEnd; i++)
|
||||
{
|
||||
PrepareCommand(ref this.commands[i]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void PrepareCommand(ref CompositionSceneCommand command)
|
||||
{
|
||||
if (command is PathCompositionSceneCommand pathCommand)
|
||||
{
|
||||
CompositionCommand composition = pathCommand.Command;
|
||||
if (composition.ClipPaths is { Count: > 0 })
|
||||
{
|
||||
IPath path = composition.SourcePath;
|
||||
DrawingOptions sourceOptions = composition.DrawingOptions;
|
||||
|
||||
if (sourceOptions.Transform != Matrix4x4.Identity)
|
||||
{
|
||||
path = path.Transform(sourceOptions.Transform);
|
||||
}
|
||||
|
||||
path = path.Clip(sourceOptions.ShapeOptions, composition.ClipPaths);
|
||||
|
||||
RasterizerOptions rasterizerOptions = composition.RasterizerOptions;
|
||||
DrawingOptions preparedOptions = WithIdentityTransform(sourceOptions);
|
||||
|
||||
// Update the command with the clipped path.
|
||||
pathCommand.Command = CompositionCommand.Create(
|
||||
path,
|
||||
composition.Brush.Transform(sourceOptions.Transform),
|
||||
preparedOptions,
|
||||
in rasterizerOptions,
|
||||
composition.TargetBounds,
|
||||
composition.DestinationOffset,
|
||||
null,
|
||||
composition.IsInsideLayer);
|
||||
}
|
||||
}
|
||||
else if (command is StrokePathCompositionSceneCommand strokePathCommand)
|
||||
{
|
||||
StrokePathCommand composition = strokePathCommand.Command;
|
||||
|
||||
if (composition.ClipPaths is { Count: > 0 })
|
||||
{
|
||||
IPath path = composition.Pen.GeneratePath(composition.SourcePath);
|
||||
DrawingOptions sourceOptions = composition.DrawingOptions;
|
||||
|
||||
if (sourceOptions.Transform != Matrix4x4.Identity)
|
||||
{
|
||||
path = path.Transform(sourceOptions.Transform);
|
||||
}
|
||||
|
||||
path = path.Clip(sourceOptions.ShapeOptions, composition.ClipPaths);
|
||||
|
||||
RasterizerOptions rasterizerOptions = composition.RasterizerOptions;
|
||||
DrawingOptions preparedOptions = WithIdentityTransform(sourceOptions);
|
||||
|
||||
command = new PathCompositionSceneCommand(
|
||||
CompositionCommand.Create(
|
||||
path,
|
||||
composition.Brush.Transform(sourceOptions.Transform),
|
||||
preparedOptions,
|
||||
in rasterizerOptions,
|
||||
composition.TargetBounds,
|
||||
composition.DestinationOffset,
|
||||
null,
|
||||
composition.IsInsideLayer));
|
||||
}
|
||||
else
|
||||
{
|
||||
// We need to dash the path here before sending it to the backend.
|
||||
Pen pen = composition.Pen;
|
||||
if (pen.StrokePattern.Length >= 2)
|
||||
{
|
||||
strokePathCommand.Command = new StrokePathCommand(
|
||||
composition.SourcePath.GenerateDashes(pen.StrokeWidth, pen.StrokePattern.Span),
|
||||
composition.Brush,
|
||||
composition.DrawingOptions,
|
||||
composition.RasterizerOptions,
|
||||
composition.TargetBounds,
|
||||
composition.DestinationOffset,
|
||||
composition.Pen,
|
||||
null,
|
||||
composition.IsInsideLayer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static DrawingOptions WithIdentityTransform(DrawingOptions source)
|
||||
=> source.Transform == Matrix4x4.Identity
|
||||
? source
|
||||
: new DrawingOptions(source.GraphicsOptions, source.ShapeOptions, Matrix4x4.Identity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.Advanced;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Extension methods for creating drawing canvas instances over ImageSharp image frames.
|
||||
/// </summary>
|
||||
public static class DrawingCanvasFactoryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a drawing canvas over an existing typed image frame.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The caller owns the returned canvas and must dispose it to replay recorded work into the frame.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
/// <param name="frame">The frame backing the canvas.</param>
|
||||
/// <param name="configuration">The configuration to use for this canvas instance.</param>
|
||||
/// <param name="options">Initial drawing options for this canvas instance.</param>
|
||||
/// <param name="clipPaths">Initial clip paths for this canvas instance.</param>
|
||||
/// <returns>A drawing canvas targeting <paramref name="frame"/>.</returns>
|
||||
public static DrawingCanvas CreateCanvas<TPixel>(
|
||||
this ImageFrame<TPixel> frame,
|
||||
Configuration configuration,
|
||||
DrawingOptions options,
|
||||
params IPath[] clipPaths)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
Guard.NotNull(frame, nameof(frame));
|
||||
Guard.NotNull(options, nameof(options));
|
||||
Guard.NotNull(clipPaths, nameof(clipPaths));
|
||||
|
||||
return new DrawingCanvas<TPixel>(
|
||||
configuration,
|
||||
options,
|
||||
frame.PixelBuffer.GetRegion(),
|
||||
clipPaths);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a drawing canvas over an existing image frame.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The caller owns the returned canvas and must dispose it to replay recorded work into the frame.
|
||||
/// </remarks>
|
||||
/// <param name="frame">The frame backing the canvas.</param>
|
||||
/// <param name="configuration">The configuration to use for this canvas instance.</param>
|
||||
/// <param name="options">Initial drawing options for this canvas instance.</param>
|
||||
/// <param name="clipPaths">Initial clip paths for this canvas instance.</param>
|
||||
/// <returns>A drawing canvas targeting <paramref name="frame"/>.</returns>
|
||||
public static DrawingCanvas CreateCanvas(
|
||||
this ImageFrame frame,
|
||||
Configuration configuration,
|
||||
DrawingOptions options,
|
||||
params IPath[] clipPaths)
|
||||
{
|
||||
Guard.NotNull(frame, nameof(frame));
|
||||
Guard.NotNull(options, nameof(options));
|
||||
Guard.NotNull(clipPaths, nameof(clipPaths));
|
||||
|
||||
CanvasFactoryVisitor visitor = new(configuration, options, clipPaths);
|
||||
frame.AcceptVisitor(visitor);
|
||||
return visitor.Value!;
|
||||
}
|
||||
|
||||
private struct CanvasFactoryVisitor : IImageFrameVisitor
|
||||
{
|
||||
private readonly Configuration configuration;
|
||||
private readonly DrawingOptions options;
|
||||
private readonly IPath[] clipPaths;
|
||||
|
||||
public CanvasFactoryVisitor(Configuration configuration, DrawingOptions options, IPath[] clipPaths)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
this.options = options;
|
||||
this.clipPaths = clipPaths;
|
||||
}
|
||||
|
||||
public DrawingCanvas? Value { get; private set; }
|
||||
|
||||
void IImageFrameVisitor.Visit<TPixel>(ImageFrame<TPixel> frame)
|
||||
=> this.Value = frame.CreateCanvas(this.configuration, this.options, this.clipPaths);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Immutable drawing state snapshot used by <see cref="DrawingCanvas{TPixel}"/>.
|
||||
/// </summary>
|
||||
internal sealed class DrawingCanvasState
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DrawingCanvasState"/> class.
|
||||
/// </summary>
|
||||
/// <param name="options">Drawing options for this state.</param>
|
||||
/// <param name="clipPaths">Clip paths for this state.</param>
|
||||
/// <param name="targetBounds">Absolute target bounds used for commands recorded in this state.</param>
|
||||
/// <param name="destinationOffset">Absolute destination offset for paths recorded in local canvas coordinates.</param>
|
||||
public DrawingCanvasState(
|
||||
DrawingOptions options,
|
||||
IReadOnlyList<IPath> clipPaths,
|
||||
Rectangle targetBounds,
|
||||
Point destinationOffset)
|
||||
{
|
||||
this.Options = options;
|
||||
this.ClipPaths = clipPaths;
|
||||
this.TargetBounds = targetBounds;
|
||||
this.DestinationOffset = destinationOffset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets drawing options associated with this state.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is the original <see cref="DrawingOptions"/> reference supplied to the state.
|
||||
/// It is not deep-cloned.
|
||||
/// </remarks>
|
||||
public DrawingOptions Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets clip paths associated with this state.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IPath> ClipPaths { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute target bounds used for commands recorded in this state.
|
||||
/// </summary>
|
||||
public Rectangle TargetBounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute destination offset for paths recorded in local canvas coordinates.
|
||||
/// </summary>
|
||||
public Point DestinationOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this state represents a compositing layer.
|
||||
/// </summary>
|
||||
public bool IsLayer { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the layer compositing options when this state represents a compositing layer.
|
||||
/// </summary>
|
||||
public GraphicsOptions? LayerOptions { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Identifies the kind of replay item stored in a drawing canvas timeline.
|
||||
/// </summary>
|
||||
internal enum DrawingCanvasTimelineEntryKind
|
||||
{
|
||||
/// <summary>
|
||||
/// A contiguous range of draw commands.
|
||||
/// </summary>
|
||||
CommandRange,
|
||||
|
||||
/// <summary>
|
||||
/// An apply barrier.
|
||||
/// </summary>
|
||||
ApplyBarrier,
|
||||
|
||||
/// <summary>
|
||||
/// An existing retained scene recorded through <see cref="DrawingCanvas.RenderScene"/>.
|
||||
/// </summary>
|
||||
Scene
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents one ordered item in the canvas replay timeline.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Command ranges reference contiguous draw commands; they are not backend scene objects yet.
|
||||
/// Apply barriers and retained scene references point into side buffers by index, keeping this
|
||||
/// type compact while preserving the exact order in which the canvas recorded replay work.
|
||||
/// </remarks>
|
||||
internal readonly struct DrawingCanvasTimelineEntry
|
||||
{
|
||||
private DrawingCanvasTimelineEntry(
|
||||
DrawingCanvasTimelineEntryKind kind,
|
||||
int index,
|
||||
int count,
|
||||
bool hasLayers)
|
||||
{
|
||||
this.Kind = kind;
|
||||
this.Index = index;
|
||||
this.Count = count;
|
||||
this.HasLayers = hasLayers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the kind of replay item represented by this entry.
|
||||
/// </summary>
|
||||
public DrawingCanvasTimelineEntryKind Kind { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the command start index for command ranges, or the side-buffer index for barriers and scenes.
|
||||
/// </summary>
|
||||
public int Index { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of commands represented by a command-range entry.
|
||||
/// </summary>
|
||||
public int Count { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the command range contains layer boundary commands.
|
||||
/// </summary>
|
||||
public bool HasLayers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a command-range entry.
|
||||
/// </summary>
|
||||
/// <param name="startIndex">The first command index.</param>
|
||||
/// <param name="count">The command count.</param>
|
||||
/// <param name="hasLayers">Indicates whether the command range contains layer boundary commands.</param>
|
||||
/// <returns>The command-range entry.</returns>
|
||||
public static DrawingCanvasTimelineEntry CreateCommandRange(int startIndex, int count, bool hasLayers)
|
||||
=> new(DrawingCanvasTimelineEntryKind.CommandRange, startIndex, count, hasLayers);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an apply-barrier entry.
|
||||
/// </summary>
|
||||
/// <param name="index">The apply-barrier index.</param>
|
||||
/// <returns>The apply-barrier entry.</returns>
|
||||
public static DrawingCanvasTimelineEntry CreateApplyBarrier(int index)
|
||||
=> new(DrawingCanvasTimelineEntryKind.ApplyBarrier, index, 0, false);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an entry for an existing retained scene recorded through <see cref="DrawingCanvas.RenderScene"/>.
|
||||
/// </summary>
|
||||
/// <param name="index">The retained-scene reference index.</param>
|
||||
/// <returns>The retained-scene entry.</returns>
|
||||
public static DrawingCanvasTimelineEntry CreateScene(int index)
|
||||
=> new(DrawingCanvasTimelineEntryKind.Scene, index, 0, false);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
internal static class DrawingHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert a <see cref="DenseMatrix{Color}"/> to a <see cref="DenseMatrix{T}"/> of the given pixel type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The type of pixel format.</typeparam>
|
||||
/// <param name="colorMatrix">The color matrix.</param>
|
||||
public static DenseMatrix<TPixel> ToPixelMatrix<TPixel>(this DenseMatrix<Color> colorMatrix)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
DenseMatrix<TPixel> result = new(colorMatrix.Columns, colorMatrix.Rows);
|
||||
Color.ToPixel(colorMatrix.Span, result.Span);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
internal enum DrawingOperationKind : byte
|
||||
{
|
||||
Fill = 0,
|
||||
Draw = 1
|
||||
}
|
||||
|
||||
internal struct DrawingOperation
|
||||
{
|
||||
public DrawingOperationKind Kind { get; set; }
|
||||
|
||||
public IPath Path { get; set; }
|
||||
|
||||
public Point RenderLocation { get; set; }
|
||||
|
||||
public IntersectionRule IntersectionRule { get; set; }
|
||||
|
||||
public byte RenderPass { get; set; }
|
||||
|
||||
public Brush? Brush { get; set; }
|
||||
|
||||
public Pen? Pen { get; set; }
|
||||
|
||||
public PixelAlphaCompositionMode PixelAlphaCompositionMode { get; set; }
|
||||
|
||||
public PixelColorBlendingMode PixelColorBlendingMode { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Provides options for influencing drawing operations, combining graphics rendering settings,
|
||||
/// shape fill-rule behavior, and an optional coordinate transform.
|
||||
/// </summary>
|
||||
public class DrawingOptions
|
||||
{
|
||||
private GraphicsOptions graphicsOptions;
|
||||
private ShapeOptions shapeOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DrawingOptions"/> class.
|
||||
/// </summary>
|
||||
public DrawingOptions()
|
||||
{
|
||||
this.graphicsOptions = new GraphicsOptions();
|
||||
this.shapeOptions = new ShapeOptions();
|
||||
this.Transform = Matrix4x4.Identity;
|
||||
}
|
||||
|
||||
internal DrawingOptions(
|
||||
GraphicsOptions graphicsOptions,
|
||||
ShapeOptions shapeOptions,
|
||||
Matrix4x4 transform)
|
||||
{
|
||||
DebugGuard.NotNull(graphicsOptions, nameof(graphicsOptions));
|
||||
DebugGuard.NotNull(shapeOptions, nameof(shapeOptions));
|
||||
|
||||
this.graphicsOptions = graphicsOptions;
|
||||
this.shapeOptions = shapeOptions;
|
||||
this.Transform = transform;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the graphics rendering options that control antialiasing, blending, alpha composition,
|
||||
/// and coverage thresholding for the drawing operation.
|
||||
/// </summary>
|
||||
public GraphicsOptions GraphicsOptions
|
||||
{
|
||||
get => this.graphicsOptions;
|
||||
set
|
||||
{
|
||||
Guard.NotNull(value, nameof(this.GraphicsOptions));
|
||||
this.graphicsOptions = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the shape options that control fill-rule intersection mode and boolean clipping behavior.
|
||||
/// </summary>
|
||||
public ShapeOptions ShapeOptions
|
||||
{
|
||||
get => this.shapeOptions;
|
||||
set
|
||||
{
|
||||
Guard.NotNull(value, nameof(this.ShapeOptions));
|
||||
this.shapeOptions = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transform matrix applied to vector output before rasterization.
|
||||
/// For strokes, the pen is expanded in local geometry space and the resulting outline is transformed before rasterization.
|
||||
/// Defaults to <see cref="Matrix4x4.Identity"/>.
|
||||
/// </summary>
|
||||
public Matrix4x4 Transform { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Adds extensions that help working with <see cref="DrawingOptions" />.
|
||||
/// </summary>
|
||||
public static class DrawingOptionsDefaultsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default drawing options against the source image processing context.
|
||||
/// </summary>
|
||||
/// <param name="context">The image processing context to retrieve defaults from.</param>
|
||||
/// <returns>The globally configured default options.</returns>
|
||||
public static DrawingOptions GetDrawingOptions(this IImageProcessingContext context)
|
||||
=> new(context.GetGraphicsOptions(), new ShapeOptions(), Matrix4x4.Identity);
|
||||
|
||||
/// <summary>
|
||||
/// Clones the path graphic options and applies changes required to force clearing.
|
||||
/// </summary>
|
||||
/// <param name="drawingOptions">The drawing options to clone</param>
|
||||
/// <returns>A clone of shapeOptions with ColorBlendingMode, AlphaCompositionMode, and BlendPercentage set</returns>
|
||||
internal static DrawingOptions CloneForClearOperation(this DrawingOptions drawingOptions)
|
||||
{
|
||||
GraphicsOptions options = drawingOptions.GraphicsOptions.DeepClone();
|
||||
options.ColorBlendingMode = PixelColorBlendingMode.Normal;
|
||||
options.AlphaCompositionMode = PixelAlphaCompositionMode.Src;
|
||||
options.BlendPercentage = 1F;
|
||||
|
||||
return new DrawingOptions(options, drawingOptions.ShapeOptions, drawingOptions.Transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Provides an implementation of a brush for painting elliptical gradients.
|
||||
/// The ellipse is defined by a center point, a point on the longest axis, and the ratio
|
||||
/// between the longest and shortest axes.
|
||||
/// </summary>
|
||||
public sealed class EllipticGradientBrush : GradientBrush
|
||||
{
|
||||
/// <inheritdoc cref="GradientBrush" />
|
||||
/// <param name="center">The center of the elliptical gradient and 0 for the color stops.</param>
|
||||
/// <param name="referenceAxisEnd">The end point of the reference axis of the ellipse.</param>
|
||||
/// <param name="axisRatio">
|
||||
/// The ratio of the axis widths.
|
||||
/// The second axis is perpendicular to the reference axis and its length is the reference axis length
|
||||
/// multiplied by this factor.
|
||||
/// </param>
|
||||
/// <param name="repetitionMode">Defines how the colors of the gradients are repeated.</param>
|
||||
/// <param name="colorStops">The color stops.</param>
|
||||
public EllipticGradientBrush(
|
||||
PointF center,
|
||||
PointF referenceAxisEnd,
|
||||
float axisRatio,
|
||||
GradientRepetitionMode repetitionMode,
|
||||
params ColorStop[] colorStops)
|
||||
: base(repetitionMode, colorStops)
|
||||
{
|
||||
this.Center = center;
|
||||
this.ReferenceAxisEnd = referenceAxisEnd;
|
||||
this.AxisRatio = axisRatio;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the center of the ellipse.
|
||||
/// </summary>
|
||||
public PointF Center { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the end point of the reference axis.
|
||||
/// </summary>
|
||||
public PointF ReferenceAxisEnd { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ratio of the secondary axis to the primary axis.
|
||||
/// </summary>
|
||||
public float AxisRatio { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Brush Transform(Matrix4x4 matrix)
|
||||
{
|
||||
PointF tc = PointF.Transform(this.Center, matrix);
|
||||
PointF tRef = PointF.Transform(this.ReferenceAxisEnd, matrix);
|
||||
|
||||
// Compute a point on the perpendicular (secondary) axis and transform it.
|
||||
float refDx = this.ReferenceAxisEnd.X - this.Center.X;
|
||||
float refDy = this.ReferenceAxisEnd.Y - this.Center.Y;
|
||||
float refLen = MathF.Sqrt((refDx * refDx) + (refDy * refDy));
|
||||
float secondLen = refLen * this.AxisRatio;
|
||||
|
||||
// Perpendicular direction (rotated 90 degrees).
|
||||
PointF secondEnd = new(
|
||||
this.Center.X + (-refDy / refLen * secondLen),
|
||||
this.Center.Y + (refDx / refLen * secondLen));
|
||||
PointF tSec = PointF.Transform(secondEnd, matrix);
|
||||
|
||||
// Derive new ratio from transformed lengths.
|
||||
float newRefLen = MathF.Sqrt(
|
||||
((tRef.X - tc.X) * (tRef.X - tc.X)) + ((tRef.Y - tc.Y) * (tRef.Y - tc.Y)));
|
||||
float newSecLen = MathF.Sqrt(
|
||||
((tSec.X - tc.X) * (tSec.X - tc.X)) + ((tSec.Y - tc.Y) * (tSec.Y - tc.Y)));
|
||||
float newRatio = newRefLen > 0f ? newSecLen / newRefLen : this.AxisRatio;
|
||||
|
||||
return new EllipticGradientBrush(tc, tRef, newRatio, this.RepetitionMode, this.ColorStopsArray);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override BrushRenderer<TPixel> CreateRenderer<TPixel>(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
RectangleF region) =>
|
||||
new EllipticGradientBrushRenderer<TPixel>(
|
||||
configuration,
|
||||
options,
|
||||
canvasWidth,
|
||||
this,
|
||||
this.ColorStopsArray,
|
||||
this.RepetitionMode);
|
||||
|
||||
/// <inheritdoc />
|
||||
private sealed class EllipticGradientBrushRenderer<TPixel> : GradientBrushRenderer<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly PointF center;
|
||||
|
||||
private readonly float cosRotation;
|
||||
|
||||
private readonly float sinRotation;
|
||||
|
||||
private readonly float referenceRadiusSquared;
|
||||
|
||||
private readonly float secondRadiusSquared;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EllipticGradientBrushRenderer{TPixel}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration instance to use when performing operations.</param>
|
||||
/// <param name="options">The graphics options.</param>
|
||||
/// <param name="canvasWidth">The canvas width for the current render pass.</param>
|
||||
/// <param name="brush">The elliptic gradient brush.</param>
|
||||
/// <param name="colorStops">Definition of colors.</param>
|
||||
/// <param name="repetitionMode">Defines how the gradient colors are repeated.</param>
|
||||
public EllipticGradientBrushRenderer(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
EllipticGradientBrush brush,
|
||||
ColorStop[] colorStops,
|
||||
GradientRepetitionMode repetitionMode)
|
||||
: base(configuration, options, canvasWidth, colorStops, repetitionMode)
|
||||
{
|
||||
this.center = brush.Center;
|
||||
|
||||
float refDx = brush.ReferenceAxisEnd.X - brush.Center.X;
|
||||
float refDy = brush.ReferenceAxisEnd.Y - brush.Center.Y;
|
||||
float rotation = MathF.Atan2(refDy, refDx);
|
||||
float referenceRadius = MathF.Sqrt((refDx * refDx) + (refDy * refDy));
|
||||
float secondRadius = referenceRadius * brush.AxisRatio;
|
||||
|
||||
this.referenceRadiusSquared = referenceRadius * referenceRadius;
|
||||
this.secondRadiusSquared = secondRadius * secondRadius;
|
||||
this.sinRotation = MathF.Sin(rotation);
|
||||
this.cosRotation = MathF.Cos(rotation);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override float PositionOnGradient(float x, float y)
|
||||
{
|
||||
float x0 = x - this.center.X;
|
||||
float y0 = y - this.center.Y;
|
||||
|
||||
float xR = (x0 * this.cosRotation) - (y0 * this.sinRotation);
|
||||
float yR = (x0 * this.sinRotation) + (y0 * this.cosRotation);
|
||||
|
||||
float xSquared = xR * xR;
|
||||
float ySquared = yR * yR;
|
||||
|
||||
return MathF.Sqrt((xSquared / this.referenceRadiusSquared) + (ySquared / this.secondRadiusSquared));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Base class for Gradient brushes
|
||||
/// </summary>
|
||||
public abstract class GradientBrush : Brush
|
||||
{
|
||||
/// <inheritdoc cref="Brush"/>
|
||||
/// <param name="repetitionMode">Defines how the colors are repeated beyond the interval [0..1]</param>
|
||||
/// <param name="colorStops">The gradient colors.</param>
|
||||
protected GradientBrush(GradientRepetitionMode repetitionMode, params ColorStop[] colorStops)
|
||||
{
|
||||
this.RepetitionMode = repetitionMode;
|
||||
|
||||
InsertionSort(colorStops, (a, b) => a.Ratio.CompareTo(b.Ratio));
|
||||
this.ColorStopsArray = colorStops;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets how the colors are repeated beyond the interval [0..1].
|
||||
/// </summary>
|
||||
public GradientRepetitionMode RepetitionMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the color stops for this gradient.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<ColorStop> ColorStops => this.ColorStopsArray;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the color stops array for use by derived applicators.
|
||||
/// </summary>
|
||||
protected ColorStop[] ColorStopsArray { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(Brush? other)
|
||||
{
|
||||
if (other is GradientBrush brush)
|
||||
{
|
||||
return this.RepetitionMode == brush.RepetitionMode
|
||||
&& this.ColorStopsArray?.SequenceEqual(brush.ColorStopsArray) == true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
=> HashCode.Combine(this.RepetitionMode, this.ColorStopsArray);
|
||||
|
||||
/// <summary>
|
||||
/// Sorts the collection in place using a stable insertion sort.
|
||||
/// <see cref="Array.Sort{T}(T[], Comparison{T})"/> is not stable and can reorder
|
||||
/// equal-ratio color stops, producing non-deterministic gradient results.
|
||||
/// </summary>
|
||||
private static void InsertionSort<T>(T[] collection, Comparison<T> comparison)
|
||||
{
|
||||
int count = collection.Length;
|
||||
for (int j = 1; j < count; j++)
|
||||
{
|
||||
T key = collection[j];
|
||||
|
||||
int i = j - 1;
|
||||
for (; i >= 0 && comparison(collection[i], key) > 0; i--)
|
||||
{
|
||||
collection[i + 1] = collection[i];
|
||||
}
|
||||
|
||||
collection[i + 1] = key;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for gradient brush applicators
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
internal abstract class GradientBrushRenderer<TPixel> : BrushRenderer<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private static readonly TPixel Transparent = Color.Transparent.ToPixel<TPixel>();
|
||||
|
||||
private readonly ColorStop[] colorStops;
|
||||
|
||||
private readonly GradientRepetitionMode repetitionMode;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GradientBrushRenderer{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration instance to use when performing operations.</param>
|
||||
/// <param name="options">The graphics options.</param>
|
||||
/// <param name="canvasWidth">The canvas width for the current render pass.</param>
|
||||
/// <param name="colorStops">An array of color stops sorted by their position.</param>
|
||||
/// <param name="repetitionMode">Defines if and how the gradient should be repeated.</param>
|
||||
protected GradientBrushRenderer(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
ColorStop[] colorStops,
|
||||
GradientRepetitionMode repetitionMode)
|
||||
: base(configuration, options, canvasWidth)
|
||||
{
|
||||
this.colorStops = colorStops;
|
||||
this.repetitionMode = repetitionMode;
|
||||
}
|
||||
|
||||
internal TPixel this[int x, int y]
|
||||
{
|
||||
get
|
||||
{
|
||||
float fx = x + 0.5f;
|
||||
float fy = y + 0.5f;
|
||||
|
||||
float positionOnCompleteGradient = this.PositionOnGradient(fx, fy);
|
||||
if (float.IsNaN(positionOnCompleteGradient))
|
||||
{
|
||||
return Transparent;
|
||||
}
|
||||
|
||||
switch (this.repetitionMode)
|
||||
{
|
||||
case GradientRepetitionMode.Repeat:
|
||||
positionOnCompleteGradient %= 1;
|
||||
break;
|
||||
case GradientRepetitionMode.Reflect:
|
||||
positionOnCompleteGradient %= 2;
|
||||
if (positionOnCompleteGradient > 1)
|
||||
{
|
||||
positionOnCompleteGradient = 2 - positionOnCompleteGradient;
|
||||
}
|
||||
|
||||
break;
|
||||
case GradientRepetitionMode.DontFill:
|
||||
if (positionOnCompleteGradient is > 1 or < 0)
|
||||
{
|
||||
return Transparent;
|
||||
}
|
||||
|
||||
break;
|
||||
case GradientRepetitionMode.None:
|
||||
default:
|
||||
// do nothing. The following could be done, but is not necessary:
|
||||
// onLocalGradient = Math.Min(0, Math.Max(1, onLocalGradient));
|
||||
break;
|
||||
}
|
||||
|
||||
(ColorStop from, ColorStop to) = this.GetGradientSegment(positionOnCompleteGradient);
|
||||
|
||||
if (from.Color.Equals(to.Color))
|
||||
{
|
||||
return from.Color.ToPixel<TPixel>();
|
||||
}
|
||||
|
||||
float onLocalGradient = (positionOnCompleteGradient - from.Ratio) / (to.Ratio - from.Ratio);
|
||||
|
||||
// TODO: This should use premultiplied vectors to avoid bad blends e.g. red -> brown <- green.
|
||||
return Color.FromScaledVector(
|
||||
Vector4.Lerp(
|
||||
from.Color.ToScaledVector4(),
|
||||
to.Color.ToScaledVector4(),
|
||||
onLocalGradient)).ToPixel<TPixel>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Apply(
|
||||
Span<TPixel> destinationRow,
|
||||
ReadOnlySpan<float> scanline,
|
||||
int x,
|
||||
int y,
|
||||
BrushWorkspace<TPixel> workspace)
|
||||
{
|
||||
Span<float> amounts = workspace.GetAmounts(scanline.Length);
|
||||
Span<TPixel> overlays = workspace.GetOverlays(scanline.Length);
|
||||
float blendPercentage = this.Options.BlendPercentage;
|
||||
|
||||
// TODO: Remove bounds checks.
|
||||
if (blendPercentage < 1)
|
||||
{
|
||||
for (int i = 0; i < scanline.Length; i++)
|
||||
{
|
||||
amounts[i] = scanline[i] * blendPercentage;
|
||||
overlays[i] = this[x + i, y];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < scanline.Length; i++)
|
||||
{
|
||||
amounts[i] = scanline[i];
|
||||
overlays[i] = this[x + i, y];
|
||||
}
|
||||
}
|
||||
|
||||
this.Blender.Blend(
|
||||
this.Configuration,
|
||||
destinationRow,
|
||||
destinationRow,
|
||||
overlays,
|
||||
amounts,
|
||||
workspace.GetBlendScratch(scanline.Length, 3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the position on the gradient for a given point.
|
||||
/// This method is abstract as it's content depends on the shape of the gradient.
|
||||
/// </summary>
|
||||
/// <param name="x">The x-coordinate of the point.</param>
|
||||
/// <param name="y">The y-coordinate of the point.</param>
|
||||
/// <returns>
|
||||
/// The position the given point has on the gradient.
|
||||
/// The position is not bound to the [0..1] interval.
|
||||
/// Values outside of that interval may be treated differently,
|
||||
/// e.g. for the <see cref="GradientRepetitionMode" /> enum.
|
||||
/// </returns>
|
||||
protected abstract float PositionOnGradient(float x, float y);
|
||||
|
||||
private (ColorStop From, ColorStop To) GetGradientSegment(float positionOnCompleteGradient)
|
||||
{
|
||||
ColorStop localGradientFrom = this.colorStops[0];
|
||||
ColorStop localGradientTo = default;
|
||||
|
||||
foreach (ColorStop colorStop in this.colorStops)
|
||||
{
|
||||
localGradientTo = colorStop;
|
||||
|
||||
if (colorStop.Ratio > positionOnCompleteGradient)
|
||||
{
|
||||
// we're done here, so break it!
|
||||
break;
|
||||
}
|
||||
|
||||
localGradientFrom = localGradientTo;
|
||||
}
|
||||
|
||||
return (localGradientFrom, localGradientTo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Modes to repeat a gradient.
|
||||
/// </summary>
|
||||
public enum GradientRepetitionMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Don't repeat, keep the color of start and end beyond those points stable.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Repeat the gradient.
|
||||
/// If it's a black-white gradient, with Repeat it will be Black->{gray}->White|Black->{gray}->White|...
|
||||
/// </summary>
|
||||
Repeat,
|
||||
|
||||
/// <summary>
|
||||
/// Reflect the gradient.
|
||||
/// Similar to <see cref="Repeat"/>, but each other repetition uses inverse order of <see cref="ColorStop"/>s.
|
||||
/// Used on a Black-White gradient, Reflect leads to Black->{gray}->White->{gray}->White...
|
||||
/// </summary>
|
||||
Reflect,
|
||||
|
||||
/// <summary>
|
||||
/// With DontFill a gradient does not touch any pixel beyond it's borders.
|
||||
/// For the <see cref="LinearGradientBrush"/> this is beyond the orthogonal through start and end,
|
||||
/// For <see cref="RadialGradientBrush" /> and <see cref="EllipticGradientBrush" /> it's beyond 1.0.
|
||||
/// </summary>
|
||||
DontFill
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Provides an implementation of an image brush for painting images within areas.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format of the source image.</typeparam>
|
||||
public sealed class ImageBrush<TPixel> : ImageBrush
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageBrush{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="image">The source image to draw.</param>
|
||||
public ImageBrush(Image<TPixel> image)
|
||||
: base(image)
|
||||
=> this.SourceImage = image;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageBrush{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="image">The source image to draw.</param>
|
||||
/// <param name="offset">An offset to apply to the image while drawing the texture.</param>
|
||||
public ImageBrush(Image<TPixel> image, Point offset)
|
||||
: base(image, offset)
|
||||
=> this.SourceImage = image;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageBrush{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="image">The source image to draw.</param>
|
||||
/// <param name="region">The region of interest within the source image.</param>
|
||||
public ImageBrush(Image<TPixel> image, RectangleF region)
|
||||
: base(image, region)
|
||||
=> this.SourceImage = image;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageBrush{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="image">The source image to draw.</param>
|
||||
/// <param name="region">The region of interest within the source image.</param>
|
||||
/// <param name="offset">An offset to apply to the image while drawing the texture.</param>
|
||||
public ImageBrush(Image<TPixel> image, RectangleF region, Point offset)
|
||||
: base(image, region, offset)
|
||||
=> this.SourceImage = image;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the typed source image used by this brush.
|
||||
/// </summary>
|
||||
public Image<TPixel> SourceImage { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The untyped base class for image brushes, used to support non-generic brush references in drawing contexts.
|
||||
/// </summary>
|
||||
public abstract class ImageBrush : Brush
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageBrush"/> class.
|
||||
/// </summary>
|
||||
/// <param name="image">The source image to draw.</param>
|
||||
protected ImageBrush(Image image)
|
||||
: this(image, image.Bounds)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageBrush"/> class.
|
||||
/// </summary>
|
||||
/// <param name="image">The image.</param>
|
||||
/// <param name="offset">
|
||||
/// An offset to apply the to image image while drawing apply the texture.
|
||||
/// </param>
|
||||
protected ImageBrush(Image image, Point offset)
|
||||
: this(image, image.Bounds, offset)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageBrush"/> class.
|
||||
/// </summary>
|
||||
/// <param name="image">The image.</param>
|
||||
/// <param name="region">
|
||||
/// The region of interest.
|
||||
/// This overrides any region used to initialize the brush applicator.
|
||||
/// </param>
|
||||
protected ImageBrush(Image image, RectangleF region)
|
||||
: this(image, region, Point.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageBrush"/> class.
|
||||
/// </summary>
|
||||
/// <param name="image">The image.</param>
|
||||
/// <param name="region">
|
||||
/// The region of interest.
|
||||
/// This overrides any region used to initialize the brush applicator.
|
||||
/// </param>
|
||||
/// <param name="offset">
|
||||
/// An offset to apply the to image image while drawing apply the texture.
|
||||
/// </param>
|
||||
protected ImageBrush(Image image, RectangleF region, Point offset)
|
||||
{
|
||||
this.UntypedImage = image;
|
||||
this.SourceRegion = RectangleF.Intersect(image.Bounds, region);
|
||||
this.Offset = offset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source image used by this brush.
|
||||
/// </summary>
|
||||
public Image UntypedImage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source region within the image.
|
||||
/// </summary>
|
||||
public RectangleF SourceRegion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the offset applied to the brush origin.
|
||||
/// </summary>
|
||||
public Point Offset { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(Brush? other)
|
||||
{
|
||||
if (other is ImageBrush ib)
|
||||
{
|
||||
return ib.UntypedImage == this.UntypedImage && ib.SourceRegion == this.SourceRegion;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => HashCode.Combine(this.UntypedImage, this.SourceRegion);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override BrushRenderer<TPixel> CreateRenderer<TPixel>(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
RectangleF region)
|
||||
{
|
||||
if (this.UntypedImage is Image<TPixel> image)
|
||||
{
|
||||
return new ImageBrushRenderer<TPixel>(configuration, options, canvasWidth, image, region, this.SourceRegion, this.Offset);
|
||||
}
|
||||
|
||||
// This will never be hit as the brush is always normalized by the drawing canvas
|
||||
// but we do it to satisfy the type system.
|
||||
ThrowIfInvalidImagePixelFormat();
|
||||
return null;
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowIfInvalidImagePixelFormat()
|
||||
=> throw new UnreachableException("The pixel format of the image is not supported by this brush renderer");
|
||||
|
||||
/// <summary>
|
||||
/// The image brush applicator.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
private sealed class ImageBrushRenderer<TPixel> : BrushRenderer<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly ImageFrame<TPixel> sourceFrame;
|
||||
|
||||
/// <summary>
|
||||
/// The region of the source image we will be using to draw from.
|
||||
/// </summary>
|
||||
private readonly Rectangle sourceRegion;
|
||||
|
||||
/// <summary>
|
||||
/// The Y offset.
|
||||
/// </summary>
|
||||
private readonly int offsetY;
|
||||
|
||||
/// <summary>
|
||||
/// The X offset.
|
||||
/// </summary>
|
||||
private readonly int offsetX;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageBrushRenderer{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration instance to use when performing operations.</param>
|
||||
/// <param name="options">The graphics options.</param>
|
||||
/// <param name="canvasWidth">The canvas width for the current render pass.</param>
|
||||
/// <param name="image">The image.</param>
|
||||
/// <param name="targetRegion">The region of the target image we will be drawing to.</param>
|
||||
/// <param name="sourceRegion">The region of the source image we will be using to source pixels to draw from.</param>
|
||||
/// <param name="offset">An offset to apply to the texture while drawing.</param>
|
||||
public ImageBrushRenderer(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
Image<TPixel> image,
|
||||
RectangleF targetRegion,
|
||||
RectangleF sourceRegion,
|
||||
Point offset)
|
||||
: base(configuration, options, canvasWidth)
|
||||
{
|
||||
this.sourceFrame = image.Frames.RootFrame;
|
||||
this.sourceRegion = Rectangle.Intersect(image.Bounds, (Rectangle)sourceRegion);
|
||||
|
||||
this.offsetY = (int)MathF.Floor(targetRegion.Top) + offset.Y;
|
||||
this.offsetX = (int)MathF.Floor(targetRegion.Left) + offset.X;
|
||||
}
|
||||
|
||||
internal TPixel this[int x, int y]
|
||||
{
|
||||
get
|
||||
{
|
||||
int srcX = ((x - this.offsetX) % this.sourceRegion.Width) + this.sourceRegion.X;
|
||||
int srcY = ((y - this.offsetY) % this.sourceRegion.Height) + this.sourceRegion.Y;
|
||||
return this.sourceFrame[srcX, srcY];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Apply(
|
||||
Span<TPixel> destinationRow,
|
||||
ReadOnlySpan<float> scanline,
|
||||
int x,
|
||||
int y,
|
||||
BrushWorkspace<TPixel> workspace)
|
||||
{
|
||||
Span<float> amountSpan = workspace.GetAmounts(scanline.Length);
|
||||
Span<TPixel> overlaySpan = workspace.GetOverlays(scanline.Length);
|
||||
|
||||
int offsetX = x - this.offsetX;
|
||||
int sourceY = ((((y - this.offsetY) % this.sourceRegion.Height) // clamp the number between -height and +height
|
||||
+ this.sourceRegion.Height) % this.sourceRegion.Height) // clamp the number between 0 and +height
|
||||
+ this.sourceRegion.Y;
|
||||
Span<TPixel> sourceRow = this.sourceFrame.PixelBuffer.DangerousGetRowSpan(sourceY);
|
||||
|
||||
for (int i = 0; i < scanline.Length; i++)
|
||||
{
|
||||
amountSpan[i] = scanline[i] * this.Options.BlendPercentage;
|
||||
|
||||
int sourceX = ((((i + offsetX) % this.sourceRegion.Width) // clamp the number between -width and +width
|
||||
+ this.sourceRegion.Width) % this.sourceRegion.Width) // clamp the number between 0 and +width
|
||||
+ this.sourceRegion.X;
|
||||
|
||||
overlaySpan[i] = sourceRow[sourceX];
|
||||
}
|
||||
|
||||
this.Blender.Blend(
|
||||
this.Configuration,
|
||||
destinationRow,
|
||||
destinationRow,
|
||||
overlaySpan,
|
||||
amountSpan,
|
||||
workspace.GetBlendScratch(scanline.Length, 3));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Provides a brush that paints linear gradients within an area.
|
||||
/// Supports both classic two-point gradients and three-point (rotated) gradients.
|
||||
/// </summary>
|
||||
public sealed class LinearGradientBrush : GradientBrush
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinearGradientBrush"/> class using
|
||||
/// a start and end point.
|
||||
/// </summary>
|
||||
/// <param name="p0">The start point of the gradient.</param>
|
||||
/// <param name="p1">The end point of the gradient.</param>
|
||||
/// <param name="repetitionMode">Defines how the colors are repeated.</param>
|
||||
/// <param name="colorStops">The ordered color stops of the gradient.</param>
|
||||
public LinearGradientBrush(
|
||||
PointF p0,
|
||||
PointF p1,
|
||||
GradientRepetitionMode repetitionMode,
|
||||
params ColorStop[] colorStops)
|
||||
: base(repetitionMode, colorStops)
|
||||
{
|
||||
this.StartPoint = p0;
|
||||
this.EndPoint = p1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinearGradientBrush"/> class using
|
||||
/// three points to define a rotated gradient axis.
|
||||
/// </summary>
|
||||
/// <param name="p0">The first point (start of the gradient).</param>
|
||||
/// <param name="p1">The second point (gradient vector endpoint).</param>
|
||||
/// <param name="rotationPoint">
|
||||
/// The rotation reference point. This defines the rotation of the gradient axis.
|
||||
/// </param>
|
||||
/// <param name="repetitionMode">Defines how the colors are repeated.</param>
|
||||
/// <param name="colorStops">The ordered color stops of the gradient.</param>
|
||||
public LinearGradientBrush(
|
||||
PointF p0,
|
||||
PointF p1,
|
||||
PointF rotationPoint,
|
||||
GradientRepetitionMode repetitionMode,
|
||||
params ColorStop[] colorStops)
|
||||
: base(repetitionMode, colorStops)
|
||||
{
|
||||
ResolveAxis(p0, p1, rotationPoint, out PointF start, out PointF end);
|
||||
this.StartPoint = start;
|
||||
this.EndPoint = end;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the start point of the gradient axis.
|
||||
/// </summary>
|
||||
public PointF StartPoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the end point of the gradient axis.
|
||||
/// </summary>
|
||||
public PointF EndPoint { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Brush Transform(Matrix4x4 matrix)
|
||||
=> new LinearGradientBrush(
|
||||
PointF.Transform(this.StartPoint, matrix),
|
||||
PointF.Transform(this.EndPoint, matrix),
|
||||
this.RepetitionMode,
|
||||
this.ColorStopsArray);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(Brush? other)
|
||||
{
|
||||
if (other is LinearGradientBrush brush)
|
||||
{
|
||||
return base.Equals(other)
|
||||
&& this.StartPoint.Equals(brush.StartPoint)
|
||||
&& this.EndPoint.Equals(brush.EndPoint);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
=> HashCode.Combine(base.GetHashCode(), this.StartPoint, this.EndPoint);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a three-point gradient axis into a two-point axis by projecting
|
||||
/// the gradient vector (p0 to p1) onto the perpendicular of the rotation vector (p0 to rotationPoint).
|
||||
/// This follows the COLRv1 font specification for rotated linear gradients.
|
||||
/// </summary>
|
||||
/// <param name="p0">The gradient start point.</param>
|
||||
/// <param name="p1">The gradient vector endpoint.</param>
|
||||
/// <param name="rotationPoint">The rotation reference point.</param>
|
||||
/// <param name="start">The resolved start point of the gradient axis.</param>
|
||||
/// <param name="end">The resolved end point of the gradient axis.</param>
|
||||
private static void ResolveAxis(PointF p0, PointF p1, PointF rotationPoint, out PointF start, out PointF end)
|
||||
{
|
||||
// Gradient vector from p0 to p1.
|
||||
float vx = p1.X - p0.X;
|
||||
float vy = p1.Y - p0.Y;
|
||||
|
||||
// Rotation vector from p0 to rotation point.
|
||||
float rx = rotationPoint.X - p0.X;
|
||||
float ry = rotationPoint.Y - p0.Y;
|
||||
|
||||
// Perpendicular to the rotation vector.
|
||||
float nx = ry;
|
||||
float ny = -rx;
|
||||
|
||||
float ndotn = (nx * nx) + (ny * ny);
|
||||
if (ndotn == 0f)
|
||||
{
|
||||
// Degenerate: p0 == rotationPoint, fall back to original axis.
|
||||
start = p0;
|
||||
end = p1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Project the gradient vector onto the perpendicular direction.
|
||||
float vdotn = (vx * nx) + (vy * ny);
|
||||
float scale = vdotn / ndotn;
|
||||
start = p0;
|
||||
end = new PointF(p0.X + (scale * nx), p0.Y + (scale * ny));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override BrushRenderer<TPixel> CreateRenderer<TPixel>(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
RectangleF region)
|
||||
=> new LinearGradientBrushRenderer<TPixel>(
|
||||
configuration,
|
||||
options,
|
||||
canvasWidth,
|
||||
this,
|
||||
this.ColorStopsArray,
|
||||
this.RepetitionMode);
|
||||
|
||||
/// <summary>
|
||||
/// Implements the gradient application logic for <see cref="LinearGradientBrush"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
private sealed class LinearGradientBrushRenderer<TPixel> : GradientBrushRenderer<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly PointF start;
|
||||
private readonly float alongX;
|
||||
private readonly float alongY;
|
||||
private readonly float alongsSquared;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LinearGradientBrushRenderer{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The ImageSharp configuration.</param>
|
||||
/// <param name="options">The graphics options.</param>
|
||||
/// <param name="canvasWidth">The canvas width for the current render pass.</param>
|
||||
/// <param name="brush">The linear gradient brush.</param>
|
||||
/// <param name="colorStops">The gradient color stops.</param>
|
||||
/// <param name="repetitionMode">Defines how the gradient repeats.</param>
|
||||
public LinearGradientBrushRenderer(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
LinearGradientBrush brush,
|
||||
ColorStop[] colorStops,
|
||||
GradientRepetitionMode repetitionMode)
|
||||
: base(configuration, options, canvasWidth, colorStops, repetitionMode)
|
||||
{
|
||||
this.start = brush.StartPoint;
|
||||
|
||||
this.alongX = brush.EndPoint.X - this.start.X;
|
||||
this.alongY = brush.EndPoint.Y - this.start.Y;
|
||||
this.alongsSquared = (this.alongX * this.alongX) + (this.alongY * this.alongY);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override float PositionOnGradient(float x, float y)
|
||||
{
|
||||
if (this.alongsSquared == 0f)
|
||||
{
|
||||
return 1f;
|
||||
}
|
||||
|
||||
float deltaX = x - this.start.X;
|
||||
float deltaY = y - this.start.Y;
|
||||
return ((deltaX * this.alongX) + (deltaY * this.alongY)) / this.alongsSquared;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Represents the per-frame painting callback executed by <see cref="PaintExtensions.Paint(IImageProcessingContext, CanvasAction)"/>.
|
||||
/// </summary>
|
||||
/// <param name="canvas">The drawing canvas for the current image frame.</param>
|
||||
public delegate void CanvasAction(DrawingCanvas canvas);
|
||||
|
||||
/// <summary>
|
||||
/// Adds image-processing extensions that paint each frame through <see cref="DrawingCanvas"/>.
|
||||
/// </summary>
|
||||
public static class PaintExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Paints each image frame using drawing options from the current context.
|
||||
/// </summary>
|
||||
/// <param name="source">The image processing context to paint.</param>
|
||||
/// <param name="action">The per-frame painting callback.</param>
|
||||
/// <returns>The <see cref="IImageProcessingContext"/> so additional processing operations can be chained.</returns>
|
||||
public static IImageProcessingContext Paint(
|
||||
this IImageProcessingContext source,
|
||||
CanvasAction action)
|
||||
=> source.Paint(source.GetDrawingOptions(), action);
|
||||
|
||||
/// <summary>
|
||||
/// Paints each image frame using the supplied drawing options.
|
||||
/// </summary>
|
||||
/// <param name="source">The image processing context to paint.</param>
|
||||
/// <param name="options">The drawing options applied when creating each frame canvas.</param>
|
||||
/// <param name="action">The per-frame painting callback.</param>
|
||||
/// <returns>The <see cref="IImageProcessingContext"/> so additional processing operations can be chained.</returns>
|
||||
public static IImageProcessingContext Paint(
|
||||
this IImageProcessingContext source,
|
||||
DrawingOptions options,
|
||||
CanvasAction action)
|
||||
{
|
||||
Guard.NotNull(options, nameof(options));
|
||||
Guard.NotNull(action, nameof(action));
|
||||
|
||||
return source.ApplyProcessor(new PaintProcessor(options, action));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing.Processors;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Defines the image processor used by <see cref="PaintExtensions.Paint(IImageProcessingContext, DrawingOptions, CanvasAction)"/>
|
||||
/// to execute a canvas callback for each image frame.
|
||||
/// </summary>
|
||||
public sealed class PaintProcessor : IImageProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PaintProcessor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="options">The drawing options used when creating each frame canvas.</param>
|
||||
/// <param name="action">The per-frame painting callback.</param>
|
||||
public PaintProcessor(DrawingOptions options, CanvasAction action)
|
||||
{
|
||||
Guard.NotNull(options, nameof(options));
|
||||
Guard.NotNull(action, nameof(action));
|
||||
|
||||
this.Options = options;
|
||||
this.Action = action;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the drawing options used when creating each frame canvas.
|
||||
/// </summary>
|
||||
public DrawingOptions Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the per-frame painting callback.
|
||||
/// </summary>
|
||||
internal CanvasAction Action { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IImageProcessor<TPixel> CreatePixelSpecificProcessor<TPixel>(
|
||||
Configuration configuration,
|
||||
Image<TPixel> source,
|
||||
Rectangle sourceRectangle)
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
=> new PaintProcessor<TPixel>(configuration, this, source, sourceRectangle);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing.Processors;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Executes the <see cref="PaintProcessor"/> callback for a specific pixel type by creating a
|
||||
/// <see cref="DrawingCanvas"/> over each frame.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
internal sealed class PaintProcessor<TPixel> : ImageProcessor<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly PaintProcessor definition;
|
||||
private readonly CanvasAction action;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PaintProcessor{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The processing configuration.</param>
|
||||
/// <param name="definition">The non-generic processor definition that owns the drawing options and callback.</param>
|
||||
/// <param name="source">The source image.</param>
|
||||
/// <param name="sourceRectangle">The source bounds passed through the processing pipeline.</param>
|
||||
public PaintProcessor(
|
||||
Configuration configuration,
|
||||
PaintProcessor definition,
|
||||
Image<TPixel> source,
|
||||
Rectangle sourceRectangle)
|
||||
: base(configuration, source, sourceRectangle)
|
||||
{
|
||||
this.definition = definition;
|
||||
this.action = definition.Action;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnFrameApply(ImageFrame<TPixel> source)
|
||||
{
|
||||
using DrawingCanvas canvas = source.CreateCanvas(this.Configuration, this.definition.Options);
|
||||
this.action(canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using SixLabors.ImageSharp.Drawing.Helpers;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Provides an implementation of a brush for painting gradients between multiple color positions in 2D coordinates.
|
||||
/// </summary>
|
||||
public sealed class PathGradientBrush : Brush
|
||||
{
|
||||
private readonly PointF[] points;
|
||||
private readonly Color[] colors;
|
||||
private readonly Edge[] edges;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PathGradientBrush"/> class.
|
||||
/// </summary>
|
||||
/// <param name="points">Points that constitute a polygon that represents the gradient area.</param>
|
||||
/// <param name="colors">Array of colors that correspond to each point in the polygon.</param>
|
||||
public PathGradientBrush(PointF[] points, Color[] colors)
|
||||
{
|
||||
Guard.NotNull(points, nameof(points));
|
||||
Guard.MustBeGreaterThanOrEqualTo(points.Length, 3, nameof(points));
|
||||
Guard.NotNull(colors, nameof(colors));
|
||||
Guard.MustBeGreaterThan(colors.Length, 0, nameof(colors));
|
||||
|
||||
int size = points.Length;
|
||||
|
||||
this.points = [.. points];
|
||||
this.colors = [.. colors];
|
||||
this.edges = new Edge[this.points.Length];
|
||||
|
||||
for (int i = 0; i < this.points.Length; i++)
|
||||
{
|
||||
this.edges[i] = new Edge(this.points[i % size], this.points[(i + 1) % size], ColorAt(i), ColorAt(i + 1));
|
||||
}
|
||||
|
||||
this.CenterColor = CalculateCenterColor(this.colors);
|
||||
|
||||
Color ColorAt(int index) => this.colors[index % this.colors.Length];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PathGradientBrush"/> class.
|
||||
/// </summary>
|
||||
/// <param name="points">Points that constitute a polygon that represents the gradient area.</param>
|
||||
/// <param name="colors">Array of colors that correspond to each point in the polygon.</param>
|
||||
/// <param name="centerColor">Color at the center of the gradient area to which the other colors converge.</param>
|
||||
public PathGradientBrush(PointF[] points, Color[] colors, Color centerColor)
|
||||
: this(points, colors)
|
||||
{
|
||||
this.CenterColor = centerColor;
|
||||
this.HasExplicitCenterColor = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the polygon points that define the gradient area.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<PointF> Points => this.points;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the colors that are mapped to the polygon points.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<Color> Colors => this.colors;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the color at the center of the gradient area.
|
||||
/// </summary>
|
||||
public Color CenterColor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the center color was explicitly supplied.
|
||||
/// </summary>
|
||||
public bool HasExplicitCenterColor { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Brush Transform(Matrix4x4 matrix)
|
||||
{
|
||||
if (matrix.IsIdentity)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
PointF[] transformedPoints = new PointF[this.points.Length];
|
||||
for (int i = 0; i < transformedPoints.Length; i++)
|
||||
{
|
||||
transformedPoints[i] = PointF.Transform(this.points[i], matrix);
|
||||
}
|
||||
|
||||
return this.HasExplicitCenterColor
|
||||
? new PathGradientBrush(transformedPoints, this.colors, this.CenterColor)
|
||||
: new PathGradientBrush(transformedPoints, this.colors);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(Brush? other)
|
||||
{
|
||||
if (other is PathGradientBrush brush)
|
||||
{
|
||||
return this.CenterColor.Equals(brush.CenterColor)
|
||||
&& this.HasExplicitCenterColor.Equals(brush.HasExplicitCenterColor)
|
||||
&& this.edges?.SequenceEqual(brush.edges) == true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
=> HashCode.Combine(this.edges, this.CenterColor, this.HasExplicitCenterColor);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override BrushRenderer<TPixel> CreateRenderer<TPixel>(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
RectangleF region)
|
||||
=> new PathGradientBrushRenderer<TPixel>(
|
||||
configuration,
|
||||
options,
|
||||
canvasWidth,
|
||||
this.edges,
|
||||
this.CenterColor,
|
||||
this.HasExplicitCenterColor);
|
||||
|
||||
private static Color CalculateCenterColor(Color[] colors)
|
||||
{
|
||||
Guard.NotNull(colors, nameof(colors));
|
||||
Guard.MustBeGreaterThan(colors.Length, 0, nameof(colors));
|
||||
|
||||
return Color.FromScaledVector(colors.Select(c => c.ToScaledVector4()).Aggregate((p1, p2) => p1 + p2) / colors.Length);
|
||||
}
|
||||
|
||||
private static float DistanceBetween(Vector2 p1, Vector2 p2) => (p2 - p1).Length();
|
||||
|
||||
private readonly struct Intersection
|
||||
{
|
||||
public Intersection(PointF point, float distance)
|
||||
{
|
||||
this.Point = point;
|
||||
this.Distance = distance;
|
||||
}
|
||||
|
||||
public PointF Point { get; }
|
||||
|
||||
public float Distance { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An edge of the polygon that represents the gradient area.
|
||||
/// </summary>
|
||||
private class Edge : IEquatable<Edge>
|
||||
{
|
||||
private readonly float length;
|
||||
|
||||
public Edge(Vector2 start, Vector2 end, Color startColor, Color endColor)
|
||||
{
|
||||
this.Start = start;
|
||||
this.End = end;
|
||||
this.StartColor = startColor.ToScaledVector4();
|
||||
this.EndColor = endColor.ToScaledVector4();
|
||||
|
||||
this.length = DistanceBetween(this.End, this.Start);
|
||||
}
|
||||
|
||||
public Vector2 Start { get; }
|
||||
|
||||
public Vector2 End { get; }
|
||||
|
||||
public Vector4 StartColor { get; }
|
||||
|
||||
public Vector4 EndColor { get; }
|
||||
|
||||
public bool Intersect(
|
||||
Vector2 start,
|
||||
Vector2 end,
|
||||
ref Vector2 ip) =>
|
||||
PolygonUtilities.LineSegmentToLineSegmentIgnoreCollinear(start, end, this.Start, this.End, ref ip);
|
||||
|
||||
public Vector4 ColorAt(float distance)
|
||||
{
|
||||
float ratio = this.length > 0 ? distance / this.length : 0;
|
||||
|
||||
return Vector4.Lerp(this.StartColor, this.EndColor, ratio);
|
||||
}
|
||||
|
||||
public Vector4 ColorAt(PointF point) => this.ColorAt(DistanceBetween(point, this.Start));
|
||||
|
||||
public bool Equals(Edge? other)
|
||||
=> other != null &&
|
||||
other.Start == this.Start &&
|
||||
other.End == this.End &&
|
||||
other.StartColor.Equals(this.StartColor) &&
|
||||
other.EndColor.Equals(this.EndColor);
|
||||
|
||||
public override bool Equals(object? obj) => this.Equals(obj as Edge);
|
||||
|
||||
public override int GetHashCode()
|
||||
=> HashCode.Combine(this.Start, this.End, this.StartColor, this.EndColor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The path gradient brush applicator.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
private sealed class PathGradientBrushRenderer<TPixel> : BrushRenderer<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly Vector2 center;
|
||||
|
||||
private readonly Vector4 centerColor;
|
||||
|
||||
private readonly bool hasSpecialCenterColor;
|
||||
|
||||
private readonly float maxDistance;
|
||||
|
||||
private readonly IList<Edge> edges;
|
||||
|
||||
private readonly TPixel centerPixel;
|
||||
|
||||
private readonly TPixel transparentPixel;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PathGradientBrushRenderer{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration instance to use when performing operations.</param>
|
||||
/// <param name="options">The graphics options.</param>
|
||||
/// <param name="canvasWidth">The canvas width for the current render pass.</param>
|
||||
/// <param name="edges">Edges of the polygon.</param>
|
||||
/// <param name="centerColor">Color at the center of the gradient area to which the other colors converge.</param>
|
||||
/// <param name="hasSpecialCenterColor">Whether the center color is different from a smooth gradient between the edges.</param>
|
||||
public PathGradientBrushRenderer(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
IList<Edge> edges,
|
||||
Color centerColor,
|
||||
bool hasSpecialCenterColor)
|
||||
: base(configuration, options, canvasWidth)
|
||||
{
|
||||
this.edges = edges;
|
||||
Vector2[] points = [.. edges.Select(s => s.Start)];
|
||||
|
||||
this.center = points.Aggregate((p1, p2) => p1 + p2) / edges.Count;
|
||||
this.centerColor = centerColor.ToScaledVector4();
|
||||
this.hasSpecialCenterColor = hasSpecialCenterColor;
|
||||
this.centerPixel = centerColor.ToPixel<TPixel>();
|
||||
this.maxDistance = points.Select(p => p - this.center).Max(d => d.Length());
|
||||
this.transparentPixel = Color.Transparent.ToPixel<TPixel>();
|
||||
}
|
||||
|
||||
internal TPixel this[int x, int y]
|
||||
{
|
||||
get
|
||||
{
|
||||
// Match other gradient brushes by evaluating at pixel centers.
|
||||
Vector2 point = new(x + 0.5F, y + 0.5F);
|
||||
|
||||
if (point == this.center)
|
||||
{
|
||||
return this.centerPixel;
|
||||
}
|
||||
|
||||
if (this.edges.Count == 3 && !this.hasSpecialCenterColor)
|
||||
{
|
||||
if (!FindPointOnTriangle(
|
||||
this.edges[0].Start,
|
||||
this.edges[1].Start,
|
||||
this.edges[2].Start,
|
||||
point,
|
||||
out float u,
|
||||
out float v))
|
||||
{
|
||||
return this.transparentPixel;
|
||||
}
|
||||
|
||||
Vector4 pointColor = ((1 - u - v) * this.edges[0].StartColor)
|
||||
+ (u * this.edges[0].EndColor)
|
||||
+ (v * this.edges[2].StartColor);
|
||||
|
||||
return TPixel.FromScaledVector4(pointColor);
|
||||
}
|
||||
|
||||
Vector2 direction = Vector2.Normalize(point - this.center);
|
||||
Vector2 end = point + (direction * this.maxDistance);
|
||||
|
||||
(Edge Edge, Vector2 Point)? isc = this.FindIntersection(point, end);
|
||||
|
||||
if (!isc.HasValue)
|
||||
{
|
||||
return this.transparentPixel;
|
||||
}
|
||||
|
||||
Vector2 intersection = isc.Value.Point;
|
||||
Vector4 edgeColor = isc.Value.Edge.ColorAt(intersection);
|
||||
|
||||
float length = DistanceBetween(intersection, this.center);
|
||||
float ratio = length > 0 ? DistanceBetween(intersection, point) / length : 0;
|
||||
|
||||
Vector4 color = Vector4.Lerp(edgeColor, this.centerColor, ratio);
|
||||
|
||||
return TPixel.FromScaledVector4(color);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Apply(
|
||||
Span<TPixel> destinationRow,
|
||||
ReadOnlySpan<float> scanline,
|
||||
int x,
|
||||
int y,
|
||||
BrushWorkspace<TPixel> workspace)
|
||||
{
|
||||
Span<float> amounts = workspace.GetAmounts(scanline.Length);
|
||||
Span<TPixel> overlays = workspace.GetOverlays(scanline.Length);
|
||||
float blendPercentage = this.Options.BlendPercentage;
|
||||
|
||||
// TODO: Remove bounds checks.
|
||||
if (blendPercentage < 1)
|
||||
{
|
||||
for (int i = 0; i < scanline.Length; i++)
|
||||
{
|
||||
amounts[i] = scanline[i] * blendPercentage;
|
||||
overlays[i] = this[x + i, y];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < scanline.Length; i++)
|
||||
{
|
||||
amounts[i] = scanline[i];
|
||||
overlays[i] = this[x + i, y];
|
||||
}
|
||||
}
|
||||
|
||||
this.Blender.Blend(
|
||||
this.Configuration,
|
||||
destinationRow,
|
||||
destinationRow,
|
||||
overlays,
|
||||
amounts,
|
||||
workspace.GetBlendScratch(scanline.Length, 3));
|
||||
}
|
||||
|
||||
private (Edge Edge, Vector2 Point)? FindIntersection(
|
||||
PointF start,
|
||||
PointF end)
|
||||
{
|
||||
Vector2 ip = default;
|
||||
Vector2 closestIntersection = default;
|
||||
Edge? closestEdge = null;
|
||||
float minDistance = float.MaxValue;
|
||||
foreach (Edge edge in this.edges)
|
||||
{
|
||||
if (!edge.Intersect(start, end, ref ip))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float d = Vector2.DistanceSquared(start, ip);
|
||||
if (d < minDistance)
|
||||
{
|
||||
minDistance = d;
|
||||
closestEdge = edge;
|
||||
closestIntersection = ip;
|
||||
}
|
||||
}
|
||||
|
||||
return closestEdge != null ? (closestEdge, closestIntersection) : null;
|
||||
}
|
||||
|
||||
private static bool FindPointOnTriangle(Vector2 v1, Vector2 v2, Vector2 v3, Vector2 point, out float u, out float v)
|
||||
{
|
||||
Vector2 e1 = v2 - v1;
|
||||
Vector2 e2 = v3 - v2;
|
||||
Vector2 e3 = v1 - v3;
|
||||
|
||||
Vector2 pv1 = point - v1;
|
||||
Vector2 pv2 = point - v2;
|
||||
Vector2 pv3 = point - v3;
|
||||
|
||||
Vector3 d1 = Vector3.Cross(new Vector3(e1.X, e1.Y, 0), new Vector3(pv1.X, pv1.Y, 0));
|
||||
Vector3 d2 = Vector3.Cross(new Vector3(e2.X, e2.Y, 0), new Vector3(pv2.X, pv2.Y, 0));
|
||||
Vector3 d3 = Vector3.Cross(new Vector3(e3.X, e3.Y, 0), new Vector3(pv3.X, pv3.Y, 0));
|
||||
|
||||
if (Math.Sign(Vector3.Dot(d1, d2)) * Math.Sign(Vector3.Dot(d1, d3)) == -1 || Math.Sign(Vector3.Dot(d1, d2)) * Math.Sign(Vector3.Dot(d2, d3)) == -1)
|
||||
{
|
||||
u = 0;
|
||||
v = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// From Real-Time Collision Detection
|
||||
// https://gamedev.stackexchange.com/questions/23743/whats-the-most-efficient-way-to-find-barycentric-coordinates
|
||||
float d00 = Vector2.Dot(e1, e1);
|
||||
float d01 = -Vector2.Dot(e1, e3);
|
||||
float d11 = Vector2.Dot(e3, e3);
|
||||
float d20 = Vector2.Dot(pv1, e1);
|
||||
float d21 = -Vector2.Dot(pv1, e3);
|
||||
float denominator = (d00 * d11) - (d01 * d01);
|
||||
u = ((d11 * d20) - (d01 * d21)) / denominator;
|
||||
v = ((d00 * d21) - (d01 * d20)) / denominator;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Provides an implementation of a pattern brush for painting patterns.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The patterns that are used to create a custom pattern brush are made up of a repeating matrix of flags,
|
||||
/// where each flag denotes whether to draw the foreground color or the background color.
|
||||
/// so to create a new bool[,] with your flags
|
||||
/// <para>
|
||||
/// For example if you wanted to create a diagonal line that repeat every 4 pixels you would use a pattern like so
|
||||
/// 1000
|
||||
/// 0100
|
||||
/// 0010
|
||||
/// 0001
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// or you want a horizontal stripe which is 3 pixels apart you would use a pattern like
|
||||
/// 1
|
||||
/// 0
|
||||
/// 0
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class PatternBrush : Brush
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PatternBrush"/> class.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">Color of the fore.</param>
|
||||
/// <param name="backColor">Color of the back.</param>
|
||||
/// <param name="pattern">The pattern.</param>
|
||||
public PatternBrush(Color foreColor, Color backColor, bool[,] pattern)
|
||||
: this(foreColor, backColor, new DenseMatrix<bool>(pattern))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PatternBrush"/> class.
|
||||
/// </summary>
|
||||
/// <param name="foreColor">Color of the fore.</param>
|
||||
/// <param name="backColor">Color of the back.</param>
|
||||
/// <param name="pattern">The pattern.</param>
|
||||
internal PatternBrush(Color foreColor, Color backColor, in DenseMatrix<bool> pattern)
|
||||
{
|
||||
this.Pattern = new DenseMatrix<Color>(pattern.Columns, pattern.Rows);
|
||||
for (int i = 0; i < pattern.Data.Length; i++)
|
||||
{
|
||||
if (pattern.Data[i])
|
||||
{
|
||||
this.Pattern.Data[i] = foreColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Pattern.Data[i] = backColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PatternBrush"/> class.
|
||||
/// </summary>
|
||||
/// <param name="brush">The brush.</param>
|
||||
internal PatternBrush(PatternBrush brush) => this.Pattern = brush.Pattern;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pattern color matrix.
|
||||
/// </summary>
|
||||
public DenseMatrix<Color> Pattern { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(Brush? other)
|
||||
{
|
||||
if (other is PatternBrush sb)
|
||||
{
|
||||
return sb.Pattern.Equals(this.Pattern);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
=> this.Pattern.GetHashCode();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override BrushRenderer<TPixel> CreateRenderer<TPixel>(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
RectangleF region)
|
||||
=>
|
||||
new PatternBrushRenderer<TPixel>(
|
||||
configuration,
|
||||
options,
|
||||
canvasWidth,
|
||||
this.Pattern.ToPixelMatrix<TPixel>());
|
||||
|
||||
/// <summary>
|
||||
/// The pattern brush applicator.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
private sealed class PatternBrushRenderer<TPixel> : BrushRenderer<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly DenseMatrix<TPixel> pattern;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PatternBrushRenderer{TPixel}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration instance to use when performing operations.</param>
|
||||
/// <param name="options">The graphics options.</param>
|
||||
/// <param name="canvasWidth">The canvas width for the current render pass.</param>
|
||||
/// <param name="pattern">The pattern.</param>
|
||||
public PatternBrushRenderer(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
in DenseMatrix<TPixel> pattern)
|
||||
: base(configuration, options, canvasWidth)
|
||||
=> this.pattern = pattern;
|
||||
|
||||
internal TPixel this[int x, int y]
|
||||
{
|
||||
get
|
||||
{
|
||||
x %= this.pattern.Columns;
|
||||
y %= this.pattern.Rows;
|
||||
|
||||
// 2d array index at row/column
|
||||
return this.pattern[y, x];
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Apply(
|
||||
Span<TPixel> destinationRow,
|
||||
ReadOnlySpan<float> scanline,
|
||||
int x,
|
||||
int y,
|
||||
BrushWorkspace<TPixel> workspace)
|
||||
{
|
||||
int patternY = y % this.pattern.Rows;
|
||||
Span<float> amounts = workspace.GetAmounts(scanline.Length);
|
||||
Span<TPixel> overlays = workspace.GetOverlays(scanline.Length);
|
||||
|
||||
for (int i = 0; i < scanline.Length; i++)
|
||||
{
|
||||
amounts[i] = Math.Clamp(scanline[i] * this.Options.BlendPercentage, 0, 1F);
|
||||
|
||||
int patternX = (x + i) % this.pattern.Columns;
|
||||
overlays[i] = this.pattern[patternY, patternX];
|
||||
}
|
||||
|
||||
this.Blender.Blend(
|
||||
this.Configuration,
|
||||
destinationRow,
|
||||
destinationRow,
|
||||
overlays,
|
||||
amounts,
|
||||
workspace.GetBlendScratch(scanline.Length, 3));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Defines a pen that can apply a pattern to a line with a set brush and thickness
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The pattern will be in to the form of
|
||||
/// <code>
|
||||
/// new float[]{ 1f, 2f, 0.5f}
|
||||
/// </code>
|
||||
/// this will be converted into a pattern that is 3.5 times longer that the width with 3 sections.
|
||||
/// <list type="bullet">
|
||||
/// <item>Section 1 will be width long (making a square) and will be filled by the brush.</item>
|
||||
/// <item>Section 2 will be width * 2 long and will be empty.</item>
|
||||
/// <item>Section 3 will be width/2 long and will be filled.</item>
|
||||
/// </list>
|
||||
/// The pattern will immediately repeat without gap.
|
||||
/// </remarks>
|
||||
public class PatternPen : Pen
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PatternPen"/> class.
|
||||
/// </summary>
|
||||
/// <param name="color">The color.</param>
|
||||
/// <param name="strokePattern">The stroke pattern.</param>
|
||||
public PatternPen(Color color, float[] strokePattern)
|
||||
: base(new SolidBrush(color), 1, strokePattern)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PatternPen"/> class.
|
||||
/// </summary>
|
||||
/// <param name="color">The color.</param>
|
||||
/// <param name="strokeWidth">The stroke width in the path's local coordinate space before any drawing transform is applied.</param>
|
||||
/// <param name="strokePattern">The stroke pattern.</param>
|
||||
public PatternPen(Color color, float strokeWidth, float[] strokePattern)
|
||||
: base(new SolidBrush(color), strokeWidth, strokePattern)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PatternPen"/> class.
|
||||
/// </summary>
|
||||
/// <param name="strokeFill">The brush used to fill the stroke outline.</param>
|
||||
/// <param name="strokeWidth">The stroke width in the path's local coordinate space before any drawing transform is applied.</param>
|
||||
/// <param name="strokePattern">The stroke pattern.</param>
|
||||
public PatternPen(Brush strokeFill, float strokeWidth, float[] strokePattern)
|
||||
: base(strokeFill, strokeWidth, strokePattern)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PatternPen"/> class.
|
||||
/// </summary>
|
||||
/// <param name="options">The pen options.</param>
|
||||
public PatternPen(PenOptions options)
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(Pen? other)
|
||||
{
|
||||
if (other is PatternPen)
|
||||
{
|
||||
return base.Equals(other);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IPath GeneratePath(IPath path, float strokeWidth)
|
||||
=> path.GenerateOutline(strokeWidth, this.StrokePattern.Span, this.StrokeOptions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// The base class for pens that can apply a pattern to a line with a set brush and thickness
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The pattern will be in to the form of
|
||||
/// <code>
|
||||
/// new float[]{ 1f, 2f, 0.5f}
|
||||
/// </code>
|
||||
/// this will be converted into a pattern that is 3.5 times longer that the width with 3 sections.
|
||||
/// <list type="bullet">
|
||||
/// <item>Section 1 will be width long (making a square) and will be filled by the brush.</item>
|
||||
/// <item>Section 2 will be width * 2 long and will be empty.</item>
|
||||
/// <item>Section 3 will be width/2 long and will be filled.</item>
|
||||
/// </list>
|
||||
/// The pattern will immediately repeat without gap.
|
||||
/// </remarks>
|
||||
public abstract class Pen : IEquatable<Pen>
|
||||
{
|
||||
private readonly float[] pattern;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Pen"/> class.
|
||||
/// </summary>
|
||||
/// <param name="strokeFill">The brush used to fill the stroke outline.</param>
|
||||
protected Pen(Brush strokeFill)
|
||||
: this(strokeFill, 1)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Pen"/> class.
|
||||
/// </summary>
|
||||
/// <param name="strokeFill">The brush used to fill the stroke outline.</param>
|
||||
/// <param name="strokeWidth">The stroke width in the path's local coordinate space before any drawing transform is applied.</param>
|
||||
protected Pen(Brush strokeFill, float strokeWidth)
|
||||
: this(strokeFill, strokeWidth, Pens.EmptyPattern)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Pen"/> class.
|
||||
/// </summary>
|
||||
/// <param name="strokeFill">The brush used to fill the stroke outline.</param>
|
||||
/// <param name="strokeWidth">The stroke width in the path's local coordinate space before any drawing transform is applied.</param>
|
||||
/// <param name="strokePattern">The stroke pattern.</param>
|
||||
protected Pen(Brush strokeFill, float strokeWidth, float[] strokePattern)
|
||||
{
|
||||
Guard.NotNull(strokeFill, nameof(strokeFill));
|
||||
|
||||
Guard.MustBeGreaterThan(strokeWidth, 0, nameof(strokeWidth));
|
||||
Guard.NotNull(strokePattern, nameof(strokePattern));
|
||||
|
||||
this.StrokeFill = strokeFill;
|
||||
this.StrokeWidth = strokeWidth;
|
||||
this.pattern = strokePattern;
|
||||
this.StrokeOptions = new StrokeOptions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Pen"/> class.
|
||||
/// </summary>
|
||||
/// <param name="options">The pen options.</param>
|
||||
protected Pen(PenOptions options)
|
||||
{
|
||||
this.StrokeFill = options.StrokeFill;
|
||||
this.StrokeWidth = options.StrokeWidth;
|
||||
this.pattern = options.StrokePattern;
|
||||
this.StrokeOptions = options.StrokeOptions ?? new StrokeOptions();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="PenOptions.StrokeFill"/>
|
||||
public Brush StrokeFill { get; }
|
||||
|
||||
/// <inheritdoc cref="PenOptions.StrokeWidth"/>
|
||||
public float StrokeWidth { get; }
|
||||
|
||||
/// <inheritdoc cref="PenOptions.StrokePattern"/>
|
||||
public ReadOnlyMemory<float> StrokePattern => this.pattern;
|
||||
|
||||
/// <inheritdoc cref="PenOptions.StrokeOptions"/>
|
||||
public StrokeOptions StrokeOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Applies the styling from the pen to a path and generate a new path with the final vector.
|
||||
/// </summary>
|
||||
/// <param name="path">The source path</param>
|
||||
/// <returns>The <see cref="IPath"/> with the pen styling applied.</returns>
|
||||
public IPath GeneratePath(IPath path)
|
||||
=> this.GeneratePath(path, this.StrokeWidth);
|
||||
|
||||
/// <summary>
|
||||
/// Applies the styling from the pen to a path and generate a new path with the final vector.
|
||||
/// </summary>
|
||||
/// <param name="path">The source path</param>
|
||||
/// <param name="strokeWidth">The stroke width in the path's local coordinate space before any drawing transform is applied.</param>
|
||||
/// <returns>The <see cref="IPath"/> with the pen styling applied.</returns>
|
||||
public abstract IPath GeneratePath(IPath path, float strokeWidth);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual bool Equals(Pen? other)
|
||||
=> other != null
|
||||
&& this.StrokeWidth == other.StrokeWidth
|
||||
&& this.StrokeFill.Equals(other.StrokeFill)
|
||||
&& this.StrokeOptions.Equals(other.StrokeOptions)
|
||||
&& this.StrokePattern.Span.SequenceEqual(other.StrokePattern.Span);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) => this.Equals(obj as Pen);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
=> HashCode.Combine(this.StrokeWidth, this.StrokeFill, this.StrokeOptions, this.pattern);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Provides a set of configurations options for pens.
|
||||
/// </summary>
|
||||
public struct PenOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PenOptions"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="strokeWidth">The stroke width in the path's local coordinate space before any drawing transform is applied.</param>
|
||||
public PenOptions(float strokeWidth)
|
||||
: this(Color.Black, strokeWidth)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PenOptions"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="color">The color.</param>
|
||||
/// <param name="strokeWidth">The stroke width in the path's local coordinate space before any drawing transform is applied.</param>
|
||||
public PenOptions(Color color, float strokeWidth)
|
||||
: this(color, strokeWidth, null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PenOptions"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="color">The color.</param>
|
||||
/// <param name="strokeWidth">The stroke width in the path's local coordinate space before any drawing transform is applied.</param>
|
||||
/// <param name="strokePattern">The stroke pattern.</param>
|
||||
public PenOptions(Color color, float strokeWidth, float[]? strokePattern)
|
||||
: this(new SolidBrush(color), strokeWidth, strokePattern)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PenOptions"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="strokeFill">The brush used to fill the stroke outline.</param>
|
||||
/// <param name="strokeWidth">The stroke width in the path's local coordinate space before any drawing transform is applied.</param>
|
||||
/// <param name="strokePattern">The stroke pattern.</param>
|
||||
public PenOptions(Brush strokeFill, float strokeWidth, float[]? strokePattern)
|
||||
{
|
||||
Guard.MustBeGreaterThan(strokeWidth, 0, nameof(strokeWidth));
|
||||
|
||||
this.StrokeFill = strokeFill;
|
||||
this.StrokeWidth = strokeWidth;
|
||||
this.StrokePattern = strokePattern ?? Pens.EmptyPattern;
|
||||
this.StrokeOptions = new StrokeOptions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brush used to fill the stroke outline. Defaults to <see cref="SolidBrush"/>.
|
||||
/// </summary>
|
||||
public Brush StrokeFill { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stroke width in the path's local coordinate space before any drawing transform is applied. Defaults to 1.
|
||||
/// </summary>
|
||||
public float StrokeWidth { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stroke pattern.
|
||||
/// </summary>
|
||||
public float[] StrokePattern { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the stroke geometry options used to stroke paths drawn with this pen.
|
||||
/// </summary>
|
||||
public StrokeOptions? StrokeOptions { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Contains a collection of common pen styles.
|
||||
/// </summary>
|
||||
public static class Pens
|
||||
{
|
||||
private static readonly float[] DashDotPattern = [3f, 1f, 1f, 1f];
|
||||
private static readonly float[] DashDotDotPattern = [3f, 1f, 1f, 1f, 1f, 1f];
|
||||
private static readonly float[] DottedPattern = [1f, 1f];
|
||||
private static readonly float[] DashedPattern = [3f, 1f];
|
||||
internal static readonly float[] EmptyPattern = [];
|
||||
|
||||
/// <summary>
|
||||
/// Create a solid pen without any drawing patterns
|
||||
/// </summary>
|
||||
/// <param name="color">The color.</param>
|
||||
/// <returns>The <see cref="Pen"/>.</returns>
|
||||
public static SolidPen Solid(Color color) => new(color);
|
||||
|
||||
/// <summary>
|
||||
/// Create a solid pen without any drawing patterns
|
||||
/// </summary>
|
||||
/// <param name="brush">The brush.</param>
|
||||
/// <returns>The <see cref="Pen"/>.</returns>
|
||||
public static SolidPen Solid(Brush brush) => new(brush);
|
||||
|
||||
/// <summary>
|
||||
/// Create a solid pen without any drawing patterns
|
||||
/// </summary>
|
||||
/// <param name="color">The color.</param>
|
||||
/// <param name="width">The width.</param>
|
||||
/// <returns>The <see cref="Pen"/>.</returns>
|
||||
public static SolidPen Solid(Color color, float width) => new(color, width);
|
||||
|
||||
/// <summary>
|
||||
/// Create a solid pen without any drawing patterns
|
||||
/// </summary>
|
||||
/// <param name="brush">The brush.</param>
|
||||
/// <param name="width">The width.</param>
|
||||
/// <returns>The <see cref="Pen"/>.</returns>
|
||||
public static SolidPen Solid(Brush brush, float width) => new(brush, width);
|
||||
|
||||
/// <summary>
|
||||
/// Create a pen with a 'Dash' drawing patterns
|
||||
/// </summary>
|
||||
/// <param name="color">The color.</param>
|
||||
/// <param name="width">The width.</param>
|
||||
/// <returns>The <see cref="Pen"/>.</returns>
|
||||
public static PatternPen Dash(Color color, float width) => new(color, width, DashedPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Create a pen with a 'Dash' drawing patterns
|
||||
/// </summary>
|
||||
/// <param name="brush">The brush.</param>
|
||||
/// <param name="width">The width.</param>
|
||||
/// <returns>The <see cref="Pen"/>.</returns>
|
||||
public static PatternPen Dash(Brush brush, float width) => new(brush, width, DashedPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Create a pen with a 'Dot' drawing patterns
|
||||
/// </summary>
|
||||
/// <param name="color">The color.</param>
|
||||
/// <param name="width">The width.</param>
|
||||
/// <returns>The <see cref="Pen"/>.</returns>
|
||||
public static PatternPen Dot(Color color, float width) => new(color, width, DottedPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Create a pen with a 'Dot' drawing patterns
|
||||
/// </summary>
|
||||
/// <param name="brush">The brush.</param>
|
||||
/// <param name="width">The width.</param>
|
||||
/// <returns>The <see cref="Pen"/>.</returns>
|
||||
public static PatternPen Dot(Brush brush, float width) => new(brush, width, DottedPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Create a pen with a 'Dash Dot' drawing patterns
|
||||
/// </summary>
|
||||
/// <param name="color">The color.</param>
|
||||
/// <param name="width">The width.</param>
|
||||
/// <returns>The <see cref="Pen"/>.</returns>
|
||||
public static PatternPen DashDot(Color color, float width) => new(color, width, DashDotPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Create a pen with a 'Dash Dot' drawing patterns
|
||||
/// </summary>
|
||||
/// <param name="brush">The brush.</param>
|
||||
/// <param name="width">The width.</param>
|
||||
/// <returns>The <see cref="Pen"/>.</returns>
|
||||
public static PatternPen DashDot(Brush brush, float width) => new(brush, width, DashDotPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Create a pen with a 'Dash Dot Dot' drawing patterns
|
||||
/// </summary>
|
||||
/// <param name="color">The color.</param>
|
||||
/// <param name="width">The width.</param>
|
||||
/// <returns>The <see cref="Pen"/>.</returns>
|
||||
public static PatternPen DashDotDot(Color color, float width) => new(color, width, DashDotDotPattern);
|
||||
|
||||
/// <summary>
|
||||
/// Create a pen with a 'Dash Dot Dot' drawing patterns
|
||||
/// </summary>
|
||||
/// <param name="brush">The brush.</param>
|
||||
/// <param name="width">The width.</param>
|
||||
/// <returns>The <see cref="Pen"/>.</returns>
|
||||
public static PatternPen DashDotDot(Brush brush, float width) => new(brush, width, DashDotDotPattern);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using SixLabors.ImageSharp.Drawing.Helpers;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// A radial gradient brush defined by either one circle or two circles.
|
||||
/// When one circle is provided, the gradient parameter is the distance from the center divided by the radius.
|
||||
/// When two circles are provided, the gradient parameter is computed along the family of circles interpolating
|
||||
/// between the start and end circles.
|
||||
/// </summary>
|
||||
public sealed class RadialGradientBrush : GradientBrush
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RadialGradientBrush"/> class using a single circle.
|
||||
/// </summary>
|
||||
/// <param name="center">The center of the circular gradient.</param>
|
||||
/// <param name="radius">The radius of the circular gradient.</param>
|
||||
/// <param name="repetitionMode">Defines how the colors in the gradient are repeated.</param>
|
||||
/// <param name="colorStops">The ordered gradient stops.</param>
|
||||
public RadialGradientBrush(
|
||||
PointF center,
|
||||
float radius,
|
||||
GradientRepetitionMode repetitionMode,
|
||||
params ColorStop[] colorStops)
|
||||
: base(repetitionMode, colorStops)
|
||||
{
|
||||
this.Center0 = center;
|
||||
this.Radius0 = radius;
|
||||
this.Center1 = null;
|
||||
this.Radius1 = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RadialGradientBrush"/> class using two circles.
|
||||
/// </summary>
|
||||
/// <param name="startCenter">The center of the starting circle.</param>
|
||||
/// <param name="startRadius">The radius of the starting circle.</param>
|
||||
/// <param name="endCenter">The center of the ending circle.</param>
|
||||
/// <param name="endRadius">The radius of the ending circle.</param>
|
||||
/// <param name="repetitionMode">Defines how the colors in the gradient are repeated.</param>
|
||||
/// <param name="colorStops">The ordered gradient stops.</param>
|
||||
public RadialGradientBrush(
|
||||
PointF startCenter,
|
||||
float startRadius,
|
||||
PointF endCenter,
|
||||
float endRadius,
|
||||
GradientRepetitionMode repetitionMode,
|
||||
params ColorStop[] colorStops)
|
||||
: base(repetitionMode, colorStops)
|
||||
{
|
||||
this.Center0 = startCenter;
|
||||
this.Radius0 = startRadius;
|
||||
this.Center1 = endCenter;
|
||||
this.Radius1 = endRadius;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the center of the starting circle.
|
||||
/// </summary>
|
||||
public PointF Center0 { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the radius of the starting circle.
|
||||
/// </summary>
|
||||
public float Radius0 { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the center of the ending circle, or <see langword="null"/> for single-circle form.
|
||||
/// </summary>
|
||||
public PointF? Center1 { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the radius of the ending circle, or <see langword="null"/> for single-circle form.
|
||||
/// </summary>
|
||||
public float? Radius1 { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this is a two-circle radial gradient.
|
||||
/// </summary>
|
||||
public bool IsTwoCircle => this.Center1.HasValue && this.Radius1.HasValue;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Brush Transform(Matrix4x4 matrix)
|
||||
{
|
||||
PointF tc0 = PointF.Transform(this.Center0, matrix);
|
||||
float scale = MatrixUtilities.GetAverageScale(in matrix);
|
||||
if (this.IsTwoCircle)
|
||||
{
|
||||
PointF tc1 = PointF.Transform(this.Center1!.Value, matrix);
|
||||
return new RadialGradientBrush(tc0, this.Radius0 * scale, tc1, this.Radius1!.Value * scale, this.RepetitionMode, this.ColorStopsArray);
|
||||
}
|
||||
|
||||
return new RadialGradientBrush(tc0, this.Radius0 * scale, this.RepetitionMode, this.ColorStopsArray);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(Brush? other)
|
||||
{
|
||||
if (other is RadialGradientBrush b)
|
||||
{
|
||||
return base.Equals(other)
|
||||
&& this.Center0.Equals(b.Center0)
|
||||
&& this.Radius0.Equals(b.Radius0)
|
||||
&& Nullable.Equals(this.Center1, b.Center1)
|
||||
&& Nullable.Equals(this.Radius1, b.Radius1);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
=> HashCode.Combine(base.GetHashCode(), this.Center0, this.Radius0, this.Center1, this.Radius1);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override BrushRenderer<TPixel> CreateRenderer<TPixel>(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
RectangleF region)
|
||||
=> new RadialGradientBrushRenderer<TPixel>(
|
||||
configuration,
|
||||
options,
|
||||
canvasWidth,
|
||||
this.Center0,
|
||||
this.Radius0,
|
||||
this.Center1,
|
||||
this.Radius1,
|
||||
this.ColorStopsArray,
|
||||
this.RepetitionMode);
|
||||
|
||||
/// <summary>
|
||||
/// The radial gradient brush applicator.
|
||||
/// </summary>
|
||||
private sealed class RadialGradientBrushRenderer<TPixel> : GradientBrushRenderer<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private const float GradientEpsilon = 1F / (1 << 12);
|
||||
|
||||
// Single-circle fields
|
||||
private readonly bool isTwoCircle;
|
||||
private readonly float c0x;
|
||||
private readonly float c0y;
|
||||
private readonly float r0;
|
||||
|
||||
// Two-circle gradient fields.
|
||||
// The transform changes coordinates so the gradient can be evaluated
|
||||
// with simple formulas around a canonical line/circle configuration.
|
||||
private readonly Matrix3x2 radialTransform;
|
||||
private readonly float focalX;
|
||||
private readonly float radius;
|
||||
private readonly bool isStrip;
|
||||
private readonly bool isCircular;
|
||||
private readonly bool isFocalOnCircle;
|
||||
private readonly bool isSwapped;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RadialGradientBrushRenderer{TPixel}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration instance to use when performing operations.</param>
|
||||
/// <param name="options">The graphics options.</param>
|
||||
/// <param name="canvasWidth">The canvas width for the current render pass.</param>
|
||||
/// <param name="center0">Center of the starting circle.</param>
|
||||
/// <param name="radius0">Radius of the starting circle.</param>
|
||||
/// <param name="center1">Center of the ending circle, or null to use single-circle form.</param>
|
||||
/// <param name="radius1">Radius of the ending circle, or null to use single-circle form.</param>
|
||||
/// <param name="colorStops">Definition of colors.</param>
|
||||
/// <param name="repetitionMode">How the colors are repeated beyond the first gradient.</param>
|
||||
public RadialGradientBrushRenderer(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
PointF center0,
|
||||
float radius0,
|
||||
PointF? center1,
|
||||
float? radius1,
|
||||
ColorStop[] colorStops,
|
||||
GradientRepetitionMode repetitionMode)
|
||||
: base(configuration, options, canvasWidth, colorStops, repetitionMode)
|
||||
{
|
||||
this.c0x = center0.X;
|
||||
this.c0y = center0.Y;
|
||||
this.r0 = radius0;
|
||||
|
||||
this.isTwoCircle = center1.HasValue && radius1.HasValue;
|
||||
|
||||
if (this.isTwoCircle)
|
||||
{
|
||||
ConicalGradientParameters parameters = CreateConicalGradientParameters(
|
||||
center0,
|
||||
radius0,
|
||||
center1!.Value,
|
||||
radius1!.Value);
|
||||
|
||||
this.radialTransform = parameters.Transform;
|
||||
this.focalX = parameters.FocalX;
|
||||
this.radius = parameters.Radius;
|
||||
this.isStrip = parameters.IsStrip;
|
||||
this.isCircular = parameters.IsCircular;
|
||||
this.isFocalOnCircle = parameters.IsFocalOnCircle;
|
||||
this.isSwapped = parameters.IsSwapped;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.radialTransform = Matrix3x2.Identity;
|
||||
this.focalX = 0F;
|
||||
this.radius = 0F;
|
||||
this.isStrip = false;
|
||||
this.isCircular = false;
|
||||
this.isFocalOnCircle = false;
|
||||
this.isSwapped = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override float PositionOnGradient(float x, float y)
|
||||
{
|
||||
if (!this.isTwoCircle)
|
||||
{
|
||||
float ux = x - this.c0x, uy = y - this.c0y;
|
||||
return MathF.Sqrt((ux * ux) + (uy * uy)) / this.r0;
|
||||
}
|
||||
|
||||
// Move the sample into the canonical coordinate system where the
|
||||
// end circle lies on the x-axis and the conic can be solved using
|
||||
// closed-form expressions.
|
||||
Vector2 local = Vector2.Transform(new Vector2(x, y), this.radialTransform);
|
||||
float localX = local.X;
|
||||
float localY = local.Y;
|
||||
float xx = localX * localX;
|
||||
float yy = localY * localY;
|
||||
float t;
|
||||
|
||||
if (this.isStrip)
|
||||
{
|
||||
// Strip gradients are bounded by a band around the axis.
|
||||
// radius stores the squared half-width in normalized space,
|
||||
// so points outside the band are invalid.
|
||||
float a = this.radius - yy;
|
||||
if (a < 0F)
|
||||
{
|
||||
return float.NaN;
|
||||
}
|
||||
|
||||
// Once inside the band, the parameter advances along the axis.
|
||||
t = MathF.Sqrt(a) + localX;
|
||||
}
|
||||
else if (this.isFocalOnCircle)
|
||||
{
|
||||
// This degenerate case reduces to a rational expression where
|
||||
// the focal point sits exactly on the limiting circle.
|
||||
if (localX == 0F)
|
||||
{
|
||||
return float.NaN;
|
||||
}
|
||||
|
||||
t = (xx + yy) / localX;
|
||||
if (t < 0F)
|
||||
{
|
||||
return float.NaN;
|
||||
}
|
||||
}
|
||||
else if (this.radius > 1F)
|
||||
{
|
||||
// Wide cones use a circular norm. The x term shifts the root
|
||||
// back into the original gradient parameterization.
|
||||
float radiusReciprocal = this.isCircular ? 0F : 1F / this.radius;
|
||||
t = MathF.Sqrt(xx + yy) - (localX * radiusReciprocal);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Narrow cones use a hyperbolic form. Points with x^2 < y^2
|
||||
// lie outside the valid branch and must not contribute.
|
||||
float a = xx - yy;
|
||||
if (a < 0F)
|
||||
{
|
||||
return float.NaN;
|
||||
}
|
||||
|
||||
// lessScale picks the correct branch of the hyperbola after
|
||||
// swaps and orientation changes.
|
||||
float lessScale = (this.isSwapped || (1F - this.focalX) < 0F) ? -1F : 1F;
|
||||
t = (lessScale * MathF.Sqrt(a)) - (localX / this.radius);
|
||||
if (t < 0F)
|
||||
{
|
||||
return float.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert back from the normalized local solution into the brush's
|
||||
// gradient parameter, then undo the earlier swap if required.
|
||||
t = this.focalX + (MathF.Sign(1F - this.focalX) * t);
|
||||
return this.isSwapped ? 1F - t : t;
|
||||
}
|
||||
|
||||
private static ConicalGradientParameters CreateConicalGradientParameters(
|
||||
PointF center0,
|
||||
float radius0,
|
||||
PointF center1,
|
||||
float radius1)
|
||||
{
|
||||
PointF p0 = center0;
|
||||
PointF p1 = center1;
|
||||
float r0 = radius0;
|
||||
float r1 = radius1;
|
||||
|
||||
if (MathF.Abs(r0 - r1) <= GradientEpsilon)
|
||||
{
|
||||
// When both circles have the same radius, the locus becomes a
|
||||
// strip: solve along the axis between the centers, with the
|
||||
// radius contributing only a perpendicular cutoff.
|
||||
float scaled = r0 / Distance(p0, p1);
|
||||
return new ConicalGradientParameters(
|
||||
TwoPointToUnitLine(p0, p1),
|
||||
0F,
|
||||
scaled * scaled,
|
||||
isStrip: true,
|
||||
isCircular: false,
|
||||
isFocalOnCircle: false,
|
||||
isSwapped: false);
|
||||
}
|
||||
|
||||
bool isCircular = false;
|
||||
if (p0 == p1)
|
||||
{
|
||||
isCircular = true;
|
||||
|
||||
// Equal centers make the conic circular. Nudge slightly so the
|
||||
// line construction below stays invertible.
|
||||
p0 = new PointF(p0.X + GradientEpsilon, p0.Y + GradientEpsilon);
|
||||
}
|
||||
|
||||
bool isSwapped = false;
|
||||
if (r1 == 0F)
|
||||
{
|
||||
isSwapped = true;
|
||||
|
||||
// Put the zero-radius focus on the start side so the later
|
||||
// formulas keep one orientation.
|
||||
(p0, p1) = (p1, p0);
|
||||
(r0, r1) = (r1, r0);
|
||||
}
|
||||
|
||||
// focalX describes where the focal point lies along the line from
|
||||
// the start circle to the end circle. Values outside [0, 1] are
|
||||
// valid and correspond to cones whose focus lies beyond an endpoint.
|
||||
float focalX = r0 / (r0 - r1);
|
||||
PointF cf = new(
|
||||
((1F - focalX) * p0.X) + (focalX * p1.X),
|
||||
((1F - focalX) * p0.Y) + (focalX * p1.Y));
|
||||
|
||||
// radius is the end-circle radius expressed in the normalized frame
|
||||
// built from the focal point and the end center.
|
||||
float radius = r1 / Distance(cf, p1);
|
||||
Matrix3x2 userToUnitLine = TwoPointToUnitLine(cf, p1);
|
||||
Matrix3x2 transform;
|
||||
bool isFocalOnCircle = false;
|
||||
|
||||
if (MathF.Abs(radius - 1F) <= GradientEpsilon)
|
||||
{
|
||||
isFocalOnCircle = true;
|
||||
|
||||
// When the focal point lies on the circle, the quadratic terms
|
||||
// collapse to a simpler rational form.
|
||||
float scale = 0.5F * MathF.Abs(1F - focalX);
|
||||
transform = userToUnitLine * Matrix3x2.CreateScale(scale);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise scale the unit-line frame so the gradient can be
|
||||
// tested with either x^2 + y^2 or x^2 - y^2, depending on
|
||||
// whether the cone opens wider or narrower than the unit case.
|
||||
float a = (radius * radius) - 1F;
|
||||
float scaleRatio = MathF.Abs(1F - focalX) / a;
|
||||
float scaleX = radius * scaleRatio;
|
||||
float scaleY = MathF.Sqrt(MathF.Abs(a)) * scaleRatio;
|
||||
transform = userToUnitLine * Matrix3x2.CreateScale(scaleX, scaleY);
|
||||
}
|
||||
|
||||
return new ConicalGradientParameters(
|
||||
transform,
|
||||
focalX,
|
||||
radius,
|
||||
isStrip: false,
|
||||
isCircular: isCircular,
|
||||
isFocalOnCircle: isFocalOnCircle,
|
||||
isSwapped: isSwapped);
|
||||
}
|
||||
|
||||
private static float Distance(Vector2 p0, Vector2 p1) => Vector2.Distance(p0, p1);
|
||||
|
||||
private static Matrix3x2 TwoPointToUnitLine(PointF p0, PointF p1)
|
||||
{
|
||||
// Build a change-of-basis that sends the segment p0->p1 to the
|
||||
// unit line. That lets the gradient math work in one fixed frame
|
||||
// instead of re-deriving equations for every brush.
|
||||
Matrix3x2 source = FromPoly2(p0, p1);
|
||||
Matrix3x2.Invert(source, out Matrix3x2 inverse);
|
||||
return inverse * FromPoly2(new PointF(0F, 0F), new PointF(1F, 0F));
|
||||
}
|
||||
|
||||
private static Matrix3x2 FromPoly2(PointF p0, PointF p1)
|
||||
|
||||
// This affine frame uses p0 as the origin and p0->p1 as one axis.
|
||||
// Its inverse is the basis change we need for normalization.
|
||||
=> new(
|
||||
p1.Y - p0.Y,
|
||||
p0.X - p1.X,
|
||||
p1.X - p0.X,
|
||||
p1.Y - p0.Y,
|
||||
p0.X,
|
||||
p0.Y);
|
||||
|
||||
private readonly struct ConicalGradientParameters
|
||||
{
|
||||
public ConicalGradientParameters(
|
||||
Matrix3x2 transform,
|
||||
float focalX,
|
||||
float radius,
|
||||
bool isStrip,
|
||||
bool isCircular,
|
||||
bool isFocalOnCircle,
|
||||
bool isSwapped)
|
||||
{
|
||||
this.Transform = transform;
|
||||
this.FocalX = focalX;
|
||||
this.Radius = radius;
|
||||
this.IsStrip = isStrip;
|
||||
this.IsCircular = isCircular;
|
||||
this.IsFocalOnCircle = isFocalOnCircle;
|
||||
this.IsSwapped = isSwapped;
|
||||
}
|
||||
|
||||
public Matrix3x2 Transform { get; }
|
||||
|
||||
public float FocalX { get; }
|
||||
|
||||
public float Radius { get; }
|
||||
|
||||
public bool IsStrip { get; }
|
||||
|
||||
public bool IsCircular { get; }
|
||||
|
||||
public bool IsFocalOnCircle { get; }
|
||||
|
||||
public bool IsSwapped { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.Drawing.Processing.Backends;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Adds extensions that allow configuring the drawing backend implementation.
|
||||
/// </summary>
|
||||
public static class RasterizerDefaultsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the drawing backend against the source image processing context.
|
||||
/// </summary>
|
||||
/// <param name="context">The image processing context to store the backend against.</param>
|
||||
/// <param name="backend">The backend to use.</param>
|
||||
/// <returns>The passed in <paramref name="context"/> to allow chaining.</returns>
|
||||
internal static IImageProcessingContext SetDrawingBackend(this IImageProcessingContext context, IDrawingBackend backend)
|
||||
{
|
||||
Guard.NotNull(backend, nameof(backend));
|
||||
context.Properties[typeof(IDrawingBackend)] = backend;
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the default drawing backend against the configuration.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration to store the backend against.</param>
|
||||
/// <param name="backend">The backend to use.</param>
|
||||
public static void SetDrawingBackend(this Configuration configuration, IDrawingBackend backend)
|
||||
{
|
||||
Guard.NotNull(backend, nameof(backend));
|
||||
configuration.Properties[typeof(IDrawingBackend)] = backend;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the drawing backend from the source image processing context.
|
||||
/// </summary>
|
||||
/// <param name="context">The image processing context to retrieve the backend from.</param>
|
||||
/// <returns>The configured backend.</returns>
|
||||
internal static IDrawingBackend GetDrawingBackend(this IImageProcessingContext context)
|
||||
{
|
||||
if (context.Properties.TryGetValue(typeof(IDrawingBackend), out object? backend) &&
|
||||
backend is IDrawingBackend configured)
|
||||
{
|
||||
return configured;
|
||||
}
|
||||
|
||||
return context.Configuration.GetDrawingBackend();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default drawing backend from the configuration.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration to retrieve the backend from.</param>
|
||||
/// <returns>The configured backend.</returns>
|
||||
internal static IDrawingBackend GetDrawingBackend(this Configuration configuration)
|
||||
{
|
||||
if (configuration.Properties.TryGetValue(typeof(IDrawingBackend), out object? backend) &&
|
||||
backend is IDrawingBackend configured)
|
||||
{
|
||||
return configured;
|
||||
}
|
||||
|
||||
IDrawingBackend defaultBackend = DefaultDrawingBackend.Instance;
|
||||
configuration.Properties[typeof(IDrawingBackend)] = defaultBackend;
|
||||
return defaultBackend;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using SixLabors.ImageSharp.Memory;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Provides an implementation of a brush that can recolor an image
|
||||
/// </summary>
|
||||
public sealed class RecolorBrush : Brush
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RecolorBrush" /> class.
|
||||
/// </summary>
|
||||
/// <param name="sourceColor">Color of the source.</param>
|
||||
/// <param name="targetColor">Color of the target.</param>
|
||||
/// <param name="threshold">The threshold as a value between 0 and 1.</param>
|
||||
public RecolorBrush(Color sourceColor, Color targetColor, float threshold)
|
||||
{
|
||||
this.SourceColor = sourceColor;
|
||||
this.Threshold = threshold;
|
||||
this.TargetColor = targetColor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the threshold.
|
||||
/// </summary>
|
||||
public float Threshold { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source color.
|
||||
/// </summary>
|
||||
public Color SourceColor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target color.
|
||||
/// </summary>
|
||||
public Color TargetColor { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override BrushRenderer<TPixel> CreateRenderer<TPixel>(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
RectangleF region)
|
||||
=> new RecolorBrushRenderer<TPixel>(
|
||||
configuration,
|
||||
options,
|
||||
canvasWidth,
|
||||
this.SourceColor.ToPixel<TPixel>(),
|
||||
this.TargetColor.ToPixel<TPixel>(),
|
||||
this.Threshold);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(Brush? other)
|
||||
{
|
||||
if (other is RecolorBrush brush)
|
||||
{
|
||||
return this.SourceColor.Equals(brush.SourceColor)
|
||||
&& this.TargetColor.Equals(brush.TargetColor)
|
||||
&& this.Threshold == brush.Threshold;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
=> HashCode.Combine(this.Threshold, this.SourceColor, this.TargetColor);
|
||||
|
||||
/// <summary>
|
||||
/// The recolor brush applicator.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
private sealed class RecolorBrushRenderer<TPixel> : BrushRenderer<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly Vector4 sourceColor;
|
||||
private readonly float threshold;
|
||||
private readonly TPixel targetColorPixel;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RecolorBrushRenderer{TPixel}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration instance to use when performing operations.</param>
|
||||
/// <param name="options">The options</param>
|
||||
/// <param name="canvasWidth">The canvas width for the current render pass.</param>
|
||||
/// <param name="sourceColor">Color of the source.</param>
|
||||
/// <param name="targetColor">Color of the target.</param>
|
||||
/// <param name="threshold">The threshold .</param>
|
||||
public RecolorBrushRenderer(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
TPixel sourceColor,
|
||||
TPixel targetColor,
|
||||
float threshold)
|
||||
: base(configuration, options, canvasWidth)
|
||||
{
|
||||
this.sourceColor = sourceColor.ToScaledVector4();
|
||||
this.targetColorPixel = targetColor;
|
||||
|
||||
// TODO: Review this. We can skip the conversion from/to Vector4.
|
||||
// Lets hack a min max extremes for a color space by letting the IPackedPixel clamp our values to something in the correct spaces :)
|
||||
TPixel maxColor = TPixel.FromVector4(new Vector4(float.MaxValue));
|
||||
TPixel minColor = TPixel.FromVector4(new Vector4(float.MinValue));
|
||||
this.threshold = Vector4.DistanceSquared(maxColor.ToVector4(), minColor.ToVector4()) * threshold;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Apply(
|
||||
Span<TPixel> destinationRow,
|
||||
ReadOnlySpan<float> scanline,
|
||||
int x,
|
||||
int y,
|
||||
BrushWorkspace<TPixel> workspace)
|
||||
{
|
||||
Span<float> amounts = workspace.GetAmounts(scanline.Length);
|
||||
Span<TPixel> overlays = workspace.GetOverlays(scanline.Length);
|
||||
|
||||
for (int i = 0; i < scanline.Length; i++)
|
||||
{
|
||||
amounts[i] = scanline[i] * this.Options.BlendPercentage;
|
||||
TPixel result = destinationRow[i];
|
||||
Vector4 background = result.ToVector4();
|
||||
float distance = Vector4.DistanceSquared(background, this.sourceColor);
|
||||
overlays[i] = distance <= this.threshold
|
||||
? this.Blender.Blend(result, this.targetColorPixel, (this.threshold - distance) / this.threshold)
|
||||
: result;
|
||||
}
|
||||
|
||||
this.Blender.Blend(
|
||||
this.Configuration,
|
||||
destinationRow,
|
||||
destinationRow,
|
||||
overlays,
|
||||
amounts,
|
||||
workspace.GetBlendScratch(scanline.Length, 3));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using SixLabors.Fonts;
|
||||
using SixLabors.Fonts.Rendering;
|
||||
using SixLabors.ImageSharp.Drawing.Helpers;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text {
|
||||
/// <content>
|
||||
/// Utilities to translate format-agnostic paints (from Fonts) into ImageSharp.Drawing brushes.
|
||||
/// </content>
|
||||
internal sealed partial class RichTextGlyphRenderer
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to create an ImageSharp.Drawing <see cref="Brush"/> from a <see cref="Paint"/>.
|
||||
/// </summary>
|
||||
/// <param name="paint">The paint definition coming from the interpreter.</param>
|
||||
/// <param name="transform">A transform to apply to the brush coordinates.</param>
|
||||
/// <param name="brush">The resulting brush, or <see langword="null"/> if the paint is unsupported.</param>
|
||||
/// <returns><see langword="true"/> if a brush could be created; otherwise, <see langword="false"/>.</returns>
|
||||
public static bool TryCreateBrush([NotNullWhen(true)] Paint? paint, Matrix4x4 transform, [NotNullWhen(true)] out Brush? brush)
|
||||
{
|
||||
brush = null;
|
||||
|
||||
if (paint is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (paint)
|
||||
{
|
||||
case SolidPaint sp:
|
||||
brush = new SolidBrush(ToColor(sp.Color, sp.Opacity));
|
||||
return true;
|
||||
|
||||
case LinearGradientPaint lg:
|
||||
return TryCreateLinearGradientBrush(lg, transform, out brush);
|
||||
case RadialGradientPaint rg:
|
||||
return TryCreateRadialGradientBrush(rg, transform, out brush);
|
||||
case SweepGradientPaint sg:
|
||||
return TryCreateSweepGradientBrush(sg, transform, out brush);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="LinearGradientBrush"/> from a <see cref="LinearGradientPaint"/>.
|
||||
/// </summary>
|
||||
/// <param name="paint">The linear gradient paint.</param>
|
||||
/// <param name="transform">The transform to apply to the gradient points.</param>
|
||||
/// <param name="brush">The resulting brush.</param>
|
||||
/// <returns><see langword="true"/> if created; otherwise, <see langword="false"/>.</returns>
|
||||
private static bool TryCreateLinearGradientBrush(LinearGradientPaint paint, Matrix4x4 transform, out Brush? brush)
|
||||
{
|
||||
// Map gradient stops (apply paint opacity multiplier to each stop's alpha).
|
||||
ColorStop[] stops = ToColorStops(paint.Stops, paint.Opacity);
|
||||
|
||||
// Map spread method.
|
||||
GradientRepetitionMode mode = MapSpread(paint.Spread);
|
||||
|
||||
PointF p0 = paint.P0;
|
||||
PointF p1 = paint.P1;
|
||||
PointF? p2 = paint.P2;
|
||||
|
||||
// Apply any transform defined on the paint.
|
||||
if (!transform.IsIdentity)
|
||||
{
|
||||
p0 = PointF.Transform(p0, transform);
|
||||
p1 = PointF.Transform(p1, transform);
|
||||
|
||||
if (p2.HasValue)
|
||||
{
|
||||
p2 = PointF.Transform(p2.Value, transform);
|
||||
}
|
||||
}
|
||||
|
||||
if (p2.HasValue)
|
||||
{
|
||||
brush = new LinearGradientBrush(p0, p1, p2.Value, mode, stops);
|
||||
return true;
|
||||
}
|
||||
|
||||
brush = new LinearGradientBrush(p0, p1, mode, stops);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="RadialGradientBrush"/> from a <see cref="RadialGradientPaint"/>.
|
||||
/// </summary>
|
||||
/// <param name="paint">The radial gradient paint.</param>
|
||||
/// <param name="transform">The transform to apply to the gradient center point.</param>
|
||||
/// <param name="brush">The resulting brush.</param>
|
||||
/// <returns><see langword="true"/> if created; otherwise, <see langword="false"/>.</returns>
|
||||
private static bool TryCreateRadialGradientBrush(RadialGradientPaint paint, Matrix4x4 transform, out Brush? brush)
|
||||
{
|
||||
// Map gradient stops (apply paint opacity multiplier to each stop's alpha).
|
||||
ColorStop[] stops = ToColorStops(paint.Stops, paint.Opacity);
|
||||
|
||||
// Map spread method.
|
||||
GradientRepetitionMode mode = MapSpread(paint.Spread);
|
||||
|
||||
// Apply any transform defined on the paint.
|
||||
PointF center0 = paint.Center0;
|
||||
PointF center1 = paint.Center1;
|
||||
float radius0 = paint.Radius0;
|
||||
float radius1 = paint.Radius1;
|
||||
if (!transform.IsIdentity)
|
||||
{
|
||||
center0 = PointF.Transform(center0, transform);
|
||||
center1 = PointF.Transform(center1, transform);
|
||||
float scale = MatrixUtilities.GetAverageScale(in transform);
|
||||
radius0 *= scale;
|
||||
radius1 *= scale;
|
||||
}
|
||||
|
||||
brush = new RadialGradientBrush(center0, radius0, center1, radius1, mode, stops);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="SweepGradientBrush"/> from a <see cref="SweepGradientPaint"/>.
|
||||
/// </summary>
|
||||
/// <param name="paint">The sweep gradient paint.</param>
|
||||
/// <param name="transform">The transform to apply to the gradient center point.</param>
|
||||
/// <param name="brush">The resulting brush.</param>
|
||||
/// <returns><see langword="true"/> if created; otherwise, <see langword="false"/>.</returns>
|
||||
private static bool TryCreateSweepGradientBrush(SweepGradientPaint paint, Matrix4x4 transform, out Brush? brush)
|
||||
{
|
||||
// Map gradient stops (apply paint opacity multiplier to each stop's alpha).
|
||||
ColorStop[] stops = ToColorStops(paint.Stops, paint.Opacity);
|
||||
|
||||
// Map spread method.
|
||||
GradientRepetitionMode mode = MapSpread(paint.Spread);
|
||||
|
||||
// Apply any transform defined on the paint.
|
||||
PointF center = paint.Center;
|
||||
if (!transform.IsIdentity)
|
||||
{
|
||||
center = PointF.Transform(center, transform);
|
||||
}
|
||||
|
||||
brush = new SweepGradientBrush(center, paint.StartAngle, paint.EndAngle, mode, stops);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps an <see cref="SpreadMethod"/> to <see cref="GradientRepetitionMode"/>.
|
||||
/// </summary>
|
||||
/// <param name="spread">The spread method.</param>
|
||||
/// <returns>The repetition mode.</returns>
|
||||
private static GradientRepetitionMode MapSpread(SpreadMethod spread)
|
||||
=> spread switch
|
||||
{
|
||||
SpreadMethod.Reflect => GradientRepetitionMode.Reflect,
|
||||
SpreadMethod.Repeat => GradientRepetitionMode.Repeat,
|
||||
|
||||
// Pad extends edge colors, which matches 'None' (not 'DontFill').
|
||||
_ => GradientRepetitionMode.None,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Converts gradient stops and applies a paint opacity multiplier.
|
||||
/// </summary>
|
||||
/// <param name="stops">The source stops.</param>
|
||||
/// <param name="paintOpacity">The paint opacity in range [0,1].</param>
|
||||
/// <returns>An array of <see cref="ColorStop"/>.</returns>
|
||||
private static ColorStop[] ToColorStops(ReadOnlySpan<GradientStop> stops, float paintOpacity)
|
||||
{
|
||||
if (stops.Length == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
ColorStop[] result = new ColorStop[stops.Length];
|
||||
|
||||
for (int i = 0; i < stops.Length; i++)
|
||||
{
|
||||
GradientStop s = stops[i];
|
||||
Color c = ToColor(s.Color, paintOpacity);
|
||||
result[i] = new ColorStop(s.Offset, c);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a <see cref="GlyphColor"/> with an additional opacity multiplier to ImageSharp <see cref="Color"/>.
|
||||
/// </summary>
|
||||
/// <param name="c">The glyph color.</param>
|
||||
/// <param name="opacity">The opacity multiplier in range [0,1].</param>
|
||||
/// <returns>The ImageSharp color.</returns>
|
||||
private static Color ToColor(in GlyphColor c, float opacity)
|
||||
{
|
||||
float a = Math.Clamp(c.A / 255f * Math.Clamp(opacity, 0f, 1f), 0f, 1f);
|
||||
byte aa = (byte)MathF.Round(a * 255f);
|
||||
return Color.FromPixel(new Rgba32(c.R, c.G, c.B, aa));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,970 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using SixLabors.Fonts;
|
||||
using SixLabors.Fonts.Rendering;
|
||||
using SixLabors.Fonts.Unicode;
|
||||
using SixLabors.ImageSharp.Drawing.Text;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text {
|
||||
/// <summary>
|
||||
/// Allows the rendering of rich text configured via <see cref="RichTextOptions"/>.
|
||||
/// </summary>
|
||||
internal sealed partial class RichTextGlyphRenderer : BaseGlyphBuilder, IDisposable
|
||||
{
|
||||
// --- Render-pass ordering constants ---
|
||||
// Within DrawTextOperations, operations are sorted first by RenderPass so that
|
||||
// fills paint beneath outlines, and outlines beneath decorations.
|
||||
private const byte RenderOrderFill = 0;
|
||||
private const byte RenderOrderOutline = 1;
|
||||
private const byte RenderOrderDecoration = 2;
|
||||
|
||||
private readonly DrawingOptions drawingOptions;
|
||||
|
||||
/// <summary>The default pen supplied by the caller (e.g. from <c>DrawText(..., pen)</c>).</summary>
|
||||
private readonly Pen? defaultPen;
|
||||
|
||||
/// <summary>The default brush supplied by the caller (e.g. from <c>DrawText(..., brush)</c>).</summary>
|
||||
private readonly Brush? defaultBrush;
|
||||
|
||||
/// <summary>
|
||||
/// When the text is laid out along a path, this holds the path internals
|
||||
/// for point-along-path queries. <see langword="null"/> for normal (linear) text.
|
||||
/// </summary>
|
||||
private readonly IPathInternals? path;
|
||||
private bool isDisposed;
|
||||
|
||||
// --- Per-glyph mutable state reset in BeginGlyph ---
|
||||
|
||||
/// <summary>The <see cref="TextRun"/> (or <see cref="RichTextRun"/>) governing the current glyph.</summary>
|
||||
private TextRun? currentTextRun;
|
||||
|
||||
/// <summary>Brush resolved from the current <see cref="RichTextRun"/>, or <see langword="null"/>.</summary>
|
||||
private Brush? currentBrush;
|
||||
|
||||
/// <summary>Pen resolved from the current <see cref="RichTextRun"/>, or <see langword="null"/>.</summary>
|
||||
private Pen? currentPen;
|
||||
|
||||
/// <summary>The fill rule for the current color layer (COLR).</summary>
|
||||
private FillRule currentFillRule;
|
||||
|
||||
/// <summary>Alpha composition mode active for the current glyph/layer.</summary>
|
||||
private PixelAlphaCompositionMode currentCompositionMode;
|
||||
|
||||
/// <summary>Color blending mode active for the current glyph/layer.</summary>
|
||||
private PixelColorBlendingMode currentBlendingMode;
|
||||
|
||||
/// <summary>Whether the current glyph uses vertical layout (affects decoration orientation).</summary>
|
||||
private bool currentDecorationIsVertical;
|
||||
|
||||
/// <summary>Set to <see langword="true"/> when <see cref="BeginLayer"/> is called, cleared in <see cref="EndGlyph"/>.</summary>
|
||||
private bool hasLayer;
|
||||
|
||||
// --- Glyph outline cache ---
|
||||
// Glyphs that share the same CacheKey (same glyph id, sub-pixel position quantized
|
||||
// to 1/AccuracyMultiple, pen reference, etc.) reuse the translated IPath from the
|
||||
// first occurrence. This avoids re-building the full outline for repeated characters.
|
||||
//
|
||||
// AccuracyMultiple = 8 means sub-pixel positions are quantized to 1/8 px steps.
|
||||
// Benchmarked to give <0.2% image difference vs. uncached, with >60% cache hit ratio.
|
||||
private const float AccuracyMultiple = 8;
|
||||
|
||||
/// <summary>Maps cache keys to their list of <see cref="GlyphRenderData"/> entries (one per layer).
|
||||
/// Owned by the enclosing <see cref="DrawingCanvas{TPixel}"/> and shared across every DrawText
|
||||
/// call on that canvas, so glyph outlines persist beyond a single text draw.</summary>
|
||||
private readonly Dictionary<CacheKey, List<GlyphRenderData>> glyphCache;
|
||||
|
||||
/// <summary>Read cursor into the cached layer list for layered cache hits.</summary>
|
||||
private int cacheReadIndex;
|
||||
|
||||
/// <summary>
|
||||
/// <see langword="true"/> when the current glyph is a cache miss and its outline
|
||||
/// must be fully rasterized; <see langword="false"/> on a cache hit (reuse path).
|
||||
/// </summary>
|
||||
private bool rasterizationRequired;
|
||||
|
||||
/// <summary>
|
||||
/// <see langword="true"/> to disable the glyph cache entirely (e.g. path-based text
|
||||
/// where every glyph has a unique transform).
|
||||
/// </summary>
|
||||
private readonly bool noCache;
|
||||
|
||||
/// <summary>The cache key computed for the current glyph in <see cref="BeginGlyph"/>.</summary>
|
||||
private CacheKey currentCacheKey;
|
||||
|
||||
/// <summary>
|
||||
/// The transformed (post-<see cref="DrawingOptions.Transform"/>) bounding-box location
|
||||
/// of the current glyph. Stored so <see cref="EndGlyph"/> can compute
|
||||
/// <see cref="GlyphRenderData.BoundsOffset"/> for future cache-hit render location estimation.
|
||||
/// </summary>
|
||||
private PointF currentTransformedBoundsLocation;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RichTextGlyphRenderer"/> class.
|
||||
/// </summary>
|
||||
/// <param name="drawingOptions">Drawing options (transform, graphics options) for the text.</param>
|
||||
/// <param name="path">Optional path to draw the text along.</param>
|
||||
/// <param name="pen">Default pen for outlined text, or <see langword="null"/> for fill-only.</param>
|
||||
/// <param name="brush">Default brush for filled text, or <see langword="null"/> for outline-only.</param>
|
||||
/// <param name="glyphCache">Caller-owned per-canvas glyph cache shared across renderer
|
||||
/// instances so glyph outlines persist beyond a single text draw.</param>
|
||||
public RichTextGlyphRenderer(
|
||||
DrawingOptions drawingOptions,
|
||||
IPath? path,
|
||||
Pen? pen,
|
||||
Brush? brush,
|
||||
Dictionary<CacheKey, List<GlyphRenderData>> glyphCache)
|
||||
: base(drawingOptions.Transform)
|
||||
{
|
||||
this.drawingOptions = drawingOptions;
|
||||
this.defaultPen = pen;
|
||||
this.defaultBrush = brush;
|
||||
this.glyphCache = glyphCache;
|
||||
this.DrawingOperations = [];
|
||||
this.currentCompositionMode = drawingOptions.GraphicsOptions.AlphaCompositionMode;
|
||||
this.currentBlendingMode = drawingOptions.GraphicsOptions.ColorBlendingMode;
|
||||
|
||||
if (path is not null)
|
||||
{
|
||||
// Path-based text gives each glyph a unique per-position transform,
|
||||
// so cache hits are vanishingly rare; disable caching entirely.
|
||||
this.rasterizationRequired = true;
|
||||
this.noCache = true;
|
||||
if (path is IPathInternals internals)
|
||||
{
|
||||
this.path = internals;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.path = new ComplexPolygon(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of <see cref="DrawingOperation"/> instances accumulated during text rendering.
|
||||
/// After <c>RenderText</c> completes, this list is consumed by
|
||||
/// <see cref="DrawingCanvas{TPixel}.DrawTextOperations"/> to build composition commands.
|
||||
/// </summary>
|
||||
public List<DrawingOperation> DrawingOperations { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void BeginText(in FontRectangle bounds) => this.DrawingOperations.Clear();
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters)
|
||||
{
|
||||
// Resolves the active brush/pen from the text run, computes the cache key,
|
||||
// and takes one of three paths:
|
||||
// 1. Non-layered cache hit without decorations: emit cached ops, return false (fast path).
|
||||
// 2. Layered or decorated cache hit: reuse cached path, return true for EndGlyph/SetDecoration.
|
||||
// 3. Cache miss: rasterize from scratch.
|
||||
this.cacheReadIndex = 0;
|
||||
this.currentDecorationIsVertical = parameters.LayoutMode is GlyphLayoutMode.Vertical or GlyphLayoutMode.VerticalRotated;
|
||||
this.currentTextRun = parameters.TextRun;
|
||||
if (parameters.TextRun is RichTextRun drawingRun)
|
||||
{
|
||||
this.currentBrush = drawingRun.Brush;
|
||||
this.currentPen = drawingRun.Pen;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.currentBrush = null;
|
||||
this.currentPen = null;
|
||||
}
|
||||
|
||||
if (!this.noCache)
|
||||
{
|
||||
// Transform the font-metric bounds by the drawing transform so that the
|
||||
// sub-pixel position and size reflect the final screen coordinates.
|
||||
// Quantize to 1/AccuracyMultiple px steps for cache key comparison.
|
||||
RectangleF currentBounds = RectangleF.Transform(
|
||||
new RectangleF(bounds.Location, new SizeF(bounds.Width, bounds.Height)),
|
||||
this.drawingOptions.Transform);
|
||||
|
||||
this.currentTransformedBoundsLocation = currentBounds.Location;
|
||||
|
||||
PointF currentBoundsDelta = currentBounds.Location - ClampToPixel(currentBounds.Location);
|
||||
PointF subPixelLocation = new(
|
||||
MathF.Round(currentBoundsDelta.X * AccuracyMultiple) / AccuracyMultiple,
|
||||
MathF.Round(currentBoundsDelta.Y * AccuracyMultiple) / AccuracyMultiple);
|
||||
|
||||
SizeF subPixelSize = new(
|
||||
MathF.Round(currentBounds.Width * AccuracyMultiple) / AccuracyMultiple,
|
||||
MathF.Round(currentBounds.Height * AccuracyMultiple) / AccuracyMultiple);
|
||||
|
||||
this.currentCacheKey = CacheKey.FromParameters(
|
||||
parameters,
|
||||
new RectangleF(subPixelLocation, subPixelSize),
|
||||
this.currentPen ?? this.defaultPen);
|
||||
|
||||
if (this.glyphCache.TryGetValue(this.currentCacheKey, out List<GlyphRenderData>? cachedEntries))
|
||||
{
|
||||
if (cachedEntries.Count > 0 && !cachedEntries[0].IsLayered
|
||||
&& this.EnabledDecorations() == TextDecorations.None)
|
||||
{
|
||||
// Non-layered cache hit without decorations: emit operations directly
|
||||
// and tell the font engine to skip the outline entirely
|
||||
// (no MoveTo/LineTo/SetDecoration/EndGlyph).
|
||||
this.EmitCachedGlyphOperations(cachedEntries[0], currentBounds.Location);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Layered or decorated cache hit: let the normal flow handle
|
||||
// per-layer state and decoration callbacks.
|
||||
this.rasterizationRequired = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Transform the glyph vectors using the original bounds
|
||||
// The default transform will automatically be applied.
|
||||
this.TransformGlyph(in bounds);
|
||||
this.rasterizationRequired = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds)
|
||||
{
|
||||
// Capture the color-layer paint, fill rule, and composite mode.
|
||||
// Setting hasLayer tells EndGlyph to skip its default single-layer path emission.
|
||||
this.hasLayer = true;
|
||||
this.currentFillRule = fillRule;
|
||||
if (TryCreateBrush(paint, this.Builder.Transform, out Brush? brush))
|
||||
{
|
||||
this.currentBrush = brush;
|
||||
this.currentCompositionMode = TextUtilities.MapCompositionMode(paint.CompositeMode);
|
||||
this.currentBlendingMode = TextUtilities.MapBlendingMode(paint.CompositeMode);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void EndLayer()
|
||||
{
|
||||
// Finalizes a color layer. On a cache miss, translates the built path to local
|
||||
// coordinates and stores it for future hits. On a cache hit, reads the stored
|
||||
// path and adjusts the render location using sub-pixel delta compensation.
|
||||
GlyphRenderData renderData = default;
|
||||
IPath? fillPath = null;
|
||||
|
||||
// Fix up the text runs colors.
|
||||
// Only if both brush and pen is null do we fallback to the default value.
|
||||
if (this.currentBrush == null && this.currentPen == null)
|
||||
{
|
||||
this.currentBrush = this.defaultBrush;
|
||||
this.currentPen = this.defaultPen;
|
||||
}
|
||||
|
||||
// When rendering layers we only fill them.
|
||||
// Any drawing of outlines is ignored as that doesn't really make sense.
|
||||
bool renderFill = this.currentBrush != null;
|
||||
|
||||
// Path has already been added to the collection via the base class.
|
||||
IPath path = this.CurrentPaths[^1];
|
||||
Point renderLocation = ClampToPixel(path.Bounds.Location);
|
||||
if (this.noCache || this.rasterizationRequired)
|
||||
{
|
||||
if (path.Bounds.Equals(RectangleF.Empty))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (renderFill)
|
||||
{
|
||||
renderData.FillPath = path.Translate(-renderLocation);
|
||||
fillPath = renderData.FillPath;
|
||||
}
|
||||
|
||||
// Capture the delta between the location and the truncated render location.
|
||||
// We can use this to offset the render location on the next instance of this glyph.
|
||||
renderData.LocationDelta = (Vector2)(path.Bounds.Location - renderLocation);
|
||||
renderData.IsLayered = true;
|
||||
|
||||
if (!this.noCache)
|
||||
{
|
||||
this.UpdateCache(renderData);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
renderData = this.glyphCache[this.currentCacheKey][this.cacheReadIndex++];
|
||||
|
||||
// Offset the render location by the delta from the cached glyph and this one.
|
||||
Vector2 previousDelta = renderData.LocationDelta;
|
||||
Vector2 currentLocation = path.Bounds.Location;
|
||||
Vector2 currentDelta = path.Bounds.Location - ClampToPixel(path.Bounds.Location);
|
||||
|
||||
if (previousDelta.Y > currentDelta.Y)
|
||||
{
|
||||
// Move the location down to match the previous location offset.
|
||||
currentLocation += new Vector2(0, previousDelta.Y - currentDelta.Y);
|
||||
}
|
||||
else if (previousDelta.Y < currentDelta.Y)
|
||||
{
|
||||
// Move the location up to match the previous location offset.
|
||||
currentLocation -= new Vector2(0, currentDelta.Y - previousDelta.Y);
|
||||
}
|
||||
else if (previousDelta.X > currentDelta.X)
|
||||
{
|
||||
// Move the location right to match the previous location offset.
|
||||
currentLocation += new Vector2(previousDelta.X - currentDelta.X, 0);
|
||||
}
|
||||
else if (previousDelta.X < currentDelta.X)
|
||||
{
|
||||
// Move the location left to match the previous location offset.
|
||||
currentLocation -= new Vector2(currentDelta.X - previousDelta.X, 0);
|
||||
}
|
||||
|
||||
renderLocation = ClampToPixel(currentLocation);
|
||||
|
||||
if (renderFill && renderData.FillPath is not null)
|
||||
{
|
||||
fillPath = renderData.FillPath;
|
||||
}
|
||||
}
|
||||
|
||||
if (fillPath is not null)
|
||||
{
|
||||
IntersectionRule fillRule = TextUtilities.MapFillRule(this.currentFillRule);
|
||||
this.DrawingOperations.Add(new DrawingOperation
|
||||
{
|
||||
Kind = DrawingOperationKind.Fill,
|
||||
Path = fillPath,
|
||||
RenderLocation = renderLocation,
|
||||
IntersectionRule = fillRule,
|
||||
Brush = this.currentBrush,
|
||||
RenderPass = RenderOrderFill,
|
||||
PixelAlphaCompositionMode = this.currentCompositionMode,
|
||||
PixelColorBlendingMode = this.currentBlendingMode
|
||||
});
|
||||
}
|
||||
|
||||
this.currentFillRule = FillRule.NonZero;
|
||||
this.currentCompositionMode = this.drawingOptions.GraphicsOptions.AlphaCompositionMode;
|
||||
this.currentBlendingMode = this.drawingOptions.GraphicsOptions.ColorBlendingMode;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override TextDecorations EnabledDecorations()
|
||||
{
|
||||
// Returns the union of decorations from TextRun.TextDecorations and any
|
||||
// decoration pens set on the current RichTextRun. The font engine uses
|
||||
// this result to decide which SetDecoration calls to emit.
|
||||
TextRun? run = this.currentTextRun;
|
||||
TextDecorations decorations = run?.TextDecorations ?? TextDecorations.None;
|
||||
|
||||
if (this.currentTextRun is RichTextRun drawingRun)
|
||||
{
|
||||
if (drawingRun.UnderlinePen != null)
|
||||
{
|
||||
decorations |= TextDecorations.Underline;
|
||||
}
|
||||
|
||||
if (drawingRun.StrikeoutPen != null)
|
||||
{
|
||||
decorations |= TextDecorations.Strikeout;
|
||||
}
|
||||
|
||||
if (drawingRun.OverlinePen != null)
|
||||
{
|
||||
decorations |= TextDecorations.Overline;
|
||||
}
|
||||
}
|
||||
|
||||
return decorations;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness)
|
||||
{
|
||||
// Emits a DrawingOperation for a text decoration. Resolves the decoration pen
|
||||
// from the current RichTextRun, re-scales the base-class path when the pen's
|
||||
// stroke width differs from the font-metric thickness, and anchors the scaling
|
||||
// per decoration type (overline to bottom edge, underline to top edge, strikeout to center).
|
||||
// Decorations are not cached.
|
||||
if (thickness == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Brush? brush = null;
|
||||
Pen? pen = null;
|
||||
if (this.currentTextRun is RichTextRun drawingRun)
|
||||
{
|
||||
brush = drawingRun.Brush;
|
||||
|
||||
if (textDecorations == TextDecorations.Strikeout)
|
||||
{
|
||||
pen = drawingRun.StrikeoutPen ?? pen;
|
||||
}
|
||||
else if (textDecorations == TextDecorations.Underline)
|
||||
{
|
||||
pen = drawingRun.UnderlinePen ?? pen;
|
||||
}
|
||||
else if (textDecorations == TextDecorations.Overline)
|
||||
{
|
||||
pen = drawingRun.OverlinePen;
|
||||
}
|
||||
}
|
||||
|
||||
// Always respect the pen stroke width if explicitly set.
|
||||
float originalThickness = thickness;
|
||||
if (pen is not null)
|
||||
{
|
||||
// Clamp the thickness to whole pixels.
|
||||
thickness = MathF.Max(1F, (float)Math.Round(pen.StrokeWidth));
|
||||
}
|
||||
else
|
||||
{
|
||||
// The thickness of the line has already been clamped in the base class.
|
||||
pen = new SolidPen((brush ?? this.defaultBrush)!, thickness);
|
||||
}
|
||||
|
||||
// Path has already been added to the collection via the base class.
|
||||
IPath path = this.CurrentPaths[^1];
|
||||
IPath outline = path;
|
||||
|
||||
if (originalThickness != thickness)
|
||||
{
|
||||
// Respect edge anchoring per decoration type:
|
||||
// - Overline: keep the base edge fixed (bottom in horizontal; left in vertical)
|
||||
// - Underline: keep the top edge fixed (top in horizontal; right in vertical)
|
||||
// - Strikeout: keep the center fixed (default behavior)
|
||||
float ratio = thickness / originalThickness;
|
||||
if (ratio != 1f)
|
||||
{
|
||||
Vector2 scale = this.currentDecorationIsVertical
|
||||
? new Vector2(ratio, 1f)
|
||||
: new Vector2(1f, ratio);
|
||||
|
||||
RectangleF b = path.Bounds;
|
||||
Vector2 center = new(b.Left + (b.Width * 0.5f), b.Top + (b.Height * 0.5f));
|
||||
Vector2 anchor = center;
|
||||
|
||||
if (textDecorations == TextDecorations.Overline)
|
||||
{
|
||||
anchor = this.currentDecorationIsVertical
|
||||
? new Vector2(b.Left, center.Y) // vertical: anchor left edge
|
||||
: new Vector2(center.X, b.Bottom); // horizontal: anchor bottom edge
|
||||
}
|
||||
else if (textDecorations == TextDecorations.Underline)
|
||||
{
|
||||
anchor = this.currentDecorationIsVertical
|
||||
? new Vector2(b.Right, center.Y) // vertical: anchor right edge
|
||||
: new Vector2(center.X, b.Top); // horizontal: anchor top edge
|
||||
}
|
||||
|
||||
// Scale about the chosen anchor so the fixed edge stays in place.
|
||||
outline = outline.Transform(Matrix4x4.CreateScale(scale.X, scale.Y, 1, new Vector3(anchor, 0)));
|
||||
}
|
||||
}
|
||||
|
||||
// Render the path here. Decorations are un-cached.
|
||||
Point renderLocation = ClampToPixel(outline.Bounds.Location);
|
||||
IPath decorationPath = outline.Translate(-renderLocation);
|
||||
Brush decorationBrush = pen.StrokeFill;
|
||||
this.DrawingOperations.Add(new DrawingOperation
|
||||
{
|
||||
Kind = DrawingOperationKind.Fill,
|
||||
Path = decorationPath,
|
||||
RenderLocation = renderLocation,
|
||||
IntersectionRule = IntersectionRule.NonZero,
|
||||
Brush = decorationBrush,
|
||||
RenderPass = RenderOrderDecoration
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void EndGlyph()
|
||||
{
|
||||
// If hasLayer is set, layers were already handled by EndLayer; skip.
|
||||
// Otherwise, on a cache miss the built path is translated to local coordinates,
|
||||
// stored for future hits, and emitted as fill and/or outline DrawingOperations.
|
||||
// On a cache hit the stored path is reused with sub-pixel delta compensation.
|
||||
if (this.hasLayer)
|
||||
{
|
||||
// The layer has already been rendered.
|
||||
this.hasLayer = false;
|
||||
return;
|
||||
}
|
||||
|
||||
GlyphRenderData renderData = default;
|
||||
IPath? glyphPath = null;
|
||||
|
||||
// Fix up the text runs colors.
|
||||
// Only if both brush and pen is null do we fallback to the default value.
|
||||
if (this.currentBrush == null && this.currentPen == null)
|
||||
{
|
||||
this.currentBrush = this.defaultBrush;
|
||||
this.currentPen = this.defaultPen;
|
||||
}
|
||||
|
||||
bool renderFill = false;
|
||||
bool renderOutline = false;
|
||||
|
||||
// If we are using the fonts color layers we ignore the request to draw an outline only
|
||||
// because that won't really work. Instead we force drawing using fill with the requested color.
|
||||
if (this.currentBrush != null)
|
||||
{
|
||||
renderFill = true;
|
||||
}
|
||||
|
||||
if (this.currentPen != null)
|
||||
{
|
||||
renderOutline = true;
|
||||
}
|
||||
|
||||
// Path has already been added to the collection via the base class.
|
||||
IPath path = this.CurrentPaths[^1];
|
||||
Point renderLocation = ClampToPixel(path.Bounds.Location);
|
||||
if (this.noCache || this.rasterizationRequired)
|
||||
{
|
||||
if (path.Bounds.Equals(RectangleF.Empty))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IPath localPath = path.Translate(-renderLocation);
|
||||
if (renderFill || renderOutline)
|
||||
{
|
||||
renderData.FillPath = localPath;
|
||||
glyphPath = renderData.FillPath;
|
||||
}
|
||||
|
||||
// Capture the delta between the location and the truncated render location.
|
||||
// We can use this to offset the render location on the next instance of this glyph.
|
||||
renderData.LocationDelta = (Vector2)(path.Bounds.Location - renderLocation);
|
||||
|
||||
// Store the offset between outline bounds and font metric bounds so that
|
||||
// cache hits in BeginGlyph can accurately estimate the path location.
|
||||
renderData.BoundsOffset = (Vector2)(path.Bounds.Location - this.currentTransformedBoundsLocation);
|
||||
|
||||
if (!this.noCache)
|
||||
{
|
||||
this.UpdateCache(renderData);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
renderData = this.glyphCache[this.currentCacheKey][this.cacheReadIndex++];
|
||||
|
||||
// Offset the render location by the delta from the cached glyph and this one.
|
||||
Vector2 previousDelta = renderData.LocationDelta;
|
||||
Vector2 currentLocation = path.Bounds.Location;
|
||||
Vector2 currentDelta = path.Bounds.Location - ClampToPixel(path.Bounds.Location);
|
||||
|
||||
if (previousDelta.Y > currentDelta.Y)
|
||||
{
|
||||
// Move the location down to match the previous location offset.
|
||||
currentLocation += new Vector2(0, previousDelta.Y - currentDelta.Y);
|
||||
}
|
||||
else if (previousDelta.Y < currentDelta.Y)
|
||||
{
|
||||
// Move the location up to match the previous location offset.
|
||||
currentLocation -= new Vector2(0, currentDelta.Y - previousDelta.Y);
|
||||
}
|
||||
else if (previousDelta.X > currentDelta.X)
|
||||
{
|
||||
// Move the location right to match the previous location offset.
|
||||
currentLocation += new Vector2(previousDelta.X - currentDelta.X, 0);
|
||||
}
|
||||
else if (previousDelta.X < currentDelta.X)
|
||||
{
|
||||
// Move the location left to match the previous location offset.
|
||||
currentLocation -= new Vector2(currentDelta.X - previousDelta.X, 0);
|
||||
}
|
||||
|
||||
renderLocation = ClampToPixel(currentLocation);
|
||||
|
||||
if (renderFill && renderData.FillPath is not null)
|
||||
{
|
||||
glyphPath = renderData.FillPath;
|
||||
}
|
||||
|
||||
if (renderOutline && renderData.FillPath is not null)
|
||||
{
|
||||
glyphPath = renderData.FillPath;
|
||||
}
|
||||
}
|
||||
|
||||
if (renderFill && glyphPath is not null)
|
||||
{
|
||||
IntersectionRule fillRule = TextUtilities.MapFillRule(this.currentFillRule);
|
||||
this.DrawingOperations.Add(new DrawingOperation
|
||||
{
|
||||
Kind = DrawingOperationKind.Fill,
|
||||
Path = glyphPath,
|
||||
RenderLocation = renderLocation,
|
||||
IntersectionRule = fillRule,
|
||||
Brush = this.currentBrush,
|
||||
RenderPass = RenderOrderFill,
|
||||
PixelAlphaCompositionMode = this.currentCompositionMode,
|
||||
PixelColorBlendingMode = this.currentBlendingMode
|
||||
});
|
||||
}
|
||||
|
||||
if (renderOutline && glyphPath is not null)
|
||||
{
|
||||
IntersectionRule outlineRule = TextUtilities.MapFillRule(this.currentFillRule);
|
||||
this.DrawingOperations.Add(new DrawingOperation
|
||||
{
|
||||
Kind = DrawingOperationKind.Draw,
|
||||
Path = glyphPath,
|
||||
RenderLocation = renderLocation,
|
||||
IntersectionRule = outlineRule,
|
||||
Pen = this.currentPen,
|
||||
RenderPass = RenderOrderOutline,
|
||||
PixelAlphaCompositionMode = this.currentCompositionMode,
|
||||
PixelColorBlendingMode = this.currentBlendingMode
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits fill and/or outline <see cref="DrawingOperation"/>s from a cached
|
||||
/// <see cref="GlyphRenderData"/> entry. Called from <see cref="BeginGlyph"/> on a
|
||||
/// non-layered, decoration-free cache hit when the font engine is told to skip
|
||||
/// the outline entirely (returns <see langword="false"/>).
|
||||
/// </summary>
|
||||
/// <param name="renderData">The cached render data containing the translated path and location delta.</param>
|
||||
/// <param name="currentBoundsLocation">The transformed bounding-box origin for the current glyph instance.</param>
|
||||
private void EmitCachedGlyphOperations(GlyphRenderData renderData, PointF currentBoundsLocation)
|
||||
{
|
||||
// Estimate the outline bounds location using the stored offset between
|
||||
// the outline bounds and the font metric bounds from the original glyph.
|
||||
PointF estimatedPathLocation = new(
|
||||
currentBoundsLocation.X + renderData.BoundsOffset.X,
|
||||
currentBoundsLocation.Y + renderData.BoundsOffset.Y);
|
||||
Point renderLocation = ComputeCacheHitRenderLocation(estimatedPathLocation, renderData.LocationDelta);
|
||||
|
||||
// Fix up the text runs colors.
|
||||
Brush? brush = this.currentBrush;
|
||||
Pen? pen = this.currentPen;
|
||||
if (brush == null && pen == null)
|
||||
{
|
||||
brush = this.defaultBrush;
|
||||
pen = this.defaultPen;
|
||||
}
|
||||
|
||||
IPath? glyphPath = renderData.FillPath;
|
||||
if (glyphPath is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (brush != null)
|
||||
{
|
||||
IntersectionRule fillRule = TextUtilities.MapFillRule(this.currentFillRule);
|
||||
this.DrawingOperations.Add(new DrawingOperation
|
||||
{
|
||||
Kind = DrawingOperationKind.Fill,
|
||||
Path = glyphPath,
|
||||
RenderLocation = renderLocation,
|
||||
IntersectionRule = fillRule,
|
||||
Brush = brush,
|
||||
RenderPass = RenderOrderFill,
|
||||
PixelAlphaCompositionMode = this.currentCompositionMode,
|
||||
PixelColorBlendingMode = this.currentBlendingMode
|
||||
});
|
||||
}
|
||||
|
||||
if (pen != null)
|
||||
{
|
||||
IntersectionRule outlineRule = TextUtilities.MapFillRule(this.currentFillRule);
|
||||
this.DrawingOperations.Add(new DrawingOperation
|
||||
{
|
||||
Kind = DrawingOperationKind.Draw,
|
||||
Path = glyphPath,
|
||||
RenderLocation = renderLocation,
|
||||
IntersectionRule = outlineRule,
|
||||
Pen = pen,
|
||||
RenderPass = RenderOrderOutline,
|
||||
PixelAlphaCompositionMode = this.currentCompositionMode,
|
||||
PixelColorBlendingMode = this.currentBlendingMode
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the pixel-snapped render location for a cache-hit glyph by compensating
|
||||
/// for the sub-pixel delta difference between the original cached glyph and the
|
||||
/// current instance. This keeps glyphs visually aligned even when their sub-pixel
|
||||
/// positions differ slightly.
|
||||
/// </summary>
|
||||
/// <param name="pathLocation">The estimated outline bounds origin for the current glyph.</param>
|
||||
/// <param name="previousDelta">The sub-pixel delta recorded when the path was first cached.</param>
|
||||
/// <returns>A pixel-snapped render location.</returns>
|
||||
private static Point ComputeCacheHitRenderLocation(PointF pathLocation, Vector2 previousDelta)
|
||||
{
|
||||
Vector2 currentLocation = (Vector2)pathLocation;
|
||||
Vector2 currentDelta = currentLocation - (Vector2)ClampToPixel(pathLocation);
|
||||
|
||||
if (previousDelta.Y > currentDelta.Y)
|
||||
{
|
||||
currentLocation += new Vector2(0, previousDelta.Y - currentDelta.Y);
|
||||
}
|
||||
else if (previousDelta.Y < currentDelta.Y)
|
||||
{
|
||||
currentLocation -= new Vector2(0, currentDelta.Y - previousDelta.Y);
|
||||
}
|
||||
else if (previousDelta.X > currentDelta.X)
|
||||
{
|
||||
currentLocation += new Vector2(previousDelta.X - currentDelta.X, 0);
|
||||
}
|
||||
else if (previousDelta.X < currentDelta.X)
|
||||
{
|
||||
currentLocation -= new Vector2(currentDelta.X - previousDelta.X, 0);
|
||||
}
|
||||
|
||||
return ClampToPixel(currentLocation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores a <see cref="GlyphRenderData"/> entry in the glyph cache under the
|
||||
/// current key. Creates the cache list on first insertion for a given key.
|
||||
/// </summary>
|
||||
private void UpdateCache(GlyphRenderData renderData)
|
||||
{
|
||||
if (!this.glyphCache.TryGetValue(this.currentCacheKey, out List<GlyphRenderData>? _))
|
||||
{
|
||||
this.glyphCache[this.currentCacheKey] = [];
|
||||
}
|
||||
|
||||
this.glyphCache[this.currentCacheKey].Add(renderData);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => this.Dispose(true);
|
||||
|
||||
/// <summary>
|
||||
/// Truncates a floating-point position to the nearest whole pixel toward negative infinity.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static Point ClampToPixel(PointF point) => Point.Truncate(point);
|
||||
|
||||
/// <summary>
|
||||
/// Applies the path-based transform to the <see cref="BaseGlyphBuilder.Builder"/>
|
||||
/// for the current glyph, positioning it along the text path (if any) or
|
||||
/// leaving the identity transform for linear text.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void TransformGlyph(in FontRectangle bounds)
|
||||
=> this.Builder.SetTransform(this.ComputeTransform(in bounds));
|
||||
|
||||
/// <summary>
|
||||
/// Computes the combined translation + rotation matrix that places a glyph
|
||||
/// along the text path. For linear text (no path), returns <see cref="Matrix4x4.Identity"/>.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private Matrix4x4 ComputeTransform(in FontRectangle bounds)
|
||||
{
|
||||
if (this.path is null)
|
||||
{
|
||||
return Matrix4x4.Identity;
|
||||
}
|
||||
|
||||
// Find the point of this intersection along the given path.
|
||||
// We want to find the point on the path that is closest to the center-bottom side of the glyph.
|
||||
Vector2 half = new(bounds.Width * .5F, 0);
|
||||
SegmentInfo pathPoint = this.path.PointAlongPath(bounds.Left + half.X);
|
||||
|
||||
// Now offset to our target point since we're aligning the top-left location of our glyph against the path.
|
||||
Vector2 translation = (Vector2)pathPoint.Point - bounds.Location - half + new Vector2(0, bounds.Top);
|
||||
return Matrix4x4.CreateTranslation(translation.X, translation.Y, 0)
|
||||
* new Matrix4x4(Matrix3x2.CreateRotation(pathPoint.Angle - MathF.PI, (Vector2)pathPoint.Point));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases managed resources (glyph cache and drawing operations list).
|
||||
/// </summary>
|
||||
/// <param name="disposing"><see langword="true"/> to release managed resources.</param>
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!this.isDisposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// The glyph cache is owned by the canvas and outlives this renderer.
|
||||
this.DrawingOperations.Clear();
|
||||
}
|
||||
|
||||
this.isDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-layer cached data for a rasterized glyph. Stores the locally-translated
|
||||
/// path and the sub-pixel deltas needed to reposition the path at a different
|
||||
/// screen location on a cache hit.
|
||||
/// </summary>
|
||||
internal struct GlyphRenderData
|
||||
{
|
||||
/// <summary>
|
||||
/// The fractional-pixel offset between the path's bounding-box origin
|
||||
/// and the truncated (pixel-snapped) render location. Used to compensate
|
||||
/// for sub-pixel position differences between cache hits.
|
||||
/// </summary>
|
||||
public Vector2 LocationDelta;
|
||||
|
||||
/// <summary>
|
||||
/// The offset between the outline path's bounding-box origin and the
|
||||
/// font-metric bounds origin. Stored on first rasterization so that
|
||||
/// <see cref="EmitCachedGlyphOperations"/> can estimate the path location
|
||||
/// from only the font-metric bounds (which are available without outline data).
|
||||
/// </summary>
|
||||
public Vector2 BoundsOffset;
|
||||
|
||||
/// <summary>
|
||||
/// The glyph outline path translated to local coordinates (origin at 0,0).
|
||||
/// Shared across all cache hits for the same <see cref="CacheKey"/>.
|
||||
/// </summary>
|
||||
public IPath? FillPath;
|
||||
|
||||
/// <summary>
|
||||
/// <see langword="true"/> if this entry belongs to a multi-layer (COLR) glyph.
|
||||
/// Non-layered cache hits with no decorations can skip the outline entirely
|
||||
/// (return <see langword="false"/> from <see cref="BeginGlyph"/>); layered hits
|
||||
/// still need the per-layer <c>BeginLayer</c>/<c>EndLayer</c> callbacks.
|
||||
/// </summary>
|
||||
public bool IsLayered;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies a unique glyph variant for caching purposes. Two glyphs with the same
|
||||
/// <see cref="CacheKey"/> share identical outline geometry and can reuse the same
|
||||
/// <see cref="GlyphRenderData.FillPath"/>. The key includes the glyph id, font metrics,
|
||||
/// sub-pixel position (quantized to <see cref="AccuracyMultiple"/>), and the pen reference
|
||||
/// (since stroke width affects the outline path).
|
||||
/// </summary>
|
||||
internal readonly struct CacheKey : IEquatable<CacheKey>
|
||||
{
|
||||
/// <summary>Gets the font family name.</summary>
|
||||
public string Font { get; init; }
|
||||
|
||||
/// <summary>Gets the glyph color variant (normal, COLR, etc.).</summary>
|
||||
public GlyphColor GlyphColor { get; init; }
|
||||
|
||||
/// <summary>Gets the glyph type (simple, composite, etc.).</summary>
|
||||
public GlyphType GlyphType { get; init; }
|
||||
|
||||
/// <summary>Gets the font style (regular, bold, italic, etc.).</summary>
|
||||
public FontStyle FontStyle { get; init; }
|
||||
|
||||
/// <summary>Gets the glyph index within the font.</summary>
|
||||
public ushort GlyphId { get; init; }
|
||||
|
||||
/// <summary>Gets the composite glyph parent index (0 for non-composite).</summary>
|
||||
public ushort CompositeGlyphId { get; init; }
|
||||
|
||||
/// <summary>Gets the Unicode code point this glyph represents.</summary>
|
||||
public CodePoint CodePoint { get; init; }
|
||||
|
||||
/// <summary>Gets the em-size at which the glyph is rendered.</summary>
|
||||
public float PointSize { get; init; }
|
||||
|
||||
/// <summary>Gets the DPI used for rendering.</summary>
|
||||
public float Dpi { get; init; }
|
||||
|
||||
/// <summary>Gets the layout mode (horizontal, vertical, vertical-rotated).</summary>
|
||||
public GlyphLayoutMode LayoutMode { get; init; }
|
||||
|
||||
/// <summary>Gets any text attributes (e.g. superscript/subscript) that affect rendering.</summary>
|
||||
public TextAttributes TextAttributes { get; init; }
|
||||
|
||||
/// <summary>Gets text decorations that may influence outline geometry.</summary>
|
||||
public TextDecorations TextDecorations { get; init; }
|
||||
|
||||
/// <summary>Gets the quantized sub-pixel bounds used for position-sensitive cache lookup.</summary>
|
||||
public RectangleF Bounds { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pen reference used for outlined text. Compared by reference equality
|
||||
/// so that different pen instances (even with the same stroke width) produce
|
||||
/// separate cache entries; this is correct because pen identity affects stroke
|
||||
/// pattern and dash style.
|
||||
/// </summary>
|
||||
public Pen? PenReference { get; init; }
|
||||
|
||||
public static bool operator ==(CacheKey left, CacheKey right) => left.Equals(right);
|
||||
|
||||
public static bool operator !=(CacheKey left, CacheKey right) => !(left == right);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="CacheKey"/> from glyph renderer parameters and quantized bounds.
|
||||
/// The grapheme index is intentionally excluded because it varies per glyph instance
|
||||
/// while the outline geometry remains the same for matching glyph+position.
|
||||
/// </summary>
|
||||
/// <param name="parameters">The glyph renderer parameters from the font engine.</param>
|
||||
/// <param name="bounds">Quantized sub-pixel bounds for position-sensitive lookup.</param>
|
||||
/// <param name="penReference">The pen reference for outlined text, or <see langword="null"/>.</param>
|
||||
/// <returns>A new cache key.</returns>
|
||||
public static CacheKey FromParameters(
|
||||
in GlyphRendererParameters parameters,
|
||||
RectangleF bounds,
|
||||
Pen? penReference)
|
||||
=> new()
|
||||
{
|
||||
// Do not include the grapheme index as that will
|
||||
// always vary per glyph instance.
|
||||
Font = parameters.Font,
|
||||
GlyphType = parameters.GlyphType,
|
||||
FontStyle = parameters.FontStyle,
|
||||
GlyphId = parameters.GlyphId,
|
||||
CompositeGlyphId = parameters.CompositeGlyphId,
|
||||
CodePoint = parameters.CodePoint,
|
||||
PointSize = parameters.PointSize,
|
||||
Dpi = parameters.Dpi,
|
||||
LayoutMode = parameters.LayoutMode,
|
||||
TextAttributes = parameters.TextRun.TextAttributes,
|
||||
TextDecorations = parameters.TextRun.TextDecorations,
|
||||
Bounds = bounds,
|
||||
PenReference = penReference
|
||||
};
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
=> obj is CacheKey key && this.Equals(key);
|
||||
|
||||
public bool Equals(CacheKey other)
|
||||
=> this.Font == other.Font &&
|
||||
this.GlyphColor.Equals(other.GlyphColor) &&
|
||||
this.GlyphType == other.GlyphType &&
|
||||
this.FontStyle == other.FontStyle &&
|
||||
this.GlyphId == other.GlyphId &&
|
||||
this.CompositeGlyphId == other.CompositeGlyphId &&
|
||||
this.CodePoint.Equals(other.CodePoint) &&
|
||||
this.PointSize == other.PointSize &&
|
||||
this.Dpi == other.Dpi &&
|
||||
this.LayoutMode == other.LayoutMode &&
|
||||
this.TextAttributes == other.TextAttributes &&
|
||||
this.TextDecorations == other.TextDecorations &&
|
||||
this.Bounds.Equals(other.Bounds) &&
|
||||
ReferenceEquals(this.PenReference, other.PenReference);
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
HashCode hash = default;
|
||||
hash.Add(this.Font);
|
||||
hash.Add(this.GlyphColor);
|
||||
hash.Add(this.GlyphType);
|
||||
hash.Add(this.FontStyle);
|
||||
hash.Add(this.GlyphId);
|
||||
hash.Add(this.CompositeGlyphId);
|
||||
hash.Add(this.CodePoint);
|
||||
hash.Add(this.PointSize);
|
||||
hash.Add(this.Dpi);
|
||||
hash.Add(this.LayoutMode);
|
||||
hash.Add(this.TextAttributes);
|
||||
hash.Add(this.TextDecorations);
|
||||
hash.Add(this.Bounds);
|
||||
hash.Add(this.PenReference is null ? 0 : RuntimeHelpers.GetHashCode(this.PenReference));
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.Fonts;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Provides configuration options for rendering and shaping of rich text.
|
||||
/// </summary>
|
||||
public class RichTextOptions : TextOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RichTextOptions" /> class.
|
||||
/// </summary>
|
||||
/// <param name="font">The font.</param>
|
||||
public RichTextOptions(Font font)
|
||||
: base(font)
|
||||
=> this.TextRuns = [];
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RichTextOptions" /> class from properties
|
||||
/// copied from the given instance.
|
||||
/// </summary>
|
||||
/// <param name="options">The options whose properties are copied into this instance.</param>
|
||||
public RichTextOptions(RichTextOptions options)
|
||||
: base(options)
|
||||
{
|
||||
List<RichTextRun> runs = new(options.TextRuns.Count);
|
||||
foreach (RichTextRun run in options.TextRuns)
|
||||
{
|
||||
runs.Add(new RichTextRun()
|
||||
{
|
||||
Brush = run.Brush,
|
||||
Pen = run.Pen,
|
||||
StrikeoutPen = run.StrikeoutPen,
|
||||
UnderlinePen = run.UnderlinePen,
|
||||
OverlinePen = run.OverlinePen,
|
||||
Start = run.Start,
|
||||
End = run.End,
|
||||
Font = run.Font,
|
||||
TextAttributes = run.TextAttributes,
|
||||
TextDecorations = run.TextDecorations,
|
||||
Placeholder = run.Placeholder
|
||||
});
|
||||
}
|
||||
|
||||
this.TextRuns = runs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional collection of text runs to apply to the body of text.
|
||||
/// </summary>
|
||||
public new IReadOnlyList<RichTextRun> TextRuns
|
||||
{
|
||||
get => (IReadOnlyList<RichTextRun>)base.TextRuns;
|
||||
set => base.TextRuns = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.Fonts;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Represents a run of drawable text spanning a series of graphemes within a string.
|
||||
/// </summary>
|
||||
public class RichTextRun : TextRun
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the brush used for filling this run.
|
||||
/// </summary>
|
||||
public Brush? Brush { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pen used for outlining this run.
|
||||
/// </summary>
|
||||
public Pen? Pen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pen used for drawing strikeout features for this run.
|
||||
/// </summary>
|
||||
public Pen? StrikeoutPen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pen used for drawing underline features for this run.
|
||||
/// </summary>
|
||||
public Pen? UnderlinePen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pen used for drawing overline features for this run.
|
||||
/// </summary>
|
||||
public Pen? OverlinePen { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Provides options for controlling how vector shapes are interpreted during rasterization,
|
||||
/// including the fill-rule intersection mode and boolean clipping operations.
|
||||
/// </summary>
|
||||
public class ShapeOptions : IDeepCloneable<ShapeOptions>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ShapeOptions"/> class.
|
||||
/// </summary>
|
||||
public ShapeOptions()
|
||||
{
|
||||
}
|
||||
|
||||
private ShapeOptions(ShapeOptions source)
|
||||
{
|
||||
this.IntersectionRule = source.IntersectionRule;
|
||||
this.BooleanOperation = source.BooleanOperation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the boolean clipping operation used when a clipping path is applied.
|
||||
/// Determines how the clip shape interacts with the target region
|
||||
/// (e.g. <see cref="BooleanOperation.Difference"/> subtracts the clip shape).
|
||||
/// <para/>
|
||||
/// Defaults to <see cref="BooleanOperation.Difference"/>.
|
||||
/// </summary>
|
||||
public BooleanOperation BooleanOperation { get; set; } = BooleanOperation.Difference;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the fill rule that determines how overlapping or nested contours affect coverage.
|
||||
/// <see cref="IntersectionRule.NonZero"/> fills any region with a non-zero winding number;
|
||||
/// <see cref="IntersectionRule.EvenOdd"/> alternates fill/hole for each contour crossing.
|
||||
/// <para/>
|
||||
/// Defaults to <see cref="IntersectionRule.NonZero"/>.
|
||||
/// </summary>
|
||||
public IntersectionRule IntersectionRule { get; set; } = IntersectionRule.NonZero;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ShapeOptions DeepClone() => new(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Provides an implementation of a solid brush for painting solid color areas.
|
||||
/// </summary>
|
||||
public sealed class SolidBrush : Brush
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SolidBrush"/> class.
|
||||
/// </summary>
|
||||
/// <param name="color">The color.</param>
|
||||
public SolidBrush(Color color) => this.Color = color;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the color.
|
||||
/// </summary>
|
||||
public Color Color { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override BrushRenderer<TPixel> CreateRenderer<TPixel>(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
RectangleF region)
|
||||
=> new SolidBrushRenderer<TPixel>(configuration, options, canvasWidth, this.Color.ToPixel<TPixel>());
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(Brush? other)
|
||||
{
|
||||
if (other is SolidBrush sb)
|
||||
{
|
||||
return sb.Color.Equals(this.Color);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode() => this.Color.GetHashCode();
|
||||
|
||||
/// <summary>
|
||||
/// The solid brush applicator.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
private sealed class SolidBrushRenderer<TPixel> : BrushRenderer<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private readonly TPixel color;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SolidBrushRenderer{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration instance to use when performing operations.</param>
|
||||
/// <param name="options">The graphics options.</param>
|
||||
/// <param name="canvasWidth">The canvas width for the current render pass.</param>
|
||||
/// <param name="color">The color.</param>
|
||||
public SolidBrushRenderer(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
TPixel color)
|
||||
: base(configuration, options, canvasWidth)
|
||||
=> this.color = color;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Apply(
|
||||
Span<TPixel> destinationRow,
|
||||
ReadOnlySpan<float> scanline,
|
||||
int x,
|
||||
int y,
|
||||
BrushWorkspace<TPixel> workspace)
|
||||
{
|
||||
// Constrain the spans to each other
|
||||
if (destinationRow.Length > scanline.Length)
|
||||
{
|
||||
destinationRow = destinationRow[..scanline.Length];
|
||||
}
|
||||
else
|
||||
{
|
||||
scanline = scanline[..destinationRow.Length];
|
||||
}
|
||||
|
||||
Configuration configuration = this.Configuration;
|
||||
if (this.Options.BlendPercentage == 1F)
|
||||
{
|
||||
this.Blender.Blend(
|
||||
configuration,
|
||||
destinationRow,
|
||||
destinationRow,
|
||||
this.color,
|
||||
scanline,
|
||||
workspace.GetBlendScratch(scanline.Length, 2));
|
||||
}
|
||||
else
|
||||
{
|
||||
Span<float> amounts = workspace.GetAmounts(scanline.Length);
|
||||
|
||||
for (int i = 0; i < scanline.Length; i++)
|
||||
{
|
||||
amounts[i] = scanline[i] * this.Options.BlendPercentage;
|
||||
}
|
||||
|
||||
this.Blender.Blend(
|
||||
configuration,
|
||||
destinationRow,
|
||||
destinationRow,
|
||||
this.color,
|
||||
amounts,
|
||||
workspace.GetBlendScratch(scanline.Length, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Defines a pen that can apply a pattern to a line with a set brush and thickness.
|
||||
/// </summary>
|
||||
public class SolidPen : Pen
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SolidPen"/> class.
|
||||
/// </summary>
|
||||
/// <param name="color">The color.</param>
|
||||
public SolidPen(Color color)
|
||||
: base(new SolidBrush(color))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SolidPen"/> class.
|
||||
/// </summary>
|
||||
/// <param name="color">The color.</param>
|
||||
/// <param name="width">The width.</param>
|
||||
public SolidPen(Color color, float width)
|
||||
: base(new SolidBrush(color), width)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SolidPen"/> class.
|
||||
/// </summary>
|
||||
/// <param name="strokeFill">The brush used to fill the stroke outline.</param>
|
||||
public SolidPen(Brush strokeFill)
|
||||
: base(strokeFill)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SolidPen"/> class.
|
||||
/// </summary>
|
||||
/// <param name="strokeFill">The brush used to fill the stroke outline.</param>
|
||||
/// <param name="strokeWidth">The stroke width in the path's local coordinate space before any drawing transform is applied.</param>
|
||||
public SolidPen(Brush strokeFill, float strokeWidth)
|
||||
: base(strokeFill, strokeWidth)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SolidPen"/> class.
|
||||
/// </summary>
|
||||
/// <param name="options">The pen options.</param>
|
||||
public SolidPen(PenOptions options)
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(Pen? other)
|
||||
{
|
||||
if (other is SolidPen)
|
||||
{
|
||||
return base.Equals(other);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IPath GeneratePath(IPath path, float strokeWidth)
|
||||
=> path.GenerateOutline(strokeWidth, this.StrokeOptions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <inheritdoc cref="PolygonClipper.StrokeOptions" />
|
||||
public sealed class StrokeOptions : IEquatable<StrokeOptions?>
|
||||
{
|
||||
/// <inheritdoc cref="PolygonClipper.StrokeOptions.MiterLimit" />
|
||||
public double MiterLimit { get; set; } = 4D;
|
||||
|
||||
/// <inheritdoc cref="PolygonClipper.StrokeOptions.ArcDetailScale" />
|
||||
public double ArcDetailScale { get; set; } = 1D;
|
||||
|
||||
/// <inheritdoc cref="PolygonClipper.StrokeOptions.LineJoin" />
|
||||
public LineJoin LineJoin { get; set; } = LineJoin.Bevel;
|
||||
|
||||
/// <inheritdoc cref="PolygonClipper.StrokeOptions.LineCap" />
|
||||
public LineCap LineCap { get; set; } = LineCap.Butt;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj) => this.Equals(obj as StrokeOptions);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(StrokeOptions? other)
|
||||
=> other is not null &&
|
||||
this.MiterLimit == other.MiterLimit &&
|
||||
this.ArcDetailScale == other.ArcDetailScale &&
|
||||
this.LineJoin == other.LineJoin &&
|
||||
this.LineCap == other.LineCap;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
=> HashCode.Combine(
|
||||
this.MiterLimit,
|
||||
this.ArcDetailScale,
|
||||
this.LineJoin,
|
||||
this.LineCap);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Processing {
|
||||
/// <summary>
|
||||
/// Provides an implementation of a brush for painting sweep (conic) gradients within areas.
|
||||
/// Angles increase counter-clockwise from +X on the design grid.
|
||||
/// </summary>
|
||||
public sealed class SweepGradientBrush : GradientBrush
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SweepGradientBrush"/> class.
|
||||
/// </summary>
|
||||
/// <param name="center">The center point of the sweep gradient in device space.</param>
|
||||
/// <param name="startAngleDegrees">
|
||||
/// The starting angle, in degrees, measured counter-clockwise from +X on the design grid.
|
||||
/// This value is stored as provided so the sign and magnitude of the sweep remain intact.
|
||||
/// </param>
|
||||
/// <param name="endAngleDegrees">
|
||||
/// The ending angle, in degrees, measured counter-clockwise from +X on the design grid.
|
||||
/// If equal to <paramref name="startAngleDegrees"/>, the gradient is treated as a full 360 degree sweep.
|
||||
/// Otherwise, the signed difference between start and end determines the sweep direction.
|
||||
/// </param>
|
||||
/// <param name="repetitionMode">Defines how the gradient colors are repeated beyond the interval [0..1].</param>
|
||||
/// <param name="colorStops">The gradient color stops. Ratios must be in [0..1] and are interpreted along the angular sweep.</param>
|
||||
public SweepGradientBrush(
|
||||
PointF center,
|
||||
float startAngleDegrees,
|
||||
float endAngleDegrees,
|
||||
GradientRepetitionMode repetitionMode,
|
||||
params ColorStop[] colorStops)
|
||||
: base(repetitionMode, colorStops)
|
||||
{
|
||||
this.Center = center;
|
||||
this.StartAngleDegrees = startAngleDegrees;
|
||||
this.EndAngleDegrees = endAngleDegrees;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the center point of the sweep gradient.
|
||||
/// </summary>
|
||||
public PointF Center { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the starting angle in degrees.
|
||||
/// </summary>
|
||||
public float StartAngleDegrees { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ending angle in degrees.
|
||||
/// </summary>
|
||||
public float EndAngleDegrees { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Brush Transform(Matrix4x4 matrix)
|
||||
{
|
||||
PointF tc = PointF.Transform(this.Center, matrix);
|
||||
|
||||
// Treat the brush as two rays starting at the center:
|
||||
// one ray for the start angle and one ray for the end angle.
|
||||
// The important value is the signed angular distance between those rays.
|
||||
// We keep that sign so a reflected transform can turn a counter-clockwise
|
||||
// sweep into a clockwise sweep instead of silently "fixing" it.
|
||||
float sweepDegrees = GetEffectiveSweepDegrees(this.StartAngleDegrees, this.EndAngleDegrees);
|
||||
float startRad = GeometryUtilities.DegreeToRadian(this.StartAngleDegrees);
|
||||
float endRad = GeometryUtilities.DegreeToRadian(this.StartAngleDegrees + sweepDegrees);
|
||||
|
||||
// The public API uses the design-grid convention, which is y-up.
|
||||
// Screen pixels are y-down, so a positive mathematical rotation uses
|
||||
// `center.Y - sin(theta)` rather than `center.Y + sin(theta)`.
|
||||
PointF startDir = PointF.Transform(new PointF(this.Center.X + MathF.Cos(startRad), this.Center.Y - MathF.Sin(startRad)), matrix);
|
||||
PointF endDir = PointF.Transform(new PointF(this.Center.X + MathF.Cos(endRad), this.Center.Y - MathF.Sin(endRad)), matrix);
|
||||
|
||||
// Convert the transformed rays back into brush angles in the same public convention:
|
||||
// counter-clockwise from +X on the design grid.
|
||||
float newStart = NormalizeDirectionDegrees(MathF.Atan2(-(startDir.Y - tc.Y), startDir.X - tc.X) * (180f / MathF.PI));
|
||||
float newEnd = NormalizeDirectionDegrees(MathF.Atan2(-(endDir.Y - tc.Y), endDir.X - tc.X) * (180f / MathF.PI));
|
||||
|
||||
// A negative determinant means the transform flips orientation.
|
||||
// That flips the direction of the sweep, so we use it to decide whether
|
||||
// the end angle should unwrap forwards or backwards from the new start.
|
||||
float determinant = (matrix.M11 * matrix.M22) - (matrix.M12 * matrix.M21);
|
||||
float directionHint = MathF.Sign(sweepDegrees);
|
||||
if (directionHint == 0F)
|
||||
{
|
||||
directionHint = 1F;
|
||||
}
|
||||
|
||||
if (determinant < 0F)
|
||||
{
|
||||
directionHint = -directionHint;
|
||||
}
|
||||
|
||||
return new SweepGradientBrush(
|
||||
tc,
|
||||
newStart,
|
||||
UnwrapSweepEndDegrees(newStart, newEnd, directionHint, MathF.Abs(sweepDegrees)),
|
||||
this.RepetitionMode,
|
||||
this.ColorStopsArray);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(Brush? other)
|
||||
{
|
||||
// Sweep brushes are equal only when they describe the same center,
|
||||
// the same signed angular interval, and the same inherited stop data.
|
||||
if (other is SweepGradientBrush brush)
|
||||
{
|
||||
return base.Equals(other)
|
||||
&& this.Center.Equals(brush.Center)
|
||||
&& this.StartAngleDegrees.Equals(brush.StartAngleDegrees)
|
||||
&& this.EndAngleDegrees.Equals(brush.EndAngleDegrees);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
=> HashCode.Combine(
|
||||
base.GetHashCode(),
|
||||
this.Center,
|
||||
this.StartAngleDegrees,
|
||||
this.EndAngleDegrees);
|
||||
|
||||
/// <summary>
|
||||
/// Converts the stored start/end angles into the signed sweep interval that the brush should render.
|
||||
/// </summary>
|
||||
/// <param name="startAngleDegrees">The starting angle in degrees.</param>
|
||||
/// <param name="endAngleDegrees">The ending angle in degrees.</param>
|
||||
/// <returns>
|
||||
/// The signed angular interval in degrees. Equal endpoints are treated as a full turn.
|
||||
/// </returns>
|
||||
// Sweep gradients interpret equal endpoints as "full turn".
|
||||
// All other cases keep the caller-provided signed angular span.
|
||||
private static float GetEffectiveSweepDegrees(float startAngleDegrees, float endAngleDegrees)
|
||||
{
|
||||
float sweepDegrees = endAngleDegrees - startAngleDegrees;
|
||||
if (MathF.Abs(sweepDegrees) < 1e-6F)
|
||||
{
|
||||
// Equal endpoints mean "full circle", not an empty span.
|
||||
return 360F;
|
||||
}
|
||||
|
||||
return sweepDegrees;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes an angle to the canonical <c>[0, 360)</c> direction range.
|
||||
/// </summary>
|
||||
/// <param name="degrees">The angle to normalize.</param>
|
||||
/// <returns>The equivalent direction in the canonical degree range.</returns>
|
||||
// Convert any equivalent direction into the canonical [0, 360) representation
|
||||
// so transformed brushes remain stable when compared or reused.
|
||||
private static float NormalizeDirectionDegrees(float degrees)
|
||||
{
|
||||
float normalized = degrees % 360F;
|
||||
if (normalized < 0F)
|
||||
{
|
||||
normalized += 360F;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reconstructs the signed end angle after independently transforming the start and end rays.
|
||||
/// </summary>
|
||||
/// <param name="startDegrees">The transformed starting angle in normalized degrees.</param>
|
||||
/// <param name="endDegrees">The transformed ending angle in normalized degrees.</param>
|
||||
/// <param name="directionHint">
|
||||
/// The expected sweep direction. Positive means unwrap forwards, negative means unwrap backwards.
|
||||
/// </param>
|
||||
/// <param name="minimumMagnitude">The minimum magnitude the restored interval must preserve.</param>
|
||||
/// <returns>The unwrapped ending angle measured relative to <paramref name="startDegrees"/>.</returns>
|
||||
// After transforming the start and end rays separately, both directions land in [0, 360).
|
||||
// This method restores the intended signed sweep by unwrapping the end angle relative to
|
||||
// the start angle, using the desired direction as the constraint.
|
||||
private static float UnwrapSweepEndDegrees(float startDegrees, float endDegrees, float directionHint, float minimumMagnitude)
|
||||
{
|
||||
float delta = endDegrees - startDegrees;
|
||||
if (directionHint >= 0F)
|
||||
{
|
||||
// Keep the end angle ahead of the start angle for a positive sweep.
|
||||
while (delta < 0F)
|
||||
{
|
||||
delta += 360F;
|
||||
}
|
||||
|
||||
if (MathF.Abs(delta) < 1e-6F && minimumMagnitude >= 360F - 1e-6F)
|
||||
{
|
||||
delta = 360F;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Keep the end angle behind the start angle for a negative sweep.
|
||||
while (delta > 0F)
|
||||
{
|
||||
delta -= 360F;
|
||||
}
|
||||
|
||||
if (MathF.Abs(delta) < 1e-6F && minimumMagnitude >= 360F - 1e-6F)
|
||||
{
|
||||
delta = -360F;
|
||||
}
|
||||
}
|
||||
|
||||
return startDegrees + delta;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override BrushRenderer<TPixel> CreateRenderer<TPixel>(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
RectangleF region) =>
|
||||
|
||||
// The renderer precomputes the angular interval once and then samples it per pixel.
|
||||
new SweepGradientBrushRenderer<TPixel>(
|
||||
configuration,
|
||||
options,
|
||||
canvasWidth,
|
||||
this,
|
||||
this.ColorStopsArray,
|
||||
this.RepetitionMode);
|
||||
|
||||
/// <summary>
|
||||
/// The sweep (conic) gradient brush applicator.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
private sealed class SweepGradientBrushRenderer<TPixel> : GradientBrushRenderer<TPixel>
|
||||
where TPixel : unmanaged, IPixel<TPixel>
|
||||
{
|
||||
private const float Tau = MathF.Tau;
|
||||
|
||||
private readonly float cx;
|
||||
|
||||
private readonly float cy;
|
||||
|
||||
private readonly float startRad;
|
||||
|
||||
private readonly float endRad;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SweepGradientBrushRenderer{TPixel}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration instance to use when performing operations.</param>
|
||||
/// <param name="options">The graphics options.</param>
|
||||
/// <param name="canvasWidth">The canvas width for the current render pass.</param>
|
||||
/// <param name="brush">The sweep gradient brush.</param>
|
||||
/// <param name="colorStops">The gradient color stops (ratios in [0..1]).</param>
|
||||
/// <param name="repetitionMode">Defines how gradient colors are repeated outside [0..1].</param>
|
||||
public SweepGradientBrushRenderer(
|
||||
Configuration configuration,
|
||||
GraphicsOptions options,
|
||||
int canvasWidth,
|
||||
SweepGradientBrush brush,
|
||||
ColorStop[] colorStops,
|
||||
GradientRepetitionMode repetitionMode)
|
||||
: base(configuration, options, canvasWidth, colorStops, repetitionMode)
|
||||
{
|
||||
this.cx = brush.Center.X;
|
||||
this.cy = brush.Center.Y;
|
||||
|
||||
// Store the interval as radians once so sampling only needs one subtraction and one divide.
|
||||
float sweepDegrees = GetEffectiveSweepDegrees(brush.StartAngleDegrees, brush.EndAngleDegrees);
|
||||
this.startRad = GeometryUtilities.DegreeToRadian(brush.StartAngleDegrees);
|
||||
this.endRad = GeometryUtilities.DegreeToRadian(brush.StartAngleDegrees + sweepDegrees);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override float PositionOnGradient(float x, float y)
|
||||
{
|
||||
// Move the sample into center-relative coordinates.
|
||||
float dx = x - this.cx;
|
||||
float dy = y - this.cy;
|
||||
|
||||
if (dx == 0f && dy == 0f)
|
||||
{
|
||||
// The center has no unique angle, so pick a stable value on the gradient.
|
||||
return 0f;
|
||||
}
|
||||
|
||||
// Convert from y-down image space back into the brush's y-up angle convention,
|
||||
// then normalize to [0, 2π) so subtraction against the stored start angle is stable.
|
||||
float angle = MathF.Atan2(-dy, dx);
|
||||
if (angle < 0f)
|
||||
{
|
||||
angle += Tau;
|
||||
}
|
||||
|
||||
// Divide by the signed angular span.
|
||||
// A positive denominator produces a counter-clockwise sweep and a negative
|
||||
// denominator produces a clockwise sweep. The base gradient code then applies
|
||||
// the repetition mode to this unbounded parameter.
|
||||
return (angle - this.startRad) / (this.endRad - this.startRad);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user