From ddb6d5e7cca039dcbab4435ee38b3013cf94f071 Mon Sep 17 00:00:00 2001 From: adroitwhiz <adroitwhiz@protonmail.com> Date: Thu, 19 Mar 2020 23:07:24 -0400 Subject: [PATCH 01/14] get the rust+wasm build process working (hopefully) --- .eslintignore | 1 + package.json | 17 +++++++++++------ src/RenderWebGL.js | 23 +++++++++++++++++++++++ swrender/.gitignore | 7 +++++++ swrender/Cargo.toml | 33 +++++++++++++++++++++++++++++++++ swrender/src/lib.rs | 19 +++++++++++++++++++ swrender/src/utils.rs | 10 ++++++++++ swrender/tests/web.rs | 13 +++++++++++++ webpack.config.js | 31 ++++++++++++++++++++----------- 9 files changed, 137 insertions(+), 17 deletions(-) create mode 100644 swrender/.gitignore create mode 100644 swrender/Cargo.toml create mode 100644 swrender/src/lib.rs create mode 100644 swrender/src/utils.rs create mode 100644 swrender/tests/web.rs diff --git a/.eslintignore b/.eslintignore index 5821b50ad..b732c67a7 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,3 +2,4 @@ dist/* node_modules/* playground/* tap-snapshots/* +swrender/build/* diff --git a/package.json b/package.json index ef1774d15..39c548182 100644 --- a/package.json +++ b/package.json @@ -12,23 +12,27 @@ "main": "./dist/node/scratch-render.js", "browser": "./src/index.js", "scripts": { - "build": "webpack --progress --colors", + "build": "npm run build:swrender && webpack --progress --colors", + "build:swrender": "cargo build --release --target wasm32-unknown-unknown --manifest-path=\"swrender/Cargo.toml\" && wasm-bindgen --target web swrender/target/wasm32-unknown-unknown/release/swrender.wasm --out-dir swrender/build", "docs": "jsdoc -c .jsdoc.json", "lint": "eslint .", "prepublish": "npm run build", "prepublish-watch": "npm run watch", - "start": "webpack-dev-server", + "start": "npm run build:swrender && webpack-dev-server", "tap": "tap test/unit test/integration", "test": "npm run lint && npm run docs && npm run build && npm run tap", "version": "json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\"", "watch": "webpack --progress --colors --watch --watch-poll" }, "devDependencies": { - "babel-core": "^6.23.1", + "@babel/core": "^7.8.7", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/polyfill": "^7.8.7", + "@babel/preset-env": "^7.8.7", + "@wasm-tool/wasm-pack-plugin": "^1.2.0", "babel-eslint": "^10.1.0", - "babel-loader": "^7.1.4", - "babel-polyfill": "^6.22.0", - "babel-preset-env": "^1.6.1", + "babel-loader": "^8.0.6", + "babel-plugin-bundled-import-meta": "^0.3.2", "copy-webpack-plugin": "^4.5.1", "docdash": "^0.4.0", "eslint": "^7.13.0", @@ -41,6 +45,7 @@ "tap": "^11.0.0", "travis-after-all": "^1.4.4", "uglifyjs-webpack-plugin": "^1.2.5", + "webassembly-loader": "^1.1.0", "webpack": "^4.8.0", "webpack-cli": "^3.1.0", "webpack-dev-server": "^3.1.4" diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index b3f6a0aaf..c29931928 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -14,6 +14,21 @@ const TextBubbleSkin = require('./TextBubbleSkin'); const EffectTransform = require('./EffectTransform'); const log = require('./util/log'); +let onLoadSwRender = null; +let swRenderLoaded = false; +// eslint-disable-next-line no-unused-vars +let swrender = null; + +const wasm = require('../swrender/build/swrender_bg.wasm'); +const swrenderInit = require('../swrender/build/swrender.js').default; + +swrenderInit(wasm) + .then(res => { + swrender = res; + swRenderLoaded = true; + if (onLoadSwRender) onLoadSwRender(); + }); + const __isTouchingDrawablesPoint = twgl.v3.create(); const __candidatesBounds = new Rectangle(); const __fenceBounds = new Rectangle(); @@ -114,6 +129,14 @@ class RenderWebGL extends EventEmitter { return twgl.getWebGLContext(canvas, {alpha: false, stencil: true, antialias: false}); } + init () { + if (swRenderLoaded) return Promise.resolve(); + + return new Promise(resolve => { + onLoadSwRender = resolve; + }); + } + /** * Create a renderer for drawing Scratch sprites to a canvas using WebGL. * Coordinates will default to Scratch 2.0 values if unspecified. diff --git a/swrender/.gitignore b/swrender/.gitignore new file mode 100644 index 000000000..9775a7636 --- /dev/null +++ b/swrender/.gitignore @@ -0,0 +1,7 @@ +/target +**/*.rs.bk +Cargo.lock +bin/ +build/ +wasm-pack.log +.cargo-ok diff --git a/swrender/Cargo.toml b/swrender/Cargo.toml new file mode 100644 index 000000000..d2a182c0e --- /dev/null +++ b/swrender/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "swrender" +version = "0.1.0" +authors = ["adroitwhiz <adroitwhiz@protonmail.com>"] +edition = "2018" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +wasm-bindgen = "0.2" + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.1", optional = true } + +# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size +# compared to the default allocator's ~10K. It is slower than the default +# allocator, however. +# +# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. +wee_alloc = { version = "0.4.2", optional = true } + +[dev-dependencies] +wasm-bindgen-test = "0.2" + +[profile.release] +opt-level = 3 diff --git a/swrender/src/lib.rs b/swrender/src/lib.rs new file mode 100644 index 000000000..579fde4dd --- /dev/null +++ b/swrender/src/lib.rs @@ -0,0 +1,19 @@ +mod utils; + +use wasm_bindgen::prelude::*; + +// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global +// allocator. +#[cfg(feature = "wee_alloc")] +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +#[wasm_bindgen] +extern { + fn alert(s: &str); +} + +#[wasm_bindgen] +pub fn greet() { + alert("Hello, swrenderTEST5!"); +} diff --git a/swrender/src/utils.rs b/swrender/src/utils.rs new file mode 100644 index 000000000..b1d7929dc --- /dev/null +++ b/swrender/src/utils.rs @@ -0,0 +1,10 @@ +pub fn set_panic_hook() { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} diff --git a/swrender/tests/web.rs b/swrender/tests/web.rs new file mode 100644 index 000000000..de5c1dafe --- /dev/null +++ b/swrender/tests/web.rs @@ -0,0 +1,13 @@ +//! Test suite for the Web and headless browsers. + +#![cfg(target_arch = "wasm32")] + +extern crate wasm_bindgen_test; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +fn pass() { + assert_eq!(1 + 1, 2); +} diff --git a/webpack.config.js b/webpack.config.js index c01112e8d..145e817f9 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -11,18 +11,27 @@ const base = { }, devtool: 'cheap-module-source-map', module: { - rules: [ - { - include: [ - path.resolve('src') - ], - test: /\.js$/, - loader: 'babel-loader', - options: { - presets: [['env', {targets: {browsers: ['last 3 versions', 'Safari >= 8', 'iOS >= 8']}}]] - } + rules: [{ + include: path.resolve('swrender'), + loader: 'babel-loader', + options: { + babelrc: false, + plugins: [ + '@babel/plugin-syntax-import-meta', + ['bundled-import-meta', { + importStyle: 'cjs' + }] + ] } - ] + }, + { + test: /\.wasm$/, + loader: 'webassembly-loader', + type: 'javascript/auto', + options: { + export: 'buffer' + } + }] }, optimization: { minimizer: [ From 1c872904ce4a74be9cbe24626d242088de60076d Mon Sep 17 00:00:00 2001 From: adroitwhiz <adroitwhiz@protonmail.com> Date: Fri, 20 Mar 2020 05:37:02 -0400 Subject: [PATCH 02/14] Implement isTouchingDrawables in Rust --- src/BitmapSkin.js | 2 +- src/Drawable.js | 14 ++++- src/PenSkin.js | 10 ++-- src/RenderWebGL.js | 34 ++++-------- src/SVGSkin.js | 11 ++-- src/Skin.js | 16 +++++- src/TextBubbleSkin.js | 5 +- swrender/src/console_log.rs | 15 ++++++ swrender/src/drawable.rs | 33 ++++++++++++ swrender/src/lib.rs | 101 +++++++++++++++++++++++++++++++++++- swrender/src/matrix.rs | 12 +++++ swrender/src/silhouette.rs | 50 ++++++++++++++++++ 12 files changed, 259 insertions(+), 44 deletions(-) create mode 100644 swrender/src/console_log.rs create mode 100644 swrender/src/drawable.rs create mode 100644 swrender/src/matrix.rs create mode 100644 swrender/src/silhouette.rs diff --git a/src/BitmapSkin.js b/src/BitmapSkin.js index e48ce43a2..b836b2ee6 100644 --- a/src/BitmapSkin.js +++ b/src/BitmapSkin.js @@ -10,7 +10,7 @@ class BitmapSkin extends Skin { * @param {!RenderWebGL} renderer - The renderer which will use this skin. */ constructor (id, renderer) { - super(id); + super(id, renderer); /** @type {!int} */ this._costumeResolution = 1; diff --git a/src/Drawable.js b/src/Drawable.js index 7a2813d88..4ca3f1dec 100644 --- a/src/Drawable.js +++ b/src/Drawable.js @@ -59,12 +59,15 @@ class Drawable { * An object which can be drawn by the renderer. * @todo double-buffer all rendering state (position, skin, effects, etc.) * @param {!int} id - This Drawable's unique ID. + * @param {!RenderWebGL} renderer - The renderer which will use this skin. * @constructor */ - constructor (id) { + constructor (id, renderer) { /** @type {!int} */ this._id = id; + this._renderer = renderer; + /** * The uniforms to be used by the vertex and pixel shaders. * Some of these are used by other parts of the renderer as well. @@ -134,6 +137,7 @@ class Drawable { dispose () { // Use the setter: disconnect events this.skin = null; + this._renderer.softwareRenderer.remove_drawable(this.id); } /** @@ -655,6 +659,14 @@ class Drawable { this.isTouching = this._isTouchingNever; } + + this._renderer.softwareRenderer.set_drawable( + this.id, + this._uniforms.u_modelMatrix, + // TODO: calculate inverse matrix in the Rust side + this._inverseMatrix, + this.skin.id + ); } /** diff --git a/src/PenSkin.js b/src/PenSkin.js index 8248b1f8d..2961a2580 100644 --- a/src/PenSkin.js +++ b/src/PenSkin.js @@ -39,13 +39,7 @@ class PenSkin extends Skin { * @listens RenderWebGL#event:NativeSizeChanged */ constructor (id, renderer) { - super(id); - - /** - * @private - * @type {RenderWebGL} - */ - this._renderer = renderer; + super(id, renderer); /** @type {Array<number>} */ this._size = null; @@ -339,6 +333,8 @@ class PenSkin extends Skin { gl.RGBA, gl.UNSIGNED_BYTE, this._silhouettePixels ); + this._newSilhouette.set_data(this._canvas.width, this._canvas.height, this._silhouettePixels); + this._silhouetteImageData.data.set(this._silhouettePixels); this._silhouette.update(this._silhouetteImageData, true /* isPremultiplied */); diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index c29931928..465daf611 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -17,14 +17,14 @@ const log = require('./util/log'); let onLoadSwRender = null; let swRenderLoaded = false; // eslint-disable-next-line no-unused-vars -let swrender = null; +const swrender = require('../swrender/build/swrender.js'); const wasm = require('../swrender/build/swrender_bg.wasm'); const swrenderInit = require('../swrender/build/swrender.js').default; swrenderInit(wasm) - .then(res => { - swrender = res; + .then(() => { + window.swrender = swrender; swRenderLoaded = true; if (onLoadSwRender) onLoadSwRender(); }); @@ -134,6 +134,10 @@ class RenderWebGL extends EventEmitter { return new Promise(resolve => { onLoadSwRender = resolve; + }).then(() => { + this.swrender = swrender; + + this.softwareRenderer = swrender.SoftwareRenderer.new(); }); } @@ -500,7 +504,7 @@ class RenderWebGL extends EventEmitter { return; } const drawableID = this._nextDrawableId++; - const drawable = new Drawable(drawableID); + const drawable = new Drawable(drawableID, this); this._allDrawables[drawableID] = drawable; this._addToDrawList(drawableID, group); @@ -992,28 +996,9 @@ class RenderWebGL extends EventEmitter { const bounds = this._candidatesBounds(candidates); const drawable = this._allDrawables[drawableID]; - const point = __isTouchingDrawablesPoint; - drawable.updateCPURenderAttributes(); - // This is an EXTREMELY brute force collision detector, but it is - // still faster than asking the GPU to give us the pixels. - for (let x = bounds.left; x <= bounds.right; x++) { - // Scratch Space - +y is top - point[0] = x; - for (let y = bounds.bottom; y <= bounds.top; y++) { - point[1] = y; - if (drawable.isTouching(point)) { - for (let index = 0; index < candidates.length; index++) { - if (candidates[index].drawable.isTouching(point)) { - return true; - } - } - } - } - } - - return false; + return this.softwareRenderer.is_touching_drawables(drawableID, candidates.map(c => c.id), bounds); } /** @@ -1475,6 +1460,7 @@ class RenderWebGL extends EventEmitter { */ _candidatesTouching (drawableID, candidateIDs) { const bounds = this._touchingBounds(drawableID); + bounds.snapToInt(); const result = []; if (bounds === null) { return result; diff --git a/src/SVGSkin.js b/src/SVGSkin.js index bf1aeca88..18c17c57d 100644 --- a/src/SVGSkin.js +++ b/src/SVGSkin.js @@ -23,10 +23,7 @@ class SVGSkin extends Skin { * @extends Skin */ constructor (id, renderer) { - super(id); - - /** @type {RenderWebGL} */ - this._renderer = renderer; + super(id, renderer); /** @type {SvgRenderer} */ this._svgRenderer = new SvgRenderer(); @@ -119,6 +116,12 @@ class SVGSkin extends Skin { // Check if this is the largest MIP created so far. Currently, silhouettes only get scaled up. if (this._largestMIPScale < scale) { this._silhouette.update(textureData); + this._renderer.softwareRenderer.set_silhouette( + this._id, + textureData.width, + textureData.height, + textureData.data + ); this._largestMIPScale = scale; } diff --git a/src/Skin.js b/src/Skin.js index ae98d50c9..18c17e999 100644 --- a/src/Skin.js +++ b/src/Skin.js @@ -9,11 +9,15 @@ class Skin extends EventEmitter { /** * Create a Skin, which stores and/or generates textures for use in rendering. * @param {int} id - The unique ID for this Skin. + * @param {!RenderWebGL} renderer - The renderer which will use this skin. * @constructor */ - constructor (id) { + constructor (id, renderer) { super(); + /** @type {RenderWebGL} */ + this._renderer = renderer; + /** @type {int} */ this._id = id; @@ -49,6 +53,8 @@ class Skin extends EventEmitter { */ this._silhouette = new Silhouette(); + renderer.softwareRenderer.set_silhouette(id, 0, 0, new Uint8Array(0)); + this.setMaxListeners(RenderConstants.SKIN_SHARE_SOFT_LIMIT); } @@ -57,6 +63,7 @@ class Skin extends EventEmitter { */ dispose () { this._id = RenderConstants.ID_NONE; + this._newSilhouette.free(); } /** @@ -155,6 +162,12 @@ class Skin extends EventEmitter { gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); this._silhouette.update(textureData); + this._renderer.softwareRenderer.set_silhouette( + this._id, + textureData.width, + textureData.height, + textureData.data + ); } /** @@ -189,6 +202,7 @@ class Skin extends EventEmitter { this._rotationCenter[1] = 0; this._silhouette.update(this._emptyImageData); + this._renderer.softwareRenderer.set_silhouette(this._id, 1, 1, this._emptyImageData); this.emit(Skin.Events.WasAltered); } diff --git a/src/TextBubbleSkin.js b/src/TextBubbleSkin.js index 0ce6ac1a2..01aa34b0e 100644 --- a/src/TextBubbleSkin.js +++ b/src/TextBubbleSkin.js @@ -34,10 +34,7 @@ class TextBubbleSkin extends Skin { * @extends Skin */ constructor (id, renderer) { - super(id); - - /** @type {RenderWebGL} */ - this._renderer = renderer; + super(id, renderer); /** @type {HTMLCanvasElement} */ this._canvas = document.createElement('canvas'); diff --git a/swrender/src/console_log.rs b/swrender/src/console_log.rs new file mode 100644 index 000000000..7ade76788 --- /dev/null +++ b/swrender/src/console_log.rs @@ -0,0 +1,15 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +extern { + #[wasm_bindgen(js_namespace = console)] + pub fn log(s: &str); +} + +#[macro_use] +mod console_log { + #[macro_export] + macro_rules! console_log { + ($($t:tt)*) => (log(&format_args!($($t)*).to_string())) + } +} diff --git a/swrender/src/drawable.rs b/swrender/src/drawable.rs new file mode 100644 index 000000000..b3fbe03d9 --- /dev/null +++ b/swrender/src/drawable.rs @@ -0,0 +1,33 @@ +use crate::silhouette::*; +use crate::matrix::*; + +pub type DrawableID = u32; + +pub struct Drawable { + pub matrix: Mat4, + pub inverse_matrix: Mat4, + pub silhouette: SilhouetteID, + pub id: DrawableID +} + +impl Drawable { + pub fn get_local_position(&self, vec: Vec2) -> Vec2 { + let v0 = vec.0 - 0.5; + let v1 = vec.1 + 0.5; + let m = self.inverse_matrix; + let d = (v0 * m[3]) + (v1 * m[7]) + m[15]; + // The RenderWebGL quad flips the texture's X axis. So rendered bottom + // left is 1, 0 and the top right is 0, 1. Flip the X axis so + // localPosition matches that transformation. + let out_x = 0.5 - (((v0 * m[0]) + (v1 * m[4]) + m[12]) / d); + let out_y = (((v0 * m[1]) + (v1 * m[5]) + m[13]) / d) + 0.5; + + (out_x, out_y) + } + + #[inline(always)] + pub fn is_touching(&self, position: Vec2, silhouette: &Silhouette) -> bool { + let local_position = self.get_local_position(position); + silhouette.get_point((local_position.0 * silhouette.width as f32) as i32, (local_position.1 * silhouette.height as f32) as i32) + } +} diff --git a/swrender/src/lib.rs b/swrender/src/lib.rs index 579fde4dd..f7251999a 100644 --- a/swrender/src/lib.rs +++ b/swrender/src/lib.rs @@ -1,7 +1,13 @@ mod utils; +mod matrix; +pub mod silhouette; +pub mod drawable; use wasm_bindgen::prelude::*; +use std::collections::HashMap; +use std::convert::TryInto; + // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global // allocator. #[cfg(feature = "wee_alloc")] @@ -11,9 +17,100 @@ static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; #[wasm_bindgen] extern { fn alert(s: &str); + + pub type Rectangle; + + #[wasm_bindgen(method, getter)] + fn left(this: &Rectangle) -> f64; + #[wasm_bindgen(method, getter)] + fn right(this: &Rectangle) -> f64; + #[wasm_bindgen(method, getter)] + fn bottom(this: &Rectangle) -> f64; + #[wasm_bindgen(method, getter)] + fn top(this: &Rectangle) -> f64; } +const ID_NONE: u32 = u32::max_value(); + #[wasm_bindgen] -pub fn greet() { - alert("Hello, swrenderTEST5!"); +pub struct SoftwareRenderer { + drawables: HashMap<drawable::DrawableID, drawable::Drawable>, + silhouettes: HashMap<silhouette::SilhouetteID, silhouette::Silhouette> +} + +#[wasm_bindgen] +impl SoftwareRenderer { + pub fn new() -> SoftwareRenderer { + let mut renderer = SoftwareRenderer { + drawables: HashMap::new(), + silhouettes: HashMap::new() + }; + + renderer.silhouettes.insert(ID_NONE, silhouette::Silhouette::new(ID_NONE)); + + utils::set_panic_hook(); + renderer + } + + pub fn set_drawable(&mut self, id: drawable::DrawableID, matrix: Box<[f32]>, inverse_matrix: Box<[f32]>, silhouette: Option<silhouette::SilhouetteID>) { + let d = self.drawables.entry(id).or_insert(drawable::Drawable { + matrix: [0.0; 16], + inverse_matrix: [0.0; 16], + silhouette: match silhouette { + Some(s) => s, + None => ID_NONE + }, + id + }); + + d.matrix = (*matrix).try_into().expect("drawable's matrix contains 16 elements"); + d.inverse_matrix = (*inverse_matrix).try_into().expect("drawable's inverse matrix contains 16 elements"); + if let Some(s) = silhouette { + d.silhouette = s; + } + } + + pub fn remove_drawable(&mut self, id: drawable::DrawableID) { + self.drawables.remove(&id); + } + + pub fn set_silhouette(&mut self, id: silhouette::SilhouetteID, w: u32, h: u32, data: Box<[u8]>) { + let s = self.silhouettes.entry(id).or_insert(silhouette::Silhouette::new(id)); + s.set_data(w, h, data); + } + + pub fn remove_silhouette(&mut self, id: silhouette::SilhouetteID) { + self.silhouettes.remove(&id); + } + + pub fn is_touching_drawables(&mut self, drawable: drawable::DrawableID, candidates: Vec<drawable::DrawableID>, rect: Rectangle) -> bool { + let left = rect.left() as i32; + let right = rect.right() as i32 + 1; + let bottom = rect.bottom() as i32 - 1; + let top = rect.top() as i32; + + let drawable = self.drawables.get(&drawable).expect("Drawable should exist"); + let silhouette = self.silhouettes.get(&drawable.silhouette).unwrap(); + let candidates: Vec<(&drawable::Drawable, &silhouette::Silhouette)> = candidates.into_iter() + .map(|c| { + let d = self.drawables.get(&c).expect("Candidate drawable should exist"); + let s = self.silhouettes.get(&d.silhouette).unwrap(); + (d, s) + }).collect(); + + for x in left..right { + for y in bottom..top { + let position = (x as f32, y as f32); + if drawable.is_touching(position, silhouette) { + for candidate in &candidates { + if candidate.0.is_touching(position, candidate.1) { + return true; + } + } + } + } + } + + false + } } diff --git a/swrender/src/matrix.rs b/swrender/src/matrix.rs new file mode 100644 index 000000000..b4bcee9f5 --- /dev/null +++ b/swrender/src/matrix.rs @@ -0,0 +1,12 @@ +pub type Mat4 = [f32; 16]; +pub type Vec2 = (f32, f32); + +trait Matrix { + fn inverse(&self) -> Self; +} + +impl Matrix for Mat4 { + fn inverse(&self) -> Mat4 { + unimplemented!() + } +} diff --git a/swrender/src/silhouette.rs b/swrender/src/silhouette.rs new file mode 100644 index 000000000..277e8492c --- /dev/null +++ b/swrender/src/silhouette.rs @@ -0,0 +1,50 @@ +use wasm_bindgen::prelude::*; + +pub type SilhouetteID = u32; + +#[wasm_bindgen] +pub struct Silhouette { + pub width: u32, + pub height: u32, + pub id: SilhouetteID, + data: Box<[u8]>, + _blank: Box<[u8; 4]> +} + +impl Silhouette { + pub fn new(id: SilhouetteID) -> Silhouette { + Silhouette { + width: 0, + height: 0, + id, + data: Box::new([0, 0, 0, 0]), + _blank: Box::new([0, 0, 0, 0]) + } + } + + pub fn set_data(&mut self, w: u32, h: u32, data: Box<[u8]>) { + assert_eq!(data.len(), (w * h * 4) as usize, "silhouette data is improperly sized"); + + self.width = w; + self.height = h; + self.data = data; + } + + pub fn get_point(&self, x: i32, y: i32) -> bool { + if x < 0 || y < 0 || (x as u32) >= self.width || (y as u32) >= self.height { + false + } else { + let idx = (((y as u32 * self.width) + x as u32) * 4) as usize; + self.data[idx+3] != 0u8 + } + } + + pub fn get_color(&self, x: i32, y: i32) -> &[u8] { + if x < 0 || y < 0 || (x as u32) >= self.width || (y as u32) >= self.height { + &self._blank[0..4] + } else { + let idx = (((y as u32 * self.width) + x as u32) * 4) as usize; + &self.data[idx..idx+4] + } + } +} From 84cea1fef730b7e90fa2dd801b2b7197fbb65c0e Mon Sep 17 00:00:00 2001 From: adroitwhiz <adroitwhiz@protonmail.com> Date: Fri, 20 Mar 2020 19:53:57 -0400 Subject: [PATCH 03/14] Add distortion effects --- src/Drawable.js | 19 ++++- src/PenSkin.js | 4 +- src/RenderWebGL.js | 2 +- src/SVGSkin.js | 7 +- src/Skin.js | 24 ++++-- swrender/src/drawable.rs | 19 ++++- swrender/src/effect_transform.rs | 135 +++++++++++++++++++++++++++++ swrender/src/lib.rs | 63 +++++++++----- swrender/src/matrix.rs | 142 ++++++++++++++++++++++++++++++- swrender/src/rectangle.rs | 33 +++++++ swrender/src/silhouette.rs | 12 +-- 11 files changed, 406 insertions(+), 54 deletions(-) create mode 100644 swrender/src/effect_transform.rs create mode 100644 swrender/src/rectangle.rs diff --git a/src/Drawable.js b/src/Drawable.js index 4ca3f1dec..872e7287e 100644 --- a/src/Drawable.js +++ b/src/Drawable.js @@ -116,6 +116,8 @@ class Drawable { * @type {int} */ this.enabledEffects = 0; + this._effectsDirty = true; + /** @todo move convex hull functionality, maybe bounds functionality overall, to Skin classes */ this._convexHullPoints = null; this._convexHullDirty = true; @@ -255,6 +257,10 @@ class Drawable { } } + setEffectsDirty () { + this._effectsDirty = true; + } + /** * Update an effect. Marks the convex hull as dirty if the effect changes shape. * @param {string} effectName The name of the effect. @@ -272,6 +278,7 @@ class Drawable { if (effectInfo.shapeChanges) { this.setConvexHullDirty(); } + this.setEffectsDirty(); } /** @@ -660,12 +667,18 @@ class Drawable { this.isTouching = this._isTouchingNever; } + let effects = null; + if (this._effectsDirty) { + effects = this._uniforms; + this._effectsDirty = false; + } + this._renderer.softwareRenderer.set_drawable( this.id, this._uniforms.u_modelMatrix, - // TODO: calculate inverse matrix in the Rust side - this._inverseMatrix, - this.skin.id + this.skin.id, + effects, + this.enabledEffects ); } diff --git a/src/PenSkin.js b/src/PenSkin.js index 2961a2580..c2096219a 100644 --- a/src/PenSkin.js +++ b/src/PenSkin.js @@ -333,11 +333,11 @@ class PenSkin extends Skin { gl.RGBA, gl.UNSIGNED_BYTE, this._silhouettePixels ); - this._newSilhouette.set_data(this._canvas.width, this._canvas.height, this._silhouettePixels); - this._silhouetteImageData.data.set(this._silhouettePixels); this._silhouette.update(this._silhouetteImageData, true /* isPremultiplied */); + this._setSilhouetteFromData(this._silhouetteImageData); + this._silhouetteDirty = false; } } diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index 465daf611..beb1272db 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -1460,11 +1460,11 @@ class RenderWebGL extends EventEmitter { */ _candidatesTouching (drawableID, candidateIDs) { const bounds = this._touchingBounds(drawableID); - bounds.snapToInt(); const result = []; if (bounds === null) { return result; } + bounds.snapToInt(); // iterate through the drawables list BACKWARDS - we want the top most item to be the first we check for (let index = candidateIDs.length - 1; index >= 0; index--) { const id = candidateIDs[index]; diff --git a/src/SVGSkin.js b/src/SVGSkin.js index 18c17c57d..3d3ec5234 100644 --- a/src/SVGSkin.js +++ b/src/SVGSkin.js @@ -116,12 +116,7 @@ class SVGSkin extends Skin { // Check if this is the largest MIP created so far. Currently, silhouettes only get scaled up. if (this._largestMIPScale < scale) { this._silhouette.update(textureData); - this._renderer.softwareRenderer.set_silhouette( - this._id, - textureData.width, - textureData.height, - textureData.data - ); + this._setSilhouetteFromData(textureData); this._largestMIPScale = scale; } diff --git a/src/Skin.js b/src/Skin.js index 18c17e999..3ceb60802 100644 --- a/src/Skin.js +++ b/src/Skin.js @@ -63,7 +63,7 @@ class Skin extends EventEmitter { */ dispose () { this._id = RenderConstants.ID_NONE; - this._newSilhouette.free(); + if (this._newSilhouette) this._newSilhouette.free(); } /** @@ -162,12 +162,7 @@ class Skin extends EventEmitter { gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); this._silhouette.update(textureData); - this._renderer.softwareRenderer.set_silhouette( - this._id, - textureData.width, - textureData.height, - textureData.data - ); + this._setSilhouetteFromData(textureData); } /** @@ -202,10 +197,23 @@ class Skin extends EventEmitter { this._rotationCenter[1] = 0; this._silhouette.update(this._emptyImageData); - this._renderer.softwareRenderer.set_silhouette(this._id, 1, 1, this._emptyImageData); + this._setSilhouetteFromData(this._emptyImageData); this.emit(Skin.Events.WasAltered); } + _setSilhouetteFromData (data) { + const size = this.size; + this._renderer.softwareRenderer.set_silhouette( + this._id, + data.width, + data.height, + data.data, + + size[0], + size[1] + ); + } + /** * Does this point touch an opaque or translucent point on this skin? * Nearest Neighbor version diff --git a/swrender/src/drawable.rs b/swrender/src/drawable.rs index b3fbe03d9..f7e04ef89 100644 --- a/swrender/src/drawable.rs +++ b/swrender/src/drawable.rs @@ -1,5 +1,6 @@ use crate::silhouette::*; use crate::matrix::*; +use crate::effect_transform::{Effects, EffectBits, transform_point, DISTORTION_EFFECT_MASK}; pub type DrawableID = u32; @@ -7,7 +8,9 @@ pub struct Drawable { pub matrix: Mat4, pub inverse_matrix: Mat4, pub silhouette: SilhouetteID, - pub id: DrawableID + pub id: DrawableID, + pub effects: Effects, + pub effect_bits: EffectBits } impl Drawable { @@ -22,12 +25,24 @@ impl Drawable { let out_x = 0.5 - (((v0 * m[0]) + (v1 * m[4]) + m[12]) / d); let out_y = (((v0 * m[1]) + (v1 * m[5]) + m[13]) / d) + 0.5; - (out_x, out_y) + Vec2(out_x, out_y) + } + + pub fn get_transformed_position(&self, vec: Vec2, skin_size: Vec2) -> Vec2 { + if (self.effect_bits & DISTORTION_EFFECT_MASK) == 0 { + vec + } else { + transform_point(vec, &self.effects, &self.effect_bits, skin_size) + } } #[inline(always)] pub fn is_touching(&self, position: Vec2, silhouette: &Silhouette) -> bool { let local_position = self.get_local_position(position); + if local_position.0 < 0f32 || local_position.0 >= 1f32 || local_position.1 < 0f32 || local_position.1 >= 1f32 { + return false; + } + let local_position = self.get_transformed_position(local_position, silhouette.nominal_size); silhouette.get_point((local_position.0 * silhouette.width as f32) as i32, (local_position.1 * silhouette.height as f32) as i32) } } diff --git a/swrender/src/effect_transform.rs b/swrender/src/effect_transform.rs new file mode 100644 index 000000000..8ef23d6ef --- /dev/null +++ b/swrender/src/effect_transform.rs @@ -0,0 +1,135 @@ +use crate::matrix::*; + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +extern { + pub type JSEffectMap; + + #[wasm_bindgen(method, getter)] + pub fn u_color(this: &JSEffectMap) -> f64; + #[wasm_bindgen(method, getter)] + pub fn u_fisheye(this: &JSEffectMap) -> f64; + #[wasm_bindgen(method, getter)] + pub fn u_whirl(this: &JSEffectMap) -> f64; + #[wasm_bindgen(method, getter)] + pub fn u_pixelate(this: &JSEffectMap) -> f64; + #[wasm_bindgen(method, getter)] + pub fn u_mosaic(this: &JSEffectMap) -> f64; + #[wasm_bindgen(method, getter)] + pub fn u_brightness(this: &JSEffectMap) -> f64; + #[wasm_bindgen(method, getter)] + pub fn u_ghost(this: &JSEffectMap) -> f64; +} + +#[derive(Default)] +pub struct Effects { + pub color: f32, + pub fisheye: f32, + pub whirl: f32, + pub pixelate: f32, + pub mosaic: f32, + pub brightness: f32, + pub ghost: f32, +} + +pub type EffectBits = u32; +pub enum EffectBitfield { + Color = 0, + Fisheye = 1, + Whirl = 2, + Pixelate = 3, + Mosaic = 4, + Brightness = 5, + Ghost = 6, +} + +pub const DISTORTION_EFFECT_MASK: EffectBits = + 1 << (EffectBitfield::Fisheye as u32) | + 1 << (EffectBitfield::Whirl as u32) | + 1 << (EffectBitfield::Pixelate as u32) | + 1 << (EffectBitfield::Mosaic as u32); + +impl Effects { + pub fn set_from_js(&mut self, effects: JSEffectMap) { + self.color = effects.u_color() as f32; + self.fisheye = effects.u_fisheye() as f32; + self.whirl = effects.u_whirl() as f32; + self.pixelate = effects.u_pixelate() as f32; + self.mosaic = effects.u_mosaic() as f32; + self.brightness = effects.u_brightness() as f32; + self.ghost = effects.u_ghost() as f32; + } +} + +const CENTER: Vec2 = Vec2(0.5, 0.5); + +pub fn transform_point(point: Vec2, effects: &Effects, effect_bits: &EffectBits, skin_size: Vec2) -> Vec2 { + let mut out = point; + + if effect_bits & (1 << (EffectBitfield::Mosaic as u32)) != 0 { + /*texcoord0 = fract(u_mosaic * texcoord0);*/ + out = Vec2( + f32::fract(effects.mosaic * out.0), + f32::fract(effects.mosaic * out.1) + ); + } + + if effect_bits & (1 << (EffectBitfield::Pixelate as u32)) != 0 { + /*vec2 pixelTexelSize = u_skinSize / u_pixelate; + texcoord0 = (floor(texcoord0 * pixelTexelSize) + kCenter) / pixelTexelSize;*/ + let pixel_texel_size_x = skin_size.0 / effects.pixelate; + let pixel_texel_size_y = skin_size.1 / effects.pixelate; + + out = Vec2( + (f32::floor(out.0 * pixel_texel_size_x) + CENTER.0) / pixel_texel_size_x, + (f32::floor(out.1 * pixel_texel_size_y) + CENTER.1) / pixel_texel_size_y + ); + } + + if effect_bits & (1 << (EffectBitfield::Whirl as u32)) != 0 { + /*const float kRadius = 0.5; + vec2 offset = texcoord0 - kCenter; + float offsetMagnitude = length(offset); + float whirlFactor = max(1.0 - (offsetMagnitude / kRadius), 0.0); + float whirlActual = u_whirl * whirlFactor * whirlFactor; + float sinWhirl = sin(whirlActual); + float cosWhirl = cos(whirlActual); + mat2 rotationMatrix = mat2( + cosWhirl, -sinWhirl, + sinWhirl, cosWhirl + ); + + texcoord0 = rotationMatrix * offset + kCenter;*/ + + const RADIUS: f32 = 0.5; + let offset = out - CENTER; + let offset_magnitude = offset.length(); + let whirl_factor = f32::max(1.0 - (offset_magnitude / RADIUS), 0.0); + let whirl_actual = effects.whirl * whirl_factor * whirl_factor; + let (sin_whirl, cos_whirl) = f32::sin_cos(whirl_actual); + + // texcoord0 = rotationMatrix * offset + kCenter; + out.0 = (cos_whirl * offset.0) + (sin_whirl * offset.1) + CENTER.0; + out.1 = (cos_whirl * offset.1) - (sin_whirl * offset.0) + CENTER.1; + } + + if effect_bits & (1 << (EffectBitfield::Fisheye as u32)) != 0 { + /* vec2 vec = (texcoord0 - kCenter) / kCenter; + float vecLength = length(vec); + float r = pow(min(vecLength, 1.0), u_fisheye) * max(1.0, vecLength); + vec2 unit = vec / vecLength; + + texcoord0 = kCenter + r * unit * kCenter;*/ + + let v = (out - CENTER) / CENTER; + + let len = v.length(); + let r = f32::powf(f32::min(len, 1.0), effects.fisheye) * f32::max(1.0, len); + let unit: Vec2 = v / Vec2(len, len); + + out = CENTER + Vec2(r, r) * unit * CENTER; + } + + out +} diff --git a/swrender/src/lib.rs b/swrender/src/lib.rs index f7251999a..b3f29594e 100644 --- a/swrender/src/lib.rs +++ b/swrender/src/lib.rs @@ -1,5 +1,7 @@ mod utils; +mod rectangle; mod matrix; +mod effect_transform; pub mod silhouette; pub mod drawable; @@ -8,28 +10,14 @@ use wasm_bindgen::prelude::*; use std::collections::HashMap; use std::convert::TryInto; +use matrix::Matrix; + // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global // allocator. #[cfg(feature = "wee_alloc")] #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; -#[wasm_bindgen] -extern { - fn alert(s: &str); - - pub type Rectangle; - - #[wasm_bindgen(method, getter)] - fn left(this: &Rectangle) -> f64; - #[wasm_bindgen(method, getter)] - fn right(this: &Rectangle) -> f64; - #[wasm_bindgen(method, getter)] - fn bottom(this: &Rectangle) -> f64; - #[wasm_bindgen(method, getter)] - fn top(this: &Rectangle) -> f64; -} - const ID_NONE: u32 = u32::max_value(); #[wasm_bindgen] @@ -52,10 +40,19 @@ impl SoftwareRenderer { renderer } - pub fn set_drawable(&mut self, id: drawable::DrawableID, matrix: Box<[f32]>, inverse_matrix: Box<[f32]>, silhouette: Option<silhouette::SilhouetteID>) { + pub fn set_drawable( + &mut self, + id: drawable::DrawableID, + matrix: Option<Box<[f32]>>, + silhouette: Option<silhouette::SilhouetteID>, + effects: Option<effect_transform::JSEffectMap>, + effect_bits: effect_transform::EffectBits + ) { let d = self.drawables.entry(id).or_insert(drawable::Drawable { matrix: [0.0; 16], inverse_matrix: [0.0; 16], + effects: effect_transform::Effects::default(), + effect_bits: 0, silhouette: match silhouette { Some(s) => s, None => ID_NONE @@ -63,27 +60,47 @@ impl SoftwareRenderer { id }); - d.matrix = (*matrix).try_into().expect("drawable's matrix contains 16 elements"); - d.inverse_matrix = (*inverse_matrix).try_into().expect("drawable's inverse matrix contains 16 elements"); + if let Some(m) = matrix { + d.matrix = (*m).try_into().expect("drawable's matrix contains 16 elements"); + d.inverse_matrix = d.matrix.inverse(); + } if let Some(s) = silhouette { d.silhouette = s; } + if let Some(fx) = effects { + d.effects.set_from_js(fx); + } + d.effect_bits = effect_bits; } pub fn remove_drawable(&mut self, id: drawable::DrawableID) { self.drawables.remove(&id); } - pub fn set_silhouette(&mut self, id: silhouette::SilhouetteID, w: u32, h: u32, data: Box<[u8]>) { + pub fn set_silhouette( + &mut self, + id: silhouette::SilhouetteID, + w: u32, + h: u32, + data: Box<[u8]>, + nominal_width: + f64, nominal_height: f64 + ) { let s = self.silhouettes.entry(id).or_insert(silhouette::Silhouette::new(id)); - s.set_data(w, h, data); + s.set_data(w, h, data, matrix::Vec2(nominal_width as f32, nominal_height as f32)); } pub fn remove_silhouette(&mut self, id: silhouette::SilhouetteID) { self.silhouettes.remove(&id); } - pub fn is_touching_drawables(&mut self, drawable: drawable::DrawableID, candidates: Vec<drawable::DrawableID>, rect: Rectangle) -> bool { + pub fn is_touching_drawables( + &mut self, + drawable: drawable::DrawableID, + candidates: + Vec<drawable::DrawableID>, + rect: rectangle::JSRectangle + ) -> bool { let left = rect.left() as i32; let right = rect.right() as i32 + 1; let bottom = rect.bottom() as i32 - 1; @@ -100,7 +117,7 @@ impl SoftwareRenderer { for x in left..right { for y in bottom..top { - let position = (x as f32, y as f32); + let position = matrix::Vec2(x as f32, y as f32); if drawable.is_touching(position, silhouette) { for candidate in &candidates { if candidate.0.is_touching(position, candidate.1) { diff --git a/swrender/src/matrix.rs b/swrender/src/matrix.rs index b4bcee9f5..5610bd7c7 100644 --- a/swrender/src/matrix.rs +++ b/swrender/src/matrix.rs @@ -1,12 +1,146 @@ +use std::ops; +use std::f32; + pub type Mat4 = [f32; 16]; -pub type Vec2 = (f32, f32); -trait Matrix { +#[derive(Copy, Clone)] +pub struct Vec2(pub f32, pub f32); + +impl ops::Add for Vec2 { + type Output = Vec2; + + fn add(self, other: Vec2) -> Vec2 { + Vec2(self.0 + other.0, self.1 + other.1) + } +} + +impl ops::Sub for Vec2 { + type Output = Vec2; + + fn sub(self, other: Vec2) -> Vec2 { + Vec2(self.0 - other.0, self.1 - other.1) + } +} + +impl ops::Mul for Vec2 { + type Output = Vec2; + + fn mul(self, other: Vec2) -> Vec2 { + Vec2(self.0 * other.0, self.1 * other.1) + } +} + +impl ops::Div for Vec2 { + type Output = Vec2; + + fn div(self, other: Vec2) -> Vec2 { + Vec2(self.0 / other.0, self.1 / other.1) + } +} + +impl ops::Neg for Vec2 { + type Output = Vec2; + + fn neg(self) -> Vec2 { + Vec2(-self.0, -self.1) + } +} + +impl Vec2 { + pub fn length(&self) -> f32 { + f32::sqrt(self.0 * self.0 + self.1 * self.1) + } +} + +pub trait Matrix { fn inverse(&self) -> Self; } impl Matrix for Mat4 { - fn inverse(&self) -> Mat4 { - unimplemented!() + fn inverse(&self) -> Self { + let m00 = self[0 * 4 + 0]; + let m01 = self[0 * 4 + 1]; + let m02 = self[0 * 4 + 2]; + let m03 = self[0 * 4 + 3]; + let m10 = self[1 * 4 + 0]; + let m11 = self[1 * 4 + 1]; + let m12 = self[1 * 4 + 2]; + let m13 = self[1 * 4 + 3]; + let m20 = self[2 * 4 + 0]; + let m21 = self[2 * 4 + 1]; + let m22 = self[2 * 4 + 2]; + let m23 = self[2 * 4 + 3]; + let m30 = self[3 * 4 + 0]; + let m31 = self[3 * 4 + 1]; + let m32 = self[3 * 4 + 2]; + let m33 = self[3 * 4 + 3]; + let tmp_0 = m22 * m33; + let tmp_1 = m32 * m23; + let tmp_2 = m12 * m33; + let tmp_3 = m32 * m13; + let tmp_4 = m12 * m23; + let tmp_5 = m22 * m13; + let tmp_6 = m02 * m33; + let tmp_7 = m32 * m03; + let tmp_8 = m02 * m23; + let tmp_9 = m22 * m03; + let tmp_10 = m02 * m13; + let tmp_11 = m12 * m03; + let tmp_12 = m20 * m31; + let tmp_13 = m30 * m21; + let tmp_14 = m10 * m31; + let tmp_15 = m30 * m11; + let tmp_16 = m10 * m21; + let tmp_17 = m20 * m11; + let tmp_18 = m00 * m31; + let tmp_19 = m30 * m01; + let tmp_20 = m00 * m21; + let tmp_21 = m20 * m01; + let tmp_22 = m00 * m11; + let tmp_23 = m10 * m01; + + let t0: f32 = (tmp_0 * m11 + tmp_3 * m21 + tmp_4 * m31) - + (tmp_1 * m11 + tmp_2 * m21 + tmp_5 * m31); + let t1 = (tmp_1 * m01 + tmp_6 * m21 + tmp_9 * m31) - + (tmp_0 * m01 + tmp_7 * m21 + tmp_8 * m31); + let t2 = (tmp_2 * m01 + tmp_7 * m11 + tmp_10 * m31) - + (tmp_3 * m01 + tmp_6 * m11 + tmp_11 * m31); + let t3 = (tmp_5 * m01 + tmp_8 * m11 + tmp_11 * m21) - + (tmp_4 * m01 + tmp_9 * m11 + tmp_10 * m21); + + let d = 1.0 / (m00 * t0 + m10 * t1 + m20 * t2 + m30 * t3); + + let mut dst: Mat4 = [0f32; 16]; + + dst[ 0] = d * t0; + dst[ 1] = d * t1; + dst[ 2] = d * t2; + dst[ 3] = d * t3; + dst[ 4] = d * ((tmp_1 * m10 + tmp_2 * m20 + tmp_5 * m30) - + (tmp_0 * m10 + tmp_3 * m20 + tmp_4 * m30)); + dst[ 5] = d * ((tmp_0 * m00 + tmp_7 * m20 + tmp_8 * m30) - + (tmp_1 * m00 + tmp_6 * m20 + tmp_9 * m30)); + dst[ 6] = d * ((tmp_3 * m00 + tmp_6 * m10 + tmp_11 * m30) - + (tmp_2 * m00 + tmp_7 * m10 + tmp_10 * m30)); + dst[ 7] = d * ((tmp_4 * m00 + tmp_9 * m10 + tmp_10 * m20) - + (tmp_5 * m00 + tmp_8 * m10 + tmp_11 * m20)); + dst[ 8] = d * ((tmp_12 * m13 + tmp_15 * m23 + tmp_16 * m33) - + (tmp_13 * m13 + tmp_14 * m23 + tmp_17 * m33)); + dst[ 9] = d * ((tmp_13 * m03 + tmp_18 * m23 + tmp_21 * m33) - + (tmp_12 * m03 + tmp_19 * m23 + tmp_20 * m33)); + dst[10] = d * ((tmp_14 * m03 + tmp_19 * m13 + tmp_22 * m33) - + (tmp_15 * m03 + tmp_18 * m13 + tmp_23 * m33)); + dst[11] = d * ((tmp_17 * m03 + tmp_20 * m13 + tmp_23 * m23) - + (tmp_16 * m03 + tmp_21 * m13 + tmp_22 * m23)); + dst[12] = d * ((tmp_14 * m22 + tmp_17 * m32 + tmp_13 * m12) - + (tmp_16 * m32 + tmp_12 * m12 + tmp_15 * m22)); + dst[13] = d * ((tmp_20 * m32 + tmp_12 * m02 + tmp_19 * m22) - + (tmp_18 * m22 + tmp_21 * m32 + tmp_13 * m02)); + dst[14] = d * ((tmp_18 * m12 + tmp_23 * m32 + tmp_15 * m02) - + (tmp_22 * m32 + tmp_14 * m02 + tmp_19 * m12)); + dst[15] = d * ((tmp_22 * m22 + tmp_16 * m02 + tmp_21 * m12) - + (tmp_20 * m12 + tmp_23 * m22 + tmp_17 * m02)); + + dst } } diff --git a/swrender/src/rectangle.rs b/swrender/src/rectangle.rs new file mode 100644 index 000000000..e4ad40b23 --- /dev/null +++ b/swrender/src/rectangle.rs @@ -0,0 +1,33 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +extern { + pub type JSRectangle; + + #[wasm_bindgen(method, getter)] + pub fn left(this: &JSRectangle) -> f64; + #[wasm_bindgen(method, getter)] + pub fn right(this: &JSRectangle) -> f64; + #[wasm_bindgen(method, getter)] + pub fn bottom(this: &JSRectangle) -> f64; + #[wasm_bindgen(method, getter)] + pub fn top(this: &JSRectangle) -> f64; +} + +pub struct Rectangle<T> { + left: T, + right: T, + bottom: T, + top: T +} + +impl Rectangle<i32> { + pub fn fromJSRectangle(rect: JSRectangle) -> Self { + Rectangle { + left: rect.left().floor() as i32, + right: rect.right().ceil() as i32, + bottom: rect.bottom().floor() as i32, + top: rect.top().ceil() as i32 + } + } +} diff --git a/swrender/src/silhouette.rs b/swrender/src/silhouette.rs index 277e8492c..d089ec667 100644 --- a/swrender/src/silhouette.rs +++ b/swrender/src/silhouette.rs @@ -1,12 +1,12 @@ -use wasm_bindgen::prelude::*; +use crate::matrix::Vec2; pub type SilhouetteID = u32; -#[wasm_bindgen] pub struct Silhouette { + pub id: SilhouetteID, pub width: u32, pub height: u32, - pub id: SilhouetteID, + pub nominal_size: Vec2, data: Box<[u8]>, _blank: Box<[u8; 4]> } @@ -14,20 +14,22 @@ pub struct Silhouette { impl Silhouette { pub fn new(id: SilhouetteID) -> Silhouette { Silhouette { + id, width: 0, height: 0, - id, + nominal_size: Vec2(0f32, 0f32), data: Box::new([0, 0, 0, 0]), _blank: Box::new([0, 0, 0, 0]) } } - pub fn set_data(&mut self, w: u32, h: u32, data: Box<[u8]>) { + pub fn set_data(&mut self, w: u32, h: u32, data: Box<[u8]>, nominal_size: Vec2) { assert_eq!(data.len(), (w * h * 4) as usize, "silhouette data is improperly sized"); self.width = w; self.height = h; self.data = data; + self.nominal_size = nominal_size; } pub fn get_point(&self, x: i32, y: i32) -> bool { From be15bc6879793b92e4f45c68b0e7b925f64cae21 Mon Sep 17 00:00:00 2001 From: adroitwhiz <adroitwhiz@protonmail.com> Date: Sat, 21 Mar 2020 20:04:32 -0400 Subject: [PATCH 04/14] Add touching color --- src/Drawable.js | 34 +---- src/EffectTransform.js | 95 -------------- src/PenSkin.js | 2 +- src/RenderWebGL.js | 118 +++++------------- src/ShaderManager.js | 3 +- src/Silhouette.js | 123 +----------------- src/Skin.js | 8 +- swrender/src/drawable.rs | 38 +++++- swrender/src/effect_transform.rs | 137 +++++++++++++++++++- swrender/src/lib.rs | 206 +++++++++++++++++++++++++++---- swrender/src/rectangle.rs | 33 ----- swrender/src/silhouette.rs | 64 ++++++++-- 12 files changed, 447 insertions(+), 414 deletions(-) delete mode 100644 swrender/src/rectangle.rs diff --git a/src/Drawable.js b/src/Drawable.js index 872e7287e..18de2a8e2 100644 --- a/src/Drawable.js +++ b/src/Drawable.js @@ -678,7 +678,8 @@ class Drawable { this._uniforms.u_modelMatrix, this.skin.id, effects, - this.enabledEffects + this.enabledEffects, + this.skin.useNearest(this._scale, this) ); } @@ -723,37 +724,6 @@ class Drawable { id |= (b & 255) << 16; return id + RenderConstants.ID_NONE; } - - /** - * Sample a color from a drawable's texture. - * The caller is responsible for ensuring this drawable's inverse matrix & its skin's silhouette are up-to-date. - * @see updateCPURenderAttributes - * @param {twgl.v3} vec The scratch space [x,y] vector - * @param {Drawable} drawable The drawable to sample the texture from - * @param {Uint8ClampedArray} dst The "color4b" representation of the texture at point. - * @param {number} [effectMask] A bitmask for which effects to use. Optional. - * @returns {Uint8ClampedArray} The dst object filled with the color4b - */ - static sampleColor4b (vec, drawable, dst, effectMask) { - const localPosition = getLocalPosition(drawable, vec); - if (localPosition[0] < 0 || localPosition[1] < 0 || - localPosition[0] > 1 || localPosition[1] > 1) { - dst[0] = 0; - dst[1] = 0; - dst[2] = 0; - dst[3] = 0; - return dst; - } - - const textColor = - // commenting out to only use nearest for now - // drawable.skin.useNearest(drawable._scale, drawable) ? - drawable.skin._silhouette.colorAtNearest(localPosition, dst); - // : drawable.skin._silhouette.colorAtLinear(localPosition, dst); - - if (drawable.enabledEffects === 0) return textColor; - return EffectTransform.transformColor(drawable, textColor, effectMask); - } } module.exports = Drawable; diff --git a/src/EffectTransform.js b/src/EffectTransform.js index 7ffae18ab..33be6959e 100644 --- a/src/EffectTransform.js +++ b/src/EffectTransform.js @@ -21,103 +21,8 @@ const CENTER_X = 0.5; */ const CENTER_Y = 0.5; -/** - * Reused memory location for storing an HSV color value. - * @type {Array<number>} - */ -const __hsv = [0, 0, 0]; - class EffectTransform { - /** - * Transform a color in-place given the drawable's effect uniforms. Will apply - * Ghost and Color and Brightness effects. - * @param {Drawable} drawable The drawable to get uniforms from. - * @param {Uint8ClampedArray} inOutColor The color to transform. - * @param {number} [effectMask] A bitmask for which effects to use. Optional. - * @returns {Uint8ClampedArray} dst filled with the transformed color - */ - static transformColor (drawable, inOutColor, effectMask) { - // If the color is fully transparent, don't bother attempting any transformations. - if (inOutColor[3] === 0) { - return inOutColor; - } - - let effects = drawable.enabledEffects; - if (typeof effectMask === 'number') effects &= effectMask; - const uniforms = drawable.getUniforms(); - - const enableColor = (effects & ShaderManager.EFFECT_INFO.color.mask) !== 0; - const enableBrightness = (effects & ShaderManager.EFFECT_INFO.brightness.mask) !== 0; - - if (enableColor || enableBrightness) { - // gl_FragColor.rgb /= gl_FragColor.a + epsilon; - // Here, we're dividing by the (previously pre-multiplied) alpha to ensure HSV is properly calculated - // for partially transparent pixels. - // epsilon is present in the shader because dividing by 0 (fully transparent pixels) messes up calculations. - // We're doing this with a Uint8ClampedArray here, so dividing by 0 just gives 255. We're later multiplying - // by 0 again, so it won't affect results. - const alpha = inOutColor[3] / 255; - inOutColor[0] /= alpha; - inOutColor[1] /= alpha; - inOutColor[2] /= alpha; - - if (enableColor) { - // vec3 hsv = convertRGB2HSV(gl_FragColor.xyz); - const hsv = rgbToHsv(inOutColor, __hsv); - - // this code forces grayscale values to be slightly saturated - // so that some slight change of hue will be visible - // const float minLightness = 0.11 / 2.0; - const minV = 0.11 / 2.0; - // const float minSaturation = 0.09; - const minS = 0.09; - // if (hsv.z < minLightness) hsv = vec3(0.0, 1.0, minLightness); - if (hsv[2] < minV) { - hsv[0] = 0; - hsv[1] = 1; - hsv[2] = minV; - // else if (hsv.y < minSaturation) hsv = vec3(0.0, minSaturation, hsv.z); - } else if (hsv[1] < minS) { - hsv[0] = 0; - hsv[1] = minS; - } - - // hsv.x = mod(hsv.x + u_color, 1.0); - // if (hsv.x < 0.0) hsv.x += 1.0; - hsv[0] = (uniforms.u_color + hsv[0] + 1); - - // gl_FragColor.rgb = convertHSV2RGB(hsl); - hsvToRgb(hsv, inOutColor); - } - - if (enableBrightness) { - const brightness = uniforms.u_brightness * 255; - // gl_FragColor.rgb = clamp(gl_FragColor.rgb + vec3(u_brightness), vec3(0), vec3(1)); - // We don't need to clamp because the Uint8ClampedArray does that for us - inOutColor[0] += brightness; - inOutColor[1] += brightness; - inOutColor[2] += brightness; - } - - // gl_FragColor.rgb *= gl_FragColor.a + epsilon; - // Now we're doing the reverse, premultiplying by the alpha once again. - inOutColor[0] *= alpha; - inOutColor[1] *= alpha; - inOutColor[2] *= alpha; - } - - if ((effects & ShaderManager.EFFECT_INFO.ghost.mask) !== 0) { - // gl_FragColor *= u_ghost - inOutColor[0] *= uniforms.u_ghost; - inOutColor[1] *= uniforms.u_ghost; - inOutColor[2] *= uniforms.u_ghost; - inOutColor[3] *= uniforms.u_ghost; - } - - return inOutColor; - } - /** * Transform a texture coordinate to one that would be select after applying shader effects. * @param {Drawable} drawable The drawable whose effects to emulate. diff --git a/src/PenSkin.js b/src/PenSkin.js index c2096219a..7bcd4cd0c 100644 --- a/src/PenSkin.js +++ b/src/PenSkin.js @@ -336,7 +336,7 @@ class PenSkin extends Skin { this._silhouetteImageData.data.set(this._silhouettePixels); this._silhouette.update(this._silhouetteImageData, true /* isPremultiplied */); - this._setSilhouetteFromData(this._silhouetteImageData); + this._setSilhouetteFromData(this._silhouetteImageData, true /* isPremultiplied */); this._silhouetteDirty = false; } diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index beb1272db..fbd346cd3 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -64,21 +64,6 @@ const MASK_TOUCHING_COLOR_TOLERANCE = 2; */ const MAX_EXTRACTED_DRAWABLE_DIMENSION = 2048; -/** - * Determines if the mask color is "close enough" (only test the 6 top bits for - * each color). These bit masks are what scratch 2 used to use, so we do the same. - * @param {Uint8Array} a A color3b or color4b value. - * @param {Uint8Array} b A color3b or color4b value. - * @returns {boolean} If the colors match within the parameters. - */ -const maskMatches = (a, b) => ( - // has some non-alpha component to test against - a[3] > 0 && - (a[0] & 0b11111100) === (b[0] & 0b11111100) && - (a[1] & 0b11111100) === (b[1] & 0b11111100) && - (a[2] & 0b11111100) === (b[2] & 0b11111100) -); - /** * Determines if the given color is "close enough" (only test the 5 top bits for * red and green, 4 bits for blue). These bit masks are what scratch 2 used to use, @@ -167,7 +152,8 @@ class RenderWebGL extends EventEmitter { } /** @type {RenderWebGL.UseGpuModes} */ - this._useGpuMode = RenderWebGL.UseGpuModes.Automatic; + // this._useGpuMode = RenderWebGL.UseGpuModes.Automatic; + this._useGpuMode = RenderWebGL.UseGpuModes.ForceCPU; /** @type {Drawable[]} */ this._allDrawables = []; @@ -817,46 +803,40 @@ class RenderWebGL extends EventEmitter { this._debugCanvas.height = bounds.height; } + const candidateIDs = candidates.map(c => c.id); + // if there are just too many pixels to CPU render efficiently, we need to let readPixels happen if (bounds.width * bounds.height * (candidates.length + 1) >= maxPixelsForCPU) { - this._isTouchingColorGpuStart(drawableID, candidates.map(({id}) => id).reverse(), bounds, color3b, mask3b); + return this._isTouchingColorGpu( + drawableID, + candidateIDs.reverse(), + bounds, + color3b, + mask3b + ); } const drawable = this._allDrawables[drawableID]; - const point = __isTouchingDrawablesPoint; - const color = __touchingColor; const hasMask = Boolean(mask3b); drawable.updateCPURenderAttributes(); - // Masked drawable ignores ghost effect - const effectMask = ~ShaderManager.EFFECT_INFO.ghost.mask; - - // Scratch Space - +y is top - for (let y = bounds.bottom; y <= bounds.top; y++) { - if (bounds.width * (y - bounds.bottom) * (candidates.length + 1) >= maxPixelsForCPU) { - return this._isTouchingColorGpuFin(bounds, color3b, y - bounds.bottom); - } - for (let x = bounds.left; x <= bounds.right; x++) { - point[1] = y; - point[0] = x; - // if we use a mask, check our sample color... - if (hasMask ? - maskMatches(Drawable.sampleColor4b(point, drawable, color, effectMask), mask3b) : - drawable.isTouching(point)) { - RenderWebGL.sampleColor3b(point, candidates, color); - if (debugCanvasContext) { - debugCanvasContext.fillStyle = `rgb(${color[0]},${color[1]},${color[2]})`; - debugCanvasContext.fillRect(x - bounds.left, bounds.bottom - y, 1, 1); - } - // ...and the target color is drawn at this pixel - if (colorMatches(color, color3b, 0)) { - return true; - } - } - } + if (hasMask) { + return this.softwareRenderer.color_is_touching_color( + drawableID, + candidateIDs, + bounds, + color3b, + mask3b + ); } - return false; + + return this.softwareRenderer.is_touching_color( + drawableID, + candidateIDs, + bounds, + color3b + ); } _getMaxPixelsForCPU () { @@ -884,7 +864,7 @@ class RenderWebGL extends EventEmitter { gl.enable(gl.BLEND); } - _isTouchingColorGpuStart (drawableID, candidateIDs, bounds, color3b, mask3b) { + _isTouchingColorGpu (drawableID, candidateIDs, bounds, color3b, mask3b) { this._doExitDrawRegion(); const gl = this._gl; @@ -951,18 +931,15 @@ class RenderWebGL extends EventEmitter { gl.disable(gl.STENCIL_TEST); this._doExitDrawRegion(); } - } - _isTouchingColorGpuFin (bounds, color3b, stop) { - const gl = this._gl; - const pixels = new Uint8Array(Math.floor(bounds.width * (bounds.height - stop) * 4)); - gl.readPixels(0, 0, bounds.width, (bounds.height - stop), gl.RGBA, gl.UNSIGNED_BYTE, pixels); + const pixels = new Uint8Array(Math.floor(bounds.width * bounds.height * 4)); + gl.readPixels(0, 0, bounds.width, bounds.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels); if (this._debugCanvas) { this._debugCanvas.width = bounds.width; this._debugCanvas.height = bounds.height; const context = this._debugCanvas.getContext('2d'); - const imageData = context.getImageData(0, 0, bounds.width, bounds.height - stop); + const imageData = context.getImageData(0, 0, bounds.width, bounds.heigh); imageData.data.set(pixels); context.putImageData(imageData, 0, 0); } @@ -2057,41 +2034,6 @@ class RenderWebGL extends EventEmitter { return hull(hullPoints, Infinity); } - /** - * Sample a "final" color from an array of drawables at a given scratch space. - * Will blend any alpha values with the drawables "below" it. - * @param {twgl.v3} vec Scratch Vector Space to sample - * @param {Array<Drawables>} drawables A list of drawables with the "top most" - * drawable at index 0 - * @param {Uint8ClampedArray} dst The color3b space to store the answer in. - * @return {Uint8ClampedArray} The dst vector with everything blended down. - */ - static sampleColor3b (vec, drawables, dst) { - dst = dst || new Uint8ClampedArray(3); - dst.fill(0); - let blendAlpha = 1; - for (let index = 0; blendAlpha !== 0 && index < drawables.length; index++) { - /* - if (left > vec[0] || right < vec[0] || - bottom > vec[1] || top < vec[0]) { - continue; - } - */ - Drawable.sampleColor4b(vec, drawables[index].drawable, __blendColor); - // Equivalent to gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA) - dst[0] += __blendColor[0] * blendAlpha; - dst[1] += __blendColor[1] * blendAlpha; - dst[2] += __blendColor[2] * blendAlpha; - blendAlpha *= (1 - (__blendColor[3] / 255)); - } - // Backdrop could be transparent, so we need to go to the "clear color" of the - // draw scene (white) as a fallback if everything was alpha - dst[0] += blendAlpha * 255; - dst[1] += blendAlpha * 255; - dst[2] += blendAlpha * 255; - return dst; - } - /** * @callback RenderWebGL#snapshotCallback * @param {string} dataURI Data URI of the snapshot of the renderer diff --git a/src/ShaderManager.js b/src/ShaderManager.js index 40822356a..b7881539b 100644 --- a/src/ShaderManager.js +++ b/src/ShaderManager.js @@ -90,7 +90,8 @@ ShaderManager.EFFECT_INFO = { color: { uniformName: 'u_color', mask: 1 << 0, - converter: x => (x / 200) % 1, + // ensure modulo (and hence hue shift) is kept positive + converter: x => (((x / 200) % 1) + 1) % 1, shapeChanges: false }, /** Fisheye effect */ diff --git a/src/Silhouette.js b/src/Silhouette.js index b96348a81..615ecda34 100644 --- a/src/Silhouette.js +++ b/src/Silhouette.js @@ -10,11 +10,6 @@ */ let __SilhouetteUpdateCanvas; -// Optimized Math.min and Math.max for integers; -// taken from https://web.archive.org/web/20190716181049/http://guihaire.com/code/?p=549 -const intMin = (i, j) => j ^ ((i ^ j) & ((i - j) >> 31)); -const intMax = (i, j) => i ^ ((i ^ j) & ((i - j) >> 31)); - /** * Internal helper function (in hopes that compiler can inline). Get a pixel * from silhouette data, or 0 if outside it's bounds. @@ -42,57 +37,6 @@ const __cornerWork = [ new Uint8ClampedArray(4) ]; -/** - * Get the color from a given silhouette at an x/y local texture position. - * Multiply color values by alpha for proper blending. - * @param {Silhouette} $0 The silhouette to sample. - * @param {number} x X position of texture [0, width). - * @param {number} y Y position of texture [0, height). - * @param {Uint8ClampedArray} dst A color 4b space. - * @return {Uint8ClampedArray} The dst vector. - */ -const getColor4b = ({_width: width, _height: height, _colorData: data}, x, y, dst) => { - // Clamp coords to edge, matching GL_CLAMP_TO_EDGE. - // (See github.com/LLK/scratch-render/blob/954cfff02b08069a082cbedd415c1fecd9b1e4fb/src/BitmapSkin.js#L88) - x = intMax(0, intMin(x, width - 1)); - y = intMax(0, intMin(y, height - 1)); - - // 0 if outside bounds, otherwise read from data. - if (x >= width || y >= height || x < 0 || y < 0) { - return dst.fill(0); - } - const offset = ((y * width) + x) * 4; - // premultiply alpha - const alpha = data[offset + 3] / 255; - dst[0] = data[offset] * alpha; - dst[1] = data[offset + 1] * alpha; - dst[2] = data[offset + 2] * alpha; - dst[3] = data[offset + 3]; - return dst; -}; - -/** - * Get the color from a given silhouette at an x/y local texture position. - * Do not multiply color values by alpha, as it has already been done. - * @param {Silhouette} $0 The silhouette to sample. - * @param {number} x X position of texture [0, width). - * @param {number} y Y position of texture [0, height). - * @param {Uint8ClampedArray} dst A color 4b space. - * @return {Uint8ClampedArray} The dst vector. - */ -const getPremultipliedColor4b = ({_width: width, _height: height, _colorData: data}, x, y, dst) => { - // Clamp coords to edge, matching GL_CLAMP_TO_EDGE. - x = intMax(0, intMin(x, width - 1)); - y = intMax(0, intMin(y, height - 1)); - - const offset = ((y * width) + x) * 4; - dst[0] = data[offset]; - dst[1] = data[offset + 1]; - dst[2] = data[offset + 2]; - dst[3] = data[offset + 3]; - return dst; -}; - class Silhouette { constructor () { /** @@ -112,13 +56,6 @@ class Silhouette { * @type {Uint8ClampedArray} */ this._colorData = null; - - // By default, silhouettes are assumed not to contain premultiplied image data, - // so when we get a color, we want to multiply it by its alpha channel. - // Point `_getColor` to the version of the function that multiplies. - this._getColor = getColor4b; - - this.colorAtNearest = this.colorAtLinear = (_, dst) => dst.fill(0); } /** @@ -127,7 +64,7 @@ class Silhouette { * @param {boolean} isPremultiplied True if the source bitmap data comes premultiplied (e.g. from readPixels). * rendering can be queried from. */ - update (bitmapData, isPremultiplied = false) { + update (bitmapData) { let imageData; if (bitmapData instanceof ImageData) { // If handed ImageData directly, use it directly. @@ -150,65 +87,7 @@ class Silhouette { imageData = ctx.getImageData(0, 0, width, height); } - if (isPremultiplied) { - this._getColor = getPremultipliedColor4b; - } else { - this._getColor = getColor4b; - } - this._colorData = imageData.data; - // delete our custom overriden "uninitalized" color functions - // let the prototype work for itself - delete this.colorAtNearest; - delete this.colorAtLinear; - } - - /** - * Sample a color from the silhouette at a given local position using - * "nearest neighbor" - * @param {twgl.v3} vec [x,y] texture space (0-1) - * @param {Uint8ClampedArray} dst The memory buffer to store the value in. (4 bytes) - * @returns {Uint8ClampedArray} dst - */ - colorAtNearest (vec, dst) { - return this._getColor( - this, - Math.floor(vec[0] * (this._width - 1)), - Math.floor(vec[1] * (this._height - 1)), - dst - ); - } - - /** - * Sample a color from the silhouette at a given local position using - * "linear interpolation" - * @param {twgl.v3} vec [x,y] texture space (0-1) - * @param {Uint8ClampedArray} dst The memory buffer to store the value in. (4 bytes) - * @returns {Uint8ClampedArray} dst - */ - colorAtLinear (vec, dst) { - const x = vec[0] * (this._width - 1); - const y = vec[1] * (this._height - 1); - - const x1D = x % 1; - const y1D = y % 1; - const x0D = 1 - x1D; - const y0D = 1 - y1D; - - const xFloor = Math.floor(x); - const yFloor = Math.floor(y); - - const x0y0 = this._getColor(this, xFloor, yFloor, __cornerWork[0]); - const x1y0 = this._getColor(this, xFloor + 1, yFloor, __cornerWork[1]); - const x0y1 = this._getColor(this, xFloor, yFloor + 1, __cornerWork[2]); - const x1y1 = this._getColor(this, xFloor + 1, yFloor + 1, __cornerWork[3]); - - dst[0] = (x0y0[0] * x0D * y0D) + (x0y1[0] * x0D * y1D) + (x1y0[0] * x1D * y0D) + (x1y1[0] * x1D * y1D); - dst[1] = (x0y0[1] * x0D * y0D) + (x0y1[1] * x0D * y1D) + (x1y0[1] * x1D * y0D) + (x1y1[1] * x1D * y1D); - dst[2] = (x0y0[2] * x0D * y0D) + (x0y1[2] * x0D * y1D) + (x1y0[2] * x1D * y0D) + (x1y1[2] * x1D * y1D); - dst[3] = (x0y0[3] * x0D * y0D) + (x0y1[3] * x0D * y1D) + (x1y0[3] * x1D * y0D) + (x1y1[3] * x1D * y1D); - - return dst; } /** diff --git a/src/Skin.js b/src/Skin.js index 3ceb60802..4f95a3cc2 100644 --- a/src/Skin.js +++ b/src/Skin.js @@ -53,7 +53,7 @@ class Skin extends EventEmitter { */ this._silhouette = new Silhouette(); - renderer.softwareRenderer.set_silhouette(id, 0, 0, new Uint8Array(0)); + renderer.softwareRenderer.set_silhouette(id, 0, 0, new Uint8Array(0), 1, 1, true); this.setMaxListeners(RenderConstants.SKIN_SHARE_SOFT_LIMIT); } @@ -201,7 +201,7 @@ class Skin extends EventEmitter { this.emit(Skin.Events.WasAltered); } - _setSilhouetteFromData (data) { + _setSilhouetteFromData (data, premultiplied = false) { const size = this.size; this._renderer.softwareRenderer.set_silhouette( this._id, @@ -210,7 +210,9 @@ class Skin extends EventEmitter { data.data, size[0], - size[1] + size[1], + + premultiplied ); } diff --git a/swrender/src/drawable.rs b/swrender/src/drawable.rs index f7e04ef89..eaa2a6ae6 100644 --- a/swrender/src/drawable.rs +++ b/swrender/src/drawable.rs @@ -1,16 +1,17 @@ use crate::silhouette::*; use crate::matrix::*; -use crate::effect_transform::{Effects, EffectBits, transform_point, DISTORTION_EFFECT_MASK}; +use crate::effect_transform::{Effects, EffectBits, transform_point, DISTORTION_EFFECT_MASK, transform_color, COLOR_EFFECT_MASK}; pub type DrawableID = u32; pub struct Drawable { + pub id: DrawableID, pub matrix: Mat4, pub inverse_matrix: Mat4, pub silhouette: SilhouetteID, - pub id: DrawableID, pub effects: Effects, - pub effect_bits: EffectBits + pub effect_bits: EffectBits, + pub use_nearest_neighbor: bool } impl Drawable { @@ -32,7 +33,7 @@ impl Drawable { if (self.effect_bits & DISTORTION_EFFECT_MASK) == 0 { vec } else { - transform_point(vec, &self.effects, &self.effect_bits, skin_size) + transform_point(vec, &self.effects, self.effect_bits, skin_size) } } @@ -43,6 +44,33 @@ impl Drawable { return false; } let local_position = self.get_transformed_position(local_position, silhouette.nominal_size); - silhouette.get_point((local_position.0 * silhouette.width as f32) as i32, (local_position.1 * silhouette.height as f32) as i32) + + if self.use_nearest_neighbor { + silhouette.is_touching_nearest(local_position) + } else { + silhouette.is_touching_linear(local_position) + } + } + + #[inline(always)] + pub fn sample_color<'a>(&self, position: Vec2, silhouette: &'a Silhouette) -> [u8; 4] { + let local_position = self.get_local_position(position); + if local_position.0 < 0f32 || local_position.0 >= 1f32 || local_position.1 < 0f32 || local_position.1 >= 1f32 { + return [0, 0, 0, 0]; + } + let local_position = self.get_transformed_position(local_position, silhouette.nominal_size); + + // TODO: linear sampling + let color = if self.use_nearest_neighbor { + silhouette.color_at_nearest(local_position) + } else { + silhouette.color_at_nearest(local_position) + }; + + if (self.effect_bits & COLOR_EFFECT_MASK) == 0 { + color + } else { + transform_color(color, &self.effects, self.effect_bits) + } } } diff --git a/swrender/src/effect_transform.rs b/swrender/src/effect_transform.rs index 8ef23d6ef..3daadfc4b 100644 --- a/swrender/src/effect_transform.rs +++ b/swrender/src/effect_transform.rs @@ -1,5 +1,6 @@ use crate::matrix::*; +use std::f32; use wasm_bindgen::prelude::*; #[wasm_bindgen] @@ -44,6 +45,11 @@ pub enum EffectBitfield { Ghost = 6, } +pub const COLOR_EFFECT_MASK: EffectBits = + 1 << (EffectBitfield::Color as u32) | + 1 << (EffectBitfield::Brightness as u32) | + 1 << (EffectBitfield::Ghost as u32); + pub const DISTORTION_EFFECT_MASK: EffectBits = 1 << (EffectBitfield::Fisheye as u32) | 1 << (EffectBitfield::Whirl as u32) | @@ -62,9 +68,138 @@ impl Effects { } } +fn rgb_to_hsv(r: f32, g: f32, b: f32) -> (f32, f32, f32) { + let mut r = r; + let mut g = g; + let mut b = b; + + let mut tmp: f32; + + let mut k = 0f32; + + if g < b { + tmp = g; + g = b; + b = tmp; + k = -1f32; + } + + if r < g { + tmp = g; + g = r; + r = tmp; + k = (-2f32 / 6f32) - k; + } + + let chroma = r - f32::min(g, b); + + let h = f32::abs(k + (g - b) / (6f32 * chroma + f32::EPSILON)); + let s = chroma / (r + f32::EPSILON); + let v = r; + + (h, s, v) +} + +fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (f32, f32, f32) { + if s < 1e-18 { + return (v, v, v); + } + + let i = (h * 6f32).floor(); + let f = (h * 6f32) - i; + let p = v * (1f32 - s); + let q = v * (1f32 - (s * f)); + let t = v * (1f32 - (s * (1f32 - f))); + + match i as u32 { + 0 => (v, t, p), + 1 => (q, v, p), + 2 => (p, v, t), + 3 => (p, q, v), + 4 => (t, p, v), + 5 => (v, p, q), + _ => unreachable!() + } +} + +pub fn transform_color<'a>(color: [u8; 4], effects: &Effects, effect_bits: EffectBits) -> [u8; 4] { + const COLOR_DIVISOR: f32 = 1f32 / 255f32; + let mut rgba: [f32; 4] = [ + (color[0] as f32) * COLOR_DIVISOR, + (color[1] as f32) * COLOR_DIVISOR, + (color[2] as f32) * COLOR_DIVISOR, + (color[3] as f32) * COLOR_DIVISOR + ]; + + let enable_color = effect_bits & (1 << (EffectBitfield::Color as u32)) != 0; + let enable_brightness = effect_bits & (1 << (EffectBitfield::Brightness as u32)) != 0; + + if enable_brightness || enable_color { + let alpha = rgba[3] + f32::EPSILON; + rgba[0] /= alpha; + rgba[1] /= alpha; + rgba[2] /= alpha; + + if enable_color { + /*vec3 hsv = convertRGB2HSV(gl_FragColor.xyz); + + // this code forces grayscale values to be slightly saturated + // so that some slight change of hue will be visible + const float minLightness = 0.11 / 2.0; + const float minSaturation = 0.09; + if (hsv.z < minLightness) hsv = vec3(0.0, 1.0, minLightness); + else if (hsv.y < minSaturation) hsv = vec3(0.0, minSaturation, hsv.z); + + hsv.x = mod(hsv.x + u_color, 1.0); + if (hsv.x < 0.0) hsv.x += 1.0; + + gl_FragColor.rgb = convertHSV2RGB(hsv);*/ + + let (mut h, mut s, mut v) = rgb_to_hsv(rgba[0], rgba[1], rgba[2]); + + const MIN_LIGHTNESS: f32 = 0.11 / 2f32; + const MIN_SATURATION: f32 = 0.09; + + if v < MIN_LIGHTNESS { + v = MIN_LIGHTNESS + } else if s < MIN_SATURATION { + s = MIN_SATURATION + } + + h = f32::fract(h + effects.color); + + let (r, g, b) = hsv_to_rgb(h, s, v); + rgba[0] = r; + rgba[1] = g; + rgba[2] = b; + } + + if enable_brightness { + // gl_FragColor.rgb = clamp(gl_FragColor.rgb + vec3(u_brightness), vec3(0), vec3(1)); + rgba[0] = (rgba[0] + effects.brightness).min(1f32).max(0f32); + rgba[1] = (rgba[1] + effects.brightness).min(1f32).max(0f32); + rgba[2] = (rgba[2] + effects.brightness).min(1f32).max(0f32); + } + + rgba[0] *= alpha; + rgba[1] *= alpha; + rgba[2] *= alpha; + } + + // gl_FragColor *= u_ghost + if effect_bits & (1 << (EffectBitfield::Ghost as u32)) != 0 { + rgba[0] *= effects.ghost; + rgba[1] *= effects.ghost; + rgba[2] *= effects.ghost; + rgba[3] *= effects.ghost; + } + + [(rgba[0] * 255f32) as u8, (rgba[1] * 255f32) as u8, (rgba[2] * 255f32) as u8, (rgba[3] * 255f32) as u8] +} + const CENTER: Vec2 = Vec2(0.5, 0.5); -pub fn transform_point(point: Vec2, effects: &Effects, effect_bits: &EffectBits, skin_size: Vec2) -> Vec2 { +pub fn transform_point(point: Vec2, effects: &Effects, effect_bits: EffectBits, skin_size: Vec2) -> Vec2 { let mut out = point; if effect_bits & (1 << (EffectBitfield::Mosaic as u32)) != 0 { diff --git a/swrender/src/lib.rs b/swrender/src/lib.rs index b3f29594e..a68bc96c8 100644 --- a/swrender/src/lib.rs +++ b/swrender/src/lib.rs @@ -1,5 +1,4 @@ mod utils; -mod rectangle; mod matrix; mod effect_transform; pub mod silhouette; @@ -12,6 +11,20 @@ use std::convert::TryInto; use matrix::Matrix; +#[wasm_bindgen] +extern { + pub type JSRectangle; + + #[wasm_bindgen(method, getter)] + pub fn left(this: &JSRectangle) -> f64; + #[wasm_bindgen(method, getter)] + pub fn right(this: &JSRectangle) -> f64; + #[wasm_bindgen(method, getter)] + pub fn bottom(this: &JSRectangle) -> f64; + #[wasm_bindgen(method, getter)] + pub fn top(this: &JSRectangle) -> f64; +} + // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global // allocator. #[cfg(feature = "wee_alloc")] @@ -46,7 +59,8 @@ impl SoftwareRenderer { matrix: Option<Box<[f32]>>, silhouette: Option<silhouette::SilhouetteID>, effects: Option<effect_transform::JSEffectMap>, - effect_bits: effect_transform::EffectBits + effect_bits: effect_transform::EffectBits, + use_nearest_neighbor: bool ) { let d = self.drawables.entry(id).or_insert(drawable::Drawable { matrix: [0.0; 16], @@ -57,6 +71,7 @@ impl SoftwareRenderer { Some(s) => s, None => ID_NONE }, + use_nearest_neighbor, id }); @@ -71,6 +86,7 @@ impl SoftwareRenderer { d.effects.set_from_js(fx); } d.effect_bits = effect_bits; + d.use_nearest_neighbor = use_nearest_neighbor; } pub fn remove_drawable(&mut self, id: drawable::DrawableID) { @@ -83,24 +99,44 @@ impl SoftwareRenderer { w: u32, h: u32, data: Box<[u8]>, - nominal_width: - f64, nominal_height: f64 + nominal_width: f64, + nominal_height: f64, + premultiplied: bool, ) { let s = self.silhouettes.entry(id).or_insert(silhouette::Silhouette::new(id)); - s.set_data(w, h, data, matrix::Vec2(nominal_width as f32, nominal_height as f32)); + s.set_data(w, h, data, matrix::Vec2(nominal_width as f32, nominal_height as f32), premultiplied); } pub fn remove_silhouette(&mut self, id: silhouette::SilhouetteID) { self.silhouettes.remove(&id); } - pub fn is_touching_drawables( - &mut self, + fn map_candidates( + &self, + candidates: Vec<drawable::DrawableID> + ) -> Vec<(&drawable::Drawable, &silhouette::Silhouette)> { + candidates.into_iter() + .map(|c| { + let d = self.drawables.get(&c).expect("Candidate drawable should exist"); + let s = self.silhouettes.get(&d.silhouette).unwrap(); + (d, s) + }).collect() + } + + fn per_rect_pixel<F>( + &self, + func: F, + rect: JSRectangle, drawable: drawable::DrawableID, - candidates: - Vec<drawable::DrawableID>, - rect: rectangle::JSRectangle - ) -> bool { + candidates: Vec<drawable::DrawableID> + ) -> bool + where F: Fn( + matrix::Vec2, + &drawable::Drawable, + &silhouette::Silhouette, + &Vec<(&drawable::Drawable, &silhouette::Silhouette)> + ) -> bool { + let left = rect.left() as i32; let right = rect.right() as i32 + 1; let bottom = rect.bottom() as i32 - 1; @@ -108,26 +144,144 @@ impl SoftwareRenderer { let drawable = self.drawables.get(&drawable).expect("Drawable should exist"); let silhouette = self.silhouettes.get(&drawable.silhouette).unwrap(); - let candidates: Vec<(&drawable::Drawable, &silhouette::Silhouette)> = candidates.into_iter() - .map(|c| { - let d = self.drawables.get(&c).expect("Candidate drawable should exist"); - let s = self.silhouettes.get(&d.silhouette).unwrap(); - (d, s) - }).collect(); - - for x in left..right { - for y in bottom..top { + let candidates = self.map_candidates(candidates); + + for y in bottom..top { + for x in left..right { let position = matrix::Vec2(x as f32, y as f32); - if drawable.is_touching(position, silhouette) { - for candidate in &candidates { - if candidate.0.is_touching(position, candidate.1) { - return true; - } - } + if func(position, drawable, silhouette, &candidates) { + return true; } } } false } + + pub fn is_touching_drawables( + &mut self, + drawable: drawable::DrawableID, + candidates: Vec<drawable::DrawableID>, + rect: JSRectangle + ) -> bool { + self.per_rect_pixel(| + position, + drawable, + silhouette, + candidates + | { + if drawable.is_touching(position, silhouette) { + for candidate in candidates { + if candidate.0.is_touching(position, candidate.1) { + return true; + } + } + } + false + }, rect, drawable, candidates) + } + + #[inline(always)] + fn color_matches( + a: [u8; 3], + b: [u8; 3] + ) -> bool { + ( + ((a[0] ^ b[0]) & 0b11111000) | + ((a[1] ^ b[1]) & 0b11111000) | + ((a[2] ^ b[2]) & 0b11110000) + ) == 0 + } + + #[inline(always)] + fn mask_matches( + a: [u8; 4], + b: [u8; 3] + ) -> bool { + a[3] != 0 && + ( + ((a[0] ^ b[0]) & 0b11111100) | + ((a[1] ^ b[1]) & 0b11111100) | + ((a[2] ^ b[2]) & 0b11111100) + ) == 0 + } + + pub fn color_is_touching_color( + &mut self, + drawable: drawable::DrawableID, + candidates: Vec<drawable::DrawableID>, + rect: JSRectangle, + color: &[u8], + mask: &[u8] + ) -> bool { + let color: [u8; 3] = (*color).try_into().expect("color contains 3 elements"); + let mask: [u8; 3] = (*mask).try_into().expect("mask contains 3 elements"); + + self.per_rect_pixel(| + position, + drawable, + silhouette, + candidates + | { + if Self::mask_matches(drawable.sample_color(position, silhouette), mask) { + let sample_color = self.sample_color(position, &candidates); + if Self::color_matches(color, sample_color) { + return true; + } + } + false + }, rect, drawable, candidates) + } + + pub fn is_touching_color( + &mut self, + drawable: drawable::DrawableID, + candidates: Vec<drawable::DrawableID>, + rect: JSRectangle, + color: &[u8] + ) -> bool { + let color: [u8; 3] = (*color).try_into().expect("color contains 3 elements"); + self.per_rect_pixel(| + position, + drawable, + silhouette, + candidates + | { + if drawable.is_touching(position, silhouette) { + let sample_color = self.sample_color(position, &candidates); + if Self::color_matches(color, sample_color) { + return true; + } + } + false + }, rect, drawable, candidates) + } + + fn sample_color( + &self, + position: matrix::Vec2, + candidates: &Vec<(&drawable::Drawable, &silhouette::Silhouette)> + ) -> [u8; 3] { + let mut dst_color: (f32, f32, f32, f32) = (0f32, 0f32, 0f32, 0f32); + let mut blend_alpha = 1f32; + + for candidate in candidates.into_iter() { + let col = candidate.0.sample_color(position, candidate.1); + dst_color.0 += (col[0] as f32) * blend_alpha; + dst_color.1 += (col[1] as f32) * blend_alpha; + dst_color.2 += (col[2] as f32) * blend_alpha; + blend_alpha *= 1f32 - (col[3] as f32 / 255f32); + + if blend_alpha == 0f32 { + break; + } + } + + let alpha8 = blend_alpha * 255f32; + dst_color.0 += alpha8; + dst_color.1 += alpha8; + dst_color.2 += alpha8; + + [dst_color.0 as u8, dst_color.1 as u8, dst_color.2 as u8] + } } diff --git a/swrender/src/rectangle.rs b/swrender/src/rectangle.rs deleted file mode 100644 index e4ad40b23..000000000 --- a/swrender/src/rectangle.rs +++ /dev/null @@ -1,33 +0,0 @@ -use wasm_bindgen::prelude::*; - -#[wasm_bindgen] -extern { - pub type JSRectangle; - - #[wasm_bindgen(method, getter)] - pub fn left(this: &JSRectangle) -> f64; - #[wasm_bindgen(method, getter)] - pub fn right(this: &JSRectangle) -> f64; - #[wasm_bindgen(method, getter)] - pub fn bottom(this: &JSRectangle) -> f64; - #[wasm_bindgen(method, getter)] - pub fn top(this: &JSRectangle) -> f64; -} - -pub struct Rectangle<T> { - left: T, - right: T, - bottom: T, - top: T -} - -impl Rectangle<i32> { - pub fn fromJSRectangle(rect: JSRectangle) -> Self { - Rectangle { - left: rect.left().floor() as i32, - right: rect.right().ceil() as i32, - bottom: rect.bottom().floor() as i32, - top: rect.top().ceil() as i32 - } - } -} diff --git a/swrender/src/silhouette.rs b/swrender/src/silhouette.rs index d089ec667..772cf4285 100644 --- a/swrender/src/silhouette.rs +++ b/swrender/src/silhouette.rs @@ -2,13 +2,28 @@ use crate::matrix::Vec2; pub type SilhouetteID = u32; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +extern { + #[wasm_bindgen(js_namespace = console)] + pub fn time(s: &str); + + #[wasm_bindgen(js_namespace = console)] + pub fn timeEnd(s: &str); + + #[wasm_bindgen(js_namespace = console)] + pub fn log(s: &str); +} + + pub struct Silhouette { pub id: SilhouetteID, pub width: u32, pub height: u32, pub nominal_size: Vec2, data: Box<[u8]>, - _blank: Box<[u8; 4]> + _blank: [u8; 4] } impl Silhouette { @@ -19,17 +34,34 @@ impl Silhouette { height: 0, nominal_size: Vec2(0f32, 0f32), data: Box::new([0, 0, 0, 0]), - _blank: Box::new([0, 0, 0, 0]) + _blank: [0, 0, 0, 0] } } - pub fn set_data(&mut self, w: u32, h: u32, data: Box<[u8]>, nominal_size: Vec2) { + pub fn set_data(&mut self, w: u32, h: u32, mut data: Box<[u8]>, nominal_size: Vec2, premultiplied: bool) { assert_eq!(data.len(), (w * h * 4) as usize, "silhouette data is improperly sized"); self.width = w; self.height = h; - self.data = data; self.nominal_size = nominal_size; + + if !premultiplied { + let pixels = (*data).chunks_mut(4); + + for pixel in pixels { + // This is indeed one branch per pixel. However, the branch predictor does a pretty good job of + // eliminating branch overhead and this saves us several instructions per pixel. + if pixel[3] == 0u8 {continue} + + let alpha = (pixel[3] as f32) / 255f32; + + pixel[0] = ((pixel[0] as f32) * alpha) as u8; + pixel[1] = ((pixel[1] as f32) * alpha) as u8; + pixel[2] = ((pixel[2] as f32) * alpha) as u8; + } + } + + self.data = data; } pub fn get_point(&self, x: i32, y: i32) -> bool { @@ -41,12 +73,30 @@ impl Silhouette { } } - pub fn get_color(&self, x: i32, y: i32) -> &[u8] { + pub fn get_color(&self, x: i32, y: i32) -> [u8; 4] { if x < 0 || y < 0 || (x as u32) >= self.width || (y as u32) >= self.height { - &self._blank[0..4] + self._blank } else { let idx = (((y as u32 * self.width) + x as u32) * 4) as usize; - &self.data[idx..idx+4] + [self.data[idx], self.data[idx + 1], self.data[idx + 2], self.data[idx + 3]] } } + + pub fn is_touching_nearest(&self, vec: Vec2) -> bool { + self.get_point((vec.0 * self.width as f32) as i32, (vec.1 * self.height as f32) as i32) + } + + pub fn color_at_nearest(&self, vec: Vec2) -> [u8; 4] { + self.get_color((vec.0 * self.width as f32) as i32, (vec.1 * self.height as f32) as i32) + } + + pub fn is_touching_linear(&self, vec: Vec2) -> bool { + let x = ((vec.0 * self.width as f32) - 0.5) as i32; + let y = ((vec.1 * self.height as f32) - 0.5) as i32; + + self.get_point(x, y) || + self.get_point(x + 1, y) || + self.get_point(x, y + 1) || + self.get_point(x + 1, y + 1) + } } From 71523fe95f12eff92896cf9b25f0f2df47ad2596 Mon Sep 17 00:00:00 2001 From: adroitwhiz <adroitwhiz@protonmail.com> Date: Sat, 21 Mar 2020 23:57:18 -0400 Subject: [PATCH 05/14] Add pick + drawableTouching --- src/Drawable.js | 95 +------------------------------------- src/RenderWebGL.js | 50 ++------------------ swrender/src/drawable.rs | 4 +- swrender/src/lib.rs | 93 ++++++++++++++++++++++++++++++------- swrender/src/silhouette.rs | 2 +- 5 files changed, 83 insertions(+), 161 deletions(-) diff --git a/src/Drawable.js b/src/Drawable.js index 18de2a8e2..965ad5143 100644 --- a/src/Drawable.js +++ b/src/Drawable.js @@ -4,55 +4,6 @@ const Rectangle = require('./Rectangle'); const RenderConstants = require('./RenderConstants'); const ShaderManager = require('./ShaderManager'); const Skin = require('./Skin'); -const EffectTransform = require('./EffectTransform'); -const log = require('./util/log'); - -/** - * An internal workspace for calculating texture locations from world vectors - * this is REUSED for memory conservation reasons - * @type {twgl.v3} - */ -const __isTouchingPosition = twgl.v3.create(); -const FLOATING_POINT_ERROR_ALLOWANCE = 1e-6; - -/** - * Convert a scratch space location into a texture space float. Uses the - * internal __isTouchingPosition as a return value, so this should be copied - * if you ever need to get two local positions and store both. Requires that - * the drawable inverseMatrix is up to date. - * - * @param {Drawable} drawable The drawable to get the inverse matrix and uniforms from - * @param {twgl.v3} vec [x,y] scratch space vector - * @return {twgl.v3} [x,y] texture space float vector - transformed by effects and matrix - */ -const getLocalPosition = (drawable, vec) => { - // Transfrom from world coordinates to Drawable coordinates. - const localPosition = __isTouchingPosition; - const v0 = vec[0]; - const v1 = vec[1]; - const m = drawable._inverseMatrix; - // var v2 = v[2]; - const d = (v0 * m[3]) + (v1 * m[7]) + m[15]; - // The RenderWebGL quad flips the texture's X axis. So rendered bottom - // left is 1, 0 and the top right is 0, 1. Flip the X axis so - // localPosition matches that transformation. - localPosition[0] = 0.5 - (((v0 * m[0]) + (v1 * m[4]) + m[12]) / d); - localPosition[1] = (((v0 * m[1]) + (v1 * m[5]) + m[13]) / d) + 0.5; - // Fix floating point issues near 0. Filed https://github.com/LLK/scratch-render/issues/688 that - // they're happening in the first place. - // TODO: Check if this can be removed after render pull 479 is merged - if (Math.abs(localPosition[0]) < FLOATING_POINT_ERROR_ALLOWANCE) localPosition[0] = 0; - if (Math.abs(localPosition[1]) < FLOATING_POINT_ERROR_ALLOWANCE) localPosition[1] = 0; - // Apply texture effect transform if the localPosition is within the drawable's space, - // and any effects are currently active. - if (drawable.enabledEffects !== 0 && - (localPosition[0] >= 0 && localPosition[0] < 1) && - (localPosition[1] >= 0 && localPosition[1] < 1)) { - - EffectTransform.transformPoint(drawable, localPosition, localPosition); - } - return localPosition; -}; class Drawable { /** @@ -129,8 +80,6 @@ class Drawable { this._transformedHullDirty = true; this._skinWasAltered = this._skinWasAltered.bind(this); - - this.isTouching = this._isTouchingNever; } /** @@ -485,36 +434,6 @@ class Drawable { this._transformedHullDirty = true; } - /** - * @function - * @name isTouching - * Check if the world position touches the skin. - * The caller is responsible for ensuring this drawable's inverse matrix & its skin's silhouette are up-to-date. - * @see updateCPURenderAttributes - * @param {twgl.v3} vec World coordinate vector. - * @return {boolean} True if the world position touches the skin. - */ - - // `updateCPURenderAttributes` sets this Drawable instance's `isTouching` method - // to one of the following three functions: - // If this drawable has no skin, set it to `_isTouchingNever`. - // Otherwise, if this drawable uses nearest-neighbor scaling at its current scale, set it to `_isTouchingNearest`. - // Otherwise, set it to `_isTouchingLinear`. - // This allows several checks to be moved from the `isTouching` function to `updateCPURenderAttributes`. - - // eslint-disable-next-line no-unused-vars - _isTouchingNever (vec) { - return false; - } - - _isTouchingNearest (vec) { - return this.skin.isTouchingNearest(getLocalPosition(this, vec)); - } - - _isTouchingLinear (vec) { - return this.skin.isTouchingLinear(getLocalPosition(this, vec)); - } - /** * Get the precise bounds for a Drawable. * This function applies the transform matrix to the known convex hull, @@ -652,20 +571,8 @@ class Drawable { */ updateCPURenderAttributes () { this.updateMatrix(); - // CPU rendering always occurs at the "native" size, so no need to scale up this._scale - if (this.skin) { - this.skin.updateSilhouette(this._scale); - - if (this.skin.useNearest(this._scale, this)) { - this.isTouching = this._isTouchingNearest; - } else { - this.isTouching = this._isTouchingLinear; - } - } else { - log.warn(`Could not find skin for drawable with id: ${this._id}`); - this.isTouching = this._isTouchingNever; - } + if (this.skin) this.skin.updateSilhouette(this._scale); let effects = null; if (this._effectsDirty) { diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index fbd346cd3..7dc7c71b7 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -29,11 +29,8 @@ swrenderInit(wasm) if (onLoadSwRender) onLoadSwRender(); }); -const __isTouchingDrawablesPoint = twgl.v3.create(); const __candidatesBounds = new Rectangle(); const __fenceBounds = new Rectangle(); -const __touchingColor = new Uint8ClampedArray(4); -const __blendColor = new Uint8ClampedArray(4); // More pixels than this and we give up to the GPU and take the cost of readPixels // Width * Height * Number of drawables at location @@ -1032,18 +1029,9 @@ class RenderWebGL extends EventEmitter { return false; } const bounds = this.clientSpaceToScratchBounds(centerX, centerY, touchWidth, touchHeight); - const worldPos = twgl.v3.create(); - drawable.updateCPURenderAttributes(); - for (worldPos[1] = bounds.bottom; worldPos[1] <= bounds.top; worldPos[1]++) { - for (worldPos[0] = bounds.left; worldPos[0] <= bounds.right; worldPos[0]++) { - if (drawable.isTouching(worldPos)) { - return true; - } - } - } - return false; + return this.softwareRenderer.drawable_touching_rect(drawableID, bounds); } /** @@ -1078,43 +1066,12 @@ class RenderWebGL extends EventEmitter { return true; } return false; - }); + }).reverse(); if (candidateIDs.length === 0) { return false; } - const hits = []; - const worldPos = twgl.v3.create(0, 0, 0); - // Iterate over the scratch pixels and check if any candidate can be - // touched at that point. - for (worldPos[1] = bounds.bottom; worldPos[1] <= bounds.top; worldPos[1]++) { - for (worldPos[0] = bounds.left; worldPos[0] <= bounds.right; worldPos[0]++) { - - // Check candidates in the reverse order they would have been - // drawn. This will determine what candiate's silhouette pixel - // would have been drawn at the point. - for (let d = candidateIDs.length - 1; d >= 0; d--) { - const id = candidateIDs[d]; - const drawable = this._allDrawables[id]; - if (drawable.isTouching(worldPos)) { - hits[id] = (hits[id] || 0) + 1; - break; - } - } - } - } - - // Bias toward selecting anything over nothing - hits[RenderConstants.ID_NONE] = 0; - - let hit = RenderConstants.ID_NONE; - for (const hitID in hits) { - if (Object.prototype.hasOwnProperty.call(hits, hitID) && (hits[hitID] > hits[hit])) { - hit = hitID; - } - } - - return Number(hit); + return this.softwareRenderer.pick(candidateIDs, bounds); } /** @@ -1441,7 +1398,6 @@ class RenderWebGL extends EventEmitter { if (bounds === null) { return result; } - bounds.snapToInt(); // iterate through the drawables list BACKWARDS - we want the top most item to be the first we check for (let index = candidateIDs.length - 1; index >= 0; index--) { const id = candidateIDs[index]; diff --git a/swrender/src/drawable.rs b/swrender/src/drawable.rs index eaa2a6ae6..805ea81c8 100644 --- a/swrender/src/drawable.rs +++ b/swrender/src/drawable.rs @@ -2,7 +2,7 @@ use crate::silhouette::*; use crate::matrix::*; use crate::effect_transform::{Effects, EffectBits, transform_point, DISTORTION_EFFECT_MASK, transform_color, COLOR_EFFECT_MASK}; -pub type DrawableID = u32; +pub type DrawableID = i32; pub struct Drawable { pub id: DrawableID, @@ -16,7 +16,7 @@ pub struct Drawable { impl Drawable { pub fn get_local_position(&self, vec: Vec2) -> Vec2 { - let v0 = vec.0 - 0.5; + let v0 = vec.0 + 0.5; let v1 = vec.1 + 0.5; let m = self.inverse_matrix; let d = (v0 * m[3]) + (v1 * m[7]) + m[15]; diff --git a/swrender/src/lib.rs b/swrender/src/lib.rs index a68bc96c8..778f1473d 100644 --- a/swrender/src/lib.rs +++ b/swrender/src/lib.rs @@ -31,7 +31,7 @@ extern { #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; -const ID_NONE: u32 = u32::max_value(); +const ID_NONE: drawable::DrawableID = -1; #[wasm_bindgen] pub struct SoftwareRenderer { @@ -127,14 +127,12 @@ impl SoftwareRenderer { &self, func: F, rect: JSRectangle, - drawable: drawable::DrawableID, - candidates: Vec<drawable::DrawableID> + drawable: drawable::DrawableID ) -> bool where F: Fn( matrix::Vec2, &drawable::Drawable, - &silhouette::Silhouette, - &Vec<(&drawable::Drawable, &silhouette::Silhouette)> + &silhouette::Silhouette ) -> bool { let left = rect.left() as i32; @@ -144,12 +142,11 @@ impl SoftwareRenderer { let drawable = self.drawables.get(&drawable).expect("Drawable should exist"); let silhouette = self.silhouettes.get(&drawable.silhouette).unwrap(); - let candidates = self.map_candidates(candidates); for y in bottom..top { for x in left..right { let position = matrix::Vec2(x as f32, y as f32); - if func(position, drawable, silhouette, &candidates) { + if func(position, drawable, silhouette) { return true; } } @@ -164,21 +161,21 @@ impl SoftwareRenderer { candidates: Vec<drawable::DrawableID>, rect: JSRectangle ) -> bool { + let candidates = self.map_candidates(candidates); self.per_rect_pixel(| position, drawable, - silhouette, - candidates + silhouette | { if drawable.is_touching(position, silhouette) { - for candidate in candidates { + for candidate in &candidates { if candidate.0.is_touching(position, candidate.1) { return true; } } } false - }, rect, drawable, candidates) + }, rect, drawable) } #[inline(always)] @@ -216,12 +213,12 @@ impl SoftwareRenderer { ) -> bool { let color: [u8; 3] = (*color).try_into().expect("color contains 3 elements"); let mask: [u8; 3] = (*mask).try_into().expect("mask contains 3 elements"); + let candidates = self.map_candidates(candidates); self.per_rect_pixel(| position, drawable, - silhouette, - candidates + silhouette | { if Self::mask_matches(drawable.sample_color(position, silhouette), mask) { let sample_color = self.sample_color(position, &candidates); @@ -230,7 +227,7 @@ impl SoftwareRenderer { } } false - }, rect, drawable, candidates) + }, rect, drawable) } pub fn is_touching_color( @@ -241,11 +238,11 @@ impl SoftwareRenderer { color: &[u8] ) -> bool { let color: [u8; 3] = (*color).try_into().expect("color contains 3 elements"); + let candidates = self.map_candidates(candidates); self.per_rect_pixel(| position, drawable, - silhouette, - candidates + silhouette | { if drawable.is_touching(position, silhouette) { let sample_color = self.sample_color(position, &candidates); @@ -254,7 +251,7 @@ impl SoftwareRenderer { } } false - }, rect, drawable, candidates) + }, rect, drawable) } fn sample_color( @@ -284,4 +281,66 @@ impl SoftwareRenderer { [dst_color.0 as u8, dst_color.1 as u8, dst_color.2 as u8] } + + pub fn drawable_touching_rect( + &mut self, + drawable: drawable::DrawableID, + rect: JSRectangle + ) -> bool { + self.per_rect_pixel(| + position, + drawable, + silhouette + | { + if drawable.is_touching(position, silhouette) { + return true; + } + false + }, rect, drawable) + } + + pub fn pick( + &mut self, + candidates: Vec<drawable::DrawableID>, + rect: JSRectangle + ) -> drawable::DrawableID { + let mut hits: HashMap<drawable::DrawableID, u32> = HashMap::new(); + hits.insert(ID_NONE, 0); + + let candidates = self.map_candidates(candidates); + + // TODO: deduplicate with per_rect_pixel + let left = rect.left() as i32; + let right = rect.right() as i32 + 1; + let bottom = rect.bottom() as i32 - 1; + let top = rect.top() as i32; + + for y in bottom..top { + for x in left..right { + let position = matrix::Vec2(x as f32, y as f32); + for candidate in &candidates { + if candidate.0.is_touching(position, candidate.1) { + hits + .entry(candidate.0.id) + .and_modify(|hit| {*hit += 1}) + .or_insert(1); + + break; + } + } + } + } + + let mut hit: drawable::DrawableID = ID_NONE; + let mut highest_hits: u32 = 0; + + for (id, num_hits) in hits.iter() { + if *num_hits > highest_hits { + hit = *id; + highest_hits = *num_hits; + } + } + + hit + } } diff --git a/swrender/src/silhouette.rs b/swrender/src/silhouette.rs index 772cf4285..29faa2943 100644 --- a/swrender/src/silhouette.rs +++ b/swrender/src/silhouette.rs @@ -1,6 +1,6 @@ use crate::matrix::Vec2; -pub type SilhouetteID = u32; +pub type SilhouetteID = i32; use wasm_bindgen::prelude::*; From dafd370092e269cf9f56348e9bcc7db205a8f44a Mon Sep 17 00:00:00 2001 From: adroitwhiz <adroitwhiz@protonmail.com> Date: Sun, 22 Mar 2020 02:50:00 -0400 Subject: [PATCH 06/14] Add convex hull --- src/Drawable.js | 27 +++++-- src/EffectTransform.js | 102 --------------------------- src/PenSkin.js | 4 +- src/RenderWebGL.js | 129 +++------------------------------- src/SVGSkin.js | 1 - src/Silhouette.js | 136 ------------------------------------ src/Skin.js | 41 ++++------- swrender/src/convex_hull.rs | 88 +++++++++++++++++++++++ swrender/src/lib.rs | 17 +++++ 9 files changed, 147 insertions(+), 398 deletions(-) delete mode 100644 src/EffectTransform.js delete mode 100644 src/Silhouette.js create mode 100644 swrender/src/convex_hull.rs diff --git a/src/Drawable.js b/src/Drawable.js index 965ad5143..84b5840c8 100644 --- a/src/Drawable.js +++ b/src/Drawable.js @@ -80,6 +80,7 @@ class Drawable { this._transformedHullDirty = true; this._skinWasAltered = this._skinWasAltered.bind(this); + this._silhouetteWasUpdated = this._silhouetteWasUpdated.bind(this); } /** @@ -122,10 +123,12 @@ class Drawable { if (this._skin !== newSkin) { if (this._skin) { this._skin.removeListener(Skin.Events.WasAltered, this._skinWasAltered); + this._skin.removeListener(Skin.Events.SilhouetteUpdated, this._silhouetteWasUpdated); } this._skin = newSkin; if (this._skin) { this._skin.addListener(Skin.Events.WasAltered, this._skinWasAltered); + this._skin.addListener(Skin.Events.SilhouetteUpdated, this._silhouetteWasUpdated); } this._skinWasAltered(); } @@ -453,6 +456,14 @@ class Drawable { // Search through transformed points to generate box on axes. result = result || new Rectangle(); result.initFromPointsAABB(transformedHullPoints); + + // Expand bounds by half a pixel per side because convex hull points lie in the centers of pixels + const silhouetteHalfPixel = (this.scale[0] / 200) * (this.skin.size[0] / this.skin.silhouetteSize[0]); + result.left -= silhouetteHalfPixel; + result.right += silhouetteHalfPixel; + result.bottom -= silhouetteHalfPixel; + result.top += silhouetteHalfPixel; + return result; } @@ -527,16 +538,12 @@ class Drawable { } const projection = twgl.m4.ortho(-1, 1, -1, 1, -1, 1); - const skinSize = this.skin.size; - const halfXPixel = 1 / skinSize[0] / 2; - const halfYPixel = 1 / skinSize[1] / 2; const tm = twgl.m4.multiply(this._uniforms.u_modelMatrix, projection); for (let i = 0; i < this._convexHullPoints.length; i++) { const point = this._convexHullPoints[i]; const dstPoint = this._transformedHullPoints[i]; - - dstPoint[0] = 0.5 + (-point[0] / skinSize[0]) - halfXPixel; - dstPoint[1] = (point[1] / skinSize[1]) - 0.5 + halfYPixel; + dstPoint[0] = 0.5 - point[0]; + dstPoint[1] = point[1] - 0.5; twgl.m4.transformPoint(tm, dstPoint, dstPoint); } @@ -601,6 +608,14 @@ class Drawable { this.setTransformDirty(); } + /** + * Respond to an internal change in the current Skin's silhouette. + * @private + */ + _silhouetteWasUpdated () { + this.setConvexHullDirty(); + } + /** * Calculate a color to represent the given ID number. At least one component of * the resulting color will be non-zero if the ID is not RenderConstants.ID_NONE. diff --git a/src/EffectTransform.js b/src/EffectTransform.js deleted file mode 100644 index 33be6959e..000000000 --- a/src/EffectTransform.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * @fileoverview - * A utility to transform a texture coordinate to another texture coordinate - * representing how the shaders apply effects. - */ - -const twgl = require('twgl.js'); - -const {rgbToHsv, hsvToRgb} = require('./util/color-conversions'); -const ShaderManager = require('./ShaderManager'); - -/** - * A texture coordinate is between 0 and 1. 0.5 is the center position. - * @const {number} - */ -const CENTER_X = 0.5; - -/** - * A texture coordinate is between 0 and 1. 0.5 is the center position. - * @const {number} - */ -const CENTER_Y = 0.5; - -class EffectTransform { - - /** - * Transform a texture coordinate to one that would be select after applying shader effects. - * @param {Drawable} drawable The drawable whose effects to emulate. - * @param {twgl.v3} vec The texture coordinate to transform. - * @param {twgl.v3} dst A place to store the output coordinate. - * @return {twgl.v3} dst - The coordinate after being transform by effects. - */ - static transformPoint (drawable, vec, dst) { - twgl.v3.copy(vec, dst); - - const effects = drawable.enabledEffects; - const uniforms = drawable.getUniforms(); - if ((effects & ShaderManager.EFFECT_INFO.mosaic.mask) !== 0) { - // texcoord0 = fract(u_mosaic * texcoord0); - dst[0] = uniforms.u_mosaic * dst[0] % 1; - dst[1] = uniforms.u_mosaic * dst[1] % 1; - } - if ((effects & ShaderManager.EFFECT_INFO.pixelate.mask) !== 0) { - const skinUniforms = drawable.skin.getUniforms(); - // vec2 pixelTexelSize = u_skinSize / u_pixelate; - const texelX = skinUniforms.u_skinSize[0] / uniforms.u_pixelate; - const texelY = skinUniforms.u_skinSize[1] / uniforms.u_pixelate; - // texcoord0 = (floor(texcoord0 * pixelTexelSize) + kCenter) / - // pixelTexelSize; - dst[0] = (Math.floor(dst[0] * texelX) + CENTER_X) / texelX; - dst[1] = (Math.floor(dst[1] * texelY) + CENTER_Y) / texelY; - } - if ((effects & ShaderManager.EFFECT_INFO.whirl.mask) !== 0) { - // const float kRadius = 0.5; - const RADIUS = 0.5; - // vec2 offset = texcoord0 - kCenter; - const offsetX = dst[0] - CENTER_X; - const offsetY = dst[1] - CENTER_Y; - // float offsetMagnitude = length(offset); - const offsetMagnitude = Math.sqrt(Math.pow(offsetX, 2) + Math.pow(offsetY, 2)); - // float whirlFactor = max(1.0 - (offsetMagnitude / kRadius), 0.0); - const whirlFactor = Math.max(1.0 - (offsetMagnitude / RADIUS), 0.0); - // float whirlActual = u_whirl * whirlFactor * whirlFactor; - const whirlActual = uniforms.u_whirl * whirlFactor * whirlFactor; - // float sinWhirl = sin(whirlActual); - const sinWhirl = Math.sin(whirlActual); - // float cosWhirl = cos(whirlActual); - const cosWhirl = Math.cos(whirlActual); - // mat2 rotationMatrix = mat2( - // cosWhirl, -sinWhirl, - // sinWhirl, cosWhirl - // ); - const rot1 = cosWhirl; - const rot2 = -sinWhirl; - const rot3 = sinWhirl; - const rot4 = cosWhirl; - - // texcoord0 = rotationMatrix * offset + kCenter; - dst[0] = (rot1 * offsetX) + (rot3 * offsetY) + CENTER_X; - dst[1] = (rot2 * offsetX) + (rot4 * offsetY) + CENTER_Y; - } - if ((effects & ShaderManager.EFFECT_INFO.fisheye.mask) !== 0) { - // vec2 vec = (texcoord0 - kCenter) / kCenter; - const vX = (dst[0] - CENTER_X) / CENTER_X; - const vY = (dst[1] - CENTER_Y) / CENTER_Y; - // float vecLength = length(vec); - const vLength = Math.sqrt((vX * vX) + (vY * vY)); - // float r = pow(min(vecLength, 1.0), u_fisheye) * max(1.0, vecLength); - const r = Math.pow(Math.min(vLength, 1), uniforms.u_fisheye) * Math.max(1, vLength); - // vec2 unit = vec / vecLength; - const unitX = vX / vLength; - const unitY = vY / vLength; - // texcoord0 = kCenter + r * unit * kCenter; - dst[0] = CENTER_X + (r * unitX * CENTER_X); - dst[1] = CENTER_Y + (r * unitY * CENTER_Y); - } - - return dst; - } -} - -module.exports = EffectTransform; diff --git a/src/PenSkin.js b/src/PenSkin.js index 7bcd4cd0c..5e5fbd998 100644 --- a/src/PenSkin.js +++ b/src/PenSkin.js @@ -334,9 +334,7 @@ class PenSkin extends Skin { ); this._silhouetteImageData.data.set(this._silhouettePixels); - this._silhouette.update(this._silhouetteImageData, true /* isPremultiplied */); - - this._setSilhouetteFromData(this._silhouetteImageData, true /* isPremultiplied */); + this._setSilhouetteFromData(this._silhouetteImageData, true /* premultiplied */); this._silhouetteDirty = false; } diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index 7dc7c71b7..6e0b31711 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -1,6 +1,5 @@ const EventEmitter = require('events'); -const hull = require('hull.js'); const twgl = require('twgl.js'); const BitmapSkin = require('./BitmapSkin'); @@ -11,7 +10,6 @@ const RenderConstants = require('./RenderConstants'); const ShaderManager = require('./ShaderManager'); const SVGSkin = require('./SVGSkin'); const TextBubbleSkin = require('./TextBubbleSkin'); -const EffectTransform = require('./EffectTransform'); const log = require('./util/log'); let onLoadSwRender = null; @@ -1864,130 +1862,19 @@ class RenderWebGL extends EventEmitter { */ _getConvexHullPointsForDrawable (drawableID) { const drawable = this._allDrawables[drawableID]; - - const [width, height] = drawable.skin.size; - // No points in the hull if invisible or size is 0. - if (!drawable.getVisible() || width === 0 || height === 0) { - return []; - } - drawable.updateCPURenderAttributes(); + const pointValues = this.softwareRenderer.drawable_convex_hull_points(drawableID); - /** - * Return the determinant of two vectors, the vector from A to B and the vector from A to C. - * - * The determinant is useful in this case to know if AC is counter-clockwise from AB. - * A positive value means that AC is counter-clockwise from AB. A negative value means AC is clockwise from AB. - * - * @param {Float32Array} A A 2d vector in space. - * @param {Float32Array} B A 2d vector in space. - * @param {Float32Array} C A 2d vector in space. - * @return {number} Greater than 0 if counter clockwise, less than if clockwise, 0 if all points are on a line. - */ - const determinant = function (A, B, C) { - // AB = B - A - // AC = C - A - // det (AB BC) = AB0 * AC1 - AB1 * AC0 - return (((B[0] - A[0]) * (C[1] - A[1])) - ((B[1] - A[1]) * (C[0] - A[0]))); - }; - - // This algorithm for calculating the convex hull somewhat resembles the monotone chain algorithm. - // The main difference is that instead of sorting the points by x-coordinate, and y-coordinate in case of ties, - // it goes through them by y-coordinate in the outer loop and x-coordinate in the inner loop. - // This gives us "left" and "right" hulls, whereas the monotone chain algorithm gives "top" and "bottom" hulls. - // Adapted from https://github.com/LLK/scratch-flash/blob/dcbeeb59d44c3be911545dfe54d46a32404f8e69/src/scratch/ScratchCostume.as#L369-L413 - - const leftHull = []; - const rightHull = []; - - // While convex hull algorithms usually push and pop values from the list of hull points, - // here, we keep indices for the "last" point in each array. Any points past these indices are ignored. - // This is functionally equivalent to pushing and popping from a "stack" of hull points. - let leftEndPointIndex = -1; - let rightEndPointIndex = -1; - - const _pixelPos = twgl.v3.create(); - const _effectPos = twgl.v3.create(); - - let currentPoint; - - // *Not* Scratch Space-- +y is bottom - // Loop over all rows of pixels, starting at the top - for (let y = 0; y < height; y++) { - _pixelPos[1] = y / height; - - // We start at the leftmost point, then go rightwards until we hit an opaque pixel - let x = 0; - for (; x < width; x++) { - _pixelPos[0] = x / width; - EffectTransform.transformPoint(drawable, _pixelPos, _effectPos); - if (drawable.skin.isTouchingLinear(_effectPos)) { - currentPoint = [x, y]; - break; - } - } - - // If we managed to loop all the way through, there are no opaque pixels on this row. Go to the next one - if (x >= width) { - continue; - } - - // Because leftEndPointIndex is initialized to -1, this is skipped for the first two rows. - // It runs only when there are enough points in the left hull to make at least one line. - // If appending the current point to the left hull makes a counter-clockwise turn, - // we want to append the current point. Otherwise, we decrement the index of the "last" hull point until the - // current point makes a counter-clockwise turn. - // This decrementing has the same effect as popping from the point list, but is hopefully faster. - while (leftEndPointIndex > 0) { - if (determinant(leftHull[leftEndPointIndex], leftHull[leftEndPointIndex - 1], currentPoint) > 0) { - break; - } else { - // leftHull.pop(); - --leftEndPointIndex; - } - } - - // This has the same effect as pushing to the point list. - // This "list head pointer" coding style leaves excess points dangling at the end of the list, - // but that doesn't matter; we simply won't copy them over to the final hull. - - // leftHull.push(currentPoint); - leftHull[++leftEndPointIndex] = currentPoint; - - // Now we repeat the process for the right side, looking leftwards for a pixel. - for (x = width - 1; x >= 0; x--) { - _pixelPos[0] = x / width; - EffectTransform.transformPoint(drawable, _pixelPos, _effectPos); - if (drawable.skin.isTouchingLinear(_effectPos)) { - currentPoint = [x, y]; - break; - } - } - - // Because we're coming at this from the right, it goes clockwise this time. - while (rightEndPointIndex > 0) { - if (determinant(rightHull[rightEndPointIndex], rightHull[rightEndPointIndex - 1], currentPoint) < 0) { - break; - } else { - --rightEndPointIndex; - } - } - - rightHull[++rightEndPointIndex] = currentPoint; - } + const points = []; - // Start off "hullPoints" with the left hull points. - const hullPoints = leftHull; - // This is where we get rid of those dangling extra points. - hullPoints.length = leftEndPointIndex + 1; - // Add points from the right side in reverse order so all points are ordered clockwise. - for (let j = rightEndPointIndex; j >= 0; --j) { - hullPoints.push(rightHull[j]); + for (let i = 0; i < pointValues.length; i += 2) { + const point = new Float32Array(2); + point[0] = pointValues[i]; + point[1] = pointValues[i + 1]; + points.push(point); } - // Simplify boundary points using hull.js. - // TODO: Remove this; this algorithm already generates convex hulls. - return hull(hullPoints, Infinity); + return points; } /** diff --git a/src/SVGSkin.js b/src/SVGSkin.js index 3d3ec5234..8cd92367a 100644 --- a/src/SVGSkin.js +++ b/src/SVGSkin.js @@ -115,7 +115,6 @@ class SVGSkin extends Skin { // Check if this is the largest MIP created so far. Currently, silhouettes only get scaled up. if (this._largestMIPScale < scale) { - this._silhouette.update(textureData); this._setSilhouetteFromData(textureData); this._largestMIPScale = scale; } diff --git a/src/Silhouette.js b/src/Silhouette.js deleted file mode 100644 index 615ecda34..000000000 --- a/src/Silhouette.js +++ /dev/null @@ -1,136 +0,0 @@ -/** - * @fileoverview - * A representation of a Skin's silhouette that can test if a point on the skin - * renders a pixel where it is drawn. - */ - -/** - * <canvas> element used to update Silhouette data from skin bitmap data. - * @type {CanvasElement} - */ -let __SilhouetteUpdateCanvas; - -/** - * Internal helper function (in hopes that compiler can inline). Get a pixel - * from silhouette data, or 0 if outside it's bounds. - * @private - * @param {Silhouette} silhouette - has data width and height - * @param {number} x - x - * @param {number} y - y - * @return {number} Alpha value for x/y position - */ -const getPoint = ({_width: width, _height: height, _colorData: data}, x, y) => { - // 0 if outside bounds, otherwise read from data. - if (x >= width || y >= height || x < 0 || y < 0) { - return 0; - } - return data[(((y * width) + x) * 4) + 3]; -}; - -/** - * Memory buffers for doing 4 corner sampling for linear interpolation - */ -const __cornerWork = [ - new Uint8ClampedArray(4), - new Uint8ClampedArray(4), - new Uint8ClampedArray(4), - new Uint8ClampedArray(4) -]; - -class Silhouette { - constructor () { - /** - * The width of the data representing the current skin data. - * @type {number} - */ - this._width = 0; - - /** - * The height of the data representing the current skin date. - * @type {number} - */ - this._height = 0; - - /** - * The data representing a skin's silhouette shape. - * @type {Uint8ClampedArray} - */ - this._colorData = null; - } - - /** - * Update this silhouette with the bitmapData for a skin. - * @param {ImageData|HTMLCanvasElement|HTMLImageElement} bitmapData An image, canvas or other element that the skin - * @param {boolean} isPremultiplied True if the source bitmap data comes premultiplied (e.g. from readPixels). - * rendering can be queried from. - */ - update (bitmapData) { - let imageData; - if (bitmapData instanceof ImageData) { - // If handed ImageData directly, use it directly. - imageData = bitmapData; - this._width = bitmapData.width; - this._height = bitmapData.height; - } else { - // Draw about anything else to our update canvas and poll image data - // from that. - const canvas = Silhouette._updateCanvas(); - const width = this._width = canvas.width = bitmapData.width; - const height = this._height = canvas.height = bitmapData.height; - const ctx = canvas.getContext('2d'); - - if (!(width && height)) { - return; - } - ctx.clearRect(0, 0, width, height); - ctx.drawImage(bitmapData, 0, 0, width, height); - imageData = ctx.getImageData(0, 0, width, height); - } - - this._colorData = imageData.data; - } - - /** - * Test if texture coordinate touches the silhouette using nearest neighbor. - * @param {twgl.v3} vec A texture coordinate. - * @return {boolean} If the nearest pixel has an alpha value. - */ - isTouchingNearest (vec) { - if (!this._colorData) return; - return getPoint( - this, - Math.floor(vec[0] * (this._width - 1)), - Math.floor(vec[1] * (this._height - 1)) - ) > 0; - } - - /** - * Test to see if any of the 4 pixels used in the linear interpolate touch - * the silhouette. - * @param {twgl.v3} vec A texture coordinate. - * @return {boolean} Any of the pixels have some alpha. - */ - isTouchingLinear (vec) { - if (!this._colorData) return; - const x = Math.floor(vec[0] * (this._width - 1)); - const y = Math.floor(vec[1] * (this._height - 1)); - return getPoint(this, x, y) > 0 || - getPoint(this, x + 1, y) > 0 || - getPoint(this, x, y + 1) > 0 || - getPoint(this, x + 1, y + 1) > 0; - } - - /** - * Get the canvas element reused by Silhouettes to update their data with. - * @private - * @return {CanvasElement} A canvas to draw bitmap data to. - */ - static _updateCanvas () { - if (typeof __SilhouetteUpdateCanvas === 'undefined') { - __SilhouetteUpdateCanvas = document.createElement('canvas'); - } - return __SilhouetteUpdateCanvas; - } -} - -module.exports = Silhouette; diff --git a/src/Skin.js b/src/Skin.js index 4f95a3cc2..7916d9142 100644 --- a/src/Skin.js +++ b/src/Skin.js @@ -3,7 +3,6 @@ const EventEmitter = require('events'); const twgl = require('twgl.js'); const RenderConstants = require('./RenderConstants'); -const Silhouette = require('./Silhouette'); class Skin extends EventEmitter { /** @@ -51,7 +50,8 @@ class Skin extends EventEmitter { * A silhouette to store touching data, skins are responsible for keeping it up to date. * @private */ - this._silhouette = new Silhouette(); + + this.silhouetteSize = [0, 0]; renderer.softwareRenderer.set_silhouette(id, 0, 0, new Uint8Array(0), 1, 1, true); @@ -161,7 +161,6 @@ class Skin extends EventEmitter { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureData); gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); - this._silhouette.update(textureData); this._setSilhouetteFromData(textureData); } @@ -196,7 +195,6 @@ class Skin extends EventEmitter { this._rotationCenter[0] = 0; this._rotationCenter[1] = 0; - this._silhouette.update(this._emptyImageData); this._setSilhouetteFromData(this._emptyImageData); this.emit(Skin.Events.WasAltered); } @@ -214,32 +212,11 @@ class Skin extends EventEmitter { premultiplied ); - } - /** - * Does this point touch an opaque or translucent point on this skin? - * Nearest Neighbor version - * The caller is responsible for ensuring this skin's silhouette is up-to-date. - * @see updateSilhouette - * @see Drawable.updateCPURenderAttributes - * @param {twgl.v3} vec A texture coordinate. - * @return {boolean} Did it touch? - */ - isTouchingNearest (vec) { - return this._silhouette.isTouchingNearest(vec); - } + this.silhouetteSize[0] = data.width; + this.silhouetteSize[1] = data.height; - /** - * Does this point touch an opaque or translucent point on this skin? - * Linear Interpolation version - * The caller is responsible for ensuring this skin's silhouette is up-to-date. - * @see updateSilhouette - * @see Drawable.updateCPURenderAttributes - * @param {twgl.v3} vec A texture coordinate. - * @return {boolean} Did it touch? - */ - isTouchingLinear (vec) { - return this._silhouette.isTouchingLinear(vec); + this.emit(Skin.Events.SilhouetteUpdated); } } @@ -253,7 +230,13 @@ Skin.Events = { * Emitted when anything about the Skin has been altered, such as the appearance or rotation center. * @event Skin.event:WasAltered */ - WasAltered: 'WasAltered' + WasAltered: 'WasAltered', + + /** + * Emitted whenever this skin's silhouette changes. + * @event Skin.event:SilhouetteUpdated + */ + SilhouetteUpdated: 'SilhouetteUpdated' }; module.exports = Skin; diff --git a/swrender/src/convex_hull.rs b/swrender/src/convex_hull.rs new file mode 100644 index 000000000..d07410762 --- /dev/null +++ b/swrender/src/convex_hull.rs @@ -0,0 +1,88 @@ +use crate::silhouette::Silhouette; +use crate::drawable::Drawable; +use crate::matrix::Vec2; + +use crate::effect_transform::transform_point; + +/// Return the determinant of two vector, the vector from A to B and the vector from A to C. +/// +/// The determinant is useful in this case to know if AC is counter-clockwise from AB. +/// A positive value means that AC is counter-clockwise from AB. A negative value means AC is clockwise from AB. +fn determinant(a: Vec2, b: Vec2, c: Vec2) -> f32 { + ((b.0 - a.0) * (c.1 - a.1)) - ((b.1 - a.1) * (c.0 - a.0)) +} + +pub fn calculate_drawable_convex_hull(drawable: &Drawable, silhouette: &Silhouette) -> Vec<Vec2> { + let mut left_hull: Vec<Vec2> = Vec::new(); + let mut right_hull: Vec<Vec2> = Vec::new(); + + let transform = |p| transform_point( + p, + &drawable.effects, + drawable.effect_bits, + silhouette.nominal_size + ); + + let mut current_point = Vec2(0f32, 0f32); + + for y in 0..silhouette.height { + let mut x: u32 = 0; + while x < silhouette.width { + let local_point = Vec2((x as f32 + 0.5) / silhouette.width as f32, (y as f32 + 0.5) / silhouette.height as f32); + let point = transform(local_point); + + if silhouette.is_touching_nearest(point) { + current_point = local_point; + break; + } + + x += 1; + } + + if x >= silhouette.width { + continue; + } + + while left_hull.len() >= 2 { + let len = left_hull.len(); + if determinant(left_hull[len - 1], left_hull[len - 2], current_point) > 0f32 { + break; + } else { + left_hull.pop(); + } + } + + left_hull.push(Vec2(current_point.0 as f32, current_point.1 as f32)); + + x = silhouette.width - 1; + + while x != 0 { + let local_point = Vec2((x as f32 + 0.5) / silhouette.width as f32, (y as f32 + 0.5) / silhouette.height as f32); + let point = transform(local_point); + + if silhouette.is_touching_nearest(point) { + current_point = local_point; + break; + } + + x -= 1; + } + + while right_hull.len() >= 2 { + let len = right_hull.len(); + if determinant(right_hull[len - 1], right_hull[len - 2], current_point) < 0f32 { + break; + } else { + right_hull.pop(); + } + } + + right_hull.push(Vec2(current_point.0 as f32, current_point.1 as f32)); + } + + right_hull.reverse(); + + left_hull.append(&mut right_hull); + + left_hull +} diff --git a/swrender/src/lib.rs b/swrender/src/lib.rs index 778f1473d..50ddac14b 100644 --- a/swrender/src/lib.rs +++ b/swrender/src/lib.rs @@ -1,6 +1,7 @@ mod utils; mod matrix; mod effect_transform; +mod convex_hull; pub mod silhouette; pub mod drawable; @@ -343,4 +344,20 @@ impl SoftwareRenderer { hit } + + pub fn drawable_convex_hull_points(&mut self, drawable: drawable::DrawableID) -> Vec<f32> { + let drawable = self.drawables.get(&drawable).expect("Drawable should exist"); + let silhouette = self.silhouettes.get(&drawable.silhouette).unwrap(); + + let hull = convex_hull::calculate_drawable_convex_hull(drawable, silhouette); + + let mut points: Vec<f32> = Vec::new(); + + for point in hull { + points.push(point.0); + points.push(point.1); + } + + points + } } From f6b195021f6fc48215489b5e77ce39347d778c72 Mon Sep 17 00:00:00 2001 From: adroitwhiz <adroitwhiz@protonmail.com> Date: Sun, 22 Mar 2020 03:37:34 -0400 Subject: [PATCH 07/14] Reformat Rust code --- swrender/rustfmt.toml | 1 + swrender/src/console_log.rs | 15 --- swrender/src/convex_hull.rs | 26 ++-- swrender/src/drawable.rs | 21 ++- swrender/src/effect_transform.rs | 38 +++--- swrender/src/lib.rs | 219 ++++++++++++++++--------------- swrender/src/matrix.rs | 106 ++++++++------- swrender/src/silhouette.rs | 51 +++++-- 8 files changed, 265 insertions(+), 212 deletions(-) create mode 100644 swrender/rustfmt.toml delete mode 100644 swrender/src/console_log.rs diff --git a/swrender/rustfmt.toml b/swrender/rustfmt.toml new file mode 100644 index 000000000..8dc0f76e3 --- /dev/null +++ b/swrender/rustfmt.toml @@ -0,0 +1 @@ +force_explicit_abi = false diff --git a/swrender/src/console_log.rs b/swrender/src/console_log.rs deleted file mode 100644 index 7ade76788..000000000 --- a/swrender/src/console_log.rs +++ /dev/null @@ -1,15 +0,0 @@ -use wasm_bindgen::prelude::*; - -#[wasm_bindgen] -extern { - #[wasm_bindgen(js_namespace = console)] - pub fn log(s: &str); -} - -#[macro_use] -mod console_log { - #[macro_export] - macro_rules! console_log { - ($($t:tt)*) => (log(&format_args!($($t)*).to_string())) - } -} diff --git a/swrender/src/convex_hull.rs b/swrender/src/convex_hull.rs index d07410762..b3683f3db 100644 --- a/swrender/src/convex_hull.rs +++ b/swrender/src/convex_hull.rs @@ -1,6 +1,6 @@ -use crate::silhouette::Silhouette; use crate::drawable::Drawable; use crate::matrix::Vec2; +use crate::silhouette::Silhouette; use crate::effect_transform::transform_point; @@ -16,19 +16,24 @@ pub fn calculate_drawable_convex_hull(drawable: &Drawable, silhouette: &Silhouet let mut left_hull: Vec<Vec2> = Vec::new(); let mut right_hull: Vec<Vec2> = Vec::new(); - let transform = |p| transform_point( - p, - &drawable.effects, - drawable.effect_bits, - silhouette.nominal_size - ); + let transform = |p| { + transform_point( + p, + &drawable.effects, + drawable.effect_bits, + silhouette.nominal_size, + ) + }; let mut current_point = Vec2(0f32, 0f32); for y in 0..silhouette.height { let mut x: u32 = 0; while x < silhouette.width { - let local_point = Vec2((x as f32 + 0.5) / silhouette.width as f32, (y as f32 + 0.5) / silhouette.height as f32); + let local_point = Vec2( + (x as f32 + 0.5) / silhouette.width as f32, + (y as f32 + 0.5) / silhouette.height as f32, + ); let point = transform(local_point); if silhouette.is_touching_nearest(point) { @@ -57,7 +62,10 @@ pub fn calculate_drawable_convex_hull(drawable: &Drawable, silhouette: &Silhouet x = silhouette.width - 1; while x != 0 { - let local_point = Vec2((x as f32 + 0.5) / silhouette.width as f32, (y as f32 + 0.5) / silhouette.height as f32); + let local_point = Vec2( + (x as f32 + 0.5) / silhouette.width as f32, + (y as f32 + 0.5) / silhouette.height as f32, + ); let point = transform(local_point); if silhouette.is_touching_nearest(point) { diff --git a/swrender/src/drawable.rs b/swrender/src/drawable.rs index 805ea81c8..757dab633 100644 --- a/swrender/src/drawable.rs +++ b/swrender/src/drawable.rs @@ -1,6 +1,9 @@ -use crate::silhouette::*; +use crate::effect_transform::{ + transform_color, transform_point, EffectBits, Effects, COLOR_EFFECT_MASK, + DISTORTION_EFFECT_MASK, +}; use crate::matrix::*; -use crate::effect_transform::{Effects, EffectBits, transform_point, DISTORTION_EFFECT_MASK, transform_color, COLOR_EFFECT_MASK}; +use crate::silhouette::*; pub type DrawableID = i32; @@ -11,7 +14,7 @@ pub struct Drawable { pub silhouette: SilhouetteID, pub effects: Effects, pub effect_bits: EffectBits, - pub use_nearest_neighbor: bool + pub use_nearest_neighbor: bool, } impl Drawable { @@ -40,7 +43,11 @@ impl Drawable { #[inline(always)] pub fn is_touching(&self, position: Vec2, silhouette: &Silhouette) -> bool { let local_position = self.get_local_position(position); - if local_position.0 < 0f32 || local_position.0 >= 1f32 || local_position.1 < 0f32 || local_position.1 >= 1f32 { + if local_position.0 < 0f32 + || local_position.0 >= 1f32 + || local_position.1 < 0f32 + || local_position.1 >= 1f32 + { return false; } let local_position = self.get_transformed_position(local_position, silhouette.nominal_size); @@ -55,7 +62,11 @@ impl Drawable { #[inline(always)] pub fn sample_color<'a>(&self, position: Vec2, silhouette: &'a Silhouette) -> [u8; 4] { let local_position = self.get_local_position(position); - if local_position.0 < 0f32 || local_position.0 >= 1f32 || local_position.1 < 0f32 || local_position.1 >= 1f32 { + if local_position.0 < 0f32 + || local_position.0 >= 1f32 + || local_position.1 < 0f32 + || local_position.1 >= 1f32 + { return [0, 0, 0, 0]; } let local_position = self.get_transformed_position(local_position, silhouette.nominal_size); diff --git a/swrender/src/effect_transform.rs b/swrender/src/effect_transform.rs index 3daadfc4b..ebb55935f 100644 --- a/swrender/src/effect_transform.rs +++ b/swrender/src/effect_transform.rs @@ -45,16 +45,14 @@ pub enum EffectBitfield { Ghost = 6, } -pub const COLOR_EFFECT_MASK: EffectBits = - 1 << (EffectBitfield::Color as u32) | - 1 << (EffectBitfield::Brightness as u32) | - 1 << (EffectBitfield::Ghost as u32); +pub const COLOR_EFFECT_MASK: EffectBits = 1 << (EffectBitfield::Color as u32) + | 1 << (EffectBitfield::Brightness as u32) + | 1 << (EffectBitfield::Ghost as u32); -pub const DISTORTION_EFFECT_MASK: EffectBits = - 1 << (EffectBitfield::Fisheye as u32) | - 1 << (EffectBitfield::Whirl as u32) | - 1 << (EffectBitfield::Pixelate as u32) | - 1 << (EffectBitfield::Mosaic as u32); +pub const DISTORTION_EFFECT_MASK: EffectBits = 1 << (EffectBitfield::Fisheye as u32) + | 1 << (EffectBitfield::Whirl as u32) + | 1 << (EffectBitfield::Pixelate as u32) + | 1 << (EffectBitfield::Mosaic as u32); impl Effects { pub fn set_from_js(&mut self, effects: JSEffectMap) { @@ -118,7 +116,7 @@ fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (f32, f32, f32) { 3 => (p, q, v), 4 => (t, p, v), 5 => (v, p, q), - _ => unreachable!() + _ => unreachable!(), } } @@ -128,7 +126,7 @@ pub fn transform_color<'a>(color: [u8; 4], effects: &Effects, effect_bits: Effec (color[0] as f32) * COLOR_DIVISOR, (color[1] as f32) * COLOR_DIVISOR, (color[2] as f32) * COLOR_DIVISOR, - (color[3] as f32) * COLOR_DIVISOR + (color[3] as f32) * COLOR_DIVISOR, ]; let enable_color = effect_bits & (1 << (EffectBitfield::Color as u32)) != 0; @@ -194,19 +192,29 @@ pub fn transform_color<'a>(color: [u8; 4], effects: &Effects, effect_bits: Effec rgba[3] *= effects.ghost; } - [(rgba[0] * 255f32) as u8, (rgba[1] * 255f32) as u8, (rgba[2] * 255f32) as u8, (rgba[3] * 255f32) as u8] + [ + (rgba[0] * 255f32) as u8, + (rgba[1] * 255f32) as u8, + (rgba[2] * 255f32) as u8, + (rgba[3] * 255f32) as u8, + ] } const CENTER: Vec2 = Vec2(0.5, 0.5); -pub fn transform_point(point: Vec2, effects: &Effects, effect_bits: EffectBits, skin_size: Vec2) -> Vec2 { +pub fn transform_point( + point: Vec2, + effects: &Effects, + effect_bits: EffectBits, + skin_size: Vec2, +) -> Vec2 { let mut out = point; if effect_bits & (1 << (EffectBitfield::Mosaic as u32)) != 0 { /*texcoord0 = fract(u_mosaic * texcoord0);*/ out = Vec2( f32::fract(effects.mosaic * out.0), - f32::fract(effects.mosaic * out.1) + f32::fract(effects.mosaic * out.1), ); } @@ -218,7 +226,7 @@ pub fn transform_point(point: Vec2, effects: &Effects, effect_bits: EffectBits, out = Vec2( (f32::floor(out.0 * pixel_texel_size_x) + CENTER.0) / pixel_texel_size_x, - (f32::floor(out.1 * pixel_texel_size_y) + CENTER.1) / pixel_texel_size_y + (f32::floor(out.1 * pixel_texel_size_y) + CENTER.1) / pixel_texel_size_y, ); } diff --git a/swrender/src/lib.rs b/swrender/src/lib.rs index 50ddac14b..690ba50c6 100644 --- a/swrender/src/lib.rs +++ b/swrender/src/lib.rs @@ -1,9 +1,9 @@ -mod utils; -mod matrix; -mod effect_transform; mod convex_hull; -pub mod silhouette; pub mod drawable; +mod effect_transform; +mod matrix; +pub mod silhouette; +mod utils; use wasm_bindgen::prelude::*; @@ -37,7 +37,7 @@ const ID_NONE: drawable::DrawableID = -1; #[wasm_bindgen] pub struct SoftwareRenderer { drawables: HashMap<drawable::DrawableID, drawable::Drawable>, - silhouettes: HashMap<silhouette::SilhouetteID, silhouette::Silhouette> + silhouettes: HashMap<silhouette::SilhouetteID, silhouette::Silhouette>, } #[wasm_bindgen] @@ -45,10 +45,12 @@ impl SoftwareRenderer { pub fn new() -> SoftwareRenderer { let mut renderer = SoftwareRenderer { drawables: HashMap::new(), - silhouettes: HashMap::new() + silhouettes: HashMap::new(), }; - renderer.silhouettes.insert(ID_NONE, silhouette::Silhouette::new(ID_NONE)); + renderer + .silhouettes + .insert(ID_NONE, silhouette::Silhouette::new(ID_NONE)); utils::set_panic_hook(); renderer @@ -61,7 +63,7 @@ impl SoftwareRenderer { silhouette: Option<silhouette::SilhouetteID>, effects: Option<effect_transform::JSEffectMap>, effect_bits: effect_transform::EffectBits, - use_nearest_neighbor: bool + use_nearest_neighbor: bool, ) { let d = self.drawables.entry(id).or_insert(drawable::Drawable { matrix: [0.0; 16], @@ -70,14 +72,16 @@ impl SoftwareRenderer { effect_bits: 0, silhouette: match silhouette { Some(s) => s, - None => ID_NONE + None => ID_NONE, }, use_nearest_neighbor, - id + id, }); if let Some(m) = matrix { - d.matrix = (*m).try_into().expect("drawable's matrix contains 16 elements"); + d.matrix = (*m) + .try_into() + .expect("drawable's matrix contains 16 elements"); d.inverse_matrix = d.matrix.inverse(); } if let Some(s) = silhouette { @@ -104,8 +108,17 @@ impl SoftwareRenderer { nominal_height: f64, premultiplied: bool, ) { - let s = self.silhouettes.entry(id).or_insert(silhouette::Silhouette::new(id)); - s.set_data(w, h, data, matrix::Vec2(nominal_width as f32, nominal_height as f32), premultiplied); + let s = self + .silhouettes + .entry(id) + .or_insert(silhouette::Silhouette::new(id)); + s.set_data( + w, + h, + data, + matrix::Vec2(nominal_width as f32, nominal_height as f32), + premultiplied, + ); } pub fn remove_silhouette(&mut self, id: silhouette::SilhouetteID) { @@ -114,34 +127,34 @@ impl SoftwareRenderer { fn map_candidates( &self, - candidates: Vec<drawable::DrawableID> + candidates: Vec<drawable::DrawableID>, ) -> Vec<(&drawable::Drawable, &silhouette::Silhouette)> { - candidates.into_iter() - .map(|c| { - let d = self.drawables.get(&c).expect("Candidate drawable should exist"); - let s = self.silhouettes.get(&d.silhouette).unwrap(); - (d, s) - }).collect() + candidates + .into_iter() + .map(|c| { + let d = self + .drawables + .get(&c) + .expect("Candidate drawable should exist"); + let s = self.silhouettes.get(&d.silhouette).unwrap(); + (d, s) + }) + .collect() } - fn per_rect_pixel<F>( - &self, - func: F, - rect: JSRectangle, - drawable: drawable::DrawableID - ) -> bool - where F: Fn( - matrix::Vec2, - &drawable::Drawable, - &silhouette::Silhouette - ) -> bool { - + fn per_rect_pixel<F>(&self, func: F, rect: JSRectangle, drawable: drawable::DrawableID) -> bool + where + F: Fn(matrix::Vec2, &drawable::Drawable, &silhouette::Silhouette) -> bool, + { let left = rect.left() as i32; let right = rect.right() as i32 + 1; let bottom = rect.bottom() as i32 - 1; let top = rect.top() as i32; - let drawable = self.drawables.get(&drawable).expect("Drawable should exist"); + let drawable = self + .drawables + .get(&drawable) + .expect("Drawable should exist"); let silhouette = self.silhouettes.get(&drawable.silhouette).unwrap(); for y in bottom..top { @@ -160,48 +173,38 @@ impl SoftwareRenderer { &mut self, drawable: drawable::DrawableID, candidates: Vec<drawable::DrawableID>, - rect: JSRectangle + rect: JSRectangle, ) -> bool { let candidates = self.map_candidates(candidates); - self.per_rect_pixel(| - position, - drawable, - silhouette - | { - if drawable.is_touching(position, silhouette) { - for candidate in &candidates { - if candidate.0.is_touching(position, candidate.1) { - return true; + self.per_rect_pixel( + |position, drawable, silhouette| { + if drawable.is_touching(position, silhouette) { + for candidate in &candidates { + if candidate.0.is_touching(position, candidate.1) { + return true; + } } } - } - false - }, rect, drawable) + false + }, + rect, + drawable, + ) } #[inline(always)] - fn color_matches( - a: [u8; 3], - b: [u8; 3] - ) -> bool { - ( - ((a[0] ^ b[0]) & 0b11111000) | - ((a[1] ^ b[1]) & 0b11111000) | - ((a[2] ^ b[2]) & 0b11110000) - ) == 0 + fn color_matches(a: [u8; 3], b: [u8; 3]) -> bool { + (((a[0] ^ b[0]) & 0b11111000) | ((a[1] ^ b[1]) & 0b11111000) | ((a[2] ^ b[2]) & 0b11110000)) + == 0 } #[inline(always)] - fn mask_matches( - a: [u8; 4], - b: [u8; 3] - ) -> bool { - a[3] != 0 && - ( - ((a[0] ^ b[0]) & 0b11111100) | - ((a[1] ^ b[1]) & 0b11111100) | - ((a[2] ^ b[2]) & 0b11111100) - ) == 0 + fn mask_matches(a: [u8; 4], b: [u8; 3]) -> bool { + a[3] != 0 + && (((a[0] ^ b[0]) & 0b11111100) + | ((a[1] ^ b[1]) & 0b11111100) + | ((a[2] ^ b[2]) & 0b11111100)) + == 0 } pub fn color_is_touching_color( @@ -210,25 +213,25 @@ impl SoftwareRenderer { candidates: Vec<drawable::DrawableID>, rect: JSRectangle, color: &[u8], - mask: &[u8] + mask: &[u8], ) -> bool { let color: [u8; 3] = (*color).try_into().expect("color contains 3 elements"); let mask: [u8; 3] = (*mask).try_into().expect("mask contains 3 elements"); let candidates = self.map_candidates(candidates); - self.per_rect_pixel(| - position, - drawable, - silhouette - | { - if Self::mask_matches(drawable.sample_color(position, silhouette), mask) { - let sample_color = self.sample_color(position, &candidates); - if Self::color_matches(color, sample_color) { - return true; + self.per_rect_pixel( + |position, drawable, silhouette| { + if Self::mask_matches(drawable.sample_color(position, silhouette), mask) { + let sample_color = self.sample_color(position, &candidates); + if Self::color_matches(color, sample_color) { + return true; + } } - } - false - }, rect, drawable) + false + }, + rect, + drawable, + ) } pub fn is_touching_color( @@ -236,29 +239,29 @@ impl SoftwareRenderer { drawable: drawable::DrawableID, candidates: Vec<drawable::DrawableID>, rect: JSRectangle, - color: &[u8] + color: &[u8], ) -> bool { let color: [u8; 3] = (*color).try_into().expect("color contains 3 elements"); let candidates = self.map_candidates(candidates); - self.per_rect_pixel(| - position, - drawable, - silhouette - | { - if drawable.is_touching(position, silhouette) { - let sample_color = self.sample_color(position, &candidates); - if Self::color_matches(color, sample_color) { - return true; + self.per_rect_pixel( + |position, drawable, silhouette| { + if drawable.is_touching(position, silhouette) { + let sample_color = self.sample_color(position, &candidates); + if Self::color_matches(color, sample_color) { + return true; + } } - } - false - }, rect, drawable) + false + }, + rect, + drawable, + ) } fn sample_color( &self, position: matrix::Vec2, - candidates: &Vec<(&drawable::Drawable, &silhouette::Silhouette)> + candidates: &Vec<(&drawable::Drawable, &silhouette::Silhouette)>, ) -> [u8; 3] { let mut dst_color: (f32, f32, f32, f32) = (0f32, 0f32, 0f32, 0f32); let mut blend_alpha = 1f32; @@ -286,24 +289,24 @@ impl SoftwareRenderer { pub fn drawable_touching_rect( &mut self, drawable: drawable::DrawableID, - rect: JSRectangle + rect: JSRectangle, ) -> bool { - self.per_rect_pixel(| - position, + self.per_rect_pixel( + |position, drawable, silhouette| { + if drawable.is_touching(position, silhouette) { + return true; + } + false + }, + rect, drawable, - silhouette - | { - if drawable.is_touching(position, silhouette) { - return true; - } - false - }, rect, drawable) + ) } pub fn pick( &mut self, candidates: Vec<drawable::DrawableID>, - rect: JSRectangle + rect: JSRectangle, ) -> drawable::DrawableID { let mut hits: HashMap<drawable::DrawableID, u32> = HashMap::new(); hits.insert(ID_NONE, 0); @@ -321,9 +324,8 @@ impl SoftwareRenderer { let position = matrix::Vec2(x as f32, y as f32); for candidate in &candidates { if candidate.0.is_touching(position, candidate.1) { - hits - .entry(candidate.0.id) - .and_modify(|hit| {*hit += 1}) + hits.entry(candidate.0.id) + .and_modify(|hit| *hit += 1) .or_insert(1); break; @@ -346,7 +348,10 @@ impl SoftwareRenderer { } pub fn drawable_convex_hull_points(&mut self, drawable: drawable::DrawableID) -> Vec<f32> { - let drawable = self.drawables.get(&drawable).expect("Drawable should exist"); + let drawable = self + .drawables + .get(&drawable) + .expect("Drawable should exist"); let silhouette = self.silhouettes.get(&drawable.silhouette).unwrap(); let hull = convex_hull::calculate_drawable_convex_hull(drawable, silhouette); diff --git a/swrender/src/matrix.rs b/swrender/src/matrix.rs index 5610bd7c7..bcc0c32ed 100644 --- a/swrender/src/matrix.rs +++ b/swrender/src/matrix.rs @@ -1,5 +1,5 @@ -use std::ops; use std::f32; +use std::ops; pub type Mat4 = [f32; 16]; @@ -74,16 +74,16 @@ impl Matrix for Mat4 { let m31 = self[3 * 4 + 1]; let m32 = self[3 * 4 + 2]; let m33 = self[3 * 4 + 3]; - let tmp_0 = m22 * m33; - let tmp_1 = m32 * m23; - let tmp_2 = m12 * m33; - let tmp_3 = m32 * m13; - let tmp_4 = m12 * m23; - let tmp_5 = m22 * m13; - let tmp_6 = m02 * m33; - let tmp_7 = m32 * m03; - let tmp_8 = m02 * m23; - let tmp_9 = m22 * m03; + let tmp_0 = m22 * m33; + let tmp_1 = m32 * m23; + let tmp_2 = m12 * m33; + let tmp_3 = m32 * m13; + let tmp_4 = m12 * m23; + let tmp_5 = m22 * m13; + let tmp_6 = m02 * m33; + let tmp_7 = m32 * m03; + let tmp_8 = m02 * m23; + let tmp_9 = m22 * m03; let tmp_10 = m02 * m13; let tmp_11 = m12 * m03; let tmp_12 = m20 * m31; @@ -99,47 +99,59 @@ impl Matrix for Mat4 { let tmp_22 = m00 * m11; let tmp_23 = m10 * m01; - let t0: f32 = (tmp_0 * m11 + tmp_3 * m21 + tmp_4 * m31) - - (tmp_1 * m11 + tmp_2 * m21 + tmp_5 * m31); - let t1 = (tmp_1 * m01 + tmp_6 * m21 + tmp_9 * m31) - - (tmp_0 * m01 + tmp_7 * m21 + tmp_8 * m31); - let t2 = (tmp_2 * m01 + tmp_7 * m11 + tmp_10 * m31) - - (tmp_3 * m01 + tmp_6 * m11 + tmp_11 * m31); - let t3 = (tmp_5 * m01 + tmp_8 * m11 + tmp_11 * m21) - - (tmp_4 * m01 + tmp_9 * m11 + tmp_10 * m21); + let t0: f32 = + (tmp_0 * m11 + tmp_3 * m21 + tmp_4 * m31) - (tmp_1 * m11 + tmp_2 * m21 + tmp_5 * m31); + let t1 = + (tmp_1 * m01 + tmp_6 * m21 + tmp_9 * m31) - (tmp_0 * m01 + tmp_7 * m21 + tmp_8 * m31); + let t2 = + (tmp_2 * m01 + tmp_7 * m11 + tmp_10 * m31) - (tmp_3 * m01 + tmp_6 * m11 + tmp_11 * m31); + let t3 = + (tmp_5 * m01 + tmp_8 * m11 + tmp_11 * m21) - (tmp_4 * m01 + tmp_9 * m11 + tmp_10 * m21); let d = 1.0 / (m00 * t0 + m10 * t1 + m20 * t2 + m30 * t3); let mut dst: Mat4 = [0f32; 16]; - dst[ 0] = d * t0; - dst[ 1] = d * t1; - dst[ 2] = d * t2; - dst[ 3] = d * t3; - dst[ 4] = d * ((tmp_1 * m10 + tmp_2 * m20 + tmp_5 * m30) - - (tmp_0 * m10 + tmp_3 * m20 + tmp_4 * m30)); - dst[ 5] = d * ((tmp_0 * m00 + tmp_7 * m20 + tmp_8 * m30) - - (tmp_1 * m00 + tmp_6 * m20 + tmp_9 * m30)); - dst[ 6] = d * ((tmp_3 * m00 + tmp_6 * m10 + tmp_11 * m30) - - (tmp_2 * m00 + tmp_7 * m10 + tmp_10 * m30)); - dst[ 7] = d * ((tmp_4 * m00 + tmp_9 * m10 + tmp_10 * m20) - - (tmp_5 * m00 + tmp_8 * m10 + tmp_11 * m20)); - dst[ 8] = d * ((tmp_12 * m13 + tmp_15 * m23 + tmp_16 * m33) - - (tmp_13 * m13 + tmp_14 * m23 + tmp_17 * m33)); - dst[ 9] = d * ((tmp_13 * m03 + tmp_18 * m23 + tmp_21 * m33) - - (tmp_12 * m03 + tmp_19 * m23 + tmp_20 * m33)); - dst[10] = d * ((tmp_14 * m03 + tmp_19 * m13 + tmp_22 * m33) - - (tmp_15 * m03 + tmp_18 * m13 + tmp_23 * m33)); - dst[11] = d * ((tmp_17 * m03 + tmp_20 * m13 + tmp_23 * m23) - - (tmp_16 * m03 + tmp_21 * m13 + tmp_22 * m23)); - dst[12] = d * ((tmp_14 * m22 + tmp_17 * m32 + tmp_13 * m12) - - (tmp_16 * m32 + tmp_12 * m12 + tmp_15 * m22)); - dst[13] = d * ((tmp_20 * m32 + tmp_12 * m02 + tmp_19 * m22) - - (tmp_18 * m22 + tmp_21 * m32 + tmp_13 * m02)); - dst[14] = d * ((tmp_18 * m12 + tmp_23 * m32 + tmp_15 * m02) - - (tmp_22 * m32 + tmp_14 * m02 + tmp_19 * m12)); - dst[15] = d * ((tmp_22 * m22 + tmp_16 * m02 + tmp_21 * m12) - - (tmp_20 * m12 + tmp_23 * m22 + tmp_17 * m02)); + dst[0] = d * t0; + dst[1] = d * t1; + dst[2] = d * t2; + dst[3] = d * t3; + dst[4] = d + * ((tmp_1 * m10 + tmp_2 * m20 + tmp_5 * m30) + - (tmp_0 * m10 + tmp_3 * m20 + tmp_4 * m30)); + dst[5] = d + * ((tmp_0 * m00 + tmp_7 * m20 + tmp_8 * m30) + - (tmp_1 * m00 + tmp_6 * m20 + tmp_9 * m30)); + dst[6] = d + * ((tmp_3 * m00 + tmp_6 * m10 + tmp_11 * m30) + - (tmp_2 * m00 + tmp_7 * m10 + tmp_10 * m30)); + dst[7] = d + * ((tmp_4 * m00 + tmp_9 * m10 + tmp_10 * m20) + - (tmp_5 * m00 + tmp_8 * m10 + tmp_11 * m20)); + dst[8] = d + * ((tmp_12 * m13 + tmp_15 * m23 + tmp_16 * m33) + - (tmp_13 * m13 + tmp_14 * m23 + tmp_17 * m33)); + dst[9] = d + * ((tmp_13 * m03 + tmp_18 * m23 + tmp_21 * m33) + - (tmp_12 * m03 + tmp_19 * m23 + tmp_20 * m33)); + dst[10] = d + * ((tmp_14 * m03 + tmp_19 * m13 + tmp_22 * m33) + - (tmp_15 * m03 + tmp_18 * m13 + tmp_23 * m33)); + dst[11] = d + * ((tmp_17 * m03 + tmp_20 * m13 + tmp_23 * m23) + - (tmp_16 * m03 + tmp_21 * m13 + tmp_22 * m23)); + dst[12] = d + * ((tmp_14 * m22 + tmp_17 * m32 + tmp_13 * m12) + - (tmp_16 * m32 + tmp_12 * m12 + tmp_15 * m22)); + dst[13] = d + * ((tmp_20 * m32 + tmp_12 * m02 + tmp_19 * m22) + - (tmp_18 * m22 + tmp_21 * m32 + tmp_13 * m02)); + dst[14] = d + * ((tmp_18 * m12 + tmp_23 * m32 + tmp_15 * m02) + - (tmp_22 * m32 + tmp_14 * m02 + tmp_19 * m12)); + dst[15] = d + * ((tmp_22 * m22 + tmp_16 * m02 + tmp_21 * m12) + - (tmp_20 * m12 + tmp_23 * m22 + tmp_17 * m02)); dst } diff --git a/swrender/src/silhouette.rs b/swrender/src/silhouette.rs index 29faa2943..efbcae39a 100644 --- a/swrender/src/silhouette.rs +++ b/swrender/src/silhouette.rs @@ -16,14 +16,13 @@ extern { pub fn log(s: &str); } - pub struct Silhouette { pub id: SilhouetteID, pub width: u32, pub height: u32, pub nominal_size: Vec2, data: Box<[u8]>, - _blank: [u8; 4] + _blank: [u8; 4], } impl Silhouette { @@ -34,12 +33,23 @@ impl Silhouette { height: 0, nominal_size: Vec2(0f32, 0f32), data: Box::new([0, 0, 0, 0]), - _blank: [0, 0, 0, 0] + _blank: [0, 0, 0, 0], } } - pub fn set_data(&mut self, w: u32, h: u32, mut data: Box<[u8]>, nominal_size: Vec2, premultiplied: bool) { - assert_eq!(data.len(), (w * h * 4) as usize, "silhouette data is improperly sized"); + pub fn set_data( + &mut self, + w: u32, + h: u32, + mut data: Box<[u8]>, + nominal_size: Vec2, + premultiplied: bool, + ) { + assert_eq!( + data.len(), + (w * h * 4) as usize, + "silhouette data is improperly sized" + ); self.width = w; self.height = h; @@ -51,7 +61,9 @@ impl Silhouette { for pixel in pixels { // This is indeed one branch per pixel. However, the branch predictor does a pretty good job of // eliminating branch overhead and this saves us several instructions per pixel. - if pixel[3] == 0u8 {continue} + if pixel[3] == 0u8 { + continue; + } let alpha = (pixel[3] as f32) / 255f32; @@ -69,7 +81,7 @@ impl Silhouette { false } else { let idx = (((y as u32 * self.width) + x as u32) * 4) as usize; - self.data[idx+3] != 0u8 + self.data[idx + 3] != 0u8 } } @@ -78,25 +90,36 @@ impl Silhouette { self._blank } else { let idx = (((y as u32 * self.width) + x as u32) * 4) as usize; - [self.data[idx], self.data[idx + 1], self.data[idx + 2], self.data[idx + 3]] + [ + self.data[idx], + self.data[idx + 1], + self.data[idx + 2], + self.data[idx + 3], + ] } } pub fn is_touching_nearest(&self, vec: Vec2) -> bool { - self.get_point((vec.0 * self.width as f32) as i32, (vec.1 * self.height as f32) as i32) + self.get_point( + (vec.0 * self.width as f32) as i32, + (vec.1 * self.height as f32) as i32, + ) } pub fn color_at_nearest(&self, vec: Vec2) -> [u8; 4] { - self.get_color((vec.0 * self.width as f32) as i32, (vec.1 * self.height as f32) as i32) + self.get_color( + (vec.0 * self.width as f32) as i32, + (vec.1 * self.height as f32) as i32, + ) } pub fn is_touching_linear(&self, vec: Vec2) -> bool { let x = ((vec.0 * self.width as f32) - 0.5) as i32; let y = ((vec.1 * self.height as f32) - 0.5) as i32; - self.get_point(x, y) || - self.get_point(x + 1, y) || - self.get_point(x, y + 1) || - self.get_point(x + 1, y + 1) + self.get_point(x, y) + || self.get_point(x + 1, y) + || self.get_point(x, y + 1) + || self.get_point(x + 1, y + 1) } } From 831b3277a0e7297d23df56036312b48bf03167f3 Mon Sep 17 00:00:00 2001 From: adroitwhiz <adroitwhiz@protonmail.com> Date: Sun, 22 Mar 2020 04:08:32 -0400 Subject: [PATCH 08/14] Document + clean up Rust code --- package.json | 1 - swrender/src/convex_hull.rs | 13 +++++++++++-- swrender/src/drawable.rs | 8 ++++++-- swrender/src/effect_transform.rs | 13 +++++++++++-- swrender/src/lib.rs | 29 ++++++++++++++++++++++++++--- swrender/src/silhouette.rs | 26 ++++++++++---------------- 6 files changed, 64 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 39c548182..4b52af6e8 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,6 @@ }, "dependencies": { "grapheme-breaker": "0.3.2", - "hull.js": "0.2.10", "ify-loader": "1.0.4", "linebreak": "0.3.0", "minilog": "3.1.0", diff --git a/swrender/src/convex_hull.rs b/swrender/src/convex_hull.rs index b3683f3db..4a880f46a 100644 --- a/swrender/src/convex_hull.rs +++ b/swrender/src/convex_hull.rs @@ -12,6 +12,7 @@ fn determinant(a: Vec2, b: Vec2, c: Vec2) -> f32 { ((b.0 - a.0) * (c.1 - a.1)) - ((b.1 - a.1) * (c.0 - a.0)) } +/// Calculate the convex hull of a particular Drawable. pub fn calculate_drawable_convex_hull(drawable: &Drawable, silhouette: &Silhouette) -> Vec<Vec2> { let mut left_hull: Vec<Vec2> = Vec::new(); let mut right_hull: Vec<Vec2> = Vec::new(); @@ -27,7 +28,10 @@ pub fn calculate_drawable_convex_hull(drawable: &Drawable, silhouette: &Silhouet let mut current_point = Vec2(0f32, 0f32); + // *Not* "Scratch-space"-- +y is down + // Loop over all rows of pixels in the silhouette, starting at the top for y in 0..silhouette.height { + // We start at the leftmost point, then go rightwards until we hit an opaque pixel let mut x: u32 = 0; while x < silhouette.width { let local_point = Vec2( @@ -44,10 +48,14 @@ pub fn calculate_drawable_convex_hull(drawable: &Drawable, silhouette: &Silhouet x += 1; } + // If we managed to loop all the way through, there are no opaque pixels on this row. Go to the next one if x >= silhouette.width { continue; } + // If appending the current point to the left hull makes a counter-clockwise turn, + // we want to append the current point. Otherwise, we remove hull points until the + // current point makes a counter-clockwise turn with the last two points. while left_hull.len() >= 2 { let len = left_hull.len(); if determinant(left_hull[len - 1], left_hull[len - 2], current_point) > 0f32 { @@ -59,8 +67,8 @@ pub fn calculate_drawable_convex_hull(drawable: &Drawable, silhouette: &Silhouet left_hull.push(Vec2(current_point.0 as f32, current_point.1 as f32)); + // Now we repeat the process for the right side, looking leftwards for a pixel. x = silhouette.width - 1; - while x != 0 { let local_point = Vec2( (x as f32 + 0.5) / silhouette.width as f32, @@ -76,6 +84,7 @@ pub fn calculate_drawable_convex_hull(drawable: &Drawable, silhouette: &Silhouet x -= 1; } + // Because we're coming at this from the right, it goes clockwise this time. while right_hull.len() >= 2 { let len = right_hull.len(); if determinant(right_hull[len - 1], right_hull[len - 2], current_point) < 0f32 { @@ -88,8 +97,8 @@ pub fn calculate_drawable_convex_hull(drawable: &Drawable, silhouette: &Silhouet right_hull.push(Vec2(current_point.0 as f32, current_point.1 as f32)); } + // Add points from the right side in reverse order so all points are ordered clockwise. right_hull.reverse(); - left_hull.append(&mut right_hull); left_hull diff --git a/swrender/src/drawable.rs b/swrender/src/drawable.rs index 757dab633..838e8b6c9 100644 --- a/swrender/src/drawable.rs +++ b/swrender/src/drawable.rs @@ -7,9 +7,10 @@ use crate::silhouette::*; pub type DrawableID = i32; +/// The software-renderer version of a Drawable. +/// The `id` matches up with the corresponding JS-world Drawable. pub struct Drawable { pub id: DrawableID, - pub matrix: Mat4, pub inverse_matrix: Mat4, pub silhouette: SilhouetteID, pub effects: Effects, @@ -18,6 +19,7 @@ pub struct Drawable { } impl Drawable { + /// Convert a "Scratch-space" location into a texture-space (0-1) location. pub fn get_local_position(&self, vec: Vec2) -> Vec2 { let v0 = vec.0 + 0.5; let v1 = vec.1 + 0.5; @@ -32,7 +34,7 @@ impl Drawable { Vec2(out_x, out_y) } - pub fn get_transformed_position(&self, vec: Vec2, skin_size: Vec2) -> Vec2 { + fn get_transformed_position(&self, vec: Vec2, skin_size: Vec2) -> Vec2 { if (self.effect_bits & DISTORTION_EFFECT_MASK) == 0 { vec } else { @@ -40,6 +42,7 @@ impl Drawable { } } + /// Check if the "Scratch-space" position touches the passed silhouette. #[inline(always)] pub fn is_touching(&self, position: Vec2, silhouette: &Silhouette) -> bool { let local_position = self.get_local_position(position); @@ -59,6 +62,7 @@ impl Drawable { } } + /// Sample a color from the given "Scratch-space" position of the passed silhouette. #[inline(always)] pub fn sample_color<'a>(&self, position: Vec2, silhouette: &'a Silhouette) -> [u8; 4] { let local_position = self.get_local_position(position); diff --git a/swrender/src/effect_transform.rs b/swrender/src/effect_transform.rs index ebb55935f..4e0c931c0 100644 --- a/swrender/src/effect_transform.rs +++ b/swrender/src/effect_transform.rs @@ -66,6 +66,9 @@ impl Effects { } } +/// Converts an RGB color value to HSV. Conversion formula +/// adapted from http://lolengine.net/blog/2013/01/13/fast-rgb-to-hsv. +/// Assumes all channels are in the range [0, 1]. fn rgb_to_hsv(r: f32, g: f32, b: f32) -> (f32, f32, f32) { let mut r = r; let mut g = g; @@ -98,6 +101,9 @@ fn rgb_to_hsv(r: f32, g: f32, b: f32) -> (f32, f32, f32) { (h, s, v) } +/// Converts an HSV color value to RRB. Conversion formula +/// adapted from https://gist.github.com/mjackson/5311256. +/// Assumes all channels are in the range [0, 1]. fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (f32, f32, f32) { if s < 1e-18 { return (v, v, v); @@ -120,6 +126,8 @@ fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (f32, f32, f32) { } } +/// Transform a color in-place according to the passed effects + effect bits. Will apply +/// Ghost and Color and Brightness effects. pub fn transform_color<'a>(color: [u8; 4], effects: &Effects, effect_bits: EffectBits) -> [u8; 4] { const COLOR_DIVISOR: f32 = 1f32 / 255f32; let mut rgba: [f32; 4] = [ @@ -200,14 +208,15 @@ pub fn transform_color<'a>(color: [u8; 4], effects: &Effects, effect_bits: Effec ] } -const CENTER: Vec2 = Vec2(0.5, 0.5); - +/// Transform a texture coordinate to one that would be used after applying shader effects. pub fn transform_point( point: Vec2, effects: &Effects, effect_bits: EffectBits, skin_size: Vec2, ) -> Vec2 { + const CENTER: Vec2 = Vec2(0.5, 0.5); + let mut out = point; if effect_bits & (1 << (EffectBitfield::Mosaic as u32)) != 0 { diff --git a/swrender/src/lib.rs b/swrender/src/lib.rs index 690ba50c6..7d4411bb0 100644 --- a/swrender/src/lib.rs +++ b/swrender/src/lib.rs @@ -56,6 +56,8 @@ impl SoftwareRenderer { renderer } + /// Update the given CPU-side drawable's attributes given its ID. + /// Will create a new drawable on the CPU side if one doesn't yet exist. pub fn set_drawable( &mut self, id: drawable::DrawableID, @@ -66,7 +68,6 @@ impl SoftwareRenderer { use_nearest_neighbor: bool, ) { let d = self.drawables.entry(id).or_insert(drawable::Drawable { - matrix: [0.0; 16], inverse_matrix: [0.0; 16], effects: effect_transform::Effects::default(), effect_bits: 0, @@ -79,10 +80,10 @@ impl SoftwareRenderer { }); if let Some(m) = matrix { - d.matrix = (*m) + let mat: matrix::Mat4 = (*m) .try_into() .expect("drawable's matrix contains 16 elements"); - d.inverse_matrix = d.matrix.inverse(); + d.inverse_matrix = mat.inverse(); } if let Some(s) = silhouette { d.silhouette = s; @@ -94,10 +95,13 @@ impl SoftwareRenderer { d.use_nearest_neighbor = use_nearest_neighbor; } + /// Delete the CPU-side drawable with the given ID. pub fn remove_drawable(&mut self, id: drawable::DrawableID) { self.drawables.remove(&id); } + /// Update the given silhouette's attributes and data given the corresponding skin's ID. + /// Will create a new silhouette if one does not exist. pub fn set_silhouette( &mut self, id: silhouette::SilhouetteID, @@ -121,10 +125,12 @@ impl SoftwareRenderer { ); } + /// Delete the silhouette that corresponds to the skin with the given ID. pub fn remove_silhouette(&mut self, id: silhouette::SilhouetteID) { self.silhouettes.remove(&id); } + /// Map a set of drawable IDs to a Vec of tuples of the given drawables + their silhouettes, fn map_candidates( &self, candidates: Vec<drawable::DrawableID>, @@ -142,6 +148,8 @@ impl SoftwareRenderer { .collect() } + /// Perform the given function on a given drawable once per pixel inside the given rectangle, + /// stopping and returning true once the function does. fn per_rect_pixel<F>(&self, func: F, rect: JSRectangle, drawable: drawable::DrawableID) -> bool where F: Fn(matrix::Vec2, &drawable::Drawable, &silhouette::Silhouette) -> bool, @@ -169,6 +177,8 @@ impl SoftwareRenderer { false } + /// Check if a particular Drawable is touching any in a set of Drawables. + /// Will only check inside the given bounds. pub fn is_touching_drawables( &mut self, drawable: drawable::DrawableID, @@ -192,12 +202,17 @@ impl SoftwareRenderer { ) } + /// Determines if the given color is "close enough" (only test the 5 top bits for + /// red and green, 4 bits for blue). These bit masks are what Scratch 2 used to use, + /// so we do the same. #[inline(always)] fn color_matches(a: [u8; 3], b: [u8; 3]) -> bool { (((a[0] ^ b[0]) & 0b11111000) | ((a[1] ^ b[1]) & 0b11111000) | ((a[2] ^ b[2]) & 0b11110000)) == 0 } + /// Determines if the mask color is "close enough" (only test the 6 top bits for + /// each color). These bit masks are what Scratch 2 used to use, so we do the same. #[inline(always)] fn mask_matches(a: [u8; 4], b: [u8; 3]) -> bool { a[3] != 0 @@ -207,6 +222,7 @@ impl SoftwareRenderer { == 0 } + /// Check if a certain color in a drawable is touching a particular color. pub fn color_is_touching_color( &mut self, drawable: drawable::DrawableID, @@ -234,6 +250,7 @@ impl SoftwareRenderer { ) } + /// Check if a certain drawable is touching a particular color. pub fn is_touching_color( &mut self, drawable: drawable::DrawableID, @@ -258,6 +275,8 @@ impl SoftwareRenderer { ) } + /// Sample a pixel from the stage at a given "Scratch-space" coordinate. + /// Will only render the passed drawables. fn sample_color( &self, position: matrix::Vec2, @@ -286,6 +305,7 @@ impl SoftwareRenderer { [dst_color.0 as u8, dst_color.1 as u8, dst_color.2 as u8] } + /// Check if the drawable with the given ID is touching any pixel in the given rectangle. pub fn drawable_touching_rect( &mut self, drawable: drawable::DrawableID, @@ -303,6 +323,8 @@ impl SoftwareRenderer { ) } + /// Return the ID of the drawable that covers the most pixels in the given rectangle. + /// Drawables earlier in the list will occlude those lower in the list. pub fn pick( &mut self, candidates: Vec<drawable::DrawableID>, @@ -347,6 +369,7 @@ impl SoftwareRenderer { hit } + /// Calculate the convex hull points for the drawable with the given ID. pub fn drawable_convex_hull_points(&mut self, drawable: drawable::DrawableID) -> Vec<f32> { let drawable = self .drawables diff --git a/swrender/src/silhouette.rs b/swrender/src/silhouette.rs index efbcae39a..8ad92411c 100644 --- a/swrender/src/silhouette.rs +++ b/swrender/src/silhouette.rs @@ -2,20 +2,7 @@ use crate::matrix::Vec2; pub type SilhouetteID = i32; -use wasm_bindgen::prelude::*; - -#[wasm_bindgen] -extern { - #[wasm_bindgen(js_namespace = console)] - pub fn time(s: &str); - - #[wasm_bindgen(js_namespace = console)] - pub fn timeEnd(s: &str); - - #[wasm_bindgen(js_namespace = console)] - pub fn log(s: &str); -} - +/// The CPU-side version of a Skin. pub struct Silhouette { pub id: SilhouetteID, pub width: u32, @@ -37,6 +24,7 @@ impl Silhouette { } } + /// Update this silhouette with the bitmap data passed in from a Skin. pub fn set_data( &mut self, w: u32, @@ -76,7 +64,8 @@ impl Silhouette { self.data = data; } - pub fn get_point(&self, x: i32, y: i32) -> bool { + /// Returns whether the pixel at the given "silhouette-space" position has an alpha > 0. + fn get_point(&self, x: i32, y: i32) -> bool { if x < 0 || y < 0 || (x as u32) >= self.width || (y as u32) >= self.height { false } else { @@ -85,7 +74,8 @@ impl Silhouette { } } - pub fn get_color(&self, x: i32, y: i32) -> [u8; 4] { + /// Get the color from a given silhouette at the given "silhouette-space" position. + fn get_color(&self, x: i32, y: i32) -> [u8; 4] { if x < 0 || y < 0 || (x as u32) >= self.width || (y as u32) >= self.height { self._blank } else { @@ -99,6 +89,7 @@ impl Silhouette { } } + /// Test if the given texture coordinate (in range [0, 1]) touches the silhouette, using nearest-neighbor interpolation. pub fn is_touching_nearest(&self, vec: Vec2) -> bool { self.get_point( (vec.0 * self.width as f32) as i32, @@ -106,6 +97,7 @@ impl Silhouette { ) } + /// Sample a color at the given texture coordinates (in range [0, 1]) using nearest-neighbor interpolation. pub fn color_at_nearest(&self, vec: Vec2) -> [u8; 4] { self.get_color( (vec.0 * self.width as f32) as i32, @@ -113,7 +105,9 @@ impl Silhouette { ) } + /// Test if the given texture coordinate (in range [0, 1]) touches the silhouette, using linear interpolation. pub fn is_touching_linear(&self, vec: Vec2) -> bool { + // TODO: this often gives incorrect results, especially for coordinates whose fractional part is close to 0.5 let x = ((vec.0 * self.width as f32) - 0.5) as i32; let y = ((vec.1 * self.height as f32) - 0.5) as i32; From b8dbc426e5cd38558be9c7635c93bb9b2c8d7b6d Mon Sep 17 00:00:00 2001 From: adroitwhiz <adroitwhiz@protonmail.com> Date: Sun, 22 Mar 2020 04:14:12 -0400 Subject: [PATCH 09/14] Fix color-is-touching-color for ghosted sprites --- src/Drawable.js | 8 ++++++-- src/RenderWebGL.js | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Drawable.js b/src/Drawable.js index 84b5840c8..e1ca60140 100644 --- a/src/Drawable.js +++ b/src/Drawable.js @@ -575,8 +575,9 @@ class Drawable { /** * Update everything necessary to render this drawable on the CPU. + * @param {int} [effectMask] An optional bitmask of effects that will be applied to this drawable on the CPU. */ - updateCPURenderAttributes () { + updateCPURenderAttributes (effectMask) { this.updateMatrix(); if (this.skin) this.skin.updateSilhouette(this._scale); @@ -587,12 +588,15 @@ class Drawable { this._effectsDirty = false; } + let {enabledEffects} = this; + if (effectMask) enabledEffects &= effectMask; + this._renderer.softwareRenderer.set_drawable( this.id, this._uniforms.u_modelMatrix, this.skin.id, effects, - this.enabledEffects, + enabledEffects, this.skin.useNearest(this._scale, this) ); } diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index 6e0b31711..57d9e86d1 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -814,7 +814,8 @@ class RenderWebGL extends EventEmitter { const drawable = this._allDrawables[drawableID]; const hasMask = Boolean(mask3b); - drawable.updateCPURenderAttributes(); + // "Color is touching color" should not ghost the drawable whose color is being masked + drawable.updateCPURenderAttributes(~ShaderManager.EFFECT_INFO.ghost.mask); if (hasMask) { return this.softwareRenderer.color_is_touching_color( From 9f45802bf8e0960ca558043512800751fe0fec75 Mon Sep 17 00:00:00 2001 From: adroitwhiz <adroitwhiz@protonmail.com> Date: Fri, 3 Apr 2020 21:18:44 -0400 Subject: [PATCH 10/14] use normal import --- package.json | 2 +- src/RenderWebGL.js | 20 +++++++------------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 4b52af6e8..57a928db9 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "browser": "./src/index.js", "scripts": { "build": "npm run build:swrender && webpack --progress --colors", - "build:swrender": "cargo build --release --target wasm32-unknown-unknown --manifest-path=\"swrender/Cargo.toml\" && wasm-bindgen --target web swrender/target/wasm32-unknown-unknown/release/swrender.wasm --out-dir swrender/build", + "build:swrender": "cargo build --release --target wasm32-unknown-unknown --manifest-path=\"swrender/Cargo.toml\" && wasm-bindgen --target bundler swrender/target/wasm32-unknown-unknown/release/swrender.wasm --out-dir swrender/build", "docs": "jsdoc -c .jsdoc.json", "lint": "eslint .", "prepublish": "npm run build", diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index 57d9e86d1..b11b0943c 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -14,15 +14,11 @@ const log = require('./util/log'); let onLoadSwRender = null; let swRenderLoaded = false; -// eslint-disable-next-line no-unused-vars -const swrender = require('../swrender/build/swrender.js'); +let swrender = null; -const wasm = require('../swrender/build/swrender_bg.wasm'); -const swrenderInit = require('../swrender/build/swrender.js').default; - -swrenderInit(wasm) - .then(() => { - window.swrender = swrender; +import('../swrender/build/swrender') + .then(swrenderImport => { + swrender = swrenderImport; swRenderLoaded = true; if (onLoadSwRender) onLoadSwRender(); }); @@ -110,13 +106,11 @@ class RenderWebGL extends EventEmitter { } init () { - if (swRenderLoaded) return Promise.resolve(); - - return new Promise(resolve => { + const swRenderPromise = swRenderLoaded ? Promise.resolve() : new Promise(resolve => { onLoadSwRender = resolve; - }).then(() => { - this.swrender = swrender; + }); + return swRenderPromise.then(() => { this.softwareRenderer = swrender.SoftwareRenderer.new(); }); } From f800e80b572f21ae04c749532fc2215ada8b8058 Mon Sep 17 00:00:00 2001 From: adroitwhiz <adroitwhiz@protonmail.com> Date: Fri, 3 Apr 2020 21:42:07 -0400 Subject: [PATCH 11/14] include swrender build in repo --- swrender/.gitignore | 1 - swrender/build/swrender.d.ts | 91 ++++++++ swrender/build/swrender.js | 398 ++++++++++++++++++++++++++++++++ swrender/build/swrender_bg.d.ts | 18 ++ swrender/build/swrender_bg.wasm | Bin 0 -> 99119 bytes 5 files changed, 507 insertions(+), 1 deletion(-) create mode 100644 swrender/build/swrender.d.ts create mode 100644 swrender/build/swrender.js create mode 100644 swrender/build/swrender_bg.d.ts create mode 100644 swrender/build/swrender_bg.wasm diff --git a/swrender/.gitignore b/swrender/.gitignore index 9775a7636..0a2f752b9 100644 --- a/swrender/.gitignore +++ b/swrender/.gitignore @@ -2,6 +2,5 @@ **/*.rs.bk Cargo.lock bin/ -build/ wasm-pack.log .cargo-ok diff --git a/swrender/build/swrender.d.ts b/swrender/build/swrender.d.ts new file mode 100644 index 000000000..27d0cb106 --- /dev/null +++ b/swrender/build/swrender.d.ts @@ -0,0 +1,91 @@ +/* tslint:disable */ +/* eslint-disable */ +export class SoftwareRenderer { + free(): void; +/** +* @returns {SoftwareRenderer} +*/ + static new(): SoftwareRenderer; +/** +* Update the given CPU-side drawable\'s attributes given its ID. +* Will create a new drawable on the CPU side if one doesn\'t yet exist. +* @param {number} id +* @param {Float32Array | undefined} matrix +* @param {number | undefined} silhouette +* @param {any | undefined} effects +* @param {number} effect_bits +* @param {boolean} use_nearest_neighbor +*/ + set_drawable(id: number, matrix: Float32Array | undefined, silhouette: number | undefined, effects: any | undefined, effect_bits: number, use_nearest_neighbor: boolean): void; +/** +* Delete the CPU-side drawable with the given ID. +* @param {number} id +*/ + remove_drawable(id: number): void; +/** +* Update the given silhouette\'s attributes and data given the corresponding skin\'s ID. +* Will create a new silhouette if one does not exist. +* @param {number} id +* @param {number} w +* @param {number} h +* @param {Uint8Array} data +* @param {number} nominal_width +* @param {number} nominal_height +* @param {boolean} premultiplied +*/ + set_silhouette(id: number, w: number, h: number, data: Uint8Array, nominal_width: number, nominal_height: number, premultiplied: boolean): void; +/** +* Delete the silhouette that corresponds to the skin with the given ID. +* @param {number} id +*/ + remove_silhouette(id: number): void; +/** +* Check if a particular Drawable is touching any in a set of Drawables. +* Will only check inside the given bounds. +* @param {number} drawable +* @param {Int32Array} candidates +* @param {any} rect +* @returns {boolean} +*/ + is_touching_drawables(drawable: number, candidates: Int32Array, rect: any): boolean; +/** +* Check if a certain color in a drawable is touching a particular color. +* @param {number} drawable +* @param {Int32Array} candidates +* @param {any} rect +* @param {Uint8Array} color +* @param {Uint8Array} mask +* @returns {boolean} +*/ + color_is_touching_color(drawable: number, candidates: Int32Array, rect: any, color: Uint8Array, mask: Uint8Array): boolean; +/** +* Check if a certain drawable is touching a particular color. +* @param {number} drawable +* @param {Int32Array} candidates +* @param {any} rect +* @param {Uint8Array} color +* @returns {boolean} +*/ + is_touching_color(drawable: number, candidates: Int32Array, rect: any, color: Uint8Array): boolean; +/** +* Check if the drawable with the given ID is touching any pixel in the given rectangle. +* @param {number} drawable +* @param {any} rect +* @returns {boolean} +*/ + drawable_touching_rect(drawable: number, rect: any): boolean; +/** +* Return the ID of the drawable that covers the most pixels in the given rectangle. +* Drawables earlier in the list will occlude those lower in the list. +* @param {Int32Array} candidates +* @param {any} rect +* @returns {number} +*/ + pick(candidates: Int32Array, rect: any): number; +/** +* Calculate the convex hull points for the drawable with the given ID. +* @param {number} drawable +* @returns {Float32Array} +*/ + drawable_convex_hull_points(drawable: number): Float32Array; +} diff --git a/swrender/build/swrender.js b/swrender/build/swrender.js new file mode 100644 index 000000000..358a25c55 --- /dev/null +++ b/swrender/build/swrender.js @@ -0,0 +1,398 @@ +import * as wasm from './swrender_bg.wasm'; + +const heap = new Array(32).fill(undefined); + +heap.push(undefined, null, true, false); + +function getObject(idx) { return heap[idx]; } + +let heap_next = heap.length; + +function dropObject(idx) { + if (idx < 36) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + +const lTextDecoder = typeof TextDecoder === 'undefined' ? require('util').TextDecoder : TextDecoder; + +let cachedTextDecoder = new lTextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + +cachedTextDecoder.decode(); + +let cachegetUint8Memory0 = null; +function getUint8Memory0() { + if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) { + cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer); + } + return cachegetUint8Memory0; +} + +function getStringFromWasm0(ptr, len) { + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + +let cachegetFloat32Memory0 = null; +function getFloat32Memory0() { + if (cachegetFloat32Memory0 === null || cachegetFloat32Memory0.buffer !== wasm.memory.buffer) { + cachegetFloat32Memory0 = new Float32Array(wasm.memory.buffer); + } + return cachegetFloat32Memory0; +} + +let WASM_VECTOR_LEN = 0; + +function passArrayF32ToWasm0(arg, malloc) { + const ptr = malloc(arg.length * 4); + getFloat32Memory0().set(arg, ptr / 4); + WASM_VECTOR_LEN = arg.length; + return ptr; +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +function passArray8ToWasm0(arg, malloc) { + const ptr = malloc(arg.length * 1); + getUint8Memory0().set(arg, ptr / 1); + WASM_VECTOR_LEN = arg.length; + return ptr; +} + +let cachegetUint32Memory0 = null; +function getUint32Memory0() { + if (cachegetUint32Memory0 === null || cachegetUint32Memory0.buffer !== wasm.memory.buffer) { + cachegetUint32Memory0 = new Uint32Array(wasm.memory.buffer); + } + return cachegetUint32Memory0; +} + +function passArray32ToWasm0(arg, malloc) { + const ptr = malloc(arg.length * 4); + getUint32Memory0().set(arg, ptr / 4); + WASM_VECTOR_LEN = arg.length; + return ptr; +} + +let cachegetInt32Memory0 = null; +function getInt32Memory0() { + if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.memory.buffer) { + cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer); + } + return cachegetInt32Memory0; +} + +function getArrayF32FromWasm0(ptr, len) { + return getFloat32Memory0().subarray(ptr / 4, ptr / 4 + len); +} + +const lTextEncoder = typeof TextEncoder === 'undefined' ? require('util').TextEncoder : TextEncoder; + +let cachedTextEncoder = new lTextEncoder('utf-8'); + +const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view); +} + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; +}); + +function passStringToWasm0(arg, malloc, realloc) { + + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length); + getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len); + + const mem = getUint8Memory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3); + const view = getUint8Memory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} +/** +*/ +export class SoftwareRenderer { + + static __wrap(ptr) { + const obj = Object.create(SoftwareRenderer.prototype); + obj.ptr = ptr; + + return obj; + } + + free() { + const ptr = this.ptr; + this.ptr = 0; + + wasm.__wbg_softwarerenderer_free(ptr); + } + /** + * @returns {SoftwareRenderer} + */ + static new() { + var ret = wasm.softwarerenderer_new(); + return SoftwareRenderer.__wrap(ret); + } + /** + * Update the given CPU-side drawable\'s attributes given its ID. + * Will create a new drawable on the CPU side if one doesn\'t yet exist. + * @param {number} id + * @param {Float32Array | undefined} matrix + * @param {number | undefined} silhouette + * @param {any | undefined} effects + * @param {number} effect_bits + * @param {boolean} use_nearest_neighbor + */ + set_drawable(id, matrix, silhouette, effects, effect_bits, use_nearest_neighbor) { + var ptr0 = isLikeNone(matrix) ? 0 : passArrayF32ToWasm0(matrix, wasm.__wbindgen_malloc); + var len0 = WASM_VECTOR_LEN; + wasm.softwarerenderer_set_drawable(this.ptr, id, ptr0, len0, !isLikeNone(silhouette), isLikeNone(silhouette) ? 0 : silhouette, isLikeNone(effects) ? 0 : addHeapObject(effects), effect_bits, use_nearest_neighbor); + } + /** + * Delete the CPU-side drawable with the given ID. + * @param {number} id + */ + remove_drawable(id) { + wasm.softwarerenderer_remove_drawable(this.ptr, id); + } + /** + * Update the given silhouette\'s attributes and data given the corresponding skin\'s ID. + * Will create a new silhouette if one does not exist. + * @param {number} id + * @param {number} w + * @param {number} h + * @param {Uint8Array} data + * @param {number} nominal_width + * @param {number} nominal_height + * @param {boolean} premultiplied + */ + set_silhouette(id, w, h, data, nominal_width, nominal_height, premultiplied) { + var ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc); + var len0 = WASM_VECTOR_LEN; + wasm.softwarerenderer_set_silhouette(this.ptr, id, w, h, ptr0, len0, nominal_width, nominal_height, premultiplied); + } + /** + * Delete the silhouette that corresponds to the skin with the given ID. + * @param {number} id + */ + remove_silhouette(id) { + wasm.softwarerenderer_remove_silhouette(this.ptr, id); + } + /** + * Check if a particular Drawable is touching any in a set of Drawables. + * Will only check inside the given bounds. + * @param {number} drawable + * @param {Int32Array} candidates + * @param {any} rect + * @returns {boolean} + */ + is_touching_drawables(drawable, candidates, rect) { + var ptr0 = passArray32ToWasm0(candidates, wasm.__wbindgen_malloc); + var len0 = WASM_VECTOR_LEN; + var ret = wasm.softwarerenderer_is_touching_drawables(this.ptr, drawable, ptr0, len0, addHeapObject(rect)); + return ret !== 0; + } + /** + * Check if a certain color in a drawable is touching a particular color. + * @param {number} drawable + * @param {Int32Array} candidates + * @param {any} rect + * @param {Uint8Array} color + * @param {Uint8Array} mask + * @returns {boolean} + */ + color_is_touching_color(drawable, candidates, rect, color, mask) { + var ptr0 = passArray32ToWasm0(candidates, wasm.__wbindgen_malloc); + var len0 = WASM_VECTOR_LEN; + var ptr1 = passArray8ToWasm0(color, wasm.__wbindgen_malloc); + var len1 = WASM_VECTOR_LEN; + var ptr2 = passArray8ToWasm0(mask, wasm.__wbindgen_malloc); + var len2 = WASM_VECTOR_LEN; + var ret = wasm.softwarerenderer_color_is_touching_color(this.ptr, drawable, ptr0, len0, addHeapObject(rect), ptr1, len1, ptr2, len2); + return ret !== 0; + } + /** + * Check if a certain drawable is touching a particular color. + * @param {number} drawable + * @param {Int32Array} candidates + * @param {any} rect + * @param {Uint8Array} color + * @returns {boolean} + */ + is_touching_color(drawable, candidates, rect, color) { + var ptr0 = passArray32ToWasm0(candidates, wasm.__wbindgen_malloc); + var len0 = WASM_VECTOR_LEN; + var ptr1 = passArray8ToWasm0(color, wasm.__wbindgen_malloc); + var len1 = WASM_VECTOR_LEN; + var ret = wasm.softwarerenderer_is_touching_color(this.ptr, drawable, ptr0, len0, addHeapObject(rect), ptr1, len1); + return ret !== 0; + } + /** + * Check if the drawable with the given ID is touching any pixel in the given rectangle. + * @param {number} drawable + * @param {any} rect + * @returns {boolean} + */ + drawable_touching_rect(drawable, rect) { + var ret = wasm.softwarerenderer_drawable_touching_rect(this.ptr, drawable, addHeapObject(rect)); + return ret !== 0; + } + /** + * Return the ID of the drawable that covers the most pixels in the given rectangle. + * Drawables earlier in the list will occlude those lower in the list. + * @param {Int32Array} candidates + * @param {any} rect + * @returns {number} + */ + pick(candidates, rect) { + var ptr0 = passArray32ToWasm0(candidates, wasm.__wbindgen_malloc); + var len0 = WASM_VECTOR_LEN; + var ret = wasm.softwarerenderer_pick(this.ptr, ptr0, len0, addHeapObject(rect)); + return ret; + } + /** + * Calculate the convex hull points for the drawable with the given ID. + * @param {number} drawable + * @returns {Float32Array} + */ + drawable_convex_hull_points(drawable) { + wasm.softwarerenderer_drawable_convex_hull_points(8, this.ptr, drawable); + var r0 = getInt32Memory0()[8 / 4 + 0]; + var r1 = getInt32Memory0()[8 / 4 + 1]; + var v0 = getArrayF32FromWasm0(r0, r1).slice(); + wasm.__wbindgen_free(r0, r1 * 4); + return v0; + } +} + +export const __wbg_left_e0e87a2e66be13a6 = function(arg0) { + var ret = getObject(arg0).left; + return ret; +}; + +export const __wbg_right_7b7bac033ade0b86 = function(arg0) { + var ret = getObject(arg0).right; + return ret; +}; + +export const __wbg_bottom_4666a55ceceeee8a = function(arg0) { + var ret = getObject(arg0).bottom; + return ret; +}; + +export const __wbg_top_84c6cfb6e6a6bd02 = function(arg0) { + var ret = getObject(arg0).top; + return ret; +}; + +export const __wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); +}; + +export const __wbg_ucolor_ec62c5e559a2a5a3 = function(arg0) { + var ret = getObject(arg0).u_color; + return ret; +}; + +export const __wbg_ufisheye_6aa56ae214de6428 = function(arg0) { + var ret = getObject(arg0).u_fisheye; + return ret; +}; + +export const __wbg_uwhirl_677f66c116ae8d9b = function(arg0) { + var ret = getObject(arg0).u_whirl; + return ret; +}; + +export const __wbg_upixelate_eb81083d476dfa89 = function(arg0) { + var ret = getObject(arg0).u_pixelate; + return ret; +}; + +export const __wbg_umosaic_7bc9d9ddd07459c3 = function(arg0) { + var ret = getObject(arg0).u_mosaic; + return ret; +}; + +export const __wbg_ubrightness_d29d8f78f9c8e71d = function(arg0) { + var ret = getObject(arg0).u_brightness; + return ret; +}; + +export const __wbg_ughost_d81ebfbc362e40b0 = function(arg0) { + var ret = getObject(arg0).u_ghost; + return ret; +}; + +export const __wbg_new_59cb74e423758ede = function() { + var ret = new Error(); + return addHeapObject(ret); +}; + +export const __wbg_stack_558ba5917b466edd = function(arg0, arg1) { + var ret = getObject(arg1).stack; + var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +}; + +export const __wbg_error_4bb6c2a97407129a = function(arg0, arg1) { + try { + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(arg0, arg1); + } +}; + +export const __wbindgen_throw = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); +}; + diff --git a/swrender/build/swrender_bg.d.ts b/swrender/build/swrender_bg.d.ts new file mode 100644 index 000000000..0cc51ba12 --- /dev/null +++ b/swrender/build/swrender_bg.d.ts @@ -0,0 +1,18 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export function __wbg_softwarerenderer_free(a: number): void; +export function softwarerenderer_new(): number; +export function softwarerenderer_set_drawable(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number): void; +export function softwarerenderer_remove_drawable(a: number, b: number): void; +export function softwarerenderer_set_silhouette(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number): void; +export function softwarerenderer_remove_silhouette(a: number, b: number): void; +export function softwarerenderer_is_touching_drawables(a: number, b: number, c: number, d: number, e: number): number; +export function softwarerenderer_color_is_touching_color(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number): number; +export function softwarerenderer_is_touching_color(a: number, b: number, c: number, d: number, e: number, f: number, g: number): number; +export function softwarerenderer_drawable_touching_rect(a: number, b: number, c: number): number; +export function softwarerenderer_pick(a: number, b: number, c: number, d: number): number; +export function softwarerenderer_drawable_convex_hull_points(a: number, b: number, c: number): void; +export function __wbindgen_malloc(a: number): number; +export function __wbindgen_free(a: number, b: number): void; +export function __wbindgen_realloc(a: number, b: number, c: number): number; diff --git a/swrender/build/swrender_bg.wasm b/swrender/build/swrender_bg.wasm new file mode 100644 index 0000000000000000000000000000000000000000..abbdcdb0f92e8ee9bb9e6bc897f35d24c5eaa281 GIT binary patch literal 99119 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�A@+@SJPq14D@CJosn)wE>P={gQ)~dUo{mEx*K9$2*P|t91qSxj<1-neN~*$ z!tBV0_o#Rf_9@(<Hb@bQIr(P-bJu}&4eX_dlu|;MQaO=l^qANa4~1dsKOaoD;M9m| zVcWi@n!2INUlVQ4M)t$$$<<Zt4M~;t;0^Qx<iG+Jn&R1yk6m?$Tw8HfL_gs0ffF6? z1qf`{dZGbDWCs#+eNuE{ArD)KvI@!FIa1PZg|C}Y9=zg_XWK|#MIMUtl&Hs)+VC&# zi%H;}1+EL5UUfaBA;{*}#WgK6Zw7!`Sp@@1@)9fZ_6@c8!mzs=-ZaFzkqzT%YV@>d zwKO=w8Mcmc4-&^dB2maGg`Y4&dL%N8zi}w~k#G2_0)LWX+hCZk`^{k*V&XD5Ujg&$ zt!ynaVF0KBhlyffhy)bP(!V91)iT<Hoqp5V>C2%E$AM+(k?EU(`7Z~BeR#(Ne8O-; zVl@KWc}5(SLQ0Gox-&*jZ}P#wk-ru|57ZTyv2gyF&x-2Ev{S0XUxt<VRp4lcKhO8= z2(Oz^^zb?HgyEHV7fkB*Iu;CjEmQ(c_9zVgyoeMzL_1o86#K<SD}c5FJTMTZksV+E zSMfY9AX{tmck4hnHs6#3)-)!EgX1cceO-kcBlPe}#eWmE7|Y`W-pGW&T#;SzJU##m z{(9Snh69@m`Q?%JZShb-q`sx9a9nF<p!-PwRljp!o`j5ifDaipx{Hh;!~3on`)*>w z{+=jvG#h0_fWk3$jM>)|xOeei(UA*dXfGsmG^z?@3$Il~$QU){Py+?Qi3k}WW4zyg z{U%E&BhVY*ohCBgegfa$4@6@)!hqAFOKz|-EJH*JLqL2}hM_B~c&iEAEa49mnG*Xs z)+U({lygz5;gyuAgg3GnI;D782<duJ@S{ZC9$ksdM{ai^RRcj$yw1hKrWBZY;lGQ@ zk`^zhTZX1uC~t>~GPoV#;}XT|pyxw<Qz`gtTB;xY_#o<NNV5qnq*fzaID|*;CkIo< zrJO9Nv|xu=O7+Eqr~@Mt!EBgaNEk9*JNgfJ>R2GUTA??3LhS}k(=x-}4;_SQWSF+P zp+r9=^MZ+BB)?bV2-MHSLNh4eJZpyM<E3MXGUQZ6!#V<p$iT!)X^>liyt<zs)TojD zk0`1cArl;?|4Rvi$7^?HW5L+lipXT}k@$?T7V;(nn3}5bpJS?u$PY)zz>vXFWKXx< zUyNBAIh;N`*2pkGIL4HnUyfN?M-a$DHnt4}gq3XlYRuB6gY1V8u{6LQ=}XS9$1Dw} z1sG_A1a-v!d|&y^n5B_s3pXvYoDjgIeG0!Fv$U*303fwmfeEc4nDt*Gk446;5EizJ zH}tjg!X#*em`xN~h`w0ZvVnatzZ<(B#q^Q7rWuhAO~Fuvm+ROt)`fSK217n_*HWE* z2(b)-ip9*i&;f-WJR)Qbg9YC9bQ>Y1KV4a<s|#hk_a=Ecu0(mK3uJA9?96N5idQQR Y)!OBSioQ_pAttjRc8B4fy3myWKVvC4g8%>k literal 0 HcmV?d00001 From 2f8808e84e78cf0edd2dc32d75bff27e0814a2ed Mon Sep 17 00:00:00 2001 From: adroitwhiz <adroitwhiz@protonmail.com> Date: Mon, 22 Jun 2020 17:01:09 -0400 Subject: [PATCH 12/14] fix typo --- src/RenderWebGL.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index b11b0943c..dc9a83c89 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -929,7 +929,7 @@ class RenderWebGL extends EventEmitter { this._debugCanvas.width = bounds.width; this._debugCanvas.height = bounds.height; const context = this._debugCanvas.getContext('2d'); - const imageData = context.getImageData(0, 0, bounds.width, bounds.heigh); + const imageData = context.getImageData(0, 0, bounds.width, bounds.height); imageData.data.set(pixels); context.putImageData(imageData, 0, 0); } From db2e2dcfc6dfcc18b3f5e05904698a69fc1f01d9 Mon Sep 17 00:00:00 2001 From: adroitwhiz <adroitwhiz@protonmail.com> Date: Mon, 22 Jun 2020 17:01:15 -0400 Subject: [PATCH 13/14] properly dispose of silhouettes --- src/Skin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Skin.js b/src/Skin.js index 7916d9142..9cc005235 100644 --- a/src/Skin.js +++ b/src/Skin.js @@ -63,7 +63,7 @@ class Skin extends EventEmitter { */ dispose () { this._id = RenderConstants.ID_NONE; - if (this._newSilhouette) this._newSilhouette.free(); + this._renderer.softwareRenderer.remove_silhouette(this.id); } /** From 5260e2e29152ce1ead6bb94fb0e820306d49bb70 Mon Sep 17 00:00:00 2001 From: adroitwhiz <adroitwhiz@protonmail.com> Date: Fri, 19 Feb 2021 14:47:24 -0500 Subject: [PATCH 14/14] rerun build with newer wasm-bindgen --- swrender/build/swrender.d.ts | 86 +++--- swrender/build/swrender.js | 400 +------------------------- swrender/build/swrender_bg.js | 403 +++++++++++++++++++++++++++ swrender/build/swrender_bg.wasm | Bin 99119 -> 117048 bytes swrender/build/swrender_bg.wasm.d.ts | 19 ++ 5 files changed, 468 insertions(+), 440 deletions(-) create mode 100644 swrender/build/swrender_bg.js create mode 100644 swrender/build/swrender_bg.wasm.d.ts diff --git a/swrender/build/swrender.d.ts b/swrender/build/swrender.d.ts index 27d0cb106..3b9fd9556 100644 --- a/swrender/build/swrender.d.ts +++ b/swrender/build/swrender.d.ts @@ -1,91 +1,93 @@ /* tslint:disable */ /* eslint-disable */ +/** +*/ export class SoftwareRenderer { free(): void; /** -* @returns {SoftwareRenderer} +* @returns {SoftwareRenderer} */ static new(): SoftwareRenderer; /** -* Update the given CPU-side drawable\'s attributes given its ID. -* Will create a new drawable on the CPU side if one doesn\'t yet exist. -* @param {number} id -* @param {Float32Array | undefined} matrix -* @param {number | undefined} silhouette -* @param {any | undefined} effects -* @param {number} effect_bits -* @param {boolean} use_nearest_neighbor +* Update the given CPU-side drawable's attributes given its ID. +* Will create a new drawable on the CPU side if one doesn't yet exist. +* @param {number} id +* @param {Float32Array | undefined} matrix +* @param {number | undefined} silhouette +* @param {any | undefined} effects +* @param {number} effect_bits +* @param {boolean} use_nearest_neighbor */ set_drawable(id: number, matrix: Float32Array | undefined, silhouette: number | undefined, effects: any | undefined, effect_bits: number, use_nearest_neighbor: boolean): void; /** * Delete the CPU-side drawable with the given ID. -* @param {number} id +* @param {number} id */ remove_drawable(id: number): void; /** -* Update the given silhouette\'s attributes and data given the corresponding skin\'s ID. +* Update the given silhouette's attributes and data given the corresponding skin's ID. * Will create a new silhouette if one does not exist. -* @param {number} id -* @param {number} w -* @param {number} h -* @param {Uint8Array} data -* @param {number} nominal_width -* @param {number} nominal_height -* @param {boolean} premultiplied +* @param {number} id +* @param {number} w +* @param {number} h +* @param {Uint8Array} data +* @param {number} nominal_width +* @param {number} nominal_height +* @param {boolean} premultiplied */ set_silhouette(id: number, w: number, h: number, data: Uint8Array, nominal_width: number, nominal_height: number, premultiplied: boolean): void; /** * Delete the silhouette that corresponds to the skin with the given ID. -* @param {number} id +* @param {number} id */ remove_silhouette(id: number): void; /** * Check if a particular Drawable is touching any in a set of Drawables. * Will only check inside the given bounds. -* @param {number} drawable -* @param {Int32Array} candidates -* @param {any} rect -* @returns {boolean} +* @param {number} drawable +* @param {Int32Array} candidates +* @param {any} rect +* @returns {boolean} */ is_touching_drawables(drawable: number, candidates: Int32Array, rect: any): boolean; /** * Check if a certain color in a drawable is touching a particular color. -* @param {number} drawable -* @param {Int32Array} candidates -* @param {any} rect -* @param {Uint8Array} color -* @param {Uint8Array} mask -* @returns {boolean} +* @param {number} drawable +* @param {Int32Array} candidates +* @param {any} rect +* @param {Uint8Array} color +* @param {Uint8Array} mask +* @returns {boolean} */ color_is_touching_color(drawable: number, candidates: Int32Array, rect: any, color: Uint8Array, mask: Uint8Array): boolean; /** * Check if a certain drawable is touching a particular color. -* @param {number} drawable -* @param {Int32Array} candidates -* @param {any} rect -* @param {Uint8Array} color -* @returns {boolean} +* @param {number} drawable +* @param {Int32Array} candidates +* @param {any} rect +* @param {Uint8Array} color +* @returns {boolean} */ is_touching_color(drawable: number, candidates: Int32Array, rect: any, color: Uint8Array): boolean; /** * Check if the drawable with the given ID is touching any pixel in the given rectangle. -* @param {number} drawable -* @param {any} rect -* @returns {boolean} +* @param {number} drawable +* @param {any} rect +* @returns {boolean} */ drawable_touching_rect(drawable: number, rect: any): boolean; /** * Return the ID of the drawable that covers the most pixels in the given rectangle. * Drawables earlier in the list will occlude those lower in the list. -* @param {Int32Array} candidates -* @param {any} rect -* @returns {number} +* @param {Int32Array} candidates +* @param {any} rect +* @returns {number} */ pick(candidates: Int32Array, rect: any): number; /** * Calculate the convex hull points for the drawable with the given ID. -* @param {number} drawable -* @returns {Float32Array} +* @param {number} drawable +* @returns {Float32Array} */ drawable_convex_hull_points(drawable: number): Float32Array; } diff --git a/swrender/build/swrender.js b/swrender/build/swrender.js index 358a25c55..e70a70410 100644 --- a/swrender/build/swrender.js +++ b/swrender/build/swrender.js @@ -1,398 +1,2 @@ -import * as wasm from './swrender_bg.wasm'; - -const heap = new Array(32).fill(undefined); - -heap.push(undefined, null, true, false); - -function getObject(idx) { return heap[idx]; } - -let heap_next = heap.length; - -function dropObject(idx) { - if (idx < 36) return; - heap[idx] = heap_next; - heap_next = idx; -} - -function takeObject(idx) { - const ret = getObject(idx); - dropObject(idx); - return ret; -} - -const lTextDecoder = typeof TextDecoder === 'undefined' ? require('util').TextDecoder : TextDecoder; - -let cachedTextDecoder = new lTextDecoder('utf-8', { ignoreBOM: true, fatal: true }); - -cachedTextDecoder.decode(); - -let cachegetUint8Memory0 = null; -function getUint8Memory0() { - if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) { - cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer); - } - return cachegetUint8Memory0; -} - -function getStringFromWasm0(ptr, len) { - return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); -} - -let cachegetFloat32Memory0 = null; -function getFloat32Memory0() { - if (cachegetFloat32Memory0 === null || cachegetFloat32Memory0.buffer !== wasm.memory.buffer) { - cachegetFloat32Memory0 = new Float32Array(wasm.memory.buffer); - } - return cachegetFloat32Memory0; -} - -let WASM_VECTOR_LEN = 0; - -function passArrayF32ToWasm0(arg, malloc) { - const ptr = malloc(arg.length * 4); - getFloat32Memory0().set(arg, ptr / 4); - WASM_VECTOR_LEN = arg.length; - return ptr; -} - -function isLikeNone(x) { - return x === undefined || x === null; -} - -function addHeapObject(obj) { - if (heap_next === heap.length) heap.push(heap.length + 1); - const idx = heap_next; - heap_next = heap[idx]; - - heap[idx] = obj; - return idx; -} - -function passArray8ToWasm0(arg, malloc) { - const ptr = malloc(arg.length * 1); - getUint8Memory0().set(arg, ptr / 1); - WASM_VECTOR_LEN = arg.length; - return ptr; -} - -let cachegetUint32Memory0 = null; -function getUint32Memory0() { - if (cachegetUint32Memory0 === null || cachegetUint32Memory0.buffer !== wasm.memory.buffer) { - cachegetUint32Memory0 = new Uint32Array(wasm.memory.buffer); - } - return cachegetUint32Memory0; -} - -function passArray32ToWasm0(arg, malloc) { - const ptr = malloc(arg.length * 4); - getUint32Memory0().set(arg, ptr / 4); - WASM_VECTOR_LEN = arg.length; - return ptr; -} - -let cachegetInt32Memory0 = null; -function getInt32Memory0() { - if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.memory.buffer) { - cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer); - } - return cachegetInt32Memory0; -} - -function getArrayF32FromWasm0(ptr, len) { - return getFloat32Memory0().subarray(ptr / 4, ptr / 4 + len); -} - -const lTextEncoder = typeof TextEncoder === 'undefined' ? require('util').TextEncoder : TextEncoder; - -let cachedTextEncoder = new lTextEncoder('utf-8'); - -const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' - ? function (arg, view) { - return cachedTextEncoder.encodeInto(arg, view); -} - : function (arg, view) { - const buf = cachedTextEncoder.encode(arg); - view.set(buf); - return { - read: arg.length, - written: buf.length - }; -}); - -function passStringToWasm0(arg, malloc, realloc) { - - if (realloc === undefined) { - const buf = cachedTextEncoder.encode(arg); - const ptr = malloc(buf.length); - getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); - WASM_VECTOR_LEN = buf.length; - return ptr; - } - - let len = arg.length; - let ptr = malloc(len); - - const mem = getUint8Memory0(); - - let offset = 0; - - for (; offset < len; offset++) { - const code = arg.charCodeAt(offset); - if (code > 0x7F) break; - mem[ptr + offset] = code; - } - - if (offset !== len) { - if (offset !== 0) { - arg = arg.slice(offset); - } - ptr = realloc(ptr, len, len = offset + arg.length * 3); - const view = getUint8Memory0().subarray(ptr + offset, ptr + len); - const ret = encodeString(arg, view); - - offset += ret.written; - } - - WASM_VECTOR_LEN = offset; - return ptr; -} -/** -*/ -export class SoftwareRenderer { - - static __wrap(ptr) { - const obj = Object.create(SoftwareRenderer.prototype); - obj.ptr = ptr; - - return obj; - } - - free() { - const ptr = this.ptr; - this.ptr = 0; - - wasm.__wbg_softwarerenderer_free(ptr); - } - /** - * @returns {SoftwareRenderer} - */ - static new() { - var ret = wasm.softwarerenderer_new(); - return SoftwareRenderer.__wrap(ret); - } - /** - * Update the given CPU-side drawable\'s attributes given its ID. - * Will create a new drawable on the CPU side if one doesn\'t yet exist. - * @param {number} id - * @param {Float32Array | undefined} matrix - * @param {number | undefined} silhouette - * @param {any | undefined} effects - * @param {number} effect_bits - * @param {boolean} use_nearest_neighbor - */ - set_drawable(id, matrix, silhouette, effects, effect_bits, use_nearest_neighbor) { - var ptr0 = isLikeNone(matrix) ? 0 : passArrayF32ToWasm0(matrix, wasm.__wbindgen_malloc); - var len0 = WASM_VECTOR_LEN; - wasm.softwarerenderer_set_drawable(this.ptr, id, ptr0, len0, !isLikeNone(silhouette), isLikeNone(silhouette) ? 0 : silhouette, isLikeNone(effects) ? 0 : addHeapObject(effects), effect_bits, use_nearest_neighbor); - } - /** - * Delete the CPU-side drawable with the given ID. - * @param {number} id - */ - remove_drawable(id) { - wasm.softwarerenderer_remove_drawable(this.ptr, id); - } - /** - * Update the given silhouette\'s attributes and data given the corresponding skin\'s ID. - * Will create a new silhouette if one does not exist. - * @param {number} id - * @param {number} w - * @param {number} h - * @param {Uint8Array} data - * @param {number} nominal_width - * @param {number} nominal_height - * @param {boolean} premultiplied - */ - set_silhouette(id, w, h, data, nominal_width, nominal_height, premultiplied) { - var ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc); - var len0 = WASM_VECTOR_LEN; - wasm.softwarerenderer_set_silhouette(this.ptr, id, w, h, ptr0, len0, nominal_width, nominal_height, premultiplied); - } - /** - * Delete the silhouette that corresponds to the skin with the given ID. - * @param {number} id - */ - remove_silhouette(id) { - wasm.softwarerenderer_remove_silhouette(this.ptr, id); - } - /** - * Check if a particular Drawable is touching any in a set of Drawables. - * Will only check inside the given bounds. - * @param {number} drawable - * @param {Int32Array} candidates - * @param {any} rect - * @returns {boolean} - */ - is_touching_drawables(drawable, candidates, rect) { - var ptr0 = passArray32ToWasm0(candidates, wasm.__wbindgen_malloc); - var len0 = WASM_VECTOR_LEN; - var ret = wasm.softwarerenderer_is_touching_drawables(this.ptr, drawable, ptr0, len0, addHeapObject(rect)); - return ret !== 0; - } - /** - * Check if a certain color in a drawable is touching a particular color. - * @param {number} drawable - * @param {Int32Array} candidates - * @param {any} rect - * @param {Uint8Array} color - * @param {Uint8Array} mask - * @returns {boolean} - */ - color_is_touching_color(drawable, candidates, rect, color, mask) { - var ptr0 = passArray32ToWasm0(candidates, wasm.__wbindgen_malloc); - var len0 = WASM_VECTOR_LEN; - var ptr1 = passArray8ToWasm0(color, wasm.__wbindgen_malloc); - var len1 = WASM_VECTOR_LEN; - var ptr2 = passArray8ToWasm0(mask, wasm.__wbindgen_malloc); - var len2 = WASM_VECTOR_LEN; - var ret = wasm.softwarerenderer_color_is_touching_color(this.ptr, drawable, ptr0, len0, addHeapObject(rect), ptr1, len1, ptr2, len2); - return ret !== 0; - } - /** - * Check if a certain drawable is touching a particular color. - * @param {number} drawable - * @param {Int32Array} candidates - * @param {any} rect - * @param {Uint8Array} color - * @returns {boolean} - */ - is_touching_color(drawable, candidates, rect, color) { - var ptr0 = passArray32ToWasm0(candidates, wasm.__wbindgen_malloc); - var len0 = WASM_VECTOR_LEN; - var ptr1 = passArray8ToWasm0(color, wasm.__wbindgen_malloc); - var len1 = WASM_VECTOR_LEN; - var ret = wasm.softwarerenderer_is_touching_color(this.ptr, drawable, ptr0, len0, addHeapObject(rect), ptr1, len1); - return ret !== 0; - } - /** - * Check if the drawable with the given ID is touching any pixel in the given rectangle. - * @param {number} drawable - * @param {any} rect - * @returns {boolean} - */ - drawable_touching_rect(drawable, rect) { - var ret = wasm.softwarerenderer_drawable_touching_rect(this.ptr, drawable, addHeapObject(rect)); - return ret !== 0; - } - /** - * Return the ID of the drawable that covers the most pixels in the given rectangle. - * Drawables earlier in the list will occlude those lower in the list. - * @param {Int32Array} candidates - * @param {any} rect - * @returns {number} - */ - pick(candidates, rect) { - var ptr0 = passArray32ToWasm0(candidates, wasm.__wbindgen_malloc); - var len0 = WASM_VECTOR_LEN; - var ret = wasm.softwarerenderer_pick(this.ptr, ptr0, len0, addHeapObject(rect)); - return ret; - } - /** - * Calculate the convex hull points for the drawable with the given ID. - * @param {number} drawable - * @returns {Float32Array} - */ - drawable_convex_hull_points(drawable) { - wasm.softwarerenderer_drawable_convex_hull_points(8, this.ptr, drawable); - var r0 = getInt32Memory0()[8 / 4 + 0]; - var r1 = getInt32Memory0()[8 / 4 + 1]; - var v0 = getArrayF32FromWasm0(r0, r1).slice(); - wasm.__wbindgen_free(r0, r1 * 4); - return v0; - } -} - -export const __wbg_left_e0e87a2e66be13a6 = function(arg0) { - var ret = getObject(arg0).left; - return ret; -}; - -export const __wbg_right_7b7bac033ade0b86 = function(arg0) { - var ret = getObject(arg0).right; - return ret; -}; - -export const __wbg_bottom_4666a55ceceeee8a = function(arg0) { - var ret = getObject(arg0).bottom; - return ret; -}; - -export const __wbg_top_84c6cfb6e6a6bd02 = function(arg0) { - var ret = getObject(arg0).top; - return ret; -}; - -export const __wbindgen_object_drop_ref = function(arg0) { - takeObject(arg0); -}; - -export const __wbg_ucolor_ec62c5e559a2a5a3 = function(arg0) { - var ret = getObject(arg0).u_color; - return ret; -}; - -export const __wbg_ufisheye_6aa56ae214de6428 = function(arg0) { - var ret = getObject(arg0).u_fisheye; - return ret; -}; - -export const __wbg_uwhirl_677f66c116ae8d9b = function(arg0) { - var ret = getObject(arg0).u_whirl; - return ret; -}; - -export const __wbg_upixelate_eb81083d476dfa89 = function(arg0) { - var ret = getObject(arg0).u_pixelate; - return ret; -}; - -export const __wbg_umosaic_7bc9d9ddd07459c3 = function(arg0) { - var ret = getObject(arg0).u_mosaic; - return ret; -}; - -export const __wbg_ubrightness_d29d8f78f9c8e71d = function(arg0) { - var ret = getObject(arg0).u_brightness; - return ret; -}; - -export const __wbg_ughost_d81ebfbc362e40b0 = function(arg0) { - var ret = getObject(arg0).u_ghost; - return ret; -}; - -export const __wbg_new_59cb74e423758ede = function() { - var ret = new Error(); - return addHeapObject(ret); -}; - -export const __wbg_stack_558ba5917b466edd = function(arg0, arg1) { - var ret = getObject(arg1).stack; - var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len0 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len0; - getInt32Memory0()[arg0 / 4 + 0] = ptr0; -}; - -export const __wbg_error_4bb6c2a97407129a = function(arg0, arg1) { - try { - console.error(getStringFromWasm0(arg0, arg1)); - } finally { - wasm.__wbindgen_free(arg0, arg1); - } -}; - -export const __wbindgen_throw = function(arg0, arg1) { - throw new Error(getStringFromWasm0(arg0, arg1)); -}; - +import * as wasm from "./swrender_bg.wasm"; +export * from "./swrender_bg.js"; \ No newline at end of file diff --git a/swrender/build/swrender_bg.js b/swrender/build/swrender_bg.js new file mode 100644 index 000000000..29301cb16 --- /dev/null +++ b/swrender/build/swrender_bg.js @@ -0,0 +1,403 @@ +import * as wasm from './swrender_bg.wasm'; + +const heap = new Array(32).fill(undefined); + +heap.push(undefined, null, true, false); + +function getObject(idx) { return heap[idx]; } + +let heap_next = heap.length; + +function dropObject(idx) { + if (idx < 36) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + +const lTextDecoder = typeof TextDecoder === 'undefined' ? (0, module.require)('util').TextDecoder : TextDecoder; + +let cachedTextDecoder = new lTextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + +cachedTextDecoder.decode(); + +let cachegetUint8Memory0 = null; +function getUint8Memory0() { + if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) { + cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer); + } + return cachegetUint8Memory0; +} + +function getStringFromWasm0(ptr, len) { + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + +let cachegetFloat32Memory0 = null; +function getFloat32Memory0() { + if (cachegetFloat32Memory0 === null || cachegetFloat32Memory0.buffer !== wasm.memory.buffer) { + cachegetFloat32Memory0 = new Float32Array(wasm.memory.buffer); + } + return cachegetFloat32Memory0; +} + +let WASM_VECTOR_LEN = 0; + +function passArrayF32ToWasm0(arg, malloc) { + const ptr = malloc(arg.length * 4); + getFloat32Memory0().set(arg, ptr / 4); + WASM_VECTOR_LEN = arg.length; + return ptr; +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +function passArray8ToWasm0(arg, malloc) { + const ptr = malloc(arg.length * 1); + getUint8Memory0().set(arg, ptr / 1); + WASM_VECTOR_LEN = arg.length; + return ptr; +} + +let cachegetUint32Memory0 = null; +function getUint32Memory0() { + if (cachegetUint32Memory0 === null || cachegetUint32Memory0.buffer !== wasm.memory.buffer) { + cachegetUint32Memory0 = new Uint32Array(wasm.memory.buffer); + } + return cachegetUint32Memory0; +} + +function passArray32ToWasm0(arg, malloc) { + const ptr = malloc(arg.length * 4); + getUint32Memory0().set(arg, ptr / 4); + WASM_VECTOR_LEN = arg.length; + return ptr; +} + +let cachegetInt32Memory0 = null; +function getInt32Memory0() { + if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.memory.buffer) { + cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer); + } + return cachegetInt32Memory0; +} + +function getArrayF32FromWasm0(ptr, len) { + return getFloat32Memory0().subarray(ptr / 4, ptr / 4 + len); +} + +const lTextEncoder = typeof TextEncoder === 'undefined' ? (0, module.require)('util').TextEncoder : TextEncoder; + +let cachedTextEncoder = new lTextEncoder('utf-8'); + +const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view); +} + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; +}); + +function passStringToWasm0(arg, malloc, realloc) { + + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length); + getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len); + + const mem = getUint8Memory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3); + const view = getUint8Memory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} +/** +*/ +export class SoftwareRenderer { + + static __wrap(ptr) { + const obj = Object.create(SoftwareRenderer.prototype); + obj.ptr = ptr; + + return obj; + } + + free() { + const ptr = this.ptr; + this.ptr = 0; + + wasm.__wbg_softwarerenderer_free(ptr); + } + /** + * @returns {SoftwareRenderer} + */ + static new() { + var ret = wasm.softwarerenderer_new(); + return SoftwareRenderer.__wrap(ret); + } + /** + * Update the given CPU-side drawable's attributes given its ID. + * Will create a new drawable on the CPU side if one doesn't yet exist. + * @param {number} id + * @param {Float32Array | undefined} matrix + * @param {number | undefined} silhouette + * @param {any | undefined} effects + * @param {number} effect_bits + * @param {boolean} use_nearest_neighbor + */ + set_drawable(id, matrix, silhouette, effects, effect_bits, use_nearest_neighbor) { + var ptr0 = isLikeNone(matrix) ? 0 : passArrayF32ToWasm0(matrix, wasm.__wbindgen_malloc); + var len0 = WASM_VECTOR_LEN; + wasm.softwarerenderer_set_drawable(this.ptr, id, ptr0, len0, !isLikeNone(silhouette), isLikeNone(silhouette) ? 0 : silhouette, isLikeNone(effects) ? 0 : addHeapObject(effects), effect_bits, use_nearest_neighbor); + } + /** + * Delete the CPU-side drawable with the given ID. + * @param {number} id + */ + remove_drawable(id) { + wasm.softwarerenderer_remove_drawable(this.ptr, id); + } + /** + * Update the given silhouette's attributes and data given the corresponding skin's ID. + * Will create a new silhouette if one does not exist. + * @param {number} id + * @param {number} w + * @param {number} h + * @param {Uint8Array} data + * @param {number} nominal_width + * @param {number} nominal_height + * @param {boolean} premultiplied + */ + set_silhouette(id, w, h, data, nominal_width, nominal_height, premultiplied) { + var ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc); + var len0 = WASM_VECTOR_LEN; + wasm.softwarerenderer_set_silhouette(this.ptr, id, w, h, ptr0, len0, nominal_width, nominal_height, premultiplied); + } + /** + * Delete the silhouette that corresponds to the skin with the given ID. + * @param {number} id + */ + remove_silhouette(id) { + wasm.softwarerenderer_remove_silhouette(this.ptr, id); + } + /** + * Check if a particular Drawable is touching any in a set of Drawables. + * Will only check inside the given bounds. + * @param {number} drawable + * @param {Int32Array} candidates + * @param {any} rect + * @returns {boolean} + */ + is_touching_drawables(drawable, candidates, rect) { + var ptr0 = passArray32ToWasm0(candidates, wasm.__wbindgen_malloc); + var len0 = WASM_VECTOR_LEN; + var ret = wasm.softwarerenderer_is_touching_drawables(this.ptr, drawable, ptr0, len0, addHeapObject(rect)); + return ret !== 0; + } + /** + * Check if a certain color in a drawable is touching a particular color. + * @param {number} drawable + * @param {Int32Array} candidates + * @param {any} rect + * @param {Uint8Array} color + * @param {Uint8Array} mask + * @returns {boolean} + */ + color_is_touching_color(drawable, candidates, rect, color, mask) { + var ptr0 = passArray32ToWasm0(candidates, wasm.__wbindgen_malloc); + var len0 = WASM_VECTOR_LEN; + var ptr1 = passArray8ToWasm0(color, wasm.__wbindgen_malloc); + var len1 = WASM_VECTOR_LEN; + var ptr2 = passArray8ToWasm0(mask, wasm.__wbindgen_malloc); + var len2 = WASM_VECTOR_LEN; + var ret = wasm.softwarerenderer_color_is_touching_color(this.ptr, drawable, ptr0, len0, addHeapObject(rect), ptr1, len1, ptr2, len2); + return ret !== 0; + } + /** + * Check if a certain drawable is touching a particular color. + * @param {number} drawable + * @param {Int32Array} candidates + * @param {any} rect + * @param {Uint8Array} color + * @returns {boolean} + */ + is_touching_color(drawable, candidates, rect, color) { + var ptr0 = passArray32ToWasm0(candidates, wasm.__wbindgen_malloc); + var len0 = WASM_VECTOR_LEN; + var ptr1 = passArray8ToWasm0(color, wasm.__wbindgen_malloc); + var len1 = WASM_VECTOR_LEN; + var ret = wasm.softwarerenderer_is_touching_color(this.ptr, drawable, ptr0, len0, addHeapObject(rect), ptr1, len1); + return ret !== 0; + } + /** + * Check if the drawable with the given ID is touching any pixel in the given rectangle. + * @param {number} drawable + * @param {any} rect + * @returns {boolean} + */ + drawable_touching_rect(drawable, rect) { + var ret = wasm.softwarerenderer_drawable_touching_rect(this.ptr, drawable, addHeapObject(rect)); + return ret !== 0; + } + /** + * Return the ID of the drawable that covers the most pixels in the given rectangle. + * Drawables earlier in the list will occlude those lower in the list. + * @param {Int32Array} candidates + * @param {any} rect + * @returns {number} + */ + pick(candidates, rect) { + var ptr0 = passArray32ToWasm0(candidates, wasm.__wbindgen_malloc); + var len0 = WASM_VECTOR_LEN; + var ret = wasm.softwarerenderer_pick(this.ptr, ptr0, len0, addHeapObject(rect)); + return ret; + } + /** + * Calculate the convex hull points for the drawable with the given ID. + * @param {number} drawable + * @returns {Float32Array} + */ + drawable_convex_hull_points(drawable) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.softwarerenderer_drawable_convex_hull_points(retptr, this.ptr, drawable); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var v0 = getArrayF32FromWasm0(r0, r1).slice(); + wasm.__wbindgen_free(r0, r1 * 4); + return v0; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } +} + +export const __wbg_left_e0e87a2e66be13a6 = function(arg0) { + var ret = getObject(arg0).left; + return ret; +}; + +export const __wbg_right_7b7bac033ade0b86 = function(arg0) { + var ret = getObject(arg0).right; + return ret; +}; + +export const __wbg_bottom_4666a55ceceeee8a = function(arg0) { + var ret = getObject(arg0).bottom; + return ret; +}; + +export const __wbg_top_84c6cfb6e6a6bd02 = function(arg0) { + var ret = getObject(arg0).top; + return ret; +}; + +export const __wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); +}; + +export const __wbg_ucolor_ec62c5e559a2a5a3 = function(arg0) { + var ret = getObject(arg0).u_color; + return ret; +}; + +export const __wbg_ufisheye_6aa56ae214de6428 = function(arg0) { + var ret = getObject(arg0).u_fisheye; + return ret; +}; + +export const __wbg_uwhirl_677f66c116ae8d9b = function(arg0) { + var ret = getObject(arg0).u_whirl; + return ret; +}; + +export const __wbg_upixelate_eb81083d476dfa89 = function(arg0) { + var ret = getObject(arg0).u_pixelate; + return ret; +}; + +export const __wbg_umosaic_7bc9d9ddd07459c3 = function(arg0) { + var ret = getObject(arg0).u_mosaic; + return ret; +}; + +export const __wbg_ubrightness_d29d8f78f9c8e71d = function(arg0) { + var ret = getObject(arg0).u_brightness; + return ret; +}; + +export const __wbg_ughost_d81ebfbc362e40b0 = function(arg0) { + var ret = getObject(arg0).u_ghost; + return ret; +}; + +export const __wbg_new_59cb74e423758ede = function() { + var ret = new Error(); + return addHeapObject(ret); +}; + +export const __wbg_stack_558ba5917b466edd = function(arg0, arg1) { + var ret = getObject(arg1).stack; + var ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; +}; + +export const __wbg_error_4bb6c2a97407129a = function(arg0, arg1) { + try { + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(arg0, arg1); + } +}; + +export const __wbindgen_throw = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); +}; + diff --git a/swrender/build/swrender_bg.wasm b/swrender/build/swrender_bg.wasm index abbdcdb0f92e8ee9bb9e6bc897f35d24c5eaa281..48dca247dc696b96b318e7f8366d8bb36d4f6130 100644 GIT binary patch literal 117048 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 literal 99119 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�A@+@SJPq14D@CJosn)wE>P={gQ)~dUo{mEx*K9$2*P|t91qSxj<1-neN~*$ z!tBV0_o#Rf_9@(<Hb@bQIr(P-bJu}&4eX_dlu|;MQaO=l^qANa4~1dsKOaoD;M9m| zVcWi@n!2INUlVQ4M)t$$$<<Zt4M~;t;0^Qx<iG+Jn&R1yk6m?$Tw8HfL_gs0ffF6? z1qf`{dZGbDWCs#+eNuE{ArD)KvI@!FIa1PZg|C}Y9=zg_XWK|#MIMUtl&Hs)+VC&# zi%H;}1+EL5UUfaBA;{*}#WgK6Zw7!`Sp@@1@)9fZ_6@c8!mzs=-ZaFzkqzT%YV@>d zwKO=w8Mcmc4-&^dB2maGg`Y4&dL%N8zi}w~k#G2_0)LWX+hCZk`^{k*V&XD5Ujg&$ zt!ynaVF0KBhlyffhy)bP(!V91)iT<Hoqp5V>C2%E$AM+(k?EU(`7Z~BeR#(Ne8O-; zVl@KWc}5(SLQ0Gox-&*jZ}P#wk-ru|57ZTyv2gyF&x-2Ev{S0XUxt<VRp4lcKhO8= z2(Oz^^zb?HgyEHV7fkB*Iu;CjEmQ(c_9zVgyoeMzL_1o86#K<SD}c5FJTMTZksV+E zSMfY9AX{tmck4hnHs6#3)-)!EgX1cceO-kcBlPe}#eWmE7|Y`W-pGW&T#;SzJU##m z{(9Snh69@m`Q?%JZShb-q`sx9a9nF<p!-PwRljp!o`j5ifDaipx{Hh;!~3on`)*>w z{+=jvG#h0_fWk3$jM>)|xOeei(UA*dXfGsmG^z?@3$Il~$QU){Py+?Qi3k}WW4zyg z{U%E&BhVY*ohCBgegfa$4@6@)!hqAFOKz|-EJH*JLqL2}hM_B~c&iEAEa49mnG*Xs z)+U({lygz5;gyuAgg3GnI;D782<duJ@S{ZC9$ksdM{ai^RRcj$yw1hKrWBZY;lGQ@ zk`^zhTZX1uC~t>~GPoV#;}XT|pyxw<Qz`gtTB;xY_#o<NNV5qnq*fzaID|*;CkIo< zrJO9Nv|xu=O7+Eqr~@Mt!EBgaNEk9*JNgfJ>R2GUTA??3LhS}k(=x-}4;_SQWSF+P zp+r9=^MZ+BB)?bV2-MHSLNh4eJZpyM<E3MXGUQZ6!#V<p$iT!)X^>liyt<zs)TojD zk0`1cArl;?|4Rvi$7^?HW5L+lipXT}k@$?T7V;(nn3}5bpJS?u$PY)zz>vXFWKXx< zUyNBAIh;N`*2pkGIL4HnUyfN?M-a$DHnt4}gq3XlYRuB6gY1V8u{6LQ=}XS9$1Dw} z1sG_A1a-v!d|&y^n5B_s3pXvYoDjgIeG0!Fv$U*303fwmfeEc4nDt*Gk446;5EizJ zH}tjg!X#*em`xN~h`w0ZvVnatzZ<(B#q^Q7rWuhAO~Fuvm+ROt)`fSK217n_*HWE* z2(b)-ip9*i&;f-WJR)Qbg9YC9bQ>Y1KV4a<s|#hk_a=Ecu0(mK3uJA9?96N5idQQR Y)!OBSioQ_pAttjRc8B4fy3myWKVvC4g8%>k diff --git a/swrender/build/swrender_bg.wasm.d.ts b/swrender/build/swrender_bg.wasm.d.ts new file mode 100644 index 000000000..0ba1f4645 --- /dev/null +++ b/swrender/build/swrender_bg.wasm.d.ts @@ -0,0 +1,19 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export function __wbg_softwarerenderer_free(a: number): void; +export function softwarerenderer_new(): number; +export function softwarerenderer_set_drawable(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number): void; +export function softwarerenderer_remove_drawable(a: number, b: number): void; +export function softwarerenderer_set_silhouette(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number): void; +export function softwarerenderer_remove_silhouette(a: number, b: number): void; +export function softwarerenderer_is_touching_drawables(a: number, b: number, c: number, d: number, e: number): number; +export function softwarerenderer_color_is_touching_color(a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number): number; +export function softwarerenderer_is_touching_color(a: number, b: number, c: number, d: number, e: number, f: number, g: number): number; +export function softwarerenderer_drawable_touching_rect(a: number, b: number, c: number): number; +export function softwarerenderer_pick(a: number, b: number, c: number, d: number): number; +export function softwarerenderer_drawable_convex_hull_points(a: number, b: number, c: number): void; +export function __wbindgen_malloc(a: number): number; +export function __wbindgen_add_to_stack_pointer(a: number): number; +export function __wbindgen_free(a: number, b: number): void; +export function __wbindgen_realloc(a: number, b: number, c: number): number;