├── .gitignore ├── package-lock.json ├── package.json ├── src ├── Color.ts ├── Layer.ts ├── Renderer.ts ├── Tile.ts ├── Vector.ts ├── index.html └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .parcel-cache/ 2 | dist/ 3 | node_modules/ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ascii-engine", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "parcel src/index.html" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "parcel": "^2.0.0-beta.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Color.ts: -------------------------------------------------------------------------------- 1 | export default class Color { 2 | private _r: number; 3 | private _g: number; 4 | private _b: number; 5 | private _a: number; 6 | private cssString: string; 7 | 8 | constructor(r: number = 255, g: number = 255, b: number = 255, a: number = 1) { 9 | this._r = r; 10 | this._g = g; 11 | this._b = b; 12 | this._a = a; 13 | 14 | this.makeCssString(); 15 | } 16 | 17 | get r() { return this._r; } 18 | get g() { return this._g; } 19 | get b() { return this._b; } 20 | get a() { return this._a; } 21 | set r(value:number) { this._r = value; this.makeCssString(); } 22 | set g(value:number) { this._g = value; this.makeCssString(); } 23 | set b(value:number) { this._b = value; this.makeCssString(); } 24 | set a(value:number) { this._a = value; this.makeCssString(); } 25 | 26 | private makeCssString() { 27 | this.cssString = `rgba(${this._r}, ${this._g}, ${this._b}, ${this._a})`; 28 | 29 | } 30 | 31 | toCssString() { 32 | return this.cssString; 33 | } 34 | 35 | clone() { 36 | return new Color(this._r, this._g, this._b, this._a); 37 | } 38 | }; 39 | 40 | -------------------------------------------------------------------------------- /src/Layer.ts: -------------------------------------------------------------------------------- 1 | import Color from "./Color"; 2 | import Tile from "./Tile"; 3 | import Vector from "./Vector"; 4 | 5 | interface DrawingOperation { 6 | tile: Tile; 7 | char: string; 8 | color: Color; 9 | background: Color; 10 | pos: Vector; 11 | isVisible: boolean; 12 | }; 13 | 14 | const drawingOperation = (tile: Tile): DrawingOperation => ({ 15 | tile, 16 | char: tile.char, 17 | color: tile.color.clone(), 18 | background: tile.background.clone(), 19 | pos: tile.pos.clone(), 20 | isVisible: tile.isVisible 21 | }); 22 | 23 | interface LayerConstructorOptions { 24 | opacity?: number; 25 | isVisible?: boolean; 26 | pos?: Vector; 27 | size: Vector; 28 | z?: number; 29 | }; 30 | 31 | export default class Layer { 32 | opacity: number; 33 | isVisible: boolean; 34 | pos: Vector; 35 | size: Vector; 36 | operations: Array = []; 37 | private _z: number; 38 | 39 | constructor(options: LayerConstructorOptions) { 40 | this.opacity = options.opacity || 1; 41 | this.isVisible = options.isVisible || true; 42 | this.pos = options.pos || Vector.Zero(); 43 | this.size = options.size; 44 | 45 | this._z = options.z || 0; 46 | } 47 | 48 | get z() { return this._z; } 49 | 50 | draw(tile: Tile) { 51 | this.operations.push(drawingOperation(tile)); 52 | } 53 | 54 | clear() { 55 | this.operations = []; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Renderer.ts: -------------------------------------------------------------------------------- 1 | import Layer from "./Layer"; 2 | 3 | export default class Renderer { 4 | private namedLayers: Record = {}; 5 | private layers: Array = []; 6 | private layerElements: Record = {}; 7 | private size: number = 30; 8 | private beforeDraw: () => void = () => {}; 9 | 10 | frames:number = 0; 11 | 12 | setSize(n: number) { 13 | this.size = n; 14 | } 15 | 16 | addLayer(name: string, layer: Layer) { 17 | if (name in this.namedLayers) { 18 | return new Error(`${name} layer already attached to renderer`); 19 | } 20 | this.namedLayers[name] = layer; 21 | this.layers.push(layer); 22 | 23 | this.orderLayers(); 24 | 25 | return this; 26 | } 27 | 28 | onBeforeDraw(cb: () => void) { 29 | this.beforeDraw = cb; 30 | } 31 | 32 | commit() { 33 | this.beforeDraw(); 34 | 35 | for (let [name, layer] of Object.entries(this.namedLayers)) { 36 | let layerEl = this.layerElements[name]; 37 | 38 | if (!layerEl) { 39 | layerEl = document.createElement('div'); 40 | layerEl.classList.add('asc-engine-layer'); 41 | layerEl.style.fontSize = `${this.size}px`; 42 | layerEl.style.top = `${layer.pos.y * this.size}px`; 43 | layerEl.style.left = `${layer.pos.x * this.size / 2}px`; 44 | layerEl.style.height = `${layer.size.y * this.size}px`; 45 | layerEl.style.width = `${layer.size.x * this.size / 2}px`; 46 | layerEl.style.zIndex = layer.z.toString(); 47 | 48 | document.getElementById('asc-engine-layer-container').appendChild(layerEl); 49 | this.layerElements[name] = layerEl; 50 | } 51 | 52 | for (let op of layer.operations) { 53 | let opEl = document.getElementById(`asc-engine-tile-${op.tile.id}`); 54 | 55 | if (!opEl) { 56 | opEl = document.createElement('div'); 57 | opEl.classList.add('asc-engine-tile'); 58 | opEl.id = `asc-engine-tile-${op.tile.id}`; 59 | 60 | layerEl.appendChild(opEl); 61 | } 62 | 63 | if (op.isVisible) { 64 | opEl.innerHTML = op.char.replace(/ /g, ' '); 65 | opEl.style.color = op.color.toCssString(); 66 | opEl.style.backgroundColor = op.background.toCssString(); 67 | opEl.style.top = `${op.pos.y * this.size}px`; 68 | opEl.style.left = `${op.pos.x * this.size / 2}px`; 69 | opEl.style.display = 'block'; 70 | } else { 71 | opEl.style.display = 'none'; 72 | } 73 | } 74 | 75 | layer.clear(); 76 | } 77 | 78 | this.frames++; 79 | } 80 | 81 | private orderLayers() { 82 | this.layers = this.layers.sort((la, lb) => la.z - lb.z); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Tile.ts: -------------------------------------------------------------------------------- 1 | import Color from "./Color"; 2 | import Vector from "./Vector"; 3 | 4 | interface TileConstructorOptions { 5 | char?: string; 6 | color?: Color; 7 | background?: Color; 8 | pos?: Vector; 9 | isVisible?: boolean; 10 | }; 11 | 12 | export default class Tile { 13 | char: string; 14 | color: Color; 15 | background: Color; 16 | pos: Vector; 17 | isVisible: boolean; 18 | 19 | readonly id: string = Math.random().toString(36).slice(2); 20 | 21 | constructor(options: TileConstructorOptions) { 22 | this.char = options.char || ' '; 23 | this.color = options.color || new Color(); 24 | this.background = options.background || new Color(0, 0, 0, 1); 25 | this.pos = options.pos || new Vector(0, 0); 26 | this.isVisible = options.isVisible || true; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Vector.ts: -------------------------------------------------------------------------------- 1 | export default class Vector { 2 | x: number; 3 | y: number; 4 | 5 | constructor(x: number, y: number) { 6 | this.x = x; 7 | this.y = y; 8 | } 9 | 10 | add(v: Vector) { 11 | this.x += v.x; 12 | this.y += v.y; 13 | return this; 14 | } 15 | 16 | clone() { 17 | return new Vector(this.x, this.y); 18 | } 19 | 20 | static add(v1:Vector, v2:Vector) { 21 | return v1.clone().add(v2); 22 | } 23 | 24 | static Zero() { 25 | return new Vector(0, 0); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | LLJS ASCII Engine 8 | 9 | 35 | 36 | 37 | 38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Color from "./Color"; 2 | import Layer from "./Layer"; 3 | import Renderer from "./Renderer"; 4 | import Tile from "./Tile"; 5 | import Vector from "./Vector"; 6 | 7 | const WIDTH = 80; 8 | const HEIGHT = 24; 9 | 10 | const layers: Record = { 11 | background: new Layer({ size: new Vector(WIDTH, HEIGHT) }), 12 | actor: new Layer({ size: new Vector(WIDTH, HEIGHT) }), 13 | }; 14 | 15 | const player = new Tile({ 16 | background: new Color(0,0,0,0), 17 | char: '@', 18 | color: new Color(255, 0, 0), 19 | isVisible: true, 20 | pos: Vector.Zero() 21 | }); 22 | 23 | const backgroundTiles = Array.from({ length: WIDTH*HEIGHT }, (_, i) => { 24 | const x = i % WIDTH;1 25 | const y = Math.floor(i / WIDTH); 26 | 27 | return new Tile({ 28 | char: '.', 29 | pos: new Vector(x, y) 30 | }); 31 | }); 32 | 33 | const renderer = new Renderer(); 34 | renderer.setSize(35); 35 | renderer.addLayer('background', layers.background); 36 | renderer.addLayer('actor', layers.actor); 37 | 38 | 39 | renderer.onBeforeDraw(() => { 40 | layers.background.operations.forEach(op => { 41 | const newAlpha = (Math.sin(op.pos.x / op.pos.y + 5) + 1) / 2; 42 | op.color.a = newAlpha; 43 | }) 44 | }) 45 | 46 | 47 | const draw = () => { 48 | backgroundTiles.forEach(tile => layers.background.draw(tile)); 49 | layers.actor.draw(player); 50 | renderer.commit(); 51 | 52 | requestAnimationFrame(draw); 53 | } 54 | 55 | draw(); 56 | 57 | document.addEventListener('keydown', e => { 58 | switch (e.key) { 59 | case 'ArrowUp': { 60 | player.pos.add(new Vector(0, -1)); 61 | break; 62 | } 63 | case 'ArrowDown': { 64 | player.pos.add(new Vector(0, 1)); 65 | break; 66 | } 67 | case 'ArrowLeft': { 68 | player.pos.add(new Vector(-1, 0)); 69 | break; 70 | } 71 | case 'ArrowRight': { 72 | player.pos.add(new Vector(1, 0)); 73 | break; 74 | } 75 | } 76 | }) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib":["ES2017", "DOM"], 4 | "downlevelIteration": true, 5 | "target": "ES6" 6 | } 7 | } --------------------------------------------------------------------------------