diff --git a/app/api/icon/README.md b/app/api/icon/README.md new file mode 100644 index 00000000..2f9a71fe --- /dev/null +++ b/app/api/icon/README.md @@ -0,0 +1,127 @@ +# Icon Generator API + +This API endpoint generates customizable icon backgrounds programmatically. It accepts query parameters to customize the icon appearance and returns an SVG image. + +## Endpoint + +``` +GET /api/icon +``` + +## Query Parameters + +### Background Fill + +- `backgroundFillType` - Fill type: `Solid`, `Linear`, or `Radial` (default: `Linear`) +- `backgroundStartColor` - Primary color in hex format (default: `#FF6363`) +- `backgroundEndColor` - Secondary color for gradients (default: `#FFA07A`) +- `backgroundAngle` - Gradient angle in degrees for Linear fill (default: `45`) +- `backgroundPosition` - Position for Radial gradient as `x%,y%` (default: `50%,50%`) +- `backgroundSpread` - Spread percentage for Radial gradient (default: `80`) + +### Background Style + +- `backgroundRadius` - Corner radius in pixels (default: `128`) +- `backgroundStrokeSize` - Border width in pixels (default: `0`) +- `backgroundStrokeColor` - Border color in hex format (default: `#000000`) +- `backgroundStrokeOpacity` - Border opacity 0-100 (default: `100`) +- `backgroundRadialGlare` - Enable radial glare effect: `true` or `false` (default: `false`) + +### Icon + +- `icon` - Icon name from Raycast icons (default: `Dots`) + - Use kebab-case names like `airplane`, `star`, `folder`, etc. + - See available icons at https://www.raycast.com/icons +- `iconColor` - Icon color in hex format (default: `#FFFFFF`) +- `iconSize` - Icon size in pixels (default: `256`) +- `iconOffsetX` - Horizontal offset in pixels (default: `0`) +- `iconOffsetY` - Vertical offset in pixels (default: `0`) + +### Output + +- `size` - Output size in pixels (default: `512`) + +## Examples + +### Basic Icon with Gradient + +```bash +curl "http://localhost:3000/api/icon?icon=airplane&backgroundStartColor=%23FF6363&backgroundEndColor=%23FFA07A" +``` + +### Solid Color Background + +```bash +curl "http://localhost:3000/api/icon?icon=star&backgroundFillType=Solid&backgroundStartColor=%234A90E2&size=256" +``` + +### Radial Gradient + +```bash +curl "http://localhost:3000/api/icon?icon=folder&backgroundFillType=Radial&backgroundStartColor=%23FF6B6B&backgroundEndColor=%234ECDC4&backgroundSpread=60" +``` + +### With Border and Glare + +```bash +curl "http://localhost:3000/api/icon?icon=heart&backgroundRadialGlare=true&backgroundStrokeSize=8&backgroundStrokeColor=%23FFFFFF&backgroundStrokeOpacity=50" +``` + +## Response + +The API returns an SVG image with `Content-Type: image/svg+xml`. + +## Error Responses + +### Invalid Icon + +```json +{ + "error": "Icon \"invalid-name\" not found", + "availableIcons": ["add-person", "airplane", "..."], + "totalIcons": 603 +} +``` + +### Server Error + +```json +{ + "error": "Failed to generate icon", + "details": "Error message" +} +``` + +## Notes + +- Icon rendering uses a placeholder circle. For full icon rendering with actual Raycast icon paths, use the web interface at `/icon` +- All color values should be URL-encoded (e.g., `#FF6363` becomes `%23FF6363`) +- The API includes aggressive caching headers for performance +- SVG output can be easily converted to PNG using tools like ImageMagick or browser APIs + +## Integration Example + +### HTML + +```html +Custom Icon +``` + +### JavaScript + +```javascript +const iconUrl = new URL("/api/icon", window.location.origin); +iconUrl.searchParams.set("icon", "star"); +iconUrl.searchParams.set("backgroundStartColor", "#4A90E2"); +iconUrl.searchParams.set("size", "512"); + +fetch(iconUrl) + .then((response) => response.text()) + .then((svg) => { + // Use the SVG + document.getElementById("icon-container").innerHTML = svg; + }); +``` diff --git a/app/api/icon/route.ts b/app/api/icon/route.ts new file mode 100644 index 00000000..3aa78832 --- /dev/null +++ b/app/api/icon/route.ts @@ -0,0 +1,211 @@ +import { NextRequest, NextResponse } from "next/server"; +import { SettingsType } from "@icon/lib/types"; +import { Icons, IconName } from "@raycast/icons"; + +// Default settings for icon generation +const DEFAULT_SETTINGS: SettingsType = { + backgroundFillType: "Linear", + backgroundStartColor: "#FF6363", + backgroundEndColor: "#FFA07A", + backgroundAngle: 45, + backgroundPosition: "50%,50%", + backgroundSpread: 80, + backgroundRadius: 128, + backgroundStrokeSize: 0, + backgroundStrokeColor: "#000000", + backgroundStrokeOpacity: 100, + backgroundRadialGlare: false, + backgroundNoiseTexture: false, + backgroundNoiseTextureOpacity: 50, + iconColor: "#FFFFFF", + iconSize: 256, + iconOffsetX: 0, + iconOffsetY: 0, + icon: "Raycast" as IconName, + fileName: "icon", + selectedPresetIndex: null, +}; + +function parseQueryParam(value: string | null, defaultValue: any, type: "string" | "number" | "boolean" = "string") { + if (value === null) return defaultValue; + + if (type === "number") { + const parsed = parseFloat(value); + return isNaN(parsed) ? defaultValue : parsed; + } + + if (type === "boolean") { + return value === "true" || value === "1"; + } + + return value; +} + +function generateSVG(settings: SettingsType, size: number, iconName: string): string { + const strokeSize = settings.backgroundStrokeSize; + const strokeWidth = isNaN(parseInt(strokeSize.toString())) ? 0 : parseInt(strokeSize.toString()); + + const rectId = `rect-${Date.now()}`; + const gradientId = `gradient-${Date.now()}`; + const radialGlareGradientId = `radial-glare-${Date.now()}`; + const gradientX = settings.backgroundPosition?.split(",")[0] || "50%"; + const gradientY = settings.backgroundPosition?.split(",")[1] || "50%"; + + // Get icon SVG content + const IconComponent = Icons[iconName as keyof typeof Icons]; + let iconSvg = ""; + + if (IconComponent) { + // Create a temporary SVG to extract the icon path + const tempDiv = { __html: "" }; + try { + // Most Raycast icons are simple SVG paths, we'll use a placeholder approach + iconSvg = ` + + + + + `; + } catch (e) { + // Fallback to a simple circle + iconSvg = ``; + } + } + + // Build gradient definition + let gradientDef = ""; + if (settings.backgroundFillType === "Radial") { + gradientDef = ` + + + + `; + } else if (settings.backgroundFillType === "Linear") { + gradientDef = ` + + + + `; + } + + const radialGlareDef = ` + + + + `; + + const fillValue = settings.backgroundFillType === "Solid" ? settings.backgroundStartColor : `url(#${gradientId})`; + + return ` + + ${gradientDef} + ${radialGlareDef} + + + ${settings.backgroundRadialGlare ? `` : ""} + + + + ${iconSvg} +`; +} + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + + // Parse all settings from query parameters + const settings: SettingsType = { + backgroundFillType: parseQueryParam(searchParams.get("backgroundFillType"), DEFAULT_SETTINGS.backgroundFillType), + backgroundStartColor: parseQueryParam( + searchParams.get("backgroundStartColor"), + DEFAULT_SETTINGS.backgroundStartColor, + ), + backgroundEndColor: parseQueryParam(searchParams.get("backgroundEndColor"), DEFAULT_SETTINGS.backgroundEndColor), + backgroundAngle: parseQueryParam(searchParams.get("backgroundAngle"), DEFAULT_SETTINGS.backgroundAngle, "number"), + backgroundPosition: parseQueryParam(searchParams.get("backgroundPosition"), DEFAULT_SETTINGS.backgroundPosition), + backgroundSpread: parseQueryParam( + searchParams.get("backgroundSpread"), + DEFAULT_SETTINGS.backgroundSpread, + "number", + ), + backgroundRadius: parseQueryParam( + searchParams.get("backgroundRadius"), + DEFAULT_SETTINGS.backgroundRadius, + "number", + ), + backgroundStrokeSize: parseQueryParam( + searchParams.get("backgroundStrokeSize"), + DEFAULT_SETTINGS.backgroundStrokeSize, + "number", + ), + backgroundStrokeColor: parseQueryParam( + searchParams.get("backgroundStrokeColor"), + DEFAULT_SETTINGS.backgroundStrokeColor, + ), + backgroundStrokeOpacity: parseQueryParam( + searchParams.get("backgroundStrokeOpacity"), + DEFAULT_SETTINGS.backgroundStrokeOpacity, + "number", + ), + backgroundRadialGlare: parseQueryParam( + searchParams.get("backgroundRadialGlare"), + DEFAULT_SETTINGS.backgroundRadialGlare, + "boolean", + ), + backgroundNoiseTexture: parseQueryParam( + searchParams.get("backgroundNoiseTexture"), + DEFAULT_SETTINGS.backgroundNoiseTexture, + "boolean", + ), + backgroundNoiseTextureOpacity: parseQueryParam( + searchParams.get("backgroundNoiseTextureOpacity"), + DEFAULT_SETTINGS.backgroundNoiseTextureOpacity, + "number", + ), + iconColor: parseQueryParam(searchParams.get("iconColor"), DEFAULT_SETTINGS.iconColor), + iconSize: parseQueryParam(searchParams.get("iconSize"), DEFAULT_SETTINGS.iconSize, "number"), + iconOffsetX: parseQueryParam(searchParams.get("iconOffsetX"), DEFAULT_SETTINGS.iconOffsetX, "number"), + iconOffsetY: parseQueryParam(searchParams.get("iconOffsetY"), DEFAULT_SETTINGS.iconOffsetY, "number"), + icon: parseQueryParam(searchParams.get("icon"), DEFAULT_SETTINGS.icon) as IconName, + fileName: parseQueryParam(searchParams.get("fileName"), DEFAULT_SETTINGS.fileName), + selectedPresetIndex: null, + }; + + // Get the icon name + const iconName = settings.icon || "Dots"; + + // Check if icon exists + const availableIcons = Object.keys(Icons); + if (!availableIcons.includes(iconName)) { + return NextResponse.json( + { + error: `Icon "${iconName}" not found`, + availableIcons: availableIcons.slice(0, 10), + totalIcons: availableIcons.length, + }, + { status: 400 }, + ); + } + + // Get size from query params + const size = parseQueryParam(searchParams.get("size"), 512, "number"); + + // Generate the SVG + const svgString = generateSVG(settings, size, iconName); + + // Return SVG + return new NextResponse(svgString, { + headers: { + "Content-Type": "image/svg+xml", + "Cache-Control": "public, max-age=31536000, immutable", + }, + }); + } catch (error) { + console.error("Error generating icon:", error); + return NextResponse.json( + { error: "Failed to generate icon", details: error instanceof Error ? error.message : String(error) }, + { status: 500 }, + ); + } +}