factories.js

import {Actor, Preloadable, Timeline, TimelineKeyframe, Vector2} from '@theatrejs/theatrejs';

import {Aseprite} from './index.js';

/**
 * @module FACTORIES
 */

/**
 * Prepares an actor with spritesheet.
 * @template {string} T The generic type of the tags.
 * @param {Object} $parameters The given parameters.
 * @param {Aseprite<T>} $parameters.$aseprite The Aseprite module manager.
 * @param {boolean} [$parameters.$loop] The loop status.
 * @param {T} $parameters.$tag The given tag.
 * @returns {typeof Actor}
 *
 * @memberof module:FACTORIES
 */
function ActorWithSpritesheet({$aseprite, $loop = false, $tag}) {

    /**
     * @ignore
     */
    class ActorWithSpritesheet extends Actor {

        /**
         * Stores the timeline.
         * @type {Timeline}
         * @private
         */
        $timeline;

        /**
         * Creates the timeline.
         * @returns {Timeline}
         * @private
         */
        $createTimeline() {

            const sprites = $aseprite.getSprites($tag);

            if (sprites.size === 0) {

                return new Timeline();
            }

            let timecode = 0;

            const keyframes = Array.from(sprites.entries()).map(([$sprite, $duration]) => {

                const timelinekeyframe = new TimelineKeyframe({

                    $onEnter: () => {

                        this.setSprite($sprite);
                    },
                    $timecode: timecode
                });

                timecode += $duration;

                return timelinekeyframe;
            });

            if ($loop === true) {

                keyframes.push(new TimelineKeyframe({

                    $onEnter: ($timeline) => {

                        $timeline.seekTimecode(0);
                    },
                    $timecode: timecode
                }));
            }

            return new Timeline(keyframes);
        }

        /**
         * Called when the actor is being created.
         * @public
         */
        onCreate() {

            this.$timeline = this.$createTimeline();
            this.$timeline.seekTimecode(0);
        }

        /**
         * Called when the actor is being updated by one tick update.
         * @param {number} $timetick The tick duration (in ms).
         * @public
         */
        onTick($timetick) {

            this.$timeline.tick($timetick);
        }
    }

    return ActorWithSpritesheet;
}

/**
 * Prepares an actor with text.
 * @param {Object} $parameters The given parameters.
 * @param {('center' | 'left' | 'right')} [$parameters.$align] The horizontal alignment.
 * @param {('bottom' | 'bottom-left' | 'bottom-right' | 'center' | 'left' | 'right' | 'top' | 'top-left' | 'top-right')} [$parameters.$anchor] The anchor position.
 * @param {Aseprite<string>} $parameters.$font The Aseprite module manager of the font (with the Aseprite tags corresponding to the characters used in the text).
 * @param {number} [$parameters.$heightLines] The height of the lines.
 * @param {number} [$parameters.$spacingCharacters] The spacing between the characters.
 * @param {string} $parameters.$text The text to use (with '\n' being a special character controlling the carriage return).
 * @returns {typeof Actor}
 *
 * @memberof module:FACTORIES
 */
function ActorWithText({$align = 'left', $anchor = 'center', $font, $heightLines = 16, $spacingCharacters = 1, $text}) {

    /**
     * @ignore
     */
    class ActorWithText extends Actor {

        /**
         * Stores the characters.
         * @type {Array<Actor>}
         * @private
         */
        $characters;

        /**
         * Called just before removing the actor.
         * @public
         */
        onBeforeRemove() {

            [...this.$characters].forEach(($characters) => {

                this.stage.removeActor($characters);
            });
        }

        /**
         * Called when the actor is being created.
         * @public
         */
        onCreate() {

            this.$characters = [];

            let widthText = 0;

            const rows = $text.split('\n').map(($row) => {

                let widthRow = 0;

                const row = Array.from($row).map(($character) => {

                    const sprites = Array.from($font.getSprites($character));

                    const heightCharacter = Math.max(...sprites.map(([$sprite]) => ($sprite.sizeTarget.y)));
                    const widthCharacter = Math.max(...sprites.map(([$sprite]) => ($sprite.sizeTarget.x)));

                    widthRow += widthCharacter;

                    return {

                        $character: $character,
                        $heightCharacter: heightCharacter,
                        $widthCharacter: widthCharacter
                    };
                });

                widthRow += ($row.length - 1) * $spacingCharacters;

                if (widthRow > widthText) {

                    widthText = widthRow;
                }

                return {

                    $heightRow: $heightLines,
                    $row: row,
                    $widthRow: widthRow
                };
            });

            const text = {

                $heightText: rows.length * $heightLines,
                $rows: rows,
                $widthText: widthText
            };

            let anchorLeft;
            let anchorTop;

            switch($anchor) {

                case 'bottom': {

                    anchorLeft = - (text.$widthText / 2);
                    anchorTop = text.$heightText;

                    break;
                }

                case 'bottom-left': {

                    anchorLeft = 0;
                    anchorTop = text.$heightText;

                    break;
                }

                case 'bottom-right': {

                    anchorLeft = - text.$widthText;
                    anchorTop = text.$heightText;

                    break;
                }

                case 'left': {

                    anchorLeft = 0;
                    anchorTop = text.$heightText / 2;

                    break;
                }

                case 'right': {

                    anchorLeft = - text.$widthText;
                    anchorTop = text.$heightText / 2;

                    break;
                }

                case 'top': {

                    anchorLeft = - (text.$widthText / 2);
                    anchorTop = 0;

                    break;
                }

                case 'top-left': {

                    anchorLeft = 0;
                    anchorTop = 0;

                    break;
                }

                case 'top-right': {

                    anchorLeft = - text.$widthText;
                    anchorTop = 0;

                    break;
                }

                case 'center':
                default: {

                    anchorLeft = - (text.$widthText / 2);
                    anchorTop = text.$heightText / 2;
                }
            }

            let left = anchorLeft;
            let top = anchorTop;

            text.$rows.forEach(({$row, $widthRow}) => {

                switch($align) {

                    case 'center': {

                        left += (text.$widthText - $widthRow) / 2;

                        break;
                    }

                    case 'right': {

                        left += (text.$widthText - $widthRow);

                        break;
                    }

                    case 'left':
                    default: {

                        break;
                    }
                }

                $row.forEach(({$character, $heightCharacter, $widthCharacter}) => {

                    const character = this.stage.createActor(

                        ActorWithSpritesheet({

                            $aseprite: $font,
                            $loop: true,
                            $tag: $character
                        })
                    )
                    .setVisible(this.visible)
                    .setZIndex(this.zIndex)
                    .translate(new Vector2(left + Math.ceil($widthCharacter / 2), top + Math.ceil($heightCharacter / 2)));

                    this.$characters.push(character);

                    left += $spacingCharacters + $widthCharacter;
                });

                left = anchorLeft;
                top -= $heightLines;
            });
        }

        /**
         * Called when the visible status is being set.
         * @param {boolean} $visible The visible status set.
         * @public
         */
        onSetVisible($visible) {

            this.$characters.forEach(($character) => {

                $character.setVisible($visible);
            });
        }

        /**
         * Called when the z-index is being set.
         * @param {number} $zIndex The z-index set.
         * @public
         */
        onSetZIndex($zIndex) {

            this.$characters.forEach(($character) => {

                $character.setZIndex($zIndex);
            });
        }

        /**
         * Called when the actor is being translated.
         * @param {Vector2} $vector The translation applied.
         * @public
         */
        onTranslate($vector) {

            this.$characters.forEach(($character) => {

                $character.translate($vector);
            });
        }
    }

    return ActorWithText;
}

/**
 * Prepares a preloadable Aseprite module.
 * @param {Aseprite<string>} $aseprite The preloadable Aseprite module.
 * @returns {typeof Preloadable}
 *
 * @memberof module:FACTORIES
 */
function PreloadableAseprite($aseprite) {

    /**
     * @ignore
     */
    class PreloadableAseprite extends Preloadable {

        /**
         * Stores the preloadable assets.
         * @type {Array<string>}
         * @public
         * @static
         */
        static preloadables = [$aseprite.textureColor];
    }

    return PreloadableAseprite;
}

export {

    ActorWithSpritesheet,
    ActorWithText,
    PreloadableAseprite
};