Skip to content

Commit

Permalink
feat(camera): improve user interact
Browse files Browse the repository at this point in the history
  • Loading branch information
adenvt committed Oct 19, 2022
1 parent cdd46c9 commit f907182
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 68 deletions.
6 changes: 3 additions & 3 deletions src/components/camera/Camera.vue
Original file line number Diff line number Diff line change
Expand Up @@ -307,10 +307,10 @@ export default defineComponent({
}
onMounted(async () => {
if (permission.isSupported)
if (permission.isSupported) {
await until(permission.state).not.toBeUndefined()
await turnOn()
await turnOn()
}
})
onBeforeUnmount(() => {
Expand Down
132 changes: 80 additions & 52 deletions src/components/cropper/Cropper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
:class="classNames">
<div
class="cropper__preview">
<canvas
ref="canvas"
class="cropper__canvas" />
<div
ref="parent"
class="cropper__image-container"
Expand All @@ -12,11 +15,17 @@
ref="target"
:src="preview"
alt="cropper-preview"
tabindex="0"
class="cropper__image"
:style="imgStyle"
:width="imgWidth"
:height="imgHeight"
@load="onImageLoaded">
crossorigin="anonymous"
@load="onImageLoaded"
@keydown.up.prevent="move(0, -1)"
@keydown.down.prevent="move(0, 1)"
@keydown.left.prevent="move(-1, 0)"
@keydown.right.prevent="move(1, 0)">
</div>
<div
v-if="!noCrop"
Expand Down Expand Up @@ -88,7 +97,6 @@ import {
import {
computed,
defineComponent,
nextTick,
onBeforeUnmount,
onMounted,
PropType,
Expand All @@ -112,6 +120,7 @@ import {
useRatioHeight,
useRatioWidth,
} from './utils/use-ratio'
import { createSpinner } from '../avatar/utils/create-image'
export default defineComponent({
components: {
Expand Down Expand Up @@ -148,11 +157,11 @@ export default defineComponent({
},
imgWidth: {
type : [String, Number],
default: 1024,
default: 512,
},
imgHeight: {
type : [String, Number],
default: 1024,
default: 512,
},
rounded: {
type : Boolean,
Expand All @@ -162,25 +171,33 @@ export default defineComponent({
type : Boolean,
default: false,
},
noAutoCrop: {
type : Boolean,
default: false,
},
},
models: {
prop : 'modelValue',
event: 'update:modelValue',
},
emits: ['update:modelValue'],
setup (props) {
emits: [
'update:modelValue',
'change',
'result',
],
setup (props, { emit }) {
const model = useVModel(props)
const x = ref(0)
const y = ref(0)
const angle = ref(0)
const scale = useClamp(1, 0.5, 2)
const preview = ref('')
const debug = ref('')
const preview = ref(createSpinner(512, 512))
const ratio = useRatio(props)
const width = useRatioWidth(props)
const height = useRatioHeight(props)
const rounded = toRef(props, 'rounded')
const src = toRef(props, 'src')
const canvas = templateRef<HTMLCanvasElement>('canvas')
const parent = templateRef<HTMLDivElement>('parent')
const target = templateRef<HTMLImageElement>('target')
Expand All @@ -194,7 +211,7 @@ export default defineComponent({
})
const imgStyle = computed<StyleValue>(() => {
return { transform: `scale(${scale.value}) rotate(${angle.value}deg) translate(${x.value}px, ${y.value}px)` }
return { transform: `rotate(${angle.value}deg) translate(${x.value}px, ${y.value}px) scale(${scale.value})` }
})
const maskStyle = computed<StyleValue>(() => {
Expand Down Expand Up @@ -230,11 +247,22 @@ export default defineComponent({
fit()
}
function onMouseWheel (event: WheelEvent) {
if (event.deltaY > 0)
zoomIn()
else
zoomOut()
function move (dx: number, dy: number) {
/**
* Translate top and left movement by any rotation's angle using formula:
* x' = x cos(θ) + y sin(θ)
* y' = −x sin(θ) + y cos(θ)
*
* See: https://math.stackexchange.com/questions/1350137/transformation-of-axes-by-rotation
*/
const COS0 = Math.cos(angle.value * Math.PI / 180)
const SIN0 = Math.sin(angle.value * Math.PI / 180)
const x1 = (dx * COS0 + dy * SIN0)
const y1 = (dy * COS0 - dx * SIN0)
x.value += x1
y.value += y1
}
function reset () {
Expand All @@ -244,13 +272,14 @@ export default defineComponent({
}
function crop () {
if (parent.value && target.value) {
if (parent.value && target.value && canvas.value) {
const pWidth = (parent.value.clientWidth * 2 / 3)
const w = width.value ?? pWidth
const h = height.value ?? (w / ratio.value)
const mScale = w / pWidth // mobile scale, responsive scale to fix crop ratio on mobile.
const result = cropImage({
canvas : canvas.value,
image : target.value,
width : w,
height : h,
Expand All @@ -261,50 +290,43 @@ export default defineComponent({
rounded: props.rounded,
})
debug.value = result
model.value = props.modelModifiers.base64
const value = props.modelModifiers.base64
? result
: fromBase64(result)
model.value = value
emit('change', value)
emit('result', value)
return result
}
}
function onMouseWheel (event: WheelEvent) {
if (target.value)
target.value.focus()
if (event.deltaY > 0)
zoomIn()
else
zoomOut()
}
function onImageLoaded () {
if (!props.noCrop)
if (!props.noCrop && !props.noAutoCrop)
crop()
}
usePinch(target, {
onpinch (event) {
angle.value += event.angle
scale.value *= event.scale
scale.value = event.scale
// angle.value += event.da
this.onmove(event)
move(event.dx, event.dy)
},
onmove (event) {
switch (angle.value) {
case -90:
case 270:
x.value += -event.dy
y.value += event.dx
break
case 90:
case -270:
x.value += event.dy
y.value += -event.dx
break
case 180:
case -180:
x.value += -event.dx
y.value += -event.dy
break
default:
x.value += event.dx
y.value += event.dy
break
}
move(event.dx, event.dy)
},
})
Expand All @@ -328,10 +350,8 @@ export default defineComponent({
scale,
angle,
], () => {
nextTick(() => {
if (!props.noCrop)
crop()
})
if (!props.noCrop && !props.noAutoCrop)
crop()
}, { debounce: 500 })
onMounted(() => {
Expand Down Expand Up @@ -360,6 +380,7 @@ export default defineComponent({
rotate,
zoomIn,
zoomOut,
move,
crop,
model,
}
Expand All @@ -371,18 +392,25 @@ export default defineComponent({
.cropper {
@apply bg-white w-full aspect-square;
&__canvas {
@apply hidden;
}
&__preview {
background-image: url("./assets/ps-neutral.png");
@apply flex w-full overflow-hidden h-auto relative aspect-square;
@apply flex w-full overflow-hidden h-auto relative aspect-square select-none;
}
&__mask {
@apply border border-white border-dashed shadow-mask absolute top-0 left-0 right-0 bottom-0 m-auto pointer-events-none touch-none max-w-[66.666667%];
@apply pointer-events-none touch-none select-none;
@apply border border-white border-dashed box-border shadow-mask absolute top-0 left-0 right-0 bottom-0 m-auto max-w-[66.666667%];
}
&__image {
@apply touch-none h-auto origin-center object-contain;
-webkit-touch-callout: none;
@apply touch-none h-auto origin-center object-contain select-none outline-none;
&-container {
@apply w-full h-full absolute top-0 left-0 bottom-0 right-0 flex items-center justify-center;
Expand Down
9 changes: 7 additions & 2 deletions src/components/cropper/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<script setup>
import Cropper from './Cropper.vue'
import img from '../camera/__mocks__/sample.jpeg'
import { ref } from 'vue-demi'

const result = ref()
</script>

# Cropper
Expand All @@ -10,6 +13,8 @@ import img from '../camera/__mocks__/sample.jpeg'

### Simple Usage

<preview>
<cropper :src="img" rounded />
<preview class="flex-col">
<cropper v-model.base64="result" :src="img" />
</preview>

<img v-if="result" :src="result" class="border" />
43 changes: 38 additions & 5 deletions src/components/cropper/utils/crop-image.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,48 @@
import { createCanvas } from '../../signature-draw/utils/canvas'

interface CropImage {
/**
* Canvas's element
*/
canvas?: HTMLCanvasElement,
/**
* Image's element to crop
*/
image: HTMLImageElement,
/**
* Crop width
*/
width: number,
/**
* Crop height
*/
height: number,
/**
* Zoom factor
*/
scale: number,
/**
* Rotate angle
*/
angle: number,
/**
* X offset
*/
x: number,
/**
* Y offset
*/
y: number,
/**
* Enable border-radius
*/
rounded: boolean,
}

/**
* Crop image using canvas
* @param options CropOption
*/
export function cropImage (options: CropImage): string {
const {
width,
Expand All @@ -23,22 +55,23 @@ export function cropImage (options: CropImage): string {
rounded,
} = options

const canvas = createCanvas(width, height)
const canvas = options.canvas ?? createCanvas(width, height)
const ctx = canvas.getContext('2d')

const imgW = image.width * scale
const imgH = image.height * scale

const offsetX = x * scale
const offsetY = y * scale
canvas.width = width
canvas.height = height

ctx.clearRect(0, 0, width, height)
ctx.translate(width / 2, height / 2)
ctx.rotate(angle * Math.PI / 180)
ctx.translate(width / -2, height / -2)
ctx.drawImage(
image,
(width - imgW) / 2 + offsetX,
(height - imgH) / 2 + offsetY,
(width - imgW) / 2 + x,
(height - imgH) / 2 + y,
imgW,
imgH,
)
Expand Down
Loading

0 comments on commit f907182

Please sign in to comment.