Browse Source

add more granular image rotation control (#2838)

* add more granular image rotation control

Fixes: https://github.com/transloadit/uppy/issues/2636

* fixup! add more granular image rotation control

* fixup! add more granular image rotation control

* fixup! add more granular image rotation control

* fixup! add more granular image rotation control

* add input-range-scss dependency

* Modify the rotation slider

- Better visual styles for the slider
- Position a toggle in the center of the slider
- Make it work better on moblile
- 90° rotation: rotate counterclockwise
- 90° rotation: update icon

* Use black background for the canvas when rotating (instead of gray)

* Use smaller range for granular rotation input

* Optimize event listeners

* Fix styles and state unsyncronisation

* Update package-lock.json

Co-authored-by: Alexander Zaytsev <nqst@users.noreply.github.com>
Co-authored-by: Artur Paikin <artur@arturpaikin.com>
Antoine du Hamel 3 years ago
parent
commit
0c510ee799

File diff suppressed because it is too large
+ 128 - 206
package-lock.json


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

@@ -2,11 +2,26 @@ const Cropper = require('cropperjs')
 const { h, Component } = require('preact')
 
 module.exports = class Editor extends Component {
+  constructor (props) {
+    super(props)
+    this.state = { rotationAngle: 0, rotationDelta: 0 }
+  }
+
   componentDidMount () {
     this.cropper = new Cropper(
       this.imgElement,
       this.props.opts.cropperOptions
     )
+    if (this.props.opts.actions.granularRotate) {
+      this.imgElement.addEventListener('crop', (ev) => {
+        const rotationAngle = ev.detail.rotate
+        this.setState({
+          rotationAngle,
+          // 405 == 360 + 45
+          rotationDelta: ((rotationAngle + 405) % 90) - 45,
+        })
+      })
+    }
   }
 
   componentWillUnmount () {
@@ -48,19 +63,53 @@ module.exports = class Editor extends Component {
       <button
         type="button"
         className="uppy-u-reset uppy-c-btn"
-        onClick={() => this.cropper.rotate(90)}
+        onClick={() => this.cropper.rotate(-90)}
         aria-label={this.props.i18n('rotate')}
         data-microtip-position="top"
         role="tooltip"
       >
         <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="M7.47 21.49C4.2 19.93 1.86 16.76 1.5 13H0c.51 6.16 5.66 11 11.95 11 .23 0 .44-.02.66-.03L8.8 20.15l-1.33 1.34zM12.05 0c-.23 0-.44.02-.66.04l3.81 3.81 1.33-1.33C19.8 4.07 22.14 7.24 22.5 11H24c-.51-6.16-5.66-11-11.95-11zM16 14h2V8c0-1.11-.9-2-2-2h-6v2h6v6zm-8 2V4H6v2H4v2h2v8c0 1.1.89 2 2 2h8v2h2v-2h2v-2H8z" />
+          <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>
     )
   }
 
+  granularRotateOnChange = (ev) => {
+    const { rotationAngle, rotationDelta } = this.state
+    const pendingRotationDelta = Number(ev.target.value) - rotationDelta
+    cancelAnimationFrame(this.granularRotateOnInputNextFrame)
+    if (pendingRotationDelta !== 0) {
+      const pendingRotationAngle = rotationAngle + pendingRotationDelta
+      this.granularRotateOnInputNextFrame = requestAnimationFrame(() => {
+        this.cropper.rotateTo(pendingRotationAngle)
+      })
+    }
+  }
+
+  renderGranularRotate () {
+    return (
+      <label
+        data-microtip-position="top"
+        role="tooltip"
+        aria-label={`${this.state.rotationAngle}º`}
+        className="uppy-ImageCropper-rangeWrapper uppy-u-reset"
+      >
+        <input
+          className="uppy-ImageCropper-range uppy-u-reset"
+          type="range"
+          onInput={this.granularRotateOnChange}
+          onChange={this.granularRotateOnChange}
+          value={this.state.rotationDelta}
+          min="-45"
+          max="44"
+          aria-label={this.props.i18n('rotate')}
+        />
+      </label>
+    )
+  }
+
   renderFlip () {
     return (
       <button
@@ -172,7 +221,7 @@ module.exports = class Editor extends Component {
 
   render () {
     const { currentImage, i18n, opts } = this.props
-    const actions = opts.actions
+    const { actions } = opts
     // eslint-disable-next-line compat/compat
     const imageURL = URL.createObjectURL(currentImage.data)
 
@@ -204,6 +253,7 @@ module.exports = class Editor extends Component {
 
           {actions.revert && this.renderRevert()}
           {actions.rotate && this.renderRotate()}
+          {actions.granularRotate && this.renderGranularRotate()}
           {actions.flip && this.renderFlip()}
           {actions.zoomIn && this.renderZoomIn()}
           {actions.zoomOut && this.renderZoomOut()}

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

@@ -56,7 +56,7 @@
 
 .cropper-modal {
   background-color: #000;
-  opacity: 0.5;
+  opacity: 1;
 }
 
 .cropper-view-box {
@@ -139,6 +139,7 @@
   background-color: #fff;
   left: 0;
   top: 0;
+  opacity: 0;
 }
 
 .cropper-line {

+ 1 - 0
packages/@uppy/image-editor/src/index.js

@@ -36,6 +36,7 @@ module.exports = class ImageEditor extends Plugin {
     const defaultActions = {
       revert: true,
       rotate: true,
+      granularRotate: true,
       flip: true,
       zoomIn: true,
       zoomOut: true,

+ 144 - 0
packages/@uppy/image-editor/src/inputrange.scss

@@ -0,0 +1,144 @@
+// 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
+// MIT License
+
+$track-color: rgba(#fff, 0.2);
+$thumb-color: #fff;
+
+$thumb-radius: 9px;
+$thumb-height: 18px;
+$thumb-width: 18px;
+$thumb-shadow-size: 0;
+$thumb-shadow-blur: 4px;
+$thumb-shadow-color: rgba(0, 0, 0, .2);
+$thumb-border-width: 0;
+$thumb-border-color: transparent;
+
+$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-radius: 5px;
+$contrast: 5%;
+
+$ie-bottom-track-color: darken($track-color, $contrast);
+
+@mixin shadow($shadow-size, $shadow-blur, $shadow-color) {
+  box-shadow: $shadow-size $shadow-size $shadow-blur $shadow-color, 0 0 $shadow-size lighten($shadow-color, 5%);
+}
+
+@mixin track {
+  cursor: default;
+  height: $track-height;
+  transition: all .2s ease;
+  width: $track-width;
+}
+
+@mixin thumb {
+  @include shadow($thumb-shadow-size, $thumb-shadow-blur, $thumb-shadow-color);
+  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;
+}
+
+[type='range'] {
+  -webkit-appearance: none;
+  background: transparent;
+  margin: $thumb-height / 2 0;
+  width: $track-width;
+
+  &::-moz-focus-outer {
+    border: 0;
+  }
+
+  &:focus {
+    outline: 0;
+
+    &::-webkit-slider-runnable-track {
+      background: lighten($track-color, $contrast);
+    }
+
+    &::-ms-fill-lower {
+      background: $track-color;
+    }
+
+    &::-ms-fill-upper {
+      background: lighten($track-color, $contrast);
+    }
+  }
+
+  &::-webkit-slider-runnable-track {
+    @include track;
+    @include shadow($track-shadow-size, $track-shadow-blur, $track-shadow-color);
+    background: $track-color;
+    border: $track-border-width solid $track-border-color;
+    border-radius: $track-radius;
+  }
+
+  &::-webkit-slider-thumb {
+    @include thumb;
+    -webkit-appearance: none;
+    margin-top: ((-$track-border-width * 2 + $track-height) / 2 - $thumb-height / 2);
+  }
+
+  &::-moz-range-track {
+    @include shadow($track-shadow-size, $track-shadow-blur, $track-shadow-color);
+    @include track;
+    background: $track-color;
+    border: $track-border-width solid $track-border-color;
+    border-radius: $track-radius;
+    height: $track-height;
+  }
+
+  &::-moz-range-thumb {
+    @include thumb;
+  }
+
+  &::-ms-track {
+    @include track;
+    background: transparent;
+    border-color: transparent;
+    border-width: ($thumb-height / 2) 0;
+    color: transparent;
+  }
+
+  &::-ms-fill-lower {
+    @include shadow($track-shadow-size, $track-shadow-blur, $track-shadow-color);
+    background: $ie-bottom-track-color;
+    border: $track-border-width solid $track-border-color;
+    border-radius: ($track-radius * 2);
+  }
+
+  &::-ms-fill-upper {
+    @include shadow($track-shadow-size, $track-shadow-blur, $track-shadow-color);
+    background: $track-color;
+    border: $track-border-width solid $track-border-color;
+    border-radius: ($track-radius * 2);
+  }
+
+  &::-ms-thumb {
+    @include thumb;
+    margin-top: $track-height / 4;
+  }
+
+  &:disabled {
+    &::-webkit-slider-thumb,
+    &::-moz-range-thumb,
+    &::-ms-thumb,
+    &::-webkit-slider-runnable-track,
+    &::-ms-fill-lower,
+    &::-ms-fill-upper {
+      cursor: not-allowed;
+    }
+  }
+}

+ 28 - 1
packages/@uppy/image-editor/src/style.scss

@@ -1,5 +1,6 @@
 @import '@uppy/core/src/_utils.scss';
 @import '@uppy/core/src/_variables.scss';
+@import './inputrange.scss';
 @import './cropper.scss';
 
 .uppy-ImageCropper {
@@ -28,6 +29,11 @@
   display: flex;
   justify-content: center;
   align-items: center;
+  padding-top: 38px;
+
+  @media screen and (min-width: 768px) {
+    padding-top: 0;
+  }
 }
 
 .uppy-ImageCropper-controls button {
@@ -57,6 +63,27 @@
   }
 }
 
+.uppy-ImageCropper-rangeWrapper {
+  position: absolute !important;
+  height: 38px;
+  top: 0;
+  right: 10px;
+  left: 10px;
+
+  @media screen and (min-width: 768px) {
+    position: static !important;
+    height: auto;
+  }
+}
+
+.uppy-ImageCropper-range {
+  @media screen and (min-width: 768px) {
+    margin-right: 15px;
+    margin-left: 5px;
+    width: 180px;
+  }
+}
+
 // Cropper overrides
 
 .uppy-ImageCropper .cropper-point {
@@ -66,4 +93,4 @@
 
 .uppy-ImageCropper .cropper-view-box {
   outline: 2px solid #39f;
-}
+}

+ 1 - 0
packages/@uppy/image-editor/types/index.d.ts

@@ -5,6 +5,7 @@ declare module ImageEditor {
   type Actions = {
     revert: boolean
     rotate: boolean
+    granularRotate: boolean
     flip: boolean
     zoomIn: boolean
     zoomOut: boolean

+ 1 - 0
website/src/docs/image-editor.md

@@ -68,6 +68,7 @@ uppy.use(ImageEditor, {
   actions: {
     revert: true,
     rotate: true,
+    granularRotate: true,
     flip: true,
     zoomIn: true,
     zoomOut: true,

Some files were not shown because too many files changed in this diff