@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,249 @@
|
|
|
1
|
+
precision mediump float;
|
|
2
|
+
|
|
3
|
+
#ifdef DRAW_MODE_silhouette
|
|
4
|
+
uniform vec4 u_silhouetteColor;
|
|
5
|
+
#else // DRAW_MODE_silhouette
|
|
6
|
+
# ifdef ENABLE_color
|
|
7
|
+
uniform float u_color;
|
|
8
|
+
# endif // ENABLE_color
|
|
9
|
+
# ifdef ENABLE_brightness
|
|
10
|
+
uniform float u_brightness;
|
|
11
|
+
# endif // ENABLE_brightness
|
|
12
|
+
#endif // DRAW_MODE_silhouette
|
|
13
|
+
|
|
14
|
+
#ifdef DRAW_MODE_colorMask
|
|
15
|
+
uniform vec3 u_colorMask;
|
|
16
|
+
uniform float u_colorMaskTolerance;
|
|
17
|
+
#endif // DRAW_MODE_colorMask
|
|
18
|
+
|
|
19
|
+
#ifdef ENABLE_fisheye
|
|
20
|
+
uniform float u_fisheye;
|
|
21
|
+
#endif // ENABLE_fisheye
|
|
22
|
+
#ifdef ENABLE_whirl
|
|
23
|
+
uniform float u_whirl;
|
|
24
|
+
#endif // ENABLE_whirl
|
|
25
|
+
#ifdef ENABLE_pixelate
|
|
26
|
+
uniform float u_pixelate;
|
|
27
|
+
uniform vec2 u_skinSize;
|
|
28
|
+
#endif // ENABLE_pixelate
|
|
29
|
+
#ifdef ENABLE_mosaic
|
|
30
|
+
uniform float u_mosaic;
|
|
31
|
+
#endif // ENABLE_mosaic
|
|
32
|
+
#ifdef ENABLE_ghost
|
|
33
|
+
uniform float u_ghost;
|
|
34
|
+
#endif // ENABLE_ghost
|
|
35
|
+
|
|
36
|
+
#ifdef DRAW_MODE_line
|
|
37
|
+
uniform vec4 u_lineColor;
|
|
38
|
+
uniform float u_lineThickness;
|
|
39
|
+
uniform float u_lineLength;
|
|
40
|
+
#endif // DRAW_MODE_line
|
|
41
|
+
|
|
42
|
+
#ifdef DRAW_MODE_background
|
|
43
|
+
uniform vec4 u_backgroundColor;
|
|
44
|
+
#endif // DRAW_MODE_background
|
|
45
|
+
|
|
46
|
+
uniform sampler2D u_skin;
|
|
47
|
+
|
|
48
|
+
#ifndef DRAW_MODE_background
|
|
49
|
+
varying vec2 v_texCoord;
|
|
50
|
+
#endif
|
|
51
|
+
|
|
52
|
+
// Add this to divisors to prevent division by 0, which results in NaNs propagating through calculations.
|
|
53
|
+
// Smaller values can cause problems on some mobile devices.
|
|
54
|
+
const float epsilon = 1e-3;
|
|
55
|
+
|
|
56
|
+
#if !defined(DRAW_MODE_silhouette) && (defined(ENABLE_color))
|
|
57
|
+
// Branchless color conversions based on code from:
|
|
58
|
+
// http://www.chilliant.com/rgb2hsv.html by Ian Taylor
|
|
59
|
+
// Based in part on work by Sam Hocevar and Emil Persson
|
|
60
|
+
// See also: https://en.wikipedia.org/wiki/HSL_and_HSV#Formal_derivation
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
// Convert an RGB color to Hue, Saturation, and Value.
|
|
64
|
+
// All components of input and output are expected to be in the [0,1] range.
|
|
65
|
+
vec3 convertRGB2HSV(vec3 rgb)
|
|
66
|
+
{
|
|
67
|
+
// Hue calculation has 3 cases, depending on which RGB component is largest, and one of those cases involves a "mod"
|
|
68
|
+
// operation. In order to avoid that "mod" we split the M==R case in two: one for G<B and one for B>G. The B>G case
|
|
69
|
+
// will be calculated in the negative and fed through abs() in the hue calculation at the end.
|
|
70
|
+
// See also: https://en.wikipedia.org/wiki/HSL_and_HSV#Hue_and_chroma
|
|
71
|
+
const vec4 hueOffsets = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
|
|
72
|
+
|
|
73
|
+
// temp1.xy = sort B & G (largest first)
|
|
74
|
+
// temp1.z = the hue offset we'll use if it turns out that R is the largest component (M==R)
|
|
75
|
+
// temp1.w = the hue offset we'll use if it turns out that R is not the largest component (M==G or M==B)
|
|
76
|
+
vec4 temp1 = rgb.b > rgb.g ? vec4(rgb.bg, hueOffsets.wz) : vec4(rgb.gb, hueOffsets.xy);
|
|
77
|
+
|
|
78
|
+
// temp2.x = the largest component of RGB ("M" / "Max")
|
|
79
|
+
// temp2.yw = the smaller components of RGB, ordered for the hue calculation (not necessarily sorted by magnitude!)
|
|
80
|
+
// temp2.z = the hue offset we'll use in the hue calculation
|
|
81
|
+
vec4 temp2 = rgb.r > temp1.x ? vec4(rgb.r, temp1.yzx) : vec4(temp1.xyw, rgb.r);
|
|
82
|
+
|
|
83
|
+
// m = the smallest component of RGB ("min")
|
|
84
|
+
float m = min(temp2.y, temp2.w);
|
|
85
|
+
|
|
86
|
+
// Chroma = M - m
|
|
87
|
+
float C = temp2.x - m;
|
|
88
|
+
|
|
89
|
+
// Value = M
|
|
90
|
+
float V = temp2.x;
|
|
91
|
+
|
|
92
|
+
return vec3(
|
|
93
|
+
abs(temp2.z + (temp2.w - temp2.y) / (6.0 * C + epsilon)), // Hue
|
|
94
|
+
C / (temp2.x + epsilon), // Saturation
|
|
95
|
+
V); // Value
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
vec3 convertHue2RGB(float hue)
|
|
99
|
+
{
|
|
100
|
+
float r = abs(hue * 6.0 - 3.0) - 1.0;
|
|
101
|
+
float g = 2.0 - abs(hue * 6.0 - 2.0);
|
|
102
|
+
float b = 2.0 - abs(hue * 6.0 - 4.0);
|
|
103
|
+
return clamp(vec3(r, g, b), 0.0, 1.0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
vec3 convertHSV2RGB(vec3 hsv)
|
|
107
|
+
{
|
|
108
|
+
vec3 rgb = convertHue2RGB(hsv.x);
|
|
109
|
+
float c = hsv.z * hsv.y;
|
|
110
|
+
return rgb * c + hsv.z - c;
|
|
111
|
+
}
|
|
112
|
+
#endif // !defined(DRAW_MODE_silhouette) && (defined(ENABLE_color))
|
|
113
|
+
|
|
114
|
+
const vec2 kCenter = vec2(0.5, 0.5);
|
|
115
|
+
|
|
116
|
+
void main()
|
|
117
|
+
{
|
|
118
|
+
#if !(defined(DRAW_MODE_line) || defined(DRAW_MODE_background))
|
|
119
|
+
vec2 texcoord0 = v_texCoord;
|
|
120
|
+
|
|
121
|
+
#ifdef ENABLE_mosaic
|
|
122
|
+
texcoord0 = fract(u_mosaic * texcoord0);
|
|
123
|
+
#endif // ENABLE_mosaic
|
|
124
|
+
|
|
125
|
+
#ifdef ENABLE_pixelate
|
|
126
|
+
{
|
|
127
|
+
// TODO: clean up "pixel" edges
|
|
128
|
+
vec2 pixelTexelSize = u_skinSize / u_pixelate;
|
|
129
|
+
texcoord0 = (floor(texcoord0 * pixelTexelSize) + kCenter) / pixelTexelSize;
|
|
130
|
+
}
|
|
131
|
+
#endif // ENABLE_pixelate
|
|
132
|
+
|
|
133
|
+
#ifdef ENABLE_whirl
|
|
134
|
+
{
|
|
135
|
+
const float kRadius = 0.5;
|
|
136
|
+
vec2 offset = texcoord0 - kCenter;
|
|
137
|
+
float offsetMagnitude = length(offset);
|
|
138
|
+
float whirlFactor = max(1.0 - (offsetMagnitude / kRadius), 0.0);
|
|
139
|
+
float whirlActual = u_whirl * whirlFactor * whirlFactor;
|
|
140
|
+
float sinWhirl = sin(whirlActual);
|
|
141
|
+
float cosWhirl = cos(whirlActual);
|
|
142
|
+
mat2 rotationMatrix = mat2(
|
|
143
|
+
cosWhirl, -sinWhirl,
|
|
144
|
+
sinWhirl, cosWhirl
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
texcoord0 = rotationMatrix * offset + kCenter;
|
|
148
|
+
}
|
|
149
|
+
#endif // ENABLE_whirl
|
|
150
|
+
|
|
151
|
+
#ifdef ENABLE_fisheye
|
|
152
|
+
{
|
|
153
|
+
vec2 vec = (texcoord0 - kCenter) / kCenter;
|
|
154
|
+
float vecLength = length(vec);
|
|
155
|
+
float r = pow(min(vecLength, 1.0), u_fisheye) * max(1.0, vecLength);
|
|
156
|
+
vec2 unit = vec / vecLength;
|
|
157
|
+
|
|
158
|
+
texcoord0 = kCenter + r * unit * kCenter;
|
|
159
|
+
}
|
|
160
|
+
#endif // ENABLE_fisheye
|
|
161
|
+
|
|
162
|
+
gl_FragColor = texture2D(u_skin, texcoord0);
|
|
163
|
+
|
|
164
|
+
#if defined(ENABLE_color) || defined(ENABLE_brightness)
|
|
165
|
+
// Divide premultiplied alpha values for proper color processing
|
|
166
|
+
// Add epsilon to avoid dividing by 0 for fully transparent pixels
|
|
167
|
+
gl_FragColor.rgb = clamp(gl_FragColor.rgb / (gl_FragColor.a + epsilon), 0.0, 1.0);
|
|
168
|
+
|
|
169
|
+
#ifdef ENABLE_color
|
|
170
|
+
{
|
|
171
|
+
vec3 hsv = convertRGB2HSV(gl_FragColor.xyz);
|
|
172
|
+
|
|
173
|
+
// this code forces grayscale values to be slightly saturated
|
|
174
|
+
// so that some slight change of hue will be visible
|
|
175
|
+
const float minLightness = 0.11 / 2.0;
|
|
176
|
+
const float minSaturation = 0.09;
|
|
177
|
+
if (hsv.z < minLightness) hsv = vec3(0.0, 1.0, minLightness);
|
|
178
|
+
else if (hsv.y < minSaturation) hsv = vec3(0.0, minSaturation, hsv.z);
|
|
179
|
+
|
|
180
|
+
hsv.x = mod(hsv.x + u_color, 1.0);
|
|
181
|
+
if (hsv.x < 0.0) hsv.x += 1.0;
|
|
182
|
+
|
|
183
|
+
gl_FragColor.rgb = convertHSV2RGB(hsv);
|
|
184
|
+
}
|
|
185
|
+
#endif // ENABLE_color
|
|
186
|
+
|
|
187
|
+
#ifdef ENABLE_brightness
|
|
188
|
+
gl_FragColor.rgb = clamp(gl_FragColor.rgb + vec3(u_brightness), vec3(0), vec3(1));
|
|
189
|
+
#endif // ENABLE_brightness
|
|
190
|
+
|
|
191
|
+
// Re-multiply color values
|
|
192
|
+
gl_FragColor.rgb *= gl_FragColor.a + epsilon;
|
|
193
|
+
|
|
194
|
+
#endif // defined(ENABLE_color) || defined(ENABLE_brightness)
|
|
195
|
+
|
|
196
|
+
#ifdef ENABLE_ghost
|
|
197
|
+
gl_FragColor *= u_ghost;
|
|
198
|
+
#endif // ENABLE_ghost
|
|
199
|
+
|
|
200
|
+
#ifdef DRAW_MODE_silhouette
|
|
201
|
+
// Discard fully transparent pixels for stencil test
|
|
202
|
+
if (gl_FragColor.a == 0.0) {
|
|
203
|
+
discard;
|
|
204
|
+
}
|
|
205
|
+
// switch to u_silhouetteColor only AFTER the alpha test
|
|
206
|
+
gl_FragColor = u_silhouetteColor;
|
|
207
|
+
#else // DRAW_MODE_silhouette
|
|
208
|
+
|
|
209
|
+
#ifdef DRAW_MODE_colorMask
|
|
210
|
+
vec3 maskDistance = abs(gl_FragColor.rgb - u_colorMask);
|
|
211
|
+
vec3 colorMaskTolerance = vec3(u_colorMaskTolerance, u_colorMaskTolerance, u_colorMaskTolerance);
|
|
212
|
+
if (any(greaterThan(maskDistance, colorMaskTolerance)))
|
|
213
|
+
{
|
|
214
|
+
discard;
|
|
215
|
+
}
|
|
216
|
+
#endif // DRAW_MODE_colorMask
|
|
217
|
+
#endif // DRAW_MODE_silhouette
|
|
218
|
+
|
|
219
|
+
#ifdef DRAW_MODE_straightAlpha
|
|
220
|
+
// Un-premultiply alpha.
|
|
221
|
+
gl_FragColor.rgb /= gl_FragColor.a + epsilon;
|
|
222
|
+
#endif
|
|
223
|
+
|
|
224
|
+
#endif // !(defined(DRAW_MODE_line) || defined(DRAW_MODE_background))
|
|
225
|
+
|
|
226
|
+
#ifdef DRAW_MODE_line
|
|
227
|
+
// Maaaaagic antialiased-line-with-round-caps shader.
|
|
228
|
+
|
|
229
|
+
// "along-the-lineness". This increases parallel to the line.
|
|
230
|
+
// It goes from negative before the start point, to 0.5 through the start to the end, then ramps up again
|
|
231
|
+
// past the end point.
|
|
232
|
+
float d = ((v_texCoord.x - clamp(v_texCoord.x, 0.0, u_lineLength)) * 0.5) + 0.5;
|
|
233
|
+
|
|
234
|
+
// Distance from (0.5, 0.5) to (d, the perpendicular coordinate). When we're in the middle of the line,
|
|
235
|
+
// d will be 0.5, so the distance will be 0 at points close to the line and will grow at points further from it.
|
|
236
|
+
// For the "caps", d will ramp down/up, giving us rounding.
|
|
237
|
+
// See https://www.youtube.com/watch?v=PMltMdi1Wzg for a rough outline of the technique used to round the lines.
|
|
238
|
+
float line = distance(vec2(0.5), vec2(d, v_texCoord.y)) * 2.0;
|
|
239
|
+
// Expand out the line by its thickness.
|
|
240
|
+
line -= ((u_lineThickness - 1.0) * 0.5);
|
|
241
|
+
// Because "distance to the center of the line" decreases the closer we get to the line, but we want more opacity
|
|
242
|
+
// the closer we are to the line, invert it.
|
|
243
|
+
gl_FragColor = u_lineColor * clamp(1.0 - line, 0.0, 1.0);
|
|
244
|
+
#endif // DRAW_MODE_line
|
|
245
|
+
|
|
246
|
+
#ifdef DRAW_MODE_background
|
|
247
|
+
gl_FragColor = u_backgroundColor;
|
|
248
|
+
#endif
|
|
249
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
precision mediump float;
|
|
2
|
+
|
|
3
|
+
#ifdef DRAW_MODE_line
|
|
4
|
+
uniform vec2 u_stageSize;
|
|
5
|
+
uniform float u_lineThickness;
|
|
6
|
+
uniform float u_lineLength;
|
|
7
|
+
// The X and Y components of u_penPoints hold the first pen point. The Z and W components hold the difference between
|
|
8
|
+
// the second pen point and the first. This is done because calculating the difference in the shader leads to floating-
|
|
9
|
+
// point error when both points have large-ish coordinates.
|
|
10
|
+
uniform vec4 u_penPoints;
|
|
11
|
+
|
|
12
|
+
// Add this to divisors to prevent division by 0, which results in NaNs propagating through calculations.
|
|
13
|
+
// Smaller values can cause problems on some mobile devices.
|
|
14
|
+
const float epsilon = 1e-3;
|
|
15
|
+
#endif
|
|
16
|
+
|
|
17
|
+
#if !(defined(DRAW_MODE_line) || defined(DRAW_MODE_background))
|
|
18
|
+
uniform mat4 u_projectionMatrix;
|
|
19
|
+
uniform mat4 u_modelMatrix;
|
|
20
|
+
attribute vec2 a_texCoord;
|
|
21
|
+
#endif
|
|
22
|
+
|
|
23
|
+
attribute vec2 a_position;
|
|
24
|
+
|
|
25
|
+
varying vec2 v_texCoord;
|
|
26
|
+
|
|
27
|
+
void main() {
|
|
28
|
+
#ifdef DRAW_MODE_line
|
|
29
|
+
// Calculate a rotated ("tight") bounding box around the two pen points.
|
|
30
|
+
// Yes, we're doing this 6 times (once per vertex), but on actual GPU hardware,
|
|
31
|
+
// it's still faster than doing it in JS combined with the cost of uniformMatrix4fv.
|
|
32
|
+
|
|
33
|
+
// Expand line bounds by sqrt(2) / 2 each side-- this ensures that all antialiased pixels
|
|
34
|
+
// fall within the quad, even at a 45-degree diagonal
|
|
35
|
+
vec2 position = a_position;
|
|
36
|
+
float expandedRadius = (u_lineThickness * 0.5) + 1.4142135623730951;
|
|
37
|
+
|
|
38
|
+
// The X coordinate increases along the length of the line. It's 0 at the center of the origin point
|
|
39
|
+
// and is in pixel-space (so at n pixels along the line, its value is n).
|
|
40
|
+
v_texCoord.x = mix(0.0, u_lineLength + (expandedRadius * 2.0), a_position.x) - expandedRadius;
|
|
41
|
+
// The Y coordinate is perpendicular to the line. It's also in pixel-space.
|
|
42
|
+
v_texCoord.y = ((a_position.y - 0.5) * expandedRadius) + 0.5;
|
|
43
|
+
|
|
44
|
+
position.x *= u_lineLength + (2.0 * expandedRadius);
|
|
45
|
+
position.y *= 2.0 * expandedRadius;
|
|
46
|
+
|
|
47
|
+
// 1. Center around first pen point
|
|
48
|
+
position -= expandedRadius;
|
|
49
|
+
|
|
50
|
+
// 2. Rotate quad to line angle
|
|
51
|
+
vec2 pointDiff = u_penPoints.zw;
|
|
52
|
+
// Ensure line has a nonzero length so it's rendered properly
|
|
53
|
+
// As long as either component is nonzero, the line length will be nonzero
|
|
54
|
+
// If the line is zero-length, give it a bit of horizontal length
|
|
55
|
+
pointDiff.x = (abs(pointDiff.x) < epsilon && abs(pointDiff.y) < epsilon) ? epsilon : pointDiff.x;
|
|
56
|
+
// The `normalized` vector holds rotational values equivalent to sine/cosine
|
|
57
|
+
// We're applying the standard rotation matrix formula to the position to rotate the quad to the line angle
|
|
58
|
+
// pointDiff can hold large values so we must divide by u_lineLength instead of calling GLSL's normalize function:
|
|
59
|
+
// https://asawicki.info/news_1596_watch_out_for_reduced_precision_normalizelength_in_opengl_es
|
|
60
|
+
vec2 normalized = pointDiff / max(u_lineLength, epsilon);
|
|
61
|
+
position = mat2(normalized.x, normalized.y, -normalized.y, normalized.x) * position;
|
|
62
|
+
|
|
63
|
+
// 3. Translate quad
|
|
64
|
+
position += u_penPoints.xy;
|
|
65
|
+
|
|
66
|
+
// 4. Apply view transform
|
|
67
|
+
position *= 2.0 / u_stageSize;
|
|
68
|
+
gl_Position = vec4(position, 0, 1);
|
|
69
|
+
#elif defined(DRAW_MODE_background)
|
|
70
|
+
gl_Position = vec4(a_position * 2.0, 0, 1);
|
|
71
|
+
#else
|
|
72
|
+
gl_Position = u_projectionMatrix * u_modelMatrix * vec4(a_position, 0, 1);
|
|
73
|
+
v_texCoord = a_texCoord;
|
|
74
|
+
#endif
|
|
75
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
class CanvasMeasurementProvider {
|
|
2
|
+
/**
|
|
3
|
+
* @param {CanvasRenderingContext2D} ctx - provides a canvas rendering context
|
|
4
|
+
* with 'font' set to the text style of the text to be wrapped.
|
|
5
|
+
*/
|
|
6
|
+
constructor (ctx) {
|
|
7
|
+
this._ctx = ctx;
|
|
8
|
+
this._cache = {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
// We don't need to set up or tear down anything here. Should these be removed altogether?
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Called by the TextWrapper before a batch of zero or more calls to measureText().
|
|
16
|
+
*/
|
|
17
|
+
beginMeasurementSession () {
|
|
18
|
+
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Called by the TextWrapper after a batch of zero or more calls to measureText().
|
|
23
|
+
*/
|
|
24
|
+
endMeasurementSession () {
|
|
25
|
+
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Measure a whole string as one unit.
|
|
30
|
+
* @param {string} text - the text to measure.
|
|
31
|
+
* @returns {number} - the length of the string.
|
|
32
|
+
*/
|
|
33
|
+
measureText (text) {
|
|
34
|
+
if (!this._cache[text]) {
|
|
35
|
+
this._cache[text] = this._ctx.measureText(text).width;
|
|
36
|
+
}
|
|
37
|
+
return this._cache[text];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = CanvasMeasurementProvider;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts an RGB color value to HSV. Conversion formula
|
|
3
|
+
* adapted from http://lolengine.net/blog/2013/01/13/fast-rgb-to-hsv.
|
|
4
|
+
* Assumes r, g, and b are in the range [0, 255] and
|
|
5
|
+
* returns h, s, and v in the range [0, 1].
|
|
6
|
+
*
|
|
7
|
+
* @param {Array<number>} rgb The RGB color value
|
|
8
|
+
* @param {number} rgb.r The red color value
|
|
9
|
+
* @param {number} rgb.g The green color value
|
|
10
|
+
* @param {number} rgb.b The blue color value
|
|
11
|
+
* @param {Array<number>} dst The array to store the HSV values in
|
|
12
|
+
* @return {Array<number>} The `dst` array passed in
|
|
13
|
+
*/
|
|
14
|
+
const rgbToHsv = ([r, g, b], dst) => {
|
|
15
|
+
let K = 0.0;
|
|
16
|
+
|
|
17
|
+
r /= 255;
|
|
18
|
+
g /= 255;
|
|
19
|
+
b /= 255;
|
|
20
|
+
let tmp = 0;
|
|
21
|
+
|
|
22
|
+
if (g < b) {
|
|
23
|
+
tmp = g;
|
|
24
|
+
g = b;
|
|
25
|
+
b = tmp;
|
|
26
|
+
|
|
27
|
+
K = -1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (r < g) {
|
|
31
|
+
tmp = r;
|
|
32
|
+
r = g;
|
|
33
|
+
g = tmp;
|
|
34
|
+
|
|
35
|
+
K = (-2 / 6) - K;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const chroma = r - Math.min(g, b);
|
|
39
|
+
const h = Math.abs(K + ((g - b) / ((6 * chroma) + Number.EPSILON)));
|
|
40
|
+
const s = chroma / (r + Number.EPSILON);
|
|
41
|
+
const v = r;
|
|
42
|
+
|
|
43
|
+
dst[0] = h;
|
|
44
|
+
dst[1] = s;
|
|
45
|
+
dst[2] = v;
|
|
46
|
+
|
|
47
|
+
return dst;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Converts an HSV color value to RGB. Conversion formula
|
|
52
|
+
* adapted from https://gist.github.com/mjackson/5311256.
|
|
53
|
+
* Assumes h, s, and v are contained in the set [0, 1] and
|
|
54
|
+
* returns r, g, and b in the set [0, 255].
|
|
55
|
+
*
|
|
56
|
+
* @param {Array<number>} hsv The HSV color value
|
|
57
|
+
* @param {number} hsv.h The hue
|
|
58
|
+
* @param {number} hsv.s The saturation
|
|
59
|
+
* @param {number} hsv.v The value
|
|
60
|
+
* @param {Uint8Array|Uint8ClampedArray} dst The array to store the RGB values in
|
|
61
|
+
* @return {Uint8Array|Uint8ClampedArray} The `dst` array passed in
|
|
62
|
+
*/
|
|
63
|
+
const hsvToRgb = ([h, s, v], dst) => {
|
|
64
|
+
if (s === 0) {
|
|
65
|
+
dst[0] = dst[1] = dst[2] = (v * 255) + 0.5;
|
|
66
|
+
return dst;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// keep hue in [0,1) so the `switch(i)` below only needs 6 cases (0-5)
|
|
70
|
+
h %= 1;
|
|
71
|
+
const i = (h * 6) | 0;
|
|
72
|
+
const f = (h * 6) - i;
|
|
73
|
+
const p = v * (1 - s);
|
|
74
|
+
const q = v * (1 - (s * f));
|
|
75
|
+
const t = v * (1 - (s * (1 - f)));
|
|
76
|
+
|
|
77
|
+
let r = 0;
|
|
78
|
+
let g = 0;
|
|
79
|
+
let b = 0;
|
|
80
|
+
|
|
81
|
+
switch (i) {
|
|
82
|
+
case 0: r = v; g = t; b = p; break;
|
|
83
|
+
case 1: r = q; g = v; b = p; break;
|
|
84
|
+
case 2: r = p; g = v; b = t; break;
|
|
85
|
+
case 3: r = p; g = q; b = v; break;
|
|
86
|
+
case 4: r = t; g = p; b = v; break;
|
|
87
|
+
case 5: r = v; g = p; b = q; break;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Add 0.5 in order to round. Setting integer TypedArray elements implicitly floors.
|
|
91
|
+
dst[0] = (r * 255) + 0.5;
|
|
92
|
+
dst[1] = (g * 255) + 0.5;
|
|
93
|
+
dst[2] = (b * 255) + 0.5;
|
|
94
|
+
return dst;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
module.exports = {rgbToHsv, hsvToRgb};
|
package/src/util/log.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const LineBreaker = require('!ify-loader!linebreak');
|
|
2
|
+
const GraphemeBreaker = require('!ify-loader!grapheme-breaker');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tell this text wrapper to use a specific measurement provider.
|
|
6
|
+
* @typedef {object} MeasurementProvider - the new measurement provider.
|
|
7
|
+
* @property {Function} beginMeasurementSession - this will be called before a batch of measurements are made.
|
|
8
|
+
* Optionally, this function may return an object to be provided to the endMeasurementSession function.
|
|
9
|
+
* @property {Function} measureText - this will be called each time a piece of text must be measured.
|
|
10
|
+
* @property {Function} endMeasurementSession - this will be called after a batch of measurements is finished.
|
|
11
|
+
* It will be passed whatever value beginMeasurementSession returned, if any.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Utility to wrap text across several lines, respecting Unicode grapheme clusters and, when possible, Unicode line
|
|
16
|
+
* break opportunities.
|
|
17
|
+
* Reference material:
|
|
18
|
+
* - Unicode Standard Annex #14: http://unicode.org/reports/tr14/
|
|
19
|
+
* - Unicode Standard Annex #29: http://unicode.org/reports/tr29/
|
|
20
|
+
* - "JavaScript has a Unicode problem" by Mathias Bynens: https://mathiasbynens.be/notes/javascript-unicode
|
|
21
|
+
*/
|
|
22
|
+
class TextWrapper {
|
|
23
|
+
/**
|
|
24
|
+
* Construct a text wrapper which will measure text using the specified measurement provider.
|
|
25
|
+
* @param {MeasurementProvider} measurementProvider - a helper object to provide text measurement services.
|
|
26
|
+
*/
|
|
27
|
+
constructor (measurementProvider) {
|
|
28
|
+
this._measurementProvider = measurementProvider;
|
|
29
|
+
this._cache = {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Wrap the provided text into lines restricted to a maximum width. See Unicode Standard Annex (UAX) #14.
|
|
34
|
+
* @param {number} maxWidth - the maximum allowed width of a line.
|
|
35
|
+
* @param {string} text - the text to be wrapped. Will be split on whitespace.
|
|
36
|
+
* @returns {Array.<string>} an array containing the wrapped lines of text.
|
|
37
|
+
*/
|
|
38
|
+
wrapText (maxWidth, text) {
|
|
39
|
+
// Normalize to canonical composition (see Unicode Standard Annex (UAX) #15)
|
|
40
|
+
text = text.normalize();
|
|
41
|
+
|
|
42
|
+
const cacheKey = `${maxWidth}-${text}`;
|
|
43
|
+
if (this._cache[cacheKey]) {
|
|
44
|
+
return this._cache[cacheKey];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const measurementSession = this._measurementProvider.beginMeasurementSession();
|
|
48
|
+
|
|
49
|
+
const breaker = new LineBreaker(text);
|
|
50
|
+
let lastPosition = 0;
|
|
51
|
+
let nextBreak;
|
|
52
|
+
let currentLine = null;
|
|
53
|
+
const lines = [];
|
|
54
|
+
|
|
55
|
+
while ((nextBreak = breaker.nextBreak())) {
|
|
56
|
+
const word = text.slice(lastPosition, nextBreak.position).replace(/\n+$/, '');
|
|
57
|
+
|
|
58
|
+
let proposedLine = (currentLine || '').concat(word);
|
|
59
|
+
let proposedLineWidth = this._measurementProvider.measureText(proposedLine);
|
|
60
|
+
|
|
61
|
+
if (proposedLineWidth > maxWidth) {
|
|
62
|
+
// The next word won't fit on this line. Will it fit on a line by itself?
|
|
63
|
+
const wordWidth = this._measurementProvider.measureText(word);
|
|
64
|
+
if (wordWidth > maxWidth) {
|
|
65
|
+
// The next word can't even fit on a line by itself. Consume it one grapheme cluster at a time.
|
|
66
|
+
let lastCluster = 0;
|
|
67
|
+
let nextCluster;
|
|
68
|
+
while (lastCluster !== (nextCluster = GraphemeBreaker.nextBreak(word, lastCluster))) {
|
|
69
|
+
const cluster = word.substring(lastCluster, nextCluster);
|
|
70
|
+
proposedLine = (currentLine || '').concat(cluster);
|
|
71
|
+
proposedLineWidth = this._measurementProvider.measureText(proposedLine);
|
|
72
|
+
if ((currentLine === null) || (proposedLineWidth <= maxWidth)) {
|
|
73
|
+
// first cluster of a new line or the cluster fits
|
|
74
|
+
currentLine = proposedLine;
|
|
75
|
+
} else {
|
|
76
|
+
// no more can fit
|
|
77
|
+
lines.push(currentLine);
|
|
78
|
+
currentLine = cluster;
|
|
79
|
+
}
|
|
80
|
+
lastCluster = nextCluster;
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
// The next word can fit on the next line. Finish the current line and move on.
|
|
84
|
+
if (currentLine !== null) lines.push(currentLine);
|
|
85
|
+
currentLine = word;
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
// The next word fits on this line. Just keep going.
|
|
89
|
+
currentLine = proposedLine;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Did we find a \n or similar?
|
|
93
|
+
if (nextBreak.required) {
|
|
94
|
+
if (currentLine !== null) lines.push(currentLine);
|
|
95
|
+
currentLine = null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
lastPosition = nextBreak.position;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
currentLine = currentLine || '';
|
|
102
|
+
if (currentLine.length > 0 || lines.length === 0) {
|
|
103
|
+
lines.push(currentLine);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this._cache[cacheKey] = lines;
|
|
107
|
+
this._measurementProvider.endMeasurementSession(measurementSession);
|
|
108
|
+
return lines;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = TextWrapper;
|