@leftium/logo 0.0.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AppLogo.svelte +146 -0
- package/dist/AppLogo.svelte.d.ts +7 -0
- package/dist/LeftiumLogo.svelte +267 -186
- package/dist/LeftiumLogo.svelte.d.ts +1 -0
- package/dist/app-logo/color-transform.d.ts +40 -0
- package/dist/app-logo/color-transform.js +142 -0
- package/dist/app-logo/config-serialization.d.ts +80 -0
- package/dist/app-logo/config-serialization.js +489 -0
- package/dist/app-logo/defaults.d.ts +60 -0
- package/dist/app-logo/defaults.js +55 -0
- package/dist/app-logo/generate-favicon-set.d.ts +44 -0
- package/dist/app-logo/generate-favicon-set.js +97 -0
- package/dist/app-logo/generate-ico.d.ts +18 -0
- package/dist/app-logo/generate-ico.js +63 -0
- package/dist/app-logo/generate-png.d.ts +16 -0
- package/dist/app-logo/generate-png.js +60 -0
- package/dist/app-logo/generate-svg.d.ts +9 -0
- package/dist/app-logo/generate-svg.js +160 -0
- package/dist/app-logo/iconify.d.ts +35 -0
- package/dist/app-logo/iconify.js +223 -0
- package/dist/app-logo/squircle.d.ts +43 -0
- package/dist/app-logo/squircle.js +213 -0
- package/dist/app-logo/types.d.ts +39 -0
- package/dist/app-logo/types.js +1 -0
- package/dist/assets/logo-parts/glow-squircle.svg +44 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.js +9 -3
- package/dist/leftium-logo/generate-svg.d.ts +29 -0
- package/dist/leftium-logo/generate-svg.js +470 -0
- package/dist/tooltip.d.ts +18 -0
- package/dist/tooltip.js +38 -0
- package/dist/webgl-ripples/webgl-ripples.d.ts +0 -4
- package/dist/webgl-ripples/webgl-ripples.js +1 -1
- package/package.json +35 -20
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import JSZip from 'jszip';
|
|
2
|
+
import { generateAppLogoSvg } from './generate-svg.js';
|
|
3
|
+
import { generateAppLogoPng } from './generate-png.js';
|
|
4
|
+
import { pngToIco } from './generate-ico.js';
|
|
5
|
+
/**
|
|
6
|
+
* Generate the full favicon file set from an AppLogo configuration.
|
|
7
|
+
*
|
|
8
|
+
* Returns SVG, ICO, and PNG blobs for all standard favicon sizes.
|
|
9
|
+
* Browser-only (requires canvas).
|
|
10
|
+
*/
|
|
11
|
+
export async function generateFaviconSet(config) {
|
|
12
|
+
const [svg, png32, appleTouchIcon, icon192, icon512] = await Promise.all([
|
|
13
|
+
generateAppLogoSvg(config, 'favicon'),
|
|
14
|
+
generateAppLogoPng(config, { variant: 'favicon', size: 32 }),
|
|
15
|
+
generateAppLogoPng(config, { variant: 'favicon', size: 180 }),
|
|
16
|
+
generateAppLogoPng(config, { variant: 'favicon', size: 192 }),
|
|
17
|
+
generateAppLogoPng(config, { variant: 'favicon', size: 512 })
|
|
18
|
+
]);
|
|
19
|
+
const ico = await pngToIco(png32);
|
|
20
|
+
return { svg, ico, appleTouchIcon, icon192, icon512 };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Generate a manifest.webmanifest JSON string.
|
|
24
|
+
*/
|
|
25
|
+
export function generateManifest(appInfo) {
|
|
26
|
+
const manifest = {
|
|
27
|
+
name: appInfo.name || 'My App',
|
|
28
|
+
short_name: appInfo.shortName || appInfo.name || 'App',
|
|
29
|
+
icons: [
|
|
30
|
+
{ src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
|
|
31
|
+
{ src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
|
|
32
|
+
{ src: '/icon.svg', sizes: 'any', type: 'image/svg+xml' }
|
|
33
|
+
],
|
|
34
|
+
theme_color: '#ffffff',
|
|
35
|
+
background_color: '#ffffff',
|
|
36
|
+
display: 'standalone'
|
|
37
|
+
};
|
|
38
|
+
return JSON.stringify(manifest, null, '\t');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Generate the HTML snippet for pasting into app.html.
|
|
42
|
+
*/
|
|
43
|
+
export function generateFaviconHtml(appInfo) {
|
|
44
|
+
const name = appInfo.name || 'My App';
|
|
45
|
+
return [
|
|
46
|
+
`\t\t<title>${name}</title>`,
|
|
47
|
+
`\t\t<link rel="icon" href="/favicon.ico" sizes="32x32">`,
|
|
48
|
+
`\t\t<link rel="icon" href="/icon.svg" type="image/svg+xml">`,
|
|
49
|
+
`\t\t<link rel="apple-touch-icon" href="/apple-touch-icon.png">`,
|
|
50
|
+
`\t\t<link rel="manifest" href="/manifest.webmanifest">`
|
|
51
|
+
].join('\n');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the full zip kit as a Blob.
|
|
55
|
+
*
|
|
56
|
+
* Zip structure mirrors a SvelteKit project root:
|
|
57
|
+
* static/favicon.ico
|
|
58
|
+
* static/icon.svg
|
|
59
|
+
* static/apple-touch-icon.png
|
|
60
|
+
* static/icon-192.png
|
|
61
|
+
* static/icon-512.png
|
|
62
|
+
* static/logo.png
|
|
63
|
+
* static/logo.webp
|
|
64
|
+
* static/logo.svg
|
|
65
|
+
* static/manifest.webmanifest
|
|
66
|
+
* static/_app-logo/config.json
|
|
67
|
+
* _snippets/favicon-html.html
|
|
68
|
+
*/
|
|
69
|
+
export async function generateZipKit(config, appInfo) {
|
|
70
|
+
const [faviconSet, logoPng, logoWebp, logoSvg] = await Promise.all([
|
|
71
|
+
generateFaviconSet(config),
|
|
72
|
+
generateAppLogoPng(config, { variant: 'logo', size: 512 }),
|
|
73
|
+
generateAppLogoPng(config, { variant: 'logo', size: 512, format: 'webp' }),
|
|
74
|
+
generateAppLogoSvg(config, 'logo')
|
|
75
|
+
]);
|
|
76
|
+
const zip = new JSZip();
|
|
77
|
+
const staticDir = zip.folder('static');
|
|
78
|
+
const appLogoDir = staticDir.folder('_app-logo');
|
|
79
|
+
const snippetsDir = zip.folder('_snippets');
|
|
80
|
+
// Favicon files
|
|
81
|
+
staticDir.file('favicon.ico', faviconSet.ico);
|
|
82
|
+
staticDir.file('icon.svg', faviconSet.svg);
|
|
83
|
+
staticDir.file('apple-touch-icon.png', faviconSet.appleTouchIcon);
|
|
84
|
+
staticDir.file('icon-192.png', faviconSet.icon192);
|
|
85
|
+
staticDir.file('icon-512.png', faviconSet.icon512);
|
|
86
|
+
// Logo files
|
|
87
|
+
staticDir.file('logo.png', logoPng);
|
|
88
|
+
staticDir.file('logo.webp', logoWebp);
|
|
89
|
+
staticDir.file('logo.svg', logoSvg);
|
|
90
|
+
// Manifest
|
|
91
|
+
staticDir.file('manifest.webmanifest', generateManifest(appInfo));
|
|
92
|
+
// Config for regeneration
|
|
93
|
+
appLogoDir.file('config.json', JSON.stringify(config, null, '\t'));
|
|
94
|
+
// HTML snippet
|
|
95
|
+
snippetsDir.file('favicon-html.html', generateFaviconHtml(appInfo));
|
|
96
|
+
return zip.generateAsync({ type: 'blob' });
|
|
97
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ICO binary container generator.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a single PNG blob in the ICO container format.
|
|
5
|
+
* The ICO format is a simple container; modern browsers accept a single 32x32 PNG inside.
|
|
6
|
+
*
|
|
7
|
+
* ICO file structure (single image):
|
|
8
|
+
* ICONDIR header (6 bytes)
|
|
9
|
+
* ICONDIRENTRY (16 bytes)
|
|
10
|
+
* PNG image data (variable)
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Wrap a PNG Blob in an ICO container.
|
|
14
|
+
*
|
|
15
|
+
* @param pngBlob - A PNG image blob (should be 32x32 for favicon.ico)
|
|
16
|
+
* @returns ICO file as a Blob
|
|
17
|
+
*/
|
|
18
|
+
export declare function pngToIco(pngBlob: Blob): Promise<Blob>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ICO binary container generator.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a single PNG blob in the ICO container format.
|
|
5
|
+
* The ICO format is a simple container; modern browsers accept a single 32x32 PNG inside.
|
|
6
|
+
*
|
|
7
|
+
* ICO file structure (single image):
|
|
8
|
+
* ICONDIR header (6 bytes)
|
|
9
|
+
* ICONDIRENTRY (16 bytes)
|
|
10
|
+
* PNG image data (variable)
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Wrap a PNG Blob in an ICO container.
|
|
14
|
+
*
|
|
15
|
+
* @param pngBlob - A PNG image blob (should be 32x32 for favicon.ico)
|
|
16
|
+
* @returns ICO file as a Blob
|
|
17
|
+
*/
|
|
18
|
+
export async function pngToIco(pngBlob) {
|
|
19
|
+
const pngBytes = new Uint8Array(await pngBlob.arrayBuffer());
|
|
20
|
+
const pngSize = pngBytes.byteLength;
|
|
21
|
+
// ICO header: 6 bytes
|
|
22
|
+
// reserved (2) = 0
|
|
23
|
+
// type (2) = 1 (icon)
|
|
24
|
+
// count (2) = 1 (number of images)
|
|
25
|
+
const HEADER_SIZE = 6;
|
|
26
|
+
const ENTRY_SIZE = 16;
|
|
27
|
+
const imageOffset = HEADER_SIZE + ENTRY_SIZE;
|
|
28
|
+
const buffer = new ArrayBuffer(imageOffset + pngSize);
|
|
29
|
+
const view = new DataView(buffer);
|
|
30
|
+
const bytes = new Uint8Array(buffer);
|
|
31
|
+
// ICONDIR header
|
|
32
|
+
view.setUint16(0, 0, true); // reserved
|
|
33
|
+
view.setUint16(2, 1, true); // type: 1 = ICO
|
|
34
|
+
view.setUint16(4, 1, true); // image count: 1
|
|
35
|
+
// ICONDIRENTRY (16 bytes at offset 6)
|
|
36
|
+
// width (1 byte): 0 means 256; use actual size for <=255
|
|
37
|
+
// height (1 byte): same
|
|
38
|
+
// We read actual dimensions from the PNG IHDR chunk (bytes 16-23)
|
|
39
|
+
const width = readPngDimension(pngBytes, 16);
|
|
40
|
+
const height = readPngDimension(pngBytes, 20);
|
|
41
|
+
bytes[6] = width >= 256 ? 0 : width; // width (0 = 256)
|
|
42
|
+
bytes[7] = height >= 256 ? 0 : height; // height (0 = 256)
|
|
43
|
+
bytes[8] = 0; // color count (0 = no palette)
|
|
44
|
+
bytes[9] = 0; // reserved
|
|
45
|
+
view.setUint16(10, 1, true); // color planes
|
|
46
|
+
view.setUint16(12, 32, true); // bits per pixel
|
|
47
|
+
view.setUint32(14, pngSize, true); // size of image data
|
|
48
|
+
view.setUint32(18, imageOffset, true); // offset of image data
|
|
49
|
+
// PNG image data
|
|
50
|
+
bytes.set(pngBytes, imageOffset);
|
|
51
|
+
return new Blob([buffer], { type: 'image/x-icon' });
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Read a 4-byte big-endian uint32 from a Uint8Array at the given offset.
|
|
55
|
+
* Used to extract width/height from the PNG IHDR chunk.
|
|
56
|
+
*/
|
|
57
|
+
function readPngDimension(bytes, offset) {
|
|
58
|
+
return (((bytes[offset] << 24) |
|
|
59
|
+
(bytes[offset + 1] << 16) |
|
|
60
|
+
(bytes[offset + 2] << 8) |
|
|
61
|
+
bytes[offset + 3]) >>>
|
|
62
|
+
0);
|
|
63
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AppLogoConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generate a PNG or WebP Blob from an AppLogo configuration.
|
|
4
|
+
*
|
|
5
|
+
* Renders the SVG onto an offscreen canvas and exports as PNG or WebP.
|
|
6
|
+
* Browser-only (requires canvas and Image).
|
|
7
|
+
*
|
|
8
|
+
* @param config - The AppLogo configuration
|
|
9
|
+
* @param options - Export options
|
|
10
|
+
* @returns PNG or WebP Blob
|
|
11
|
+
*/
|
|
12
|
+
export declare function generateAppLogoPng(config: AppLogoConfig, options?: {
|
|
13
|
+
variant?: 'logo' | 'favicon';
|
|
14
|
+
size?: number;
|
|
15
|
+
format?: 'png' | 'webp';
|
|
16
|
+
}): Promise<Blob>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { generateAppLogoSvg } from './generate-svg.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generate a PNG or WebP Blob from an AppLogo configuration.
|
|
4
|
+
*
|
|
5
|
+
* Renders the SVG onto an offscreen canvas and exports as PNG or WebP.
|
|
6
|
+
* Browser-only (requires canvas and Image).
|
|
7
|
+
*
|
|
8
|
+
* @param config - The AppLogo configuration
|
|
9
|
+
* @param options - Export options
|
|
10
|
+
* @returns PNG or WebP Blob
|
|
11
|
+
*/
|
|
12
|
+
export async function generateAppLogoPng(config, options) {
|
|
13
|
+
const size = options?.size ?? 512;
|
|
14
|
+
const mimeType = options?.format === 'webp' ? 'image/webp' : 'image/png';
|
|
15
|
+
// Override size in config for SVG generation
|
|
16
|
+
const sizedConfig = {
|
|
17
|
+
...config,
|
|
18
|
+
[options?.variant === 'favicon' ? 'favicon' : 'logo']: {
|
|
19
|
+
...(options?.variant === 'favicon' ? config.favicon : config.logo),
|
|
20
|
+
size
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const svg = await generateAppLogoSvg(sizedConfig, options?.variant);
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const img = new Image();
|
|
26
|
+
const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
|
|
27
|
+
const url = URL.createObjectURL(blob);
|
|
28
|
+
img.onload = () => {
|
|
29
|
+
try {
|
|
30
|
+
const canvas = document.createElement('canvas');
|
|
31
|
+
canvas.width = size;
|
|
32
|
+
canvas.height = size;
|
|
33
|
+
const ctx = canvas.getContext('2d');
|
|
34
|
+
if (!ctx) {
|
|
35
|
+
reject(new Error('Failed to get canvas 2d context'));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
ctx.drawImage(img, 0, 0, size, size);
|
|
39
|
+
canvas.toBlob((outBlob) => {
|
|
40
|
+
URL.revokeObjectURL(url);
|
|
41
|
+
if (outBlob) {
|
|
42
|
+
resolve(outBlob);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
reject(new Error('Canvas toBlob returned null'));
|
|
46
|
+
}
|
|
47
|
+
}, mimeType);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
URL.revokeObjectURL(url);
|
|
51
|
+
reject(err);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
img.onerror = () => {
|
|
55
|
+
URL.revokeObjectURL(url);
|
|
56
|
+
reject(new Error('Failed to load SVG into Image element'));
|
|
57
|
+
};
|
|
58
|
+
img.src = url;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AppLogoConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generate a complete SVG string for an AppLogo configuration.
|
|
4
|
+
*
|
|
5
|
+
* @param config - The AppLogo configuration
|
|
6
|
+
* @param variant - 'logo' or 'favicon' to use per-output overrides
|
|
7
|
+
* @returns Complete SVG string
|
|
8
|
+
*/
|
|
9
|
+
export declare function generateAppLogoSvg(config: AppLogoConfig, variant?: 'logo' | 'favicon'): Promise<string>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { APP_LOGO_DEFAULTS, DEFAULT_EMOJI_STYLE } from './defaults.js';
|
|
2
|
+
import { resolveIcon } from './iconify.js';
|
|
3
|
+
import { applyColorMode } from './color-transform.js';
|
|
4
|
+
import { generateCornerPath } from './squircle.js';
|
|
5
|
+
/**
|
|
6
|
+
* Resolve merged props from an AppLogoConfig + variant.
|
|
7
|
+
*/
|
|
8
|
+
function resolveProps(config, variant) {
|
|
9
|
+
const overrides = variant === 'favicon' ? config.favicon : config.logo;
|
|
10
|
+
return {
|
|
11
|
+
icon: overrides?.icon ?? config.icon ?? APP_LOGO_DEFAULTS.icon,
|
|
12
|
+
iconColor: overrides?.iconColor ?? config.iconColor ?? APP_LOGO_DEFAULTS.iconColor,
|
|
13
|
+
iconColorMode: overrides?.iconColorMode ?? config.iconColorMode ?? APP_LOGO_DEFAULTS.iconColorMode,
|
|
14
|
+
iconSize: overrides?.iconSize ?? APP_LOGO_DEFAULTS.iconSize,
|
|
15
|
+
iconOffsetX: overrides?.iconOffsetX ?? APP_LOGO_DEFAULTS.iconOffsetX,
|
|
16
|
+
iconOffsetY: overrides?.iconOffsetY ?? APP_LOGO_DEFAULTS.iconOffsetY,
|
|
17
|
+
iconRotation: overrides?.iconRotation ?? APP_LOGO_DEFAULTS.iconRotation,
|
|
18
|
+
grayscaleLightness: overrides?.grayscaleLightness ?? 100,
|
|
19
|
+
cornerRadius: overrides?.cornerRadius ?? config.cornerRadius ?? APP_LOGO_DEFAULTS.cornerRadius,
|
|
20
|
+
cornerShape: overrides?.cornerShape ?? config.cornerShape ?? APP_LOGO_DEFAULTS.cornerShape,
|
|
21
|
+
background: overrides?.background ?? config.background ?? APP_LOGO_DEFAULTS.background,
|
|
22
|
+
size: overrides?.size ?? APP_LOGO_DEFAULTS.size
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Convert a CSS gradient angle (degrees, clockwise from top) to SVG linearGradient coordinates.
|
|
27
|
+
*
|
|
28
|
+
* CSS: 0deg = bottom-to-top, 90deg = left-to-right, 180deg = top-to-bottom
|
|
29
|
+
* SVG: x1,y1 = start point, x2,y2 = end point (as percentages)
|
|
30
|
+
*
|
|
31
|
+
* @param angleDeg - CSS gradient angle in degrees
|
|
32
|
+
* @param position - Shift gradient along its axis as % (-100 to 100), default 0
|
|
33
|
+
* @param scale - Scale gradient length (1 = full span, 0.5 = half, 2 = double), default 1
|
|
34
|
+
*/
|
|
35
|
+
function angleToGradientCoords(angleDeg, position = 0, scale = 1) {
|
|
36
|
+
// CSS gradient angles: 0deg = to top, 90deg = to right, clockwise.
|
|
37
|
+
// The gradient line runs FROM the opposite side TO the angle direction.
|
|
38
|
+
// In SVG coords (Y-axis down): convert CSS angle to SVG start/end points.
|
|
39
|
+
const rad = (angleDeg * Math.PI) / 180;
|
|
40
|
+
// Direction vector: CSS 0deg = up (dx=0, dy=-1 in SVG), rotates clockwise
|
|
41
|
+
const dx = Math.sin(rad);
|
|
42
|
+
const dy = -Math.cos(rad);
|
|
43
|
+
// Half-length of the gradient vector, scaled
|
|
44
|
+
const halfLen = 50 * scale;
|
|
45
|
+
// Position shift along the gradient axis (% of full span)
|
|
46
|
+
const shiftX = dx * (position / 100) * 50;
|
|
47
|
+
const shiftY = dy * (position / 100) * 50;
|
|
48
|
+
// Center point, shifted by position
|
|
49
|
+
const cx = 50 + shiftX;
|
|
50
|
+
const cy = 50 + shiftY;
|
|
51
|
+
// Start and end points
|
|
52
|
+
const x1 = Math.round(cx - dx * halfLen);
|
|
53
|
+
const y1 = Math.round(cy - dy * halfLen);
|
|
54
|
+
const x2 = Math.round(cx + dx * halfLen);
|
|
55
|
+
const y2 = Math.round(cy + dy * halfLen);
|
|
56
|
+
return { x1: `${x1}%`, y1: `${y1}%`, x2: `${x2}%`, y2: `${y2}%` };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build a <linearGradient> element for the gradient background.
|
|
60
|
+
* Returns the element without wrapping <defs>.
|
|
61
|
+
*/
|
|
62
|
+
function buildGradientElement(gradient, id) {
|
|
63
|
+
const angle = gradient.angle ?? 45;
|
|
64
|
+
const position = gradient.position ?? 0;
|
|
65
|
+
const scale = gradient.scale ?? 1;
|
|
66
|
+
const coords = angleToGradientCoords(angle, position, scale);
|
|
67
|
+
const stops = gradient.colors
|
|
68
|
+
.map((color, i) => {
|
|
69
|
+
const offset = gradient.stops ? gradient.stops[i] : i / (gradient.colors.length - 1);
|
|
70
|
+
return `<stop offset="${(offset * 100).toFixed(1)}%" stop-color="${color}"/>`;
|
|
71
|
+
})
|
|
72
|
+
.join('\n ');
|
|
73
|
+
return `<linearGradient id="${id}" x1="${coords.x1}" y1="${coords.y1}" x2="${coords.x2}" y2="${coords.y2}">
|
|
74
|
+
${stops}
|
|
75
|
+
</linearGradient>`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Build the background element (solid color or gradient fill).
|
|
79
|
+
*
|
|
80
|
+
* For 'round' corners, uses a standard <rect> with rx/ry.
|
|
81
|
+
* For other corner shapes, uses a <path> with the superellipse curve.
|
|
82
|
+
*/
|
|
83
|
+
function buildBackground(background, size, cornerRadius, cornerShape, gradientId) {
|
|
84
|
+
const fill = typeof background === 'string' ? background : `url(#${gradientId})`;
|
|
85
|
+
// For 'round' shape, use standard rect with rx/ry (cleaner SVG)
|
|
86
|
+
if (cornerShape === 'round') {
|
|
87
|
+
const rx = cornerRadius > 0
|
|
88
|
+
? ` rx="${(cornerRadius / 100) * size}" ry="${(cornerRadius / 100) * size}"`
|
|
89
|
+
: '';
|
|
90
|
+
return {
|
|
91
|
+
defs: '',
|
|
92
|
+
element: `<rect width="${size}" height="${size}"${rx} fill="${fill}"/>`
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// For other shapes, generate the superellipse path
|
|
96
|
+
const pathD = generateCornerPath(size, cornerRadius, cornerShape);
|
|
97
|
+
const clipId = 'app-logo-clip';
|
|
98
|
+
return {
|
|
99
|
+
defs: `<clipPath id="${clipId}"><path d="${pathD}"/></clipPath>`,
|
|
100
|
+
element: `<rect width="${size}" height="${size}" fill="${fill}" clip-path="url(#${clipId})"/>`
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Build the icon layer as an SVG group, positioned and scaled within the square.
|
|
105
|
+
*/
|
|
106
|
+
function buildIconLayer(svgContent, viewBox, size, iconSize, iconOffsetX, iconOffsetY, iconRotation) {
|
|
107
|
+
// Parse viewBox
|
|
108
|
+
const [vbX, vbY, vbW, vbH] = viewBox.split(' ').map(Number);
|
|
109
|
+
// Icon pixel size
|
|
110
|
+
const iconPx = (iconSize / 100) * size;
|
|
111
|
+
// Scale factor to fit icon into iconPx
|
|
112
|
+
const scale = iconPx / Math.max(vbW, vbH);
|
|
113
|
+
// Actual rendered dimensions (preserving aspect ratio)
|
|
114
|
+
const renderedW = vbW * scale;
|
|
115
|
+
const renderedH = vbH * scale;
|
|
116
|
+
// Center position with offset
|
|
117
|
+
const offsetXPx = (iconOffsetX / 100) * size;
|
|
118
|
+
const offsetYPx = (iconOffsetY / 100) * size;
|
|
119
|
+
const tx = (size - renderedW) / 2 + offsetXPx;
|
|
120
|
+
const ty = (size - renderedH) / 2 + offsetYPx;
|
|
121
|
+
// Rotation around the icon's center
|
|
122
|
+
const rotateAttr = iconRotation !== 0
|
|
123
|
+
? ` rotate(${iconRotation}, ${(renderedW / 2).toFixed(2)}, ${(renderedH / 2).toFixed(2)})`
|
|
124
|
+
: '';
|
|
125
|
+
return `<g transform="translate(${tx.toFixed(2)}, ${ty.toFixed(2)})${rotateAttr} scale(${scale.toFixed(4)}) translate(${-vbX}, ${-vbY})">
|
|
126
|
+
${svgContent}
|
|
127
|
+
</g>`;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Generate a complete SVG string for an AppLogo configuration.
|
|
131
|
+
*
|
|
132
|
+
* @param config - The AppLogo configuration
|
|
133
|
+
* @param variant - 'logo' or 'favicon' to use per-output overrides
|
|
134
|
+
* @returns Complete SVG string
|
|
135
|
+
*/
|
|
136
|
+
export async function generateAppLogoSvg(config, variant) {
|
|
137
|
+
const props = resolveProps(config, variant);
|
|
138
|
+
const size = props.size;
|
|
139
|
+
const gradientId = 'app-logo-bg';
|
|
140
|
+
// Resolve icon (pass emojiStyle for emoji auto-mapping)
|
|
141
|
+
const emojiStyle = config.emojiStyle ?? DEFAULT_EMOJI_STYLE;
|
|
142
|
+
const resolved = await resolveIcon(props.icon, emojiStyle);
|
|
143
|
+
// Apply color mode (supports all IconColorMode values)
|
|
144
|
+
const coloredContent = applyColorMode(resolved.svgContent, resolved.isMonochrome, props.iconColorMode, props.iconColor, props.grayscaleLightness);
|
|
145
|
+
// Build SVG parts
|
|
146
|
+
const isGradient = typeof props.background !== 'string';
|
|
147
|
+
const gradientEl = isGradient
|
|
148
|
+
? buildGradientElement(props.background, gradientId)
|
|
149
|
+
: '';
|
|
150
|
+
const bg = buildBackground(props.background, size, props.cornerRadius, props.cornerShape, gradientId);
|
|
151
|
+
const iconLayer = buildIconLayer(coloredContent, resolved.viewBox, size, props.iconSize, props.iconOffsetX, props.iconOffsetY, props.iconRotation);
|
|
152
|
+
// Combine all defs (gradient + clip path)
|
|
153
|
+
const allDefs = [gradientEl, bg.defs].filter(Boolean).join('\n ');
|
|
154
|
+
const defsBlock = allDefs ? `<defs>\n ${allDefs}\n </defs>` : '';
|
|
155
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
|
|
156
|
+
${defsBlock}
|
|
157
|
+
${bg.element}
|
|
158
|
+
${iconLayer}
|
|
159
|
+
</svg>`;
|
|
160
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { IconSourceType } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolved icon data ready for rendering/export.
|
|
4
|
+
*/
|
|
5
|
+
export interface ResolvedIcon {
|
|
6
|
+
/** The raw SVG markup (without the outer <svg> wrapper for Iconify/data-url/svg sources) */
|
|
7
|
+
svgContent: string;
|
|
8
|
+
/** The viewBox of the original icon SVG */
|
|
9
|
+
viewBox: string;
|
|
10
|
+
/** Whether the icon uses currentColor (monochrome) vs hardcoded colors (multicolor) */
|
|
11
|
+
isMonochrome: boolean;
|
|
12
|
+
/** The detected source type */
|
|
13
|
+
sourceType: IconSourceType;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Resolve an emoji to its Iconify icon slug within a given set.
|
|
17
|
+
*
|
|
18
|
+
* Strategy A: Use the Iconify search API, which accepts emoji characters
|
|
19
|
+
* directly and returns matching icon names.
|
|
20
|
+
*
|
|
21
|
+
* Returns the icon name (slug) or null if not found.
|
|
22
|
+
* Results are cached by `${prefix}:${emoji}`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveEmojiSlug(emoji: string, prefix: string): Promise<string | null>;
|
|
25
|
+
/**
|
|
26
|
+
* Resolve an icon prop value to SVG data ready for rendering.
|
|
27
|
+
*
|
|
28
|
+
* Handles all 4 source types: Iconify ID, emoji, inline SVG, data URL.
|
|
29
|
+
* For emoji, auto-maps to an Iconify emoji set unless emojiStyle is 'native'.
|
|
30
|
+
*
|
|
31
|
+
* @param icon - The icon prop value
|
|
32
|
+
* @param emojiStyle - Iconify emoji set prefix (default: DEFAULT_EMOJI_STYLE).
|
|
33
|
+
* Use 'native' to disable auto-mapping and render as <text>.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveIcon(icon: string, emojiStyle?: string): Promise<ResolvedIcon>;
|