From 1407a3bc9bdf815eaab6b2c218ce53fcb4a1fa84 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Fri, 16 Jan 2026 15:15:55 +0000 Subject: [PATCH] Generated by Spark: Thats neat, we can add fun effects like gradients, spray paint, image filters --- src/components/FaviconDesigner.tsx | 403 ++++++++++++++++++++++++++--- 1 file changed, 374 insertions(+), 29 deletions(-) diff --git a/src/components/FaviconDesigner.tsx b/src/components/FaviconDesigner.tsx index 243f47e..cd7d10c 100644 --- a/src/components/FaviconDesigner.tsx +++ b/src/components/FaviconDesigner.tsx @@ -26,10 +26,17 @@ import { Copy, FloppyDisk, PencilSimple, - Eraser + Eraser, + Gradient, + Sparkle, + Drop, + MagicWand } from '@phosphor-icons/react' import { toast } from 'sonner' +type BrushEffect = 'solid' | 'gradient' | 'spray' | 'glow' +type CanvasFilter = 'none' | 'blur' | 'brightness' | 'contrast' | 'grayscale' | 'sepia' | 'invert' | 'saturate' | 'hue-rotate' | 'pixelate' + interface FaviconElement { id: string type: 'circle' | 'square' | 'triangle' | 'star' | 'heart' | 'polygon' | 'text' | 'emoji' | 'freehand' @@ -45,6 +52,9 @@ interface FaviconElement { emoji?: string paths?: Array<{ x: number; y: number }> strokeWidth?: number + brushEffect?: BrushEffect + gradientColor?: string + glowIntensity?: number } interface FaviconDesign { @@ -55,6 +65,8 @@ interface FaviconDesign { elements: FaviconElement[] createdAt: number updatedAt: number + filter?: CanvasFilter + filterIntensity?: number } const PRESET_SIZES = [16, 32, 48, 64, 128, 256, 512] @@ -101,6 +113,9 @@ export function FaviconDesigner() { const [drawMode, setDrawMode] = useState<'select' | 'draw' | 'erase'>('select') const [brushSize, setBrushSize] = useState(3) const [brushColor, setBrushColor] = useState('#ffffff') + const [brushEffect, setBrushEffect] = useState('solid') + const [gradientColor, setGradientColor] = useState('#ff00ff') + const [glowIntensity, setGlowIntensity] = useState(10) const [currentPath, setCurrentPath] = useState>([]) const canvasRef = useRef(null) const drawingCanvasRef = useRef(null) @@ -131,17 +146,56 @@ export function FaviconDesigner() { ctx.save() if (element.type === 'freehand' && element.paths && element.paths.length > 0) { - ctx.strokeStyle = element.color - ctx.lineWidth = element.strokeWidth || 3 + const effect = element.brushEffect || 'solid' + const strokeWidth = element.strokeWidth || 3 + + if (effect === 'glow') { + ctx.shadowColor = element.color + ctx.shadowBlur = element.glowIntensity || 10 + } + + if (effect === 'gradient' && element.gradientColor) { + const bounds = getPathBounds(element.paths) + const gradient = ctx.createLinearGradient( + bounds.minX, + bounds.minY, + bounds.maxX, + bounds.maxY + ) + gradient.addColorStop(0, element.color) + gradient.addColorStop(1, element.gradientColor) + ctx.strokeStyle = gradient + } else { + ctx.strokeStyle = element.color + } + + ctx.lineWidth = strokeWidth ctx.lineCap = 'round' ctx.lineJoin = 'round' - ctx.beginPath() - ctx.moveTo(element.paths[0].x, element.paths[0].y) - for (let i = 1; i < element.paths.length; i++) { - ctx.lineTo(element.paths[i].x, element.paths[i].y) + if (effect === 'spray') { + element.paths.forEach((point, i) => { + if (i % 2 === 0) { + for (let j = 0; j < 3; j++) { + const offsetX = (Math.random() - 0.5) * strokeWidth * 2 + const offsetY = (Math.random() - 0.5) * strokeWidth * 2 + ctx.fillStyle = element.color + ctx.beginPath() + ctx.arc(point.x + offsetX, point.y + offsetY, strokeWidth / 3, 0, Math.PI * 2) + ctx.fill() + } + } + }) + } else { + ctx.beginPath() + ctx.moveTo(element.paths[0].x, element.paths[0].y) + for (let i = 1; i < element.paths.length; i++) { + ctx.lineTo(element.paths[i].x, element.paths[i].y) + } + ctx.stroke() } - ctx.stroke() + + ctx.shadowBlur = 0 } else { ctx.translate(element.x, element.y) ctx.rotate((element.rotation * Math.PI) / 180) @@ -191,6 +245,84 @@ export function FaviconDesigner() { ctx.restore() }) + + if (activeDesign.filter && activeDesign.filter !== 'none') { + applyCanvasFilter(ctx, activeDesign.filter, activeDesign.filterIntensity || 50) + } + } + + const getPathBounds = (paths: Array<{ x: number; y: number }>) => { + const xs = paths.map(p => p.x) + const ys = paths.map(p => p.y) + return { + minX: Math.min(...xs), + maxX: Math.max(...xs), + minY: Math.min(...ys), + maxY: Math.max(...ys), + } + } + + const applyCanvasFilter = (ctx: CanvasRenderingContext2D, filter: CanvasFilter, intensity: number) => { + const canvas = ctx.canvas + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) + const data = imageData.data + + switch (filter) { + case 'blur': + ctx.filter = `blur(${intensity / 10}px)` + ctx.drawImage(canvas, 0, 0) + ctx.filter = 'none' + break + case 'brightness': + ctx.filter = `brightness(${intensity / 50})` + ctx.drawImage(canvas, 0, 0) + ctx.filter = 'none' + break + case 'contrast': + ctx.filter = `contrast(${intensity / 50})` + ctx.drawImage(canvas, 0, 0) + ctx.filter = 'none' + break + case 'grayscale': + ctx.filter = `grayscale(${intensity / 100})` + ctx.drawImage(canvas, 0, 0) + ctx.filter = 'none' + break + case 'sepia': + ctx.filter = `sepia(${intensity / 100})` + ctx.drawImage(canvas, 0, 0) + ctx.filter = 'none' + break + case 'invert': + ctx.filter = `invert(${intensity / 100})` + ctx.drawImage(canvas, 0, 0) + ctx.filter = 'none' + break + case 'saturate': + ctx.filter = `saturate(${intensity / 50})` + ctx.drawImage(canvas, 0, 0) + ctx.filter = 'none' + break + case 'hue-rotate': + ctx.filter = `hue-rotate(${intensity * 3.6}deg)` + ctx.drawImage(canvas, 0, 0) + ctx.filter = 'none' + break + case 'pixelate': + const pixelSize = Math.max(1, Math.floor(intensity / 10)) + const tempCanvas = document.createElement('canvas') + tempCanvas.width = canvas.width / pixelSize + tempCanvas.height = canvas.height / pixelSize + const tempCtx = tempCanvas.getContext('2d') + if (tempCtx) { + tempCtx.imageSmoothingEnabled = false + tempCtx.drawImage(canvas, 0, 0, tempCanvas.width, tempCanvas.height) + ctx.imageSmoothingEnabled = false + ctx.drawImage(tempCanvas, 0, 0, canvas.width, canvas.height) + ctx.imageSmoothingEnabled = true + } + break + } } const drawStar = (ctx: CanvasRenderingContext2D, cx: number, cy: number, spikes: number, outerRadius: number, innerRadius: number) => { @@ -463,18 +595,50 @@ export function FaviconDesigner() { if (!ctx) return if (drawMode === 'draw') { - ctx.strokeStyle = brushColor + if (brushEffect === 'glow') { + ctx.shadowColor = brushColor + ctx.shadowBlur = glowIntensity + } + + if (brushEffect === 'gradient' && currentPath.length > 0) { + const gradient = ctx.createLinearGradient( + currentPath[0].x, + currentPath[0].y, + coords.x, + coords.y + ) + gradient.addColorStop(0, brushColor) + gradient.addColorStop(1, gradientColor) + ctx.strokeStyle = gradient + } else { + ctx.strokeStyle = brushColor + } + ctx.lineWidth = brushSize ctx.lineCap = 'round' ctx.lineJoin = 'round' if (currentPath.length > 0) { const prevPoint = currentPath[currentPath.length - 1] - ctx.beginPath() - ctx.moveTo(prevPoint.x, prevPoint.y) - ctx.lineTo(coords.x, coords.y) - ctx.stroke() + + if (brushEffect === 'spray') { + for (let i = 0; i < 5; i++) { + const offsetX = (Math.random() - 0.5) * brushSize * 2 + const offsetY = (Math.random() - 0.5) * brushSize * 2 + ctx.fillStyle = brushColor + ctx.beginPath() + ctx.arc(coords.x + offsetX, coords.y + offsetY, brushSize / 3, 0, Math.PI * 2) + ctx.fill() + } + } else { + ctx.beginPath() + ctx.moveTo(prevPoint.x, prevPoint.y) + ctx.lineTo(coords.x, coords.y) + ctx.stroke() + } } + + ctx.shadowBlur = 0 } else if (drawMode === 'erase') { ctx.globalCompositeOperation = 'destination-out' ctx.lineWidth = brushSize * 2 @@ -509,6 +673,9 @@ export function FaviconDesigner() { rotation: 0, paths: currentPath, strokeWidth: brushSize, + brushEffect: brushEffect, + gradientColor: brushEffect === 'gradient' ? gradientColor : undefined, + glowIntensity: brushEffect === 'glow' ? glowIntensity : undefined, } setDesigns((current) => @@ -667,7 +834,9 @@ export function FaviconDesigner() { {drawMode !== 'select' && ( - {drawMode === 'draw' ? `Brush: ${brushSize}px` : `Eraser: ${brushSize * 2}px`} + {drawMode === 'draw' + ? `${brushEffect.charAt(0).toUpperCase() + brushEffect.slice(1)}: ${brushSize}px` + : `Eraser: ${brushSize * 2}px`} )} @@ -778,6 +947,43 @@ export function FaviconDesigner() { +
+ + +
+ + {activeDesign.filter && activeDesign.filter !== 'none' && ( +
+ + handleUpdateDesign({ filterIntensity: value })} + min={0} + max={100} + step={1} + /> +
+ )} +
@@ -813,22 +1019,91 @@ export function FaviconDesigner() { {drawMode === 'draw' && ( -
- -
- setBrushColor(e.target.value)} - className="w-20 h-10" - /> - setBrushColor(e.target.value)} - placeholder="#ffffff" - /> + <> +
+ +
-
+ +
+ +
+ setBrushColor(e.target.value)} + className="w-20 h-10" + /> + setBrushColor(e.target.value)} + placeholder="#ffffff" + /> +
+
+ + {brushEffect === 'gradient' && ( +
+ +
+ setGradientColor(e.target.value)} + className="w-20 h-10" + /> + setGradientColor(e.target.value)} + placeholder="#ff00ff" + /> +
+
+ )} + + {brushEffect === 'glow' && ( +
+ + setGlowIntensity(value)} + min={1} + max={30} + step={1} + /> +
+ )} + )}
@@ -915,6 +1190,44 @@ export function FaviconDesigner() { {selectedElement.type === 'freehand' && ( <> +
+ + +
+
@@ -932,6 +1245,38 @@ export function FaviconDesigner() {
+ {selectedElement.brushEffect === 'gradient' && ( +
+ +
+ handleUpdateElement({ gradientColor: e.target.value })} + className="w-20 h-10" + /> + handleUpdateElement({ gradientColor: e.target.value })} + placeholder="#ff00ff" + /> +
+
+ )} + + {selectedElement.brushEffect === 'glow' && ( +
+ + handleUpdateElement({ glowIntensity: value })} + min={1} + max={30} + step={1} + /> +
+ )} +