1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-17 20:11:46 +02:00

Add support for interactive selection of zoom, combined with focus, in InputfieldImage. To enable zoom, go to your image field settings, and on the Input tab locate the "Focus point selection" field and choose "Focus point and zoom". Bump version to 3.0.92.

This commit is contained in:
Ryan Cramer
2018-02-16 11:28:25 -05:00
parent 5c6e54e24d
commit 8fe1eb13f4
5 changed files with 180 additions and 94 deletions

View File

@@ -579,6 +579,7 @@ class Pageimage extends Pagefile {
'nameWidth' => null, // override width to use for filename, int when populated 'nameWidth' => null, // override width to use for filename, int when populated
'nameHeight' => null, // override height to use for filename, int when populated 'nameHeight' => null, // override height to use for filename, int when populated
'focus' => true, // allow single dimension resizes to use focus area? 'focus' => true, // allow single dimension resizes to use focus area?
'zoom' => null, // zoom override, used only if focus is applicable, int when populated
); );
$this->error = ''; $this->error = '';
@@ -593,7 +594,8 @@ class Pageimage extends Pagefile {
if($options['cropping'] === true && empty($options['cropExtra']) && $options['focus'] && $this->hasFocus && $width && $height) { if($options['cropping'] === true && empty($options['cropExtra']) && $options['focus'] && $this->hasFocus && $width && $height) {
// crop to focus area // crop to focus area
$focus = $this->focus(); $focus = $this->focus();
if(is_int($options['zoom'])) $focus['zoom'] = $options['zoom']; // override
$options['cropping'] = array("$focus[left]%", "$focus[top]%", "$focus[zoom]"); $options['cropping'] = array("$focus[left]%", "$focus[top]%", "$focus[zoom]");
$crop = ''; // do not add suffix $crop = ''; // do not add suffix

View File

@@ -45,7 +45,7 @@ class ProcessWire extends Wire {
* Reversion revision number * Reversion revision number
* *
*/ */
const versionRevision = 91; const versionRevision = 92;
/** /**
* Version suffix string (when applicable) * Version suffix string (when applicable)

View File

@@ -30,9 +30,6 @@ function InputfieldImage($) {
// grid items to retry for sizing by setGridSize() methods // grid items to retry for sizing by setGridSize() methods
var retryGridItems = []; var retryGridItems = [];
// whether or not zoom-focus feature is available
var useZoomFocus = false;
/** /**
* Whether or not AJAX drag/drop upload is allowed? * Whether or not AJAX drag/drop upload is allowed?
* *
@@ -370,14 +367,15 @@ function InputfieldImage($) {
function startFocus($edit) { function startFocus($edit) {
var $img, $el, $thumb, $input, $focusArea, $focusCircle, $inputfield, var $img, $el, $thumb, $input, $focusArea, $focusCircle, $inputfield,
focusData = null, gridSize, mode, focusData = null, gridSize, mode, $zoomSlider, $zoomBox, lastZoomPercent = 0,
$zoomSlider, $zoomBox, lastZoomPercent = 0, useZoomFocus = false;
lastZoomVisual = 0, startZoomVisual = -1;
$inputfield = $edit.closest('.Inputfield'); $inputfield = $edit.closest('.Inputfield');
gridSize = getCookieData($inputfield, 'size'); gridSize = getCookieData($inputfield, 'size');
mode = getCookieData($inputfield, 'mode'); mode = getCookieData($inputfield, 'mode');
if($inputfield.hasClass('InputfieldImageFocusZoom')) useZoomFocus = true;
if($edit.hasClass('gridImage')) { if($edit.hasClass('gridImage')) {
// list mode // list mode
$el = $edit; $el = $edit;
@@ -443,8 +441,8 @@ function InputfieldImage($) {
var $overlay = $focusCircle.parent(); var $overlay = $focusCircle.parent();
var w = $overlay.width(); var w = $overlay.width();
var h = $overlay.height(); var h = $overlay.height();
var x = Math.round(((focus.left / 100) * w) - ($focusCircle.width() / 1.7)); var x = Math.round(((focus.left / 100) * w) - ($focusCircle.width() / 2)); // 1.7
var y = Math.round(((focus.top / 100) * h) - ($focusCircle.height() / 2.3)); var y = Math.round(((focus.top / 100) * h) - ($focusCircle.height() / 2)); // 2.3
if(x < 0) x = 0; if(x < 0) x = 0;
if(y < 0) y = 0; if(y < 0) y = 0;
$focusCircle.css({ $focusCircle.css({
@@ -477,63 +475,52 @@ function InputfieldImage($) {
// set the initial position for the focus circle // set the initial position for the focus circle
setFocusDragPosition(); setFocusDragPosition();
// function called whenever the slider is moved // function called whenever the slider is moved or circle is dragged with zoom active
var zoomSlide = function(zoomPercent) { var zoomSlide = function(zoomPercent) {
var zoomBoxSize, focusCircleSize, focus, top, left, scale, faWidth, faHeight;
// if no zoomPercent argument provided, use the last one
if(typeof zoomPercent == "undefined") zoomPercent = lastZoomPercent; if(typeof zoomPercent == "undefined") zoomPercent = lastZoomPercent;
lastZoomPercent = zoomPercent; lastZoomPercent = zoomPercent;
var w = (100 - zoomPercent) + '%'; faWidth = $focusArea.width();
$zoomBox.width(w); faHeight = $focusArea.height();
var zoomBoxSize = $zoomBox.width();
var focusCircleSize = $focusCircle.height(); if(faWidth > faHeight) {
$zoomBox.height(zoomBoxSize) $zoomBox.height((100 - zoomPercent) + '%'); // set width in percent
zoomBoxSize = $zoomBox.height(); // get width in pixels
$zoomBox.width(zoomBoxSize); // match width to ensure square zoom box
} else {
$zoomBox.width((100 - zoomPercent) + '%'); // set width in percent
zoomBoxSize = $zoomBox.width(); // get width in pixels
$zoomBox.height(zoomBoxSize); // match width to ensure square zoom box
}
var zoom = zoomPercent; // apply the zoom box position
var top = parseInt($focusCircle.css('top')); // top of drag item focus = getFocus();
top += Math.floor(focusCircleSize / 2); // plus half the height of drag item var crop = getFocusZoomCropDimensions(focus.left, focus.top, zoomPercent, faWidth, faHeight, zoomBoxSize, zoomBoxSize);
top -= Math.ceil(zoomBoxSize / 2) - 3; // minus half the height of the zoom box (-3) $zoomBox.css({
top: crop.top + 'px',
var left = parseInt($focusCircle.css('left')); left: crop.left + 'px',
left += Math.floor(focusCircleSize / 2);
left -= Math.ceil(zoomBoxSize / 2) - 3;
if(top < 0) top = 0;
if(left < 0) left = 0;
// constrain to corners
if(top + zoomBoxSize > $focusArea.height()) top = $focusArea.height() - zoomBoxSize;
if(left + zoomBoxSize > $focusArea.width()) left = $focusArea.width() - zoomBoxSize;
$zoomBox.css({
top: top + 'px',
left: left + 'px'
}); });
setFocusProperty('zoom', zoomPercent); // save zoom percent
focus.zoom = crop.zoom; // crop.zoom may have been adjusted to prevent upscaling
// determine when to visually start showing zoom (in grid mode) setFocusProperty('zoom', crop.zoom);
var zoomVisual = zoomPercent;
if(zoomBoxSize > $img.height() || zoomBoxSize > $img.width()) { // update the preview if in gride mode
zoomVisual = 0; if(mode == 'grid') setGridSizeItem($thumb.parent(), gridSize, false, focus);
} else {
if(!lastZoomVisual) startZoomVisual = zoomVisual;
zoomVisual = (zoomVisual - startZoomVisual)+1;
}
if(mode == 'grid') setGridSizeItem($thumb.parent(), gridSize, false, zoomVisual);
lastZoomVisual = zoomVisual;
/*
console.log('lastZoomVisual=' + lastZoomVisual + ', startZoomVisual=' + startZoomVisual +
', img.height=' + $img.height() + ', img.width=' + $img.width() +
', zoomBoxSize=' + zoomBoxSize + ', zoomVisual=' + zoomVisual);
*/
}; // zoomSlide }; // zoomSlide
// function called when the focus item is dragged // function called when the focus item is dragged
var dragEvent = function(event, ui) { var dragEvent = function(event, ui) {
var $this = $(this); var $this = $(this);
var circleSize = $this.outerHeight();
var w = $this.parent().width(); var w = $this.parent().width();
var h = $this.parent().height(); var h = $this.parent().height();
var t = ui.position.top > 0 ? ui.position.top + ($this.width() / 2) : 0; var t = ui.position.top > 0 ? ui.position.top + (circleSize / 2) : 0;
var l = ui.position.left > 0 ? ui.position.left + ($this.height() / 2) : 0; var l = ui.position.left > 0 ? ui.position.left + (circleSize / 2) : 0;
var oldFocus = getFocus();
var newFocus = { var newFocus = {
'top': t > 0 ? ((t / h) * 100) : 0, 'top': t > 0 ? ((t / h) * 100) : 0,
'left': l > 0 ? ((l / w) * 100) : 0, 'left': l > 0 ? ((l / w) * 100) : 0,
@@ -541,9 +528,9 @@ function InputfieldImage($) {
}; };
setFocus(newFocus); setFocus(newFocus);
if(useZoomFocus) { if(useZoomFocus) {
zoomSlide(); zoomSlide(newFocus.zoom);
} else if(mode == 'grid') { } else if(mode == 'grid') {
setGridSizeItem($thumb.parent(), gridSize, false); setGridSizeItem($thumb.parent(), gridSize, false, newFocus);
} }
}; // dragEvent }; // dragEvent
@@ -562,15 +549,15 @@ function InputfieldImage($) {
}); });
$zoomBox = $("<div />").addClass('focusZoomBox').css({ $zoomBox = $("<div />").addClass('focusZoomBox').css({
'position': 'absolute', 'position': 'absolute',
'background': 'rgba(0,0,0,0.5)', 'background': 'rgba(0,0,0,0.5)'
'box-shadow': '0 0 20px rgba(0,0,0,.9)' //'box-shadow': '0 0 20px rgba(0,0,0,.9)'
}); });
$focusArea.prepend($zoomBox); $focusArea.prepend($zoomBox);
$img.after($zoomSlider); $img.after($zoomSlider);
$thumb.attr('src', $img.attr('src')); $thumb.attr('src', $img.attr('src'));
$zoomSlider.slider({ $zoomSlider.slider({
min: 0, min: 0,
max: 80, max: 50,
value: zoom, value: zoom,
range: 'max', range: 'max',
slide: function(event, ui) { slide: function(event, ui) {
@@ -601,6 +588,92 @@ function InputfieldImage($) {
} }
} }
} }
/**
* Get focus zoom position for either X or Y (duplicated from Horsts PHP version in ImageSizerEngine)
*
* @param focus Left or Top percentage
* @param sourceDimension Width or Height of source image
* @param cropDimension Width or Height of cropped image
* @returns {number}
*
*/
function getFocusZoomPosition(focus, sourceDimension, cropDimension) {
focus = parseInt(focus); // string with float value and percent char, (needs to be converted to integer)
var source = 100; // the source-dimensions percent-value (100)
var target = (cropDimension / sourceDimension * 100); // the crop-dimensions percent-value
var rest = source - target; // the unused dimension-part-value in percent
var tmp = focus + (target / 2); // temp value
var position = 0;
// calculate the position in pixel !
if(tmp >= 100) {
position = sourceDimension - cropDimension;
} else if(tmp <= Math.floor(rest / 2)) {
position = 0;
} else {
position = Math.ceil((sourceDimension - cropDimension) / 100 * focus);
}
return position;
}
/**
* Get focus zoom crop dimensions (duplicated from Horsts PHP version in ImageSizerEngine)
*
* @param left Left percent
* @param top Top percent
* @param zoom Zoom percent
* @param fullWidth Width of full size image
* @param fullHeight Height of full size image
* @param finalWidth Width of target cropped image
* @param finalHeight Height of target cropped image
* @returns {{left: number, top: number, width: number, height: number}}
*
*/
function getFocusZoomCropDimensions(left, top, zoom, fullWidth, fullHeight, finalWidth, finalHeight) {
// calculate the max crop dimensions
var ratioFinal = finalWidth / finalHeight; // get the ratio of the requested crop
var percentW = finalWidth / fullWidth * 100; // calculate percentage of the crop width in regard of the original width
var percentH = finalHeight / fullHeight * 100; // calculate percentage of the crop height in regard of the original height
if(percentW >= percentH) { // check wich one is greater
var maxW = fullWidth; // if percentW is greater, maxW becomes the original Width
var maxH = fullWidth / ratioFinal; // ... and maxH gets calculated via the ratio
} else {
var maxH = fullHeight; // if percentH is greater, maxH becomes the original Height
var maxW = fullHeight * ratioFinal; // ... and maxW gets calculated via the ratio
}
// calculate the zoomed dimensions
var cropW = maxW - (maxW * zoom / 100); // to get the final crop Width and Height, the amount for zoom-in
var cropH = maxH - (maxH * zoom / 100); // needs to get stripped out
// validate against the minimal dimensions
var upscaling = true;
if(!upscaling) { // if upscaling isn't allowed, we decrease the zoom, so that we get a crop with the min-Dimensions
if(cropW < finalWidth) {
cropW = finalWidth;
cropH = finalWidth / ratioFinal;
}
if(cropH < finalHeight) {
cropH = finalHeight;
cropW = finalHeight * ratioFinal;
}
}
// calculate the crop positions
var tmpX = getFocusZoomPosition(left, fullWidth, cropW); // calculate the x-position
var tmpY = getFocusZoomPosition(top, fullHeight, cropH); // calculate the y-position
return {
'left': tmpX,
'top': tmpY,
'width': cropW,
'height': cropH,
'zoom': zoom // adjusted zoom
};
}
/** /**
* Tear down the InputfieldImageEdit panel * Tear down the InputfieldImageEdit panel
@@ -989,10 +1062,10 @@ function InputfieldImage($) {
* @param $item * @param $item
* @param gridSize * @param gridSize
* @param ragged * @param ragged
* @param zoom * @param focus optional
* *
*/ */
function setGridSizeItem($item, gridSize, ragged, zoom) { function setGridSizeItem($item, gridSize, ragged, focus) {
if($item.hasClass('gridImage__overflow')) { if($item.hasClass('gridImage__overflow')) {
var $img = $item.children('img'); var $img = $item.children('img');
@@ -1009,9 +1082,7 @@ function InputfieldImage($) {
return; return;
} }
if(typeof zoom == "undefined") zoom = 0; var zoom = 0;
var focus = {};
var w = $img.width(); var w = $img.width();
var h = $img.height(); var h = $img.height();
var dataW = parseInt($img.attr('data-w')); var dataW = parseInt($img.attr('data-w'));
@@ -1019,7 +1090,7 @@ function InputfieldImage($) {
if(!w) w = dataW; if(!w) w = dataW;
if(!h) h = dataH; if(!h) h = dataH;
if(!ragged) { if(!ragged && typeof focus == "undefined") {
var focusStr = $img.attr('data-focus'); var focusStr = $img.attr('data-focus');
if(typeof focusStr == "undefined") focusStr = '50.0 50.0 0'; if(typeof focusStr == "undefined") focusStr = '50.0 50.0 0';
var focusArray = focusStr.split(' '); var focusArray = focusStr.split(' ');
@@ -1029,6 +1100,7 @@ function InputfieldImage($) {
zoom: parseInt(focusArray[2]) zoom: parseInt(focusArray[2])
}; };
} }
if(!ragged) zoom = focus.zoom;
if(ragged) { if(ragged) {
// show full thumbnail (not square) // show full thumbnail (not square)
@@ -1041,44 +1113,33 @@ function InputfieldImage($) {
'transform': 'translate3d(-50%, -50%, 0)' 'transform': 'translate3d(-50%, -50%, 0)'
}); });
} else if(zoom > 0 && useZoomFocus) { } else if(zoom > 0 && $item.closest('.InputfieldImageFocusZoom').length) {
// focus with zoom // focus with zoom
if(w >= h) { if(w >= h) {
$img.attr('height', gridSize).removeAttr('width');
var maxHeight = '100%'; var maxHeight = '100%';
var maxWidth = 'none'; var maxWidth = 'none';
} else { } else {
var maxHeight = 'none'; var maxHeight = 'none';
var maxWidth = '100%'; var maxWidth = '100%';
$img.attr('width', gridSize).removeAttr('height');
} }
var scale = 1 + ((zoom / 100) * 2);
var top = focus.top; var top = focus.top;
var left = focus.left; var left = focus.left;
var scale = 1 + (zoom / 25); //(zoom * 0.037); if(top > 92 || top > 100) top = 100;
if(scale < 0) scale = 0; if(left > 92 || left > 100) left = 100;
if(left < 1.0) left = 0.001; if(top < 0) top = 0;
if(top < 1.0) top = 0.001; if(left < 0) left = 0;
if(left >= 55) { var crop = getFocusZoomCropDimensions(left, top, zoom, w, h, gridSize, gridSize);
left += (left * 0.15);
} else if(left <= 45) {
left -= (left * 0.15);
}
if(top > 50) {
top += (top * 0.1);
} else if(top < 50) {
top -= (top * 0.1);
}
if(left > 100) left = 100;
if(top > 100) top = 100;
$img.css({ $img.css({
'max-height': maxHeight,
'max-width': maxWidth,
'top': top + '%', 'top': top + '%',
'left': left + '%', 'left': left + '%',
'transform-origin': 'top left', 'transform-origin': 'top left',
'transform': 'scale(' + scale + ') translate3d(-' + (left) + '%, -' + (top) + '%, 0)' 'transform': 'scale(' + scale + ') translate3d(-' + left + '%, -' + top + '%, 0)',
'max-width': maxWidth,
'max-height': maxHeight
}); });
// console.log("top=" + top + ", left=" + left + ", scale=" + scale); // console.log("top=" + top + "%, left=" + left + "%, zoom=" + zoom + ", scale=" + scale);
} else if(w >= h) { } else if(w >= h) {
// image width greater than height // image width greater than height
@@ -1171,6 +1232,7 @@ function InputfieldImage($) {
$aPrev.removeClass(activeClass); $aPrev.removeClass(activeClass);
$a.addClass(activeClass); $a.addClass(activeClass);
stopFocus($inputfield);
if(href == 'list') { if(href == 'list') {
if(!$inputfield.hasClass('InputfieldImageEditAll')) { if(!$inputfield.hasClass('InputfieldImageEditAll')) {

File diff suppressed because one or more lines are too long

View File

@@ -108,6 +108,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
if($gridSize >= (self::defaultGridSize * 2)) $gridSize = self::defaultGridSize; // establish max of 259 if($gridSize >= (self::defaultGridSize * 2)) $gridSize = self::defaultGridSize; // establish max of 259
$this->set('gridSize', $gridSize); $this->set('gridSize', $gridSize);
$this->set('gridMode', 'grid'); // one of "grid", "left" or "list" $this->set('gridMode', 'grid'); // one of "grid", "left" or "list"
$this->set('focusMode', 'on'); // One of "on", "zoom" or "off"
// adminThumbScale is no longer in use (here in case descending module using it) // adminThumbScale is no longer in use (here in case descending module using it)
$this->set('adminThumbScale', empty($options['scale']) ? 1.0 : (float) $options['scale']); $this->set('adminThumbScale', empty($options['scale']) ? 1.0 : (float) $options['scale']);
@@ -175,6 +176,10 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
$jqueryCore->use('cookie'); $jqueryCore->use('cookie');
$modules->loadModuleFileAssets('InputfieldFile'); $modules->loadModuleFileAssets('InputfieldFile');
$modules->getInstall("JqueryMagnific"); $modules->getInstall("JqueryMagnific");
if(!$renderValueMode && $this->focusMode == 'zoom') {
$this->addClass('InputfieldImageFocusZoom', 'wrapClass');
}
$config->js('InputfieldImage', array( $config->js('InputfieldImage', array(
'labels' => $this->labels, 'labels' => $this->labels,
@@ -776,10 +781,12 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
$out .= "<button type='button' data-href='$editUrl' class='InputfieldImageButtonCrop $modalButtonClass' $modalAttrs>$buttonText</button>"; $out .= "<button type='button' data-href='$editUrl' class='InputfieldImageButtonCrop $modalButtonClass' $modalAttrs>$buttonText</button>";
// Focus // Focus
$iconA = $pagefile->hasFocus ? 'fa-check-circle-o' : 'fa-circle-o'; if($this->focusMode && $this->focusMode != 'off') {
$iconB = $pagefile->hasFocus ? 'fa-check-circle' : 'fa-dot-circle-o'; $iconA = $pagefile->hasFocus ? 'fa-check-circle-o' : 'fa-circle-o';
$buttonText = str_replace('{out}', "<i class='fa $iconA' data-toggle='$iconA $iconB'></i> $labels[focus]", $this->themeSettings['buttonText']); $iconB = $pagefile->hasFocus ? 'fa-check-circle' : 'fa-dot-circle-o';
$out .= "<button type='button' class='InputfieldImageButtonFocus $buttonClass'>$buttonText</button>"; $buttonText = str_replace('{out}', "<i class='fa $iconA' data-toggle='$iconA $iconB'></i> $labels[focus]", $this->themeSettings['buttonText']);
$out .= "<button type='button' class='InputfieldImageButtonFocus $buttonClass'>$buttonText</button>";
}
// Variations // Variations
$buttonText = "<i class='fa fa-files-o'></i> $labels[variations] <span class='ui-priority-secondary'>($variationCount)</span>"; $buttonText = "<i class='fa fa-files-o'></i> $labels[variations] <span class='ui-priority-secondary'>($variationCount)</span>";
@@ -969,6 +976,21 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu
$f->addOption('left', '[i.fa.fa-tasks][/i] ' . $this->_('Proportional grid images')); $f->addOption('left', '[i.fa.fa-tasks][/i] ' . $this->_('Proportional grid images'));
$f->addOption('list', '[i.fa.fa-th-list][/i] ' . $this->_('Vertical list (verbose)')); $f->addOption('list', '[i.fa.fa-th-list][/i] ' . $this->_('Vertical list (verbose)'));
$f->attr('value', $this->gridMode); $f->attr('value', $this->gridMode);
$f->columnWidth = 50;
$inputfields->add($f);
/** @var InputfieldRadios $f */
$f = $this->wire('modules')->get('InputfieldRadios');
$f->attr('name', 'focusMode');
$f->label = $this->_('Focus point selection');
$f->description = $this->_('Enables a draggable focus point to select the subject of an image. This helps to generate non-proportional crops.') . ' ' .
$this->_('A preview of the focus point is also shown when images are in the “Square grid images” mode.');
$f->addOption('on', $this->_('Focus point'));
$f->addOption('zoom', $this->_('Focus point and zoom'));
$f->addOption('off', $this->_('Disabled'));
$f->attr('value', $this->focusMode);
$f->icon = 'crosshairs';
$f->columnWidth = 50;
$inputfields->add($f); $inputfields->add($f);
/** @var InputfieldFieldset $fieldset */ /** @var InputfieldFieldset $fieldset */