View gameobject trees within Prodigy Game.
// ==UserScript==
// @name Prodigy Tree Viewer
// @namespace https://hipposgrumm.dev/
// @version 1.2.0
// @description View gameobject trees within Prodigy Game.
// @author Hipposgrumm
// @license gpl-3.0
// @match *://*.prodigygame.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=prodigygame.com
// @grant none
// @run-at document-end
// ==/UserScript==
/*jshint esversion: 11 */
(async function() {
let gameaccess;
for (let timeout in [0, 100, 500, 1000, 5000]) {
if (timeout > 0) await new Promise(resolve => setTimeout(resolve, timeout));
try {
gameaccess = window.Boot?.prototype.game; // Math
if (!gameaccess) gameaccess = window.GameFramework?.FrameworkApp?.instance?.game; // English
if (gameaccess) break;
} catch (e) {}
}
if (!gameaccess) return; // Can't do anything since there's no game world.
let css = `
<link rel="stylesheet" href="https://code.prodigygame.com/assets/Font-Awesome/css/font-awesome-bb53ad7bff.min.css">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,300,400,500,600,700" rel="stylesheet" type="text/css" async>
<style>
/* https://www.w3schools.com/howto/howto_js_treeview.asp */
.tree ul {
font-size: 14px;
list-style-type: none;
padding-inline-start: 20px;
user-select: none; /* Prevent text selection */
}
.treename {
cursor: pointer;
display: inline-block;
min-width: 10em;
padding: 3px;
border-radius: 5px;
}
.treename.spinebone {
color: #39E;
}
.treename:hover {
background-color: #8884;
}
.treename:active {
background-color: #8882;
}
.selected > .treename {
background-color: #8888;
}
.caret {
cursor: pointer;
}
.caret::before {
content: "\u25B6";
display: inline-block;
margin-right: 6px;
}
.makeinvisible::before {
color: transparent;
cursor: auto;
}
.caret-down::before {
transform: rotate(90deg);
}
</style>
<style>
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-thumb {
background: #5F606A;
}
::-webkit-scrollbar-thumb:hover {
background: #4F505A;
cursor: pointer;
}
body {
display: flex;
font-family: "Open Sans";
color: #DDD;
background-color: #1A1A1E;
}
input,textarea,select,button {
font-family: "Open Sans";
color: #E5E5E5;
background-color: #363638;
border-color: #444;
border-radius: 5px;
}
.hidden {
display: none;
}
div.tree {
height: calc(100vh - 40px);
background-color: #242429;
overflow: auto;
white-space: nowrap;
padding: 10px;
border-radius: 10px;
}
div.splitter {
width: 10px;
cursor: col-resize;
}
div.dataview {
height: calc(100vh - 40px);
background-color: #202023;
overflow: auto;
white-space: nowrap;
flex: 1;
padding: 10px;
border-radius: 10px;
}
div.dataitem {
display: grid;
grid-template-columns: minmax(150px, 210px) 1fr;
padding: 2px 10px;
height: 25px;
}
.dataitem span {
vertical-align: bottom;
user-select: none;
}
.dataitem > span {
vertical-align: middle;
width: 150px;
min-width: 50px;
}
.dataview textarea {
resize: vertical;
}
.dataitem > input,textarea,select {
min-width: 0px;
}
.dataview div.sub {
background-color: #242427;
padding: 10px;
border-radius: 10px;
}
button.degtoggle {
background-color: #363638;
border-color: #444;
aspect-ratio: 1 / 1;
height: 25px;
}
button.degtoggle.rad {
background-image: url("data:image/svg+xml;base64,`+btoa(`<svg xmlns="http://www.w3.org/2000/svg" fill="#DDD" viewBox="0 0 500 500">
<path d="M 71.235 164.76 L 55.596 164.695 C 55.581 164.696 68.346 113.835 89.175 88.947 C 106.336 68.108 131.312 61.924 149.8 61.7 L 444.154 61.724 L 444.2 114.6 L 354.67 114.631 C 354.669 120.296 343.483 203.929 339.545 264.172 C 336.436 311.736 334.283 367.164 380.158 371.445 C 418.882 373.364 427.109 335.185 427.683 322.619 L 442.8 322.6 C 439.3 365.492 419.567 433.28 357.153 432.362 C 308.314 429.455 293.292 398.763 287.689 363.431 C 278.473 329.546 306.818 121.136 304.62 114.733 L 206.6 114.7 C 207.422 115.05 193.43 261.761 187.564 293.505 C 183.956 324.61 174.05 370.806 161.744 395.17 C 145.189 429.984 121.953 435.333 105.175 428.222 C 77.642 414.754 81.181 388.386 90.763 374.381 C 96.317 367.136 123.771 328.393 131.818 309.928 C 142.793 288.981 151.542 249.678 153.494 236.773 C 159.835 216 167.064 115.489 166.297 115.387 L 130.092 115.44 C 111.64 115.885 97.442 126.385 88.196 139.247 C 87.33 138.9 71.158 164.683 71.2 164.7 L 71.235 164.76 Z" />
</svg>`)+`")
}
button.degtoggle.deg {
background-image: url("data:image/svg+xml;base64,`+btoa(`<svg xmlns="http://www.w3.org/2000/svg" fill="#DDD" viewBox="0 0 500 500">
<path d="M 159.139 277.589 C 210.158 294.834 247.443 340.673 243.385 399.989 L 288.465 398.242 C 291.281 358.175 261.569 271.201 184.684 242.627 L 159.139 277.589 Z" />
<path d="M 250.213 80.675 L 60.809 408.222 L 60.829 419.278 L 439.19 419.325 L 439.1 377.9 L 127.222 377.894 L 290.819 106.63 L 250.213 80.675 Z" />
</svg>`)+`")
}
div.imageview {
max-height: 200px;
border-style: inset;
}
.imageview img {
width: 100%;
height: 100%;
object-fit: contain;
}
.imageview p.errmsg {
position: relative;
text-align: center;
margin: 0;
display: none;
color: #F00;
/* https://www.w3schools.com/howto/howto_css_center-vertical.asp */
top: 50%;
left: 50%;
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
div.colorview {
border-style: inset;
}
</style>
`;
let EXPORT_MIMETYPES = {
"html": "text/html",
"css": "text/css",
"js": "application/javascript",
"woff": "application/font-woff",
"woff2": "application/font-woff2",
"png": "image/png",
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"gif": "image/gif",
"txt": "text/plain",
"json": "application/json",
"mp4": "video/mp4",
"ogg": "application/ogg",
"zip": "application/zip"
};
let DEG2RAD = Math.PI / 180;
let RAD2DEG = 180 / Math.PI;
let colordetectorcanvas = document.createElement('canvas');
colordetectorcanvas.width = 1;
colordetectorcanvas.height = 1;
function intFromColor(color) {
if (typeof(color) == 'number') return color;
let ctx = colordetectorcanvas.getContext('2d');
ctx.fillStyle = color;
ctx.fillRect(0,0,1,1);
let pixel = ctx.getImageData(0,0,1,1).data;
return (pixel[0] << 16) | (pixel[1] << 8) | pixel[2];
}
let PIXI_CLASSES = [];
let PIXI_CLASSES_SPINE = [];
for (let key in window.PIXI) {
let val = window.PIXI[key];
if (typeof(val) == 'function' && val.hasOwnProperty("prototype")) PIXI_CLASSES[key] = val;
}
for (let key in window.PIXI.spine) {
let val = window.PIXI.spine[key];
if (typeof(val) == 'function' && val.hasOwnProperty("prototype")) PIXI_CLASSES_SPINE[key] = val;
}
{
let fun;
fun = (e) => {
window.EXTRACTED_CORS_POLICY = {};
e.originalPolicy.split(new RegExp('; ?')).forEach(v => {
if (!v) return;
let vs = [];
let name;
v.split(' ').every((e,i) => {
if (i == 0) name = e;
else {
let val;
if (e.charAt(0) != "'") val = e;
else if (e == "'self'") {
val = document.location.origin;
} else if (e == "'none'") {
vs = [];
return false;
}
if (val) vs.push(new RegExp(val.replaceAll(':', '\\:').replaceAll('/', '\\/').replaceAll('*', '(.*)')));
}
return true;
});
window.EXTRACTED_CORS_POLICY[name] = vs;
});
document.removeEventListener("securitypolicyviolation", fun);
};
document.addEventListener("securitypolicyviolation", fun);
}
// PROBABLY EXPENSIVE, USE SPARINGLY
async function checkCORSAllow(url, type) {
if (!window.EXTRACTED_CORS_POLICY) {
let img = new Image();
img.crossOrigin = "anonymous";
img.src = "//cors-test-please-ignore"; // triggers the function above
// Source - https://stackoverflow.com/a/52652681
// Posted by Lightbeard
// Retrieved 2026-04-21, License - CC BY-SA 4.0
await new Promise(resolve => {
let loop;
loop = setInterval(() => {
if (window.EXTRACTED_CORS_POLICY) {
clearInterval(loop);
resolve();
}
}, 10);
});
}
let origin = url.split(new RegExp('(?<![\:\/])\/'))[0]; // Doesn't match ':/' properly but we really don't care.
return Boolean((window.EXTRACTED_CORS_POLICY[type] ?? []).find(e => origin.match(e)));
}
class TextureManaged {
constructor(texture) {
if (!texture.castToBaseTexture) { // checks if function exists
console.log(texture);
throw new Error("object is not a texture.");
}
this._id = -67;
this.texture = texture;
}
hasChanged() {
return this._id != this.texture.castToBaseTexture().dirtyId;
}
getDataUrl() {
if (this.hasChanged()) this._url = renderPIXITexture(this);
return this._url;
}
}
let PIXITextureShader = new window.PIXI.Filter(null, `
// nocache: ${Date.now() /* Cached shaders sometimes simply refuse to work at all. */}
precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D uSampler;
uniform bool doMatte;
void main(void) {
vec4 fg = texture2D(uSampler, vTextureCoord);
if (doMatte) fg.r = fg.a;
fg.a = 1.0;
gl_FragColor = fg;
}
`, {doMatte: false});
let PIXIBufferApp = new window.PIXI.Application({ backgroundAlpha: 0 });
function renderPIXITexture(textures) {
if (!gameaccess) throw new Error("Attempted to render PIXI texture before viewer window was created.");
let single = !Array.isArray(textures);
if (single) textures = [textures];
let outs = [];
let outcanvas = document.createElement('canvas');
let outctx = outcanvas.getContext('2d');
let gl = gameaccess.renderer.gl;
textures.forEach(mantex => {
if (!(mantex instanceof TextureManaged)) {
console.log("Skipping texture that isn't managed.");
return;
}
let tex = mantex.texture;
outctx.clearRect(0, 0, outcanvas.width, outcanvas.height);
outcanvas.width = tex.width;
outcanvas.height = tex.height;
let sprite = new window.PIXI.Sprite(tex);
let texsrc = tex.castToBaseTexture().resource;
if (texsrc && (texsrc.bitmap || texsrc.source)) {
PIXIBufferApp.renderer.resize(tex.width, tex.height);
PIXIBufferApp.renderer.render(sprite);
outctx.drawImage(PIXIBufferApp.renderer.view, 0, 0);
} else {
sprite.filters = [PIXITextureShader];
for (;sprite.x>-tex.width;sprite.x-=gameaccess.width) {
for (;sprite.y>-tex.height;sprite.y-=gameaccess.height) {
let width = Math.min(tex.width+sprite.x, gameaccess.width);
let height = Math.min(tex.height+sprite.y, gameaccess.height);
let vOff = gameaccess.height - height;
let pixels = new Uint8Array(width * height * 4);
PIXITextureShader.uniforms.doMatte = false;
gameaccess.renderer.render(sprite);
gl.readPixels(0, vOff, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
{
let red = new Uint8Array(pixels.length);
PIXITextureShader.uniforms.doMatte = true;
gameaccess.renderer.render(sprite);
gl.readPixels(0, vOff, width, height, gl.RGBA, gl.UNSIGNED_BYTE, red);
for (let i=0;i<pixels.length;i+=4) {
pixels[i+3] = red[i];
}
}
pixels = new Uint8ClampedArray(pixels, width, height);
let wid = width*4;
for (let r=0;r<(((pixels.length/wid)/2)*wid);r+=wid) {
let ir = pixels.length-r-wid;
for (let i=0;i<wid;i++) {
let iri=ir+i, ri=r+i;
let t = pixels[iri];
pixels[iri] = pixels[ri];
pixels[ri] = t;
}
}
outctx.putImageData(new ImageData(pixels, width, height), -sprite.x, -sprite.y);
}
}
}
outs.push(outcanvas.toDataURL());
mantex._id = tex.castToBaseTexture().dirtyId;
});
if (single) return outs[0];
else return outs;
}
/// An abstract container that can be used to modify objects that ignores the descrepancies between GameObject types
class ManipulatableObject {
/// Creates inner classes for this class
static createInnerClasses() {
/// Contains all data for this object's management in the DOM.
ManipulatableObject.ElementData = class {
constructor(obj, list, parent) {
this._obj = obj;
this._list = list;
this._item = document.createElement('li');
this._caret = document.createElement('span');
this._caret.classList.add("caret");
this._caret.addEventListener("click", () => this.doToggle());
this._caret.classList.add("makeinvisible");
this._caretHidden = true;
this._item.appendChild(this._caret);
if (obj.active != null) {
this._checkbox = document.createElement('input');
this._checkbox.type = "checkbox";
this._checkbox.addEventListener("change", () => {
obj.active = this._checkbox.checked;
});
this._checkbox.checked = obj.active;
this._item.appendChild(this._checkbox);
}
this._nametext = " ";
this._txt = document.createElement('span');
this._txt.classList.add("treename");
if (obj.skeleton || parent?.skeleton) {
this._txt.classList.add("spinebone");
}
this._txt.addEventListener("click", () => openObject(obj));
this._item.appendChild(this._txt);
this._childList = document.createElement('ul');
this._childList.classList.add("hidden");
this._item.appendChild(this._childList);
this._hideChildren = true;
}
/// Expands/collapses this item's child list.
doToggle() {
if (!this._childList) return;
this._hideChildren = !this._hideChildren;
if (this._hideChildren) {
this._childList.classList.add("hidden");
this._caret.classList.remove("caret-down");
this.updateChildren();
} else {
this._childList.classList.remove("hidden");
this._caret.classList.add("caret-down");
}
}
updateChildren(addition=null) {
if (this._caretHidden) {
if (this._obj.node.children.length > 0) {
this._caret.classList.remove("makeinvisible");
this._caretHidden = false;
return;
}
} else if (this._obj.node.children.length == 0) {
this._caret.classList.add("makeinvisible");
this._caretHidden = true;
}
if (!this.isChildrenVisible()) return; // if the children are hidden we don't need to worry about them right now
this._obj.node.sortChildren(addition);
if (!this._obj.node.sortingDirtyAccept()) return;
// https://stackoverflow.com/q/34685316
var elements = document.createDocumentFragment();
this._obj.node.children.forEach(child => {
elements.appendChild(child.object.element._item);
});
this._childList.innerHTML = null;
this._childList.appendChild(elements);
}
/// @returns {bool} Whether this item's list of children is collapsed or not
get childrenHidden() { return this._hideChildren; }
/// @returns {Object} List Element containing this object
get list() { return this._list; }
/// @returns {Object} ListItem Element representing this object
get item() { return this._item; }
/// @returns {Object} List Element for this object's children
get childList() { return this._childList; }
/// @returns {Object} Checkbox that determines if the GameObject is visible/active
get checkbox() { return this._checkbox; }
get txt() { return this._txt; }
get txt_inner() { return this._nametext; }
set txt_inner(val) {
this._nametext = val;
if (!val?.trim()) this._txt.innerHTML = "\u00A0"; // This prevents it from becoming flat.
else this._txt.innerHTML = val;
}
isChildrenVisible() {
let par = this._obj.node;
while (par) {
if (par.object.element._hideChildren) return false;
par = par.parent;
}
return true;
}
};
delete ManipulatableObject.createInnerClasses;
}
constructor(data, list, parent) {
if (!data) throw new Error("Value passed to 'data' parameter of ManipulatableObject.new() is null or empty.");
this._inTree = true;
if (data instanceof ManipulatableObject) {
this._treeobject = data._treeobject;
this._pixiobject = data._pixiobject;
this._data = data._data;
this._typename = data._typename;
this._pixiTransform = data._pixiTransform;
} else {
if (data.hasOwnProperty("_legacyTransform")) { // PIXI GameObject
this._treeobject = data.legacyTransform; // potential reference to Tree GameObject
this._pixiobject = data;
} else if (data.transform) { // Tree GameObject
this._treeobject = data;
this._pixiobject = data.gameObjectRef; // potential reference to PIXI GameObject
} else {
throw new Error("Object passed to 'data' parameter of ManipulatableObject.new() doesn't appear to be a GameObject.");
}
this._data = data; // Useful for referencing names common to both GameObject types
this._typename = null;
for (let name in PIXI_CLASSES) {
let clazz = PIXI_CLASSES[name];
if (clazz == data.constructor) {
this._typename = "pixi/"+name;
break;
}
}
for (let name in PIXI_CLASSES_SPINE) {
let clazz = PIXI_CLASSES_SPINE[name];
if (clazz == data.constructor) {
this._typename = "pixi/spine/"+name;
break;
}
}
this._pixiTransform = this._treeobject ? this._treeobject.transform : this._pixiobject.transform?.pixiTransform;
}
this._element = new ManipulatableObject.ElementData(this, list, parent);
}
compare(other) {
return this._treeobject == other._treeobject &&
this._pixiobject == other._pixiobject;
}
get inTree() { return this._inTree; }
get treeObject() { return this._treeobject; }
get pixiObject() { return this._pixiobject; }
get typename() { return this._typename; }
get name() {
if (this._data.name != null) return this._data.name;
if (!this._pixiobject && !this._data.hasOwnProperty("name")) return null;
else return "";
}
get active() {
if (this._pixiobject) return this._pixiobject.active;
else if (this._treeobject?.hasOwnProperty("visible")) {
return this._treeobject.visible;
} else return null;
}
set active(val) {
if (this._pixiobject) this._pixiobject.active = val;
else if (this._treeobject?.hasOwnProperty("visible")) {
this._treeobject.visible = val;
}
}
get pos() { return this._pixiTransform.position; }
get rot() { // Rotation in radians.
return this._pixiTransform ? this._pixiTransform.rotation : null;
}
set rot(val) {
if (this._pixiTransform) this._pixiTransform.rotation = val;
}
get scl() { return this._pixiTransform.scale; }
get skw() { return this._pixiTransform.skew; } // Skew in radians.
get piv() { return this._pixiTransform.pivot; }
get transLoc() { return this._pixiTransform.localTransform; }
get transWld() { return this._pixiTransform.worldTransform; }
get alpha() {
if (!this._treeobject) return null;
return this._treeobject.alpha;
}
set alpha(val) {
if (this._treeobject) this._treeobject.alpha = val;
}
get text() {
if (this._data.text != null) return this._data.text;
if (!this._data.hasOwnProperty("_text")) return null;
return this._data.text ?? "";
}
get textStyle() {
return this._data.style;
}
textStylePrepare() {
if (this._textStyleModified) return;
this._data.style = this._data.style.clone();
this._textStyleModified = true;
}
get texture() {
if (this._texture?.texture != this._data.texture) {
this._texture = this._data.texture ? new TextureManaged(this._data.texture) : null;
}
return this._texture;
}
set texture(val) {
if (!(val instanceof TextureManaged)) val = new TextureManaged(val);
this._texture = val;
this._data.texture = val.texture;
}
get skeleton() {
return this._data.skeleton;
}
get data() { return this._data; }
get element() { return this._element; }
get node() { return this._node; }
} ManipulatableObject.createInnerClasses();
class ObjectHeirarchy {
static createInnerClasses() {
ObjectHeirarchy.Node = class {
constructor(obj, heirarchy, parent) {
obj._node = this;
this._obj = obj;
this._heirarchy = heirarchy;
this._parent = parent;
this._siblingUp = null;
this._siblingDown = null;
this._children = [];
this._sortingDirty = true;
this._listTreeObjsLast = [];
this._listPixiObjsLast = [];
}
get object() { return this._obj; }
get parent() { return this._parent; }
get siblingUp() { return this._siblingUp; }
get siblingDown() { return this._siblingDown; }
get children() { return this._children; }
sortingDirtyAccept() {
if (!this._sortingDirty) return false;
this._sortingDirty = false;
return true;
}
sortChildren(addition=null) {
let listTreeObjs = this._obj.treeObject?.children ?? [];
let listPixiObjs = this._obj.pixiObject?.children ?? [];
if (addition) {
if (addition._obj.treeObject) this._listTreeObjsLast.push(addition._obj.treeObject);
if (addition._obj.pixiObject) this._listPixiObjsLast.push(addition._obj.pixiObject);
this._obj.element.childList.appendChild(addition._obj.element.item);
}
if (
this._listTreeObjsLast.length == listTreeObjs.length &&
this._listTreeObjsLast.every((e,i) => e == listTreeObjs[i]) &&
this._listPixiObjsLast.length == listPixiObjs.length &&
this._listPixiObjsLast.every((e,i) => e == listPixiObjs[i])
) return;
this._listTreeObjsLast = Array.from(listTreeObjs);
this._listPixiObjsLast = Array.from(listPixiObjs);
let items = [], extras = [];
this._children.forEach(e => {
let added = false;
e._tmp_treeInd = e._obj.treeObject ? listTreeObjs.indexOf(e._obj.treeObject) : -1;
if (e._tmp_treeInd >= 0) {
items.push(e);
added = true;
}
e._tmp_pixiInd = e._obj.pixiObject ? listPixiObjs.indexOf(e._obj.pixiObject) : -1;
if (!added) {
if (e._tmp_pixiInd >= 0) {
items.push(e);
} else extras.push(e);
}
});
items.sort((a, b) => {
let ind1 = a._tmp_pixiInd;
let ind2 = b._tmp_pixiInd;
if (ind1 < 0 || ind2 < 0) return 0;
else return ind1-ind2;
});
items.sort((a, b) => {
let ind1 = a._tmp_treeInd;
let ind2 = b._tmp_treeInd;
if (ind1 < 0 || ind2 < 0) return 0;
else return ind1-ind2;
});
let newChildren = items.concat(extras);
newChildren.forEach((e, i) => {
e._siblingUp = (i > 0) ? newChildren[i-1] : null;
e._siblingDown = (i < (newChildren.length-1)) ? newChildren[i+1] : null;
delete e._tmp_treeInd; delete e._tmp_pixiInd;
});
let sorted = this._children.length != newChildren.length ||
!this._children.every((e,i) => e == newChildren[i]);
this._children = newChildren;
if (sorted) this._sortingDirty = true;
}
add(obj) {
let node = new ObjectHeirarchy.Node(obj, this._heirarchy, this);
if (obj.treeObject) {
this._heirarchy._treeObjectsKeys.push(obj.treeObject);
this._heirarchy._treeObjectsVals.push(node);
}
if (obj.pixiObject) {
this._heirarchy._pixiObjectsKeys.push(obj.pixiObject);
this._heirarchy._pixiObjectsVals.push(node);
}
this._heirarchy._allnodes.push(node);
if (this._children.length > 0) {
let lastChild = this._children[this._children.length-1];
lastChild._siblingDown = node;
node._siblingUp = lastChild;
}
this._children.push(node);
this._obj.element.updateChildren(node);
}
removeSelf(fromRecursion=false) {
this._children.forEach(c=>c.removeSelf(true));
let ind = this._heirarchy._allnodes.indexOf(this);
if (ind < 0) return;
if (this._siblingUp) this._siblingUp._siblingDown = this._siblingDown;
if (this._siblingDown) this._siblingDown._siblingUp = this._siblingUp;
this._heirarchy._allnodes.splice(ind, 1);
ind = this._heirarchy._treeObjectsVals.indexOf(this);
if (ind >= 0) {
this._heirarchy._treeObjectsKeys.splice(ind, 1);
this._heirarchy._treeObjectsVals.splice(ind, 1);
}
ind = this._heirarchy._pixiObjectsVals.indexOf(this);
if (ind >= 0) {
this._heirarchy._pixiObjectsKeys.splice(ind, 1);
this._heirarchy._pixiObjectsVals.splice(ind, 1);
}
this._obj._inTree = false;
if (!fromRecursion) {
this._obj.element.item.remove();
this._parent._children.splice(this._parent._children.indexOf(this), 1);
}
}
};
delete ObjectHeirarchy.createInnerClasses;
}
constructor(obj) {
this.add(obj, null);
}
add(obj, parent) {
if (parent != null && !(parent instanceof ObjectHeirarchy.Node)) {
if (!(parent instanceof ManipulatableObject)) throw new Error("Unusable 'parent' for adding object to heirarchy.");
if (parent.treeObject) parent = this._treeObjectsVals[this._treeObjectsKeys.indexOf(parent.treeObject)];
else if (parent.pixiObject) parent = this._pixiObjectsVals[this._pixiObjectsKeys.indexOf(parent.pixiObject)];
else throw new Error("This ManipulatableObject is incapable of being a 'parent'.");
}
if (parent) parent.add(obj);
else {
this._root = new ObjectHeirarchy.Node(obj, this, null);
if (obj.treeObject) {
this._treeObjectsKeys = [obj.treeObject];
this._treeObjectsVals = [this._root];
} else {
this._treeObjectsKeys = [];
this._treeObjectsVals = [];
}
if (obj.pixiObject) {
this._pixiObjectsKeys = [obj.pixiObject];
this._pixiObjectsVals = [this._root];
} else {
this._pixiObjectsKeys = [];
this._pixiObjectsVals = [];
}
this._allnodes = [this._root];
obj.element.list.appendChild(obj.element.item);
}
}
remove(obj) {
if (!(obj instanceof ObjectHeirarchy.Node)) {
if (obj instanceof ManipulatableObject) {
obj = this._allnodes.find(o => o.object == obj);
} else throw new Error("Value of 'obj' is not a node, cannot remove it.");
}
obj.removeSelf();
}
} ObjectHeirarchy.createInnerClasses();
let createDataitem = function(nameHTML, elementType, elementSetup, elementLayout="auto") {
let div = document.createElement('div');
let label = document.createElement('span');
let vals = document.createElement('div');
div.classList.add("dataitem");
div.appendChild(label);
div.appendChild(vals);
label.innerHTML = nameHTML;
vals.style.display = "inline-grid";
vals.style["grid-template-columns"] = elementLayout;
if (!Array.isArray(elementType)) {
let val = document.createElement(elementType);
if (!Array.isArray(elementSetup)) {
if (elementSetup) elementSetup(val, [val]);
} else if (elementSetup.length > 0) {
if (elementSetup[0]) elementSetup[0](val, [val]);
}
vals.appendChild(val);
return div;
} else if (elementType.length == 0) return div;
let elements = [];
elementType.forEach(e => {
let val = document.createElement(e);
elements.push(val);
vals.appendChild(val);
});
if (!Array.isArray(elementSetup)) {
if (elementSetup) elementSetup(elements[0], Array.from(elements));
return div;
}
elements.every((e, i) => {
if (i >= elementSetup.length) return false;
if (elementSetup[i]) elementSetup[i](e, Array.from(elements));
return true;
});
return div;
};
let dataItemDegRad = function(dataviewData, modename, onSetDegrees, onSetRadians) {
let modenameLoc = dataviewData;
while (true) {
let dotpos = modename.indexOf('.');
if (dotpos < 0) break;
modenameLoc = modenameLoc[modename.slice(0, dotpos)];
modename = modename.substring(dotpos+1);
}
return (radDeg, elements) => {
radDeg.classList.add("degtoggle");
radDeg.addEventListener("click", () => {
var mode = !modenameLoc[modename];
modenameLoc[modename] = mode;
if (mode) {
radDeg.title = "Mode: Degrees";
radDeg.classList.remove("rad");
radDeg.classList.add("deg");
onSetDegrees(dataviewData, elements);
} else {
radDeg.title = "Mode: Radians";
radDeg.classList.add("rad");
radDeg.classList.remove("deg");
onSetRadians(dataviewData, elements);
}
});
radDeg.title = "Mode: Degrees";
radDeg.classList.add("deg");
modenameLoc[modename] = true;
};
};
let treeDiv, dataview;
let treeData = null, dataviewData = null, dataviewUpdaters = null;
let selected = null;
function openObject(obj) {
if (obj instanceof ObjectHeirarchy.Node) obj = obj.object;
if (selected) selected.element.item.classList.remove("selected");
selected = obj;
while (dataview.firstChild) dataview.firstChild.remove();
dataviewData = {"extra":{},"current":{"extra":{}}};
dataviewUpdaters = [];
if (!selected) return;
selected.element.item.classList.add("selected");
{
let par = obj.node.parent;
while (par) {
if (par.object.element.childrenHidden) par.object.element.doToggle();
par = par.parent;
}
}
{
let treeRect = treeDiv.getBoundingClientRect();
let itemRect = selected.element.txt.getBoundingClientRect();
let scrollX = treeDiv.scrollLeft, scrollY = treeDiv.scrollTop;
if (itemRect.left < treeRect.left) scrollX -= treeRect.left-itemRect.left;
else if (itemRect.right > treeRect.right) scrollX += itemRect.right-treeRect.right;
if (itemRect.top < treeRect.top) scrollY -= treeRect.top-itemRect.top;
else if (itemRect.bottom > treeRect.bottom) scrollY += itemRect.bottom-treeRect.bottom;
treeDiv.scroll(scrollX, scrollY);
}
dataview.appendChild(createDataitem("Class ID", 'input', text => {
text.type = "text";
text.readOnly = true;
let clazz = Object.getPrototypeOf(obj.data);
let path = clazz.constructor.name;
while (true) {
let par = Object.getPrototypeOf(clazz);
if (!par) break;
path = par.constructor.name+' '+path;
clazz = par;
}
text.value = path;
}));
if (obj.name != null) {
dataview.appendChild(createDataitem("Name", 'input', text => {
text.type = "text";
text.value = obj.name;
text.readOnly = true;
}));
}
if (obj.active != null) {
dataview.appendChild(createDataitem("Is Active", 'input', check => {
check.type = "checkbox";
check.addEventListener("change", () => {
obj.element.checkbox.checked = obj.active = check.checked;
});
check.checked = obj.active;
dataviewData.active = check;
}));
dataviewUpdaters.push(() => {
dataviewData.active.checked = selected.active;
});
}
if (obj._pixiTransform) {
let transformDiv = document.createElement('div');
transformDiv.classList.add("sub");
transformDiv.appendChild(createDataitem("Position", ['span', 'input', 'span', 'input'], [
label => { label.innerHTML = "X:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
dataviewData.current.posX = obj.pos.x = input.valueAsNumber;
});
input.value = dataviewData.current.posX = obj.pos.x;
dataviewData.posX = input;
},
label => { label.innerHTML = ", Y:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
dataviewData.current.posY = obj.pos.y = input.valueAsNumber;
});
input.value = dataviewData.current.posY = obj.pos.y;
dataviewData.posY = input;
}
], "auto minmax(0, 1fr) auto minmax(0, 1fr)"));
dataviewUpdaters.push(() => {
if (dataviewData.posX.value == dataviewData.current.posX) {
dataviewData.posX.value = dataviewData.current.posX = selected.pos.x;
}
if (dataviewData.posY.value == dataviewData.current.posY) {
dataviewData.posY.value = dataviewData.current.posY = selected.pos.y;
}
});
transformDiv.appendChild(createDataitem("Rotation", ['input', 'button'], [
input => {
input.type = "number";
input.addEventListener("change", () => {
let val = input.valueAsNumber;
if (dataviewData.rot_degmode) val *= DEG2RAD;
dataviewData.current.rot = obj.rot = val;
});
input.value = dataviewData.current.rot = obj.rot * RAD2DEG;
dataviewData.rot = input;
}, dataItemDegRad(dataviewData, "rot_degmode", (dataviewData, elements) => {
elements[0].value = dataviewData.current.rot = obj.rot * RAD2DEG;
dataviewData.rot.step = 1;
}, (dataviewData, elements) => {
elements[0].value = dataviewData.current.rot = obj.rot;
dataviewData.rot.step = DEG2RAD;
})
], "minmax(0, 1fr) auto"));
dataviewUpdaters.push(() => {
if (dataviewData.rot.value != dataviewData.current.rot) return;
let val = selected.rot;
if (dataviewData.rot_degmode) val *= RAD2DEG;
dataviewData.rot.value = dataviewData.current.rot = val;
});
transformDiv.appendChild(createDataitem("Scale", ['span', 'input', 'span', 'input'], [
label => { label.innerHTML = "X:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
dataviewData.current.sclX = obj.scl.x = input.valueAsNumber;
});
input.value = dataviewData.current.sclX = obj.scl.x;
dataviewData.sclX = input;
},
label => { label.innerHTML = ", Y:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
dataviewData.current.sclY = obj.scl.y = input.valueAsNumber;
});
input.value = dataviewData.current.sclY = obj.scl.y;
dataviewData.sclY = input;
}
], "auto minmax(0, 1fr) auto minmax(0, 1fr)"));
dataviewUpdaters.push(() => {
if (dataviewData.sclX.value == dataviewData.current.sclX) {
dataviewData.sclX.value = dataviewData.current.sclX = selected.scl.x;
}
if (dataviewData.sclY.value == dataviewData.current.sclY) {
dataviewData.sclY.value = dataviewData.current.sclY = selected.scl.y;
}
});
transformDiv.appendChild(createDataitem("Skew", ['span', 'input', 'span', 'input', 'button'], [
label => { label.innerHTML = "X:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
let val = input.valueAsNumber;
if (dataviewData.skw_degmode) val *= DEG2RAD;
dataviewData.current.skwX = obj.skw.x = val;
});
input.step = 1;
input.value = dataviewData.current.skwX = obj.skw.x * RAD2DEG;
dataviewData.skwX = input;
},
label => { label.innerHTML = ", Y:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
let val = input.valueAsNumber;
if (dataviewData.skw_degmode) val *= DEG2RAD;
dataviewData.current.skwY = obj.skw.y = val;
});
input.step = 1;
input.value = dataviewData.current.skwY = obj.skw.y * RAD2DEG;
dataviewData.skwY = input;
}, dataItemDegRad(dataviewData, "skw_degmode", (dataviewData, elements) => {
elements[1].value = dataviewData.current.skwX = obj.skw.x * RAD2DEG;
elements[3].value = dataviewData.current.skwY = obj.skw.y * RAD2DEG;
dataviewData.skwX.step = dataviewData.skwY.step = 1;
}, (dataviewData, elements) => {
elements[1].value = dataviewData.current.skwX = obj.skw.x;
elements[3].value = dataviewData.current.skwY = obj.skw.y;
dataviewData.skwX.step = dataviewData.skwY.step = DEG2RAD;
})
], "auto minmax(0, 1fr) auto minmax(0, 1fr) auto"));
dataviewUpdaters.push(() => {
if (dataviewData.skwX.value == dataviewData.current.skwX) {
let val = selected.skw.x;
if (dataviewData.skw_degmode) val *= RAD2DEG;
dataviewData.skwX.value = dataviewData.current.skwX = val;
}
if (dataviewData.skwY.value == dataviewData.current.skwY) {
let val = selected.skw.y;
if (dataviewData.skw_degmode) val *= RAD2DEG;
dataviewData.skwY.value = dataviewData.current.skwY = val;
}
});
transformDiv.appendChild(createDataitem("Pivot", ['span', 'input', 'span', 'input'], [
label => { label.innerHTML = "X:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
dataviewData.current.pivX = obj.piv.x = input.valueAsNumber;
});
input.value = dataviewData.current.pivX = obj.piv.x;
dataviewData.pivX = input;
},
label => { label.innerHTML = ", Y:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
dataviewData.current.pivY = obj.piv.y = input.valueAsNumber;
});
input.value = dataviewData.current.pivY = obj.piv.y;
dataviewData.pivY = input;
}
], "auto minmax(0, 1fr) auto minmax(0, 1fr)"));
dataviewUpdaters.push(() => {
if (dataviewData.pivX.value == dataviewData.current.pivX) {
dataviewData.pivX.value = dataviewData.current.pivX = selected.piv.x;
}
if (dataviewData.pivY.value == dataviewData.current.pivY) {
dataviewData.pivY.value = dataviewData.current.pivY = selected.piv.y;
}
});
if (obj.text != null) {
let transLoc = createDataitem("Local Transform", ['span', 'input', 'span', 'input', 'span', 'input', 'span', 'input', 'span', 'input', 'span', 'input'], [
label => { label.innerHTML = "PosX:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
dataviewData.current.transLocTX = obj.transLoc.tx = input.valueAsNumber;
obj.data.dirty = true;
});
input.value = dataviewData.current.transLocTX = obj.transLoc.tx;
dataviewData.transLocTX = input;
},
label => { label.innerHTML = ", PosY:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
dataviewData.current.transLocTY = obj.transLoc.ty = input.valueAsNumber;
obj.data.dirty = true;
});
input.value = dataviewData.current.transLocTY = obj.transLoc.ty;
dataviewData.transLocTY = input;
},
label => { label.innerHTML = "SclX:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
dataviewData.current.transLocA = obj.transLoc.a = input.valueAsNumber;
obj.data.dirty = true;
});
input.value = dataviewData.current.transLocA = obj.transLoc.a;
dataviewData.transLocA = input;
},
label => { label.innerHTML = ", SclY:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
dataviewData.current.transLocD = obj.transLoc.d = input.valueAsNumber;
obj.data.dirty = true;
});
input.value = dataviewData.current.transLocD = obj.transLoc.d;
dataviewData.transLocD = input;
},
label => { label.innerHTML = "SkwX:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
dataviewData.current.transLocC = obj.transLoc.c = input.valueAsNumber;
obj.data.dirty = true;
});
input.value = dataviewData.current.transLocC = obj.transLoc.c;
dataviewData.transLocC = input;
},
label => { label.innerHTML = ", SkwY:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
dataviewData.current.transLocB = obj.transLoc.b = input.valueAsNumber;
obj.data.dirty = true;
});
input.value = dataviewData.current.transLocB = obj.transLoc.b;
dataviewData.transLocB = input;
}
], "auto minmax(0, 1fr) auto minmax(0, 1fr)");
transLoc.style.height = "75px";
transformDiv.appendChild(transLoc);
dataviewUpdaters.push(() => {
if (dataviewData.transLocA.value == dataviewData.current.transLocA) {
dataviewData.transLocA.value = dataviewData.current.transLocA = selected.transLoc.a;
}
if (dataviewData.transLocB.value == dataviewData.current.transLocB) {
dataviewData.transLocB.value = dataviewData.current.transLocB = selected.transLoc.b;
}
if (dataviewData.transLocC.value == dataviewData.current.transLocC) {
dataviewData.transLocC.value = dataviewData.current.transLocC = selected.transLoc.c;
}
if (dataviewData.transLocD.value == dataviewData.current.transLocD) {
dataviewData.transLocD.value = dataviewData.current.transLocD = selected.transLoc.d;
}
if (dataviewData.transLocTX.value == dataviewData.current.transLocTX) {
dataviewData.transLocTX.value = dataviewData.current.transLocTX = selected.transLoc.tx;
}
if (dataviewData.transLocTY.value == dataviewData.current.transLocTY) {
dataviewData.transLocTY.value = dataviewData.current.transLocTY = selected.transLoc.ty;
}
});
let transWld = createDataitem("World Transform", ['span', 'input', 'span', 'input', 'span', 'input', 'span', 'input', 'span', 'input', 'span', 'input'], [
label => { label.innerHTML = "PosX:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
dataviewData.current.transWldTX = obj.transWld.tx = input.valueAsNumber;
obj.data.dirty = true;
});
input.value = dataviewData.current.transWldTX = obj.transWld.tx;
dataviewData.transWldTX = input;
},
label => { label.innerHTML = ", PosY:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
dataviewData.current.transWldTY = obj.transWld.ty = input.valueAsNumber;
obj.data.dirty = true;
});
input.value = dataviewData.current.transWldTY = obj.transWld.ty;
dataviewData.transWldTY = input;
},
label => { label.innerHTML = "SclX:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
dataviewData.current.transWldA = obj.transWld.a = input.valueAsNumber;
obj.data.dirty = true;
});
input.value = dataviewData.current.transWldA = obj.transWld.a;
dataviewData.transWldA = input;
},
label => { label.innerHTML = ", SclY:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
dataviewData.current.transWldD = obj.transWld.d = input.valueAsNumber;
obj.data.dirty = true;
});
input.value = dataviewData.current.transWldD = obj.transWld.d;
dataviewData.transWldD = input;
},
label => { label.innerHTML = "SkwX:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
dataviewData.current.transWldC = obj.transWld.c = input.valueAsNumber;
obj.data.dirty = true;
});
input.value = dataviewData.current.transWldC = obj.transWld.c;
dataviewData.transWldC = input;
},
label => { label.innerHTML = ", SkwY:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
dataviewData.current.transWldB = obj.transWld.b = input.valueAsNumber;
obj.data.dirty = true;
});
input.value = dataviewData.current.transWldB = obj.transWld.b;
dataviewData.transWldB = input;
}
], "auto minmax(0, 1fr) auto minmax(0, 1fr)");
transWld.style.height = "75px";
transformDiv.appendChild(transWld);
dataviewUpdaters.push(() => {
if (dataviewData.transWldA.value == dataviewData.current.transWldA) {
dataviewData.transWldA.value = dataviewData.current.transWldA = selected.transWld.a;
}
if (dataviewData.transWldB.value == dataviewData.current.transWldb) {
dataviewData.transWldB.value = dataviewData.current.transWldB = selected.transWld.b;
}
if (dataviewData.transWldC.value == dataviewData.current.transLocC) {
dataviewData.transWldC.value = dataviewData.current.transLocC = selected.transWld.c;
}
if (dataviewData.transWldD.value == dataviewData.current.transWldD) {
dataviewData.transWldD.value = dataviewData.current.transWldD = selected.transWld.d;
}
if (dataviewData.transWldTX.value == dataviewData.current.transWldTX) {
dataviewData.transWldTX.value = dataviewData.current.transWldTX = selected.transWld.tx;
}
if (dataviewData.transWldTY.value == dataviewData.current.transWldTY) {
dataviewData.transWldTY.value = dataviewData.current.transWldTY = selected.transWld.ty;
}
});
}
dataview.appendChild(transformDiv);
}
if (obj.alpha != null) {
dataview.appendChild(createDataitem("Opacity", ['input', 'input'], [
(slider, elements) => {
slider.type = "range";
slider.min = 0;
slider.max = 1;
slider.step = 0.001;
slider.value = obj.alpha;
slider.addEventListener("input", () => {
let val = slider.valueAsNumber;
obj.alpha = val;
elements[1].value = dataviewData.current.alphaslider = dataviewData.current.alpha = val;
});
dataviewData.current.alphaslider = Math.min(Math.max(obj.alpha, 0), 1);
slider.value = dataviewData.current.alphaslider;
dataviewData.alphaslider = slider;
slider.style["min-width"] = "50px";
},
(input, elements) => {
input.type = "number";
input.step = 0.1;
input.addEventListener("change", () => {
let val = dataviewData.current.alpha = input.valueAsNumber;
obj.alpha = val;
elements[0].value = Math.min(Math.max(val, 0), 1);
});
input.value = dataviewData.current.alpha = obj.alpha;
dataviewData.alpha = input;
input.style["min-width"] = 0;
}
], "auto 75px"));
dataviewUpdaters.push(() => {
if (dataviewData.alpha.value != dataviewData.current.alpha) return;
let val = dataviewData.current.alpha = selected.alpha;
dataviewData.alphaslider.value = val;
dataviewData.alpha.value = val;
});
}
if (obj.text != null) {
let item = createDataitem("Text", 'textarea', [
(text, elements) => {
text.addEventListener("change", () => {
obj.data.text = dataviewData.current.extra.text = text.value;
});
text.value = dataviewData.current.extra.text = obj.data.text;
dataviewData.extra.text = text;
text.style["min-width"] = 0;
}
]);
item.style["min-height"] = item.style.height;
item.style.height = "auto";
dataview.appendChild(item);
dataviewUpdaters.push(() => {
if (dataviewData.extra.text.value != dataviewData.current.extra.text) return;
dataviewData.extra.text.value = dataviewData.current.extra.text = selected.data.text;
});
dataviewData.textStyle = {};
dataviewData.current.textStyle = {};
let styleDiv = document.createElement('div');
styleDiv.classList.add("sub");
styleDiv.appendChild(createDataitem("Font", 'input', input => {
input.type = "text";
input.addEventListener("change", () => {
obj.textStylePrepare();
dataviewData.current.textStyle.font = obj.textStyle.fontFamily = input.value;
});
input.value = dataviewData.current.textStyle.font = obj.textStyle.fontFamily;
dataviewData.textStyle.font = input;
}, "minmax(0, 1fr) auto"));
dataviewUpdaters.push(() => {
if (dataviewData.textStyle.font.value != dataviewData.current.textStyle.font) return;
dataviewData.textStyle.font.value = dataviewData.current.textStyle.font = selected.textStyle.fontFamily;
});
styleDiv.appendChild(createDataitem("Font Size", 'input', input => {
input.type = "text";
let pxReg = new RegExp("\\dpx$");
input.addEventListener("change", () => {
obj.textStylePrepare();
let val = input.value;
dataviewData.current.textStyle.fontSize = val;
if (val.match(pxReg)) {
obj.textStyle.fontSize = parseInt(val.slice(0, -2));
} else {
let valnum = parseInt(val);
if (!isNaN(valnum)) {
obj.textStyle.fontSize = valnum+"px";
} else {
obj.textStyle.fontSize = val;
}
}
obj.textStyle.fontSize = val;
});
let fontval = obj.textStyle.fontSize;
if (typeof(fontval) == 'number') fontval = fontval+"px";
input.value = dataviewData.current.textStyle.fontSize = fontval;
dataviewData.textStyle.fontSize = input;
}, "minmax(0, 1fr) auto"));
dataviewUpdaters.push(() => {
if (dataviewData.textStyle.fontSize.value != dataviewData.current.textStyle.fontSize) return;
let fontval = selected.textStyle.fontSize;
if (typeof(fontval) == 'number') fontval = fontval+"px";
dataviewData.textStyle.fontSize.value = dataviewData.current.textStyle.fontSize = fontval;
});
styleDiv.appendChild(createDataitem("Font Style", 'select', dropdown => {
[['normal', "Normal"], ['italic', "Italic"], ['oblique', "Oblique"]].forEach(vals => {
let opt = document.createElement('option');
opt.innerHTML = vals[1];
opt.value = vals[0];
dropdown.appendChild(opt);
});
dropdown.addEventListener("change", () => {
obj.textStylePrepare();
obj.textStyle.fontStyle = dropdown.value;
});
dropdown.value = obj.textStyle.fontStyle;
dataviewData.textStyle.fontStyle = dropdown;
}));
dataviewUpdaters.push(() => {
dataviewData.textStyle.fontStyle.value = selected.textStyle.fontStyle;
});
styleDiv.appendChild(createDataitem("Font Variant", 'select', dropdown => {
[['normal', "Normal"], ['small-caps', "Small Caps"]].forEach(vals => {
let opt = document.createElement('option');
opt.innerHTML = vals[1];
opt.value = vals[0];
dropdown.appendChild(opt);
});
dropdown.addEventListener("change", () => {
obj.textStylePrepare();
obj.textStyle.fontVariant = dropdown.value;
});
dropdown.value = obj.textStyle.fontVariant;
dataviewData.textStyle.fontVariant = dropdown;
}));
dataviewUpdaters.push(() => {
dataviewData.textStyle.fontVariant.value = selected.textStyle.fontVariant;
});
styleDiv.appendChild(createDataitem("Font Weight", 'select', dropdown => {
[['normal', "Normal"], ['bold', "Bold"], ['bolder', "Bolder (+100)"], ['lighter', "Lighter (-100)"], ['100', "100"], ['200', "200"], ['300', "400"], ['400', "400 (Normal)"], ['500', "500"], ['600', "600"], ['700', "700 (Bold)"], ['800', "800"], ['900', "900"]].forEach(vals => {
let opt = document.createElement('option');
opt.innerHTML = vals[1];
opt.value = vals[0];
dropdown.appendChild(opt);
});
dropdown.addEventListener("change", () => {
obj.textStylePrepare();
obj.textStyle.fontWeight = dropdown.value;
});
dropdown.value = obj.textStyle.fontWeight;
dataviewData.textStyle.fontWeight = dropdown;
}));
dataviewUpdaters.push(() => {
dataviewData.textStyle.fontWeight.value = selected.textStyle.fontWeight;
});
{
let color = intFromColor(obj.textStyle.stroke);
dataviewData.textStyle.strokeColor = {};
dataviewData.current.textStyle.strokeColor = {};
styleDiv.appendChild(createDataitem("Stroke Color", ['div', 'span', 'input', 'span', 'input', 'span', 'input'], [
colorview => {
colorview.classList.add("colorview");
colorview.style.width = colorview.style.height = "19px";
colorview.style["background-color"] = obj.textStyle.stroke;
dataviewData.textStyle.strokeColor.view = colorview;
},
label => { label.innerHTML = "R:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
obj.textStylePrepare();
let val = Math.min(Math.max(Math.round(input.valueAsNumber), 0), 255);
input.value = dataviewData.current.textStyle.strokeColor.r = val;
color = (color & 0x00FFFF) | (val << 16);
obj.textStyle.stroke = color;
});
input.value = dataviewData.current.textStyle.strokeColor.r = color >> 16;
dataviewData.textStyle.strokeColor.r = input;
},
label => { label.innerHTML = ", G:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
obj.textStylePrepare();
let val = Math.min(Math.max(Math.round(input.valueAsNumber), 0), 255);
input.value = dataviewData.current.textStyle.strokeColor.g = val;
color = (color & 0xFF00FF) | (val << 8);
obj.textStyle.stroke = color;
});
input.value = dataviewData.current.textStyle.strokeColor.g = (color >> 8) & 0xFF;
dataviewData.textStyle.strokeColor.g = input;
},
label => { label.innerHTML = ", B:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
obj.textStylePrepare();
let val = Math.min(Math.max(Math.round(input.valueAsNumber), 0), 255);
input.value = dataviewData.current.textStyle.strokeColor.b = val;
color = (color & 0xFFFF00) | val;
obj.textStyle.stroke = color;
});
input.value = dataviewData.current.textStyle.strokeColor.b = color & 0xFF;
dataviewData.textStyle.strokeColor.b = input;
}
], "auto auto minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr)"));
}
dataviewUpdaters.push(() => {
let color = intFromColor(selected.textStyle.stroke);
dataviewData.textStyle.strokeColor.view.style["background-color"] = selected.textStyle.stroke;
if (dataviewData.textStyle.strokeColor.r.value == dataviewData.current.textStyle.strokeColor.r) {
dataviewData.textStyle.strokeColor.r.value = dataviewData.current.textStyle.strokeColor.r = color >> 16;
}
if (dataviewData.textStyle.strokeColor.g.value == dataviewData.current.textStyle.strokeColor.g) {
dataviewData.textStyle.strokeColor.g.value = dataviewData.current.textStyle.strokeColor.g = (color >> 8) & 0xFF;
}
if (dataviewData.textStyle.strokeColor.b.value == dataviewData.current.textStyle.strokeColor.b) {
dataviewData.textStyle.strokeColor.b.value = dataviewData.current.textStyle.strokeColor.b = color & 0xFF;
}
});
styleDiv.appendChild(createDataitem("Stroke Thickness", 'input', input => {
input.type = "number";
input.addEventListener("change", () => {
obj.textStylePrepare();
dataviewData.current.textStyle.strokeThickness = obj.textStyle.strokeThickness = input.valueAsNumber;
});
input.value = dataviewData.current.textStyle.strokeThickness = obj.textStyle.strokeThickness;
dataviewData.textStyle.strokeThickness = input;
}, "minmax(0, 1fr) auto"));
dataviewUpdaters.push(() => {
if (dataviewData.textStyle.strokeThickness.value != dataviewData.current.textStyle.strokeThickness) return;
dataviewData.textStyle.strokeThickness.value = dataviewData.current.textStyle.strokeThickness = selected.textStyle.strokeThickness;
});
{
let color = intFromColor(obj.textStyle.fill);
dataviewData.textStyle.fillColor = {};
dataviewData.current.textStyle.fillColor = {};
styleDiv.appendChild(createDataitem("Fill Color", ['div', 'span', 'input', 'span', 'input', 'span', 'input'], [
colorview => {
colorview.classList.add("colorview");
colorview.style.width = colorview.style.height = "19px";
colorview.style["background-color"] = obj.textStyle.fill;
dataviewData.textStyle.fillColor.view = colorview;
},
label => { label.innerHTML = "R:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
obj.textStylePrepare();
let val = Math.min(Math.max(Math.round(input.valueAsNumber), 0), 255);
input.value = dataviewData.current.textStyle.fillColor.r = val;
color = (color & 0x00FFFF) | (val << 16);
obj.textStyle.fill = color;
});
input.value = dataviewData.current.textStyle.fillColor.r = color >> 16;
dataviewData.textStyle.fillColor.r = input;
},
label => { label.innerHTML = ", G:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
obj.textStylePrepare();
let val = Math.min(Math.max(Math.round(input.valueAsNumber), 0), 255);
input.value = dataviewData.current.textStyle.fillColor.g = val;
color = (color & 0xFF00FF) | (val << 8);
obj.textStyle.fill = color;
});
input.value = dataviewData.current.textStyle.fillColor.g = (color >> 8) & 0xFF;
dataviewData.textStyle.fillColor.g = input;
},
label => { label.innerHTML = ", B:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
obj.textStylePrepare();
let val = Math.min(Math.max(Math.round(input.valueAsNumber), 0), 255);
input.value = dataviewData.current.textStyle.fillColor.b = val;
color = (color & 0xFFFF00) | val;
obj.textStyle.fill = color;
});
input.value = dataviewData.current.textStyle.fillColor.b = color & 0xFF;
dataviewData.textStyle.fillColor.b = input;
}
], "auto auto minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr)"));
}
dataviewUpdaters.push(() => {
let color = intFromColor(selected.textStyle.fill);
dataviewData.textStyle.fillColor.view.style["background-color"] = selected.textStyle.fill;
if (dataviewData.textStyle.fillColor.r.value == dataviewData.current.textStyle.fillColor.r) {
dataviewData.textStyle.fillColor.r.value = dataviewData.current.textStyle.fillColor.r = color >> 16;
}
if (dataviewData.textStyle.fillColor.g.value == dataviewData.current.textStyle.fillColor.g) {
dataviewData.textStyle.fillColor.g.value = dataviewData.current.textStyle.fillColor.g = (color >> 8) & 0xFF;
}
if (dataviewData.textStyle.fillColor.b.value == dataviewData.current.textStyle.fillColor.b) {
dataviewData.textStyle.fillColor.b.value = dataviewData.current.textStyle.fillColor.b = color & 0xFF;
}
});
styleDiv.appendChild(createDataitem("Align", 'select', dropdown => {
[['left', "Left"], ['center', "Center"], ['right', "Right"], ['justify', "Justify"]].forEach(vals => {
let opt = document.createElement('option');
opt.innerHTML = vals[1];
opt.value = vals[0];
dropdown.appendChild(opt);
});
dropdown.addEventListener("change", () => {
obj.textStylePrepare();
obj.textStyle.align = dropdown.value;
});
dropdown.value = obj.textStyle.align;
dataviewData.textStyle.align = dropdown;
}));
dataviewUpdaters.push(() => {
dataviewData.textStyle.align.value = selected.textStyle.align;
});
styleDiv.appendChild(createDataitem("Drop Shadow", 'input', check => {
check.type = "checkbox";
check.addEventListener("change", () => {
obj.textStylePrepare();
obj.textStyle.dropShadow = check.checked;
});
check.checked = obj.textStyle.dropShadow;
dataviewData.textStyle.hasDropShadow = check;
}));
dataviewUpdaters.push(() => {
dataviewData.textStyle.hasDropShadow.checked = selected.textStyle.dropShadow;
});
{
let color = intFromColor(obj.textStyle.dropShadowColor);
dataviewData.textStyle.dropShadowColor = {};
dataviewData.current.textStyle.dropShadowColor = {};
styleDiv.appendChild(createDataitem("Drop Shadow Color", ['div', 'span', 'input', 'span', 'input', 'span', 'input', 'span', 'input'], [
colorview => {
colorview.classList.add("colorview");
colorview.style.width = colorview.style.height = "19px";
colorview.style["background-color"] = obj.textStyle.dropShadowColor;
dataviewData.textStyle.dropShadowColor.view = colorview;
},
label => { label.innerHTML = "R:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
obj.textStylePrepare();
let val = Math.min(Math.max(Math.round(input.valueAsNumber), 0), 255);
input.value = dataviewData.current.textStyle.dropShadowColor.r = val;
color = (color & 0x00FFFF) | (val << 16);
obj.textStyle.dropShadowColor = color;
});
input.value = dataviewData.current.textStyle.dropShadowColor.r = color >> 16;
dataviewData.textStyle.dropShadowColor.r = input;
},
label => { label.innerHTML = ", G:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
obj.textStylePrepare();
let val = Math.min(Math.max(Math.round(input.valueAsNumber), 0), 255);
input.value = dataviewData.current.textStyle.dropShadowColor.g = val;
color = (color & 0xFF00FF) | (val << 8);
obj.textStyle.dropShadowColor = color;
});
input.value = dataviewData.current.textStyle.dropShadowColor.g = (color >> 8) & 0xFF;
dataviewData.textStyle.dropShadowColor.g = input;
},
label => { label.innerHTML = ", B:"; },
input => {
input.type = "number";
input.addEventListener("change", () => {
obj.textStylePrepare();
let val = Math.min(Math.max(Math.round(input.valueAsNumber), 0), 255);
input.value = dataviewData.current.textStyle.dropShadowColor.b = val;
color = (color & 0xFFFF00) | val;
obj.textStyle.dropShadowColor = color;
});
input.value = dataviewData.current.textStyle.dropShadowColor.b = color & 0xFF;
dataviewData.textStyle.dropShadowColor.b = input;
},
label => { label.innerHTML = ", A:"; },
input => {
input.type = "number";
input.step = 0.1;
input.addEventListener("change", () => {
obj.textStylePrepare();
let val = Math.min(Math.max(input.valueAsNumber, 0), 1);
input.value = dataviewData.current.textStyle.dropShadowColor.a = val;
obj.textStyle.dropShadowAlpha = val;
});
input.value = dataviewData.current.textStyle.dropShadowColor.a = obj.textStyle.dropShadowAlpha;
dataviewData.textStyle.dropShadowColor.a = input;
}
], "auto auto minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr)"));
}
dataviewUpdaters.push(() => {
let color = intFromColor(selected.textStyle.dropShadowColor);
dataviewData.textStyle.dropShadowColor.view.style["background-color"] = selected.textStyle.dropShadowColor;
if (dataviewData.textStyle.dropShadowColor.r.value == dataviewData.current.textStyle.dropShadowColor.r) {
dataviewData.textStyle.dropShadowColor.r.value = dataviewData.current.textStyle.dropShadowColor.r = color >> 16;
}
if (dataviewData.textStyle.dropShadowColor.g.value == dataviewData.current.textStyle.dropShadowColor.g) {
dataviewData.textStyle.dropShadowColor.g.value = dataviewData.current.textStyle.dropShadowColor.g = (color >> 8) & 0xFF;
}
if (dataviewData.textStyle.dropShadowColor.b.value == dataviewData.current.textStyle.dropShadowColor.b) {
dataviewData.textStyle.dropShadowColor.b.value = dataviewData.current.textStyle.dropShadowColor.b = color & 0xFF;
}
if (dataviewData.textStyle.dropShadowColor.a.value == dataviewData.current.textStyle.dropShadowColor.a) {
dataviewData.textStyle.dropShadowColor.a.value = dataviewData.current.textStyle.dropShadowColor.a = selected.textStyle.dropShadowAlpha;
}
});
styleDiv.appendChild(createDataitem("Drop Shadow Angle", ['input', 'button'], [
input => {
input.type = "number";
input.addEventListener("change", () => {
obj.textStylePrepare();
let val = input.valueAsNumber;
if (dataviewData.textStyle.dropShadowRot_degmode) val *= DEG2RAD;
dataviewData.current.textStyle.dropShadowAngle = obj.textStyle.dropShadowAngle = val;
});
input.value = dataviewData.current.textStyle.dropShadowAngle = obj.textStyle.dropShadowAngle * RAD2DEG;
dataviewData.textStyle.dropShadowAngle = input;
}, dataItemDegRad(dataviewData, "textStyle.dropShadowRot_degmode", (dataviewData, elements) => {
elements[0].value = dataviewData.current.textStyle.dropShadowAngle = obj.textStyle.dropShadowAngle * RAD2DEG;
dataviewData.textStyle.dropShadowAngle.step = 1;
}, (dataviewData, elements) => {
elements[0].value = dataviewData.current.textStyle.dropShadowAngle = obj.textStyle.dropShadowAngle;
dataviewData.textStyle.dropShadowAngle.step = DEG2RAD;
})
], "minmax(0, 1fr) auto"));
dataviewUpdaters.push(() => {
if (dataviewData.textStyle.dropShadowAngle.value != dataviewData.current.textStyle.dropShadowAngle) return;
let val = selected.textStyle.dropShadowAngle;
if (dataviewData.textStyle.dropShadowRot_degmode) val *= RAD2DEG;
dataviewData.textStyle.dropShadowAngle.value = dataviewData.current.textStyle.dropShadowAngle = val;
});
styleDiv.appendChild(createDataitem("Drop Shadow Blur", 'input', input => {
input.type = "number";
input.addEventListener("change", () => {
obj.textStylePrepare();
dataviewData.current.textStyle.dropShadowBlur = obj.textStyle.dropShadowBlur = input.valueAsNumber;
});
input.value = dataviewData.current.textStyle.dropShadowBlur = obj.textStyle.dropShadowBlur;
dataviewData.textStyle.dropShadowBlur = input;
}, "minmax(0, 1fr) auto"));
dataviewUpdaters.push(() => {
if (dataviewData.textStyle.dropShadowBlur.value != dataviewData.current.textStyle.dropShadowBlur) return;
dataviewData.textStyle.dropShadowBlur.value = dataviewData.current.textStyle.dropShadowBlur = selected.textStyle.dropShadowBlur;
});
styleDiv.appendChild(createDataitem("Drop Shadow Distance", 'input', input => {
input.type = "number";
input.addEventListener("change", () => {
obj.textStylePrepare();
dataviewData.current.textStyle.dropShadowDistance = obj.textStyle.dropShadowDistance = input.valueAsNumber;
});
input.value = dataviewData.current.textStyle.dropShadowDistance = obj.textStyle.dropShadowDistance;
dataviewData.textStyle.dropShadowDistance = input;
}, "minmax(0, 1fr) auto"));
dataviewUpdaters.push(() => {
if (dataviewData.textStyle.dropShadowDistance.value != dataviewData.current.textStyle.dropShadowDistance) return;
dataviewData.textStyle.dropShadowDistance.value = dataviewData.current.textStyle.dropShadowDistance = selected.textStyle.dropShadowDistance;
});
styleDiv.appendChild(createDataitem("Letter Spacing", 'input', input => {
input.type = "number";
input.addEventListener("change", () => {
obj.textStylePrepare();
dataviewData.current.textStyle.letterSpacing = obj.textStyle.letterSpacing = input.valueAsNumber;
});
input.value = dataviewData.current.textStyle.letterSpacing = obj.textStyle.letterSpacing;
dataviewData.textStyle.letterSpacing = input;
}, "minmax(0, 1fr) auto"));
dataviewUpdaters.push(() => {
if (dataviewData.textStyle.letterSpacing.value != dataviewData.current.textStyle.letterSpacing) return;
dataviewData.textStyle.letterSpacing.value = dataviewData.current.textStyle.letterSpacing = selected.textStyle.letterSpacing;
});
styleDiv.appendChild(createDataitem("Line Spacing", 'input', input => {
input.type = "number";
input.addEventListener("change", () => {
obj.textStylePrepare();
dataviewData.current.textStyle.lineSpacing = obj.textStyle.leading = input.valueAsNumber;
});
input.value = dataviewData.current.textStyle.lineSpacing = obj.textStyle.leading;
dataviewData.textStyle.lineSpacing = input;
}, "minmax(0, 1fr) auto"));
dataviewUpdaters.push(() => {
if (dataviewData.textStyle.lineSpacing.value != dataviewData.current.textStyle.lineSpacing) return;
dataviewData.textStyle.lineSpacing.value = dataviewData.current.textStyle.lineSpacing = selected.textStyle.leading;
});
styleDiv.appendChild(createDataitem("Line Spacing 2", 'input', input => {
input.type = "number";
input.addEventListener("change", () => {
obj.textStylePrepare();
dataviewData.current.textStyle.letterHeight = obj.textStyle.lineHeight = input.valueAsNumber;
});
input.value = dataviewData.current.textStyle.letterHeight = obj.textStyle.lineHeight;
dataviewData.textStyle.letterHeight = input;
}, "minmax(0, 1fr) auto"));
dataviewUpdaters.push(() => {
if (dataviewData.textStyle.letterHeight.value != dataviewData.current.textStyle.letterHeight) return;
dataviewData.textStyle.letterHeight.value = dataviewData.current.textStyle.letterHeight = selected.textStyle.lineHeight;
});
styleDiv.appendChild(createDataitem("Baseline", 'select', dropdown => {
[['alphabetic', "Alphabetic"], ['top', "Top"], ['hanging', "Hanging"], ['middle', "Middle"], ['ideographic', "Ideographic"], ['bottom', "Bottom"]].forEach(vals => {
let opt = document.createElement('option');
opt.innerHTML = vals[1];
opt.value = vals[0];
dropdown.appendChild(opt);
});
dropdown.addEventListener("change", () => {
obj.textStylePrepare();
obj.textStyle.textBaseline = dropdown.value;
});
dropdown.value = obj.textStyle.textBaseline;
dataviewData.textStyle.baseline = dropdown;
}));
dataviewUpdaters.push(() => {
dataviewData.textStyle.baseline.value = selected.textStyle.textBaseline;
});
styleDiv.appendChild(createDataitem("Trim Transparent Borders", 'input', check => {
check.type = "checkbox";
check.addEventListener("change", () => {
obj.textStylePrepare();
obj.textStyle.trim = check.checked;
});
check.checked = obj.textStyle.trim;
dataviewData.textStyle.trim = check;
}));
dataviewUpdaters.push(() => {
dataviewData.textStyle.trim.checked = selected.textStyle.trim;
});
styleDiv.appendChild(createDataitem("Padding", 'input', input => {
input.type = "number";
input.addEventListener("change", () => {
obj.textStylePrepare();
dataviewData.current.textStyle.padding = obj.textStyle.padding = input.valueAsNumber;
});
input.value = dataviewData.current.textStyle.padding = obj.textStyle.padding;
dataviewData.textStyle.padding = input;
}, "minmax(0, 1fr) auto"));
dataviewUpdaters.push(() => {
if (dataviewData.textStyle.padding.value != dataviewData.current.textStyle.padding) return;
dataviewData.textStyle.padding.value = dataviewData.current.textStyle.padding = selected.textStyle.padding;
});
styleDiv.appendChild(createDataitem("Whitespace Handling", 'select', dropdown => {
[['normal', "Collapse Space and Newline"], ['pre', "Preserve Space and Newline"], ['pre-wrap', "Preserve Space, Collapse Newline"], ['pre-line', "Collapse Space, Preserve Newline"]].forEach(vals => {
let opt = document.createElement('option');
opt.innerHTML = vals[1];
opt.value = vals[0];
dropdown.appendChild(opt);
});
dropdown.addEventListener("change", () => {
obj.textStylePrepare();
obj.textStyle.whiteSpace = dropdown.value;
});
dropdown.value = obj.textStyle.whiteSpace;
dataviewData.textStyle.baseline = dropdown;
}));
dataviewUpdaters.push(() => {
dataviewData.textStyle.baseline.value = selected.textStyle.whiteSpace;
});
styleDiv.appendChild(createDataitem("Word Wrap", 'input', check => {
check.type = "checkbox";
check.addEventListener("change", () => {
obj.textStylePrepare();
obj.textStyle.wordWrap = check.checked;
});
check.checked = obj.textStyle.wordWrap;
dataviewData.textStyle.wordWrap = check;
}));
dataviewUpdaters.push(() => {
dataviewData.textStyle.wordWrap.checked = selected.textStyle.wordWrap;
});
styleDiv.appendChild(createDataitem("Word Wrap Width", 'input', input => {
input.type = "number";
input.addEventListener("change", () => {
obj.textStylePrepare();
dataviewData.current.textStyle.wordWrapWidth = obj.textStyle.wordWrapWidth = input.valueAsNumber;
});
input.value = dataviewData.current.textStyle.wordWrapWidth = obj.textStyle.wordWrapWidth;
dataviewData.textStyle.wordWrapWidth = input;
}, "minmax(0, 1fr) auto"));
dataviewUpdaters.push(() => {
if (dataviewData.textStyle.wordWrapWidth.value != dataviewData.current.textStyle.wordWrapWidth) return;
dataviewData.textStyle.wordWrapWidth.value = dataviewData.current.textStyle.wordWrapWidth = selected.textStyle.wordWrapWidth;
});
styleDiv.appendChild(createDataitem("Break Words", 'input', check => {
check.type = "checkbox";
check.addEventListener("change", () => {
obj.textStylePrepare();
obj.textStyle.breakWords = check.checked;
});
check.checked = obj.textStyle.breakWords;
dataviewData.textStyle.breakWords = check;
}));
dataviewUpdaters.push(() => {
dataviewData.textStyle.breakWords.checked = selected.textStyle.breakWords;
});
styleDiv.appendChild(createDataitem("Line Join", 'select', dropdown => {
[['miter', "Miter"], ['round', "Round"], ['bevel', "Bevel"]].forEach(vals => {
let opt = document.createElement('option');
opt.innerHTML = vals[1];
opt.value = vals[0];
dropdown.appendChild(opt);
});
dropdown.addEventListener("change", () => {
obj.textStylePrepare();
obj.textStyle.lineJoin = dropdown.value;
});
dropdown.value = obj.textStyle.lineJoin;
dataviewData.textStyle.lineJoin = dropdown;
}));
dataviewUpdaters.push(() => {
dataviewData.textStyle.lineJoin.value = selected.textStyle.lineJoin;
});
styleDiv.appendChild(createDataitem("(LineJoin) Miter Limit", 'input', input => {
input.type = "number";
input.addEventListener("change", () => {
obj.textStylePrepare();
dataviewData.current.textStyle.lineJoinMiterLimit = obj.textStyle.miterLimit = input.valueAsNumber;
});
input.value = dataviewData.current.textStyle.lineJoinMiterLimit = obj.textStyle.miterLimit;
dataviewData.textStyle.lineJoinMiterLimit = input;
}, "minmax(0, 1fr) auto"));
dataviewUpdaters.push(() => {
if (dataviewData.textStyle.lineJoinMiterLimit.value != dataviewData.current.textStyle.lineJoinMiterLimit) return;
dataviewData.textStyle.lineJoinMiterLimit.value = dataviewData.current.textStyle.lineJoinMiterLimit = selected.textStyle.miterLimit;
});
dataview.appendChild(styleDiv);
}
if (obj.data.hasOwnProperty("_texture")) {
let item = createDataitem("Texture", ['div', 'a', 'a', 'button'], [
container => {
container.classList.add("imageview");
container.style["grid-column"] = "span 3";
let img = document.createElement('img');
let errMsg = document.createElement('p');
errMsg.classList.add("errmsg");
container.appendChild(img);
container.appendChild(errMsg);
dataviewData.extra.texture = img;
dataviewData.extra.texturefailure = errMsg;
},
btncont => {
btncont.target = "_blank";
btncont.rel = "noopener noreferrer";
let btn = document.createElement('button');
btn.innerHTML = "<i class=\"fa fa-external-link\"></i> Open";
btn.style.width = "100%";
btncont.appendChild(btn);
dataviewData.extra.textureOpenBtn = btn;
btncont.style["min-width"] = btn.style["min-width"] = 0;
btn.style.overflow = "clip";
},
btncont => {
let btn = document.createElement('button');
btn.innerHTML = "<i class=\"fa fa-download\"></i> Export";
btn.style.width = "100%";
btncont.appendChild(btn);
dataviewData.extra.textureExportBtn = btn;
btncont.style["min-width"] = btn.style["min-width"] = 0;
btn.style.overflow = "clip";
},
btn => {
let filecont = document.createElement('label');
filecont.innerHTML = "<i class=\"fa fa-files-o\"></i> Change";
// Source - https://stackoverflow.com/a/27165977
// Posted by nkron, modified by community. See post 'Timeline' for change history
// Retrieved 2026-04-23, License - CC BY-SA 4.0
if (selected.text != null) btn.disabled = true;
else {
let fileInp = document.createElement('input');
fileInp.style.display = "none";
fileInp.type = "file";
fileInp.accept = "image/*";
let reader = new FileReader();
reader.onload = () => {
selected.texture = window.PIXI.Texture.from(reader.result);
};
fileInp.addEventListener("change", evt => {
reader.readAsDataURL(evt.target.files[0]);
});
fileInp.style["min-width"] = 0;
filecont.appendChild(fileInp);
}
btn.appendChild(filecont);
btn.style["min-width"] = 0;
btn.style.overflow = "clip";
}
], "1fr 1fr 1fr");
item.style["min-height"] = item.style.height;
item.style.height = "auto";
dataview.appendChild(item);
dataviewUpdaters.push(() => {
if (dataviewData.current.extra.texture) {
if (selected.data.texture != dataviewData.current.extra.texture) selected.texture = selected.data.texture;
else if (!selected.texture?.hasChanged()) return;
}
dataviewData.extra.texture.style.display = "block";
dataviewData.extra.texturefailure.style.display = "none";
let url = selected.texture.texture.baseTexture.resource?.url;
dataviewData.extra.textureOpenBtn.disabled = !Boolean(url);
dataviewData.extra.textureOpenBtn.parentElement.href = url;
try {
let srcImgData = selected.texture.getDataUrl();
dataviewData.extra.textureExportBtn.parentElement.href = srcImgData;
let filename = selected.texture.texture.baseTexture.cacheId;
if (!filename) {
if (url) {
filename = url.substring(url.lastIndexOf('/')+1); // If there is no slash, it will be -1, which when 1 is added makes 0. This works out perfectly.
let lastDot = filename.lastIndexOf('.');
filename = filename.substring(0, lastDot);
} else filename = "texture";
}
dataviewData.extra.textureExportBtn.parentElement.download = filename+".png";
dataviewData.extra.texture.src = srcImgData;
} catch (e) {
console.error(e);
if (!dataviewData.extra.texture) return;
dataviewData.extra.texture.style.display = "none";
dataviewData.extra.texturefailure.style.display = "block";
dataviewData.extra.texturefailure.innerHTML = "Something went wrong.";
}
dataviewData.current.extra.texture = selected.data.texture;
});
}
dataview.appendChild(createDataitem("Create Child Sprite", 'button', btn => {
btn.innerHTML = "this.addChild(new PIXI.Sprite())";
btn.addEventListener("click", () => {
obj.data.addChild(new window.PIXI.Sprite());
});
}));
{
let desBtn;
dataview.appendChild(createDataitem("Destroy Object", 'button', btn => {
desBtn = btn;
btn.innerHTML = "this.destroy()";
}));
dataviewUpdaters.push(menu => {
if (!desBtn) return;
desBtn.addEventListener("click", () => {
if (!menu.confirm("Are you sure you want to destroy this object and its descendants?")) return;
selected.data.destroy();
openObject(selected.node.parent.object);
});
desBtn = null;
});
}
}
function updateTreeObject(obj) {
if (obj.name != null && obj.name.trim() != "") {
if (obj.element.txt_inner != obj.name) {
obj.element.txt_inner = obj.name;
}
} else if (obj.element.txt_inner != null) {
obj.element.txt_inner = null;
obj.element.txt.innerHTML = `<i> ${obj.typename ? obj.typename : "Unknown"}</i>`;
}
let activeVal = obj.active;
if (obj.element.checkbox && obj.element.checkbox.checked != activeVal) {
obj.element.checkbox.checked = activeVal;
}
obj.element.updateChildren();
let updatedChildren = [];
["treeObject", "pixiObject"].forEach(type => { // Probably a horrendous way of going about iterating this, but it lets me so it makes my brain happy.
if (obj[type]?.children != null) {
obj[type].children.forEach(childData => { // Finds and adds new children.
let child = obj.node.children.find(o => o.object[type] == childData)?.object;
if (!child) {
child = new ManipulatableObject(childData, obj.element.childList, obj);
obj.node.add(child);
}
updateTreeObject(child);
updatedChildren.push(child);
});
}
});
obj.node.children.forEach(child => { // Removes old children.
if (!updatedChildren.includes(child.object)) {
child.removeSelf();
}
});
}
// needed because can't open popup until there is an interaction
let createprodigytreeviewermodbutton = document.createElement('button');
document.body.appendChild(createprodigytreeviewermodbutton);
createprodigytreeviewermodbutton.innerText = "Open Tree View";
createprodigytreeviewermodbutton.style = "position: fixed; top: 10px; left: 10px;";
createprodigytreeviewermodbutton.onclick = () => {
if (!gameaccess) {
createprodigytreeviewermodbutton.remove();
console.log(`No game detected.
____________________
< How did we get here? >
--------------------
\\ ^__^
\\ (oo)\\_______
(__)\\ )\\/\\\\
||----w |
|| ||
`); // https://github.com/cowsay-org/cowsay
return;
}
// Source - https://stackoverflow.com/a/16992521
// Posted by Vadim
// Retrieved 2026-03-18, License - CC BY-SA 3.0
var menu = window.open("", "_blank", "height=500,width=900,status=yes,toolbar=0,menubar=0,location=0");
if (!menu.document.body || !menu.document.body.innerHTML) {
menu.document.write(css);
menu.document.write(`
<!-- https://www.w3schools.com/howto/howto_js_treeview.asp -->
<span style="display:none">load-bearing element; the script crashes without this</span>
`);
treeDiv = document.createElement('div');
treeDiv.classList.add("tree");
menu.document.body.appendChild(treeDiv);
menu.window.addEventListener("keydown", function (evt) {
if (!selected) return;
if (['INPUT', 'TEXTAREA'].includes(menu.document.activeElement.tagName.toUpperCase())) return;
switch (evt.key) {
case ' ': {
evt.preventDefault();
if (!selected.element.checkbox) return;
let val = !selected.active;
selected.active = val;
selected.element.checkbox.checked = val;
if (dataviewData.active) dataviewData.active.checked = val;
} break;
case "ArrowUp":
evt.preventDefault();
if (selected.node.siblingUp) {
let up = selected.node.siblingUp;
while (!up.object.element.childrenHidden && up.children.length > 0) up = up.children[up.children.length-1];
openObject(up);
} else if (selected.node.parent) openObject(selected.node.parent);
else return;
break;
case "ArrowLeft":
evt.preventDefault();
if (!selected.element.childrenHidden) selected.element.doToggle();
else if (selected.node.parent) openObject(selected.node.parent);
else return;
break;
case "ArrowDown":
evt.preventDefault();
if (!selected.element.childrenHidden && selected.node.children.length > 0) openObject(selected.node.children[0]);
else if (selected.node.siblingDown) openObject(selected.node.siblingDown);
else {
let par = selected.node.parent;
while (par && !par.siblingDown) par = par.parent;
if (par && par.siblingDown) openObject(par.siblingDown);
else return;
}
break;
case "ArrowRight":
evt.preventDefault();
if (selected.node.children.length == 0) return;
if (selected.element.childrenHidden) selected.element.doToggle();
else openObject(selected.node.children[0]);
break;
}
});
let drag = false; let moveX;
let splitter = document.createElement('div');
splitter.classList.add("splitter");
menu.document.body.appendChild(splitter);
dataview = document.createElement('div');
dataview.classList.add("dataview");
menu.document.body.appendChild(dataview);
treeDiv.style.width = "300px";
let treeDivResize = x => {
let bodyStyle = menu.window.getComputedStyle(menu.document.body);
let treeStyle = menu.window.getComputedStyle(treeDiv);
treeDiv.style.width = (x-32)+"px";
};
splitter.addEventListener("mousedown", function (evt) {
drag = true;
treeDivResize(evt.x);
});
menu.window.addEventListener("mouseup", function(evt) {
drag = false;
});
menu.window.addEventListener("mousemove", function (evt) {
if (drag) treeDivResize(evt.x);
});
menu.updateTree = setInterval(() => {
if (!gameaccess) {
menu.document.write(`
<p style="color: #F00; font-size: 20px;">Prodigy instance lost.</p>
`);
clearInterval(menu.updateTree);
return;
}
if (menu.closed) {
clearInterval(menu.updateTree);
treeData = null;
return;
}
let root = gameaccess.stage;
let pathUp = [];
/*while (root.parent != null) {
pathUp.push(root.parent.children.indexOf(root));
root = root.parent; // The stage might not be the root object.
}*/ // but tbf it's not much of interest anyway
if (treeData != null && treeData.data != root) {
openObject(null);
treeData.element.list.remove();
treeData = null;
}
let newTree = !treeData;
if (newTree) {
let treeBase = document.createElement('ul');
treeBase.style = "margin: 0; padding: 0;";
treeDiv.appendChild(treeBase);
treeData = new ManipulatableObject(root, treeBase, null);
new ObjectHeirarchy(treeData);
}
updateTreeObject(treeData);
if (newTree) {
treeData.element.doToggle();
pathUp.forEach(num => treeData.node.children[num].element.doToggle());
}
}, 100);
menu.updateData = setInterval(() => {
if (!gameaccess || menu.closed) {
clearInterval(menu.updateData);
return;
}
if (!selected?.inTree) return;
dataviewUpdaters.forEach(u => u(menu));
}, 5);
}
};
})();