Skip to content

Commit 4a913db

Browse files
committed
Add text bubble style customization
Supersedes #14
1 parent 4be3034 commit 4a913db

File tree

2 files changed

+67
-42
lines changed

2 files changed

+67
-42
lines changed

src/TextBubbleSkin.js

+60-42
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,23 @@ const twgl = require('twgl.js');
33
const CanvasMeasurementProvider = require('./util/canvas-measurement-provider');
44
const Skin = require('./Skin');
55

6-
const BubbleStyle = {
7-
MAX_LINE_WIDTH: 170, // Maximum width, in Scratch pixels, of a single line of text
8-
9-
MIN_WIDTH: 50, // Minimum width, in Scratch pixels, of a text bubble
10-
STROKE_WIDTH: 4, // Thickness of the stroke around the bubble. Only half's visible because it's drawn under the fill
11-
PADDING: 10, // Padding around the text area
12-
CORNER_RADIUS: 16, // Radius of the rounded corners
13-
TAIL_HEIGHT: 12, // Height of the speech bubble's "tail". Probably should be a constant.
14-
15-
FONT: 'Helvetica', // Font to render the text with
16-
FONT_SIZE: 14, // Font size, in Scratch pixels
17-
FONT_HEIGHT_RATIO: 0.9, // Height, in Scratch pixels, of the text, as a proportion of the font's size
18-
LINE_HEIGHT: 16, // Spacing between each line of text
19-
20-
COLORS: {
21-
BUBBLE_FILL: 'white',
22-
BUBBLE_STROKE: 'rgba(0, 0, 0, 0.15)',
23-
TEXT_FILL: '#575E75'
24-
}
6+
const DEFAULT_BUBBLE_STYLE = {
7+
maxLineWidth: 170, // Maximum width, in Scratch pixels, of a single line of text
8+
9+
minWidth: 50, // Minimum width, in Scratch pixels, of a text bubble
10+
strokeWidth: 4, // Thickness of the stroke around the bubble. Only half's visible because it's drawn under the fill
11+
padding: 10, // Padding around the text area
12+
cornerRadius: 16, // Radius of the rounded corners
13+
tailHeight: 12, // Height of the speech bubble's "tail". Probably should be a constant.
14+
15+
font: 'Helvetica', // Font to render the text with
16+
fontSize: 14, // Font size, in Scratch pixels
17+
fontHeightRatio: 0.9, // Height, in Scratch pixels, of the text, as a proportion of the font's size
18+
lineHeight: 16, // Spacing between each line of text
19+
20+
bubbleFill: 'white',
21+
bubbleStroke: 'rgba(0, 0, 0, 0.15)',
22+
textFill: '#575E75'
2523
};
2624

2725
const MAX_SCALE = 10;
@@ -64,6 +62,13 @@ class TextBubbleSkin extends Skin {
6462
/** @type {boolean} */
6563
this._textureDirty = true;
6664

65+
/**
66+
* Use setStyle() instead of modfying directly.
67+
* Supplied values are considered trusted and will not be further checked or sanitized.
68+
* Updating skin style will not reposition drawables.
69+
*/
70+
this._style = DEFAULT_BUBBLE_STYLE;
71+
6772
this.measurementProvider = new CanvasMeasurementProvider(this._canvas.getContext('2d'));
6873
this.textWrapper = renderer.createTextWrapper(this.measurementProvider);
6974

@@ -108,18 +113,33 @@ class TextBubbleSkin extends Skin {
108113
this.emitWasAltered();
109114
}
110115

116+
/**
117+
* Change style used for rendering the bubble. Properties not specified will be unchanged.
118+
* Given argument will be copied internally, so you can freely change it later without
119+
* affecting the skin.
120+
* @param {object} newStyle New styles to be applied.
121+
*/
122+
setStyle (newStyle) {
123+
this._style = Object.assign({}, this._style, newStyle);
124+
this._restyleCanvas();
125+
this._textDirty = true;
126+
this._textureDirty = true;
127+
this.emitWasAltered();
128+
}
129+
111130
/**
112131
* Re-style the canvas after resizing it. This is necessary to ensure proper text measurement.
113132
*/
114133
_restyleCanvas () {
115-
this._canvas.getContext('2d').font = `${BubbleStyle.FONT_SIZE}px ${BubbleStyle.FONT}, sans-serif`;
134+
this.measurementProvider.clearCache();
135+
this._canvas.getContext('2d').font = `${this._style.fontSize}px ${this._style.font}, sans-serif`;
116136
}
117137

118138
/**
119139
* Update the array of wrapped lines and the text dimensions.
120140
*/
121141
_reflowLines () {
122-
this._lines = this.textWrapper.wrapText(BubbleStyle.MAX_LINE_WIDTH, this._text);
142+
this._lines = this.textWrapper.wrapText(this._style.maxLineWidth, this._text);
123143

124144
// Measure width of longest line to avoid extra-wide bubbles
125145
let longestLineWidth = 0;
@@ -128,14 +148,14 @@ class TextBubbleSkin extends Skin {
128148
}
129149

130150
// Calculate the canvas-space sizes of the padded text area and full text bubble
131-
const paddedWidth = Math.max(longestLineWidth, BubbleStyle.MIN_WIDTH) + (BubbleStyle.PADDING * 2);
132-
const paddedHeight = (BubbleStyle.LINE_HEIGHT * this._lines.length) + (BubbleStyle.PADDING * 2);
151+
const paddedWidth = Math.max(longestLineWidth, this._style.minWidth) + (this._style.padding * 2);
152+
const paddedHeight = (this._style.lineHeight * this._lines.length) + (this._style.padding * 2);
133153

134154
this._textAreaSize.width = paddedWidth;
135155
this._textAreaSize.height = paddedHeight;
136156

137-
this._size[0] = paddedWidth + BubbleStyle.STROKE_WIDTH;
138-
this._size[1] = paddedHeight + BubbleStyle.STROKE_WIDTH + BubbleStyle.TAIL_HEIGHT;
157+
this._size[0] = paddedWidth + this._style.strokeWidth;
158+
this._size[1] = paddedHeight + this._style.strokeWidth + this._style.tailHeight;
139159

140160
this._textDirty = false;
141161
}
@@ -158,14 +178,13 @@ class TextBubbleSkin extends Skin {
158178
// Resize the canvas to the correct screen-space size
159179
this._canvas.width = Math.ceil(this._size[0] * scale);
160180
this._canvas.height = Math.ceil(this._size[1] * scale);
161-
this._restyleCanvas();
162181

163182
// Reset the transform before clearing to ensure 100% clearage
164183
ctx.setTransform(1, 0, 0, 1, 0, 0);
165184
ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
166185

167186
ctx.scale(scale, scale);
168-
ctx.translate(BubbleStyle.STROKE_WIDTH * 0.5, BubbleStyle.STROKE_WIDTH * 0.5);
187+
ctx.translate(this._style.strokeWidth * 0.5, this._style.strokeWidth * 0.5);
169188

170189
// If the text bubble points leftward, flip the canvas
171190
ctx.save();
@@ -176,16 +195,16 @@ class TextBubbleSkin extends Skin {
176195

177196
// Draw the bubble's rounded borders
178197
ctx.beginPath();
179-
ctx.moveTo(BubbleStyle.CORNER_RADIUS, paddedHeight);
180-
ctx.arcTo(0, paddedHeight, 0, paddedHeight - BubbleStyle.CORNER_RADIUS, BubbleStyle.CORNER_RADIUS);
181-
ctx.arcTo(0, 0, paddedWidth, 0, BubbleStyle.CORNER_RADIUS);
182-
ctx.arcTo(paddedWidth, 0, paddedWidth, paddedHeight, BubbleStyle.CORNER_RADIUS);
183-
ctx.arcTo(paddedWidth, paddedHeight, paddedWidth - BubbleStyle.CORNER_RADIUS, paddedHeight,
184-
BubbleStyle.CORNER_RADIUS);
198+
ctx.moveTo(this._style.cornerRadius, paddedHeight);
199+
ctx.arcTo(0, paddedHeight, 0, paddedHeight - this._style.cornerRadius, this._style.cornerRadius);
200+
ctx.arcTo(0, 0, paddedWidth, 0, this._style.cornerRadius);
201+
ctx.arcTo(paddedWidth, 0, paddedWidth, paddedHeight, this._style.cornerRadius);
202+
ctx.arcTo(paddedWidth, paddedHeight, paddedWidth - this._style.cornerRadius, paddedHeight,
203+
this._style.cornerRadius);
185204

186205
// Translate the canvas so we don't have to do a bunch of width/height arithmetic
187206
ctx.save();
188-
ctx.translate(paddedWidth - BubbleStyle.CORNER_RADIUS, paddedHeight);
207+
ctx.translate(paddedWidth - this._style.cornerRadius, paddedHeight);
189208

190209
// Draw the bubble's "tail"
191210
if (this._bubbleType === 'say') {
@@ -212,9 +231,9 @@ class TextBubbleSkin extends Skin {
212231
// Un-translate the canvas and fill + stroke the text bubble
213232
ctx.restore();
214233

215-
ctx.fillStyle = BubbleStyle.COLORS.BUBBLE_FILL;
216-
ctx.strokeStyle = BubbleStyle.COLORS.BUBBLE_STROKE;
217-
ctx.lineWidth = BubbleStyle.STROKE_WIDTH;
234+
ctx.fillStyle = this._style.bubbleFill;
235+
ctx.strokeStyle = this._style.bubbleStroke;
236+
ctx.lineWidth = this._style.strokeWidth;
218237

219238
ctx.stroke();
220239
ctx.fill();
@@ -223,16 +242,15 @@ class TextBubbleSkin extends Skin {
223242
ctx.restore();
224243

225244
// Draw each line of text
226-
ctx.fillStyle = BubbleStyle.COLORS.TEXT_FILL;
227-
ctx.font = `${BubbleStyle.FONT_SIZE}px ${BubbleStyle.FONT}, sans-serif`;
245+
ctx.fillStyle = this._style.textFill;
228246
const lines = this._lines;
229247
for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) {
230248
const line = lines[lineNumber];
231249
ctx.fillText(
232250
line,
233-
BubbleStyle.PADDING,
234-
BubbleStyle.PADDING + (BubbleStyle.LINE_HEIGHT * lineNumber) +
235-
(BubbleStyle.FONT_HEIGHT_RATIO * BubbleStyle.FONT_SIZE)
251+
this._style.padding,
252+
this._style.padding + (this._style.lineHeight * lineNumber) +
253+
(this._style.fontHeightRatio * this._style.fontSize)
236254
);
237255
}
238256

src/util/canvas-measurement-provider.js

+7
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ class CanvasMeasurementProvider {
3636
}
3737
return this._cache[text];
3838
}
39+
40+
/**
41+
* Resets the internal cache. Call after the canvas's font changes.
42+
*/
43+
clearCache () {
44+
this._cache = {};
45+
}
3946
}
4047

4148
module.exports = CanvasMeasurementProvider;

0 commit comments

Comments
 (0)