import React, { useState, useRef, useEffect, useCallback } from 'react'; import { Link } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { ArrowLeftIcon, PaletteIcon, PlusIcon, TrashIcon, ArrowsClockwiseIcon, TextAaIcon, SelectionIcon, InfoIcon, DownloadSimpleIcon, ArrowsOutCardinalIcon, } from '@phosphor-icons/react'; import Seo from '../../components/Seo'; import { useToast } from '../../hooks/useToast'; const DEFAULT_COLORS = [ '#ef4444', '#10b981', '#3b82f6', '#f59e0b', '#8b5cf6', '#ec4899', ]; const BlendLabPage = () => { const appName = 'Blend Lab'; const { addToast } = useToast(); const canvasRef = useRef(null); const [blobs, setBlobs] = useState([ { id: 1, color: '#ef4444', x: 20, y: 20, size: 40 }, { id: 2, color: '#10b981', x: 70, y: 30, size: 50 }, { id: 3, color: '#3b82f6', x: 40, y: 70, size: 45 }, ]); const [blurAmount, setBlurAmount] = useState(60); const [noiseOpacity, setNoiseOpacity] = useState(0.15); // Text Layer 1 const [topText, setTopText] = useState('DESIGN'); const [topFontSize, setTopFontSize] = useState(12); const [topFontColor, setTopFontColor] = useState('#FFFFFF'); const [topFontWeight, setTopFontWeight] = useState(900); const [topX, setTopX] = useState(50); const [topY, setTopY] = useState(45); // Text Layer 2 const [bottomText, setBottomText] = useState('STUDIO'); const [bottomFontSize, setBottomFontSize] = useState(8); const [bottomFontColor, setBottomFontColor] = useState('#FFFFFF'); const [bottomFontWeight, setBottomFontWeight] = useState(400); const [bottomX, setBottomX] = useState(50); const [bottomY, setBottomY] = useState(55); const drawComposition = useCallback( (ctx, width, height) => { const scale = width / 1000; // 1. Strict Black Background ctx.fillStyle = '#050505'; ctx.fillRect(0, 0, width, height); // 2. Draw Blobs ctx.save(); ctx.filter = `blur(${blurAmount * scale}px)`; blobs.forEach((blob) => { ctx.save(); ctx.globalAlpha = 0.8; ctx.globalCompositeOperation = 'screen'; ctx.fillStyle = blob.color; ctx.beginPath(); const radius = (blob.size / 100) * width; ctx.arc( (blob.x / 100) * width, (blob.y / 100) * height, radius, 0, Math.PI * 2, ); ctx.fill(); ctx.restore(); }); ctx.restore(); // 3. Tiled Noise Grain const noiseSize = 256; const noiseCanvas = document.createElement('canvas'); noiseCanvas.width = noiseSize; noiseCanvas.height = noiseSize; const nCtx = noiseCanvas.getContext('2d'); const nData = nCtx.createImageData(noiseSize, noiseSize); for (let i = 0; i < nData.data.length; i += 4) { const val = Math.random() * 255; nData.data[i] = nData.data[i + 1] = nData.data[i + 2] = val; nData.data[i + 3] = 255; } nCtx.putImageData(nData, 0, 0); ctx.save(); ctx.globalAlpha = noiseOpacity; ctx.globalCompositeOperation = 'overlay'; const pattern = ctx.createPattern(noiseCanvas, 'repeat'); ctx.fillStyle = pattern; ctx.fillRect(0, 0, width, height); ctx.restore(); // 4. Typography ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; // Top Text ctx.save(); ctx.fillStyle = topFontColor; const scaledTopSize = topFontSize * 16 * scale; ctx.font = `${topFontWeight} ${scaledTopSize}px "Playfair Display", serif`; ctx.shadowColor = 'rgba(0,0,0,0.5)'; ctx.shadowBlur = 30 * scale; ctx.fillText(topText, (topX / 100) * width, (topY / 100) * height); ctx.restore(); // Bottom Text if (bottomText) { ctx.save(); ctx.fillStyle = bottomFontColor; const scaledBottomSize = bottomFontSize * 16 * scale; ctx.font = `${bottomFontWeight} ${scaledBottomSize}px "Playfair Display", serif`; ctx.shadowColor = 'rgba(0,0,0,0.5)'; ctx.shadowBlur = 30 * scale; ctx.fillText( bottomText, (bottomX / 100) * width, (bottomY / 100) * height, ); ctx.restore(); } }, [ blobs, blurAmount, noiseOpacity, topText, topFontSize, topFontColor, topFontWeight, topX, topY, bottomText, bottomFontSize, bottomFontColor, bottomFontWeight, bottomX, bottomY, ], ); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; ctx.scale(dpr, dpr); document.fonts.ready.then(() => { drawComposition(ctx, rect.width, rect.height); }); }, [drawComposition]); const handleDownload = () => { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const W = 3840; const H = 2160; canvas.width = W; canvas.height = H; drawComposition(ctx, W, H); const link = document.createElement('a'); link.download = `fezcodex-master-${Date.now()}.png`; link.href = canvas.toDataURL('image/png', 1.0); link.click(); addToast({ title: 'Export Complete', message: '4K master sequence generated.', }); }; const addBlob = () => { if (blobs.length >= 12) { addToast({ title: 'Limit Reached', message: 'Maximum color layers active.', }); return; } const newBlob = { id: Date.now(), color: DEFAULT_COLORS[Math.floor(Math.random() * DEFAULT_COLORS.length)], x: Math.random() * 80 + 10, y: Math.random() * 80 + 10, size: Math.random() * 30 + 30, }; setBlobs([...blobs, newBlob]); }; const updateBlob = (id, field, value) => { setBlobs(blobs.map((b) => (b.id === id ? { ...b, [field]: value } : b))); }; const removeBlob = (id) => { setBlobs(blobs.filter((b) => b.id !== id)); }; return (
Chromatic synthesis lab. Map entities across the coordinate matrix and apply diffusion filters to create high-impact compositions.
Synthesis engine utilizes a unified Canvas rendering protocol to ensure perfect parity between live calibration and high-resolution export sequences.