mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2024-05-31 13:41:32 +00:00
scripting: started on interact(), ui.select, chromas
This commit is contained in:
parent
6cee4e26e4
commit
8fc94aad25
25
css/ui.css
25
css/ui.css
|
@ -169,10 +169,6 @@ div.mem_info a.selected {
|
||||||
.btn_label {
|
.btn_label {
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
}
|
}
|
||||||
.btn_group.debug_group {
|
|
||||||
}
|
|
||||||
.btn_group.view_group {
|
|
||||||
}
|
|
||||||
.btn_active {
|
.btn_active {
|
||||||
color: #33cc33 !important;
|
color: #33cc33 !important;
|
||||||
}
|
}
|
||||||
|
@ -343,8 +339,6 @@ canvas.pixelated {
|
||||||
a.toolbarMenuButton {
|
a.toolbarMenuButton {
|
||||||
padding:0.3em;
|
padding:0.3em;
|
||||||
}
|
}
|
||||||
a.dropdown-toggle {
|
|
||||||
}
|
|
||||||
a.dropdown-toggle:hover {
|
a.dropdown-toggle:hover {
|
||||||
color:#ffffff;
|
color:#ffffff;
|
||||||
text-shadow: 0px 0px 2px black;
|
text-shadow: 0px 0px 2px black;
|
||||||
|
@ -767,8 +761,6 @@ div.asset_toolbar {
|
||||||
.tree-level-9 { background-color: #7b8363;}
|
.tree-level-9 { background-color: #7b8363;}
|
||||||
.tree-level-10 { background-color: #738363;}
|
.tree-level-10 { background-color: #738363;}
|
||||||
|
|
||||||
.waverow {
|
|
||||||
}
|
|
||||||
.waverow.editable:hover {
|
.waverow.editable:hover {
|
||||||
background-color: #336633;
|
background-color: #336633;
|
||||||
}
|
}
|
||||||
|
@ -794,7 +786,7 @@ div.asset_toolbar {
|
||||||
image-rendering: crisp-edges;
|
image-rendering: crisp-edges;
|
||||||
}
|
}
|
||||||
.scripting-cell canvas:hover {
|
.scripting-cell canvas:hover {
|
||||||
border-color:#ccc;
|
border-color:#aaa;
|
||||||
}
|
}
|
||||||
.scripting-flex canvas {
|
.scripting-flex canvas {
|
||||||
min-height: 2vw;
|
min-height: 2vw;
|
||||||
|
@ -809,6 +801,8 @@ div.scripting-editor {
|
||||||
}
|
}
|
||||||
div.scripting-color {
|
div.scripting-color {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
|
min-width: 3em;
|
||||||
|
min-height: 3em;
|
||||||
}
|
}
|
||||||
div.scripting-color span {
|
div.scripting-color span {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
@ -825,3 +819,16 @@ div.scripting-flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
div.scripting-select > div {
|
||||||
|
border-radius: 2px;
|
||||||
|
border-style: dotted;
|
||||||
|
border-color: transparent;
|
||||||
|
|
||||||
|
}
|
||||||
|
div.scripting-select > div:hover {
|
||||||
|
border-color: #eee;
|
||||||
|
}
|
||||||
|
div.scripting-select > .scripting-selected {
|
||||||
|
border-style: solid;
|
||||||
|
border-color: #eee;
|
||||||
|
}
|
||||||
|
|
|
@ -75,7 +75,7 @@ export class Environment {
|
||||||
}
|
}
|
||||||
error(varname: string, msg: string) {
|
error(varname: string, msg: string) {
|
||||||
let obj = this.declvars && this.declvars[varname];
|
let obj = this.declvars && this.declvars[varname];
|
||||||
console.log(varname, obj);
|
console.log('ERROR', varname, obj, this);
|
||||||
throw new RuntimeError(obj && obj.loc, msg);
|
throw new RuntimeError(obj && obj.loc, msg);
|
||||||
}
|
}
|
||||||
print(args: any[]) {
|
print(args: any[]) {
|
||||||
|
@ -108,10 +108,18 @@ export class Environment {
|
||||||
return parent() && parent().type === 'ExpressionStatement' && parent(2) && parent(2).type === 'Program';
|
return parent() && parent().type === 'ExpressionStatement' && parent(2) && parent(2).type === 'Program';
|
||||||
}
|
}
|
||||||
const convertTopToPrint = () => {
|
const convertTopToPrint = () => {
|
||||||
if (isTopLevel()) update(`print(${source()});`)
|
if (isTopLevel()) {
|
||||||
|
let printkey = `$$print__${this.seq++}`;
|
||||||
|
update(`this.${printkey} = io.data.load(${source()}, ${JSON.stringify(printkey)})`);
|
||||||
|
//update(`print(${source()});`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const left = node['left'];
|
const left = node['left'];
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
|
// add preamble, postamble
|
||||||
|
case 'Program':
|
||||||
|
update(`${this.preamble}${source()}${this.postamble}`)
|
||||||
|
break;
|
||||||
// error on forbidden keywords
|
// error on forbidden keywords
|
||||||
case 'Identifier':
|
case 'Identifier':
|
||||||
if (GLOBAL_BADLIST.indexOf(source()) >= 0) {
|
if (GLOBAL_BADLIST.indexOf(source()) >= 0) {
|
||||||
|
@ -125,7 +133,7 @@ export class Environment {
|
||||||
if (isTopLevel()) {
|
if (isTopLevel()) {
|
||||||
if (left && left.type === 'Identifier') {
|
if (left && left.type === 'Identifier') {
|
||||||
if (!this.declvars[left.name]) {
|
if (!this.declvars[left.name]) {
|
||||||
update(`var ${left.name}=io.data.get(this.${source()}, ${JSON.stringify(left.name)})`)
|
update(`var ${left.name}=io.data.load(this.${source()}, ${JSON.stringify(left.name)})`)
|
||||||
this.declvars[left.name] = left;
|
this.declvars[left.name] = left;
|
||||||
} else {
|
} else {
|
||||||
update(`${left.name}=this.${source()}`)
|
update(`${left.name}=this.${source()}`)
|
||||||
|
@ -157,7 +165,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, this.builtins);
|
const fn = new AsyncFunction('$$', code).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(), []);
|
||||||
}
|
}
|
||||||
|
@ -171,8 +179,9 @@ export class Environment {
|
||||||
if (o.BYTES_PER_ELEMENT > 0) return; // typed array, don't bother
|
if (o.BYTES_PER_ELEMENT > 0) return; // typed array, don't bother
|
||||||
checked.add(o); // so we don't recurse if cycle
|
checked.add(o); // so we don't recurse if cycle
|
||||||
function prkey() { return fullkey.join('.') }
|
function prkey() { return fullkey.join('.') }
|
||||||
|
// go through all object properties recursively
|
||||||
for (var [key, value] of Object.entries(o)) {
|
for (var [key, value] of Object.entries(o)) {
|
||||||
if (value == null && fullkey.length == 0) {
|
if (value == null && fullkey.length == 0 && !key.startsWith("$$")) {
|
||||||
this.error(key, `"${key}" has no value.`)
|
this.error(key, `"${key}" has no value.`)
|
||||||
}
|
}
|
||||||
fullkey.push(key);
|
fullkey.push(key);
|
||||||
|
@ -180,7 +189,7 @@ export class Environment {
|
||||||
if (fullkey.length == 1)
|
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)
|
this.error(fullkey[0], `"${prkey()}" is a function. Did you forget to pass parameters?`); // TODO? did you mean (needs to see entire expr)
|
||||||
else
|
else
|
||||||
this.error(fullkey[0], `This expression may be incomplete.`); // TODO? did you mean (needs to see entire expr)
|
this.error(fullkey[0], `This expression may be incomplete, or it contains a function object: ${prkey()}`); // TODO? did you mean (needs to see entire expr)
|
||||||
}
|
}
|
||||||
if (typeof value === 'symbol') {
|
if (typeof value === 'symbol') {
|
||||||
this.error(fullkey[0], `"${prkey()}" is a Symbol, and can't be used.`) // TODO?
|
this.error(fullkey[0], `"${prkey()}" is a Symbol, and can't be used.`) // TODO?
|
||||||
|
@ -231,11 +240,12 @@ export class Environment {
|
||||||
while (frame >= 0) {
|
while (frame >= 0) {
|
||||||
console.log(frames[frame]);
|
console.log(frames[frame]);
|
||||||
if (frames[frame].fileName.endsWith('Function')) {
|
if (frames[frame].fileName.endsWith('Function')) {
|
||||||
|
// TODO: use source map
|
||||||
errors.push( {
|
errors.push( {
|
||||||
path: this.path,
|
path: this.path,
|
||||||
msg: e.message,
|
msg: e.message,
|
||||||
line: frames[frame].lineNumber - LINE_NUMBER_OFFSET,
|
line: frames[frame].lineNumber - LINE_NUMBER_OFFSET,
|
||||||
start: frames[frame].columnNumber,
|
//start: frames[frame].columnNumber,
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
--frame;
|
--frame;
|
||||||
|
@ -252,6 +262,7 @@ export class Environment {
|
||||||
getLoadableState() {
|
getLoadableState() {
|
||||||
let updated = null;
|
let updated = null;
|
||||||
for (let [key, value] of Object.entries(this.declvars)) {
|
for (let [key, value] of Object.entries(this.declvars)) {
|
||||||
|
// TODO: use Loadable
|
||||||
if (typeof value['$$getstate'] === 'function') {
|
if (typeof value['$$getstate'] === 'function') {
|
||||||
let loadable = <any>value as io.Loadable;
|
let loadable = <any>value as io.Loadable;
|
||||||
if (updated == null) updated = {};
|
if (updated == null) updated = {};
|
||||||
|
|
|
@ -47,14 +47,18 @@ 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,
|
blit(src: BitmapType,
|
||||||
destx: number, desty: number,
|
destx: number, desty: number,
|
||||||
srcx: number, srcy: number)
|
srcx: number, srcy: number)
|
||||||
{
|
{
|
||||||
|
destx |= 0;
|
||||||
|
desty |= 0;
|
||||||
|
srcx |= 0;
|
||||||
|
srcy |= 0;
|
||||||
for (var y=0; y<src.height; y++) {
|
for (var y=0; y<src.height; y++) {
|
||||||
for (var x=0; x<src.width; x++) {
|
for (var x=0; x<src.width; x++) {
|
||||||
let rgba = src.getrgba(x+srcx, y+srcy);
|
let rgba = src.getrgba(x+srcx, y+srcy);
|
||||||
dest.set(x+destx, y+desty, rgba);
|
this.set(x+destx, y+desty, rgba);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -210,7 +214,7 @@ export function montage(bitmaps: BitmapType[], options?: MontageOptions) {
|
||||||
let x = 0;
|
let x = 0;
|
||||||
let y = 0;
|
let y = 0;
|
||||||
bitmaps.forEach((bmp) => {
|
bitmaps.forEach((bmp) => {
|
||||||
result.blit(bmp, result, x, y, 0, 0);
|
result.blit(bmp, x, y, 0, 0);
|
||||||
hitrects.push({x, y, w: bmp.width, h: bmp.height })
|
hitrects.push({x, y, w: bmp.width, h: bmp.height })
|
||||||
x += bmp.width + gap;
|
x += bmp.width + gap;
|
||||||
if (x >= result.width) {
|
if (x >= result.width) {
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
|
|
||||||
import _chroma from 'chroma-js'
|
import _chroma from 'chroma-js'
|
||||||
import { isArray } from '../../util';
|
import { isArray, rgb2bgr } from '../../util';
|
||||||
|
|
||||||
export type ColorSource = number | [number,number,number] | [number,number,number,number] | string;
|
export type Chroma = { _rgb: [number,number,number,number] };
|
||||||
|
|
||||||
|
export type ColorSource = number | [number,number,number] | [number,number,number,number] | string | Chroma;
|
||||||
|
|
||||||
function checkCount(count) {
|
function checkCount(count) {
|
||||||
if (count < 0 || count > 65536) {
|
if (count < 0 || count > 65536) {
|
||||||
|
@ -10,6 +12,14 @@ function checkCount(count) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isPalette(object): object is Palette {
|
||||||
|
return object['colors'] instanceof Uint32Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isChroma(object): object is Chroma {
|
||||||
|
return object['_rgb'] instanceof Array;
|
||||||
|
}
|
||||||
|
|
||||||
export class Palette {
|
export class Palette {
|
||||||
readonly colors: Uint32Array;
|
readonly colors: Uint32Array;
|
||||||
|
|
||||||
|
@ -28,12 +38,18 @@ export class Palette {
|
||||||
get(index: number) {
|
get(index: number) {
|
||||||
return this.colors[index];
|
return this.colors[index];
|
||||||
}
|
}
|
||||||
|
chromas() {
|
||||||
|
return Array.from(this.colors).map((rgba) => from(rgba & 0xffffff));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const chroma = _chroma;
|
export const chroma = _chroma;
|
||||||
|
|
||||||
export function from(obj: ColorSource) {
|
export function from(obj: ColorSource) {
|
||||||
return _chroma(obj as any);
|
if (typeof obj === 'number')
|
||||||
|
return _chroma(rgb2bgr(obj & 0xffffff));
|
||||||
|
else
|
||||||
|
return _chroma(obj as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rgb(obj: ColorSource) : number;
|
export function rgb(obj: ColorSource) : number;
|
||||||
|
@ -47,6 +63,9 @@ export function rgba(obj: ColorSource) : number;
|
||||||
export function rgba(r: number, g: number, b: number, a: number) : 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 {
|
export function rgba(obj: ColorSource, g?: number, b?: number, a?: number) : number {
|
||||||
|
if (isChroma(obj)) {
|
||||||
|
return rgba(obj._rgb[0], obj._rgb[1], obj._rgb[2], obj._rgb[3]);
|
||||||
|
}
|
||||||
if (typeof obj === 'number') {
|
if (typeof obj === 'number') {
|
||||||
let r = obj;
|
let r = obj;
|
||||||
if (typeof g === 'number' && typeof b === 'number')
|
if (typeof g === 'number' && typeof b === 'number')
|
||||||
|
|
|
@ -7,6 +7,10 @@ var $$cache: WeakMap<object,FileData> = new WeakMap();
|
||||||
var $$store: WorkingStore;
|
var $$store: WorkingStore;
|
||||||
// backing store for data
|
// backing store for data
|
||||||
var $$data: {} = {};
|
var $$data: {} = {};
|
||||||
|
// events
|
||||||
|
var $$seq = 0;
|
||||||
|
// if an event is specified, it goes here
|
||||||
|
export const EVENT_KEY = "$$event";
|
||||||
|
|
||||||
export function $$setupFS(store: WorkingStore) {
|
export function $$setupFS(store: WorkingStore) {
|
||||||
$$store = store;
|
$$store = store;
|
||||||
|
@ -20,18 +24,28 @@ export function $$loadData(data: {}) {
|
||||||
|
|
||||||
// object that can load state from backing store
|
// object that can load state from backing store
|
||||||
export interface Loadable {
|
export interface Loadable {
|
||||||
reset() : void;
|
// called during script, from io.data.load()
|
||||||
|
$$reset() : void;
|
||||||
|
$$setstate?(newstate: {}) : void;
|
||||||
|
// called after script, from io.data.save()
|
||||||
$$getstate() : {};
|
$$getstate() : {};
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace data {
|
export namespace data {
|
||||||
export function get(object: Loadable, key: string): Loadable {
|
export function load(object: Loadable, key: string): Loadable {
|
||||||
|
if (object == null) return object;
|
||||||
let override = $$data && $$data[key];
|
let override = $$data && $$data[key];
|
||||||
if (override) Object.assign(object, override);
|
if (override && object.$$setstate) {
|
||||||
else if (object.reset) object.reset();
|
object.$$setstate(override);
|
||||||
|
} else if (override) {
|
||||||
|
Object.assign(object, override);
|
||||||
|
} else if (object.$$reset) {
|
||||||
|
object.$$reset();
|
||||||
|
data.save(object, key);
|
||||||
|
}
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
export function set(object: Loadable, key: string): Loadable {
|
export function save(object: Loadable, key: string): Loadable {
|
||||||
if ($$data && object.$$getstate) {
|
if ($$data && object.$$getstate) {
|
||||||
$$data[key] = object.$$getstate();
|
$$data[key] = object.$$getstate();
|
||||||
}
|
}
|
||||||
|
@ -114,3 +128,80 @@ export function readlines(url: string) : string[] {
|
||||||
export function splitlines(text: string) : string[] {
|
export function splitlines(text: string) : string[] {
|
||||||
return text.split(/\n|\r\n/g);
|
return text.split(/\n|\r\n/g);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// an object that can become interactive, identified by ID
|
||||||
|
export interface Interactive {
|
||||||
|
$$interact: InteractionRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InteractEvent {
|
||||||
|
interactid : number;
|
||||||
|
type: string;
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
button?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// InteractionRecord maps a target object to an interaction ID
|
||||||
|
// the $$callback is used once per script eval, then gets nulled
|
||||||
|
// whether or not it's invoked
|
||||||
|
// event comes from $$data.$$event
|
||||||
|
export class InteractionRecord implements Loadable {
|
||||||
|
interactid : number;
|
||||||
|
lastevent : {} = null;
|
||||||
|
constructor(
|
||||||
|
public readonly interacttarget: Interactive,
|
||||||
|
private $$callback
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
$$reset() {
|
||||||
|
this.$$setstate({interactid: ++$$seq})
|
||||||
|
}
|
||||||
|
$$setstate(newstate: {interactid: number}) {
|
||||||
|
this.interactid = newstate.interactid;
|
||||||
|
this.interacttarget.$$interact = this;
|
||||||
|
let event : InteractEvent = $$data[EVENT_KEY];
|
||||||
|
if (event && event.interactid == this.interactid) {
|
||||||
|
if (this.$$callback) {
|
||||||
|
this.$$callback(event);
|
||||||
|
}
|
||||||
|
this.lastevent = event;
|
||||||
|
$$data[EVENT_KEY] = null;
|
||||||
|
}
|
||||||
|
this.$$callback = null;
|
||||||
|
}
|
||||||
|
$$getstate() {
|
||||||
|
//TODO: this isn't always cleared before we serialize (e.g. if exception or move element)
|
||||||
|
this.$$callback = null;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isInteractive(obj: object): obj is Interactive {
|
||||||
|
return !!((obj as Interactive).$$interact);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function interact(object: any, callback) : InteractionRecord {
|
||||||
|
// TODO: limit to Bitmap, etc
|
||||||
|
if (typeof object === 'object') {
|
||||||
|
return new InteractionRecord(object, callback);
|
||||||
|
}
|
||||||
|
throw new Error(`This object is not capable of interaction.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: what if this isn't top level?
|
||||||
|
export class Mutable<T> implements Loadable {
|
||||||
|
value : T;
|
||||||
|
constructor(public readonly initial : T) { }
|
||||||
|
$$reset() {
|
||||||
|
this.value = this.initial;
|
||||||
|
}
|
||||||
|
$$setstate(newstate) {
|
||||||
|
this.value = newstate.value;
|
||||||
|
}
|
||||||
|
$$getstate() {
|
||||||
|
return { value: this.value };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
|
|
||||||
import * as io from "./io";
|
import * as io from "./io";
|
||||||
|
|
||||||
export class ScriptUISliderType {
|
export interface ScriptUIType {
|
||||||
|
uitype : string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScriptUISliderType implements ScriptUIType {
|
||||||
readonly uitype = 'slider';
|
readonly uitype = 'slider';
|
||||||
value: number;
|
value: number;
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -18,7 +22,7 @@ export class ScriptUISlider extends ScriptUISliderType implements io.Loadable {
|
||||||
this.initvalue = value;
|
this.initvalue = value;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
reset() {
|
$$reset() {
|
||||||
this.value = this.initvalue != null ? this.initvalue : this.min;
|
this.value = this.initvalue != null ? this.initvalue : this.min;
|
||||||
}
|
}
|
||||||
$$getstate() {
|
$$getstate() {
|
||||||
|
@ -29,3 +33,34 @@ export class ScriptUISlider extends ScriptUISliderType implements io.Loadable {
|
||||||
export function slider(min: number, max: number, step?: number) {
|
export function slider(min: number, max: number, step?: number) {
|
||||||
return new ScriptUISlider(min, max, step || 1);
|
return new ScriptUISlider(min, max, step || 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
|
||||||
|
export class ScriptUISelectType<T> implements ScriptUIType {
|
||||||
|
readonly uitype = 'select';
|
||||||
|
value: T;
|
||||||
|
index: number = -1;
|
||||||
|
constructor(
|
||||||
|
readonly options: T[]
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScriptUISelect<T> extends ScriptUISelectType<T> implements io.Loadable {
|
||||||
|
initindex : number;
|
||||||
|
initial(index: number) {
|
||||||
|
this.initindex = index;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
$$reset() {
|
||||||
|
this.index = this.initindex >= 0 ? this.initindex : -1;
|
||||||
|
this.value = null;
|
||||||
|
}
|
||||||
|
$$getstate() {
|
||||||
|
return { value: this.value, index: this.index };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function select(options: any[]) {
|
||||||
|
return new ScriptUISelect(options);
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
|
|
||||||
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, createRef, VNode } from 'preact';
|
||||||
import { Cell } from "../env";
|
import { Cell } from "../env";
|
||||||
import { findIntegerFactors, hex, rgb2bgr, RGBA } from "../../util";
|
import { findIntegerFactors, hex, isArray, rgb2bgr } 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 * as color from "../lib/color";
|
||||||
import { ScriptUISlider, ScriptUISliderType } from "../lib/scriptui";
|
import { ScriptUISelectType, ScriptUISliderType, ScriptUIType } from "../lib/scriptui";
|
||||||
import { current_project } from "../../../ide/ui";
|
import { current_project } from "../../../ide/ui";
|
||||||
|
import { EVENT_KEY, InteractEvent, Interactive, isInteractive } from "../lib/io";
|
||||||
|
|
||||||
const MAX_STRING_LEN = 100;
|
const MAX_STRING_LEN = 100;
|
||||||
const DEFAULT_ASPECT = 1;
|
const DEFAULT_ASPECT = 1;
|
||||||
|
@ -44,18 +45,39 @@ class ColorComponent extends Component<ColorComponentProps> {
|
||||||
let rgb = this.props.rgbavalue & 0xffffff;
|
let rgb = this.props.rgbavalue & 0xffffff;
|
||||||
let bgr = rgb2bgr(rgb);
|
let bgr = rgb2bgr(rgb);
|
||||||
var htmlcolor = `#${hex(bgr,6)}`;
|
var htmlcolor = `#${hex(bgr,6)}`;
|
||||||
var textcolor = (rgb & 0x008000) ? 'black' : 'white';
|
var textcolor = (rgb & 0x008000) ? '#222' : '#ddd';
|
||||||
var printcolor = hex(rgb & 0xffffff, 6); // TODO: show index instead?
|
var printcolor = hex(rgb & 0xffffff, 6); // TODO: show index instead?
|
||||||
return h('div', {
|
return h('div', {
|
||||||
class: 'scripting-color',
|
class: 'scripting-item scripting-color',
|
||||||
style: `background-color: ${htmlcolor}; color: ${textcolor}`,
|
style: `background-color: ${htmlcolor}; color: ${textcolor}`,
|
||||||
alt: htmlcolor, // TODO
|
alt: htmlcolor, // TODO
|
||||||
}, [
|
}, [
|
||||||
h('span', { }, printcolor )
|
//h('span', { }, printcolor )
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendInteraction(iobj: Interactive, type: string, event: Event, xtraprops: {}) {
|
||||||
|
let irec = iobj.$$interact;
|
||||||
|
let ievent : InteractEvent = {interactid: irec.interactid, type, ...xtraprops};
|
||||||
|
if (event instanceof PointerEvent) {
|
||||||
|
const canvas = event.target as HTMLCanvasElement;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const scaleX = canvas.width / rect.width;
|
||||||
|
const scaleY = canvas.height / rect.height;
|
||||||
|
const x = (event.clientX - rect.left) * scaleX;
|
||||||
|
const y = (event.clientY - rect.top) * scaleY;
|
||||||
|
ievent.x = Math.round(x);
|
||||||
|
ievent.y = Math.round(y);
|
||||||
|
// TODO: pressure, etc.
|
||||||
|
}
|
||||||
|
// TODO: add events to queue?
|
||||||
|
current_project.updateDataItems([{
|
||||||
|
key: EVENT_KEY,
|
||||||
|
value: ievent
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
|
||||||
interface BitmapComponentProps {
|
interface BitmapComponentProps {
|
||||||
bitmap: BitmapType;
|
bitmap: BitmapType;
|
||||||
width: number;
|
width: number;
|
||||||
|
@ -63,20 +85,47 @@ interface BitmapComponentProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
class BitmapComponent extends Component<BitmapComponentProps> {
|
class BitmapComponent extends Component<BitmapComponentProps> {
|
||||||
|
ref = createRef(); // TODO: can we use the ref?
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
ctx: CanvasRenderingContext2D;
|
ctx: CanvasRenderingContext2D;
|
||||||
imageData: ImageData;
|
imageData: ImageData;
|
||||||
datau32: Uint32Array;
|
datau32: Uint32Array;
|
||||||
|
pressed = false;
|
||||||
|
|
||||||
constructor(props: BitmapComponentProps) {
|
constructor(props: BitmapComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
}
|
}
|
||||||
render(virtualDom, containerNode, replaceNode) {
|
render(virtualDom, containerNode, replaceNode) {
|
||||||
return h('canvas', {
|
let props = {
|
||||||
|
class: 'scripting-item',
|
||||||
|
ref: this.ref,
|
||||||
width: this.props.width,
|
width: this.props.width,
|
||||||
height: this.props.height,
|
height: this.props.height,
|
||||||
...this.props
|
}
|
||||||
});
|
let obj : any = this.props.bitmap;
|
||||||
|
if (isInteractive(obj)) {
|
||||||
|
return h('canvas', {
|
||||||
|
onPointerMove: (e: PointerEvent) => {
|
||||||
|
sendInteraction(obj, 'move', e, { pressed: this.pressed });
|
||||||
|
},
|
||||||
|
onPointerDown: (e: PointerEvent) => {
|
||||||
|
this.pressed = true;
|
||||||
|
this.canvas.setPointerCapture(e.pointerId);
|
||||||
|
sendInteraction(obj, 'down', e, { });
|
||||||
|
},
|
||||||
|
onPointerUp: (e: PointerEvent) => {
|
||||||
|
this.pressed = false;
|
||||||
|
sendInteraction(obj, 'up', e, { });
|
||||||
|
},
|
||||||
|
onPointerOut: (e: PointerEvent) => {
|
||||||
|
this.pressed = false;
|
||||||
|
sendInteraction(obj, 'out', e, { });
|
||||||
|
},
|
||||||
|
...props
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return h('canvas', props);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
|
@ -124,56 +173,6 @@ class BitmapComponent extends Component<BitmapComponentProps> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BitmapEditorState {
|
|
||||||
isEditing: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
class BitmapEditor extends Component<BitmapComponentProps, BitmapEditorState> {
|
|
||||||
render(virtualDom, containerNode, replaceNode) {
|
|
||||||
// TODO: focusable?
|
|
||||||
let bitmapProps = {
|
|
||||||
onClick: (event) => {
|
|
||||||
if (!this.state.isEditing) {
|
|
||||||
this.setState({ isEditing: true });
|
|
||||||
} else {
|
|
||||||
// TODO: process click
|
|
||||||
}
|
|
||||||
},
|
|
||||||
...this.props
|
|
||||||
}
|
|
||||||
let bitmapRender = h(BitmapComponent, bitmapProps, []);
|
|
||||||
let okCancel = null;
|
|
||||||
if (this.state.isEditing) {
|
|
||||||
okCancel = h('div', {}, [
|
|
||||||
button('Ok', () => this.commitChanges()),
|
|
||||||
button('Cancel', () => this.abandonChanges()),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
return h('div', {
|
|
||||||
tabIndex: 0,
|
|
||||||
class: this.state.isEditing ? 'scripting-editor' : '' // TODO
|
|
||||||
}, [
|
|
||||||
bitmapRender,
|
|
||||||
okCancel,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
commitChanges() {
|
|
||||||
this.cancelEditing();
|
|
||||||
}
|
|
||||||
abandonChanges() {
|
|
||||||
this.cancelEditing();
|
|
||||||
}
|
|
||||||
cancelEditing() {
|
|
||||||
this.setState({ isEditing: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function button(label: string, action: () => void) {
|
|
||||||
return h('button', {
|
|
||||||
onClick: action
|
|
||||||
}, [label]);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ObjectTreeComponentProps {
|
interface ObjectTreeComponentProps {
|
||||||
name?: string;
|
name?: string;
|
||||||
object: {} | [];
|
object: {} | [];
|
||||||
|
@ -191,7 +190,7 @@ class ObjectKeyValueComponent extends Component<ObjectTreeComponentProps, Object
|
||||||
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;
|
let propName = this.props.name || null;
|
||||||
if (propName.startsWith("$$")) propName = null;
|
if (propName && 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`
|
||||||
|
@ -201,7 +200,7 @@ class ObjectKeyValueComponent extends Component<ObjectTreeComponentProps, Object
|
||||||
onClick: expandable ? () => this.toggleExpand() : null
|
onClick: expandable ? () => this.toggleExpand() : null
|
||||||
}, [
|
}, [
|
||||||
h('span', { class: 'tree-key' }, [ propName, expandable ]),
|
h('span', { class: 'tree-key' }, [ propName, expandable ]),
|
||||||
h('span', { class: 'tree-value' }, [ getShortName(this.props.object) ])
|
h('span', { class: 'tree-value scripting-item' }, [ 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
|
||||||
]);
|
]);
|
||||||
|
@ -256,27 +255,39 @@ function isIndexedBitmap(object): object is IndexedBitmap {
|
||||||
function isRGBABitmap(object): object is RGBABitmap {
|
function isRGBABitmap(object): object is RGBABitmap {
|
||||||
return object['rgba'] instanceof Uint32Array;
|
return object['rgba'] instanceof Uint32Array;
|
||||||
}
|
}
|
||||||
function isPalette(object): object is Palette {
|
|
||||||
return object['colors'] instanceof Uint32Array;
|
function objectToChildren(object: any) : any[] {
|
||||||
|
if (color.isPalette(object)) {
|
||||||
|
return new color.Palette(object.colors).chromas();
|
||||||
|
} else if (isArray(object)) {
|
||||||
|
return Array.from(object);
|
||||||
|
} else if (object != null) {
|
||||||
|
return [ object ]
|
||||||
|
} else {
|
||||||
|
return [ ]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function objectToDiv(object: any, name: string, objpath: string) {
|
function objectToDiv(object: any, name: string, objpath: string): VNode<any> {
|
||||||
var props = { class: '', key: `${objpath}__obj` };
|
|
||||||
var children = [];
|
|
||||||
// TODO: tile editor
|
|
||||||
// TODO: limit # of items
|
// TODO: limit # of items
|
||||||
// TODO: detect table
|
// TODO: detect table
|
||||||
//return objectToContentsDiv(object);
|
|
||||||
if (object == null) {
|
if (object == null) {
|
||||||
return object + "";
|
return h('span', { }, object + "");
|
||||||
} else if (object['uitype']) {
|
} else if (object['uitype']) {
|
||||||
return h(UISliderComponent, { iokey: objpath, uiobject: object });
|
let cons = UI_COMPONENTS[object['uitype']];
|
||||||
|
if (!cons) throw new Error(`Unknown UI component type: ${object['uitype']}`);
|
||||||
|
return h(cons, { iokey: objpath, uiobject: object });
|
||||||
} else if (object['literaltext']) {
|
} else if (object['literaltext']) {
|
||||||
return h("pre", { }, [ object['literaltext'] ]);
|
return h("pre", { }, [ object['literaltext'] ]);
|
||||||
} else if (isIndexedBitmap(object) || isRGBABitmap(object)) {
|
} else if (isIndexedBitmap(object) || isRGBABitmap(object)) {
|
||||||
return h(BitmapEditor, { bitmap: object, width: object.width, height: object.height });
|
return h(BitmapComponent, { bitmap: object, width: object.width, height: object.height });
|
||||||
} else if (isPalette(object)) {
|
} else if (color.isChroma(object)) {
|
||||||
if (object.colors.length <= 64) {
|
return h(ColorComponent, { rgbavalue: color.rgb(object) });
|
||||||
|
} else if (color.isPalette(object)) {
|
||||||
|
// TODO?
|
||||||
|
if (object.colors.length <= 256) {
|
||||||
|
let children = [];
|
||||||
|
let props = { class: '', key: `${objpath}__obj` };
|
||||||
props.class += ' scripting-flex ';
|
props.class += ' scripting-flex ';
|
||||||
object.colors.forEach((val) => {
|
object.colors.forEach((val) => {
|
||||||
children.push(h(ColorComponent, { rgbavalue: val }));
|
children.push(h(ColorComponent, { rgbavalue: val }));
|
||||||
|
@ -317,25 +328,27 @@ function objectToContentsDiv(object: {} | [], objpath: string) {
|
||||||
return h('div', { class: 'scripting-flex' }, objectDivs);
|
return h('div', { class: 'scripting-flex' }, objectDivs);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UISliderComponentProps {
|
///
|
||||||
|
|
||||||
|
interface UIComponentProps {
|
||||||
iokey: string;
|
iokey: string;
|
||||||
uiobject: ScriptUISliderType;
|
uiobject: ScriptUIType;
|
||||||
}
|
}
|
||||||
|
|
||||||
class UISliderComponent extends Component<UISliderComponentProps> {
|
class UISliderComponent extends Component<UIComponentProps> {
|
||||||
render(virtualDom, containerNode, replaceNode) {
|
render(virtualDom, containerNode, replaceNode) {
|
||||||
let slider = this.props.uiobject;
|
let slider = this.props.uiobject as ScriptUISliderType;
|
||||||
return h('div', {}, [
|
return h('div', {}, [
|
||||||
this.props.iokey,
|
this.props.iokey,
|
||||||
h('input', {
|
h('input', {
|
||||||
type: 'range',
|
type: 'range',
|
||||||
min: slider.min / slider.step,
|
min: slider.min / slider.step,
|
||||||
max: slider.max / slider.step,
|
max: slider.max / slider.step,
|
||||||
value: this.props.uiobject.value / slider.step,
|
value: slider.value / slider.step,
|
||||||
onInput: (ev) => {
|
onInput: (ev) => {
|
||||||
let newValue = { value: parseFloat(ev.target.value) * slider.step };
|
let newUIValue = { value: parseFloat(ev.target.value) * slider.step };
|
||||||
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: newUIValue}]);
|
||||||
}
|
}
|
||||||
}, []),
|
}, []),
|
||||||
getShortName(slider.value)
|
getShortName(slider.value)
|
||||||
|
@ -343,6 +356,46 @@ class UISliderComponent extends Component<UISliderComponentProps> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class UISelectComponent extends Component<UIComponentProps> {
|
||||||
|
ref = createRef();
|
||||||
|
render(virtualDom, containerNode, replaceNode) {
|
||||||
|
let select = this.props.uiobject as ScriptUISelectType<any>;
|
||||||
|
let children = objectToChildren(select.options);
|
||||||
|
return h('div', {
|
||||||
|
class: 'scripting-select scripting-flex',
|
||||||
|
ref: this.ref,
|
||||||
|
onClick: (e) => {
|
||||||
|
// iterate parents until we find select div, then find index of child
|
||||||
|
let target = e.target as HTMLElement;
|
||||||
|
while (target.parentElement && target.parentElement != this.ref.current) {
|
||||||
|
target = target.parentElement;
|
||||||
|
}
|
||||||
|
const selindex = Array.from(target.parentElement.children).indexOf(target);
|
||||||
|
if (selindex >= 0 && selindex < children.length) {
|
||||||
|
let newUIValue = { value: children[selindex], index: selindex };
|
||||||
|
this.setState(this.state);
|
||||||
|
current_project.updateDataItems([{key: this.props.iokey, value: newUIValue}]);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Could not find click target of ${this.props.iokey}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
children.map((child, index) => {
|
||||||
|
let div = objectToDiv(child, null, `${this.props.iokey}__select_${index}`);
|
||||||
|
let selected = (index == select.index);
|
||||||
|
return h('div', { class: selected ? 'scripting-selected' : '' }, [ div ]);
|
||||||
|
}))
|
||||||
|
// TODO: show current selection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const UI_COMPONENTS = {
|
||||||
|
'slider': UISliderComponent,
|
||||||
|
'select': UISelectComponent,
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
|
||||||
export class Notebook {
|
export class Notebook {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly maindoc: HTMLDocument,
|
public readonly maindoc: HTMLDocument,
|
||||||
|
|
|
@ -11,16 +11,6 @@ class ScriptingPlatform implements Platform {
|
||||||
|
|
||||||
constructor(mainElement: HTMLElement) {
|
constructor(mainElement: HTMLElement) {
|
||||||
this.mainElement = mainElement;
|
this.mainElement = mainElement;
|
||||||
/*
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe
|
|
||||||
this.iframe = $(`<iframe sandbox="allow-same-origin" width="100%" height="100%"/>`).appendTo(mainElement)[0] as HTMLIFrameElement;
|
|
||||||
mainElement.classList.add("vertical-scroll"); //100% height
|
|
||||||
mainElement.style.overflowY = 'auto';
|
|
||||||
this.iframe.onload = (e) => {
|
|
||||||
let head = this.iframe.contentDocument.head;
|
|
||||||
head.appendChild($(`<link rel="stylesheet" href="css/script.css">`)[0]);
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
this.notebook = new Notebook(document, mainElement);
|
this.notebook = new Notebook(document, mainElement);
|
||||||
}
|
}
|
||||||
start() {
|
start() {
|
||||||
|
@ -51,11 +41,6 @@ class ScriptingPlatform implements Platform {
|
||||||
return [
|
return [
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
showHelp() {
|
|
||||||
window.open("https://github.com/showdownjs/showdown/wiki/Showdown's-Markdown-syntax", "_help");
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PLATFORMS['script'] = ScriptingPlatform;
|
PLATFORMS['script'] = ScriptingPlatform;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user