Merge branch 'develop' into greenkeeper/twgl.js-3.5.0

This commit is contained in:
Chris Willis-Ford 2017-09-06 16:17:26 -06:00 committed by GitHub
commit 1e54b1c894
10 changed files with 272 additions and 15 deletions

View File

@ -1,6 +1,6 @@
language: node_js language: node_js
node_js: node_js:
- "4.2" - 6
- "node" - "node"
sudo: false sudo: false
cache: cache:

View File

@ -17,22 +17,23 @@
"prepublish": "npm run build", "prepublish": "npm run build",
"prepublish-watch": "npm run watch", "prepublish-watch": "npm run watch",
"start": "webpack-dev-server", "start": "webpack-dev-server",
"test": "npm run lint && npm run docs", "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)'\"", "version": "json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\"",
"watch": "webpack --progress --colors --watch --watch-poll" "watch": "webpack --progress --colors --watch --watch-poll"
}, },
"devDependencies": { "devDependencies": {
"babel-core": "^6.23.1", "babel-core": "^6.23.1",
"babel-eslint": "^7.1.1", "babel-eslint": "^7.1.1",
"babel-loader": "^6.3.2", "babel-loader": "^7.1.2",
"babel-polyfill": "^6.22.0", "babel-polyfill": "^6.22.0",
"babel-preset-es2015": "^6.22.0", "babel-preset-es2015": "^6.22.0",
"base64-loader": "^1.0.0", "base64-loader": "^1.0.0",
"copy-webpack-plugin": "^4.0.1", "copy-webpack-plugin": "^4.0.1",
"docdash": "^0.4.0", "docdash": "^0.4.0",
"eslint": "^3.16.1", "eslint": "^4.6.1",
"eslint-config-scratch": "^3.1.0", "eslint-config-scratch": "^4.0.0",
"gh-pages": "^0.12.0", "gh-pages": "^1.0.0",
"hull.js": "0.2.10", "hull.js": "0.2.10",
"jsdoc": "^3.5.3", "jsdoc": "^3.5.3",
"json": "^9.0.4", "json": "^9.0.4",
@ -41,7 +42,7 @@
"tap": "^10.3.0", "tap": "^10.3.0",
"travis-after-all": "^1.4.4", "travis-after-all": "^1.4.4",
"twgl.js": "3.5.0", "twgl.js": "3.5.0",
"webpack": "^2.2.1", "webpack": "^3.5.6",
"webpack-dev-server": "^2.4.1" "webpack-dev-server": "^2.4.1"
} }
} }

View File

@ -201,7 +201,7 @@ class Drawable {
twgl.m4.rotateZ(modelMatrix, rotation, modelMatrix); twgl.m4.rotateZ(modelMatrix, rotation, modelMatrix);
// Adjust rotation center relative to the skin. // Adjust rotation center relative to the skin.
var rotationAdjusted = twgl.v3.subtract(this.skin.rotationCenter, twgl.v3.divScalar(this.skin.size, 2)); let rotationAdjusted = twgl.v3.subtract(this.skin.rotationCenter, twgl.v3.divScalar(this.skin.size, 2));
rotationAdjusted = twgl.v3.multiply(rotationAdjusted, this.scale); rotationAdjusted = twgl.v3.multiply(rotationAdjusted, this.scale);
rotationAdjusted = twgl.v3.divScalar(rotationAdjusted, 100); rotationAdjusted = twgl.v3.divScalar(rotationAdjusted, 100);
rotationAdjusted[1] *= -1; // Y flipped to Scratch coordinate. rotationAdjusted[1] *= -1; // Y flipped to Scratch coordinate.

View File

@ -189,7 +189,7 @@ class PenSkin extends Skin {
const r = Math.round(color4f[0] * 255); const r = Math.round(color4f[0] * 255);
const g = Math.round(color4f[1] * 255); const g = Math.round(color4f[1] * 255);
const b = Math.round(color4f[2] * 255); const b = Math.round(color4f[2] * 255);
const a = color4f[3]; // Alpha is 0 to 1 (not 0 to 255 like r,g,b) const a = color4f[3]; // Alpha is 0 to 1 (not 0 to 255 like r,g,b)
context.strokeStyle = `rgba(${r},${g},${b},${a})`; context.strokeStyle = `rgba(${r},${g},${b},${a})`;
context.lineCap = 'round'; context.lineCap = 'round';

View File

@ -22,7 +22,7 @@ module.exports = {
* @enum {string} * @enum {string}
*/ */
Events: { Events: {
/** /**
* NativeSizeChanged event * NativeSizeChanged event
* *
* @event RenderWebGL#event:NativeSizeChanged * @event RenderWebGL#event:NativeSizeChanged

View File

@ -5,6 +5,7 @@ const twgl = require('twgl.js');
const BitmapSkin = require('./BitmapSkin'); const BitmapSkin = require('./BitmapSkin');
const Drawable = require('./Drawable'); const Drawable = require('./Drawable');
const Rectangle = require('./Rectangle');
const PenSkin = require('./PenSkin'); const PenSkin = require('./PenSkin');
const RenderConstants = require('./RenderConstants'); const RenderConstants = require('./RenderConstants');
const ShaderManager = require('./ShaderManager'); const ShaderManager = require('./ShaderManager');
@ -222,6 +223,33 @@ class RenderWebGL extends EventEmitter {
return skinId; return skinId;
} }
/**
* Update an existing SVG skin, or create an SVG skin if the previous skin was not SVG.
* @param {!int} skinId the ID for the skin to change.
* @param {!string} svgData - new SVG to use.
* @param {?Array<number>} rotationCenter Optional: rotation center of the skin. If not supplied, the center of the
* skin will be used
*/
updateSVGSkin (skinId, svgData, rotationCenter) {
if (this._allSkins[skinId] instanceof SVGSkin) {
this._allSkins[skinId].setSVG(svgData, rotationCenter);
return;
}
const newSkin = new SVGSkin(skinId, this);
newSkin.setSVG(svgData, rotationCenter);
const oldSkin = this._allSkins[skinId];
this._allSkins[skinId] = newSkin;
// Tell drawables to update
for (const drawable of this._allDrawables) {
if (drawable && drawable.skin === oldSkin) {
drawable.skin = newSkin;
}
}
oldSkin.dispose();
}
/** /**
* Destroy an existing skin. Do not use the skin or its ID after calling this. * Destroy an existing skin. Do not use the skin or its ID after calling this.
* @param {!int} skinId - The ID of the skin to destroy. * @param {!int} skinId - The ID of the skin to destroy.
@ -684,6 +712,74 @@ class RenderWebGL extends EventEmitter {
}; };
} }
/**
* @typedef ColorExtraction
* @property {Uint8Array} data Raw pixel data for the drawable
* @property {int} width Drawable bounding box width
* @property {int} height Drawable bounding box height
* @property {object} color Color object with RGBA properties at picked location
*/
/**
* Return drawable pixel data and color at a given position
* @param {int} x The client x coordinate of the picking location.
* @param {int} y The client y coordinate of the picking location.
* @param {int} radius The client radius to extract pixels with.
* @return {?ColorExtraction} Data about the picked color
*/
extractColor (x, y, radius) {
const scratchX = Math.round(this._nativeSize[0] * ((x / this._gl.canvas.clientWidth) - 0.5));
const scratchY = Math.round(-this._nativeSize[1] * ((y / this._gl.canvas.clientHeight) - 0.5));
const gl = this._gl;
twgl.bindFramebufferInfo(gl, this._queryBufferInfo);
const bounds = new Rectangle();
bounds.initFromBounds(scratchX - radius, scratchX + radius, scratchY - radius, scratchY + radius);
const pickX = scratchX - bounds.left;
const pickY = bounds.top - scratchY;
gl.viewport(0, 0, bounds.width, bounds.height);
const projection = twgl.m4.ortho(bounds.left, bounds.right, bounds.top, bounds.bottom, -1, 1);
gl.clearColor.apply(gl, this._backgroundColor);
gl.clear(gl.COLOR_BUFFER_BIT);
this._drawThese(this._drawList, ShaderManager.DRAW_MODE.default, projection);
const data = new Uint8Array(Math.floor(bounds.width * bounds.height * 4));
gl.readPixels(0, 0, bounds.width, bounds.height, gl.RGBA, gl.UNSIGNED_BYTE, data);
const pixelBase = Math.floor(4 * ((pickY * bounds.width) + pickX));
const color = {
r: data[pixelBase],
g: data[pixelBase + 1],
b: data[pixelBase + 2],
a: data[pixelBase + 3]
};
if (this._debugCanvas) {
this._debugCanvas.width = bounds.width;
this._debugCanvas.height = bounds.height;
const ctx = this._debugCanvas.getContext('2d');
const imageData = ctx.createImageData(bounds.width, bounds.height);
imageData.data.set(data);
ctx.putImageData(imageData, 0, 0);
ctx.strokeStyle = 'black';
ctx.fillStyle = `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`;
ctx.rect(pickX - 4, pickY - 4, 8, 8);
ctx.fill();
ctx.stroke();
}
return {
data: data,
width: bounds.width,
height: bounds.height,
color: color
};
}
/** /**
* Get the candidate bounding box for a touching query. * Get the candidate bounding box for a touching query.
* @param {int} drawableID ID for drawable of query. * @param {int} drawableID ID for drawable of query.

View File

@ -1,8 +1,5 @@
const twgl = require('twgl.js'); const twgl = require('twgl.js');
const vertexShaderText = require('raw-loader!./shaders/sprite.vert');
const fragmentShaderText = require('raw-loader!./shaders/sprite.frag');
class ShaderManager { class ShaderManager {
/** /**
@ -65,8 +62,11 @@ class ShaderManager {
} }
const definesText = `${defines.join('\n')}\n`; const definesText = `${defines.join('\n')}\n`;
const vsFullText = definesText + vertexShaderText;
const fsFullText = definesText + fragmentShaderText; /* eslint-disable global-require */
const vsFullText = definesText + require('raw-loader!./shaders/sprite.vert');
const fsFullText = definesText + require('raw-loader!./shaders/sprite.frag');
/* eslint-enable global-require */
return twgl.createProgramInfo(this._gl, [vsFullText, fsFullText]); return twgl.createProgramInfo(this._gl, [vsFullText, fsFullText]);
} }

View File

@ -126,6 +126,11 @@
}; };
} }
canvas.onmousemove = function(event) {
var mousePos = getMousePos(event, canvas);
renderer.extractColor(mousePos.x, mousePos.y, 30);
};
canvas.onclick = function(event) { canvas.onclick = function(event) {
var mousePos = getMousePos(event, canvas); var mousePos = getMousePos(event, canvas);
var pickID = renderer.pick(mousePos.x, mousePos.y); var pickID = renderer.pick(mousePos.x, mousePos.y);

13
test/fixtures/MockSkin.js vendored Normal file
View File

@ -0,0 +1,13 @@
const Skin = require('../../src/Skin');
class MockSkin extends Skin {
set size (dimensions) {
this.dimensions = dimensions;
}
get size () {
return this.dimensions || [0, 0];
}
}
module.exports = MockSkin;

142
test/unit/DrawableTests.js Normal file
View File

@ -0,0 +1,142 @@
const test = require('tap').test;
// Mock `window` and `document.createElement` for twgl.js.
global.window = {};
global.document = {
createElement: () => ({getContext: () => {}})
};
const Drawable = require('../../src/Drawable');
const MockSkin = require('../fixtures/MockSkin');
const Rectangle = require('../../src/Rectangle');
/**
* Returns a Rectangle-like object, with dimensions rounded to the given number
* of digits.
* @param {Rectangle} rect The source rectangle.
* @param {int} decimals The number of decimal points to snap to.
* @returns {object} An object with left/right/top/bottom attributes.
*/
const snapToNearest = function (rect, decimals = 3) {
return {
left: rect.left.toFixed(decimals),
right: rect.right.toFixed(decimals),
bottom: rect.bottom.toFixed(decimals),
top: rect.top.toFixed(decimals)
};
};
test('translate by position', t => {
const expected = new Rectangle();
const drawable = new Drawable();
drawable.skin = new MockSkin();
drawable.skin.size = [200, 50];
expected.initFromBounds(0, 200, -50, 0);
t.same(snapToNearest(drawable.getAABB()), expected);
drawable.updateProperties({position: [1, 2]});
expected.initFromBounds(1, 201, -48, 2);
t.same(snapToNearest(drawable.getAABB()), expected);
t.end();
});
test('translate by costume center', t => {
const expected = new Rectangle();
const drawable = new Drawable();
drawable.skin = new MockSkin();
drawable.skin.size = [200, 50];
drawable.skin.setRotationCenter(1, 0);
expected.initFromBounds(-1, 199, -50, 0);
t.same(snapToNearest(drawable.getAABB()), expected);
drawable.skin.setRotationCenter(0, -2);
expected.initFromBounds(0, 200, -52, -2);
t.same(snapToNearest(drawable.getAABB()), expected);
t.end();
});
test('translate and rotate', t => {
const expected = new Rectangle();
const drawable = new Drawable();
drawable.skin = new MockSkin();
drawable.skin.size = [200, 50];
drawable.updateProperties({position: [1, 2], direction: 0});
expected.initFromBounds(1, 51, 2, 202);
t.same(snapToNearest(drawable.getAABB()), expected);
drawable.updateProperties({direction: 180});
expected.initFromBounds(-49, 1, -198, 2);
t.same(snapToNearest(drawable.getAABB()), expected);
drawable.skin.setRotationCenter(100, 25);
drawable.updateProperties({direction: 270, position: [0, 0]});
expected.initFromBounds(-100, 100, -25, 25);
t.same(snapToNearest(drawable.getAABB()), expected);
drawable.updateProperties({direction: 90});
t.same(snapToNearest(drawable.getAABB()), expected);
t.end();
});
test('rotate by non-right-angles', t => {
const expected = new Rectangle();
const drawable = new Drawable();
drawable.skin = new MockSkin();
drawable.skin.size = [10, 10];
drawable.skin.setRotationCenter(5, 5);
expected.initFromBounds(-5, 5, -5, 5);
t.same(snapToNearest(drawable.getAABB()), expected);
drawable.updateProperties({direction: 45});
expected.initFromBounds(-7.071, 7.071, -7.071, 7.071);
t.same(snapToNearest(drawable.getAABB()), expected);
t.end();
});
test('scale', t => {
const expected = new Rectangle();
const drawable = new Drawable();
drawable.skin = new MockSkin();
drawable.skin.size = [200, 50];
drawable.updateProperties({scale: [100, 50]});
expected.initFromBounds(0, 200, -25, 0);
t.same(snapToNearest(drawable.getAABB()), expected);
drawable.skin.setRotationCenter(0, 25);
expected.initFromBounds(0, 200, -12.5, 12.5);
t.same(snapToNearest(drawable.getAABB()), expected);
drawable.skin.setRotationCenter(150, 50);
drawable.updateProperties({scale: [50, 50]});
expected.initFromBounds(-75, 25, 0, 25);
t.same(snapToNearest(drawable.getAABB()), expected);
t.end();
});
test('rotate and scale', t => {
const expected = new Rectangle();
const drawable = new Drawable();
drawable.skin = new MockSkin();
drawable.skin.size = [100, 1000];
drawable.skin.setRotationCenter(50, 50);
expected.initFromBounds(-50, 50, -950, 50);
t.same(snapToNearest(drawable.getAABB()), expected);
drawable.updateProperties({scale: [40, 60]});
drawable.skin.setRotationCenter(50, 50);
expected.initFromBounds(-20, 20, -570, 30);
t.same(snapToNearest(drawable.getAABB()), expected);
t.end();
});