diff --git a/readme.md b/readme.md
index da7ce57..264677c 100644
--- a/readme.md
+++ b/readme.md
@@ -23,25 +23,26 @@ It targets .NET 10+ desktop and web apps, providing a clean and flexible API tha
```csharp
using KappaDuck.Quack.Core;
using KappaDuck.Quack.Events;
+using KappaDuck.Quack.Graphics.Rendering;
using KappaDuck.Quack.Input;
+using KappaDuck.Quack.Video.Pixels;
using KappaDuck.Quack.Windows;
using EngineScope _ = QuackEngine.Init(Subsystem.Video);
-using Window window = new("Quack!", 1920, 1080)
-{
- Resizable = true
-};
+using Window window = new("Quack!", 1920, 1080) { Resizable = true };
+using Renderer renderer = new(window);
while (window.IsOpen)
{
while (window.Poll(out Event e))
{
if (e is QuitRequestedEvent or KeyPressedEvent { Key: Key.Escape })
- {
return;
- }
}
+
+ renderer.Clear(Colors.Black);
+ renderer.Present();
}
```
@@ -102,7 +103,7 @@ During active development, `KappaDuck.Quack` references the **pre-release** vers
| Quack! | Runtimes | SDL3 | SDL_image | SDL_ttf | SDL_mixer |
| :------: | :------------: | :------: | :-------: | :-----: | :-------: |
-| `source` | `0.1.0-beta.2` | `3.4.10` | `3.4.4` | `3.2.2` | `3.2.2` |
+| `source` | `0.1.0-beta.4` | `3.4.12` | `3.4.4` | `3.2.2` | `3.2.4` |
## Development & Sandbox
@@ -129,20 +130,19 @@ The sandbox project requires at least one `.cs` file to compile. All `.cs` files
```csharp
using KappaDuck.Quack.Core;
using KappaDuck.Quack.Events;
+using KappaDuck.Quack.Input;
using KappaDuck.Quack.Windows;
using EngineScope _ = QuackEngine.Init(Subsystem.Video);
-using Window window = new("Quack! Sandbox", 1280, 720);
+
+using Window window = new("Quack!", 1920, 1080) { Resizable = true };
while (window.IsOpen)
{
while (window.Poll(out Event e))
{
- if (e is QuitEvent || e is KeyEvent { Key: Key.Escape })
- {
- window.Close();
+ if (e is QuitRequestedEvent or KeyPressedEvent { Key: Key.Escape })
return;
- }
}
}
```
diff --git a/runtimes/utils/sdl.env b/runtimes/utils/sdl.env
index 1a4a23b..25bdd61 100644
--- a/runtimes/utils/sdl.env
+++ b/runtimes/utils/sdl.env
@@ -1,4 +1,4 @@
-SDL3=3.4.10
+SDL3=3.4.12
SDL3_IMAGE=3.4.4
SDL3_TTF=3.2.2
-SDL3_MIXER=3.2.2
+SDL3_MIXER=3.2.4
diff --git a/src/KappaDuck.Quack/Graphics/Drawing/IDrawable.cs b/src/KappaDuck.Quack/Graphics/Drawing/IDrawable.cs
new file mode 100644
index 0000000..7ad976e
--- /dev/null
+++ b/src/KappaDuck.Quack/Graphics/Drawing/IDrawable.cs
@@ -0,0 +1,25 @@
+// Copyright (c) KappaDuck.
+// Licensed under the MIT license.
+
+using KappaDuck.Quack.Graphics.Rendering;
+
+namespace KappaDuck.Quack.Graphics.Drawing;
+
+///
+/// Represents an object that can draw itself onto an , such as a sprite, shape or anything.
+///
+public interface IDrawable
+{
+ ///
+ /// Draws this object onto .
+ ///
+ ///
+ /// Implementations fold their own transform into (for example with
+ /// state = state with { Transform = state.Transform * localTransform }), set the texture they need, then
+ /// lower themselves to the target's vertex draws. Call rather than
+ /// this method directly.
+ ///
+ /// The target to draw onto.
+ /// The render state to draw with, carrying the accumulated transform, blend mode and texture.
+ void Draw(IRenderTarget target, RenderState state);
+}
diff --git a/src/KappaDuck.Quack/Graphics/Drawing/IRenderTarget.cs b/src/KappaDuck.Quack/Graphics/Drawing/IRenderTarget.cs
new file mode 100644
index 0000000..dea07da
--- /dev/null
+++ b/src/KappaDuck.Quack/Graphics/Drawing/IRenderTarget.cs
@@ -0,0 +1,47 @@
+// Copyright (c) KappaDuck.
+// Licensed under the MIT license.
+
+using KappaDuck.Quack.Graphics.Primitives;
+using KappaDuck.Quack.Graphics.Rendering;
+
+namespace KappaDuck.Quack.Graphics.Drawing;
+
+///
+/// Represents a surface that can be drawn onto, such as a .
+///
+///
+/// Drawables are rendered with ; they in turn lower themselves to the vertex overloads,
+/// which apply the render state's transform, blend mode and texture.
+///
+public interface IRenderTarget
+{
+ ///
+ /// Draws onto this target using the state.
+ ///
+ /// The object to draw.
+ void Draw(IDrawable drawable);
+
+ ///
+ /// Draws onto this target using the given render state.
+ ///
+ /// The object to draw.
+ /// The render state to draw with.
+ void Draw(IDrawable drawable, RenderState state);
+
+ ///
+ /// Draws a sequence of vertices as triangles onto this target.
+ ///
+ /// The render state's transform is applied to each vertex, and its texture, when set, textures the triangles.
+ /// The vertices to draw, taken three at a time as triangles.
+ /// The render state to draw with.
+ void Draw(ReadOnlySpan vertices, RenderState state);
+
+ ///
+ /// Draws indexed vertices as triangles onto this target.
+ ///
+ /// The render state's transform is applied to each vertex, and its texture, when set, textures the triangles.
+ /// The vertices to draw.
+ /// Indices into , taken three at a time as triangles.
+ /// The render state to draw with.
+ void Draw(ReadOnlySpan vertices, ReadOnlySpan indices, RenderState state);
+}
diff --git a/src/KappaDuck.Quack/Graphics/Drawing/Sprite.cs b/src/KappaDuck.Quack/Graphics/Drawing/Sprite.cs
new file mode 100644
index 0000000..40c25a8
--- /dev/null
+++ b/src/KappaDuck.Quack/Graphics/Drawing/Sprite.cs
@@ -0,0 +1,145 @@
+// Copyright (c) KappaDuck.
+// Licensed under the MIT license.
+
+using KappaDuck.Quack.Geometry;
+using KappaDuck.Quack.Graphics.Primitives;
+using KappaDuck.Quack.Graphics.Rendering;
+using KappaDuck.Quack.Video.Pixels;
+
+namespace KappaDuck.Quack.Graphics.Drawing;
+
+///
+/// A drawable image, or a region of one, that can be positioned, rotated, scaled and tinted.
+///
+///
+/// A sprite is a lightweight view onto a : many sprites can share one texture. Draw it with
+/// .
+///
+public class Sprite : Transformable, IDrawable
+{
+ private static readonly int[] _indices = [0, 1, 2, 0, 2, 3];
+ private readonly Vertex[] _vertices = new Vertex[4];
+
+ private Texture _texture;
+ private RectI _region;
+ private Color _color = Colors.White;
+
+ ///
+ /// Creates a sprite that displays the whole of .
+ ///
+ /// The texture to display.
+ public Sprite(Texture texture) : this(texture, new RectI(0, 0, texture.Width, texture.Height))
+ {
+ }
+
+ ///
+ /// Creates a sprite that displays a region of .
+ ///
+ /// The texture to display.
+ /// The region of the texture to display, in texture pixels.
+ public Sprite(Texture texture, RectI region)
+ {
+ _texture = texture;
+ _region = region;
+
+ UpdateVertices();
+ }
+
+ ///
+ /// Gets or sets the texture the sprite displays.
+ ///
+ /// The current is kept; assign it again if the new texture has different dimensions.
+ public Texture Texture
+ {
+ get => _texture;
+ set
+ {
+ _texture = value;
+ UpdateVertices();
+ }
+ }
+
+ ///
+ /// Gets or sets the region of the the sprite displays, in texture pixels.
+ ///
+ public RectI Region
+ {
+ get => _region;
+ set
+ {
+ _region = value;
+ UpdateVertices();
+ }
+ }
+
+ ///
+ /// Gets or sets a color multiplied into the sprite, tinting it.
+ ///
+ /// Defaults to , which leaves the texture unchanged; the color's alpha sets the sprite's opacity.
+ public Color Color
+ {
+ get => _color;
+ set
+ {
+ _color = value;
+ UpdateColors();
+ }
+ }
+
+ ///
+ /// Gets the sprite's bounding rectangle in its own local space, before its transform is applied.
+ ///
+ /// Its size matches , with the top-left corner at (0, 0).
+ public Rect LocalBounds => new(0f, 0f, _region.Width, _region.Height);
+
+ ///
+ /// Gets the sprite's axis-aligned bounding rectangle in its parent's space, after its transform is applied.
+ ///
+ /// When the sprite is rotated, this is the smallest upright rectangle that contains it.
+ public Rect GlobalBounds => Transform.TransformRect(LocalBounds);
+
+ ///
+ public void Draw(IRenderTarget target, RenderState state)
+ {
+ state = state with
+ {
+ Transform = state.Transform * Transform,
+ Texture = _texture
+ };
+
+ target.Draw(_vertices, _indices, state);
+ }
+
+ private void UpdateVertices()
+ {
+ float width = _region.Width;
+ float height = _region.Height;
+
+ _vertices[0].Position = new PointF(0f, 0f);
+ _vertices[1].Position = new PointF(width, 0f);
+ _vertices[2].Position = new PointF(width, height);
+ _vertices[3].Position = new PointF(0f, height);
+
+ float left = (float)_region.X / _texture.Width;
+ float top = (float)_region.Y / _texture.Height;
+ float right = (float)(_region.X + _region.Width) / _texture.Width;
+ float bottom = (float)(_region.Y + _region.Height) / _texture.Height;
+
+ _vertices[0].TextureCoordinate = new PointF(left, top);
+ _vertices[1].TextureCoordinate = new PointF(right, top);
+ _vertices[2].TextureCoordinate = new PointF(right, bottom);
+ _vertices[3].TextureCoordinate = new PointF(left, bottom);
+
+ UpdateColors();
+ }
+
+ private void UpdateColors()
+ {
+ ColorF color = _color.ToColorF();
+
+ _vertices[0].Color = color;
+ _vertices[1].Color = color;
+ _vertices[2].Color = color;
+ _vertices[3].Color = color;
+ }
+}
diff --git a/src/KappaDuck.Quack/Graphics/Rendering/RenderState.cs b/src/KappaDuck.Quack/Graphics/Rendering/RenderState.cs
new file mode 100644
index 0000000..e2281cd
--- /dev/null
+++ b/src/KappaDuck.Quack/Graphics/Rendering/RenderState.cs
@@ -0,0 +1,50 @@
+// Copyright (c) KappaDuck.
+// Licensed under the MIT license.
+
+using KappaDuck.Quack.Geometry;
+using KappaDuck.Quack.Graphics.Drawing;
+
+namespace KappaDuck.Quack.Graphics.Rendering;
+
+///
+/// The set of parameters that control how a drawable is rendered onto an : the transform
+/// applied to its vertices, the blend mode, and the texture it is drawn with.
+///
+///
+/// Create a render state with an object initializer, for example new RenderState { Texture = texture }, or from
+/// , then layer on additional transforms with a with expression. Avoid
+/// default(RenderState): it leaves the unset rather than at
+/// .
+///
+public readonly record struct RenderState
+{
+ ///
+ /// Creates a render state with an identity , alpha blending and no texture.
+ ///
+ public RenderState()
+ {
+ Transform = Transform.Identity;
+ BlendMode = BlendMode.Blend;
+ Texture = null;
+ }
+
+ ///
+ /// Gets the default render state: an identity transform, alpha blending and no texture.
+ ///
+ public static RenderState Default { get; } = new();
+
+ ///
+ /// Gets the transform applied to the vertices before they are drawn. Defaults to .
+ ///
+ public Transform Transform { get; init; }
+
+ ///
+ /// Gets the blend mode used to combine the drawing with the target. Defaults to .
+ ///
+ public BlendMode BlendMode { get; init; }
+
+ ///
+ /// Gets the texture the vertices are drawn with, or to draw untextured geometry.
+ ///
+ public Texture? Texture { get; init; }
+}
diff --git a/src/KappaDuck.Quack/Graphics/Rendering/Renderer.cs b/src/KappaDuck.Quack/Graphics/Rendering/Renderer.cs
index 4a1d369..71f9491 100644
--- a/src/KappaDuck.Quack/Graphics/Rendering/Renderer.cs
+++ b/src/KappaDuck.Quack/Graphics/Rendering/Renderer.cs
@@ -9,6 +9,7 @@
using KappaDuck.Quack.Video;
using KappaDuck.Quack.Video.Pixels;
using KappaDuck.Quack.Windows;
+using System.Buffers;
using System.ComponentModel;
namespace KappaDuck.Quack.Graphics.Rendering;
@@ -20,8 +21,10 @@ namespace KappaDuck.Quack.Graphics.Rendering;
/// A window can host only one renderer or surface at a time; creating a renderer binds the
/// window until the renderer is disposed.
///
-public sealed class Renderer : IDisposable
+public sealed class Renderer : IRenderTarget, IDisposable
{
+ private const int MaxStackVertices = 64;
+
private SDL_Renderer* _handle;
private Window? _window;
@@ -377,7 +380,7 @@ public Vector2 Scale
}
///
- /// Gets or sets the texture addressing mode used in or .
+ /// Gets or sets the texture addressing mode used in or .
///
/// The renderer is disposed.
public (TextureAddressMode Horizontal, TextureAddressMode Vertical) TextureAddressMode
@@ -568,27 +571,22 @@ public void Dispose()
_handle = null;
}
- ///
- /// Draws the vertices to the render target.
- ///
- /// The vertices to draw to the render target.
- public void Draw(ReadOnlySpan vertices)
- {
- ThrowIfDisposed();
- SDLThrowHelper.ThrowIfFailed(SDL3.UnsafeRenderGeometry(_handle, null, vertices, vertices.Length, null, 0));
- }
+ ///
+ public void Draw(IDrawable drawable) => Draw(drawable, RenderState.Default);
- ///
- /// Draws the vertices to the render target.
- ///
- /// The vertices to draw to the render target.
- /// The indices to draw the vertices in the correct order.
- public void Draw(ReadOnlySpan vertices, ReadOnlySpan indices)
+ ///
+ public void Draw(IDrawable drawable, RenderState state)
{
ThrowIfDisposed();
- SDLThrowHelper.ThrowIfFailed(SDL3.RenderGeometry(_handle, null, vertices, vertices.Length, indices, indices.Length));
+ drawable.Draw(this, state);
}
+ ///
+ public void Draw(ReadOnlySpan vertices, RenderState state) => DrawGeometry(vertices, [], state);
+
+ ///
+ public void Draw(ReadOnlySpan vertices, ReadOnlySpan indices, RenderState state) => DrawGeometry(vertices, indices, state);
+
///
/// Draws a single point.
///
@@ -993,5 +991,48 @@ public void SetDrawingColor(ColorF color)
SDLThrowHelper.ThrowIfFailed(SDL3.SetRenderDrawColorFloat(_handle, color.R, color.G, color.B, color.A));
}
+ private void DrawGeometry(ReadOnlySpan vertices, ReadOnlySpan indices, RenderState state)
+ {
+ ThrowIfDisposed();
+
+ BlendMode = state.BlendMode;
+ SDL_Texture* texture = state.Texture?.Handle;
+
+ if (state.Transform == Transform.Identity || state.Transform == default)
+ {
+ Submit(texture, vertices, indices);
+ return;
+ }
+
+ int count = vertices.Length;
+
+ Vertex[]? rented = count > MaxStackVertices ? ArrayPool.Shared.Rent(count) : null;
+ Span transformed = rented ?? stackalloc Vertex[count];
+
+ for (int i = 0; i < count; i++)
+ {
+ Vertex vertex = vertices[i];
+ vertex.Position = state.Transform.TransformPoint(vertex.Position);
+
+ transformed[i] = vertex;
+ }
+
+ Submit(texture, transformed, indices);
+
+ if (rented is not null)
+ ArrayPool.Shared.Return(rented);
+ }
+
+ private void Submit(SDL_Texture* texture, ReadOnlySpan vertices, ReadOnlySpan indices)
+ {
+ if (indices.IsEmpty)
+ {
+ SDLThrowHelper.ThrowIfFailed(SDL3.UnsafeRenderGeometry(_handle, texture, vertices, vertices.Length, null, 0));
+ return;
+ }
+
+ SDLThrowHelper.ThrowIfFailed(SDL3.RenderGeometry(_handle, texture, vertices, vertices.Length, indices, indices.Length));
+ }
+
private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_handle is null, typeof(Renderer));
}
diff --git a/src/KappaDuck.Quack/Graphics/Rendering/Transformable.cs b/src/KappaDuck.Quack/Graphics/Rendering/Transformable.cs
new file mode 100644
index 0000000..69ceae3
--- /dev/null
+++ b/src/KappaDuck.Quack/Graphics/Rendering/Transformable.cs
@@ -0,0 +1,132 @@
+// Copyright (c) KappaDuck.
+// Licensed under the MIT license.
+
+using KappaDuck.Quack.Geometry;
+
+namespace KappaDuck.Quack.Graphics.Rendering;
+
+///
+/// A base class for objects that can be positioned, rotated, scaled and pivoted in 2D space.
+///
+///
+/// The is derived from , , and
+/// , and is recomputed only when one of them changes.
+///
+public abstract class Transformable
+{
+ private bool _dirty;
+
+ ///
+ /// Gets or sets the position of the object's in its parent's space. Defaults to (0, 0).
+ ///
+ public PointF Position
+ {
+ get;
+ set
+ {
+ field = value;
+ _dirty = true;
+ }
+ }
+
+ ///
+ /// Gets or sets the local point that refers to and that rotation and scaling pivot around.
+ ///
+ /// Defaults to (0, 0), the top-left corner. Set it to the object's center to rotate and scale about the middle.
+ public PointF Origin
+ {
+ get;
+ set
+ {
+ field = value;
+ _dirty = true;
+ }
+ }
+
+ ///
+ /// Gets or sets the rotation applied to the object, clockwise on screen. Defaults to no rotation.
+ ///
+ public Angle Rotation
+ {
+ get;
+ set
+ {
+ field = value;
+ _dirty = true;
+ }
+ }
+
+ ///
+ /// Gets or sets the horizontal and vertical scale factors. Defaults to (1, 1).
+ ///
+ public Vector2 Scale
+ {
+ get;
+ set
+ {
+ field = value;
+ _dirty = true;
+ }
+ } = Vector2.One;
+
+ ///
+ /// Gets the combined transform that places the object in its parent's space, built from
+ /// , , and .
+ ///
+ public Transform Transform
+ {
+ get
+ {
+ if (_dirty)
+ {
+ field = Transform.Create(Position, Rotation, Scale, Origin);
+ _dirty = false;
+ }
+
+ return field;
+ }
+ } = Transform.Identity;
+
+ ///
+ /// Moves the object relative to its current .
+ ///
+ /// The displacement to add to the position.
+ public void Move(Vector2 offset) => Position += offset;
+
+ ///
+ /// Moves the object towards a target position by at most , stopping exactly at it once within reach.
+ ///
+ /// The position to move towards.
+ /// The maximum distance to move this step. Should be non-negative.
+ public void MoveTowards(PointF target, float maxDistance)
+ {
+ Vector2 displacement = target - Position;
+ float distance = displacement.Magnitude;
+
+ if (distance <= maxDistance || MathF.ApproximatelyZero(distance))
+ {
+ Position = target;
+ return;
+ }
+
+ Position += displacement / distance * maxDistance;
+ }
+
+ ///
+ /// Rotates the object relative to its current .
+ ///
+ /// The angle to add to the rotation, clockwise on screen.
+ public void Rotate(Angle angle) => Rotation += angle;
+
+ ///
+ /// Scales the object relative to its current .
+ ///
+ /// The horizontal and vertical factors to multiply the scale by.
+ public void ScaleBy(Vector2 factor) => Scale *= factor;
+
+ ///
+ /// Scales the object uniformly relative to its current .
+ ///
+ /// The factor to multiply both scale components by.
+ public void ScaleBy(float factor) => Scale *= factor;
+}
diff --git a/tests/Unit.Tests/UI/Progress/Reporters/AsyncIndeterminateProgressReporterTests.cs b/tests/Unit.Tests/UI/Progress/Reporters/AsyncIndeterminateProgressReporterTests.cs
index c9b3e7c..d53f4a4 100644
--- a/tests/Unit.Tests/UI/Progress/Reporters/AsyncIndeterminateProgressReporterTests.cs
+++ b/tests/Unit.Tests/UI/Progress/Reporters/AsyncIndeterminateProgressReporterTests.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using KappaDuck.Quack.UI.Progress.Reporters;
+using System.Diagnostics.CodeAnalysis;
namespace Unit.Tests.UI.Progress.Reporters;
@@ -12,6 +13,7 @@ internal sealed class AsyncIndeterminateProgressReporterTests : IDisposable
public void Dispose() => _reporter.Dispose();
[Test]
+ [SuppressMessage("Performance", "CA1849:Call async methods when in an async method", Justification = "The test requires to test the method Cancel")]
public async Task CancelShouldRequestToCancelTheToken()
{
_reporter.Cancel();
diff --git a/tests/Unit.Tests/UI/Progress/Reporters/AsyncProgressReporterTests.cs b/tests/Unit.Tests/UI/Progress/Reporters/AsyncProgressReporterTests.cs
index fc6a31b..60a1771 100644
--- a/tests/Unit.Tests/UI/Progress/Reporters/AsyncProgressReporterTests.cs
+++ b/tests/Unit.Tests/UI/Progress/Reporters/AsyncProgressReporterTests.cs
@@ -3,6 +3,7 @@
using KappaDuck.Quack.UI.Progress;
using KappaDuck.Quack.UI.Progress.Reporters;
+using System.Diagnostics.CodeAnalysis;
namespace Unit.Tests.UI.Progress.Reporters;
@@ -19,6 +20,7 @@ public AsyncProgressReporterTests()
public void Dispose() => _reporter.Dispose();
[Test]
+ [SuppressMessage("Performance", "CA1849:Call async methods when in an async method", Justification = "The test requires to test the method Cancel")]
public async Task CancelShouldRequestToCancelTheToken()
{
_reporter.Cancel();