Make crop editor usable #22

Merged
Aaron merged 27 commits from crop-editor into master 2020-12-30 20:06:17 +01:00
7 changed files with 461 additions and 242 deletions

View File

@ -116,18 +116,25 @@ class EditAsset extends HTMLController
if (!preg_match('~^(?<width>\d+)x(?<height>\d+)(?<suffix>_c(?<method>[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.
Aaron marked this conversation as resolved
Review

Edge case: suffix can be null, as the group is optional in the regex. Handle this.

Edge case: `suffix` can be null, as the group is optional in the regex. Handle this.
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
Review

Is this still needed?

Is this still needed?
Review

It ought to be done, but I'd like to refactor thumbnail file management in general so this kind of thing is not left to the individual controllers.

It ought to be done, but I'd like to refactor thumbnail file management in general so this kind of thing is not left to the individual controllers.
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,

View File

@ -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))

View File

@ -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 = '';

View File

@ -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 */
}

View File

@ -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

View File

@ -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";
}
}

View File

@ -138,10 +138,10 @@ class EditAssetForm extends SubTemplate
<h3>Thumbnails</h3>
View: <select id="thumbnail_src">';
foreach ($this->thumbs as $thumb)
$first = INF;
foreach ($this->thumbs as $i => $thumb)
{
if (!$thumb['status'])
continue;
$first = min($i, $first);
echo '
<option data-url="', $thumb['url'], '" data-crop_width="', $thumb['dimensions'][0], '" data-crop_height="', $thumb['dimensions'][1], '"',
@ -171,18 +171,16 @@ class EditAssetForm extends SubTemplate
echo '
</select>
<a id="thumbnail_link" href="', $this->thumbs[0]['url'], '" target="_blank">
<img id="thumbnail" src="', $this->thumbs[0]['url'], '" alt="Thumbnail" style="width: 100%; height: auto;">
<a id="thumbnail_link" href="', $this->thumbs[$first]['url'], '" target="_blank">
<img id="thumbnail" src="', $this->thumbs[$first]['url'], '" alt="Thumbnail" style="width: 100%; height: auto;">
</a>
</div>
<script type="text/javascript">
setTimeout(function() {
document.getElementById("thumbnail_src").addEventListener("change", function(event) {
var selection = event.target.options[event.target.selectedIndex];
document.getElementById("thumbnail_link").href = selection.dataset.url;
document.getElementById("thumbnail").src = selection.dataset.url;
});
}, 100);
<script type="text/javascript" defer="defer">
document.getElementById("thumbnail_src").addEventListener("change", event => {
let selection = event.target.options[event.target.selectedIndex];
document.getElementById("thumbnail_link").href = selection.dataset.url;
document.getElementById("thumbnail").src = selection.dataset.url;
});
</script>';
}
@ -193,27 +191,27 @@ class EditAssetForm extends SubTemplate
echo '
<script type="text/javascript" src="', BASEURL, '/js/crop_editor.js"></script>
<script type="text/javascript">
setTimeout(function() {
var editor = new CropEditor({
submit_url: "', BASEURL, '/editasset/",
original_image_src: "', $this->asset->getUrl(), '",
editor_container_parent_id: "asset_form",
thumbnail_select_id: "thumbnail_src",
drag_target: "drag_target",
asset_id: ', $this->asset->getId(), ',
after_save: function(data) {
// Update thumbnail
document.getElementById("thumbnail").src = data.url + "?" + (new Date()).getTime();
<script type="text/javascript" defer="defer">
let editor = new CropEditor({
submit_url: "', BASEURL, '/editasset/",
original_image_src: "', $this->asset->getUrl(), '",
editor_container_parent_id: "asset_form",
thumbnail_select_id: "thumbnail_src",
drag_target: ".crop_image_container",
asset_id: ', $this->asset->getId(), ',
after_save: function(data) {
// Update thumbnail
document.getElementById("thumbnail").src = data.url + "?" + (new Date()).getTime();
// Update select
var src = document.getElementById("thumbnail_src");
src.options[src.selectedIndex].dataset.crop_region = data.value;
// Update select
let src = document.getElementById("thumbnail_src");
let option = src.options[src.selectedIndex];
option.dataset.crop_region = data.value;
option.textContent = option.textContent.replace(/top|bottom|centre|slice/, "exact");
// TODO: update meta
}
});
}, 100);
// TODO: update meta
Review

Is this needed for this PR?

Is this needed for this PR?
Review

Thanks. No, this just still hasn't been implemented; it wasn't before, either. I'd like to refactor the form a little before I take this on.

Thanks. No, this just still hasn't been implemented; it wasn't before, either. I'd like to refactor the form a little before I take this on.
}
});
</script>';
}
@ -256,9 +254,6 @@ class EditAssetForm extends SubTemplate
foreach ($this->thumbs as $thumb)
{
if (!$thumb['status'])
continue;
echo '
<option value="thumb_', implode('x', $thumb['dimensions']);