Loading index.html +12 −7 Changes for index.html: 12 added lines, 7 removed lines. Original line number Diff line number Diff line Loading @@ -101,18 +101,22 @@ </span> <div class="export-panel" v-show="phase === 'export'"> <span>X: </span> <span>{{ Math.round(exportParams.offset[0]) }}</span> <span>{{ Math.round(exportParams.area.x) }}</span> <span>Y: </span> <span>{{ Math.round(exportParams.offset[1]) }}</span> <span>{{ Math.round(exportParams.area.y) }}</span> <span>Width: </span> <span>{{ Math.round(exportParams.size[0]) }}</span> <span>{{ Math.round(exportParams.area.w) }}</span> <span>Height: </span> <span>{{ Math.round(exportParams.size[1]) }}</span> <span>{{ Math.round(exportParams.area.h) }}</span> <span>Inner: </span> <span>{{ `${exportParams.innerGrab}` }}</span> <span>Outer: </span> <span>{{ `${exportParams.outerGrab}` }}</span> <span>Reset</span> <button v-icon="zoom-fit" @click="fn.setExportParams()"></button> <span>Reset to view</span> <span>To view</span> <button v-icon="zoom-fit" @click="fn.setExportParams('view')"></button> <span>Reset to full</span> <span>To full</span> <button v-icon="zoom-fit" @click="fn.setExportParams('full')"></button> </div> <div class="description-container"> Loading @@ -126,7 +130,8 @@ <canvas id="ivoire-lightball"></canvas> <div class="reset-button-container"> <button class="btn-dark btn-icon" @click="app.resetLightball()" :title="t('controls.resetlightball')" v-icon="reset" v-if="phase==='render'"></button> :title="t('controls.resetlightball')" v-icon="reset" v-if="phase==='render' || phase==='export'"></button> </div> </div> </div> Loading src/app.ts +60 −32 Changes for src/app.ts: 60 added lines, 32 removed lines. Original line number Diff line number Diff line Loading @@ -17,8 +17,9 @@ import { 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 @@ -66,6 +67,7 @@ export class App { public static readonly LANGS = ['en', 'fr'] as const; public static readonly DEFLANG = 'en'; public static readonly EXPORT_WINDOW_GRAB_MARGIN = 50; private static _app?: App; public static get app() { Loading @@ -73,6 +75,10 @@ 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 @@ -96,6 +102,7 @@ export class App { try { this.setPrograms(); this.requestRender(); resetMessage(); } catch (err: any) { catchError(false, err); } Loading Loading @@ -236,9 +243,10 @@ export class App { static get DEFAULT_EXPORT_PARAMS() { return { size: [0, 0] as [number, number], offset: [0, 0] as [number, number], area: new Rect(0, 0, 0, 0), zoom: 1, outerGrab: new Rect(0, 0, 0, 0), innerGrab: new Rect(0, 0, 0, 0), }; } Loading Loading @@ -442,6 +450,15 @@ export class App { let output = `Rendered at ${Date.now()}\n`; if (this.source && (this.rx.phase === 'render' || this.rx.phase === 'export')) { if (this.rx.phase === 'export') { const margin = Math.floor(App.EXPORT_WINDOW_GRAB_MARGIN / Math.min(this.state.viewport.zoom, 1)), area = this.rx.raw().exportParams.area, xMargin = Math.min(margin, Math.floor(area.w - margin * 1.5)), yMargin = Math.min(margin, Math.floor(area.h - margin * 1.5)); const inner = area.grow(-xMargin, -yMargin); this.rx.exportParams.innerGrab = inner; this.rx.exportParams.outerGrab = inner.grow(margin * 1.5); } const exportedState = exportValues(this.state); let data: RenderData = { ...exportedState, Loading Loading @@ -671,7 +688,7 @@ export class App { const source = this.source; if (!source) return; this.rx.phase = 'export'; if (this.rx.exportParams.size[0] === 0 || this.rx.exportParams.size[1] === 0) { if (this.rx.exportParams.area.w === 0 || this.rx.exportParams.area.h === 0) { this.setExportParams(); } this.requestRender(); Loading @@ -686,26 +703,28 @@ export class App { (source.totalSize[0] > this.maxCanvasSize[0] || source.totalSize[1] > this.maxCanvasSize[1]) ? 'view' : 'full'; let canvas = new Rect(0, 0, ...source.totalSize); if (from === 'view') { const tr = this.state.viewport.clipToTexture([1, 1]); const bl = this.state.viewport.clipToTexture([-1, -1]); let canvas = new Rect(0, 0, ...source.totalSize), area = Rect.fromCorners(vec2.floor(tr, tr), vec2.ceil(bl, bl)); let area = Rect.fromCorners(vec2.floor(tr, tr), vec2.ceil(bl, bl)); area = area.intersection(canvas)!; let zoom = Math.pow( 2, Math.max(-source.zoomDepth, Math.min(0, Math.ceil(Math.log2(this.state.viewport.zoom)))), ); this.rx.exportParams = { size: [area.w, area.h], offset: [area.x, area.y], area, zoom, outerGrab: area, innerGrab: area, }; } else if (from === 'full') { this.rx.exportParams = { size: [...source.totalSize], offset: [0, 0], area: canvas, zoom: 1, outerGrab: canvas, innerGrab: canvas, }; } else throw new Error(`Unknown argument from = ${from}`); this.requestRender(); Loading Loading @@ -779,61 +798,70 @@ export class App { * @param y vertical coordinate of the quadrant */ private getExportWindowQuadrant([x, y]: [0 | 1 | 2, 0 | 1 | 2]): vec2 { const params = this.rx.exportParams; return [params.offset[0] + (x * params.size[0]) / 2, params.offset[1] + (y * params.size[1]) / 2]; const area = this.rx.exportParams.area; return [area.x + (x * area.w) / 2, area.y + (y * area.h) / 2]; } /** * Sets the coordinates of the given quadrant of the export window divided into thirds on both axes * @param x horizontal coordinate of the quadrant * @param y vertical coordinate of the quadrant * @param qx horizontal coordinate of the quadrant * @param qy vertical coordinate of the quadrant * @param value position to set the given quadrant to // * @param keepRatio whether aspect ratio should be kept unmodified by the operation */ private setExportWindowQuadrant([qx, qy]: [0 | 1 | 2, 0 | 1 | 2], value: vec2) { if (!this.source) return; const curVal = this.getExportWindowQuadrant([qx, qy]); const params = this.rx.exportParams; const diff = vec2.sub(curVal, value, curVal), xy = this.rx.exportParams.offset, wh = this.rx.exportParams.size, { xy, wh } = params.area, total = this.source.totalSize; if (qx === 1 && qy === 1) { // Center this.rx.exportParams.offset = [ params.area = new Rect( clamp(Math.round(xy[0] + diff[0]), 0, total[0] - wh[0]), clamp(Math.round(xy[1] + diff[1]), 0, total[1] - wh[1])]; clamp(Math.round(xy[1] + diff[1]), 0, total[1] - wh[1]), ...wh, ); return; } // Switch from offset/size to start/end coordinates let xyMin = [xy[0], xy[1]] as [number, number], xyMax = [xy[0] + wh[0], xy[1] + wh[1]] as [number, number]; if (qx === 1 || qy === 1) { // Middle column or row if (qx === 1 || qy === 1) { // Middle column or row const ax = qy === 1 ? 0 : 1; if (qx === 0 || qy === 0) { // Left or bottom if (qx === 0 || qy === 0) { // Left or bottom xyMin[ax] = clamp(Math.round(xyMin[ax] + diff[ax]), 0, xyMax[ax] - 1); } else { // Right or top } else { // Right or top xyMax[ax] = clamp(Math.round(xyMax[ax] + diff[ax]), xyMin[ax] + 1, total[ax]); } } else { // Corner for (const ax of [0, 1] as const) { if ([qx, qy][ax] === 0) { // Left or bottom if ([qx, qy][ax] === 0) { // Left or bottom xyMin[ax] = clamp(Math.round(xyMin[ax] + diff[ax]), 0, xyMax[ax] - 1); } else { // Right or top } else { // Right or top xyMax[ax] = clamp(Math.round(xyMax[ax] + diff[ax]), xyMin[ax] + 1, total[ax]); } } } this.rx.exportParams.offset = xyMin; this.rx.exportParams.size = [xyMax[0] - xyMin[0], xyMax[1] - xyMin[1]]; params.area = Rect.fromCorners(xyMin, xyMax); } private getQuadrantCursor(xy: [0 | 1 | 2, 0 | 1 | 2] | null): string | null { return xy && [ return ( xy && [ ['sw-resize', 'w-resize', 'nw-resize'], ['s-resize', 'move', 'n-resize'], ['se-resize', 'e-resize', 'ne-resize'], ][xy[0]][xy[1]]; ][xy[0]][xy[1]] ); } private bindPointerEvents() { Loading Loading @@ -898,12 +926,12 @@ export class App { const exportWindowPart = (pos: vec2): [0 | 1 | 2, 0 | 1 | 2] | null => { if (this.rx.phase !== 'export' || lightball) return null; const [x, y] = this.rx.exportParams.offset, [w, h] = this.rx.exportParams.size; if (!new Rect(x, y, w, h).includes(pos)) return null; const exPar = this.rx.raw().exportParams; if (!exPar.outerGrab.includes(pos)) return null; return [ pos[0] < x + w / 3 ? 0 : pos[0] < x + (w * 2) / 3 ? 1 : 2, pos[1] < y + h / 3 ? 0 : pos[1] < y + (h * 2) / 3 ? 1 : 2, pos[0] >= exPar.innerGrab.x ? (pos[0] >= exPar.innerGrab.x2 ? 2 : 1) : 0, pos[1] >= exPar.innerGrab.y ? (pos[1] >= exPar.innerGrab.y2 ? 2 : 1) : 0, ]; }; Loading src/gl-program.ts +32 −11 Changes for src/gl-program.ts: 32 added lines, 11 removed lines. Original line number Diff line number Diff line Loading @@ -428,11 +428,7 @@ export class LightballProgram extends GLProgramBase< circle = vec3.fromValues(center[0], center[1], radius), alpha = LightballProgram.ALPHA[ data.pointerState.overLightball ? data.pointerState.dragging ? 'active' : 'hover' : 'inactive' data.pointerState.overLightball ? (data.pointerState.dragging ? 'active' : 'hover') : 'inactive' ]; gl.uniformMatrix4fv(this.uniformLocations.projectionMatrix, false, projection); Loading @@ -457,6 +453,7 @@ export class ExportRectProgram extends GLProgramBase< [keyof typeof ExportRectProgram.attributes], [keyof typeof ExportRectProgram.uniformNames] > { private static readonly NONE_GRAB_RECT: readonly [number, number, number, number] = [0, 0, -1, -1]; public static readonly PARAMS = { cornerRadius: 25, lineSize: 1, Loading @@ -468,8 +465,9 @@ export class ExportRectProgram extends GLProgramBase< static uniformNames = { color: 'uColor', exportRect: 'uExportRect', windowPartHover: 'uWindowPartHover', grabRect: 'uGrabRect', lineWidth: 'uLineWidth', grabStatus: 'uGrabStatus', projectionMatrix: 'uProjectionMatrix', renderRect: 'uRenderRect', }; Loading Loading @@ -504,11 +502,34 @@ export class ExportRectProgram extends GLProgramBase< gl.uniformMatrix4fv(this.uniformLocations.projectionMatrix, false, data.viewport.projectionMat); let grabRect = ExportRectProgram.NONE_GRAB_RECT, grabStatus = 0; if (data.pointerState.exportWindowHover) { const windowPartVal = [...data.pointerState.exportWindowHover!, data.pointerState.dragging ? 2 : 1]; gl.uniform3iv(this.uniformLocations.windowPartHover, windowPartVal); } else gl.uniform3i(this.uniformLocations.windowPartHover, -1, -1, 0); gl.uniform4f(this.uniformLocations.exportRect, ...data.exportParams!.offset, ...data.exportParams!.size); const quad = data.pointerState.exportWindowHover, xBnds = [ data.exportParams.outerGrab.xmin, data.exportParams.innerGrab.xmin, data.exportParams.innerGrab.xmax, data.exportParams.outerGrab.xmax, ], yBnds = [ data.exportParams.outerGrab.ymin, data.exportParams.innerGrab.ymin, data.exportParams.innerGrab.ymax, data.exportParams.outerGrab.ymax, ]; grabRect = [ xBnds[quad[0]], yBnds[quad[1]], xBnds[quad[0] + 1] - xBnds[quad[0]], yBnds[quad[1] + 1] - yBnds[quad[1]], ]; grabStatus = data.pointerState.dragging ? 2 : 1; } gl.uniform4fv(this.uniformLocations.exportRect, data.exportParams.area.xywh); gl.uniform4fv(this.uniformLocations.grabRect, grabRect); gl.uniform1i(this.uniformLocations.grabStatus, grabStatus); gl.uniform4f(this.uniformLocations.renderRect, 0, 0, ...data.renderArea); gl.uniform1f(this.uniformLocations.lineWidth, lineSize); gl.uniform4fv(this.uniformLocations.color, color); Loading @@ -520,7 +541,7 @@ export class ExportRectProgram extends GLProgramBase< output += `Area: ${area}\n color: ${color}\n lineSize: ${lineSize}\n` + ` exportRect: ${new Rect(...data.exportParams.offset, ...data.exportParams.size)}\n` + ` exportRect: ${data.exportParams.area}\n` + ` renderRect: ${new Rect(0, 0, ...data.renderArea)}\n`; return output; Loading src/shaders/exportRect.frag +11 −13 Changes for src/shaders/exportRect.frag: 11 added lines, 13 removed lines. Original line number Diff line number Diff line precision mediump float; uniform ivec3 uWindowPartHover; uniform mediump vec4 uColor; uniform mediump vec4 uRenderRect; uniform mediump vec4 uExportRect; uniform mediump vec4 uGrabRect; uniform mediump int uGrabStatus; uniform mediump float uLineWidth; varying mediump vec2 vVertexCoord; Loading @@ -16,25 +17,22 @@ float rectDist(vec2 pos, vec4 rect) { void main(void) { float dist = rectDist(vVertexCoord, uExportRect); float grabDist = uGrabStatus > 0 ? rectDist(vVertexCoord, uGrabRect) : 1000000.; bool inRender = bool(rectDist(vVertexCoord, uRenderRect) <= 0.); if (dist < 0.) { vec4 windowPart = vec4(uExportRect.xy + vec2(uWindowPartHover.xy) * uExportRect.zw / 3., uExportRect.zw / 3.); if (uWindowPartHover.z != 0 && rectDist(vVertexCoord, windowPart) <= 0.) { // Export window quadrant hover gl_FragColor = vec4(0., 0., 0., float(uWindowPartHover.z) * .2); } else { discard; // Inside export rectangle } } else if (dist <= uLineWidth * 4.) { bool onGrabBorder = bool(grabDist <= 0. && grabDist > -uLineWidth * 2.); if (dist >= 0. && dist <= uLineWidth * 4. && !onGrabBorder) { gl_FragColor = uColor; if (!inRender) // Full opacity outside of render area gl_FragColor.a = 1.; } else if (inRender) { } else if (grabDist <= 0.) { // Export window quadrant hover gl_FragColor = vec4(0., 0., 0., float(uGrabStatus) * (onGrabBorder ? .4 : .2)); } else if (dist > 0. && inRender) { // In render area, outside of export window gl_FragColor = vec4(0., 0., 0., mod((vVertexCoord.x + vVertexCoord.y) / (uLineWidth * 8.), 2.) > 1. ? .2 : .1); } else { // Outside of render area // Outside of render area or inside export window discard; } } No newline at end of file src/style.sass +3 −5 Changes for src/style.sass: 3 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -143,11 +143,9 @@ body .export-panel display: grid grid-template-columns: 1fr 1fr .title margin-bottom: 40px grid-template-columns: auto 1fr grid-column-gap: 1rem justify-items: start .drop-shadow Loading Loading
index.html +12 −7 Changes for index.html: 12 added lines, 7 removed lines. Original line number Diff line number Diff line Loading @@ -101,18 +101,22 @@ </span> <div class="export-panel" v-show="phase === 'export'"> <span>X: </span> <span>{{ Math.round(exportParams.offset[0]) }}</span> <span>{{ Math.round(exportParams.area.x) }}</span> <span>Y: </span> <span>{{ Math.round(exportParams.offset[1]) }}</span> <span>{{ Math.round(exportParams.area.y) }}</span> <span>Width: </span> <span>{{ Math.round(exportParams.size[0]) }}</span> <span>{{ Math.round(exportParams.area.w) }}</span> <span>Height: </span> <span>{{ Math.round(exportParams.size[1]) }}</span> <span>{{ Math.round(exportParams.area.h) }}</span> <span>Inner: </span> <span>{{ `${exportParams.innerGrab}` }}</span> <span>Outer: </span> <span>{{ `${exportParams.outerGrab}` }}</span> <span>Reset</span> <button v-icon="zoom-fit" @click="fn.setExportParams()"></button> <span>Reset to view</span> <span>To view</span> <button v-icon="zoom-fit" @click="fn.setExportParams('view')"></button> <span>Reset to full</span> <span>To full</span> <button v-icon="zoom-fit" @click="fn.setExportParams('full')"></button> </div> <div class="description-container"> Loading @@ -126,7 +130,8 @@ <canvas id="ivoire-lightball"></canvas> <div class="reset-button-container"> <button class="btn-dark btn-icon" @click="app.resetLightball()" :title="t('controls.resetlightball')" v-icon="reset" v-if="phase==='render'"></button> :title="t('controls.resetlightball')" v-icon="reset" v-if="phase==='render' || phase==='export'"></button> </div> </div> </div> Loading
src/app.ts +60 −32 Changes for src/app.ts: 60 added lines, 32 removed lines. Original line number Diff line number Diff line Loading @@ -17,8 +17,9 @@ import { 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 @@ -66,6 +67,7 @@ export class App { public static readonly LANGS = ['en', 'fr'] as const; public static readonly DEFLANG = 'en'; public static readonly EXPORT_WINDOW_GRAB_MARGIN = 50; private static _app?: App; public static get app() { Loading @@ -73,6 +75,10 @@ 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 @@ -96,6 +102,7 @@ export class App { try { this.setPrograms(); this.requestRender(); resetMessage(); } catch (err: any) { catchError(false, err); } Loading Loading @@ -236,9 +243,10 @@ export class App { static get DEFAULT_EXPORT_PARAMS() { return { size: [0, 0] as [number, number], offset: [0, 0] as [number, number], area: new Rect(0, 0, 0, 0), zoom: 1, outerGrab: new Rect(0, 0, 0, 0), innerGrab: new Rect(0, 0, 0, 0), }; } Loading Loading @@ -442,6 +450,15 @@ export class App { let output = `Rendered at ${Date.now()}\n`; if (this.source && (this.rx.phase === 'render' || this.rx.phase === 'export')) { if (this.rx.phase === 'export') { const margin = Math.floor(App.EXPORT_WINDOW_GRAB_MARGIN / Math.min(this.state.viewport.zoom, 1)), area = this.rx.raw().exportParams.area, xMargin = Math.min(margin, Math.floor(area.w - margin * 1.5)), yMargin = Math.min(margin, Math.floor(area.h - margin * 1.5)); const inner = area.grow(-xMargin, -yMargin); this.rx.exportParams.innerGrab = inner; this.rx.exportParams.outerGrab = inner.grow(margin * 1.5); } const exportedState = exportValues(this.state); let data: RenderData = { ...exportedState, Loading Loading @@ -671,7 +688,7 @@ export class App { const source = this.source; if (!source) return; this.rx.phase = 'export'; if (this.rx.exportParams.size[0] === 0 || this.rx.exportParams.size[1] === 0) { if (this.rx.exportParams.area.w === 0 || this.rx.exportParams.area.h === 0) { this.setExportParams(); } this.requestRender(); Loading @@ -686,26 +703,28 @@ export class App { (source.totalSize[0] > this.maxCanvasSize[0] || source.totalSize[1] > this.maxCanvasSize[1]) ? 'view' : 'full'; let canvas = new Rect(0, 0, ...source.totalSize); if (from === 'view') { const tr = this.state.viewport.clipToTexture([1, 1]); const bl = this.state.viewport.clipToTexture([-1, -1]); let canvas = new Rect(0, 0, ...source.totalSize), area = Rect.fromCorners(vec2.floor(tr, tr), vec2.ceil(bl, bl)); let area = Rect.fromCorners(vec2.floor(tr, tr), vec2.ceil(bl, bl)); area = area.intersection(canvas)!; let zoom = Math.pow( 2, Math.max(-source.zoomDepth, Math.min(0, Math.ceil(Math.log2(this.state.viewport.zoom)))), ); this.rx.exportParams = { size: [area.w, area.h], offset: [area.x, area.y], area, zoom, outerGrab: area, innerGrab: area, }; } else if (from === 'full') { this.rx.exportParams = { size: [...source.totalSize], offset: [0, 0], area: canvas, zoom: 1, outerGrab: canvas, innerGrab: canvas, }; } else throw new Error(`Unknown argument from = ${from}`); this.requestRender(); Loading Loading @@ -779,61 +798,70 @@ export class App { * @param y vertical coordinate of the quadrant */ private getExportWindowQuadrant([x, y]: [0 | 1 | 2, 0 | 1 | 2]): vec2 { const params = this.rx.exportParams; return [params.offset[0] + (x * params.size[0]) / 2, params.offset[1] + (y * params.size[1]) / 2]; const area = this.rx.exportParams.area; return [area.x + (x * area.w) / 2, area.y + (y * area.h) / 2]; } /** * Sets the coordinates of the given quadrant of the export window divided into thirds on both axes * @param x horizontal coordinate of the quadrant * @param y vertical coordinate of the quadrant * @param qx horizontal coordinate of the quadrant * @param qy vertical coordinate of the quadrant * @param value position to set the given quadrant to // * @param keepRatio whether aspect ratio should be kept unmodified by the operation */ private setExportWindowQuadrant([qx, qy]: [0 | 1 | 2, 0 | 1 | 2], value: vec2) { if (!this.source) return; const curVal = this.getExportWindowQuadrant([qx, qy]); const params = this.rx.exportParams; const diff = vec2.sub(curVal, value, curVal), xy = this.rx.exportParams.offset, wh = this.rx.exportParams.size, { xy, wh } = params.area, total = this.source.totalSize; if (qx === 1 && qy === 1) { // Center this.rx.exportParams.offset = [ params.area = new Rect( clamp(Math.round(xy[0] + diff[0]), 0, total[0] - wh[0]), clamp(Math.round(xy[1] + diff[1]), 0, total[1] - wh[1])]; clamp(Math.round(xy[1] + diff[1]), 0, total[1] - wh[1]), ...wh, ); return; } // Switch from offset/size to start/end coordinates let xyMin = [xy[0], xy[1]] as [number, number], xyMax = [xy[0] + wh[0], xy[1] + wh[1]] as [number, number]; if (qx === 1 || qy === 1) { // Middle column or row if (qx === 1 || qy === 1) { // Middle column or row const ax = qy === 1 ? 0 : 1; if (qx === 0 || qy === 0) { // Left or bottom if (qx === 0 || qy === 0) { // Left or bottom xyMin[ax] = clamp(Math.round(xyMin[ax] + diff[ax]), 0, xyMax[ax] - 1); } else { // Right or top } else { // Right or top xyMax[ax] = clamp(Math.round(xyMax[ax] + diff[ax]), xyMin[ax] + 1, total[ax]); } } else { // Corner for (const ax of [0, 1] as const) { if ([qx, qy][ax] === 0) { // Left or bottom if ([qx, qy][ax] === 0) { // Left or bottom xyMin[ax] = clamp(Math.round(xyMin[ax] + diff[ax]), 0, xyMax[ax] - 1); } else { // Right or top } else { // Right or top xyMax[ax] = clamp(Math.round(xyMax[ax] + diff[ax]), xyMin[ax] + 1, total[ax]); } } } this.rx.exportParams.offset = xyMin; this.rx.exportParams.size = [xyMax[0] - xyMin[0], xyMax[1] - xyMin[1]]; params.area = Rect.fromCorners(xyMin, xyMax); } private getQuadrantCursor(xy: [0 | 1 | 2, 0 | 1 | 2] | null): string | null { return xy && [ return ( xy && [ ['sw-resize', 'w-resize', 'nw-resize'], ['s-resize', 'move', 'n-resize'], ['se-resize', 'e-resize', 'ne-resize'], ][xy[0]][xy[1]]; ][xy[0]][xy[1]] ); } private bindPointerEvents() { Loading Loading @@ -898,12 +926,12 @@ export class App { const exportWindowPart = (pos: vec2): [0 | 1 | 2, 0 | 1 | 2] | null => { if (this.rx.phase !== 'export' || lightball) return null; const [x, y] = this.rx.exportParams.offset, [w, h] = this.rx.exportParams.size; if (!new Rect(x, y, w, h).includes(pos)) return null; const exPar = this.rx.raw().exportParams; if (!exPar.outerGrab.includes(pos)) return null; return [ pos[0] < x + w / 3 ? 0 : pos[0] < x + (w * 2) / 3 ? 1 : 2, pos[1] < y + h / 3 ? 0 : pos[1] < y + (h * 2) / 3 ? 1 : 2, pos[0] >= exPar.innerGrab.x ? (pos[0] >= exPar.innerGrab.x2 ? 2 : 1) : 0, pos[1] >= exPar.innerGrab.y ? (pos[1] >= exPar.innerGrab.y2 ? 2 : 1) : 0, ]; }; Loading
src/gl-program.ts +32 −11 Changes for src/gl-program.ts: 32 added lines, 11 removed lines. Original line number Diff line number Diff line Loading @@ -428,11 +428,7 @@ export class LightballProgram extends GLProgramBase< circle = vec3.fromValues(center[0], center[1], radius), alpha = LightballProgram.ALPHA[ data.pointerState.overLightball ? data.pointerState.dragging ? 'active' : 'hover' : 'inactive' data.pointerState.overLightball ? (data.pointerState.dragging ? 'active' : 'hover') : 'inactive' ]; gl.uniformMatrix4fv(this.uniformLocations.projectionMatrix, false, projection); Loading @@ -457,6 +453,7 @@ export class ExportRectProgram extends GLProgramBase< [keyof typeof ExportRectProgram.attributes], [keyof typeof ExportRectProgram.uniformNames] > { private static readonly NONE_GRAB_RECT: readonly [number, number, number, number] = [0, 0, -1, -1]; public static readonly PARAMS = { cornerRadius: 25, lineSize: 1, Loading @@ -468,8 +465,9 @@ export class ExportRectProgram extends GLProgramBase< static uniformNames = { color: 'uColor', exportRect: 'uExportRect', windowPartHover: 'uWindowPartHover', grabRect: 'uGrabRect', lineWidth: 'uLineWidth', grabStatus: 'uGrabStatus', projectionMatrix: 'uProjectionMatrix', renderRect: 'uRenderRect', }; Loading Loading @@ -504,11 +502,34 @@ export class ExportRectProgram extends GLProgramBase< gl.uniformMatrix4fv(this.uniformLocations.projectionMatrix, false, data.viewport.projectionMat); let grabRect = ExportRectProgram.NONE_GRAB_RECT, grabStatus = 0; if (data.pointerState.exportWindowHover) { const windowPartVal = [...data.pointerState.exportWindowHover!, data.pointerState.dragging ? 2 : 1]; gl.uniform3iv(this.uniformLocations.windowPartHover, windowPartVal); } else gl.uniform3i(this.uniformLocations.windowPartHover, -1, -1, 0); gl.uniform4f(this.uniformLocations.exportRect, ...data.exportParams!.offset, ...data.exportParams!.size); const quad = data.pointerState.exportWindowHover, xBnds = [ data.exportParams.outerGrab.xmin, data.exportParams.innerGrab.xmin, data.exportParams.innerGrab.xmax, data.exportParams.outerGrab.xmax, ], yBnds = [ data.exportParams.outerGrab.ymin, data.exportParams.innerGrab.ymin, data.exportParams.innerGrab.ymax, data.exportParams.outerGrab.ymax, ]; grabRect = [ xBnds[quad[0]], yBnds[quad[1]], xBnds[quad[0] + 1] - xBnds[quad[0]], yBnds[quad[1] + 1] - yBnds[quad[1]], ]; grabStatus = data.pointerState.dragging ? 2 : 1; } gl.uniform4fv(this.uniformLocations.exportRect, data.exportParams.area.xywh); gl.uniform4fv(this.uniformLocations.grabRect, grabRect); gl.uniform1i(this.uniformLocations.grabStatus, grabStatus); gl.uniform4f(this.uniformLocations.renderRect, 0, 0, ...data.renderArea); gl.uniform1f(this.uniformLocations.lineWidth, lineSize); gl.uniform4fv(this.uniformLocations.color, color); Loading @@ -520,7 +541,7 @@ export class ExportRectProgram extends GLProgramBase< output += `Area: ${area}\n color: ${color}\n lineSize: ${lineSize}\n` + ` exportRect: ${new Rect(...data.exportParams.offset, ...data.exportParams.size)}\n` + ` exportRect: ${data.exportParams.area}\n` + ` renderRect: ${new Rect(0, 0, ...data.renderArea)}\n`; return output; Loading
src/shaders/exportRect.frag +11 −13 Changes for src/shaders/exportRect.frag: 11 added lines, 13 removed lines. Original line number Diff line number Diff line precision mediump float; uniform ivec3 uWindowPartHover; uniform mediump vec4 uColor; uniform mediump vec4 uRenderRect; uniform mediump vec4 uExportRect; uniform mediump vec4 uGrabRect; uniform mediump int uGrabStatus; uniform mediump float uLineWidth; varying mediump vec2 vVertexCoord; Loading @@ -16,25 +17,22 @@ float rectDist(vec2 pos, vec4 rect) { void main(void) { float dist = rectDist(vVertexCoord, uExportRect); float grabDist = uGrabStatus > 0 ? rectDist(vVertexCoord, uGrabRect) : 1000000.; bool inRender = bool(rectDist(vVertexCoord, uRenderRect) <= 0.); if (dist < 0.) { vec4 windowPart = vec4(uExportRect.xy + vec2(uWindowPartHover.xy) * uExportRect.zw / 3., uExportRect.zw / 3.); if (uWindowPartHover.z != 0 && rectDist(vVertexCoord, windowPart) <= 0.) { // Export window quadrant hover gl_FragColor = vec4(0., 0., 0., float(uWindowPartHover.z) * .2); } else { discard; // Inside export rectangle } } else if (dist <= uLineWidth * 4.) { bool onGrabBorder = bool(grabDist <= 0. && grabDist > -uLineWidth * 2.); if (dist >= 0. && dist <= uLineWidth * 4. && !onGrabBorder) { gl_FragColor = uColor; if (!inRender) // Full opacity outside of render area gl_FragColor.a = 1.; } else if (inRender) { } else if (grabDist <= 0.) { // Export window quadrant hover gl_FragColor = vec4(0., 0., 0., float(uGrabStatus) * (onGrabBorder ? .4 : .2)); } else if (dist > 0. && inRender) { // In render area, outside of export window gl_FragColor = vec4(0., 0., 0., mod((vVertexCoord.x + vVertexCoord.y) / (uLineWidth * 8.), 2.) > 1. ? .2 : .1); } else { // Outside of render area // Outside of render area or inside export window discard; } } No newline at end of file
src/style.sass +3 −5 Changes for src/style.sass: 3 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -143,11 +143,9 @@ body .export-panel display: grid grid-template-columns: 1fr 1fr .title margin-bottom: 40px grid-template-columns: auto 1fr grid-column-gap: 1rem justify-items: start .drop-shadow Loading