Compositor Effects

SharpConsoleUI provides a powerful compositor-style buffer manipulation API that enables advanced visual effects like transitions, filters, blur, and custom rendering overlays. This system exposes the internal CharacterBuffer through safe, event-based hooks that fire at precise points in the rendering pipeline.

Table of Contents

  1. Overview
  2. Core API
  3. Quick Start
  4. Use Cases
  5. Examples
  6. Best Practices
  7. Performance Considerations
  8. Thread Safety
  9. API Reference
  10. CanvasControl: Per-Control Drawing Surface

Overview

The compositor effects system allows you to manipulate the rendered buffer after controls have painted but before conversion to ANSI strings. This is the ideal hook point for applying post-processing effects without interfering with the normal rendering pipeline.

Key Features

  • Pre-Paint Hook: PreBufferPaint event fires after buffer clear, before controls paint (for backgrounds)
  • Post-Paint Hook: PostBufferPaint event fires after painting, before ANSI conversion (for effects)
  • Direct Buffer Access: Full CharacterBuffer API for cell-level manipulation
  • Immutable Snapshots: BufferSnapshot for safe buffer capture (screenshots, recording)
  • Zero Overhead: Event system has no cost when not used
  • Thread Safe: Event fires within existing render lock
  • Flexible: Supports transitions, filters, overlays, and custom effects

Architecture

Window Rendering Pipeline:
┌─────────────────────┐
│ RebuildDOMTree()    │  Build layout nodes
└──────────┬──────────┘
           │
┌──────────▼──────────┐
│ PerformDOMLayout()  │  Measure & Arrange
└──────────┬──────────┘
           │
┌──────────▼──────────┐
│ Buffer.Clear()      │  Clear buffer with background color
└──────────┬──────────┘
           │
┌──────────▼──────────────────────┐
│ PreBufferPaint Event Fires      │  ◄── BACKGROUNDS GO HERE
│ (Paint backgrounds, fractals)   │      (before controls)
└──────────┬──────────────────────┘
           │
┌──────────▼──────────┐
│ PaintDOM()          │  Paint controls to CharacterBuffer
└──────────┬──────────┘
           │
┌──────────▼──────────────────────┐
│ PostBufferPaint Event Fires     │  ◄── EFFECTS GO HERE
│ (Fade, blur, overlays)          │      (after controls)
└──────────┬──────────────────────┘
           │
┌──────────▼──────────┐
│ BufferToLines()     │  Convert to ANSI strings
└─────────────────────┘

Core API

1. Window.Renderer Property

Exposes the window's internal renderer for accessing rendering internals.

public WindowRenderer? Renderer { get; }

2. WindowRenderer.Buffer Property

Provides direct access to the character buffer.

public CharacterBuffer? Buffer { get; }

CAUTION: Direct buffer manipulation should only be done via the PostBufferPaint event to avoid race conditions. Reading is safe at any time.

3. WindowRenderer.PreBufferPaint Event

Event that fires after the buffer is cleared but before controls are painted. Ideal for custom backgrounds.

public delegate void BufferPaintDelegate(
    CharacterBuffer buffer,
    LayoutRect dirtyRegion,
    LayoutRect clipRect);

public event BufferPaintDelegate? PreBufferPaint;

Parameters:

  • buffer: The character buffer (cleared with background color)
  • dirtyRegion: The region being painted (or full bounds if entire buffer)
  • clipRect: The clipping rectangle used during paint

Use cases:

  • Animated backgrounds (fractals, patterns)
  • Custom window backgrounds
  • Full-buffer graphics that controls render ON TOP of

4. WindowRenderer.PostBufferPaint Event

Event that fires after painting controls but before converting to ANSI strings.

public delegate void BufferPaintDelegate(
    CharacterBuffer buffer,
    LayoutRect dirtyRegion,
    LayoutRect clipRect);

public event BufferPaintDelegate? PostBufferPaint;

Parameters:

  • buffer: The character buffer that was just painted
  • dirtyRegion: The region that was painted (or full bounds if entire buffer)
  • clipRect: The clipping rectangle used during paint

Use cases:

  • Transitions (fade in/out)
  • Filters (blur, color grading)
  • Overlays (glow, highlights)

5. BufferSnapshot

Immutable snapshot of a CharacterBuffer at a point in time.

public readonly record struct BufferSnapshot(int Width, int Height, Cell[,] Cells)
{
    public Cell GetCell(int x, int y);
    // Cell.Character is a Rune (full Unicode scalar value)
    // Cell.IsWideContinuation marks right-half of wide characters
    // Cell.Combiners stores zero-width combining marks
}

Creating Snapshots:

public BufferSnapshot CreateSnapshot();

Snapshots perform a deep copy of all cells, creating an independent copy safe for concurrent access, serialization, or comparison.

Quick Start

Basic Effect Example

public class MyWindow : Window
{
    public MyWindow(ConsoleWindowSystem windowSystem) : base(windowSystem)
    {
        Title = "Effect Demo";

        // Subscribe to post-paint event
        Renderer.PostBufferPaint += ApplyMyEffect;
    }

    private void ApplyMyEffect(CharacterBuffer buffer, LayoutRect dirtyRegion, LayoutRect clipRect)
    {
        // Manipulate buffer after painting
        for (int y = 0; y < buffer.Height; y++)
        {
            for (int x = 0; x < buffer.Width; x++)
            {
                var cell = buffer.GetCell(x, y);
                if (cell.IsWideContinuation) continue; // Skip wide char right-half
                // Modify cell colors, characters, etc.
                buffer.SetCell(x, y, cell.Character, modifiedFg, modifiedBg);
            }
        }
    }
}

Screenshot Example

private void TakeScreenshot()
{
    var buffer = Renderer?.Buffer;
    if (buffer == null) return;

    var snapshot = buffer.CreateSnapshot();

    // Convert to text (handles wide chars and Rune encoding)
    var lines = new List<string>();
    for (int y = 0; y < snapshot.Height; y++)
    {
        var sb = new StringBuilder();
        for (int x = 0; x < snapshot.Width; x++)
        {
            var cell = snapshot.GetCell(x, y);
            if (cell.IsWideContinuation) continue; // Skip continuation cells
            sb.AppendRune(cell.Character); // Rune → UTF-16 encoding
        }
        lines.Add(sb.ToString());
    }

    File.WriteAllLines("screenshot.txt", lines);
}

Use Cases

1. Transition Effects

For fade and slide transitions, use the built-in Animation Framework which handles timing, easing, and cleanup automatically:

// Fade in
WindowAnimations.FadeIn(window);

// Fade out and close
WindowAnimations.FadeOut(window, onComplete: () =>
    ws.CloseWindow(window));

// Slide in from left
WindowAnimations.SlideIn(window, SlideDirection.Left);

These use PostBufferPaint hooks internally. For custom transition effects, you can use the same pattern with ColorBlendHelper:

ColorBlendHelper.ApplyColorOverlay(buffer, Color.Black, intensity, foregroundBlendRatio: 1.0f);

2. Blur and Filter Effects

Apply post-processing filters like blur for modal backgrounds or focus effects.

Example: Box blur

private void ApplyBlur(CharacterBuffer buffer, LayoutRect dirtyRegion, LayoutRect clipRect)
{
    int radius = 2;
    var blurred = new CharacterBuffer(buffer.Width, buffer.Height);

    for (int y = 0; y < buffer.Height; y++)
    {
        for (int x = 0; x < buffer.Width; x++)
        {
            // Skip continuation cells (right half of wide characters)
            var cell = buffer.GetCell(x, y);
            if (cell.IsWideContinuation) continue;

            var avgFg = AverageColorInRadius(buffer, x, y, radius, c => c.Foreground);
            var avgBg = AverageColorInRadius(buffer, x, y, radius, c => c.Background);

            blurred.SetCell(x, y, '░', avgFg, avgBg);
        }
    }

    // Copy blurred buffer back
    buffer.CopyFrom(blurred, LayoutRect.FromDimensions(0, 0, buffer.Width, buffer.Height));
}

private Color AverageColorInRadius(CharacterBuffer buffer, int cx, int cy, int radius,
    Func<Cell, Color> selector)
{
    int r = 0, g = 0, b = 0, count = 0;

    for (int dy = -radius; dy <= radius; dy++)
    {
        for (int dx = -radius; dx <= radius; dx++)
        {
            int x = cx + dx, y = cy + dy;
            if (x >= 0 && x < buffer.Width && y >= 0 && y < buffer.Height)
            {
                var color = selector(buffer.GetCell(x, y));
                r += color.R;
                g += color.G;
                b += color.B;
                count++;
            }
        }
    }

    return Color.FromArgb((byte)(r / count), (byte)(g / count), (byte)(b / count));
}

3. Custom Overlays

Draw glow effects, highlights, or decorations on top of rendered content.

Example: Glow around focused control

private void DrawFocusGlow(CharacterBuffer buffer, LayoutRect dirtyRegion, LayoutRect clipRect)
{
    var focusedControl = GetFocusedControl();
    if (focusedControl == null) return;

    var layoutNode = Renderer.GetLayoutNode(focusedControl);
    if (layoutNode == null) return;

    var bounds = layoutNode.AbsoluteBounds;
    Color glowColor = Color.Cyan;

    // Draw glow border (skip wide char continuation cells to avoid breaking pairs)
    for (int x = bounds.Left - 1; x <= bounds.Right; x++)
    {
        if (x >= 0 && x < buffer.Width)
        {
            // Top
            if (bounds.Top - 1 >= 0)
            {
                var cell = buffer.GetCell(x, bounds.Top - 1);
                if (!cell.IsWideContinuation)
                    buffer.SetCell(x, bounds.Top - 1, cell.Character, glowColor, cell.Background);
            }

            // Bottom
            if (bounds.Bottom < buffer.Height)
            {
                var cell = buffer.GetCell(x, bounds.Bottom);
                if (!cell.IsWideContinuation)
                    buffer.SetCell(x, bounds.Bottom, cell.Character, glowColor, cell.Background);
            }
        }
    }
}

4. Screenshots and Recording

Capture buffer state for saving to file or creating recordings.

Example: Save screenshot

private void TakeScreenshot()
{
    var buffer = Renderer?.Buffer;
    if (buffer == null) return;

    var snapshot = buffer.CreateSnapshot();
    var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
    var filename = $"screenshot_{timestamp}.txt";

    var lines = new List<string>
    {
        $"=== Screenshot captured at {DateTime.Now} ===",
        $"Size: {snapshot.Width} x {snapshot.Height}",
        new string('=', 60),
        ""
    };

    for (int y = 0; y < snapshot.Height; y++)
    {
        var sb = new StringBuilder();
        for (int x = 0; x < snapshot.Width; x++)
        {
            var cell = snapshot.GetCell(x, y);
            if (cell.IsWideContinuation) continue; // Skip wide char right-half
            sb.AppendRune(cell.Character); // Rune → proper UTF-16 encoding
        }
        lines.Add(sb.ToString());
    }

    File.WriteAllLines(filename, lines);
}

Example: Record frames

private List<BufferSnapshot> _frames = new();

public void StartRecording()
{
    _frames.Clear();

    Renderer.PostBufferPaint += (buffer, dirtyRegion, clipRect) =>
    {
        _frames.Add(buffer.CreateSnapshot());
    };
}

public void StopRecording()
{
    Renderer.PostBufferPaint -= RecordFrame;
    // Process _frames (save as animated GIF, video, etc.)
}

5. Buffer Compositing

Manually composite multiple buffer snapshots for advanced effects.

public class CustomCompositor
{
    private List<(BufferSnapshot snapshot, int z)> _layers = new();

    public void AddLayer(BufferSnapshot snapshot, int z)
    {
        _layers.Add((snapshot, z));
        _layers = _layers.OrderBy(l => l.z).ToList();
    }

    public CharacterBuffer Render()
    {
        if (_layers.Count == 0) return null;

        var first = _layers[0].snapshot;
        var result = new CharacterBuffer(first.Width, first.Height);

        foreach (var (snapshot, _) in _layers)
        {
            for (int y = 0; y < snapshot.Height; y++)
            {
                for (int x = 0; x < snapshot.Width; x++)
                {
                    var cell = snapshot.GetCell(x, y);
                    if (cell.IsWideContinuation) continue; // Preserve wide char pairs
                    if (cell.Character != new Rune(' ')) // Simple alpha test
                    {
                        result.SetCell(x, y, cell.Character, cell.Foreground, cell.Background);
                    }
                }
            }
        }

        return result;
    }
}

Examples

The Examples/CompositorEffectsExample project demonstrates all major use cases:

FadeInWindow.cs

Demonstrates a smooth fade-in transition effect that gradually blends from black to the window's rendered content over 60 frames.

Key Features:

  • Animated fade using System.Threading.Timer
  • Color blending algorithm
  • Progress tracking (0.0 to 1.0)
  • Automatic cleanup on completion

Run: Launch example, press 1 or click "Fade-In Transition"

ModalBlurWindow.cs

Demonstrates a box blur effect that can be toggled on/off, with adjustable blur radius.

Key Features:

  • Interactive blur toggle (B key)
  • Adjustable blur radius (+/- keys)
  • Radius display in title
  • Buffer copy to avoid feedback loop

Run: Launch example, press 2 or click "Modal Blur Effect"

ScreenshotWindow.cs

Demonstrates capturing buffer snapshots and saving to file with metadata.

Key Features:

  • BufferSnapshot usage
  • File I/O with timestamps
  • Screenshot counter
  • Success/error notifications

Run: Launch example, press 3 or click "Screenshot Capture", then press F12 or S

Running Examples

cd Examples/CompositorEffectsExample
dotnet run

Best Practices

1. Use Dirty Regions for Optimization

Only process the area that was actually repainted:

private void ApplyEffect(CharacterBuffer buffer, LayoutRect dirtyRegion, LayoutRect clipRect)
{
    // Only process dirty region, not entire buffer
    for (int y = dirtyRegion.Top; y < dirtyRegion.Bottom; y++)
    {
        for (int x = dirtyRegion.Left; x < dirtyRegion.Right; x++)
        {
            // Apply effect
        }
    }
}

2. Cache Expensive Operations

Don't recalculate the same values every frame:

private float[]? _blurWeights;

private void ApplyBlur(CharacterBuffer buffer, LayoutRect dirtyRegion, LayoutRect clipRect)
{
    // Calculate weights once
    if (_blurWeights == null)
    {
        _blurWeights = CalculateGaussianWeights(_blurRadius);
    }

    // Use cached weights
    ApplyBlurWithWeights(buffer, _blurWeights);
}

3. Unsubscribe Events Properly

Always unsubscribe from events when no longer needed:

public class MyWindow : Window
{
    public MyWindow(ConsoleWindowSystem windowSystem) : base(windowSystem)
    {
        Renderer.PostBufferPaint += ApplyEffect;
        OnClosing += (s, e) => Cleanup();
    }

    private void Cleanup()
    {
        if (Renderer != null)
        {
            Renderer.PostBufferPaint -= ApplyEffect;
        }
    }
}

4. Avoid Feedback Loops in Blur/Copy Operations

When copying entire buffer back, use a temporary buffer:

// GOOD: Use temporary buffer
var temp = new CharacterBuffer(buffer.Width, buffer.Height);
ApplyEffectTo(buffer, temp);
buffer.CopyFrom(temp, ...);

// BAD: Modifying buffer while reading causes feedback
for (int y = 0; y < buffer.Height; y++)
{
    for (int x = 0; x < buffer.Width; x++)
    {
        var avg = AverageNeighbors(buffer, x, y); // Reading modified cells!
        buffer.SetCell(x, y, ...);
    }
}

5. Conditional Effect Application

Only apply effects when necessary:

private bool _effectEnabled = true;
private float _effectIntensity = 1.0f;

private void ApplyEffect(CharacterBuffer buffer, LayoutRect dirtyRegion, LayoutRect clipRect)
{
    if (!_effectEnabled || _effectIntensity <= 0f)
        return; // Skip processing

    // Apply effect with intensity
}

6. Handle Wide Characters in Effects

When iterating over buffer cells in effects, skip IsWideContinuation cells to avoid breaking wide character pairs (CJK, emoji). Overwriting a continuation cell without its base cell corrupts the display:

// GOOD: Skip continuation cells
for (int y = 0; y < buffer.Height; y++)
{
    for (int x = 0; x < buffer.Width; x++)
    {
        var cell = buffer.GetCell(x, y);
        if (cell.IsWideContinuation) continue; // Preserve wide char pairs
        // Apply effect to base cells only
    }
}

// BAD: Modifying continuation cells breaks wide characters
for (int x = 0; x < buffer.Width; x++)
{
    var cell = buffer.GetCell(x, y);
    buffer.SetCell(x, y, '█', cell.Foreground, cell.Background); // Breaks wide chars!
}

Note: SetCell() accepts both char and Rune. When modifying only colors (not the character), consider preserving the original Rune from the cell to keep wide characters and combiners intact.

7. Use StringBuilder for Text Construction

When converting snapshots to text, use StringBuilder:

// GOOD
var sb = new StringBuilder();
for (int x = 0; x < width; x++)
    sb.Append(snapshot.GetCell(x, y).Character);
string line = sb.ToString();

// BAD
string line = "";
for (int x = 0; x < width; x++)
    line += snapshot.GetCell(x, y).Character; // Creates many temporary strings

Performance Considerations

Complexity

  • Full buffer iteration: O(width × height) - Use sparingly
  • Dirty region iteration: O(dirty_width × dirty_height) - Preferred
  • Blur effects: O(width × height × radius²) - Expensive, cache when possible

Optimization Strategies

  1. Process Only Dirty Regions: Use the dirtyRegion parameter
  2. Early Exit: Skip processing when effect is disabled/completed
  3. Incremental Updates: For animations, only update changed values
  4. Use Lookup Tables: Pre-calculate color blends, blur weights, etc.
  5. Limit Effect Area: Apply effects only to specific rectangles

Performance Example

// Optimized blur effect
private void ApplyBlur(CharacterBuffer buffer, LayoutRect dirtyRegion, LayoutRect clipRect)
{
    if (!_blurEnabled) return; // Early exit

    // Only blur the dirty region + blur radius margin
    var effectRegion = new LayoutRect(
        Math.Max(0, dirtyRegion.Left - _blurRadius),
        Math.Max(0, dirtyRegion.Top - _blurRadius),
        Math.Min(buffer.Width, dirtyRegion.Right + _blurRadius),
        Math.Min(buffer.Height, dirtyRegion.Bottom + _blurRadius)
    );

    // Process only effectRegion, not entire buffer
    for (int y = effectRegion.Top; y < effectRegion.Bottom; y++)
    {
        for (int x = effectRegion.Left; x < effectRegion.Right; x++)
        {
            // Apply blur
        }
    }
}

Thread Safety

The PostBufferPaint event fires within the existing render lock, ensuring thread safety:

// WindowRenderer.RebuildContentCacheDOM()
lock (_renderLock)
{
    PaintDOM(clipRect, backgroundColor);

    // Event fires within lock - thread safe
    PostBufferPaint?.Invoke(_buffer, dirtyRegion, clipRect);

    return BufferToLines(foregroundColor, backgroundColor);
}

Safe Operations

All CharacterBuffer operations are safe within the event handler:

  • GetCell(x, y) - Thread safe (within lock)
  • SetCell(x, y, ...) - Thread safe (within lock)
  • CreateSnapshot() - Thread safe (creates independent copy)

Unsafe Operations

Do NOT perform long-running operations in the event handler:

// BAD: Blocks rendering
private void ApplyEffect(CharacterBuffer buffer, LayoutRect dirtyRegion, LayoutRect clipRect)
{
    Thread.Sleep(1000); // BLOCKS RENDERING!
    File.WriteAllText(...); // BLOCKS RENDERING!
}

// GOOD: Defer expensive work
private void ApplyEffect(CharacterBuffer buffer, LayoutRect dirtyRegion, LayoutRect clipRect)
{
    var snapshot = buffer.CreateSnapshot(); // Fast

    // Defer expensive work
    Task.Run(() =>
    {
        ProcessSnapshot(snapshot);
        File.WriteAllText(...);
    });
}

API Reference

Window Class

namespace SharpConsoleUI
{
    public class Window
    {
        /// <summary>
        /// Gets the window's renderer, providing access to rendering internals.
        /// </summary>
        /// <remarks>
        /// Exposes the renderer for advanced scenarios like custom buffer effects,
        /// transitions, and compositor-style manipulations.
        /// </remarks>
        public Windows.WindowRenderer? Renderer { get; }
    }
}

WindowRenderer Class

namespace SharpConsoleUI.Windows
{
    public class WindowRenderer
    {
        /// <summary>
        /// Delegate for buffer post-processing after painting but before
        /// the buffer is consumed by the console driver.
        /// </summary>
        /// <param name="buffer">The character buffer that was just painted.
        /// Cells use Rune (not char) for full Unicode support.
        /// Check cell.IsWideContinuation to skip wide character continuation cells.</param>
        /// <param name="dirtyRegion">The region that was painted (or full bounds if entire buffer).</param>
        /// <param name="clipRect">The clipping rectangle used during paint.</param>
        public delegate void PostBufferPaintDelegate(
            CharacterBuffer buffer,
            LayoutRect dirtyRegion,
            LayoutRect clipRect);

        /// <summary>
        /// Raised after painting controls to the buffer but before converting to ANSI strings.
        /// </summary>
        /// <remarks>
        /// This event allows custom effects, transitions, filters, or compositor-style
        /// manipulations on the rendered buffer. The buffer can be safely modified here.
        ///
        /// Example use cases:
        /// - Fade in/out transitions
        /// - Blur effects for modal backgrounds
        /// - Glow effects around focused controls
        /// - Custom overlays and effects
        /// </remarks>
        public event PostBufferPaintDelegate? PostBufferPaint;

        /// <summary>
        /// Gets the current character buffer for this window.
        /// </summary>
        /// <remarks>
        /// CAUTION: Direct buffer manipulation should only be done via PostBufferPaint event
        /// to avoid race conditions. Reading is safe at any time.
        /// </remarks>
        public CharacterBuffer? Buffer { get; }
    }
}

CharacterBuffer Class

namespace SharpConsoleUI.Layout
{
    public struct Cell : IEquatable<Cell>
    {
        public Rune Character;          // Full Unicode scalar value
        public Color Foreground;
        public Color Background;
        public TextDecoration Decorations;
        public bool Dirty;
        public bool IsWideContinuation; // Right half of wide character (CJK, emoji)
        public string? Combiners;       // Zero-width combining marks
    }

    public class CharacterBuffer
    {
        /// <summary>
        /// Immutable snapshot of a CharacterBuffer at a point in time.
        /// </summary>
        public readonly record struct BufferSnapshot(int Width, int Height, Cell[,] Cells)
        {
            public Cell GetCell(int x, int y);
        }

        public BufferSnapshot CreateSnapshot();

        // Key cell-level methods:
        public void SetCell(int x, int y, Rune character, Color fg, Color bg);
        public void SetCell(int x, int y, char character, Color fg, Color bg); // wraps in Rune
        public void WriteString(int x, int y, string text, Color fg, Color bg);
    }
}

CanvasControl: Per-Control Drawing Surface

While compositor effects operate on the entire window buffer, CanvasControl provides a self-contained drawing surface within a single control. It exposes the same CharacterBuffer drawing primitives (lines, circles, polygons, gradients, text) through a CanvasGraphics wrapper that translates to canvas-local coordinates.

When to Use Each Approach

Compositor Effects CanvasControl
Scope Entire window buffer Single control region
Coordinates Absolute buffer positions Local (0,0 = top-left of canvas)
Persistence Runs each frame Internal buffer retains content
Threading Fires within render lock BeginPaint()/EndPaint() from any thread
Drawing API Raw CharacterBuffer cell access CanvasGraphics with 30+ methods
Best for Post-processing (blur, fade, color grading), custom backgrounds Games, visualizations, interactive drawing, animated graphics

Combined Usage

You can use both in the same window. For example, a CanvasControl for the main drawing area with a PostBufferPaint effect applied to the entire window:

// Canvas for interactive drawing
var canvas = new CanvasControl
{
    HorizontalAlignment = HorizontalAlignment.Stretch,
    VerticalAlignment = VerticalAlignment.Fill,
    AutoSize = true
};

canvas.Paint += (sender, e) =>
{
    var g = e.Graphics;
    g.DrawCircle(e.CanvasWidth / 2, e.CanvasHeight / 2, 8, '*', Color.Cyan, Color.Black);
};

// Window-level fade effect on top
window.Renderer.PostBufferPaint += (buffer, dirty, clip) =>
{
    // Fade the entire window including the canvas
    for (int y = dirty.Top; y < dirty.Bottom; y++)
        for (int x = dirty.Left; x < dirty.Right; x++)
        {
            var cell = buffer.GetCell(x, y);
            buffer.SetCell(x, y, cell.Character,
                BlendColor(cell.Foreground, Color.Black, 0.3f),
                BlendColor(cell.Background, Color.Black, 0.3f));
        }
};

See the CanvasControl documentation for the full API reference and examples.

See Also