mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2025-07-16 05:24:07 +00:00
scripting: print(), css, palette layout, flex
make syncdev/prod: fixed mime type upload
This commit is contained in:
4
Makefile
4
Makefile
@@ -52,7 +52,7 @@ VERSION := $(shell git tag -l --points-at HEAD)
|
|||||||
syncdev: distro
|
syncdev: distro
|
||||||
cp config.js $(TMP)
|
cp config.js $(TMP)
|
||||||
#aws --profile pzp s3 sync --follow-symlinks $(TMP)/ s3://8bitworkshop.com/dev/
|
#aws --profile pzp s3 sync --follow-symlinks $(TMP)/ s3://8bitworkshop.com/dev/
|
||||||
s3cmd -c ~/.s3pzp sync -MFP $(TMP)/ s3://8bitworkshop.com/dev/
|
s3cmd -c ~/.s3pzp sync -MFP --no-mime-magic $(TMP)/ s3://8bitworkshop.com/dev/
|
||||||
rsync --stats -riltz --delete --chmod=a+rx -e "ssh" $(TMP)/ config.js $(RSYNC_PATH)/dev/
|
rsync --stats -riltz --delete --chmod=a+rx -e "ssh" $(TMP)/ config.js $(RSYNC_PATH)/dev/
|
||||||
|
|
||||||
syncprod: distro
|
syncprod: distro
|
||||||
@@ -63,5 +63,5 @@ syncprod: distro
|
|||||||
read
|
read
|
||||||
cp config.js $(TMP)
|
cp config.js $(TMP)
|
||||||
#aws --profile pzp s3 sync --follow-symlinks $(TMP)/ s3://8bitworkshop.com/v$(VERSION)/
|
#aws --profile pzp s3 sync --follow-symlinks $(TMP)/ s3://8bitworkshop.com/v$(VERSION)/
|
||||||
s3cmd -c ~/.s3pzp sync -MFP $(TMP)/ config.js s3://8bitworkshop.com/v$(VERSION)/
|
s3cmd -c ~/.s3pzp sync -MFP --no-mime-magic $(TMP)/ config.js s3://8bitworkshop.com/v$(VERSION)/
|
||||||
rsync --stats -riltz --chmod=a+rx -e "ssh" $(TMP)/ config.js $(RSYNC_PATH)/v$(VERSION)/
|
rsync --stats -riltz --chmod=a+rx -e "ssh" $(TMP)/ config.js $(RSYNC_PATH)/v$(VERSION)/
|
||||||
|
43
css/ui.css
43
css/ui.css
@@ -718,6 +718,7 @@ div.asset_toolbar {
|
|||||||
padding-top: 0.5em;
|
padding-top: 0.5em;
|
||||||
}
|
}
|
||||||
.tree-header {
|
.tree-header {
|
||||||
|
display: flex;
|
||||||
border: 2px solid #555;
|
border: 2px solid #555;
|
||||||
border-radius:8px;
|
border-radius:8px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -730,8 +731,11 @@ div.asset_toolbar {
|
|||||||
padding-right:0.75em;
|
padding-right:0.75em;
|
||||||
font-size: small;
|
font-size: small;
|
||||||
}
|
}
|
||||||
|
.tree-key {
|
||||||
|
padding-right:1em;
|
||||||
|
}
|
||||||
.tree-value {
|
.tree-value {
|
||||||
float:right;
|
margin-left:auto;
|
||||||
font-weight:normal;
|
font-weight:normal;
|
||||||
padding-right:1em;
|
padding-right:1em;
|
||||||
}
|
}
|
||||||
@@ -779,24 +783,45 @@ div.asset_toolbar {
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
.scripting-cell canvas {
|
.scripting-cell canvas {
|
||||||
height: 20vw;
|
min-height: 20em;
|
||||||
max-width: 95%;
|
max-width: 95%;
|
||||||
border: 2px solid #222;
|
border: 2px solid #222;
|
||||||
outline-color: #ccc;
|
outline-color: #ccc;
|
||||||
background: #000;
|
background: #000;
|
||||||
padding: 6px;
|
padding: 4px;
|
||||||
margin: 6px;
|
margin: 1px;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
}
|
}
|
||||||
.scripting-cell canvas:hover {
|
.scripting-cell canvas:hover {
|
||||||
outline:none;
|
|
||||||
border-color:#ccc;
|
border-color:#ccc;
|
||||||
}
|
}
|
||||||
|
.scripting-flex canvas {
|
||||||
|
min-height: 2vw;
|
||||||
|
max-height: 20vw;
|
||||||
|
}
|
||||||
|
div.scripting-editor {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.scripting-editor canvas {
|
||||||
|
min-height: 30vw;
|
||||||
|
max-height: 50vw;
|
||||||
|
}
|
||||||
div.scripting-color {
|
div.scripting-color {
|
||||||
padding:0.1em;
|
padding: 0.5em;
|
||||||
margin:0.1em;
|
}
|
||||||
|
div.scripting-color span {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
div.scripting-color:hover span {
|
||||||
|
visibility: visible;
|
||||||
}
|
}
|
||||||
div.scripting-grid {
|
div.scripting-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat( auto-fit, minmax(2em, 1fr) );
|
grid-template-columns: repeat( auto-fill, minmax(4em, 1fr) );
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
}
|
}
|
||||||
|
div.scripting-flex {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
@@ -77,7 +77,8 @@
|
|||||||
"test-worker": "NODE_PATH=$(pwd) mocha --timeout 60000 test/cli/testworker.js",
|
"test-worker": "NODE_PATH=$(pwd) mocha --timeout 60000 test/cli/testworker.js",
|
||||||
"test-platforms": "NODE_PATH=$(pwd) mocha --timeout 60000 test/cli/testplatforms.js",
|
"test-platforms": "NODE_PATH=$(pwd) mocha --timeout 60000 test/cli/testplatforms.js",
|
||||||
"test-verilog": "NODE_PATH=$(pwd) mocha --timeout 60000 --reporter mocha-simple-html-reporter --reporter-options output=test/output/verilog.html test/cli/testverilog.js",
|
"test-verilog": "NODE_PATH=$(pwd) mocha --timeout 60000 --reporter mocha-simple-html-reporter --reporter-options output=test/output/verilog.html test/cli/testverilog.js",
|
||||||
"test-web": "nightwatch -e chrome test/web",
|
"test-web-quick": "nightwatch -e chrome test/web/testembed.js",
|
||||||
|
"test-web-all": "nightwatch -e chrome test/web",
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"fuzzbasic": "jsfuzz gen/common/basic/fuzz.js ~/basic/corpus/ --versifier false",
|
"fuzzbasic": "jsfuzz gen/common/basic/fuzz.js ~/basic/corpus/ --versifier false",
|
||||||
"fuzzhdl": "jsfuzz -r binaryen gen/common/hdl/fuzz.js ~/verilator/corpus/ --versifier false",
|
"fuzzhdl": "jsfuzz -r binaryen gen/common/hdl/fuzz.js ~/verilator/corpus/ --versifier false",
|
||||||
|
@@ -55,14 +55,19 @@ export class Environment {
|
|||||||
obj: {};
|
obj: {};
|
||||||
seq: number;
|
seq: number;
|
||||||
declvars : {[name : string] : acorn.Node};
|
declvars : {[name : string] : acorn.Node};
|
||||||
|
builtins : {}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly globalenv: any,
|
public readonly globalenv: any,
|
||||||
public readonly path: string
|
public readonly path: string
|
||||||
) {
|
) {
|
||||||
var badlst = Object.getOwnPropertyNames(this.globalenv).filter(name => GLOBAL_GOODLIST.indexOf(name) < 0);
|
var badlst = Object.getOwnPropertyNames(this.globalenv).filter(name => GLOBAL_GOODLIST.indexOf(name) < 0);
|
||||||
|
this.builtins = {
|
||||||
|
print: (...args) => this.print(args),
|
||||||
|
...IMPORTS
|
||||||
|
}
|
||||||
this.preamble = `'use strict';var ${badlst.join(',')};`;
|
this.preamble = `'use strict';var ${badlst.join(',')};`;
|
||||||
for (var impname in IMPORTS) {
|
for (var impname in this.builtins) {
|
||||||
this.preamble += `var ${impname}=$$.${impname};`
|
this.preamble += `var ${impname}=$$.${impname};`
|
||||||
}
|
}
|
||||||
this.preamble += '{\n';
|
this.preamble += '{\n';
|
||||||
@@ -73,6 +78,11 @@ export class Environment {
|
|||||||
console.log(varname, obj);
|
console.log(varname, obj);
|
||||||
throw new RuntimeError(obj && obj.loc, msg);
|
throw new RuntimeError(obj && obj.loc, msg);
|
||||||
}
|
}
|
||||||
|
print(args: any[]) {
|
||||||
|
if (args && args.length > 0 && args[0] != null) {
|
||||||
|
this.obj[`$$print__${this.seq++}`] = args.length == 1 ? args[0] : args;
|
||||||
|
}
|
||||||
|
}
|
||||||
preprocess(code: string): string {
|
preprocess(code: string): string {
|
||||||
this.declvars = {};
|
this.declvars = {};
|
||||||
this.seq = 0;
|
this.seq = 0;
|
||||||
@@ -94,15 +104,20 @@ export class Environment {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const result = yufka(code, options, (node, { update, source, parent }) => {
|
const result = yufka(code, options, (node, { update, source, parent }) => {
|
||||||
function isTopLevel() {
|
const isTopLevel = () => {
|
||||||
return parent() && parent().type === 'ExpressionStatement' && parent(2) && parent(2).type === 'Program';
|
return parent() && parent().type === 'ExpressionStatement' && parent(2) && parent(2).type === 'Program';
|
||||||
}
|
}
|
||||||
let left = node['left'];
|
const convertTopToPrint = () => {
|
||||||
|
if (isTopLevel()) update(`print(${source()});`)
|
||||||
|
}
|
||||||
|
const left = node['left'];
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
// error on forbidden keywords
|
// error on forbidden keywords
|
||||||
case 'Identifier':
|
case 'Identifier':
|
||||||
if (GLOBAL_BADLIST.indexOf(source()) >= 0) {
|
if (GLOBAL_BADLIST.indexOf(source()) >= 0) {
|
||||||
update(`__FORBIDDEN__KEYWORD__${source()}__`) // TODO? how to preserve line number?
|
update(`__FORBIDDEN__KEYWORD__${source()}__`) // TODO? how to preserve line number?
|
||||||
|
} else {
|
||||||
|
convertTopToPrint();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
// x = expr --> var x = expr (first use)
|
// x = expr --> var x = expr (first use)
|
||||||
@@ -118,10 +133,19 @@ export class Environment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
// convert lone expressions to print()
|
||||||
|
case 'UnaryExpression':
|
||||||
|
case 'BinaryExpression':
|
||||||
|
case 'CallExpression':
|
||||||
|
case 'MemberExpression':
|
||||||
|
convertTopToPrint();
|
||||||
|
break;
|
||||||
// literal comments
|
// literal comments
|
||||||
case 'Literal':
|
case 'Literal':
|
||||||
if (isTopLevel() && typeof node['value'] === 'string') {
|
if (typeof node['value'] === 'string' && isTopLevel()) {
|
||||||
update(`this.$$doc__${this.seq++} = { literaltext: ${source()} };`);
|
update(`this.$$doc__${this.seq++} = { literaltext: ${source()} };`);
|
||||||
|
} else {
|
||||||
|
convertTopToPrint();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -133,7 +157,7 @@ export class Environment {
|
|||||||
code = this.preprocess(code);
|
code = this.preprocess(code);
|
||||||
this.obj = {};
|
this.obj = {};
|
||||||
const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
|
const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
|
||||||
const fn = new AsyncFunction('$$', this.preamble + code + this.postamble).bind(this.obj, IMPORTS);
|
const fn = new AsyncFunction('$$', this.preamble + code + this.postamble).bind(this.obj, this.builtins);
|
||||||
await fn.call(this);
|
await fn.call(this);
|
||||||
this.checkResult(this.obj, new Set(), []);
|
this.checkResult(this.obj, new Set(), []);
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import * as fastpng from 'fast-png';
|
|||||||
import { Palette } from './color';
|
import { Palette } from './color';
|
||||||
import * as io from './io'
|
import * as io from './io'
|
||||||
import * as color from './color'
|
import * as color from './color'
|
||||||
|
import { findIntegerFactors, RGBA } from '../../util';
|
||||||
|
|
||||||
export type PixelMapFunction = (x: number, y: number) => number;
|
export type PixelMapFunction = (x: number, y: number) => number;
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ export abstract class AbstractBitmap<T> {
|
|||||||
abstract setarray(arr: ArrayLike<number>) : AbstractBitmap<T>;
|
abstract setarray(arr: ArrayLike<number>) : AbstractBitmap<T>;
|
||||||
abstract set(x: number, y: number, val: number) : AbstractBitmap<T>;
|
abstract set(x: number, y: number, val: number) : AbstractBitmap<T>;
|
||||||
abstract get(x: number, y: number): number;
|
abstract get(x: number, y: number): number;
|
||||||
|
abstract getrgba(x: number, y: number): number;
|
||||||
|
|
||||||
inbounds(x: number, y: number): boolean {
|
inbounds(x: number, y: number): boolean {
|
||||||
return (x >= 0 && x < this.width && y >= 0 && y < this.height);
|
return (x >= 0 && x < this.width && y >= 0 && y < this.height);
|
||||||
@@ -45,6 +47,17 @@ export abstract class AbstractBitmap<T> {
|
|||||||
dest.assign((x, y) => this.get(x + srcx, y + srcy));
|
dest.assign((x, y) => this.get(x + srcx, y + srcy));
|
||||||
return dest;
|
return dest;
|
||||||
}
|
}
|
||||||
|
blit(src: BitmapType, dest: BitmapType,
|
||||||
|
destx: number, desty: number,
|
||||||
|
srcx: number, srcy: number)
|
||||||
|
{
|
||||||
|
for (var y=0; y<src.height; y++) {
|
||||||
|
for (var x=0; x<src.width; x++) {
|
||||||
|
let rgba = src.getrgba(x+srcx, y+srcy);
|
||||||
|
dest.set(x+destx, y+desty, rgba);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RGBABitmap extends AbstractBitmap<RGBABitmap> {
|
export class RGBABitmap extends AbstractBitmap<RGBABitmap> {
|
||||||
@@ -70,6 +83,9 @@ export class RGBABitmap extends AbstractBitmap<RGBABitmap> {
|
|||||||
get(x: number, y: number): number {
|
get(x: number, y: number): number {
|
||||||
return this.inbounds(x,y) ? this.rgba[y * this.width + x] : 0;
|
return this.inbounds(x,y) ? this.rgba[y * this.width + x] : 0;
|
||||||
}
|
}
|
||||||
|
getrgba(x: number, y: number): number {
|
||||||
|
return this.get(x, y);
|
||||||
|
}
|
||||||
blank(width: number, height: number) : RGBABitmap {
|
blank(width: number, height: number) : RGBABitmap {
|
||||||
return new RGBABitmap(width, height);
|
return new RGBABitmap(width, height);
|
||||||
}
|
}
|
||||||
@@ -106,7 +122,6 @@ export abstract class MappedBitmap extends AbstractBitmap<MappedBitmap> {
|
|||||||
get(x: number, y: number): number {
|
get(x: number, y: number): number {
|
||||||
return this.inbounds(x,y) ? this.pixels[y * this.width + x] : 0;
|
return this.inbounds(x,y) ? this.pixels[y * this.width + x] : 0;
|
||||||
}
|
}
|
||||||
abstract getRGBAForIndex(index: number): number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IndexedBitmap extends MappedBitmap {
|
export class IndexedBitmap extends MappedBitmap {
|
||||||
@@ -122,8 +137,8 @@ export class IndexedBitmap extends MappedBitmap {
|
|||||||
this.palette = color.palette.colors(1 << this.bitsPerPixel);
|
this.palette = color.palette.colors(1 << this.bitsPerPixel);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRGBAForIndex(index: number): number {
|
getrgba(x: number, y: number): number {
|
||||||
return this.palette.colors[index];
|
return this.palette && this.palette.colors[this.get(x, y)];
|
||||||
}
|
}
|
||||||
blank(width: number, height: number) : IndexedBitmap {
|
blank(width: number, height: number) : IndexedBitmap {
|
||||||
let bitmap = new IndexedBitmap(width, height, this.bitsPerPixel);
|
let bitmap = new IndexedBitmap(width, height, this.bitsPerPixel);
|
||||||
@@ -153,9 +168,64 @@ export function decode(arr: Uint8Array, fmt: PixelEditorImageFormat) {
|
|||||||
// TODO: guess if missing w/h/count?
|
// TODO: guess if missing w/h/count?
|
||||||
// TODO: reverse mapping
|
// TODO: reverse mapping
|
||||||
// TODO: maybe better composable functions
|
// TODO: maybe better composable functions
|
||||||
return pixels.map(data => new IndexedBitmap(fmt.w, fmt.h, fmt.bpp | 1, data));
|
let bpp = (fmt.bpp||1) * (fmt.np||1);
|
||||||
|
return pixels.map(data => new IndexedBitmap(fmt.w, fmt.h, bpp, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BitmapAnalysis {
|
||||||
|
min: {w: number, h: number};
|
||||||
|
max: {w: number, h: number};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function analyze(bitmaps: BitmapType[]) {
|
||||||
|
let r = {min:{w:0,h:0}, max:{w:0,h:0}};
|
||||||
|
for (let bmp of bitmaps) {
|
||||||
|
if (!(bmp instanceof AbstractBitmap)) return null;
|
||||||
|
r.min.w = Math.min(bmp.width);
|
||||||
|
r.max.w = Math.max(bmp.width);
|
||||||
|
r.min.h = Math.min(bmp.height);
|
||||||
|
r.max.h = Math.max(bmp.height);
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MontageOptions {
|
||||||
|
analysis?: BitmapAnalysis;
|
||||||
|
gap?: number;
|
||||||
|
aspect?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function montage(bitmaps: BitmapType[], options?: MontageOptions) {
|
||||||
|
let minmax = (options && options.analysis) || analyze(bitmaps);
|
||||||
|
if (minmax == null) throw new Error(`Expected an array of bitmaps`);
|
||||||
|
let hitrects = [];
|
||||||
|
let aspect = (options && options.aspect) || 1;
|
||||||
|
let gap = (options && options.gap) || 0;
|
||||||
|
if (minmax.min.w == minmax.max.w && minmax.min.h == minmax.max.h) {
|
||||||
|
let totalPixels = minmax.min.w * minmax.min.h * bitmaps.length;
|
||||||
|
let factors = findIntegerFactors(totalPixels, minmax.max.w, minmax.max.h, aspect);
|
||||||
|
let columns = Math.ceil(factors.a / minmax.min.w); // TODO: rounding?
|
||||||
|
let rows = Math.ceil(factors.b / minmax.min.h);
|
||||||
|
let result = new RGBABitmap(factors.a + gap * (columns-1), factors.b + gap * (rows-1));
|
||||||
|
let x = 0;
|
||||||
|
let y = 0;
|
||||||
|
bitmaps.forEach((bmp) => {
|
||||||
|
result.blit(bmp, result, x, y, 0, 0);
|
||||||
|
hitrects.push({x, y, w: bmp.width, h: bmp.height })
|
||||||
|
x += bmp.width + gap;
|
||||||
|
if (x >= result.width) {
|
||||||
|
x = 0;
|
||||||
|
y += bmp.height + gap;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
throw new Error(`combine() only supports uniformly-sized images right now`); // TODO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/////
|
||||||
|
|
||||||
export namespace png {
|
export namespace png {
|
||||||
export function read(url: string): BitmapType {
|
export function read(url: string): BitmapType {
|
||||||
return decode(io.readbin(url));
|
return decode(io.readbin(url));
|
||||||
@@ -325,6 +395,22 @@ export type PixelEditorImageFormat = {
|
|||||||
skip?:number
|
skip?:number
|
||||||
aspect?:number
|
aspect?:number
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function remapBits(x:number, arr:number[]) : number {
|
||||||
|
if (!arr) return x;
|
||||||
|
var y = 0;
|
||||||
|
for (var i=0; i<arr.length; i++) {
|
||||||
|
var s = arr[i];
|
||||||
|
if (s < 0) {
|
||||||
|
s = -s-1;
|
||||||
|
y ^= 1 << s;
|
||||||
|
}
|
||||||
|
if (x & (1 << i)) {
|
||||||
|
y ^= 1 << s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
export function convertWordsToImages(words:ArrayLike<number>, fmt:PixelEditorImageFormat) : Uint8Array[] {
|
export function convertWordsToImages(words:ArrayLike<number>, fmt:PixelEditorImageFormat) : Uint8Array[] {
|
||||||
var width = fmt.w;
|
var width = fmt.w;
|
||||||
@@ -346,7 +432,7 @@ export type PixelEditorImageFormat = {
|
|||||||
var shift = 0;
|
var shift = 0;
|
||||||
for (var x=0; x<width; x++) {
|
for (var x=0; x<width; x++) {
|
||||||
var color = 0;
|
var color = 0;
|
||||||
var ofs = ofs0; // TODO: remapBits(ofs0, fmt.remap);
|
var ofs = remapBits(ofs0, fmt.remap);
|
||||||
// TODO: if (fmt.reindex) { [ofs, shift] = reindexMask(x, fmt.reindex); ofs += ofs0; }
|
// TODO: if (fmt.reindex) { [ofs, shift] = reindexMask(x, fmt.reindex); ofs += ofs0; }
|
||||||
for (var p=0; p<nplanes; p++) {
|
for (var p=0; p<nplanes; p++) {
|
||||||
var byte = words[ofs + p*pofs + skip];
|
var byte = words[ofs + p*pofs + skip];
|
||||||
|
@@ -4,12 +4,19 @@ import { isArray } from '../../util';
|
|||||||
|
|
||||||
export type ColorSource = number | [number,number,number] | [number,number,number,number] | string;
|
export type ColorSource = number | [number,number,number] | [number,number,number,number] | string;
|
||||||
|
|
||||||
|
function checkCount(count) {
|
||||||
|
if (count < 0 || count > 65536) {
|
||||||
|
throw new Error("Palettes cannot have more than 2^16 (65536) colors.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class Palette {
|
export class Palette {
|
||||||
readonly colors: Uint32Array;
|
readonly colors: Uint32Array;
|
||||||
|
|
||||||
constructor(arg: number | any[] | Uint32Array) {
|
constructor(arg: number | any[] | Uint32Array) {
|
||||||
// TODO: more array types
|
// TODO: more array types
|
||||||
if (typeof arg === 'number') {
|
if (typeof arg === 'number') {
|
||||||
|
checkCount(arg);
|
||||||
this.colors = new Uint32Array(arg);
|
this.colors = new Uint32Array(arg);
|
||||||
} else if (arg instanceof Uint32Array) {
|
} else if (arg instanceof Uint32Array) {
|
||||||
this.colors = new Uint32Array(arg);
|
this.colors = new Uint32Array(arg);
|
||||||
@@ -42,9 +49,10 @@ export function rgba(r: number, g: number, b: number, a: number) : number;
|
|||||||
export function rgba(obj: ColorSource, g?: number, b?: number, a?: number) : number {
|
export function rgba(obj: ColorSource, g?: number, b?: number, a?: number) : number {
|
||||||
if (typeof obj === 'number') {
|
if (typeof obj === 'number') {
|
||||||
let r = obj;
|
let r = obj;
|
||||||
if (g != null && b != null)
|
if (typeof g === 'number' && typeof b === 'number')
|
||||||
return ((r & 0xff) << 0) | ((g & 0xff) << 8) | ((b & 0xff) << 16) | ((a & 0xff) << 24);
|
return ((r & 0xff) << 0) | ((g & 0xff) << 8) | ((b & 0xff) << 16) | ((a & 0xff) << 24);
|
||||||
return obj;
|
else
|
||||||
|
return obj;
|
||||||
}
|
}
|
||||||
if (typeof obj !== 'string' && isArray(obj) && typeof obj[0] === 'number') {
|
if (typeof obj !== 'string' && isArray(obj) && typeof obj[0] === 'number') {
|
||||||
let arr = obj;
|
let arr = obj;
|
||||||
@@ -75,6 +83,7 @@ type ColorGenFunc = (index: number) => number;
|
|||||||
|
|
||||||
export namespace palette {
|
export namespace palette {
|
||||||
export function from(obj: number | any[] | Uint32Array | ColorGenFunc, count?: number) {
|
export function from(obj: number | any[] | Uint32Array | ColorGenFunc, count?: number) {
|
||||||
|
checkCount(count);
|
||||||
if (typeof obj === 'function') {
|
if (typeof obj === 'function') {
|
||||||
if (!count) throw new Error(`You must also pass the number of colors to generate.`)
|
if (!count) throw new Error(`You must also pass the number of colors to generate.`)
|
||||||
var pal = new Palette(count);
|
var pal = new Palette(count);
|
||||||
@@ -87,7 +96,7 @@ export namespace palette {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function mono() {
|
export function mono() {
|
||||||
return greys(1);
|
return greys(2);
|
||||||
}
|
}
|
||||||
function rgb2() {
|
function rgb2() {
|
||||||
return new Palette([
|
return new Palette([
|
||||||
@@ -124,6 +133,7 @@ export namespace palette {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function helix(count: number) {
|
export function helix(count: number) {
|
||||||
|
checkCount(count);
|
||||||
return new Palette(chroma.cubehelix().scale().colors(count));
|
return new Palette(chroma.cubehelix().scale().colors(count));
|
||||||
}
|
}
|
||||||
export function factors(count: number, mult?: number) {
|
export function factors(count: number, mult?: number) {
|
||||||
|
@@ -76,7 +76,7 @@ export function fetchurl(url: string, type?: 'binary' | 'text'): FileData {
|
|||||||
|
|
||||||
export function readnocache(url: string, type?: 'binary' | 'text'): FileData {
|
export function readnocache(url: string, type?: 'binary' | 'text'): FileData {
|
||||||
if (url.startsWith('http:') || url.startsWith('https:')) {
|
if (url.startsWith('http:') || url.startsWith('https:')) {
|
||||||
return fetchurl(url);
|
return fetchurl(url, type);
|
||||||
}
|
}
|
||||||
if ($$store) {
|
if ($$store) {
|
||||||
return $$store.getFileData(url);
|
return $$store.getFileData(url);
|
||||||
@@ -93,6 +93,8 @@ export function read(url: string, type?: 'binary' | 'text'): FileData {
|
|||||||
}
|
}
|
||||||
let data = readnocache(url, type);
|
let data = readnocache(url, type);
|
||||||
if (data == null) throw new Error(`Cannot find resource "${url}"`);
|
if (data == null) throw new Error(`Cannot find resource "${url}"`);
|
||||||
|
if (type === 'text' && typeof data !== 'string') throw new Error(`Resource "${url}" is not a string`);
|
||||||
|
if (type === 'binary' && !(data instanceof Uint8Array)) throw new Error(`Resource "${url}" is not a binary file`);
|
||||||
$$cache.set(cachekey, data);
|
$$cache.set(cachekey, data);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,8 @@ export class ScriptUISliderType {
|
|||||||
value: number;
|
value: number;
|
||||||
constructor(
|
constructor(
|
||||||
readonly min: number,
|
readonly min: number,
|
||||||
readonly max: number
|
readonly max: number,
|
||||||
|
readonly step: number
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,6 +26,6 @@ export class ScriptUISlider extends ScriptUISliderType implements io.Loadable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function slider(min: number, max: number) {
|
export function slider(min: number, max: number, step?: number) {
|
||||||
return new ScriptUISlider(min, max);
|
return new ScriptUISlider(min, max, step || 1);
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
import { BitmapType, IndexedBitmap, RGBABitmap } from "../lib/bitmap";
|
import { BitmapType, IndexedBitmap, RGBABitmap } from "../lib/bitmap";
|
||||||
import { Component, render, h, ComponentType } from 'preact';
|
import { Component, render, h, ComponentType } from 'preact';
|
||||||
import { Cell } from "../env";
|
import { Cell } from "../env";
|
||||||
import { hex, rgb2bgr, RGBA } from "../../util";
|
import { findIntegerFactors, hex, rgb2bgr, RGBA } from "../../util";
|
||||||
import { dumpRAM } from "../../emu";
|
import { dumpRAM } from "../../emu";
|
||||||
// TODO: can't call methods from this end
|
// TODO: can't call methods from this end
|
||||||
import { Palette } from "../lib/color";
|
import { Palette } from "../lib/color";
|
||||||
@@ -10,6 +10,30 @@ import { ScriptUISlider, ScriptUISliderType } from "../lib/scriptui";
|
|||||||
import { current_project } from "../../../ide/ui";
|
import { current_project } from "../../../ide/ui";
|
||||||
|
|
||||||
const MAX_STRING_LEN = 100;
|
const MAX_STRING_LEN = 100;
|
||||||
|
const DEFAULT_ASPECT = 1;
|
||||||
|
|
||||||
|
interface ObjectStats {
|
||||||
|
type: 'prim' | 'complex' | 'bitmap'
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
units: 'em' | 'px'
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
class ObjectAnalyzer {
|
||||||
|
recurse(obj: any) : ObjectStats {
|
||||||
|
if (typeof obj === 'string') {
|
||||||
|
return { type: 'prim', width: obj.length, height: 1, units: 'em' }
|
||||||
|
} else if (obj instanceof Uint8Array) {
|
||||||
|
return { type: 'complex', width: 60, height: Math.ceil(obj.length / 16), units: 'em' }
|
||||||
|
} else if (typeof obj === 'object') {
|
||||||
|
let stats : ObjectStats = { type: 'complex', width: 0, height: 0, units: 'em'};
|
||||||
|
return stats; // TODO
|
||||||
|
} else {
|
||||||
|
return { type: 'prim', width: 12, height: 1, units: 'em' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface ColorComponentProps {
|
interface ColorComponentProps {
|
||||||
rgbavalue: number;
|
rgbavalue: number;
|
||||||
@@ -18,13 +42,17 @@ interface ColorComponentProps {
|
|||||||
class ColorComponent extends Component<ColorComponentProps> {
|
class ColorComponent extends Component<ColorComponentProps> {
|
||||||
render(virtualDom, containerNode, replaceNode) {
|
render(virtualDom, containerNode, replaceNode) {
|
||||||
let rgb = this.props.rgbavalue & 0xffffff;
|
let rgb = this.props.rgbavalue & 0xffffff;
|
||||||
var htmlcolor = `#${hex(rgb2bgr(rgb),6)}`;
|
let bgr = rgb2bgr(rgb);
|
||||||
var textcol = (rgb & 0x008000) ? 'black' : 'white';
|
var htmlcolor = `#${hex(bgr,6)}`;
|
||||||
|
var textcolor = (rgb & 0x008000) ? 'black' : 'white';
|
||||||
|
var printcolor = hex(rgb & 0xffffff, 6); // TODO: show index instead?
|
||||||
return h('div', {
|
return h('div', {
|
||||||
class: 'scripting-color',
|
class: 'scripting-color',
|
||||||
style: `background-color: ${htmlcolor}; color: ${textcol}`,
|
style: `background-color: ${htmlcolor}; color: ${textcolor}`,
|
||||||
alt: htmlcolor, // TODO
|
alt: htmlcolor, // TODO
|
||||||
}, '\u00a0');
|
}, [
|
||||||
|
h('span', { }, printcolor )
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +73,6 @@ class BitmapComponent extends Component<BitmapComponentProps> {
|
|||||||
}
|
}
|
||||||
render(virtualDom, containerNode, replaceNode) {
|
render(virtualDom, containerNode, replaceNode) {
|
||||||
return h('canvas', {
|
return h('canvas', {
|
||||||
class: 'pixelated',
|
|
||||||
width: this.props.width,
|
width: this.props.width,
|
||||||
height: this.props.height,
|
height: this.props.height,
|
||||||
...this.props
|
...this.props
|
||||||
@@ -124,7 +151,7 @@ class BitmapEditor extends Component<BitmapComponentProps, BitmapEditorState> {
|
|||||||
}
|
}
|
||||||
return h('div', {
|
return h('div', {
|
||||||
tabIndex: 0,
|
tabIndex: 0,
|
||||||
class: this.state.isEditing ? 'scripting-cell' : '' // TODO
|
class: this.state.isEditing ? 'scripting-editor' : '' // TODO
|
||||||
}, [
|
}, [
|
||||||
bitmapRender,
|
bitmapRender,
|
||||||
okCancel,
|
okCancel,
|
||||||
@@ -163,6 +190,8 @@ class ObjectKeyValueComponent extends Component<ObjectTreeComponentProps, Object
|
|||||||
let hdrclass = '';
|
let hdrclass = '';
|
||||||
if (expandable)
|
if (expandable)
|
||||||
hdrclass = this.state.expanded ? 'tree-expanded' : 'tree-collapsed'
|
hdrclass = this.state.expanded ? 'tree-expanded' : 'tree-collapsed'
|
||||||
|
let propName = this.props.name || null;
|
||||||
|
if (propName.startsWith("$$")) propName = null;
|
||||||
return h('div', {
|
return h('div', {
|
||||||
class: 'tree-content',
|
class: 'tree-content',
|
||||||
key: `${this.props.objpath}__tree`
|
key: `${this.props.objpath}__tree`
|
||||||
@@ -171,10 +200,8 @@ class ObjectKeyValueComponent extends Component<ObjectTreeComponentProps, Object
|
|||||||
class: 'tree-header ' + hdrclass,
|
class: 'tree-header ' + hdrclass,
|
||||||
onClick: expandable ? () => this.toggleExpand() : null
|
onClick: expandable ? () => this.toggleExpand() : null
|
||||||
}, [
|
}, [
|
||||||
this.props.name + "",
|
h('span', { class: 'tree-key' }, [ propName, expandable ]),
|
||||||
h('span', { class: 'tree-value' }, [
|
h('span', { class: 'tree-value' }, [ getShortName(this.props.object) ])
|
||||||
getShortName(this.props.object)
|
|
||||||
])
|
|
||||||
]),
|
]),
|
||||||
this.state.expanded ? objectToContentsDiv(this.props.object, this.props.objpath) : null
|
this.state.expanded ? objectToContentsDiv(this.props.object, this.props.objpath) : null
|
||||||
]);
|
]);
|
||||||
@@ -243,26 +270,25 @@ function objectToDiv(object: any, name: string, objpath: string) {
|
|||||||
if (object == null) {
|
if (object == null) {
|
||||||
return object + "";
|
return object + "";
|
||||||
} else if (object['uitype']) {
|
} else if (object['uitype']) {
|
||||||
children.push(h(UISliderComponent, { iokey: objpath, uiobject: object }));
|
return h(UISliderComponent, { iokey: objpath, uiobject: object });
|
||||||
} else if (object['literaltext']) {
|
} else if (object['literaltext']) {
|
||||||
children.push(h("pre", { }, [ object['literaltext'] ])); // TODO
|
return h("pre", { }, [ object['literaltext'] ]);
|
||||||
} else if (isIndexedBitmap(object)) {
|
} else if (isIndexedBitmap(object) || isRGBABitmap(object)) {
|
||||||
//Object.setPrototypeOf(object, IndexedBitmap.prototype); // TODO: use Object.create()?
|
return h(BitmapEditor, { bitmap: object, width: object.width, height: object.height });
|
||||||
addBitmapComponent(children, object);
|
|
||||||
} else if (isRGBABitmap(object)) {
|
|
||||||
//Object.setPrototypeOf(object, RGBABitmap.prototype); // TODO: use Object.create()?
|
|
||||||
addBitmapComponent(children, object);
|
|
||||||
} else if (isPalette(object)) {
|
} else if (isPalette(object)) {
|
||||||
// TODO: make sets of 2/4/8/16/etc
|
if (object.colors.length <= 64) {
|
||||||
props.class += ' scripting-grid ';
|
props.class += ' scripting-flex ';
|
||||||
object.colors.forEach((val) => {
|
object.colors.forEach((val) => {
|
||||||
children.push(h(ColorComponent, { rgbavalue: val }));
|
children.push(h(ColorComponent, { rgbavalue: val }));
|
||||||
})
|
})
|
||||||
|
return h('div', props, children);
|
||||||
|
} else {
|
||||||
|
let {a,b} = findIntegerFactors(object.colors.length, 1, 1, DEFAULT_ASPECT);
|
||||||
|
return objectToDiv({ rgba: object.colors, width: a, height: b }, name, objpath);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return h(ObjectKeyValueComponent, { name, object, objpath }, []);
|
return h(ObjectKeyValueComponent, { name, object, objpath }, []);
|
||||||
}
|
}
|
||||||
let div = h('div', props, children);
|
|
||||||
return div;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fixedArrayToDiv(tyarr: Array<number>, bpel: number, objpath: string) {
|
function fixedArrayToDiv(tyarr: Array<number>, bpel: number, objpath: string) {
|
||||||
@@ -288,11 +314,7 @@ function objectToContentsDiv(object: {} | [], objpath: string) {
|
|||||||
}
|
}
|
||||||
let objectEntries = Object.entries(object);
|
let objectEntries = Object.entries(object);
|
||||||
let objectDivs = objectEntries.map(entry => objectToDiv(entry[1], entry[0], `${objpath}.${entry[1]}`));
|
let objectDivs = objectEntries.map(entry => objectToDiv(entry[1], entry[0], `${objpath}.${entry[1]}`));
|
||||||
return h('div', {}, objectDivs);
|
return h('div', { class: 'scripting-flex' }, objectDivs);
|
||||||
}
|
|
||||||
|
|
||||||
function addBitmapComponent(children, bitmap: BitmapType) {
|
|
||||||
children.push(h(BitmapEditor, { bitmap: bitmap, width: bitmap.width, height: bitmap.height }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UISliderComponentProps {
|
interface UISliderComponentProps {
|
||||||
@@ -307,17 +329,16 @@ class UISliderComponent extends Component<UISliderComponentProps> {
|
|||||||
this.props.iokey,
|
this.props.iokey,
|
||||||
h('input', {
|
h('input', {
|
||||||
type: 'range',
|
type: 'range',
|
||||||
min: slider.min,
|
min: slider.min / slider.step,
|
||||||
max: slider.max,
|
max: slider.max / slider.step,
|
||||||
value: this.props.uiobject.value,
|
value: this.props.uiobject.value / slider.step,
|
||||||
onInput: (ev) => {
|
onInput: (ev) => {
|
||||||
let newValue = { value: ev.target.value };
|
let newValue = { value: parseFloat(ev.target.value) * slider.step };
|
||||||
slider.value = parseFloat(ev.target.value);
|
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
current_project.updateDataItems([{key: this.props.iokey, value: newValue}]);
|
current_project.updateDataItems([{key: this.props.iokey, value: newValue}]);
|
||||||
}
|
}
|
||||||
}, []),
|
}, []),
|
||||||
slider.value
|
getShortName(slider.value)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -328,22 +349,15 @@ export class Notebook {
|
|||||||
public readonly maindiv: HTMLElement
|
public readonly maindiv: HTMLElement
|
||||||
) {
|
) {
|
||||||
maindiv.classList.add('vertical-scroll');
|
maindiv.classList.add('vertical-scroll');
|
||||||
//maindiv.classList.add('container')
|
|
||||||
}
|
}
|
||||||
updateCells(cells: Cell[]) {
|
updateCells(cells: Cell[]) {
|
||||||
let hTree = cells.map(cell => {
|
let hTree = cells.map(cell => {
|
||||||
//return objectToDiv(cell.object, cell.id)
|
|
||||||
return h('div', {
|
return h('div', {
|
||||||
class: 'scripting-cell',
|
class: 'scripting-cell',
|
||||||
key: `${cell.id}__cell`
|
key: `${cell.id}__cell`
|
||||||
}, [
|
}, [
|
||||||
objectToDiv(cell.object, cell.id, cell.id)
|
objectToDiv(cell.object, cell.id, cell.id)
|
||||||
])
|
])
|
||||||
/*
|
|
||||||
let cellDiv = objectToDiv(cell.object, cell.id);
|
|
||||||
cellDiv.props['class'] += ' scripting-cell ';
|
|
||||||
return cellDiv;
|
|
||||||
*/
|
|
||||||
});
|
});
|
||||||
render(hTree, this.maindiv);
|
render(hTree, this.maindiv);
|
||||||
}
|
}
|
||||||
|
@@ -631,3 +631,34 @@ export function escapeHTML(s: string): string {
|
|||||||
return s.replace(/[&]/g, '&').replace(/[<]/g, '<').replace(/[>]/g, '>');
|
return s.replace(/[&]/g, '&').replace(/[<]/g, '<').replace(/[>]/g, '>');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lame factorization for displaying bitmaps
|
||||||
|
// returns a > b such that a * b == x (or higher), a >= mina, b >= minb
|
||||||
|
export function findIntegerFactors(x: number, mina: number, minb: number, aspect: number) : {a: number, b: number} {
|
||||||
|
let a = x;
|
||||||
|
let b = 1;
|
||||||
|
if (minb > 1 && minb < a) {
|
||||||
|
a = Math.ceil(x / minb);
|
||||||
|
b = minb;
|
||||||
|
}
|
||||||
|
while (a > b) {
|
||||||
|
let a2 = a;
|
||||||
|
let b2 = b;
|
||||||
|
if ((a & 1) == 0) {
|
||||||
|
b2 = b * 2;
|
||||||
|
a2 = a / 2;
|
||||||
|
}
|
||||||
|
if ((a % 3) == 0) {
|
||||||
|
b2 = b * 3;
|
||||||
|
a2 = a / 3;
|
||||||
|
}
|
||||||
|
if ((a % 5) == 0) {
|
||||||
|
b2 = b * 5;
|
||||||
|
a2 = a / 5;
|
||||||
|
}
|
||||||
|
if (a2 < mina) break;
|
||||||
|
if (a2 < b2 * aspect) break;
|
||||||
|
a = a2;
|
||||||
|
b = b2;
|
||||||
|
}
|
||||||
|
return {a, b};
|
||||||
|
}
|
||||||
|
@@ -70,7 +70,7 @@ class VCSPlatform extends BasePlatform {
|
|||||||
async start() {
|
async start() {
|
||||||
var self : VCSPlatform = this;
|
var self : VCSPlatform = this;
|
||||||
// load Javatari and configure settings
|
// load Javatari and configure settings
|
||||||
await loadScript("javatari.js/release/javatari/javatari.js");
|
await loadScript("javatari/javatari.js");
|
||||||
Javatari.AUTO_START = false;
|
Javatari.AUTO_START = false;
|
||||||
Javatari.SHOW_ERRORS = false;
|
Javatari.SHOW_ERRORS = false;
|
||||||
Javatari.CARTRIDGE_CHANGE_DISABLED = true;
|
Javatari.CARTRIDGE_CHANGE_DISABLED = true;
|
||||||
|
@@ -27,6 +27,7 @@ export async function runJavascript(step: BuildStep): Promise<BuildStepResult> {
|
|||||||
let output : RunResult = { cells, state };
|
let output : RunResult = { cells, state };
|
||||||
return { output: output };
|
return { output: output };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
return { errors: env.extractErrors(e) };
|
return { errors: env.extractErrors(e) };
|
||||||
} finally {
|
} finally {
|
||||||
io.$$setupFS(null);
|
io.$$setupFS(null);
|
||||||
|
Reference in New Issue
Block a user