@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
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview
|
|
3
|
+
* A utility to transform a texture coordinate to another texture coordinate
|
|
4
|
+
* representing how the shaders apply effects.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const twgl = require('twgl.js');
|
|
8
|
+
|
|
9
|
+
const {rgbToHsv, hsvToRgb} = require('./util/color-conversions');
|
|
10
|
+
const ShaderManager = require('./ShaderManager');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A texture coordinate is between 0 and 1. 0.5 is the center position.
|
|
14
|
+
* @const {number}
|
|
15
|
+
*/
|
|
16
|
+
const CENTER_X = 0.5;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A texture coordinate is between 0 and 1. 0.5 is the center position.
|
|
20
|
+
* @const {number}
|
|
21
|
+
*/
|
|
22
|
+
const CENTER_Y = 0.5;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Reused memory location for storing an HSV color value.
|
|
26
|
+
* @type {Array<number>}
|
|
27
|
+
*/
|
|
28
|
+
const __hsv = [0, 0, 0];
|
|
29
|
+
|
|
30
|
+
class EffectTransform {
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Transform a color in-place given the drawable's effect uniforms. Will apply
|
|
34
|
+
* Ghost and Color and Brightness effects.
|
|
35
|
+
* @param {Drawable} drawable The drawable to get uniforms from.
|
|
36
|
+
* @param {Uint8ClampedArray} inOutColor The color to transform.
|
|
37
|
+
* @param {number} [effectMask] A bitmask for which effects to use. Optional.
|
|
38
|
+
* @returns {Uint8ClampedArray} dst filled with the transformed color
|
|
39
|
+
*/
|
|
40
|
+
static transformColor (drawable, inOutColor, effectMask) {
|
|
41
|
+
// If the color is fully transparent, don't bother attempting any transformations.
|
|
42
|
+
if (inOutColor[3] === 0) {
|
|
43
|
+
return inOutColor;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let effects = drawable.enabledEffects;
|
|
47
|
+
if (typeof effectMask === 'number') effects &= effectMask;
|
|
48
|
+
const uniforms = drawable.getUniforms();
|
|
49
|
+
|
|
50
|
+
const enableColor = (effects & ShaderManager.EFFECT_INFO.color.mask) !== 0;
|
|
51
|
+
const enableBrightness = (effects & ShaderManager.EFFECT_INFO.brightness.mask) !== 0;
|
|
52
|
+
|
|
53
|
+
if (enableColor || enableBrightness) {
|
|
54
|
+
// gl_FragColor.rgb /= gl_FragColor.a + epsilon;
|
|
55
|
+
// Here, we're dividing by the (previously pre-multiplied) alpha to ensure HSV is properly calculated
|
|
56
|
+
// for partially transparent pixels.
|
|
57
|
+
// epsilon is present in the shader because dividing by 0 (fully transparent pixels) messes up calculations.
|
|
58
|
+
// We're doing this with a Uint8ClampedArray here, so dividing by 0 just gives 255. We're later multiplying
|
|
59
|
+
// by 0 again, so it won't affect results.
|
|
60
|
+
const alpha = inOutColor[3] / 255;
|
|
61
|
+
inOutColor[0] /= alpha;
|
|
62
|
+
inOutColor[1] /= alpha;
|
|
63
|
+
inOutColor[2] /= alpha;
|
|
64
|
+
|
|
65
|
+
if (enableColor) {
|
|
66
|
+
// vec3 hsv = convertRGB2HSV(gl_FragColor.xyz);
|
|
67
|
+
const hsv = rgbToHsv(inOutColor, __hsv);
|
|
68
|
+
|
|
69
|
+
// this code forces grayscale values to be slightly saturated
|
|
70
|
+
// so that some slight change of hue will be visible
|
|
71
|
+
// const float minLightness = 0.11 / 2.0;
|
|
72
|
+
const minV = 0.11 / 2.0;
|
|
73
|
+
// const float minSaturation = 0.09;
|
|
74
|
+
const minS = 0.09;
|
|
75
|
+
// if (hsv.z < minLightness) hsv = vec3(0.0, 1.0, minLightness);
|
|
76
|
+
if (hsv[2] < minV) {
|
|
77
|
+
hsv[0] = 0;
|
|
78
|
+
hsv[1] = 1;
|
|
79
|
+
hsv[2] = minV;
|
|
80
|
+
// else if (hsv.y < minSaturation) hsv = vec3(0.0, minSaturation, hsv.z);
|
|
81
|
+
} else if (hsv[1] < minS) {
|
|
82
|
+
hsv[0] = 0;
|
|
83
|
+
hsv[1] = minS;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// hsv.x = mod(hsv.x + u_color, 1.0);
|
|
87
|
+
// if (hsv.x < 0.0) hsv.x += 1.0;
|
|
88
|
+
hsv[0] = (uniforms.u_color + hsv[0] + 1);
|
|
89
|
+
|
|
90
|
+
// gl_FragColor.rgb = convertHSV2RGB(hsl);
|
|
91
|
+
hsvToRgb(hsv, inOutColor);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (enableBrightness) {
|
|
95
|
+
const brightness = uniforms.u_brightness * 255;
|
|
96
|
+
// gl_FragColor.rgb = clamp(gl_FragColor.rgb + vec3(u_brightness), vec3(0), vec3(1));
|
|
97
|
+
// We don't need to clamp because the Uint8ClampedArray does that for us
|
|
98
|
+
inOutColor[0] += brightness;
|
|
99
|
+
inOutColor[1] += brightness;
|
|
100
|
+
inOutColor[2] += brightness;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// gl_FragColor.rgb *= gl_FragColor.a + epsilon;
|
|
104
|
+
// Now we're doing the reverse, premultiplying by the alpha once again.
|
|
105
|
+
inOutColor[0] *= alpha;
|
|
106
|
+
inOutColor[1] *= alpha;
|
|
107
|
+
inOutColor[2] *= alpha;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if ((effects & ShaderManager.EFFECT_INFO.ghost.mask) !== 0) {
|
|
111
|
+
// gl_FragColor *= u_ghost
|
|
112
|
+
inOutColor[0] *= uniforms.u_ghost;
|
|
113
|
+
inOutColor[1] *= uniforms.u_ghost;
|
|
114
|
+
inOutColor[2] *= uniforms.u_ghost;
|
|
115
|
+
inOutColor[3] *= uniforms.u_ghost;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return inOutColor;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Transform a texture coordinate to one that would be select after applying shader effects.
|
|
123
|
+
* @param {Drawable} drawable The drawable whose effects to emulate.
|
|
124
|
+
* @param {twgl.v3} vec The texture coordinate to transform.
|
|
125
|
+
* @param {twgl.v3} dst A place to store the output coordinate.
|
|
126
|
+
* @return {twgl.v3} dst - The coordinate after being transform by effects.
|
|
127
|
+
*/
|
|
128
|
+
static transformPoint (drawable, vec, dst) {
|
|
129
|
+
twgl.v3.copy(vec, dst);
|
|
130
|
+
|
|
131
|
+
const effects = drawable.enabledEffects;
|
|
132
|
+
const uniforms = drawable.getUniforms();
|
|
133
|
+
if ((effects & ShaderManager.EFFECT_INFO.mosaic.mask) !== 0) {
|
|
134
|
+
// texcoord0 = fract(u_mosaic * texcoord0);
|
|
135
|
+
dst[0] = uniforms.u_mosaic * dst[0] % 1;
|
|
136
|
+
dst[1] = uniforms.u_mosaic * dst[1] % 1;
|
|
137
|
+
}
|
|
138
|
+
if ((effects & ShaderManager.EFFECT_INFO.pixelate.mask) !== 0) {
|
|
139
|
+
const skinUniforms = drawable.skin.getUniforms();
|
|
140
|
+
// vec2 pixelTexelSize = u_skinSize / u_pixelate;
|
|
141
|
+
const texelX = skinUniforms.u_skinSize[0] / uniforms.u_pixelate;
|
|
142
|
+
const texelY = skinUniforms.u_skinSize[1] / uniforms.u_pixelate;
|
|
143
|
+
// texcoord0 = (floor(texcoord0 * pixelTexelSize) + kCenter) /
|
|
144
|
+
// pixelTexelSize;
|
|
145
|
+
dst[0] = (Math.floor(dst[0] * texelX) + CENTER_X) / texelX;
|
|
146
|
+
dst[1] = (Math.floor(dst[1] * texelY) + CENTER_Y) / texelY;
|
|
147
|
+
}
|
|
148
|
+
if ((effects & ShaderManager.EFFECT_INFO.whirl.mask) !== 0) {
|
|
149
|
+
// const float kRadius = 0.5;
|
|
150
|
+
const RADIUS = 0.5;
|
|
151
|
+
// vec2 offset = texcoord0 - kCenter;
|
|
152
|
+
const offsetX = dst[0] - CENTER_X;
|
|
153
|
+
const offsetY = dst[1] - CENTER_Y;
|
|
154
|
+
// float offsetMagnitude = length(offset);
|
|
155
|
+
const offsetMagnitude = Math.sqrt(Math.pow(offsetX, 2) + Math.pow(offsetY, 2));
|
|
156
|
+
// float whirlFactor = max(1.0 - (offsetMagnitude / kRadius), 0.0);
|
|
157
|
+
const whirlFactor = Math.max(1.0 - (offsetMagnitude / RADIUS), 0.0);
|
|
158
|
+
// float whirlActual = u_whirl * whirlFactor * whirlFactor;
|
|
159
|
+
const whirlActual = uniforms.u_whirl * whirlFactor * whirlFactor;
|
|
160
|
+
// float sinWhirl = sin(whirlActual);
|
|
161
|
+
const sinWhirl = Math.sin(whirlActual);
|
|
162
|
+
// float cosWhirl = cos(whirlActual);
|
|
163
|
+
const cosWhirl = Math.cos(whirlActual);
|
|
164
|
+
// mat2 rotationMatrix = mat2(
|
|
165
|
+
// cosWhirl, -sinWhirl,
|
|
166
|
+
// sinWhirl, cosWhirl
|
|
167
|
+
// );
|
|
168
|
+
const rot1 = cosWhirl;
|
|
169
|
+
const rot2 = -sinWhirl;
|
|
170
|
+
const rot3 = sinWhirl;
|
|
171
|
+
const rot4 = cosWhirl;
|
|
172
|
+
|
|
173
|
+
// texcoord0 = rotationMatrix * offset + kCenter;
|
|
174
|
+
dst[0] = (rot1 * offsetX) + (rot3 * offsetY) + CENTER_X;
|
|
175
|
+
dst[1] = (rot2 * offsetX) + (rot4 * offsetY) + CENTER_Y;
|
|
176
|
+
}
|
|
177
|
+
if ((effects & ShaderManager.EFFECT_INFO.fisheye.mask) !== 0) {
|
|
178
|
+
// vec2 vec = (texcoord0 - kCenter) / kCenter;
|
|
179
|
+
const vX = (dst[0] - CENTER_X) / CENTER_X;
|
|
180
|
+
const vY = (dst[1] - CENTER_Y) / CENTER_Y;
|
|
181
|
+
// float vecLength = length(vec);
|
|
182
|
+
const vLength = Math.sqrt((vX * vX) + (vY * vY));
|
|
183
|
+
// float r = pow(min(vecLength, 1.0), u_fisheye) * max(1.0, vecLength);
|
|
184
|
+
const r = Math.pow(Math.min(vLength, 1), uniforms.u_fisheye) * Math.max(1, vLength);
|
|
185
|
+
// vec2 unit = vec / vecLength;
|
|
186
|
+
const unitX = vX / vLength;
|
|
187
|
+
const unitY = vY / vLength;
|
|
188
|
+
// texcoord0 = kCenter + r * unit * kCenter;
|
|
189
|
+
dst[0] = CENTER_X + (r * unitX * CENTER_X);
|
|
190
|
+
dst[1] = CENTER_Y + (r * unitY * CENTER_Y);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return dst;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = EffectTransform;
|
package/src/PenSkin.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
const twgl = require('twgl.js');
|
|
2
|
+
|
|
3
|
+
const RenderConstants = require('./RenderConstants');
|
|
4
|
+
const Skin = require('./Skin');
|
|
5
|
+
|
|
6
|
+
const ShaderManager = require('./ShaderManager');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Attributes to use when drawing with the pen
|
|
10
|
+
* @typedef {object} PenSkin#PenAttributes
|
|
11
|
+
* @property {number} [diameter] - The size (diameter) of the pen.
|
|
12
|
+
* @property {Array<number>} [color4f] - The pen color as an array of [r,g,b,a], each component in the range [0,1].
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The pen attributes to use when unspecified.
|
|
17
|
+
* @type {PenSkin#PenAttributes}
|
|
18
|
+
* @memberof PenSkin
|
|
19
|
+
* @private
|
|
20
|
+
* @const
|
|
21
|
+
*/
|
|
22
|
+
const DefaultPenAttributes = {
|
|
23
|
+
color4f: [0, 0, 1, 1],
|
|
24
|
+
diameter: 1
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Reused memory location for storing a premultiplied pen color.
|
|
29
|
+
* @type {FloatArray}
|
|
30
|
+
*/
|
|
31
|
+
const __premultipliedColor = [0, 0, 0, 0];
|
|
32
|
+
|
|
33
|
+
class PenSkin extends Skin {
|
|
34
|
+
/**
|
|
35
|
+
* Create a Skin which implements a Scratch pen layer.
|
|
36
|
+
* @param {int} id - The unique ID for this Skin.
|
|
37
|
+
* @param {RenderWebGL} renderer - The renderer which will use this Skin.
|
|
38
|
+
* @extends Skin
|
|
39
|
+
* @listens RenderWebGL#event:NativeSizeChanged
|
|
40
|
+
*/
|
|
41
|
+
constructor (id, renderer) {
|
|
42
|
+
super(id);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @private
|
|
46
|
+
* @type {RenderWebGL}
|
|
47
|
+
*/
|
|
48
|
+
this._renderer = renderer;
|
|
49
|
+
|
|
50
|
+
/** @type {Array<number>} */
|
|
51
|
+
this._size = null;
|
|
52
|
+
|
|
53
|
+
/** @type {WebGLFramebuffer} */
|
|
54
|
+
this._framebuffer = null;
|
|
55
|
+
|
|
56
|
+
/** @type {boolean} */
|
|
57
|
+
this._silhouetteDirty = false;
|
|
58
|
+
|
|
59
|
+
/** @type {Uint8Array} */
|
|
60
|
+
this._silhouettePixels = null;
|
|
61
|
+
|
|
62
|
+
/** @type {ImageData} */
|
|
63
|
+
this._silhouetteImageData = null;
|
|
64
|
+
|
|
65
|
+
/** @type {object} */
|
|
66
|
+
this._lineOnBufferDrawRegionId = {
|
|
67
|
+
enter: () => this._enterDrawLineOnBuffer(),
|
|
68
|
+
exit: () => this._exitDrawLineOnBuffer()
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** @type {object} */
|
|
72
|
+
this._usePenBufferDrawRegionId = {
|
|
73
|
+
enter: () => this._enterUsePenBuffer(),
|
|
74
|
+
exit: () => this._exitUsePenBuffer()
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** @type {twgl.BufferInfo} */
|
|
78
|
+
this._lineBufferInfo = twgl.createBufferInfoFromArrays(this._renderer.gl, {
|
|
79
|
+
a_position: {
|
|
80
|
+
numComponents: 2,
|
|
81
|
+
data: [
|
|
82
|
+
1, 0,
|
|
83
|
+
0, 0,
|
|
84
|
+
1, 1,
|
|
85
|
+
1, 1,
|
|
86
|
+
0, 0,
|
|
87
|
+
0, 1
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const NO_EFFECTS = 0;
|
|
93
|
+
/** @type {twgl.ProgramInfo} */
|
|
94
|
+
this._lineShader = this._renderer._shaderManager.getShader(ShaderManager.DRAW_MODE.line, NO_EFFECTS);
|
|
95
|
+
|
|
96
|
+
this.onNativeSizeChanged = this.onNativeSizeChanged.bind(this);
|
|
97
|
+
this._renderer.on(RenderConstants.Events.NativeSizeChanged, this.onNativeSizeChanged);
|
|
98
|
+
|
|
99
|
+
this._setCanvasSize(renderer.getNativeSize());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Dispose of this object. Do not use it after calling this method.
|
|
104
|
+
*/
|
|
105
|
+
dispose () {
|
|
106
|
+
this._renderer.removeListener(RenderConstants.Events.NativeSizeChanged, this.onNativeSizeChanged);
|
|
107
|
+
this._renderer.gl.deleteTexture(this._texture);
|
|
108
|
+
this._texture = null;
|
|
109
|
+
super.dispose();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @return {Array<number>} the "native" size, in texels, of this skin. [width, height]
|
|
114
|
+
*/
|
|
115
|
+
get size () {
|
|
116
|
+
return this._size;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
useNearest (scale) {
|
|
120
|
+
// Use nearest-neighbor interpolation when scaling up the pen skin-- this matches Scratch 2.0.
|
|
121
|
+
// When scaling it down, use linear interpolation to avoid giving pen lines a "dashed" appearance.
|
|
122
|
+
return Math.max(scale[0], scale[1]) >= 100;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {Array<number>} scale The X and Y scaling factors to be used, as percentages of this skin's "native" size.
|
|
127
|
+
* @return {WebGLTexture} The GL texture representation of this skin when drawing at the given size.
|
|
128
|
+
*/
|
|
129
|
+
// eslint-disable-next-line no-unused-vars
|
|
130
|
+
getTexture (scale) {
|
|
131
|
+
return this._texture;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Clear the pen layer.
|
|
136
|
+
*/
|
|
137
|
+
clear () {
|
|
138
|
+
this._renderer.enterDrawRegion(this._usePenBufferDrawRegionId);
|
|
139
|
+
|
|
140
|
+
/* Reset framebuffer to transparent black */
|
|
141
|
+
const gl = this._renderer.gl;
|
|
142
|
+
gl.clearColor(0, 0, 0, 0);
|
|
143
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
144
|
+
|
|
145
|
+
this._silhouetteDirty = true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Draw a point on the pen layer.
|
|
150
|
+
* @param {PenAttributes} penAttributes - how the point should be drawn.
|
|
151
|
+
* @param {number} x - the X coordinate of the point to draw.
|
|
152
|
+
* @param {number} y - the Y coordinate of the point to draw.
|
|
153
|
+
*/
|
|
154
|
+
drawPoint (penAttributes, x, y) {
|
|
155
|
+
this.drawLine(penAttributes, x, y, x, y);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Draw a line on the pen layer.
|
|
160
|
+
* @param {PenAttributes} penAttributes - how the line should be drawn.
|
|
161
|
+
* @param {number} x0 - the X coordinate of the beginning of the line.
|
|
162
|
+
* @param {number} y0 - the Y coordinate of the beginning of the line.
|
|
163
|
+
* @param {number} x1 - the X coordinate of the end of the line.
|
|
164
|
+
* @param {number} y1 - the Y coordinate of the end of the line.
|
|
165
|
+
*/
|
|
166
|
+
drawLine (penAttributes, x0, y0, x1, y1) {
|
|
167
|
+
// For compatibility with Scratch 2.0, offset pen lines of width 1 and 3 so they're pixel-aligned.
|
|
168
|
+
// See https://github.com/LLK/scratch-render/pull/314
|
|
169
|
+
const diameter = penAttributes.diameter || DefaultPenAttributes.diameter;
|
|
170
|
+
const offset = (diameter === 1 || diameter === 3) ? 0.5 : 0;
|
|
171
|
+
|
|
172
|
+
this._drawLineOnBuffer(
|
|
173
|
+
penAttributes,
|
|
174
|
+
x0 + offset, y0 + offset,
|
|
175
|
+
x1 + offset, y1 + offset
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
this._silhouetteDirty = true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Prepare to draw lines in the _lineOnBufferDrawRegionId region.
|
|
183
|
+
*/
|
|
184
|
+
_enterDrawLineOnBuffer () {
|
|
185
|
+
const gl = this._renderer.gl;
|
|
186
|
+
|
|
187
|
+
twgl.bindFramebufferInfo(gl, this._framebuffer);
|
|
188
|
+
|
|
189
|
+
gl.viewport(0, 0, this._size[0], this._size[1]);
|
|
190
|
+
|
|
191
|
+
const currentShader = this._lineShader;
|
|
192
|
+
gl.useProgram(currentShader.program);
|
|
193
|
+
twgl.setBuffersAndAttributes(gl, currentShader, this._lineBufferInfo);
|
|
194
|
+
|
|
195
|
+
const uniforms = {
|
|
196
|
+
u_skin: this._texture,
|
|
197
|
+
u_stageSize: this._size
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
twgl.setUniforms(currentShader, uniforms);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Return to a base state from _lineOnBufferDrawRegionId.
|
|
205
|
+
*/
|
|
206
|
+
_exitDrawLineOnBuffer () {
|
|
207
|
+
const gl = this._renderer.gl;
|
|
208
|
+
|
|
209
|
+
twgl.bindFramebufferInfo(gl, null);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Prepare to do things with this PenSkin's framebuffer
|
|
214
|
+
*/
|
|
215
|
+
_enterUsePenBuffer () {
|
|
216
|
+
twgl.bindFramebufferInfo(this._renderer.gl, this._framebuffer);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Return to a base state
|
|
221
|
+
*/
|
|
222
|
+
_exitUsePenBuffer () {
|
|
223
|
+
twgl.bindFramebufferInfo(this._renderer.gl, null);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Draw a line on the framebuffer.
|
|
228
|
+
* Note that the point coordinates are in the following coordinate space:
|
|
229
|
+
* +y is down, (0, 0) is the center, and the coords range from (-width / 2, -height / 2) to (height / 2, width / 2).
|
|
230
|
+
* @param {PenAttributes} penAttributes - how the line should be drawn.
|
|
231
|
+
* @param {number} x0 - the X coordinate of the beginning of the line.
|
|
232
|
+
* @param {number} y0 - the Y coordinate of the beginning of the line.
|
|
233
|
+
* @param {number} x1 - the X coordinate of the end of the line.
|
|
234
|
+
* @param {number} y1 - the Y coordinate of the end of the line.
|
|
235
|
+
*/
|
|
236
|
+
_drawLineOnBuffer (penAttributes, x0, y0, x1, y1) {
|
|
237
|
+
const gl = this._renderer.gl;
|
|
238
|
+
|
|
239
|
+
const currentShader = this._lineShader;
|
|
240
|
+
|
|
241
|
+
this._renderer.enterDrawRegion(this._lineOnBufferDrawRegionId);
|
|
242
|
+
|
|
243
|
+
// Premultiply pen color by pen transparency
|
|
244
|
+
const penColor = penAttributes.color4f || DefaultPenAttributes.color4f;
|
|
245
|
+
__premultipliedColor[0] = penColor[0] * penColor[3];
|
|
246
|
+
__premultipliedColor[1] = penColor[1] * penColor[3];
|
|
247
|
+
__premultipliedColor[2] = penColor[2] * penColor[3];
|
|
248
|
+
__premultipliedColor[3] = penColor[3];
|
|
249
|
+
|
|
250
|
+
// Fun fact: Doing this calculation in the shader has the potential to overflow the floating-point range.
|
|
251
|
+
// 'mediump' precision is only required to have a range up to 2^14 (16384), so any lines longer than 2^7 (128)
|
|
252
|
+
// can overflow that, because you're squaring the operands, and they could end up as "infinity".
|
|
253
|
+
// Even GLSL's `length` function won't save us here:
|
|
254
|
+
// https://asawicki.info/news_1596_watch_out_for_reduced_precision_normalizelength_in_opengl_es
|
|
255
|
+
const lineDiffX = x1 - x0;
|
|
256
|
+
const lineDiffY = y1 - y0;
|
|
257
|
+
const lineLength = Math.sqrt((lineDiffX * lineDiffX) + (lineDiffY * lineDiffY));
|
|
258
|
+
|
|
259
|
+
const uniforms = {
|
|
260
|
+
u_lineColor: __premultipliedColor,
|
|
261
|
+
u_lineThickness: penAttributes.diameter || DefaultPenAttributes.diameter,
|
|
262
|
+
u_lineLength: lineLength,
|
|
263
|
+
u_penPoints: [x0, -y0, lineDiffX, -lineDiffY]
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
twgl.setUniforms(currentShader, uniforms);
|
|
267
|
+
|
|
268
|
+
twgl.drawBufferInfo(gl, this._lineBufferInfo, gl.TRIANGLES);
|
|
269
|
+
|
|
270
|
+
this._silhouetteDirty = true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* React to a change in the renderer's native size.
|
|
275
|
+
* @param {object} event - The change event.
|
|
276
|
+
*/
|
|
277
|
+
onNativeSizeChanged (event) {
|
|
278
|
+
this._setCanvasSize(event.newSize);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Set the size of the pen canvas.
|
|
283
|
+
* @param {Array<int>} canvasSize - the new width and height for the canvas.
|
|
284
|
+
* @private
|
|
285
|
+
*/
|
|
286
|
+
_setCanvasSize (canvasSize) {
|
|
287
|
+
const [width, height] = canvasSize;
|
|
288
|
+
|
|
289
|
+
this._size = canvasSize;
|
|
290
|
+
this._rotationCenter[0] = width / 2;
|
|
291
|
+
this._rotationCenter[1] = height / 2;
|
|
292
|
+
|
|
293
|
+
const gl = this._renderer.gl;
|
|
294
|
+
|
|
295
|
+
this._texture = twgl.createTexture(
|
|
296
|
+
gl,
|
|
297
|
+
{
|
|
298
|
+
mag: gl.NEAREST,
|
|
299
|
+
min: gl.NEAREST,
|
|
300
|
+
wrap: gl.CLAMP_TO_EDGE,
|
|
301
|
+
width,
|
|
302
|
+
height
|
|
303
|
+
}
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const attachments = [
|
|
307
|
+
{
|
|
308
|
+
format: gl.RGBA,
|
|
309
|
+
attachment: this._texture
|
|
310
|
+
}
|
|
311
|
+
];
|
|
312
|
+
if (this._framebuffer) {
|
|
313
|
+
twgl.resizeFramebufferInfo(gl, this._framebuffer, attachments, width, height);
|
|
314
|
+
} else {
|
|
315
|
+
this._framebuffer = twgl.createFramebufferInfo(gl, attachments, width, height);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
gl.clearColor(0, 0, 0, 0);
|
|
319
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
320
|
+
|
|
321
|
+
this._silhouettePixels = new Uint8Array(Math.floor(width * height * 4));
|
|
322
|
+
this._silhouetteImageData = new ImageData(width, height);
|
|
323
|
+
|
|
324
|
+
this._silhouetteDirty = true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* If there have been pen operations that have dirtied the canvas, update
|
|
329
|
+
* now before someone wants to use our silhouette.
|
|
330
|
+
*/
|
|
331
|
+
updateSilhouette () {
|
|
332
|
+
if (this._silhouetteDirty) {
|
|
333
|
+
this._renderer.enterDrawRegion(this._usePenBufferDrawRegionId);
|
|
334
|
+
// Sample the framebuffer's pixels into the silhouette instance
|
|
335
|
+
const gl = this._renderer.gl;
|
|
336
|
+
gl.readPixels(
|
|
337
|
+
0, 0,
|
|
338
|
+
this._size[0], this._size[1],
|
|
339
|
+
gl.RGBA, gl.UNSIGNED_BYTE, this._silhouettePixels
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
this._silhouetteImageData.data.set(this._silhouettePixels);
|
|
343
|
+
this._silhouette.update(this._silhouetteImageData, true /* isPremultiplied */);
|
|
344
|
+
|
|
345
|
+
this._silhouetteDirty = false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
module.exports = PenSkin;
|