Generated by Spark: Thats neat, we can add fun effects like gradients, spray paint, image filters

This commit is contained in:
2026-01-16 15:15:55 +00:00
committed by GitHub
parent 4eb6db788d
commit 1407a3bc9b

View File

@@ -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<BrushEffect>('solid')
const [gradientColor, setGradientColor] = useState('#ff00ff')
const [glowIntensity, setGlowIntensity] = useState(10)
const [currentPath, setCurrentPath] = useState<Array<{ x: number; y: number }>>([])
const canvasRef = useRef<HTMLCanvasElement>(null)
const drawingCanvasRef = useRef<HTMLCanvasElement>(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() {
</Badge>
{drawMode !== 'select' && (
<Badge className="absolute -bottom-3 left-1/2 -translate-x-1/2 bg-accent">
{drawMode === 'draw' ? `Brush: ${brushSize}px` : `Eraser: ${brushSize * 2}px`}
{drawMode === 'draw'
? `${brushEffect.charAt(0).toUpperCase() + brushEffect.slice(1)}: ${brushSize}px`
: `Eraser: ${brushSize * 2}px`}
</Badge>
)}
</div>
@@ -778,6 +947,43 @@ export function FaviconDesigner() {
</div>
</div>
<div>
<Label>Image Filter</Label>
<Select
value={activeDesign.filter || 'none'}
onValueChange={(value) => handleUpdateDesign({ filter: value as CanvasFilter })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
<SelectItem value="blur">Blur</SelectItem>
<SelectItem value="brightness">Brightness</SelectItem>
<SelectItem value="contrast">Contrast</SelectItem>
<SelectItem value="grayscale">Grayscale</SelectItem>
<SelectItem value="sepia">Sepia</SelectItem>
<SelectItem value="invert">Invert</SelectItem>
<SelectItem value="saturate">Saturate</SelectItem>
<SelectItem value="hue-rotate">Hue Rotate</SelectItem>
<SelectItem value="pixelate">Pixelate</SelectItem>
</SelectContent>
</Select>
</div>
{activeDesign.filter && activeDesign.filter !== 'none' && (
<div>
<Label>Filter Intensity: {activeDesign.filterIntensity || 50}%</Label>
<Slider
value={[activeDesign.filterIntensity || 50]}
onValueChange={([value]) => handleUpdateDesign({ filterIntensity: value })}
min={0}
max={100}
step={1}
/>
</div>
)}
<Separator />
<div>
@@ -813,22 +1019,91 @@ export function FaviconDesigner() {
</Label>
{drawMode === 'draw' && (
<div>
<Label>Brush Color</Label>
<div className="flex gap-2">
<Input
type="color"
value={brushColor}
onChange={(e) => setBrushColor(e.target.value)}
className="w-20 h-10"
/>
<Input
value={brushColor}
onChange={(e) => setBrushColor(e.target.value)}
placeholder="#ffffff"
/>
<>
<div>
<Label>Brush Effect</Label>
<Select value={brushEffect} onValueChange={(value) => setBrushEffect(value as BrushEffect)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="solid">
<div className="flex items-center gap-2">
<PencilSimple size={16} />
Solid
</div>
</SelectItem>
<SelectItem value="gradient">
<div className="flex items-center gap-2">
<Gradient size={16} />
Gradient
</div>
</SelectItem>
<SelectItem value="spray">
<div className="flex items-center gap-2">
<Drop size={16} />
Spray Paint
</div>
</SelectItem>
<SelectItem value="glow">
<div className="flex items-center gap-2">
<Sparkle size={16} />
Glow
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div>
<Label>Brush Color</Label>
<div className="flex gap-2">
<Input
type="color"
value={brushColor}
onChange={(e) => setBrushColor(e.target.value)}
className="w-20 h-10"
/>
<Input
value={brushColor}
onChange={(e) => setBrushColor(e.target.value)}
placeholder="#ffffff"
/>
</div>
</div>
{brushEffect === 'gradient' && (
<div>
<Label>Gradient End Color</Label>
<div className="flex gap-2">
<Input
type="color"
value={gradientColor}
onChange={(e) => setGradientColor(e.target.value)}
className="w-20 h-10"
/>
<Input
value={gradientColor}
onChange={(e) => setGradientColor(e.target.value)}
placeholder="#ff00ff"
/>
</div>
</div>
)}
{brushEffect === 'glow' && (
<div>
<Label>Glow Intensity: {glowIntensity}px</Label>
<Slider
value={[glowIntensity]}
onValueChange={([value]) => setGlowIntensity(value)}
min={1}
max={30}
step={1}
/>
</div>
)}
</>
)}
<div>
@@ -915,6 +1190,44 @@ export function FaviconDesigner() {
{selectedElement.type === 'freehand' && (
<>
<div>
<Label>Brush Effect</Label>
<Select
value={selectedElement.brushEffect || 'solid'}
onValueChange={(value) => handleUpdateElement({ brushEffect: value as BrushEffect })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="solid">
<div className="flex items-center gap-2">
<PencilSimple size={16} />
Solid
</div>
</SelectItem>
<SelectItem value="gradient">
<div className="flex items-center gap-2">
<Gradient size={16} />
Gradient
</div>
</SelectItem>
<SelectItem value="spray">
<div className="flex items-center gap-2">
<Drop size={16} />
Spray Paint
</div>
</SelectItem>
<SelectItem value="glow">
<div className="flex items-center gap-2">
<Sparkle size={16} />
Glow
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Stroke Color</Label>
<div className="flex gap-2">
@@ -932,6 +1245,38 @@ export function FaviconDesigner() {
</div>
</div>
{selectedElement.brushEffect === 'gradient' && (
<div>
<Label>Gradient End Color</Label>
<div className="flex gap-2">
<Input
type="color"
value={selectedElement.gradientColor || '#ff00ff'}
onChange={(e) => handleUpdateElement({ gradientColor: e.target.value })}
className="w-20 h-10"
/>
<Input
value={selectedElement.gradientColor || '#ff00ff'}
onChange={(e) => handleUpdateElement({ gradientColor: e.target.value })}
placeholder="#ff00ff"
/>
</div>
</div>
)}
{selectedElement.brushEffect === 'glow' && (
<div>
<Label>Glow Intensity: {selectedElement.glowIntensity || 10}px</Label>
<Slider
value={[selectedElement.glowIntensity || 10]}
onValueChange={([value]) => handleUpdateElement({ glowIntensity: value })}
min={1}
max={30}
step={1}
/>
</div>
)}
<div>
<Label>Stroke Width: {selectedElement.strokeWidth || 3}px</Label>
<Slider