Loading index.html +30 −21 Changes for index.html: 30 added lines, 21 removed lines. Original line number Diff line number Diff line Loading @@ -10,8 +10,8 @@ let FF_FOUC_FIX; </script> </head> <body v-scope> <div class="body-content" :style="{cursor: fn.getCursor()}"> <body v-scope v-effect="document.title = title"> <div class="body-content" :style="{cursor: fn.getCursorType()}"> <h1 class="title" title="Image Viewer with Online Interactive Relighting" v-text="title"></h1> <header class="navbar" v-cloak> Loading @@ -19,23 +19,23 @@ <label for="select-dataset" v-t="controls.selected"></label> <span v-t="colon"></span> <select name="dataset" id="select-dataset" class="btn-dark" :disabled="datasetsDisabled" :value="dataset" @change="app.onDatasetSelect($event)"> :value="dataset" @change="fn.onDatasetSelect($event)"> <optgroup label="" :label="t('controls.hosted')" class="builtins"> <option v-for="ds in datasets.server" :value="ds.id" v-text="loc(ds.name, locale)"></option> <option v-for="ds in datasets.server" :value="ds.id" v-t.dyn="ds.name"></option> </optgroup> <optgroup label="" :label="t('controls.inmemory')" class="inmemory"> <option v-for="ds in datasets.memory" :value="ds.id" v-text="loc(ds.name, locale)"></option> <option v-for="ds in datasets.memory" :value="ds.id" v-t.dyn="ds.name"></option> <option class="action" value="create" v-t="controls.createinmemory"></option> </optgroup> </select> <button id="button-delete-dataset" class="btn-dark" v-show="dataset.startsWith('memory:')" @click="app.deleteMemoryDataset(dataset)" v-t="controls.deleteinmemory"></button> @click="fn.deleteMemoryDataset(dataset)" v-t="controls.deleteinmemory"></button> </div> <div> <button class="btn-dark btn-icon" aria-label="Fullscreen" title="Fullscreen" v-icon="fullscreen" v-show="requestFullscreen" @click="requestFullscreen"></button> <select aria-label="Language" class="btn-dark" name="lang" :value="locale" @change="app.setLocale($event.target.value)"> :value="locale" @change="fn.setLocale($event.target.value)"> <option v-for="lang in locales" :value="lang.id">{{lang.name}}</option> </select> <button class="btn-dark" @click="$refs.docDialog.showModal()"> Loading Loading @@ -133,23 +133,32 @@ @click="fn.setExportParams('view')"></button> <span v-t="export.settoview"></span></span> <span><button class="btn-dark btn-icon" v-icon="zoom-fit" @click="fn.setExportParams('full')"></button> <span v-t="export.settofull"></span></span> <span>Zoom: <input aria-label="Zoom numerator" type="number" name="export-zoom-num" min="1" max="1024" step="1" :value="exportParams.zoom[0]" @change="fn.setExportZoom(0, $el.value)"> : <input aria-label="Zoom denominator" type="number" name="export-zoom-denom" min="1" max="1024" step="1" :value="exportParams.zoom[1]" @change="fn.setExportZoom(1, $el.value)"> </span> <span>Export image size: {{fn.getExportSize().join(' : ')}}</span> </div> <span v-show="phase === 'render' || phase === 'export'"> <button id="button-export" class="btn-dark btn-icon" @click="fn.exportImage" v-icon="export"></button> <button id="button-export" class="btn-dark btn-icon" @click="fn.exportImage()" v-icon="export"></button> <label for="button-export" v-t="export.button"></label> </span> <hr> <div class="description-container"> <div v-if="description"> <h3 v-t="controls.description"></h3> <div class="content" v-text="description && loc(description, locale)"></div> <div class="content" v-t.dyn="description"></div> </div> <pre v-if="output"><code v-text="output"></code></pre> </div> <div class="lightball-container"> <canvas id="ivoire-lightball" :class="{lightballEdit}"></canvas> <div class="lightball-buttons-container" v-if="phase==='render' || phase==='export'"> <button class="btn-dark btn-icon" @click="app.resetLightball()" <button class="btn-dark btn-icon" @click="fn.resetLightball()" :title="t('controls.resetlightball')" v-icon="reset"></button> <button class="btn-dark btn-icon" v-icon="edit" aria-label="Edit angle values" title="Edit angle values" @click="lightballEdit = !lightballEdit" Loading @@ -175,24 +184,24 @@ <main id="app" v-cloak> <div class="view-controls"> <button class="btn-dark btn-icon" :title="t('controls.fitview')" @click="fn.zoomFit()" v-icon="zoom-fit"></button> <button class="btn-dark btn-icon" :title="t('controls.fitviewv')" @click="fn.zoomFit('v')" v-icon="zoom-vfit"></button> <button class="btn-dark btn-icon" :title="t('controls.fitviewh')" @click="fn.zoomFit('h')" v-icon="zoom-hfit"></button> <button class="btn-dark btn-icon" :title="t('controls.fitview')" @click="fn.state.viewport.zoomFit()" v-icon="zoom-fit"></button> <button class="btn-dark btn-icon" :title="t('controls.fitviewv')" @click="fn.state.viewport.zoomFit('v')" v-icon="zoom-vfit"></button> <button class="btn-dark btn-icon" :title="t('controls.fitviewh')" @click="fn.state.viewport.zoomFit('h')" v-icon="zoom-hfit"></button> </div> <canvas id="ivoire-display" :style="{cursor: fn.getCursor()}"></canvas> <canvas id="ivoire-display" :style="{cursor: fn.getCursorType()}"></canvas> <progress aria-label="Downloading data" id="load-progress" class="hidden" max="100"></progress> </main> </div> <dialog id="message" class="drop-shadow" ref="messageDialog" @click="app.onDialogClick"> <dialog id="message" class="drop-shadow" ref="messageDialog" @click="fn.onDialogClick"> <h1></h1> <p></p> <button class="btn-dark" type="reset" autofocus @click="$refs.messageDialog.close()" v-t="ok">Ok</button> </dialog> <dialog id="create-inmemory" ref="memCreateDia" class="drop-shadow" @click="app.onDialogClick" <dialog id="create-inmemory" ref="memCreateDia" class="drop-shadow" @click="fn.onDialogClick" @close="fn.cancelCreateMemoryDataset($el.returnValue)"> <h1 v-t="memcreate.title"></h1> <form action="#"> Loading @@ -214,7 +223,7 @@ <button class="btn" type="submit" v-t="memcreate.create" @click="fn.submitCreateMemoryDataset($event, $refs.memCreateDia)"></button> </dialog> <dialog id="export-success" ref="exportDialog" @click="app.onDialogClick" <dialog id="export-success" ref="exportDialog" @click="fn.onDialogClick" @close="fn.onExportDialogClose($refs.exportImg)"> <h3 v-t="export.success"></h3> <img src="" alt="Exported image" ref="exportImg"> Loading @@ -225,7 +234,7 @@ </div> <a href="" id="export-download-anchor" class="hidden"></a> </dialog> <dialog id="documentation" @click="app.onDialogClick" ref="docDialog"> <dialog id="documentation" @click="fn.onDialogClick" ref="docDialog"> <button type="reset" class="btn-close btn-icon" v-icon="close" @click="$refs.docDialog.close()"></button> <div class="logos"> <a target="_blank" href="https://www.greyc.fr"> Loading src/app.ts +53 −52 Changes for src/app.ts: 53 added lines, 52 removed lines. Original line number Diff line number Diff line Loading @@ -6,20 +6,19 @@ import { resizeCanvasToDisplaySize, SHADER_UPDATE_EVENT, shaderUpdateNotify, WebGLCanvasRenderingContext WebGLCanvasRenderingContext, } from './gl-program.ts'; import { ControlUpdateEvent, getStateDefaults, LIGHT_ANGLE_DEFAULTS, State } from './control.ts'; import { arraysEq, catchError, clamp, DEBUG, exportValues, getLightSourceVec, matToString, resetMessage, roundTo, VERBOSE VERBOSE, } from './utils.ts'; import { DownloadManager } from './download.ts'; import { Dataset, MemoryDataset, SingleDataset, TiledDataset } from './dataset.ts'; Loading Loading @@ -51,6 +50,7 @@ export class App { public readonly state: State; public readonly i18n: I18n; public readonly rx: typeof this.DEFAULT_RX_STATE; public readonly rawRx: typeof this.DEFAULT_RX_STATE; private renderRequested = false; private maxCanvasSize: [number, number] | undefined = undefined; public readonly gls: { Loading @@ -75,10 +75,6 @@ export class App { return this._app; } public static get appInitialized(): boolean { return this._app !== undefined; } public constructor(translations: Record<string, Record<string, any>>) { if (App._app) throw new Error(); // There can be only one Loading Loading @@ -110,7 +106,8 @@ export class App { import.meta.hot.on('vite:afterUpdate', () => this.requestRender()); } this.rx = reactive(this.DEFAULT_RX_STATE); this.rawRx = this.DEFAULT_RX_STATE; this.rx = reactive(this.rawRx); this.initControls(); this.rx.phase = 'load-datasets'; Loading Loading @@ -181,15 +178,11 @@ export class App { if (locale) { console.debug('Setting locale to', locale); } const i18n = new I18n(translations, { return new I18n(translations, { locale, defaultLocale: App.DEFLANG, }); i18n.onChange(() => { this.rx.locale = this.i18n.locale; this.rx.t = (k: string) => this.i18n.t(k); // Trigger update }); return i18n; } private get DEFAULT_RX_STATE() { Loading @@ -197,7 +190,6 @@ export class App { const fn = Object.fromEntries( ( [ this.getLocString, this.setLocale, this.deleteMemoryDataset, this.onDatasetSelect, Loading @@ -211,15 +203,15 @@ export class App { this.onExportDialogClose, this.downloadExportImage, this.setExportParams, this.setExportZoom, this.getExportSize, this.onExportParamInput, this.requestRender, this.getCursor, this.getCursorType, ] as Array<(..._: any[]) => any> ) .map((m) => [m.name, m.bind(this)] as const) .concat([['zoomFit', this.state.viewport.zoomFit.bind(this.state.viewport)]]), ).map((m) => [m.name, m.bind(this)] as const), ); const rx = { return { fn, phase: 'initial' as RunPhase, title: 'IVOIRe' + (import.meta.env.VITE_TITLE_TAG && ` (${import.meta.env.VITE_TITLE_TAG})`), Loading @@ -232,7 +224,10 @@ export class App { datasetsDisabled: false, dataset: '', requestFullscreen: null as (() => void) | null, t: (k: string) => this.i18n.t(k), t: (k: string): string => { void this.rx.locale; return this.i18n.t(k); }, loc: this.getLocString.bind(this), locale: this.i18n.locale, locales: App.LANGS.map((id) => ({ id, name: this.i18n.translations[id].lang as string })), Loading @@ -240,24 +235,24 @@ export class App { output: null as string | null, exportParams: App.DEFAULT_EXPORT_PARAMS, totalSize: [0, 0] as [number, number], pointerState: this.state.pointerState, lightballEdit: false, raw: () => rx, // Function to un-proxify the rx object }; return rx as Omit<typeof rx, 'raw'> & { raw: () => typeof rx }; } static get DEFAULT_EXPORT_PARAMS() { return { area: new Rect(0, 0, 0, 0), dezoom: 0, zoom: [1, 1] as [number, number], zoomtype: 'int' as 'float' | 'int' | 'pow2', outerGrab: new Rect(0, 0, 0, 0), innerGrab: new Rect(0, 0, 0, 0), lockedDim: 'end', }; } private getLocString(value: LocString, locale: string) { getLocString(value: LocString | null) { if (!value) return null; const locale = this.rx.locale; let text = typeof value === 'string' ? value : value[locale] || value[this.i18n.defaultLocale]!; if (!text) { console.error('Missing locale'); Loading @@ -269,6 +264,7 @@ export class App { public setLocale(locale: string) { this.i18n.locale = locale; localStorage.setItem('lang', locale); this.rx.locale = this.i18n.locale; } get datasets(): Dataset[] { Loading Loading @@ -460,7 +456,7 @@ export class App { if (this.source && (this.rx.phase === 'render' || this.rx.phase === 'export')) { if (this.rx.phase === 'export') { const margin = App.EXPORT_WINDOW_GRAB_MARGIN / Math.min(this.state.viewport.zoom, 1), area = this.rx.raw().exportParams.area, area = this.rawRx.exportParams.area, innerW = Math.max(margin / 4, area.w - margin), innerH = Math.max(margin / 4, area.h - margin); const inner = area.grow((innerW - area.w) / 2, (innerH - area.h) / 2); Loading @@ -472,7 +468,7 @@ export class App { ...exportedState, renderArea: this.source.totalSize, lightSource: getLightSourceVec(exportedState), exportParams: this.rx.phase === 'export' ? this.rx.raw().exportParams : null, exportParams: this.rx.phase === 'export' ? this.rawRx.exportParams : null, }; let join = [...data.lightSource].map((v) => roundTo(v)).join(', '); Loading Loading @@ -706,26 +702,37 @@ export class App { const bl = this.state.viewport.clipToTexture([-0.95, -0.95]); let area = Rect.fromCorners(vec2.floor(tr, tr), vec2.ceil(bl, bl)); area = area.intersection(canvas)!; let dezoom = Math.max(-source.zoomDepth, Math.min(0, Math.ceil(Math.log2(this.state.viewport.zoom)))); // let dezoom = Math.max(-source.zoomDepth, Math.min(0, Math.ceil(Math.log2(this.state.viewport.zoom)))); this.rx.exportParams.area = area; this.rx.exportParams.dezoom = dezoom; } else if (from === 'full') { this.rx.exportParams.area = canvas; this.rx.exportParams.dezoom = 0; } else throw new Error(`Unknown argument from = ${from}`); this.requestRender(); } private setExportZoom(part: 0 | 1, value: number) { console.log('setExportZoom', part, value); this.rx.exportParams.zoom[part] = value; } private getExportSize(): [number, number] { const params = this.rx.exportParams; return [ Math.ceil((params.area.w * params.zoom[0]) / params.zoom[1]), Math.ceil((params.area.h * params.zoom[0]) / params.zoom[1]), ]; } private onExportParamInput(ev: InputEvent) { if (this.rx.phase !== 'export') return; if (!(ev.currentTarget instanceof HTMLInputElement)) throw new Error(); const val = ev.currentTarget.valueAsNumber; const area = this.rx.raw().exportParams.area, const area = this.rawRx.exportParams.area, parts = ev.currentTarget.name.split('-'), ax = parts[0] === 'x' ? 0 : 1, start = area.xy, end = [area.x2, area.y2] as [number, number], total = this.rx.raw().totalSize, total = this.source!.totalSize, DIMS = ['start', 'size', 'end'], dim = DIMS.indexOf(parts[1]), lockDim = DIMS.indexOf(this.rx.exportParams.lockedDim); Loading Loading @@ -783,16 +790,9 @@ export class App { ...exportedState, lightSource: getLightSourceVec(exportedState), renderArea: source.totalSize, exportParams: this.rx.raw().exportParams, exportParams: this.rawRx.exportParams, dataset, }; let dezoom = 0; while ( data.exportParams.area.w / Math.pow(2, dezoom) > this.maxCanvasSize[0] || data.exportParams.area.h / Math.pow(2, dezoom) > this.maxCanvasSize[1] ) dezoom += 1; data.exportParams.dezoom = dezoom; this.source = null; DownloadManager.withProgressBar<Blob>( async (prog) => Loading Loading @@ -912,7 +912,7 @@ export class App { // Corner if (keepRatio) { let diagonal = [prev.w * (1 - qx), prev.h * (1 - qy)] as const; vec2.scale(diff, diagonal, vec2.dot(diff, diagonal) / Math.pow(vec2.len(diagonal), 2)); vec2.scale(diff, diagonal, vec2.dot(diff, diagonal) / vec2.len(diagonal) ** 2); } for (const ax of [0, 1] as const) { setMinMax(ax); Loading @@ -922,16 +922,17 @@ export class App { } } private getCursor(): string | null { private getCursorType(): string | null { const { exportWindowHover: xy, dragging } = this.rx.state.pointerState; return ( xy && (xy && [ ['nesw-resize', 'ew-resize', 'nwse-resize'], ['ns-resize', null, 'ns-resize'], ['nwse-resize', 'ew-resize', 'nesw-resize'], ][xy[0]][xy[1]] ) || (dragging ? 'grabbing' : null); ][xy[0]][xy[1]]) || (dragging ? 'grabbing' : null) ); } private bindPointerEvents() { Loading @@ -956,7 +957,7 @@ export class App { lastMoveEvent = new PointerEvent('pointerleave'); const onPointerEvent = (ev: PointerEventData | WheelEvent) => { const viewport = this.state.viewport!; const pointerState = this.rx.pointerState; const pointerState = this.rx.state.pointerState; if (!this.source) return; let lightball = pointerState.dragging Loading Loading @@ -999,7 +1000,7 @@ export class App { this.requestRender(); }; const highlight = DEBUG const highlight = import.meta.env.DEV ? () => { if (!ev.shiftKey || !(ev.buttons & 1) || !this.source || this.rx.phase !== 'render') return false; const texCoord = viewport.screenToTexture(evpos); Loading @@ -1021,7 +1022,7 @@ export class App { const exportWindowPart = (pos: vec2): [0 | 1 | 2, 0 | 1 | 2] | null => { if (this.rx.phase !== 'export' || lightball) return null; const exPar = this.rx.raw().exportParams; const exPar = this.rawRx.exportParams; if (!exPar.outerGrab.includes(pos)) return null; return [ Loading @@ -1035,7 +1036,7 @@ export class App { if (this.state.pointerState.dragging || ev.currentTarget === canvas) { ev.stopPropagation(); const delta = (-Math.min(Math.max(Math.abs(ev.deltaY), 10), 50) * Math.sign(ev.deltaY)) / 10; viewport.zoomAround(Math.pow(SCROLL_FACTOR, delta), evpos); viewport.zoomAround(SCROLL_FACTOR ** delta, evpos); } } else if (ev.type === 'pointerdown') { if (pointerState.dragging || !ev.isPrimary || (ev.button !== 0 && ev.button !== 1)) Loading @@ -1051,9 +1052,9 @@ export class App { vec2.sub( pointerState.origin, pointerState.origin, this.rx.raw().exportParams.area.getQuadrantCoords([x, y]), this.rawRx.exportParams.area.getQuadrantCoords([x, y]), ); pointerState.originWindow = this.rx.raw().exportParams.area.clone(); pointerState.originWindow = this.rawRx.exportParams.area.clone(); } doUpdate(); window.addEventListener('wheel', onPointerEvent, { passive: false }); Loading src/dataset.ts +90 −87 File changed.Preview size limit exceeded, changes collapsed. Show changes src/gl-program.ts +5 −6 Changes for src/gl-program.ts: 5 added lines, 6 removed lines. Original line number Diff line number Diff line Loading @@ -6,7 +6,7 @@ import exportRectVtxSource from './shaders/exportRect.vert'; import exportRectFrgSource from './shaders/exportRect.frag'; import { mat4, vec2, vec3 } from 'gl-matrix'; import { Controls, CONTROLS_SET_UNIFORMS, State } from './control.ts'; import { DEBUG, Exported, GL, roundTo } from './utils.ts'; import { Exported, GL, roundTo } from './utils.ts'; import { Rect, TextureSource, TextureTile } from './tiling.ts'; import { ExportParams } from './app.ts'; Loading Loading @@ -180,7 +180,7 @@ abstract class GLProgramBase<Attribs extends string[], Uniforms extends string[] private loadShader(type: GLenum, source: string) { const shader = this.gl.createShader(type)!; if (DEBUG) source = '#define DEBUG 1\n' + source; if (import.meta.env.DEV) source = '#define DEBUG 1\n' + source; // Send the source to the shader object this.gl.shaderSource(shader, source); Loading Loading @@ -308,8 +308,8 @@ export class RelightProgram extends GLProgramBase< public renderArea(source: TextureSource, data: RenderData, area: Rect, zoom: number) { super.render(data); const wScale = zoom * 2 / this.gl.drawingBufferWidth, hScale = zoom * 2 / this.gl.drawingBufferHeight, const wScale = (zoom * 2) / this.gl.drawingBufferWidth, hScale = (zoom * 2) / this.gl.drawingBufferHeight, proj = mat4.fromScaling(mat4.create(), [wScale, hScale, 1]), offset = [-area.x - area.w / 2, -area.y - area.h / 2, 0] as const; mat4.translate(proj, proj, offset); Loading @@ -331,8 +331,7 @@ export class RelightProgram extends GLProgramBase< points .map((p) => [p[0] * area.w + area.x, p[1] * area.h + area.y] as vec2) .map((p): [vec2, vec2] => [p, vec2.transformMat4([0, 0], p, proj)]) .map(([p1, p2]) => ` [${roundTo(p1, 3).join(', ')}] => [${roundTo(p2, 3).join(', ')}]`) .map(([p1, p2]) => ` [${roundTo(p1, 3).join(', ')}] => [${roundTo(p2, 3).join(', ')}]`) .join('\n'), ); this.renderWithProj(source, proj, data); Loading src/main.ts +5 −4 Changes for src/main.ts: 5 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -44,10 +44,12 @@ try { } }); }); vApp.directive('t', ({ el, exp, modifiers, effect }) => { vApp.directive('t', ({ el, exp, get, modifiers, effect }) => { effect(() => { const content = app.rx.t(exp); if (modifiers && modifiers.raw) { const content = modifiers && modifiers.dyn ? app.getLocString(get()) : app.rx.t(exp); if (!content) { el.replaceChildren(); } else if (modifiers && modifiers.raw) { el.innerHTML = content; } else { el.textContent = content; Loading @@ -55,7 +57,6 @@ try { }); }); vApp.mount(); document.title = app.rx.title; } catch (e) { catchError(true, e as Error); } Loading
index.html +30 −21 Changes for index.html: 30 added lines, 21 removed lines. Original line number Diff line number Diff line Loading @@ -10,8 +10,8 @@ let FF_FOUC_FIX; </script> </head> <body v-scope> <div class="body-content" :style="{cursor: fn.getCursor()}"> <body v-scope v-effect="document.title = title"> <div class="body-content" :style="{cursor: fn.getCursorType()}"> <h1 class="title" title="Image Viewer with Online Interactive Relighting" v-text="title"></h1> <header class="navbar" v-cloak> Loading @@ -19,23 +19,23 @@ <label for="select-dataset" v-t="controls.selected"></label> <span v-t="colon"></span> <select name="dataset" id="select-dataset" class="btn-dark" :disabled="datasetsDisabled" :value="dataset" @change="app.onDatasetSelect($event)"> :value="dataset" @change="fn.onDatasetSelect($event)"> <optgroup label="" :label="t('controls.hosted')" class="builtins"> <option v-for="ds in datasets.server" :value="ds.id" v-text="loc(ds.name, locale)"></option> <option v-for="ds in datasets.server" :value="ds.id" v-t.dyn="ds.name"></option> </optgroup> <optgroup label="" :label="t('controls.inmemory')" class="inmemory"> <option v-for="ds in datasets.memory" :value="ds.id" v-text="loc(ds.name, locale)"></option> <option v-for="ds in datasets.memory" :value="ds.id" v-t.dyn="ds.name"></option> <option class="action" value="create" v-t="controls.createinmemory"></option> </optgroup> </select> <button id="button-delete-dataset" class="btn-dark" v-show="dataset.startsWith('memory:')" @click="app.deleteMemoryDataset(dataset)" v-t="controls.deleteinmemory"></button> @click="fn.deleteMemoryDataset(dataset)" v-t="controls.deleteinmemory"></button> </div> <div> <button class="btn-dark btn-icon" aria-label="Fullscreen" title="Fullscreen" v-icon="fullscreen" v-show="requestFullscreen" @click="requestFullscreen"></button> <select aria-label="Language" class="btn-dark" name="lang" :value="locale" @change="app.setLocale($event.target.value)"> :value="locale" @change="fn.setLocale($event.target.value)"> <option v-for="lang in locales" :value="lang.id">{{lang.name}}</option> </select> <button class="btn-dark" @click="$refs.docDialog.showModal()"> Loading Loading @@ -133,23 +133,32 @@ @click="fn.setExportParams('view')"></button> <span v-t="export.settoview"></span></span> <span><button class="btn-dark btn-icon" v-icon="zoom-fit" @click="fn.setExportParams('full')"></button> <span v-t="export.settofull"></span></span> <span>Zoom: <input aria-label="Zoom numerator" type="number" name="export-zoom-num" min="1" max="1024" step="1" :value="exportParams.zoom[0]" @change="fn.setExportZoom(0, $el.value)"> : <input aria-label="Zoom denominator" type="number" name="export-zoom-denom" min="1" max="1024" step="1" :value="exportParams.zoom[1]" @change="fn.setExportZoom(1, $el.value)"> </span> <span>Export image size: {{fn.getExportSize().join(' : ')}}</span> </div> <span v-show="phase === 'render' || phase === 'export'"> <button id="button-export" class="btn-dark btn-icon" @click="fn.exportImage" v-icon="export"></button> <button id="button-export" class="btn-dark btn-icon" @click="fn.exportImage()" v-icon="export"></button> <label for="button-export" v-t="export.button"></label> </span> <hr> <div class="description-container"> <div v-if="description"> <h3 v-t="controls.description"></h3> <div class="content" v-text="description && loc(description, locale)"></div> <div class="content" v-t.dyn="description"></div> </div> <pre v-if="output"><code v-text="output"></code></pre> </div> <div class="lightball-container"> <canvas id="ivoire-lightball" :class="{lightballEdit}"></canvas> <div class="lightball-buttons-container" v-if="phase==='render' || phase==='export'"> <button class="btn-dark btn-icon" @click="app.resetLightball()" <button class="btn-dark btn-icon" @click="fn.resetLightball()" :title="t('controls.resetlightball')" v-icon="reset"></button> <button class="btn-dark btn-icon" v-icon="edit" aria-label="Edit angle values" title="Edit angle values" @click="lightballEdit = !lightballEdit" Loading @@ -175,24 +184,24 @@ <main id="app" v-cloak> <div class="view-controls"> <button class="btn-dark btn-icon" :title="t('controls.fitview')" @click="fn.zoomFit()" v-icon="zoom-fit"></button> <button class="btn-dark btn-icon" :title="t('controls.fitviewv')" @click="fn.zoomFit('v')" v-icon="zoom-vfit"></button> <button class="btn-dark btn-icon" :title="t('controls.fitviewh')" @click="fn.zoomFit('h')" v-icon="zoom-hfit"></button> <button class="btn-dark btn-icon" :title="t('controls.fitview')" @click="fn.state.viewport.zoomFit()" v-icon="zoom-fit"></button> <button class="btn-dark btn-icon" :title="t('controls.fitviewv')" @click="fn.state.viewport.zoomFit('v')" v-icon="zoom-vfit"></button> <button class="btn-dark btn-icon" :title="t('controls.fitviewh')" @click="fn.state.viewport.zoomFit('h')" v-icon="zoom-hfit"></button> </div> <canvas id="ivoire-display" :style="{cursor: fn.getCursor()}"></canvas> <canvas id="ivoire-display" :style="{cursor: fn.getCursorType()}"></canvas> <progress aria-label="Downloading data" id="load-progress" class="hidden" max="100"></progress> </main> </div> <dialog id="message" class="drop-shadow" ref="messageDialog" @click="app.onDialogClick"> <dialog id="message" class="drop-shadow" ref="messageDialog" @click="fn.onDialogClick"> <h1></h1> <p></p> <button class="btn-dark" type="reset" autofocus @click="$refs.messageDialog.close()" v-t="ok">Ok</button> </dialog> <dialog id="create-inmemory" ref="memCreateDia" class="drop-shadow" @click="app.onDialogClick" <dialog id="create-inmemory" ref="memCreateDia" class="drop-shadow" @click="fn.onDialogClick" @close="fn.cancelCreateMemoryDataset($el.returnValue)"> <h1 v-t="memcreate.title"></h1> <form action="#"> Loading @@ -214,7 +223,7 @@ <button class="btn" type="submit" v-t="memcreate.create" @click="fn.submitCreateMemoryDataset($event, $refs.memCreateDia)"></button> </dialog> <dialog id="export-success" ref="exportDialog" @click="app.onDialogClick" <dialog id="export-success" ref="exportDialog" @click="fn.onDialogClick" @close="fn.onExportDialogClose($refs.exportImg)"> <h3 v-t="export.success"></h3> <img src="" alt="Exported image" ref="exportImg"> Loading @@ -225,7 +234,7 @@ </div> <a href="" id="export-download-anchor" class="hidden"></a> </dialog> <dialog id="documentation" @click="app.onDialogClick" ref="docDialog"> <dialog id="documentation" @click="fn.onDialogClick" ref="docDialog"> <button type="reset" class="btn-close btn-icon" v-icon="close" @click="$refs.docDialog.close()"></button> <div class="logos"> <a target="_blank" href="https://www.greyc.fr"> Loading
src/app.ts +53 −52 Changes for src/app.ts: 53 added lines, 52 removed lines. Original line number Diff line number Diff line Loading @@ -6,20 +6,19 @@ import { resizeCanvasToDisplaySize, SHADER_UPDATE_EVENT, shaderUpdateNotify, WebGLCanvasRenderingContext WebGLCanvasRenderingContext, } from './gl-program.ts'; import { ControlUpdateEvent, getStateDefaults, LIGHT_ANGLE_DEFAULTS, State } from './control.ts'; import { arraysEq, catchError, clamp, DEBUG, exportValues, getLightSourceVec, matToString, resetMessage, roundTo, VERBOSE VERBOSE, } from './utils.ts'; import { DownloadManager } from './download.ts'; import { Dataset, MemoryDataset, SingleDataset, TiledDataset } from './dataset.ts'; Loading Loading @@ -51,6 +50,7 @@ export class App { public readonly state: State; public readonly i18n: I18n; public readonly rx: typeof this.DEFAULT_RX_STATE; public readonly rawRx: typeof this.DEFAULT_RX_STATE; private renderRequested = false; private maxCanvasSize: [number, number] | undefined = undefined; public readonly gls: { Loading @@ -75,10 +75,6 @@ export class App { return this._app; } public static get appInitialized(): boolean { return this._app !== undefined; } public constructor(translations: Record<string, Record<string, any>>) { if (App._app) throw new Error(); // There can be only one Loading Loading @@ -110,7 +106,8 @@ export class App { import.meta.hot.on('vite:afterUpdate', () => this.requestRender()); } this.rx = reactive(this.DEFAULT_RX_STATE); this.rawRx = this.DEFAULT_RX_STATE; this.rx = reactive(this.rawRx); this.initControls(); this.rx.phase = 'load-datasets'; Loading Loading @@ -181,15 +178,11 @@ export class App { if (locale) { console.debug('Setting locale to', locale); } const i18n = new I18n(translations, { return new I18n(translations, { locale, defaultLocale: App.DEFLANG, }); i18n.onChange(() => { this.rx.locale = this.i18n.locale; this.rx.t = (k: string) => this.i18n.t(k); // Trigger update }); return i18n; } private get DEFAULT_RX_STATE() { Loading @@ -197,7 +190,6 @@ export class App { const fn = Object.fromEntries( ( [ this.getLocString, this.setLocale, this.deleteMemoryDataset, this.onDatasetSelect, Loading @@ -211,15 +203,15 @@ export class App { this.onExportDialogClose, this.downloadExportImage, this.setExportParams, this.setExportZoom, this.getExportSize, this.onExportParamInput, this.requestRender, this.getCursor, this.getCursorType, ] as Array<(..._: any[]) => any> ) .map((m) => [m.name, m.bind(this)] as const) .concat([['zoomFit', this.state.viewport.zoomFit.bind(this.state.viewport)]]), ).map((m) => [m.name, m.bind(this)] as const), ); const rx = { return { fn, phase: 'initial' as RunPhase, title: 'IVOIRe' + (import.meta.env.VITE_TITLE_TAG && ` (${import.meta.env.VITE_TITLE_TAG})`), Loading @@ -232,7 +224,10 @@ export class App { datasetsDisabled: false, dataset: '', requestFullscreen: null as (() => void) | null, t: (k: string) => this.i18n.t(k), t: (k: string): string => { void this.rx.locale; return this.i18n.t(k); }, loc: this.getLocString.bind(this), locale: this.i18n.locale, locales: App.LANGS.map((id) => ({ id, name: this.i18n.translations[id].lang as string })), Loading @@ -240,24 +235,24 @@ export class App { output: null as string | null, exportParams: App.DEFAULT_EXPORT_PARAMS, totalSize: [0, 0] as [number, number], pointerState: this.state.pointerState, lightballEdit: false, raw: () => rx, // Function to un-proxify the rx object }; return rx as Omit<typeof rx, 'raw'> & { raw: () => typeof rx }; } static get DEFAULT_EXPORT_PARAMS() { return { area: new Rect(0, 0, 0, 0), dezoom: 0, zoom: [1, 1] as [number, number], zoomtype: 'int' as 'float' | 'int' | 'pow2', outerGrab: new Rect(0, 0, 0, 0), innerGrab: new Rect(0, 0, 0, 0), lockedDim: 'end', }; } private getLocString(value: LocString, locale: string) { getLocString(value: LocString | null) { if (!value) return null; const locale = this.rx.locale; let text = typeof value === 'string' ? value : value[locale] || value[this.i18n.defaultLocale]!; if (!text) { console.error('Missing locale'); Loading @@ -269,6 +264,7 @@ export class App { public setLocale(locale: string) { this.i18n.locale = locale; localStorage.setItem('lang', locale); this.rx.locale = this.i18n.locale; } get datasets(): Dataset[] { Loading Loading @@ -460,7 +456,7 @@ export class App { if (this.source && (this.rx.phase === 'render' || this.rx.phase === 'export')) { if (this.rx.phase === 'export') { const margin = App.EXPORT_WINDOW_GRAB_MARGIN / Math.min(this.state.viewport.zoom, 1), area = this.rx.raw().exportParams.area, area = this.rawRx.exportParams.area, innerW = Math.max(margin / 4, area.w - margin), innerH = Math.max(margin / 4, area.h - margin); const inner = area.grow((innerW - area.w) / 2, (innerH - area.h) / 2); Loading @@ -472,7 +468,7 @@ export class App { ...exportedState, renderArea: this.source.totalSize, lightSource: getLightSourceVec(exportedState), exportParams: this.rx.phase === 'export' ? this.rx.raw().exportParams : null, exportParams: this.rx.phase === 'export' ? this.rawRx.exportParams : null, }; let join = [...data.lightSource].map((v) => roundTo(v)).join(', '); Loading Loading @@ -706,26 +702,37 @@ export class App { const bl = this.state.viewport.clipToTexture([-0.95, -0.95]); let area = Rect.fromCorners(vec2.floor(tr, tr), vec2.ceil(bl, bl)); area = area.intersection(canvas)!; let dezoom = Math.max(-source.zoomDepth, Math.min(0, Math.ceil(Math.log2(this.state.viewport.zoom)))); // let dezoom = Math.max(-source.zoomDepth, Math.min(0, Math.ceil(Math.log2(this.state.viewport.zoom)))); this.rx.exportParams.area = area; this.rx.exportParams.dezoom = dezoom; } else if (from === 'full') { this.rx.exportParams.area = canvas; this.rx.exportParams.dezoom = 0; } else throw new Error(`Unknown argument from = ${from}`); this.requestRender(); } private setExportZoom(part: 0 | 1, value: number) { console.log('setExportZoom', part, value); this.rx.exportParams.zoom[part] = value; } private getExportSize(): [number, number] { const params = this.rx.exportParams; return [ Math.ceil((params.area.w * params.zoom[0]) / params.zoom[1]), Math.ceil((params.area.h * params.zoom[0]) / params.zoom[1]), ]; } private onExportParamInput(ev: InputEvent) { if (this.rx.phase !== 'export') return; if (!(ev.currentTarget instanceof HTMLInputElement)) throw new Error(); const val = ev.currentTarget.valueAsNumber; const area = this.rx.raw().exportParams.area, const area = this.rawRx.exportParams.area, parts = ev.currentTarget.name.split('-'), ax = parts[0] === 'x' ? 0 : 1, start = area.xy, end = [area.x2, area.y2] as [number, number], total = this.rx.raw().totalSize, total = this.source!.totalSize, DIMS = ['start', 'size', 'end'], dim = DIMS.indexOf(parts[1]), lockDim = DIMS.indexOf(this.rx.exportParams.lockedDim); Loading Loading @@ -783,16 +790,9 @@ export class App { ...exportedState, lightSource: getLightSourceVec(exportedState), renderArea: source.totalSize, exportParams: this.rx.raw().exportParams, exportParams: this.rawRx.exportParams, dataset, }; let dezoom = 0; while ( data.exportParams.area.w / Math.pow(2, dezoom) > this.maxCanvasSize[0] || data.exportParams.area.h / Math.pow(2, dezoom) > this.maxCanvasSize[1] ) dezoom += 1; data.exportParams.dezoom = dezoom; this.source = null; DownloadManager.withProgressBar<Blob>( async (prog) => Loading Loading @@ -912,7 +912,7 @@ export class App { // Corner if (keepRatio) { let diagonal = [prev.w * (1 - qx), prev.h * (1 - qy)] as const; vec2.scale(diff, diagonal, vec2.dot(diff, diagonal) / Math.pow(vec2.len(diagonal), 2)); vec2.scale(diff, diagonal, vec2.dot(diff, diagonal) / vec2.len(diagonal) ** 2); } for (const ax of [0, 1] as const) { setMinMax(ax); Loading @@ -922,16 +922,17 @@ export class App { } } private getCursor(): string | null { private getCursorType(): string | null { const { exportWindowHover: xy, dragging } = this.rx.state.pointerState; return ( xy && (xy && [ ['nesw-resize', 'ew-resize', 'nwse-resize'], ['ns-resize', null, 'ns-resize'], ['nwse-resize', 'ew-resize', 'nesw-resize'], ][xy[0]][xy[1]] ) || (dragging ? 'grabbing' : null); ][xy[0]][xy[1]]) || (dragging ? 'grabbing' : null) ); } private bindPointerEvents() { Loading @@ -956,7 +957,7 @@ export class App { lastMoveEvent = new PointerEvent('pointerleave'); const onPointerEvent = (ev: PointerEventData | WheelEvent) => { const viewport = this.state.viewport!; const pointerState = this.rx.pointerState; const pointerState = this.rx.state.pointerState; if (!this.source) return; let lightball = pointerState.dragging Loading Loading @@ -999,7 +1000,7 @@ export class App { this.requestRender(); }; const highlight = DEBUG const highlight = import.meta.env.DEV ? () => { if (!ev.shiftKey || !(ev.buttons & 1) || !this.source || this.rx.phase !== 'render') return false; const texCoord = viewport.screenToTexture(evpos); Loading @@ -1021,7 +1022,7 @@ export class App { const exportWindowPart = (pos: vec2): [0 | 1 | 2, 0 | 1 | 2] | null => { if (this.rx.phase !== 'export' || lightball) return null; const exPar = this.rx.raw().exportParams; const exPar = this.rawRx.exportParams; if (!exPar.outerGrab.includes(pos)) return null; return [ Loading @@ -1035,7 +1036,7 @@ export class App { if (this.state.pointerState.dragging || ev.currentTarget === canvas) { ev.stopPropagation(); const delta = (-Math.min(Math.max(Math.abs(ev.deltaY), 10), 50) * Math.sign(ev.deltaY)) / 10; viewport.zoomAround(Math.pow(SCROLL_FACTOR, delta), evpos); viewport.zoomAround(SCROLL_FACTOR ** delta, evpos); } } else if (ev.type === 'pointerdown') { if (pointerState.dragging || !ev.isPrimary || (ev.button !== 0 && ev.button !== 1)) Loading @@ -1051,9 +1052,9 @@ export class App { vec2.sub( pointerState.origin, pointerState.origin, this.rx.raw().exportParams.area.getQuadrantCoords([x, y]), this.rawRx.exportParams.area.getQuadrantCoords([x, y]), ); pointerState.originWindow = this.rx.raw().exportParams.area.clone(); pointerState.originWindow = this.rawRx.exportParams.area.clone(); } doUpdate(); window.addEventListener('wheel', onPointerEvent, { passive: false }); Loading
src/gl-program.ts +5 −6 Changes for src/gl-program.ts: 5 added lines, 6 removed lines. Original line number Diff line number Diff line Loading @@ -6,7 +6,7 @@ import exportRectVtxSource from './shaders/exportRect.vert'; import exportRectFrgSource from './shaders/exportRect.frag'; import { mat4, vec2, vec3 } from 'gl-matrix'; import { Controls, CONTROLS_SET_UNIFORMS, State } from './control.ts'; import { DEBUG, Exported, GL, roundTo } from './utils.ts'; import { Exported, GL, roundTo } from './utils.ts'; import { Rect, TextureSource, TextureTile } from './tiling.ts'; import { ExportParams } from './app.ts'; Loading Loading @@ -180,7 +180,7 @@ abstract class GLProgramBase<Attribs extends string[], Uniforms extends string[] private loadShader(type: GLenum, source: string) { const shader = this.gl.createShader(type)!; if (DEBUG) source = '#define DEBUG 1\n' + source; if (import.meta.env.DEV) source = '#define DEBUG 1\n' + source; // Send the source to the shader object this.gl.shaderSource(shader, source); Loading Loading @@ -308,8 +308,8 @@ export class RelightProgram extends GLProgramBase< public renderArea(source: TextureSource, data: RenderData, area: Rect, zoom: number) { super.render(data); const wScale = zoom * 2 / this.gl.drawingBufferWidth, hScale = zoom * 2 / this.gl.drawingBufferHeight, const wScale = (zoom * 2) / this.gl.drawingBufferWidth, hScale = (zoom * 2) / this.gl.drawingBufferHeight, proj = mat4.fromScaling(mat4.create(), [wScale, hScale, 1]), offset = [-area.x - area.w / 2, -area.y - area.h / 2, 0] as const; mat4.translate(proj, proj, offset); Loading @@ -331,8 +331,7 @@ export class RelightProgram extends GLProgramBase< points .map((p) => [p[0] * area.w + area.x, p[1] * area.h + area.y] as vec2) .map((p): [vec2, vec2] => [p, vec2.transformMat4([0, 0], p, proj)]) .map(([p1, p2]) => ` [${roundTo(p1, 3).join(', ')}] => [${roundTo(p2, 3).join(', ')}]`) .map(([p1, p2]) => ` [${roundTo(p1, 3).join(', ')}] => [${roundTo(p2, 3).join(', ')}]`) .join('\n'), ); this.renderWithProj(source, proj, data); Loading
src/main.ts +5 −4 Changes for src/main.ts: 5 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -44,10 +44,12 @@ try { } }); }); vApp.directive('t', ({ el, exp, modifiers, effect }) => { vApp.directive('t', ({ el, exp, get, modifiers, effect }) => { effect(() => { const content = app.rx.t(exp); if (modifiers && modifiers.raw) { const content = modifiers && modifiers.dyn ? app.getLocString(get()) : app.rx.t(exp); if (!content) { el.replaceChildren(); } else if (modifiers && modifiers.raw) { el.innerHTML = content; } else { el.textContent = content; Loading @@ -55,7 +57,6 @@ try { }); }); vApp.mount(); document.title = app.rx.title; } catch (e) { catchError(true, e as Error); }