Skip to content

Commit 7a88f0f

Browse files
committed
feat: cropperjs 2.x の API を用いて既存の実装を書き換える
1 parent 75a829f commit 7a88f0f

File tree

1 file changed

+163
-64
lines changed

1 file changed

+163
-64
lines changed
Lines changed: 163 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,72 @@
11
<template>
2-
<div
2+
<cropper-canvas
33
v-if="originalImgUrl"
4-
:class="$style.cropper"
4+
ref="cropperCanvasRef"
55
:data-is-rounded="$boolAttr(rounded)"
6+
:class="$style.cropper"
7+
background
8+
@actionend="apply"
69
>
7-
<img ref="imgEle" :src="originalImgUrl" />
8-
</div>
10+
<cropper-image
11+
ref="cropperImageRef"
12+
:src="originalImgUrl"
13+
rotatable
14+
scalable
15+
skewable
16+
translatable
17+
@transform="onCropperImageTransform"
18+
/>
19+
<cropper-shade />
20+
<cropper-handle action="move" plain />
21+
22+
<cropper-selection
23+
ref="cropperSelectionRef"
24+
aspect-ratio="1"
25+
initial-aspect-ratio="1"
26+
initial-coverage="1"
27+
:movable="cropBoxMovable"
28+
:resizable="cropBoxResizable"
29+
outlined
30+
@change="onCropperSelectionChange"
31+
>
32+
<cropper-grid role="grid" covered />
33+
<cropper-crosshair centered />
34+
35+
<cropper-handle
36+
:action="dragMode"
37+
theme-color="rgba(255, 255, 255, 0.35)"
38+
@dblclick="toggleActionOnDblclick"
39+
/>
40+
<cropper-handle action="n-resize" />
41+
<cropper-handle action="e-resize" />
42+
<cropper-handle action="s-resize" />
43+
<cropper-handle action="w-resize" />
44+
<cropper-handle action="ne-resize" />
45+
<cropper-handle action="nw-resize" />
46+
<cropper-handle action="se-resize" />
47+
<cropper-handle action="sw-resize" />
48+
</cropper-selection>
49+
</cropper-canvas>
950
</template>
1051

1152
<script lang="ts" setup>
12-
import Cropper from 'cropperjs'
13-
import 'cropperjs/dist/cropper.css'
14-
import { onUnmounted, ref, shallowRef, watchEffect } from 'vue'
53+
import { computed, onMounted, ref, useTemplateRef } from 'vue'
1554
import useObjectURL from '/@/composables/dom/useObjectURL'
55+
import CropperCanvas from '@cropper/element-canvas'
56+
import CropperImage from '@cropper/element-image'
57+
import CropperGrid from '@cropper/element-grid'
58+
import CropperCrosshair from '@cropper/element-crosshair'
59+
import CropperShade from '@cropper/element-shade'
60+
import CropperSelection, { type Selection } from '@cropper/element-selection'
61+
import CropperHandle from '@cropper/element-handle'
62+
63+
CropperCanvas.$define()
64+
CropperImage.$define()
65+
CropperGrid.$define()
66+
CropperCrosshair.$define()
67+
CropperShade.$define()
68+
CropperSelection.$define()
69+
CropperHandle.$define()
1670
1771
const modelValue = defineModel<File>({ required: true })
1872
@@ -25,81 +79,126 @@ withDefaults(
2579
}
2680
)
2781
28-
// スタンプ編集用の設定
29-
const cropperGifOptions = {
30-
viewMode: 3,
31-
aspectRatio: 1,
32-
autoCropArea: 1,
33-
dragMode: 'none',
34-
cropBoxMovable: false,
35-
cropBoxResizable: false,
36-
toggleDragModeOnDblclick: false
37-
} as const
38-
const cropperDefaultOptions = {
39-
viewMode: 3,
40-
aspectRatio: 1,
41-
autoCropArea: 1,
42-
autoCrop: true,
43-
dragMode: 'move' as const
44-
} as const
45-
4682
const originalImg = ref<File>(modelValue.value)
4783
const originalImgUrl = useObjectURL(originalImg)
4884
49-
let cropper: Cropper | undefined
50-
const imgEle = shallowRef<HTMLImageElement>()
51-
52-
const updateImgView = () => {
53-
modelValue.value = originalImg.value
54-
55-
if (!imgEle.value) return
56-
57-
const isGif = originalImg.value.type === 'image/gif'
58-
const options = isGif
59-
? cropperGifOptions
60-
: {
61-
...cropperDefaultOptions,
62-
cropend: () => {
63-
cropper?.getCroppedCanvas().toBlob((blob: Blob | null) => {
64-
if (!blob) return
65-
66-
modelValue.value = new File([blob], originalImg.value.name, {
67-
type: blob.type
68-
})
69-
}, originalImg.value.type)
70-
},
71-
ready: () => {
72-
cropper?.getCroppedCanvas().toBlob((blob: Blob | null) => {
73-
if (!blob) return
74-
75-
modelValue.value = new File([blob], originalImg.value.name, {
76-
type: blob.type
77-
})
78-
}, originalImg.value.type)
79-
}
80-
}
81-
82-
if (cropper) cropper.destroy()
83-
cropper = new Cropper(imgEle.value, options)
84-
cropper.replace(originalImgUrl.value ?? '')
85+
const cropperCanvas = useTemplateRef<CropperCanvas>('cropperCanvasRef')
86+
const cropperImage = useTemplateRef<CropperImage>('cropperImageRef')
87+
const cropperSelection = useTemplateRef<CropperSelection>('cropperSelectionRef')
88+
89+
const isGif = computed(() => originalImg.value.type === 'image/gif')
90+
91+
const cropBoxMovable = computed(() => !isGif.value)
92+
const cropBoxResizable = computed(() => !isGif.value)
93+
const dragMode = computed(() => (isGif.value ? 'none' : 'move'))
94+
95+
const toggleActionOnDblclick = (event: MouseEvent) => {
96+
if (isGif.value) return
97+
98+
const cropperHandle = event.target as CropperHandle
99+
cropperHandle.action = cropperHandle.action === 'move' ? 'select' : 'move'
100+
}
101+
const inSelection = (selection: Selection, maxSelection: Selection) => {
102+
return (
103+
selection.x >= maxSelection.x &&
104+
selection.y >= maxSelection.y &&
105+
selection.x + selection.width <= maxSelection.x + maxSelection.width &&
106+
selection.y + selection.height <= maxSelection.y + maxSelection.height
107+
)
108+
}
109+
110+
const onCropperImageTransform = (event: CustomEvent) => {
111+
if (!cropperCanvas.value || !cropperImage.value) {
112+
return
113+
}
114+
115+
const cropperCanvasRect = cropperCanvas.value.getBoundingClientRect()
116+
const cropperImageClone = cropperImage.value.cloneNode() as CropperImage
117+
cropperImageClone.style.transform = `matrix(${event.detail.matrix.join(', ')})`
118+
cropperImageClone.style.opacity = '0'
119+
cropperCanvas.value.appendChild(cropperImageClone)
120+
const cropperImageRect = cropperImageClone.getBoundingClientRect()
121+
cropperCanvas.value.removeChild(cropperImageClone)
122+
123+
if (
124+
cropperImageRect.top > cropperCanvasRect.top ||
125+
cropperImageRect.right < cropperCanvasRect.right ||
126+
cropperImageRect.bottom < cropperCanvasRect.bottom ||
127+
cropperImageRect.left > cropperCanvasRect.left
128+
) {
129+
event.preventDefault()
130+
}
131+
132+
const selection = cropperSelection.value as Selection
133+
const maxSelection: Selection = {
134+
x: cropperImageRect.left - cropperCanvasRect.left,
135+
y: cropperImageRect.top - cropperCanvasRect.top,
136+
width: cropperImageRect.width,
137+
height: cropperImageRect.height
138+
}
139+
140+
if (!inSelection(selection, maxSelection)) {
141+
event.preventDefault()
142+
}
143+
}
144+
145+
const onCropperSelectionChange = (event: CustomEvent) => {
146+
if (!cropperCanvas.value) {
147+
return
148+
}
149+
150+
const cropperCanvasRect = cropperCanvas.value.getBoundingClientRect()
151+
const selection = event.detail as Selection
152+
153+
if (!cropperImage.value) return
154+
155+
const cropperImageRect = cropperImage.value.getBoundingClientRect()
156+
const maxSelection: Selection = {
157+
x: cropperImageRect.left - cropperCanvasRect.left,
158+
y: cropperImageRect.top - cropperCanvasRect.top,
159+
width: cropperImageRect.width,
160+
height: cropperImageRect.height
161+
}
162+
163+
if (!inSelection(selection, maxSelection)) {
164+
event.preventDefault()
165+
}
85166
}
86167
87-
watchEffect(updateImgView)
168+
const apply = async () => {
169+
const canvas = await cropperSelection.value?.$toCanvas()
170+
if (!canvas) return
88171
89-
onUnmounted(() => {
90-
if (cropper) cropper.destroy()
172+
canvas.toBlob((blob: Blob | null) => {
173+
if (!blob) return
174+
175+
modelValue.value = new File([blob], originalImg.value.name, {
176+
type: blob.type
177+
})
178+
}, originalImg.value.type)
179+
}
180+
181+
onMounted(() => {
182+
cropperImage.value?.$ready(apply)
91183
})
92184
</script>
93185

94186
<style lang="scss" module>
95187
.cropper {
96188
width: 280px;
97189
height: 280px;
190+
98191
&[data-is-rounded] {
99192
:global(.cropper-view-box),
100193
:global(.cropper-face) {
101194
border-radius: 50%;
102195
}
103196
}
197+
198+
* {
199+
// _reset.scss で none になってるので戻す (何に?)
200+
// initial や revert ではダメで,revert-layer でないと cropper-shade が表示されないが,理由はわからない.
201+
outline: revert-layer;
202+
}
104203
}
105204
</style>

0 commit comments

Comments
 (0)