Compare commits
1 Commits
greenkeepe
...
greenkeepe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a807f5f521 |
@@ -1,4 +1,3 @@
|
||||
dist/*
|
||||
node_modules/*
|
||||
playground/*
|
||||
tap-snapshots/*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['scratch', 'scratch/node', 'scratch/es6']
|
||||
extends: ['scratch', 'scratch/node']
|
||||
};
|
||||
|
||||
16
.npmignore
16
.npmignore
@@ -1,16 +0,0 @@
|
||||
# Development files
|
||||
.eslintrc.js
|
||||
/.editorconfig
|
||||
/.eslintignore
|
||||
/.gitattributes
|
||||
/.github
|
||||
/.jsdoc.json
|
||||
/.travis.yml
|
||||
/test
|
||||
/webpack.config.js
|
||||
|
||||
# Build created files
|
||||
/playground
|
||||
|
||||
# Exclude already built packages from testing with npm pack
|
||||
/scratch-render-*.{tar,tgz}
|
||||
|
||||
57
.travis.yml
57
.travis.yml
@@ -1,14 +1,9 @@
|
||||
language: node_js
|
||||
dist: trusty
|
||||
addons:
|
||||
chrome: stable
|
||||
node_js:
|
||||
- 8
|
||||
- node
|
||||
- 6
|
||||
- "node"
|
||||
env:
|
||||
- NODE_ENV=production
|
||||
before_install:
|
||||
- google-chrome-stable --headless --no-sandbox --remote-debugging-port=9222 &
|
||||
install:
|
||||
- npm --production=false install
|
||||
- npm --production=false update
|
||||
@@ -16,27 +11,27 @@ sudo: false
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
jobs:
|
||||
include:
|
||||
- stage: test
|
||||
script:
|
||||
- npm run lint
|
||||
- npm run docs
|
||||
- npm run tap
|
||||
- stage: deploy
|
||||
node_js: 8
|
||||
script: npm run build
|
||||
before_deploy:
|
||||
- VPKG=$($(npm bin)/json -f package.json version)
|
||||
- export VERSION=${VPKG}-prerelease.$(date +%Y%m%d%H%M%S)
|
||||
- npm --no-git-tag-version version $VERSION
|
||||
- git config --global user.email $(git log --pretty=format:"%ae" -n1)
|
||||
- git config --global user.name $(git log --pretty=format:"%an" -n1)
|
||||
deploy:
|
||||
provider: npm
|
||||
skip_cleanup: true
|
||||
"on":
|
||||
all_branches: true
|
||||
condition: $RELEASE_BRANCHES =~ $TRAVIS_BRANCH
|
||||
email: $NPM_EMAIL
|
||||
api_key: $NPM_TOKEN
|
||||
after_script:
|
||||
- |
|
||||
# RELEASE_BRANCHES and NPM_TOKEN defined in Travis settings panel
|
||||
declare exitCode
|
||||
$(npm bin)/travis-after-all
|
||||
exitCode=$?
|
||||
if [[
|
||||
# Execute after all jobs finish successfully
|
||||
$exitCode = 0 &&
|
||||
# Only release on release branches
|
||||
$RELEASE_BRANCHES =~ $TRAVIS_BRANCH &&
|
||||
# Don't release on PR builds
|
||||
$TRAVIS_PULL_REQUEST = "false"
|
||||
]]; then
|
||||
# Authenticate NPM
|
||||
echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" > .npmrc
|
||||
# Set version to timestamp
|
||||
npm --no-git-tag-version version $($(npm bin)/json -f package.json version)-prerelease.$(date +%s)
|
||||
npm publish
|
||||
# Publish to gh-pages as most recent committer
|
||||
git config --global user.email $(git log --pretty=format:"%ce" -n1)
|
||||
git config --global user.name $(git log --pretty=format:"%cn" -n1)
|
||||
./node_modules/.bin/gh-pages -x -r https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git -d playground -m "Build for $(git log --pretty=format:%H)"
|
||||
fi
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
The Scratch trademarks, including the Scratch name, logo, the Scratch Cat, Gobo, Pico, Nano, Tera and Giga graphics (the "Marks"), are property of the Massachusetts Institute of Technology (MIT). Marks may not be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
The Scratch trademarks, including the Scratch name, logo, the Scratch Cat, Gobo, Pico, Nano, Tera and Giga graphics (the "Marks"), are property of the Massachusetts Institute of Technology (MIT), and the use of the Marks is governed by this policy.
|
||||
|
||||
You may use the Marks to refer to Scratch in Substantially Unmodified form.
|
||||
|
||||
"Substantially Unmodified" means the source code provided by MIT, possibly with minor modifications including but not limited to: bug fixes (including security), changing the locations of files for better integration with the host operating system, adding documentation, and changes to the dynamic linking of libraries.
|
||||
|
||||
A version is not "Substantially Unmodified" if it incorporates features not present in a release of Scratch by MIT. If you do make a substantial modification, to avoid confusion with versions of Scratch produced by MIT you must remove all Marks from your version of the software and refrain from using any of the Marks to refer to your version.
|
||||
|
||||
38
package.json
38
package.json
@@ -10,7 +10,7 @@
|
||||
"url": "git+ssh://git@github.com/LLK/scratch-render.git"
|
||||
},
|
||||
"main": "./dist/node/scratch-render.js",
|
||||
"browser": "./src/index.js",
|
||||
"browser": "./dist/web/scratch-render.js",
|
||||
"scripts": {
|
||||
"build": "webpack --progress --colors",
|
||||
"docs": "jsdoc -c .jsdoc.json",
|
||||
@@ -18,42 +18,36 @@
|
||||
"prepublish": "npm run build",
|
||||
"prepublish-watch": "npm run watch",
|
||||
"start": "webpack-dev-server",
|
||||
"tap": "tap test/unit test/integration",
|
||||
"test": "npm run lint && npm run docs && npm run build && npm run tap",
|
||||
"tap": "./node_modules/.bin/tap ./test/unit/*.js",
|
||||
"test": "npm run lint && npm run docs && 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-eslint": "^8.2.1",
|
||||
"babel-loader": "^7.1.4",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-polyfill": "^6.22.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"chromeless": "^1.5.1",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
"babel-preset-es2015": "^6.22.0",
|
||||
"base64-loader": "^1.0.0",
|
||||
"copy-webpack-plugin": "^4.0.1",
|
||||
"docdash": "^0.4.0",
|
||||
"eslint": "^4.6.1",
|
||||
"eslint-config-scratch": "^5.0.0",
|
||||
"gh-pages": "^1.0.0",
|
||||
"jsdoc": "^3.5.5",
|
||||
"json": "^9.0.4",
|
||||
"scratch-vm": "0.1.0-prerelease.1527254075",
|
||||
"tap": "^11.0.0",
|
||||
"travis-after-all": "^1.4.4",
|
||||
"uglifyjs-webpack-plugin": "^1.2.5",
|
||||
"webpack": "^4.15.1",
|
||||
"webpack-cli": "^2.0.15",
|
||||
"webpack-dev-server": "^3.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"grapheme-breaker": "0.3.2",
|
||||
"hull.js": "0.2.10",
|
||||
"ify-loader": "1.0.4",
|
||||
"jsdoc": "^3.5.5",
|
||||
"json": "^9.0.4",
|
||||
"linebreak": "0.3.0",
|
||||
"minilog": "3.1.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"scratch-storage": "^0.4.0",
|
||||
"scratch-svg-renderer": "0.2.0-prerelease.20180613184320",
|
||||
"twgl.js": "4.4.0"
|
||||
"scratch-svg-renderer": "0.1.0-prerelease.20180329174139",
|
||||
"tap": "^11.0.0",
|
||||
"travis-after-all": "^1.4.4",
|
||||
"twgl.js": "4.4.0",
|
||||
"webpack": "^4.5.0",
|
||||
"webpack-dev-server": "^2.8.2",
|
||||
"xml-escape": "1.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const twgl = require('twgl.js');
|
||||
|
||||
const Skin = require('./Skin');
|
||||
const Silhouette = require('./Silhouette');
|
||||
|
||||
class BitmapSkin extends Skin {
|
||||
/**
|
||||
@@ -23,6 +24,8 @@ class BitmapSkin extends Skin {
|
||||
|
||||
/** @type {Array<int>} */
|
||||
this._textureSize = [0, 0];
|
||||
|
||||
this._silhouette = new Silhouette();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,7 +90,7 @@ class BitmapSkin extends Skin {
|
||||
}
|
||||
|
||||
// Do these last in case any of the above throws an exception
|
||||
this._costumeResolution = costumeResolution || 2;
|
||||
this._costumeResolution = costumeResolution || 1;
|
||||
this._textureSize = BitmapSkin._getBitmapSize(bitmapData);
|
||||
|
||||
if (typeof rotationCenter === 'undefined') rotationCenter = this.calculateRotationCenter();
|
||||
@@ -114,6 +117,14 @@ class BitmapSkin extends Skin {
|
||||
return [bitmapData.width, bitmapData.height];
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this point touch an opaque or translucent point on this skin?
|
||||
* @param {twgl.v3} vec A texture coordinate.
|
||||
* @return {boolean} Did it touch?
|
||||
*/
|
||||
isTouching (vec) {
|
||||
return this._silhouette.isTouching(vec);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BitmapSkin;
|
||||
|
||||
@@ -376,7 +376,7 @@ class Drawable {
|
||||
* @return {boolean} True if the world position touches the skin.
|
||||
*/
|
||||
isTouching (vec) {
|
||||
if (!(this.skin && this._visible)) {
|
||||
if (!this.skin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -411,32 +411,7 @@ class Drawable {
|
||||
// Apply texture effect transform.
|
||||
EffectTransform.transformPoint(this, localPosition, localPosition);
|
||||
|
||||
if (this.useNearest) {
|
||||
return this.skin.isTouchingNearest(localPosition);
|
||||
}
|
||||
return this.skin.isTouchingLinear(localPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the drawable use NEAREST NEIGHBOR or LINEAR INTERPOLATION mode
|
||||
*/
|
||||
get useNearest () {
|
||||
// We can't use nearest neighbor unless we are a multiple of 90 rotation
|
||||
if (this._direction % 90 !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Raster skins (bitmaps) should always prefer nearest neighbor
|
||||
if (this.skin.isRaster) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the scale of the skin is very close to 100 (0.99999 variance is okay I guess)
|
||||
if (Math.abs(this.scale[0]) > 99 && Math.abs(this.scale[0]) < 101 &&
|
||||
Math.abs(this.scale[1]) > 99 && Math.abs(this.scale[1]) < 101) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return this.skin.isTouching(localPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,9 +26,10 @@ class EffectTransform {
|
||||
* @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.
|
||||
* @return {twgl.v3} The coordinate after being transform by effects.
|
||||
*/
|
||||
static transformPoint (drawable, vec, dst = twgl.v3.create()) {
|
||||
static transformPoint (drawable, vec, dst) {
|
||||
dst = dst || twgl.v3.create();
|
||||
twgl.v3.copy(vec, dst);
|
||||
|
||||
const uniforms = drawable.getUniforms();
|
||||
|
||||
@@ -2,6 +2,7 @@ const twgl = require('twgl.js');
|
||||
|
||||
const RenderConstants = require('./RenderConstants');
|
||||
const Skin = require('./Skin');
|
||||
const Silhouette = require('./Silhouette');
|
||||
|
||||
/**
|
||||
* Attributes to use when drawing with the pen
|
||||
@@ -49,6 +50,9 @@ class PenSkin extends Skin {
|
||||
/** @type {WebGLTexture} */
|
||||
this._texture = null;
|
||||
|
||||
/** @type {Silhouette} */
|
||||
this._silhouette = new Silhouette();
|
||||
|
||||
/** @type {boolean} */
|
||||
this._silhouetteDirty = false;
|
||||
|
||||
@@ -210,16 +214,18 @@ class PenSkin extends Skin {
|
||||
}
|
||||
|
||||
/**
|
||||
* If there have been pen operations that have dirtied the canvas, update
|
||||
* now before someone wants to use our silhouette.
|
||||
* Does this point touch an opaque or translucent point on this skin?
|
||||
* @param {twgl.v3} vec A texture coordinate.
|
||||
* @return {boolean} Did it touch?
|
||||
*/
|
||||
updateSilhouette () {
|
||||
isTouching (vec) {
|
||||
if (this._silhouetteDirty) {
|
||||
if (this._canvasDirty) {
|
||||
this.getTexture();
|
||||
}
|
||||
this._silhouette.update(this._canvas);
|
||||
}
|
||||
return this._silhouette.isTouching(vec);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ class Rectangle {
|
||||
this.right = -Infinity;
|
||||
this.top = -Infinity;
|
||||
this.bottom = Infinity;
|
||||
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const x = points[i][0];
|
||||
const y = points[i][1];
|
||||
@@ -115,41 +114,6 @@ class Rectangle {
|
||||
this.top = Math.ceil(this.top);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the intersection of two bounding Rectangles.
|
||||
* Could be an impossible box if they don't intersect.
|
||||
* @param {Rectangle} a One rectangle
|
||||
* @param {Rectangle} b Other rectangle
|
||||
* @param {?Rectangle} result A resulting storage rectangle (safe to pass
|
||||
* a or b if you want to overwrite one)
|
||||
* @returns {Rectangle} resulting rectangle
|
||||
*/
|
||||
static intersect (a, b, result = new Rectangle()) {
|
||||
result.left = Math.max(a.left, b.left);
|
||||
result.right = Math.min(a.right, b.right);
|
||||
result.top = Math.min(a.top, b.top);
|
||||
result.bottom = Math.max(a.bottom, b.bottom);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the union of two bounding Rectangles.
|
||||
* @param {Rectangle} a One rectangle
|
||||
* @param {Rectangle} b Other rectangle
|
||||
* @param {?Rectangle} result A resulting storage rectangle (safe to pass
|
||||
* a or b if you want to overwrite one)
|
||||
* @returns {Rectangle} resulting rectangle
|
||||
*/
|
||||
static union (a, b, result = new Rectangle()) {
|
||||
result.left = Math.min(a.left, b.left);
|
||||
result.right = Math.max(a.right, b.right);
|
||||
// Scratch Space - +y is up
|
||||
result.top = Math.max(a.top, b.top);
|
||||
result.bottom = Math.min(a.bottom, b.bottom);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Width of the Rectangle.
|
||||
* @return {number} Width of rectangle.
|
||||
@@ -165,7 +129,6 @@ class Rectangle {
|
||||
get height () {
|
||||
return Math.abs(this.top - this.bottom);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Rectangle;
|
||||
|
||||
@@ -12,10 +12,6 @@ const ShaderManager = require('./ShaderManager');
|
||||
const SVGSkin = require('./SVGSkin');
|
||||
const SVGTextBubble = require('./util/svg-text-bubble');
|
||||
const EffectTransform = require('./EffectTransform');
|
||||
const log = require('./util/log');
|
||||
|
||||
const __isTouchingDrawablesPoint = twgl.v3.create();
|
||||
const __candidatesBounds = new Rectangle();
|
||||
|
||||
/**
|
||||
* @callback RenderWebGL#idFilterFunc
|
||||
@@ -121,22 +117,6 @@ class RenderWebGL extends EventEmitter {
|
||||
/** @type {Array<int>} */
|
||||
this._drawList = [];
|
||||
|
||||
// A list of layer group names in the order they should appear
|
||||
// from furthest back to furthest in front.
|
||||
/** @type {Array<String>} */
|
||||
this._groupOrdering = [];
|
||||
|
||||
/**
|
||||
* @typedef LayerGroup
|
||||
* @property {int} groupIndex The relative position of this layer group in the group ordering
|
||||
* @property {int} drawListOffset The absolute position of this layer group in the draw list
|
||||
* This number gets updated as drawables get added to or deleted from the draw list.
|
||||
*/
|
||||
|
||||
// Map of group name to layer group
|
||||
/** @type {Object.<string, LayerGroup>} */
|
||||
this._layerGroups = {};
|
||||
|
||||
/** @type {int} */
|
||||
this._nextDrawableId = RenderConstants.ID_NONE + 1;
|
||||
|
||||
@@ -315,29 +295,6 @@ class RenderWebGL extends EventEmitter {
|
||||
|
||||
const newSkin = new SVGSkin(skinId, this);
|
||||
newSkin.setSVG(svgData, rotationCenter);
|
||||
this._reskin(skinId, newSkin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing bitmap skin, or create a bitmap skin if the previous skin was not bitmap.
|
||||
* @param {!int} skinId the ID for the skin to change.
|
||||
* @param {!ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} imgData - new contents for this skin.
|
||||
* @param {!number} bitmapResolution - the resolution scale for a bitmap costume.
|
||||
* @param {?Array<number>} rotationCenter Optional: rotation center of the skin. If not supplied, the center of the
|
||||
* skin will be used
|
||||
*/
|
||||
updateBitmapSkin (skinId, imgData, bitmapResolution, rotationCenter) {
|
||||
if (this._allSkins[skinId] instanceof BitmapSkin) {
|
||||
this._allSkins[skinId].setBitmap(imgData, bitmapResolution, rotationCenter);
|
||||
return;
|
||||
}
|
||||
|
||||
const newSkin = new BitmapSkin(skinId, this);
|
||||
newSkin.setBitmap(imgData, bitmapResolution, rotationCenter);
|
||||
this._reskin(skinId, newSkin);
|
||||
}
|
||||
|
||||
_reskin (skinId, newSkin) {
|
||||
const oldSkin = this._allSkins[skinId];
|
||||
this._allSkins[skinId] = newSkin;
|
||||
|
||||
@@ -345,8 +302,6 @@ class RenderWebGL extends EventEmitter {
|
||||
for (const drawable of this._allDrawables) {
|
||||
if (drawable && drawable.skin === oldSkin) {
|
||||
drawable.skin = newSkin;
|
||||
drawable.setConvexHullDirty();
|
||||
drawable.setTransformDirty();
|
||||
}
|
||||
}
|
||||
oldSkin.dispose();
|
||||
@@ -377,100 +332,31 @@ class RenderWebGL extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Create a new Drawable and add it to the scene.
|
||||
* @param {string} group Layer group to add the drawable to
|
||||
* @returns {int} The ID of the new Drawable.
|
||||
*/
|
||||
createDrawable (group) {
|
||||
if (!group || !this._layerGroups.hasOwnProperty(group)) {
|
||||
log.warn('Cannot create a drawable without a known layer group');
|
||||
return;
|
||||
}
|
||||
createDrawable () {
|
||||
const drawableID = this._nextDrawableId++;
|
||||
const drawable = new Drawable(drawableID);
|
||||
this._allDrawables[drawableID] = drawable;
|
||||
this._addToDrawList(drawableID, group);
|
||||
this._drawList.push(drawableID);
|
||||
|
||||
drawable.skin = null;
|
||||
|
||||
return drawableID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the layer group ordering for the renderer.
|
||||
* @param {Array<string>} groupOrdering The ordered array of layer group
|
||||
* names
|
||||
*/
|
||||
setLayerGroupOrdering (groupOrdering) {
|
||||
this._groupOrdering = groupOrdering;
|
||||
for (let i = 0; i < this._groupOrdering.length; i++) {
|
||||
this._layerGroups[this._groupOrdering[i]] = {
|
||||
groupIndex: i,
|
||||
drawListOffset: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
_addToDrawList (drawableID, group) {
|
||||
const currentLayerGroup = this._layerGroups[group];
|
||||
const currentGroupOrderingIndex = currentLayerGroup.groupIndex;
|
||||
|
||||
const drawListOffset = this._endIndexForKnownLayerGroup(currentLayerGroup);
|
||||
this._drawList.splice(drawListOffset, 0, drawableID);
|
||||
|
||||
this._updateOffsets('add', currentGroupOrderingIndex);
|
||||
}
|
||||
|
||||
_updateOffsets (updateType, currentGroupOrderingIndex) {
|
||||
for (let i = currentGroupOrderingIndex + 1; i < this._groupOrdering.length; i++) {
|
||||
const laterGroupName = this._groupOrdering[i];
|
||||
if (updateType === 'add') {
|
||||
this._layerGroups[laterGroupName].drawListOffset++;
|
||||
} else if (updateType === 'delete'){
|
||||
this._layerGroups[laterGroupName].drawListOffset--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Given a layer group, return the index where it ends (non-inclusive),
|
||||
// e.g. the returned index does not have a drawable from this layer group in it)
|
||||
_endIndexForKnownLayerGroup (layerGroup) {
|
||||
const groupIndex = layerGroup.groupIndex;
|
||||
if (groupIndex === this._groupOrdering.length - 1) {
|
||||
return this._drawList.length;
|
||||
}
|
||||
return this._layerGroups[this._groupOrdering[groupIndex + 1]].drawListOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a Drawable, removing it from the scene.
|
||||
* @param {int} drawableID The ID of the Drawable to remove.
|
||||
* @param {string} group Group name that the drawable belongs to
|
||||
*/
|
||||
destroyDrawable (drawableID, group) {
|
||||
if (!group || !this._layerGroups.hasOwnProperty(group)) {
|
||||
log.warn('Cannot destroy drawable without known layer group.');
|
||||
return;
|
||||
}
|
||||
destroyDrawable (drawableID) {
|
||||
const drawable = this._allDrawables[drawableID];
|
||||
drawable.dispose();
|
||||
delete this._allDrawables[drawableID];
|
||||
|
||||
const currentLayerGroup = this._layerGroups[group];
|
||||
const endIndex = this._endIndexForKnownLayerGroup(currentLayerGroup);
|
||||
|
||||
let index = currentLayerGroup.drawListOffset;
|
||||
while (index < endIndex) {
|
||||
if (this._drawList[index] === drawableID) {
|
||||
break;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
if (index < endIndex) {
|
||||
let index;
|
||||
while ((index = this._drawList.indexOf(drawableID)) >= 0) {
|
||||
this._drawList.splice(index, 1);
|
||||
this._updateOffsets('delete', currentLayerGroup.groupIndex);
|
||||
} else {
|
||||
log.warn('Could not destroy drawable that could not be found in layer group.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,55 +369,28 @@ class RenderWebGL extends EventEmitter {
|
||||
* "go to front": setDrawableOrder(id, Infinity);
|
||||
* @param {int} drawableID ID of Drawable to reorder.
|
||||
* @param {number} order New absolute order or relative order adjusment.
|
||||
* @param {string=} group Name of layer group drawable belongs to.
|
||||
* Reordering will not take place if drawable cannot be found within the bounds
|
||||
* of the layer group.
|
||||
* @param {boolean=} optIsRelative If set, `order` refers to a relative change.
|
||||
* @param {number=} optMin If set, order constrained to be at least `optMin`.
|
||||
* @return {?number} New order if changed, or null.
|
||||
*/
|
||||
setDrawableOrder (drawableID, order, group, optIsRelative, optMin) {
|
||||
if (!group || !this._layerGroups.hasOwnProperty(group)) {
|
||||
log.warn('Cannot set the order of a drawable without a known layer group.');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentLayerGroup = this._layerGroups[group];
|
||||
const startIndex = currentLayerGroup.drawListOffset;
|
||||
const endIndex = this._endIndexForKnownLayerGroup(currentLayerGroup);
|
||||
|
||||
let oldIndex = startIndex;
|
||||
while (oldIndex < endIndex) {
|
||||
if (this._drawList[oldIndex] === drawableID) {
|
||||
break;
|
||||
}
|
||||
oldIndex++;
|
||||
}
|
||||
|
||||
if (oldIndex < endIndex) {
|
||||
setDrawableOrder (drawableID, order, optIsRelative, optMin) {
|
||||
const oldIndex = this._drawList.indexOf(drawableID);
|
||||
if (oldIndex >= 0) {
|
||||
// Remove drawable from the list.
|
||||
if (order === 0) {
|
||||
return oldIndex;
|
||||
}
|
||||
|
||||
const _ = this._drawList.splice(oldIndex, 1)[0];
|
||||
const drawable = this._drawList.splice(oldIndex, 1)[0];
|
||||
// Determine new index.
|
||||
let newIndex = order;
|
||||
if (optIsRelative) {
|
||||
newIndex += oldIndex;
|
||||
}
|
||||
|
||||
const possibleMin = (optMin || 0) + startIndex;
|
||||
const min = (possibleMin >= startIndex && possibleMin < endIndex) ? possibleMin : startIndex;
|
||||
newIndex = Math.max(newIndex, min);
|
||||
|
||||
newIndex = Math.min(newIndex, endIndex);
|
||||
|
||||
if (optMin) {
|
||||
newIndex = Math.max(newIndex, optMin);
|
||||
}
|
||||
newIndex = Math.max(newIndex, 0);
|
||||
// Insert at new index.
|
||||
this._drawList.splice(newIndex, 0, drawableID);
|
||||
return newIndex;
|
||||
this._drawList.splice(newIndex, 0, drawable);
|
||||
return this._drawList.indexOf(drawable);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -619,29 +478,9 @@ class RenderWebGL extends EventEmitter {
|
||||
* @param {int} drawableID The ID of the Drawable to measure.
|
||||
* @return {Array<number>} Skin size, width and height.
|
||||
*/
|
||||
getCurrentSkinSize (drawableID) {
|
||||
getSkinSize (drawableID) {
|
||||
const drawable = this._allDrawables[drawableID];
|
||||
return this.getSkinSize(drawable.skin.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of a skin by ID.
|
||||
* @param {int} skinID The ID of the Skin to measure.
|
||||
* @return {Array<number>} Skin size, width and height.
|
||||
*/
|
||||
getSkinSize (skinID) {
|
||||
const skin = this._allSkins[skinID];
|
||||
return skin.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rotation center of a skin by ID.
|
||||
* @param {int} skinID The ID of the Skin
|
||||
* @return {Array<number>} The rotationCenterX and rotationCenterY
|
||||
*/
|
||||
getSkinRotationCenter (skinID) {
|
||||
const skin = this._allSkins[skinID];
|
||||
return skin.calculateRotationCenter();
|
||||
return drawable.skin.size;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -656,14 +495,14 @@ class RenderWebGL extends EventEmitter {
|
||||
const gl = this._gl;
|
||||
twgl.bindFramebufferInfo(gl, this._queryBufferInfo);
|
||||
|
||||
const candidates = this._candidatesTouching(drawableID, this._drawList);
|
||||
if (candidates.length === 0) {
|
||||
const bounds = this._touchingBounds(drawableID);
|
||||
if (!bounds) {
|
||||
return false;
|
||||
}
|
||||
const candidateIDs = this._filterCandidatesTouching(drawableID, this._drawList, bounds);
|
||||
if (!candidateIDs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bounds = this._candidatesBounds(candidates);
|
||||
|
||||
const candidateIDs = candidates.map(({id}) => id);
|
||||
|
||||
// Limit size of viewport to the bounds around the target Drawable,
|
||||
// and create the projection matrix for the draw.
|
||||
@@ -747,36 +586,72 @@ class RenderWebGL extends EventEmitter {
|
||||
/**
|
||||
* Check if a particular Drawable is touching any in a set of Drawables.
|
||||
* @param {int} drawableID The ID of the Drawable to check.
|
||||
* @param {?Array<int>} candidateIDs The Drawable IDs to check, otherwise all drawables in the renderer
|
||||
* @returns {boolean} True if the Drawable is touching one of candidateIDs.
|
||||
* @param {Array<int>} candidateIDs The Drawable IDs to check, otherwise all.
|
||||
* @returns {boolean} True iff the Drawable is touching one of candidateIDs.
|
||||
*/
|
||||
isTouchingDrawables (drawableID, candidateIDs = this._drawList) {
|
||||
isTouchingDrawables (drawableID, candidateIDs) {
|
||||
candidateIDs = candidateIDs || this._drawList;
|
||||
|
||||
const candidates = this._candidatesTouching(drawableID, candidateIDs);
|
||||
if (candidates.length === 0) {
|
||||
const gl = this._gl;
|
||||
|
||||
twgl.bindFramebufferInfo(gl, this._queryBufferInfo);
|
||||
|
||||
const bounds = this._touchingBounds(drawableID);
|
||||
if (!bounds) {
|
||||
return false;
|
||||
}
|
||||
candidateIDs = this._filterCandidatesTouching(drawableID, candidateIDs, bounds);
|
||||
if (!candidateIDs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the union of all the candidates intersections.
|
||||
const bounds = this._candidatesBounds(candidates);
|
||||
// Limit size of viewport to the bounds around the target Drawable,
|
||||
// and create the projection matrix for the draw.
|
||||
gl.viewport(0, 0, bounds.width, bounds.height);
|
||||
const projection = twgl.m4.ortho(bounds.left, bounds.right, bounds.top, bounds.bottom, -1, 1);
|
||||
|
||||
const drawable = this._allDrawables[drawableID];
|
||||
const point = __isTouchingDrawablesPoint;
|
||||
const noneColor = Drawable.color4fFromID(RenderConstants.ID_NONE);
|
||||
gl.clearColor.apply(gl, noneColor);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
gl.enable(gl.STENCIL_TEST);
|
||||
gl.stencilFunc(gl.ALWAYS, 1, 1);
|
||||
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
|
||||
gl.colorMask(false, false, false, false);
|
||||
this._drawThese([drawableID], ShaderManager.DRAW_MODE.silhouette, projection);
|
||||
|
||||
gl.stencilFunc(gl.EQUAL, 1, 1);
|
||||
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
|
||||
gl.colorMask(true, true, true, true);
|
||||
|
||||
this._drawThese(candidateIDs, ShaderManager.DRAW_MODE.silhouette, projection,
|
||||
{idFilterFunc: testID => testID !== drawableID}
|
||||
);
|
||||
} finally {
|
||||
gl.colorMask(true, true, true, true);
|
||||
gl.disable(gl.STENCIL_TEST);
|
||||
}
|
||||
|
||||
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);
|
||||
imageData.data.set(pixels);
|
||||
context.putImageData(imageData, 0, 0);
|
||||
}
|
||||
|
||||
for (let pixelBase = 0; pixelBase < pixels.length; pixelBase += 4) {
|
||||
const pixelID = Drawable.color3bToID(
|
||||
pixels[pixelBase],
|
||||
pixels[pixelBase + 1],
|
||||
pixels[pixelBase + 2]);
|
||||
if (pixelID > RenderConstants.ID_NONE) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1045,47 +920,27 @@ class RenderWebGL extends EventEmitter {
|
||||
* could possibly intersect the given bounds.
|
||||
* @param {int} drawableID - ID for drawable of query.
|
||||
* @param {Array<int>} candidateIDs - Candidates for touching query.
|
||||
* @return {?Array< {id, drawable, intersection} >} Filtered candidates with useful data.
|
||||
* @param {Rectangle} bounds - Bounds to limit candidates to.
|
||||
* @return {?Array<int>} Filtered candidateIDs, or null if none.
|
||||
*/
|
||||
_candidatesTouching (drawableID, candidateIDs) {
|
||||
const bounds = this._touchingBounds(drawableID);
|
||||
if (!bounds) {
|
||||
return [];
|
||||
_filterCandidatesTouching (drawableID, candidateIDs, bounds) {
|
||||
// Filter candidates by rough bounding box intersection.
|
||||
// Do this before _drawThese, so we can prevent any GL operations
|
||||
// and readback by returning early.
|
||||
candidateIDs = candidateIDs.filter(testID => {
|
||||
if (testID === drawableID) return false;
|
||||
// Only draw items which could possibly overlap target Drawable.
|
||||
const candidate = this._allDrawables[testID];
|
||||
const candidateBounds = candidate.getFastBounds();
|
||||
return bounds.intersects(candidateBounds);
|
||||
});
|
||||
if (candidateIDs.length === 0) {
|
||||
// No possible intersections based on bounding boxes.
|
||||
return null;
|
||||
}
|
||||
return candidateIDs.reduce((result, id) => {
|
||||
if (id !== drawableID) {
|
||||
const drawable = this._allDrawables[id];
|
||||
const candidateBounds = drawable.getFastBounds();
|
||||
|
||||
if (bounds.intersects(candidateBounds)) {
|
||||
result.push({
|
||||
id,
|
||||
drawable,
|
||||
intersection: Rectangle.intersect(bounds, candidateBounds)
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
return candidateIDs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get the union bounds from a set of candidates returned from the above method
|
||||
* @private
|
||||
* @param {Array<object>} candidates info from _candidatesTouching
|
||||
* @return {Rectangle} the outer bounding box union
|
||||
*/
|
||||
_candidatesBounds (candidates) {
|
||||
return candidates.reduce((memo, {intersection}) => {
|
||||
if (!memo) {
|
||||
return intersection;
|
||||
}
|
||||
// store the union of the two rectangles in our static rectangle instance
|
||||
return Rectangle.union(memo, intersection, __candidatesBounds);
|
||||
}, null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the position, direction, scale, or effect properties of this Drawable.
|
||||
* @param {int} drawableID The ID of the Drawable to update.
|
||||
@@ -1310,6 +1165,12 @@ class RenderWebGL extends EventEmitter {
|
||||
* @private
|
||||
*/
|
||||
_drawThese (drawables, drawMode, projection, opts = {}) {
|
||||
const near = function (a, b, relativeTolerance = 0.01) {
|
||||
const absA = Math.abs(a);
|
||||
const absB = Math.abs(b);
|
||||
const error = Math.abs(a - b) / Math.max(absA, absB);
|
||||
return error < relativeTolerance;
|
||||
};
|
||||
|
||||
const gl = this._gl;
|
||||
let currentShader = null;
|
||||
@@ -1328,11 +1189,7 @@ class RenderWebGL extends EventEmitter {
|
||||
// the ignoreVisibility flag is used (e.g. for stamping or touchingColor).
|
||||
if (!drawable.getVisible() && !opts.ignoreVisibility) continue;
|
||||
|
||||
// Combine drawable scale with the native vs. backing pixel ratio
|
||||
const drawableScale = [
|
||||
drawable.scale[0] * this._gl.canvas.width / this._nativeSize[0],
|
||||
drawable.scale[1] * this._gl.canvas.height / this._nativeSize[1]
|
||||
];
|
||||
const drawableScale = drawable.scale;
|
||||
|
||||
// If the skin or texture isn't ready yet, skip it.
|
||||
if (!drawable.skin || !drawable.skin.getTexture(drawableScale)) continue;
|
||||
@@ -1362,9 +1219,9 @@ class RenderWebGL extends EventEmitter {
|
||||
}
|
||||
|
||||
if (uniforms.u_skin) {
|
||||
twgl.setTextureParameters(
|
||||
gl, uniforms.u_skin, {minMag: drawable.useNearest ? gl.NEAREST : gl.LINEAR}
|
||||
);
|
||||
const useNearest =
|
||||
(drawable._direction % 90 === 0) && (near(drawableScale, 100) || drawable.skin.isRaster);
|
||||
twgl.setTextureParameters(gl, uniforms.u_skin, {minMag: useNearest ? gl.NEAREST : gl.LINEAR});
|
||||
}
|
||||
|
||||
twgl.setUniforms(currentShader, uniforms);
|
||||
@@ -1430,7 +1287,7 @@ class RenderWebGL extends EventEmitter {
|
||||
for (; x < width; x++) {
|
||||
_pixelPos[0] = x / width;
|
||||
EffectTransform.transformPoint(drawable, _pixelPos, _effectPos);
|
||||
if (drawable.skin.isTouchingLinear(_effectPos)) {
|
||||
if (drawable.skin.isTouching(_effectPos)) {
|
||||
Q = [x, y];
|
||||
break;
|
||||
}
|
||||
@@ -1460,7 +1317,7 @@ class RenderWebGL extends EventEmitter {
|
||||
for (x = width - 1; x >= 0; x--) {
|
||||
_pixelPos[0] = x / width;
|
||||
EffectTransform.transformPoint(drawable, _pixelPos, _effectPos);
|
||||
if (drawable.skin.isTouchingLinear(_effectPos)) {
|
||||
if (drawable.skin.isTouching(_effectPos)) {
|
||||
Q = [x, y];
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const twgl = require('twgl.js');
|
||||
|
||||
const Silhouette = require('./Silhouette');
|
||||
const Skin = require('./Skin');
|
||||
const SvgRenderer = require('scratch-svg-renderer').SVGRenderer;
|
||||
const SvgRenderer = require('scratch-svg-renderer');
|
||||
|
||||
const MAX_TEXTURE_DIMENSION = 2048;
|
||||
|
||||
@@ -30,6 +31,8 @@ class SVGSkin extends Skin {
|
||||
|
||||
/** @type {Number} */
|
||||
this._maxTextureScale = 0;
|
||||
|
||||
this._silhouette = new Silhouette();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,6 +129,14 @@ class SVGSkin extends Skin {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this point touch an opaque or translucent point on this skin?
|
||||
* @param {twgl.v3} vec A texture coordinate.
|
||||
* @return {boolean} Did it touch?
|
||||
*/
|
||||
isTouching (vec) {
|
||||
return this._silhouette.isTouching(vec);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SVGSkin;
|
||||
|
||||
@@ -10,23 +10,6 @@
|
||||
*/
|
||||
let __SilhouetteUpdateCanvas;
|
||||
|
||||
/**
|
||||
* Internal helper function (in hopes that compiler can inline). Get a pixel
|
||||
* from silhouette data, or 0 if outside it's bounds.
|
||||
* @private
|
||||
* @param {Silhouette} silhouette - has data width and height
|
||||
* @param {number} x - x
|
||||
* @param {number} y - y
|
||||
* @return {number} Alpha value for x/y position
|
||||
*/
|
||||
const getPoint = ({_width: width, _height: height, _data: data}, x, y) => {
|
||||
// 0 if outside bouds, otherwise read from data.
|
||||
if (x >= width || y >= height || x < 0 || y < 0) {
|
||||
return 0;
|
||||
}
|
||||
return data[(y * width) + x];
|
||||
};
|
||||
|
||||
class Silhouette {
|
||||
constructor () {
|
||||
/**
|
||||
@@ -59,9 +42,6 @@ class Silhouette {
|
||||
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);
|
||||
const imageData = ctx.getImageData(0, 0, width, height);
|
||||
@@ -74,33 +54,17 @@ class Silhouette {
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if texture coordinate touches the silhouette using nearest neighbor.
|
||||
* Does this point touch the silhouette?
|
||||
* @param {twgl.v3} vec A texture coordinate.
|
||||
* @return {boolean} If the nearest pixel has an alpha value.
|
||||
* @return {boolean} Did the point touch?
|
||||
*/
|
||||
isTouchingNearest (vec) {
|
||||
if (!this._data) return;
|
||||
return getPoint(
|
||||
this,
|
||||
Math.round(vec[0] * (this._width - 1)),
|
||||
Math.round(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._data) 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;
|
||||
isTouching (vec) {
|
||||
const x = Math.floor(vec[0] * this._width);
|
||||
const y = Math.floor(vec[1] * this._height);
|
||||
return (
|
||||
x < this._width && x >= 0 &&
|
||||
y < this._height && y >= 0 &&
|
||||
this._data[(y * this._width) + x] !== 0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
32
src/Skin.js
32
src/Skin.js
@@ -3,7 +3,6 @@ const EventEmitter = require('events');
|
||||
const twgl = require('twgl.js');
|
||||
|
||||
const RenderConstants = require('./RenderConstants');
|
||||
const Silhouette = require('./Silhouette');
|
||||
|
||||
/**
|
||||
* Truncate a number into what could be stored in a 32 bit floating point value.
|
||||
@@ -53,12 +52,6 @@ class Skin extends EventEmitter {
|
||||
u_skin: null
|
||||
};
|
||||
|
||||
/**
|
||||
* A silhouette to store touching data, skins are responsible for keeping it up to date.
|
||||
* @private
|
||||
*/
|
||||
this._silhouette = new Silhouette();
|
||||
|
||||
this.setMaxListeners(RenderConstants.SKIN_SHARE_SOFT_LIMIT);
|
||||
}
|
||||
|
||||
@@ -147,35 +140,14 @@ class Skin extends EventEmitter {
|
||||
return this._uniforms;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the skin defers silhouette operations until the last possible minute,
|
||||
* this will be called before isTouching uses the silhouette.
|
||||
* @abstract
|
||||
*/
|
||||
updateSilhouette () {}
|
||||
|
||||
/**
|
||||
* Does this point touch an opaque or translucent point on this skin?
|
||||
* Nearest Neighbor version
|
||||
* @param {twgl.v3} vec A texture coordinate.
|
||||
* @return {boolean} Did it touch?
|
||||
*/
|
||||
isTouchingNearest (vec) {
|
||||
this.updateSilhouette();
|
||||
return this._silhouette.isTouchingNearest(vec);
|
||||
isTouching () {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this point touch an opaque or translucent point on this skin?
|
||||
* Linear Interpolation version
|
||||
* @param {twgl.v3} vec A texture coordinate.
|
||||
* @return {boolean} Did it touch?
|
||||
*/
|
||||
isTouchingLinear (vec) {
|
||||
this.updateSilhouette();
|
||||
return this._silhouette.isTouchingLinear(vec);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,16 +38,15 @@
|
||||
var canvas = document.getElementById('scratch-stage');
|
||||
var fudge = 90;
|
||||
var renderer = new ScratchRender(canvas);
|
||||
renderer.setLayerGroupOrdering(['group1']);
|
||||
|
||||
var drawableID = renderer.createDrawable('group1');
|
||||
var drawableID = renderer.createDrawable();
|
||||
renderer.updateDrawableProperties(drawableID, {
|
||||
position: [0, 0],
|
||||
scale: [100, 100],
|
||||
direction: 90
|
||||
});
|
||||
|
||||
var drawableID2 = renderer.createDrawable('group1');
|
||||
var drawableID2 = renderer.createDrawable();
|
||||
var wantBitmapSkin = false;
|
||||
|
||||
// Bitmap (squirrel)
|
||||
|
||||
@@ -46,7 +46,7 @@ varying vec2 v_texCoord;
|
||||
// See also: https://en.wikipedia.org/wiki/HSL_and_HSV#Formal_derivation
|
||||
|
||||
// Smaller values can cause problems with "color" and "brightness" effects on some mobile devices
|
||||
const float epsilon = 1e-3;
|
||||
const float epsilon = 1e-4;
|
||||
|
||||
// Convert an RGB color to Hue, Saturation, and Lightness.
|
||||
// All components of input and output are expected to be in the [0,1] range.
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
const minilog = require('minilog');
|
||||
minilog.enable();
|
||||
|
||||
module.exports = minilog('scratch-render');
|
||||
@@ -1,30 +1,17 @@
|
||||
const SVGTextWrapper = require('./svg-text-wrapper');
|
||||
const SvgRenderer = require('scratch-svg-renderer').SVGRenderer;
|
||||
const SvgRenderer = require('scratch-svg-renderer');
|
||||
const xmlescape = require('xml-escape');
|
||||
|
||||
const MAX_LINE_LENGTH = 170;
|
||||
const MIN_WIDTH = 50;
|
||||
const STROKE_WIDTH = 4;
|
||||
|
||||
class SVGTextBubble {
|
||||
constructor () {
|
||||
this.svgRenderer = new SvgRenderer();
|
||||
this.svgTextWrapper = new SVGTextWrapper(this.makeSvgTextElement);
|
||||
this.svgTextWrapper = new SVGTextWrapper();
|
||||
this._textSizeCache = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {SVGElement} an SVG text node with the properties that we want for speech bubbles.
|
||||
*/
|
||||
makeSvgTextElement () {
|
||||
const svgText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
svgText.setAttribute('alignment-baseline', 'text-before-edge');
|
||||
svgText.setAttribute('font-size', '14');
|
||||
svgText.setAttribute('fill', '#575E75');
|
||||
// TODO Do we want to use the new default sans font instead of Helvetica?
|
||||
svgText.setAttribute('font-family', 'Helvetica');
|
||||
return svgText;
|
||||
}
|
||||
|
||||
_speechBubble (w, h, radius, pointsLeft) {
|
||||
let pathString = `
|
||||
M 0 ${radius}
|
||||
@@ -58,7 +45,7 @@ class SVGTextBubble {
|
||||
<path
|
||||
d="${pathString}"
|
||||
stroke="rgba(0, 0, 0, 0.15)"
|
||||
stroke-width="${STROKE_WIDTH}"
|
||||
stroke-width="4"
|
||||
fill="rgba(0, 0, 0, 0.15)"
|
||||
stroke-line-join="round"
|
||||
/>
|
||||
@@ -114,7 +101,7 @@ class SVGTextBubble {
|
||||
rx="${rx}" ry="${ry}"
|
||||
fill="rgba(0, 0, 0, 0.15)"
|
||||
stroke="rgba(0, 0, 0, 0.15)"
|
||||
stroke-width="${STROKE_WIDTH}"
|
||||
stroke-width="4"
|
||||
/>
|
||||
<ellipse
|
||||
cx="${cx}" cy="${cy}"
|
||||
@@ -138,8 +125,7 @@ class SVGTextBubble {
|
||||
|
||||
return `
|
||||
<g>
|
||||
<path d="${pathString}" stroke="rgba(0, 0, 0, 0.15)" stroke-width="${STROKE_WIDTH}"
|
||||
fill="rgba(0, 0, 0, 0.15)" />
|
||||
<path d="${pathString}" stroke="rgba(0, 0, 0, 0.15)" stroke-width="4" fill="rgba(0, 0, 0, 0.15)" />
|
||||
<path d="${pathString}" stroke="none" fill="white" />
|
||||
${ellipses.join('\n')}
|
||||
</g>`;
|
||||
@@ -147,38 +133,29 @@ class SVGTextBubble {
|
||||
|
||||
|
||||
_getTextSize () {
|
||||
const svgString = this._wrapSvgFragment(this._textFragment);
|
||||
const svgString = this._wrapSvgFragment(this._textFragment());
|
||||
if (!this._textSizeCache[svgString]) {
|
||||
this._textSizeCache[svgString] = this.svgRenderer.measure(svgString);
|
||||
}
|
||||
return this._textSizeCache[svgString];
|
||||
}
|
||||
|
||||
_wrapSvgFragment (fragment, width, height) {
|
||||
let svgString = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1"`;
|
||||
if (width && height) {
|
||||
const fullWidth = width + STROKE_WIDTH;
|
||||
const fullHeight = height + STROKE_WIDTH + 12;
|
||||
svgString = `${svgString} viewBox="
|
||||
${-STROKE_WIDTH / 2} ${-STROKE_WIDTH / 2} ${fullWidth} ${fullHeight}"
|
||||
width="${fullWidth}" height="${fullHeight}">`;
|
||||
} else {
|
||||
svgString = `${svgString}>`;
|
||||
}
|
||||
svgString = `${svgString} ${fragment} </svg>`;
|
||||
return svgString;
|
||||
_wrapSvgFragment (fragment) {
|
||||
return `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
|
||||
${fragment}
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
_buildTextFragment (text) {
|
||||
const textNode = this.svgTextWrapper.wrapText(MAX_LINE_LENGTH, text);
|
||||
const serializer = new XMLSerializer();
|
||||
return serializer.serializeToString(textNode);
|
||||
_textFragment () {
|
||||
return `<text fill="#575E75">${xmlescape(this.lines.join('\n'))}</text>`;
|
||||
}
|
||||
|
||||
buildString (type, text, pointsLeft) {
|
||||
this.type = type;
|
||||
this.pointsLeft = pointsLeft;
|
||||
this._textFragment = this._buildTextFragment(text);
|
||||
this.lines = this.svgTextWrapper.wrapText(MAX_LINE_LENGTH, text);
|
||||
|
||||
let fragment = '';
|
||||
|
||||
@@ -192,8 +169,8 @@ class SVGTextBubble {
|
||||
} else {
|
||||
fragment += this._thinkBubble(fullWidth, fullHeight, radius, this.pointsLeft);
|
||||
}
|
||||
fragment += `<g transform="translate(${padding - x}, ${padding - y})">${this._textFragment}</g>`;
|
||||
return this._wrapSvgFragment(fragment, fullWidth, fullHeight);
|
||||
fragment += `<g transform="translate(${padding - x}, ${padding - y})">${this._textFragment()}</g>`;
|
||||
return this._wrapSvgFragment(fragment);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,14 +5,9 @@ const TextWrapper = require('./text-wrapper');
|
||||
* For use with TextWrapper.
|
||||
*/
|
||||
class SVGMeasurementProvider {
|
||||
/**
|
||||
* @param {function} makeTextElement - provides a text node of an SVGElement
|
||||
* with the style of the text to be wrapped.
|
||||
*/
|
||||
constructor (makeTextElement) {
|
||||
constructor () {
|
||||
this._svgRoot = null;
|
||||
this._cache = {};
|
||||
this.makeTextElement = makeTextElement;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,7 +61,13 @@ class SVGMeasurementProvider {
|
||||
|
||||
const svgRoot = document.createElementNS(svgNamespace, 'svg');
|
||||
const svgGroup = document.createElementNS(svgNamespace, 'g');
|
||||
const svgText = this.makeTextElement();
|
||||
const svgText = document.createElementNS(svgNamespace, 'text');
|
||||
|
||||
// Normalize text attributes to match what the svg-renderer does.
|
||||
// @TODO This code should be shared with the svg-renderer.
|
||||
svgText.setAttribute('alignment-baseline', 'text-before-edge');
|
||||
svgText.setAttribute('font-size', '14');
|
||||
svgText.setAttribute('font-family', 'Helvetica');
|
||||
|
||||
// hide from the user, including screen readers
|
||||
svgRoot.setAttribute('style', 'position:absolute;visibility:hidden');
|
||||
@@ -95,32 +96,8 @@ class SVGMeasurementProvider {
|
||||
* TextWrapper specialized for SVG text.
|
||||
*/
|
||||
class SVGTextWrapper extends TextWrapper {
|
||||
/**
|
||||
* @param {function} makeTextElement - provides a text node of an SVGElement
|
||||
* with the style of the text to be wrapped.
|
||||
*/
|
||||
constructor (makeTextElement) {
|
||||
super(new SVGMeasurementProvider(makeTextElement));
|
||||
this.makeTextElement = makeTextElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the provided text into lines restricted to a maximum width. See Unicode Standard Annex (UAX) #14.
|
||||
* @param {number} maxWidth - the maximum allowed width of a line.
|
||||
* @param {string} text - the text to be wrapped. Will be split on whitespace.
|
||||
* @returns {SVGElement} wrapped text node
|
||||
*/
|
||||
wrapText (maxWidth, text) {
|
||||
const lines = super.wrapText(maxWidth, text);
|
||||
const textElement = this.makeTextElement();
|
||||
for (const line of lines) {
|
||||
const tspanNode = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
||||
tspanNode.setAttribute('x', '0');
|
||||
tspanNode.setAttribute('dy', '1.2em');
|
||||
tspanNode.textContent = line;
|
||||
textElement.appendChild(tspanNode);
|
||||
}
|
||||
return textElement;
|
||||
constructor () {
|
||||
super(new SVGMeasurementProvider());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const LineBreaker = require('!ify-loader!linebreak');
|
||||
const GraphemeBreaker = require('!ify-loader!grapheme-breaker');
|
||||
const LineBreaker = require('linebreak');
|
||||
const GraphemeBreaker = require('grapheme-breaker');
|
||||
|
||||
/**
|
||||
* Tell this text wrapper to use a specific measurement provider.
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/* IMPORTANT
|
||||
* This snapshot file is auto-generated, but designed for humans.
|
||||
* It should be checked into source control and tracked carefully.
|
||||
* Re-generate by setting TAP_SNAPSHOT=1 and running tests.
|
||||
* Make sure to inspect the output below. Do not ignore changes!
|
||||
*/
|
||||
'use strict'
|
||||
exports[`test/integration/scratch-tests.js TAP bubble snapshot > bubble-text-snapshot 1`] = `
|
||||
<text xmlns="http://www.w3.org/2000/svg" alignment-baseline="text-before-edge" font-size="14" fill="#575E75" font-family="Helvetica"><tspan x="0" dy="1.2em"><e*&%$&^$></!abc'></tspan></text>
|
||||
`
|
||||
@@ -1,43 +0,0 @@
|
||||
<body>
|
||||
<script src="../../node_modules/scratch-vm/dist/web/scratch-vm.js"></script>
|
||||
<script src="../../node_modules/scratch-storage/dist/web/scratch-storage.js"></script>
|
||||
<!-- note: this uses the BUILT version of scratch-render! make sure to npm run build -->
|
||||
<script src="../../dist/web/scratch-render.js"></script>
|
||||
|
||||
<canvas id="test" width="480" height="360"></canvas>
|
||||
<input type="file" id="file" name="file">
|
||||
|
||||
<script>
|
||||
// These variables are going to be available in the "window global" intentionally.
|
||||
// Allows you easy access to debug with `vm.greenFlag()` etc.
|
||||
|
||||
var render = new ScratchRender(document.getElementById('test'));
|
||||
var vm = new VirtualMachine();
|
||||
var storage = new ScratchStorage();
|
||||
|
||||
vm.attachStorage(storage);
|
||||
vm.attachRenderer(render);
|
||||
|
||||
document.getElementById('file').addEventListener('click', e => {
|
||||
document.body.removeChild(document.getElementById('loaded'));
|
||||
});
|
||||
|
||||
document.getElementById('file').addEventListener('change', e => {
|
||||
const reader = new FileReader();
|
||||
const thisFileInput = e.target;
|
||||
reader.onload = () => {
|
||||
vm.start();
|
||||
vm.loadProject(reader.result)
|
||||
.then(() => {
|
||||
// we add a `#loaded` div to our document, the integration suite
|
||||
// waits for that element to show up to assume the vm is ready
|
||||
// to play!
|
||||
const div = document.createElement('div');
|
||||
div.id='loaded';
|
||||
document.body.appendChild(div);
|
||||
});
|
||||
};
|
||||
reader.readAsArrayBuffer(thisFileInput.files[0]);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
@@ -1,127 +0,0 @@
|
||||
/* global vm, render, Promise */
|
||||
const {Chromeless} = require('chromeless');
|
||||
const test = require('tap').test;
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const chromeless = new Chromeless();
|
||||
|
||||
const indexHTML = path.resolve(__dirname, 'index.html');
|
||||
const testDir = (...args) => path.resolve(__dirname, 'scratch-tests', ...args);
|
||||
|
||||
const testFile = file => test(file, async t => {
|
||||
// start each test by going to the index.html, and loading the scratch file
|
||||
const says = await chromeless.goto(`file://${indexHTML}`)
|
||||
.setFileInput('#file', testDir(file))
|
||||
// the index.html handler for file input will add a #loaded element when it
|
||||
// finishes.
|
||||
.wait('#loaded')
|
||||
.evaluate(() => {
|
||||
// This function is run INSIDE the integration chrome browser via some
|
||||
// injection and .toString() magic. We can return some "simple data"
|
||||
// back across as a promise, so we will just log all the says that happen
|
||||
// for parsing after.
|
||||
|
||||
// this becomes the `says` in the outer scope
|
||||
const messages = [];
|
||||
const TIMEOUT = 5000;
|
||||
|
||||
vm.runtime.on('SAY', (_, __, message) => {
|
||||
messages.push(message);
|
||||
});
|
||||
|
||||
vm.greenFlag();
|
||||
const startTime = Date.now();
|
||||
|
||||
return Promise.resolve()
|
||||
.then(async () => {
|
||||
// waiting for all threads to complete, then we return
|
||||
while (vm.runtime.threads.some(thread => vm.runtime.isActiveThread(thread))) {
|
||||
if ((Date.now() - startTime) >= TIMEOUT) {
|
||||
// if we push the message after end, the failure from tap is not very useful:
|
||||
// "not ok test after end() was called"
|
||||
messages.unshift(`fail Threads still running after ${TIMEOUT}ms`);
|
||||
break;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
}
|
||||
|
||||
return messages;
|
||||
});
|
||||
});
|
||||
|
||||
// Map string messages to tap reporting methods. This will be used
|
||||
// with events from scratch's runtime emitted on block instructions.
|
||||
let didPlan = false;
|
||||
let didEnd = false;
|
||||
const reporters = {
|
||||
comment (message) {
|
||||
t.comment(message);
|
||||
},
|
||||
pass (reason) {
|
||||
t.pass(reason);
|
||||
},
|
||||
fail (reason) {
|
||||
t.fail(reason);
|
||||
},
|
||||
plan (count) {
|
||||
didPlan = true;
|
||||
t.plan(Number(count));
|
||||
},
|
||||
end () {
|
||||
didEnd = true;
|
||||
t.end();
|
||||
}
|
||||
};
|
||||
|
||||
// loop over each "SAY" we caught from the VM and use the reporters
|
||||
says.forEach(text => {
|
||||
// first word of the say is going to be a "command"
|
||||
const command = text.split(/\s+/, 1)[0].toLowerCase();
|
||||
if (reporters[command]) {
|
||||
return reporters[command](text.substring(command.length).trim());
|
||||
}
|
||||
|
||||
// Default to a comment with the full text if we didn't match
|
||||
// any command prefix
|
||||
return reporters.comment(text);
|
||||
});
|
||||
|
||||
if (!didPlan) {
|
||||
t.comment('did not say "plan NUMBER_OF_TESTS"');
|
||||
}
|
||||
|
||||
// End must be called so that tap knows the test is done. If
|
||||
// the test has a SAY "end" block but that block did not
|
||||
// execute, this explicit failure will raise that issue so
|
||||
// it can be resolved.
|
||||
if (!didEnd) {
|
||||
t.fail('did not say "end"');
|
||||
t.end();
|
||||
}
|
||||
});
|
||||
|
||||
const testBubbles = () => test('bubble snapshot', async t => {
|
||||
const bubbleSvg = await chromeless.goto(`file://${indexHTML}`)
|
||||
.evaluate(() => {
|
||||
const testString = '<e*&%$&^$></!abc\'>';
|
||||
return render._svgTextBubble._buildTextFragment(testString);
|
||||
});
|
||||
t.matchSnapshot(bubbleSvg, 'bubble-text-snapshot');
|
||||
t.end();
|
||||
});
|
||||
|
||||
// immediately invoked async function to let us wait for each test to finish before starting the next.
|
||||
(async () => {
|
||||
const files = fs.readdirSync(testDir())
|
||||
.filter(uri => uri.endsWith('.sb2') || uri.endsWidth('.sb3'));
|
||||
|
||||
for (const file of files) {
|
||||
await testFile(file);
|
||||
}
|
||||
|
||||
await testBubbles();
|
||||
|
||||
// close the browser window we used
|
||||
await chromeless.end();
|
||||
})();
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,9 +1,8 @@
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const path = require('path');
|
||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
|
||||
const base = {
|
||||
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
|
||||
devServer: {
|
||||
contentBase: false,
|
||||
host: '0.0.0.0',
|
||||
@@ -19,19 +18,21 @@ const base = {
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [['env', {targets: {browsers: ['last 3 versions', 'Safari >= 8', 'iOS >= 8']}}]]
|
||||
presets: ['es2015']
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /node_modules[\\/](linebreak|grapheme-breaker)[\\/].*\.js$/,
|
||||
loader: 'ify-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new UglifyJsPlugin({
|
||||
include: /\.min\.js$/
|
||||
})
|
||||
]
|
||||
},
|
||||
plugins: []
|
||||
plugins: process.env.NODE_ENV === 'production' ? [
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
include: /\.min\.js$/,
|
||||
minimize: true
|
||||
})
|
||||
] : []
|
||||
};
|
||||
|
||||
module.exports = [
|
||||
@@ -80,14 +81,6 @@ module.exports = [
|
||||
libraryTarget: 'commonjs2',
|
||||
path: path.resolve('dist', 'node'),
|
||||
filename: '[name].js'
|
||||
},
|
||||
externals: {
|
||||
'!ify-loader!grapheme-breaker': 'grapheme-breaker',
|
||||
'!ify-loader!linebreak': 'linebreak',
|
||||
'hull.js': true,
|
||||
'scratch-svg-renderer': true,
|
||||
'twgl.js': true,
|
||||
'xml-escape': true
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user