From 954a5d5554faa35ec8ba4b7b5df7635edbfb6bb8 Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Wed, 30 Apr 2025 05:12:02 -0600 Subject: [PATCH] Updated Image Viewer Node with High Performance Canvas and Zoomable Preview Window --- .../Image Processing/Node_Image_Viewer.jsx | 182 ++++++++++-------- 1 file changed, 98 insertions(+), 84 deletions(-) diff --git a/Data/Server/WebUI/src/nodes/Image Processing/Node_Image_Viewer.jsx b/Data/Server/WebUI/src/nodes/Image Processing/Node_Image_Viewer.jsx index 9798b62..41a6a5c 100644 --- a/Data/Server/WebUI/src/nodes/Image Processing/Node_Image_Viewer.jsx +++ b/Data/Server/WebUI/src/nodes/Image Processing/Node_Image_Viewer.jsx @@ -1,110 +1,125 @@ ////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: /Data/WebUI/src/nodes/Image Processing/Node_Image_Viewer.jsx -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useRef } from "react"; import { Handle, Position, useReactFlow } from "reactflow"; if (!window.BorealisValueBus) window.BorealisValueBus = {}; if (!window.BorealisUpdateRate) window.BorealisUpdateRate = 100; -const ImageViewerNode = ({ id, data }) => { +const ImageViewerNode = ({ id }) => { const { getEdges } = useReactFlow(); const [imageBase64, setImageBase64] = useState(""); - const [selectedType, setSelectedType] = useState("base64"); + const canvasRef = useRef(null); + const overlayDivRef = useRef(null); + const [isZoomed, setIsZoomed] = useState(false); - // Monitor upstream input and propagate to ValueBus + // Poll upstream for base64 image and propagate useEffect(() => { const interval = setInterval(() => { const edges = getEdges(); - const inputEdge = edges.find(e => e.target === id); - if (inputEdge) { - const sourceId = inputEdge.source; - const valueBus = window.BorealisValueBus || {}; - const value = valueBus[sourceId]; - if (typeof value === "string") { - setImageBase64(value); - window.BorealisValueBus[id] = value; - } + const inp = edges.find(e => e.target === id); + if (inp) { + const val = window.BorealisValueBus[inp.source] || ""; + setImageBase64(val); + window.BorealisValueBus[id] = val; } else { setImageBase64(""); window.BorealisValueBus[id] = ""; } - }, window.BorealisUpdateRate || 100); - + }, window.BorealisUpdateRate); return () => clearInterval(interval); - }, [id, getEdges]); + }, [getEdges, id]); - const handleDownload = async () => { - if (!imageBase64) return; - const blob = await (await fetch(`data:image/png;base64,${imageBase64}`)).blob(); - - if (window.showSaveFilePicker) { - try { - const fileHandle = await window.showSaveFilePicker({ - suggestedName: "image.png", - types: [{ - description: "PNG Image", - accept: { "image/png": [".png"] } - }] - }); - const writable = await fileHandle.createWritable(); - await writable.write(blob); - await writable.close(); - } catch (e) { - console.warn("Save cancelled:", e); - } + // Draw the image into canvas for high performance + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (imageBase64) { + const img = new Image(); + img.onload = () => { + canvas.width = img.width; + canvas.height = img.height; + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(img, 0, 0); + }; + img.src = "data:image/png;base64," + imageBase64; } else { - const a = document.createElement("a"); - a.href = URL.createObjectURL(blob); - a.download = "image.png"; - a.style.display = "none"; - document.body.appendChild(a); - a.click(); - URL.revokeObjectURL(a.href); - document.body.removeChild(a); + ctx.clearRect(0, 0, canvas.width, canvas.height); } + }, [imageBase64]); + + // Manage zoom overlay on image click + useEffect(() => { + if (!isZoomed || !imageBase64) return; + const div = document.createElement("div"); + overlayDivRef.current = div; + Object.assign(div.style, { + position: "fixed", + top: "0", + left: "0", + width: "100%", + height: "100%", + backgroundColor: "rgba(0,0,0,0.8)", + display: "flex", + alignItems: "center", + justifyContent: "center", + zIndex: "1000", + cursor: "zoom-out", + transition: "opacity 0.3s ease" + }); + const handleOverlayClick = () => setIsZoomed(false); + div.addEventListener("click", handleOverlayClick); + + const ovCanvas = document.createElement("canvas"); + const ctx = ovCanvas.getContext("2d"); + const img = new Image(); + img.onload = () => { + let w = img.width; + let h = img.height; + const maxW = window.innerWidth * 0.8; + const maxH = window.innerHeight * 0.8; + const scale = Math.min(1, maxW / w, maxH / h); + ovCanvas.width = w; + ovCanvas.height = h; + ctx.clearRect(0, 0, w, h); + ctx.drawImage(img, 0, 0); + ovCanvas.style.width = `${w * scale}px`; + ovCanvas.style.height = `${h * scale}px`; + ovCanvas.style.transition = "transform 0.3s ease"; + }; + img.src = "data:image/png;base64," + imageBase64; + div.appendChild(ovCanvas); + document.body.appendChild(div); + + // Cleanup when unzooming + return () => { + div.removeEventListener("click", handleOverlayClick); + if (overlayDivRef.current) { + document.body.removeChild(overlayDivRef.current); + overlayDivRef.current = null; + } + }; + }, [isZoomed, imageBase64]); + + const handleClick = () => { + if (imageBase64) setIsZoomed(z => !z); }; return (
Image Viewer
-
- - - +
{imageBase64 ? ( - <> - Live - - + ) : (
Waiting for image... @@ -120,13 +135,12 @@ export default { type: "Image_Viewer", label: "Image Viewer", description: ` -Displays base64 image and exports it +Displays base64 image via canvas for high performance - Accepts upstream base64 image -- Shows preview -- Provides "Export to PNG" button -- Outputs the same base64 to downstream +- Renders with canvas for speed +- Click to zoom/unzoom overlay with smooth transition `.trim(), - content: "Visual preview of base64 image with optional PNG export.", + content: "Visual preview of base64 image with zoom overlay.", component: ImageViewerNode };