Compare commits

...

52 Commits

Author SHA1 Message Date
greenkeeper[bot]
2951b872cf chore(package): update eslint to version 6.2.0
Closes #307
2019-08-18 23:09:38 +00:00
Karishma Chadha
c2e32d2baf Merge pull request #494 from LLK/revert-467-rect-init-matrix
Revert "Initialialize AABB Rectangle "
2019-08-13 11:38:41 -04:00
Karishma Chadha
c8839b2feb Merge pull request #493 from LLK/revert-470-skin-alter-push
Revert "Skin alter push"
2019-08-13 11:38:24 -04:00
Karishma Chadha
c8f7496fba Revert "Initialialize AABB Rectangle " 2019-08-13 11:26:07 -04:00
Karishma Chadha
4a28cffcd4 Revert "Skin alter push" 2019-08-13 11:22:27 -04:00
DD Liu
d464f3e82e Merge pull request #482 from LLK/greenkeeper/scratch-svg-renderer-0.2.0-prerelease.20190715144718
Update scratch-svg-renderer to the latest version 🚀
2019-07-15 11:50:45 -04:00
DD Liu
fcda622b5a Update to latest svg renderer 2019-07-15 11:50:14 -04:00
greenkeeper[bot]
ae507dfabb fix(package): update scratch-svg-renderer to version 0.2.0-prerelease.20190715144718 2019-07-15 15:16:46 +00:00
Chris Willis-Ford
cc5fea803e Merge pull request #470 from mzgoddard/skin-alter-push
Skin alter push
2019-07-01 10:43:11 -07:00
Chris Willis-Ford
32063a2953 Merge pull request #467 from mzgoddard/rect-init-matrix
Initialialize AABB Rectangle
2019-07-01 10:31:54 -07:00
Michael "Z" Goddard
994e9be00b move the initFromModelMatrix process docs into a jsdoc tutorial 2019-06-19 18:53:05 -04:00
Michael "Z" Goddard
b7004878ff set jsdoc tutorials to come from docs 2019-06-19 18:36:17 -04:00
Michael "Z" Goddard
f187be6b31 add MockSkinPool for testing 2019-06-18 17:18:49 -04:00
Michael "Z" Goddard
14b01bd63c cache PenSkin's _canvasSize 2019-06-18 17:18:49 -04:00
Michael "Z" Goddard
24b535eb76 cache svg renderer size and view offset 2019-06-18 17:18:48 -04:00
Michael "Z" Goddard
f9b1a04d1a cache Skin.size 2019-06-18 17:18:48 -04:00
Michael "Z" Goddard
ab603ffa92 push skin alteration down from renderwebgl 2019-06-18 17:18:48 -04:00
Michael "Z" Goddard
5d7957ff9b document out how initFromModelMatrix works 2019-06-11 14:32:33 -04:00
Michael "Z" Goddard
a840089bc9 fence bounds 2019-06-11 13:05:22 -04:00
Michael "Z" Goddard
bf47f69b04 use a destination parameter for bounds; add initFromMatrixRadius
- pass bounds as a destination parameter
- add initFromMatrixRadius
- use initFromMatrixRadius in getAABB
2019-06-06 14:39:56 -04:00
Karishma Chadha
27c70a7542 Merge pull request #463 from LLK/greenkeeper/scratch-svg-renderer-0.2.0-prerelease.20190523193400
Update scratch-svg-renderer to the latest version 🚀
2019-06-05 11:02:10 -04:00
Benjamin Wheeler
a79df7af59 Merge pull request #466 from LLK/revert-423-more-circular-pen-dots
Revert "Tweak scalingVector to make dots appear to be more circular"
2019-05-30 11:58:22 -04:00
Benjamin Wheeler
ed6c707cba Revert "Tweak scalingVector to make dots appear to be more circular" 2019-05-30 11:50:54 -04:00
Chris Willis-Ford
6bdaebcb3b Merge pull request #426 from adroitwhiz/playground-improvements
Playground improvements
2019-05-28 16:10:36 -07:00
Chris Willis-Ford
8b54df18af Merge pull request #423 from ktbee/more-circular-pen-dots
Tweak scalingVector to make dots appear to be more circular
2019-05-28 15:39:34 -07:00
Paul Kaplan
e98fb37434 Merge pull request #451 from adroitwhiz/canvas-text-bubble
Implement canvas-based TextBubbleSkin
2019-05-24 08:54:11 -04:00
greenkeeper[bot]
c3d07db39a fix(package): update scratch-svg-renderer to version 0.2.0-prerelease.20190523193400 2019-05-23 19:37:48 +00:00
Karishma Chadha
5ef4ae63ba Merge pull request #458 from LLK/greenkeeper/scratch-svg-renderer-0.2.0-prerelease.20190521170426
Update scratch-svg-renderer to the latest version 🚀
2019-05-22 17:29:49 -04:00
adroitwhiz
ab14e224d6 add default fudge min/max values 2019-05-22 05:02:44 -04:00
adroitwhiz
1a5bd39f77 replace janky boolean logic 2019-05-22 05:01:16 -04:00
adroitwhiz
e478ad4590 es6ify playground.js 2019-05-22 04:58:06 -04:00
greenkeeper[bot]
f5ddc24c7e fix(package): update scratch-svg-renderer to version 0.2.0-prerelease.20190521170426 2019-05-21 17:05:46 +00:00
adroitwhiz
c313f6ac89 fix documentation 2019-05-18 00:46:09 -04:00
Katie Broida
9fc82a8fc3 Tweak scalingVector to make dots appear to be more circular instead of oval 2019-05-13 13:27:08 -04:00
adroitwhiz
e8d30d7629 Merge branch 'develop' of https://github.com/LLK/scratch-render into playground-improvements 2019-05-05 16:52:05 -04:00
adroitwhiz
fab2a52a96 build's fixed 🦀 2019-05-05 16:46:13 -04:00
adroitwhiz
7bb8b3829e roundBounds! it does nothing! 2019-05-05 03:06:04 -04:00
adroitwhiz
b60b2aadde wait, is the build fixed? 2019-05-01 02:16:38 -04:00
adroitwhiz
e8fb7daaec remove no-longer-applicable unit test 2019-04-30 23:04:26 -04:00
adroitwhiz
57e40e20ab Revert playground changes 2019-04-30 22:56:17 -04:00
adroitwhiz
1021877ba6 Canvas-based TextBubbleSkin 2019-04-30 22:51:43 -04:00
adroitwhiz
b92354b1bf Merge branch 'develop' of https://github.com/LLK/scratch-render into playground-improvements 2019-04-23 13:21:41 -04:00
adroitwhiz
6c8b5bc2a9 Fix webpack glob 2019-04-23 13:21:40 -04:00
adroitwhiz
95a3c0dc6f Appease ESLint 2019-04-19 13:50:52 -04:00
adroitwhiz
05928eb400 Add very basic pen testing to playground 2019-04-19 13:44:47 -04:00
adroitwhiz
924050baaf Merge branch 'develop' of https://github.com/LLK/scratch-render into playground-improvements 2019-03-29 10:19:41 -04:00
adroitwhiz
0b9ee47fa1 Add "Scale (both)" option and fix quotes 2019-03-29 10:19:37 -04:00
adroitwhiz
3e710e66ec Move playground style rules into stylesheet 2019-03-24 03:03:51 -04:00
adroitwhiz
2f14126d0b Add stage scale slider to playground 2019-03-24 02:50:11 -04:00
adroitwhiz
4e9223adc6 More fixes for loading in playground 2019-03-24 02:43:15 -04:00
adroitwhiz
5419d3d2c3 Use updated Scratch cat SVG in playground 2019-03-24 02:42:13 -04:00
adroitwhiz
d4df59b23b Fix playground not re-reading inputs after page reload 2019-03-24 02:38:05 -04:00
13 changed files with 467 additions and 402 deletions

View File

@@ -32,7 +32,7 @@
"chromeless": "^1.5.1",
"copy-webpack-plugin": "^4.5.1",
"docdash": "^0.4.0",
"eslint": "^4.6.1",
"eslint": "^6.2.0",
"eslint-config-scratch": "^5.0.0",
"gh-pages": "^1.0.0",
"jsdoc": "^3.5.5",
@@ -53,7 +53,7 @@
"minilog": "3.1.0",
"raw-loader": "^0.5.1",
"scratch-storage": "^1.0.0",
"scratch-svg-renderer": "0.2.0-prerelease.20190419183947",
"scratch-svg-renderer": "0.2.0-prerelease.20190715153806",
"twgl.js": "4.4.0"
}
}

View File

@@ -10,7 +10,7 @@ const PenSkin = require('./PenSkin');
const RenderConstants = require('./RenderConstants');
const ShaderManager = require('./ShaderManager');
const SVGSkin = require('./SVGSkin');
const SVGTextBubble = require('./util/svg-text-bubble');
const TextBubbleSkin = require('./TextBubbleSkin');
const EffectTransform = require('./EffectTransform');
const log = require('./util/log');
@@ -184,8 +184,6 @@ class RenderWebGL extends EventEmitter {
/** @type {Array.<snapshotCallback>} */
this._snapshotCallbacks = [];
this._svgTextBubble = new SVGTextBubble();
this._createGeometry();
this.on(RenderConstants.Events.NativeSizeChanged, this.onNativeSizeChanged);
@@ -343,8 +341,11 @@ class RenderWebGL extends EventEmitter {
* @returns {!int} the ID for the new skin.
*/
createTextSkin (type, text, pointsLeft) {
const bubbleSvg = this._svgTextBubble.buildString(type, text, pointsLeft);
return this.createSVGSkin(bubbleSvg, [0, 0]);
const skinId = this._nextSkinId++;
const newSkin = new TextBubbleSkin(skinId, this);
newSkin.setTextBubble(type, text, pointsLeft);
this._allSkins[skinId] = newSkin;
return skinId;
}
/**
@@ -407,8 +408,14 @@ class RenderWebGL extends EventEmitter {
* @param {!boolean} pointsLeft - which side the bubble is pointing.
*/
updateTextSkin (skinId, type, text, pointsLeft) {
const bubbleSvg = this._svgTextBubble.buildString(type, text, pointsLeft);
this.updateSVGSkin(skinId, bubbleSvg, [0, 0]);
if (this._allSkins[skinId] instanceof TextBubbleSkin) {
this._allSkins[skinId].setTextBubble(type, text, pointsLeft);
return;
}
const newSkin = new TextBubbleSkin(skinId, this);
newSkin.setTextBubble(type, text, pointsLeft);
this._reskin(skinId, newSkin);
}

287
src/TextBubbleSkin.js Normal file
View File

@@ -0,0 +1,287 @@
const twgl = require('twgl.js');
const TextWrapper = require('./util/text-wrapper');
const CanvasMeasurementProvider = require('./util/canvas-measurement-provider');
const Skin = require('./Skin');
const BubbleStyle = {
MAX_LINE_WIDTH: 170, // Maximum width, in Scratch pixels, of a single line of text
MIN_WIDTH: 50, // Minimum width, in Scratch pixels, of a text bubble
STROKE_WIDTH: 4, // Thickness of the stroke around the bubble. Only half's visible because it's drawn under the fill
PADDING: 10, // Padding around the text area
CORNER_RADIUS: 16, // Radius of the rounded corners
TAIL_HEIGHT: 12, // Height of the speech bubble's "tail". Probably should be a constant.
FONT: 'Helvetica', // Font to render the text with
FONT_SIZE: 14, // Font size, in Scratch pixels
FONT_HEIGHT_RATIO: 0.9, // Height, in Scratch pixels, of the text, as a proportion of the font's size
LINE_HEIGHT: 16, // Spacing between each line of text
COLORS: {
BUBBLE_FILL: 'white',
BUBBLE_STROKE: 'rgba(0, 0, 0, 0.15)',
TEXT_FILL: '#575E75'
}
};
class TextBubbleSkin extends Skin {
/**
* Create a new text bubble skin.
* @param {!int} id - The ID for this Skin.
* @param {!RenderWebGL} renderer - The renderer which will use this skin.
* @constructor
* @extends Skin
*/
constructor (id, renderer) {
super(id);
/** @type {RenderWebGL} */
this._renderer = renderer;
/** @type {HTMLCanvasElement} */
this._canvas = document.createElement('canvas');
/** @type {WebGLTexture} */
this._texture = null;
/** @type {Array<number>} */
this._size = [0, 0];
/** @type {number} */
this._renderedScale = 0;
/** @type {Array<string>} */
this._lines = [];
this._textSize = {width: 0, height: 0};
this._textAreaSize = {width: 0, height: 0};
/** @type {string} */
this._bubbleType = '';
/** @type {boolean} */
this._pointsLeft = false;
/** @type {boolean} */
this._textDirty = true;
/** @type {boolean} */
this._textureDirty = true;
this.measurementProvider = new CanvasMeasurementProvider(this._canvas.getContext('2d'));
this.textWrapper = new TextWrapper(this.measurementProvider);
this._restyleCanvas();
}
/**
* Dispose of this object. Do not use it after calling this method.
*/
dispose () {
if (this._texture) {
this._renderer.gl.deleteTexture(this._texture);
this._texture = null;
}
this._canvas = null;
super.dispose();
}
/**
* @return {Array<number>} the dimensions, in Scratch units, of this skin.
*/
get size () {
if (this._textDirty) {
this._reflowLines();
}
return this._size;
}
/**
* Set parameters for this text bubble.
* @param {!string} type - either "say" or "think".
* @param {!string} text - the text for the bubble.
* @param {!boolean} pointsLeft - which side the bubble is pointing.
*/
setTextBubble (type, text, pointsLeft) {
this._text = text;
this._bubbleType = type;
this._pointsLeft = pointsLeft;
this._textDirty = true;
this._textureDirty = true;
this.emit(Skin.Events.WasAltered);
}
/**
* Re-style the canvas after resizing it. This is necessary to ensure proper text measurement.
*/
_restyleCanvas () {
this._canvas.getContext('2d').font = `${BubbleStyle.FONT_SIZE}px ${BubbleStyle.FONT}, sans-serif`;
}
/**
* Update the array of wrapped lines and the text dimensions.
*/
_reflowLines () {
this._lines = this.textWrapper.wrapText(BubbleStyle.MAX_LINE_WIDTH, this._text);
// Measure width of longest line to avoid extra-wide bubbles
let longestLine = 0;
for (const line of this._lines) {
longestLine = Math.max(longestLine, this.measurementProvider.measureText(line));
}
this._textSize.width = longestLine;
this._textSize.height = BubbleStyle.LINE_HEIGHT * this._lines.length;
// Calculate the canvas-space sizes of the padded text area and full text bubble
const paddedWidth = Math.max(this._textSize.width, BubbleStyle.MIN_WIDTH) + (BubbleStyle.PADDING * 2);
const paddedHeight = this._textSize.height + (BubbleStyle.PADDING * 2);
this._textAreaSize.width = paddedWidth;
this._textAreaSize.height = paddedHeight;
this._size[0] = paddedWidth + BubbleStyle.STROKE_WIDTH;
this._size[1] = paddedHeight + BubbleStyle.STROKE_WIDTH + BubbleStyle.TAIL_HEIGHT;
this._textDirty = false;
}
/**
* Render this text bubble at a certain scale, using the current parameters, to the canvas.
* @param {number} scale The scale to render the bubble at
*/
_renderTextBubble (scale) {
const ctx = this._canvas.getContext('2d');
if (this._textDirty) {
this._reflowLines();
}
// Calculate the canvas-space sizes of the padded text area and full text bubble
const paddedWidth = this._textAreaSize.width;
const paddedHeight = this._textAreaSize.height;
// Resize the canvas to the correct screen-space size
this._canvas.width = Math.ceil(this._size[0] * scale);
this._canvas.height = Math.ceil(this._size[1] * scale);
this._restyleCanvas();
// Reset the transform before clearing to ensure 100% clearage
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
ctx.scale(scale, scale);
ctx.translate(BubbleStyle.STROKE_WIDTH * 0.5, BubbleStyle.STROKE_WIDTH * 0.5);
// If the text bubble points leftward, flip the canvas
ctx.save();
if (this._pointsLeft) {
ctx.scale(-1, 1);
ctx.translate(-paddedWidth, 0);
}
// Draw the bubble's rounded borders
ctx.moveTo(BubbleStyle.CORNER_RADIUS, paddedHeight);
ctx.arcTo(0, paddedHeight, 0, paddedHeight - BubbleStyle.CORNER_RADIUS, BubbleStyle.CORNER_RADIUS);
ctx.arcTo(0, 0, paddedWidth, 0, BubbleStyle.CORNER_RADIUS);
ctx.arcTo(paddedWidth, 0, paddedWidth, paddedHeight, BubbleStyle.CORNER_RADIUS);
ctx.arcTo(paddedWidth, paddedHeight, paddedWidth - BubbleStyle.CORNER_RADIUS, paddedHeight,
BubbleStyle.CORNER_RADIUS);
// Translate the canvas so we don't have to do a bunch of width/height arithmetic
ctx.save();
ctx.translate(paddedWidth - BubbleStyle.CORNER_RADIUS, paddedHeight);
// Draw the bubble's "tail"
if (this._bubbleType === 'say') {
// For a speech bubble, draw one swoopy thing
ctx.bezierCurveTo(0, 4, 4, 8, 4, 10);
ctx.arcTo(4, 12, 2, 12, 2);
ctx.bezierCurveTo(-1, 12, -11, 8, -16, 0);
ctx.closePath();
} else {
// For a thinking bubble, draw a partial circle attached to the bubble...
ctx.arc(-16, 0, 4, 0, Math.PI);
ctx.closePath();
// and two circles detached from it
ctx.moveTo(-7, 7.25);
ctx.arc(-9.25, 7.25, 2.25, 0, Math.PI * 2);
ctx.moveTo(0, 9.5);
ctx.arc(-1.5, 9.5, 1.5, 0, Math.PI * 2);
}
// Un-translate the canvas and fill + stroke the text bubble
ctx.restore();
ctx.fillStyle = BubbleStyle.COLORS.BUBBLE_FILL;
ctx.strokeStyle = BubbleStyle.COLORS.BUBBLE_STROKE;
ctx.lineWidth = BubbleStyle.STROKE_WIDTH;
ctx.stroke();
ctx.fill();
// Un-flip the canvas if it was flipped
ctx.restore();
// Draw each line of text
ctx.fillStyle = BubbleStyle.COLORS.TEXT_FILL;
ctx.font = `${BubbleStyle.FONT_SIZE}px ${BubbleStyle.FONT}, sans-serif`;
const lines = this._lines;
for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) {
const line = lines[lineNumber];
ctx.fillText(
line,
BubbleStyle.PADDING,
BubbleStyle.PADDING + (BubbleStyle.LINE_HEIGHT * lineNumber) +
(BubbleStyle.FONT_HEIGHT_RATIO * BubbleStyle.FONT_SIZE)
);
}
this._renderedScale = scale;
}
/**
* @param {Array<number>} scale - The scaling factors to be used, each in the [0,100] range.
* @return {WebGLTexture} The GL texture representation of this skin when drawing at the given scale.
*/
getTexture (scale) {
// The texture only ever gets uniform scale. Take the larger of the two axes.
const scaleMax = scale ? Math.max(Math.abs(scale[0]), Math.abs(scale[1])) : 100;
const requestedScale = scaleMax / 100;
// If we already rendered the text bubble at this scale, we can skip re-rendering it.
if (this._textureDirty || this._renderedScale !== requestedScale) {
this._renderTextBubble(requestedScale);
this._textureDirty = false;
const context = this._canvas.getContext('2d');
const textureData = context.getImageData(0, 0, this._canvas.width, this._canvas.height);
const gl = this._renderer.gl;
if (this._texture === null) {
const textureOptions = {
auto: true,
wrap: gl.CLAMP_TO_EDGE,
src: textureData
};
this._texture = twgl.createTexture(gl, textureOptions);
}
gl.bindTexture(gl.TEXTURE_2D, this._texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureData);
this._silhouette.update(textureData);
}
return this._texture;
}
}
module.exports = TextBubbleSkin;

View File

@@ -3,13 +3,11 @@
<head>
<meta charset="UTF-8">
<title>Scratch WebGL rendering demo</title>
<style>
#scratch-stage { width: 480px; }
</style>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body style="background: lightsteelblue">
<canvas id="scratch-stage" width="10" height="10" style="border:3px dashed black"></canvas>
<canvas id="debug-canvas" width="10" height="10" style="border:3px dashed red"></canvas>
<body>
<canvas id="scratch-stage" width="10" height="10"></canvas>
<canvas id="debug-canvas" width="10" height="10"></canvas>
<p>
<label for="fudgeproperty">Property to tweak:</label>
<select id="fudgeproperty">
@@ -18,6 +16,7 @@
<option value="direction">Direction</option>
<option value="scalex">Scale X</option>
<option value="scaley">Scale Y</option>
<option value="scaleboth">Scale (both dimensions)</option>
<option value="color">Color</option>
<option value="fisheye">Fisheye</option>
<option value="whirl">Whirl</option>
@@ -30,8 +29,12 @@
<input type="range" id="fudge" style="width:50%" value="90" min="-90" max="270" step="any">
</p>
<p>
<label for="fudgeMin">Min:</label><input id="fudgeMin" type="number">
<label for="fudgeMax">Max:</label><input id="fudgeMax" type="number">
<label for="stage-scale">Stage scale:</label>
<input type="range" style="width:50%" id="stage-scale" value="1" min="1" max="2.5" step="any">
</p>
<p>
<label for="fudgeMin">Min:</label><input id="fudgeMin" type="number" value="0">
<label for="fudgeMax">Max:</label><input id="fudgeMax" type="number" value="200">
</p>
<script src="playground.js"></script>
</body>

View File

@@ -1,26 +1,32 @@
const ScratchRender = require('../RenderWebGL');
const getMousePosition = require('./getMousePosition');
var canvas = document.getElementById('scratch-stage');
var fudge = 90;
var renderer = new ScratchRender(canvas);
const canvas = document.getElementById('scratch-stage');
let fudge = 90;
const renderer = new ScratchRender(canvas);
renderer.setLayerGroupOrdering(['group1']);
var drawableID = renderer.createDrawable('group1');
const drawableID = renderer.createDrawable('group1');
renderer.updateDrawableProperties(drawableID, {
position: [0, 0],
scale: [100, 100],
direction: 90
});
var drawableID2 = renderer.createDrawable('group1');
var wantBitmapSkin = false;
const WantedSkinType = {
bitmap: 'bitmap',
vector: 'vector',
pen: 'pen'
};
const drawableID2 = renderer.createDrawable('group1');
const wantedSkin = WantedSkinType.vector;
// Bitmap (squirrel)
var image = new Image();
const image = new Image();
image.addEventListener('load', () => {
var bitmapSkinId = renderer.createBitmapSkin(image);
if (wantBitmapSkin) {
const bitmapSkinId = renderer.createBitmapSkin(image);
if (wantedSkin === WantedSkinType.bitmap) {
renderer.updateDrawableProperties(drawableID2, {
skinId: bitmapSkinId
});
@@ -30,44 +36,83 @@ image.crossOrigin = 'anonymous';
image.src = 'https://cdn.assets.scratch.mit.edu/internalapi/asset/7e24c99c1b853e52f8e7f9004416fa34.png/get/';
// SVG (cat 1-a)
var xhr = new XMLHttpRequest();
const xhr = new XMLHttpRequest();
xhr.addEventListener('load', function () {
var skinId = renderer.createSVGSkin(xhr.responseText);
if (!wantBitmapSkin) {
const skinId = renderer.createSVGSkin(xhr.responseText);
if (wantedSkin === WantedSkinType.vector) {
renderer.updateDrawableProperties(drawableID2, {
skinId: skinId
});
}
});
xhr.open('GET', 'https://cdn.assets.scratch.mit.edu/internalapi/asset/f88bf1935daea28f8ca098462a31dbb0.svg/get/');
xhr.open('GET', 'https://cdn.assets.scratch.mit.edu/internalapi/asset/b7853f557e4426412e64bb3da6531a99.svg/get/');
xhr.send();
var posX = 0;
var posY = 0;
var scaleX = 100;
var scaleY = 100;
var fudgeProperty = 'posx';
if (wantedSkin === WantedSkinType.pen) {
const penSkinID = renderer.createPenSkin();
const fudgePropertyInput = document.getElementById('fudgeproperty');
fudgePropertyInput.addEventListener('change', event => {
fudgeProperty = event.target.value;
});
renderer.updateDrawableProperties(drawableID2, {
skinId: penSkinID
});
canvas.addEventListener('click', event => {
let rect = canvas.getBoundingClientRect();
let x = event.clientX - rect.left;
let y = event.clientY - rect.top;
renderer.penLine(penSkinID, {
color4f: [Math.random(), Math.random(), Math.random(), 1],
diameter: 8
},
x - 240, 180 - y, (Math.random() * 480) - 240, (Math.random() * 360) - 180);
});
}
let posX = 0;
let posY = 0;
let scaleX = 100;
let scaleY = 100;
let fudgeProperty = 'posx';
const fudgeInput = document.getElementById('fudge');
const fudgePropertyInput = document.getElementById('fudgeproperty');
const fudgeMinInput = document.getElementById('fudgeMin');
fudgeMinInput.addEventListener('change', event => {
fudgeInput.min = event.target.valueAsNumber;
});
const fudgeMaxInput = document.getElementById('fudgeMax');
fudgeMaxInput.addEventListener('change', event => {
/* eslint require-jsdoc: 0 */
const updateFudgeProperty = event => {
fudgeProperty = event.target.value;
};
const updateFudgeMin = event => {
fudgeInput.min = event.target.valueAsNumber;
};
const updateFudgeMax = event => {
fudgeInput.max = event.target.valueAsNumber;
});
};
fudgePropertyInput.addEventListener('change', updateFudgeProperty);
fudgePropertyInput.addEventListener('init', updateFudgeProperty);
fudgeMinInput.addEventListener('change', updateFudgeMin);
fudgeMinInput.addEventListener('init', updateFudgeMin);
fudgeMaxInput.addEventListener('change', updateFudgeMax);
fudgeMaxInput.addEventListener('init', updateFudgeMax);
// Ugly hack to properly set the values of the inputs on page load,
// since they persist across reloads, at least in Firefox.
// The best ugly hacks are the ones that reduce code duplication!
fudgePropertyInput.dispatchEvent(new CustomEvent('init'));
fudgeMinInput.dispatchEvent(new CustomEvent('init'));
fudgeMaxInput.dispatchEvent(new CustomEvent('init'));
fudgeInput.dispatchEvent(new CustomEvent('init'));
const handleFudgeChanged = function (event) {
fudge = event.target.valueAsNumber;
var props = {};
const props = {};
switch (fudgeProperty) {
case 'posx':
props.position = [fudge, posY];
@@ -88,6 +133,11 @@ const handleFudgeChanged = function (event) {
props.scale = [scaleX, fudge];
scaleY = fudge;
break;
case 'scaleboth':
props.scale = [fudge, fudge];
scaleX = fudge;
scaleY = fudge;
break;
case 'color':
props.color = fudge;
break;
@@ -112,17 +162,28 @@ const handleFudgeChanged = function (event) {
}
renderer.updateDrawableProperties(drawableID2, props);
};
fudgeInput.addEventListener('input', handleFudgeChanged);
fudgeInput.addEventListener('change', handleFudgeChanged);
fudgeInput.addEventListener('init', handleFudgeChanged);
const updateStageScale = event => {
renderer.resize(480 * event.target.valueAsNumber, 360 * event.target.valueAsNumber);
};
const stageScaleInput = document.getElementById('stage-scale');
stageScaleInput.addEventListener('input', updateStageScale);
stageScaleInput.addEventListener('change', updateStageScale);
canvas.addEventListener('mousemove', event => {
var mousePos = getMousePosition(event, canvas);
const mousePos = getMousePosition(event, canvas);
renderer.extractColor(mousePos.x, mousePos.y, 30);
});
canvas.addEventListener('click', event => {
var mousePos = getMousePosition(event, canvas);
var pickID = renderer.pick(mousePos.x, mousePos.y);
const mousePos = getMousePosition(event, canvas);
const pickID = renderer.pick(mousePos.x, mousePos.y);
console.log('You clicked on ' + (pickID < 0 ? 'nothing' : 'ID# ' + pickID));
if (pickID >= 0) {
console.dir(renderer.extractDrawable(pickID, mousePos.x, mousePos.y));
@@ -137,5 +198,5 @@ const drawStep = function () {
};
drawStep();
var debugCanvas = /** @type {canvas} */ document.getElementById('debug-canvas');
const debugCanvas = /** @type {canvas} */ document.getElementById('debug-canvas');
renderer.setDebugCanvas(debugCanvas);

View File

@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<title>Scratch WebGL Query Playground</title>
<link rel="stylesheet" type="text/css" href="style.css">
<style>
input[type=range][orient=vertical] {
writing-mode: bt-lr; /* IE */
@@ -11,8 +12,6 @@
padding: 0 0.5rem;
}
canvas {
border: 3px dashed black;
/* https://stackoverflow.com/a/7665647 */
image-rendering: optimizeSpeed; /* Older versions of FF */
image-rendering: -moz-crisp-edges; /* FF 6.0+ */
@@ -23,7 +22,7 @@
}
</style>
</head>
<body style="background: lightsteelblue">
<body>
<div>
<fieldset>
<legend>Query Canvases</legend>
@@ -63,7 +62,7 @@
<input id="cursorY" type="range" orient="vertical" step="0.25" value="0" />
</td>
<td>
<canvas id="renderCanvas" width="480" height="360" style="border:3px dashed black"></canvas>
<canvas id="renderCanvas" width="480" height="360"></canvas>
</td>
</tr>
</table>

11
src/playground/style.css Normal file
View File

@@ -0,0 +1,11 @@
body {
background: lightsteelblue;
}
canvas {
border: 3px dashed black;
}
#debug-canvas {
border-color: red;
}

View File

@@ -0,0 +1,41 @@
class CanvasMeasurementProvider {
/**
* @param {CanvasRenderingContext2D} ctx - provides a canvas rendering context
* with 'font' set to the text style of the text to be wrapped.
*/
constructor (ctx) {
this._ctx = ctx;
this._cache = {};
}
// We don't need to set up or tear down anything here. Should these be removed altogether?
/**
* Called by the TextWrapper before a batch of zero or more calls to measureText().
*/
beginMeasurementSession () {
}
/**
* Called by the TextWrapper after a batch of zero or more calls to measureText().
*/
endMeasurementSession () {
}
/**
* Measure a whole string as one unit.
* @param {string} text - the text to measure.
* @returns {number} - the length of the string.
*/
measureText (text) {
if (!this._cache[text]) {
this._cache[text] = this._ctx.measureText(text).width;
}
return this._cache[text];
}
}
module.exports = CanvasMeasurementProvider;

View File

@@ -1,205 +0,0 @@
const SVGTextWrapper = require('./svg-text-wrapper');
const SvgRenderer = require('scratch-svg-renderer').SVGRenderer;
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._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}
A ${radius} ${radius} 0 0 1 ${radius} 0
L ${w - radius} 0
A ${radius} ${radius} 0 0 1 ${w} ${radius}
L ${w} ${h - radius}
A ${radius} ${radius} 0 0 1 ${w - radius} ${h}`;
if (pointsLeft) {
pathString += `
L 32 ${h}
c -5 8 -15 12 -18 12
a 2 2 0 0 1 -2 -2
c 0 -2 4 -6 4 -10`;
} else {
pathString += `
L ${w - 16} ${h}
c 0 4 4 8 4 10
a 2 2 0 0 1 -2 2
c -3 0 -13 -4 -18 -12`;
}
pathString += `
L ${radius} ${h}
A ${radius} ${radius} 0 0 1 0 ${h - radius}
Z`;
return `
<g>
<path
d="${pathString}"
stroke="rgba(0, 0, 0, 0.15)"
stroke-width="${STROKE_WIDTH}"
fill="rgba(0, 0, 0, 0.15)"
stroke-line-join="round"
/>
<path
d="${pathString}"
stroke="none"
fill="white" />
</g>`;
}
_thinkBubble (w, h, radius, pointsLeft) {
const e1rx = 2.25;
const e1ry = 2.25;
const e2rx = 1.5;
const e2ry = 1.5;
const e1x = 16 + 7 + e1rx;
const e1y = 5 + h + e1ry;
const e2x = 16 + e2rx;
const e2y = 8 + h + e2ry;
const insetR = 4;
const pInset1 = 12 + radius;
const pInset2 = pInset1 + (2 * insetR);
let pathString = `
M 0 ${radius}
A ${radius} ${radius} 0 0 1 ${radius} 0
L ${w - radius} 0
A ${radius} ${radius} 0 0 1 ${w} ${radius}
L ${w} ${h - radius}
A ${radius} ${radius} 0 0 1 ${w - radius} ${h}`;
if (pointsLeft) {
pathString += `
L ${pInset2} ${h}
A ${insetR} ${insetR} 0 0 1 ${pInset2 - insetR} ${h + insetR}
A ${insetR} ${insetR} 0 0 1 ${pInset1} ${h}`;
} else {
pathString += `
L ${w - pInset1} ${h}
A ${insetR} ${insetR} 0 0 1 ${w - pInset1 - insetR} ${h + insetR}
A ${insetR} ${insetR} 0 0 1 ${w - pInset2} ${h}`;
}
pathString += `
L ${radius} ${h}
A ${radius} ${radius} 0 0 1 0 ${h - radius}
Z`;
const ellipseSvg = (cx, cy, rx, ry) => `
<g>
<ellipse
cx="${cx}" cy="${cy}"
rx="${rx}" ry="${ry}"
fill="rgba(0, 0, 0, 0.15)"
stroke="rgba(0, 0, 0, 0.15)"
stroke-width="${STROKE_WIDTH}"
/>
<ellipse
cx="${cx}" cy="${cy}"
rx="${rx}" ry="${ry}"
fill="white"
stroke="none"
/>
</g>`;
let ellipses = [];
if (pointsLeft) {
ellipses = [
ellipseSvg(e1x, e1y, e1rx, e1ry),
ellipseSvg(e2x, e2y, e2rx, e2ry)
];
} else {
ellipses = [
ellipseSvg(w - e1x, e1y, e1rx, e1ry),
ellipseSvg(w - e2x, e2y, e2rx, e2ry)
];
}
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="none" fill="white" />
${ellipses.join('\n')}
</g>`;
}
_getTextSize (textFragment) {
const svgString = this._wrapSvgFragment(textFragment);
if (!this._textSizeCache[svgString]) {
this._textSizeCache[svgString] = this.svgRenderer.measure(svgString);
if (this._textSizeCache[svgString].height === 0) {
// The speech bubble is empty, so use the height of a single line with content (or else it renders
// weirdly, see issue #302).
const dummyFragment = this._buildTextFragment('X');
this._textSizeCache[svgString] = this._getTextSize(dummyFragment);
}
}
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;
}
_buildTextFragment (text) {
const textNode = this.svgTextWrapper.wrapText(MAX_LINE_LENGTH, text);
const serializer = new XMLSerializer();
return serializer.serializeToString(textNode);
}
buildString (type, text, pointsLeft) {
this.type = type;
this.pointsLeft = pointsLeft;
this._textFragment = this._buildTextFragment(text);
let fragment = '';
const radius = 16;
const {x, y, width, height} = this._getTextSize(this._textFragment);
const padding = 10;
const fullWidth = Math.max(MIN_WIDTH, width) + (2 * padding);
const fullHeight = height + (2 * padding);
if (this.type === 'say') {
fragment += this._speechBubble(fullWidth, fullHeight, radius, this.pointsLeft);
} 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);
}
}
module.exports = SVGTextBubble;

View File

@@ -1,127 +0,0 @@
const TextWrapper = require('./text-wrapper');
/**
* Measure text by using a hidden SVG attached to the DOM.
* 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) {
this._svgRoot = null;
this._cache = {};
this.makeTextElement = makeTextElement;
}
/**
* Detach the hidden SVG element from the DOM and forget all references to it and its children.
*/
dispose () {
if (this._svgRoot) {
this._svgRoot.parentElement.removeChild(this._svgRoot);
this._svgRoot = null;
this._svgText = null;
}
}
/**
* Called by the TextWrapper before a batch of zero or more calls to measureText().
*/
beginMeasurementSession () {
if (!this._svgRoot) {
this._init();
}
}
/**
* Called by the TextWrapper after a batch of zero or more calls to measureText().
*/
endMeasurementSession () {
this._svgText.textContent = '';
this.dispose();
}
/**
* Measure a whole string as one unit.
* @param {string} text - the text to measure.
* @returns {number} - the length of the string.
*/
measureText (text) {
if (!this._cache[text]) {
this._svgText.textContent = text;
this._cache[text] = this._svgText.getComputedTextLength();
}
return this._cache[text];
}
/**
* Create a simple SVG containing a text node, hide it, and attach it to the DOM. The text node will be used to
* collect text measurements. The SVG must be attached to the DOM: otherwise measurements will generally be zero.
* @private
*/
_init () {
const svgNamespace = 'http://www.w3.org/2000/svg';
const svgRoot = document.createElementNS(svgNamespace, 'svg');
const svgGroup = document.createElementNS(svgNamespace, 'g');
const svgText = this.makeTextElement();
// hide from the user, including screen readers
svgRoot.setAttribute('style', 'position:absolute;visibility:hidden');
document.body.appendChild(svgRoot);
svgRoot.appendChild(svgGroup);
svgGroup.appendChild(svgText);
/**
* The root SVG element.
* @type {SVGSVGElement}
* @private
*/
this._svgRoot = svgRoot;
/**
* The leaf SVG element used for text measurement.
* @type {SVGTextElement}
* @private
*/
this._svgText = svgText;
}
}
/**
* 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;
}
}
module.exports = SVGTextWrapper;

View File

@@ -16,7 +16,7 @@ const GraphemeBreaker = require('!ify-loader!grapheme-breaker');
* break opportunities.
* Reference material:
* - Unicode Standard Annex #14: http://unicode.org/reports/tr14/
* - Unicode Standard Annex #39: http://unicode.org/reports/tr29/
* - Unicode Standard Annex #29: http://unicode.org/reports/tr29/
* - "JavaScript has a Unicode problem" by Mathias Bynens: https://mathiasbynens.be/notes/javascript-unicode
*/
class TextWrapper {

View File

@@ -1,4 +1,4 @@
/* global vm, render, Promise */
/* global vm, Promise */
const {Chromeless} = require('chromeless');
const test = require('tap').test;
const path = require('path');
@@ -101,16 +101,6 @@ const testFile = file => test(file, async t => {
}
});
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())
@@ -120,8 +110,6 @@ const testBubbles = () => test('bubble snapshot', async t => {
await testFile(file);
}
await testBubbles();
// close the browser window we used
await chromeless.end();
})();

View File

@@ -51,7 +51,7 @@ module.exports = [
new CopyWebpackPlugin([
{
context: 'src/playground',
from: '*.html'
from: '*.+(html|css)'
}
])
])