@scratch/scratch-render 11.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.nvmrc +1 -0
- package/CHANGELOG.md +2281 -0
- package/LICENSE +12 -0
- package/README.md +87 -0
- package/TRADEMARK +1 -0
- package/commitlint.config.js +4 -0
- package/docs/Rectangle-AABB-Matrix.md +192 -0
- package/package.json +84 -0
- package/release.config.js +10 -0
- package/src/BitmapSkin.js +120 -0
- package/src/Drawable.js +734 -0
- package/src/EffectTransform.js +197 -0
- package/src/PenSkin.js +350 -0
- package/src/Rectangle.js +196 -0
- package/src/RenderConstants.js +34 -0
- package/src/RenderWebGL.js +2029 -0
- package/src/SVGSkin.js +239 -0
- package/src/ShaderManager.js +187 -0
- package/src/Silhouette.js +257 -0
- package/src/Skin.js +235 -0
- package/src/TextBubbleSkin.js +284 -0
- package/src/index.js +7 -0
- package/src/playground/getMousePosition.js +37 -0
- package/src/playground/index.html +41 -0
- package/src/playground/playground.js +202 -0
- package/src/playground/queryPlayground.html +73 -0
- package/src/playground/queryPlayground.js +196 -0
- package/src/playground/style.css +11 -0
- package/src/shaders/sprite.frag +249 -0
- package/src/shaders/sprite.vert +75 -0
- package/src/util/canvas-measurement-provider.js +41 -0
- package/src/util/color-conversions.js +97 -0
- package/src/util/log.js +4 -0
- package/src/util/text-wrapper.js +112 -0
package/src/Skin.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
const EventEmitter = require('events');
|
|
2
|
+
|
|
3
|
+
const twgl = require('twgl.js');
|
|
4
|
+
|
|
5
|
+
const RenderConstants = require('./RenderConstants');
|
|
6
|
+
const Silhouette = require('./Silhouette');
|
|
7
|
+
|
|
8
|
+
class Skin extends EventEmitter {
|
|
9
|
+
/**
|
|
10
|
+
* Create a Skin, which stores and/or generates textures for use in rendering.
|
|
11
|
+
* @param {int} id - The unique ID for this Skin.
|
|
12
|
+
* @constructor
|
|
13
|
+
*/
|
|
14
|
+
constructor (id) {
|
|
15
|
+
super();
|
|
16
|
+
|
|
17
|
+
/** @type {int} */
|
|
18
|
+
this._id = id;
|
|
19
|
+
|
|
20
|
+
/** @type {Vec3} */
|
|
21
|
+
this._rotationCenter = twgl.v3.create(0, 0);
|
|
22
|
+
|
|
23
|
+
/** @type {WebGLTexture} */
|
|
24
|
+
this._texture = null;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The uniforms to be used by the vertex and pixel shaders.
|
|
28
|
+
* Some of these are used by other parts of the renderer as well.
|
|
29
|
+
* @type {Object.<string,*>}
|
|
30
|
+
* @private
|
|
31
|
+
*/
|
|
32
|
+
this._uniforms = {
|
|
33
|
+
/**
|
|
34
|
+
* The nominal (not necessarily current) size of the current skin.
|
|
35
|
+
* @type {Array<number>}
|
|
36
|
+
*/
|
|
37
|
+
u_skinSize: [0, 0],
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The actual WebGL texture object for the skin.
|
|
41
|
+
* @type {WebGLTexture}
|
|
42
|
+
*/
|
|
43
|
+
u_skin: null
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A silhouette to store touching data, skins are responsible for keeping it up to date.
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
this._silhouette = new Silhouette();
|
|
51
|
+
|
|
52
|
+
this.setMaxListeners(RenderConstants.SKIN_SHARE_SOFT_LIMIT);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Dispose of this object. Do not use it after calling this method.
|
|
57
|
+
*/
|
|
58
|
+
dispose () {
|
|
59
|
+
this._id = RenderConstants.ID_NONE;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @return {int} the unique ID for this Skin.
|
|
64
|
+
*/
|
|
65
|
+
get id () {
|
|
66
|
+
return this._id;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @returns {Vec3} the origin, in object space, about which this Skin should rotate.
|
|
71
|
+
*/
|
|
72
|
+
get rotationCenter () {
|
|
73
|
+
return this._rotationCenter;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @abstract
|
|
78
|
+
* @return {Array<number>} the "native" size, in texels, of this skin.
|
|
79
|
+
*/
|
|
80
|
+
get size () {
|
|
81
|
+
return [0, 0];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Should this skin's texture be filtered with nearest-neighbor or linear interpolation at the given scale?
|
|
86
|
+
* @param {?Array<Number>} scale The screen-space X and Y scaling factors at which this skin's texture will be
|
|
87
|
+
* displayed, as percentages (100 means 1 "native size" unit is 1 screen pixel; 200 means 2 screen pixels, etc).
|
|
88
|
+
* @param {Drawable} drawable The drawable that this skin's texture will be applied to.
|
|
89
|
+
* @return {boolean} True if this skin's texture, as returned by {@link getTexture}, should be filtered with
|
|
90
|
+
* nearest-neighbor interpolation.
|
|
91
|
+
*/
|
|
92
|
+
// eslint-disable-next-line no-unused-vars
|
|
93
|
+
useNearest (scale, drawable) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get the center of the current bounding box
|
|
99
|
+
* @return {Array<number>} the center of the current bounding box
|
|
100
|
+
*/
|
|
101
|
+
calculateRotationCenter () {
|
|
102
|
+
return [this.size[0] / 2, this.size[1] / 2];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @abstract
|
|
107
|
+
* @param {Array<number>} scale - The scaling factors to be used.
|
|
108
|
+
* @return {WebGLTexture} The GL texture representation of this skin when drawing at the given size.
|
|
109
|
+
*/
|
|
110
|
+
// eslint-disable-next-line no-unused-vars
|
|
111
|
+
getTexture (scale) {
|
|
112
|
+
return this._emptyImageTexture;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get the bounds of the drawable for determining its fenced position.
|
|
117
|
+
* @param {Array<number>} drawable - The Drawable instance this skin is using.
|
|
118
|
+
* @param {?Rectangle} result - Optional destination for bounds calculation.
|
|
119
|
+
* @return {!Rectangle} The drawable's bounds. For compatibility with Scratch 2, we always use getAABB.
|
|
120
|
+
*/
|
|
121
|
+
getFenceBounds (drawable, result) {
|
|
122
|
+
return drawable.getAABB(result);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Update and returns the uniforms for this skin.
|
|
127
|
+
* @param {Array<number>} scale - The scaling factors to be used.
|
|
128
|
+
* @returns {object.<string, *>} the shader uniforms to be used when rendering with this Skin.
|
|
129
|
+
*/
|
|
130
|
+
getUniforms (scale) {
|
|
131
|
+
this._uniforms.u_skin = this.getTexture(scale);
|
|
132
|
+
this._uniforms.u_skinSize = this.size;
|
|
133
|
+
return this._uniforms;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* If the skin defers silhouette operations until the last possible minute,
|
|
138
|
+
* this will be called before isTouching uses the silhouette.
|
|
139
|
+
* @abstract
|
|
140
|
+
*/
|
|
141
|
+
updateSilhouette () {}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Set this skin's texture to the given image.
|
|
145
|
+
* @param {ImageData|HTMLCanvasElement} textureData - The canvas or image data to set the texture to.
|
|
146
|
+
*/
|
|
147
|
+
_setTexture (textureData) {
|
|
148
|
+
const gl = this._renderer.gl;
|
|
149
|
+
|
|
150
|
+
gl.bindTexture(gl.TEXTURE_2D, this._texture);
|
|
151
|
+
// Premultiplied alpha is necessary for proper blending.
|
|
152
|
+
// See http://www.realtimerendering.com/blog/gpus-prefer-premultiplication/
|
|
153
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
|
|
154
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureData);
|
|
155
|
+
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
156
|
+
|
|
157
|
+
this._silhouette.update(textureData);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Set the contents of this skin to an empty skin.
|
|
162
|
+
* @fires Skin.event:WasAltered
|
|
163
|
+
*/
|
|
164
|
+
setEmptyImageData () {
|
|
165
|
+
// Free up the current reference to the _texture
|
|
166
|
+
this._texture = null;
|
|
167
|
+
|
|
168
|
+
if (!this._emptyImageData) {
|
|
169
|
+
// Create a transparent pixel
|
|
170
|
+
this._emptyImageData = new ImageData(1, 1);
|
|
171
|
+
|
|
172
|
+
// Create a new texture and update the silhouette
|
|
173
|
+
const gl = this._renderer.gl;
|
|
174
|
+
|
|
175
|
+
const textureOptions = {
|
|
176
|
+
auto: true,
|
|
177
|
+
wrap: gl.CLAMP_TO_EDGE,
|
|
178
|
+
src: this._emptyImageData
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Note: we're using _emptyImageTexture here instead of _texture
|
|
182
|
+
// so that we can cache this empty texture for later use as needed.
|
|
183
|
+
// this._texture can get modified by other skins (e.g. BitmapSkin
|
|
184
|
+
// and SVGSkin, so we can't use that same field for caching)
|
|
185
|
+
this._emptyImageTexture = twgl.createTexture(gl, textureOptions);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this._rotationCenter[0] = 0;
|
|
189
|
+
this._rotationCenter[1] = 0;
|
|
190
|
+
|
|
191
|
+
this._silhouette.update(this._emptyImageData);
|
|
192
|
+
this.emit(Skin.Events.WasAltered);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Does this point touch an opaque or translucent point on this skin?
|
|
197
|
+
* Nearest Neighbor version
|
|
198
|
+
* The caller is responsible for ensuring this skin's silhouette is up-to-date.
|
|
199
|
+
* @see updateSilhouette
|
|
200
|
+
* @see Drawable.updateCPURenderAttributes
|
|
201
|
+
* @param {twgl.v3} vec A texture coordinate.
|
|
202
|
+
* @return {boolean} Did it touch?
|
|
203
|
+
*/
|
|
204
|
+
isTouchingNearest (vec) {
|
|
205
|
+
return this._silhouette.isTouchingNearest(vec);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Does this point touch an opaque or translucent point on this skin?
|
|
210
|
+
* Linear Interpolation version
|
|
211
|
+
* The caller is responsible for ensuring this skin's silhouette is up-to-date.
|
|
212
|
+
* @see updateSilhouette
|
|
213
|
+
* @see Drawable.updateCPURenderAttributes
|
|
214
|
+
* @param {twgl.v3} vec A texture coordinate.
|
|
215
|
+
* @return {boolean} Did it touch?
|
|
216
|
+
*/
|
|
217
|
+
isTouchingLinear (vec) {
|
|
218
|
+
return this._silhouette.isTouchingLinear(vec);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* These are the events which can be emitted by instances of this class.
|
|
225
|
+
* @enum {string}
|
|
226
|
+
*/
|
|
227
|
+
Skin.Events = {
|
|
228
|
+
/**
|
|
229
|
+
* Emitted when anything about the Skin has been altered, such as the appearance or rotation center.
|
|
230
|
+
* @event Skin.event:WasAltered
|
|
231
|
+
*/
|
|
232
|
+
WasAltered: 'WasAltered'
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
module.exports = Skin;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
const twgl = require('twgl.js');
|
|
2
|
+
|
|
3
|
+
const TextWrapper = require('./util/text-wrapper');
|
|
4
|
+
const CanvasMeasurementProvider = require('./util/canvas-measurement-provider');
|
|
5
|
+
const Skin = require('./Skin');
|
|
6
|
+
|
|
7
|
+
const BubbleStyle = {
|
|
8
|
+
MAX_LINE_WIDTH: 170, // Maximum width, in Scratch pixels, of a single line of text
|
|
9
|
+
|
|
10
|
+
MIN_WIDTH: 50, // Minimum width, in Scratch pixels, of a text bubble
|
|
11
|
+
STROKE_WIDTH: 4, // Thickness of the stroke around the bubble. Only half's visible because it's drawn under the fill
|
|
12
|
+
PADDING: 10, // Padding around the text area
|
|
13
|
+
CORNER_RADIUS: 16, // Radius of the rounded corners
|
|
14
|
+
TAIL_HEIGHT: 12, // Height of the speech bubble's "tail". Probably should be a constant.
|
|
15
|
+
|
|
16
|
+
FONT: 'Helvetica', // Font to render the text with
|
|
17
|
+
FONT_SIZE: 14, // Font size, in Scratch pixels
|
|
18
|
+
FONT_HEIGHT_RATIO: 0.9, // Height, in Scratch pixels, of the text, as a proportion of the font's size
|
|
19
|
+
LINE_HEIGHT: 16, // Spacing between each line of text
|
|
20
|
+
|
|
21
|
+
COLORS: {
|
|
22
|
+
BUBBLE_FILL: 'white',
|
|
23
|
+
BUBBLE_STROKE: 'rgba(0, 0, 0, 0.15)',
|
|
24
|
+
TEXT_FILL: '#575E75'
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
class TextBubbleSkin extends Skin {
|
|
29
|
+
/**
|
|
30
|
+
* Create a new text bubble skin.
|
|
31
|
+
* @param {!int} id - The ID for this Skin.
|
|
32
|
+
* @param {!RenderWebGL} renderer - The renderer which will use this skin.
|
|
33
|
+
* @constructor
|
|
34
|
+
* @extends Skin
|
|
35
|
+
*/
|
|
36
|
+
constructor (id, renderer) {
|
|
37
|
+
super(id);
|
|
38
|
+
|
|
39
|
+
/** @type {RenderWebGL} */
|
|
40
|
+
this._renderer = renderer;
|
|
41
|
+
|
|
42
|
+
/** @type {HTMLCanvasElement} */
|
|
43
|
+
this._canvas = document.createElement('canvas');
|
|
44
|
+
|
|
45
|
+
/** @type {Array<number>} */
|
|
46
|
+
this._size = [0, 0];
|
|
47
|
+
|
|
48
|
+
/** @type {number} */
|
|
49
|
+
this._renderedScale = 0;
|
|
50
|
+
|
|
51
|
+
/** @type {Array<string>} */
|
|
52
|
+
this._lines = [];
|
|
53
|
+
|
|
54
|
+
/** @type {object} */
|
|
55
|
+
this._textAreaSize = {width: 0, height: 0};
|
|
56
|
+
|
|
57
|
+
/** @type {string} */
|
|
58
|
+
this._bubbleType = '';
|
|
59
|
+
|
|
60
|
+
/** @type {boolean} */
|
|
61
|
+
this._pointsLeft = false;
|
|
62
|
+
|
|
63
|
+
/** @type {boolean} */
|
|
64
|
+
this._textDirty = true;
|
|
65
|
+
|
|
66
|
+
/** @type {boolean} */
|
|
67
|
+
this._textureDirty = true;
|
|
68
|
+
|
|
69
|
+
this.measurementProvider = new CanvasMeasurementProvider(this._canvas.getContext('2d'));
|
|
70
|
+
this.textWrapper = new TextWrapper(this.measurementProvider);
|
|
71
|
+
|
|
72
|
+
this._restyleCanvas();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Dispose of this object. Do not use it after calling this method.
|
|
77
|
+
*/
|
|
78
|
+
dispose () {
|
|
79
|
+
if (this._texture) {
|
|
80
|
+
this._renderer.gl.deleteTexture(this._texture);
|
|
81
|
+
this._texture = null;
|
|
82
|
+
}
|
|
83
|
+
this._canvas = null;
|
|
84
|
+
super.dispose();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @return {Array<number>} the dimensions, in Scratch units, of this skin.
|
|
89
|
+
*/
|
|
90
|
+
get size () {
|
|
91
|
+
if (this._textDirty) {
|
|
92
|
+
this._reflowLines();
|
|
93
|
+
}
|
|
94
|
+
return this._size;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Set parameters for this text bubble.
|
|
99
|
+
* @param {!string} type - either "say" or "think".
|
|
100
|
+
* @param {!string} text - the text for the bubble.
|
|
101
|
+
* @param {!boolean} pointsLeft - which side the bubble is pointing.
|
|
102
|
+
*/
|
|
103
|
+
setTextBubble (type, text, pointsLeft) {
|
|
104
|
+
this._text = text;
|
|
105
|
+
this._bubbleType = type;
|
|
106
|
+
this._pointsLeft = pointsLeft;
|
|
107
|
+
|
|
108
|
+
this._textDirty = true;
|
|
109
|
+
this._textureDirty = true;
|
|
110
|
+
this.emit(Skin.Events.WasAltered);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Re-style the canvas after resizing it. This is necessary to ensure proper text measurement.
|
|
115
|
+
*/
|
|
116
|
+
_restyleCanvas () {
|
|
117
|
+
this._canvas.getContext('2d').font = `${BubbleStyle.FONT_SIZE}px ${BubbleStyle.FONT}, sans-serif`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Update the array of wrapped lines and the text dimensions.
|
|
122
|
+
*/
|
|
123
|
+
_reflowLines () {
|
|
124
|
+
this._lines = this.textWrapper.wrapText(BubbleStyle.MAX_LINE_WIDTH, this._text);
|
|
125
|
+
|
|
126
|
+
// Measure width of longest line to avoid extra-wide bubbles
|
|
127
|
+
let longestLineWidth = 0;
|
|
128
|
+
for (const line of this._lines) {
|
|
129
|
+
longestLineWidth = Math.max(longestLineWidth, this.measurementProvider.measureText(line));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Calculate the canvas-space sizes of the padded text area and full text bubble
|
|
133
|
+
const paddedWidth = Math.max(longestLineWidth, BubbleStyle.MIN_WIDTH) + (BubbleStyle.PADDING * 2);
|
|
134
|
+
const paddedHeight = (BubbleStyle.LINE_HEIGHT * this._lines.length) + (BubbleStyle.PADDING * 2);
|
|
135
|
+
|
|
136
|
+
this._textAreaSize.width = paddedWidth;
|
|
137
|
+
this._textAreaSize.height = paddedHeight;
|
|
138
|
+
|
|
139
|
+
this._size[0] = paddedWidth + BubbleStyle.STROKE_WIDTH;
|
|
140
|
+
this._size[1] = paddedHeight + BubbleStyle.STROKE_WIDTH + BubbleStyle.TAIL_HEIGHT;
|
|
141
|
+
|
|
142
|
+
this._textDirty = false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Render this text bubble at a certain scale, using the current parameters, to the canvas.
|
|
147
|
+
* @param {number} scale The scale to render the bubble at
|
|
148
|
+
*/
|
|
149
|
+
_renderTextBubble (scale) {
|
|
150
|
+
const ctx = this._canvas.getContext('2d');
|
|
151
|
+
|
|
152
|
+
if (this._textDirty) {
|
|
153
|
+
this._reflowLines();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Calculate the canvas-space sizes of the padded text area and full text bubble
|
|
157
|
+
const paddedWidth = this._textAreaSize.width;
|
|
158
|
+
const paddedHeight = this._textAreaSize.height;
|
|
159
|
+
|
|
160
|
+
// Resize the canvas to the correct screen-space size
|
|
161
|
+
this._canvas.width = Math.ceil(this._size[0] * scale);
|
|
162
|
+
this._canvas.height = Math.ceil(this._size[1] * scale);
|
|
163
|
+
this._restyleCanvas();
|
|
164
|
+
|
|
165
|
+
// Reset the transform before clearing to ensure 100% clearage
|
|
166
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
167
|
+
ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
|
168
|
+
|
|
169
|
+
ctx.scale(scale, scale);
|
|
170
|
+
ctx.translate(BubbleStyle.STROKE_WIDTH * 0.5, BubbleStyle.STROKE_WIDTH * 0.5);
|
|
171
|
+
|
|
172
|
+
// If the text bubble points leftward, flip the canvas
|
|
173
|
+
ctx.save();
|
|
174
|
+
if (this._pointsLeft) {
|
|
175
|
+
ctx.scale(-1, 1);
|
|
176
|
+
ctx.translate(-paddedWidth, 0);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Draw the bubble's rounded borders
|
|
180
|
+
ctx.beginPath();
|
|
181
|
+
ctx.moveTo(BubbleStyle.CORNER_RADIUS, paddedHeight);
|
|
182
|
+
ctx.arcTo(0, paddedHeight, 0, paddedHeight - BubbleStyle.CORNER_RADIUS, BubbleStyle.CORNER_RADIUS);
|
|
183
|
+
ctx.arcTo(0, 0, paddedWidth, 0, BubbleStyle.CORNER_RADIUS);
|
|
184
|
+
ctx.arcTo(paddedWidth, 0, paddedWidth, paddedHeight, BubbleStyle.CORNER_RADIUS);
|
|
185
|
+
ctx.arcTo(paddedWidth, paddedHeight, paddedWidth - BubbleStyle.CORNER_RADIUS, paddedHeight,
|
|
186
|
+
BubbleStyle.CORNER_RADIUS);
|
|
187
|
+
|
|
188
|
+
// Translate the canvas so we don't have to do a bunch of width/height arithmetic
|
|
189
|
+
ctx.save();
|
|
190
|
+
ctx.translate(paddedWidth - BubbleStyle.CORNER_RADIUS, paddedHeight);
|
|
191
|
+
|
|
192
|
+
// Draw the bubble's "tail"
|
|
193
|
+
if (this._bubbleType === 'say') {
|
|
194
|
+
// For a speech bubble, draw one swoopy thing
|
|
195
|
+
ctx.bezierCurveTo(0, 4, 4, 8, 4, 10);
|
|
196
|
+
ctx.arcTo(4, 12, 2, 12, 2);
|
|
197
|
+
ctx.bezierCurveTo(-1, 12, -11, 8, -16, 0);
|
|
198
|
+
|
|
199
|
+
ctx.closePath();
|
|
200
|
+
} else {
|
|
201
|
+
// For a thinking bubble, draw a partial circle attached to the bubble...
|
|
202
|
+
ctx.arc(-16, 0, 4, 0, Math.PI);
|
|
203
|
+
|
|
204
|
+
ctx.closePath();
|
|
205
|
+
|
|
206
|
+
// and two circles detached from it
|
|
207
|
+
ctx.moveTo(-7, 7.25);
|
|
208
|
+
ctx.arc(-9.25, 7.25, 2.25, 0, Math.PI * 2);
|
|
209
|
+
|
|
210
|
+
ctx.moveTo(0, 9.5);
|
|
211
|
+
ctx.arc(-1.5, 9.5, 1.5, 0, Math.PI * 2);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Un-translate the canvas and fill + stroke the text bubble
|
|
215
|
+
ctx.restore();
|
|
216
|
+
|
|
217
|
+
ctx.fillStyle = BubbleStyle.COLORS.BUBBLE_FILL;
|
|
218
|
+
ctx.strokeStyle = BubbleStyle.COLORS.BUBBLE_STROKE;
|
|
219
|
+
ctx.lineWidth = BubbleStyle.STROKE_WIDTH;
|
|
220
|
+
|
|
221
|
+
ctx.stroke();
|
|
222
|
+
ctx.fill();
|
|
223
|
+
|
|
224
|
+
// Un-flip the canvas if it was flipped
|
|
225
|
+
ctx.restore();
|
|
226
|
+
|
|
227
|
+
// Draw each line of text
|
|
228
|
+
ctx.fillStyle = BubbleStyle.COLORS.TEXT_FILL;
|
|
229
|
+
ctx.font = `${BubbleStyle.FONT_SIZE}px ${BubbleStyle.FONT}, sans-serif`;
|
|
230
|
+
const lines = this._lines;
|
|
231
|
+
for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) {
|
|
232
|
+
const line = lines[lineNumber];
|
|
233
|
+
ctx.fillText(
|
|
234
|
+
line,
|
|
235
|
+
BubbleStyle.PADDING,
|
|
236
|
+
BubbleStyle.PADDING + (BubbleStyle.LINE_HEIGHT * lineNumber) +
|
|
237
|
+
(BubbleStyle.FONT_HEIGHT_RATIO * BubbleStyle.FONT_SIZE)
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this._renderedScale = scale;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
updateSilhouette (scale = [100, 100]) {
|
|
245
|
+
// Ensure a silhouette exists.
|
|
246
|
+
this.getTexture(scale);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* @param {Array<number>} scale - The scaling factors to be used, each in the [0,100] range.
|
|
251
|
+
* @return {WebGLTexture} The GL texture representation of this skin when drawing at the given scale.
|
|
252
|
+
*/
|
|
253
|
+
getTexture (scale) {
|
|
254
|
+
// The texture only ever gets uniform scale. Take the larger of the two axes.
|
|
255
|
+
const scaleMax = scale ? Math.max(Math.abs(scale[0]), Math.abs(scale[1])) : 100;
|
|
256
|
+
const requestedScale = scaleMax / 100;
|
|
257
|
+
|
|
258
|
+
// If we already rendered the text bubble at this scale, we can skip re-rendering it.
|
|
259
|
+
if (this._textureDirty || this._renderedScale !== requestedScale) {
|
|
260
|
+
this._renderTextBubble(requestedScale);
|
|
261
|
+
this._textureDirty = false;
|
|
262
|
+
|
|
263
|
+
const context = this._canvas.getContext('2d');
|
|
264
|
+
const textureData = context.getImageData(0, 0, this._canvas.width, this._canvas.height);
|
|
265
|
+
|
|
266
|
+
const gl = this._renderer.gl;
|
|
267
|
+
|
|
268
|
+
if (this._texture === null) {
|
|
269
|
+
const textureOptions = {
|
|
270
|
+
auto: false,
|
|
271
|
+
wrap: gl.CLAMP_TO_EDGE
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
this._texture = twgl.createTexture(gl, textureOptions);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
this._setTexture(textureData);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return this._texture;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
module.exports = TextBubbleSkin;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Adapted from code by Simon Sarris: http://stackoverflow.com/a/10450761
|
|
2
|
+
const getMousePos = function (event, element) {
|
|
3
|
+
const stylePaddingLeft = parseInt(document.defaultView.getComputedStyle(element, null).paddingLeft, 10) || 0;
|
|
4
|
+
const stylePaddingTop = parseInt(document.defaultView.getComputedStyle(element, null).paddingTop, 10) || 0;
|
|
5
|
+
const styleBorderLeft = parseInt(document.defaultView.getComputedStyle(element, null).borderLeftWidth, 10) || 0;
|
|
6
|
+
const styleBorderTop = parseInt(document.defaultView.getComputedStyle(element, null).borderTopWidth, 10) || 0;
|
|
7
|
+
|
|
8
|
+
// Some pages have fixed-position bars at the top or left of the page
|
|
9
|
+
// They will mess up mouse coordinates and this fixes that
|
|
10
|
+
const html = document.body.parentNode;
|
|
11
|
+
const htmlTop = html.offsetTop;
|
|
12
|
+
const htmlLeft = html.offsetLeft;
|
|
13
|
+
|
|
14
|
+
// Compute the total offset. It's possible to cache this if you want
|
|
15
|
+
let offsetX = 0;
|
|
16
|
+
let offsetY = 0;
|
|
17
|
+
if (typeof element.offsetParent !== 'undefined') {
|
|
18
|
+
do {
|
|
19
|
+
offsetX += element.offsetLeft;
|
|
20
|
+
offsetY += element.offsetTop;
|
|
21
|
+
} while ((element = element.offsetParent));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Add padding and border style widths to offset
|
|
25
|
+
// Also add the <html> offsets in case there's a position:fixed bar
|
|
26
|
+
// This part is not strictly necessary, it depends on your styling
|
|
27
|
+
offsetX += stylePaddingLeft + styleBorderLeft + htmlLeft;
|
|
28
|
+
offsetY += stylePaddingTop + styleBorderTop + htmlTop;
|
|
29
|
+
|
|
30
|
+
// We return a simple javascript object with x and y defined
|
|
31
|
+
return {
|
|
32
|
+
x: event.pageX - offsetX,
|
|
33
|
+
y: event.pageY - offsetY
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
module.exports = getMousePos;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Scratch WebGL rendering demo</title>
|
|
6
|
+
<link rel="stylesheet" type="text/css" href="style.css">
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<canvas id="scratch-stage" width="10" height="10"></canvas>
|
|
10
|
+
<canvas id="debug-canvas" width="10" height="10"></canvas>
|
|
11
|
+
<p>
|
|
12
|
+
<label for="fudgeproperty">Property to tweak:</label>
|
|
13
|
+
<select id="fudgeproperty">
|
|
14
|
+
<option value="posx">Position X</option>
|
|
15
|
+
<option value="posy">Position Y</option>
|
|
16
|
+
<option value="direction">Direction</option>
|
|
17
|
+
<option value="scalex">Scale X</option>
|
|
18
|
+
<option value="scaley">Scale Y</option>
|
|
19
|
+
<option value="scaleboth">Scale (both dimensions)</option>
|
|
20
|
+
<option value="color">Color</option>
|
|
21
|
+
<option value="fisheye">Fisheye</option>
|
|
22
|
+
<option value="whirl">Whirl</option>
|
|
23
|
+
<option value="pixelate">Pixelate</option>
|
|
24
|
+
<option value="mosaic">Mosaic</option>
|
|
25
|
+
<option value="brightness">Brightness</option>
|
|
26
|
+
<option value="ghost">Ghost</option>
|
|
27
|
+
</select>
|
|
28
|
+
<label for="fudge">Property Value:</label>
|
|
29
|
+
<input type="range" id="fudge" style="width:50%" value="90" min="-90" max="270" step="any">
|
|
30
|
+
</p>
|
|
31
|
+
<p>
|
|
32
|
+
<label for="stage-scale">Stage scale:</label>
|
|
33
|
+
<input type="range" style="width:50%" id="stage-scale" value="1" min="1" max="2.5" step="any">
|
|
34
|
+
</p>
|
|
35
|
+
<p>
|
|
36
|
+
<label for="fudgeMin">Min:</label><input id="fudgeMin" type="number" value="0">
|
|
37
|
+
<label for="fudgeMax">Max:</label><input id="fudgeMax" type="number" value="200">
|
|
38
|
+
</p>
|
|
39
|
+
<script src="playground.js"></script>
|
|
40
|
+
</body>
|
|
41
|
+
</html>
|