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)) if (!preg_match('~^(?<width>\d+)x(?<height>\d+)(?<suffix>_c(?<method>[best]?))?$~', $selector, $thumb))
continue; continue;
$has_crop_boundary = isset($metadata['crop_' . $thumb['width'] . 'x' . $thumb['height']]); $dimensions = $thumb['width'] . 'x' . $thumb['height'];
$has_custom_image = isset($metadata['custom_' . $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[] = [ $thumbs[] = [
'dimensions' => [(int) $thumb['width'], (int) $thumb['height']], 'dimensions' => [(int) $thumb['width'], (int) $thumb['height']],
'cropped' => !$has_custom_image && (!empty($thumb['suffix']) || $has_crop_boundary), '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_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, 'custom_image' => $has_custom_image,
'filename' => $filename, 'filename' => $filename,
'full_path' => THUMBSDIR . '/' . $subdir . '/' . $filename, 'url' => $thumb_url,
'url' => THUMBSURL . '/' . $subdir . '/' . $filename,
'status' => file_exists(THUMBSDIR . '/' . $subdir . '/' . $filename),
]; ];
} }
@ -144,18 +151,19 @@ class EditAsset extends HTMLController
$crop_value = $data->crop_width . ',' . $data->crop_height . ',' . $data->source_x . ',' . $data->source_y; $crop_value = $data->crop_width . ',' . $data->crop_height . ',' . $data->source_x . ',' . $data->source_y;
$meta[$crop_key] = $crop_value; $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; $custom_key = 'custom_' . $data->thumb_width . 'x' . $data->thumb_height;
if (isset($meta[$custom_key])) 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]); unset($meta[$custom_key]);
}
// Save meta changes so far.
$image->setMetaData($meta);
// Force a rebuild of related thumbnails. // Force a rebuild of related thumbnails.
$thumb_key = 'thumb_' . $data->thumb_width . 'x' . $data->thumb_height; $image->removeThumbnailsOfSize($data->thumb_width, $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);
$payload = [ $payload = [
'key' => $crop_key, 'key' => $crop_key,

View File

@ -133,9 +133,9 @@ class Image extends Asset
public function removeAllThumbnails() 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)) if (is_file($thumb_path))
unlink($thumb_path); unlink($thumb_path);
} }
@ -146,6 +146,30 @@ class Image extends Asset
['id_asset' => $this->id_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) public function replaceThumbnail($descriptor, $tmp_file)
{ {
if (!is_file($tmp_file)) if (!is_file($tmp_file))

View File

@ -164,6 +164,8 @@ class Thumbnail
$this->filename_suffix .= 's'; $this->filename_suffix .= 's';
elseif ($this->crop_mode === self::CROP_MODE_SLICE_BOTTOM) elseif ($this->crop_mode === self::CROP_MODE_SLICE_BOTTOM)
$this->filename_suffix .= 'b'; $this->filename_suffix .= 'b';
elseif ($this->crop_mode === self::CROP_MODE_BOUNDARY)
$this->filename_suffix .= 'e';
} }
else else
$this->filename_suffix = ''; $this->filename_suffix = '';

View File

@ -145,46 +145,55 @@ body {
/* Crop editor /* Crop editor
----------------*/ ----------------*/
#crop_editor { #crop_editor {
display: flex;
flex-direction: column;
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
height: 100%; height: 100%;
width: 100%; width: 100%;
background: #000; background: rgba(0, 0, 0, 0.8);
z-index: 100; z-index: 100;
color: #fff; color: #fff;
} }
#crop_editor input { #crop_editor input[type=number] {
width: 50px; width: 50px;
background: #555; background: #555;
color: #fff; color: #fff;
} }
.crop_image_container { #crop_editor input[type=checkbox] {
position: relative; vertical-align: middle;
} }
.crop_position { .crop_position {
background: rgba(0, 0, 0, 1.0);
border: none;
padding: 5px; padding: 5px;
text-align: center; text-align: center;
} }
.crop_position input, .crop_position .btn { .crop_position input, .crop_position .btn {
margin: 0 5px; margin: 0 5px;
} }
.crop_image_container {
position: relative;
flex-grow: 1;
max-height: calc(100% - 34px);
}
.crop_image_container img { .crop_image_container img {
height: auto; border: 1px solid #000;
width: auto; max-height: 100%;
max-width: 100%; max-width: 100%;
max-height: 700px;
} }
#crop_boundary { #crop_boundary {
border: 1px solid rgba(255, 255, 255, 0.75); border: 1px dashed rgb(255, 255, 255);
background: rgba(255, 255, 255, 0.75); background: rgba(255, 255, 255, 0.4);
cursor: move;
position: absolute; position: absolute;
z-index: 200; z-index: 200;
width: 500px; width: 500px;
height: 300px; height: 300px;
top: 400px; top: 400px;
left: 300px; left: 300px;
filter: invert(100%); /* temp */
} }

View File

@ -375,34 +375,59 @@ footer a {
input, select, .btn { input, select, .btn {
background: #fff; background: #fff;
border: 1px solid #ccc; border: 1px solid #dbdbdb;
border-radius: 4px;
color: #000; color: #000;
font: 13px/1.7 "Open Sans", "Helvetica", sans-serif; font: 13px/1.7 "Open Sans", "Helvetica", sans-serif;
padding: 3px; 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 { textarea {
border: 1px solid #ccc; border: 1px solid #dbdbdb;
font: 12px/1.4 'Monaco', 'Inconsolata', 'DejaVu Sans Mono', monospace; border-radius: 4px;
font: 14px/1.4 'Inconsolata', 'DejaVu Sans Mono', monospace;
padding: 0.75%; 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 { .btn-red {
background: #F3B076; background: #eebbaa;
border-color: #C98245; border-color: #cc9988;
}
.btn-red:hover, .btn-red:focus {
border-color: #bb7766;
color: #000; color: #000;
} }
.btn-red:focus:not(:active) {
box-shadow: 0px 0px 0px 2px rgba(241, 70, 104, 0.25);
}
/* Login box styles /* Login box styles

View File

@ -1,9 +1,10 @@
function CropEditor(opt) { class CropEditor {
constructor(opt) {
this.opt = opt; this.opt = opt;
this.edit_crop_button = document.createElement("span"); this.edit_crop_button = document.createElement("span");
this.edit_crop_button.className = "btn"; this.edit_crop_button.className = "btn";
this.edit_crop_button.innerHTML = "Edit crop"; this.edit_crop_button.textContent = "Edit crop";
this.edit_crop_button.addEventListener('click', this.show.bind(this)); this.edit_crop_button.addEventListener('click', this.show.bind(this));
this.thumbnail_select = document.getElementById(opt.thumbnail_select_id); this.thumbnail_select = document.getElementById(opt.thumbnail_select_id);
@ -11,56 +12,85 @@ function CropEditor(opt) {
this.thumbnail_select.parentNode.insertBefore(this.edit_crop_button, this.thumbnail_select.nextSibling); this.thumbnail_select.parentNode.insertBefore(this.edit_crop_button, this.thumbnail_select.nextSibling);
this.toggleCropButton(); this.toggleCropButton();
} }
CropEditor.prototype.buildContainer = function() { initDOM() {
this.container = document.createElement("div"); this.container = document.createElement("div");
this.container.id = "crop_editor"; this.container.id = "crop_editor";
this.position = document.createElement("div"); this.initPositionForm();
this.initImageContainer();
this.parent = document.getElementById(this.opt.editor_container_parent_id);
this.parent.appendChild(this.container);
}
initPositionForm() {
this.position = document.createElement("fieldset");
this.position.className = "crop_position"; this.position.className = "crop_position";
this.container.appendChild(this.position); this.container.appendChild(this.position);
var source_x_label = document.createTextNode("Source X:"); let source_x_label = document.createTextNode("Source X:");
this.position.appendChild(source_x_label); this.position.appendChild(source_x_label);
this.source_x = document.createElement("input"); 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.source_x.addEventListener("keyup", this.positionBoundary.bind(this));
this.position.appendChild(this.source_x); this.position.appendChild(this.source_x);
var source_y_label = document.createTextNode("Source Y:"); let source_y_label = document.createTextNode("Source Y:");
this.position.appendChild(source_y_label); this.position.appendChild(source_y_label);
this.source_y = document.createElement("input"); 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.source_y.addEventListener("keyup", this.positionBoundary.bind(this));
this.position.appendChild(this.source_y); this.position.appendChild(this.source_y);
var crop_width_label = document.createTextNode("Crop width:"); let crop_width_label = document.createTextNode("Crop width:");
this.position.appendChild(crop_width_label); this.position.appendChild(crop_width_label);
this.crop_width = document.createElement("input"); 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.crop_width.addEventListener("keyup", this.positionBoundary.bind(this));
this.position.appendChild(this.crop_width); this.position.appendChild(this.crop_width);
var crop_height_label = document.createTextNode("Crop height:"); let crop_height_label = document.createTextNode("Crop height:");
this.position.appendChild(crop_height_label); this.position.appendChild(crop_height_label);
this.crop_height = document.createElement("input"); 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.crop_height.addEventListener("keyup", this.positionBoundary.bind(this));
this.position.appendChild(this.crop_height); this.position.appendChild(this.crop_height);
this.crop_constrain_label = document.createElement("label");
this.position.appendChild(this.crop_constrain_label);
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.crop_constrain_text = document.createTextNode('Constrain proportions');
this.crop_constrain_label.appendChild(this.crop_constrain_text);
this.save_button = document.createElement("span"); this.save_button = document.createElement("span");
this.save_button.className = "btn"; this.save_button.className = "btn";
this.save_button.innerHTML = "Save"; this.save_button.textContent = "Save";
this.save_button.addEventListener('click', this.save.bind(this)); this.save_button.addEventListener('click', this.save.bind(this));
this.position.appendChild(this.save_button); this.position.appendChild(this.save_button);
this.abort_button = document.createElement("span"); this.abort_button = document.createElement("span");
this.abort_button.className = "btn btn-red"; this.abort_button.className = "btn btn-red";
this.abort_button.innerHTML = "Abort"; this.abort_button.textContent = "Abort";
this.abort_button.addEventListener('click', this.hide.bind(this)); this.abort_button.addEventListener('click', this.hide.bind(this));
this.position.appendChild(this.abort_button); this.position.appendChild(this.abort_button);
}
initImageContainer() {
this.image_container = document.createElement("div"); this.image_container = document.createElement("div");
this.image_container.className = "crop_image_container"; this.image_container.className = "crop_image_container";
this.container.appendChild(this.image_container); this.container.appendChild(this.image_container);
@ -70,74 +100,90 @@ CropEditor.prototype.buildContainer = function() {
this.image_container.appendChild(this.crop_boundary); this.image_container.appendChild(this.crop_boundary);
this.original_image = document.createElement("img"); this.original_image = document.createElement("img");
this.original_image.draggable = false;
this.original_image.id = "original_image"; this.original_image.id = "original_image";
this.original_image.src = this.opt.original_image_src; this.original_image.src = this.opt.original_image_src;
this.image_container.appendChild(this.original_image); this.image_container.appendChild(this.original_image);
}
this.parent = document.getElementById(this.opt.editor_container_parent_id); setDefaultCrop(cropAspectRatio, cropMethod) {
this.parent.appendChild(this.container); let source = this.original_image;
}; let sourceAspectRatio = source.naturalWidth / source.naturalHeight;
CropEditor.prototype.setInputValues = function() {
var current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset;
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);
// Cropping from the centre? // Cropping from the centre?
if (current.crop_method === "c") { if (cropMethod === "c" || cropMethod === "s") {
// Crop vertically from the centre, using the entire width. // Crop vertically from the centre, using the entire width.
if (source_ratio < crop_ratio) { if (sourceAspectRatio <= cropAspectRatio) {
this.crop_width.value = this.original_image.naturalWidth; this.crop_width.value = source.naturalWidth;
this.crop_height.value = Math.ceil(this.original_image.naturalWidth / crop_ratio); this.crop_height.value = Math.ceil(source.naturalWidth / cropAspectRatio);
this.source_x.value = 0; 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. // Crop horizontally from the centre, using the entire height.
else { else {
this.crop_width.value = Math.ceil(current.crop_width * this.original_image.naturalHeight / current.crop_height); this.crop_width.value = Math.ceil(cropAspectRatio * source.naturalHeight);
this.crop_height.value = this.original_image.naturalHeight; this.crop_height.value = source.naturalHeight;
this.source_x.value = Math.ceil((this.original_image.naturalWidth - this.crop_width.value) / 2); this.source_x.value = Math.ceil((source.naturalWidth - this.crop_width.value) / 2);
this.source_y.value = 0; this.source_y.value = 0;
} }
} }
// Cropping a top or bottom slice? // Cropping a top or bottom slice?
else { else {
// Can we actually take a top or bottom slice from the original image? // Can we actually take a top or bottom slice from the original image?
if (source_ratio < crop_ratio) { if (sourceAspectRatio <= cropAspectRatio) {
this.crop_width.value = this.original_image.naturalWidth; this.crop_width.value = source.naturalWidth;
this.crop_height.value = Math.floor(this.original_image.naturalHeight / crop_ratio); this.crop_height.value = Math.floor(source.naturalWidth / cropAspectRatio);
this.source_x.value = "0"; 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. // Otherwise, take a vertical slice from the centre.
else { else {
this.crop_width.value = Math.floor(this.original_image.naturalHeight * crop_ratio); this.crop_width.value = Math.floor(source.naturalHeight * cropAspectRatio);
this.crop_height.value = this.original_image.naturalHeight; this.crop_height.value = source.naturalHeight;
this.source_x.value = Math.floor((this.original_image.naturalWidth - this.crop_width.value) / 2); this.source_x.value = Math.floor((source.naturalWidth - this.crop_width.value) / 2);
this.source_y.value = "0"; this.source_y.value = "0";
} }
} }
}
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 { } else {
var region = current.crop_region.split(','); let region = current.crop_region.split(',');
this.crop_width.value = region[0]; this.crop_width.value = region[0];
this.crop_height.value = region[1]; this.crop_height.value = region[1];
this.source_x.value = region[2]; this.source_x.value = region[2];
this.source_y.value = region[3]; this.source_y.value = region[3];
} }
};
CropEditor.prototype.showContainer = function() { this.crop_width.min = 1;
this.container.style.display = "block"; this.crop_height.min = 1;
this.setInputValues(); 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.positionBoundary();
} this.addEvents();
}
CropEditor.prototype.save = function() { save() {
var current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset; let current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset;
var payload = { let payload = {
thumb_width: current.crop_width, thumb_width: current.crop_width,
thumb_height: current.crop_height, thumb_height: current.crop_height,
crop_method: current.crop_method, crop_method: current.crop_method,
@ -146,16 +192,16 @@ CropEditor.prototype.save = function() {
source_x: this.source_x.value, source_x: this.source_x.value,
source_y: this.source_y.value source_y: this.source_y.value
}; };
var req = HttpRequest("post", this.opt.submitUrl + "?id=" + this.opt.asset_id + "&updatethumb", let req = HttpRequest("post", this.opt.submitUrl + "?id=" + this.opt.asset_id + "&updatethumb",
"data=" + encodeURIComponent(JSON.stringify(payload)), function(response) { "data=" + encodeURIComponent(JSON.stringify(payload)), function(response) {
this.opt.after_save(response); this.opt.after_save(response);
this.hide(); this.hide();
}.bind(this)); }.bind(this));
}; }
CropEditor.prototype.show = function() { show() {
if (typeof this.container === "undefined") { if (typeof this.container === "undefined") {
this.buildContainer(); this.initDOM();
} }
// Defer showing and positioning until image is loaded. // Defer showing and positioning until image is loaded.
@ -163,56 +209,166 @@ CropEditor.prototype.show = function() {
if (this.original_image.naturalWidth > 0) { if (this.original_image.naturalWidth > 0) {
this.showContainer(); this.showContainer();
} else { } else {
this.original_image.addEventListener("load", function() { this.original_image.addEventListener("load", event => this.showContainer());
this.showContainer(); }
}.bind(this));
} }
};
CropEditor.prototype.hide = function() { hide() {
this.container.style.display = "none"; this.container.style.display = "none";
}; }
CropEditor.prototype.addEvents = function(event) { addEvents(event) {
var drag_target = document.getElementById(opt.drag_target); let cropTarget = this.image_container;
drag_target.addEventListener('dragstart', this.dragStart); cropTarget.addEventListener('mousedown', this.cropSelectionStart.bind(this));
drag_target.addEventListener('drag', this.drag); cropTarget.addEventListener('mousemove', this.cropSelection.bind(this));
drag_target.addEventListener('dragend', this.dragEnd); cropTarget.addEventListener('mouseup', this.cropSelectionEnd.bind(this));
}; // cropTarget.addEventListener('mouseout', this.cropSelectionEnd.bind(this));
CropEditor.prototype.dragStart = function(event) { this.original_image.addEventListener('mousedown', event => {return false});
console.log(event); this.original_image.addEventListener('dragstart', event => {return false});
event.preventDefault();
};
CropEditor.prototype.dragEnd = function(event) { let moveTarget = this.crop_boundary;
console.log(event); 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) { window.addEventListener('resize', this.positionBoundary.bind(this));
console.log(event); }
};
CropEditor.prototype.toggleCropButton = function() { cropSelectionStart(event) {
var current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset; if (this.isMoving) {
return false;
}
let dragStartX = event.x - this.image_container.offsetLeft;
let dragStartY = event.y - this.image_container.offsetTop;
if (dragStartX > this.original_image.clientWidth ||
dragStartY > this.original_image.clientHeight) {
return;
}
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" : ""; this.edit_crop_button.style.display = typeof current.crop_method === "undefined" ? "none" : "";
}; }
CropEditor.prototype.positionBoundary = function(event) { positionBoundary(event) {
var source_x = parseInt(this.source_x.value), let scaleFactor = this.getScaleFactor();
source_y = parseInt(this.source_y.value), crop_boundary.style.left = parseInt(this.source_x.value) / scaleFactor + "px";
crop_width = parseInt(this.crop_width.value), crop_boundary.style.top = parseInt(this.source_y.value) / scaleFactor + "px";
crop_height = parseInt(this.crop_height.value), crop_boundary.style.width = parseInt(this.crop_width.value) / scaleFactor + "px";
real_width = this.original_image.naturalWidth, crop_boundary.style.height = parseInt(this.crop_height.value) / scaleFactor + "px";
real_height = this.original_image.naturalHeight, }
scaled_width = this.original_image.clientWidth, }
scaled_height = this.original_image.clientHeight;
var width_scale = scaled_width / real_width,
height_scale = scaled_height / real_height;
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";
};

View File

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