Ver Fonte

More image editor improvements (#4676)

* Editor.jsx - make granular rotation [-45, 45] instead of [-45, 44]

* Editor.jsx - add labels to all buttons

* css - center granularRotation's label

* locale - Revert -> Cancel -> Reset

* locale - Rotate => Rotate 90°

Co-authored-by: Alexander Zaytsev <nqst@users.noreply.github.com>

* Update packages/@uppy/image-editor/src/locale.js

locale - Flip => Flip horizontally

Co-authored-by: Alexander Zaytsev <nqst@users.noreply.github.com>

* typo fix

* Editor.jsx - add cropbox limitation

* Editor.jsx - always behave as if cropbox is still wrapping the full image on rotation

* Editor.jsx - cropper settings => `.addEventListener`

* cropper.scss - move all Uppy-specific changes into `style.scss`

* css - enable theme-dependent backgrounds

* inputrange.scss - update the version, move all Uppy-specific changes into `style.scss`

* style.scss - improve toolbox styles

* style.scss - make slider :hover/:focus visible!

* prettier - run prettier on our new code

* image editor - fix Alex's "weird image" error

* image editor - add the `quality: 0.8` explanation

* image editor - set the minimum height and width of the cropbox

---------

Co-authored-by: Alexander Zaytsev <nqst@users.noreply.github.com>
Evgenia Karunus há 1 ano atrás
pai
commit
47e9f70761

+ 149 - 84
packages/@uppy/image-editor/src/Editor.jsx

@@ -1,7 +1,10 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
 import Cropper from 'cropperjs'
 import { h, Component } from 'preact'
 import getCanvasDataThatFitsPerfectlyIntoContainer from './utils/getCanvasDataThatFitsPerfectlyIntoContainer.js'
 import getScaleFactorThatRemovesDarkCorners from './utils/getScaleFactorThatRemovesDarkCorners.js'
+import limitCropboxMovementOnMove from './utils/limitCropboxMovementOnMove.js'
+import limitCropboxMovementOnResize from './utils/limitCropboxMovementOnResize.js'
 
 export default class Editor extends Component {
   constructor (props) {
@@ -9,7 +12,10 @@ export default class Editor extends Component {
     this.state = {
       angle90Deg: 0,
       angleGranular: 0,
+      prevCropboxData: null,
     }
+    this.storePrevCropboxData = this.storePrevCropboxData.bind(this)
+    this.limitCropboxMovement = this.limitCropboxMovement.bind(this)
   }
 
   componentDidMount () {
@@ -18,11 +24,39 @@ export default class Editor extends Component {
       this.imgElement,
       opts.cropperOptions,
     )
+
+    this.imgElement.addEventListener('cropstart', this.storePrevCropboxData)
+    this.imgElement.addEventListener('cropend', this.limitCropboxMovement)
+
     storeCropperInstance(this.cropper)
   }
 
   componentWillUnmount () {
     this.cropper.destroy()
+
+    this.imgElement.removeEventListener('cropstart', this.storePrevCropboxData)
+    this.imgElement.removeEventListener('cropend', this.limitCropboxMovement)
+  }
+
+  // eslint-disable-next-line react/sort-comp
+  storePrevCropboxData () {
+    this.setState({ prevCropboxData: this.cropper.getCropBoxData() })
+  }
+
+  limitCropboxMovement (event) {
+    const canvasData = this.cropper.getCanvasData()
+    const cropboxData = this.cropper.getCropBoxData()
+    const { prevCropboxData } = this.state
+
+    // 1. When we grab the cropbox in the middle and move it
+    if (event.detail.action === 'all') {
+      const newCropboxData = limitCropboxMovementOnMove(canvasData, cropboxData, prevCropboxData)
+      if (newCropboxData) this.cropper.setCropBoxData(newCropboxData)
+    // When we stretch the cropbox by one of its sides
+    } else {
+      const newCropboxData = limitCropboxMovementOnResize(canvasData, cropboxData, prevCropboxData)
+      if (newCropboxData) this.cropper.setCropBoxData(newCropboxData)
+    }
   }
 
   onRotate90Deg = () => {
@@ -50,7 +84,7 @@ export default class Editor extends Component {
   }
 
   onRotateGranular = (ev) => {
-    //  1. Set stsate
+    //  1. Set state
     const newGranularAngle = Number(ev.target.value)
     this.setState({ angleGranular: newGranularAngle })
 
@@ -60,8 +94,8 @@ export default class Editor extends Component {
     this.cropper.rotateTo(newAngle)
 
     // 3. Scale the image so that it fits into the cropbox
-    const cropboxData = this.cropper.getCropBoxData()
-    const scaleFactor = getScaleFactorThatRemovesDarkCorners(cropboxData, newGranularAngle)
+    const image = this.cropper.getImageData()
+    const scaleFactor = getScaleFactorThatRemovesDarkCorners(image.naturalWidth, image.naturalHeight, newGranularAngle)
     // Preserve flip
     const scaleFactorX = this.cropper.getImageData().scaleX < 0 ? -scaleFactor : scaleFactor
     this.cropper.scale(scaleFactorX, scaleFactor)
@@ -72,12 +106,11 @@ export default class Editor extends Component {
     const { angleGranular } = this.state
 
     return (
-      // eslint-disable-next-line jsx-a11y/label-has-associated-control
       <label
-        data-microtip-position="top"
         role="tooltip"
         aria-label={`${angleGranular}º`}
-        className="uppy-ImageCropper-rangeWrapper uppy-u-reset"
+        data-microtip-position="top"
+        className="uppy-ImageCropper-rangeWrapper"
       >
         <input
           className="uppy-ImageCropper-range uppy-u-reset"
@@ -86,7 +119,7 @@ export default class Editor extends Component {
           onChange={this.onRotateGranular}
           value={angleGranular}
           min="-45"
-          max="44"
+          max="45"
           aria-label={i18n('rotate')}
         />
       </label>
@@ -97,22 +130,26 @@ export default class Editor extends Component {
     const { i18n } = this.props
 
     return (
-      <button
-        type="button"
-        className="uppy-u-reset uppy-c-btn"
+      <label
+        role="tooltip"
         aria-label={i18n('revert')}
         data-microtip-position="top"
-        onClick={() => {
-          this.cropper.reset()
-          this.cropper.setAspectRatio(0)
-          this.setState({ angle90Deg: 0, angleGranular: 0 })
-        }}
       >
-        <svg aria-hidden="true" className="uppy-c-icon" width="24" height="24" viewBox="0 0 24 24">
-          <path d="M0 0h24v24H0z" fill="none" />
-          <path d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z" />
-        </svg>
-      </button>
+        <button
+          type="button"
+          className="uppy-u-reset uppy-c-btn"
+          onClick={() => {
+            this.cropper.reset()
+            this.cropper.setAspectRatio(0)
+            this.setState({ angle90Deg: 0, angleGranular: 0 })
+          }}
+        >
+          <svg aria-hidden="true" className="uppy-c-icon" width="24" height="24" viewBox="0 0 24 24">
+            <path d="M0 0h24v24H0z" fill="none" />
+            <path d="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z" />
+          </svg>
+        </button>
+      </label>
     )
   }
 
@@ -120,18 +157,22 @@ export default class Editor extends Component {
     const { i18n } = this.props
 
     return (
-      <button
-        type="button"
-        className="uppy-u-reset uppy-c-btn"
-        onClick={this.onRotate90Deg}
+      <label
+        role="tooltip"
         aria-label={i18n('rotate')}
         data-microtip-position="top"
       >
-        <svg aria-hidden="true" className="uppy-c-icon" width="24" height="24" viewBox="0 0 24 24">
-          <path d="M0 0h24v24H0V0zm0 0h24v24H0V0z" fill="none" />
-          <path d="M14 10a2 2 0 012 2v7a2 2 0 01-2 2H6a2 2 0 01-2-2v-7a2 2 0 012-2h8zm0 1.75H6a.25.25 0 00-.243.193L5.75 12v7a.25.25 0 00.193.243L6 19.25h8a.25.25 0 00.243-.193L14.25 19v-7a.25.25 0 00-.193-.243L14 11.75zM12 .76V4c2.3 0 4.61.88 6.36 2.64a8.95 8.95 0 012.634 6.025L21 13a1 1 0 01-1.993.117L19 13h-.003a6.979 6.979 0 00-2.047-4.95 6.97 6.97 0 00-4.652-2.044L12 6v3.24L7.76 5 12 .76z" />
-        </svg>
-      </button>
+        <button
+          type="button"
+          className="uppy-u-reset uppy-c-btn"
+          onClick={this.onRotate90Deg}
+        >
+          <svg aria-hidden="true" className="uppy-c-icon" width="24" height="24" viewBox="0 0 24 24">
+            <path d="M0 0h24v24H0V0zm0 0h24v24H0V0z" fill="none" />
+            <path d="M14 10a2 2 0 012 2v7a2 2 0 01-2 2H6a2 2 0 01-2-2v-7a2 2 0 012-2h8zm0 1.75H6a.25.25 0 00-.243.193L5.75 12v7a.25.25 0 00.193.243L6 19.25h8a.25.25 0 00.243-.193L14.25 19v-7a.25.25 0 00-.193-.243L14 11.75zM12 .76V4c2.3 0 4.61.88 6.36 2.64a8.95 8.95 0 012.634 6.025L21 13a1 1 0 01-1.993.117L19 13h-.003a6.979 6.979 0 00-2.047-4.95 6.97 6.97 0 00-4.652-2.044L12 6v3.24L7.76 5 12 .76z" />
+          </svg>
+        </button>
+      </label>
     )
   }
 
@@ -139,18 +180,22 @@ export default class Editor extends Component {
     const { i18n } = this.props
 
     return (
-      <button
-        type="button"
-        className="uppy-u-reset uppy-c-btn"
+      <label
+        role="tooltip"
         aria-label={i18n('flipHorizontal')}
         data-microtip-position="top"
-        onClick={() => this.cropper.scaleX(-this.cropper.getData().scaleX || -1)}
       >
-        <svg aria-hidden="true" className="uppy-c-icon" width="24" height="24" viewBox="0 0 24 24">
-          <path d="M0 0h24v24H0z" fill="none" />
-          <path d="M15 21h2v-2h-2v2zm4-12h2V7h-2v2zM3 5v14c0 1.1.9 2 2 2h4v-2H5V5h4V3H5c-1.1 0-2 .9-2 2zm16-2v2h2c0-1.1-.9-2-2-2zm-8 20h2V1h-2v22zm8-6h2v-2h-2v2zM15 5h2V3h-2v2zm4 8h2v-2h-2v2zm0 8c1.1 0 2-.9 2-2h-2v2z" />
-        </svg>
-      </button>
+        <button
+          type="button"
+          className="uppy-u-reset uppy-c-btn"
+          onClick={() => this.cropper.scaleX(-this.cropper.getData().scaleX || -1)}
+        >
+          <svg aria-hidden="true" className="uppy-c-icon" width="24" height="24" viewBox="0 0 24 24">
+            <path d="M0 0h24v24H0z" fill="none" />
+            <path d="M15 21h2v-2h-2v2zm4-12h2V7h-2v2zM3 5v14c0 1.1.9 2 2 2h4v-2H5V5h4V3H5c-1.1 0-2 .9-2 2zm16-2v2h2c0-1.1-.9-2-2-2zm-8 20h2V1h-2v22zm8-6h2v-2h-2v2zM15 5h2V3h-2v2zm4 8h2v-2h-2v2zm0 8c1.1 0 2-.9 2-2h-2v2z" />
+          </svg>
+        </button>
+      </label>
     )
   }
 
@@ -158,19 +203,23 @@ export default class Editor extends Component {
     const { i18n } = this.props
 
     return (
-      <button
-        type="button"
-        className="uppy-u-reset uppy-c-btn"
+      <label
+        role="tooltip"
         aria-label={i18n('zoomIn')}
         data-microtip-position="top"
-        onClick={() => this.cropper.zoom(0.1)}
       >
-        <svg aria-hidden="true" className="uppy-c-icon" height="24" viewBox="0 0 24 24" width="24">
-          <path d="M0 0h24v24H0V0z" fill="none" />
-          <path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
-          <path d="M12 10h-2v2H9v-2H7V9h2V7h1v2h2v1z" />
-        </svg>
-      </button>
+        <button
+          type="button"
+          className="uppy-u-reset uppy-c-btn"
+          onClick={() => this.cropper.zoom(0.1)}
+        >
+          <svg aria-hidden="true" className="uppy-c-icon" height="24" viewBox="0 0 24 24" width="24">
+            <path d="M0 0h24v24H0V0z" fill="none" />
+            <path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
+            <path d="M12 10h-2v2H9v-2H7V9h2V7h1v2h2v1z" />
+          </svg>
+        </button>
+      </label>
     )
   }
 
@@ -178,18 +227,22 @@ export default class Editor extends Component {
     const { i18n } = this.props
 
     return (
-      <button
-        type="button"
-        className="uppy-u-reset uppy-c-btn"
+      <label
+        role="tooltip"
         aria-label={i18n('zoomOut')}
         data-microtip-position="top"
-        onClick={() => this.cropper.zoom(-0.1)}
       >
-        <svg aria-hidden="true" className="uppy-c-icon" width="24" height="24" viewBox="0 0 24 24">
-          <path d="M0 0h24v24H0V0z" fill="none" />
-          <path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14zM7 9h5v1H7z" />
-        </svg>
-      </button>
+        <button
+          type="button"
+          className="uppy-u-reset uppy-c-btn"
+          onClick={() => this.cropper.zoom(-0.1)}
+        >
+          <svg aria-hidden="true" className="uppy-c-icon" width="24" height="24" viewBox="0 0 24 24">
+            <path d="M0 0h24v24H0V0z" fill="none" />
+            <path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14zM7 9h5v1H7z" />
+          </svg>
+        </button>
+      </label>
     )
   }
 
@@ -197,18 +250,22 @@ export default class Editor extends Component {
     const { i18n } = this.props
 
     return (
-      <button
-        type="button"
-        className="uppy-u-reset uppy-c-btn"
+      <label
+        role="tooltip"
         aria-label={i18n('aspectRatioSquare')}
         data-microtip-position="top"
-        onClick={() => this.cropper.setAspectRatio(1)}
       >
-        <svg aria-hidden="true" className="uppy-c-icon" width="24" height="24" viewBox="0 0 24 24">
-          <path d="M0 0h24v24H0z" fill="none" />
-          <path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z" />
-        </svg>
-      </button>
+        <button
+          type="button"
+          className="uppy-u-reset uppy-c-btn"
+          onClick={() => this.cropper.setAspectRatio(1)}
+        >
+          <svg aria-hidden="true" className="uppy-c-icon" width="24" height="24" viewBox="0 0 24 24">
+            <path d="M0 0h24v24H0z" fill="none" />
+            <path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z" />
+          </svg>
+        </button>
+      </label>
     )
   }
 
@@ -216,18 +273,22 @@ export default class Editor extends Component {
     const { i18n } = this.props
 
     return (
-      <button
-        type="button"
-        className="uppy-u-reset uppy-c-btn"
+      <label
+        role="tooltip"
         aria-label={i18n('aspectRatioLandscape')}
         data-microtip-position="top"
-        onClick={() => this.cropper.setAspectRatio(16 / 9)}
       >
-        <svg aria-hidden="true" className="uppy-c-icon" width="24" height="24" viewBox="0 0 24 24">
-          <path d="M 19,4.9999992 V 17.000001 H 4.9999998 V 6.9999992 H 19 m 0,-2 H 4.9999998 c -1.0999999,0 -1.9999999,0.9000001 -1.9999999,2 V 17.000001 c 0,1.1 0.9,2 1.9999999,2 H 19 c 1.1,0 2,-0.9 2,-2 V 6.9999992 c 0,-1.0999999 -0.9,-2 -2,-2 z" />
-          <path fill="none" d="M0 0h24v24H0z" />
-        </svg>
-      </button>
+        <button
+          type="button"
+          className="uppy-u-reset uppy-c-btn"
+          onClick={() => this.cropper.setAspectRatio(16 / 9)}
+        >
+          <svg aria-hidden="true" className="uppy-c-icon" width="24" height="24" viewBox="0 0 24 24">
+            <path d="M 19,4.9999992 V 17.000001 H 4.9999998 V 6.9999992 H 19 m 0,-2 H 4.9999998 c -1.0999999,0 -1.9999999,0.9000001 -1.9999999,2 V 17.000001 c 0,1.1 0.9,2 1.9999999,2 H 19 c 1.1,0 2,-0.9 2,-2 V 6.9999992 c 0,-1.0999999 -0.9,-2 -2,-2 z" />
+            <path fill="none" d="M0 0h24v24H0z" />
+          </svg>
+        </button>
+      </label>
     )
   }
 
@@ -235,18 +296,22 @@ export default class Editor extends Component {
     const { i18n } = this.props
 
     return (
-      <button
-        type="button"
-        className="uppy-u-reset uppy-c-btn"
+      <label
+        role="tooltip"
         aria-label={i18n('aspectRatioPortrait')}
         data-microtip-position="top"
-        onClick={() => this.cropper.setAspectRatio(9 / 16)}
       >
-        <svg aria-hidden="true" className="uppy-c-icon" width="24" height="24" viewBox="0 0 24 24">
-          <path d="M 19.000001,19 H 6.999999 V 5 h 10.000002 v 14 m 2,0 V 5 c 0,-1.0999999 -0.9,-1.9999999 -2,-1.9999999 H 6.999999 c -1.1,0 -2,0.9 -2,1.9999999 v 14 c 0,1.1 0.9,2 2,2 h 10.000002 c 1.1,0 2,-0.9 2,-2 z" />
-          <path d="M0 0h24v24H0z" fill="none" />
-        </svg>
-      </button>
+        <button
+          type="button"
+          className="uppy-u-reset uppy-c-btn"
+          onClick={() => this.cropper.setAspectRatio(9 / 16)}
+        >
+          <svg aria-hidden="true" className="uppy-c-icon" width="24" height="24" viewBox="0 0 24 24">
+            <path d="M 19.000001,19 H 6.999999 V 5 h 10.000002 v 14 m 2,0 V 5 c 0,-1.0999999 -0.9,-1.9999999 -2,-1.9999999 H 6.999999 c -1.1,0 -2,0.9 -2,1.9999999 v 14 c 0,1.1 0.9,2 2,2 h 10.000002 c 1.1,0 2,-0.9 2,-2 z" />
+            <path d="M0 0h24v24H0z" fill="none" />
+          </svg>
+        </button>
+      </label>
     )
   }
 

+ 5 - 0
packages/@uppy/image-editor/src/ImageEditor.jsx

@@ -21,6 +21,8 @@ export default class ImageEditor extends UIPlugin {
       background: false,
       autoCropArea: 1,
       responsive: true,
+      minCropBoxWidth: 70,
+      minCropBoxHeight: 70,
       croppedCanvasOptions: {},
     }
 
@@ -36,6 +38,9 @@ export default class ImageEditor extends UIPlugin {
       cropWidescreenVertical: true,
     }
 
+    // Why is the default quality smaller than 1?
+    // Because `quality: 1` increases the image size by orders of magnitude - 0.8 seems to be the sweet spot.
+    // (see https://github.com/fengyuanchen/cropperjs/issues/538#issuecomment-1776279427)
     const defaultOptions = {
       quality: 0.8,
     }

+ 1 - 7
packages/@uppy/image-editor/src/cropper.scss

@@ -56,7 +56,7 @@
 
 .cropper-modal {
   background-color: #000;
-  opacity: 1;
+  opacity: 0.5;
 }
 
 .cropper-view-box {
@@ -66,11 +66,6 @@
   overflow: hidden;
   outline: 1px solid #39f;
   outline-color: rgba(51, 153, 255, 0.75);
-  background: repeating-conic-gradient(
-      rgba(0, 0, 0, 0.5) 0% 25%,
-      rgba(255, 255, 255, 0.5) 0% 50%
-    )
-    50% / 16px 16px;
 }
 
 .cropper-dashed {
@@ -144,7 +139,6 @@
   top: 0;
   left: 0;
   background-color: #fff;
-  opacity: 0;
 }
 
 .cropper-line {

+ 34 - 40
packages/@uppy/image-editor/src/inputrange.scss

@@ -1,35 +1,35 @@
 // Styling Cross-Browser Compatible Range Inputs with Sass
 // Github: https://github.com/darlanrod/input-range-sass
 // Author: Darlan Rod https://github.com/darlanrod
-// Version 1.5.2
+// Version 1.5.3
 // MIT License
 
 @use 'sass:math';
 
-$track-color: rgba(#fff, 0.2);
-$thumb-color: #fff;
+$track-color: #eceff1 !default;
+$thumb-color: #607d8b !default;
 
-$thumb-radius: 9px;
-$thumb-height: 18px;
-$thumb-width: 18px;
-$thumb-shadow-size: 0;
-$thumb-shadow-blur: 4px;
-$thumb-shadow-color: rgba(0, 0, 0, 0.2);
-$thumb-border-width: 0;
-$thumb-border-color: transparent;
+$thumb-radius: 12px !default;
+$thumb-height: 24px !default;
+$thumb-width: 24px !default;
+$thumb-shadow-size: 4px !default;
+$thumb-shadow-blur: 4px !default;
+$thumb-shadow-color: rgba(0, 0, 0, 0.2) !default;
+$thumb-border-width: 2px !default;
+$thumb-border-color: #eceff1 !default;
 
-$track-width: 100%;
-$track-height: 4px;
-$track-shadow-size: 0;
-$track-shadow-blur: 0;
-$track-shadow-color: transparent;
-$track-border-width: 0;
-$track-border-color: transparent;
+$track-width: 100% !default;
+$track-height: 8px !default;
+$track-shadow-size: 1px !default;
+$track-shadow-blur: 1px !default;
+$track-shadow-color: rgba(0, 0, 0, 0.2) !default;
+$track-border-width: 2px !default;
+$track-border-color: #cfd8dc !default;
 
-$track-radius: 5px;
-$contrast: 5%;
+$track-radius: 5px !default;
+$contrast: 5% !default;
 
-$ie-bottom-track-color: darken($track-color, $contrast);
+$ie-bottom-track-color: darken($track-color, $contrast) !default;
 
 @mixin shadow($shadow-size, $shadow-blur, $shadow-color) {
   box-shadow:
@@ -38,29 +38,30 @@ $ie-bottom-track-color: darken($track-color, $contrast);
 }
 
 @mixin track {
-  width: $track-width;
-  height: $track-height;
   cursor: default;
+  height: $track-height;
   transition: all 0.2s ease;
+  width: $track-width;
 }
 
 @mixin thumb {
   @include shadow($thumb-shadow-size, $thumb-shadow-blur, $thumb-shadow-color);
-
-  box-sizing: border-box;
-  width: $thumb-width;
-  height: $thumb-height;
   background: $thumb-color;
   border: $thumb-border-width solid $thumb-border-color;
   border-radius: $thumb-radius;
+  box-sizing: border-box;
   cursor: default;
+  height: $thumb-height;
+  width: $thumb-width;
 }
 
+// This is the only Uppy-specific change to this file - we do not want these styles to spread to all range inputs on the page
+// [type='range'] {
 .uppy-ImageCropper-range {
-  width: $track-width;
-  margin: math.div($thumb-height, 2) 0;
-  background: transparent;
   -webkit-appearance: none;
+  background: transparent;
+  margin: math.div($thumb-height, 2) 0;
+  width: $track-width;
 
   &::-moz-focus-outer {
     border: 0;
@@ -89,7 +90,6 @@ $ie-bottom-track-color: darken($track-color, $contrast);
       $track-shadow-blur,
       $track-shadow-color
     );
-
     background: $track-color;
     border: $track-border-width solid $track-border-color;
     border-radius: $track-radius;
@@ -97,14 +97,13 @@ $ie-bottom-track-color: darken($track-color, $contrast);
 
   &::-webkit-slider-thumb {
     @include thumb;
-
+    -webkit-appearance: none;
     margin-top: (
       math.div((-$track-border-width * 2 + $track-height), 2) - math.div(
           $thumb-height,
           2
         )
     );
-    -webkit-appearance: none;
   }
 
   &::-moz-range-track {
@@ -114,11 +113,10 @@ $ie-bottom-track-color: darken($track-color, $contrast);
       $track-shadow-color
     );
     @include track;
-
-    height: $track-height;
     background: $track-color;
     border: $track-border-width solid $track-border-color;
     border-radius: $track-radius;
+    height: math.div($track-height, 2);
   }
 
   &::-moz-range-thumb {
@@ -127,11 +125,10 @@ $ie-bottom-track-color: darken($track-color, $contrast);
 
   &::-ms-track {
     @include track;
-
-    color: transparent;
     background: transparent;
     border-color: transparent;
     border-width: math.div($thumb-height, 2) 0;
+    color: transparent;
   }
 
   &::-ms-fill-lower {
@@ -140,7 +137,6 @@ $ie-bottom-track-color: darken($track-color, $contrast);
       $track-shadow-blur,
       $track-shadow-color
     );
-
     background: $ie-bottom-track-color;
     border: $track-border-width solid $track-border-color;
     border-radius: ($track-radius * 2);
@@ -152,7 +148,6 @@ $ie-bottom-track-color: darken($track-color, $contrast);
       $track-shadow-blur,
       $track-shadow-color
     );
-
     background: $track-color;
     border: $track-border-width solid $track-border-color;
     border-radius: ($track-radius * 2);
@@ -160,7 +155,6 @@ $ie-bottom-track-color: darken($track-color, $contrast);
 
   &::-ms-thumb {
     @include thumb;
-
     margin-top: math.div($track-height, 4);
   }
 

+ 3 - 3
packages/@uppy/image-editor/src/locale.js

@@ -1,10 +1,10 @@
 export default {
   strings: {
-    revert: 'Revert',
-    rotate: 'Rotate',
+    revert: 'Reset',
+    rotate: 'Rotate 90°',
     zoomIn: 'Zoom in',
     zoomOut: 'Zoom out',
-    flipHorizontal: 'Flip horizontal',
+    flipHorizontal: 'Flip horizontally',
     aspectRatioSquare: 'Crop square',
     aspectRatioLandscape: 'Crop landscape (16:9)',
     aspectRatioPortrait: 'Crop portrait (9:16)',

+ 79 - 11
packages/@uppy/image-editor/src/style.scss

@@ -1,6 +1,24 @@
+@use 'inputrange' with (
+  $track-color: rgba(#fff, 0.2),
+  $thumb-color: #fff,
+
+  $thumb-radius: 9px,
+  $thumb-height: 16px,
+  $thumb-width: 16px,
+  $thumb-shadow-size: 0,
+  $thumb-border-width: 0,
+  $thumb-border-color: transparent,
+
+  // If you change this `$track-height`, also change `::-moz-range-track` way below
+  $track-height: 4px,
+  $track-shadow-size: 0,
+  $track-shadow-blur: 0,
+  $track-shadow-color: transparent,
+  $track-border-width: 0,
+  $track-border-color: transparent
+);
 @import '@uppy/core/src/_utils.scss';
 @import '@uppy/core/src/_variables.scss';
-@import './inputrange.scss';
 @import './cropper.scss';
 
 .uppy-ImageCropper {
@@ -34,6 +52,7 @@
   color: $white;
   background-color: rgba($black, 0.6);
   transform: translateX(-50%);
+  border-radius: 5px;
 
   .uppy-size--md & {
     padding-top: 0;
@@ -43,6 +62,7 @@
 .uppy-ImageCropper-controls button {
   width: 35px;
   height: 35px;
+  border-radius: 5px;
 
   svg {
     padding: 3px;
@@ -58,35 +78,30 @@
   }
 
   &:hover {
-    background-color: rgba($blue, 0.8);
+    background-color: rgba(white, 0.5);
   }
 
   &:focus {
-    background-color: rgba($blue, 0.8);
+    background-color: rgba(white, 0.5);
     outline: none;
   }
 }
 
-.uppy-ImageCropper-rangeWrapper {
+.uppy-Dashboard:not(.uppy-size--md) .uppy-ImageCropper-rangeWrapper {
   position: absolute !important;
   top: 0;
   right: 10px;
   left: 10px;
   height: 38px;
-
-  .uppy-size--md & {
-    position: static !important;
-    height: auto;
-  }
 }
 
 .uppy-size--md .uppy-ImageCropper-range {
   width: 180px;
-  margin-right: 15px;
+  margin-right: 5px;
   margin-left: 5px;
 }
 
-// Cropper overrides
+// Overrides: cropper.scss
 
 .uppy-ImageCropper .cropper-point {
   width: 8px;
@@ -95,4 +110,57 @@
 
 .uppy-ImageCropper .cropper-view-box {
   outline: 2px solid #39f;
+
+  // This adds a checkered background to transparent pngs
+  // (see https://github.com/transloadit/uppy/pull/4194).
+  // Cropper handles checkered bgs via the `background: true` option and a `.cropper-bg` css class, however those will span through the entire cropper background, and we only want the checkered background for transparent pngs.
+  background: repeating-conic-gradient(
+      rgba(189, 189, 189, 0.2) 0% 25%,
+      white 0% 50%
+    )
+    50% / 16px 16px;
+  [data-uppy-theme='dark'] & {
+    background: repeating-conic-gradient(rgb(43, 42, 42) 0% 25%, black 0% 50%)
+      50%/16px 16px;
+  }
+}
+
+.uppy-ImageCropper .cropper-modal {
+  opacity: 0.9;
+  background-color: white;
+
+  [data-uppy-theme='dark'] & {
+    opacity: 0.7;
+    background-color: black;
+  }
+}
+
+.uppy-ImageCropper .cropper-face {
+  opacity: 0;
+}
+
+// Overrides: inputrange.scss
+
+.uppy-ImageCropper-range {
+  &::-moz-range-track {
+    // We have to adjust this manually every time we change `$track-height`
+    // in the beginning of the file.
+    height: 4px;
+  }
+
+  &:focus,
+  &:hover {
+    &::-webkit-slider-runnable-track {
+      background: rgba(white, 0.5);
+    }
+    &::-ms-fill-lower {
+      background: rgba(white, 0.5);
+    }
+    &::-ms-fill-upper {
+      background: rgba(white, 0.5);
+    }
+    &::-moz-range-track {
+      background: rgba(white, 0.5);
+    }
+  }
 }

+ 1 - 4
packages/@uppy/image-editor/src/utils/getScaleFactorThatRemovesDarkCorners.js

@@ -2,12 +2,9 @@ function toRadians (angle) {
   return angle * (Math.PI / 180)
 }
 
-function getScaleFactorThatRemovesDarkCorners (cropboxData, granularAngle) {
+function getScaleFactorThatRemovesDarkCorners (w, h, granularAngle) {
   const α = Math.abs(toRadians(granularAngle))
 
-  const w = cropboxData.width
-  const h = cropboxData.height
-
   const scaleFactor = Math.max(
     (Math.sin(α) * w + Math.cos(α) * h) / h,
     (Math.sin(α) * h + Math.cos(α) * w) / w,

+ 37 - 0
packages/@uppy/image-editor/src/utils/limitCropboxMovementOnMove.js

@@ -0,0 +1,37 @@
+function limitCropboxMovementOnMove (canvas, cropbox, prevCropbox) {
+  // For the left boundary
+  if (cropbox.left < canvas.left) {
+    return ({
+      left: canvas.left,
+      width: prevCropbox.width,
+    })
+  }
+
+  // For the top boundary
+  if (cropbox.top < canvas.top) {
+    return ({
+      top: canvas.top,
+      height: prevCropbox.height,
+    })
+  }
+
+  // For the right boundary
+  if (cropbox.left + cropbox.width > canvas.left + canvas.width) {
+    return ({
+      left: (canvas.left + canvas.width) - prevCropbox.width,
+      width: prevCropbox.width,
+    })
+  }
+
+  // For the bottom boundary
+  if (cropbox.top + cropbox.height > canvas.top + canvas.height) {
+    return ({
+      top: (canvas.top + canvas.height) - prevCropbox.height,
+      height: prevCropbox.height,
+    })
+  }
+
+  return null
+}
+
+export default limitCropboxMovementOnMove

+ 37 - 0
packages/@uppy/image-editor/src/utils/limitCropboxMovementOnResize.js

@@ -0,0 +1,37 @@
+function limitCropboxMovementOnResize (canvas, cropboxData, prevCropbox) {
+  // For the left boundary
+  if (cropboxData.left < canvas.left) {
+    return ({
+      left: canvas.left,
+      width: (prevCropbox.left + prevCropbox.width) - canvas.left,
+    })
+  }
+
+  // For the top boundary
+  if (cropboxData.top < canvas.top) {
+    return ({
+      top: canvas.top,
+      height: (prevCropbox.top + prevCropbox.height) - canvas.top,
+    })
+  }
+
+  // For the right boundary
+  if (cropboxData.left + cropboxData.width > canvas.left + canvas.width) {
+    return ({
+      left: prevCropbox.left,
+      width: (canvas.left + canvas.width) - prevCropbox.left,
+    })
+  }
+
+  // For the bottom boundary
+  if (cropboxData.top + cropboxData.height > canvas.top + canvas.height) {
+    return ({
+      top: prevCropbox.top,
+      height: (canvas.top + canvas.height) - prevCropbox.top,
+    })
+  }
+
+  return null
+}
+
+export default limitCropboxMovementOnResize