diff --git a/src/routes/components/Image.svelte b/src/routes/components/Image.svelte index b9b35ebb..bfe6f71d 100644 --- a/src/routes/components/Image.svelte +++ b/src/routes/components/Image.svelte @@ -50,15 +50,59 @@ return variant_entries.join(', '); } - // Apply scale to image - let image_style = $derived(` - object-position: ${node.focal_point_x * 100}% ${node.focal_point_y * 100}%; - transform: scale(${node.scale}); - transform-origin: ${node.focal_point_x * 100}% ${node.focal_point_y * 100}%; - object-fit: ${node.object_fit}; - `); + let is_scale_down = $derived(node.object_fit === 'scale-down'); + + // For scale-down: cap the img element to its DPR-corrected natural size + // and position it within the parent using the focal point. + // SVGs don't need DPR correction since they scale cleanly. + let dpr = $derived(typeof window !== 'undefined' ? window.devicePixelRatio : 2); + + let css_natural_width = $derived( + is_scale_down && node.width && !is_svg + ? Math.round(node.width / dpr) + : undefined + ); + let css_natural_height = $derived( + is_scale_down && node.height && !is_svg + ? Math.round(node.height / dpr) + : undefined + ); + // Apply scale to image + let image_style = $derived.by(() => { + if (is_scale_down) { + // Position the img absolutely within the overflow-hidden parent. + // left/top set by focal point %, translate pulls it back so the + // focal point itself lands at that position — same trick as + // background-position semantics. + let parts = [ + `position: absolute`, + `left: ${node.focal_point_x * 100}%`, + `top: ${node.focal_point_y * 100}%`, + `translate: ${-node.focal_point_x * 100}% ${-node.focal_point_y * 100}%`, + `transform: scale(${node.scale})`, + `transform-origin: ${node.focal_point_x * 100}% ${node.focal_point_y * 100}%`, + `object-fit: contain` + ]; + + if (css_natural_width !== undefined) { + parts.push(`max-width: ${css_natural_width}px`); + } + if (css_natural_height !== undefined) { + parts.push(`max-height: ${css_natural_height}px`); + } + + return parts.join('; ') + ';'; + } + + return [ + `object-position: ${node.focal_point_x * 100}% ${node.focal_point_y * 100}%`, + `transform: scale(${node.scale})`, + `transform-origin: ${node.focal_point_x * 100}% ${node.focal_point_y * 100}%`, + `object-fit: ${node.object_fit}` + ].join('; ') + ';'; + }); {#if display_src} @@ -70,6 +114,7 @@ alt={node.alt} width={node.width} height={node.height} + class:scale-down-mode={is_scale_down} style={image_style} /> {/if} @@ -81,5 +126,8 @@ transform-origin: center center; } - + img.scale-down-mode { + width: auto; + height: auto; + } \ No newline at end of file diff --git a/src/routes/components/MediaControls.svelte b/src/routes/components/MediaControls.svelte index fb9545d8..637d29f8 100644 --- a/src/routes/components/MediaControls.svelte +++ b/src/routes/components/MediaControls.svelte @@ -71,12 +71,14 @@ e.preventDefault(); } + const OBJECT_FIT_MODES = ['cover', 'contain', 'scale-down']; + function handle_double_click(e) { + const current_index = OBJECT_FIT_MODES.indexOf(media_node.object_fit); + const next_index = (current_index + 1) % OBJECT_FIT_MODES.length; const tr = svedit.session.tr; tr.set([...path, 'scale'], MIN_SCALE); - // tr.set([...path, 'focal_point_x'], 0.5); - // tr.set([...path, 'focal_point_y'], 0.5); - tr.set([...path, 'object_fit'], media_node.object_fit === 'cover' ? 'contain' : 'cover'); + tr.set([...path, 'object_fit'], OBJECT_FIT_MODES[next_index]); svedit.session.apply(tr, { batch: true }); } @@ -192,4 +194,4 @@ background: var(--svedit-editing-stroke); transform: translate(-50%, -50%); } - \ No newline at end of file +