import React, { useState, useEffect, useRef } from 'react'; import { NodeData } from '../components/GraphicsNodeEditor/GraphicsContext'; import { FileUpload, Slider, Text, Stack } from '..'; import { saturationNodeType } from './EffectSaturation'; import { vignetteMaskNodeType } from './EffectMask'; // --- Helper function to work with ImageData --- const createImageData = (width: number, height: number): ImageData => { const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); return ctx!.createImageData(width, height); }; // --- Custom Components for Node Bodies --- const LoadImageComponent: React.FC<{ data: NodeData; onUpdateData: (d: any) => void; }> = ({ onUpdateData }) => { const handleFileSelect = (file: File | null) => { if (!file) { onUpdateData({ imageData: null }); return; } const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); if (ctx) { ctx.drawImage(img, 0, 0); const imageData = ctx.getImageData(0, 0, img.width, img.height); onUpdateData({ imageData }); } }; img.src = e.target?.result as string; }; reader.readAsDataURL(file); }; return (
); }; const DisplayImageComponent: React.FC<{ inputs: Record }> = ({ inputs }) => { const canvasRef = useRef(null); const imageData = inputs.value as ImageData | undefined; useEffect(() => { const canvas = canvasRef.current; if (canvas && imageData) { canvas.width = imageData.width; canvas.height = imageData.height; const ctx = canvas.getContext('2d'); ctx?.putImageData(imageData, 0, 0); } }, [imageData]); return (
{imageData ? ( ) : ( Connect an image source )}
); }; const BrightnessContrastComponent: React.FC<{ data: NodeData; onUpdateData: (d: any) => void; }> = ({ data, onUpdateData }) => { const brightness = data.data?.brightness ?? 0; const contrast = data.data?.contrast ?? 0; return (
Brightness ({brightness}) onUpdateData({ brightness: v })} /> Contrast ({contrast}) onUpdateData({ contrast: v })} />
); }; // --- Node Type Definitions --- export const loadImageNodeType: Omit = { label: 'Load Image', inputs: [], outputs: [{ id: 'image', label: 'Image', type: 'image', color: '#9333ea' }], component: LoadImageComponent, process: (inputs, data) => ({ image: data?.imageData ?? null }), data: { imageData: null }, }; export const displayImageNodeType: Omit = { label: 'Display Image', inputs: [{ id: 'value', label: 'Image', type: 'image' }], outputs: [], component: DisplayImageComponent, }; export const grayscaleNodeType: Omit = { label: 'Grayscale', inputs: [{ id: 'image', label: 'Image', type: 'image' }], outputs: [{ id: 'image', label: 'Image', type: 'image', color: '#9333ea' }], process: (inputs) => { const imageData = inputs.image as ImageData; if (!imageData) return { image: null }; const newImageData = createImageData(imageData.width, imageData.height); const data = imageData.data; const newData = newImageData.data; for (let i = 0; i < data.length; i += 4) { const avg = (data[i] + data[i + 1] + data[i + 2]) / 3; newData[i] = avg; newData[i + 1] = avg; newData[i + 2] = avg; newData[i + 3] = data[i + 3]; // Alpha } return { image: newImageData }; }, }; export const invertNodeType: Omit = { label: 'Invert Colors', inputs: [{ id: 'image', label: 'Image', type: 'image' }], outputs: [{ id: 'image', label: 'Image', type: 'image', color: '#9333ea' }], process: (inputs) => { const imageData = inputs.image as ImageData; if (!imageData) return { image: null }; const newImageData = createImageData(imageData.width, imageData.height); const data = imageData.data; const newData = newImageData.data; for (let i = 0; i < data.length; i += 4) { newData[i] = 255 - data[i]; newData[i + 1] = 255 - data[i + 1]; newData[i + 2] = 255 - data[i + 2]; newData[i + 3] = data[i + 3]; // Alpha } return { image: newImageData }; }, }; export const sepiaNodeType: Omit = { label: 'Sepia', inputs: [{ id: 'image', label: 'Image', type: 'image' }], outputs: [{ id: 'image', label: 'Image', type: 'image', color: '#9333ea' }], process: (inputs) => { const imageData = inputs.image as ImageData; if (!imageData) return { image: null }; const newImageData = createImageData(imageData.width, imageData.height); const data = imageData.data; const newData = newImageData.data; for (let i = 0; i < data.length; i += 4) { const r = data[i], g = data[i+1], b = data[i+2]; newData[i] = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189); newData[i + 1] = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168); newData[i + 2] = Math.min(255, r * 0.272 + g * 0.534 + b * 0.131); newData[i + 3] = data[i + 3]; } return { image: newImageData }; }, }; export const brightnessContrastNodeType: Omit = { label: 'Brightness / Contrast', inputs: [{ id: 'image', label: 'Image', type: 'image' }], outputs: [{ id: 'image', label: 'Image', type: 'image', color: '#9333ea' }], component: BrightnessContrastComponent, data: { brightness: 0, contrast: 0 }, process: (inputs, data) => { const imageData = inputs.image as ImageData; const { brightness = 0, contrast = 0 } = data || {}; if (!imageData) return { image: null }; const newImageData = createImageData(imageData.width, imageData.height); const d = imageData.data; const newD = newImageData.data; const factor = (259 * (contrast + 255)) / (255 * (259 - contrast)); for (let i = 0; i < d.length; i += 4) { let r = d[i] + brightness; let g = d[i+1] + brightness; let b = d[i+2] + brightness; r = factor * (r - 128) + 128; g = factor * (g - 128) + 128; b = factor * (b - 128) + 128; newD[i] = Math.max(0, Math.min(255, r)); newD[i+1] = Math.max(0, Math.min(255, g)); newD[i+2] = Math.max(0, Math.min(255, b)); newD[i+3] = d[i+3]; } return { image: newImageData }; }, }; export const creatableImageNodeTypes = { 'Load Image': loadImageNodeType, 'Display Image': displayImageNodeType, 'Grayscale': grayscaleNodeType, 'Invert Colors': invertNodeType, 'Sepia': sepiaNodeType, 'Brightness / Contrast': brightnessContrastNodeType, 'Saturation': saturationNodeType, 'Vignette Mask': vignetteMaskNodeType, };