Files
watermark-wizard/src/components/HeroView.vue
Julian Freeman de0ed2bdc2 watermark pos
2026-01-19 00:27:47 -04:00

196 lines
6.0 KiB
Vue

<script setup lang="ts">
import { useGalleryStore } from "../stores/gallery";
import { convertFileSrc } from "@tauri-apps/api/core";
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue";
const store = useGalleryStore();
const isDragging = ref(false);
const dragStart = ref({ x: 0, y: 0 });
const imgRef = ref<HTMLImageElement | null>(null);
const parentRef = ref<HTMLElement | null>(null); // The black background container
// These dimensions will exactly match the rendered image size
const imageRect = ref({ width: 0, height: 0 });
let resizeObserver: ResizeObserver | null = null;
const calculateLayout = () => {
if (!imgRef.value || !parentRef.value || !store.selectedImage) return;
// Wait for image natural dimensions to be available
const natW = imgRef.value.naturalWidth;
const natH = imgRef.value.naturalHeight;
if (!natW || !natH) return; // Not loaded yet
const parentW = parentRef.value.clientWidth;
const parentH = parentRef.value.clientHeight;
// Calculate 'contain' fit manually
const scale = Math.min(
(parentW - 64) / natW, // 64px = 2rem padding * 2 sides (p-8)
(parentH - 64) / natH
);
// Prevent scaling up if image is smaller than screen?
// Usually hero view scales up to fit. Let's stick to contain logic (can scale up).
const finalW = Math.floor(natW * scale);
const finalH = Math.floor(natH * scale);
imageRect.value = { width: finalW, height: finalH };
};
onMounted(() => {
// Observe the parent container (window size changes)
if (parentRef.value) {
resizeObserver = new ResizeObserver(() => {
calculateLayout();
});
resizeObserver.observe(parentRef.value);
}
window.addEventListener('resize', calculateLayout);
});
onUnmounted(() => {
if (resizeObserver) resizeObserver.disconnect();
window.removeEventListener('resize', calculateLayout);
});
// Re-calculate when image changes
watch(() => store.selectedImage, () => {
// Reset size until loaded to avoid jump
// imageRect.value = { width: 0, height: 0 };
nextTick(() => calculateLayout());
});
// Use either manual position (if override is true) or ZCA suggestion
const position = computed(() => {
if (store.watermarkSettings.manual_override) {
return store.watermarkSettings.manual_position;
}
return store.selectedImage?.zcaSuggestion ? { x: store.selectedImage.zcaSuggestion.x, y: store.selectedImage.zcaSuggestion.y } : { x: 0.5, y: 0.99 };
});
const onMouseDown = (e: MouseEvent) => {
e.preventDefault();
isDragging.value = true;
dragStart.value = { x: e.clientX, y: e.clientY };
};
const onMouseMove = (e: MouseEvent) => {
// Need exact dimensions for drag calculation
if (!isDragging.value || !store.selectedImage || imageRect.value.width === 0) return;
const rect = imageRect.value; // Use our calculated rect, not bounding client rect (though they should match)
const deltaX = (e.clientX - dragStart.value.x) / rect.width;
const deltaY = (e.clientY - dragStart.value.y) / rect.height;
// Update manual position
let newX = position.value.x + deltaX;
let newY = position.value.y + deltaY;
// Clamp logic (0-1) - This is now strictly inside the image content area
const padding = 0.005;
newX = Math.max(padding, Math.min(1 - padding, newX));
newY = Math.max(padding, Math.min(1 - padding, newY));
// Set store to manual mode immediately
store.updateWatermarkSettings({
manual_override: true,
manual_position: { x: newX, y: newY }
});
dragStart.value = { x: e.clientX, y: e.clientY };
};
const onMouseUp = () => {
isDragging.value = false;
};
const onMouseLeave = () => {
isDragging.value = false;
};
</script>
<template>
<div
ref="parentRef"
class="absolute inset-0 flex items-center justify-center bg-black overflow-hidden"
@mousemove="onMouseMove"
@mouseup="onMouseUp"
@mouseleave="onMouseLeave"
>
<!--
Dynamic Wrapper:
Dimensions strictly equal to the rendered image size.
This serves as the coordinate system for the watermark.
-->
<div
v-if="store.selectedImage"
class="relative shadow-2xl"
:style="{
width: imageRect.width + 'px',
height: imageRect.height + 'px'
}"
>
<img
ref="imgRef"
:src="convertFileSrc(store.selectedImage.path)"
class="block w-full h-full select-none pointer-events-none"
alt="Hero Image"
loading="eager"
decoding="sync"
fetchpriority="high"
@load="calculateLayout"
@error="(e) => console.error('Hero Image Load Error:', e)"
/>
<!-- Text Watermark Overlay -->
<div
v-if="store.watermarkSettings.text"
class="absolute cursor-move select-none whitespace-nowrap font-sans font-medium"
:style="{
left: (position.x * 100) + '%',
top: (position.y * 100) + '%',
transform: 'translate(-50%, -50%)',
opacity: store.watermarkSettings.opacity,
color: store.watermarkSettings.color,
/* Scale based on HEIGHT of the IMAGE */
fontSize: (imageRect.height * store.watermarkSettings.scale) + 'px',
height: '0px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}"
@mousedown="onMouseDown"
>
<span>
{{ store.watermarkSettings.text }}
</span>
<!-- Selection Ring when dragging -->
<div v-if="isDragging" class="absolute -inset-2 border border-blue-500 rounded-sm"></div>
</div>
</div>
<div v-else class="text-gray-500 flex flex-col items-center">
<p>No image selected</p>
</div>
</div>
</template>
<style scoped>
.image-container {
/* Removed container-type: size to prevent layout collapse */
}
span {
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000;
}
</style>