@forgespace/branding-mcp 0.4.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/.env.example +3 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +22 -0
- package/.github/workflows/ci.yml +73 -0
- package/.github/workflows/release-automation.yml +56 -0
- package/.github/workflows/security-scan.yml +37 -0
- package/.gitleaks.toml +14 -0
- package/.prettierrc.json +10 -0
- package/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +203 -0
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/data/README.md +13 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/branding-core/ai/brand-interpreter.d.ts +5 -0
- package/dist/lib/branding-core/ai/brand-interpreter.d.ts.map +1 -0
- package/dist/lib/branding-core/ai/brand-interpreter.js +16 -0
- package/dist/lib/branding-core/ai/brand-interpreter.js.map +1 -0
- package/dist/lib/branding-core/ai/claude-interpreter.d.ts +5 -0
- package/dist/lib/branding-core/ai/claude-interpreter.d.ts.map +1 -0
- package/dist/lib/branding-core/ai/claude-interpreter.js +55 -0
- package/dist/lib/branding-core/ai/claude-interpreter.js.map +1 -0
- package/dist/lib/branding-core/ai/intent-applier.d.ts +4 -0
- package/dist/lib/branding-core/ai/intent-applier.d.ts.map +1 -0
- package/dist/lib/branding-core/ai/intent-applier.js +29 -0
- package/dist/lib/branding-core/ai/intent-applier.js.map +1 -0
- package/dist/lib/branding-core/ai/keyword-interpreter.d.ts +4 -0
- package/dist/lib/branding-core/ai/keyword-interpreter.d.ts.map +1 -0
- package/dist/lib/branding-core/ai/keyword-interpreter.js +85 -0
- package/dist/lib/branding-core/ai/keyword-interpreter.js.map +1 -0
- package/dist/lib/branding-core/ai/prompts.d.ts +4 -0
- package/dist/lib/branding-core/ai/prompts.d.ts.map +1 -0
- package/dist/lib/branding-core/ai/prompts.js +79 -0
- package/dist/lib/branding-core/ai/prompts.js.map +1 -0
- package/dist/lib/branding-core/ai/types.d.ts +27 -0
- package/dist/lib/branding-core/ai/types.d.ts.map +1 -0
- package/dist/lib/branding-core/ai/types.js +2 -0
- package/dist/lib/branding-core/ai/types.js.map +1 -0
- package/dist/lib/branding-core/documents/html-generator.d.ts +3 -0
- package/dist/lib/branding-core/documents/html-generator.d.ts.map +1 -0
- package/dist/lib/branding-core/documents/html-generator.js +31 -0
- package/dist/lib/branding-core/documents/html-generator.js.map +1 -0
- package/dist/lib/branding-core/documents/pdf-generator.d.ts +3 -0
- package/dist/lib/branding-core/documents/pdf-generator.d.ts.map +1 -0
- package/dist/lib/branding-core/documents/pdf-generator.js +20 -0
- package/dist/lib/branding-core/documents/pdf-generator.js.map +1 -0
- package/dist/lib/branding-core/exporters/css-variables.d.ts +3 -0
- package/dist/lib/branding-core/exporters/css-variables.d.ts.map +1 -0
- package/dist/lib/branding-core/exporters/css-variables.js +62 -0
- package/dist/lib/branding-core/exporters/css-variables.js.map +1 -0
- package/dist/lib/branding-core/exporters/design-tokens.d.ts +3 -0
- package/dist/lib/branding-core/exporters/design-tokens.d.ts.map +1 -0
- package/dist/lib/branding-core/exporters/design-tokens.js +75 -0
- package/dist/lib/branding-core/exporters/design-tokens.js.map +1 -0
- package/dist/lib/branding-core/exporters/figma-tokens.d.ts +9 -0
- package/dist/lib/branding-core/exporters/figma-tokens.d.ts.map +1 -0
- package/dist/lib/branding-core/exporters/figma-tokens.js +69 -0
- package/dist/lib/branding-core/exporters/figma-tokens.js.map +1 -0
- package/dist/lib/branding-core/exporters/react-theme.d.ts +3 -0
- package/dist/lib/branding-core/exporters/react-theme.d.ts.map +1 -0
- package/dist/lib/branding-core/exporters/react-theme.js +61 -0
- package/dist/lib/branding-core/exporters/react-theme.js.map +1 -0
- package/dist/lib/branding-core/exporters/sass-variables.d.ts +3 -0
- package/dist/lib/branding-core/exporters/sass-variables.d.ts.map +1 -0
- package/dist/lib/branding-core/exporters/sass-variables.js +65 -0
- package/dist/lib/branding-core/exporters/sass-variables.js.map +1 -0
- package/dist/lib/branding-core/exporters/tailwind-preset.d.ts +3 -0
- package/dist/lib/branding-core/exporters/tailwind-preset.d.ts.map +1 -0
- package/dist/lib/branding-core/exporters/tailwind-preset.js +55 -0
- package/dist/lib/branding-core/exporters/tailwind-preset.js.map +1 -0
- package/dist/lib/branding-core/generators/border-system.d.ts +3 -0
- package/dist/lib/branding-core/generators/border-system.d.ts.map +1 -0
- package/dist/lib/branding-core/generators/border-system.js +37 -0
- package/dist/lib/branding-core/generators/border-system.js.map +1 -0
- package/dist/lib/branding-core/generators/color-palette.d.ts +7 -0
- package/dist/lib/branding-core/generators/color-palette.d.ts.map +1 -0
- package/dist/lib/branding-core/generators/color-palette.js +117 -0
- package/dist/lib/branding-core/generators/color-palette.js.map +1 -0
- package/dist/lib/branding-core/generators/favicon-generator.d.ts +3 -0
- package/dist/lib/branding-core/generators/favicon-generator.d.ts.map +1 -0
- package/dist/lib/branding-core/generators/favicon-generator.js +23 -0
- package/dist/lib/branding-core/generators/favicon-generator.js.map +1 -0
- package/dist/lib/branding-core/generators/gradient-system.d.ts +3 -0
- package/dist/lib/branding-core/generators/gradient-system.d.ts.map +1 -0
- package/dist/lib/branding-core/generators/gradient-system.js +74 -0
- package/dist/lib/branding-core/generators/gradient-system.js.map +1 -0
- package/dist/lib/branding-core/generators/logo-generator.d.ts +4 -0
- package/dist/lib/branding-core/generators/logo-generator.d.ts.map +1 -0
- package/dist/lib/branding-core/generators/logo-generator.js +130 -0
- package/dist/lib/branding-core/generators/logo-generator.js.map +1 -0
- package/dist/lib/branding-core/generators/motion-system.d.ts +3 -0
- package/dist/lib/branding-core/generators/motion-system.d.ts.map +1 -0
- package/dist/lib/branding-core/generators/motion-system.js +91 -0
- package/dist/lib/branding-core/generators/motion-system.js.map +1 -0
- package/dist/lib/branding-core/generators/og-image-generator.d.ts +3 -0
- package/dist/lib/branding-core/generators/og-image-generator.d.ts.map +1 -0
- package/dist/lib/branding-core/generators/og-image-generator.js +72 -0
- package/dist/lib/branding-core/generators/og-image-generator.js.map +1 -0
- package/dist/lib/branding-core/generators/shadow-system.d.ts +3 -0
- package/dist/lib/branding-core/generators/shadow-system.d.ts.map +1 -0
- package/dist/lib/branding-core/generators/shadow-system.js +44 -0
- package/dist/lib/branding-core/generators/shadow-system.js.map +1 -0
- package/dist/lib/branding-core/generators/spacing-scale.d.ts +3 -0
- package/dist/lib/branding-core/generators/spacing-scale.d.ts.map +1 -0
- package/dist/lib/branding-core/generators/spacing-scale.js +27 -0
- package/dist/lib/branding-core/generators/spacing-scale.js.map +1 -0
- package/dist/lib/branding-core/generators/typography-system.d.ts +3 -0
- package/dist/lib/branding-core/generators/typography-system.d.ts.map +1 -0
- package/dist/lib/branding-core/generators/typography-system.js +121 -0
- package/dist/lib/branding-core/generators/typography-system.js.map +1 -0
- package/dist/lib/branding-core/index.d.ts +24 -0
- package/dist/lib/branding-core/index.d.ts.map +1 -0
- package/dist/lib/branding-core/index.js +22 -0
- package/dist/lib/branding-core/index.js.map +1 -0
- package/dist/lib/branding-core/validators/brand-consistency.d.ts +3 -0
- package/dist/lib/branding-core/validators/brand-consistency.d.ts.map +1 -0
- package/dist/lib/branding-core/validators/brand-consistency.js +70 -0
- package/dist/lib/branding-core/validators/brand-consistency.js.map +1 -0
- package/dist/lib/branding-core/validators/contrast-checker.d.ts +3 -0
- package/dist/lib/branding-core/validators/contrast-checker.d.ts.map +1 -0
- package/dist/lib/branding-core/validators/contrast-checker.js +33 -0
- package/dist/lib/branding-core/validators/contrast-checker.js.map +1 -0
- package/dist/lib/branding-core/validators/token-schema.d.ts +10 -0
- package/dist/lib/branding-core/validators/token-schema.d.ts.map +1 -0
- package/dist/lib/branding-core/validators/token-schema.js +43 -0
- package/dist/lib/branding-core/validators/token-schema.js.map +1 -0
- package/dist/lib/config.d.ts +7 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +8 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/logger.d.ts +3 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +10 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/types.d.ts +208 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/resources/brand-knowledge.d.ts +3 -0
- package/dist/resources/brand-knowledge.d.ts.map +1 -0
- package/dist/resources/brand-knowledge.js +53 -0
- package/dist/resources/brand-knowledge.js.map +1 -0
- package/dist/resources/brand-templates.d.ts +3 -0
- package/dist/resources/brand-templates.d.ts.map +1 -0
- package/dist/resources/brand-templates.js +68 -0
- package/dist/resources/brand-templates.js.map +1 -0
- package/dist/tools/create-brand-guidelines.d.ts +3 -0
- package/dist/tools/create-brand-guidelines.d.ts.map +1 -0
- package/dist/tools/create-brand-guidelines.js +85 -0
- package/dist/tools/create-brand-guidelines.js.map +1 -0
- package/dist/tools/export-design-tokens.d.ts +3 -0
- package/dist/tools/export-design-tokens.d.ts.map +1 -0
- package/dist/tools/export-design-tokens.js +37 -0
- package/dist/tools/export-design-tokens.js.map +1 -0
- package/dist/tools/generate-brand-assets.d.ts +3 -0
- package/dist/tools/generate-brand-assets.d.ts.map +1 -0
- package/dist/tools/generate-brand-assets.js +37 -0
- package/dist/tools/generate-brand-assets.js.map +1 -0
- package/dist/tools/generate-brand-identity.d.ts +3 -0
- package/dist/tools/generate-brand-identity.d.ts.map +1 -0
- package/dist/tools/generate-brand-identity.js +73 -0
- package/dist/tools/generate-brand-identity.js.map +1 -0
- package/dist/tools/generate-color-palette.d.ts +3 -0
- package/dist/tools/generate-color-palette.d.ts.map +1 -0
- package/dist/tools/generate-color-palette.js +33 -0
- package/dist/tools/generate-color-palette.js.map +1 -0
- package/dist/tools/generate-typography-system.d.ts +3 -0
- package/dist/tools/generate-typography-system.d.ts.map +1 -0
- package/dist/tools/generate-typography-system.js +28 -0
- package/dist/tools/generate-typography-system.js.map +1 -0
- package/dist/tools/refine-brand-element.d.ts +3 -0
- package/dist/tools/refine-brand-element.d.ts.map +1 -0
- package/dist/tools/refine-brand-element.js +41 -0
- package/dist/tools/refine-brand-element.js.map +1 -0
- package/dist/tools/validate-brand-consistency.d.ts +3 -0
- package/dist/tools/validate-brand-consistency.d.ts.map +1 -0
- package/dist/tools/validate-brand-consistency.js +25 -0
- package/dist/tools/validate-brand-consistency.js.map +1 -0
- package/docs/API.md +110 -0
- package/docs/DATA_SOURCES.md +69 -0
- package/docs/INTEGRATION.md +58 -0
- package/eslint.config.js +52 -0
- package/jest.config.js +40 -0
- package/package.json +78 -0
- package/src/__tests__/integration/brand-generation.test.ts +84 -0
- package/src/__tests__/integration/mcp-server.test.ts +18 -0
- package/src/__tests__/unit/ai-interpreter.test.ts +172 -0
- package/src/__tests__/unit/border-system.test.ts +77 -0
- package/src/__tests__/unit/color-palette.test.ts +161 -0
- package/src/__tests__/unit/contrast-checker.test.ts +124 -0
- package/src/__tests__/unit/design-tokens.test.ts +184 -0
- package/src/__tests__/unit/favicon-generator.test.ts +80 -0
- package/src/__tests__/unit/gradient-system.test.ts +122 -0
- package/src/__tests__/unit/logo-generator.test.ts +146 -0
- package/src/__tests__/unit/motion-system.test.ts +91 -0
- package/src/__tests__/unit/og-image-generator.test.ts +115 -0
- package/src/__tests__/unit/shadow-system.test.ts +63 -0
- package/src/__tests__/unit/spacing-scale.test.ts +60 -0
- package/src/__tests__/unit/typography-system.test.ts +71 -0
- package/src/index.ts +59 -0
- package/src/lib/branding-core/ai/brand-interpreter.ts +30 -0
- package/src/lib/branding-core/ai/claude-interpreter.ts +76 -0
- package/src/lib/branding-core/ai/intent-applier.ts +59 -0
- package/src/lib/branding-core/ai/keyword-interpreter.ts +95 -0
- package/src/lib/branding-core/ai/prompts.ts +93 -0
- package/src/lib/branding-core/ai/types.ts +36 -0
- package/src/lib/branding-core/documents/html-generator.ts +32 -0
- package/src/lib/branding-core/documents/pdf-generator.ts +21 -0
- package/src/lib/branding-core/exporters/css-variables.ts +71 -0
- package/src/lib/branding-core/exporters/design-tokens.ts +86 -0
- package/src/lib/branding-core/exporters/figma-tokens.ts +87 -0
- package/src/lib/branding-core/exporters/react-theme.ts +69 -0
- package/src/lib/branding-core/exporters/sass-variables.ts +74 -0
- package/src/lib/branding-core/exporters/tailwind-preset.ts +67 -0
- package/src/lib/branding-core/generators/border-system.ts +41 -0
- package/src/lib/branding-core/generators/color-palette.ts +147 -0
- package/src/lib/branding-core/generators/favicon-generator.ts +33 -0
- package/src/lib/branding-core/generators/gradient-system.ts +120 -0
- package/src/lib/branding-core/generators/logo-generator.ts +152 -0
- package/src/lib/branding-core/generators/motion-system.ts +98 -0
- package/src/lib/branding-core/generators/og-image-generator.ts +97 -0
- package/src/lib/branding-core/generators/shadow-system.ts +66 -0
- package/src/lib/branding-core/generators/spacing-scale.ts +29 -0
- package/src/lib/branding-core/generators/typography-system.ts +128 -0
- package/src/lib/branding-core/index.ts +28 -0
- package/src/lib/branding-core/validators/brand-consistency.ts +79 -0
- package/src/lib/branding-core/validators/contrast-checker.ts +37 -0
- package/src/lib/branding-core/validators/token-schema.ts +50 -0
- package/src/lib/config.ts +13 -0
- package/src/lib/logger.ts +12 -0
- package/src/lib/types.ts +236 -0
- package/src/resources/brand-knowledge.ts +60 -0
- package/src/resources/brand-templates.ts +70 -0
- package/src/tools/create-brand-guidelines.ts +94 -0
- package/src/tools/export-design-tokens.ts +52 -0
- package/src/tools/generate-brand-assets.ts +48 -0
- package/src/tools/generate-brand-identity.ts +115 -0
- package/src/tools/generate-color-palette.ts +43 -0
- package/src/tools/generate-typography-system.ts +42 -0
- package/src/tools/refine-brand-element.ts +65 -0
- package/src/tools/validate-brand-consistency.ts +32 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ColorHarmony,
|
|
3
|
+
ColorPalette,
|
|
4
|
+
ColorSwatch,
|
|
5
|
+
ColorTheme,
|
|
6
|
+
ContrastResult,
|
|
7
|
+
HslColor,
|
|
8
|
+
} from '../../types.js';
|
|
9
|
+
|
|
10
|
+
const HARMONY_ANGLES: Record<ColorHarmony, number[]> = {
|
|
11
|
+
complementary: [180],
|
|
12
|
+
analogous: [-30, 30],
|
|
13
|
+
triadic: [120, 240],
|
|
14
|
+
'split-complementary': [150, 210],
|
|
15
|
+
tetradic: [90, 180, 270],
|
|
16
|
+
monochromatic: [0, 0],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function hslToHex(h: number, s: number, l: number): string {
|
|
20
|
+
const hNorm = ((h % 360) + 360) % 360;
|
|
21
|
+
const sNorm = s / 100;
|
|
22
|
+
const lNorm = l / 100;
|
|
23
|
+
const a = sNorm * Math.min(lNorm, 1 - lNorm);
|
|
24
|
+
const f = (n: number): string => {
|
|
25
|
+
const k = (n + hNorm / 30) % 12;
|
|
26
|
+
const color = lNorm - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
|
27
|
+
return Math.round(255 * color)
|
|
28
|
+
.toString(16)
|
|
29
|
+
.padStart(2, '0');
|
|
30
|
+
};
|
|
31
|
+
return `#${f(0)}${f(8)}${f(4)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hexToHsl(hex: string): HslColor {
|
|
35
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
36
|
+
if (!result) return { h: 0, s: 0, l: 0 };
|
|
37
|
+
const r = parseInt(result[1], 16) / 255;
|
|
38
|
+
const g = parseInt(result[2], 16) / 255;
|
|
39
|
+
const b = parseInt(result[3], 16) / 255;
|
|
40
|
+
const max = Math.max(r, g, b);
|
|
41
|
+
const min = Math.min(r, g, b);
|
|
42
|
+
const l = (max + min) / 2;
|
|
43
|
+
if (max === min) return { h: 0, s: 0, l: Math.round(l * 100) };
|
|
44
|
+
const d = max - min;
|
|
45
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
46
|
+
let h = 0;
|
|
47
|
+
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
48
|
+
else if (max === g) h = ((b - r) / d + 2) / 6;
|
|
49
|
+
else h = ((r - g) / d + 4) / 6;
|
|
50
|
+
return {
|
|
51
|
+
h: Math.round(h * 360),
|
|
52
|
+
s: Math.round(s * 100),
|
|
53
|
+
l: Math.round(l * 100),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function relativeLuminance(hex: string): number {
|
|
58
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
59
|
+
if (!result) return 0;
|
|
60
|
+
const channels = [result[1], result[2], result[3]].map((c) => {
|
|
61
|
+
const v = parseInt(c, 16) / 255;
|
|
62
|
+
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
63
|
+
});
|
|
64
|
+
return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function checkContrast(fg: string, bg: string): ContrastResult {
|
|
68
|
+
const l1 = relativeLuminance(fg);
|
|
69
|
+
const l2 = relativeLuminance(bg);
|
|
70
|
+
const lighter = Math.max(l1, l2);
|
|
71
|
+
const darker = Math.min(l1, l2);
|
|
72
|
+
const ratio = (lighter + 0.05) / (darker + 0.05);
|
|
73
|
+
return {
|
|
74
|
+
ratio: Math.round(ratio * 100) / 100,
|
|
75
|
+
aa: ratio >= 4.5,
|
|
76
|
+
aaLarge: ratio >= 3,
|
|
77
|
+
aaa: ratio >= 7,
|
|
78
|
+
aaaLarge: ratio >= 4.5,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function generateNeutrals(baseHue: number, theme: ColorTheme): ColorSwatch[] {
|
|
83
|
+
const steps = [95, 90, 80, 60, 40, 20, 10, 5];
|
|
84
|
+
if (theme === 'dark') steps.reverse();
|
|
85
|
+
return steps.map((l, i) => ({
|
|
86
|
+
name: `neutral-${(i + 1) * 100}`,
|
|
87
|
+
hex: hslToHex(baseHue, 5, l),
|
|
88
|
+
hsl: { h: baseHue, s: 5, l },
|
|
89
|
+
usage: `Neutral shade ${i + 1}`,
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function makeSwatch(name: string, h: number, s: number, l: number, usage: string): ColorSwatch {
|
|
94
|
+
return {
|
|
95
|
+
name,
|
|
96
|
+
hex: hslToHex(h, s, l),
|
|
97
|
+
hsl: { h: ((h % 360) + 360) % 360, s, l },
|
|
98
|
+
usage,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function generateColorPalette(
|
|
103
|
+
baseColor?: string,
|
|
104
|
+
harmony: ColorHarmony = 'complementary',
|
|
105
|
+
theme: ColorTheme = 'both'
|
|
106
|
+
): ColorPalette {
|
|
107
|
+
const base = baseColor ? hexToHsl(baseColor) : { h: 250, s: 65, l: 50 };
|
|
108
|
+
const angles = HARMONY_ANGLES[harmony];
|
|
109
|
+
|
|
110
|
+
const primary = makeSwatch('primary', base.h, base.s, base.l, 'Primary brand color');
|
|
111
|
+
const secondary = makeSwatch(
|
|
112
|
+
'secondary',
|
|
113
|
+
base.h + (angles[0] ?? 180),
|
|
114
|
+
base.s - 10,
|
|
115
|
+
base.l + 5,
|
|
116
|
+
'Secondary brand color'
|
|
117
|
+
);
|
|
118
|
+
const accent = makeSwatch(
|
|
119
|
+
'accent',
|
|
120
|
+
base.h + (angles[1] ?? angles[0] ?? 120),
|
|
121
|
+
Math.min(base.s + 15, 100),
|
|
122
|
+
base.l,
|
|
123
|
+
'Accent and highlight color'
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const neutrals = generateNeutrals(base.h, theme);
|
|
127
|
+
|
|
128
|
+
const semantic = {
|
|
129
|
+
success: makeSwatch('success', 142, 70, 45, 'Success states'),
|
|
130
|
+
warning: makeSwatch('warning', 38, 92, 50, 'Warning states'),
|
|
131
|
+
error: makeSwatch('error', 0, 84, 60, 'Error states'),
|
|
132
|
+
info: makeSwatch('info', 210, 79, 56, 'Informational states'),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const white = '#ffffff';
|
|
136
|
+
const dark = '#1a1a1a';
|
|
137
|
+
const contrast: Record<string, ContrastResult> = {
|
|
138
|
+
'primary-on-white': checkContrast(primary.hex, white),
|
|
139
|
+
'primary-on-dark': checkContrast(primary.hex, dark),
|
|
140
|
+
'secondary-on-white': checkContrast(secondary.hex, white),
|
|
141
|
+
'accent-on-white': checkContrast(accent.hex, white),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
return { primary, secondary, accent, neutral: neutrals, semantic, contrast };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { hslToHex, hexToHsl };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { FaviconSet, FaviconSize } from '../../types.js';
|
|
2
|
+
|
|
3
|
+
function rescaleSvg(iconSvg: string, size: FaviconSize, brandColor: string): string {
|
|
4
|
+
const strokeScale = size <= 32 ? 2 : 1;
|
|
5
|
+
let svg = iconSvg
|
|
6
|
+
.replace(/width="[^"]*"/, `width="${size}"`)
|
|
7
|
+
.replace(/height="[^"]*"/, `height="${size}"`)
|
|
8
|
+
.replace(/viewBox="[^"]*"/, `viewBox="0 0 64 64"`);
|
|
9
|
+
|
|
10
|
+
if (strokeScale > 1) {
|
|
11
|
+
svg = svg.replace(
|
|
12
|
+
/stroke-width="(\d+)"/g,
|
|
13
|
+
(_, w) => `stroke-width="${Number(w) * strokeScale}"`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!svg.includes('fill=') && !svg.includes('<circle')) {
|
|
18
|
+
svg = svg.replace('<svg ', `<svg style="color:${brandColor}" `);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return svg;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function generateFavicons(iconSvg: string, brandColor: string): FaviconSet {
|
|
25
|
+
const sizes: FaviconSize[] = [16, 32, 180, 512];
|
|
26
|
+
const result = {} as Record<FaviconSize, string>;
|
|
27
|
+
|
|
28
|
+
for (const size of sizes) {
|
|
29
|
+
result[size] = rescaleSvg(iconSvg, size, brandColor);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { sizes: result };
|
|
33
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BrandStyle,
|
|
3
|
+
ColorPalette,
|
|
4
|
+
Gradient,
|
|
5
|
+
GradientPresetName,
|
|
6
|
+
GradientStop,
|
|
7
|
+
GradientSystem,
|
|
8
|
+
GradientType,
|
|
9
|
+
HslColor,
|
|
10
|
+
} from '../../types.js';
|
|
11
|
+
import { hexToHsl, hslToHex } from './color-palette.js';
|
|
12
|
+
|
|
13
|
+
interface StyleGradientConfig {
|
|
14
|
+
type: GradientType;
|
|
15
|
+
angle: number;
|
|
16
|
+
stops: number;
|
|
17
|
+
lightnessVariance: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const STYLE_GRADIENT_CONFIG: Record<BrandStyle, StyleGradientConfig> = {
|
|
21
|
+
minimal: { type: 'linear', angle: 180, stops: 2, lightnessVariance: 5 },
|
|
22
|
+
bold: { type: 'linear', angle: 45, stops: 3, lightnessVariance: 25 },
|
|
23
|
+
elegant: { type: 'linear', angle: 135, stops: 2, lightnessVariance: 10 },
|
|
24
|
+
playful: { type: 'conic', angle: 0, stops: 3, lightnessVariance: 20 },
|
|
25
|
+
corporate: { type: 'linear', angle: 180, stops: 2, lightnessVariance: 8 },
|
|
26
|
+
tech: { type: 'linear', angle: 315, stops: 2, lightnessVariance: 15 },
|
|
27
|
+
organic: { type: 'radial', angle: 0, stops: 3, lightnessVariance: 12 },
|
|
28
|
+
retro: { type: 'linear', angle: 90, stops: 3, lightnessVariance: 18 },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function shiftLightness(hsl: HslColor, amount: number): string {
|
|
32
|
+
const l = Math.max(0, Math.min(100, hsl.l + amount));
|
|
33
|
+
return hslToHex(hsl.h, hsl.s, l);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildStops(
|
|
37
|
+
baseHex: string,
|
|
38
|
+
endHex: string,
|
|
39
|
+
count: number,
|
|
40
|
+
variance: number
|
|
41
|
+
): GradientStop[] {
|
|
42
|
+
const baseHsl = hexToHsl(baseHex);
|
|
43
|
+
const endHsl = hexToHsl(endHex);
|
|
44
|
+
if (count === 2) {
|
|
45
|
+
return [
|
|
46
|
+
{ color: baseHex, position: 0 },
|
|
47
|
+
{ color: endHex, position: 100 },
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
const midHex = shiftLightness(
|
|
51
|
+
{ h: (baseHsl.h + endHsl.h) / 2, s: baseHsl.s, l: baseHsl.l },
|
|
52
|
+
variance / 2
|
|
53
|
+
);
|
|
54
|
+
return [
|
|
55
|
+
{ color: baseHex, position: 0 },
|
|
56
|
+
{ color: midHex, position: 50 },
|
|
57
|
+
{ color: endHex, position: 100 },
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function toCssValue(type: GradientType, angle: number, stops: GradientStop[]): string {
|
|
62
|
+
const stopStr = stops.map((s) => `${s.color} ${s.position}%`).join(', ');
|
|
63
|
+
if (type === 'radial') return `radial-gradient(circle, ${stopStr})`;
|
|
64
|
+
if (type === 'conic') return `conic-gradient(from ${angle}deg, ${stopStr})`;
|
|
65
|
+
return `linear-gradient(${angle}deg, ${stopStr})`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createGradient(type: GradientType, angle: number, stops: GradientStop[]): Gradient {
|
|
69
|
+
return {
|
|
70
|
+
type,
|
|
71
|
+
angle: type !== 'radial' ? angle : undefined,
|
|
72
|
+
stops,
|
|
73
|
+
cssValue: toCssValue(type, angle, stops),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildPreset(
|
|
78
|
+
name: GradientPresetName,
|
|
79
|
+
colors: ColorPalette,
|
|
80
|
+
config: StyleGradientConfig
|
|
81
|
+
): Gradient {
|
|
82
|
+
const { type, angle, stops: count, lightnessVariance } = config;
|
|
83
|
+
const primary = colors.primary.hex;
|
|
84
|
+
const secondary = colors.secondary.hex;
|
|
85
|
+
const accent = colors.accent.hex;
|
|
86
|
+
const neutralLight = colors.neutral[0]?.hex ?? '#f5f5f5';
|
|
87
|
+
const neutralDark = colors.neutral[colors.neutral.length - 1]?.hex ?? '#1a1a1a';
|
|
88
|
+
|
|
89
|
+
const presetMap: Record<GradientPresetName, () => GradientStop[]> = {
|
|
90
|
+
hero: () => buildStops(primary, secondary, count, lightnessVariance),
|
|
91
|
+
button: () => buildStops(accent, primary, count, lightnessVariance),
|
|
92
|
+
card: () =>
|
|
93
|
+
buildStops(
|
|
94
|
+
neutralLight,
|
|
95
|
+
shiftLightness(hexToHsl(neutralLight), -lightnessVariance),
|
|
96
|
+
2,
|
|
97
|
+
lightnessVariance
|
|
98
|
+
),
|
|
99
|
+
text: () => buildStops(primary, accent, count, lightnessVariance),
|
|
100
|
+
background: () => buildStops(neutralLight, neutralDark, 2, lightnessVariance),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const stops = presetMap[name]();
|
|
104
|
+
return createGradient(type, angle, stops);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function generateGradientSystem(
|
|
108
|
+
colors: ColorPalette,
|
|
109
|
+
style: BrandStyle = 'minimal'
|
|
110
|
+
): GradientSystem {
|
|
111
|
+
const config = STYLE_GRADIENT_CONFIG[style];
|
|
112
|
+
const presetNames: GradientPresetName[] = ['hero', 'button', 'card', 'text', 'background'];
|
|
113
|
+
|
|
114
|
+
const presets = {} as Record<GradientPresetName, Gradient>;
|
|
115
|
+
for (const name of presetNames) {
|
|
116
|
+
presets[name] = buildPreset(name, colors, config);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { presets };
|
|
120
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { BrandStyle, LogoConfig, LogoOutput } from '../../types.js';
|
|
2
|
+
|
|
3
|
+
export function defaultLogoConfig(brandName: string, primaryColor: string): LogoConfig {
|
|
4
|
+
return {
|
|
5
|
+
text: brandName,
|
|
6
|
+
font: 'Inter',
|
|
7
|
+
fontSize: 48,
|
|
8
|
+
color: primaryColor,
|
|
9
|
+
backgroundColor: 'transparent',
|
|
10
|
+
width: 400,
|
|
11
|
+
height: 120,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function generateWordmark(config: LogoConfig): string {
|
|
16
|
+
const { text, font, fontSize, color, backgroundColor, width, height } = config;
|
|
17
|
+
const initial = text.charAt(0).toUpperCase();
|
|
18
|
+
const circleR = height * 0.35;
|
|
19
|
+
const circleX = circleR + 20;
|
|
20
|
+
const circleY = height / 2;
|
|
21
|
+
const textX = circleX + circleR + 16;
|
|
22
|
+
const textY = height / 2;
|
|
23
|
+
const fontFamily = `'${font}', sans-serif`;
|
|
24
|
+
|
|
25
|
+
return [
|
|
26
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="400" height="120" viewBox="0 0 400 120">`,
|
|
27
|
+
backgroundColor !== 'transparent'
|
|
28
|
+
? ` <rect width="${width}" height="${height}" fill="${backgroundColor}" rx="8"/>`
|
|
29
|
+
: '',
|
|
30
|
+
` <circle cx="${circleX}" cy="${circleY}" r="${circleR}" fill="${color}"/>`,
|
|
31
|
+
` <text x="${circleX}" y="${circleY}" fill="white" font-size="${fontSize * 0.7}" font-family="${fontFamily}" font-weight="700" text-anchor="middle" dominant-baseline="central">${initial}</text>`,
|
|
32
|
+
` <text x="${textX}" y="${textY}" fill="${color}" font-size="${fontSize}" font-family="${fontFamily}" font-weight="600" dominant-baseline="central">${text}</text>`,
|
|
33
|
+
'</svg>',
|
|
34
|
+
]
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const MONOGRAM_SHAPES: Record<BrandStyle, string> = {
|
|
40
|
+
minimal: 'roundedSquare',
|
|
41
|
+
bold: 'hexagon',
|
|
42
|
+
elegant: 'thinCircle',
|
|
43
|
+
playful: 'blob',
|
|
44
|
+
corporate: 'rectangle',
|
|
45
|
+
tech: 'diamond',
|
|
46
|
+
organic: 'ellipse',
|
|
47
|
+
retro: 'octagon',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function monogramContainer(style: BrandStyle, color: string): string {
|
|
51
|
+
const shape = MONOGRAM_SHAPES[style] ?? 'thinCircle';
|
|
52
|
+
const containers: Record<string, string> = {
|
|
53
|
+
roundedSquare: `<rect x="10" y="10" width="100" height="100" rx="16" fill="${color}"/>`,
|
|
54
|
+
hexagon: `<polygon points="60,5 110,30 110,90 60,115 10,90 10,30" fill="${color}"/>`,
|
|
55
|
+
thinCircle: `<circle cx="60" cy="60" r="50" fill="none" stroke="${color}" stroke-width="3"/>`,
|
|
56
|
+
blob: `<ellipse cx="60" cy="60" rx="52" ry="48" fill="${color}"/>`,
|
|
57
|
+
rectangle: `<rect x="10" y="15" width="100" height="90" rx="4" fill="${color}"/>`,
|
|
58
|
+
diamond: `<polygon points="60,5 115,60 60,115 5,60" fill="${color}"/>`,
|
|
59
|
+
ellipse: `<ellipse cx="60" cy="60" rx="55" ry="45" fill="${color}"/>`,
|
|
60
|
+
octagon: `<polygon points="35,5 85,5 115,35 115,85 85,115 35,115 5,85 5,35" fill="${color}"/>`,
|
|
61
|
+
};
|
|
62
|
+
return containers[shape] ?? containers.thinCircle;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function generateMonogram(config: LogoConfig): string {
|
|
66
|
+
const { text, font, color } = config;
|
|
67
|
+
const initial = text.charAt(0).toUpperCase();
|
|
68
|
+
const style = config.style ?? 'minimal';
|
|
69
|
+
const fontFamily = `'${font}', sans-serif`;
|
|
70
|
+
const isThinCircle = MONOGRAM_SHAPES[style] === 'thinCircle';
|
|
71
|
+
const textColor = isThinCircle ? color : 'white';
|
|
72
|
+
|
|
73
|
+
return [
|
|
74
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120">`,
|
|
75
|
+
` ${monogramContainer(style, color)}`,
|
|
76
|
+
` <text x="60" y="60" fill="${textColor}" font-size="56" font-family="${fontFamily}" font-weight="700" text-anchor="middle" dominant-baseline="central">${initial}</text>`,
|
|
77
|
+
'</svg>',
|
|
78
|
+
].join('\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const ABSTRACT_BUILDERS: Record<BrandStyle, (c: string) => string> = {
|
|
82
|
+
minimal: (c) =>
|
|
83
|
+
` <circle cx="40" cy="60" r="30" fill="${c}" opacity="0.8"/>` +
|
|
84
|
+
`\n <circle cx="60" cy="40" r="30" fill="${c}" opacity="0.5"/>` +
|
|
85
|
+
`\n <circle cx="80" cy="60" r="30" fill="${c}" opacity="0.3"/>`,
|
|
86
|
+
bold: (c) =>
|
|
87
|
+
` <rect x="10" y="10" width="50" height="50" fill="${c}" opacity="0.9"/>` +
|
|
88
|
+
`\n <rect x="40" y="40" width="50" height="50" fill="${c}" opacity="0.6"/>` +
|
|
89
|
+
`\n <rect x="60" y="20" width="40" height="40" fill="${c}" opacity="0.3"/>`,
|
|
90
|
+
elegant: (c) =>
|
|
91
|
+
` <path d="M60,10 A50,50 0 0,1 110,60" fill="none" stroke="${c}" stroke-width="2"/>` +
|
|
92
|
+
`\n <path d="M60,25 A35,35 0 0,1 95,60" fill="none" stroke="${c}" stroke-width="2"/>` +
|
|
93
|
+
`\n <path d="M60,40 A20,20 0 0,1 80,60" fill="none" stroke="${c}" stroke-width="2"/>`,
|
|
94
|
+
playful: (c) =>
|
|
95
|
+
` <circle cx="30" cy="40" r="8" fill="${c}"/>` +
|
|
96
|
+
`\n <circle cx="60" cy="25" r="12" fill="${c}" opacity="0.7"/>` +
|
|
97
|
+
`\n <circle cx="90" cy="50" r="10" fill="${c}" opacity="0.5"/>` +
|
|
98
|
+
`\n <circle cx="50" cy="80" r="14" fill="${c}" opacity="0.6"/>`,
|
|
99
|
+
corporate: (c) =>
|
|
100
|
+
` <rect x="10" y="10" width="30" height="100" fill="${c}" opacity="0.3"/>` +
|
|
101
|
+
`\n <rect x="45" y="10" width="30" height="100" fill="${c}" opacity="0.5"/>` +
|
|
102
|
+
`\n <rect x="80" y="10" width="30" height="100" fill="${c}" opacity="0.7"/>`,
|
|
103
|
+
tech: (c) =>
|
|
104
|
+
` <line x1="10" y1="60" x2="50" y2="30" stroke="${c}" stroke-width="2"/>` +
|
|
105
|
+
`\n <line x1="50" y1="30" x2="90" y2="50" stroke="${c}" stroke-width="2"/>` +
|
|
106
|
+
`\n <line x1="90" y1="50" x2="110" y2="20" stroke="${c}" stroke-width="2"/>` +
|
|
107
|
+
`\n <circle cx="50" cy="30" r="4" fill="${c}"/>` +
|
|
108
|
+
`\n <circle cx="90" cy="50" r="4" fill="${c}"/>`,
|
|
109
|
+
organic: (c) =>
|
|
110
|
+
` <path d="M10,60 Q30,20 60,60 Q90,100 110,60" fill="none" stroke="${c}" stroke-width="3"/>` +
|
|
111
|
+
`\n <path d="M10,80 Q30,40 60,80 Q90,120 110,80" fill="none" stroke="${c}" stroke-width="2" opacity="0.5"/>`,
|
|
112
|
+
retro: (c) =>
|
|
113
|
+
` <polygon points="60,10 75,35 110,35 82,55 93,85 60,67 27,85 38,55 10,35 45,35" fill="${c}"/>` +
|
|
114
|
+
`\n <polygon points="60,25 70,42 90,42 74,53 80,70 60,60 40,70 46,53 30,42 50,42" fill="white" opacity="0.3"/>`,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
function generateAbstract(config: LogoConfig): string {
|
|
118
|
+
const { color } = config;
|
|
119
|
+
const style = config.style ?? 'minimal';
|
|
120
|
+
const builder = ABSTRACT_BUILDERS[style] ?? ABSTRACT_BUILDERS.minimal;
|
|
121
|
+
|
|
122
|
+
return [
|
|
123
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 120 120">`,
|
|
124
|
+
builder(color),
|
|
125
|
+
'</svg>',
|
|
126
|
+
].join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function generateIcon(config: LogoConfig): string {
|
|
130
|
+
const { text, font, color } = config;
|
|
131
|
+
const initial = text.charAt(0).toUpperCase();
|
|
132
|
+
const fontFamily = `'${font}', sans-serif`;
|
|
133
|
+
|
|
134
|
+
return [
|
|
135
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">`,
|
|
136
|
+
` <circle cx="32" cy="32" r="28" fill="${color}"/>`,
|
|
137
|
+
` <text x="32" y="32" fill="white" font-size="32" font-family="${fontFamily}" font-weight="700" text-anchor="middle" dominant-baseline="central">${initial}</text>`,
|
|
138
|
+
'</svg>',
|
|
139
|
+
].join('\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function generateSvgLogo(config: LogoConfig): LogoOutput {
|
|
143
|
+
const wordmark = generateWordmark(config);
|
|
144
|
+
const monogram = generateMonogram(config);
|
|
145
|
+
const abstract = generateAbstract(config);
|
|
146
|
+
const icon = generateIcon(config);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
svg: wordmark,
|
|
150
|
+
variants: { wordmark, monogram, abstract, icon },
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { BrandStyle, DurationName, EasingName, MotionSystem } from '../../types.js';
|
|
2
|
+
|
|
3
|
+
const STYLE_DURATIONS: Record<BrandStyle, Record<DurationName, number>> = {
|
|
4
|
+
minimal: { instant: 0, fast: 100, normal: 200, slow: 300, slower: 400 },
|
|
5
|
+
bold: { instant: 0, fast: 100, normal: 200, slow: 300, slower: 450 },
|
|
6
|
+
elegant: { instant: 0, fast: 200, normal: 350, slow: 500, slower: 700 },
|
|
7
|
+
playful: { instant: 0, fast: 150, normal: 250, slow: 400, slower: 600 },
|
|
8
|
+
corporate: { instant: 0, fast: 120, normal: 200, slow: 300, slower: 400 },
|
|
9
|
+
tech: { instant: 0, fast: 80, normal: 150, slow: 250, slower: 350 },
|
|
10
|
+
organic: { instant: 0, fast: 180, normal: 300, slow: 450, slower: 650 },
|
|
11
|
+
retro: { instant: 0, fast: 150, normal: 250, slow: 350, slower: 500 },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const STYLE_EASINGS: Record<BrandStyle, Record<EasingName, string>> = {
|
|
15
|
+
minimal: {
|
|
16
|
+
'ease-in': 'cubic-bezier(0.4, 0, 1, 1)',
|
|
17
|
+
'ease-out': 'cubic-bezier(0, 0, 0.2, 1)',
|
|
18
|
+
'ease-in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
19
|
+
spring: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
|
20
|
+
bounce: 'cubic-bezier(0.34, 1.2, 0.64, 1)',
|
|
21
|
+
},
|
|
22
|
+
bold: {
|
|
23
|
+
'ease-in': 'cubic-bezier(0.5, 0, 1, 1)',
|
|
24
|
+
'ease-out': 'cubic-bezier(0, 0, 0.15, 1)',
|
|
25
|
+
'ease-in-out': 'cubic-bezier(0.5, 0, 0.15, 1)',
|
|
26
|
+
spring: 'cubic-bezier(0.22, 1.8, 0.36, 1)',
|
|
27
|
+
bounce: 'cubic-bezier(0.22, 1.5, 0.36, 1)',
|
|
28
|
+
},
|
|
29
|
+
elegant: {
|
|
30
|
+
'ease-in': 'cubic-bezier(0.42, 0, 1, 1)',
|
|
31
|
+
'ease-out': 'cubic-bezier(0, 0, 0.58, 1)',
|
|
32
|
+
'ease-in-out': 'cubic-bezier(0.42, 0, 0.58, 1)',
|
|
33
|
+
spring: 'cubic-bezier(0.25, 1.2, 0.5, 1)',
|
|
34
|
+
bounce: 'cubic-bezier(0.25, 1.1, 0.5, 1)',
|
|
35
|
+
},
|
|
36
|
+
playful: {
|
|
37
|
+
'ease-in': 'cubic-bezier(0.4, 0, 1, 1)',
|
|
38
|
+
'ease-out': 'cubic-bezier(0, 0, 0.2, 1)',
|
|
39
|
+
'ease-in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
40
|
+
spring: 'cubic-bezier(0.18, 2.0, 0.4, 1)',
|
|
41
|
+
bounce: 'cubic-bezier(0.18, 1.8, 0.4, 1)',
|
|
42
|
+
},
|
|
43
|
+
corporate: {
|
|
44
|
+
'ease-in': 'cubic-bezier(0.4, 0, 1, 1)',
|
|
45
|
+
'ease-out': 'cubic-bezier(0, 0, 0.2, 1)',
|
|
46
|
+
'ease-in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
47
|
+
spring: 'cubic-bezier(0.3, 1.3, 0.6, 1)',
|
|
48
|
+
bounce: 'cubic-bezier(0.3, 1.15, 0.6, 1)',
|
|
49
|
+
},
|
|
50
|
+
tech: {
|
|
51
|
+
'ease-in': 'cubic-bezier(0.55, 0, 1, 1)',
|
|
52
|
+
'ease-out': 'cubic-bezier(0, 0, 0.1, 1)',
|
|
53
|
+
'ease-in-out': 'cubic-bezier(0.55, 0, 0.1, 1)',
|
|
54
|
+
spring: 'cubic-bezier(0.2, 1.6, 0.4, 1)',
|
|
55
|
+
bounce: 'cubic-bezier(0.2, 1.4, 0.4, 1)',
|
|
56
|
+
},
|
|
57
|
+
organic: {
|
|
58
|
+
'ease-in': 'cubic-bezier(0.35, 0, 0.9, 1)',
|
|
59
|
+
'ease-out': 'cubic-bezier(0.1, 0, 0.3, 1)',
|
|
60
|
+
'ease-in-out': 'cubic-bezier(0.35, 0, 0.3, 1)',
|
|
61
|
+
spring: 'cubic-bezier(0.28, 1.4, 0.5, 1)',
|
|
62
|
+
bounce: 'cubic-bezier(0.28, 1.25, 0.5, 1)',
|
|
63
|
+
},
|
|
64
|
+
retro: {
|
|
65
|
+
'ease-in': 'cubic-bezier(0.5, 0, 1, 1)',
|
|
66
|
+
'ease-out': 'cubic-bezier(0, 0, 0.3, 1)',
|
|
67
|
+
'ease-in-out': 'cubic-bezier(0.5, 0, 0.3, 1)',
|
|
68
|
+
spring: 'cubic-bezier(0.3, 1.5, 0.5, 1)',
|
|
69
|
+
bounce: 'cubic-bezier(0.3, 1.3, 0.5, 1)',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function buildTransitions(
|
|
74
|
+
durations: Record<DurationName, number>,
|
|
75
|
+
defaultEasing: string
|
|
76
|
+
): Record<string, string> {
|
|
77
|
+
return {
|
|
78
|
+
fade: `opacity ${durations.normal}ms ${defaultEasing}`,
|
|
79
|
+
slide: `transform ${durations.normal}ms ${defaultEasing}`,
|
|
80
|
+
scale: `transform ${durations.fast}ms ${defaultEasing}`,
|
|
81
|
+
color: `color ${durations.slow}ms ${defaultEasing}, background-color ${durations.slow}ms ${defaultEasing}`,
|
|
82
|
+
all: `all ${durations.normal}ms ${defaultEasing}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function generateMotionSystem(style: BrandStyle = 'minimal'): MotionSystem {
|
|
87
|
+
const durationValues = STYLE_DURATIONS[style];
|
|
88
|
+
const easingValues = STYLE_EASINGS[style];
|
|
89
|
+
const durations = {} as Record<DurationName, string>;
|
|
90
|
+
for (const [name, ms] of Object.entries(durationValues)) {
|
|
91
|
+
durations[name as DurationName] = `${ms}ms`;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
durations,
|
|
95
|
+
easings: easingValues,
|
|
96
|
+
transitions: buildTransitions(durationValues, easingValues['ease-out']),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { BrandIdentity, OgImageOutput, OgTemplate } from '../../types.js';
|
|
2
|
+
|
|
3
|
+
function getGradientColors(brand: BrandIdentity): [string, string] {
|
|
4
|
+
return [brand.colors.primary.hex, brand.colors.secondary.hex];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function stripSvgWrapper(svg: string): string {
|
|
8
|
+
return svg.replace(/<svg[^>]*>/, '').replace(/<\/svg>/, '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function buildSvg(
|
|
12
|
+
w: number,
|
|
13
|
+
h: number,
|
|
14
|
+
colors: [string, string],
|
|
15
|
+
font: string,
|
|
16
|
+
title: string,
|
|
17
|
+
subtitle: string,
|
|
18
|
+
logoSection: string
|
|
19
|
+
): string {
|
|
20
|
+
return [
|
|
21
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">`,
|
|
22
|
+
` <defs>`,
|
|
23
|
+
` <style>@import url('https://fonts.googleapis.com/css2?family=${encodeURIComponent(font)}');</style>`,
|
|
24
|
+
` <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">`,
|
|
25
|
+
` <stop offset="0%" style="stop-color:${colors[0]}"/>`,
|
|
26
|
+
` <stop offset="100%" style="stop-color:${colors[1]}"/>`,
|
|
27
|
+
` </linearGradient>`,
|
|
28
|
+
` </defs>`,
|
|
29
|
+
` <rect width="${w}" height="${h}" fill="url(#bg)"/>`,
|
|
30
|
+
logoSection,
|
|
31
|
+
` <text x="${w / 2}" y="${h / 2 - 20}" fill="white" font-size="56" font-family="'${font}', sans-serif" font-weight="700" text-anchor="middle" dominant-baseline="central">${title}</text>`,
|
|
32
|
+
subtitle
|
|
33
|
+
? ` <text x="${w / 2}" y="${h / 2 + 40}" fill="white" font-size="28" font-family="'${font}', sans-serif" font-weight="400" text-anchor="middle" dominant-baseline="central" opacity="0.8">${subtitle}</text>`
|
|
34
|
+
: '',
|
|
35
|
+
'</svg>',
|
|
36
|
+
]
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.join('\n');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildDefaultTemplate(brand: BrandIdentity, title?: string, subtitle?: string): string {
|
|
42
|
+
const displayTitle = title ?? brand.name;
|
|
43
|
+
const displaySub = subtitle ?? brand.tagline ?? '';
|
|
44
|
+
const font = brand.typography.headingFont;
|
|
45
|
+
const logoSvg = brand.logo?.variants?.icon ?? '';
|
|
46
|
+
const logoSection = logoSvg
|
|
47
|
+
? `<g transform="translate(540, 80) scale(1.5)">${stripSvgWrapper(logoSvg)}</g>`
|
|
48
|
+
: '';
|
|
49
|
+
|
|
50
|
+
return buildSvg(1200, 630, getGradientColors(brand), font, displayTitle, displaySub, logoSection);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildArticleTemplate(brand: BrandIdentity, title?: string, subtitle?: string): string {
|
|
54
|
+
const displayTitle = title ?? 'Untitled Article';
|
|
55
|
+
const displaySub = subtitle ?? '';
|
|
56
|
+
const font = brand.typography.headingFont;
|
|
57
|
+
const logoSvg = brand.logo?.variants?.icon ?? '';
|
|
58
|
+
const logoSection = logoSvg
|
|
59
|
+
? `<g transform="translate(1080, 30) scale(0.8)">${stripSvgWrapper(logoSvg)}</g>`
|
|
60
|
+
: '';
|
|
61
|
+
|
|
62
|
+
return buildSvg(1200, 630, getGradientColors(brand), font, displayTitle, displaySub, logoSection);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildSocialTemplate(brand: BrandIdentity, title?: string): string {
|
|
66
|
+
const displayTitle = title ?? brand.name;
|
|
67
|
+
const font = brand.typography.headingFont;
|
|
68
|
+
const colors = getGradientColors(brand);
|
|
69
|
+
const logoSvg = brand.logo?.variants?.icon ?? '';
|
|
70
|
+
const logoSection = logoSvg
|
|
71
|
+
? `<g transform="translate(536, 300) scale(2)">${stripSvgWrapper(logoSvg)}</g>`
|
|
72
|
+
: '';
|
|
73
|
+
|
|
74
|
+
return buildSvg(1200, 1200, colors, font, displayTitle, '', logoSection);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function generateOgImage(
|
|
78
|
+
brand: BrandIdentity,
|
|
79
|
+
template: OgTemplate = 'default',
|
|
80
|
+
title?: string,
|
|
81
|
+
subtitle?: string
|
|
82
|
+
): OgImageOutput {
|
|
83
|
+
const builders: Record<OgTemplate, () => string> = {
|
|
84
|
+
default: () => buildDefaultTemplate(brand, title, subtitle),
|
|
85
|
+
article: () => buildArticleTemplate(brand, title, subtitle),
|
|
86
|
+
social: () => buildSocialTemplate(brand, title),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const svg = builders[template]();
|
|
90
|
+
const dimensions: Record<OgTemplate, { width: number; height: number }> = {
|
|
91
|
+
default: { width: 1200, height: 630 },
|
|
92
|
+
article: { width: 1200, height: 630 },
|
|
93
|
+
social: { width: 1200, height: 1200 },
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return { template, svg, ...dimensions[template] };
|
|
97
|
+
}
|