Skip to content
Drawing a Canvas Scaled & Rotated

Drawing a Canvas Scaled & Rotated

Place a scaled and rotated canvas through a matrix.

Drawing a Canvas Scaled & Rotated

Usage

from std.math import pi

from canvas.buffer import Canvas
from canvas.color import Color
from canvas.compose import Filter, draw_canvas
from canvas.geometry import Matrix2D
from canvas.io.png import write_png
from canvas.path import Path, fill_path_aa
from canvas.shapes.circles import fill_circle_aa
from canvas.shapes.lines import draw_line_aa
from canvas.shapes.rects import fill_rect
from canvas.text.font_cache import FontCache
from canvas.text.render import draw_text

comptime W = 700
comptime H = 430
comptime SHEET = Color(250, 249, 246)
comptime INK = Color(45, 50, 60)
comptime LABEL = Color(90, 95, 105)
comptime BLUE = Color(50, 110, 190)
comptime RED = Color(210, 80, 60)
comptime PALE = Color(232, 236, 242)


def _tile() raises -> Canvas:
    """The 64x64 source: a checker for the filters to disagree about,
    and a disk and a triangle for edges that were anti-aliased before
    they were resampled.
    """
    var t = Canvas(64, 64, Color(252, 252, 250))
    for row in range(8):
        for col in range(8):
            if (row + col) % 2 == 0:
                fill_rect(t, col * 8, row * 8, 8, 8, PALE)
    fill_circle_aa(t, 32.0, 32.0, 21.0, BLUE)
    var tri = Path()
    tri.move_to(32.0, 14.0)
    tri.line_to(48.0, 46.0)
    tri.line_to(16.0, 46.0)
    tri.close()
    fill_path_aa(t, tri, RED)
    draw_line_aa(t, 2.0, 2.0, 61.0, 61.0, Color(120, 130, 145), 1.5)
    return t^


def _figure(mut c: Canvas, x: Float64, y: Float64, scale: Float64) raises:
    """A small figure drawn at `scale` with its top-left at (x, y):
    what the bottom row renders once at 2x and once at 1x.
    """
    fill_rect(
        c,
        Int(x),
        Int(y),
        Int(240.0 * scale),
        Int(80.0 * scale),
        Color(255, 255, 255),
    )
    fill_circle_aa(c, x + 44.0 * scale, y + 40.0 * scale, 26.0 * scale, BLUE)
    fill_circle_aa(c, x + 96.0 * scale, y + 40.0 * scale, 18.0 * scale, RED)
    var wedge = Path()
    wedge.move_to(x + 130.0 * scale, y + 66.0 * scale)
    wedge.line_to(x + 168.0 * scale, y + 14.0 * scale)
    wedge.line_to(x + 206.0 * scale, y + 66.0 * scale)
    wedge.close()
    fill_path_aa(c, wedge, Color(90, 160, 120))
    draw_line_aa(
        c,
        x + 12.0 * scale,
        y + 70.0 * scale,
        x + 228.0 * scale,
        y + 12.0 * scale,
        INK,
        1.5 * scale,
    )


def main() raises:
    var cache = FontCache()
    var sheet = Canvas(W, H, SHEET)
    var tile = _tile()

    draw_text(
        sheet,
        30.0,
        30.0,
        "draw_canvas through a Matrix2D: scale, rotate, crop, fade",
        INK,
        size=15.0,
        cache=cache,
    )

    # Doubled two ways. The matrix maps the tile's texel space, where
    # pixel (i, j) covers [i, i+1) x [j, j+1), so a factor of two puts
    # each source pixel on exactly four destination pixels and the
    # nearest-neighbor panel is the tile with no new colors in it.
    draw_canvas(
        sheet,
        tile,
        Matrix2D.scaling(2.0, 2.0).then(Matrix2D.translation(30.0, 60.0)),
        filter=Filter.NEAREST,
    )
    draw_canvas(
        sheet,
        tile,
        Matrix2D.scaling(2.0, 2.0).then(Matrix2D.translation(190.0, 60.0)),
        filter=Filter.BILINEAR,
    )

    # A quarter of a turn's worth of rotation about the tile's own
    # center: move the center to the origin, scale and turn there, and
    # put it where the panel is.
    var turned = (
        Matrix2D.translation(-32.0, -32.0)
        .then(Matrix2D.scaling(1.8, 1.8))
        .then(Matrix2D.rotation(25.0 * pi / 180.0))
        .then(Matrix2D.translation(414.0, 124.0))
    )
    draw_canvas(sheet, tile, turned)

    # The top-left quadrant at 4x. The crop picks the source pixels and
    # the matrix picks where they land, so the two are independent.
    draw_canvas(
        sheet,
        tile,
        0,
        0,
        32,
        32,
        Matrix2D.scaling(4.0, 4.0).then(Matrix2D.translation(510.0, 60.0)),
        filter=Filter.NEAREST,
    )

    for name_and_x in [
        ("2x nearest", 30.0),
        ("2x bilinear", 190.0),
        ("1.8x turned 25", 350.0),
        ("top-left quarter, 4x", 510.0),
    ]:
        draw_text(
            sheet,
            name_and_x[1],
            210.0,
            name_and_x[0],
            LABEL,
            size=12.0,
            cache=cache,
        )

    # Half opacity over a rule, to show the fade applying to a mapped
    # draw as it does to a blit.
    fill_rect(sheet, 30, 252, 640, 6, Color(200, 205, 215))
    draw_canvas(
        sheet,
        tile,
        Matrix2D.scaling(1.2, 1.2).then(Matrix2D.translation(30.0, 222.0)),
        0.5,
    )
    draw_text(
        sheet,
        140.0,
        250.0,
        "1.2x at opacity 0.5",
        LABEL,
        size=12.0,
        cache=cache,
    )

    # Supersampling: the figure rendered at 2x into its own canvas and
    # placed at 0.5x, beside the same figure rendered at 1x.
    var big = Canvas(480, 160, Color(255, 255, 255))
    _figure(big, 0.0, 0.0, 2.0)
    draw_canvas(
        sheet,
        big,
        Matrix2D.scaling(0.5, 0.5).then(Matrix2D.translation(30.0, 300.0)),
        filter=Filter.BILINEAR,
    )
    _figure(sheet, 400.0, 300.0, 1.0)

    draw_text(
        sheet,
        30.0,
        400.0,
        "rendered at 2x, placed at 0.5x",
        LABEL,
        size=12.0,
        cache=cache,
    )
    draw_text(
        sheet, 400.0, 400.0, "rendered at 1x", LABEL, size=12.0, cache=cache
    )

    write_png(sheet, "examples/out_draw_image.png")
    print("wrote examples/out_draw_image.png")