mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 21:54:56 +00:00
Refactor FaviconDesigner into modular components
Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
@@ -4,7 +4,6 @@ import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
@@ -14,96 +13,17 @@ import {
|
||||
Plus,
|
||||
Trash,
|
||||
Download,
|
||||
CircleNotch,
|
||||
Square,
|
||||
Triangle,
|
||||
Star,
|
||||
Heart,
|
||||
Polygon,
|
||||
TextT,
|
||||
Image as ImageIcon,
|
||||
ArrowCounterClockwise,
|
||||
Copy,
|
||||
FloppyDisk,
|
||||
PencilSimple,
|
||||
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'
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
color: string
|
||||
rotation: number
|
||||
text?: string
|
||||
fontSize?: number
|
||||
fontWeight?: string
|
||||
emoji?: string
|
||||
paths?: Array<{ x: number; y: number }>
|
||||
strokeWidth?: number
|
||||
brushEffect?: BrushEffect
|
||||
gradientColor?: string
|
||||
glowIntensity?: number
|
||||
}
|
||||
|
||||
interface FaviconDesign {
|
||||
id: string
|
||||
name: string
|
||||
size: number
|
||||
backgroundColor: string
|
||||
elements: FaviconElement[]
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
filter?: CanvasFilter
|
||||
filterIntensity?: number
|
||||
}
|
||||
|
||||
const PRESET_SIZES = [16, 32, 48, 64, 128, 256, 512]
|
||||
const ELEMENT_TYPES = [
|
||||
{ value: 'circle', label: 'Circle', icon: CircleNotch },
|
||||
{ value: 'square', label: 'Square', icon: Square },
|
||||
{ value: 'triangle', label: 'Triangle', icon: Triangle },
|
||||
{ value: 'star', label: 'Star', icon: Star },
|
||||
{ value: 'heart', label: 'Heart', icon: Heart },
|
||||
{ value: 'polygon', label: 'Polygon', icon: Polygon },
|
||||
{ value: 'text', label: 'Text', icon: TextT },
|
||||
{ value: 'emoji', label: 'Emoji', icon: ImageIcon },
|
||||
]
|
||||
|
||||
const DEFAULT_DESIGN: FaviconDesign = {
|
||||
id: 'default',
|
||||
name: 'My Favicon',
|
||||
size: 128,
|
||||
backgroundColor: '#7c3aed',
|
||||
elements: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'text',
|
||||
x: 64,
|
||||
y: 64,
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: '#ffffff',
|
||||
rotation: 0,
|
||||
text: 'CF',
|
||||
fontSize: 48,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
],
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
import { BrushEffect, CanvasFilter, FaviconElement, FaviconDesign } from './FaviconDesigner/types'
|
||||
import { PRESET_SIZES, ELEMENT_TYPES, DEFAULT_DESIGN } from './FaviconDesigner/constants'
|
||||
import { drawCanvas } from './FaviconDesigner/canvasUtils'
|
||||
|
||||
export function FaviconDesigner() {
|
||||
const [designs, setDesigns] = useKV<FaviconDesign[]>('favicon-designs', [DEFAULT_DESIGN])
|
||||
@@ -125,255 +45,11 @@ export function FaviconDesigner() {
|
||||
const selectedElement = activeDesign.elements.find((e) => e.id === selectedElementId)
|
||||
|
||||
useEffect(() => {
|
||||
drawCanvas()
|
||||
}, [activeDesign])
|
||||
|
||||
const drawCanvas = () => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const size = activeDesign.size
|
||||
canvas.width = size
|
||||
canvas.height = size
|
||||
|
||||
ctx.fillStyle = activeDesign.backgroundColor
|
||||
ctx.fillRect(0, 0, size, size)
|
||||
|
||||
activeDesign.elements.forEach((element) => {
|
||||
ctx.save()
|
||||
|
||||
if (element.type === 'freehand' && element.paths && element.paths.length > 0) {
|
||||
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'
|
||||
|
||||
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.shadowBlur = 0
|
||||
} else {
|
||||
ctx.translate(element.x, element.y)
|
||||
ctx.rotate((element.rotation * Math.PI) / 180)
|
||||
ctx.fillStyle = element.color
|
||||
|
||||
switch (element.type) {
|
||||
case 'circle':
|
||||
ctx.beginPath()
|
||||
ctx.arc(0, 0, element.width / 2, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
break
|
||||
case 'square':
|
||||
ctx.fillRect(-element.width / 2, -element.height / 2, element.width, element.height)
|
||||
break
|
||||
case 'triangle':
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(0, -element.height / 2)
|
||||
ctx.lineTo(element.width / 2, element.height / 2)
|
||||
ctx.lineTo(-element.width / 2, element.height / 2)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
break
|
||||
case 'star':
|
||||
drawStar(ctx, 0, 0, 5, element.width / 2, element.width / 4)
|
||||
break
|
||||
case 'heart':
|
||||
drawHeart(ctx, 0, 0, element.width)
|
||||
break
|
||||
case 'polygon':
|
||||
drawPolygon(ctx, 0, 0, 6, element.width / 2)
|
||||
break
|
||||
case 'text':
|
||||
ctx.fillStyle = element.color
|
||||
ctx.font = `${element.fontWeight || 'bold'} ${element.fontSize || 32}px sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(element.text || '', 0, 0)
|
||||
break
|
||||
case 'emoji':
|
||||
ctx.font = `${element.fontSize || 32}px sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(element.emoji || '😀', 0, 0)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
ctx.restore()
|
||||
})
|
||||
|
||||
if (activeDesign.filter && activeDesign.filter !== 'none') {
|
||||
applyCanvasFilter(ctx, activeDesign.filter, activeDesign.filterIntensity || 50)
|
||||
if (canvas) {
|
||||
drawCanvas(canvas, activeDesign)
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
let rot = (Math.PI / 2) * 3
|
||||
let x = cx
|
||||
let y = cy
|
||||
const step = Math.PI / spikes
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(cx, cy - outerRadius)
|
||||
for (let i = 0; i < spikes; i++) {
|
||||
x = cx + Math.cos(rot) * outerRadius
|
||||
y = cy + Math.sin(rot) * outerRadius
|
||||
ctx.lineTo(x, y)
|
||||
rot += step
|
||||
|
||||
x = cx + Math.cos(rot) * innerRadius
|
||||
y = cy + Math.sin(rot) * innerRadius
|
||||
ctx.lineTo(x, y)
|
||||
rot += step
|
||||
}
|
||||
ctx.lineTo(cx, cy - outerRadius)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
const drawHeart = (ctx: CanvasRenderingContext2D, x: number, y: number, size: number) => {
|
||||
const topCurveHeight = size * 0.3
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x, y + topCurveHeight)
|
||||
ctx.bezierCurveTo(x, y, x - size / 2, y - topCurveHeight, x - size / 2, y + topCurveHeight)
|
||||
ctx.bezierCurveTo(x - size / 2, y + (size + topCurveHeight) / 2, x, y + (size + topCurveHeight) / 1.2, x, y + size)
|
||||
ctx.bezierCurveTo(x, y + (size + topCurveHeight) / 1.2, x + size / 2, y + (size + topCurveHeight) / 2, x + size / 2, y + topCurveHeight)
|
||||
ctx.bezierCurveTo(x + size / 2, y - topCurveHeight, x, y, x, y + topCurveHeight)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
const drawPolygon = (ctx: CanvasRenderingContext2D, x: number, y: number, sides: number, radius: number) => {
|
||||
ctx.beginPath()
|
||||
for (let i = 0; i < sides; i++) {
|
||||
const angle = (i * 2 * Math.PI) / sides - Math.PI / 2
|
||||
const px = x + radius * Math.cos(angle)
|
||||
const py = y + radius * Math.sin(angle)
|
||||
if (i === 0) ctx.moveTo(px, py)
|
||||
else ctx.lineTo(px, py)
|
||||
}
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
}, [activeDesign])
|
||||
|
||||
const handleAddElement = (type: FaviconElement['type']) => {
|
||||
const newElement: FaviconElement = {
|
||||
@@ -720,7 +396,10 @@ export function FaviconDesigner() {
|
||||
}
|
||||
|
||||
setCurrentPath([])
|
||||
drawCanvas()
|
||||
const canvas = canvasRef.current
|
||||
if (canvas) {
|
||||
drawCanvas(canvas, activeDesign)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCanvasMouseLeave = () => {
|
||||
|
||||
247
src/components/FaviconDesigner/canvasUtils.ts
Normal file
247
src/components/FaviconDesigner/canvasUtils.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { FaviconElement, FaviconDesign, CanvasFilter } from './types'
|
||||
|
||||
export function 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),
|
||||
}
|
||||
}
|
||||
|
||||
export function drawStar(ctx: CanvasRenderingContext2D, cx: number, cy: number, spikes: number, outerRadius: number, innerRadius: number) {
|
||||
let rot = (Math.PI / 2) * 3
|
||||
let x = cx
|
||||
let y = cy
|
||||
const step = Math.PI / spikes
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(cx, cy - outerRadius)
|
||||
for (let i = 0; i < spikes; i++) {
|
||||
x = cx + Math.cos(rot) * outerRadius
|
||||
y = cy + Math.sin(rot) * outerRadius
|
||||
ctx.lineTo(x, y)
|
||||
rot += step
|
||||
|
||||
x = cx + Math.cos(rot) * innerRadius
|
||||
y = cy + Math.sin(rot) * innerRadius
|
||||
ctx.lineTo(x, y)
|
||||
rot += step
|
||||
}
|
||||
ctx.lineTo(cx, cy - outerRadius)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
export function drawHeart(ctx: CanvasRenderingContext2D, x: number, y: number, size: number) {
|
||||
const topCurveHeight = size * 0.3
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x, y + topCurveHeight)
|
||||
ctx.bezierCurveTo(x, y, x - size / 2, y - topCurveHeight, x - size / 2, y + topCurveHeight)
|
||||
ctx.bezierCurveTo(x - size / 2, y + (size + topCurveHeight) / 2, x, y + (size + topCurveHeight) / 1.2, x, y + size)
|
||||
ctx.bezierCurveTo(x, y + (size + topCurveHeight) / 1.2, x + size / 2, y + (size + topCurveHeight) / 2, x + size / 2, y + topCurveHeight)
|
||||
ctx.bezierCurveTo(x + size / 2, y - topCurveHeight, x, y, x, y + topCurveHeight)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
export function drawPolygon(ctx: CanvasRenderingContext2D, x: number, y: number, sides: number, radius: number) {
|
||||
ctx.beginPath()
|
||||
for (let i = 0; i < sides; i++) {
|
||||
const angle = (i * 2 * Math.PI) / sides - Math.PI / 2
|
||||
const px = x + radius * Math.cos(angle)
|
||||
const py = y + radius * Math.sin(angle)
|
||||
if (i === 0) ctx.moveTo(px, py)
|
||||
else ctx.lineTo(px, py)
|
||||
}
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
export function applyCanvasFilter(ctx: CanvasRenderingContext2D, filter: CanvasFilter, intensity: number) {
|
||||
const canvas = ctx.canvas
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function drawElement(ctx: CanvasRenderingContext2D, element: FaviconElement) {
|
||||
ctx.save()
|
||||
|
||||
if (element.type === 'freehand' && element.paths && element.paths.length > 0) {
|
||||
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'
|
||||
|
||||
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.shadowBlur = 0
|
||||
} else {
|
||||
ctx.translate(element.x, element.y)
|
||||
ctx.rotate((element.rotation * Math.PI) / 180)
|
||||
ctx.fillStyle = element.color
|
||||
|
||||
switch (element.type) {
|
||||
case 'circle':
|
||||
ctx.beginPath()
|
||||
ctx.arc(0, 0, element.width / 2, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
break
|
||||
case 'square':
|
||||
ctx.fillRect(-element.width / 2, -element.height / 2, element.width, element.height)
|
||||
break
|
||||
case 'triangle':
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(0, -element.height / 2)
|
||||
ctx.lineTo(element.width / 2, element.height / 2)
|
||||
ctx.lineTo(-element.width / 2, element.height / 2)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
break
|
||||
case 'star':
|
||||
drawStar(ctx, 0, 0, 5, element.width / 2, element.width / 4)
|
||||
break
|
||||
case 'heart':
|
||||
drawHeart(ctx, 0, 0, element.width)
|
||||
break
|
||||
case 'polygon':
|
||||
drawPolygon(ctx, 0, 0, 6, element.width / 2)
|
||||
break
|
||||
case 'text':
|
||||
ctx.fillStyle = element.color
|
||||
ctx.font = `${element.fontWeight || 'bold'} ${element.fontSize || 32}px sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(element.text || '', 0, 0)
|
||||
break
|
||||
case 'emoji':
|
||||
ctx.font = `${element.fontSize || 32}px sans-serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(element.emoji || '😀', 0, 0)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
export function drawCanvas(canvas: HTMLCanvasElement, design: FaviconDesign) {
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const size = design.size
|
||||
canvas.width = size
|
||||
canvas.height = size
|
||||
|
||||
ctx.fillStyle = design.backgroundColor
|
||||
ctx.fillRect(0, 0, size, size)
|
||||
|
||||
design.elements.forEach((element) => {
|
||||
drawElement(ctx, element)
|
||||
})
|
||||
|
||||
if (design.filter && design.filter !== 'none') {
|
||||
applyCanvasFilter(ctx, design.filter, design.filterIntensity || 50)
|
||||
}
|
||||
}
|
||||
48
src/components/FaviconDesigner/constants.ts
Normal file
48
src/components/FaviconDesigner/constants.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
CircleNotch,
|
||||
Square,
|
||||
Triangle,
|
||||
Star,
|
||||
Heart,
|
||||
Polygon,
|
||||
TextT,
|
||||
Image as ImageIcon,
|
||||
} from '@phosphor-icons/react'
|
||||
import { FaviconDesign } from './types'
|
||||
|
||||
export const PRESET_SIZES = [16, 32, 48, 64, 128, 256, 512]
|
||||
|
||||
export const ELEMENT_TYPES = [
|
||||
{ value: 'circle', label: 'Circle', icon: CircleNotch },
|
||||
{ value: 'square', label: 'Square', icon: Square },
|
||||
{ value: 'triangle', label: 'Triangle', icon: Triangle },
|
||||
{ value: 'star', label: 'Star', icon: Star },
|
||||
{ value: 'heart', label: 'Heart', icon: Heart },
|
||||
{ value: 'polygon', label: 'Polygon', icon: Polygon },
|
||||
{ value: 'text', label: 'Text', icon: TextT },
|
||||
{ value: 'emoji', label: 'Emoji', icon: ImageIcon },
|
||||
]
|
||||
|
||||
export const DEFAULT_DESIGN: FaviconDesign = {
|
||||
id: 'default',
|
||||
name: 'My Favicon',
|
||||
size: 128,
|
||||
backgroundColor: '#7c3aed',
|
||||
elements: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'text',
|
||||
x: 64,
|
||||
y: 64,
|
||||
width: 100,
|
||||
height: 100,
|
||||
color: '#ffffff',
|
||||
rotation: 0,
|
||||
text: 'CF',
|
||||
fontSize: 48,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
],
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
34
src/components/FaviconDesigner/types.ts
Normal file
34
src/components/FaviconDesigner/types.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export type BrushEffect = 'solid' | 'gradient' | 'spray' | 'glow'
|
||||
export type CanvasFilter = 'none' | 'blur' | 'brightness' | 'contrast' | 'grayscale' | 'sepia' | 'invert' | 'saturate' | 'hue-rotate' | 'pixelate'
|
||||
|
||||
export interface FaviconElement {
|
||||
id: string
|
||||
type: 'circle' | 'square' | 'triangle' | 'star' | 'heart' | 'polygon' | 'text' | 'emoji' | 'freehand'
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
color: string
|
||||
rotation: number
|
||||
text?: string
|
||||
fontSize?: number
|
||||
fontWeight?: string
|
||||
emoji?: string
|
||||
paths?: Array<{ x: number; y: number }>
|
||||
strokeWidth?: number
|
||||
brushEffect?: BrushEffect
|
||||
gradientColor?: string
|
||||
glowIntensity?: number
|
||||
}
|
||||
|
||||
export interface FaviconDesign {
|
||||
id: string
|
||||
name: string
|
||||
size: number
|
||||
backgroundColor: string
|
||||
elements: FaviconElement[]
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
filter?: CanvasFilter
|
||||
filterIntensity?: number
|
||||
}
|
||||
Reference in New Issue
Block a user