/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ // This file re-generates 'larryScale_generated.cpp'. // To run it, install Node 8.0+, then run 'node larryScale_generator.js'. const fs = require('fs'); // Compass directions const Direction = { W: 0, NW: 1, N: 2, NE: 3, E: 4, SE: 5, S: 6, SW: 7, sanitize(direction) { return ((direction % 8) + 8) % 8; } }; function getVector(direction) { switch (direction) { case Direction.W: return [-1, 0]; case Direction.NW: return [-1, -1]; case Direction.N: return [0, -1]; case Direction.NE: return [1, -1]; case Direction.E: return [1, 0]; case Direction.SE: return [1, 1]; case Direction.S: return [0, 1]; case Direction.SW: return [-1, 1]; default: throw new Error(`Invalid direction: ${direction}`); } } // An equality matrix is a combination of eight Boolean flags indicating whether // each of the surrounding pixels has the same color as the central pixel. // // +-----------+-----------+-----------+ // | NW = 0x02 | N = 0x04 | NE = 0x08 | // +-----------+-----------+-----------+ // | W = 0x01 | Reference | E = 0x10 | // +-----------+-----------+-----------+ // | SW = 0x80 | S = 0x40 | SE = 0x20 | // +-----------+-----------+-----------+ class EqualityMatrix { constructor(value) { this.value = value; } get(direction) { const mask = 0x01 << Direction.sanitize(direction); return (this.value & mask) != 0; } set(direction, flag) { const mask = 0x01 << Direction.sanitize(direction); this.value = this.value & ~mask | (flag ? mask : 0x00); } get w() { return this.get(Direction.W); } set w(flag) { this.set(Direction.W, flag); } get nw() { return this.get(Direction.NW); } set nw(flag) { this.set(Direction.NW, flag); } get n() { return this.get(Direction.N); } set n(flag) { this.set(Direction.N, flag); } get ne() { return this.get(Direction.NE); } set ne(flag) { this.set(Direction.NE, flag); } get e() { return this.get(Direction.E); } set e(flag) { this.set(Direction.E, flag); } get se() { return this.get(Direction.SE); } set se(flag) { this.set(Direction.SE, flag); } get s() { return this.get(Direction.S); } set s(flag) { this.set(Direction.S, flag); } get sw() { return this.get(Direction.SW); } set sw(flag) { this.set(Direction.SW, flag); } toBraille() { return getBrailleColumn(this.nw, this.w, this.sw) + getBrailleColumn(this.n, true, this.s) + getBrailleColumn(this.ne, this.e, this.se); } } function getBrailleColumn(top, middle, bottom) { const codepoint = 0x2800 | (top ? 1 : 0) | (middle ? 2 : 0) | (bottom ? 4 : 0); return String.fromCodePoint(codepoint); } function indent(string, tabCount = 1) { const indentation = '\t'.repeat(tabCount); return string .split(/\r?\n/) .map(s => indentation + s) .join('\n'); } function toHex(number, minLength = 2) { const hex = number.toString(16); const padding = '0'.repeat(Math.max(minLength - hex.length, 0)); return `0x${padding}${hex}`; } function generateCaseLabel(matrix) { return `case ${toHex(matrix.value)} /*${matrix.toBraille()}*/:` } function generateCaseBlock(matrixes, body) { const maxLabelsPerLine = 8; const labels = matrixes .map(generateCaseLabel) .reduce((a, b, index) => a + ((index % maxLabelsPerLine === 0) ? '\n' : '\t') + b); return `${labels}\n${indent(body)}`; } function generateSwitchBlock(variableName, getCaseBody) { const matrixesByBody = new Map(); for (let value = 0; value <= 0xFF; value++) { const matrix = new EqualityMatrix(value); const body = getCaseBody(matrix); if (!matrixesByBody.has(body)) { matrixesByBody.set(body, []); } matrixesByBody.get(body).push(matrix); } const orderedPairs = [...matrixesByBody.entries()] // For readability: order cases by increasing code length .sort((a, b) => a[0].length - b[0].length); const switchStatements = orderedPairs .map(([body, matrixes]) => generateCaseBlock(matrixes, body)) .join('\n'); const comment = '// Note: There is a case label for every possible value, so we don\'t need a default label, but one is added to avoid any compiler warnings.'; return `${comment}\nswitch (${variableName}) {\ndefault: ${switchStatements}\n}`; } const PixelType = { // Pixel is part of a line LINE: 'line', // Pixel is part of a fill FILL: 'fill', // Pixel is part of a line *or* a fill INDETERMINATE: 'indeterminate' }; function getPixelType(matrix) { // Single pixels are fills if (matrix.value === 0) return PixelType.FILL; // 2x2 blocks are fills if ( (matrix.n && matrix.ne && matrix.e) || (matrix.e && matrix.se && matrix.s) || (matrix.s && matrix.sw && matrix.w) || (matrix.w && matrix.nw && matrix.n) ) return PixelType.FILL; // A pixel adjacent to a 2x2 block is a fill. // This requires reading out of the matrix, so we can't be sure. if ( (matrix.n && matrix.ne) || (matrix.ne && matrix.e) || (matrix.e && matrix.se) || (matrix.se && matrix.s) || (matrix.s && matrix.sw) || (matrix.sw && matrix.w) || (matrix.w && matrix.nw) || (matrix.nw && matrix.n) ) return PixelType.INDETERMINATE; // Everything else is part of a line return PixelType.LINE; } function isPowerOfTwo(number) { return Math.log2(number) % 1 === 0; } // Upscales a line pixel to 2x2. // Returns a 4-element array of Booleans in order top-left, top-right, bottom-left, bottom-right. // Each Boolean indicates whether the upscaled pixel should be filled with the original color. function getLineUpscaleFlags(matrix) { // The rules for upscaling lines are *not* symmetrical but biased toward the left // Special rules for upscaling smooth angled lines switch (matrix.value) { case 0x34 /*⠀⠃⠆*/: return [false, true, false, false]; // [ ▀] case 0x58 /*⠀⠆⠃*/: return [false, false, false, true]; // [ ▄] case 0x43 /*⠃⠆⠀*/: return [false, false, true, false]; // [▄ ] case 0x61 /*⠂⠆⠄*/: return [false, false, true, false]; // [▄ ] case 0x16 /*⠁⠃⠂*/: return [false, true, false, false]; // [ ▀] case 0xD0 /*⠄⠆⠂*/: return [false, false, false, true]; // [ ▄] case 0x24 /*⠀⠃⠄*/: case 0x48 /*⠀⠆⠁*/: return [false, true, false, true]; // [ █] case 0x21 /*⠂⠂⠄*/: case 0x90 /*⠄⠂⠂*/: return [false, false, true, true]; // [▄▄] case 0x50 /*⠀⠆⠂*/: return [true, true, true, false]; // [█▀] } // Generic rules for upscaling lines // Ignore diagonals next to fully-adjacent pixels matrix = new EqualityMatrix(matrix.value); if (matrix.w) { matrix.sw = matrix.nw = false; } if (matrix.n) { matrix.nw = matrix.ne = false; } if (matrix.e) { matrix.ne = matrix.se = false; } if (matrix.s) { matrix.se = matrix.sw = false; } // Mirror single lines if (isPowerOfTwo(matrix.value)) { matrix.value |= (matrix.value << 4) | (matrix.value >> 4); } return [ matrix.w || matrix.nw || matrix.n, matrix.ne || matrix.e, matrix.s || matrix.sw, matrix.se ]; } // Upscales a fill pixel to 2x2. // Same result format as getLineUpscaleFlags. function getFillUpscaleFlags(matrix) { // The rules for upscaling fills are *not* symmetrical but biased toward the top-left // Special rules for upscaling cornered fills switch (matrix.value) { case 0xE1 /*⠆⠆⠄*/: return [false, false, true, true]; // [▄▄] case 0x0F /*⠃⠃⠁*/: return [true, true, false, false]; // [▀▀] case 0xC3 /*⠇⠆⠀*/: case 0x87 /*⠇⠃⠀*/: return [true, false, true, false]; // [█ ] } // Generic rules for upscaling fills if (!matrix.s && !matrix.se && !matrix.e && (matrix.sw || matrix.ne)) { return [true, true, true, false]; // [█▀] } else if (!matrix.n && !matrix.ne && !matrix.e && (matrix.nw || matrix.se)) { return [true, false, true, true]; // [█▄] } else { return [true, true, true, true]; // [██] } } function formatOffset(number) { if (number < 0) { return ` - ${-number}`; } if (number > 0) { return ` + ${number}`; } return ''; } function generatePixelUpscaleCode(matrix, flags, pixelRecords, { generateBreak = true } = {}) { const targetsByValue = new Map(); function addAssignment(param, value) { if (targetsByValue.has(value)) { targetsByValue.get(value).push(param); } else { targetsByValue.set(value, [param]); } } for (const pixelRecord of pixelRecords) { const param = pixelRecord.param; const useSourceColor = flags .filter((flag, index) => pixelRecord.flagIndexes.includes(index)) .some(flag => flag); if (useSourceColor) { addAssignment(param, 'pixel'); } else { const sourceDirections = pixelRecord.sourceDirections .filter(d => !matrix.get(d)); const value = sourceDirections .filter(d => !matrix.get(d)) // We don't want to get our own color .map(d => { const vector = getVector(d); const otherValueCode = `src.get(x${formatOffset(vector[0])}, y${formatOffset(vector[1])})`; return `!linePixels.get(x${formatOffset(vector[0])}, y${formatOffset(vector[1])}) ? ${otherValueCode} : `; }) .join('') + 'pixel'; addAssignment(param, value); } } return [...targetsByValue.entries()] .map(([value, targets]) => [...targets, value].join(' = ') + ';') .concat(generateBreak ? ['break;'] : []) .join('\n'); } function generateScalePixelFunction(width, height, pixelRecords) { const params = pixelRecords .map((pixelRecord, index) => `Color &${pixelRecord.param}`) .join(', '); const header = `inline void scalePixelTo${width}x${height}(\n\tconst MarginedBitmap &src,\n\tconst MarginedBitmap &linePixels,\n\tint x, int y,\n\t// Out parameters\n\t${params}\n)`; const prefix = 'const Color pixel = src.get(x, y);\n' + 'const EqualityMatrix matrix = getEqualityMatrix(src.getPointerTo(x, y), src.getStride());'; const switchBlock = generateSwitchBlock('matrix', matrix => { const pixelType = getPixelType(matrix); switch (pixelType) { case PixelType.LINE: return generatePixelUpscaleCode(matrix, getLineUpscaleFlags(matrix), pixelRecords); case PixelType.FILL: return generatePixelUpscaleCode(matrix, getFillUpscaleFlags(matrix), pixelRecords); case PixelType.INDETERMINATE: const lineUpscaleCode = generatePixelUpscaleCode(matrix, getLineUpscaleFlags(matrix), pixelRecords, { generateBreak: false }); const fillUpscaleCode = generatePixelUpscaleCode(matrix, getFillUpscaleFlags(matrix), pixelRecords, { generateBreak: false }); return `if (linePixels.get(x, y)) {\n${indent(lineUpscaleCode)}\n} else {\n${indent(fillUpscaleCode)}\n}\nbreak;`; } }); return `${header} {\n${indent(prefix)}\n\n${indent(switchBlock)}\n}`; } function generateScalePixelTo2x2() { const pixelRecords = [ { param: 'topLeft', flagIndexes: [0], sourceDirections: [Direction.N, Direction.W] }, { param: 'topRight', flagIndexes: [1], sourceDirections: [Direction.N, Direction.E] }, { param: 'bottomLeft', flagIndexes: [2], sourceDirections: [Direction.S, Direction.W] }, { param: 'bottomRight', flagIndexes: [3], sourceDirections: [Direction.S, Direction.E] } ]; return generateScalePixelFunction(2, 2, pixelRecords); } function generateScalePixelTo2x1() { const pixelRecords = [ { param: 'left', flagIndexes: [0, 2], sourceDirections: [Direction.N, Direction.W, Direction.S] }, { param: 'right', flagIndexes: [1, 3], sourceDirections: [Direction.N, Direction.E, Direction.S] } ]; return generateScalePixelFunction(2, 1, pixelRecords); } function generateScalePixelTo1x2() { const pixelRecords = [ { param: 'top', flagIndexes: [0, 1], sourceDirections: [Direction.N, Direction.W, Direction.E] }, { param: 'bottom', flagIndexes: [2, 3], sourceDirections: [Direction.S, Direction.W, Direction.E] } ]; return generateScalePixelFunction(1, 2, pixelRecords); } const generators = [generateScalePixelTo2x2, generateScalePixelTo2x1, generateScalePixelTo1x2]; const generatedFunctions = generators .map(generator => generator()) .join('\n\n'); const legalese = fs.readFileSync(__filename, 'utf8').match(/\/\*[\s\S]*?\*\//)[0]; const headerComment = '// This file was generated by larryScale_generator.js.\n// Do not edit directly! Instead, edit the generator script and run it.' fs.writeFileSync('./larryScale_generated.cpp', `${legalese}\n\n${headerComment}\n\n${generatedFunctions}\n`);