Commit 9fd03b13 authored by Laureline Nevin's avatar Laureline Nevin ⌨️
Browse files

Added manual edit buttons on controls

parent 6872c56f
Loading
Loading
Loading
Loading
+24 −8
Changes for index.html: 24 added lines, 8 removed lines.
Original line number Diff line number Diff line
@@ -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>
@@ -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>
@@ -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">
@@ -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>
+27 −0
Changes for public/icons.svg: 27 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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>
+1 −1
Changes for src/app.ts: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -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);
+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;
@@ -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;
    }
@@ -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() {
@@ -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);
@@ -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();
        }
    }

@@ -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 {
@@ -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() {
@@ -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 {
@@ -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);
        });
    }

@@ -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);
@@ -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),
}
};
+2 −0
Changes for src/style.sass: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@
body
	width: 100%
	height: 100%
	margin: 0

.body-content
	box-sizing: border-box
@@ -106,6 +107,7 @@ body

		.button-input-edit
			right: 0
			top: 0
			position: absolute
		//top: -.3rem