UNPKG

avg-core-ts

Version:

Develop Adventure Game (or ADV, Visual Novel, Galgame, etc.) on your own!

550 lines (466 loc) 15 kB
/** * @file Textwindow sprite class * @author MicroMatrix <yangpan.1985@bytedance.com> * @copyright 2012-2020 bytedance * @link git@code.byted.org:yangpan.1985/avg.git */ /* eslint-disable */ const PIXI = require('pixi.js'); // import { TransitionPlugin } from './Transition/TransitionPlugin' // import { TransitionFilter } from './Transition/TransitionFilter' import core from 'core/core'; /** * Class representing a TextWindow. <br> * @extends PIXI.Container */ export default class TextWindow extends PIXI.Container { constructor() { super(); this.visible = false; this.zorder = 50; // this.filters = [new TransitionFilter()]; // text layer this.textCanvas = document.createElement('canvas'); this.textContext = this.textCanvas.getContext('2d'); this.textTexture = PIXI.Texture.fromCanvas(this.textCanvas); this.textTexture.trim = new PIXI.Rectangle(); this.text = ''; this.textIndex = 0; this.textRendering = false; this.style = { font: 'sans-serif', size: 24, color: '#ffffff', bold: false, italic: false, strike: false, underline: false, shadow: false, shadowColor: '#000000', stroke: false, strokeColor: '#000000', xInterval: 0, yInterval: 12, }; this.textSpeed = 20; // characters per second this.textRectangle = [20, 55, 1008, 231]; this.m_currentTextWidth = 0; this.m_currentTextHeight = 0; this.m_lastTime = 0; this.m_resolve = null; this.resolution = 1; this.textSprite = new PIXI.Sprite(this.textTexture); this.textSprite.x = this.textRectangle[0]; this.textSprite.y = this.textRectangle[1]; this.addChildAt(this.textSprite, this.children.length); } /** * Specify index. * @method setIndex * @param {number} index - the id of TextWindow * @returns {TextWindow} - this */ setIndex(index) { this.index = index; } /** * Specify background image. * Background can only be image or color, and cannot be both. * @method setBackgroundFile * @param {string} filename * @returns {TextWindow} - this */ setBackgroundFile(filename) { this.removeChild(this.background); this.background && this.background.destroy(); if (filename) { this.background = new PIXI.Sprite(core.getTexture(filename)); this.addChildAt(this.background, 0); } else { this.background = null; } } /** * Specify background color. * Background can only be image or color, and cannot be both. * @method setBackgroundColor * @param {string} color * @returns {TextWindow} - this */ setBackgroundColor(color) { this.removeChild(this.background); this.background && this.background.destroy(); if (color) { this.background = new PIXI.Graphics(); /* 绘制*/ const rect = this.textRectangle; this.background.beginFill(color, 1.0).drawRect(rect[0], rect[1], rect[2], rect[3]); this.addChildAt(this.background, 0); } else { this.background = null; } } /** * Specify background opacity of the textwindow. * @method setOpacity * @param {number} opacity - value between 0 - 1.0 * @returns {TextWindow} - this */ setOpacity(value) { (this.background) && (this.background.alpha = value); } /** * Specify coordinate relative to its parent. * @method setPosition * @param {Array} position - [x, y] * @returns {TextWindow} - this */ setPosition(pos) { this.x = pos[0]; this.y = pos[1]; } /** * Specify the area text can be rendered to. * @method setTextRectangle * @param {Array} position - [x, y, width, height] * @returns {TextWindow} - this */ setTextRectangle(rect) { this.textRectangle = rect; this.textSprite.x = this.textRectangle[0]; this.textSprite.y = this.textRectangle[1]; this.initTextRender(true); } /** * Specify intervals between letters. * @method setXInterval * @param {number} value, px. * @returns {TextSprite} - this */ setXInterval(value = 0) { this.style.xInterval = value; } /** * Specify intervals between lines. * @method setYInterval * @param {number} value, px. * @returns {TextSprite} - this */ setYInterval(value = 12) { this.style.yInterval = value; } /** * Show or hide textwindow. * @method setVisible * @param {boolean} enable if no value passes, it will be a switch. * @returns {TextSprite} - this */ setVisible(value = false) { if (typeof value === 'undefined') { this.visible = !this.visible; } else { this.visible = !!value; } } /** * @method setTextSize * @param {number} value */ setTextSize(value = 24) { this.style.size = value; } /** * @method setTextFont * @param {string} name */ setTextFont(name = 'sans-serif') { this.style.font = name; } /** * @method setTextColor * @param {number} color */ setTextColor(color = '#ffffff') { this.style.color = color; } /** * @method setTextBold * @param {boolean} value */ setTextBold(bool) { if (typeof bool === 'undefined') { this.style.bold = !this.style.bold; } else { this.style.bold = !!bool; } } /** * @method setTextItalic * @param {boolean} enable if no value passes, it will be a switch. */ setTextItalic(bool) { if (typeof bool === 'undefined') { this.style.italic = !this.style.italic; } else { this.style.italic = !!bool; } } /** * @method setTextStrike * @param {boolean} enable if no value passes, it will be a switch. */ setTextStrike(bool) { if (typeof bool === 'undefined') { this.style.strike = !this.style.strike; } else { this.style.strike = !!bool; } } /** * @method setTextUnderline * @param {boolean} enable if no value passes, it will be a switch. */ setTextUnderline(bool) { if (typeof bool === 'undefined') { this.style.underline = !this.style.underline; } else { this.style.underline = !!bool; } } /** * @method setTextShadow * @param {boolean} enable * @param {string} color */ setTextShadow(bool, color) { this.style.shadow = !!bool; this.style.shadowColor = color || this.style.shadowColor || '#000000'; } /** * @method setTextStroke * @param {boolean} enable * @param {string} color */ setTextStroke(bool, color) { this.style.stroke = !!bool; this.style.strokeColor = color || this.style.strokeColor || '#000000'; } /** * Specify the speed of letters printing. * @method setTextSpeed * @param {number} value - letters per second. */ setTextSpeed(value = 20) { this.textSpeed = value; } /** * Clone a new TextWindow. * @method clone * @returns {TextSprite} - clone object. */ clone() { return clone(this); } /** * @method setTextCursor * @param {Object.Sprite} sprite * @param {boolean} follow - follow the last letter, or it is fixed. * @param {Array} [position] - must be defined if follow is false. */ setTextCursor(sprite, follow, pos) { this.textCursor = sprite; this.addChildAt(this.textCursor, this.children.length); this.textCursorFollow = !!follow; if (!follow) { sprite.x = pos[0]; sprite.y = pos[1]; } } /** * Specify the location that the next letter will be printed. * @method relocate * @param {Number} x - x-coordinate, set to null if you don't want to change it. * @param {Number} y - y-coordinate, set to null if you don't want to change it. */ relocate(x, y) { this.m_currentTextWidth = x || this.m_currentTextWidth; this.m_currentTextHeight = y || this.m_currentTextHeight; if (this.textCursor && this.textCursorFollow) { this.textCursor.x = this.m_currentTextWidth; this.textCursor.y = this.m_currentTextHeight; } } // text printing /** * switch next new line * @method newline */ newline() { this.m_currentTextWidth = 0; this.m_currentTextHeight += this.style.size * this.resolution + this.style.yInterval * this.resolution; } /** * draw text * @method drawText * @param {String} text what you want to print */ drawText(text, clear = true) { if (clear) { this.text = text; } else { this.text += text; } this.initTextRender(clear); return this.wait(); } /** * clear all text in textwindow * @method clearText */ clearText() { this.textCanvas.width = this.textCanvas.width; } completeText() { this.m_lastTime = -99999999; if (this.textRendering) { return this.wait(); } return null; } /** * called before draw a text * @method initTextRender * @private */ initTextRender(clear) { if (clear) { this.textCanvas.width = this.textRectangle[2] * this.resolution; this.textCanvas.height = this.textRectangle[3] * this.resolution; } // this.textContext.clear(); // set text style this.textContext.textBaseline = 'top'; { let style = ''; if (this.style.bold) { style += 'bold '; } if (this.style.italic) { style += 'italic '; } if (!style.length) { style = 'normal '; } this.textContext.font = `${style + this.style.size * this.resolution}px ${this.style.font}`; } this.textContext.fillStyle = (typeof this.style.color === 'number') ? (`#${this.style.color.toString(16)}`) : this.style.color; if (this.style.shadow) { this.textContext.shadowBlur = 0; this.textContext.shadowOffsetX = this.style.size * this.resolution / 12 * 0.414; this.textContext.shadowOffsetY = this.style.size * this.resolution / 12 * 0.414; this.textContext.shadowColor = (typeof this.style.shadowColor === 'number') ? (`#${this.style.shadowColor.toString(16)}`) : this.style.shadowColor; } if (this.style.stroke) { this.textContext.strokeStyle = (typeof this.style.strokeColor === 'number') ? (`#${this.style.strokeColor.toString(16)}`) : this.style.strokeColor; this.textContext.lineWidth = this.style.size * this.resolution / 24; } // reset status variables this.textRendering = true; this.m_lastTime = Date.now(); if (clear) { this.textIndex = 0; this.m_currentTextWidth = 0; this.m_currentTextHeight = 0; } } async updateTransform() { if (!this.textRendering) { super.updateTransform(); return; } const delta = Date.now() - this.m_lastTime; if (delta < 1000 / this.textSpeed) { return; } this.m_lastTime = Date.now(); let count = Math.floor(delta / 1000 * this.textSpeed); if (this.textIndex + count >= this.text.length - 1) { count = this.text.length - this.textIndex; } // hide cursor (if exist) if (this.textCursor) { this.textCursor.visible = false; } for (let i = this.textIndex; i < this.textIndex + count; i++) { let character; if (this.text.codePointAt(i) > 0xFFFF) { character = this.text.at(i); i++; // if the last character is a emoji or ext-hanzi, must avoid it be cut. if (i === this.textIndex + count) { count++; } } else { character = this.text[i]; } if (character === '\n') { this.newline(); continue; } // print characters, calculate delta this.textContext.fillText(character, this.m_currentTextWidth, this.m_currentTextHeight); if (this.style.stroke) { this.textContext.strokeText(character, this.m_currentTextWidth, this.m_currentTextHeight); } const width = this.textContext.measureText(character).width; // 字号已经*this.resolution,无需再乘 this.m_currentTextWidth += width + this.style.xInterval * this.resolution; if (this.m_currentTextWidth + width >= this.textRectangle[2] * this.resolution) { this.m_currentTextWidth = 0; this.m_currentTextHeight += this.style.size * this.resolution + this.style.yInterval * this.resolution; } } this.textIndex += count; this.updateTexture(); super.updateTransform(); // stop condition if (this.textIndex >= this.text.length - 1) { this.textRendering = false; if (this.m_resolve) { this.m_resolve(); this.m_resolve = null; } // move cursor (if exist) if (this.textCursor && this.textCursorFollow) { this.textCursor.x = this.m_currentTextWidth; this.textCursor.y = this.m_currentTextHeight; } if (this.textCursor) { this.textCursor.visible = true; } // restore cursor } } updateTexture() { const texture = this.textTexture; texture.baseTexture.hasLoaded = true; texture.baseTexture.resolution = this.resolution; texture.baseTexture.width = this.textCanvas.width / this.resolution; texture.baseTexture.height = this.textCanvas.height / this.resolution; // texture.crop.width = texture._frame.width = this.textCanvas.width / this.resolution; // texture.crop.height = texture._frame.height = this.textCanvas.height / this.resolution; texture.trim.x = 0; texture.trim.y = 0; texture.trim.width = texture._frame.width; texture.trim.height = texture._frame.height; this.textSprite._width = this.textCanvas.width / this.resolution; this.textSprite._height = this.textCanvas.height / this.resolution; texture.baseTexture.emit('update', texture.baseTexture); } wait() { if (!this.m_promise) { this.m_promise = new Promise((resolve, reject) => { this.m_resolve = resolve; }) .then(() => this.m_promise = null); } if (this.visible && this.renderable) { return this.m_promise; } else { return Promise.resolve(); } } removeChildren() { super.removeChildren(); /** * It is a bit confusing. * When .destroy() was called, it will call .removeChildren() * because of the implement of Container.destory(), * but that time `this.background` had been destroyed, so .addChild() will throw an error. * * TODO: find reason */ try { this.addChild(this.background); this.addChild(this.textSprite); this.addChild(this.textCursor); } catch (e) {} } // addChild(...args) { // super.addChild(...args); // } destroy() { this.background.destroy(); this.textSprite.destroy(); this.textCursor.destroy(); super.destroy(); } } // TransitionPlugin(TextWindow); function clone(origin) { const originProto = Object.getPrototypeOf(origin); return Object.assign(Object.create(originProto), origin); } // module.exports = TextWindow;