use std::borrow::Cow;
use js_sys::Array;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{
CanvasGradient, CanvasRenderingContext2d, CanvasWindingRule, HtmlCanvasElement, OffscreenCanvas, OffscreenCanvasRenderingContext2d, Path2d,
};
use winit::{platform::web::WindowExtWebSys, window::Window};
use super::{
bitmap::WebBitmap,
brush::{BrushData, WebBrush},
device::WebDeviceContext,
font,
layer::WebLayer,
matrix::WebMatrix,
path::WebPath,
};
use crate::{
base::Dimension,
canvas::CanvasBackend,
error::GraphicsError,
font::{FontStyle, FontWeight},
geometry::{FRect, FSize, ISize},
gradient::GradientData,
matrix::MatrixBackend,
paint::{BlendMode, Cap, Color, DrawMode, Join, Paint},
path::PathBackend,
text::TextAlignment,
Float,
};
const LINE_HEIGHT_FACTOR: Float = 1.2;
#[derive(Clone, Debug)]
enum RenderTarget {
HTMLCanvas(HtmlCanvasElement),
OffscreenCanvas(OffscreenCanvas),
}
impl Dimension for RenderTarget {
fn size(&self) -> FSize {
match self {
Self::HTMLCanvas(canvas) => FSize::new(canvas.width() as Float, canvas.height() as Float),
Self::OffscreenCanvas(canvas) => FSize::new(canvas.width() as Float, canvas.height() as Float),
}
}
fn pixel_size(&self) -> ISize {
match self {
Self::HTMLCanvas(canvas) => ISize::new(canvas.width() as i32, canvas.height() as i32),
Self::OffscreenCanvas(canvas) => ISize::new(canvas.width() as i32, canvas.height() as i32),
}
}
}
#[derive(Clone, Debug)]
pub(super) enum Context {
Canvas(CanvasRenderingContext2d),
OffscreenCanvas(OffscreenCanvasRenderingContext2d),
}
#[derive(Clone, Debug)]
pub struct WebCanvas {
render_target: RenderTarget,
context: Context,
current_paint: Paint<WebBrush>,
paint_stack: Vec<Paint<WebBrush>>,
}
impl CanvasBackend for WebCanvas {
type DeviceContextType = WebDeviceContext;
type BitmapType = WebBitmap;
type BrushType = WebBrush;
type LayerType = WebLayer;
type MatrixType = WebMatrix;
type PathType = WebPath;
fn from_layer(_context: Option<&Self::DeviceContextType>, layer: &Self::LayerType) -> Result<Self, GraphicsError> {
let canvas = layer.layer();
Ok(Self {
render_target: RenderTarget::OffscreenCanvas(canvas.clone()),
context: Context::OffscreenCanvas(canvas.get_context("2d").unwrap().unwrap().dyn_into::<OffscreenCanvasRenderingContext2d>().unwrap()),
current_paint: Paint::new(),
paint_stack: Vec::new(),
})
}
fn from_window(_context: Option<&Self::DeviceContextType>, window: &Window) -> Result<Self, GraphicsError> {
let canvas = window.canvas().ok_or(GraphicsError::InvalidHandle(stringify!(window).to_string()))?;
Ok(Self {
render_target: RenderTarget::HTMLCanvas(canvas.clone()),
context: Context::Canvas(canvas.get_context("2d").unwrap().unwrap().dyn_into::<CanvasRenderingContext2d>().unwrap()),
current_paint: Paint::new(),
paint_stack: Vec::new(),
})
}
fn device_context(&self) -> Option<&Self::DeviceContextType> {
None
}
fn size(&self) -> FSize {
self.render_target.size()
}
fn pixel_size(&self) -> ISize {
self.render_target.pixel_size()
}
fn save(&mut self) -> bool {
self.paint_stack.push(self.current_paint.clone());
match &self.context {
Context::Canvas(context) => context.save(),
Context::OffscreenCanvas(context) => context.save(),
}
true
}
fn restore(&mut self) -> bool {
self.paint_stack
.pop()
.map(|paint| {
self.current_paint = paint;
match &self.context {
Context::Canvas(context) => context.restore(),
Context::OffscreenCanvas(context) => context.restore(),
}
})
.is_some()
}
fn begin_draw(&mut self) {
}
fn end_draw(&mut self) {
}
fn get_opacity(&self) -> Float {
match &self.context {
Context::Canvas(context) => context.global_alpha(),
Context::OffscreenCanvas(context) => context.global_alpha(),
}
}
fn set_opacity(&mut self, opacity: Float) {
match &self.context {
Context::Canvas(context) => context.set_global_alpha(opacity),
Context::OffscreenCanvas(context) => context.set_global_alpha(opacity),
}
}
fn get_blend_mode(&self) -> BlendMode {
self.current_paint.get_blend_mode()
}
fn set_blend_mode(&mut self, mode: BlendMode) {
self.current_paint.set_blend_mode(mode);
match &self.context {
Context::Canvas(context) => context.set_global_composite_operation(mode.into()),
Context::OffscreenCanvas(context) => context.set_global_composite_operation(mode.into()),
}
.ok();
}
fn get_draw_mode(&self) -> DrawMode {
self.current_paint.get_draw_mode()
}
fn set_draw_mode(&mut self, mode: DrawMode) {
self.current_paint.set_draw_mode(mode);
}
fn get_line_dash(&self) -> (Option<Vec<Float>>, Float) {
let (dashes, offset) = match &self.context {
Context::Canvas(context) => (context.get_line_dash(), context.line_dash_offset()),
Context::OffscreenCanvas(context) => (context.get_line_dash(), context.line_dash_offset()),
};
(Some(dashes.iter().filter_map(|dash| dash.as_f64()).collect()), offset)
}
fn set_line_dash(&mut self, dashes: &[Float], offset: Float) {
let js_array = Array::new_with_length(dashes.len() as u32);
for (i, segment) in dashes.iter().enumerate() {
js_array.set(i as u32, JsValue::from_f64(*segment));
}
match &self.context {
Context::Canvas(context) => {
context.set_line_dash(&js_array.into()).ok();
context.set_line_dash_offset(offset);
}
Context::OffscreenCanvas(context) => {
context.set_line_dash(&js_array.into()).ok();
context.set_line_dash_offset(offset);
}
}
}
fn get_matrix(&self) -> Self::MatrixType {
let matrix = match &self.context {
Context::Canvas(context) => context.get_transform(),
Context::OffscreenCanvas(context) => context.get_transform(),
};
if let Ok(matrix) = matrix {
matrix.into()
} else {
WebMatrix::identity()
}
}
fn set_matrix(&mut self, matrix: &Self::MatrixType) {
let matrix = matrix.matrix();
match &self.context {
Context::Canvas(context) => context.set_transform(matrix.a(), matrix.b(), matrix.c(), matrix.d(), matrix.e(), matrix.f()),
Context::OffscreenCanvas(context) => context.set_transform(matrix.a(), matrix.b(), matrix.c(), matrix.d(), matrix.e(), matrix.f()),
}
.ok();
}
fn concat_matrix(&mut self, matrix: &Self::MatrixType) {
let matrix = matrix.matrix();
match &self.context {
Context::Canvas(context) => context.transform(matrix.a(), matrix.b(), matrix.c(), matrix.d(), matrix.e(), matrix.f()),
Context::OffscreenCanvas(context) => context.transform(matrix.a(), matrix.b(), matrix.c(), matrix.d(), matrix.e(), matrix.f()),
}
.ok();
}
fn get_miter_limit(&self) -> Float {
match &self.context {
Context::Canvas(context) => context.miter_limit(),
Context::OffscreenCanvas(context) => context.miter_limit(),
}
}
fn set_miter_limit(&mut self, miter_limit: Float) {
match &self.context {
Context::Canvas(context) => context.set_miter_limit(miter_limit),
Context::OffscreenCanvas(context) => context.set_miter_limit(miter_limit),
};
}
fn get_smooth(&self) -> bool {
self.current_paint.get_smooth()
}
fn set_smooth(&mut self, smooth: bool) {
self.current_paint.set_smooth(smooth);
match &self.context {
Context::Canvas(context) => context.set_image_smoothing_enabled(smooth),
Context::OffscreenCanvas(context) => context.set_image_smoothing_enabled(smooth),
};
}
fn get_stroke_cap(&self) -> Cap {
self.current_paint.get_stroke_cap()
}
fn set_stroke_cap(&mut self, cap: Cap) {
self.current_paint.set_stroke_cap(cap);
match &self.context {
Context::Canvas(context) => context.set_line_cap(cap.into()),
Context::OffscreenCanvas(context) => context.set_line_cap(cap.into()),
};
}
fn get_stroke_join(&self) -> Join {
self.current_paint.get_stroke_join()
}
fn set_stroke_join(&mut self, join: Join) {
self.current_paint.set_stroke_join(join);
match &self.context {
Context::Canvas(context) => context.set_line_join(join.into()),
Context::OffscreenCanvas(context) => context.set_line_join(join.into()),
};
}
fn get_stroke_width(&self) -> Float {
match &self.context {
Context::Canvas(context) => context.line_width(),
Context::OffscreenCanvas(context) => context.line_width(),
}
}
fn set_stroke_width(&mut self, width: Float) {
match &self.context {
Context::Canvas(context) => context.set_line_width(width),
Context::OffscreenCanvas(context) => context.set_line_width(width),
};
}
fn set_brush(&mut self, brush: &Self::BrushType) {
self.current_paint.set_brush(brush);
match &self.context {
Context::Canvas(context) => {
self.internal_set_brush(context, brush);
}
Context::OffscreenCanvas(context) => {
self.internal_set_brush(context, brush);
}
}
}
fn get_font_family(&self) -> &str {
self.current_paint.get_font_family()
}
fn set_font_family(&mut self, family: &str) {
if self.current_paint.get_font_family() != family {
self.current_paint.set_font_family(family);
self.set_font();
}
}
fn get_font_size(&self) -> Float {
self.current_paint.get_font_size()
}
fn set_font_size(&mut self, size: Float) {
if self.current_paint.get_font_size() != size {
self.current_paint.set_font_size(size);
self.set_font();
}
}
fn get_font_style(&self) -> FontStyle {
self.current_paint.get_font_style()
}
fn set_font_style(&mut self, style: FontStyle) {
if self.current_paint.get_font_style() != style {
self.current_paint.set_font_style(style);
self.set_font();
}
}
fn get_font_weight(&self) -> FontWeight {
self.current_paint.get_font_weight()
}
fn set_font_weight(&mut self, weight: FontWeight) {
if self.current_paint.get_font_weight() != weight {
self.current_paint.set_font_weight(weight);
self.set_font();
}
}
fn get_text_alignment(&self) -> TextAlignment {
self.current_paint.get_text_alignment()
}
fn set_text_alignment(&mut self, alignment: TextAlignment) {
self.current_paint.set_text_alignment(alignment);
}
fn get_text_color(&self) -> Color {
self.current_paint.get_text_color()
}
fn set_text_color(&mut self, color: Color) {
self.current_paint.set_text_color(color);
}
fn rotate(&mut self, angle: Float) {
match &self.context {
Context::Canvas(context) => context.rotate(angle),
Context::OffscreenCanvas(context) => context.rotate(angle),
}
.ok();
}
fn scale(&mut self, sx: Float, sy: Float) {
match &self.context {
Context::Canvas(context) => context.scale(sx, sy),
Context::OffscreenCanvas(context) => context.scale(sx, sy),
}
.ok();
}
fn translate(&mut self, dx: Float, dy: Float) {
match &self.context {
Context::Canvas(context) => context.translate(dx, dy),
Context::OffscreenCanvas(context) => context.translate(dx, dy),
}
.ok();
}
fn clear(&mut self, color: Color) {
let color_str: Cow<'static, str> = color.into();
match &self.context {
Context::Canvas(context) => self.internal_clear(context, &color_str),
Context::OffscreenCanvas(context) => self.internal_clear(context, &color_str),
};
}
fn clip_to_rect(&mut self, rect: FRect) {
let path = WebPath::new(None);
if let Ok(mut path) = path {
path.add_rect(rect.point.x, rect.point.y, rect.size.width, rect.size.height);
self.clip_to_path(&path);
match &self.context {
Context::Canvas(context) => context.clip_with_path_2d(path.path()),
Context::OffscreenCanvas(context) => context.clip_with_path_2d(path.path()),
};
}
}
fn clip_to_path(&mut self, path: &Self::PathType) {
match &self.context {
Context::Canvas(context) => context.clip_with_path_2d_and_winding(path.path(), path.get_fill_type().into()),
Context::OffscreenCanvas(context) => context.clip_with_path_2d_and_winding(path.path(), path.get_fill_type().into()),
};
}
fn draw_path(&mut self, path: &Self::PathType) -> bool {
if path.is_empty() {
return false;
};
match self.current_paint.get_draw_mode() {
DrawMode::Fill => {
let fill_type: CanvasWindingRule = path.get_fill_type().into();
self.fill_with_path(path.path(), fill_type);
}
DrawMode::Stroke => {
self.stroke_with_path(path.path());
}
DrawMode::FillStroke => {
let fill_type = path.get_fill_type();
let fill_type = fill_type.into();
self.fill_with_path(path.path(), fill_type);
self.stroke_with_path(path.path());
}
}
true
}
fn draw_bitmap(&mut self, bitmap: &Self::BitmapType, source_rect: Option<FRect>, dest_rect: FRect) -> bool {
let bitmap = bitmap.image_bitmap();
if let Some(source_rect) = source_rect {
match &self.context {
Context::Canvas(context) => context.draw_image_with_image_bitmap_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
bitmap,
source_rect.point.x,
source_rect.point.y,
source_rect.size.width,
source_rect.size.height,
dest_rect.point.x,
dest_rect.point.y,
dest_rect.size.width,
dest_rect.size.height,
),
Context::OffscreenCanvas(context) => context.draw_image_with_image_bitmap_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
bitmap,
source_rect.point.x,
source_rect.point.y,
source_rect.size.width,
source_rect.size.height,
dest_rect.point.x,
dest_rect.point.y,
dest_rect.size.width,
dest_rect.size.height,
),
}
.is_ok()
} else {
match &self.context {
Context::Canvas(context) => context.draw_image_with_image_bitmap_and_dw_and_dh(
bitmap,
dest_rect.point.x,
dest_rect.point.y,
dest_rect.size.width,
dest_rect.size.height,
),
Context::OffscreenCanvas(context) => context.draw_image_with_image_bitmap_and_dw_and_dh(
bitmap,
dest_rect.point.x,
dest_rect.point.y,
dest_rect.size.width,
dest_rect.size.height,
),
}
.is_ok()
}
}
fn draw_layer(&mut self, layer: &Self::LayerType, dest_rect: FRect) -> bool {
let canvas = layer.layer();
match &self.context {
Context::Canvas(context) => context.draw_image_with_offscreen_canvas_and_dw_and_dh(
canvas,
dest_rect.point.x,
dest_rect.point.y,
dest_rect.size.width,
dest_rect.size.height,
),
Context::OffscreenCanvas(context) => context.draw_image_with_offscreen_canvas_and_dw_and_dh(
canvas,
dest_rect.point.x,
dest_rect.point.y,
dest_rect.size.width,
dest_rect.size.height,
),
}
.ok();
true
}
fn draw_text(&mut self, text: &str, rect: FRect) -> bool {
let lines: Vec<&str> = text.split('\n').collect();
let alignment = self.current_paint.get_text_alignment();
let x = match alignment {
TextAlignment::Left => rect.point.x,
TextAlignment::Center => rect.point.x + rect.size.width / 2.0,
TextAlignment::Right => rect.point.x + rect.size.width,
};
let text_height = self.current_paint.get_font_size() * LINE_HEIGHT_FACTOR;
let mut y = rect.point.y + (text_height - self.current_paint.get_font_size());
let color_str: Cow<'static, str> = self.current_paint.get_text_color().into();
self.set_font();
for line in lines {
let metrics = match &self.context {
Context::Canvas(context) => context.measure_text(line).unwrap(),
Context::OffscreenCanvas(context) => context.measure_text(line).unwrap(),
};
let text_base_y = y + metrics.actual_bounding_box_ascent();
match &self.context {
Context::Canvas(context) => {
context.save();
context.set_fill_style_str(&color_str);
context.set_text_align(alignment.into());
context.fill_text(line, x, text_base_y).ok();
context.restore();
}
Context::OffscreenCanvas(context) => {
context.save();
context.set_fill_style_str(&color_str);
context.set_text_align(alignment.into());
context.fill_text(line, x, text_base_y).ok();
context.restore();
}
}
y += text_height;
}
true
}
fn draw_text_at(&mut self, text: &str, x: Float, y: Float) -> bool {
let color_str: Cow<'static, str> = self.current_paint.get_text_color().into();
self.set_font();
match &self.context {
Context::Canvas(context) => {
context.save();
context.set_fill_style_str(&color_str);
context.fill_text(text, x, y).ok();
context.restore();
}
Context::OffscreenCanvas(context) => {
context.save();
context.set_fill_style_str(&color_str);
context.fill_text(text, x, y).ok();
context.restore();
}
}
true
}
fn fill_rect(&mut self, _x: Float, _y: Float, _width: Float, _height: Float, _mask: Option<&Self::BitmapType>) -> bool {
false
}
}
trait ContextDrawing {
fn save(&self);
fn restore(&self);
fn create_linear_gradient(&self, x0: f64, y0: f64, x1: f64, y1: f64) -> CanvasGradient;
fn create_radial_gradient(&self, x0: f64, y0: f64, r0: f64, x1: f64, y1: f64, r1: f64) -> Result<CanvasGradient, JsValue>;
fn set_fill_style_str(&self, value: &str);
fn set_stroke_style_str(&self, value: &str);
fn set_fill_style_canvas_gradient(&self, value: &CanvasGradient);
fn set_stroke_style_canvas_gradient(&self, value: &CanvasGradient);
fn fill_rect(&self, x: f64, y: f64, w: f64, h: f64);
}
impl ContextDrawing for CanvasRenderingContext2d {
fn save(&self) {
self.save();
}
fn restore(&self) {
self.restore();
}
fn create_linear_gradient(&self, x0: f64, y0: f64, x1: f64, y1: f64) -> CanvasGradient {
self.create_linear_gradient(x0, y0, x1, y1)
}
fn create_radial_gradient(&self, x0: f64, y0: f64, r0: f64, x1: f64, y1: f64, r1: f64) -> Result<CanvasGradient, JsValue> {
self.create_radial_gradient(x0, y0, r0, x1, y1, r1)
}
fn set_fill_style_str(&self, value: &str) {
self.set_fill_style_str(value);
}
fn set_stroke_style_str(&self, value: &str) {
self.set_stroke_style_str(value);
}
fn set_fill_style_canvas_gradient(&self, value: &CanvasGradient) {
self.set_fill_style_canvas_gradient(value);
}
fn set_stroke_style_canvas_gradient(&self, value: &CanvasGradient) {
self.set_stroke_style_canvas_gradient(value);
}
fn fill_rect(&self, x: f64, y: f64, w: f64, h: f64) {
self.fill_rect(x, y, w, h);
}
}
impl ContextDrawing for OffscreenCanvasRenderingContext2d {
fn save(&self) {
self.save();
}
fn restore(&self) {
self.restore();
}
fn create_linear_gradient(&self, x0: f64, y0: f64, x1: f64, y1: f64) -> CanvasGradient {
self.create_linear_gradient(x0, y0, x1, y1)
}
fn create_radial_gradient(&self, x0: f64, y0: f64, r0: f64, x1: f64, y1: f64, r1: f64) -> Result<CanvasGradient, JsValue> {
self.create_radial_gradient(x0, y0, r0, x1, y1, r1)
}
fn set_fill_style_str(&self, style: &str) {
self.set_fill_style_str(style);
}
fn set_stroke_style_str(&self, style: &str) {
self.set_stroke_style_str(style);
}
fn set_fill_style_canvas_gradient(&self, style: &CanvasGradient) {
self.set_fill_style_canvas_gradient(style);
}
fn set_stroke_style_canvas_gradient(&self, style: &CanvasGradient) {
self.set_stroke_style_canvas_gradient(style);
}
fn fill_rect(&self, x: f64, y: f64, w: f64, h: f64) {
self.fill_rect(x, y, w, h);
}
}
impl WebCanvas {
fn internal_set_brush<T: ContextDrawing>(&self, context: &T, brush: &WebBrush) {
match &brush.data() {
BrushData::Solid(color) => {
let color_str: Cow<'static, str> = (*color).into();
context.set_fill_style_str(&color_str);
context.set_stroke_style_str(&color_str);
}
BrushData::Gradient(gradient) => {
let canvas_gradient = match gradient.data() {
GradientData::Linear((start, end)) => Some(context.create_linear_gradient(start.x, start.y, end.x, end.y)),
GradientData::Radial((start, end, r)) => context.create_radial_gradient(start.x, start.y, 0.0, end.x, end.y, *r).ok(),
};
if let Some(canvas_gradient) = canvas_gradient {
for (offset, color) in gradient.color_stops() {
let color_str: Cow<'static, str> = (*color).into();
canvas_gradient.add_color_stop(*offset as f32, &color_str).ok();
}
context.set_fill_style_canvas_gradient(&canvas_gradient);
context.set_stroke_style_canvas_gradient(&canvas_gradient);
}
}
}
}
fn internal_clear<T: ContextDrawing>(&self, context: &T, color: &str) {
let (canvas_width, canvas_height) = self.size().into();
context.save();
context.set_fill_style_str(color);
context.fill_rect(0.0, 0.0, canvas_width, canvas_height);
context.restore();
}
fn fill_with_path(&self, path: &Path2d, fill_type: CanvasWindingRule) {
match &self.context {
Context::Canvas(context) => context.fill_with_path_2d_and_winding(path, fill_type),
Context::OffscreenCanvas(context) => context.fill_with_path_2d_and_winding(path, fill_type),
}
}
fn stroke_with_path(&self, path: &Path2d) {
match &self.context {
Context::Canvas(context) => context.stroke_with_path(path),
Context::OffscreenCanvas(context) => context.stroke_with_path(path),
}
}
fn set_font(&mut self) {
let font_string = font::gen_font_string(
self.current_paint.get_font_family(),
self.current_paint.get_font_size(),
self.current_paint.get_font_style(),
self.current_paint.get_font_weight(),
);
match &self.context {
Context::Canvas(context) => context.set_font(&font_string),
Context::OffscreenCanvas(context) => context.set_font(&font_string),
}
}
}