// konva-utils.js // attachStageResizer(root, stage, onResize?) export function attachStageResizer(root, stage, onResize) { const observer = new ResizeObserver(() => { if (!stage) return; stage.width(root.clientWidth); stage.height(root.clientHeight); stage.batchDraw(); if (typeof onResize === 'function') { onResize(); } }); observer.observe(root); return observer; } // fitImageToStage: вписать целиком (contain) export function fitImageToStage(konvaImage, stage) { const img = konvaImage.image(); const sw = stage.width(); const sh = stage.height(); const iw = img.naturalWidth; const ih = img.naturalHeight; const scale = Math.min(sw / iw, sh / ih); konvaImage.width(iw); konvaImage.height(ih); konvaImage.scale({ x: scale, y: scale }); konvaImage.x((sw - iw * scale) / 2); konvaImage.y((sh - ih * scale) / 2); } // coverImageToStage: заполнить (cover) export function coverImageToStage(konvaImage, stage) { const img = konvaImage.image(); const sw = stage.width(); const sh = stage.height(); const iw = img.naturalWidth; const ih = img.naturalHeight; const scale = Math.max(sw / iw, sh / ih); konvaImage.width(iw); konvaImage.height(ih); konvaImage.scale({ x: scale, y: scale }); konvaImage.x((sw - iw * scale) / 2); konvaImage.y((sh - ih * scale) / 2); } // возвращает { scale, x, y } для cover export function calcCoverTransform(konvaImage, stage) { const img = konvaImage.image(); const sw = stage.width(); const sh = stage.height(); const iw = img.naturalWidth; const ih = img.naturalHeight; const scale = Math.max(sw / iw, sh / ih); const targetWidth = iw * scale; const targetHeight = ih * scale; const x = (sw - targetWidth) / 2; const y = (sh - targetHeight) / 2; return { scale, x, y }; } // Обновить существующее изображение до cover (без анимации) export function updateImageToCover(konvaImage, stage) { const { scale, x, y } = calcCoverTransform(konvaImage, stage); konvaImage.width(konvaImage.image().naturalWidth); konvaImage.height(konvaImage.image().naturalHeight); konvaImage.scale({ x: scale, y: scale }); konvaImage.x(x); konvaImage.y(y); } // Добавить картинку: сначала показать в реальном размере, через delayMs анимированно перейти в cover. // Возвращает функцию cancel(), которую нужно вызывать при unmounted. export function addImageWithPreview(stage, layer, url, delayMs = 1200, opts = {}) { // opts: { crossOrigin, initialPosition: 'top-left'|'center', coverDuration } const crossOrigin = opts.crossOrigin ?? 'Anonymous'; const initialPosition = opts.initialPosition ?? 'top-left'; const coverDuration = opts.coverDuration ?? 0.45; let timeoutId = null; let activeTween = null; let konvaImage = null; // helper для очистки function cancel() { if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } if (activeTween) { // finish or destroy tween try { activeTween.finish(); } catch (e) { /* ignore */ } activeTween = null; } // не удаляем сам konvaImage — оставляем на слое, но можно удалить при желании } Konva.Image.fromURL(url, (imgNode) => { konvaImage = imgNode; const img = konvaImage.image(); const iw = img.naturalWidth; const ih = img.naturalHeight; // показать в реальном размере (scale = 1) konvaImage.width(iw); konvaImage.height(ih); konvaImage.scale({ x: 1, y: 1 }); if (initialPosition === 'center') { // центрируем реальный размер по сцене const sw = stage.width(); const sh = stage.height(); konvaImage.x((sw - iw) / 2); konvaImage.y((sh - ih) / 2); } else { konvaImage.x(0); konvaImage.y(0); } konvaImage.draggable(true); layer.add(konvaImage); layer.draw(); // через delayMs — плавно переводим в cover if (delayMs > 0) { timeoutId = setTimeout(() => { timeoutId = null; const { scale, x, y } = calcCoverTransform(konvaImage, stage); // анимируем: изменяем scaleX/scaleY и x/y activeTween = konvaImage.to({ duration: coverDuration, scaleX: scale, scaleY: scale, x, y, easing: Konva.Easings.EaseInOut, onFinish: () => { activeTween = null; } }); }, delayMs); } }, { crossOrigin }); return cancel; }