# Text wrap

> style.maxWidth breaks a text's lines by itself, and measureText says how it is set: a sign that fits its words, and a dialogue written letter by letter.

Example `text-wrap`, running at https://nacatamalon.com/en/examples/text-wrap. The whole catalogue: https://nacatamalon.com/llms/examples.md.

`style.maxWidth` is how wide a line can be, in pixels, before its words go on to the next. Lines break at spaces, and a word wider than the whole box breaks between two letters. A `\n` still starts a line wherever it is. The engine breaks the lines again whenever the text, its size or the width changes, so a dialogue box never needs line breaks put in by hand.

The block is as wide as `maxWidth`, so `align` and `anchor` place the lines in the box rather than around the words. The sign changes its width every frame, and `measureText(words)` asks the text itself how it is set now, with its own font and style: the size of its block, to fit the sign to it, and its lines, to count them. The same works with a bitmap font, a vector one and `drawText`.

The dialogue is written letter by letter. Shown as they come, a word would start at the end of a line and jump down once it no longer fit. So each page is first broken with `measureText(page, style)`, given the words and the same style the dialogue box has, which gives the lines they will be set in before they are shown, and the letters shown are the first ones of those lines. Click, or press Space, to finish a page or go on to the next.

## Code

`index.ts`:

```ts
import { createGame } from 'nacatamalon';
import { Scene } from './scenes/demo';
import { WIDTH, HEIGHT } from './settings';

const game = createGame('#app', {
    width: WIDTH,
    height: HEIGHT,
    background: '#14121c',
    scaling: 'integer'
});

// Declare Scenes
game({
    Scene
});
```

`components/dialogue.ts`:

```ts
import { createSprite, createText, measureText, useKeyboard, usePointer, useUpdate } from 'nacatamalon';
import { DIALOGUE } from '../settings';
import { PAGES } from '../data/pages';

export const Dialogue = () => {
    // A dialogue box in the font the engine carries, written letter by letter.
    const corner = {
        x: 0,
        y: 0
    };

    createSprite({
        width: 304,
        height: 76,
        tint: '#78dcff',
        anchor: corner,
        transform: {
            x: 8,
            y: 116
        }
    });
    createSprite({
        width: 300,
        height: 72,
        tint: '#101830',
        anchor: corner,
        transform: {
            x: 10,
            y: 118
        }
    });

    const style = {
        maxWidth: DIALOGUE.maxWidth,
        lineSpacing: DIALOGUE.lineSpacing
    };

    const dialogue = createText({
        text: '',
        style,
        transform: {
            x: 18,
            y: 126
        }
    });

    const more = createText({
        text: '▼',
        tint: '#78dcff',
        transform: {
            x: 298,
            y: 180
        }
    });

    // The whole page is broken into lines first, and the letters shown are the first ones of those
    // lines. Shown as they come instead, a word would start at the end of a line and jump to the
    // next one as soon as it grew too long for it.
    let page = 0;
    let lines = measureText(PAGES[page], style).lines.join('\n');
    let shown = 0;

    const next = () => {
        if (shown < lines.length) {
            // Still writing: the first press shows the rest of the page at once.
            shown = lines.length;

            return;
        }

        page = (page + 1) % PAGES.length;
        lines = measureText(PAGES[page], style).lines.join('\n');
        shown = 0;
    };

    usePointer().onDown(next);

    const keys = useKeyboard();

    useUpdate((delta, time) => {
        if (keys.justPressed('Space') || keys.justPressed('Enter')) {
            next();
        }

        shown = Math.min(lines.length, shown + DIALOGUE.lettersPerSecond * delta);
        dialogue.text = lines.slice(0, Math.floor(shown));
        more.visible = shown === lines.length && Math.floor(time * 2) % 2 === 0;
    });
};
```

`components/wrapping_sign.ts`:

```ts
import { createSprite, createText, measureText, useLoadFont, useUpdate } from 'nacatamalon';
import { SIGN, COMIC } from '../settings';

export const WrappingSign = () => {
    const comic = useLoadFont(COMIC);

    // A sign that grows and shrinks: the same words set again in a box of every width. The block
    // is as wide as maxWidth, so a centred anchor and align: 'center' centre the lines in the box.
    const sign = createSprite({
        width: 0,
        height: 0,
        tint: '#2a2440',
        anchor: {
            x: 0.5,
            y: 0.5
        },
        transform: {
            x: 160,
            y: 52
        }
    });

    const words = createText({
        text: 'Lines break at spaces, centred in the box',
        font: comic,
        tint: '#ffc450',
        style: {
            fontSize: 20,
            align: 'center',
            maxWidth: 200
        },
        anchor: {
            x: 0.5,
            y: 0.5
        },
        transform: {
            x: 160,
            y: 52
        },
    });

    const count = createText({
        text: '',
        tint: '#6a6488',
        anchor: {
            x: 1,
            y: 0
        },
        transform: {
            x: 312,
            y: 104
        }
    });

    useUpdate((_delta, time) => {
        words.style.maxWidth = SIGN.width + Math.sin(time * SIGN.speed) * SIGN.amplitude;

        // The sign asks the text itself how it is set now: the size of its block, to fit the sign
        // to it, and its lines, to say how many there are.
        const set = measureText(words);

        sign.width = set.width + 16;
        sign.height = set.height + 8;
        count.text = set.lines.length + ' LINES';
    });
};
```

`data/pages.ts`:

```ts
// The last page includes a word wider than the box, to demonstrate letter wrapping.
const VILLAGE = 'You made it to the village at last! The bridge to the north washed away in the storm, so nobody has crossed in days.';
const FERRYMAN = 'If you want to get across, find the old ferryman. He lives in the hut past the windmill, and he does not like visitors.';
const SHOUT = 'The ferryman? He yelled AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH! and slammed the door.';

export const PAGES = [VILLAGE, FERRYMAN, SHOUT];
```

`scenes/demo.ts`:

```ts
import { createScene, useSpawn } from 'nacatamalon';
import { WrappingSign } from '../components/wrapping_sign';
import { Dialogue } from '../components/dialogue';

export const Scene = () => {
    useSpawn(WrappingSign)();
    useSpawn(Dialogue)();

    return createScene();
};
```

`settings.ts`:

```ts
export const WIDTH = 320;
export const HEIGHT = 200;
export const DIALOGUE = {
    lettersPerSecond: 40,
    maxWidth: 284,
    lineSpacing: 4
};
export const SIGN = {
    width: 170,
    amplitude: 70,
    speed: 0.8
};
export const COMIC = {
    src: '/example-assets/fonts/vector/Bangers-Regular.ttf'
};
```
