Skip to content

Commit 429661e

Browse files
obiotclaude
andcommitted
Add visible character control to Text and BitmapText (#1411)
Add `visibleCharacters` and `visibleRatio` properties to both Text and BitmapText for progressive text reveal and typewriter effects. Animate `visibleRatio` with Tween for character-by-character text display. BitmapText: skip glyph rendering past the visible count in the draw loop. Text: substring each line before fillText/strokeText, re-render canvas texture when visibleCharacters changes. Also add `repeatDelay` to Tween — delay before each repeat cycle. Clean up outdated `me.` references in Text/BitmapText JSDoc examples. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d920c9f commit 429661e

7 files changed

Lines changed: 303 additions & 25 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ Graphics
5757
- 3D mesh rendering with OBJ/MTL model loading, perspective projection and hardware depth testing
5858
- Built-in shader effects (Flash, Outline, Glow, Dissolve, CRT, Hologram, etc.) and custom shader support via `ShaderEffect` for per-sprite fragment effects (WebGL)
5959
- Trail renderable for fading, tapering ribbons behind moving objects (speed lines, sword slashes, magic trails)
60-
- System & Bitmap Text
60+
- System & Bitmap Text with built-in typewriter effect
6161
- Video sprite playback
6262

6363
Sound

packages/examples/src/examples/text/text.ts

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
Renderable,
66
Stage,
77
Text,
8+
Tween,
89
} from "melonjs";
910

1011
/**
@@ -123,19 +124,29 @@ export class TextScreen extends Stage {
123124
1,
124125
);
125126

126-
// ---- Multiline text (right aligned) ----
127-
app.world.addChild(
128-
new Text(165, 290, {
129-
font: "Arial",
130-
size: 14,
131-
fillStyle: "white",
132-
textAlign: "right",
133-
textBaseline: "top",
134-
lineHeight: 1.1,
135-
text: "this is another web font \nwith right alignment\nand it still works!",
136-
}),
137-
1,
138-
);
127+
// ---- Multiline text (right aligned) with typewriter ----
128+
const rightText = new Text(165, 290, {
129+
font: "Arial",
130+
size: 14,
131+
fillStyle: "white",
132+
textAlign: "right",
133+
textBaseline: "top",
134+
lineHeight: 1.1,
135+
text: "this is another web font \nwith right alignment\nusing typewriter effect!",
136+
});
137+
rightText.visibleCharacters = 0;
138+
app.world.addChild(rightText, 1);
139+
140+
new Tween(rightText)
141+
.to(
142+
{ visibleRatio: 1.0 },
143+
{
144+
duration: 5000,
145+
repeat: Infinity,
146+
repeatDelay: 1500,
147+
},
148+
)
149+
.start();
139150

140151
// ---- Fancy BitmapText multiline with word wrap ----
141152
const fancy = new BitmapText(620, 230, {
@@ -151,16 +162,28 @@ export class TextScreen extends Stage {
151162
);
152163
app.world.addChild(fancy, 1);
153164

154-
// ---- BitmapText multiline centered ----
165+
// ---- BitmapText multiline centered with typewriter effect ----
155166
const bMulti = new BitmapText(w / 2, 400, {
156167
font: "xolo12",
157-
size: 2.5,
168+
size: 1.5,
158169
textAlign: "center",
159170
textBaseline: "top",
160-
text: "THIS IS A MULTILINE\n BITMAP TEXT WITH MELONJS\nAND IT WORKS",
171+
text: "THIS IS A MULTILINE BITMAP TEXT\n USING TYPEWRITER EFFECT",
161172
});
173+
bMulti.visibleCharacters = 0;
162174
app.world.addChild(bMulti, 1);
163175

176+
new Tween(bMulti)
177+
.to(
178+
{ visibleRatio: 1.0 },
179+
{
180+
duration: 5000,
181+
repeat: Infinity,
182+
repeatDelay: 1500,
183+
},
184+
)
185+
.start();
186+
164187
// ---- BitmapText baseline test (y=375) ----
165188
xPos = 0;
166189
const tmpBFont = new BitmapText(0, 0, { font: "arialfancy", size: 1.275 });

packages/melonjs/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
## [19.2.0] (melonJS 2) - _unreleased_
44

55
### Added
6+
- Text: `visibleCharacters` and `visibleRatio` properties on Text and BitmapText for progressive text reveal and typewriter effects. Animate `visibleRatio` with Tween for character-by-character text display.
7+
- Tween: `repeatDelay(ms)` method and `repeatDelay` option in `to()` — adds a delay before each repeat cycle.
68
- Camera: FBO-based post-processing pipeline — assign a `ShaderEffect` to any camera's `shader` property to apply full-screen post-effects (vignette, scanlines, desaturation, etc.). Works with multiple cameras independently (e.g. main camera + minimap with different effects). Renderer manages FBO lifecycle via `beginPostEffect()`/`endPostEffect()`/`blitEffect()` methods.
79
- Renderable: multi-pass post-effect chaining — `postEffects` array replaces single `shader` property. Multiple effects are applied in sequence via FBO ping-pong (e.g. `sprite.addPostEffect(new DesaturateEffect(r)); sprite.addPostEffect(new InvertEffect(r));`). Single-effect renderables use a zero-overhead `customShader` fast path (no FBO). Camera manages its own FBO lifecycle; per-sprite effects use a separate FBO pair.
810
- Renderable: `addPostEffect(effect)`, `getPostEffect(EffectClass?)`, `removePostEffect(effect)`, `clearPostEffects()` — manage post-processing shader effects on any renderable

packages/melonjs/src/renderable/text/bitmaptext.js

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ export default class BitmapText extends Renderable {
2626
* @param {number} [settings.wordWrapWidth] - the maximum length in CSS pixel for a single segment of text
2727
* @param {(string|string[])} [settings.text] - a string, or an array of strings
2828
* @example
29-
* // Use me.loader.preload or me.loader.load to load assets
30-
* me.loader.preload([
31-
* { name: "arial", type: "binary" src: "data/font/arial.fnt" },
32-
* { name: "arial", type: "image" src: "data/font/arial.png" },
29+
* // Use loader.preload or loader.load to load assets
30+
* loader.preload([
31+
* { name: "arial", type: "binary", src: "data/font/arial.fnt" },
32+
* { name: "arial", type: "image", src: "data/font/arial.png" },
3333
* ])
3434
* // Then create an instance of your bitmap font:
35-
* let myFont = new me.BitmapText(x, y, {font:"arial", text:"Hello"});
35+
* let myFont = new BitmapText(x, y, {font:"arial", text:"Hello"});
3636
* // two possibilities for using "myFont"
3737
* // either call the draw function from your Renderable draw function
3838
* myFont.draw(renderer, "Hello!", 0, 0);
@@ -133,6 +133,22 @@ export default class BitmapText extends Renderable {
133133
this.anchorPoint.set(0, 0);
134134
}
135135

136+
/**
137+
* the number of characters to display (use -1 to show all).
138+
* Useful for typewriter effects combined with Tween.
139+
* @public
140+
* @type {number}
141+
* @default -1
142+
* @see BitmapText#visibleRatio
143+
* @example
144+
* // show only the first 5 characters
145+
* bitmapText.visibleCharacters = 5;
146+
* // typewriter effect
147+
* bitmapText.visibleCharacters = 0;
148+
* new Tween(bitmapText).to({ visibleRatio: 1.0 }, { duration: 2000 }).start();
149+
*/
150+
this.visibleCharacters = -1;
151+
136152
// instance to text metrics functions
137153
this.metrics = new TextMetrics(this);
138154

@@ -188,6 +204,33 @@ export default class BitmapText extends Renderable {
188204
return this;
189205
}
190206

207+
/**
208+
* the ratio of visible characters (0.0 to 1.0).
209+
* Setting this automatically updates {@link visibleCharacters}.
210+
* @public
211+
* @type {number}
212+
*/
213+
get visibleRatio() {
214+
if (this.visibleCharacters === -1) {
215+
return 1.0;
216+
}
217+
const total = this._text.reduce((sum, line) => {
218+
return sum + line.length;
219+
}, 0);
220+
return total > 0 ? this.visibleCharacters / total : 1.0;
221+
}
222+
223+
set visibleRatio(value) {
224+
if (value >= 1.0) {
225+
this.visibleCharacters = -1;
226+
} else {
227+
const total = this._text.reduce((sum, line) => {
228+
return sum + line.length;
229+
}, 0);
230+
this.visibleCharacters = Math.floor(value * total);
231+
}
232+
}
233+
191234
/**
192235
* update the bounding box for this Bitmap Text.
193236
* @param {boolean} [absolute=true] - update the bounds size and position in (world) absolute coordinates
@@ -314,6 +357,10 @@ export default class BitmapText extends Renderable {
314357
break;
315358
}
316359

360+
// running character counter for visibleCharacters
361+
let charCount = 0;
362+
const maxChars = this.visibleCharacters;
363+
317364
for (let i = 0; i < this._text.length; i++) {
318365
x = lX;
319366
const string = this._text[i].trimEnd();
@@ -335,6 +382,11 @@ export default class BitmapText extends Renderable {
335382
// draw the string
336383
let lastGlyph = null;
337384
for (let c = 0, len = string.length; c < len; c++) {
385+
// stop drawing when visibleCharacters limit is reached
386+
if (maxChars !== -1 && charCount >= maxChars) {
387+
return;
388+
}
389+
338390
// calculate the char index
339391
const ch = string.charCodeAt(c);
340392
const glyph = this.fontData.glyphs[ch];
@@ -371,6 +423,8 @@ export default class BitmapText extends Renderable {
371423
"BitmapText: no defined Glyph in for " + String.fromCharCode(ch),
372424
);
373425
}
426+
427+
charCount++;
374428
}
375429
// increment line
376430
y += stringHeight;

packages/melonjs/src/renderable/text/text.js

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export default class Text extends Renderable {
3838
* @param {number} [settings.wordWrapWidth] - the maximum length in CSS pixel for a single segment of text
3939
* @param {(string|string[])} [settings.text=""] - a string, or an array of strings
4040
* @example
41-
* let font = new me.Text(0, 0, {font: "Arial", size: 8, fillStyle: this.color});
41+
* let font = new Text(0, 0, {font: "Arial", size: 8, fillStyle: this.color});
4242
*/
4343
constructor(x, y, settings) {
4444
// call the parent constructor
@@ -179,6 +179,20 @@ export default class Text extends Renderable {
179179
offscreenCanvas: false,
180180
});
181181

182+
/**
183+
* the number of characters to display (use -1 to show all).
184+
* Useful for typewriter effects combined with Tween.
185+
* @public
186+
* @type {number}
187+
* @default -1
188+
* @see Text#visibleRatio
189+
* @example
190+
* // typewriter effect
191+
* text.visibleCharacters = 0;
192+
* new Tween(text).to({ visibleRatio: 1.0 }, { duration: 2000 }).start();
193+
*/
194+
this.visibleCharacters = -1;
195+
182196
// instance to text metrics functions
183197
this.metrics = new TextMetrics(this);
184198

@@ -317,6 +331,34 @@ export default class Text extends Renderable {
317331
return this;
318332
}
319333

334+
/**
335+
* the ratio of visible characters (0.0 to 1.0).
336+
* Setting this automatically updates {@link visibleCharacters}.
337+
* @public
338+
* @type {number}
339+
*/
340+
get visibleRatio() {
341+
if (this.visibleCharacters === -1) {
342+
return 1.0;
343+
}
344+
const total = this._text.reduce((sum, line) => {
345+
return sum + line.length;
346+
}, 0);
347+
return total > 0 ? this.visibleCharacters / total : 1.0;
348+
}
349+
350+
set visibleRatio(value) {
351+
if (value >= 1.0) {
352+
this.visibleCharacters = -1;
353+
} else {
354+
const total = this._text.reduce((sum, line) => {
355+
return sum + line.length;
356+
}, 0);
357+
this.visibleCharacters = Math.floor(value * total);
358+
}
359+
this.isDirty = true;
360+
}
361+
320362
/**
321363
* update the bounding box for this Text, accounting for textAlign and textBaseline.
322364
* @param {boolean} [absolute=true] - update in absolute coordinates
@@ -382,6 +424,18 @@ export default class Text extends Renderable {
382424
* @param {CanvasRenderer|WebGLRenderer} renderer - Reference to the destination renderer instance
383425
*/
384426
draw(renderer) {
427+
// re-render the canvas texture when visibleCharacters changes
428+
if (this.isDirty && this.visibleCharacters !== -1) {
429+
this.canvasTexture.invalidate(renderer);
430+
this.canvasTexture.clear();
431+
this._drawFont(
432+
this.canvasTexture.context,
433+
this._text,
434+
this.pos.x - this.metrics.x,
435+
this.pos.y - this.metrics.y,
436+
);
437+
}
438+
385439
// adjust x,y position based on the bounding box
386440
let x = this.metrics.x;
387441
let y = this.metrics.y;
@@ -402,8 +456,20 @@ export default class Text extends Renderable {
402456
_drawFont(context, text, x, y) {
403457
setContextStyle(context, this);
404458

459+
let remaining = this.visibleCharacters;
460+
405461
for (let i = 0; i < text.length; i++) {
406-
const string = text[i].trimEnd();
462+
let string = text[i].trimEnd();
463+
464+
// limit visible characters if needed
465+
if (remaining !== -1) {
466+
if (remaining <= 0) {
467+
break;
468+
}
469+
string = string.substring(0, remaining);
470+
remaining -= string.length;
471+
}
472+
407473
// draw the string
408474
if (this.fillStyle.alpha > 0) {
409475
context.fillText(string, x, y);

packages/melonjs/src/tweens/tween.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export default class Tween {
6161
_yoyo: boolean;
6262
_reversed: boolean;
6363
_delayTime: number;
64+
_repeatDelayTime: number;
6465
_startTime: number | null;
6566
_easingFunction: EasingFunction;
6667
_interpolationFunction: InterpolationFunction;
@@ -117,6 +118,7 @@ export default class Tween {
117118
this._yoyo = false;
118119
this._reversed = false;
119120
this._delayTime = 0;
121+
this._repeatDelayTime = 0;
120122
this._startTime = null;
121123
this._easingFunction = Easing.Linear.None;
122124
this._interpolationFunction = Interpolation.Linear;
@@ -224,6 +226,7 @@ export default class Tween {
224226
* @param [options.delay] - delay before starting, in milliseconds
225227
* @param [options.yoyo] - bounce back to original values when finished (use with `repeat`)
226228
* @param [options.repeat] - number of times to repeat (use `Infinity` for endless loops)
229+
* @param [options.repeatDelay] - delay in milliseconds before each repeat cycle
227230
* @param [options.interpolation] - interpolation function for array values
228231
* @param [options.autoStart] - start the tween immediately without calling `start()`
229232
* @returns this instance for object chaining
@@ -236,6 +239,7 @@ export default class Tween {
236239
yoyo?: boolean | undefined;
237240
repeat?: number | undefined;
238241
delay?: number | undefined;
242+
repeatDelay?: number | undefined;
239243
interpolation?: InterpolationFunction | undefined;
240244
autoStart?: boolean | undefined;
241245
},
@@ -258,6 +262,9 @@ export default class Tween {
258262
if (options.delay !== undefined) {
259263
this.delay(options.delay);
260264
}
265+
if (options.repeatDelay !== undefined) {
266+
this.repeatDelay(options.repeatDelay);
267+
}
261268
if (options.interpolation !== undefined) {
262269
this.interpolation(options.interpolation);
263270
}
@@ -347,6 +354,16 @@ export default class Tween {
347354
return this;
348355
}
349356

357+
/**
358+
* Set a delay before each repeat.
359+
* @param amount - delay in milliseconds before each repeat cycle
360+
* @returns this instance for object chaining
361+
*/
362+
repeatDelay(amount: number) {
363+
this._repeatDelayTime = amount;
364+
return this;
365+
}
366+
350367
/**
351368
* Allows the tween to bounce back to their original value when finished.
352369
* To be used together with repeat to create endless loops.
@@ -507,7 +524,7 @@ export default class Tween {
507524
this._reversed = !this._reversed;
508525
}
509526

510-
this._startTime = time + this._delayTime;
527+
this._startTime = time + (this._repeatDelayTime || this._delayTime);
511528

512529
return true;
513530
} else {

0 commit comments

Comments
 (0)