11<script setup>
22import { ref , computed , onMounted , onBeforeUnmount , getCurrentInstance , defineComponent , h } from ' vue' ;
33import { toast } from ' @statamic/cms/api' ;
4- import { Stack , Button , Input , Textarea , Field , Heading , Subheading , Description } from ' @statamic/cms/ui' ;
4+ import { Stack , Button , Input , Textarea , Field , Heading , Subheading , Description , Slider } from ' @statamic/cms/ui' ;
55import FairuAssetActions from ' ./FairuAssetActions.vue' ;
66import { fairuGetFile , fairuUpdateFile } from ' ../utils/fetches' ;
77
@@ -63,6 +63,14 @@ const caption = ref('');
6363const description = ref (' ' );
6464const focalX = ref (50 );
6565const focalY = ref (50 );
66+ const zoom = ref (1 );
67+
68+ const ZOOM_MIN = 1 ;
69+ const ZOOM_MAX = 4 ;
70+ const ZOOM_STEP = 0.1 ;
71+ const ZOOM_STEP_FINE = 0.01 ;
72+ const fineZoom = ref (false );
73+ const zoomStep = computed (() => (fineZoom .value ? ZOOM_STEP_FINE : ZOOM_STEP ));
6674
6775const pickerEl = ref (null );
6876const imageEl = ref (null );
@@ -95,15 +103,26 @@ const imageUrl = computed(() => {
95103});
96104
97105const focalCss = computed (() => ` ${ focalX .value } % ${ focalY .value } %` );
106+ const formattedZoom = computed (() => {
107+ const decimals = fineZoom .value ? 2 : 1 ;
108+ return ` ${ zoom .value .toFixed (decimals)} ×` ;
109+ });
110+ const previewImageStyle = computed (() => ({
111+ objectPosition: focalCss .value ,
112+ transform: ` scale(${ zoom .value } )` ,
113+ transformOrigin: focalCss .value ,
114+ }));
98115
99116function parseFocalPoint (str ) {
100- if (! str || typeof str !== ' string' ) return { x: 50 , y: 50 };
117+ if (! str || typeof str !== ' string' ) return { x: 50 , y: 50 , z : 1 };
101118 const parts = str .split (' -' );
102119 const x = parseFloat (parts[0 ]);
103120 const y = parseFloat (parts[1 ]);
121+ const z = parseFloat (parts[2 ]);
104122 return {
105123 x: Number .isFinite (x) ? clamp (x, 0 , 100 ) : 50 ,
106124 y: Number .isFinite (y) ? clamp (y, 0 , 100 ) : 50 ,
125+ z: Number .isFinite (z) ? clamp (z, ZOOM_MIN , ZOOM_MAX ) : 1 ,
107126 };
108127}
109128
@@ -142,11 +161,15 @@ function handlePointerUp() {
142161function resetFocal () {
143162 focalX .value = 50 ;
144163 focalY .value = 50 ;
164+ zoom .value = 1 ;
145165}
146166
147167function focalToString () {
148- if (focalX .value === 50 && focalY .value === 50 ) return null ;
149- return ` ${ Math .round (focalX .value * 10 ) / 10 } -${ Math .round (focalY .value * 10 ) / 10 } -1` ;
168+ const x = Math .round (focalX .value * 10 ) / 10 ;
169+ const y = Math .round (focalY .value * 10 ) / 10 ;
170+ const z = Math .round (zoom .value * 100 ) / 100 ;
171+ if (x === 50 && y === 50 && z === 1 ) return null ;
172+ return ` ${ x} -${ y} -${ z} ` ;
150173}
151174
152175async function load () {
@@ -161,6 +184,7 @@ async function load() {
161184 const fp = parseFocalPoint (file? .focal_point );
162185 focalX .value = fp .x ;
163186 focalY .value = fp .y ;
187+ zoom .value = fp .z ;
164188 } catch (err) {
165189 console .error (err);
166190 toast .error (__ (' fairu::fieldtype.editor.load_error' ));
@@ -198,12 +222,26 @@ function handleImageLoad(evt) {
198222 }
199223}
200224
225+ function handleShiftKey (evt ) {
226+ if (evt .key === ' Shift' ) fineZoom .value = evt .type === ' keydown' ;
227+ }
228+
229+ function handleWindowBlur () {
230+ fineZoom .value = false ;
231+ }
232+
201233onMounted (() => {
202234 load ();
235+ window .addEventListener (' keydown' , handleShiftKey);
236+ window .addEventListener (' keyup' , handleShiftKey);
237+ window .addEventListener (' blur' , handleWindowBlur);
203238});
204239
205240onBeforeUnmount (() => {
206241 window .removeEventListener (' pointermove' , handlePointerMove);
242+ window .removeEventListener (' keydown' , handleShiftKey);
243+ window .removeEventListener (' keyup' , handleShiftKey);
244+ window .removeEventListener (' blur' , handleWindowBlur);
207245});
208246< / script>
209247
@@ -363,10 +401,34 @@ onBeforeUnmount(() => {
363401 < / dt>
364402 < dd class = " font-mono" > {{ formattedCoordinates }}< / dd>
365403 < / div>
404+ < div>
405+ < dt class = " uppercase tracking-wider text-gray-500 dark:text-gray-400" >
406+ {{ __ (' fairu::fieldtype.editor.info_zoom' ) }}
407+ < / dt>
408+ < dd class = " font-mono" > {{ formattedZoom }}< / dd>
409+ < / div>
366410 < Description class = " pt-1" > {{ __ (' fairu::fieldtype.editor.focal_point_hint' ) }}< / Description>
367411 < / dl>
368412 < / div>
369413
414+ <!-- Zoom -->
415+ < div>
416+ < div class = " flex items-center justify-between gap-3 mb-1" >
417+ < Subheading> {{ __ (' fairu::fieldtype.editor.zoom_title' ) }}< / Subheading>
418+ < span
419+ class = " text-xs font-mono"
420+ : class = " fineZoom ? 'text-blue-600 dark:text-blue-400' : 'text-gray-600 dark:text-gray-300'" >
421+ {{ formattedZoom }}< span v- if = " fineZoom" class = " ml-1 opacity-70" > ·fine< / span>
422+ < / span>
423+ < / div>
424+ < Slider
425+ v- model= " zoom"
426+ : min= " ZOOM_MIN"
427+ : max= " ZOOM_MAX"
428+ : step= " zoomStep" / >
429+ < Description class = " mt-1" > {{ __ (' fairu::fieldtype.editor.zoom_hint' ) }}< / Description>
430+ < / div>
431+
370432 <!-- Previews -->
371433 < div>
372434 < Subheading class = " mb-2" > {{ __ (' fairu::fieldtype.editor.preview' ) }}< / Subheading>
@@ -376,23 +438,23 @@ onBeforeUnmount(() => {
376438 v- if = " imageUrl"
377439 : src= " imageUrl"
378440 class = " size-full object-cover"
379- : style= " { objectPosition: focalCss } "
441+ : style= " previewImageStyle "
380442 draggable= " false" / >
381443 < / div>
382444 < div class = " aspect-square bg-gray-100 dark:bg-gray-800 rounded overflow-hidden" >
383445 < img
384446 v- if = " imageUrl"
385447 : src= " imageUrl"
386448 class = " size-full object-cover"
387- : style= " { objectPosition: focalCss } "
449+ : style= " previewImageStyle "
388450 draggable= " false" / >
389451 < / div>
390452 < div class = " aspect-[3/4] bg-gray-100 dark:bg-gray-800 rounded overflow-hidden" >
391453 < img
392454 v- if = " imageUrl"
393455 : src= " imageUrl"
394456 class = " size-full object-cover"
395- : style= " { objectPosition: focalCss } "
457+ : style= " previewImageStyle "
396458 draggable= " false" / >
397459 < / div>
398460 < / div>
0 commit comments