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..57a928db9 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 bundler 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,13 +45,13 @@ "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" }, "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/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..e1ca60140 100644 --- a/src/Drawable.js +++ b/src/Drawable.js @@ -4,67 +4,21 @@ 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 { /** * 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. @@ -113,6 +67,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; @@ -124,8 +80,7 @@ class Drawable { this._transformedHullDirty = true; this._skinWasAltered = this._skinWasAltered.bind(this); - - this.isTouching = this._isTouchingNever; + this._silhouetteWasUpdated = this._silhouetteWasUpdated.bind(this); } /** @@ -134,6 +89,7 @@ class Drawable { dispose () { // Use the setter: disconnect events this.skin = null; + this._renderer.softwareRenderer.remove_drawable(this.id); } /** @@ -167,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(); } @@ -251,6 +209,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. @@ -268,6 +230,7 @@ class Drawable { if (effectInfo.shapeChanges) { this.setConvexHullDirty(); } + this.setEffectsDirty(); } /** @@ -474,36 +437,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, @@ -523,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; } @@ -597,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); } @@ -638,23 +575,30 @@ 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(); - // 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) { + effects = this._uniforms; + 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, + enabledEffects, + this.skin.useNearest(this._scale, this) + ); } /** @@ -668,6 +612,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. @@ -698,37 +650,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 deleted file mode 100644 index 7ffae18ab..000000000 --- a/src/EffectTransform.js +++ /dev/null @@ -1,197 +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; - -/** - * 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. - * @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 8248b1f8d..5e5fbd998 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; @@ -340,7 +334,7 @@ class PenSkin extends Skin { ); this._silhouetteImageData.data.set(this._silhouettePixels); - this._silhouette.update(this._silhouetteImageData, true /* isPremultiplied */); + this._setSilhouetteFromData(this._silhouetteImageData, true /* premultiplied */); this._silhouetteDirty = false; } diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index b3f6a0aaf..dc9a83c89 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,14 +10,21 @@ 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'); -const __isTouchingDrawablesPoint = twgl.v3.create(); +let onLoadSwRender = null; +let swRenderLoaded = false; +let swrender = null; + +import('../swrender/build/swrender') + .then(swrenderImport => { + swrender = swrenderImport; + swRenderLoaded = true; + if (onLoadSwRender) onLoadSwRender(); + }); + 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 @@ -49,21 +55,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, @@ -114,6 +105,16 @@ class RenderWebGL extends EventEmitter { return twgl.getWebGLContext(canvas, {alpha: false, stencil: true, antialias: false}); } + init () { + const swRenderPromise = swRenderLoaded ? Promise.resolve() : new Promise(resolve => { + onLoadSwRender = resolve; + }); + + return swRenderPromise.then(() => { + this.softwareRenderer = swrender.SoftwareRenderer.new(); + }); + } + /** * Create a renderer for drawing Scratch sprites to a canvas using WebGL. * Coordinates will default to Scratch 2.0 values if unspecified. @@ -140,7 +141,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 = []; @@ -477,7 +479,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); @@ -790,46 +792,41 @@ 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; + // "Color is touching color" should not ghost the drawable whose color is being masked + drawable.updateCPURenderAttributes(~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 () { @@ -857,7 +854,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; @@ -924,18 +921,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.height); imageData.data.set(pixels); context.putImageData(imageData, 0, 0); } @@ -969,28 +963,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); } /** @@ -1047,18 +1022,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); } /** @@ -1093,43 +1059,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); } /** @@ -1922,165 +1857,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. + const points = []; - // 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; + 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); } - // 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]); - } - - // Simplify boundary points using hull.js. - // TODO: Remove this; this algorithm already generates convex hulls. - 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; + return points; } /** diff --git a/src/SVGSkin.js b/src/SVGSkin.js index bf1aeca88..8cd92367a 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(); @@ -118,7 +115,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._setSilhouetteFromData(textureData); this._largestMIPScale = scale; } 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 deleted file mode 100644 index b96348a81..000000000 --- a/src/Silhouette.js +++ /dev/null @@ -1,257 +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; - -// 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. - * @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) -]; - -/** - * 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 () { - /** - * 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; - - // 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); - } - - /** - * 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, isPremultiplied = false) { - 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); - } - - 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; - } - - /** - * 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 ae98d50c9..9cc005235 100644 --- a/src/Skin.js +++ b/src/Skin.js @@ -3,17 +3,20 @@ const EventEmitter = require('events'); const twgl = require('twgl.js'); const RenderConstants = require('./RenderConstants'); -const Silhouette = require('./Silhouette'); 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; @@ -47,7 +50,10 @@ 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); this.setMaxListeners(RenderConstants.SKIN_SHARE_SOFT_LIMIT); } @@ -57,6 +63,7 @@ class Skin extends EventEmitter { */ dispose () { this._id = RenderConstants.ID_NONE; + this._renderer.softwareRenderer.remove_silhouette(this.id); } /** @@ -154,7 +161,7 @@ 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); } /** @@ -188,34 +195,28 @@ 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); } - /** - * 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); - } + _setSilhouetteFromData (data, premultiplied = false) { + const size = this.size; + this._renderer.softwareRenderer.set_silhouette( + this._id, + data.width, + data.height, + data.data, - /** - * 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); + size[0], + size[1], + + premultiplied + ); + + this.silhouetteSize[0] = data.width; + this.silhouetteSize[1] = data.height; + + this.emit(Skin.Events.SilhouetteUpdated); } } @@ -229,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/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/.gitignore b/swrender/.gitignore new file mode 100644 index 000000000..0a2f752b9 --- /dev/null +++ b/swrender/.gitignore @@ -0,0 +1,6 @@ +/target +**/*.rs.bk +Cargo.lock +bin/ +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/build/swrender.d.ts b/swrender/build/swrender.d.ts new file mode 100644 index 000000000..3b9fd9556 --- /dev/null +++ b/swrender/build/swrender.d.ts @@ -0,0 +1,93 @@ +/* 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..e70a70410 --- /dev/null +++ b/swrender/build/swrender.js @@ -0,0 +1,2 @@ +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.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.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 new file mode 100644 index 000000000..48dca247d Binary files /dev/null and b/swrender/build/swrender_bg.wasm differ 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; 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/convex_hull.rs b/swrender/src/convex_hull.rs new file mode 100644 index 000000000..4a880f46a --- /dev/null +++ b/swrender/src/convex_hull.rs @@ -0,0 +1,105 @@ +use crate::drawable::Drawable; +use crate::matrix::Vec2; +use crate::silhouette::Silhouette; + +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)) +} + +/// 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(); + + let transform = |p| { + transform_point( + p, + &drawable.effects, + drawable.effect_bits, + silhouette.nominal_size, + ) + }; + + 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( + (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 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 { + break; + } else { + left_hull.pop(); + } + } + + 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, + (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; + } + + // 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 { + break; + } else { + right_hull.pop(); + } + } + + 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 new file mode 100644 index 000000000..838e8b6c9 --- /dev/null +++ b/swrender/src/drawable.rs @@ -0,0 +1,91 @@ +use crate::effect_transform::{ + transform_color, transform_point, EffectBits, Effects, COLOR_EFFECT_MASK, + DISTORTION_EFFECT_MASK, +}; +use crate::matrix::*; +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 inverse_matrix: Mat4, + pub silhouette: SilhouetteID, + pub effects: Effects, + pub effect_bits: EffectBits, + pub use_nearest_neighbor: bool, +} + +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; + 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; + + Vec2(out_x, out_y) + } + + 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) + } + } + + /// 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); + 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); + + if self.use_nearest_neighbor { + silhouette.is_touching_nearest(local_position) + } else { + silhouette.is_touching_linear(local_position) + } + } + + /// 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); + 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 new file mode 100644 index 000000000..4e0c931c0 --- /dev/null +++ b/swrender/src/effect_transform.rs @@ -0,0 +1,287 @@ +use crate::matrix::*; + +use std::f32; +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 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); + +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; + } +} + +/// 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; + 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) +} + +/// 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); + } + + 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!(), + } +} + +/// 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] = [ + (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, + ] +} + +/// 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 { + /*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 new file mode 100644 index 000000000..7d4411bb0 --- /dev/null +++ b/swrender/src/lib.rs @@ -0,0 +1,391 @@ +mod convex_hull; +pub mod drawable; +mod effect_transform; +mod matrix; +pub mod silhouette; +mod utils; + +use wasm_bindgen::prelude::*; + +use std::collections::HashMap; +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")] +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +const ID_NONE: drawable::DrawableID = -1; + +#[wasm_bindgen] +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 + } + + /// 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, + matrix: Option>, + silhouette: Option, + effects: Option, + effect_bits: effect_transform::EffectBits, + use_nearest_neighbor: bool, + ) { + let d = self.drawables.entry(id).or_insert(drawable::Drawable { + inverse_matrix: [0.0; 16], + effects: effect_transform::Effects::default(), + effect_bits: 0, + silhouette: match silhouette { + Some(s) => s, + None => ID_NONE, + }, + use_nearest_neighbor, + id, + }); + + if let Some(m) = matrix { + let mat: matrix::Mat4 = (*m) + .try_into() + .expect("drawable's matrix contains 16 elements"); + d.inverse_matrix = mat.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; + 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, + w: u32, + h: u32, + data: Box<[u8]>, + 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), + premultiplied, + ); + } + + /// 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, + ) -> 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() + } + + /// 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, + { + 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(); + + 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) { + return true; + } + } + } + + 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, + candidates: Vec, + 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; + } + } + } + false + }, + rect, + drawable, + ) + } + + /// 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 + && (((a[0] ^ b[0]) & 0b11111100) + | ((a[1] ^ b[1]) & 0b11111100) + | ((a[2] ^ b[2]) & 0b11111100)) + == 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, + 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"); + 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; + } + } + false + }, + rect, + drawable, + ) + } + + /// Check if a certain drawable is touching a particular color. + 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"); + 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; + } + } + false + }, + rect, + drawable, + ) + } + + /// 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, + 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] + } + + /// 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, + rect: JSRectangle, + ) -> bool { + self.per_rect_pixel( + |position, drawable, silhouette| { + if drawable.is_touching(position, silhouette) { + return true; + } + false + }, + rect, + drawable, + ) + } + + /// 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, + 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 + } + + /// 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 + .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 + } +} diff --git a/swrender/src/matrix.rs b/swrender/src/matrix.rs new file mode 100644 index 000000000..bcc0c32ed --- /dev/null +++ b/swrender/src/matrix.rs @@ -0,0 +1,158 @@ +use std::f32; +use std::ops; + +pub type Mat4 = [f32; 16]; + +#[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) -> 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/silhouette.rs b/swrender/src/silhouette.rs new file mode 100644 index 000000000..8ad92411c --- /dev/null +++ b/swrender/src/silhouette.rs @@ -0,0 +1,119 @@ +use crate::matrix::Vec2; + +pub type SilhouetteID = i32; + +/// The CPU-side version of a Skin. +pub struct Silhouette { + pub id: SilhouetteID, + pub width: u32, + pub height: u32, + pub nominal_size: Vec2, + data: Box<[u8]>, + _blank: [u8; 4], +} + +impl Silhouette { + pub fn new(id: SilhouetteID) -> Silhouette { + Silhouette { + id, + width: 0, + height: 0, + nominal_size: Vec2(0f32, 0f32), + data: Box::new([0, 0, 0, 0]), + _blank: [0, 0, 0, 0], + } + } + + /// Update this silhouette with the bitmap data passed in from a Skin. + 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.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; + } + + /// 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 { + let idx = (((y as u32 * self.width) + x as u32) * 4) as usize; + self.data[idx + 3] != 0u8 + } + } + + /// 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 { + 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], + ] + } + } + + /// 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, + (vec.1 * self.height as f32) as i32, + ) + } + + /// 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, + (vec.1 * self.height as f32) as i32, + ) + } + + /// 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; + + self.get_point(x, y) + || self.get_point(x + 1, y) + || self.get_point(x, y + 1) + || self.get_point(x + 1, y + 1) + } +} 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: [