diff --git a/controllers/EditAsset.php b/controllers/EditAsset.php index f3ee61b6..8fc7bf4e 100644 --- a/controllers/EditAsset.php +++ b/controllers/EditAsset.php @@ -116,18 +116,25 @@ class EditAsset extends HTMLController if (!preg_match('~^(?\d+)x(?\d+)(?_c(?[best]?))?$~', $selector, $thumb)) continue; - $has_crop_boundary = isset($metadata['crop_' . $thumb['width'] . 'x' . $thumb['height']]); - $has_custom_image = isset($metadata['custom_' . $thumb['width'] . 'x' . $thumb['height']]); + $dimensions = $thumb['width'] . 'x' . $thumb['height']; + + // Does the thumbnail exist on disk? If not, use an url to generate it. + if (!$filename || !file_exists(THUMBSDIR . '/' . $subdir . '/' . $filename)) + $thumb_url = BASEURL . '/thumbnail/' . $image->getId() . '/' . $dimensions . ($thumb['suffix'] ?? '') . '/'; + else + $thumb_url = THUMBSURL . '/' . $subdir . '/' . $filename; + + $has_crop_boundary = isset($metadata['crop_' . $dimensions]); + $has_custom_image = isset($metadata['custom_' . $dimensions]); + $thumbs[] = [ 'dimensions' => [(int) $thumb['width'], (int) $thumb['height']], 'cropped' => !$has_custom_image && (!empty($thumb['suffix']) || $has_crop_boundary), 'crop_method' => !$has_custom_image && !empty($thumb['method']) ? $thumb['method'] : (!empty($thumb['suffix']) ? 'c' : null), - 'crop_region' => $has_crop_boundary ? $metadata['crop_' . $thumb['width'] . 'x' . $thumb['height']] : null, + 'crop_region' => $has_crop_boundary ? $metadata['crop_' . $dimensions] : null, 'custom_image' => $has_custom_image, 'filename' => $filename, - 'full_path' => THUMBSDIR . '/' . $subdir . '/' . $filename, - 'url' => THUMBSURL . '/' . $subdir . '/' . $filename, - 'status' => file_exists(THUMBSDIR . '/' . $subdir . '/' . $filename), + 'url' => $thumb_url, ]; } @@ -144,18 +151,19 @@ class EditAsset extends HTMLController $crop_value = $data->crop_width . ',' . $data->crop_height . ',' . $data->source_x . ',' . $data->source_y; $meta[$crop_key] = $crop_value; - // If we uploaded a custom thumbnail, stop considering it such. + // If we previously uploaded a custom thumbnail, stop considering it such. $custom_key = 'custom_' . $data->thumb_width . 'x' . $data->thumb_height; if (isset($meta[$custom_key])) + { + // TODO: delete from disk unset($meta[$custom_key]); + } + + // Save meta changes so far. + $image->setMetaData($meta); // Force a rebuild of related thumbnails. - $thumb_key = 'thumb_' . $data->thumb_width . 'x' . $data->thumb_height; - foreach ($meta as $meta_key => $meta_value) - if ($meta_key === $thumb_key || strpos($meta_key, $thumb_key . '_') !== false) - unset($meta[$meta_key]); - - $image->setMetaData($meta); + $image->removeThumbnailsOfSize($data->thumb_width, $data->thumb_height); $payload = [ 'key' => $crop_key, diff --git a/models/Image.php b/models/Image.php index dae64229..c8205f83 100644 --- a/models/Image.php +++ b/models/Image.php @@ -133,9 +133,9 @@ class Image extends Asset public function removeAllThumbnails() { - foreach ($this->thumbnails as $key => $value) + foreach ($this->thumbnails as $key => $filename) { - $thumb_path = THUMBSDIR . '/' . $this->subdir . '/' . $value; + $thumb_path = THUMBSDIR . '/' . $this->subdir . '/' . $filename; if (is_file($thumb_path)) unlink($thumb_path); } @@ -146,6 +146,30 @@ class Image extends Asset ['id_asset' => $this->id_asset]); } + public function removeThumbnailsOfSize($width, $height) + { + foreach ($this->thumbnails as $key => $filename) + { + if (strpos($key, $width . 'x' . $height) !== 0) + continue; + + $thumb_path = THUMBSDIR . '/' . $this->subdir . '/' . $filename; + if (is_file($thumb_path)) + unlink($thumb_path); + } + + return Registry::get('db')->query(' + DELETE FROM assets_thumbs + WHERE id_asset = {int:id_asset} AND + width = {int:width} AND + height = {int:height}', + [ + 'height' => $height, + 'id_asset' => $this->id_asset, + 'width' => $width, + ]); + } + public function replaceThumbnail($descriptor, $tmp_file) { if (!is_file($tmp_file)) diff --git a/models/Thumbnail.php b/models/Thumbnail.php index 70da00bb..d0b982f4 100644 --- a/models/Thumbnail.php +++ b/models/Thumbnail.php @@ -164,6 +164,8 @@ class Thumbnail $this->filename_suffix .= 's'; elseif ($this->crop_mode === self::CROP_MODE_SLICE_BOTTOM) $this->filename_suffix .= 'b'; + elseif ($this->crop_mode === self::CROP_MODE_BOUNDARY) + $this->filename_suffix .= 'e'; } else $this->filename_suffix = ''; diff --git a/public/css/admin.css b/public/css/admin.css index 4e58d3ea..f3f3b902 100644 --- a/public/css/admin.css +++ b/public/css/admin.css @@ -145,46 +145,55 @@ body { /* Crop editor ----------------*/ #crop_editor { + display: flex; + flex-direction: column; position: fixed; top: 0; left: 0; height: 100%; width: 100%; - background: #000; + background: rgba(0, 0, 0, 0.8); z-index: 100; color: #fff; } -#crop_editor input { +#crop_editor input[type=number] { width: 50px; background: #555; color: #fff; } -.crop_image_container { - position: relative; +#crop_editor input[type=checkbox] { + vertical-align: middle; } .crop_position { + background: rgba(0, 0, 0, 1.0); + border: none; padding: 5px; text-align: center; } .crop_position input, .crop_position .btn { margin: 0 5px; } + +.crop_image_container { + position: relative; + flex-grow: 1; + max-height: calc(100% - 34px); +} .crop_image_container img { - height: auto; - width: auto; + border: 1px solid #000; + max-height: 100%; max-width: 100%; - max-height: 700px; } #crop_boundary { - border: 1px solid rgba(255, 255, 255, 0.75); - background: rgba(255, 255, 255, 0.75); + border: 1px dashed rgb(255, 255, 255); + background: rgba(255, 255, 255, 0.4); + cursor: move; position: absolute; z-index: 200; width: 500px; height: 300px; top: 400px; left: 300px; - filter: invert(100%); /* temp */ } diff --git a/public/css/default.css b/public/css/default.css index ce28dada..4e0f5dc4 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -375,34 +375,59 @@ footer a { input, select, .btn { background: #fff; - border: 1px solid #ccc; + border: 1px solid #dbdbdb; + border-radius: 4px; color: #000; font: 13px/1.7 "Open Sans", "Helvetica", sans-serif; padding: 3px; } -input[type=submit], button, .btn { - background: #ddd; - border-radius: 3px; - border: 1px solid #aaa; - display: inline-block; - font: inherit; - padding: 4px 5px; -} -input[type=submit]:hover, button:hover, .btn:hover { - background-color: #d0d0d0; - border-color: #a0a0a0; -} textarea { - border: 1px solid #ccc; - font: 12px/1.4 'Monaco', 'Inconsolata', 'DejaVu Sans Mono', monospace; + border: 1px solid #dbdbdb; + border-radius: 4px; + font: 14px/1.4 'Inconsolata', 'DejaVu Sans Mono', monospace; padding: 0.75%; - width: 98.5%; } + +input[type=submit], button, .btn { + background-color: #eee; + border-color: #dbdbdb; + border-width: 1px; + border-radius: 4px; + color: #363636; + cursor: pointer; + display: inline-block; + justify-content: center; + padding-bottom: calc(0.4em - 1px); + padding-left: 0.8em; + padding-right: 0.8em; + padding-top: calc(0.4em - 1px); + text-align: center; + white-space: nowrap; +} +input:hover, select:hover, button:hover, .btn:hover { + border-color: #b5b5b5; +} +input:focus, select:focus, button:focus, .btn:focus { + border-color: #3273dc; +} +input:focus:not(:active), select:focus:not(:active), button:focus:not(:active), .btn:focus:not(:active) { + box-shadow: 0px 0px 0px 2px rgba(50, 115, 220, 0.25); +} +input:active, select:active, button:active, .btn:active { + border-color: #4a4a4a; +} + .btn-red { - background: #F3B076; - border-color: #C98245; + background: #eebbaa; + border-color: #cc9988; +} +.btn-red:hover, .btn-red:focus { + border-color: #bb7766; color: #000; } +.btn-red:focus:not(:active) { + box-shadow: 0px 0px 0px 2px rgba(241, 70, 104, 0.25); +} /* Login box styles diff --git a/public/js/crop_editor.js b/public/js/crop_editor.js index e010d035..555ec832 100644 --- a/public/js/crop_editor.js +++ b/public/js/crop_editor.js @@ -1,218 +1,374 @@ -function CropEditor(opt) { - this.opt = opt; +class CropEditor { + constructor(opt) { + this.opt = opt; - this.edit_crop_button = document.createElement("span"); - this.edit_crop_button.className = "btn"; - this.edit_crop_button.innerHTML = "Edit crop"; - this.edit_crop_button.addEventListener('click', this.show.bind(this)); + this.edit_crop_button = document.createElement("span"); + this.edit_crop_button.className = "btn"; + this.edit_crop_button.textContent = "Edit crop"; + this.edit_crop_button.addEventListener('click', this.show.bind(this)); - this.thumbnail_select = document.getElementById(opt.thumbnail_select_id); - this.thumbnail_select.addEventListener('change', this.toggleCropButton.bind(this)); - this.thumbnail_select.parentNode.insertBefore(this.edit_crop_button, this.thumbnail_select.nextSibling); + this.thumbnail_select = document.getElementById(opt.thumbnail_select_id); + this.thumbnail_select.addEventListener('change', this.toggleCropButton.bind(this)); + this.thumbnail_select.parentNode.insertBefore(this.edit_crop_button, this.thumbnail_select.nextSibling); - this.toggleCropButton(); -} + this.toggleCropButton(); + } -CropEditor.prototype.buildContainer = function() { - this.container = document.createElement("div"); - this.container.id = "crop_editor"; + initDOM() { + this.container = document.createElement("div"); + this.container.id = "crop_editor"; - this.position = document.createElement("div"); - this.position.className = "crop_position"; - this.container.appendChild(this.position); + this.initPositionForm(); + this.initImageContainer(); - var source_x_label = document.createTextNode("Source X:"); - this.position.appendChild(source_x_label); + this.parent = document.getElementById(this.opt.editor_container_parent_id); + this.parent.appendChild(this.container); + } - this.source_x = document.createElement("input"); - this.source_x.addEventListener("keyup", this.positionBoundary.bind(this)); - this.position.appendChild(this.source_x); + initPositionForm() { + this.position = document.createElement("fieldset"); + this.position.className = "crop_position"; + this.container.appendChild(this.position); - var source_y_label = document.createTextNode("Source Y:"); - this.position.appendChild(source_y_label); + let source_x_label = document.createTextNode("Source X:"); + this.position.appendChild(source_x_label); - this.source_y = document.createElement("input"); - this.source_y.addEventListener("keyup", this.positionBoundary.bind(this)); - this.position.appendChild(this.source_y); + this.source_x = document.createElement("input"); + this.source_x.type = 'number'; + this.source_x.addEventListener("change", this.positionBoundary.bind(this)); + this.source_x.addEventListener("keyup", this.positionBoundary.bind(this)); + this.position.appendChild(this.source_x); - var crop_width_label = document.createTextNode("Crop width:"); - this.position.appendChild(crop_width_label); + let source_y_label = document.createTextNode("Source Y:"); + this.position.appendChild(source_y_label); - this.crop_width = document.createElement("input"); - this.crop_width.addEventListener("keyup", this.positionBoundary.bind(this)); - this.position.appendChild(this.crop_width); + this.source_y = document.createElement("input"); + this.source_y.type = 'number'; + this.source_y.addEventListener("change", this.positionBoundary.bind(this)); + this.source_y.addEventListener("keyup", this.positionBoundary.bind(this)); + this.position.appendChild(this.source_y); - var crop_height_label = document.createTextNode("Crop height:"); - this.position.appendChild(crop_height_label); + let crop_width_label = document.createTextNode("Crop width:"); + this.position.appendChild(crop_width_label); - this.crop_height = document.createElement("input"); - this.crop_height.addEventListener("keyup", this.positionBoundary.bind(this)); - this.position.appendChild(this.crop_height); + this.crop_width = document.createElement("input"); + this.crop_width.type = 'number'; + this.crop_width.addEventListener("change", this.positionBoundary.bind(this)); + this.crop_width.addEventListener("keyup", this.positionBoundary.bind(this)); + this.position.appendChild(this.crop_width); - this.save_button = document.createElement("span"); - this.save_button.className = "btn"; - this.save_button.innerHTML = "Save"; - this.save_button.addEventListener('click', this.save.bind(this)); - this.position.appendChild(this.save_button); + let crop_height_label = document.createTextNode("Crop height:"); + this.position.appendChild(crop_height_label); - this.abort_button = document.createElement("span"); - this.abort_button.className = "btn btn-red"; - this.abort_button.innerHTML = "Abort"; - this.abort_button.addEventListener('click', this.hide.bind(this)); - this.position.appendChild(this.abort_button); + this.crop_height = document.createElement("input"); + this.crop_height.type = 'number'; + this.crop_height.addEventListener("change", this.positionBoundary.bind(this)); + this.crop_height.addEventListener("keyup", this.positionBoundary.bind(this)); + this.position.appendChild(this.crop_height); - this.image_container = document.createElement("div"); - this.image_container.className = "crop_image_container"; - this.container.appendChild(this.image_container); + this.crop_constrain_label = document.createElement("label"); + this.position.appendChild(this.crop_constrain_label); - this.crop_boundary = document.createElement("div"); - this.crop_boundary.id = "crop_boundary"; - this.image_container.appendChild(this.crop_boundary); + this.crop_constrain = document.createElement("input"); + this.crop_constrain.checked = true; + this.crop_constrain.type = 'checkbox'; + this.crop_constrain_label.appendChild(this.crop_constrain); - this.original_image = document.createElement("img"); - this.original_image.id = "original_image"; - this.original_image.src = this.opt.original_image_src; - this.image_container.appendChild(this.original_image); + this.crop_constrain_text = document.createTextNode('Constrain proportions'); + this.crop_constrain_label.appendChild(this.crop_constrain_text); - this.parent = document.getElementById(this.opt.editor_container_parent_id); - this.parent.appendChild(this.container); -}; + this.save_button = document.createElement("span"); + this.save_button.className = "btn"; + this.save_button.textContent = "Save"; + this.save_button.addEventListener('click', this.save.bind(this)); + this.position.appendChild(this.save_button); -CropEditor.prototype.setInputValues = function() { - var current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset; + this.abort_button = document.createElement("span"); + this.abort_button.className = "btn btn-red"; + this.abort_button.textContent = "Abort"; + this.abort_button.addEventListener('click', this.hide.bind(this)); + this.position.appendChild(this.abort_button); + } - if (typeof current.crop_region === "undefined") { - var source_ratio = this.original_image.naturalWidth / this.original_image.naturalHeight, - crop_ratio = current.crop_width / current.crop_height, - min_dim = Math.min(this.original_image.naturalWidth, this.original_image.naturalHeight); + initImageContainer() { + this.image_container = document.createElement("div"); + this.image_container.className = "crop_image_container"; + this.container.appendChild(this.image_container); + + this.crop_boundary = document.createElement("div"); + this.crop_boundary.id = "crop_boundary"; + this.image_container.appendChild(this.crop_boundary); + + this.original_image = document.createElement("img"); + this.original_image.draggable = false; + this.original_image.id = "original_image"; + this.original_image.src = this.opt.original_image_src; + this.image_container.appendChild(this.original_image); + } + + setDefaultCrop(cropAspectRatio, cropMethod) { + let source = this.original_image; + let sourceAspectRatio = source.naturalWidth / source.naturalHeight; // Cropping from the centre? - if (current.crop_method === "c") { + if (cropMethod === "c" || cropMethod === "s") { // Crop vertically from the centre, using the entire width. - if (source_ratio < crop_ratio) { - this.crop_width.value = this.original_image.naturalWidth; - this.crop_height.value = Math.ceil(this.original_image.naturalWidth / crop_ratio); + if (sourceAspectRatio <= cropAspectRatio) { + this.crop_width.value = source.naturalWidth; + this.crop_height.value = Math.ceil(source.naturalWidth / cropAspectRatio); this.source_x.value = 0; - this.source_y.value = Math.ceil((this.original_image.naturalHeight - this.crop_height.value) / 2); + this.source_y.value = Math.ceil((source.naturalHeight - this.crop_height.value) / 2); } // Crop horizontally from the centre, using the entire height. else { - this.crop_width.value = Math.ceil(current.crop_width * this.original_image.naturalHeight / current.crop_height); - this.crop_height.value = this.original_image.naturalHeight; - this.source_x.value = Math.ceil((this.original_image.naturalWidth - this.crop_width.value) / 2); + this.crop_width.value = Math.ceil(cropAspectRatio * source.naturalHeight); + this.crop_height.value = source.naturalHeight; + this.source_x.value = Math.ceil((source.naturalWidth - this.crop_width.value) / 2); this.source_y.value = 0; } } // Cropping a top or bottom slice? else { // Can we actually take a top or bottom slice from the original image? - if (source_ratio < crop_ratio) { - this.crop_width.value = this.original_image.naturalWidth; - this.crop_height.value = Math.floor(this.original_image.naturalHeight / crop_ratio); + if (sourceAspectRatio <= cropAspectRatio) { + this.crop_width.value = source.naturalWidth; + this.crop_height.value = Math.floor(source.naturalWidth / cropAspectRatio); this.source_x.value = "0"; - this.source_y.value = current.crop_method.indexOf("t") !== -1 ? "0" : this.original_image.naturalHeight - this.crop_height.value; + this.source_y.value = cropMethod.indexOf("t") !== -1 ? "0" : source.naturalHeight - this.crop_height.value; } // Otherwise, take a vertical slice from the centre. else { - this.crop_width.value = Math.floor(this.original_image.naturalHeight * crop_ratio); - this.crop_height.value = this.original_image.naturalHeight; - this.source_x.value = Math.floor((this.original_image.naturalWidth - this.crop_width.value) / 2); + this.crop_width.value = Math.floor(source.naturalHeight * cropAspectRatio); + this.crop_height.value = source.naturalHeight; + this.source_x.value = Math.floor((source.naturalWidth - this.crop_width.value) / 2); this.source_y.value = "0"; } } - } else { - var region = current.crop_region.split(','); - this.crop_width.value = region[0]; - this.crop_height.value = region[1]; - this.source_x.value = region[2]; - this.source_y.value = region[3]; - } -}; - -CropEditor.prototype.showContainer = function() { - this.container.style.display = "block"; - this.setInputValues(); - this.positionBoundary(); -} - -CropEditor.prototype.save = function() { - var current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset; - var payload = { - thumb_width: current.crop_width, - thumb_height: current.crop_height, - crop_method: current.crop_method, - crop_width: this.crop_width.value, - crop_height: this.crop_height.value, - source_x: this.source_x.value, - source_y: this.source_y.value - }; - var req = HttpRequest("post", this.opt.submitUrl + "?id=" + this.opt.asset_id + "&updatethumb", - "data=" + encodeURIComponent(JSON.stringify(payload)), function(response) { - this.opt.after_save(response); - this.hide(); - }.bind(this)); -}; - -CropEditor.prototype.show = function() { - if (typeof this.container === "undefined") { - this.buildContainer(); } - // Defer showing and positioning until image is loaded. - // !!! TODO: add a spinner in the mean time? - if (this.original_image.naturalWidth > 0) { - this.showContainer(); - } else { - this.original_image.addEventListener("load", function() { + setPositionFormValues() { + let current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset; + + if (typeof current.crop_region === "undefined") { + let aspectRatio = current.crop_width / current.crop_height; + this.setDefaultCrop(aspectRatio, current.crop_method); + } else { + let region = current.crop_region.split(','); + this.crop_width.value = region[0]; + this.crop_height.value = region[1]; + this.source_x.value = region[2]; + this.source_y.value = region[3]; + } + + this.crop_width.min = 1; + this.crop_height.min = 1; + this.source_x.min = 0; + this.source_y.min = 0; + + let source = this.original_image; + this.crop_width.max = source.naturalWidth; + this.crop_height.max = source.naturalHeight; + this.source_x.max = source.naturalWidth - 1; + this.source_y.max = source.naturalHeight - 1; + + this.crop_constrain_text.textContent = `Constrain proportions (${current.crop_width} × ${current.crop_height})`; + } + + showContainer() { + this.container.style.display = ''; + this.setPositionFormValues(); + this.positionBoundary(); + this.addEvents(); + } + + save() { + let current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset; + let payload = { + thumb_width: current.crop_width, + thumb_height: current.crop_height, + crop_method: current.crop_method, + crop_width: this.crop_width.value, + crop_height: this.crop_height.value, + source_x: this.source_x.value, + source_y: this.source_y.value + }; + let req = HttpRequest("post", this.opt.submitUrl + "?id=" + this.opt.asset_id + "&updatethumb", + "data=" + encodeURIComponent(JSON.stringify(payload)), function(response) { + this.opt.after_save(response); + this.hide(); + }.bind(this)); + } + + show() { + if (typeof this.container === "undefined") { + this.initDOM(); + } + + // Defer showing and positioning until image is loaded. + // !!! TODO: add a spinner in the mean time? + if (this.original_image.naturalWidth > 0) { this.showContainer(); - }.bind(this)); + } else { + this.original_image.addEventListener("load", event => this.showContainer()); + } } -}; -CropEditor.prototype.hide = function() { - this.container.style.display = "none"; -}; + hide() { + this.container.style.display = "none"; + } -CropEditor.prototype.addEvents = function(event) { - var drag_target = document.getElementById(opt.drag_target); - drag_target.addEventListener('dragstart', this.dragStart); - drag_target.addEventListener('drag', this.drag); - drag_target.addEventListener('dragend', this.dragEnd); -}; + addEvents(event) { + let cropTarget = this.image_container; + cropTarget.addEventListener('mousedown', this.cropSelectionStart.bind(this)); + cropTarget.addEventListener('mousemove', this.cropSelection.bind(this)); + cropTarget.addEventListener('mouseup', this.cropSelectionEnd.bind(this)); + // cropTarget.addEventListener('mouseout', this.cropSelectionEnd.bind(this)); -CropEditor.prototype.dragStart = function(event) { - console.log(event); - event.preventDefault(); -}; + this.original_image.addEventListener('mousedown', event => {return false}); + this.original_image.addEventListener('dragstart', event => {return false}); -CropEditor.prototype.dragEnd = function(event) { - console.log(event); -}; + let moveTarget = this.crop_boundary; + moveTarget.addEventListener('mousedown', this.moveSelectionStart.bind(this)); + moveTarget.addEventListener('mousemove', this.moveSelection.bind(this)); + moveTarget.addEventListener('mouseup', this.moveSelectionEnd.bind(this)); -CropEditor.prototype.drag = function(event) { - console.log(event); -}; + window.addEventListener('resize', this.positionBoundary.bind(this)); + } -CropEditor.prototype.toggleCropButton = function() { - var current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset; - this.edit_crop_button.style.display = typeof current.crop_method === "undefined" ? "none" : ""; -}; + cropSelectionStart(event) { + if (this.isMoving) { + return false; + } -CropEditor.prototype.positionBoundary = function(event) { - var source_x = parseInt(this.source_x.value), - source_y = parseInt(this.source_y.value), - crop_width = parseInt(this.crop_width.value), - crop_height = parseInt(this.crop_height.value), - real_width = this.original_image.naturalWidth, - real_height = this.original_image.naturalHeight, - scaled_width = this.original_image.clientWidth, - scaled_height = this.original_image.clientHeight; + let dragStartX = event.x - this.image_container.offsetLeft; + let dragStartY = event.y - this.image_container.offsetTop; - var width_scale = scaled_width / real_width, - height_scale = scaled_height / real_height; + if (dragStartX > this.original_image.clientWidth || + dragStartY > this.original_image.clientHeight) { + return; + } - crop_boundary.style.left = (this.source_x.value) * width_scale + "px"; - crop_boundary.style.top = (this.source_y.value) * height_scale + "px"; - crop_boundary.style.width = (this.crop_width.value) * width_scale + "px"; - crop_boundary.style.height = (this.crop_height.value) * height_scale + "px"; -}; + this.isDragging = true; + this.dragStartX = dragStartX; + this.dragStartY = dragStartY; + } + + cropSelectionEnd(event) { + this.isDragging = false; + this.handleCropSelectionEvent(event); + } + + cropSelection(event) { + this.handleCropSelectionEvent(event); + } + + getScaleFactor() { + return this.original_image.naturalWidth / this.original_image.clientWidth; + } + + handleCropSelectionEvent(event) { + if (!this.isDragging) { + return; + } + + this.dragEndX = event.x - this.image_container.offsetLeft; + this.dragEndY = event.y - this.image_container.offsetTop; + + let scaleFactor = this.getScaleFactor(); + + this.source_x.value = Math.ceil(Math.min(this.dragStartX, this.dragEndX) * scaleFactor); + this.source_y.value = Math.ceil(Math.min(this.dragStartY, this.dragEndY) * scaleFactor); + + let width = Math.ceil(Math.abs(this.dragEndX - this.dragStartX) * scaleFactor); + this.crop_width.value = Math.min(width, this.original_image.naturalWidth - this.source_x.value); + + let height = Math.ceil(Math.abs(this.dragEndY - this.dragStartY) * scaleFactor); + this.crop_height.value = Math.min(height, this.original_image.naturalHeight - this.source_y.value); + + if (this.crop_constrain.checked) { + let current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset; + + let currentAspectRatio = parseInt(this.crop_width.value) / parseInt(this.crop_height.value); + let targetAspectRatio = current.crop_width / current.crop_height; + + if (Math.abs(currentAspectRatio - targetAspectRatio) > 0.001) { + // Landscape? + if (targetAspectRatio > 1.0) { + let height = Math.ceil(this.crop_width.value / targetAspectRatio); + if (parseInt(this.source_y.value) + height > this.original_image.naturalHeight) { + height = this.original_image.naturalHeight - this.source_y.value; + } + this.crop_width.value = height * targetAspectRatio; + this.crop_height.value = height; + } + // Portrait? + else { + let width = Math.ceil(this.crop_height.value * targetAspectRatio); + if (parseInt(this.source_x.value) + width > this.original_image.naturalWidth) { + width = this.original_image.naturalWidth - this.source_x.value; + } + this.crop_width.value = width; + this.crop_height.value = width / targetAspectRatio; + } + } + } + + this.positionBoundary(); + } + + handleCropMoveEvent(event) { + if (!this.isMoving) { + return; + } + + this.dragEndX = event.x - this.crop_boundary.offsetLeft; + this.dragEndY = event.y - this.crop_boundary.offsetTop; + + let scaleFactor = this.getScaleFactor(); + + let x = parseInt(this.source_x.value) + Math.ceil((this.dragEndX - this.dragStartX) * scaleFactor); + if (x + parseInt(this.crop_width.value) > this.original_image.naturalWidth) { + x += this.original_image.naturalWidth - (x + parseInt(this.crop_width.value)); + } + this.source_x.value = Math.max(x, 0); + + let y = parseInt(this.source_y.value) + Math.ceil((this.dragEndY - this.dragStartY) * scaleFactor); + if (y + parseInt(this.crop_height.value) > this.original_image.naturalHeight) { + y += this.original_image.naturalHeight - (y + parseInt(this.crop_height.value)); + } + this.source_y.value = Math.max(y, 0); + + this.positionBoundary(); + } + + moveSelectionStart(event) { + if (this.isDragging) { + return false; + } + this.isMoving = true; + this.dragStartX = event.x - this.crop_boundary.offsetLeft; + this.dragStartY = event.y - this.crop_boundary.offsetTop; + } + + moveSelectionEnd(event) { + this.isMoving = false; + this.handleCropMoveEvent(event); + } + + moveSelection(event) { + this.handleCropMoveEvent(event); + } + + toggleCropButton() { + let current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset; + this.edit_crop_button.style.display = typeof current.crop_method === "undefined" ? "none" : ""; + } + + positionBoundary(event) { + let scaleFactor = this.getScaleFactor(); + crop_boundary.style.left = parseInt(this.source_x.value) / scaleFactor + "px"; + crop_boundary.style.top = parseInt(this.source_y.value) / scaleFactor + "px"; + crop_boundary.style.width = parseInt(this.crop_width.value) / scaleFactor + "px"; + crop_boundary.style.height = parseInt(this.crop_height.value) / scaleFactor + "px"; + } +} diff --git a/templates/EditAssetForm.php b/templates/EditAssetForm.php index 1dd3ed6a..490b0d24 100644 --- a/templates/EditAssetForm.php +++ b/templates/EditAssetForm.php @@ -138,10 +138,10 @@ class EditAssetForm extends SubTemplate

Thumbnails

View: