Skip to main content

Setting Up a Canvas Painter

Painters are custom rendering components that can draw using the Canvas API. They attach to UI elements and execute during the paint phase, allowing you to add custom vector graphics, animations, and effects.

Basic Painter Structure

A painter extends the Painter base class and implements two key methods:

using EvolveUI;
using EvolveUI.Rendering;
using UnityEngine;
using Canvas = EvolveUI.Rendering.Canvas;

public class MyPainter : Painter {

private Canvas canvas;

// Called when the painter is attached to an element
public override PaintDetach Attach(ElementId elementId, PaintAttachment attachment) {
// Create the canvas instance
canvas = new Canvas(Assets);

// Return a detach handler to clean up when the element is destroyed
return new PaintDetach(this, static (paintInfo) => {
((MyPainter)paintInfo.cookie).canvas.Dispose();
});
}

// Called every frame to render
protected override void Paint(Gfx gfx, StyleResult styleResult) {
// Call Next() to continue the paint chain (can be before or after your drawing)
Next();

// Clear previous frame's geometry
canvas.Clear();

// Draw using the Canvas API
canvas.SetFillColor(255, 100, 100, 255);
canvas.FillRect(0, 0, 200, 100);

// Submit the canvas to be rendered
gfx.DrawCanvas(canvas);
}
}

Key Concepts

The Attach Method

Attach is called once when the painter is first connected to an element. Use it to:

  • Create your Canvas instance
  • Load textures or fonts
  • Initialize any state

The Assets property provides access to UIAssetManager for loading resources.

The PaintDetach Handler

Return a PaintDetach to clean up resources when the element is destroyed:

return new PaintDetach(this, static (paintInfo) => {
var painter = (MyPainter)paintInfo.cookie;
painter.canvas.Dispose();
// Clean up other resources...
});

The cookie parameter passes your painter instance to the static delegate.

The Paint Method

Paint is called every frame. The parameters provide:

  • gfx - Graphics context for submitting draw calls and accessing time info
  • styleResult - The element's computed styles and layout information

Calling Next()

Next() continues the paint chain to other painters and the default element rendering. Where you call it matters:

// Draw AFTER the element (overlay)
protected override void Paint(Gfx gfx, StyleResult styleResult) {
Next(); // Element renders first
// Your drawing goes on top
canvas.Clear();
canvas.SetFillColor(255, 0, 0, 128);
canvas.FillRect(0, 0, 100, 100);
gfx.DrawCanvas(canvas);
}

// Draw BEFORE the element (background)
protected override void Paint(Gfx gfx, StyleResult styleResult) {
canvas.Clear();
canvas.SetFillColor(0, 0, 255, 128);
canvas.FillRect(0, 0, 100, 100);
gfx.DrawCanvas(canvas);
Next(); // Element renders on top
}

Complete Example

Here's a painter that draws an animated gradient background:

using EvolveUI;
using EvolveUI.Rendering;
using UnityEngine;
using Canvas = EvolveUI.Rendering.Canvas;

public class AnimatedGradientPainter : Painter {

private Canvas canvas;
private float time;

public override PaintDetach Attach(ElementId elementId, PaintAttachment attachment) {
canvas = new Canvas(Assets);

return new PaintDetach(this, static (paintInfo) => {
((AnimatedGradientPainter)paintInfo.cookie).canvas.Dispose();
});
}

protected override void Paint(Gfx gfx, StyleResult styleResult) {
Next();

canvas.Clear();
time += gfx.timeInfo.deltaTime;

// Get the element's layout size
var size = styleResult.GetLayoutSize();
float width = size.x;
float height = size.y;

// Animated color cycling
float hue1 = (time * 30) % 360;
float hue2 = (time * 30 + 180) % 360;
Color color1 = Color.HSVToRGB(hue1 / 360f, 0.7f, 0.9f);
Color color2 = Color.HSVToRGB(hue2 / 360f, 0.7f, 0.9f);

// Draw gradient background
canvas.SetLinearGradient(0, 0, width, height,
new Color32((byte)(color1.r * 255), (byte)(color1.g * 255), (byte)(color1.b * 255), 255),
new Color32((byte)(color2.r * 255), (byte)(color2.g * 255), (byte)(color2.b * 255), 255));
canvas.FillRect(0, 0, width, height);

gfx.DrawCanvas(canvas);
}
}

Accessing Element Information

The StyleResult provides access to the element's computed properties:

protected override void Paint(Gfx gfx, StyleResult styleResult) {
// Layout information
var size = styleResult.GetLayoutSize(); // Element dimensions
var position = styleResult.GetLayoutPosition(); // Element position

// Style properties
float opacity = styleResult.Opacity;
Color textColor = styleResult.TextColor;
Color bgColor = styleResult.BackgroundColor;

// ... use these values in your drawing
}

Loading Resources

Load fonts and textures in the Attach method:

private Canvas canvas;
private VectorFont titleFont;
private TextureId patternTexture;

public override PaintDetach Attach(ElementId elementId, PaintAttachment attachment) {
canvas = new Canvas(Assets);

// Load a font
titleFont = Assets.LoadVectorFont("/path/to/font.ttf");

// Register a texture for patterns
Texture2D tex = Resources.Load<Texture2D>("MyPattern");
patternTexture = Assets.RegisterTexture(tex);

return new PaintDetach(this, static (paintInfo) => {
((MyPainter)paintInfo.cookie).canvas.Dispose();
});
}

protected override void Paint(Gfx gfx, StyleResult styleResult) {
Next();
canvas.Clear();

// Use the loaded font
canvas.SetFont(titleFont);
canvas.SetFontSize(32);
canvas.SetTextFillColor(Color.white);
canvas.Text(10, 10, 400, "Hello World");

// Use the pattern texture
canvas.SetPatternTexture(patternTexture);
canvas.SetPatternMode(PatternSpace.Bounds, PatternSizing.Tile, PatternRepeat.Repeat);
canvas.SetPatternTileSize(0.2f, 0.2f);
canvas.FillRect(0, 100, 200, 200);
canvas.ClearPattern();

gfx.DrawCanvas(canvas);
}

Paint Attachment Points

When attaching a painter to an element, you specify when it should run in the paint order:

AttachmentDescription
BeforeEverythingBefore any element rendering
BeforeShadowBefore shadow rendering
AfterShadowAfter shadow, before frame
BeforeFrameBefore frame/border rendering
AfterFrameAfter frame, before element content
BeforeElementJust before element content
AfterElementJust after element content
BeforeChildrenBefore child elements render
AfterChildrenAfter child elements render
AfterEverythingAfter all element rendering

Skipping Default Rendering

You can skip the element's default rendering or its children:

protected override void Paint(Gfx gfx, StyleResult styleResult) {
// Skip the element's normal background/border rendering
SkipDefaultPainting = true;

// Skip rendering child elements
SkipChildrenPainting = true;

// Draw your custom content instead
canvas.Clear();
// ...
gfx.DrawCanvas(canvas);

Next();
}

Tips

  1. Always call canvas.Clear() at the start of Paint() to remove the previous frame's geometry.

  2. Always call gfx.DrawCanvas(canvas) to submit your drawing. Without this, nothing renders.

  3. Call Next() to continue the paint chain. Omitting it will prevent other painters and default rendering.

  4. Dispose the canvas in the PaintDetach handler to avoid memory leaks.

  5. Use gfx.timeInfo.deltaTime for smooth animations that are frame-rate independent.