Skip to content

Commit 51437f2

Browse files
Optimize scenes, tweak docs.
1 parent 853f60a commit 51437f2

5 files changed

Lines changed: 138 additions & 71 deletions

File tree

samples/DrawShapesWithImageSharp/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Typography sheet covering the rich-text surface:
3030
- A "Wrapping and measurement" panel that wraps text with `WrappingLength` and uses `TextMeasurer.MeasureRenderableBounds` (advance ∪ ink) to size a callout that never clips.
3131
- A "Bidi and fallback" panel mixing Latin, Arabic, and Hebrew with `FallbackFontFamilies = [ArabicFontFamily]`.
3232
- A "Vertical mixed layout" panel using `LayoutMode.VerticalMixedRightLeft` for traditional Korean vertical typography with sideways Latin and digit runs.
33-
- Curve text driven by `RichTextOptions.Path = bezier` and `WrappingLength = bezier.ComputeLength()`.
33+
- Curve text drawn with the explicit `canvas.DrawText(options, text, bezier, brush, pen)` path overload and `WrappingLength = bezier.ComputeLength()`.
3434

3535
### `04-image-processing-mask.png`
3636
Image-compositing scene demonstrating four ways a photograph (`tests/Images/Input/Jpg/baseline/Lake.jpg`, linked into this project via `Content Include`) interacts with an `IPath`:

samples/WebGPUExternalSurfaceDemo/Scenes/ManualTextFlowScene.cs

Lines changed: 69 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
using SixLabors.Fonts;
66
using SixLabors.ImageSharp.Drawing;
77
using SixLabors.ImageSharp.Drawing.Processing;
8+
using Brush = SixLabors.ImageSharp.Drawing.Processing.Brush;
89
using Brushes = SixLabors.ImageSharp.Drawing.Processing.Brushes;
910
using Color = SixLabors.ImageSharp.Color;
1011
using Font = SixLabors.Fonts.Font;
1112
using FontFamily = SixLabors.Fonts.FontFamily;
1213
using FontStyle = SixLabors.Fonts.FontStyle;
14+
using Pen = SixLabors.ImageSharp.Drawing.Processing.Pen;
1315
using Pens = SixLabors.ImageSharp.Drawing.Processing.Pens;
1416
using PointF = SixLabors.ImageSharp.PointF;
1517
using Size = SixLabors.ImageSharp.Size;
@@ -67,10 +69,28 @@ internal sealed class ManualTextFlowScene : RenderScene
6769
private static readonly Color SlotColor = Color.SteelBlue.WithAlpha(.24F);
6870
private static readonly Color CircleColor = Color.SteelBlue.WithAlpha(.18F);
6971
private static readonly Color TextColor = Color.ParseHex("#111827");
72+
private static readonly Brush BackgroundBrush = Brushes.Solid(BackgroundColor);
73+
private static readonly Brush PageBrush = Brushes.Solid(PageColor);
74+
private static readonly Brush SlotBrush = Brushes.Solid(SlotColor);
75+
private static readonly Brush ObstacleBrush = Brushes.Solid(CircleColor);
76+
private static readonly Brush TextBrush = Brushes.Solid(TextColor);
77+
private static readonly Pen PageOutlinePen = Pens.Solid(PageOutlineColor, 1.5F);
78+
private static readonly Pen ObstacleOutlinePen = Pens.Solid(Color.SteelBlue, 2F);
7079

7180
private readonly Font bodyFont;
7281
private readonly TextBlock textBlock;
82+
private readonly List<BandInterval> scanBlockedIntervals = new(16);
83+
private readonly List<BandInterval> blockedIntervals = new(16);
84+
private readonly List<BandInterval> slots = new(8);
85+
private readonly List<float> scanYs = new(8);
86+
private readonly List<float> intersections = new(16);
7387
private PointF obstacleCenter;
88+
private ManualTextFlowObstacleShape obstacleShape = ManualTextFlowObstacleShape.Circle;
89+
private IPath? cachedObstaclePath;
90+
private LinearGeometry? cachedObstacleGeometry;
91+
private PointF cachedObstacleCenter;
92+
private float cachedObstacleSize;
93+
private ManualTextFlowObstacleShape cachedObstacleShape;
7494
private bool hasPointer;
7595

7696
public ManualTextFlowScene()
@@ -98,12 +118,26 @@ public ManualTextFlowScene()
98118
/// <summary>
99119
/// Gets or sets the closed shape used as the flow obstacle.
100120
/// </summary>
101-
public ManualTextFlowObstacleShape ObstacleShape { get; set; } = ManualTextFlowObstacleShape.Circle;
121+
public ManualTextFlowObstacleShape ObstacleShape
122+
{
123+
get => this.obstacleShape;
124+
set
125+
{
126+
if (this.obstacleShape == value)
127+
{
128+
return;
129+
}
130+
131+
this.obstacleShape = value;
132+
this.cachedObstaclePath = null;
133+
this.cachedObstacleGeometry = null;
134+
}
135+
}
102136

103137
public override void Paint(DrawingCanvas canvas, TimeSpan deltaTime)
104138
{
105139
Size viewportSize = canvas.Bounds.Size;
106-
canvas.Fill(Brushes.Solid(BackgroundColor), canvas.Bounds);
140+
canvas.Fill(BackgroundBrush, canvas.Bounds);
107141

108142
// The page rectangle is the region we subtract obstacle coverage from.
109143
// Text slots are always produced within these left/right limits.
@@ -132,23 +166,33 @@ public override void Paint(DrawingCanvas canvas, TimeSpan deltaTime)
132166
// The visible obstacle is deliberately just an IPath. The text-flow code
133167
// below never needs to know whether this came from a circle, star, rectangle,
134168
// or any other closed shape that can be linearized by ImageSharp.Drawing.
135-
IPath obstaclePath = this.CreateObstaclePath(obstacleCenter, obstacleSize);
136-
LinearGeometry obstacleGeometry = obstaclePath.ToLinearGeometry(Vector2.One);
169+
// Linearization is the expensive part, so reuse it until the obstacle input
170+
// changes through pointer movement, resizing, or the shape selector.
171+
IPath? obstaclePath = this.cachedObstaclePath;
172+
LinearGeometry? obstacleGeometry = this.cachedObstacleGeometry;
173+
if (obstaclePath is null
174+
|| obstacleGeometry is null
175+
|| !this.cachedObstacleCenter.Equals(obstacleCenter)
176+
|| this.cachedObstacleSize != obstacleSize
177+
|| this.cachedObstacleShape != this.obstacleShape)
178+
{
179+
obstaclePath = this.CreateObstaclePath(obstacleCenter, obstacleSize);
180+
obstacleGeometry = obstaclePath.ToLinearGeometry(Vector2.One);
181+
this.cachedObstaclePath = obstaclePath;
182+
this.cachedObstacleGeometry = obstacleGeometry;
183+
this.cachedObstacleCenter = obstacleCenter;
184+
this.cachedObstacleSize = obstacleSize;
185+
this.cachedObstacleShape = this.obstacleShape;
186+
}
137187

138-
canvas.Fill(Brushes.Solid(PageColor), new RectangularPolygon(pageLeft, pageTop, pageRight - pageLeft, pageBottom - pageTop));
139-
canvas.Draw(Pens.Solid(PageOutlineColor, 1.5F), new RectangularPolygon(pageLeft, pageTop, pageRight - pageLeft, pageBottom - pageTop));
140-
canvas.Fill(Brushes.Solid(CircleColor), obstaclePath);
141-
canvas.Draw(Pens.Solid(Color.SteelBlue, 2F), obstaclePath);
188+
canvas.Fill(PageBrush, new RectangularPolygon(pageLeft, pageTop, pageRight - pageLeft, pageBottom - pageTop));
189+
canvas.Draw(PageOutlinePen, new RectangularPolygon(pageLeft, pageTop, pageRight - pageLeft, pageBottom - pageTop));
190+
canvas.Fill(ObstacleBrush, obstaclePath);
191+
canvas.Draw(ObstacleOutlinePen, obstaclePath);
142192

143193
// Rows can be split into several usable slots by an arbitrary closed path.
144-
// Keep the working buffers outside the row loop so pointer movement does
145-
// not introduce avoidable per-line allocations.
146-
List<BandInterval> scanBlockedIntervals = new(16);
147-
List<BandInterval> blockedIntervals = new(16);
148-
List<BandInterval> slots = new(8);
149-
List<float> scanYs = new(8);
150-
List<float> intersections = new(16);
151-
194+
// The scene owns the temporary buffers so continuous repainting does not
195+
// allocate fresh lists for every row of every frame.
152196
while (hasMoreText && y < pageBottom)
153197
{
154198
float rowProbeHeight = this.bodyFont.Size * 1.45F;
@@ -167,16 +211,16 @@ public override void Paint(DrawingCanvas canvas, TimeSpan deltaTime)
167211
pageRight,
168212
obstaclePadding,
169213
minSlotWidth,
170-
scanBlockedIntervals,
171-
blockedIntervals,
172-
slots,
173-
scanYs,
174-
intersections);
214+
this.scanBlockedIntervals,
215+
this.blockedIntervals,
216+
this.slots,
217+
this.scanYs,
218+
this.intersections);
175219

176220
float rowHeight = rowProbeHeight;
177-
for (int i = 0; i < slots.Count && hasMoreText; i++)
221+
for (int i = 0; i < this.slots.Count && hasMoreText; i++)
178222
{
179-
BandInterval slot = slots[i];
223+
BandInterval slot = this.slots[i];
180224
float slotWidth = slot.Width;
181225

182226
// Each MoveNext call consumes just enough prepared text for the
@@ -194,8 +238,8 @@ public override void Paint(DrawingCanvas canvas, TimeSpan deltaTime)
194238
// The translucent slot fill is a visual aid for the sample. It
195239
// makes the row splitting visible so readers can compare the
196240
// available rectangles with the selected obstacle shape.
197-
canvas.Fill(Brushes.Solid(SlotColor), new RectangularPolygon(slot.Left, y, slotWidth, lineHeight));
198-
canvas.DrawText(line, new PointF(slot.Left, y), Brushes.Solid(TextColor), pen: null);
241+
canvas.Fill(SlotBrush, new RectangularPolygon(slot.Left, y, slotWidth, lineHeight));
242+
canvas.DrawText(line, new PointF(slot.Left, y), TextBrush, pen: null);
199243

200244
rowHeight = MathF.Max(rowHeight, lineHeight);
201245
}

0 commit comments

Comments
 (0)