using XFESystem.Desktop; namespace XFESystem.Browser; /// /// A bounded, transparent command tile. Compose source pixels once, then inverse /// sample them into the destination so downscaling does not repeatedly blend /// adjacent source rows over one another. No framebuffer readback is required. /// internal sealed class BrowserAffineRasterCanvas : IDesktopCoverageCanvas, IDesktopRenderYieldCanvas { private readonly IDesktopCanvas target; private readonly BrowserCanvasSurface surface; private readonly BrowserAffineTransform inverse; private readonly BrowserRect destination; private readonly int left, top; public BrowserRect SourceClip { get; } public BrowserAffineRasterCanvas(IDesktopCanvas target, BrowserRect bounds, BrowserRect clip, BrowserAffineTransform matrix) { this.target = target; if (!matrix.TryInverse(out inverse)) throw new ArgumentException("A singular transform cannot be painted."); double scale = ScalePercent / 100d; SourceClip = inverse.Bounds(clip).Intersect(bounds); left = (int)Math.Floor(SourceClip.Left * scale) - 1; top = (int)Math.Floor(SourceClip.Top * scale) - 1; int width = Math.Max(0, (int)Math.Ceiling(SourceClip.Right * scale) - left + 1); int height = Math.Max(0, (int)Math.Ceiling(SourceClip.Bottom * scale) - top + 1); if ((long)width * height > 16_777_216) throw new BrowserLimitException("The transformed paint tile exceeds its pixel limit."); surface = new(width, height); destination = matrix.Bounds(SourceClip).Intersect(clip).Intersect(new(0, 0, Width, Height)); } public int Width => target.Width; public int Height => target.Height; public int NativeWidth => target.NativeWidth; public int NativeHeight => target.NativeHeight; public int ScalePercent => target.ScalePercent; public bool RenderYieldRequested => target is IDesktopRenderYieldCanvas yielding && yielding.RenderYieldRequested; public void BlendCoverage(uint rgb, int x, int y, int width, int height, ReadOnlySpan coverage) { if (width < 0 || height < 0 || (long)width * height != coverage.Length) throw new ArgumentException("Invalid transformed coverage dimensions."); int x0 = (int)Math.Clamp((long)left - x, 0, width), x1 = (int)Math.Clamp((long)left + surface.Width - x, 0, width); int y0 = (int)Math.Clamp((long)top - y, 0, height), y1 = (int)Math.Clamp((long)top + surface.Height - y, 0, height); for (int row = y0; row < y1; row++) { if (RenderYieldRequested) throw new DesktopRenderYieldException(); for (int column = x0; column < x1; column++) surface.BlendPixel(x + column - left, y + row - top, (uint)coverage[row * width + column] << 24 | rgb); } } public void Fill(uint rgb, int x, int y, int width, int height) { double scale = ScalePercent / 100d; int x0 = (int)Math.Round(x * scale), x1 = (int)Math.Round((x + (double)width) * scale); int y0 = (int)Math.Round(y * scale), y1 = (int)Math.Round((y + (double)height) * scale); surface.Fill(x0 - left, y0 - top, x1 - x0, y1 - y0, 0xFF000000 | rgb); } public void Text(string text, uint rgb, int x, int y) => throw new NotSupportedException("Transformed text requires glyph coverage."); public void Line(uint rgb, int x1, int y1, int x2, int y2) => throw new NotSupportedException("Transformed lines require coverage."); public void Present() { } public void Flush() { double scale = ScalePercent / 100d; int x0 = Math.Max(0, (int)Math.Floor(destination.Left * scale)), x1 = Math.Min(NativeWidth, (int)Math.Ceiling(destination.Right * scale)); int y0 = Math.Max(0, (int)Math.Floor(destination.Top * scale)), y1 = Math.Min(NativeHeight, (int)Math.Ceiling(destination.Bottom * scale)); if (x1 <= x0 || y1 <= y0) return; var alpha = new byte[x1 - x0]; var colors = new uint[alpha.Length]; for (int y = y0; y < y1; y++) { if (RenderYieldRequested) throw new DesktopRenderYieldException(); for (int x = x0; x < x1; x++) { double a = 0, r = 0, g = 0, b = 0; // Four destination subpixels retain thin rotated/downscaled edges. for (int sample = 0; sample < 4; sample++) { double ox = (sample & 1) == 0 ? .25 : .75, oy = sample < 2 ? .25 : .75; var source = inverse.Point((x + ox) / scale, (y + oy) / scale); double sx = source.X * scale - left - .5, sy = source.Y * scale - top - .5; int ix = (int)Math.Floor(sx), iy = (int)Math.Floor(sy); for (int row = 0; row < 2; row++) for (int column = 0; column < 2; column++) { uint pixel = surface.Pixel(ix + column, iy + row); double weight = (column == 0 ? 1 - (sx - ix) : sx - ix) * (row == 0 ? 1 - (sy - iy) : sy - iy) / 4; double coverage = (pixel >> 24) * weight; a += coverage; r += ((pixel >> 16) & 255) * coverage; g += ((pixel >> 8) & 255) * coverage; b += (pixel & 255) * coverage; } } alpha[x - x0] = (byte)Math.Clamp(Math.Round(a), 0, 255); colors[x - x0] = a <= 0 ? 0 : (uint)Math.Clamp(Math.Round(r / a), 0, 255) << 16 | (uint)Math.Clamp(Math.Round(g / a), 0, 255) << 8 | (uint)Math.Clamp(Math.Round(b / a), 0, 255); } for (int start = 0; start < alpha.Length;) { int end = start + 1; while (end < alpha.Length && colors[end] == colors[start]) end++; if (target is IDesktopCoverageCanvas coverage) coverage.BlendCoverage(colors[start], x0 + start, y, end - start, 1, alpha.AsSpan(start, end - start)); else if (alpha[start] > 0) target.Fill(colors[start], (int)((x0 + start) / scale), (int)(y / scale), Math.Max(1, (int)((end - start) / scale)), Math.Max(1, (int)(1 / scale))); start = end; } } } }