Vector Drawing
OneJS exposes Unity's Painter2D API for GPU-accelerated vector graphics. Any element can render custom vector content via the onGenerateVisualContent prop.
Use cases: Charts, graphs, custom shapes, progress indicators, path animations, game UI overlays.
Basic Usage
import { View, render } from "onejs-react"
function Circle() {
return (
<View
style={{ width: 200, height: 200, backgroundColor: "#333" }}
onGenerateVisualContent={(mgc) => {
const p = mgc.painter2D
// Set fill color (red)
p.fillColor = new CS.UnityEngine.Color(1, 0, 0, 1)
// Draw a circle
p.BeginPath()
p.Arc(
new CS.UnityEngine.Vector2(100, 100), // center
80, // radius
CS.UnityEngine.UIElements.Angle.Degrees(0),
CS.UnityEngine.UIElements.Angle.Degrees(360),
CS.UnityEngine.UIElements.ArcDirection.Clockwise
)
p.Fill(CS.UnityEngine.UIElements.FillRule.NonZero)
}}
/>
)
}
render(<Circle />, __root)How It Works
The onGenerateVisualContent prop maps directly to Unity's generateVisualContent delegate on VisualElement:
- React reconciler assigns your callback to the element's
generateVisualContentproperty - Unity calls your callback during the repaint phase
- Your callback receives a
MeshGenerationContextwith access topainter2D - Drawing commands are batched and rendered on the GPU
Path Operations
Painter2D uses a path-based drawing model similar to HTML5 Canvas:
onGenerateVisualContent={(mgc) => {
const p = mgc.painter2D
// Start a new path
p.BeginPath()
// Move to starting point (no line drawn)
p.MoveTo(new CS.UnityEngine.Vector2(10, 10))
// Draw lines
p.LineTo(new CS.UnityEngine.Vector2(100, 10))
p.LineTo(new CS.UnityEngine.Vector2(100, 100))
// Close path back to start
p.ClosePath()
// Render the path
p.strokeColor = new CS.UnityEngine.Color(1, 1, 1, 1)
p.lineWidth = 2
p.Stroke()
}}Path Methods
| Method | Description |
|---|---|
BeginPath() | Start a new path, clearing any existing path data |
MoveTo(point) | Move to point without drawing a line |
LineTo(point) | Draw a line from current position to point |
ClosePath() | Close the path by drawing a line back to the starting point |
Arc(center, radius, startAngle, endAngle, direction) | Draw an arc or circle |
ArcTo(p1, p2, radius) | Draw an arc tangent to two lines |
BezierCurveTo(cp1, cp2, end) | Draw a cubic bezier curve |
QuadraticCurveTo(cp, end) | Draw a quadratic bezier curve |
Rendering Methods
| Method | Description |
|---|---|
Fill(fillRule) | Fill the current path using the specified fill rule |
Stroke() | Stroke (outline) the current path |
FillRule.NonZero- Standard fill (most common)FillRule.OddEven- Alternate fill for complex shapes with holes
Styling Properties
onGenerateVisualContent={(mgc) => {
const p = mgc.painter2D
// Colors
p.fillColor = new CS.UnityEngine.Color(0.2, 0.6, 1.0, 1.0) // RGBA
p.strokeColor = new CS.UnityEngine.Color(1, 1, 1, 0.8)
// Line styling
p.lineWidth = 3
p.lineCap = CS.UnityEngine.UIElements.LineCap.Round
p.lineJoin = CS.UnityEngine.UIElements.LineJoin.Round
p.miterLimit = 10
}}Properties Reference
| Property | Type | Description |
|---|---|---|
fillColor | Color | Fill color for Fill() |
strokeColor | Color | Stroke color for Stroke() |
lineWidth | float | Stroke width in pixels |
lineCap | LineCap | Line end style: Butt, Round, Square |
lineJoin | LineJoin | Line corner style: Miter, Round, Bevel |
miterLimit | float | Limit for miter joins before beveling |
Triggering Repaints
The callback is only invoked when Unity decides the element needs repainting. To trigger a repaint when your drawing state changes, call MarkDirtyRepaint():
import { View, render } from "onejs-react"
import { useState, useEffect, useRef } from "react"
function AnimatedCircle() {
const ref = useRef<CS.UnityEngine.UIElements.VisualElement>(null)
const [radius, setRadius] = useState(50)
// Animate radius
useEffect(() => {
const interval = setInterval(() => {
setRadius(r => 30 + Math.sin(Date.now() / 500) * 20)
}, 16)
return () => clearInterval(interval)
}, [])
// Trigger repaint when radius changes
useEffect(() => {
ref.current?.MarkDirtyRepaint()
}, [radius])
return (
<View
ref={ref}
style={{ width: 200, height: 200 }}
onGenerateVisualContent={(mgc) => {
const p = mgc.painter2D
p.fillColor = new CS.UnityEngine.Color(0, 0.5, 1, 1)
p.BeginPath()
p.Arc(
new CS.UnityEngine.Vector2(100, 100),
radius,
CS.UnityEngine.UIElements.Angle.Degrees(0),
CS.UnityEngine.UIElements.Angle.Degrees(360),
CS.UnityEngine.UIElements.ArcDirection.Clockwise
)
p.Fill(CS.UnityEngine.UIElements.FillRule.NonZero)
}}
/>
)
}Practical Examples
Pie Chart
function PieChart({ data }: { data: { value: number; color: Color }[] }) {
const total = data.reduce((sum, d) => sum + d.value, 0)
return (
<View
style={{ width: 200, height: 200 }}
onGenerateVisualContent={(mgc) => {
const p = mgc.painter2D
const center = new CS.UnityEngine.Vector2(100, 100)
const radius = 80
const Angle = CS.UnityEngine.UIElements.Angle
const Clockwise = CS.UnityEngine.UIElements.ArcDirection.Clockwise
let startAngle = -90 // Start at top
for (const slice of data) {
const sweepAngle = (slice.value / total) * 360
const endAngle = startAngle + sweepAngle
p.fillColor = slice.color
p.BeginPath()
p.MoveTo(center)
p.Arc(
center,
radius,
Angle.Degrees(startAngle),
Angle.Degrees(endAngle),
Clockwise
)
p.ClosePath()
p.Fill(CS.UnityEngine.UIElements.FillRule.NonZero)
startAngle = endAngle
}
}}
/>
)
}Progress Ring
function ProgressRing({ progress }: { progress: number }) {
return (
<View
style={{ width: 120, height: 120 }}
onGenerateVisualContent={(mgc) => {
const p = mgc.painter2D
const center = new CS.UnityEngine.Vector2(60, 60)
const radius = 50
const thickness = 8
const Angle = CS.UnityEngine.UIElements.Angle
const Clockwise = CS.UnityEngine.UIElements.ArcDirection.Clockwise
// Background ring
p.strokeColor = new CS.UnityEngine.Color(0.2, 0.2, 0.2, 1)
p.lineWidth = thickness
p.lineCap = CS.UnityEngine.UIElements.LineCap.Round
p.BeginPath()
p.Arc(center, radius, Angle.Degrees(0), Angle.Degrees(360), Clockwise)
p.Stroke()
// Progress arc
p.strokeColor = new CS.UnityEngine.Color(0.2, 0.8, 0.4, 1)
p.BeginPath()
p.Arc(
center,
radius,
Angle.Degrees(-90),
Angle.Degrees(-90 + 360 * progress),
Clockwise
)
p.Stroke()
}}
/>
)
}Custom Shape
function Star({ points = 5, outerRadius = 50, innerRadius = 25 }) {
return (
<View
style={{ width: 120, height: 120 }}
onGenerateVisualContent={(mgc) => {
const p = mgc.painter2D
const cx = 60, cy = 60
p.fillColor = new CS.UnityEngine.Color(1, 0.8, 0, 1)
p.BeginPath()
for (let i = 0; i < points * 2; i++) {
const radius = i % 2 === 0 ? outerRadius : innerRadius
const angle = (Math.PI / points) * i - Math.PI / 2
const x = cx + Math.cos(angle) * radius
const y = cy + Math.sin(angle) * radius
if (i === 0) {
p.MoveTo(new CS.UnityEngine.Vector2(x, y))
} else {
p.LineTo(new CS.UnityEngine.Vector2(x, y))
}
}
p.ClosePath()
p.Fill(CS.UnityEngine.UIElements.FillRule.NonZero)
}}
/>
)
}Comparison with HTML5 Canvas
If you're familiar with HTML5 Canvas, here are the key differences:
| Feature | Unity Painter2D | HTML5 Canvas |
|---|---|---|
| Transforms | Not built-in | translate(), rotate(), scale() |
| State Stack | Not built-in | save(), restore() |
| Gradients | Limited (strokeGradient) | createLinearGradient, createRadialGradient |
| Text | Via MeshGenerationContext | fillText(), strokeText() |
| Images | Via DrawVectorImage() | drawImage() |
| Shadows | Not available | shadowBlur, shadowColor |
| Clipping | Via nested VisualElements | clip() |
Handling Transforms
Since Painter2D lacks built-in transforms, calculate transformed points manually:
// Helper for rotation
function rotatePoint(x: number, y: number, cx: number, cy: number, angle: number) {
const cos = Math.cos(angle)
const sin = Math.sin(angle)
const dx = x - cx
const dy = y - cy
return new CS.UnityEngine.Vector2(
cx + dx * cos - dy * sin,
cy + dx * sin + dy * cos
)
}
// Usage in drawing
const rotatedPoint = rotatePoint(x, y, centerX, centerY, Math.PI / 4)
p.LineTo(rotatedPoint)Text Drawing
For text, use MeshGenerationContext directly instead of painter2D:
onGenerateVisualContent={(mgc) => {
// Vector drawing
const p = mgc.painter2D
p.fillColor = new CS.UnityEngine.Color(0.2, 0.2, 0.2, 1)
// ... draw background
// Text drawing (separate API)
// Note: DrawText API depends on Unity version
// Check Unity documentation for MeshGenerationContext.DrawText
}}Performance Tips
- Minimize path complexity: Complex paths with many points are more expensive
- Batch similar draws: Group elements with similar colors/styles
- Avoid frequent repaints: Only call
MarkDirtyRepaint()when necessary - Use simple shapes when possible: Arcs and lines are faster than complex bezier curves
- Consider caching: For static graphics, render once and avoid state changes
API Reference
Types
// Available via CS.UnityEngine namespace
type Vector2 = CS.UnityEngine.Vector2
type Color = CS.UnityEngine.Color
// Available via CS.UnityEngine.UIElements namespace
type Angle = CS.UnityEngine.UIElements.Angle
type ArcDirection = CS.UnityEngine.UIElements.ArcDirection
type LineCap = CS.UnityEngine.UIElements.LineCap
type LineJoin = CS.UnityEngine.UIElements.LineJoin
type FillRule = CS.UnityEngine.UIElements.FillRule
type Painter2D = CS.UnityEngine.UIElements.Painter2D
type MeshGenerationContext = CS.UnityEngine.UIElements.MeshGenerationContextCreating Values
// Vector2
new CS.UnityEngine.Vector2(x, y)
// Color (RGBA, 0-1 range)
new CS.UnityEngine.Color(r, g, b, a)
// Angle
CS.UnityEngine.UIElements.Angle.Degrees(degrees)
CS.UnityEngine.UIElements.Angle.Radians(radians)
// Enums
CS.UnityEngine.UIElements.ArcDirection.Clockwise
CS.UnityEngine.UIElements.ArcDirection.CounterClockwise
CS.UnityEngine.UIElements.FillRule.NonZero
CS.UnityEngine.UIElements.FillRule.OddEven
CS.UnityEngine.UIElements.LineCap.Butt
CS.UnityEngine.UIElements.LineCap.Round
CS.UnityEngine.UIElements.LineCap.Square
CS.UnityEngine.UIElements.LineJoin.Miter
CS.UnityEngine.UIElements.LineJoin.Round
CS.UnityEngine.UIElements.LineJoin.Bevel