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,
|
9 |
|
10 | MIN_WIDTH: 50,
|
11 | STROKE_WIDTH: 4,
|
12 | PADDING: 10,
|
13 | CORNER_RADIUS: 16,
|
14 | TAIL_HEIGHT: 12,
|
15 |
|
16 | FONT: 'Helvetica',
|
17 | FONT_SIZE: 14,
|
18 | FONT_HEIGHT_RATIO: 0.9,
|
19 | LINE_HEIGHT: 16,
|
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 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 | constructor (id, renderer) {
|
37 | super(id);
|
38 |
|
39 |
|
40 | this._renderer = renderer;
|
41 |
|
42 |
|
43 | this._canvas = document.createElement('canvas');
|
44 |
|
45 |
|
46 | this._size = [0, 0];
|
47 |
|
48 |
|
49 | this._renderedScale = 0;
|
50 |
|
51 |
|
52 | this._lines = [];
|
53 |
|
54 |
|
55 | this._textAreaSize = {width: 0, height: 0};
|
56 |
|
57 |
|
58 | this._bubbleType = '';
|
59 |
|
60 |
|
61 | this._pointsLeft = false;
|
62 |
|
63 |
|
64 | this._textDirty = true;
|
65 |
|
66 |
|
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 |
|
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 |
|
89 |
|
90 | get size () {
|
91 | if (this._textDirty) {
|
92 | this._reflowLines();
|
93 | }
|
94 | return this._size;
|
95 | }
|
96 |
|
97 | |
98 |
|
99 |
|
100 |
|
101 |
|
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 |
|
115 |
|
116 | _restyleCanvas () {
|
117 | this._canvas.getContext('2d').font = `${BubbleStyle.FONT_SIZE}px ${BubbleStyle.FONT}, sans-serif`;
|
118 | }
|
119 |
|
120 | |
121 |
|
122 |
|
123 | _reflowLines () {
|
124 | this._lines = this.textWrapper.wrapText(BubbleStyle.MAX_LINE_WIDTH, this._text);
|
125 |
|
126 |
|
127 | let longestLineWidth = 0;
|
128 | for (const line of this._lines) {
|
129 | longestLineWidth = Math.max(longestLineWidth, this.measurementProvider.measureText(line));
|
130 | }
|
131 |
|
132 |
|
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 |
|
147 |
|
148 |
|
149 | _renderTextBubble (scale) {
|
150 | const ctx = this._canvas.getContext('2d');
|
151 |
|
152 | if (this._textDirty) {
|
153 | this._reflowLines();
|
154 | }
|
155 |
|
156 |
|
157 | const paddedWidth = this._textAreaSize.width;
|
158 | const paddedHeight = this._textAreaSize.height;
|
159 |
|
160 |
|
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 |
|
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 |
|
173 | ctx.save();
|
174 | if (this._pointsLeft) {
|
175 | ctx.scale(-1, 1);
|
176 | ctx.translate(-paddedWidth, 0);
|
177 | }
|
178 |
|
179 |
|
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 |
|
189 | ctx.save();
|
190 | ctx.translate(paddedWidth - BubbleStyle.CORNER_RADIUS, paddedHeight);
|
191 |
|
192 |
|
193 | if (this._bubbleType === 'say') {
|
194 |
|
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 |
|
202 | ctx.arc(-16, 0, 4, 0, Math.PI);
|
203 |
|
204 | ctx.closePath();
|
205 |
|
206 |
|
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 |
|
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 |
|
225 | ctx.restore();
|
226 |
|
227 |
|
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 |
|
246 | this.getTexture(scale);
|
247 | }
|
248 |
|
249 | |
250 |
|
251 |
|
252 |
|
253 | getTexture (scale) {
|
254 |
|
255 | const scaleMax = scale ? Math.max(Math.abs(scale[0]), Math.abs(scale[1])) : 100;
|
256 | const requestedScale = scaleMax / 100;
|
257 |
|
258 |
|
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;
|