import { createElementEvent, IconStyle } from "./rdtkview.js";
function loadRDView() {
(window as any).RDViewRender = RDViewRender;
}
class Color {
readonly red: number;
readonly green: number;
readonly blue: number;
readonly alpha: number;
constructor(red: number, green: number, blue: number, alpha: number) {
this.red = red;
this.green = green;
this.blue = blue;
this.alpha = alpha;
};
}
enum EventCondition {
true = "true",
false = "false",
both = "both",
}
class RDViewWindow {
Row: number = 4;
Column: number = 8;
Scale: number = 1;
}
class RDEvent {
Name: string = "";
PrefixTab?: string = undefined;
Properties: KeyValuePair[] = [];
Style: RDStyle = { Beat: 0, Y: 0 };
ClickStyle?: RDStyle;
Condition?: EventCondition;
Tag?: boolean;
}
class RDStyle {
Beat: number = 0;
Y: number = 0;
Brightness?: number;
Hue?: number;
Grayscale?: number;
Opacity?: number;
}
class RDGroup {
Width: number = 1;
Height: number = 1;
Outline: Color = new Color(255, 0, 0, 255);
Style: RDStyle = new RDStyle();
ClickStyle: RDStyle = new RDStyle();
ClickOutline?: Color;
Children: (RDEvent | RDGroup)[] = [];
}
function RDViewRender(lang: string, code: string): HTMLElement {
const view: RDViewWindow = new RDViewWindow();
var objs: (RDEvent | RDGroup)[] = [];
try {
objs = ReadObjs(code);
} catch (error) {
return ErrorElement(`${error}`);
}
const args = lang.split(/\s+/g);
if (1 < args.length && args.length <= 4) {
if (args[1]) {
view.Column = parseInt(args[1]);
}
if (args[2]) {
view.Row = parseInt(args[2]);
}
if (args[3]) {
view.Scale = parseFloat(args[3]);
}
}
return ConstructObjs(view, objs);
}
function ErrorElement(message: string = ""): HTMLElement {
const el = document.createElement("div");
el.className = "rdtk-error";
el.textContent = message;
return el;
}
function PrefixToTab(prefix: string): string | undefined {
switch (prefix) {
case "es": return "Sounds";
case "eb": return "Rows";
case "ea": return "Actions";
case "ed": return "Sprites";
case "er": return "Rooms";
case "ec": return "Unknown";
default: return undefined;
}
}
function ReadObjs(line: string): (RDEvent | RDGroup)[] {
const objs: (RDEvent | RDGroup)[] = [];
var reader = new Reader(line);
while (!reader.IsEnd()) {
var event: (RDEvent | RDGroup);
var name: string = "";
var prefix: string = "";
try {
name = reader.ReadSymbol();
if (reader.TestRead('.', false)) {
prefix = name;
name = reader.ReadSymbol();
}
}
catch (e) {
name = "";
}
if (name.toLowerCase() === 'g') {
event = new RDGroup();
}
else {
const ev = new RDEvent();
ev.Name = name;
ev.PrefixTab = PrefixToTab(prefix);
event = ev;
}
if (event instanceof RDEvent) {
try { event.Style.Beat = reader.ReadFloat(); } catch (e) { }
try { event.Style.Y = reader.ReadInt(); } catch (e) { }
if (reader.TestPeek('[')) {
const pairString: string = reader.ReadQuoted('[', ']');
const keyvalues = new Reader(pairString, reader).ReadPairs();
ReadProperties(reader, event, keyvalues);
}
reader.TestRead(';', true);
}
else if (event instanceof RDGroup) {
try { event.Style.Beat = reader.ReadFloat(); } catch (e) { }
try { event.Style.Y = reader.ReadInt(); } catch (e) { }
try { event.Width = reader.ReadFloat(); } catch (e) { }
try { event.Height = reader.ReadInt(); } catch (e) { }
try { event.Outline = reader.ReadColor(); } catch (e) { }
if (reader.TestPeek('[')) {
const pairString: string = reader.ReadQuoted('[', ']');
const keyvalues = new Reader(pairString, reader).ReadPairs();
ReadProperties(reader, event, keyvalues);
}
if (reader.TestPeek('{')) {
const pairString: string = reader.ReadQuoted('{', '}');
const objs2 = ReadObjs(pairString);
event.Children = objs2;
}
reader.TestRead(';', true);
}
objs.push(event);
}
return objs;
}
interface KeyValuePair {
Index: number;
Key: string;
Value: string;
}
function IsEmptyOrSpace(string: string): boolean {
return (string) ? /\s/.test(string) : false;
}
class Reader {
private string: string;
private position: number = 0;
private line: number = 0;
private offl: number = 0;
private offp: number = 0;
constructor(string: string, reader: Reader | undefined = undefined) {
this.string = string;
if (reader) {
this.offl = reader.line;
this.offp = reader.position;
}
};
public IsEnd(): boolean {
this.JumpSpace();
return this.position >= this.string.length;
}
private JumpSpace(): void {
while (IsEmptyOrSpace(this.string[this.position])) {
if (this.string[this.position] === '\n') {
this.line++;
}
this.position++;
}
}
public Peek(): string {
this.JumpSpace();
return this.string[this.position];
}
public TestPeek(str: string, allowThrow: boolean = false): boolean {
this.JumpSpace();
var c = this.string.substring(this.position, this.position + str.length);
if (c === str) {
return true;
}
else {
if (allowThrow) {
throw new Error(`Expecting a string '${str}' but got '${c}': ${this.GetPositionDescription()}.`);
}
else {
return false;
}
}
}
public TestRead(str: string, allowThrow: boolean = false): boolean {
this.JumpSpace();
var c = this.string.substring(this.position, this.position + str.length);
if (c === str) {
this.position++;
return true;
}
else {
if (allowThrow) {
throw new Error(`Expecting a char '${str}' but got '${c}': ${this.GetPositionDescription()}.`);
}
else {
return false;
}
}
}
public ReadChar(): string {
var c = this.string[this.position];
if (c === '\n') {
this.line++;
}
this.position++;
return c;
}
public ReadSymbol(): string {
this.JumpSpace();
var name = this.string.substring(this.position).match(/^\w+/)?.[0];
if (name) {
this.position += name.length;
return name;
}
throw new Error(`Expecting a key but missing: ${this.GetPositionDescription()}.`);
}
public ReadColor(): Color {
this.JumpSpace();
var color = this.string.substring(this.position).match(/^#[0-9A-Fa-f]{3}([0-9A-Fa-f]{1}([0-9A-Fa-f]{2}([0-9A-Fa-f]{2})?)?)?/)?.[0].substring(1);
if (!color) {
throw new Error(`Expecting a color but missing: ${this.GetPositionDescription()}`);
}
var result: Color;
switch (color.length) {
case 3:
result = new Color(
parseInt(color.substring(0, 1), 16) * 16,
parseInt(color.substring(1, 2), 16) * 16,
parseInt(color.substring(2, 3), 16) * 16,
255
);
break;
case 4:
result = new Color(
parseInt(color.substring(0, 1), 16) * 16,
parseInt(color.substring(1, 2), 16) * 16,
parseInt(color.substring(2, 3), 16) * 16,
parseInt(color.substring(3, 4), 16) * 16
);
break;
case 6:
result = new Color(
parseInt(color.substring(0, 2), 16),
parseInt(color.substring(2, 4), 16),
parseInt(color.substring(4, 6), 16),
255
);
break;
case 8:
result = new Color(
parseInt(color.substring(0, 2), 16),
parseInt(color.substring(2, 4), 16),
parseInt(color.substring(4, 6), 16),
parseInt(color.substring(6, 8), 16)
);
break;
default:
result = new Color(255, 0, 0, 255);
break;
}
this.position += color.length + 1;
return result;
}
public ReadInt(): number {
this.JumpSpace();
var name = this.string.substring(this.position).match(/^[+-]?[0-9]+/)?.[0];
if (name) {
this.position += name.length;
return parseInt(name);
}
throw new Error(`Expecting an int but missing: ${this.GetPositionDescription()}.`);
}
public ReadFloat(): number {
this.JumpSpace();
var name = this.string.substring(this.position).match(/^[+-]?([0-9]*[.])?[0-9]+/)?.[0];
if (name) {
this.position += name.length;
return parseFloat(name);
}
throw new Error(`Expecting a float but missing: ${this.GetPositionDescription()}.`);
}
public ReadPairs(separator: string = ',', equals: string = '='): KeyValuePair[] {
var pairs: KeyValuePair[] = [];
var flag: boolean = !this.IsEnd();
while (flag) {
if (this.IsEnd()) {
break;
}
const index = this.position + this.offp;
const name = this.ReadSymbol();
this.TestRead(equals, true);
const value = this.ReadWhileNotEnd([separator]);
pairs.push({ Key: name, Value: value, Index: index });
if (flag = !this.IsEnd()) {
this.TestRead(separator, true);
}
}
return pairs;
}
public ReadWhileNotEnd(ends: string[] = [',', ']']): string {
var str: string = '';
while (!(this.position >= this.string.length) && !ends.find(i => this.string.startsWith(i, this.position))) {
str += this.string[this.position];
this.position++;
}
return str.trim();
}
public ReadQuoted(left: string, right: string): string {
this.JumpSpace();
var level: number = 0;
var result = '';
if (this.string.startsWith(left, this.position)) { level++; this.position += left.length; }
else { throw new Error(`Cannot find the left half "${left}": ${this.GetPositionDescription()}`); }
while (!(this.position >= this.string.length) && level > 0) {
if (this.string.startsWith(left, this.position)) {
level++;
this.position += left.length;
result += left;
}
if (this.string.startsWith(right, this.position)) {
level--;
this.position += right.length;
if (level === 0) {
return result;
}
else {
result += right;
}
}
result += this.string[this.position];
this.position++;
}
throw new Error(`Cannot find the right half "${right}": ${this.GetPositionDescription()}`);
}
public GetPositionDescription(position: number | undefined = undefined): string {
const s = this.string.split('\n');
var curp: number = this.offp;
var line: number = this.offl;
if (position !== undefined) {
while (curp + s[line].length + 1 < position - this.offp) {
curp += s[line].length + 1;
line += 1;
}
}
else {
curp += s.slice(0, this.line).reduce((sum, i) => sum + i.length + 1, 0);
line += this.line;
position = this.position;
}
return `
at "${s[line]}".`;
}
}
function FindValue(pairs: KeyValuePair[], key: string): KeyValuePair | undefined {
const finded = pairs.find(i => i.Key === key);
if (finded) {
return finded;
}
return undefined;
}
function ReadProperties(reader: Reader, event: RDEvent | RDGroup, pairs: KeyValuePair[]) {
if (event instanceof RDGroup) {
var value: KeyValuePair | undefined;
if (value = FindValue(pairs, "hue")) { event.Style.Hue = parseFloat(value.Value); }
if (value = FindValue(pairs, "brightness")) { event.Style.Brightness = parseFloat(value.Value); }
if (value = FindValue(pairs, "grayscale")) { event.Style.Grayscale = parseFloat(value.Value); }
if (value = FindValue(pairs, "opacity")) { event.Style.Opacity = parseFloat(value.Value); }
if (value = FindValue(pairs, "offset")) {
reader = new Reader(value.Value);
event.ClickStyle ??= new RDStyle();
try { event.ClickStyle.Beat = reader.ReadFloat(); } catch (e) { }
try { event.ClickStyle.Y = reader.ReadInt(); } catch (e) { }
try { event.ClickOutline = reader.ReadColor(); } catch (e) { }
}
if (value = FindValue(pairs, "offset_hue")) { event.ClickStyle ??= new RDStyle(); event.ClickStyle.Hue = parseFloat(value.Value); }
if (value = FindValue(pairs, "offset_brightness")) { event.ClickStyle ??= new RDStyle(); event.ClickStyle.Brightness = parseFloat(value.Value); }
if (value = FindValue(pairs, "offset_grayscale")) { event.ClickStyle ??= new RDStyle(); event.ClickStyle.Grayscale = parseFloat(value.Value); }
if (value = FindValue(pairs, "offset_opacity")) { event.ClickStyle ??= new RDStyle(); event.ClickStyle.Opacity = parseFloat(value.Value); }
}
else if (event instanceof RDEvent) {
for (const pair of pairs) {
const key = pair.Key;
const value = pair.Value;
switch (key) {
case "if":
if (["true", "false", "both"].find(i => i === value)) {
event.Condition = value === "true" ? EventCondition.true :
value === "false" ? EventCondition.false : EventCondition.both;
}
else {
throw new Error(`Expecting "true", "false" or "both", but find "${value}".`);
}
break;
case "tag":
event.Tag = value === "true";
break;
case "hue":
event.Style.Hue = parseFloat(value);
break;
case "brightness":
event.Style.Brightness = parseFloat(value);
break;
case "grayscale":
event.Style.Grayscale = parseFloat(value);
break;
case "opacity":
event.Style.Opacity = parseFloat(value);
break;
case "offset": {
const offsetReader = new Reader(value);
event.ClickStyle ??= new RDStyle();
try { event.ClickStyle.Beat = offsetReader.ReadFloat(); } catch (e) { }
try { event.ClickStyle.Y = offsetReader.ReadInt(); } catch (e) { }
break;
}
case "offset_hue":
event.ClickStyle ??= new RDStyle();
event.ClickStyle.Hue = parseFloat(value);
break;
case "offset_brightness":
event.ClickStyle ??= new RDStyle();
event.ClickStyle.Brightness = parseFloat(value);
break;
case "offset_grayscale":
event.ClickStyle ??= new RDStyle();
event.ClickStyle.Grayscale = parseFloat(value);
break;
case "offset_opacity":
event.ClickStyle ??= new RDStyle();
event.ClickStyle.Opacity = parseFloat(value);
break;
default:
event.Properties.push(pair);
}
}
}
}
const CELL_SIZE = 28;
const RDTK_SCALE = 2;
const NUMERIC_KEYS = new Set([
"tick", "swing", "hold", "pulse", "customPulse", "interval", "delay", "loop",
"subdivision", "crotchetsPerBar", "syncoBeat", "duration", "angle", "bpm",
"beatsPerMinute", "volume", "speed", "startOpacity", "endOpacity", "subdivOffset",
]);
const SAY_PHRASES = new Set([
"SayReaDyGetSetGoNew", "SayReaDyGetSetOne", "SayGetSetGo", "SayGetSetOne",
"JustSayRea", "JustSayDy", "JustSayGet", "JustSaySet", "JustSayAnd",
"JustSayGo", "JustSayStop", "JustSayAndStop", "SaySwitch", "SayWatch",
"SayListen", "Count1", "Count2", "Count3", "Count4", "Count5", "Count6",
"Count7", "Count8", "Count9", "Count10", "SayReadyGetSetGo", "JustSayReady",
]);
function CoerceValue(key: string, value: string): any {
if (value === "true") { return true; }
if (value === "false") { return false; }
if (NUMERIC_KEYS.has(key) && /^[+-]?(\d+(\.\d+)?|\.\d+)$/.test(value)) {
return Number(value);
}
return value;
}
function ToRDtkObject(ev: RDEvent): any {
const obj: any = { type: ev.Name };
let tab: string | undefined = undefined;
for (const pair of ev.Properties) {
let key = pair.Key;
const value = pair.Value;
if (key === "tab") { tab = value; continue; }
if (key === "tag") { continue; }
if (ev.Name === "AddOneshotBeat" && key === "type") {
const types = value.split(/\s+/).filter(i => i.length > 0);
for (const t of types) {
switch (t) {
case "freezeshot": obj.freezeBurnMode = "Freezeshot"; break;
case "burnshot": obj.freezeBurnMode = "Burnshot"; break;
case "skipshot": obj.skipshot = true; break;
}
}
continue;
}
if (ev.Name === "AddOneshotBeat" && key === "hold") {
obj.hold = !(value === "false" || value === "0" || value === "0.0");
continue;
}
if (key === "rowxs") { key = "pattern"; }
else if (ev.Name === "AddOneshotBeat" && key === "loop") { key = "loops"; }
else if (ev.Name === "AddOneshotBeat" && key === "subdivision") { key = "subdivisions"; }
if (ev.Name === "SayReadyGetSetGo" && key === "phraseToSay") {
if (SAY_PHRASES.has(value)) { obj.phraseToSay = value; }
continue;
}
obj[key] = CoerceValue(key, value);
}
if (tab !== undefined) {
obj.tab = tab;
}
else if (ev.PrefixTab !== undefined) {
obj.tab = ev.PrefixTab;
}
if (ev.Tag === true) {
obj.tag = true;
}
return obj;
}
interface RDtkNode extends HTMLElement {
__setRDtkState?: (hover: boolean, active: boolean) => void;
}
function ConstructObjs(view: RDViewWindow, objs: (RDEvent | RDGroup)[]): HTMLElement {
const content = document.createElement("div");
content.className = "RDViewContent";
content.style.setProperty("--column", `${view.Column}`);
content.style.setProperty("--row", `${view.Row}`);
content.style.setProperty("--scale", `${view.Scale}`);
const canvas = document.createElement("div");
canvas.className = "RDView rdtk-rdview";
canvas.style.setProperty("--column", `${view.Column}`);
canvas.style.setProperty("--row", `${view.Row}`);
canvas.style.setProperty("--scale", `${view.Scale}`);
for (const obj of objs) {
const node = BuildObject(obj);
if (node) { canvas.appendChild(node); }
}
content.appendChild(canvas);
return content;
}
function BuildObject(obj: RDEvent | RDGroup): HTMLElement {
return obj instanceof RDGroup ? BuildGroup(obj) : BuildEvent(obj);
}
function BuildEvent(ev: RDEvent): HTMLElement {
const wrapper = document.createElement("div");
wrapper.className = "rdtk-event";
wrapper.style.left = `${ev.Style.Beat * CELL_SIZE}px`;
wrapper.style.top = `${ev.Style.Y * CELL_SIZE}px`;
const state: IconStyle = { enabled: true, active: false, hover: false, scale: RDTK_SCALE, showDuration: false };
const rdobj = ToRDtkObject(ev);
const inner = rdobj ? createElementEvent(rdobj, state) : null;
if (inner) {
inner.style.position = "absolute";
inner.style.left = "0px";
inner.style.top = "0px";
wrapper.appendChild(inner);
}
else {
wrapper.classList.add("rdtk-unknown");
wrapper.textContent = ev.Name || "unknown";
}
ApplyFilter(wrapper, ev.Style);
if (ev.Condition !== undefined) {
const condition = document.createElement("div");
condition.className = `rdtk-condition condition-${ev.Condition}`;
wrapper.appendChild(condition);
}
const offsetBeat = ev.ClickStyle?.Beat ?? 0;
const offsetY = ev.ClickStyle?.Y ?? 0;
const hasOffset = offsetBeat !== 0 || offsetY !== 0;
function setState(hover: boolean, active: boolean): void {
state.hover = hover;
state.active = active;
inner?.onStateChange(state);
wrapper.classList.toggle("hover", hover);
wrapper.classList.toggle("active", active);
wrapper.style.transform = (active && hasOffset)
? `translate(${offsetBeat * CELL_SIZE}px, ${offsetY * CELL_SIZE}px)`
: "";
ApplyFilter(wrapper, active && ev.ClickStyle ? ev.ClickStyle : ev.Style);
}
wrapper.addEventListener("mouseover", (e) => {
if (!wrapper.contains(e.relatedTarget as Node | null)) { setState(true, state.active); }
});
wrapper.addEventListener("mouseout", (e) => {
if (!wrapper.contains(e.relatedTarget as Node | null)) { setState(false, false); }
});
wrapper.addEventListener("mousedown", () => setState(true, true));
wrapper.addEventListener("mouseup", () => setState(state.hover, false));
(wrapper as RDtkNode).__setRDtkState = setState;
return wrapper;
}
function BuildGroup(grp: RDGroup): HTMLElement {
const wrapper = document.createElement("div");
wrapper.className = "rdtk-group";
wrapper.style.left = `${grp.Style.Beat * CELL_SIZE}px`;
wrapper.style.top = `${grp.Style.Y * CELL_SIZE}px`;
wrapper.style.width = `${grp.Width * CELL_SIZE}px`;
wrapper.style.height = `${grp.Height * CELL_SIZE}px`;
wrapper.style.outlineColor = ToHex(grp.Outline);
ApplyFilter(wrapper, grp.Style);
for (const child of grp.Children) {
const node = BuildObject(child);
if (node) { wrapper.appendChild(node); }
}
const offsetBeat = grp.ClickStyle?.Beat ?? 0;
const offsetY = grp.ClickStyle?.Y ?? 0;
const hasOffset = offsetBeat !== 0 || offsetY !== 0;
function setState(hover: boolean, active: boolean): void {
wrapper.classList.toggle("hover", hover);
wrapper.classList.toggle("active", active);
wrapper.style.transform = (active && hasOffset)
? `translate(${offsetBeat * CELL_SIZE}px, ${offsetY * CELL_SIZE}px)`
: "";
wrapper.style.outlineColor = active ? ToHex(grp.ClickOutline ?? grp.Outline) : ToHex(grp.Outline);
ApplyFilter(wrapper, active && grp.ClickStyle ? grp.ClickStyle : grp.Style);
}
wrapper.addEventListener("mouseover", (e) => {
if (!wrapper.contains(e.relatedTarget as Node | null)) { setState(true, false); }
});
wrapper.addEventListener("mouseout", (e) => {
if (!wrapper.contains(e.relatedTarget as Node | null)) { setState(false, false); }
});
wrapper.addEventListener("mousedown", () => setState(true, true));
wrapper.addEventListener("mouseup", () => setState(true, false));
(wrapper as RDtkNode).__setRDtkState = setState;
return wrapper;
}
function ApplyFilter(el: HTMLElement, style: RDStyle): void {
const parts: string[] = [];
if (style.Hue !== undefined) { parts.push(`hue-rotate(${style.Hue}deg)`); }
if (style.Brightness !== undefined) { parts.push(`brightness(${style.Brightness}%)`); }
if (style.Grayscale !== undefined) { parts.push(`grayscale(${style.Grayscale}%)`); }
if (style.Opacity !== undefined) { parts.push(`opacity(${style.Opacity}%)`); }
el.style.filter = parts.join(" ");
}
function ToHex(color: Color) {
const hex =
parseInt(color.red.toFixed(0)).toString(16).padStart(2, '0') +
parseInt(color.green.toFixed(0)).toString(16).padStart(2, '0') +
parseInt(color.blue.toFixed(0)).toString(16).padStart(2, '0') +
parseInt(color.alpha.toFixed(0)).toString(16).padStart(2, '0');
return '#' + hex;
}
(window as any).loadRDView = loadRDView;