diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index 71d5802c9..7f6eb7d03 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -396,6 +396,10 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { return false; } + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { + return false; + } + const target = document.querySelector('[data-canvas-chat-input-anchor="true"]') as HTMLElement | null; if (!target) return false; @@ -408,72 +412,195 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { return false; } - const clone = document.createElement("img"); - clone.src = data.src; - clone.alt = ""; - clone.style.position = "fixed"; - clone.style.left = `${sourceRect.left}px`; - clone.style.top = `${sourceRect.top}px`; - clone.style.width = `${sourceRect.width}px`; - clone.style.height = `${sourceRect.height}px`; - clone.style.objectFit = "cover"; - clone.style.borderRadius = "14px"; - clone.style.pointerEvents = "none"; - clone.style.zIndex = "99999"; - clone.style.boxShadow = "0 14px 30px rgba(15, 23, 42, 0.28)"; - clone.style.transformOrigin = "center center"; - clone.style.transition = - "transform 560ms cubic-bezier(0.22, 1, 0.36, 1), opacity 560ms ease, filter 560ms ease"; - clone.style.willChange = "transform, opacity, filter"; - document.body.appendChild(clone); + const waitForAnimation = (animation: Animation | null, fallbackMs: number) => + new Promise((resolve) => { + if (!animation) { + resolve(); + return; + } + let finished = false; + const done = () => { + if (finished) return; + finished = true; + resolve(); + }; + const timeout = window.setTimeout(done, fallbackMs); + animation.addEventListener( + "finish", + () => { + window.clearTimeout(timeout); + done(); + }, + { once: true } + ); + animation.addEventListener( + "cancel", + () => { + window.clearTimeout(timeout); + done(); + }, + { once: true } + ); + }); const sourceCenterX = sourceRect.left + sourceRect.width / 2; const sourceCenterY = sourceRect.top + sourceRect.height / 2; + + const labelText = displayTitle.length > 12 ? `${displayTitle.slice(0, 10)}...` : displayTitle; + const tagWidth = Math.min(172, Math.max(112, 58 + labelText.length * 8)); + const tagHeight = 34; + const tagLeft = Math.min( + Math.max(sourceCenterX - tagWidth / 2, 8), + window.innerWidth - tagWidth - 8 + ); + const defaultTagTop = sourceRect.top - 46; + const tagTop = + defaultTagTop < 10 + ? Math.min(sourceRect.bottom + 10, window.innerHeight - tagHeight - 10) + : defaultTagTop; + + const flightTag = document.createElement("div"); + flightTag.style.position = "fixed"; + flightTag.style.left = `${sourceRect.left}px`; + flightTag.style.top = `${sourceRect.top}px`; + flightTag.style.width = `${sourceRect.width}px`; + flightTag.style.height = `${sourceRect.height}px`; + flightTag.style.borderRadius = "14px"; + flightTag.style.overflow = "hidden"; + flightTag.style.pointerEvents = "none"; + flightTag.style.zIndex = "99999"; + flightTag.style.background = "#ffffff"; + flightTag.style.border = "1px solid rgba(148, 163, 184, 0.24)"; + flightTag.style.boxShadow = "0 14px 30px rgba(15, 23, 42, 0.28)"; + flightTag.style.transformOrigin = "center center"; + flightTag.style.willChange = "transform, opacity, filter"; + + const previewImage = document.createElement("img"); + previewImage.src = data.src; + previewImage.alt = ""; + previewImage.style.width = "100%"; + previewImage.style.height = "100%"; + previewImage.style.objectFit = "cover"; + flightTag.appendChild(previewImage); + document.body.appendChild(flightTag); + + const compressDx = tagLeft + tagWidth / 2 - sourceCenterX; + const compressDy = tagTop + tagHeight / 2 - sourceCenterY; + const compressScaleX = tagWidth / sourceRect.width; + const compressScaleY = tagHeight / sourceRect.height; + + const compressAnimation = flightTag.animate( + [ + { + transform: "translate(0px, 0px) scale(1, 1)", + borderRadius: "14px", + boxShadow: "0 14px 30px rgba(15, 23, 42, 0.28)", + }, + { + offset: 0.72, + transform: `translate(${compressDx * 1.03}px, ${compressDy * 1.03}px) scale(${compressScaleX * 1.05}, ${compressScaleY * 1.08})`, + borderRadius: "999px", + boxShadow: "0 10px 24px rgba(59, 130, 246, 0.24)", + }, + { + transform: `translate(${compressDx}px, ${compressDy}px) scale(${compressScaleX}, ${compressScaleY})`, + borderRadius: "999px", + boxShadow: "0 8px 20px rgba(59, 130, 246, 0.2)", + }, + ], + { + duration: 320, + easing: "cubic-bezier(0.2, 0.9, 0.2, 1.2)", + fill: "forwards", + } + ); + await waitForAnimation(compressAnimation, 420); + + flightTag.style.left = `${tagLeft}px`; + flightTag.style.top = `${tagTop}px`; + flightTag.style.width = `${tagWidth}px`; + flightTag.style.height = `${tagHeight}px`; + flightTag.style.transform = "none"; + flightTag.style.borderRadius = "999px"; + flightTag.style.boxShadow = "0 8px 20px rgba(59, 130, 246, 0.2)"; + flightTag.style.display = "flex"; + flightTag.style.alignItems = "center"; + flightTag.style.gap = "6px"; + flightTag.style.padding = "4px 10px 4px 4px"; + flightTag.style.background = "rgba(255,255,255,0.96)"; + flightTag.style.backdropFilter = "blur(6px)"; + flightTag.innerHTML = ""; + + const chipImage = document.createElement("img"); + chipImage.src = data.src; + chipImage.alt = ""; + chipImage.style.width = "24px"; + chipImage.style.height = "24px"; + chipImage.style.objectFit = "cover"; + chipImage.style.borderRadius = "999px"; + chipImage.style.border = "1px solid rgba(148, 163, 184, 0.35)"; + + const chipLabel = document.createElement("span"); + chipLabel.textContent = labelText || "图片"; + chipLabel.style.fontSize = "12px"; + chipLabel.style.fontWeight = "500"; + chipLabel.style.color = "rgb(51, 65, 85)"; + chipLabel.style.whiteSpace = "nowrap"; + chipLabel.style.overflow = "hidden"; + chipLabel.style.textOverflow = "ellipsis"; + + flightTag.appendChild(chipImage); + flightTag.appendChild(chipLabel); + + const tagCenterX = tagLeft + tagWidth / 2; + const tagCenterY = tagTop + tagHeight / 2; const targetCenterX = targetRect.left + targetRect.width / 2; - const targetCenterY = targetRect.top + targetRect.height / 2; - const scale = Math.max(0.12, Math.min(0.24, Math.min(targetRect.width / sourceRect.width, targetRect.height / sourceRect.height))); + const targetCenterY = targetRect.top + targetRect.height * 0.55; + const flyDx = targetCenterX - tagCenterX; + const flyDy = targetCenterY - tagCenterY; + + const flyAnimation = flightTag.animate( + [ + { transform: "translate(0px, 0px) scale(1)", opacity: 1, filter: "blur(0px)" }, + { + offset: 0.55, + transform: `translate(${flyDx * 0.64}px, ${flyDy * 0.62 - 46}px) scale(0.94)`, + opacity: 0.98, + filter: "blur(0px)", + }, + { + offset: 0.84, + transform: `translate(${flyDx + 9}px, ${flyDy - 7}px) scale(0.74)`, + opacity: 0.82, + filter: "blur(0.4px)", + }, + { + transform: `translate(${flyDx}px, ${flyDy}px) scale(0.64)`, + opacity: 0.15, + filter: "blur(1.2px)", + }, + ], + { + duration: 760, + easing: "cubic-bezier(0.22, 1, 0.36, 1)", + fill: "forwards", + } + ); target.animate( [ { boxShadow: "0 0 0 0 rgba(59,130,246,0)", transform: "scale(1)" }, - { boxShadow: "0 0 0 6px rgba(59,130,246,0.14)", transform: "scale(1.015)" }, + { offset: 0.58, boxShadow: "0 0 0 8px rgba(59,130,246,0.14)", transform: "scale(1.02)" }, { boxShadow: "0 0 0 0 rgba(59,130,246,0)", transform: "scale(1)" }, ], - { duration: 520, easing: "cubic-bezier(0.22, 1, 0.36, 1)" } + { duration: 720, easing: "cubic-bezier(0.2, 0.9, 0.2, 1.2)" } ); - await new Promise((resolve) => { - let done = false; - const finish = () => { - if (done) return; - done = true; - clone.remove(); - resolve(); - }; - - const timeout = window.setTimeout(finish, 700); - clone.addEventListener( - "transitionend", - () => { - window.clearTimeout(timeout); - finish(); - }, - { once: true } - ); - - requestAnimationFrame(() => { - requestAnimationFrame(() => { - const dx = targetCenterX - sourceCenterX; - const dy = targetCenterY - sourceCenterY; - clone.style.transform = `translate(${dx}px, ${dy}px) scale(${scale})`; - clone.style.opacity = "0.14"; - clone.style.filter = "blur(1.2px)"; - }); - }); - }); + await waitForAnimation(flyAnimation, 860); + flightTag.remove(); return true; - }, [data.src]); + }, [data.src, displayTitle]); const handleAddToChat = useCallback(async () => { if (!data.src || isProcessing || isAddingToChat) return;