mod canvas;
mod display_area;
mod gpu;
mod render;
mod text;
pub use canvas::*;
pub use display_area::*;
use div::DivHandle;
pub use gpu::{
CustomShader, GpuConfig, GpuMesh, GpuTriangle, GpuVertex, RenderPipelineHandle, UniformValue,
VertexDescriptor,
};
pub use render::*;
pub use text::*;
use crate::*;
use crate::{graphics::AbstractMesh, Vector};
use crate::{graphics::ImageLoader, graphics::TextureConfig, quicksilver_compat::Color};
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{Element, HtmlCanvasElement};
pub struct Display {
browser_region: Rectangle,
game_coordinates: Vector,
canvas: WebGLCanvas,
background_color: Option<Color>,
div: DivHandle,
tessellation_buffer: AbstractMesh,
}
pub struct DisplayConfig {
pub canvas: CanvasConfig,
pub pixels: Vector,
pub texture_config: TextureConfig,
pub gpu_config: GpuConfig,
pub update_delay_ms: i32,
pub background: Option<Color>,
pub capture_touch: bool,
}
impl Default for DisplayConfig {
fn default() -> Self {
Self {
canvas: CanvasConfig::HtmlId("paddle-canvas"),
pixels: Vector::new(1280, 720),
update_delay_ms: 8,
texture_config: Default::default(),
gpu_config: Default::default(),
background: None,
capture_touch: true,
}
}
}
pub enum CanvasConfig {
HtmlId(&'static str),
HtmlElement(HtmlCanvasElement),
}
use crate::{NutsCheck, PaddleResult, Rectangle, Transform};
impl Display {
pub(super) fn new(config: DisplayConfig) -> PaddleResult<Self> {
let canvas = match config.canvas {
CanvasConfig::HtmlElement(el) => el,
CanvasConfig::HtmlId(id) => canvas_by_id(id)?,
};
let parent_element = canvas.parent_element().expect("Canvas has no parent");
if config.capture_touch {
let parent_html = parent_element
.clone()
.dyn_into::<web_sys::HtmlElement>()
.expect("Canvas parent is not an HTML element");
parent_html
.style()
.set_property("touch-action", "none")
.expect("Setting CSS failed");
}
let game_coordinates = config.pixels;
let canvas = WebGLCanvas::new(canvas, config.pixels, &config.gpu_config)?;
let browser_region = find_browser_region(canvas.html_element())?;
let size = (game_coordinates.x as u32, game_coordinates.y as u32);
div::init_ex_with_element(parent_element, (0, 0), Some(size))
.expect("Div initialization failed");
div::resize(
browser_region.width() as u32,
browser_region.height() as u32,
)
.expect("Div initialization failed");
ImageLoader::register(canvas.clone_webgl(), config.texture_config);
let background_color = config.background;
let div = div::new_styled::<_, _, &'static str, _, _>(
0,
0,
size.0,
size.1,
"",
&[],
&[("pointer-events", "None")],
)?;
div.set_css("z-index", &(-1).to_string())?;
Ok(Self {
canvas,
browser_region,
game_coordinates,
background_color,
div,
tessellation_buffer: AbstractMesh::new(),
})
}
pub(crate) fn canvas_mut(&mut self) -> &mut WebGLCanvas {
&mut self.canvas
}
pub fn browser_region(&self) -> Rectangle {
self.browser_region
}
pub fn resolution(&self) -> Vector {
self.canvas.resolution()
}
pub fn webgl_transform(&self) -> Transform {
Transform::scale((1.0, -1.0))
* Transform::translate((-1.0, -1.0))
* Transform::scale(self.resolution().recip() * 2.0)
}
pub fn clear(&mut self) {
if let Some(col) = self.background_color {
self.canvas.clear(col);
}
}
pub fn game_to_browser_coordinates(&self) -> Transform {
Transform::scale(
self.browser_region
.size
.times(self.game_coordinates.recip()),
) * Transform::translate(self.browser_region.pos)
}
pub fn game_to_browser_area(&self, mut rect: Rectangle) -> Rectangle {
rect.pos += self.browser_region.pos;
rect.size = rect.size.times(
self.browser_region
.size
.times(self.game_coordinates.recip()),
);
rect
}
pub fn browser_to_game_pixel_ratio(&self) -> f32 {
self.browser_region.width() / self.game_coordinates.x
}
pub fn fit_to_visible_area(&mut self, margin: f64) -> PaddleResult<()> {
let web_window = web_sys::window().unwrap();
let w = web_window
.inner_width()
.map_err(JsError::from_js_value)?
.as_f64()
.unwrap();
let h = web_window
.inner_height()
.map_err(JsError::from_js_value)?
.as_f64()
.unwrap();
let (w, h) = scale_16_to_9(
w - self.browser_region.x() as f64 - margin,
h - self.browser_region.y() as f64 - margin,
);
self.canvas.set_size((w as f32, h as f32));
self.adjust_display()?;
Ok(())
}
pub fn adjust_display(&mut self) -> PaddleResult<()> {
self.update_browser_region();
let (x, y) = self.div_offset()?;
div::reposition(x, y)?;
div::resize(
self.browser_region.size.x as u32,
self.browser_region.size.y as u32,
)?;
Ok(())
}
fn update_browser_region(&mut self) {
if let Some(br) = find_browser_region(self.canvas.html_element()).nuts_check() {
self.browser_region = br;
}
}
fn div_offset(&self) -> PaddleResult<(u32, u32)> {
find_div_offset(
self.canvas.html_element().clone().into(),
&self.browser_region,
)
}
pub fn draw_ex<'a>(
&'a mut self,
draw: &impl Tessellate,
paint: &impl Paint,
trans: Transform,
z: i16,
) {
self.tessellation_buffer.clear();
draw.tessellate(&mut self.tessellation_buffer);
let area = draw.bounding_box();
let trans = Transform::translate(quicksilver_compat::Shape::center(&area))
* trans
* Transform::translate(-quicksilver_compat::Shape::center(&area));
self.canvas
.render(&self.tessellation_buffer, area, trans, paint, z);
}
pub fn draw_mesh_ex<'a>(
&mut self,
mesh: &AbstractMesh,
paint: &impl Paint,
area: Rectangle,
t: Transform,
z: i16,
) {
self.canvas.render(mesh, area, t, paint, z);
}
pub fn new_render_pipeline(
&mut self,
vertex_shader_text: &'static str,
fragment_shader_text: &'static str,
vertex_descriptor: VertexDescriptor,
uniform_values: &[(&'static str, UniformValue)],
) -> crate::PaddleResult<crate::RenderPipelineHandle> {
self.canvas.new_render_pipeline(
vertex_shader_text,
fragment_shader_text,
vertex_descriptor,
uniform_values,
)
}
pub fn update_uniform(
&mut self,
rp: RenderPipelineHandle,
name: &'static str,
value: &UniformValue,
) {
self.canvas.update_uniform(rp, name, value)
}
}
fn find_browser_region(canvas: &HtmlCanvasElement) -> PaddleResult<Rectangle> {
let web_window = web_sys::window().unwrap();
let dom_rect = canvas.get_bounding_client_rect();
let page_x_offset = web_window.page_x_offset().map_err(JsError::from_js_value)?;
let page_y_offset = web_window.page_y_offset().map_err(JsError::from_js_value)?;
let x = dom_rect.x() + page_x_offset;
let y = dom_rect.y() + page_y_offset;
let w = dom_rect.width();
let h = dom_rect.height();
let browser_region = Rectangle::new((x as f32, y as f32), (w as f32, h as f32));
Ok(browser_region)
}
fn find_div_offset(canvas: Element, browser_region: &Rectangle) -> PaddleResult<(u32, u32)> {
let npa = nearest_positioned_ancestor(canvas).map_err(JsError::from_js_value)?;
let npa_rect = npa.get_bounding_client_rect();
let npa_pos = Vector::new(npa_rect.x(), npa_rect.y());
let offset = browser_region.pos - npa_pos;
Ok((offset.x as u32, offset.y as u32))
}
fn nearest_positioned_ancestor(element: Element) -> Result<Element, JsValue> {
let web_window = web_sys::window().unwrap();
let mut npa = element;
loop {
if let Some(property) = &web_window.get_computed_style(&npa)? {
match property.get_property_value("position")?.as_str() {
"" | "static" => {
}
"absolute" | "relative" | "fixed" | "sticky" => {
return Ok(npa);
}
_ => {
return Err("Unexpected position attribute".into());
}
}
}
if let Some(parent) = npa.parent_element() {
npa = parent;
} else {
return Ok(npa);
}
}
}
fn canvas_by_id(id: &str) -> PaddleResult<HtmlCanvasElement> {
let document = web_sys::window().unwrap().document().unwrap();
let canvas = document
.get_element_by_id(id)
.ok_or_else(|| ErrorMessage::technical(format!("No canvas with id {}", id)))?;
canvas.dyn_into::<HtmlCanvasElement>().map_err(|e| {
ErrorMessage::technical(format!(
"Not a canvas. Err: {}",
e.to_string().as_string().unwrap()
))
})
}
fn scale_16_to_9(w: f64, h: f64) -> (f64, f64) {
if w * 9.0 > h * 16.0 {
(h * 16.0 / 9.0, h)
} else {
(w, w * 9.0 / 16.0)
}
}