diff --git a/@shared/konva-utils.js b/@shared/konva-utils.js
index a9fabf0..8e2ce8f 100644
--- a/@shared/konva-utils.js
+++ b/@shared/konva-utils.js
@@ -1,48 +1,153 @@
-// konva-utils.js
-export function attachStageResizer(root, stage) {
- const observer = new ResizeObserver(() => {
- if (!stage) return;
-
- stage.width(root.clientWidth);
- stage.height(root.clientHeight);
- stage.batchDraw();
- });
-
- observer.observe(root);
- return observer;
-}
-
-// масштабирует картинку по stage
-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);
-}
-
-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);
-}
+// 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;
+}
diff --git a/README.md b/README.md
index 3e49ff7..31ace03 100644
--- a/README.md
+++ b/README.md
@@ -4,4 +4,5 @@ nue2 + konva
## Особенности
- вынес часть кода из компонента в отдельный файл.
-- 23.12.2025 - картинка загружается, пока бех масштабироваания
+- 23.12.2025 - картинка загружается, пока без масштабироваания
+- 24.12.2025 - теперь с масштабированием (fit/cover)
diff --git a/konva-editor.html b/konva-editor.html
index bd21599..9e57906 100644
--- a/konva-editor.html
+++ b/konva-editor.html
@@ -1,17 +1,19 @@