// Canvas backend: interpret the backend-neutral `DrawCmd` list (see draw.mbt)
// into HTML5 Canvas `ctx.*` JavaScript. Shares the AST fold with SVG/PDF.
//
// Canvas is y-down like the baked draw list (no flip), and supports gradients
// and per-region clipping/alpha natively, so it is the most faithful backend.
///|
/// Render the image to Canvas-drawing JavaScript for the given size.
pub fn Image::to_js(self : Image, width : Double, height : Double) -> String {
let js : Array[String] = []
let id = [0] // gradient variable counter
for cmd in self.to_draw_list(width, height) {
canvas_cmd(cmd, width, height, js, id)
}
let mut doc = @canvas.new_canvas(width, height)
for c in js {
doc = doc.add_command(c)
}
doc.to_js()
}
///|
fn canvas_cmd(
cmd : DrawCmd,
width : Double,
height : Double,
js : Array[String],
id : Array[Int],
) -> Unit {
match cmd {
FillPath(path, paint, area) => {
canvas_path(path, js)
match area {
Outline(w) => {
canvas_set_paint(paint, "strokeStyle", js, id)
js.push("ctx.lineWidth = \{w};")
js.push("ctx.lineCap = 'round';")
js.push("ctx.lineJoin = 'round';")
js.push("ctx.stroke();")
}
_ => {
canvas_set_paint(paint, "fillStyle", js, id)
js.push("ctx.fill('\{canvas_rule(area)}');")
}
}
}
FillViewport(paint) => {
canvas_set_paint(paint, "fillStyle", js, id)
js.push("ctx.fillRect(0, 0, \{width}, \{height});")
}
RasterCell(x, y, w, h, c) => {
js.push("ctx.fillStyle = '\{canvas_color(c)}';")
js.push("ctx.fillRect(\{x}, \{y}, \{w}, \{h});")
}
PushClip(path, area) => {
js.push("ctx.save();")
canvas_path(path, js)
js.push("ctx.clip('\{canvas_rule(area)}');")
}
PopClip => js.push("ctx.restore();")
PushOpacity(a) => {
js.push("ctx.save();")
js.push("ctx.globalAlpha = \{a};")
}
PopOpacity => js.push("ctx.restore();")
DrawText(content, x, y, size, color) => {
js.push("ctx.font = '\{size}px Arial, sans-serif';")
js.push("ctx.fillStyle = '\{canvas_color(color)}';")
js.push("ctx.textAlign = 'center';")
js.push("ctx.textBaseline = 'middle';")
js.push("ctx.fillText('\{js_text_escape(content)}', \{x}, \{y});")
}
}
}
///|
/// Escape a string for a single-quoted JavaScript literal, including line
/// terminators (a raw newline in a JS string literal is a syntax error).
fn js_text_escape(s : String) -> String {
let parts : Array[String] = []
for ch in s {
parts.push(
match ch {
'\\' => "\\\\"
'\'' => "\\'"
'\n' => "\\n"
'\r' => "\\r"
'\t' => "\\t"
_ => ch.to_string()
},
)
}
parts.join("")
}
///|
fn canvas_rule(area : Area) -> String {
match area {
Anz => "nonzero"
Aeo => "evenodd"
Outline(_) => "nonzero" // strokes are handled before canvas_rule
}
}
///|
/// Set `ctx.` ("fillStyle" or "strokeStyle") from a paint, creating a
/// gradient object if needed.
fn canvas_set_paint(
paint : Paint,
prop : String,
js : Array[String],
id : Array[Int],
) -> Unit {
match paint {
Solid(c) => js.push("ctx.\{prop} = '\{canvas_color(c)}';")
Linear(stops, p0, p1) => {
let g = "grad\{id[0]}"
id[0] = id[0] + 1
js.push(
"const \{g} = ctx.createLinearGradient(\{p0.x}, \{p0.y}, \{p1.x}, \{p1.y});",
)
canvas_stops(g, stops, js)
js.push("ctx.\{prop} = \{g};")
}
Radial(stops, c, r) => {
let g = "grad\{id[0]}"
id[0] = id[0] + 1
js.push(
"const \{g} = ctx.createRadialGradient(\{c.x}, \{c.y}, 0, \{c.x}, \{c.y}, \{r});",
)
canvas_stops(g, stops, js)
js.push("ctx.\{prop} = \{g};")
}
}
}
///|
fn canvas_stops(g : String, stops : Array[Stop], js : Array[String]) -> Unit {
for s in stops {
js.push("\{g}.addColorStop(\{s.offset}, '\{canvas_color(s.color)}');")
}
}
///|
fn canvas_color(c : Color) -> String {
if c.a < 1.0 {
let r = (c.r * 255.0).to_int()
let g = (c.g * 255.0).to_int()
let b = (c.b * 255.0).to_int()
"rgba(\{r},\{g},\{b},\{c.a})"
} else {
@color.to_hex(c)
}
}
///|
/// Emit `ctx` path-construction calls for a (baked) path.
fn canvas_path(path : Path, js : Array[String]) -> Unit {
js.push("ctx.beginPath();")
let mut cur = Point(0.0, 0.0)
let mut start = Point(0.0, 0.0)
for seg in path.0 {
match seg {
MoveTo(p) => {
js.push("ctx.moveTo(\{p.x}, \{p.y});")
cur = p
start = p
}
LineTo(p) => {
js.push("ctx.lineTo(\{p.x}, \{p.y});")
cur = p
}
CurveTo(c1, c2, e) => {
js.push(
"ctx.bezierCurveTo(\{c1.x}, \{c1.y}, \{c2.x}, \{c2.y}, \{e.x}, \{e.y});",
)
cur = e
}
QCurveTo(c, e) => {
js.push("ctx.quadraticCurveTo(\{c.x}, \{c.y}, \{e.x}, \{e.y});")
cur = e
}
EArcTo(rx, ry, rot, la, sw, e) => {
for pt in flatten_arc(cur, rx, ry, rot, la, sw, e) {
js.push("ctx.lineTo(\{pt.x}, \{pt.y});")
}
cur = e
}
Close => {
js.push("ctx.closePath();")
cur = start
}
}
}
}