diff --git a/resources/css/cp.css b/resources/css/cp.css index dd8ab0f1172..481323b3b6f 100644 --- a/resources/css/cp.css +++ b/resources/css/cp.css @@ -1,4 +1,5 @@ @import './vendors/codemirror.css'; +@import './vendors/cropper.css'; @import './core/animation.css'; @import './core/layout.css'; diff --git a/resources/css/vendors/cropper.css b/resources/css/vendors/cropper.css new file mode 100644 index 00000000000..521ca3c784f --- /dev/null +++ b/resources/css/vendors/cropper.css @@ -0,0 +1,39 @@ +/* CROPPER +=================================================== */ +/* Where the crop box touches the edge of the image, draw its border inside it so it isn't clipped, and make the handles more obvious. */ +.crop-editor-canvas { + .cropper-crop-box::before { + content: ''; + position: absolute; + inset: 0; + border: 0 solid #39f; + pointer-events: none; + } + + &.at-top .cropper-crop-box::before { border-top-width: 2px; } + &.at-right .cropper-crop-box::before { border-right-width: 2px; } + &.at-bottom .cropper-crop-box::before { border-bottom-width: 2px; } + &.at-left .cropper-crop-box::before { border-left-width: 2px; } + + &.at-top :is(.point-n, .point-ne, .point-nw), + &.at-right :is(.point-e, .point-ne, .point-se), + &.at-bottom :is(.point-s, .point-se, .point-sw), + &.at-left :is(.point-w, .point-nw, .point-sw) { + min-width: 6px; + min-height: 6px; + background-color: #39f; + box-shadow: 0 0 0 1px #fff; + opacity: 1; + } + + /* Give the smaller handles _exactly_ the same drag area as the enlarged ones for consistent usability. */ + .cropper-point::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 6px; + height: 6px; + translate: -50% -50%; + } +} diff --git a/resources/js/components/assets/Editor/CropEditor.vue b/resources/js/components/assets/Editor/CropEditor.vue index 46da37d98c6..567786234d5 100644 --- a/resources/js/components/assets/Editor/CropEditor.vue +++ b/resources/js/components/assets/Editor/CropEditor.vue @@ -39,6 +39,7 @@ const showConfirmation = ref(false); const uploading = ref(false); const pendingCrop = ref(null); const cropDimensions = ref(null); +const cropEdges = ref({}); const defaultQuality = Statamic.$config.get('cropQuality') || 90; function normalizeFormat(format) { @@ -123,6 +124,7 @@ function resetState() { uploading.value = false; pendingCrop.value = null; cropDimensions.value = null; + cropEdges.value = {}; quality.value = initialQuality(); format.value = sourceFormat.value; background.value = 'white'; @@ -160,7 +162,7 @@ function createCropper(imageElement) { aspectRatio: NaN, viewMode: 1, dragMode: 'crop', - autoCropArea: 0.9, + autoCropArea: 1, restore: false, guides: true, center: true, @@ -177,6 +179,7 @@ function createCropper(imageElement) { cropstart: onCropStart, cropmove: onCropMove, cropend: onCropEnd, + ready: updateCropEdges, crop: onCrop, }); } @@ -186,6 +189,30 @@ function onCrop(event) { width: Math.round(event.detail.width), height: Math.round(event.detail.height), }; + + updateCropEdges(); +} + +// Track which edges of the visible image the crop box touches, so the controls on those +// edges can be drawn inside the crop box instead of being clipped. +function updateCropEdges() { + if (!cropper.value) return; + + const container = cropper.value.getContainerData(); + const canvas = cropper.value.getCanvasData(); + const box = cropper.value.getCropBoxData(); + + const top = Math.max(canvas.top, 0); + const left = Math.max(canvas.left, 0); + const right = Math.min(canvas.left + canvas.width, container.width); + const bottom = Math.min(canvas.top + canvas.height, container.height); + + cropEdges.value = { + 'at-top': box.top - top < 1, + 'at-left': box.left - left < 1, + 'at-right': right - (box.left + box.width) < 1, + 'at-bottom': bottom - (box.top + box.height) < 1, + }; } function onCropStart() { @@ -480,7 +507,7 @@ function close() {