use std::rc::Rc;
mod code {
pub const FILL_STYLE: u8 = 1;
pub const STROKE_STYLE: u8 = 2;
pub const GLOBAL_ALPHA: u8 = 3;
pub const LINE_WIDTH: u8 = 4;
pub const LINE_WIDTH_PX: u8 = 5;
pub const LINE_CAP: u8 = 6;
pub const FILL_RECT: u8 = 7;
pub const FILL_CIRCLE: u8 = 8;
pub const FILL_CIRCLE_PX: u8 = 9;
pub const STROKE_CIRCLE: u8 = 10;
pub const POLYLINE: u8 = 11;
pub const LINE_JOIN: u8 = 12;
}
const UNITS_PIXELS: u8 = 0;
const UNITS_VIEW_BOX: u8 = 1;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ViewBox {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl ViewBox {
pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
Self {
x,
y,
width,
height,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LineCap {
Butt,
Round,
Square,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LineJoin {
Miter,
Round,
Bevel,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Drawing {
bytes: Rc<Vec<u8>>,
}
impl Drawing {
pub fn new(view_box: ViewBox) -> Self {
let mut bytes = vec![UNITS_VIEW_BOX];
for value in [view_box.x, view_box.y, view_box.width, view_box.height] {
bytes.extend_from_slice(&value.to_le_bytes());
}
Self {
bytes: Rc::new(bytes),
}
}
pub fn pixels() -> Self {
Self {
bytes: Rc::new(vec![UNITS_PIXELS]),
}
}
pub fn fill_style(&mut self, color: &str) -> &mut Self {
self.text(code::FILL_STYLE, color)
}
pub fn stroke_style(&mut self, color: &str) -> &mut Self {
self.text(code::STROKE_STYLE, color)
}
pub fn global_alpha(&mut self, alpha: f32) -> &mut Self {
self.op(code::GLOBAL_ALPHA, &[alpha])
}
pub fn line_width(&mut self, width: f32) -> &mut Self {
self.op(code::LINE_WIDTH, &[width])
}
pub fn line_width_px(&mut self, width: f32) -> &mut Self {
self.op(code::LINE_WIDTH_PX, &[width])
}
pub fn line_cap(&mut self, cap: LineCap) -> &mut Self {
let value = match cap {
LineCap::Butt => 0,
LineCap::Round => 1,
LineCap::Square => 2,
};
let bytes = self.bytes_mut();
bytes.push(code::LINE_CAP);
bytes.push(value);
self
}
pub fn line_join(&mut self, join: LineJoin) -> &mut Self {
let value = match join {
LineJoin::Miter => 0,
LineJoin::Round => 1,
LineJoin::Bevel => 2,
};
let bytes = self.bytes_mut();
bytes.push(code::LINE_JOIN);
bytes.push(value);
self
}
pub fn fill_rect(&mut self, x: f32, y: f32, width: f32, height: f32) -> &mut Self {
self.op(code::FILL_RECT, &[x, y, width, height])
}
pub fn fill_circle(&mut self, x: f32, y: f32, radius: f32) -> &mut Self {
self.op(code::FILL_CIRCLE, &[x, y, radius])
}
pub fn fill_circle_px(&mut self, x: f32, y: f32, radius: f32) -> &mut Self {
self.op(code::FILL_CIRCLE_PX, &[x, y, radius])
}
pub fn stroke_circle(&mut self, x: f32, y: f32, radius: f32) -> &mut Self {
self.op(code::STROKE_CIRCLE, &[x, y, radius])
}
pub fn polyline(&mut self, points: impl IntoIterator<Item = (f32, f32)>) -> &mut Self {
let bytes = self.bytes_mut();
bytes.push(code::POLYLINE);
let count_at = bytes.len();
bytes.extend_from_slice(&0u32.to_le_bytes());
let mut count: u32 = 0;
for (x, y) in points {
bytes.extend_from_slice(&x.to_le_bytes());
bytes.extend_from_slice(&y.to_le_bytes());
count += 1;
}
bytes[count_at..count_at + 4].copy_from_slice(&count.to_le_bytes());
self
}
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
fn op(&mut self, code: u8, args: &[f32]) -> &mut Self {
let bytes = self.bytes_mut();
bytes.push(code);
for arg in args {
bytes.extend_from_slice(&arg.to_le_bytes());
}
self
}
fn text(&mut self, code: u8, value: &str) -> &mut Self {
let mut end = value.len().min(u16::MAX as usize);
while !value.is_char_boundary(end) {
end -= 1;
}
let bytes = self.bytes_mut();
bytes.push(code);
bytes.extend_from_slice(&(end as u16).to_le_bytes());
bytes.extend_from_slice(&value.as_bytes()[..end]);
self
}
fn bytes_mut(&mut self) -> &mut Vec<u8> {
Rc::make_mut(&mut self.bytes)
}
}
#[cfg(test)]
mod tests {
use super::*;
pub(crate) fn fixture() -> Drawing {
let mut drawing = Drawing::new(ViewBox::new(-8.0, -5.0, 16.0, 10.0));
drawing
.fill_style("#ffd27f")
.stroke_style("żółć")
.global_alpha(0.5)
.line_width(2.0)
.line_width_px(1.5)
.line_cap(LineCap::Round)
.line_join(LineJoin::Bevel)
.fill_rect(-8.0, -5.0, 16.0, 10.0)
.fill_circle(1.0, 2.0, 3.0)
.fill_circle_px(-1.0, -2.0, 4.0)
.stroke_circle(0.0, 0.0, 6.0)
.polyline([(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)]);
drawing
}
const FIXTURE: &[u8] = &[
1, 0, 0, 0, 193, 0, 0, 160, 192, 0, 0, 128, 65, 0, 0, 32, 65, 1, 7, 0, 35, 102, 102, 100,
50, 55, 102, 2, 8, 0, 197, 188, 195, 179, 197, 130, 196, 135, 3, 0, 0, 0, 63, 4, 0, 0, 0,
64, 5, 0, 0, 192, 63, 6, 1, 12, 2, 7, 0, 0, 0, 193, 0, 0, 160, 192, 0, 0, 128, 65, 0, 0,
32, 65, 8, 0, 0, 128, 63, 0, 0, 0, 64, 0, 0, 64, 64, 9, 0, 0, 128, 191, 0, 0, 0, 192, 0, 0,
128, 64, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 64, 11, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 128, 63, 0, 0, 128, 63, 0, 0, 0, 64, 0, 0, 0, 0,
];
#[test]
fn matches_the_cross_language_fixture() {
assert_eq!(
fixture().as_bytes(),
FIXTURE,
"the drawing format changed - update canvas.test.ts to match, or this is a bug"
);
}
#[test]
fn a_drawing_in_pixels_has_no_view_box() {
let mut drawing = Drawing::pixels();
drawing.fill_circle(1.0, 2.0, 3.0);
assert_eq!(drawing.as_bytes()[0], UNITS_PIXELS);
assert_eq!(drawing.as_bytes().len(), 1 + 1 + 12);
}
#[test]
fn drawings_are_equal_when_they_draw_the_same() {
let draw = || {
let mut drawing = Drawing::pixels();
drawing.fill_style("red").fill_circle(1.0, 2.0, 3.0);
drawing
};
let mut moved = draw();
moved.fill_circle(1.0, 2.0, 3.0);
assert_eq!(draw(), draw());
assert_ne!(draw(), moved);
}
#[test]
fn a_clone_draws_on_alone() {
let mut drawing = Drawing::pixels();
drawing.fill_circle(1.0, 2.0, 3.0);
let kept = drawing.clone();
drawing.fill_circle(4.0, 5.0, 6.0);
assert_eq!(kept.as_bytes().len(), 1 + 13);
assert_eq!(drawing.as_bytes().len(), 1 + 13 + 13);
}
#[test]
fn a_polyline_counts_its_points() {
let mut drawing = Drawing::pixels();
drawing.polyline((0..300).map(|step| (step as f32, 0.0)));
let bytes = drawing.as_bytes();
assert_eq!(bytes[1], code::POLYLINE);
assert_eq!(
u32::from_le_bytes([bytes[2], bytes[3], bytes[4], bytes[5]]),
300
);
assert_eq!(bytes.len(), 1 + 1 + 4 + 300 * 8);
}
#[test]
fn a_style_too_long_is_cut_between_characters() {
let long = "ż".repeat(40_000);
let mut drawing = Drawing::pixels();
drawing.fill_style(&long);
let bytes = drawing.as_bytes();
let length = u16::from_le_bytes([bytes[2], bytes[3]]) as usize;
assert_eq!(length, 65_534);
assert!(std::str::from_utf8(&bytes[4..4 + length]).is_ok());
}
}