@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,223 @@
|
|
|
1
|
+
import { detectIconSource, DEFAULT_EMOJI_STYLE } from './defaults.js';
|
|
2
|
+
/** Cached Iconify SVG strings keyed by icon ID */
|
|
3
|
+
const iconCache = new Map();
|
|
4
|
+
/** Cached emoji slug resolution keyed by `${prefix}:${emoji}` */
|
|
5
|
+
const emojiSlugCache = new Map();
|
|
6
|
+
/**
|
|
7
|
+
* Fetch an Iconify icon SVG by its ID (e.g. "mdi:rocket-launch").
|
|
8
|
+
* Results are cached in memory for the session.
|
|
9
|
+
*/
|
|
10
|
+
async function fetchIconifySvg(iconId) {
|
|
11
|
+
const cached = iconCache.get(iconId);
|
|
12
|
+
if (cached)
|
|
13
|
+
return cached;
|
|
14
|
+
const [prefix, name] = iconId.split(':');
|
|
15
|
+
if (!prefix || !name) {
|
|
16
|
+
throw new Error(`Invalid Iconify icon ID: "${iconId}". Expected format: "prefix:name"`);
|
|
17
|
+
}
|
|
18
|
+
const url = `https://api.iconify.design/${prefix}/${name}.svg`;
|
|
19
|
+
const response = await fetch(url);
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
throw new Error(`Failed to fetch icon "${iconId}": ${response.status} ${response.statusText}`);
|
|
22
|
+
}
|
|
23
|
+
const svg = await response.text();
|
|
24
|
+
iconCache.set(iconId, svg);
|
|
25
|
+
return svg;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Decode a data URL to an SVG string.
|
|
29
|
+
* Supports both base64 and plain-text encoded data URLs.
|
|
30
|
+
*/
|
|
31
|
+
function decodeDataUrl(dataUrl) {
|
|
32
|
+
// data:image/svg+xml;base64,PHN2Zy...
|
|
33
|
+
// data:image/svg+xml,%3Csvg...
|
|
34
|
+
// data:image/svg+xml;utf8,<svg...
|
|
35
|
+
const commaIndex = dataUrl.indexOf(',');
|
|
36
|
+
if (commaIndex === -1) {
|
|
37
|
+
throw new Error('Invalid data URL: no comma separator found');
|
|
38
|
+
}
|
|
39
|
+
const meta = dataUrl.substring(0, commaIndex);
|
|
40
|
+
const data = dataUrl.substring(commaIndex + 1);
|
|
41
|
+
if (meta.includes(';base64')) {
|
|
42
|
+
return atob(data);
|
|
43
|
+
}
|
|
44
|
+
// URL-encoded or UTF-8
|
|
45
|
+
return decodeURIComponent(data);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Parse viewBox from an SVG string. Falls back to "0 0 24 24" (Iconify default).
|
|
49
|
+
*/
|
|
50
|
+
function parseViewBox(svg) {
|
|
51
|
+
const match = svg.match(/viewBox=["']([^"']+)["']/);
|
|
52
|
+
return match?.[1] ?? '0 0 24 24';
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Extract the inner content of an SVG element (everything between <svg> and </svg>).
|
|
56
|
+
*/
|
|
57
|
+
function extractSvgContent(svg) {
|
|
58
|
+
// Remove the outer <svg ...> and </svg> tags
|
|
59
|
+
const openTagEnd = svg.indexOf('>');
|
|
60
|
+
const closeTagStart = svg.lastIndexOf('</svg>');
|
|
61
|
+
if (openTagEnd === -1 || closeTagStart === -1) {
|
|
62
|
+
return svg; // Return as-is if we can't parse
|
|
63
|
+
}
|
|
64
|
+
return svg.substring(openTagEnd + 1, closeTagStart).trim();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Detect whether an SVG is monochrome (uses currentColor) or multicolor (hardcoded fills).
|
|
68
|
+
*
|
|
69
|
+
* Heuristic: if the SVG contains "currentColor" it's monochrome.
|
|
70
|
+
* If it contains hardcoded color values (hex, rgb, named colors) in fill/stroke attributes,
|
|
71
|
+
* it's multicolor.
|
|
72
|
+
*/
|
|
73
|
+
function detectMonochrome(svg) {
|
|
74
|
+
// If it explicitly uses currentColor, it's monochrome
|
|
75
|
+
if (svg.includes('currentColor'))
|
|
76
|
+
return true;
|
|
77
|
+
// Check for hardcoded fill/stroke colors (hex, rgb, named colors)
|
|
78
|
+
// If we find fill or stroke with a value that's not "none" or "currentColor", it's multicolor
|
|
79
|
+
const colorAttrPattern = /(?:fill|stroke)=["'](?!none|currentColor)([^"']+)["']/gi;
|
|
80
|
+
const hasHardcodedColors = colorAttrPattern.test(svg);
|
|
81
|
+
// Also check for fill/stroke in style attributes
|
|
82
|
+
const styleColorPattern = /(?:fill|stroke)\s*:\s*(?!none|currentColor)([^;"']+)/gi;
|
|
83
|
+
const hasStyleColors = styleColorPattern.test(svg);
|
|
84
|
+
if (hasHardcodedColors || hasStyleColors)
|
|
85
|
+
return false;
|
|
86
|
+
// No colors found at all -- treat as monochrome (will use currentColor behavior)
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
// ─── Emoji → Iconify slug resolution ────────────────────────────────────
|
|
90
|
+
/**
|
|
91
|
+
* Convert an emoji string to its Unicode codepoint sequence (e.g. "🚀" → "1f680").
|
|
92
|
+
* Strips variation selectors (U+FE0E, U+FE0F).
|
|
93
|
+
*/
|
|
94
|
+
function emojiToCodepoints(emoji) {
|
|
95
|
+
const codepoints = [];
|
|
96
|
+
for (const codePoint of emoji) {
|
|
97
|
+
const cp = codePoint.codePointAt(0);
|
|
98
|
+
if (cp === undefined)
|
|
99
|
+
continue;
|
|
100
|
+
// Skip variation selectors
|
|
101
|
+
if (cp === 0xfe0e || cp === 0xfe0f)
|
|
102
|
+
continue;
|
|
103
|
+
codepoints.push(cp.toString(16));
|
|
104
|
+
}
|
|
105
|
+
return codepoints.join('-');
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Resolve an emoji to its Iconify icon slug within a given set.
|
|
109
|
+
*
|
|
110
|
+
* Strategy A: Use the Iconify search API, which accepts emoji characters
|
|
111
|
+
* directly and returns matching icon names.
|
|
112
|
+
*
|
|
113
|
+
* Returns the icon name (slug) or null if not found.
|
|
114
|
+
* Results are cached by `${prefix}:${emoji}`.
|
|
115
|
+
*/
|
|
116
|
+
export async function resolveEmojiSlug(emoji, prefix) {
|
|
117
|
+
const cacheKey = `${prefix}:${emoji}`;
|
|
118
|
+
const cached = emojiSlugCache.get(cacheKey);
|
|
119
|
+
if (cached !== undefined)
|
|
120
|
+
return cached;
|
|
121
|
+
try {
|
|
122
|
+
// Strategy A: Iconify search API
|
|
123
|
+
const encoded = encodeURIComponent(emoji);
|
|
124
|
+
const url = `https://api.iconify.design/search?query=${encoded}&prefix=${prefix}&limit=1`;
|
|
125
|
+
const response = await fetch(url);
|
|
126
|
+
if (response.ok) {
|
|
127
|
+
const data = await response.json();
|
|
128
|
+
// Response format: { icons: ["prefix:name", ...] }
|
|
129
|
+
if (data.icons && data.icons.length > 0) {
|
|
130
|
+
const fullId = data.icons[0];
|
|
131
|
+
// Extract just the name part (after the prefix:)
|
|
132
|
+
const colonIndex = fullId.indexOf(':');
|
|
133
|
+
const slug = colonIndex >= 0 ? fullId.substring(colonIndex + 1) : fullId;
|
|
134
|
+
emojiSlugCache.set(cacheKey, slug);
|
|
135
|
+
return slug;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Strategy B fallback: Try codepoint-based name (many sets use this)
|
|
139
|
+
const codepoints = emojiToCodepoints(emoji);
|
|
140
|
+
if (codepoints) {
|
|
141
|
+
// Try fetching directly — if it exists, the codepoint IS the name
|
|
142
|
+
const directUrl = `https://api.iconify.design/${prefix}/${codepoints}.svg`;
|
|
143
|
+
const directResponse = await fetch(directUrl, { method: 'HEAD' });
|
|
144
|
+
if (directResponse.ok) {
|
|
145
|
+
emojiSlugCache.set(cacheKey, codepoints);
|
|
146
|
+
return codepoints;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Not found
|
|
150
|
+
emojiSlugCache.set(cacheKey, null);
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// Network error — don't cache failures
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// ─── Main resolution ────────────────────────────────────────────────────
|
|
159
|
+
/**
|
|
160
|
+
* Resolve an icon prop value to SVG data ready for rendering.
|
|
161
|
+
*
|
|
162
|
+
* Handles all 4 source types: Iconify ID, emoji, inline SVG, data URL.
|
|
163
|
+
* For emoji, auto-maps to an Iconify emoji set unless emojiStyle is 'native'.
|
|
164
|
+
*
|
|
165
|
+
* @param icon - The icon prop value
|
|
166
|
+
* @param emojiStyle - Iconify emoji set prefix (default: DEFAULT_EMOJI_STYLE).
|
|
167
|
+
* Use 'native' to disable auto-mapping and render as <text>.
|
|
168
|
+
*/
|
|
169
|
+
export async function resolveIcon(icon, emojiStyle = DEFAULT_EMOJI_STYLE) {
|
|
170
|
+
const sourceType = detectIconSource(icon);
|
|
171
|
+
switch (sourceType) {
|
|
172
|
+
case 'iconify': {
|
|
173
|
+
const svg = await fetchIconifySvg(icon);
|
|
174
|
+
return {
|
|
175
|
+
svgContent: extractSvgContent(svg),
|
|
176
|
+
viewBox: parseViewBox(svg),
|
|
177
|
+
isMonochrome: detectMonochrome(svg),
|
|
178
|
+
sourceType
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
case 'data-url': {
|
|
182
|
+
const svg = decodeDataUrl(icon);
|
|
183
|
+
return {
|
|
184
|
+
svgContent: extractSvgContent(svg),
|
|
185
|
+
viewBox: parseViewBox(svg),
|
|
186
|
+
isMonochrome: detectMonochrome(svg),
|
|
187
|
+
sourceType
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
case 'svg': {
|
|
191
|
+
const svg = icon.trim();
|
|
192
|
+
return {
|
|
193
|
+
svgContent: extractSvgContent(svg),
|
|
194
|
+
viewBox: parseViewBox(svg),
|
|
195
|
+
isMonochrome: detectMonochrome(svg),
|
|
196
|
+
sourceType
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
case 'emoji': {
|
|
200
|
+
// Auto-map emoji to Iconify SVG icon
|
|
201
|
+
if (emojiStyle !== 'native') {
|
|
202
|
+
const slug = await resolveEmojiSlug(icon, emojiStyle);
|
|
203
|
+
if (slug) {
|
|
204
|
+
const iconId = `${emojiStyle}:${slug}`;
|
|
205
|
+
const svg = await fetchIconifySvg(iconId);
|
|
206
|
+
return {
|
|
207
|
+
svgContent: extractSvgContent(svg),
|
|
208
|
+
viewBox: parseViewBox(svg),
|
|
209
|
+
isMonochrome: detectMonochrome(svg),
|
|
210
|
+
sourceType: 'iconify' // Resolved as Iconify, even though input was emoji
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Fallback: platform-native <text> rendering
|
|
215
|
+
return {
|
|
216
|
+
svgContent: `<text x="50%" y="50%" dominant-baseline="central" text-anchor="middle" font-size="80" font-family="'Apple Color Emoji','Segoe UI Emoji','Noto Color Emoji',sans-serif">${icon}</text>`,
|
|
217
|
+
viewBox: '0 0 100 100',
|
|
218
|
+
isMonochrome: false,
|
|
219
|
+
sourceType
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Superellipse / corner-shape path generation.
|
|
3
|
+
*
|
|
4
|
+
* Follows the CSS Borders Level 4 specification for corner-shape rendering.
|
|
5
|
+
* Maps CornerShape keywords to superellipse parameters and generates SVG paths.
|
|
6
|
+
*
|
|
7
|
+
* The CSS spec defines the curve parametrically:
|
|
8
|
+
* K = 2^abs(curvature)
|
|
9
|
+
* For T in [0, 1]: point = mapPointToCorner(T^K, (1-T)^K)
|
|
10
|
+
*
|
|
11
|
+
* @see https://drafts.csswg.org/css-borders-4/#corner-shape-rendering
|
|
12
|
+
*/
|
|
13
|
+
import type { CornerShape } from './types.js';
|
|
14
|
+
/**
|
|
15
|
+
* Map a CornerShape keyword or superellipse(n) to its numeric curvature parameter.
|
|
16
|
+
*
|
|
17
|
+
* CSS spec keyword -> superellipse parameter:
|
|
18
|
+
* round -> 1 (standard elliptical arc)
|
|
19
|
+
* squircle -> 2 (iOS-style continuous curvature)
|
|
20
|
+
* square -> Infinity (sharp 90 deg corner)
|
|
21
|
+
* bevel -> 0 (straight diagonal cut)
|
|
22
|
+
* scoop -> -1 (inward concave curve)
|
|
23
|
+
* notch -> -Infinity (inward right-angle cut)
|
|
24
|
+
*/
|
|
25
|
+
export declare function cornerShapeToK(shape: CornerShape): number;
|
|
26
|
+
/**
|
|
27
|
+
* Generate an SVG path `d` attribute for a rounded rectangle with a
|
|
28
|
+
* superellipse corner shape.
|
|
29
|
+
*
|
|
30
|
+
* The curve for each corner is sampled parametrically per the CSS Borders L4 spec:
|
|
31
|
+
* K = 2^abs(curvature)
|
|
32
|
+
* point(T) = mapPointToCorner(T^K, (1-T)^K) for T in [0, 1]
|
|
33
|
+
*
|
|
34
|
+
* For convex shapes (curvature > 0), the curve bulges outward.
|
|
35
|
+
* For concave shapes (curvature < 0), the center reference flips to the
|
|
36
|
+
* outer corner, creating inward "scoop" curves.
|
|
37
|
+
*
|
|
38
|
+
* @param size - Square side length in pixels
|
|
39
|
+
* @param cornerRadius - Corner radius as percentage (0-50)
|
|
40
|
+
* @param cornerShape - CornerShape keyword or superellipse(n)
|
|
41
|
+
* @returns SVG path `d` string
|
|
42
|
+
*/
|
|
43
|
+
export declare function generateCornerPath(size: number, cornerRadius: number, cornerShape: CornerShape): string;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Superellipse / corner-shape path generation.
|
|
3
|
+
*
|
|
4
|
+
* Follows the CSS Borders Level 4 specification for corner-shape rendering.
|
|
5
|
+
* Maps CornerShape keywords to superellipse parameters and generates SVG paths.
|
|
6
|
+
*
|
|
7
|
+
* The CSS spec defines the curve parametrically:
|
|
8
|
+
* K = 2^abs(curvature)
|
|
9
|
+
* For T in [0, 1]: point = mapPointToCorner(T^K, (1-T)^K)
|
|
10
|
+
*
|
|
11
|
+
* @see https://drafts.csswg.org/css-borders-4/#corner-shape-rendering
|
|
12
|
+
*/
|
|
13
|
+
/** Number of line segments to sample per corner arc. */
|
|
14
|
+
const SEGMENTS = 64;
|
|
15
|
+
/**
|
|
16
|
+
* Map a CornerShape keyword or superellipse(n) to its numeric curvature parameter.
|
|
17
|
+
*
|
|
18
|
+
* CSS spec keyword -> superellipse parameter:
|
|
19
|
+
* round -> 1 (standard elliptical arc)
|
|
20
|
+
* squircle -> 2 (iOS-style continuous curvature)
|
|
21
|
+
* square -> Infinity (sharp 90 deg corner)
|
|
22
|
+
* bevel -> 0 (straight diagonal cut)
|
|
23
|
+
* scoop -> -1 (inward concave curve)
|
|
24
|
+
* notch -> -Infinity (inward right-angle cut)
|
|
25
|
+
*/
|
|
26
|
+
export function cornerShapeToK(shape) {
|
|
27
|
+
switch (shape) {
|
|
28
|
+
case 'round':
|
|
29
|
+
return 1;
|
|
30
|
+
case 'squircle':
|
|
31
|
+
return 2;
|
|
32
|
+
case 'square':
|
|
33
|
+
return Infinity;
|
|
34
|
+
case 'bevel':
|
|
35
|
+
return 0;
|
|
36
|
+
case 'scoop':
|
|
37
|
+
return -1;
|
|
38
|
+
case 'notch':
|
|
39
|
+
return -Infinity;
|
|
40
|
+
default: {
|
|
41
|
+
// superellipse(n) -- parse the numeric value
|
|
42
|
+
const match = shape.match(/^superellipse\((.+)\)$/);
|
|
43
|
+
if (match) {
|
|
44
|
+
const val = match[1].trim();
|
|
45
|
+
if (val === 'infinity')
|
|
46
|
+
return Infinity;
|
|
47
|
+
if (val === '-infinity')
|
|
48
|
+
return -Infinity;
|
|
49
|
+
const n = Number(val);
|
|
50
|
+
if (!isNaN(n))
|
|
51
|
+
return n;
|
|
52
|
+
}
|
|
53
|
+
return 1; // fallback to round
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Generate an SVG path `d` attribute for a rounded rectangle with a
|
|
59
|
+
* superellipse corner shape.
|
|
60
|
+
*
|
|
61
|
+
* The curve for each corner is sampled parametrically per the CSS Borders L4 spec:
|
|
62
|
+
* K = 2^abs(curvature)
|
|
63
|
+
* point(T) = mapPointToCorner(T^K, (1-T)^K) for T in [0, 1]
|
|
64
|
+
*
|
|
65
|
+
* For convex shapes (curvature > 0), the curve bulges outward.
|
|
66
|
+
* For concave shapes (curvature < 0), the center reference flips to the
|
|
67
|
+
* outer corner, creating inward "scoop" curves.
|
|
68
|
+
*
|
|
69
|
+
* @param size - Square side length in pixels
|
|
70
|
+
* @param cornerRadius - Corner radius as percentage (0-50)
|
|
71
|
+
* @param cornerShape - CornerShape keyword or superellipse(n)
|
|
72
|
+
* @returns SVG path `d` string
|
|
73
|
+
*/
|
|
74
|
+
export function generateCornerPath(size, cornerRadius, cornerShape) {
|
|
75
|
+
// No rounding needed
|
|
76
|
+
if (cornerRadius <= 0) {
|
|
77
|
+
return `M0,0 H${size} V${size} H0 Z`;
|
|
78
|
+
}
|
|
79
|
+
const curvature = cornerShapeToK(cornerShape);
|
|
80
|
+
// square (Infinity): sharp corners, radius is ignored
|
|
81
|
+
if (curvature === Infinity) {
|
|
82
|
+
return `M0,0 H${size} V${size} H0 Z`;
|
|
83
|
+
}
|
|
84
|
+
// Convert percentage to pixels, clamped to half the side
|
|
85
|
+
const r = Math.min((cornerRadius / 100) * size, size / 2);
|
|
86
|
+
// notch (-Infinity): inward right-angle cut at each corner.
|
|
87
|
+
// Goes from tangent point on one edge INWARD to the corner-region center,
|
|
88
|
+
// then back out to the tangent point on the other edge (concave square corner).
|
|
89
|
+
if (curvature === -Infinity) {
|
|
90
|
+
return [
|
|
91
|
+
`M${rd(r)},0`,
|
|
92
|
+
`H${rd(size - r)}`,
|
|
93
|
+
// top-right: inward to center of corner region, then out
|
|
94
|
+
`L${rd(size - r)},${rd(r)}`,
|
|
95
|
+
`L${rd(size)},${rd(r)}`,
|
|
96
|
+
`V${rd(size - r)}`,
|
|
97
|
+
// bottom-right: inward to center, then out
|
|
98
|
+
`L${rd(size - r)},${rd(size - r)}`,
|
|
99
|
+
`L${rd(size - r)},${rd(size)}`,
|
|
100
|
+
`H${rd(r)}`,
|
|
101
|
+
// bottom-left: inward to center, then out
|
|
102
|
+
`L${rd(r)},${rd(size - r)}`,
|
|
103
|
+
`L0,${rd(size - r)}`,
|
|
104
|
+
`V${rd(r)}`,
|
|
105
|
+
// top-left: inward to center, then out
|
|
106
|
+
`L${rd(r)},${rd(r)}`,
|
|
107
|
+
`L${rd(r)},0`,
|
|
108
|
+
`Z`
|
|
109
|
+
].join(' ');
|
|
110
|
+
}
|
|
111
|
+
// bevel (0): straight diagonal cut from tangent to tangent
|
|
112
|
+
if (curvature === 0) {
|
|
113
|
+
return [
|
|
114
|
+
`M${rd(r)},0`,
|
|
115
|
+
`H${rd(size - r)}`,
|
|
116
|
+
`L${rd(size)},${rd(r)}`,
|
|
117
|
+
`V${rd(size - r)}`,
|
|
118
|
+
`L${rd(size - r)},${rd(size)}`,
|
|
119
|
+
`H${rd(r)}`,
|
|
120
|
+
`L0,${rd(size - r)}`,
|
|
121
|
+
`V${rd(r)}`,
|
|
122
|
+
`Z`
|
|
123
|
+
].join(' ');
|
|
124
|
+
}
|
|
125
|
+
// round (1): use SVG arc commands for a perfect quarter-ellipse.
|
|
126
|
+
// This matches what browsers render for border-radius and is exact,
|
|
127
|
+
// unlike the parametric approximation.
|
|
128
|
+
if (curvature === 1) {
|
|
129
|
+
return [
|
|
130
|
+
`M${rd(r)},0`,
|
|
131
|
+
`H${rd(size - r)}`,
|
|
132
|
+
`A${rd(r)},${rd(r)} 0 0 1 ${rd(size)},${rd(r)}`,
|
|
133
|
+
`V${rd(size - r)}`,
|
|
134
|
+
`A${rd(r)},${rd(r)} 0 0 1 ${rd(size - r)},${rd(size)}`,
|
|
135
|
+
`H${rd(r)}`,
|
|
136
|
+
`A${rd(r)},${rd(r)} 0 0 1 0,${rd(size - r)}`,
|
|
137
|
+
`V${rd(r)}`,
|
|
138
|
+
`A${rd(r)},${rd(r)} 0 0 1 ${rd(r)},0`,
|
|
139
|
+
`Z`
|
|
140
|
+
].join(' ');
|
|
141
|
+
}
|
|
142
|
+
// Parametric superellipse curve.
|
|
143
|
+
// K = 2^abs(curvature) per the CSS spec: "Let K be 0.5^(-abs(curvature))"
|
|
144
|
+
const K = Math.pow(2, Math.abs(curvature));
|
|
145
|
+
// Invert: for the outline path (not clip-out), convex shapes use outer corner as ref
|
|
146
|
+
const useOuterAsRef = curvature > 0;
|
|
147
|
+
// Sample the corner arc and return SVG line-to commands.
|
|
148
|
+
//
|
|
149
|
+
// The CSS spec generates a "clip-out" path (what to REMOVE from the rect).
|
|
150
|
+
// Since we generate the OUTLINE path (what to KEEP), we invert the reference:
|
|
151
|
+
// - Convex (squircle, round): curveCenter = outerCorner (curve bows outward)
|
|
152
|
+
// - Concave (scoop): curveCenter = cornerCenter (curve bows inward)
|
|
153
|
+
//
|
|
154
|
+
// The parametric curve maps (T^K, (1-T)^K) through mapPointToCorner,
|
|
155
|
+
// which scales by vectors from curveCenter to the two tangent points.
|
|
156
|
+
const parts = [`M${rd(size - r)},0`];
|
|
157
|
+
// Top-right corner: start=(size-r, 0), end=(size, r), outer=(size, 0), center=(size-r, r)
|
|
158
|
+
parts.push(sampleCorner(size - r, 0, size, r, size, 0, size - r, r, K, useOuterAsRef));
|
|
159
|
+
// Right edge
|
|
160
|
+
parts.push(`L${rd(size)},${rd(size - r)}`);
|
|
161
|
+
// Bottom-right corner: start=(size, size-r), end=(size-r, size), outer=(size, size), center=(size-r, size-r)
|
|
162
|
+
parts.push(sampleCorner(size, size - r, size - r, size, size, size, size - r, size - r, K, useOuterAsRef));
|
|
163
|
+
// Bottom edge
|
|
164
|
+
parts.push(`L${rd(r)},${rd(size)}`);
|
|
165
|
+
// Bottom-left corner: start=(r, size), end=(0, size-r), outer=(0, size), center=(r, size-r)
|
|
166
|
+
parts.push(sampleCorner(r, size, 0, size - r, 0, size, r, size - r, K, useOuterAsRef));
|
|
167
|
+
// Left edge
|
|
168
|
+
parts.push(`L0,${rd(r)}`);
|
|
169
|
+
// Top-left corner: start=(0, r), end=(r, 0), outer=(0, 0), center=(r, r)
|
|
170
|
+
parts.push(sampleCorner(0, r, r, 0, 0, 0, r, r, K, useOuterAsRef));
|
|
171
|
+
parts.push('Z');
|
|
172
|
+
return parts.join(' ');
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Sample a superellipse corner arc as SVG line-to commands.
|
|
176
|
+
*
|
|
177
|
+
* @param sx, sy - Start point (tangent point on the "start" edge)
|
|
178
|
+
* @param ex, ey - End point (tangent point on the "end" edge)
|
|
179
|
+
* @param ox, oy - Outer corner point (the actual rectangle corner)
|
|
180
|
+
* @param cx, cy - Center point (diagonally opposite the outer corner within the corner region)
|
|
181
|
+
* @param K - Exponent: 2^abs(curvature)
|
|
182
|
+
* @param useOuterAsRef - Whether to use outer corner as the curve reference point
|
|
183
|
+
*/
|
|
184
|
+
function sampleCorner(sx, sy, ex, ey, ox, oy, cx, cy, K, useOuterAsRef) {
|
|
185
|
+
// For convex shapes (outline path), use outer corner so curve bows outward.
|
|
186
|
+
// For concave shapes (outline path), use inner center so curve bows inward.
|
|
187
|
+
const refX = useOuterAsRef ? ox : cx;
|
|
188
|
+
const refY = useOuterAsRef ? oy : cy;
|
|
189
|
+
// Vectors from curveCenter to the tangent points
|
|
190
|
+
const toEndX = ex - refX;
|
|
191
|
+
const toEndY = ey - refY;
|
|
192
|
+
const toStartX = sx - refX;
|
|
193
|
+
const toStartY = sy - refY;
|
|
194
|
+
const cmds = [];
|
|
195
|
+
// Sample T from 0 to 1 (exclusive of 0 since we're already at start)
|
|
196
|
+
for (let i = 1; i <= SEGMENTS; i++) {
|
|
197
|
+
const t = i / SEGMENTS;
|
|
198
|
+
const xParam = Math.pow(t, K);
|
|
199
|
+
const yParam = Math.pow(1 - t, K);
|
|
200
|
+
// mapPointToCorner(xParam, yParam):
|
|
201
|
+
// refPoint + toEnd * xParam + toStart * yParam
|
|
202
|
+
const px = refX + toEndX * xParam + toStartX * yParam;
|
|
203
|
+
const py = refY + toEndY * xParam + toStartY * yParam;
|
|
204
|
+
cmds.push(`L${rd(px)},${rd(py)}`);
|
|
205
|
+
}
|
|
206
|
+
return cmds.join(' ');
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Round a number to 2 decimal places for clean SVG output.
|
|
210
|
+
*/
|
|
211
|
+
function rd(n) {
|
|
212
|
+
return Math.round(n * 100) / 100;
|
|
213
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type IconColorMode = 'auto' | 'original' | 'monochrome' | 'grayscale' | 'grayscale-tint' | {
|
|
2
|
+
hue: number;
|
|
3
|
+
saturation?: number;
|
|
4
|
+
};
|
|
5
|
+
export type CornerShape = 'round' | 'squircle' | 'square' | 'bevel' | 'scoop' | 'notch' | `superellipse(${number})`;
|
|
6
|
+
export interface GradientConfig {
|
|
7
|
+
colors: string[];
|
|
8
|
+
stops?: number[];
|
|
9
|
+
angle?: number;
|
|
10
|
+
position?: number;
|
|
11
|
+
scale?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface AppLogoProps {
|
|
14
|
+
icon?: string;
|
|
15
|
+
iconColor?: string;
|
|
16
|
+
iconColorMode?: IconColorMode;
|
|
17
|
+
iconSize?: number;
|
|
18
|
+
iconOffsetX?: number;
|
|
19
|
+
iconOffsetY?: number;
|
|
20
|
+
iconRotation?: number;
|
|
21
|
+
grayscaleLightness?: number;
|
|
22
|
+
cornerRadius?: number;
|
|
23
|
+
cornerShape?: CornerShape;
|
|
24
|
+
background?: string | GradientConfig;
|
|
25
|
+
size?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface AppLogoConfig {
|
|
28
|
+
icon: string;
|
|
29
|
+
iconColor?: string;
|
|
30
|
+
iconColorMode?: IconColorMode;
|
|
31
|
+
background?: AppLogoProps['background'];
|
|
32
|
+
cornerRadius?: number;
|
|
33
|
+
cornerShape?: CornerShape;
|
|
34
|
+
emojiStyle?: string;
|
|
35
|
+
logo?: Partial<AppLogoProps>;
|
|
36
|
+
favicon?: Partial<AppLogoProps>;
|
|
37
|
+
}
|
|
38
|
+
/** Detected source type of the icon prop value */
|
|
39
|
+
export type IconSourceType = 'iconify' | 'emoji' | 'svg' | 'data-url';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2
|
+
<!-- Squircle variant of glow (50% radius, K=2 superellipse) -->
|
|
3
|
+
|
|
4
|
+
<svg
|
|
5
|
+
width="1375.7469"
|
|
6
|
+
height="1375.7469"
|
|
7
|
+
viewBox="0 0 1375.7469 1375.7469"
|
|
8
|
+
version="1.1"
|
|
9
|
+
id="svg1"
|
|
10
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
11
|
+
xmlns:svg="http://www.w3.org/2000/svg">
|
|
12
|
+
<defs
|
|
13
|
+
id="defs1">
|
|
14
|
+
<filter
|
|
15
|
+
style="color-interpolation-filters:sRGB"
|
|
16
|
+
id="filter3"
|
|
17
|
+
x="-0.10799999"
|
|
18
|
+
y="-0.108"
|
|
19
|
+
width="1.216"
|
|
20
|
+
height="1.216">
|
|
21
|
+
<feGaussianBlur
|
|
22
|
+
stdDeviation="50.911691"
|
|
23
|
+
id="feGaussianBlur3" />
|
|
24
|
+
</filter>
|
|
25
|
+
</defs>
|
|
26
|
+
<g
|
|
27
|
+
id="layer1"
|
|
28
|
+
transform="translate(-122.54569,-162.64743)">
|
|
29
|
+
<g
|
|
30
|
+
id="g1"
|
|
31
|
+
transform="translate(253.40376,284.58137)"
|
|
32
|
+
style="display:inline">
|
|
33
|
+
<g
|
|
34
|
+
id="g8"
|
|
35
|
+
style="display:inline">
|
|
36
|
+
<path
|
|
37
|
+
style="opacity:0.7;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter3)"
|
|
38
|
+
id="path2"
|
|
39
|
+
transform="translate(-8.6700182, 0.25409326)"
|
|
40
|
+
d="M565.69,0 L600.22,0 L633.15,0 L664.52,0 L694.39,0.01 L722.8,0.02 L749.81,0.04 L775.45,0.08 L799.78,0.14 L822.83,0.22 L844.67,0.34 L865.32,0.49 L884.84,0.7 L903.27,0.96 L920.64,1.3 L937,1.71 L952.38,2.21 L966.84,2.82 L980.4,3.54 L993.11,4.39 L1004.99,5.39 L1016.1,6.56 L1026.45,7.9 L1036.09,9.44 L1045.05,11.19 L1053.37,13.17 L1061.07,15.41 L1068.18,17.92 L1074.74,20.72 L1080.77,23.85 L1086.31,27.31 L1091.38,31.14 L1096.02,35.36 L1100.23,39.99 L1104.06,45.06 L1107.52,50.6 L1110.65,56.63 L1113.45,63.19 L1115.96,70.31 L1118.2,78 L1120.18,86.32 L1121.94,95.28 L1123.47,104.92 L1124.81,115.27 L1125.98,126.38 L1126.98,138.26 L1127.83,150.97 L1128.55,164.53 L1129.16,178.99 L1129.66,194.37 L1130.08,210.73 L1130.41,228.11 L1130.67,246.53 L1130.88,266.05 L1131.03,286.7 L1131.15,308.54 L1131.23,331.59 L1131.29,355.92 L1131.33,381.56 L1131.35,408.57 L1131.36,436.98 L1131.37,466.85 L1131.37,498.22 L1131.37,531.15 L1131.37,565.69 L1131.37,565.69 L1131.37,600.22 L1131.37,633.15 L1131.37,664.52 L1131.36,694.39 L1131.35,722.8 L1131.33,749.81 L1131.29,775.45 L1131.23,799.78 L1131.15,822.83 L1131.03,844.67 L1130.88,865.32 L1130.67,884.84 L1130.41,903.27 L1130.08,920.64 L1129.66,937 L1129.16,952.38 L1128.55,966.84 L1127.83,980.4 L1126.98,993.11 L1125.98,1004.99 L1124.81,1016.1 L1123.47,1026.45 L1121.94,1036.09 L1120.18,1045.05 L1118.2,1053.37 L1115.96,1061.07 L1113.45,1068.18 L1110.65,1074.74 L1107.52,1080.77 L1104.06,1086.31 L1100.23,1091.38 L1096.02,1096.02 L1091.38,1100.23 L1086.31,1104.06 L1080.77,1107.52 L1074.74,1110.65 L1068.18,1113.45 L1061.07,1115.96 L1053.37,1118.2 L1045.05,1120.18 L1036.09,1121.94 L1026.45,1123.47 L1016.1,1124.81 L1004.99,1125.98 L993.11,1126.98 L980.4,1127.83 L966.84,1128.55 L952.38,1129.16 L937,1129.66 L920.64,1130.08 L903.27,1130.41 L884.84,1130.67 L865.32,1130.88 L844.67,1131.03 L822.83,1131.15 L799.78,1131.23 L775.45,1131.29 L749.81,1131.33 L722.8,1131.35 L694.39,1131.36 L664.52,1131.37 L633.15,1131.37 L600.22,1131.37 L565.69,1131.37 L565.69,1131.37 L531.15,1131.37 L498.22,1131.37 L466.85,1131.37 L436.98,1131.36 L408.57,1131.35 L381.56,1131.33 L355.92,1131.29 L331.59,1131.23 L308.54,1131.15 L286.7,1131.03 L266.05,1130.88 L246.53,1130.67 L228.11,1130.41 L210.73,1130.08 L194.37,1129.66 L178.99,1129.16 L164.53,1128.55 L150.97,1127.83 L138.26,1126.98 L126.38,1125.98 L115.27,1124.81 L104.92,1123.47 L95.28,1121.94 L86.32,1120.18 L78,1118.2 L70.31,1115.96 L63.19,1113.45 L56.63,1110.65 L50.6,1107.52 L45.06,1104.06 L39.99,1100.23 L35.36,1096.02 L31.14,1091.38 L27.31,1086.31 L23.85,1080.77 L20.72,1074.74 L17.92,1068.18 L15.41,1061.07 L13.17,1053.37 L11.19,1045.05 L9.44,1036.09 L7.9,1026.45 L6.56,1016.1 L5.39,1004.99 L4.39,993.11 L3.54,980.4 L2.82,966.84 L2.21,952.38 L1.71,937 L1.3,920.64 L0.96,903.27 L0.7,884.84 L0.49,865.32 L0.34,844.67 L0.22,822.83 L0.14,799.78 L0.08,775.45 L0.04,749.81 L0.02,722.8 L0.01,694.39 L0,664.52 L0,633.15 L0,600.22 L0,565.69 L0,565.69 L0,531.15 L0,498.22 L0,466.85 L0.01,436.98 L0.02,408.57 L0.04,381.56 L0.08,355.92 L0.14,331.59 L0.22,308.54 L0.34,286.7 L0.49,266.05 L0.7,246.53 L0.96,228.11 L1.3,210.73 L1.71,194.37 L2.21,178.99 L2.82,164.53 L3.54,150.97 L4.39,138.26 L5.39,126.38 L6.56,115.27 L7.9,104.92 L9.44,95.28 L11.19,86.32 L13.17,78 L15.41,70.31 L17.92,63.19 L20.72,56.63 L23.85,50.6 L27.31,45.06 L31.14,39.99 L35.36,35.36 L39.99,31.14 L45.06,27.31 L50.6,23.85 L56.63,20.72 L63.19,17.92 L70.31,15.41 L78,13.17 L86.32,11.19 L95.28,9.44 L104.92,7.9 L115.27,6.56 L126.38,5.39 L138.26,4.39 L150.97,3.54 L164.53,2.82 L178.99,2.21 L194.37,1.71 L210.73,1.3 L228.11,0.96 L246.53,0.7 L266.05,0.49 L286.7,0.34 L308.54,0.22 L331.59,0.14 L355.92,0.08 L381.56,0.04 L408.57,0.02 L436.98,0.01 L466.85,0 L498.22,0 L531.15,0 L565.69,0 Z" />
|
|
41
|
+
</g>
|
|
42
|
+
</g>
|
|
43
|
+
</g>
|
|
44
|
+
</svg>
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import LeftiumLogo from './LeftiumLogo.svelte';
|
|
2
|
-
import toggleAnimation from './LeftiumLogo.svelte';
|
|
3
|
-
import setAnimated from './LeftiumLogo.svelte';
|
|
1
|
+
import LeftiumLogo, { toggleAnimation, setAnimated } from './LeftiumLogo.svelte';
|
|
4
2
|
import favicon from './assets/favicon.svg';
|
|
5
3
|
export { LeftiumLogo, toggleAnimation, setAnimated, favicon };
|
|
4
|
+
export { default as AppLogo } from './AppLogo.svelte';
|
|
5
|
+
export type { AppLogoProps, AppLogoConfig, GradientConfig, IconColorMode, CornerShape } from './app-logo/types.js';
|
|
6
|
+
export { generateAppLogoSvg } from './app-logo/generate-svg.js';
|
|
7
|
+
export { generateAppLogoPng } from './app-logo/generate-png.js';
|
|
8
|
+
export { LEFTIUM_GRADIENT } from './app-logo/defaults.js';
|
|
9
|
+
export { generateCornerPath, cornerShapeToK } from './app-logo/squircle.js';
|
|
10
|
+
export { applyColorMode } from './app-logo/color-transform.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
// Reexport your entry components here
|
|
2
|
-
import LeftiumLogo from './LeftiumLogo.svelte';
|
|
3
|
-
import toggleAnimation from './LeftiumLogo.svelte';
|
|
4
|
-
import setAnimated from './LeftiumLogo.svelte';
|
|
2
|
+
import LeftiumLogo, { toggleAnimation, setAnimated } from './LeftiumLogo.svelte';
|
|
5
3
|
import favicon from './assets/favicon.svg';
|
|
6
4
|
export { LeftiumLogo, toggleAnimation, setAnimated, favicon };
|
|
5
|
+
// Phase 1: AppLogo component and generation utilities
|
|
6
|
+
export { default as AppLogo } from './AppLogo.svelte';
|
|
7
|
+
export { generateAppLogoSvg } from './app-logo/generate-svg.js';
|
|
8
|
+
export { generateAppLogoPng } from './app-logo/generate-png.js';
|
|
9
|
+
export { LEFTIUM_GRADIENT } from './app-logo/defaults.js';
|
|
10
|
+
// Phase 2: Advanced styling utilities
|
|
11
|
+
export { generateCornerPath, cornerShapeToK } from './app-logo/squircle.js';
|
|
12
|
+
export { applyColorMode } from './app-logo/color-transform.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a self-contained static SVG for the Leftium logo.
|
|
3
|
+
*
|
|
4
|
+
* Replicates the LeftiumLogo.svelte layout geometry as pure SVG,
|
|
5
|
+
* with all assets inlined as base64 data URIs.
|
|
6
|
+
*/
|
|
7
|
+
export type BoundingBox = 'square' | 'default' | 'encircled' | 'cropped';
|
|
8
|
+
export interface LeftiumLogoConfig {
|
|
9
|
+
/** Output canvas width in pixels. For 'cropped' mode the height will differ. Default: 800 */
|
|
10
|
+
size?: number;
|
|
11
|
+
/** Corner shape of the blue square. Default: true (squircle) */
|
|
12
|
+
squircle?: boolean;
|
|
13
|
+
/** Bounding box mode. Default: 'default' */
|
|
14
|
+
boundingBox?: BoundingBox;
|
|
15
|
+
/** Background fill. 'transparent' or a CSS color string e.g. '#ffffff'. Default: 'transparent' */
|
|
16
|
+
background?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Generate a complete, self-contained SVG string for the static Leftium logo.
|
|
20
|
+
*
|
|
21
|
+
* All asset SVGs are inlined as base64 data URIs so the output is a single
|
|
22
|
+
* portable file with no external dependencies.
|
|
23
|
+
*/
|
|
24
|
+
export declare function generateLeftiumLogoSvg(config?: LeftiumLogoConfig): string;
|
|
25
|
+
/**
|
|
26
|
+
* Rasterise the Leftium logo to a PNG or WebP Blob.
|
|
27
|
+
* Browser-only (requires canvas + Image).
|
|
28
|
+
*/
|
|
29
|
+
export declare function generateLeftiumLogoPng(config: LeftiumLogoConfig, format?: 'png' | 'webp'): Promise<Blob>;
|