Editor/HotCodeEditor/Ui/MarkRasterizer.cs

Rasterizer for the project logo mark. It scans vector shapes per layer into an RGBA8888 pixel buffer, supports even-odd fills, cutouts, antialiasing via supersampled scanlines, optional rounded tile background and premultiplied alpha compositing.

Native Interop
using System;
using System.Collections.Generic;

/// <summary>
/// One layer of the logo: shapes filled even-odd in a colour, or a "cut" that punches through to the background.
/// </summary>
public readonly record struct LogoLayer( (byte R, byte G, byte B) Color, bool Cut, (float X, float Y)[][] Shapes );

/// <summary>
/// Paints the logo's layers into an RGBA image with smooth edges. Paint.DrawPolygon can't do even-odd fills
/// or cut-outs (the logo's &lt;/&gt; is punched through the pepper), so the logo is rasterized here instead.
/// Each layer is a scanline pass over all its edges, composited in order.
/// </summary>
public static class MarkRasterizer
{
	const int SubRows = 4;

	/// <summary>
	/// Renders layers (unit box, y down) as RGBA8888, placed at (offsetX, offsetY) and scaled by
	/// <paramref name="scale"/> (pixels). With <paramref name="tileColor"/> it all sits on a rounded tile in that
	/// colour (cuts show the tile); without, the background is transparent (cuts are see-through).
	/// </summary>
	public static byte[] Render( int size, IReadOnlyList<LogoLayer> layers, float offsetX, float offsetY, float scale,
		(byte R, byte G, byte B)? tileColor, float cornerRadius )
	{
		var count = size * size;

		// Premultiplied colour + alpha, so "over" and "cut" are simple blends
		var r = new float[count];
		var g = new float[count];
		var b = new float[count];
		var a = new float[count];

		if ( tileColor is { } tile )
		{
			Array.Fill( r, tile.R / 255f );
			Array.Fill( g, tile.G / 255f );
			Array.Fill( b, tile.B / 255f );
			Array.Fill( a, 1f );
		}

		foreach ( var layer in layers )
		{
			var coverage = Coverage( size, layer.Shapes, offsetX, offsetY, scale );
			var (cr, cg, cb) = layer.Cut && tileColor is { } bg
				? (bg.R / 255f, bg.G / 255f, bg.B / 255f)
				: (layer.Color.R / 255f, layer.Color.G / 255f, layer.Color.B / 255f);

			for ( int i = 0; i < count; i++ )
			{
				var c = Math.Clamp( coverage[i], 0, 1 );
				if ( c <= 0 ) continue;

				if ( layer.Cut && tileColor is null )
				{
					// Punch a hole: everything underneath fades out
					r[i] *= 1 - c; g[i] *= 1 - c; b[i] *= 1 - c; a[i] *= 1 - c;
					continue;
				}

				r[i] = r[i] * (1 - c) + cr * c;
				g[i] = g[i] * (1 - c) + cg * c;
				b[i] = b[i] * (1 - c) + cb * c;
				a[i] = a[i] * (1 - c) + c;
			}
		}

		var pixels = new byte[count * 4];
		for ( int py = 0; py < size; py++ )
		{
			for ( int px = 0; px < size; px++ )
			{
				var i = py * size + px;
				var alpha = a[i];
				var o = i * 4;

				if ( alpha > 0 )
				{
					pixels[o + 0] = ToByte( r[i] / alpha );
					pixels[o + 1] = ToByte( g[i] / alpha );
					pixels[o + 2] = ToByte( b[i] / alpha );
				}

				if ( tileColor is not null ) alpha *= RoundedRectCoverage( px + 0.5f, py + 0.5f, size, cornerRadius );
				pixels[o + 3] = ToByte( alpha );
			}
		}

		return pixels;
	}

	/// <summary>
	/// Even-odd coverage of a set of shapes, per pixel. All edges go into one scanline pass, so overlapping
	/// shapes cancel out, which is what lets the &lt;/&gt; strokes cross cleanly.
	/// </summary>
	private static float[] Coverage( int size, (float X, float Y)[][] shapes, float offsetX, float offsetY, float scale )
	{
		var coverage = new float[size * size];
		var crossings = new List<float>();

		var edges = new List<(float X0, float Y0, float X1, float Y1)>();
		foreach ( var shape in shapes )
		{
			for ( int i = 0; i < shape.Length; i++ )
			{
				var p = shape[i];
				var q = shape[(i + 1) % shape.Length];
				if ( p.Y == q.Y ) continue;
				edges.Add( (offsetX + p.X * scale, offsetY + p.Y * scale, offsetX + q.X * scale, offsetY + q.Y * scale) );
			}
		}

		for ( int py = 0; py < size; py++ )
		{
			for ( int sub = 0; sub < SubRows; sub++ )
			{
				var y = py + (sub + 0.5f) / SubRows;

				crossings.Clear();
				foreach ( var e in edges )
				{
					if ( (y >= e.Y0) == (y >= e.Y1) ) continue;
					crossings.Add( e.X0 + (y - e.Y0) * (e.X1 - e.X0) / (e.Y1 - e.Y0) );
				}

				crossings.Sort();
				for ( int k = 0; k + 1 < crossings.Count; k += 2 )
					AddSpan( coverage, py * size, size, crossings[k], crossings[k + 1], 1f / SubRows );
			}
		}

		return coverage;
	}

	/// <summary>
	/// Adds a horizontal span's coverage to a row, with fractional coverage for the end pixels.
	/// </summary>
	private static void AddSpan( float[] coverage, int row, int width, float a, float b, float weight )
	{
		a = Math.Clamp( a, 0, width );
		b = Math.Clamp( b, 0, width );
		if ( b <= a ) return;

		var ia = (int)a;
		var ib = (int)b;

		if ( ia == ib )
		{
			coverage[row + Math.Min( ia, width - 1 )] += (b - a) * weight;
			return;
		}

		coverage[row + ia] += (ia + 1 - a) * weight;
		for ( int i = ia + 1; i < ib; i++ ) coverage[row + i] += weight;
		if ( ib < width ) coverage[row + ib] += (b - ib) * weight;
	}

	/// <summary>
	/// How much of a pixel (by its centre) is inside a size x size square with rounded corners.
	/// </summary>
	private static float RoundedRectCoverage( float x, float y, int size, float radius )
	{
		var half = size / 2f;
		var qx = MathF.Abs( x - half ) - (half - radius);
		var qy = MathF.Abs( y - half ) - (half - radius);
		var outside = MathF.Sqrt( MathF.Max( qx, 0 ) * MathF.Max( qx, 0 ) + MathF.Max( qy, 0 ) * MathF.Max( qy, 0 ) );
		var distance = outside + MathF.Min( MathF.Max( qx, qy ), 0 ) - radius;
		return Math.Clamp( 0.5f - distance, 0, 1 );
	}

	private static byte ToByte( float v ) => (byte)Math.Round( Math.Clamp( v, 0, 1 ) * 255 );
}