UNPKG

@scidian/osui

Version:

Lightweight JavaScript UI library.

468 lines (414 loc) 18.1 kB
// -- USAGE -- // import { Gooey } from 'osui'; // const gui = new Gooey('Geometries'); // instantiate // const folder1 = gui.addFolder('Geometry'); // create folder // // -- STLYING -- // gui.color('#bb0000'); // sets gui color // gui.opacity(0.5); // background opacity // gui.scale(1.0); // scale multiplier // gui.width(10); // sets panel width // // -- PROPERTIES -- // const params = {q // myFunction: () => console.log('hello world'), // myBoolean: true, // myListInt: 0, // myListString: 'b', // myNumber: 75, // myString: 'California', // myVector: [ 1, 0, 0 ], // or new Vec3(), // // myColorString: '#ffffff', // myColorInt: 0xffffff, // myColorObject: { r: 1, g: 1, b: 1 }, // myColorArray: [ 1, 1, 1 ], // } // // -- TYPES ----------- OBJECT VALUE ------ EXAMPLE -- // button function folder.add(params, 'myFunction'); // checkbox boolean folder.add(params, 'myBoolean'); // color multiple types folder.addColor(params, 'myColor______'); // list number / string folder.add(params, 'myList', [ 'a', 'b', 'c', 'd', 'e' ]); // multiple numbers array folder.add(params, 'myVector'); // number / slider number folder.add(params, 'myNumber', min, max, step, precision); // text box string folder.add(params, 'myString'); // // -- UPDATING -- // params = { myNumber: 5.0 }; // const controller = folder1.add(params, 'myNumber'); // ... // params.myNumber += 2.0; // controller.updateDisplay(); import { ColorScheme } from './utils/ColorScheme.js'; import { Css } from './utils/Css.js'; import { Iris } from './utils/Iris.js'; import { Div } from './core/Div.js'; import { FlexSpacer } from './layout/FlexSpacer.js'; import { OVERFLOW } from './utils/Popper.js'; import { PANEL_STYLES } from './panels/Panel.js'; import { PropertyList, LEFT_SPACING } from './data/PropertyList.js'; import { RESIZERS } from './constants.js'; import { Shrinkable } from './panels/Shrinkable.js'; import { Tabbed } from './panels/Tabbed.js'; import { Titled } from './panels/Titled.js'; import { Button } from './input/Button.js'; import { Checkbox } from './input/Checkbox.js'; import { Color } from './input/Color.js'; import { Dropdown } from './input/Dropdown.js'; import { NumberBox } from './input/Number.js'; import { Slider } from './input/Slider.js'; import { TextBox } from './input/TextBox.js'; const _clr = new Iris(); class Gooey extends Tabbed { constructor(title, opacity) { super({ style: PANEL_STYLES.FANCY }); this.addClass('osui-gooey'); this.toggleResize(RESIZERS.LEFT, true); this.minWidth = 180; this.opacity(opacity); const titlePanel = new Titled({ title: title, collapsible: true }); this.add(titlePanel); this.contents = function() { return titlePanel.scroller; }; document.body.appendChild(this.dom); } addFolder(folderName = '', icon = '') { if (folderName && folderName !== '') folderName = capitalize(folderName); const folder = new Folder({ title: folderName, icon }); this.add(folder); return folder; } color(color) { ColorScheme.changeColor(color); return this; } opacity(opacity) { if (opacity == null || Number.isNaN(opacity)) return; opacity = Math.min(Math.max(opacity, 0.0), 1.0); Css.setVariable('--panel-transparency', opacity); return this; } scale(multiplier) { if (!multiplier) multiplier = 1.0; multiplier = parseFloat(multiplier); const newSize = 14 /* base size = 14px */ * multiplier; const fontSize = Math.min(Math.max(newSize, 7 /* min font size */), 42 /* max font size */); Css.setVariable('--font-size', Css.toPx(fontSize, this.dom)); return this; } width(width) { if (width == null) return; if (width < this.minWidth * Css.guiScale(this.dom)) width = this.minWidth * Css.guiScale(this.dom); if (width > this.maxWidth * Css.guiScale(this.dom)) width = this.maxWidth * Css.guiScale(this.dom); this.setWidth(Css.toEm(width, this.dom)); return this; } } export { Gooey }; /***** Local Classes *****/ class Folder extends Shrinkable { constructor({ title, icon, } = {}) { super({ text: title, icon }); // Build osui this.props = new PropertyList('45%', LEFT_SPACING.NORMAL); this.add(this.props); // Add function (replaces osui.add() functionality) this.add = function(params, variable, a, b, c, d) { const value = params[variable]; if (value == undefined) { return null; } else if (typeof value === 'boolean') { return this.addBoolean(params, variable); } else if (typeof value === 'number') { if (Array.isArray(a) && a.length > 0) { return this.addList(params, variable, a); } else { return this.addNumber(params, variable, a, b, c, d); } } else if (typeof value === 'string') { if (Array.isArray(a) && a.length > 0) { return this.addList(params, variable, a); } else { return this.addString(params, variable); } } else if (typeof value === 'function') { return this.addFunction(params, variable); } else if (Array.isArray(value) && value.length > 0) { return this.addVector(params, variable); } }; } addBoolean(params, variable) { const prop = new Property(); const boolBox = new Checkbox(); boolBox.onChange(() => { params[variable] = boolBox.getValue(); if (typeof prop.change === 'function') prop.change(); if (typeof prop.finishChange === 'function') prop.finishChange(); }); const row = this.props.addRow(prettyTitle(variable), boolBox, new FlexSpacer()); prop.name = function(name) { row.leftWidget.setInnerHtml(name); return prop; }; prop.updateDisplay = function() { boolBox.setValue(params[variable]); return prop; }; prop.updateDisplay(); return prop; } addColor(params, variable) { let type; let value = params[variable]; if (value == undefined) { return null; } else if (typeof value === 'string') { type = 'string'; } else if (Array.isArray(value)) { type = 'array'; } else if (typeof value === 'object') { type = 'object'; } else { type = 'number'; } const prop = new Property(); const colorButton = new Color(); function setVariable(newValue) { _clr.set(newValue); switch (type) { case 'string': params[variable] = _clr.hexString(); break; case 'array': _clr.toArray(params[variable]); break; case 'object': _clr.getRGB(params[variable]); break; case 'number': params[variable] = _clr.hex(); break; default: } } colorButton.onInput(() => { setVariable(colorButton.getHexValue()); if (typeof prop.change === 'function') prop.change(); }); colorButton.onChange(() => { setVariable(colorButton.getHexValue()); if (typeof prop.change === 'function') prop.change(); if (typeof prop.finishChange === 'function') prop.finishChange(); }); const row = this.props.addRow(prettyTitle(variable), colorButton); prop.name = function(name) { row.leftWidget.setInnerHtml(name); return prop; }; prop.updateDisplay = function() { colorButton.setValue(_clr.set(params[variable]).hex()); return prop; }; prop.updateDisplay(); return prop; } addFunction(params, variable) { const prop = new Property(); const button = new Button(prettyTitle(variable)); button.onClick(() => params[variable]()); // this.props.addRowWithoutTitle(button); // prop.name = function(name) { button.setInnerHtml(name); return self; }; const row = this.props.addRow(prettyTitle(variable), button); prop.name = function(name, buttonText) { row.leftWidget.setInnerHtml(name); if (buttonText) button.setInnerHtml(buttonText); return prop; }; return prop; } addList(params, variable, options) { const prop = new Property(); const type = (typeof params[variable] === 'string') ? 'string' : 'number'; const selectOptions = {}; for (let option of options) selectOptions[option] = option; const selectDropDown = new Dropdown(); selectDropDown.overflowMenu = OVERFLOW.LEFT; selectDropDown.setOptions(selectOptions); selectDropDown.onChange(() => { params[variable] = (type === 'string') ? selectDropDown.getValue() : selectDropDown.getIndex(); if (typeof prop.change === 'function') prop.change(); if (typeof prop.finishChange === 'function') prop.finishChange(); }); const row = this.props.addRow(prettyTitle(variable), selectDropDown); prop.name = function(name) { row.leftWidget.setInnerHtml(name); return prop; }; prop.updateDisplay = function() { if (type === 'string') selectDropDown.setValue(params[variable]); else selectDropDown.setIndex(params[variable]); return prop; }; prop.updateDisplay(); return prop; } addNumber(params, variable, min = -Infinity, max = Infinity, step = 'any', precision = 2) { const prop = new Property(); const slider = new Slider(); const slideBox = new NumberBox(); slider.onInput(() => { params[variable] = slider.getValue(); slideBox.setValue(slider.getValue()); if (typeof prop.change === 'function') prop.change(); }); slider.onChange(() => { params[variable] = slider.getValue(); slideBox.setValue(slider.getValue()); if (typeof prop.change === 'function') prop.change(); if (typeof prop.finishChange === 'function') prop.finishChange(); }); slideBox.onChange(() => { params[variable] = slideBox.getValue(); slider.setValue(slideBox.getValue()); if (typeof prop.change === 'function') prop.change(); if (typeof prop.finishChange === 'function') prop.finishChange(); }); slider.onPointerDown((event) => event.stopPropagation()); slider.onWheel((event) => event.stopPropagation()); slideBox.onWheel((event) => event.stopPropagation()); slider.setRange(min, max).setPrecision(precision); slideBox.setRange(min, max).setPrecision(precision); function setStep(newStep) { let min = slideBox.min, max = slideBox.max; slider.setStep(newStep); if (newStep === 'any') newStep = (Number.isFinite(max) && Number.isFinite(min)) ? (max - min) / 20 : 1; slideBox.setStep(newStep); } setStep(step); const digits = countDigits(parseInt(max)) + ((precision > 0) ? precision + 0.5 : 0); slideBox.dom.style.setProperty('--min-width', `${digits + 1.5}ch`); slideBox.setStyle('marginLeft', '0.14286em'); function checkForMinMax() { if (Number.isFinite(Number(slider.slider.dom.min)) && Number.isFinite(Number(slider.slider.dom.max))) { slideBox.addClass('osui-property-tiny-row'); slider.setDisplay(''); } else { slideBox.removeClass('osui-property-tiny-row'); slider.setDisplay('none'); } } checkForMinMax(); const row = this.props.addRow(prettyTitle(variable), slider, slideBox); prop.name = function(name) { row.leftWidget.setInnerHtml(name); return prop; }; prop.min = function(min) { slider.setMin(min); slideBox.setMin(min); checkForMinMax(); return prop; }; prop.max = function(max) { slider.setMax(max); slideBox.setMax(max); checkForMinMax(); return prop; }; prop.step = function(step) { setStep(step); return prop; }; prop.precision = function(precision) { slider.setPrecision(precision); slideBox.setPrecision(precision); return prop; }; prop.updateDisplay = function() { slider.setValue(params[variable]); slideBox.setValue(params[variable]); params[variable] = slider.getValue(); /* to apply min / max */ return prop; }; prop.updateDisplay(); return prop; } addString(params, variable) { const prop = new Property(); const textBox = new TextBox(); textBox.onChange(() => { params[variable] = textBox.getValue(); if (typeof prop.change === 'function') prop.change(); if (typeof prop.finishChange === 'function') prop.finishChange(); }); const row = this.props.addRow(prettyTitle(variable), textBox); prop.name = function(name) { row.leftWidget.setInnerHtml(name); return prop; }; prop.updateDisplay = function() { textBox.setValue(params[variable]); return prop; }; prop.updateDisplay(); return prop; } addVector(params, variable, min = -Infinity, max = Infinity, step = 'any', precision = 2) { const prop = new Property(); const vector = params[variable]; const row = this.props.addRow(prettyTitle(variable)); const boxes = []; for (let i = 0; i < vector.length; i++) { const box = new NumberBox(); boxes.push(box); box.onChange(() => { vector[i] = box.getValue(); if (typeof prop.change === 'function') prop.change(); if (typeof prop.finishChange === 'function') prop.finishChange(); }); box.onWheel((event) => event.stopPropagation()); box.setRange(min, max).setPrecision(precision); row.rightWidget.add(box); if (i < vector.length - 1) row.rightWidget.add(new Div().setStyle('min-width', '3px')); } function setStep(newStep) { let min = boxes[0].min, max = boxes[0].max; if (newStep === 'any') newStep = (Number.isFinite(max) && Number.isFinite(min)) ? (max - min) / 20 : 1; for (let i = 0; i < vector.length; i++) { boxes[i].setStep(newStep); } } setStep(step); prop.name = function(name) { row.leftWidget.setInnerHtml(name); return prop; }; prop.min = function(min) { for (let i = 0; i < boxes.length; i++) { boxes[i].setMin(min); } return prop; }; prop.max = function(max) { for (let i = 0; i < boxes.length; i++) { boxes[i].setMax(max); } return prop; }; prop.step = function(step) { for (let i = 0; i < boxes.length; i++) { boxes[i].setStep(step); } return prop; }; prop.precision = function(precision) { for (let i = 0; i < boxes.length; i++) { boxes[i].setPrecision(precision); } return prop; }; prop.updateDisplay = function() { for (let i = 0; i < vector.length; i++) { boxes[i].setValue(params[variable][i]); params[variable][i] = boxes[i].getValue(); /* to apply min / max */ } return prop; }; prop.updateDisplay(); return prop; } } class Property { constructor() { const self = this; // Callbacks this.change = null; this.finishChange = null; // Update Functions this.name = function() { return self; }; // to be overwritten in 'add_______' this.updateDisplay = function() { return self; }; // to be overwritten in 'add_______' // Number Functions this.min = function() { return self; }; // to be overwritten in 'addNumber' this.max = function() { return self; }; // to be overwritten in 'addNumber' this.step = function() { return self; }; // to be overwritten in 'addNumber' this.precision = function() { return self; }; // to be overwritten in 'addNumber' } onChange(callback) { this.change = callback; return this; } onFinishChange(callback) { this.finishChange = callback; return this; } } /******************** INTERNAL ********************/ /** * Capitalizes every word in a string and adds spaces between CamelCaseWords * * @param {String} string * @returns */ function prettyTitle(string) { return addSpaces(capitalize(string)); } /** * Adds spaces between 'CamelCaseWords' -> 'Camel Case Words' * * @param {String} string * @returns {String} */ function addSpaces(string) { if (!string || string === '') return ''; return String(string).replace(/([A-Z])/g, ' $1').trim(); } /** * Capitalizes the first letter of every word in a string * * @param {String} string string to be capitalized * @returns new capitalized string */ function capitalize(string) { if (!string || string === '') return ''; let words = String(string); words = words.split(' '); for (let i = 0; i < words.length; i++) { words[i] = words[i][0].toUpperCase() + words[i].substring(1); } return words.join(' '); } /** * Counts number of digits in a number * * @param {Number} number * @returns {Number} */ function countDigits(number) { return parseFloat(number).toString().length; }