Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
276 changes: 247 additions & 29 deletions moli-canvas/src/rect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,261 @@ use crate::types::{CanvasRect, surface_matches_len};

pub fn canonicalize_fill_style(raw: &str) -> Option<String> {
let value = raw.trim().to_ascii_lowercase();
match value.as_str() {
"black" => Some("#000000".to_owned()),
"red" => Some("#ff0000".to_owned()),
"rebeccapurple" => Some("#663399".to_owned()),
_ if value.starts_with('#') => canonical_hex_color(&value),
_ => None,
if value.starts_with('#') {
return canonical_hex_color(&value);
}
if let Some((red, green, blue)) = css_named_color_rgb(&value) {
return Some(format!("#{red:02x}{green:02x}{blue:02x}"));
}
if value == "transparent" {
return Some("rgba(0, 0, 0, 0)".to_owned());
}
if let Some((red, green, blue, alpha)) = parse_rgb_function(&value) {
if alpha == u8::MAX {
return Some(format!("rgb({red}, {green}, {blue})"));
}
let alpha = (f64::from(alpha) / 255.0 * 100.0).round() / 100.0;
return Some(format!("rgba({red}, {green}, {blue}, {alpha:.2})"));
}
None
}

pub fn fill_style_rgba(style: &str) -> [u8; 4] {
if let Some(hex) = style.strip_prefix('#')
&& hex.len() == 6
{
return [
u8::from_str_radix(&hex[0..2], 16).unwrap_or(0),
u8::from_str_radix(&hex[2..4], 16).unwrap_or(0),
u8::from_str_radix(&hex[4..6], 16).unwrap_or(0),
255,
];
}
if let Some(body) = style
.strip_prefix("rgba(")
.and_then(|value| value.strip_suffix(')'))
{
let parts = body.split(',').map(|part| part.trim()).collect::<Vec<_>>();
if parts.len() == 4 {
let red = parts[0].parse::<u8>().unwrap_or(0);
let green = parts[1].parse::<u8>().unwrap_or(0);
let blue = parts[2].parse::<u8>().unwrap_or(0);
let alpha =
(parts[3].parse::<f64>().unwrap_or(1.0).clamp(0.0, 1.0) * 255.0).round() as u8;
return [red, green, blue, alpha];
}
let value = style.trim().to_ascii_lowercase();
if let Some(rgba) = hex_color_rgba(&value) {
return rgba;
}
if let Some((red, green, blue, alpha)) = parse_rgb_function(&value) {
return [red, green, blue, alpha];
}
if let Some((red, green, blue)) = css_named_color_rgb(&value) {
return [red, green, blue, 255];
}
if value == "transparent" {
return [0, 0, 0, 0];
}
[0, 0, 0, 255]
}

fn hex_color_rgba(value: &str) -> Option<[u8; 4]> {
let hex = value.strip_prefix('#')?;
if hex.is_empty() || !hex.chars().all(|char| char.is_ascii_hexdigit()) {
return None;
}
let (red, green, blue, alpha) = match hex.len() {
3 => (
hex[0..1].repeat(2),
hex[1..2].repeat(2),
hex[2..3].repeat(2),
"ff".to_owned(),
),
4 => (
hex[0..1].repeat(2),
hex[1..2].repeat(2),
hex[2..3].repeat(2),
hex[3..4].repeat(2),
),
6 => (
hex[0..2].to_owned(),
hex[2..4].to_owned(),
hex[4..6].to_owned(),
"ff".to_owned(),
),
8 => (
hex[0..2].to_owned(),
hex[2..4].to_owned(),
hex[4..6].to_owned(),
hex[6..8].to_owned(),
),
_ => return None,
};
Some([
u8::from_str_radix(&red, 16).ok()?,
u8::from_str_radix(&green, 16).ok()?,
u8::from_str_radix(&blue, 16).ok()?,
u8::from_str_radix(&alpha, 16).ok()?,
])
}

fn parse_rgb_function(value: &str) -> Option<(u8, u8, u8, u8)> {
let body = value
.strip_prefix("rgb(")
.or_else(|| value.strip_prefix("rgba("))?
.strip_suffix(')')?;
let parts = body.split(',').map(|part| part.trim()).collect::<Vec<_>>();
let red = parse_channel(parts.first()?)?;
let green = parse_channel(parts.get(1)?)?;
let blue = parse_channel(parts.get(2)?)?;
let alpha = match parts.get(3) {
Some(alpha) => {
let parsed = if let Some(percent) = alpha.strip_suffix('%') {
percent.trim().parse::<f64>().ok()? / 100.0
} else {
alpha.trim().parse::<f64>().ok()?
};
(parsed.clamp(0.0, 1.0) * 255.0).round() as u8
}
None => 255,
};
Some((red, green, blue, alpha))
}

fn parse_channel(value: &str) -> Option<u8> {
if let Some(percent) = value.strip_suffix('%') {
let value = percent.trim().parse::<f64>().ok()?;
return Some((value.clamp(0.0, 100.0) / 100.0 * 255.0).round() as u8);
}
let value = value.trim().parse::<f64>().ok()?;
Some(value.clamp(0.0, 255.0) as u8)
}

fn css_named_color_rgb(value: &str) -> Option<(u8, u8, u8)> {
Some(match value.to_ascii_lowercase().as_str() {
"aliceblue" => (240, 248, 255),
"antiquewhite" => (250, 235, 215),
"aqua" => (0, 255, 255),
"aquamarine" => (127, 255, 212),
"azure" => (240, 255, 255),
"beige" => (245, 245, 220),
"bisque" => (255, 228, 196),
"black" => (0, 0, 0),
"blanchedalmond" => (255, 235, 205),
"blue" => (0, 0, 255),
"blueviolet" => (138, 43, 226),
"brown" => (165, 42, 42),
"burlywood" => (222, 184, 135),
"cadetblue" => (95, 158, 160),
"chartreuse" => (127, 255, 0),
"chocolate" => (210, 105, 30),
"coral" => (255, 127, 80),
"cornflowerblue" => (100, 149, 237),
"cornsilk" => (255, 248, 220),
"crimson" => (220, 20, 60),
"cyan" => (0, 255, 255),
"darkblue" => (0, 0, 139),
"darkcyan" => (0, 139, 139),
"darkgoldenrod" => (184, 134, 11),
"darkgray" | "darkgrey" => (169, 169, 169),
"darkgreen" => (0, 100, 0),
"darkkhaki" => (189, 183, 107),
"darkmagenta" => (139, 0, 139),
"darkolivegreen" => (85, 107, 47),
"darkorange" => (255, 140, 0),
"darkorchid" => (153, 50, 204),
"darkred" => (139, 0, 0),
"darksalmon" => (233, 150, 122),
"darkseagreen" => (143, 188, 143),
"darkslateblue" => (72, 61, 139),
"darkslategray" | "darkslategrey" => (47, 79, 79),
"darkturquoise" => (0, 206, 209),
"darkviolet" => (148, 0, 211),
"deeppink" => (255, 20, 147),
"deepskyblue" => (0, 191, 255),
"dimgray" | "dimgrey" => (105, 105, 105),
"dodgerblue" => (30, 144, 255),
"firebrick" => (178, 34, 34),
"floralwhite" => (255, 250, 240),
"forestgreen" => (34, 139, 34),
"fuchsia" => (255, 0, 255),
"gainsboro" => (220, 220, 220),
"ghostwhite" => (248, 248, 255),
"gold" => (255, 215, 0),
"goldenrod" => (218, 165, 32),
"gray" | "grey" => (128, 128, 128),
"green" => (0, 128, 0),
"greenyellow" => (173, 255, 47),
"honeydew" => (240, 255, 240),
"hotpink" => (255, 105, 180),
"indianred" => (205, 92, 92),
"indigo" => (75, 0, 130),
"ivory" => (255, 255, 240),
"khaki" => (240, 230, 140),
"lavender" => (230, 230, 250),
"lavenderblush" => (255, 240, 245),
"lawngreen" => (124, 252, 0),
"lemonchiffon" => (255, 250, 205),
"lightblue" => (173, 216, 230),
"lightcoral" => (240, 128, 128),
"lightcyan" => (224, 255, 255),
"lightgoldenrodyellow" => (250, 250, 210),
"lightgray" | "lightgrey" => (211, 211, 211),
"lightgreen" => (144, 238, 144),
"lightpink" => (255, 182, 193),
"lightsalmon" => (255, 160, 122),
"lightseagreen" => (32, 178, 170),
"lightskyblue" => (135, 206, 250),
"lightslategray" | "lightslategrey" => (119, 136, 153),
"lightsteelblue" => (176, 196, 222),
"lightyellow" => (255, 255, 224),
"lime" => (0, 255, 0),
"limegreen" => (50, 205, 50),
"linen" => (250, 240, 230),
"magenta" => (255, 0, 255),
"maroon" => (128, 0, 0),
"mediumaquamarine" => (102, 205, 170),
"mediumblue" => (0, 0, 205),
"mediumorchid" => (186, 85, 211),
"mediumpurple" => (147, 112, 219),
"mediumseagreen" => (60, 179, 113),
"mediumslateblue" => (123, 104, 238),
"mediumspringgreen" => (0, 250, 154),
"mediumturquoise" => (72, 209, 204),
"mediumvioletred" => (199, 21, 133),
"midnightblue" => (25, 25, 112),
"mintcream" => (245, 255, 250),
"mistyrose" => (255, 228, 225),
"moccasin" => (255, 228, 181),
"navajowhite" => (255, 222, 173),
"navy" => (0, 0, 128),
"oldlace" => (253, 245, 230),
"olive" => (128, 128, 0),
"olivedrab" => (107, 142, 35),
"orange" => (255, 165, 0),
"orangered" => (255, 69, 0),
"orchid" => (218, 112, 214),
"palegoldenrod" => (238, 232, 170),
"palegreen" => (152, 251, 152),
"paleturquoise" => (175, 238, 238),
"palevioletred" => (219, 112, 147),
"papayawhip" => (255, 239, 213),
"peachpuff" => (255, 218, 185),
"peru" => (205, 133, 63),
"pink" => (255, 192, 203),
"plum" => (221, 160, 221),
"powderblue" => (176, 224, 230),
"purple" => (128, 0, 128),
"rebeccapurple" => (102, 51, 153),
"red" => (255, 0, 0),
"rosybrown" => (188, 143, 143),
"royalblue" => (65, 105, 225),
"saddlebrown" => (139, 69, 19),
"salmon" => (250, 128, 114),
"sandybrown" => (244, 164, 96),
"seagreen" => (46, 139, 87),
"seashell" => (255, 245, 238),
"sienna" => (160, 82, 45),
"silver" => (192, 192, 192),
"skyblue" => (135, 206, 235),
"slateblue" => (106, 90, 205),
"slategray" | "slategrey" => (112, 128, 144),
"snow" => (255, 250, 250),
"springgreen" => (0, 255, 127),
"steelblue" => (70, 130, 180),
"tan" => (210, 180, 140),
"teal" => (0, 128, 128),
"thistle" => (216, 191, 216),
"tomato" => (255, 99, 71),
"turquoise" => (64, 224, 208),
"violet" => (238, 130, 238),
"wheat" => (245, 222, 179),
"white" => (255, 255, 255),
"whitesmoke" => (245, 245, 245),
"yellow" => (255, 255, 0),
"yellowgreen" => (154, 205, 50),
_ => return None,
})
}

pub fn normalize_rect(x: f64, y: f64, width: f64, height: f64) -> Option<CanvasRect> {
if !x.is_finite() || !y.is_finite() || !width.is_finite() || !height.is_finite() {
return None;
Expand Down
49 changes: 39 additions & 10 deletions moli-renderer-v8/src/context_bootstrap/canvas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,21 @@ const CANVAS_CONTEXT_IMAGE_SMOOTHING_QUALITY_SLOT: &str =
const CANVAS_CONTEXT_GLOBAL_ALPHA_SLOT: &str = "__moliCanvasContextGlobalAlpha";
const CANVAS_CONTEXT_GLOBAL_COMPOSITE_OPERATION_SLOT: &str =
"__moliCanvasContextGlobalCompositeOperation";
const CANVAS_CONTEXT_LINE_WIDTH_SLOT: &str = "__moliCanvasContextLineWidth";
const CANVAS_CONTEXT_LINE_CAP_SLOT: &str = "__moliCanvasContextLineCap";
const CANVAS_CONTEXT_LINE_JOIN_SLOT: &str = "__moliCanvasContextLineJoin";
const CANVAS_CONTEXT_MITER_LIMIT_SLOT: &str = "__moliCanvasContextMiterLimit";
const CANVAS_CONTEXT_LINE_DASH_OFFSET_SLOT: &str = "__moliCanvasContextLineDashOffset";
const CANVAS_CONTEXT_STROKE_STYLE_SLOT: &str = "__moliCanvasContextStrokeStyle";

pub(crate) const DEFAULT_GLOBAL_ALPHA: f64 = 1.0;
pub(crate) const DEFAULT_GLOBAL_COMPOSITE_OPERATION: &str = "source-over";
pub(crate) const DEFAULT_LINE_WIDTH: f64 = 1.0;
pub(crate) const DEFAULT_LINE_CAP: &str = "butt";
pub(crate) const DEFAULT_LINE_JOIN: &str = "miter";
pub(crate) const DEFAULT_MITER_LIMIT: f64 = 10.0;
pub(crate) const DEFAULT_LINE_DASH_OFFSET: f64 = 0.0;
pub(crate) const DEFAULT_STROKE_STYLE: &str = "#000000";

/// Composite operations recognised by the HTML Canvas 2D spec.
///
Expand Down Expand Up @@ -179,6 +191,7 @@ mod helpers;
mod image_bitmap;
mod objects;
mod offscreen;
mod path;
mod webgl;

pub(crate) use backing_store::{
Expand All @@ -192,23 +205,39 @@ pub(crate) use constructors::{
webgl_rendering_context_constructor_callback,
};
pub(crate) use context2d::{
canvas_context_clear_rect_callback, canvas_context_create_image_data_callback,
canvas_context_create_linear_gradient_callback, canvas_context_draw_image_callback,
canvas_context_fill_rect_callback, canvas_context_fill_style_getter_callback,
canvas_context_fill_style_setter_callback, canvas_context_fill_text_callback,
canvas_context_font_getter_callback, canvas_context_font_setter_callback,
canvas_context_get_image_data_callback, canvas_context_get_line_dash_callback,
canvas_context_global_alpha_getter_callback, canvas_context_global_alpha_setter_callback,
canvas_context_arc_callback, canvas_context_arc_to_callback,
canvas_context_begin_path_callback, canvas_context_bezier_curve_to_callback,
canvas_context_clear_rect_callback, canvas_context_close_path_callback,
canvas_context_create_image_data_callback, canvas_context_create_linear_gradient_callback,
canvas_context_draw_image_callback, canvas_context_ellipse_callback,
canvas_context_fill_callback, canvas_context_fill_rect_callback,
canvas_context_fill_style_getter_callback, canvas_context_fill_style_setter_callback,
canvas_context_fill_text_callback, canvas_context_font_getter_callback,
canvas_context_font_setter_callback, canvas_context_get_image_data_callback,
canvas_context_get_line_dash_callback, canvas_context_global_alpha_getter_callback,
canvas_context_global_alpha_setter_callback,
canvas_context_global_composite_operation_getter_callback,
canvas_context_global_composite_operation_setter_callback,
canvas_context_image_smoothing_enabled_getter_callback,
canvas_context_image_smoothing_enabled_setter_callback,
canvas_context_image_smoothing_quality_getter_callback,
canvas_context_image_smoothing_quality_setter_callback,
canvas_context_is_point_in_path_callback, canvas_context_measure_text_callback,
canvas_context_is_point_in_path_callback, canvas_context_line_cap_getter_callback,
canvas_context_line_cap_setter_callback, canvas_context_line_dash_offset_getter_callback,
canvas_context_line_dash_offset_setter_callback, canvas_context_line_join_getter_callback,
canvas_context_line_join_setter_callback, canvas_context_line_to_callback,
canvas_context_line_width_getter_callback, canvas_context_line_width_setter_callback,
canvas_context_measure_text_callback, canvas_context_miter_limit_getter_callback,
canvas_context_miter_limit_setter_callback, canvas_context_move_to_callback,
canvas_context_noop_callback, canvas_context_put_image_data_callback,
canvas_context_rect_callback, canvas_context_set_line_dash_callback,
canvas_context_stroke_text_callback, canvas_gradient_add_color_stop_callback,
canvas_context_quadratic_curve_to_callback, canvas_context_rect_callback,
canvas_context_reset_transform_callback, canvas_context_rotate_callback,
canvas_context_scale_callback, canvas_context_set_line_dash_callback,
canvas_context_set_transform_callback, canvas_context_stroke_callback,
canvas_context_stroke_rect_callback, canvas_context_stroke_style_getter_callback,
canvas_context_stroke_style_setter_callback, canvas_context_stroke_text_callback,
canvas_context_transform_callback, canvas_context_translate_callback,
canvas_gradient_add_color_stop_callback,
};
pub(crate) use image_bitmap::window_create_image_bitmap_callback;
pub(crate) use objects::{
Expand Down
Loading
Loading