Loading index.html +24 −8 Changes for index.html: 24 added lines, 8 removed lines. Original line number Diff line number Diff line Loading @@ -17,7 +17,8 @@ <select name="dataset" id="select-dataset" class="btn-dark" disabled> <optgroup label="Hosted" class="builtins" data-i18n="attr:label:controls.hosted"></optgroup> <optgroup label="In-memory" class="inmemory" data-i18n="attr:label:controls.inmemory"> <option class="action" value="create" data-i18n="content:controls.createinmemory">Create from images… <option class="action" value="create" data-i18n="content:controls.createinmemory">Create from images… </option> </optgroup> </select> Loading @@ -38,11 +39,13 @@ </header> <div class="controls"> <label data-phase="render" for="select-rendermode"><span data-i18n="content:controls.rendermode">Render mode</span> <label data-phase="render" for="select-rendermode"><span data-i18n="content:controls.rendermode">Render mode</span> <span data-i18n="content:colon">:</span></label> <select data-phase="render" name="rendermode" id="select-rendermode"> <option value="relight" data-i18n="content:controls.mode.relight">Relighting (with color)</option> <option value="relight_nocolor" data-i18n="content:controls.mode.relightnocolor">Relighting (no color)</option> <option value="relight_nocolor" data-i18n="content:controls.mode.relightnocolor">Relighting (no color) </option> <option value="albedo" data-i18n="content:controls.mode.albedo">Albedo</option> <option value="normals" data-i18n="content:controls.mode.normals">Normal map</option> </select> Loading @@ -50,25 +53,37 @@ <label for="input-specular"><span data-i18n="content:controls.specular">Specular reflection</span> <span data-i18n="content:colon">:</span> <span class="value"></span></label> <span class="value"></span> <button class="btn-dark btn-icon btn-slim button-input-edit" data-icon="edit" aria-label="Edit value" title="Edit value"></button> </label> <input type="range" name="specular" id="input-specular" min="0" max="1" value="0" step=".01"/> </div> <div class="input-box" data-phase="render"> <label for="input-diffuse"><span data-i18n="content:controls.diffuse">Diffuse reflection</span> <span data-i18n="content:colon">:</span> <span class="value"></span></label> <span class="value"></span> <button class="btn-dark btn-icon btn-slim button-input-edit" data-icon="edit" aria-label="Edit value" title="Edit value"></button> </label> <input type="range" name="diffuse" id="input-diffuse" min="0" max="1" value="0" step=".01"/> </div> <div class="input-box" data-phase="render"> <label for="input-shine"><span data-i18n="content:controls.shine">Shine</span> <span data-i18n="content:colon">:</span> <span class="value"></span></label> <span class="value"></span> <button class="btn-dark btn-icon btn-slim button-input-edit" data-icon="edit" aria-label="Edit value" title="Edit value"></button> </label> <input type="range" name="shine" id="input-shine" min="1" max="100" value="1" step="1"/> </div> <div class="input-box" data-phase="render"> <label for="input-ambiant"><span data-i18n="content:controls.ambiant">Ambiant lighting</span> <span data-i18n="content:colon">:</span> <span class="value"></span></label> <span class="value"></span> <button class="btn-dark btn-icon btn-slim button-input-edit" data-icon="edit" aria-label="Edit value" title="Edit value"></button> </label> <input type="range" name="ambiant" id="input-ambiant" min="0" max="1" value="0" step=".01"/> </div> <span data-phase="render"> Loading Loading @@ -98,7 +113,8 @@ <button class="btn-dark btn-icon" id="button-zoom-vfit" title="Fit view vertically" data-i18n="attr:title:controls.fitviewv" data-icon="zoom-vfit"></button> <button class="btn-dark btn-icon" id="button-zoom-hfit" title="Fit view horizontally" data-i18n="attr:title:controls.fitviewh" data-icon="zoom-hfit"></button> title="Fit view horizontally" data-i18n="attr:title:controls.fitviewh" data-icon="zoom-hfit"></button> </div> <canvas id="ivoire-display"></canvas> <progress aria-label="Downloading data" id="load-progress" class="hidden" max="100"></progress> Loading public/icons.svg +27 −0 Changes for public/icons.svg: 27 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -115,5 +115,32 @@ style="stroke-width:1" /> </g> </g> <g id="g7804" transform="translate(896)"> <g id="edit"> <path style="fill:none;stroke:currentColor;stroke-width:16;stroke-linecap:butt" d="M 24,80 80,23.9539 C 95.9539,7.9999979 120,32 104,48 L 48,104 H 24 Z" id="path1108" /> <path style="fill:currentColor;stroke:none" d="m 26.43949,77.558504 24,23.999996 L 24,104 Z" id="path7168" /> </g> </g> <g id="g8825" transform="translate(0, 128)"> <g id="close" style="fill:none;stroke:currentColor"> <path id="path8239" style="stroke-width:16;stroke-linecap:round" d="m 104,24 -80,80 m 0,-80 80,80" /> </g> </g> </g> </svg> src/app.ts +1 −1 Changes for src/app.ts: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -466,7 +466,7 @@ export class App { ); const dsSelect = document.querySelector('select#select-dataset')!; this.datasetSelect = new SelectControl('', dsSelect, []); this.datasetSelect = new SelectControl('', dsSelect); this.datasetSelect.addEventListener(ControlUpdateEvent.type, ((ev: ControlUpdateEvent<string>) => { this.onDatasetSelect(ev); }) as EventListener); Loading src/control.ts +120 −64 Changes for src/control.ts: 120 added lines, 64 removed lines. Original line number Diff line number Diff line import type {RunPhase} from "./app.ts"; import {vec2} from "gl-matrix"; import {Viewport} from "./viewport.ts"; import {Exportable, ExportableSym} from "./utils.ts"; import { vec2 } from 'gl-matrix'; import { Viewport } from './viewport.ts'; import { Exportable, ExportableSym } from './utils.ts'; export class ControlUpdateEvent<T> extends Event { public static readonly type = "formupdate"; public static readonly type = 'formupdate'; readonly prevValue: T; readonly newValue: T; readonly source?: Event; Loading @@ -21,14 +20,13 @@ abstract class FormControl<T> extends EventTarget implements Exportable<T> { private _defaultValue: T; private _value: T; private _disabled = false; private readonly _phases: RunPhase[]; protected readonly _element: HTMLElement; private readonly valueSpan: HTMLElement | null; protected constructor(value: T, el: HTMLElement, phases: RunPhase[], valueEl?: HTMLElement) { protected constructor(value: T, el: HTMLElement, valueEl?: HTMLElement) { super(); this._defaultValue = this._value = value; this._phases = phases; this._element = el; this.valueSpan = valueEl || null; } Loading @@ -37,18 +35,13 @@ abstract class FormControl<T> extends EventTarget implements Exportable<T> { return this._element; } public get phases(): RunPhase[] { return this._phases; } public get disabled() { return this._disabled; } set disabled(value: boolean) { this._disabled = value; if ("disabled" in this.element) this.element.disabled = value; if ('disabled' in this.element) this.element.disabled = value; } public get defaultValue() { Loading @@ -65,7 +58,7 @@ abstract class FormControl<T> extends EventTarget implements Exportable<T> { public set value(value: T) { if (this._disabled) { console.warn("Tried to set value while disabled"); console.warn('Tried to set value while disabled'); return; } this.updateValue(value); Loading @@ -79,8 +72,7 @@ abstract class FormControl<T> extends EventTarget implements Exportable<T> { this.updateValueEl(); } else { this._value = oldVal; if (source) source.preventDefault(); if (source) source.preventDefault(); } } Loading @@ -95,14 +87,7 @@ abstract class FormControl<T> extends EventTarget implements Exportable<T> { public abstract updateElement(): void; private updateValueEl(): void { if (this.valueSpan) this.valueSpan.innerText = this.valueToString(); } protected static getInputLabelValueEl(el: { labels: HTMLInputElement["labels"] }): HTMLElement | undefined { return [...el.labels || []] .map((l) => l.querySelector(".value") as HTMLElement) .find((e) => e); if (this.valueSpan) this.valueSpan.innerText = this.valueToString(); } public [ExportableSym](): T { Loading @@ -110,14 +95,14 @@ abstract class FormControl<T> extends EventTarget implements Exportable<T> { } } // @ts-ignore class CheckboxControl extends FormControl<boolean> { declare protected _element: HTMLInputElement; public constructor(value: boolean, el: HTMLInputElement, phases: RunPhase[]) { if (!(el instanceof HTMLInputElement) || el.type !== "checkbox") throw new Error("Invalid input element"); super(value, el, phases); el.addEventListener("input", (ev) => this.updateValue(this._element.checked, ev)); public constructor(value: boolean, el: HTMLInputElement) { if (!(el instanceof HTMLInputElement) || el.type !== 'checkbox') throw new Error('Invalid input element'); super(value, el); el.addEventListener('input', (ev) => this.updateValue(this._element.checked, ev)); } public updateElement() { Loading @@ -125,25 +110,97 @@ class CheckboxControl extends FormControl<boolean> { } } class RangeControl extends FormControl<number> { declare protected _element: HTMLInputElement; private _editBtn: HTMLButtonElement | null; private _manual = false; constructor(value: number, el: Element, phases: RunPhase[]) { if (!(el instanceof HTMLInputElement) || el.type !== "range") throw new Error("Invalid input element"); super(value, el, phases, CheckboxControl.getInputLabelValueEl(el)); el.addEventListener("input", (ev) => this.updateValue(parseFloat(this._element.value), ev)); constructor(value: number, el: Element) { if (!(el instanceof HTMLInputElement) || el.type !== 'range') throw new Error('Invalid input element'); const { valueEl, editEl } = RangeControl.getAssociatedEls(el); super(value, el, valueEl); this._editBtn = editEl as HTMLButtonElement || null; el.addEventListener('input', (ev) => this.onInput(ev as InputEvent)); if (editEl) editEl.addEventListener('click', () => (this.manual = true)); } public updateElement(): void { this._element.value = this.value.toString(); if (this.manual) this.manual = false; this._element.valueAsNumber = this.value; } protected onInput(ev: InputEvent) { const el = ev.currentTarget; if (!(el instanceof HTMLInputElement)) throw new Error(); if (el.validity.valid && el.valueAsNumber !== this.value) this.updateValue(el.valueAsNumber, ev); else if (el.validity.rangeOverflow) this.updateValue(parseFloat(el.max), ev); else if (el.validity.rangeUnderflow) this.updateValue(parseFloat(el.min), ev); } private set manual(value: boolean) { if (value) { if (this.manual) return; this._manual = true; const el = this.makeManualEditInput(); this._element.replaceWith(el); el.select(); } else { if (!this.manual) return; this._manual = false; document.getElementById(this._element.id)!.replaceWith(this._element); this.updateElement(); } this._editBtn?.classList.toggle("hidden", value); } protected makeManualEditInput(): HTMLInputElement { const el = this._element.cloneNode() as typeof this._element; el.type = 'number'; el.addEventListener('input', (ev) => this.onInput(ev as InputEvent)); el.addEventListener('blur', () => (this.manual = false)); el.addEventListener('keyup', (ev) => { if ((ev.key === 'Enter' || ev.key === 'Escape') && this._manual) { this.manual = false; ev.preventDefault(); } }); return el; } protected get manual() { return this._manual; } private static getAssociatedEls(el: { labels: HTMLInputElement['labels'] }): { valueEl: HTMLElement | undefined; editEl: HTMLElement | undefined; } { let valueEl: HTMLElement | undefined = undefined, editEl: HTMLElement | undefined = undefined; Array.from(el.labels || []).forEach((l) => { valueEl = (l.querySelector('.value') as HTMLElement) || valueEl; editEl = (l.querySelector('.button-input-edit') as HTMLElement) || editEl; }); return { valueEl, editEl }; } } class RangePercentControl extends RangeControl { constructor(value: number, el: Element, phases: RunPhase[]) { super(value, el, phases); constructor(value: number, el: Element) { super(value, el); } protected makeManualEditInput(): HTMLInputElement { const el = super.makeManualEditInput(); el.max = String(parseFloat(el.max) * 100); el.valueAsNumber *= 100; el.step = String(parseFloat(el.step) * 100); el.min = String(parseFloat(el.min) * 100); return el; } protected updateValue(value: number, source?: Event) { super.updateValue((source && this.manual) ? value / 100 : value, source); } protected valueToString(): string { Loading @@ -151,17 +208,15 @@ class RangePercentControl extends RangeControl { } } export class SelectControl extends FormControl<string> { declare protected _element: HTMLSelectElement; constructor(value: string, el: Element, phases: RunPhase[]) { if (!(el instanceof HTMLSelectElement) || el.type !== "select-one") throw new Error("Invalid select element"); super(value, el, phases); el.addEventListener("input", (ev) => { constructor(value: string, el: Element) { if (!(el instanceof HTMLSelectElement) || el.type !== 'select-one') throw new Error('Invalid select element'); super(value, el); el.addEventListener('input', (ev) => { let optn = el.selectedOptions[0]; if (optn) this.updateValue(optn.value, ev); if (optn) this.updateValue(optn.value, ev); }); } Loading @@ -174,56 +229,58 @@ export class SelectControl extends FormControl<string> { } } function getControlDefaults() { let ctrlEl = document.querySelector(".controls"); let ctrlEl = document.querySelector('.controls'); if (!ctrlEl) throw new Error("Couldn't find control element"); const phases: RunPhase[] = ["render"]; // HTML-controlled values // noinspection JSUnusedGlobalSymbols let ctrls = { rendermode: (el: Element) => new SelectControl("relight", el, phases), specular: (el: Element) => new RangePercentControl(.2, el, phases), ambiant: (el: Element) => new RangePercentControl(.5, el, phases), diffuse: (el: Element) => new RangePercentControl(.3, el, phases), shine: (el: Element) => new RangeControl(15, el, phases), rendermode: (el: Element) => new SelectControl('relight', el), specular: (el: Element) => new RangePercentControl(0.2, el), ambiant: (el: Element) => new RangePercentControl(0.5, el), diffuse: (el: Element) => new RangePercentControl(0.3, el), shine: (el: Element) => new RangeControl(15, el), }; return Object.fromEntries(Object.entries(ctrls).map(([key, func]) => { return Object.fromEntries( Object.entries(ctrls).map(([key, func]) => { const el = ctrlEl.querySelector(`[name="${key}"]`); if (!el) throw new Error(`Couldn't find control with name ${key}`); return [key, func(el)]; })) as { [key in keyof typeof ctrls]: ReturnType<typeof ctrls[key]> }; }), ) as { [key in keyof typeof ctrls]: ReturnType<(typeof ctrls)[key]> }; } export function getStateDefaults() { return { ...LIGHT_ANGLE_DEFAULTS, phase: "initial" as RunPhase, viewport: new Viewport(), controls: getControlDefaults(), pointerState: { dragging: false, overLightball: false, origin: vec2.create(), } }, }; } export const LIGHT_ANGLE_DEFAULTS = { lighttheta: 90, lightphi: 0, } }; export type Controls = ReturnType<typeof getControlDefaults>; export type State = ReturnType<typeof getStateDefaults>; export const RENDER_MODES = (() => { const modes = ["relight", "relight_nocolor", "albedo", "normals"] as const; const res = modes as typeof modes & { [key in typeof modes[number]]: number }; modes.forEach((m, i) => res[m] = i); const modes = ['relight', 'relight_nocolor', 'albedo', 'normals'] as const; const res = modes as typeof modes & { [key in (typeof modes)[number]]: number }; modes.forEach((m, i) => (res[m] = i)); return res; })(); export const CONTROLS_SET_UNIFORMS: { [key in keyof Controls]: (gl: WebGLRenderingContext, loc: WebGLUniformLocation, value: any) => void } = { export const CONTROLS_SET_UNIFORMS: { [key in keyof Controls]: (gl: WebGLRenderingContext, loc: WebGLUniformLocation, value: any) => void; } = { rendermode: (gl: WebGLRenderingContext, loc: WebGLUniformLocation, val: any) => { val = RENDER_MODES.indexOf(val); gl.uniform1i(loc, val > -1 ? val : 0); Loading @@ -232,5 +289,4 @@ export const CONTROLS_SET_UNIFORMS: { [key in keyof Controls]: (gl: WebGLRenderi ambiant: (gl: WebGLRenderingContext, loc: WebGLUniformLocation, val: any) => gl.uniform1f(loc, val), diffuse: (gl: WebGLRenderingContext, loc: WebGLUniformLocation, val: any) => gl.uniform1f(loc, val), shine: (gl: WebGLRenderingContext, loc: WebGLUniformLocation, val: any) => gl.uniform1f(loc, val), } }; src/style.sass +2 −0 Changes for src/style.sass: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ body width: 100% height: 100% margin: 0 .body-content box-sizing: border-box Loading Loading @@ -106,6 +107,7 @@ body .button-input-edit right: 0 top: 0 position: absolute //top: -.3rem Loading Loading
index.html +24 −8 Changes for index.html: 24 added lines, 8 removed lines. Original line number Diff line number Diff line Loading @@ -17,7 +17,8 @@ <select name="dataset" id="select-dataset" class="btn-dark" disabled> <optgroup label="Hosted" class="builtins" data-i18n="attr:label:controls.hosted"></optgroup> <optgroup label="In-memory" class="inmemory" data-i18n="attr:label:controls.inmemory"> <option class="action" value="create" data-i18n="content:controls.createinmemory">Create from images… <option class="action" value="create" data-i18n="content:controls.createinmemory">Create from images… </option> </optgroup> </select> Loading @@ -38,11 +39,13 @@ </header> <div class="controls"> <label data-phase="render" for="select-rendermode"><span data-i18n="content:controls.rendermode">Render mode</span> <label data-phase="render" for="select-rendermode"><span data-i18n="content:controls.rendermode">Render mode</span> <span data-i18n="content:colon">:</span></label> <select data-phase="render" name="rendermode" id="select-rendermode"> <option value="relight" data-i18n="content:controls.mode.relight">Relighting (with color)</option> <option value="relight_nocolor" data-i18n="content:controls.mode.relightnocolor">Relighting (no color)</option> <option value="relight_nocolor" data-i18n="content:controls.mode.relightnocolor">Relighting (no color) </option> <option value="albedo" data-i18n="content:controls.mode.albedo">Albedo</option> <option value="normals" data-i18n="content:controls.mode.normals">Normal map</option> </select> Loading @@ -50,25 +53,37 @@ <label for="input-specular"><span data-i18n="content:controls.specular">Specular reflection</span> <span data-i18n="content:colon">:</span> <span class="value"></span></label> <span class="value"></span> <button class="btn-dark btn-icon btn-slim button-input-edit" data-icon="edit" aria-label="Edit value" title="Edit value"></button> </label> <input type="range" name="specular" id="input-specular" min="0" max="1" value="0" step=".01"/> </div> <div class="input-box" data-phase="render"> <label for="input-diffuse"><span data-i18n="content:controls.diffuse">Diffuse reflection</span> <span data-i18n="content:colon">:</span> <span class="value"></span></label> <span class="value"></span> <button class="btn-dark btn-icon btn-slim button-input-edit" data-icon="edit" aria-label="Edit value" title="Edit value"></button> </label> <input type="range" name="diffuse" id="input-diffuse" min="0" max="1" value="0" step=".01"/> </div> <div class="input-box" data-phase="render"> <label for="input-shine"><span data-i18n="content:controls.shine">Shine</span> <span data-i18n="content:colon">:</span> <span class="value"></span></label> <span class="value"></span> <button class="btn-dark btn-icon btn-slim button-input-edit" data-icon="edit" aria-label="Edit value" title="Edit value"></button> </label> <input type="range" name="shine" id="input-shine" min="1" max="100" value="1" step="1"/> </div> <div class="input-box" data-phase="render"> <label for="input-ambiant"><span data-i18n="content:controls.ambiant">Ambiant lighting</span> <span data-i18n="content:colon">:</span> <span class="value"></span></label> <span class="value"></span> <button class="btn-dark btn-icon btn-slim button-input-edit" data-icon="edit" aria-label="Edit value" title="Edit value"></button> </label> <input type="range" name="ambiant" id="input-ambiant" min="0" max="1" value="0" step=".01"/> </div> <span data-phase="render"> Loading Loading @@ -98,7 +113,8 @@ <button class="btn-dark btn-icon" id="button-zoom-vfit" title="Fit view vertically" data-i18n="attr:title:controls.fitviewv" data-icon="zoom-vfit"></button> <button class="btn-dark btn-icon" id="button-zoom-hfit" title="Fit view horizontally" data-i18n="attr:title:controls.fitviewh" data-icon="zoom-hfit"></button> title="Fit view horizontally" data-i18n="attr:title:controls.fitviewh" data-icon="zoom-hfit"></button> </div> <canvas id="ivoire-display"></canvas> <progress aria-label="Downloading data" id="load-progress" class="hidden" max="100"></progress> Loading
public/icons.svg +27 −0 Changes for public/icons.svg: 27 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -115,5 +115,32 @@ style="stroke-width:1" /> </g> </g> <g id="g7804" transform="translate(896)"> <g id="edit"> <path style="fill:none;stroke:currentColor;stroke-width:16;stroke-linecap:butt" d="M 24,80 80,23.9539 C 95.9539,7.9999979 120,32 104,48 L 48,104 H 24 Z" id="path1108" /> <path style="fill:currentColor;stroke:none" d="m 26.43949,77.558504 24,23.999996 L 24,104 Z" id="path7168" /> </g> </g> <g id="g8825" transform="translate(0, 128)"> <g id="close" style="fill:none;stroke:currentColor"> <path id="path8239" style="stroke-width:16;stroke-linecap:round" d="m 104,24 -80,80 m 0,-80 80,80" /> </g> </g> </g> </svg>
src/app.ts +1 −1 Changes for src/app.ts: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -466,7 +466,7 @@ export class App { ); const dsSelect = document.querySelector('select#select-dataset')!; this.datasetSelect = new SelectControl('', dsSelect, []); this.datasetSelect = new SelectControl('', dsSelect); this.datasetSelect.addEventListener(ControlUpdateEvent.type, ((ev: ControlUpdateEvent<string>) => { this.onDatasetSelect(ev); }) as EventListener); Loading
src/control.ts +120 −64 Changes for src/control.ts: 120 added lines, 64 removed lines. Original line number Diff line number Diff line import type {RunPhase} from "./app.ts"; import {vec2} from "gl-matrix"; import {Viewport} from "./viewport.ts"; import {Exportable, ExportableSym} from "./utils.ts"; import { vec2 } from 'gl-matrix'; import { Viewport } from './viewport.ts'; import { Exportable, ExportableSym } from './utils.ts'; export class ControlUpdateEvent<T> extends Event { public static readonly type = "formupdate"; public static readonly type = 'formupdate'; readonly prevValue: T; readonly newValue: T; readonly source?: Event; Loading @@ -21,14 +20,13 @@ abstract class FormControl<T> extends EventTarget implements Exportable<T> { private _defaultValue: T; private _value: T; private _disabled = false; private readonly _phases: RunPhase[]; protected readonly _element: HTMLElement; private readonly valueSpan: HTMLElement | null; protected constructor(value: T, el: HTMLElement, phases: RunPhase[], valueEl?: HTMLElement) { protected constructor(value: T, el: HTMLElement, valueEl?: HTMLElement) { super(); this._defaultValue = this._value = value; this._phases = phases; this._element = el; this.valueSpan = valueEl || null; } Loading @@ -37,18 +35,13 @@ abstract class FormControl<T> extends EventTarget implements Exportable<T> { return this._element; } public get phases(): RunPhase[] { return this._phases; } public get disabled() { return this._disabled; } set disabled(value: boolean) { this._disabled = value; if ("disabled" in this.element) this.element.disabled = value; if ('disabled' in this.element) this.element.disabled = value; } public get defaultValue() { Loading @@ -65,7 +58,7 @@ abstract class FormControl<T> extends EventTarget implements Exportable<T> { public set value(value: T) { if (this._disabled) { console.warn("Tried to set value while disabled"); console.warn('Tried to set value while disabled'); return; } this.updateValue(value); Loading @@ -79,8 +72,7 @@ abstract class FormControl<T> extends EventTarget implements Exportable<T> { this.updateValueEl(); } else { this._value = oldVal; if (source) source.preventDefault(); if (source) source.preventDefault(); } } Loading @@ -95,14 +87,7 @@ abstract class FormControl<T> extends EventTarget implements Exportable<T> { public abstract updateElement(): void; private updateValueEl(): void { if (this.valueSpan) this.valueSpan.innerText = this.valueToString(); } protected static getInputLabelValueEl(el: { labels: HTMLInputElement["labels"] }): HTMLElement | undefined { return [...el.labels || []] .map((l) => l.querySelector(".value") as HTMLElement) .find((e) => e); if (this.valueSpan) this.valueSpan.innerText = this.valueToString(); } public [ExportableSym](): T { Loading @@ -110,14 +95,14 @@ abstract class FormControl<T> extends EventTarget implements Exportable<T> { } } // @ts-ignore class CheckboxControl extends FormControl<boolean> { declare protected _element: HTMLInputElement; public constructor(value: boolean, el: HTMLInputElement, phases: RunPhase[]) { if (!(el instanceof HTMLInputElement) || el.type !== "checkbox") throw new Error("Invalid input element"); super(value, el, phases); el.addEventListener("input", (ev) => this.updateValue(this._element.checked, ev)); public constructor(value: boolean, el: HTMLInputElement) { if (!(el instanceof HTMLInputElement) || el.type !== 'checkbox') throw new Error('Invalid input element'); super(value, el); el.addEventListener('input', (ev) => this.updateValue(this._element.checked, ev)); } public updateElement() { Loading @@ -125,25 +110,97 @@ class CheckboxControl extends FormControl<boolean> { } } class RangeControl extends FormControl<number> { declare protected _element: HTMLInputElement; private _editBtn: HTMLButtonElement | null; private _manual = false; constructor(value: number, el: Element, phases: RunPhase[]) { if (!(el instanceof HTMLInputElement) || el.type !== "range") throw new Error("Invalid input element"); super(value, el, phases, CheckboxControl.getInputLabelValueEl(el)); el.addEventListener("input", (ev) => this.updateValue(parseFloat(this._element.value), ev)); constructor(value: number, el: Element) { if (!(el instanceof HTMLInputElement) || el.type !== 'range') throw new Error('Invalid input element'); const { valueEl, editEl } = RangeControl.getAssociatedEls(el); super(value, el, valueEl); this._editBtn = editEl as HTMLButtonElement || null; el.addEventListener('input', (ev) => this.onInput(ev as InputEvent)); if (editEl) editEl.addEventListener('click', () => (this.manual = true)); } public updateElement(): void { this._element.value = this.value.toString(); if (this.manual) this.manual = false; this._element.valueAsNumber = this.value; } protected onInput(ev: InputEvent) { const el = ev.currentTarget; if (!(el instanceof HTMLInputElement)) throw new Error(); if (el.validity.valid && el.valueAsNumber !== this.value) this.updateValue(el.valueAsNumber, ev); else if (el.validity.rangeOverflow) this.updateValue(parseFloat(el.max), ev); else if (el.validity.rangeUnderflow) this.updateValue(parseFloat(el.min), ev); } private set manual(value: boolean) { if (value) { if (this.manual) return; this._manual = true; const el = this.makeManualEditInput(); this._element.replaceWith(el); el.select(); } else { if (!this.manual) return; this._manual = false; document.getElementById(this._element.id)!.replaceWith(this._element); this.updateElement(); } this._editBtn?.classList.toggle("hidden", value); } protected makeManualEditInput(): HTMLInputElement { const el = this._element.cloneNode() as typeof this._element; el.type = 'number'; el.addEventListener('input', (ev) => this.onInput(ev as InputEvent)); el.addEventListener('blur', () => (this.manual = false)); el.addEventListener('keyup', (ev) => { if ((ev.key === 'Enter' || ev.key === 'Escape') && this._manual) { this.manual = false; ev.preventDefault(); } }); return el; } protected get manual() { return this._manual; } private static getAssociatedEls(el: { labels: HTMLInputElement['labels'] }): { valueEl: HTMLElement | undefined; editEl: HTMLElement | undefined; } { let valueEl: HTMLElement | undefined = undefined, editEl: HTMLElement | undefined = undefined; Array.from(el.labels || []).forEach((l) => { valueEl = (l.querySelector('.value') as HTMLElement) || valueEl; editEl = (l.querySelector('.button-input-edit') as HTMLElement) || editEl; }); return { valueEl, editEl }; } } class RangePercentControl extends RangeControl { constructor(value: number, el: Element, phases: RunPhase[]) { super(value, el, phases); constructor(value: number, el: Element) { super(value, el); } protected makeManualEditInput(): HTMLInputElement { const el = super.makeManualEditInput(); el.max = String(parseFloat(el.max) * 100); el.valueAsNumber *= 100; el.step = String(parseFloat(el.step) * 100); el.min = String(parseFloat(el.min) * 100); return el; } protected updateValue(value: number, source?: Event) { super.updateValue((source && this.manual) ? value / 100 : value, source); } protected valueToString(): string { Loading @@ -151,17 +208,15 @@ class RangePercentControl extends RangeControl { } } export class SelectControl extends FormControl<string> { declare protected _element: HTMLSelectElement; constructor(value: string, el: Element, phases: RunPhase[]) { if (!(el instanceof HTMLSelectElement) || el.type !== "select-one") throw new Error("Invalid select element"); super(value, el, phases); el.addEventListener("input", (ev) => { constructor(value: string, el: Element) { if (!(el instanceof HTMLSelectElement) || el.type !== 'select-one') throw new Error('Invalid select element'); super(value, el); el.addEventListener('input', (ev) => { let optn = el.selectedOptions[0]; if (optn) this.updateValue(optn.value, ev); if (optn) this.updateValue(optn.value, ev); }); } Loading @@ -174,56 +229,58 @@ export class SelectControl extends FormControl<string> { } } function getControlDefaults() { let ctrlEl = document.querySelector(".controls"); let ctrlEl = document.querySelector('.controls'); if (!ctrlEl) throw new Error("Couldn't find control element"); const phases: RunPhase[] = ["render"]; // HTML-controlled values // noinspection JSUnusedGlobalSymbols let ctrls = { rendermode: (el: Element) => new SelectControl("relight", el, phases), specular: (el: Element) => new RangePercentControl(.2, el, phases), ambiant: (el: Element) => new RangePercentControl(.5, el, phases), diffuse: (el: Element) => new RangePercentControl(.3, el, phases), shine: (el: Element) => new RangeControl(15, el, phases), rendermode: (el: Element) => new SelectControl('relight', el), specular: (el: Element) => new RangePercentControl(0.2, el), ambiant: (el: Element) => new RangePercentControl(0.5, el), diffuse: (el: Element) => new RangePercentControl(0.3, el), shine: (el: Element) => new RangeControl(15, el), }; return Object.fromEntries(Object.entries(ctrls).map(([key, func]) => { return Object.fromEntries( Object.entries(ctrls).map(([key, func]) => { const el = ctrlEl.querySelector(`[name="${key}"]`); if (!el) throw new Error(`Couldn't find control with name ${key}`); return [key, func(el)]; })) as { [key in keyof typeof ctrls]: ReturnType<typeof ctrls[key]> }; }), ) as { [key in keyof typeof ctrls]: ReturnType<(typeof ctrls)[key]> }; } export function getStateDefaults() { return { ...LIGHT_ANGLE_DEFAULTS, phase: "initial" as RunPhase, viewport: new Viewport(), controls: getControlDefaults(), pointerState: { dragging: false, overLightball: false, origin: vec2.create(), } }, }; } export const LIGHT_ANGLE_DEFAULTS = { lighttheta: 90, lightphi: 0, } }; export type Controls = ReturnType<typeof getControlDefaults>; export type State = ReturnType<typeof getStateDefaults>; export const RENDER_MODES = (() => { const modes = ["relight", "relight_nocolor", "albedo", "normals"] as const; const res = modes as typeof modes & { [key in typeof modes[number]]: number }; modes.forEach((m, i) => res[m] = i); const modes = ['relight', 'relight_nocolor', 'albedo', 'normals'] as const; const res = modes as typeof modes & { [key in (typeof modes)[number]]: number }; modes.forEach((m, i) => (res[m] = i)); return res; })(); export const CONTROLS_SET_UNIFORMS: { [key in keyof Controls]: (gl: WebGLRenderingContext, loc: WebGLUniformLocation, value: any) => void } = { export const CONTROLS_SET_UNIFORMS: { [key in keyof Controls]: (gl: WebGLRenderingContext, loc: WebGLUniformLocation, value: any) => void; } = { rendermode: (gl: WebGLRenderingContext, loc: WebGLUniformLocation, val: any) => { val = RENDER_MODES.indexOf(val); gl.uniform1i(loc, val > -1 ? val : 0); Loading @@ -232,5 +289,4 @@ export const CONTROLS_SET_UNIFORMS: { [key in keyof Controls]: (gl: WebGLRenderi ambiant: (gl: WebGLRenderingContext, loc: WebGLUniformLocation, val: any) => gl.uniform1f(loc, val), diffuse: (gl: WebGLRenderingContext, loc: WebGLUniformLocation, val: any) => gl.uniform1f(loc, val), shine: (gl: WebGLRenderingContext, loc: WebGLUniformLocation, val: any) => gl.uniform1f(loc, val), } };
src/style.sass +2 −0 Changes for src/style.sass: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ body width: 100% height: 100% margin: 0 .body-content box-sizing: border-box Loading Loading @@ -106,6 +107,7 @@ body .button-input-edit right: 0 top: 0 position: absolute //top: -.3rem Loading