UNPKG

jeri

Version:

JavaScript Extended-Range Image viewer

388 lines 16.9 kB
"use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __assign = (this && this.__assign) || Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; Object.defineProperty(exports, "__esModule", { value: true }); var React = require("react"); var styled_components_1 = require("styled-components"); var linalg_1 = require("../../utils/linalg"); var number_aware_compare_1 = require("../../utils/number-aware-compare"); var fullscreen_1 = require("../../utils/fullscreen"); var HelpScreen_1 = require("./HelpScreen"); var Layer_1 = require("../../layers/Layer"); var ImageFrameWithLoading_1 = require("../ImageFrameWithLoading"); var navigation_1 = require("./navigation"); var MainDiv = (_a = ["\n background-color: #333;\n font-size: .9em;\n position: absolute;\n top: 0; bottom: 0; left: 0; right: 0;\n display: flex;\n flex-direction: column;\n color: #AAA;\n"], _a.raw = ["\n background-color: #333;\n font-size: .9em;\n position: absolute;\n top: 0; bottom: 0; left: 0; right: 0;\n display: flex;\n flex-direction: column;\n color: #AAA;\n"], styled_components_1.default.div(_a)); var ImageArea = (_b = ["\n flex-grow: 1;\n position: relative;\n"], _b.raw = ["\n flex-grow: 1;\n position: relative;\n"], styled_components_1.default.div(_b)); // A little hack to allow detecting shift click var SHIFT_IS_DOWN = false; document.addEventListener('keydown', function (ev) { if (ev.key === 'Shift') { SHIFT_IS_DOWN = true; } }); document.addEventListener('keyup', function (ev) { if (ev.key === 'Shift') { SHIFT_IS_DOWN = false; } }); var ImageViewer = /** @class */ (function (_super) { __extends(ImageViewer, _super); function ImageViewer(props) { var _this = _super.call(this, props) || this; _this.menuData = _this.props.data; if (props.sortMenu) { _this.menuData = _this.sortMenuRows(_this.menuData); } // Set the initial state _this.state = { activeRow: 0, selection: _this.getDefaultSelection(_this.menuData).slice(1), viewTransform: { default: 0.0 }, exposure: { default: 1.0 }, helpIsOpen: false, defaultTransformation: linalg_1.Matrix4x4.create(), transformationNeedsUpdate: true, hasFocus: false, }; // Make sure 'this' is available in the keyboard handler when assigned to the keyup event _this.keyboardHandler = _this.keyboardHandler.bind(_this); _this.setFocus = _this.setFocus.bind(_this); _this.unsetFocus = _this.unsetFocus.bind(_this); return _this; } ImageViewer.prototype.componentDidMount = function () { this.mainContainer.setAttribute('tabindex', '1'); this.mainContainer.addEventListener('keydown', this.keyboardHandler); this.mainContainer.addEventListener('focus', this.setFocus); this.mainContainer.addEventListener('focusout', this.unsetFocus); }; ImageViewer.prototype.componentDidUpdate = function () { if (this.imageFrame && this.state.transformationNeedsUpdate) { this.imageFrame.setTransformation(this.state.defaultTransformation); this.setState({ transformationNeedsUpdate: false }); } }; ImageViewer.prototype.componentWillReceiveProps = function (nextProps) { this.menuData = nextProps.data; if (this.props.sortMenu) { this.menuData = this.sortMenuRows(this.menuData); } this.validateSelection(this.state.selection, this.state.activeRow); }; ImageViewer.prototype.componentWillUnmount = function () { this.mainContainer.removeEventListener('keydown', this.keyboardHandler); }; ImageViewer.prototype.setTransformation = function (transformation) { if (this.imageFrame != null) { this.imageFrame.setTransformation(transformation); } this.setState({ defaultTransformation: transformation }); }; ImageViewer.prototype.render = function () { var _this = this; var rows = this.activeRows(this.menuData, this.state.selection); var imageSpec = this.imageSpec(); return (React.createElement(MainDiv, { innerRef: function (div) { return _this.mainContainer = div; } }, React.createElement("div", null, rows.map(function (row, i) { return (React.createElement(navigation_1.NavRow, { key: row.title, row: row, selection: _this.state.selection[i], handleClick: _this.navigateTo.bind(_this, rows, i), removeCommonPrefix: _this.props.removeCommonPrefix, active: _this.state.activeRow === i })); })), React.createElement(ImageArea, null, React.createElement(ImageFrameWithLoading_1.default, { viewTransform: this.state.viewTransform[imageSpec.tonemapGroup], exposure: this.state.exposure[imageSpec.tonemapGroup] || 1.0, gamma: 1.0, offset: 0.0, imageSpec: imageSpec, ref: function (frame) { return _this.imageFrame = (frame != null) ? frame.imageFrame : null; }, allowMovement: true, enableMouseEvents: this.state.hasFocus }), this.state.helpIsOpen ? React.createElement(HelpScreen_1.default, null) : null))); }; /** * Select the active rows from the navigation data tree, according to the given selection * * @param tree navigation datastructure * @param selection array of the titles of selected items from top to bottom */ ImageViewer.prototype.activeRows = function (tree, selection) { if (selection.length === 0) { // Base case of the recursion return []; } else { // Find the child with this name if (!tree.hasOwnProperty('children')) { throw new Error("Can't find match for " + selection); } var node = tree; var res = node.children.find(function (child) { return child.title === selection[0]; }); if (res == null) { throw new Error("Failed to find a match for " + selection); } else { return [node].concat(this.activeRows(res, selection.slice(1))); } } }; /** * Recursively sort the input data * * It's a bit smart, for example bathroom-32 will come before bathroom-128, * and the word Color always goes first. * @param tree to be sored */ ImageViewer.prototype.sortMenuRows = function (tree) { var _this = this; if (tree.hasOwnProperty('children')) { var node = tree; var children = node.children.map(function (child) { return _this.sortMenuRows(child); }); children.sort(function (a, b) { if (a.title === b.title) { return 0; } else if (a.title === 'Color') { return -1; } else if (b.title === 'Color') { return 1; } else { return number_aware_compare_1.default(a.title, b.title); } }); return { title: node.title, children: children, }; } else { return tree; } }; /** * Find the image to be shown based on the current selection */ ImageViewer.prototype.currentImage = function (currentSelection) { if (currentSelection === void 0) { currentSelection = this.state.selection; } var selection = currentSelection.slice(); var tree = this.menuData; var _loop_1 = function () { var entry = selection.shift(); tree = tree.children.find(function (item) { return item.title === entry; }); }; while (selection.length > 0) { _loop_1(); } return tree; // tslint:disable-line }; /** * Specification for the current image to load */ ImageViewer.prototype.imageSpec = function (currentSelection) { if (currentSelection === void 0) { currentSelection = this.state.selection; } var img = this.currentImage(currentSelection); if (img.hasOwnProperty('lossMap')) { var config = img; return { type: 'Difference', lossFunction: Layer_1.lossFunctionFromString(config.lossMap.function), urlA: this.props.baseUrl + config.lossMap.imageA, urlB: this.props.baseUrl + config.lossMap.imageB, tonemapGroup: config.tonemapGroup || 'default', }; } else { return { type: 'Url', url: this.props.baseUrl + img.image, tonemapGroup: img.tonemapGroup || 'default', }; } }; /** * Navigate to a particular image * * @param rows: a list of the rows currently visible * @param rowIndex: the index of the row in which to switch tabs * @param title: the title of the requested node * * For rows > rowIndex, we select children matching the current selection titles * if they exist. Otherwise, we resort to the default selection (first elements). */ ImageViewer.prototype.navigateTo = function (rows, rowIndex, title) { var selection = this.state.selection.slice(); selection[rowIndex] = title; var activeRow = this.state.activeRow; if (SHIFT_IS_DOWN) { // Set active row on shift click activeRow = rowIndex; } this.validateSelection(selection, activeRow); }; /** * Make sure that the current selection is valid given the current menuData * * If a title in the selection does not exist in the respective row, take the default * (first) element of the row. * @param wishes the desired selection, which might not be valid given the selected menu items */ ImageViewer.prototype.validateSelection = function (wishes, activeRow) { var selection = []; var i = 0; var root = this.menuData; while (root.hasOwnProperty('children')) { var candidate = root.children.find(function (row) { return row.title === wishes[i]; }); if (candidate) { root = candidate; selection.push(candidate.title); } else { root = root.children[0]; // resort to the first selection.push(root.title); } i++; } this.setState({ selection: selection, activeRow: Math.min(activeRow, selection.length - 1), }); }; /** * Return the titles of the first items of a sorted tree * @param tree a sorted navigation data structure */ ImageViewer.prototype.getDefaultSelection = function (tree) { if (tree.hasOwnProperty('children')) { var node = tree; if (node.children.length > 0) { return [node.title].concat(this.getDefaultSelection(node.children[0])); } else { return [node.title]; } } else { return [tree.title]; } }; ImageViewer.prototype.dumpTransformation = function () { if (this.imageFrame != null) { var transformation = this.imageFrame.getTransformation(); console.log(transformation.data); } }; ImageViewer.prototype.keyboardHandler = function (event) { var _this = this; var key = event.key; var actions = {}; var actionsUnderShift = {}; // Number keys var goToNumber = function (i) { return function () { var rows = _this.activeRows(_this.menuData, _this.state.selection); var activeRow = _this.state.activeRow; var goTo = rows[activeRow].children[i]; if (goTo != null) { _this.navigateTo(rows, activeRow, goTo.title); } }; }; actions['0'] = goToNumber(9); for (var i = 1; i <= 9; ++i) { actions[i.toString()] = goToNumber(i - 1); } // Arrows var moveInLine = function (offset) { return function () { var rows = _this.activeRows(_this.menuData, _this.state.selection); var activeRow = _this.state.activeRow; var currentTitle = _this.state.selection[activeRow]; var currentIndex = rows[activeRow].children.findIndex(function (n) { return n.title === currentTitle; }); var nextIndex = (currentIndex + offset + rows[activeRow].children.length) % rows[activeRow].children.length; var goTo = rows[activeRow].children[nextIndex]; _this.navigateTo(rows, activeRow, goTo.title); }; }; actionsUnderShift.ArrowLeft = moveInLine(-1); actionsUnderShift.ArrowRight = moveInLine(1); actions['-'] = moveInLine(-1); actions['='] = moveInLine(1); var moveUpDown = function (offset) { return function () { var nextRow = _this.state.activeRow + offset; if (nextRow < 0) { nextRow = 0; } if (nextRow >= _this.state.selection.length - 1) { nextRow = _this.state.selection.length - 1; } _this.setState({ activeRow: nextRow }); }; }; actionsUnderShift.ArrowUp = moveUpDown(-1); actionsUnderShift.ArrowDown = moveUpDown(1); actions['['] = moveUpDown(-1); actions[']'] = moveUpDown(1); // ViewTransform controls var changeViewTransform = function () { return function () { var tonemapGroup = _this.imageSpec().tonemapGroup; var viewTransform = __assign({}, _this.state.viewTransform, (_a = {}, _a[tonemapGroup] = (Math.abs(_this.state.viewTransform[tonemapGroup] - 1)), _a)); _this.setState({ viewTransform: viewTransform }); var _a; }; }; actions.t = changeViewTransform(); // Exposure controls var changeExposure = function (multiplier) { return function () { var tonemapGroup = _this.imageSpec().tonemapGroup; var exposure = __assign({}, _this.state.exposure, (_a = {}, _a[tonemapGroup] = multiplier * (_this.state.exposure[tonemapGroup] || 1.0), _a)); _this.setState({ exposure: exposure }); var _a; }; }; actions.e = changeExposure(1.1); actions.E = changeExposure(1.0 / 1.1); // Reset actions.r = function () { _this.setState({ viewTransform: { default: 0.0 } }); _this.setState({ exposure: { default: 1.0 } }); if (_this.imageFrame) { _this.imageFrame.reset(); } }; // Toggle help actions['/'] = actions['?'] = function () { _this.setState({ helpIsOpen: !_this.state.helpIsOpen }); }; actions.Escape = function () { _this.setState({ helpIsOpen: false }); }; // Go fullscreen actions.f = function () { return fullscreen_1.default(_this.mainContainer); }; // Dump the current transformation actions.d = function () { return _this.dumpTransformation(); }; if (actions.hasOwnProperty(key) && !event.metaKey && !event.altKey && !event.ctrlKey) { event.preventDefault(); actions[key](); return; } if (actionsUnderShift.hasOwnProperty(key) && event.shiftKey) { event.preventDefault(); actionsUnderShift[key](); return; } }; ImageViewer.prototype.setFocus = function () { this.setState({ hasFocus: true }); }; ImageViewer.prototype.unsetFocus = function () { this.setState({ hasFocus: false }); }; ImageViewer.defaultProps = { baseUrl: '', sortMenu: false, removeCommonPrefix: false, }; return ImageViewer; }(React.Component)); exports.default = ImageViewer; var _a, _b; //# sourceMappingURL=index.js.map