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' 
1554import  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
1771const   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- 
4682const   originalImg =  ref <File >(modelValue .value )
4783const   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