diff --git a/srcjs/output_binding_image.js b/srcjs/output_binding_image.js index e8fbd4aea..5acc3fb24 100644 --- a/srcjs/output_binding_image.js +++ b/srcjs/output_binding_image.js @@ -188,8 +188,10 @@ outputBindings.register(imageOutputBinding, 'shiny.imageOutput'); var imageutils = {}; -// Modifies the panel objects in a coordmap, adding scale(), scaleInv(), -// and clip() functions to each one. +// Modifies the panel objects in a coordmap, adding scaleImgToData(), +// scaleDataToImg(), and clipImg() functions to each one. The panel objects +// use img and data coordinates only; they do not use css coordinates. The +// domain is in data coordinates; the range is in img coordinates. imageutils.initPanelScales = function(coordmap) { // Map a value x from a domain to a range. If clip is true, clip it to the // range. @@ -240,34 +242,47 @@ imageutils.initPanelScales = function(coordmap) { var xscaler = scaler1D(d.left, d.right, r.left, r.right, xlog); var yscaler = scaler1D(d.bottom, d.top, r.bottom, r.top, ylog); - panel.scale = function(val, clip) { - return { - x: xscaler.scale(val.x, clip), - y: yscaler.scale(val.y, clip) - }; + // Given an object of form {x:1, y:2}, or {x:1, xmin:2:, ymax: 3}, convert + // from data coordinates to img. Whether a value is converted as x or y + // depends on the first character of the key. + panel.scaleDataToImg = function(val, clip) { + return mapValues(val, (value, key) => { + const prefix = key.substring(0, 1); + if (prefix === "x") { + return xscaler.scale(value, clip); + } else if (prefix === "y") { + return yscaler.scale(value, clip); + } + return null; + }); }; - panel.scaleInv = function(val, clip) { - return { - x: xscaler.scaleInv(val.x, clip), - y: yscaler.scaleInv(val.y, clip) - }; + panel.scaleImgToData = function(val, clip) { + return mapValues(val, (value, key) => { + const prefix = key.substring(0, 1); + if (prefix === "x") { + return xscaler.scaleInv(value, clip); + } else if (prefix === "y") { + return yscaler.scaleInv(value, clip); + } + return null; + }); }; - // Given a scaled offset (in pixels), clip it to the nearest panel region. - panel.clip = function(offset) { + // Given a scaled offset (in img pixels), clip it to the nearest panel region. + panel.clipImg = function(offset_img) { var newOffset = { - x: offset.x, - y: offset.y + x: offset_img.x, + y: offset_img.y }; var bounds = panel.range; - if (offset.x > bounds.right) newOffset.x = bounds.right; - else if (offset.x < bounds.left) newOffset.x = bounds.left; + if (offset_img.x > bounds.right) newOffset.x = bounds.right; + else if (offset_img.x < bounds.left) newOffset.x = bounds.left; - if (offset.y > bounds.bottom) newOffset.y = bounds.bottom; - else if (offset.y < bounds.top) newOffset.y = bounds.top; + if (offset_img.y > bounds.bottom) newOffset.y = bounds.bottom; + else if (offset_img.y < bounds.top) newOffset.y = bounds.top; return newOffset; }; @@ -282,12 +297,24 @@ imageutils.initPanelScales = function(coordmap) { // This adds functions to the coordmap object to handle various -// coordinate-mapping tasks, and send information to the server. -// The input coordmap is an array of objects, each of which represents a panel. -// coordmap must be an array, even if empty, so that it can be modified in -// place; when empty, we add a dummy panel to the array. -// It also calls initPanelScales, which modifies each panel object to have -// scale, scaleInv, and clip functions. +// coordinate-mapping tasks, and send information to the server. The input +// coordmap is an array of objects, each of which represents a panel. coordmap +// must be an array, even if empty, so that it can be modified in place; when +// empty, we add a dummy panel to the array. It also calls initPanelScales, +// which modifies each panel object to have scaleImgToData, scaleDataToImg, +// and clip functions. +// +// There are three coordinate spaces which we need to translate between: +// +// 1. css: The pixel coordinates in the web browser, also known as CSS pixels. +// The origin is the upper-left corner of the (not including padding +// and border). +// 2. img: The pixel coordinates of the image data. A common case is on a +// HiDPI device, where the source PNG image could be 1000 pixels wide but +// be displayed in 500 CSS pixels. Another case is when the image has +// additional scaling due to CSS transforms or width. +// 3. data: The coordinates in the data space. This is a bit more complicated +// than the other two, because there can be multiple panels (as in facets). imageutils.initCoordmap = function($el, coordmap) { var el = $el[0]; @@ -313,167 +340,107 @@ imageutils.initCoordmap = function($el, coordmap) { imageutils.initPanelScales(coordmap); - // Returns the ratio that an element has been scaled (for example, by CSS - // transforms) in the x and y directions. - function findScalingRatio($el) { - const boundingRect = $el[0].getBoundingClientRect(); - return { - x: boundingRect.width / $el.outerWidth(), - y: boundingRect.height / $el.outerHeight() - }; - } - - function findOrigin($el) { - const offset = $el.offset(); - const scaling_ratio = findScalingRatio($el); - - // Find the size of the padding and border, for the top and left. This is - // before any transforms. - const paddingBorder = { - left: parseInt($el.css("border-left")) + parseInt($el.css("padding-left")), - top: parseInt($el.css("border-top")) + parseInt($el.css("padding-top")) - }; - - // offset() returns the upper left corner of the element relative to the - // page, but it includes padding and border. Here we find the upper left - // of the element, not including padding and border. - return { - x: offset.left + scaling_ratio.x * paddingBorder.left, - y: offset.top + scaling_ratio.y * paddingBorder.top - }; - } - - // Find the dimensions of a tag, after transforms, and without padding and - // border. - function findDims($el) { - // If there's any padding/border, we need to find the ratio of the actual - // element content compared to the element plus padding and border. - const content_ratio = { - x: $el.width() / $el.outerWidth(), - y: $el.height() / $el.outerHeight() - }; - - // Get the dimensions of the element _after_ any CSS transforms. This - // includes the padding and border. - const bounding_rect = $el[0].getBoundingClientRect(); - - // Dimensions of the element after any CSS transforms, and without - // padding/border. - return { - x: content_ratio.x * bounding_rect.width, - y: content_ratio.y * bounding_rect.height - }; - } - - // Returns the x and y ratio that image content (like a PNG) is scaled to on - // screen. If the image data is 1000 pixels wide and is scaled to 300 pixels - // on screen, then this returns 0.3. (Note the 300 pixels refers to CSS - // pixels.) - function findImgPixelScalingRatio($img) { - const img_dims = findDims($img); - return { - x: img_dims.x / $img[0].naturalWidth, - y: img_dims.y / $img[0].naturalHeight - }; - } - - // This returns the offset of the mouse, relative to the img, but with some - // extra sauce. First, it returns the offset in the pixel dimensions of the - // image as if it were scaled to 100%. If the img content is 1000 pixels - // wide, but is scaled to 400 pixels on screen, and the mouse is on the far - // right side, then this will return x:1000. Second, if there is any padding - // or border around the img, it handles that. Third, if there are any - // scaling transforms on the image, it handles that as well. - coordmap.mouseOffset = function(mouseEvent) { + // This returns the offset of the mouse in CSS pixels relative to the img, + // but not including the padding or border, if present. + coordmap.mouseOffsetCss = function(mouseEvent) { const $img = $el.find("img"); const img_origin = findOrigin($img); // The offset of the mouse from the upper-left corner of the img, in // pixels. - const offset_raw = { + return { x: mouseEvent.pageX - img_origin.x, y: mouseEvent.pageY - img_origin.y }; + }; - const pixel_scaling = findImgPixelScalingRatio($img); + // Given an offset in an img in CSS pixels, return the corresponding offset + // in source image pixels. The offset_css can have properties like "x", + // "xmin", "y", and "ymax" -- anything that starts with "x" and "y". If the + // img content is 1000 pixels wide, but is scaled to 400 pixels on screen, + // and the input is x:400, then this will return x:1000. + coordmap.scaleCssToImg = function(offset_css) { + const $img = $el.find("img"); + const pixel_scaling = findImgToCssScalingRatio($img); - return { - x: offset_raw.x / pixel_scaling.x, - y: offset_raw.y / pixel_scaling.y + const result = mapValues(offset_css, (value, key) => { + const prefix = key.substring(0, 1); + + if (prefix === "x") { + return offset_css[key] / pixel_scaling.x; + } else if (prefix === "y") { + return offset_css[key] / pixel_scaling.y; + } + return null; + }); + + return result; + }; + + // Given an offset in an img, in source image pixels, return the + // corresponding offset in CSS pixels. If the img content is 1000 pixels + // wide, but is scaled to 400 pixels on screen, and the input is x:1000, + // then this will return x:400. + coordmap.scaleImgToCss = function(offset_img) { + const $img = $el.find("img"); + const pixel_scaling = findImgToCssScalingRatio($img); + + const result = mapValues(offset_img, (value, key) => { + const prefix = key.substring(0, 1); + + if (prefix === "x") { + return offset_img[key] * pixel_scaling.x; + } else if (prefix === "y") { + return offset_img[key] * pixel_scaling.y; + } + return null; + }); + + return result; + }; + + // Given an offset in css pixels, return an object representing which panel + // it's in. The `expand` argument tells it to expand the panel area by that + // many pixels. It's possible for an offset to be within more than one + // panel, because of the `expand` value. If that's the case, find the + // nearest panel. + coordmap.getPanelCss = function(offset_css, expand = 0) { + const offset_img = coordmap.scaleCssToImg(offset_css); + const x = offset_img.x; + const y = offset_img.y; + + // Convert expand from css pixels to img pixels + const $img = $el.find("img"); + const imgToCssRatio = findImgToCssScalingRatio($img); + const expand_img = { + x: expand / imgToCssRatio.x, + y: expand / imgToCssRatio.y }; - }; - // Given two sets of x/y coordinates, return an object representing the - // min and max x and y values. (This could be generalized to any number - // of points). - coordmap.findBox = function(offset1, offset2) { - return { - xmin: Math.min(offset1.x, offset2.x), - xmax: Math.max(offset1.x, offset2.x), - ymin: Math.min(offset1.y, offset2.y), - ymax: Math.max(offset1.y, offset2.y) - }; - }; - - - // Shift an array of values so that they are within a min and max. - // The vals will be shifted so that they maintain the same spacing - // internally. If the range in vals is larger than the range of - // min and max, the result might not make sense. - coordmap.shiftToRange = function(vals, min, max) { - if (!(vals instanceof Array)) - vals = [vals]; - - var maxval = Math.max.apply(null, vals); - var minval = Math.min.apply(null, vals); - var shiftAmount = 0; - if (maxval > max) { - shiftAmount = max - maxval; - } else if (minval < min) { - shiftAmount = min - minval; - } - - var newvals = []; - for (var i=0; i= b.left - expand && - y <= b.bottom + expand && - y >= b.top - expand) + if (x <= b.right + expand_img.x && + x >= b.left - expand_img.x && + y <= b.bottom + expand_img.y && + y >= b.top - expand_img.y) { matches.push(coordmap[i]); // Find distance from edges for x and y var xdist = 0; var ydist = 0; - if (x > b.right && x <= b.right + expand) { + if (x > b.right && x <= b.right + expand_img.x) { xdist = x - b.right; - } else if (x < b.left && x >= b.left - expand) { + } else if (x < b.left && x >= b.left - expand_img.x) { xdist = x - b.left; } - if (y > b.bottom && y <= b.bottom + expand) { + if (y > b.bottom && y <= b.bottom + expand_img.y) { ydist = y - b.bottom; - } else if (y < b.top && y >= b.top - expand) { + } else if (y < b.top && y >= b.top - expand_img.y) { ydist = y - b.top; } @@ -495,12 +462,10 @@ imageutils.initCoordmap = function($el, coordmap) { return null; }; - // Is an offset in a panel? If supplied, `expand` tells us to expand the - // panels by that many pixels in all directions. - coordmap.isInPanel = function(offset, expand) { - expand = expand || 0; - - if (coordmap.getPanel(offset, expand)) + // Is an offset (in css pixels) in a panel? If supplied, `expand` tells us + // to expand the panels by that many pixels in all directions. + coordmap.isInPanelCss = function(offset_css, expand = 0) { + if (coordmap.getPanelCss(offset_css, expand)) return true; return false; @@ -518,9 +483,9 @@ imageutils.initCoordmap = function($el, coordmap) { return; } - var offset = coordmap.mouseOffset(e); + const offset_css = coordmap.mouseOffsetCss(e); // If outside of plotting region - if (!coordmap.isInPanel(offset)) { + if (!coordmap.isInPanelCss(offset_css)) { if (nullOutside) { exports.setInputValue(inputId, null); return; @@ -528,10 +493,11 @@ imageutils.initCoordmap = function($el, coordmap) { if (clip) return; } - if (clip && !coordmap.isInPanel(offset)) return; + if (clip && !coordmap.isInPanelCss(offset_css)) return; - var panel = coordmap.getPanel(offset); - var coords = panel.scaleInv(offset); + const panel = coordmap.getPanelCss(offset_css); + + const coords = panel.scaleImgToData(coordmap.scaleCssToImg(offset_css)); // Add the panel (facet) variables, if present $.extend(coords, panel.panel_vars); @@ -550,6 +516,44 @@ imageutils.initCoordmap = function($el, coordmap) { }; +// Given two sets of x/y coordinates, return an object representing the min +// and max x and y values. (This could be generalized to any number of +// points). +imageutils.findBox = function(offset1, offset2) { + return { + xmin: Math.min(offset1.x, offset2.x), + xmax: Math.max(offset1.x, offset2.x), + ymin: Math.min(offset1.y, offset2.y), + ymax: Math.max(offset1.y, offset2.y) + }; +}; + +// Shift an array of values so that they are within a min and max. The vals +// will be shifted so that they maintain the same spacing internally. If the +// range in vals is larger than the range of min and max, the result might not +// make sense. +imageutils.shiftToRange = function(vals, min, max) { + if (!(vals instanceof Array)) + vals = [vals]; + + var maxval = Math.max.apply(null, vals); + var minval = Math.min.apply(null, vals); + var shiftAmount = 0; + if (maxval > max) { + shiftAmount = max - maxval; + } else if (minval < min) { + shiftAmount = min - minval; + } + + var newvals = []; + for (var i=0; i= bounds.xmin && - offset.y <= bounds.ymax && offset.y >= bounds.ymin; + function isInsideBrush(offset_css) { + var bounds = state.boundsCss; + return offset_css.x <= bounds.xmax && offset_css.x >= bounds.xmin && + offset_css.y <= bounds.ymax && offset_css.y >= bounds.ymin; } // Return true if offset is inside a region to start a resize - function isInResizeArea(offset) { - var sides = whichResizeSides(offset); + function isInResizeArea(offset_css) { + var sides = whichResizeSides(offset_css); return sides.left || sides.right || sides.top || sides.bottom; } // Return an object representing which resize region(s) the cursor is in. - function whichResizeSides(offset) { - var b = state.boundsPx; + function whichResizeSides(offset_css) { + const b = state.boundsCss; // Bounds with expansion - var e = { + const e = { xmin: b.xmin - resizeExpand, xmax: b.xmax + resizeExpand, ymin: b.ymin - resizeExpand, ymax: b.ymax + resizeExpand }; - var res = { - left: false, - right: false, - top: false, + const res = { + left: false, + right: false, + top: false, bottom: false }; if ((opts.brushDirection === 'xy' || opts.brushDirection === 'x') && - (offset.y <= e.ymax && offset.y >= e.ymin)) + (offset_css.y <= e.ymax && offset_css.y >= e.ymin)) { - if (offset.x < b.xmin && offset.x >= e.xmin) + if (offset_css.x < b.xmin && offset_css.x >= e.xmin) res.left = true; - else if (offset.x > b.xmax && offset.x <= e.xmax) + else if (offset_css.x > b.xmax && offset_css.x <= e.xmax) res.right = true; } if ((opts.brushDirection === 'xy' || opts.brushDirection === 'y') && - (offset.x <= e.xmax && offset.x >= e.xmin)) + (offset_css.x <= e.xmax && offset_css.x >= e.xmin)) { - if (offset.y < b.ymin && offset.y >= e.ymin) + if (offset_css.y < b.ymin && offset_css.y >= e.ymin) res.top = true; - else if (offset.y > b.ymax && offset.y <= e.ymax) + else if (offset_css.y > b.ymax && offset_css.y <= e.ymax) res.bottom = true; } @@ -1150,24 +1161,24 @@ imageutils.createBrush = function($el, opts, coordmap, expandPixels) { } - // Sets the bounds of the brush, given a box and optional panel. This - // will fit the box bounds into the panel, so we don't brush outside of it. - // This knows whether we're brushing in the x, y, or xy directions, and sets - // bounds accordingly. - // If no box is passed in, just return current bounds. - function boundsPx(box) { - if (box === undefined) - return state.boundsPx; + // Sets the bounds of the brush (in CSS pixels), given a box and optional + // panel. This will fit the box bounds into the panel, so we don't brush + // outside of it. This knows whether we're brushing in the x, y, or xy + // directions, and sets bounds accordingly. If no box is passed in, just + // return current bounds. + function boundsCss(box_css) { + if (box_css === undefined) + return state.boundsCss; - var min = { x: box.xmin, y: box.ymin }; - var max = { x: box.xmax, y: box.ymax }; + let min_css = { x: box_css.xmin, y: box_css.ymin }; + let max_css = { x: box_css.xmax, y: box_css.ymax }; - var panel = state.panel; - var panelBounds = panel.range; + const panel = state.panel; + const panelBounds_img = panel.range; if (opts.brushClip) { - min = panel.clip(min); - max = panel.clip(max); + min_css = imgToCss(panel.clipImg(cssToImg(min_css))); + max_css = imgToCss(panel.clipImg(cssToImg(max_css))); } if (opts.brushDirection === 'xy') { @@ -1175,27 +1186,27 @@ imageutils.createBrush = function($el, opts, coordmap, expandPixels) { } else if (opts.brushDirection === 'x') { // Extend top and bottom of plotting area - min.y = panelBounds.top; - max.y = panelBounds.bottom; + min_css.y = imgToCss({y: panelBounds_img.top }).y; + max_css.y = imgToCss({y: panelBounds_img.bottom}).y; } else if (opts.brushDirection === 'y') { - min.x = panelBounds.left; - max.x = panelBounds.right; + min_css.x = imgToCss({x: panelBounds_img.left }).x; + max_css.x = imgToCss({x: panelBounds_img.right}).x; } - state.boundsPx = { - xmin: min.x, - xmax: max.x, - ymin: min.y, - ymax: max.y + state.boundsCss = { + xmin: min_css.x, + xmax: max_css.x, + ymin: min_css.y, + ymax: max_css.y }; // Positions in data space - var minData = state.panel.scaleInv(min); - var maxData = state.panel.scaleInv(max); + const min_data = state.panel.scaleImgToData(cssToImg(min_css)); + const max_data = state.panel.scaleImgToData(cssToImg(max_css)); // For reversed scales, the min and max can be reversed, so use findBox // to ensure correct order. - state.boundsData = coordmap.findBox(minData, maxData); + state.boundsData = imageutils.findBox(min_data, max_data); // Round to 14 significant digits to avoid spurious changes in FP values // (#1634). state.boundsData = mapValues(state.boundsData, val => roundSignif(val, 14)); @@ -1209,24 +1220,20 @@ imageutils.createBrush = function($el, opts, coordmap, expandPixels) { } // Get or set the bounds of the brush using coordinates in the data space. - function boundsData(box) { - if (box === undefined) { + function boundsData(box_data) { + if (box_data === undefined) { return state.boundsData; } - var min = { x: box.xmin, y: box.ymin }; - var max = { x: box.xmax, y: box.ymax }; - - var minPx = state.panel.scale(min); - var maxPx = state.panel.scale(max); + const box_css = imgToCss(state.panel.scaleDataToImg(box_data)); // The scaling function can reverse the direction of the axes, so we need to // find the min and max again. - boundsPx({ - xmin: Math.min(minPx.x, maxPx.x), - xmax: Math.max(minPx.x, maxPx.x), - ymin: Math.min(minPx.y, maxPx.y), - ymax: Math.max(minPx.y, maxPx.y) + boundsCss({ + xmin: Math.min(box_css.xmin, box_css.xmax), + xmax: Math.max(box_css.xmin, box_css.xmax), + ymin: Math.min(box_css.ymin, box_css.ymax), + ymax: Math.max(box_css.ymin, box_css.ymax) }); return undefined; } @@ -1275,29 +1282,30 @@ imageutils.createBrush = function($el, opts, coordmap, expandPixels) { function updateDiv() { // Need parent offset relative to page to calculate mouse offset // relative to page. - var imgOffset = $el.offset(); - var b = state.boundsPx; + const img_offset_css = findOrigin($el.find("img")); + const b = state.boundsCss; + $div.offset({ - top: imgOffset.top + b.ymin, - left: imgOffset.left + b.xmin + top: img_offset_css.y + b.ymin, + left: img_offset_css.x + b.xmin }) .outerWidth(b.xmax - b.xmin + 1) .outerHeight(b.ymax - b.ymin + 1); } - function down(offset) { - if (offset === undefined) + function down(offset_css) { + if (offset_css === undefined) return state.down; - state.down = offset; + state.down = offset_css; return undefined; } - function up(offset) { - if (offset === undefined) + function up(offset_css) { + if (offset_css === undefined) return state.up; - state.up = offset; + state.up = offset_css; return undefined; } @@ -1308,23 +1316,22 @@ imageutils.createBrush = function($el, opts, coordmap, expandPixels) { function startBrushing() { state.brushing = true; addDiv(); - state.panel = coordmap.getPanel(state.down, expandPixels); + state.panel = coordmap.getPanelCss(state.down, expandPixels); - boundsPx(coordmap.findBox(state.down, state.down)); + boundsCss(imageutils.findBox(state.down, state.down)); updateDiv(); } - function brushTo(offset) { - boundsPx(coordmap.findBox(state.down, offset)); + function brushTo(offset_css) { + boundsCss(imageutils.findBox(state.down, offset_css)); $div.show(); updateDiv(); } function stopBrushing() { state.brushing = false; - // Save the final bounding box of the brush - boundsPx(coordmap.findBox(state.down, state.up)); + boundsCss(imageutils.findBox(state.down, state.up)); } function isDragging() { @@ -1333,17 +1340,17 @@ imageutils.createBrush = function($el, opts, coordmap, expandPixels) { function startDragging() { state.dragging = true; - state.changeStartBounds = $.extend({}, state.boundsPx); + state.changeStartBounds = $.extend({}, state.boundsCss); } - function dragTo(offset) { + function dragTo(offset_css) { // How far the brush was dragged - var dx = offset.x - state.down.x; - var dy = offset.y - state.down.y; + const dx = offset_css.x - state.down.x; + const dy = offset_css.y - state.down.y; // Calculate what new positions would be, before clipping. - var start = state.changeStartBounds; - var newBounds = { + const start = state.changeStartBounds; + let newBounds_css = { xmin: start.xmin + dx, xmax: start.xmax + dx, ymin: start.ymin + dy, @@ -1352,25 +1359,26 @@ imageutils.createBrush = function($el, opts, coordmap, expandPixels) { // Clip to the plotting area if (opts.brushClip) { - var panelBounds = state.panel.range; + const panelBounds_img = state.panel.range; + const newBounds_img = cssToImg(newBounds_css); // Convert to format for shiftToRange - var xvals = [ newBounds.xmin, newBounds.xmax ]; - var yvals = [ newBounds.ymin, newBounds.ymax ]; + let xvals_img = [ newBounds_img.xmin, newBounds_img.xmax ]; + let yvals_img = [ newBounds_img.ymin, newBounds_img.ymax ]; - xvals = coordmap.shiftToRange(xvals, panelBounds.left, panelBounds.right); - yvals = coordmap.shiftToRange(yvals, panelBounds.top, panelBounds.bottom); + xvals_img = imageutils.shiftToRange(xvals_img, panelBounds_img.left, panelBounds_img.right); + yvals_img = imageutils.shiftToRange(yvals_img, panelBounds_img.top, panelBounds_img.bottom); // Convert back to bounds format - newBounds = { - xmin: xvals[0], - xmax: xvals[1], - ymin: yvals[0], - ymax: yvals[1] - }; + newBounds_css = imgToCss({ + xmin: xvals_img[0], + xmax: xvals_img[1], + ymin: yvals_img[0], + ymax: yvals_img[1] + }); } - boundsPx(newBounds); + boundsCss(newBounds_css); updateDiv(); } @@ -1384,32 +1392,40 @@ imageutils.createBrush = function($el, opts, coordmap, expandPixels) { function startResizing() { state.resizing = true; - state.changeStartBounds = $.extend({}, state.boundsPx); + state.changeStartBounds = $.extend({}, state.boundsCss); state.resizeSides = whichResizeSides(state.down); } - function resizeTo(offset) { + function resizeTo(offset_css) { // How far the brush was dragged - var dx = offset.x - state.down.x; - var dy = offset.y - state.down.y; + const d_css = { + x: offset_css.x - state.down.x, + y: offset_css.y - state.down.y + }; + + const d_img = cssToImg(d_css); // Calculate what new positions would be, before clipping. - var b = $.extend({}, state.changeStartBounds); - var panelBounds = state.panel.range; + const b_img = cssToImg(state.changeStartBounds); + const panelBounds_img = state.panel.range; if (state.resizeSides.left) { - b.xmin = coordmap.shiftToRange([b.xmin + dx], panelBounds.left, b.xmax)[0]; + const xmin_img = imageutils.shiftToRange(b_img.xmin + d_img.x, panelBounds_img.left, b_img.xmax)[0]; + b_img.xmin = xmin_img; } else if (state.resizeSides.right) { - b.xmax = coordmap.shiftToRange([b.xmax + dx], b.xmin, panelBounds.right)[0]; + const xmax_img = imageutils.shiftToRange(b_img.xmax + d_img.x, b_img.xmin, panelBounds_img.right)[0]; + b_img.xmax = xmax_img; } if (state.resizeSides.top) { - b.ymin = coordmap.shiftToRange([b.ymin + dy], panelBounds.top, b.ymax)[0]; + const ymin_img = imageutils.shiftToRange(b_img.ymin + d_img.y, panelBounds_img.top, b_img.ymax)[0]; + b_img.ymin = ymin_img; } else if (state.resizeSides.bottom) { - b.ymax = coordmap.shiftToRange([b.ymax + dy], b.ymin, panelBounds.bottom)[0]; + const ymax_img = imageutils.shiftToRange(b_img.ymax + d_img.y, b_img.ymin, panelBounds_img.bottom)[0]; + b_img.ymax = ymax_img; } - boundsPx(b); + boundsCss(imgToCss(b_img)); updateDiv(); } @@ -1425,7 +1441,7 @@ imageutils.createBrush = function($el, opts, coordmap, expandPixels) { isInResizeArea: isInResizeArea, whichResizeSides: whichResizeSides, - boundsPx: boundsPx, + boundsCss: boundsCss, boundsData: boundsData, getPanel: getPanel, @@ -1455,3 +1471,73 @@ exports.resetBrush = function(brushId) { brushId: brushId, outputId: null }); }; + + +// ----------------------------------------------------------------------- +// Utility functions for finding dimensions and locations of DOM elements +// ----------------------------------------------------------------------- + +// Returns the ratio that an element has been scaled (for example, by CSS +// transforms) in the x and y directions. +function findScalingRatio($el) { + const boundingRect = $el[0].getBoundingClientRect(); + return { + x: boundingRect.width / $el.outerWidth(), + y: boundingRect.height / $el.outerHeight() + }; +} + +function findOrigin($el) { + const offset = $el.offset(); + const scaling_ratio = findScalingRatio($el); + + // Find the size of the padding and border, for the top and left. This is + // before any transforms. + const paddingBorder = { + left: parseInt($el.css("border-left")) + parseInt($el.css("padding-left")), + top: parseInt($el.css("border-top")) + parseInt($el.css("padding-top")) + }; + + // offset() returns the upper left corner of the element relative to the + // page, but it includes padding and border. Here we find the upper left + // of the element, not including padding and border. + return { + x: offset.left + scaling_ratio.x * paddingBorder.left, + y: offset.top + scaling_ratio.y * paddingBorder.top + }; +} + +// Find the dimensions of a tag, after transforms, and without padding and +// border. +function findDims($el) { + // If there's any padding/border, we need to find the ratio of the actual + // element content compared to the element plus padding and border. + const content_ratio = { + x: $el.width() / $el.outerWidth(), + y: $el.height() / $el.outerHeight() + }; + + // Get the dimensions of the element _after_ any CSS transforms. This + // includes the padding and border. + const bounding_rect = $el[0].getBoundingClientRect(); + + // Dimensions of the element after any CSS transforms, and without + // padding/border. + return { + x: content_ratio.x * bounding_rect.width, + y: content_ratio.y * bounding_rect.height + }; +} + +// Returns the x and y ratio that image content (like a PNG) is scaled to on +// screen. If the image data is 1000 pixels wide and is scaled to 300 pixels +// on screen, then this returns 0.3. (Note the 300 pixels refers to CSS +// pixels.) +// TODO: memoize, don't take $img as input? +function findImgToCssScalingRatio($img) { + const img_dims = findDims($img); + return { + x: img_dims.x / $img[0].naturalWidth, + y: img_dims.y / $img[0].naturalHeight + }; +} diff --git a/srcjs/utils.js b/srcjs/utils.js index 0b3616a20..aa58c9ccb 100644 --- a/srcjs/utils.js +++ b/srcjs/utils.js @@ -249,7 +249,7 @@ function mapValues(obj, f) { const newObj = {}; for (let key in obj) { if (obj.hasOwnProperty(key)) - newObj[key] = f(obj[key]); + newObj[key] = f(obj[key], key, obj); } return newObj; }