diff --git a/package-lock.json b/package-lock.json index 5938777a..78b51e98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,12 @@ "version": "3.9.0", "license": "GPL-3.0", "dependencies": { + "@types/chroma-js": "^2.1.3", "@types/emscripten": "^1.39.5", "@wasmer/wasi": "^0.12.0", "@wasmer/wasmfs": "^0.12.0", "binaryen": "^101.0.0", + "chroma-js": "^2.1.2", "clipboard": "^2.0.6", "error-stack-parser": "^2.0.6", "fast-png": "^5.0.4", @@ -725,6 +727,11 @@ "@types/jquery": "*" } }, + "node_modules/@types/chroma-js": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz", + "integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g==" + }, "node_modules/@types/debug": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", @@ -1853,6 +1860,14 @@ "fsevents": "~2.3.2" } }, + "node_modules/chroma-js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.1.2.tgz", + "integrity": "sha512-ri/ouYDWuxfus3UcaMxC1Tfp3IE9K5iQzxc2hSxbBRVNQFut1UuGAsZmiAf2mOUubzGJwgMSv9lHg+XqLaz1QQ==", + "dependencies": { + "cross-env": "^6.0.3" + } + }, "node_modules/chromedriver": { "version": "92.0.1", "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-92.0.1.tgz", @@ -2233,11 +2248,25 @@ "buffer": "^5.1.0" } }, + "node_modules/cross-env": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-6.0.3.tgz", + "integrity": "sha512-+KqxF6LCvfhWvADcDPqo64yVIB31gv/jQulX2NGzKS/g3GEVz6/pt4wjHFtFWsHMddebWD/sDthJemzM4MaAag==", + "dependencies": { + "cross-spawn": "^7.0.0" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4950,8 +4979,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "devOptional": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "node_modules/isstream": { "version": "0.1.2", @@ -7608,7 +7636,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -8525,7 +8552,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -8537,7 +8563,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -9673,7 +9698,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -10682,6 +10706,11 @@ "@types/jquery": "*" } }, + "@types/chroma-js": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz", + "integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g==" + }, "@types/debug": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", @@ -11599,6 +11628,14 @@ "readdirp": "~3.6.0" } }, + "chroma-js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.1.2.tgz", + "integrity": "sha512-ri/ouYDWuxfus3UcaMxC1Tfp3IE9K5iQzxc2hSxbBRVNQFut1UuGAsZmiAf2mOUubzGJwgMSv9lHg+XqLaz1QQ==", + "requires": { + "cross-env": "^6.0.3" + } + }, "chromedriver": { "version": "92.0.1", "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-92.0.1.tgz", @@ -11907,11 +11944,18 @@ "buffer": "^5.1.0" } }, + "cross-env": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-6.0.3.tgz", + "integrity": "sha512-+KqxF6LCvfhWvADcDPqo64yVIB31gv/jQulX2NGzKS/g3GEVz6/pt4wjHFtFWsHMddebWD/sDthJemzM4MaAag==", + "requires": { + "cross-spawn": "^7.0.0" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -13995,8 +14039,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "devOptional": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "isstream": { "version": "0.1.2", @@ -16195,8 +16238,7 @@ "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" }, "path-parse": { "version": "1.0.7", @@ -16894,7 +16936,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "requires": { "shebang-regex": "^3.0.0" } @@ -16902,8 +16943,7 @@ "shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, "shiki": { "version": "0.9.6", @@ -17815,7 +17855,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "requires": { "isexe": "^2.0.0" } diff --git a/package.json b/package.json index 47a9543a..44ed7be9 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,12 @@ }, "license": "GPL-3.0", "dependencies": { + "@types/chroma-js": "^2.1.3", "@types/emscripten": "^1.39.5", "@wasmer/wasi": "^0.12.0", "@wasmer/wasmfs": "^0.12.0", "binaryen": "^101.0.0", + "chroma-js": "^2.1.2", "clipboard": "^2.0.6", "error-stack-parser": "^2.0.6", "fast-png": "^5.0.4", diff --git a/src/common/basic/runtime.ts b/src/common/basic/runtime.ts index 01d23e4f..01bae2e1 100644 --- a/src/common/basic/runtime.ts +++ b/src/common/basic/runtime.ts @@ -2,6 +2,7 @@ import * as basic from "./compiler"; import { EmuHalt } from "../emu"; import { SourceLocation } from "../workertypes"; +import { isArray } from "../util"; function isLiteral(arg: basic.Expr): arg is basic.Literal { return (arg as any).value != null; @@ -33,10 +34,6 @@ interface CompiledStatement { $run?: () => void; } -function isArray(obj) { - return obj != null && (Array.isArray(obj) || obj.BYTES_PER_ELEMENT); -} - class RNG { next : () => number; seed : (aa,bb,cc,dd) => void; diff --git a/src/common/script/env.ts b/src/common/script/env.ts index 277034bf..c4ad6d03 100644 --- a/src/common/script/env.ts +++ b/src/common/script/env.ts @@ -69,8 +69,9 @@ export class Environment { this.postamble = '\n}'; } error(varname: string, msg: string) { - console.log(varname, this.declvars[varname]); - throw new RuntimeError(this.declvars && this.declvars[varname].loc, msg); + let obj = this.declvars && this.declvars[varname]; + console.log(varname, obj); + throw new RuntimeError(obj && obj.loc, msg); } preprocess(code: string): string { this.declvars = {}; @@ -94,7 +95,7 @@ export class Environment { }; const result = yufka(code, options, (node, { update, source, parent }) => { function isTopLevel() { - return parent().type === 'ExpressionStatement' && parent(2) && parent(2).type === 'Program'; + return parent() && parent().type === 'ExpressionStatement' && parent(2) && parent(2).type === 'Program'; } let left = node['left']; switch (node.type) { @@ -152,7 +153,10 @@ export class Environment { } fullkey.push(key); if (typeof value === 'function') { - this.error(fullkey[0], `"${prkey()}" is a function. Did you forget to pass parameters?`); // TODO? did you mean (needs to see entire expr) + if (fullkey.length == 1) + this.error(fullkey[0], `"${prkey()}" is a function. Did you forget to pass parameters?`); // TODO? did you mean (needs to see entire expr) + else + this.error(fullkey[0], `This expression may be incomplete.`); // TODO? did you mean (needs to see entire expr) } if (typeof value === 'symbol') { this.error(fullkey[0], `"${prkey()}" is a Symbol, and can't be used.`) // TODO? diff --git a/src/common/script/lib/bitmap.ts b/src/common/script/lib/bitmap.ts index 8596d9c4..59970fa9 100644 --- a/src/common/script/lib/bitmap.ts +++ b/src/common/script/lib/bitmap.ts @@ -1,4 +1,5 @@ +// TODO: dynamic import import * as fastpng from 'fast-png'; import { Palette } from './color'; import * as io from './io' @@ -118,7 +119,7 @@ export class IndexedBitmap extends MappedBitmap { initial?: Uint8Array | PixelMapFunction ) { super(width, height, bitsPerPixel || 8, initial); - this.palette = color.palette.colors(this.bitsPerPixel); + this.palette = color.palette.colors(1 << this.bitsPerPixel); } getRGBAForIndex(index: number): number { @@ -172,11 +173,7 @@ export namespace png { } function convertIndexedToBitmap(png: fastpng.IDecodedPNG): IndexedBitmap { var palarr = png.palette as [number, number, number, number][]; - var palette = new Palette(palarr.length); - for (let i = 0; i < palarr.length; i++) { - // TODO: alpha? - palette.colors[i] = color.arr2rgba(palarr[i]) | 0xff000000; - } + var palette = new Palette(palarr); let bitmap = new IndexedBitmap(png.width, png.height, png.depth); if (png.depth == 8) { bitmap.pixels.set(png.data); @@ -194,12 +191,12 @@ export namespace png { return bitmap; } function convertRGBAToBitmap(png: fastpng.IDecodedPNG): RGBABitmap { - let bitmap = new RGBABitmap(png.width, png.height); - let rgba = [0, 0, 0, 0]; + const bitmap = new RGBABitmap(png.width, png.height); + const rgba : [number,number,number,number] = [0, 0, 0, 0]; for (let i = 0; i < bitmap.rgba.length; i++) { for (let j = 0; j < 4; j++) rgba[j] = png.data[i * 4 + j]; - bitmap.rgba[i] = color.arr2rgba(rgba); + bitmap.rgba[i] = color.rgba(rgba); } // TODO: aspect etc return bitmap; diff --git a/src/common/script/lib/color.ts b/src/common/script/lib/color.ts index e9e12a03..637fcf1f 100644 --- a/src/common/script/lib/color.ts +++ b/src/common/script/lib/color.ts @@ -1,28 +1,61 @@ +import _chroma from 'chroma-js' +import { isArray } from '../../util'; + +export type ColorSource = number | [number,number,number] | [number,number,number,number] | string; + export class Palette { - colors: Uint32Array; + readonly colors: Uint32Array; - constructor(arg: number | number[] | Uint32Array) { + constructor(arg: number | any[] | Uint32Array) { + // TODO: more array types if (typeof arg === 'number') { - if (!(arg >= 1 && arg <= 65536)) throw new Error('Invalid palette size ' + arg); this.colors = new Uint32Array(arg); - } else { + } else if (arg instanceof Uint32Array) { this.colors = new Uint32Array(arg); - } + } else if (isArray(arg)) { + this.colors = new Uint32Array(arg.map(rgb)); + } else + throw new Error(`Invalid Palette constructor`) + } + get(index: number) { + return this.colors[index]; } } -export function rgb(r: number, g: number, b: number): number { - return ((r & 0xff) << 0) | ((g & 0xff) << 8) | ((b & 0xff) << 16) | 0xff000000; +export const chroma = _chroma; + +export function from(obj: ColorSource) { + return _chroma(obj as any); } -export function arr2rgba(arr: number[] | Uint8Array): number { - let v = 0; - v |= (arr[0] & 0xff) << 0; - v |= (arr[1] & 0xff) << 8; - v |= (arr[2] & 0xff) << 16; - v |= (arr[3] & 0xff) << 24; - return v; +export function rgb(obj: ColorSource) : number; +export function rgb(r: number, g: number, b: number) : number; + +export function rgb(obj: any, g?: number, b?: number) : number { + return rgba(obj, g, b, 0xff) | 0xff000000; +} + +export function rgba(obj: ColorSource) : number; +export function rgba(r: number, g: number, b: number, a: number) : number; + +export function rgba(obj: ColorSource, g?: number, b?: number, a?: number) : number { + if (typeof obj === 'number') { + let r = obj; + if (g != null && b != null) + return ((r & 0xff) << 0) | ((g & 0xff) << 8) | ((b & 0xff) << 16) | ((a & 0xff) << 24); + return obj; + } + if (typeof obj !== 'string' && isArray(obj) && typeof obj[0] === 'number') { + let arr = obj; + let v = 0; + v |= (arr[0] & 0xff) << 0; + v |= (arr[1] & 0xff) << 8; + v |= (arr[2] & 0xff) << 16; + v |= (arr[3] & 0xff) << 24; + return v; + } + return rgba(from(obj).rgb()); } export function rgba2arr(v: number): number[] { @@ -34,18 +67,29 @@ export function rgba2arr(v: number): number[] { ] } +export function rgb2arr(v: number): number[] { + return rgba2arr(v).slice(0,3); +} + +type ColorGenFunc = (index: number) => number; + export namespace palette { - export function generate(bpp: number, func: (index: number) => number) { - var pal = new Palette(1 << bpp); - for (var i = 0; i < pal.colors.length; i++) { - pal.colors[i] = 0xff000000 | func(i); + export function from(obj: number | any[] | Uint32Array | ColorGenFunc, count?: number) { + if (typeof obj === 'function') { + if (!count) throw new Error(`You must also pass the number of colors to generate.`) + var pal = new Palette(count); + for (var i = 0; i < pal.colors.length; i++) { + pal.colors[i] = rgba(obj(i)); + } + return pal; + } else { + return new Palette(obj); } - return pal; } export function mono() { return greys(1); } - export function rgb2() { + function rgb2() { return new Palette([ rgb(0, 0, 0), rgb(0, 0, 255), @@ -53,7 +97,7 @@ export namespace palette { rgb(0, 255, 0), ]); } - export function rgb3() { + function rgb3() { return new Palette([ rgb(0, 0, 0), rgb(0, 0, 255), @@ -65,23 +109,26 @@ export namespace palette { rgb(255, 255, 255), ]); } - export function greys(bpp: number) { - return generate(bpp, (i) => { - let v = 255 * i / ((1 << bpp) - 1); + export function greys(count: number) { + return from((i) => { + let v = 255 * i / (count - 1); return rgb(v,v,v); - }); + }, count); } - export function colors(bpp: number) { - switch (bpp) { - case 1: return mono(); - case 2: return rgb2(); - case 3: return rgb3(); - default: return factors(bpp); // TODO + export function colors(count: number) { + switch (count) { + case 2: return mono(); + case 4: return rgb2(); + case 8: return rgb3(); + default: return factors(count); // TODO } } - export function factors(bpp: number, mult?: number) { + export function helix(count: number) { + return new Palette(chroma.cubehelix().scale().colors(count)); + } + export function factors(count: number, mult?: number) { mult = mult || 0x031f0f; - return generate(bpp, (i) => i * mult); + return from((i) => rgb(i * mult), count); } // TODO: https://www.iquilezles.org/www/articles/palettes/palettes.htm } diff --git a/src/common/util.ts b/src/common/util.ts index 9284d848..c5c526cd 100644 --- a/src/common/util.ts +++ b/src/common/util.ts @@ -492,6 +492,14 @@ export function getRootBasePlatform(platform : string) : string { return getRootPlatform(getBasePlatform(platform)); } +export function isArray(obj: any) : obj is ArrayLike { + return obj != null && (Array.isArray(obj) || isTypedArray(obj)); +} + +export function isTypedArray(obj: any) : obj is ArrayLike { + return obj != null && obj['BYTES_PER_ELEMENT']; +} + export function convertDataToUint8Array(data: string|Uint8Array) : Uint8Array { return (typeof data === 'string') ? stringToByteArray(data) : data; }