Skip to content

create.color

Colours: Color, Gradient and BlendMode.

Color

struct Color

Implements Copyable, Deinitable, Equatable, ImplicitlyCopyable, Movable, Writable.

Constants

  • TRANSPARENT = Color(UInt8(0), UInt8(0), UInt8(0), UInt8(0))
  • BLACK = Color(UInt8(0))
  • WHITE = Color(UInt8(255))
  • DARK_GRAY = Color(UInt8(64))
  • GRAY = Color(UInt8(128))
  • LIGHT_GRAY = Color(UInt8(192))
  • RED = Color(UInt8(255), UInt8(0), UInt8(0), UInt8(255))
  • GREEN = Color(UInt8(0), UInt8(255), UInt8(0), UInt8(255))
  • BLUE = Color(UInt8(0), UInt8(0), UInt8(255), UInt8(255))
  • CYAN = Color(UInt8(0), UInt8(255), UInt8(255), UInt8(255))
  • MAGENTA = Color(UInt8(255), UInt8(0), UInt8(255), UInt8(255))
  • YELLOW = Color(UInt8(255), UInt8(255), UInt8(0), UInt8(255))
  • ORANGE = Color(UInt8(255), UInt8(128), UInt8(0), UInt8(255))
  • PINK = Color(UInt8(255), UInt8(128), UInt8(192), UInt8(255))
  • PURPLE = Color(UInt8(128), UInt8(0), UInt8(255), UInt8(255))
  • BROWN = Color(UInt8(128), UInt8(64), UInt8(0), UInt8(255))

Fields

  • r: UInt8
  • g: UInt8
  • b: UInt8
  • a: UInt8

Constructors

@staticmethod
def __init__(out self, r: UInt8, g: UInt8, b: UInt8, a: UInt8 = UInt8(255))
@staticmethod
def __init__(out self, gray: UInt8)

__eq__

def __eq__(self, other: Self) -> Bool

__ne__

def __ne__(self, other: Self) -> Bool

luminance

def luminance(self) -> Float64

WCAG relative luminance, 0.0 (black) to 1.0 (white).

Answers "is this background light or dark" — the usual caller picks black or white text against it, with 0.18 as the crossover.

to_hsv

def to_hsv(self) -> Tuple[Float64, Float64, Float64]

Inverse of hsv: hue in degrees, saturation and value in 0..1.

Hue is 0.0 for grays, which have none.

over

def over(self, dst: Self) -> Self

Composite this color over dst, source-over.

self.a decides how much of self shows through. Compositing over an opaque color yields an opaque color, so this is what a background fill does against the framebuffer.

with_alpha

def with_alpha(self, a: UInt8) -> Self

This color at a different opacity, 0 (invisible) to 255 (opaque).

The RGB channels are carried over untouched, so a named constant or a computed color stays spelled once: Color.RED.with_alpha(128) rather than respelling all three channels to change the fourth.

hex

@staticmethod
def hex(rgb: Int) -> Self

Build a color from a packed literal: Color.hex(0x336699).

@staticmethod
def hex(code: String) raises -> Self

Build a color from a CSS hex string: Color.hex("#336699").

Accepts #rgb, #rgba, #rrggbb and #rrggbbaa; the leading # is optional and case does not matter. The short forms double each digit, as CSS does, so "#f80" is "#ff8800". Alpha defaults to opaque.

This is the form to paste into from a design tool, and the only one that carries alpha in a single token. hex(Int) stays the form for a literal written in source, since a parse can raise and so cannot run at compile time.

Raises If the string is not one of the four accepted lengths, or holds a character that is not a hex digit.

hsv

@staticmethod
def hsv(h: Float64, s: Float64, v: Float64) -> Self

Build a color from hue in degrees (wraps) and saturation/value in 0..1.

Hue cycling is the point: Color.hsv(Float64(context.frame_count()) % 360.0, 1.0, 1.0) sweeps the spectrum without hand-mixing channels.

lerp

def lerp(self, other: Self, t: Float64) -> Self

A new colour t of the way from this one to other, t clamped to 0..1; this one is unchanged. Mixes in sRGB, like a paint program's gradient — not perceptually uniform, but what the eye expects from two swatches.

Gradient

struct Gradient

A smooth run of colours across a shape's fill, or across the background.

var sky = Gradient.linear(Color.hex(0x0B1D3A), Color.hex(0xF4A261))
canvas.background(sky)
canvas.fill(Gradient.radial(Color.WHITE, Color.WHITE.with_alpha(0)))
canvas.circle((0, 0), 80)

Placed by the shape, not the world. A gradient spans the bounding box of the shape it fills, in the shape's own coordinates, so it moves and turns with the shape and one Style looks the same on every entity.

  • linear runs along direction, top to bottom by default. The line is just long enough for the box's two farthest corners to land exactly on the first and last stop, as in CSS.
  • radial runs out from center, given in the box's own unit space ((0, 0) its middle, (1, 1) its top-right corner), to the box's edges: a circle on a square box, an ellipse on a long one. Its size stays put when center moves.

Stops are positions from 0 to 1, each with a colour. Given only colours, they are spaced evenly; given (position, colour) pairs, each position is clamped to 0..1 and to no less than the stop before it. Before the first stop and after the last, the end colours carry on. Colours blend premultiplied, so a fade to Color.TRANSPARENT doesn't pass through grey.

A zero direction has no line to run along, and a box with no width (or height) has nothing to span, so the gradient paints its first stop. direction is kept as given, so == and printing round-trip.

Copying one is cheap: the stops are shared, never changed after construction.

Implements Copyable, Deinitable, Equatable, ImplicitlyCopyable, Movable, Writable.

Fields

  • direction: Vector2D: linear only: which way the colours run, first stop to last.
  • center: Point2D: radial only: where the first stop sits, in the box's unit space.

__eq__

def __eq__(self, other: Self) -> Bool

__ne__

def __ne__(self, other: Self) -> Bool

linear

@staticmethod
def linear(first: Color, *rest: Color, *, direction: Vector2D = Vector2D.DOWN) -> Self

Evenly spaced colours along direction; one colour is a solid.

@staticmethod
def linear(
    stops: List[Tuple[Float64, Color]],
    *,
    direction: Vector2D = Vector2D.DOWN,
) -> Self

Colours at the given positions along direction: Gradient.linear([(0.0, Color.BLUE), (0.7, Color.PURPLE), (1.0, Color.ORANGE)]).

radial

@staticmethod
def radial(
    first: Color,
    *rest: Color,
    *,
    center: Point2D = Point2D(SIMD(0), SIMD(0)),
) -> Self

Evenly spaced colours out from center to the box's edges.

@staticmethod
def radial(
    stops: List[Tuple[Float64, Color]],
    *,
    center: Point2D = Point2D(SIMD(0), SIMD(0)),
) -> Self

Colours at the given positions out from center to the box's edges.

BlendMode

struct BlendMode

How a shape, glyph or image combines with what is already rendered.

Set with canvas.blend_mode or the blend_mode keyword of canvas.style/Style, and read by every render call after it, like any other style setting. Each mode is a per-channel formula over the source colour s and the colour already there, d, both as fractions of 255:

Mode Result Looks like
NORMAL s Paint over paint — the default
ADD d + s Light: glows, sparks, fire. Only brightens
SUBTRACT d - s Removing light. Only darkens
MULTIPLY d * s Stacked tinted film: shadows. Only darkens
SCREEN 1 - (1 - d) * (1 - s) Projected light. Only brightens, softer than ADD

Results are clamped to [0, 1]. The source's alpha (after opacity) scales how much of the effect shows, so a half-transparent ADD adds half as much. The framebuffer's own alpha channel always composites source-over, whatever the mode.

Modes apply to render calls, not to canvas.background, which always paints normally. A shape's fill and its stroke overlap along the edge, so under a mode that accumulates (ADD, MULTIPLY, ...) the overlap shows twice; switch the stroke off for a clean glow. A shadow is combined by its caster's mode, so under ADD it brightens rather than darkens.

with canvas.style(blend_mode=BlendMode.ADD, stroke_enabled=False):
    for p in self.sparks:
        canvas.circle(p.position, 6)

Implements Copyable, Deinitable, Equatable, ImplicitlyCopyable, Movable, Writable.

Constants

  • NORMAL = BlendMode(Int(0))
  • ADD = BlendMode(Int(1))
  • MULTIPLY = BlendMode(Int(2))
  • SUBTRACT = BlendMode(Int(3))
  • SCREEN = BlendMode(Int(4))

Fields

  • value: Int

Constructors

@staticmethod
def __init__(out self, value: Int)

__eq__

def __eq__(self, other: Self) -> Bool

__ne__

def __ne__(self, other: Self) -> Bool