From ddb6d5e7cca039dcbab4435ee38b3013cf94f071 Mon Sep 17 00:00:00 2001 From: adroitwhiz 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 "] +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 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} */ 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, + silhouettes: HashMap +} + +#[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) { + 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, 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 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) { + pub fn set_drawable( + &mut self, + id: drawable::DrawableID, + matrix: Option>, + silhouette: Option, + effects: Option, + 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, rect: Rectangle) -> bool { + pub fn is_touching_drawables( + &mut self, + drawable: drawable::DrawableID, + candidates: + Vec, + 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 { + left: T, + right: T, + bottom: T, + top: T +} + +impl Rectangle { + 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 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} - */ -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 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>, silhouette: Option, effects: Option, - 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 + ) -> 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( + &self, + func: F, + rect: JSRectangle, drawable: drawable::DrawableID, - candidates: - Vec, - rect: rectangle::JSRectangle - ) -> bool { + candidates: Vec + ) -> 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, + 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, + 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, + 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 { - left: T, - right: T, - bottom: T, - top: T -} - -impl Rectangle { - 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 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: 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, 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, + rect: JSRectangle + ) -> drawable::DrawableID { + let mut hits: HashMap = 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 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. - */ - -/** - * 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 { + let mut left_hull: Vec = Vec::new(); + let mut right_hull: Vec = 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 { + 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 = 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 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 = Vec::new(); let mut right_hull: Vec = 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, - silhouettes: HashMap + silhouettes: HashMap, } #[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, effects: Option, 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 + candidates: Vec, ) -> 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( - &self, - func: F, - rect: JSRectangle, - drawable: drawable::DrawableID - ) -> bool - where F: Fn( - matrix::Vec2, - &drawable::Drawable, - &silhouette::Silhouette - ) -> bool { - + fn per_rect_pixel(&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, - 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, 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, 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, - rect: JSRectangle + rect: JSRectangle, ) -> drawable::DrawableID { let mut hits: HashMap = 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 { - 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 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 { let mut left_hull: Vec = Vec::new(); let mut right_hull: Vec = 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, @@ -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(&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, @@ -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 { 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 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 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 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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

)b5?zepK%5WWkhWH+!$)Ak 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�A@+@SJPq14D@CJosn)wE>P={gQ)~dUo{mEx*K9$2*P|t91qSxj<1-neN~*$ z!tBV0_o#Rf_9@(d zwKO=w8Mcmc4-&^dB2maGg`Y4&dL%N8zi}w~k#G2_0)LWX+hCZk`^{k*V&XD5Ujg&$ zt!ynaVF0KBhlyffhy)bP(!V91)iTC2%E$AM+(k?EU(`7Z~BeR#(Ne8O-; zVl@KWc}5(SLQ0Gox-&*jZ}P#wk-ru|57ZTyv2gyF&x-2Ev{S0XUxtw 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~GPoV#;}XT|pyxwR2GUTA??3LhS}k(=x-}4;_SQWSF+P zp+r9=^MZ+BB)?bV2-MHSLNh4eJZpyMliytvXFWKXx< 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>Y1KV4ak 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;

)b5?zepK%5WWkhWH+!$)Ak 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�A@+@SJPq14D@CJosn)wE>P={gQ)~dUo{mEx*K9$2*P|t91qSxj<1-neN~*$ z!tBV0_o#Rf_9@(d zwKO=w8Mcmc4-&^dB2maGg`Y4&dL%N8zi}w~k#G2_0)LWX+hCZk`^{k*V&XD5Ujg&$ zt!ynaVF0KBhlyffhy)bP(!V91)iTC2%E$AM+(k?EU(`7Z~BeR#(Ne8O-; zVl@KWc}5(SLQ0Gox-&*jZ}P#wk-ru|57ZTyv2gyF&x-2Ev{S0XUxtw 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~GPoV#;}XT|pyxwR2GUTA??3LhS}k(=x-}4;_SQWSF+P zp+r9=^MZ+BB)?bV2-MHSLNh4eJZpyMliytvXFWKXx< 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>Y1KV4ak literal 0 HcmV?d00001 From 2f8808e84e78cf0edd2dc32d75bff27e0814a2ed Mon Sep 17 00:00:00 2001 From: adroitwhiz 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 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 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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