← Files UnityARCHIVED FILE
skills/ui-uitk/references/painter2d.md
10.5 KB · Oct 3, 2026 · 06:34 UTC
# Custom Visuals with Painter2D
## Table of Contents
- [The Pattern](#the-pattern)
- [Canvas-to-Painter2D Mapping](#canvas-to-painter2d-mapping)
- [Gradient Fills](#gradient-fills)
- [GradientElement — Full Example](#gradientelement--full-example)
- [Other Use Cases](#other-use-cases)
- [Rules](#rules)
USS cannot draw gradients, arbitrary shapes, arcs, or procedural patterns. For these, use the **Painter2D** API via the `generateVisualContent` callback.
Painter2D is modeled on the **HTML Canvas 2D** context — `BeginPath`, `MoveTo`, `LineTo`, `Arc`, `BezierCurveTo`, `Fill`, `Stroke` all map directly. Key differences from Canvas: text is drawn via `ctx.DrawText()` on the `MeshGenerationContext` (not on Painter2D itself), no `drawImage()` (use `fillTexture` or child elements with USS `background-image`), angles use `Angle.Degrees()` / `Angle.Turns()` structs, arc direction is an enum (`ArcDirection.Clockwise` / `.CounterClockwise`), and coordinates are local to the element's content rect.
**Drawing text:** Use `ctx.DrawText(string text, Vector2 pos, float fontSize, Color color, FontAsset font)` on the `MeshGenerationContext` directly. Pass `null` for `font` to use the element's USS font. This is useful when text must be positioned precisely within custom-drawn visuals — for simpler cases, child `Label` elements are easier.
## The Pattern
Every custom-drawn element: extend `VisualElement` directly (never `Label`, `Button`, etc. — Painter2D won't render correctly on those), subscribe to `generateVisualContent`, draw with `ctx.painter2D`, call `MarkDirtyRepaint()` when properties change. For animations, call `MarkDirtyRepaint()` every frame from an update loop.
```csharp
[UxmlElement]
public partial class MyCustomVisual : VisualElement
{
float m_Value = 0.5f;
[UxmlAttribute]
public float Value
{
get => m_Value;
set { m_Value = value; MarkDirtyRepaint(); }
}
public MyCustomVisual()
{
generateVisualContent += OnGenerateVisualContent;
}
void OnGenerateVisualContent(MeshGenerationContext ctx)
{
float w = contentRect.width;
float h = contentRect.height;
if (w < 1f || h < 1f) return;
var painter = ctx.painter2D;
// ... drawing commands
}
}
```
Name classes to match their purpose — `GradientCard`, `RadialProgress`, `WaveformDisplay`, etc.
## Canvas-to-Painter2D Mapping
| HTML Canvas 2D | Unity Painter2D |
|----------------|-----------------|
| `beginPath()` | `BeginPath()` |
| `moveTo(x, y)` | `MoveTo(new Vector2(x, y))` |
| `lineTo(x, y)` | `LineTo(new Vector2(x, y))` |
| `arc(cx, cy, r, start, end)` | `Arc(Vector2 center, float radius, Angle start, Angle end, ArcDirection dir)` |
| `arcTo(x1, y1, x2, y2, r)` | `ArcTo(Vector2 p1, Vector2 p2, float radius)` |
| `bezierCurveTo(...)` | `BezierCurveTo(Vector2 ctrl1, Vector2 ctrl2, Vector2 end)` |
| `quadraticCurveTo(...)` | `QuadraticCurveTo(Vector2 ctrl, Vector2 end)` |
| `closePath()` | `ClosePath()` |
| `rect(x, y, w, h)` | **No equivalent** — trace manually with `MoveTo`/`LineTo`/`ClosePath` |
| `fill()` | `Fill(FillRule rule = NonZero)` — use `OddEven` for holes/cutouts |
| `stroke()` | `Stroke()` |
| `lineWidth` | `lineWidth` |
| `strokeStyle` | `strokeColor` / `strokeGradient` / `strokeFillGradient` |
| `fillStyle` | `fillColor` / `fillGradient` / `fillTexture` |
| `lineCap` | `lineCap` — `LineCap.Butt` (default), `.Round`, `.Square` |
| `lineJoin` | `lineJoin` — `LineJoin.Miter` (default), `.Bevel`, `.Round` |
| `setLineDash([...])` | `SetDashPattern(float[])` |
| `lineDashOffset` | `dashOffset` |
Both `Fill()` and `Stroke()` can be called on the same path.
Angle helpers: `Angle.Degrees(float)`, `Angle.Radians(float)`, `Angle.Turns(float)`.
## Gradient Fills
USS has no `linear-gradient()` or `radial-gradient()`. Use `FillGradient`:
```csharp
// Linear — two-color shorthand
FillGradient.MakeLinearGradient(Color startColor, Color endColor, Vector2 start, Vector2 end, AddressMode mode)
// Linear — multi-stop via Gradient object
FillGradient.MakeLinearGradient(Gradient gradient, Vector2 start, Vector2 end, AddressMode mode)
// Radial — two-color shorthand
FillGradient.MakeRadialGradient(Color startColor, Color endColor, Vector2 center, float radius, Vector2 focus, AddressMode mode)
// Radial — multi-stop via Gradient object
FillGradient.MakeRadialGradient(Gradient gradient, Vector2 center, float radius, Vector2 focus, AddressMode mode)
```
`AddressMode`: `Clamp` (extend edge color), `Repeat` (tile), `Mirror` (reflect).
**Linear gradient direction** — controlled by start/end points:
| Direction | Start | End |
|-----------|-------|-----|
| Top → Bottom | `(0, 0)` | `(0, height)` |
| Left → Right | `(0, 0)` | `(width, 0)` |
| Diagonal | `(0, 0)` | `(width, height)` |
**Radial gradient** — set `focus` off-center to shift the bright spot.
## GradientElement — Full Example
A custom element rendering a linear gradient with rounded corners and optional border stroke. All properties exposed as UXML attributes.
```csharp
using UnityEngine;
using UnityEngine.UIElements;
[UxmlElement]
public partial class GradientElement : VisualElement
{
Color m_StartColor = new Color(0.13f, 0.59f, 0.95f);
Color m_EndColor = new Color(0.61f, 0.15f, 0.69f);
Color m_BorderColor = Color.white;
float m_BorderWidth = 2f;
float m_CornerRadius = 8f;
float m_GradientAlpha = 1f;
[UxmlAttribute]
public Color StartColor
{
get => m_StartColor;
set { m_StartColor = value; MarkDirtyRepaint(); }
}
[UxmlAttribute]
public Color EndColor
{
get => m_EndColor;
set { m_EndColor = value; MarkDirtyRepaint(); }
}
[UxmlAttribute]
public Color BorderColor
{
get => m_BorderColor;
set { m_BorderColor = value; MarkDirtyRepaint(); }
}
[UxmlAttribute]
public float BorderWidth
{
get => m_BorderWidth;
set { m_BorderWidth = value; MarkDirtyRepaint(); }
}
[UxmlAttribute]
public float CornerRadius
{
get => m_CornerRadius;
set { m_CornerRadius = value; MarkDirtyRepaint(); }
}
[UxmlAttribute]
public float GradientAlpha
{
get => m_GradientAlpha;
set { m_GradientAlpha = Mathf.Clamp01(value); MarkDirtyRepaint(); }
}
public GradientElement()
{
generateVisualContent += OnGenerateVisualContent;
}
void OnGenerateVisualContent(MeshGenerationContext ctx)
{
float w = contentRect.width;
float h = contentRect.height;
if (w < 1f || h < 1f)
return;
DrawGradientBackground(
ctx.painter2D, w, h,
m_StartColor, m_EndColor, m_GradientAlpha,
m_CornerRadius,
m_BorderColor, m_BorderWidth);
}
static void DrawGradientBackground(
Painter2D painter,
float width, float height,
Color startColor, Color endColor, float alpha,
float cornerRadius,
Color borderColor, float borderWidth)
{
float r = Mathf.Min(cornerRadius, Mathf.Min(width, height) * 0.5f);
var start = startColor;
var end = endColor;
start.a *= alpha;
end.a *= alpha;
painter.fillGradient = FillGradient.MakeLinearGradient(
BuildGradient(start, end),
new Vector2(0f, 0f),
new Vector2(0f, height),
AddressMode.Clamp);
painter.BeginPath();
TraceRoundedRect(painter, 0f, 0f, width, height, r);
painter.Fill();
if (borderWidth <= 0f)
return;
float half = borderWidth * 0.5f;
painter.strokeColor = borderColor;
painter.lineWidth = borderWidth;
painter.lineJoin = LineJoin.Round;
painter.BeginPath();
TraceRoundedRect(
painter, half, half,
width - borderWidth, height - borderWidth,
Mathf.Max(0f, r - half));
painter.Stroke();
}
static Gradient BuildGradient(Color start, Color end)
{
var gradient = new Gradient();
gradient.SetKeys(
new[] { new GradientColorKey(start, 0f), new GradientColorKey(end, 1f) },
new[] { new GradientAlphaKey(start.a, 0f), new GradientAlphaKey(end.a, 1f) });
return gradient;
}
static void TraceRoundedRect(Painter2D p, float x, float y, float w, float h, float r)
{
p.MoveTo(new Vector2(x + r, y));
p.LineTo(new Vector2(x + w - r, y));
p.ArcTo(new Vector2(x + w, y), new Vector2(x + w, y + r), r);
p.LineTo(new Vector2(x + w, y + h - r));
p.ArcTo(new Vector2(x + w, y + h), new Vector2(x + w - r, y + h), r);
p.LineTo(new Vector2(x + r, y + h));
p.ArcTo(new Vector2(x, y + h), new Vector2(x, y + h - r), r);
p.LineTo(new Vector2(x, y + r));
p.ArcTo(new Vector2(x, y), new Vector2(x + r, y), r);
p.ClosePath();
}
}
```
### Usage in UXML
```uxml
<ui:UXML xmlns:ui="UnityEngine.UIElements">
<ui:Style src="Screen.uss" />
<GradientElement class="gradient-card"
start-color="#2196F3" end-color="#9C27B0"
gradient-alpha="0.9" corner-radius="12"
border-color="#FFFFFF" border-width="1">
<ui:Label text="Card Title" class="card-title" />
<ui:Label text="Description text goes here" class="card-desc" />
</GradientElement>
</ui:UXML>
```
The element participates in flexbox, accepts children, and can be styled with USS for sizing, padding, and margin. The gradient draws behind child content.
## Other Use Cases
Painter2D handles any visual USS cannot express — progress rings (`Arc()` with dynamic `endAngle`), custom shapes (polygons, stars, badges), charts (bar fills, pie segments, sparklines), decorative elements (wave patterns, bezier flourishes), and animated visuals (drive properties from C#, call `MarkDirtyRepaint()` each frame).
## Rules
- **Extend `VisualElement` directly** — never `Label`, `Button`, etc.
- **Guard zero dimensions** — `if (contentRect.width < 1f || contentRect.height < 1f) return;`
- **`BeginPath()` before every path** — omitting it causes silent failures
- **No `Rect()` method** — Painter2D has no rectangle convenience method. Trace rectangles manually with `MoveTo`/`LineTo`/`ClosePath` (see `TraceRoundedRect` in the gradient example)
- **Set style properties before `BeginPath()`** — `lineWidth`, `strokeColor`, `fillColor`, etc.
- **Never mutate the element inside `generateVisualContent`** — no style changes, no adding children, no `MarkDirtyRepaint()` from within the callback
- **`LineCap.Butt` for precise arc endpoints** — `Round` extends past the endpoint by half the line width
SHA-256: 8090626a558ddccf64092c82c85dfd3eec6791cedcedd45c21d42aa29f8fef08