using Age.Engine.Model;
namespace Age.Engine.Text;
/// AGE's deterministic 32-bit glyph-mask compositor, independent of mask provenance.
public static class AgeGlyphMaskCompositor
{
public static void DrawGlyph(
RgbaImage destination,
GlyphMask mask,
int cellX,
int cellY,
int topToBaseline,
AdvTextStyle style)
{
ArgumentNullException.ThrowIfNull(destination);
ArgumentNullException.ThrowIfNull(mask);
ValidateDestination(destination);
int baselineY = checked(cellY + topToBaseline);
if (style.RenderMode == 1)
DrawMask(destination, mask,
checked(cellX + style.EffectOffsetX),
checked(baselineY + style.EffectOffsetY),
style.EffectColor);
else if (style.RenderMode == 3)
foreach ((int x, int y) in GetMode3OutlineOffsets(
style.EffectOffsetX, style.EffectOffsetY))
DrawMask(destination, mask, checked(cellX + x), checked(baselineY + y),
style.EffectColor);
DrawMask(destination, mask, cellX, baselineY, style.TextColor);
if (style.RenderMode == 2)
DrawMask(destination, mask, cellX, baselineY, style.TextColor, coverageShift: 2);
}
///
/// Draw at a GDI-style baseline. mask.OriginX moves right from the pen and mask.OriginY moves
/// upward from the baseline. coverageShift=2 reproduces AGE's mode-2 quarter-coverage pass.
///
public static bool DrawMask(
RgbaImage destination,
GlyphMask mask,
int baselineX,
int baselineY,
long rgb,
int coverageShift = 0)
{
ArgumentNullException.ThrowIfNull(destination);
ArgumentNullException.ThrowIfNull(mask);
ValidateDestination(destination);
if (coverageShift is < 0 or > 4)
throw new ArgumentOutOfRangeException(nameof(coverageShift));
int red = (int)((rgb >> 16) & 0xff);
int green = (int)((rgb >> 8) & 0xff);
int blue = (int)(rgb & 0xff);
int left = checked(baselineX + mask.OriginX);
int top = checked(baselineY - mask.OriginY);
ReadOnlySpan coverage = mask.Coverage.Span;
bool changed = false;
for (int row = 0; row < mask.Height; row++)
{
int y = checked(top + row);
if ((uint)y >= (uint)destination.Height) continue;
int sourceRow = row * mask.Stride;
for (int column = 0; column < mask.Width; column++)
{
int x = checked(left + column);
if ((uint)x >= (uint)destination.Width) continue;
int level = coverage[sourceRow + column] >> coverageShift;
if (level == 0) continue;
int alpha = level * 255 / 16;
BlendTextPixel(destination.Pixels,
checked((y * destination.Width + x) * 4),
red, green, blue, alpha);
changed = true;
}
}
return changed;
}
public static IReadOnlyList<(int X, int Y)> GetMode3OutlineOffsets(int radiusX, int radiusY)
{
double radius = Math.Sqrt((double)radiusX * radiusX + (double)radiusY * radiusY);
if (radius == 0) return Array.Empty<(int, int)>();
int sampleCount = checked((int)Math.Ceiling(radius * 8.0));
var result = new (int X, int Y)[sampleCount];
double step = 360.0 / (radius * 8.0);
for (int index = 0; index < result.Length; index++)
{
double radians = index * step * Math.PI / 180.0;
result[index] = (
RoundNearest(Math.Cos(radians) * radiusX),
RoundNearest(Math.Sin(radians) * radiusY));
}
return result;
}
private static int RoundNearest(double value)
=> checked((int)(value < 0 ? value - 0.5 : value + 0.5));
private static void BlendTextPixel(
byte[] destination, int offset, int red, int green, int blue, int alpha)
{
int destinationAlpha = destination[offset + 3];
if (alpha == 255 || destinationAlpha == 0)
{
destination[offset] = (byte)red;
destination[offset + 1] = (byte)green;
destination[offset + 2] = (byte)blue;
destination[offset + 3] = (byte)alpha;
return;
}
int inverse = 255 - alpha;
destination[offset] =
(byte)((destination[offset] * inverse + red * alpha) / 255);
destination[offset + 1] =
(byte)((destination[offset + 1] * inverse + green * alpha) / 255);
destination[offset + 2] =
(byte)((destination[offset + 2] * inverse + blue * alpha) / 255);
destination[offset + 3] = (byte)Math.Max(destinationAlpha, alpha);
}
private static void ValidateDestination(RgbaImage destination)
{
if (destination.Width <= 0 || destination.Height <= 0
|| destination.Pixels.Length != checked(destination.Width * destination.Height * 4))
throw new ArgumentException("Destination must be a non-empty tightly packed RGBA image.",
nameof(destination));
}
}