UNPKG

rot-js

Version:

A roguelike toolkit in JavaScript

279 lines (278 loc) 9.47 kB
import Hex from "./hex.js"; import Rect from "./rect.js"; import Tile from "./tile.js"; import TileGL from "./tile-gl.js"; import Term from "./term.js"; import * as Text from "../text.js"; import { DEFAULT_WIDTH, DEFAULT_HEIGHT } from "../constants.js"; const BACKENDS = { "hex": Hex, "rect": Rect, "tile": Tile, "tile-gl": TileGL, "term": Term }; const DEFAULT_OPTIONS = { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT, transpose: false, layout: "rect", fontSize: 15, spacing: 1, border: 0, forceSquareRatio: false, fontFamily: "monospace", fontStyle: "", fg: "#ccc", bg: "#000", tileWidth: 32, tileHeight: 32, tileMap: {}, tileSet: null, tileColorize: false }; /** * @class Visual map display */ export default class Display { constructor(options = {}) { this._data = {}; this._dirty = false; // false = nothing, true = all, object = dirty cells this._options = {}; options = Object.assign({}, DEFAULT_OPTIONS, options); this.setOptions(options); this.DEBUG = this.DEBUG.bind(this); this._tick = this._tick.bind(this); this._backend.schedule(this._tick); } /** * Debug helper, ideal as a map generator callback. Always bound to this. * @param {int} x * @param {int} y * @param {int} what */ DEBUG(x, y, what) { let colors = [this._options.bg, this._options.fg]; this.draw(x, y, null, null, colors[what % colors.length]); } /** * Clear the whole display (cover it with background color) */ clear() { this._data = {}; this._dirty = true; } /** * @see ROT.Display */ setOptions(options) { Object.assign(this._options, options); if (options.width || options.height || options.fontSize || options.fontFamily || options.spacing || options.layout) { if (options.layout) { let ctor = BACKENDS[options.layout]; this._backend = new ctor(); } this._backend.setOptions(this._options); this._dirty = true; } return this; } /** * Returns currently set options */ getOptions() { return this._options; } /** * Returns the DOM node of this display */ getContainer() { return this._backend.getContainer(); } /** * Compute the maximum width/height to fit into a set of given constraints * @param {int} availWidth Maximum allowed pixel width * @param {int} availHeight Maximum allowed pixel height * @returns {int[2]} cellWidth,cellHeight */ computeSize(availWidth, availHeight) { return this._backend.computeSize(availWidth, availHeight); } /** * Compute the maximum font size to fit into a set of given constraints * @param {int} availWidth Maximum allowed pixel width * @param {int} availHeight Maximum allowed pixel height * @returns {int} fontSize */ computeFontSize(availWidth, availHeight) { return this._backend.computeFontSize(availWidth, availHeight); } computeTileSize(availWidth, availHeight) { let width = Math.floor(availWidth / this._options.width); let height = Math.floor(availHeight / this._options.height); return [width, height]; } /** * Convert a DOM event (mouse or touch) to map coordinates. Uses first touch for multi-touch. * @param {Event} e event * @returns {int[2]} -1 for values outside of the canvas */ eventToPosition(e) { let x, y; if ("touches" in e) { x = e.touches[0].clientX; y = e.touches[0].clientY; } else { x = e.clientX; y = e.clientY; } return this._backend.eventToPosition(x, y); } /** * @param {int} x * @param {int} y * @param {string || string[]} ch One or more chars (will be overlapping themselves) * @param {string} [fg] foreground color * @param {string} [bg] background color */ draw(x, y, ch, fg, bg) { if (!fg) { fg = this._options.fg; } if (!bg) { bg = this._options.bg; } let key = `${x},${y}`; this._data[key] = [x, y, ch, fg, bg]; if (this._dirty === true) { return; } // will already redraw everything if (!this._dirty) { this._dirty = {}; } // first! this._dirty[key] = true; } /** * @param {int} x * @param {int} y * @param {string || string[]} ch One or more chars (will be overlapping themselves) * @param {string || null} [fg] foreground color * @param {string || null} [bg] background color */ drawOver(x, y, ch, fg, bg) { const key = `${x},${y}`; const existing = this._data[key]; if (existing) { existing[2] = ch || existing[2]; existing[3] = fg || existing[3]; existing[4] = bg || existing[4]; } else { this.draw(x, y, ch, fg, bg); } } /** * Draws a text at given position. Optionally wraps at a maximum length. Currently does not work with hex layout. * @param {int} x * @param {int} y * @param {string} text May contain color/background format specifiers, %c{name}/%b{name}, both optional. %c{}/%b{} resets to default. * @param {int} [maxWidth] wrap at what width? * @returns {int} lines drawn */ drawText(x, y, text, maxWidth) { let fg = null; let bg = null; let cx = x; let cy = y; let lines = 1; if (!maxWidth) { maxWidth = this._options.width - x; } let tokens = Text.tokenize(text, maxWidth); while (tokens.length) { // interpret tokenized opcode stream let token = tokens.shift(); switch (token.type) { case Text.TYPE_TEXT: let isSpace = false, isPrevSpace = false, isFullWidth = false, isPrevFullWidth = false; for (let i = 0; i < token.value.length; i++) { let cc = token.value.charCodeAt(i); let c = token.value.charAt(i); if (this._options.layout === "term") { let cch = cc >> 8; let isCJK = cch === 0x11 || (cch >= 0x2e && cch <= 0x9f) || (cch >= 0xac && cch <= 0xd7) || (cc >= 0xA960 && cc <= 0xA97F); if (isCJK) { this.draw(cx + 0, cy, c, fg, bg); this.draw(cx + 1, cy, "\t", fg, bg); cx += 2; continue; } } // Assign to `true` when the current char is full-width. isFullWidth = (cc > 0xff00 && cc < 0xff61) || (cc > 0xffdc && cc < 0xffe8) || cc > 0xffee; // Current char is space, whatever full-width or half-width both are OK. isSpace = (c.charCodeAt(0) == 0x20 || c.charCodeAt(0) == 0x3000); // The previous char is full-width and // current char is nether half-width nor a space. if (isPrevFullWidth && !isFullWidth && !isSpace) { cx++; } // add an extra position // The current char is full-width and // the previous char is not a space. if (isFullWidth && !isPrevSpace) { cx++; } // add an extra position this.draw(cx++, cy, c, fg, bg); isPrevSpace = isSpace; isPrevFullWidth = isFullWidth; } break; case Text.TYPE_FG: fg = token.value || null; break; case Text.TYPE_BG: bg = token.value || null; break; case Text.TYPE_NEWLINE: cx = x; cy++; lines++; break; } } return lines; } /** * Timer tick: update dirty parts */ _tick() { this._backend.schedule(this._tick); if (!this._dirty) { return; } if (this._dirty === true) { // draw all this._backend.clear(); for (let id in this._data) { this._draw(id, false); } // redraw cached data } else { // draw only dirty for (let key in this._dirty) { this._draw(key, true); } } this._dirty = false; } /** * @param {string} key What to draw * @param {bool} clearBefore Is it necessary to clean before? */ _draw(key, clearBefore) { let data = this._data[key]; if (data[4] != this._options.bg) { clearBefore = true; } this._backend.draw(data, clearBefore); } } Display.Rect = Rect; Display.Hex = Hex; Display.Tile = Tile; Display.TileGL = TileGL; Display.Term = Term;