///|
/// 2D drawing context. Mirrors a subset of `CanvasRenderingContext2D`.
pub struct Context {
canvas : Canvas
mut state : DrawState
stack : Array[DrawState]
mut current_path : Path2D
glyph_cache : Map[Int, @font.GlyphBitmap]
}
///|
pub fn Context::new(canvas : Canvas) -> Context {
{
canvas,
state: DrawState::default(),
stack: [],
current_path: Path2D::new(),
glyph_cache: {},
}
}
///|
pub fn Context::set_fill_style(self : Context, c : Color) -> Unit {
self.state.fill_style = FillStyle::Solid(c)
}
///|
pub fn Context::set_stroke_style(self : Context, c : Color) -> Unit {
self.state.stroke_style = FillStyle::Solid(c)
}
///|
pub fn Context::set_fill_style_of(self : Context, style : FillStyle) -> Unit {
self.state.fill_style = style
}
///|
pub fn Context::set_stroke_style_of(self : Context, style : FillStyle) -> Unit {
self.state.stroke_style = style
}
///|
pub fn Context::set_line_width(self : Context, w : Double) -> Unit {
self.state.line_width = w
}
///|
pub fn Context::set_global_alpha(self : Context, a : Double) -> Unit {
self.state.global_alpha = a
}
///|
pub fn Context::set_font(
self : Context,
font : @font.TTFont,
size_px : Double,
) -> Unit {
self.glyph_cache.clear()
self.state.font = Some(font)
self.state.font_size = size_px
}
///|
pub fn Context::set_text_align(self : Context, align : TextAlign) -> Unit {
self.state.text_align = align
}
///|
pub fn Context::set_text_baseline(
self : Context,
baseline : TextBaseline,
) -> Unit {
self.state.text_baseline = baseline
}
///|
/// Push the current draw state onto the save stack.
pub fn Context::save(self : Context) -> Unit {
self.stack.push(self.state.snapshot())
}
///|
/// Pop the last saved state off the stack. A `restore` with no prior `save`
/// is a no-op (matches HTML Canvas semantics).
pub fn Context::restore(self : Context) -> Unit {
match self.stack.pop() {
Some(prev) => self.state = prev
None => ()
}
}
///|
pub fn Context::translate(self : Context, x : Double, y : Double) -> Unit {
self.state.transform = self.state.transform.translate(x, y)
}
///|
pub fn Context::scale(self : Context, sx : Double, sy : Double) -> Unit {
self.state.transform = self.state.transform.scale(sx, sy)
}
///|
pub fn Context::rotate(self : Context, angle : Double) -> Unit {
self.state.transform = self.state.transform.rotate(angle)
}
///|
pub fn Context::transform(
self : Context,
a : Double,
b : Double,
c : Double,
d : Double,
e : Double,
f : Double,
) -> Unit {
self.state.transform = self.state.transform.multiply(
Matrix2D::of(a, b, c, d, e, f),
)
}
///|
pub fn Context::set_transform(
self : Context,
a : Double,
b : Double,
c : Double,
d : Double,
e : Double,
f : Double,
) -> Unit {
self.state.transform = Matrix2D::of(a, b, c, d, e, f)
}
///|
pub fn Context::reset_transform(self : Context) -> Unit {
self.state.transform = Matrix2D::identity()
}
///|
pub fn Context::fill_path(self : Context, path : Path2D) -> Unit {
let polylines = flatten(path.commands, self.state.transform, 0.25)
fill_polylines(
self.canvas.pixels,
self.canvas.width,
self.canvas.height,
polylines,
self.state.fill_style,
self.state.transform,
self.state.clip,
self.state.global_alpha,
self.canvas.antialias,
)
}
///|
pub fn Context::stroke_path(self : Context, path : Path2D) -> Unit {
let polylines = flatten(path.commands, self.state.transform, 0.25)
let effective = if is_valid_dash_pattern(self.state.line_dash) {
dash_polylines(polylines, self.state.line_dash, self.state.line_dash_offset)
} else {
polylines
}
stroke_polylines(
self.canvas.pixels,
self.canvas.width,
self.canvas.height,
effective,
self.state.stroke_style,
self.state.line_width,
self.state.line_cap,
self.state.line_join,
self.state.transform,
self.state.clip,
self.state.global_alpha,
self.canvas.antialias,
)
}
///|
pub fn Context::fill_rect(
self : Context,
x : Double,
y : Double,
w : Double,
h : Double,
) -> Unit {
let path = Path2D::new()
path.rect(x, y, w, h)
self.fill_path(path)
}
///|
pub fn Context::stroke_rect(
self : Context,
x : Double,
y : Double,
w : Double,
h : Double,
) -> Unit {
let path = Path2D::new()
path.rect(x, y, w, h)
self.stroke_path(path)
}
///|
pub fn Context::begin_path(self : Context) -> Unit {
self.current_path = Path2D::new()
}
///|
pub fn Context::close_path(self : Context) -> Unit {
self.current_path.close()
}
///|
pub fn Context::move_to(self : Context, x : Double, y : Double) -> Unit {
self.current_path.move_to(x, y)
}
///|
pub fn Context::line_to(self : Context, x : Double, y : Double) -> Unit {
self.current_path.line_to(x, y)
}
///|
pub fn Context::quadratic_curve_to(
self : Context,
cpx : Double,
cpy : Double,
x : Double,
y : Double,
) -> Unit {
self.current_path.quadratic_curve_to(cpx, cpy, x, y)
}
///|
pub fn Context::bezier_curve_to(
self : Context,
cp1x : Double,
cp1y : Double,
cp2x : Double,
cp2y : Double,
x : Double,
y : Double,
) -> Unit {
self.current_path.bezier_curve_to(cp1x, cp1y, cp2x, cp2y, x, y)
}
///|
pub fn Context::arc(
self : Context,
x : Double,
y : Double,
radius : Double,
start_angle : Double,
end_angle : Double,
counterclockwise? : Bool = false,
) -> Unit {
self.current_path.arc(x, y, radius, start_angle, end_angle, counterclockwise~)
}
///|
pub fn Context::rect(
self : Context,
x : Double,
y : Double,
w : Double,
h : Double,
) -> Unit {
self.current_path.rect(x, y, w, h)
}
///|
/// Fills the current_path (built by begin_path / move_to / line_to / ...).
pub fn Context::fill(self : Context) -> Unit {
self.fill_path(self.current_path)
}
///|
/// Strokes the current_path (built by begin_path / move_to / line_to / ...).
pub fn Context::stroke(self : Context) -> Unit {
self.stroke_path(self.current_path)
}
///|
pub fn Context::clear_rect(
self : Context,
x : Double,
y : Double,
w : Double,
h : Double,
) -> Unit {
let cmds : Array[PathCmd] = [
PathCmd::MoveTo(x, y),
PathCmd::LineTo(x + w, y),
PathCmd::LineTo(x + w, y + h),
PathCmd::LineTo(x, y + h),
PathCmd::Close,
]
let polylines = flatten(cmds, self.state.transform, 0.25)
clear_polylines(
self.canvas.pixels,
self.canvas.width,
self.canvas.height,
polylines,
)
}
///|
/// Draw an image onto the canvas at `(dx, dy)`.
///
/// - `scale` optionally rescales the destination rect (defaults to `(1, 1)`).
/// - `src_rect` optionally selects a sub-region of the source (defaults to
/// the whole image).
/// - `smoothing` toggles bilinear (`true`) vs nearest-neighbor (`false`)
/// sampling. Defaults to `true` to mirror the HTML Canvas default.
pub fn Context::draw_image(
self : Context,
image : @image.ImageData,
dx : Double,
dy : Double,
scale? : (Double, Double)? = None,
src_rect? : (Double, Double, Double, Double)? = None,
smoothing? : Bool = true,
) -> Unit {
let (sx, sy, sw, sh) = match src_rect {
Some((a, b, c, d)) => (a, b, c, d)
None => (0.0, 0.0, image.width.to_double(), image.height.to_double())
}
let (scale_x, scale_y) = match scale {
Some((a, b)) => (a, b)
None => (1.0, 1.0)
}
draw_image_raw(
self.canvas.pixels,
self.canvas.width,
self.canvas.height,
image,
dx,
dy,
scale_x,
scale_y,
sx,
sy,
sw,
sh,
self.state.transform,
self.state.global_alpha,
smoothing,
)
}
///|
/// Intersects the current path with the active clip region. Subsequent
/// fills and strokes are masked to the intersection. Matches HTML Canvas
/// `ctx.clip()` semantics (non-zero winding, intersection-only).
///
/// On first use, allocates a zeroed ClipMask sized to the canvas framebuffer
/// and rasterizes the current path into it. On subsequent calls, rasterizes
/// the path into a temporary mask and multiplies it into the existing mask.
/// Uses the canvas's `antialias` setting for the clip rasterization.
pub fn Context::clip(self : Context) -> Unit {
let polylines = flatten(
self.current_path.commands,
self.state.transform,
0.25,
)
let aa = self.canvas.antialias
match self.state.clip {
None => {
let mask = ClipMask::new(self.canvas.width, self.canvas.height)
for i in 0.. {
let tmp = ClipMask::new(self.canvas.width, self.canvas.height)
for i in 0.. Unit {
self.state.line_dash_offset = offset
}
///|
/// Set the dash pattern for subsequent strokes. An empty array produces a
/// solid line. If the array length is odd, it is internally doubled (e.g.
/// `[5, 10, 15]` becomes `[5, 10, 15, 5, 10, 15]`). Negative, NaN, or
/// infinite values are stored as-is and cause the pattern to be silently
/// treated as solid at stroke time. Matches HTML Canvas `setLineDash`.
pub fn Context::set_line_dash(self : Context, segments : Array[Double]) -> Unit {
let n = segments.length()
if n == 0 {
self.state.line_dash = []
return
}
let len = if n % 2 == 0 { n } else { n * 2 }
let copy : Array[Double] = []
for i in 0.. Unit {
self.state.line_cap = cap
}
///|
/// Set the line join shape for subsequent strokes. Matches HTML Canvas
/// `ctx.lineJoin`. `Miter` preserves the existing automatic bevel
/// fallback at the fixed 10.0 miter limit.
pub fn Context::set_line_join(self : Context, join : LineJoin) -> Unit {
self.state.line_join = join
}
///|
pub fn Context::ellipse(
self : Context,
x : Double,
y : Double,
rx : Double,
ry : Double,
rotation : Double,
start_angle : Double,
end_angle : Double,
counterclockwise? : Bool = false,
) -> Unit {
self.current_path.ellipse(
x,
y,
rx,
ry,
rotation,
start_angle,
end_angle,
counterclockwise~,
)
}
///|
pub fn Context::arc_to(
self : Context,
x1 : Double,
y1 : Double,
x2 : Double,
y2 : Double,
radius : Double,
) -> Unit {
self.current_path.arc_to(x1, y1, x2, y2, radius)
}
///|
pub fn Context::round_rect(
self : Context,
x : Double,
y : Double,
w : Double,
h : Double,
radii : Array[Double],
) -> Unit {
self.current_path.round_rect(x, y, w, h, radii)
}
///|
/// Convenience for a rounded rect with all four corners at the same radius.
pub fn Context::round_rect_uniform(
self : Context,
x : Double,
y : Double,
w : Double,
h : Double,
r : Double,
) -> Unit {
self.current_path.round_rect(x, y, w, h, [r])
}