@jjlmoya/utils-audiovisual 1.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.
Files changed (120) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +198 -0
  3. package/src/category/i18n/es.ts +198 -0
  4. package/src/category/i18n/fr.ts +198 -0
  5. package/src/category/index.ts +17 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +4 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +32 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +42 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/seo_length.test.ts +22 -0
  21. package/src/tests/tool_validation.test.ts +17 -0
  22. package/src/tool/chromaticLens/bibliography.astro +17 -0
  23. package/src/tool/chromaticLens/component.astro +178 -0
  24. package/src/tool/chromaticLens/i18n/en.ts +246 -0
  25. package/src/tool/chromaticLens/i18n/es.ts +244 -0
  26. package/src/tool/chromaticLens/i18n/fr.ts +244 -0
  27. package/src/tool/chromaticLens/index.ts +43 -0
  28. package/src/tool/chromaticLens/logic.ts +87 -0
  29. package/src/tool/chromaticLens/seo.astro +15 -0
  30. package/src/tool/chromaticLens/style.css +308 -0
  31. package/src/tool/chromaticLens/ui.ts +109 -0
  32. package/src/tool/collageMaker/bibliography.astro +17 -0
  33. package/src/tool/collageMaker/component.astro +302 -0
  34. package/src/tool/collageMaker/i18n/en.ts +233 -0
  35. package/src/tool/collageMaker/i18n/es.ts +231 -0
  36. package/src/tool/collageMaker/i18n/fr.ts +231 -0
  37. package/src/tool/collageMaker/index.ts +51 -0
  38. package/src/tool/collageMaker/logic.ts +134 -0
  39. package/src/tool/collageMaker/seo.astro +15 -0
  40. package/src/tool/collageMaker/style.css +386 -0
  41. package/src/tool/exifCleaner/bibliography.astro +18 -0
  42. package/src/tool/exifCleaner/component.astro +162 -0
  43. package/src/tool/exifCleaner/i18n/en.ts +277 -0
  44. package/src/tool/exifCleaner/i18n/es.ts +277 -0
  45. package/src/tool/exifCleaner/i18n/fr.ts +277 -0
  46. package/src/tool/exifCleaner/index.ts +57 -0
  47. package/src/tool/exifCleaner/logic.ts +135 -0
  48. package/src/tool/exifCleaner/seo.astro +18 -0
  49. package/src/tool/exifCleaner/style.css +289 -0
  50. package/src/tool/exifCleaner/ui.ts +117 -0
  51. package/src/tool/imageCompressor/bibliography.astro +17 -0
  52. package/src/tool/imageCompressor/component.astro +262 -0
  53. package/src/tool/imageCompressor/i18n/en.ts +232 -0
  54. package/src/tool/imageCompressor/i18n/es.ts +230 -0
  55. package/src/tool/imageCompressor/i18n/fr.ts +230 -0
  56. package/src/tool/imageCompressor/index.ts +50 -0
  57. package/src/tool/imageCompressor/logic.ts +79 -0
  58. package/src/tool/imageCompressor/seo.astro +15 -0
  59. package/src/tool/imageCompressor/style.css +503 -0
  60. package/src/tool/printQualityCalculator/bibliography.astro +18 -0
  61. package/src/tool/printQualityCalculator/component.astro +318 -0
  62. package/src/tool/printQualityCalculator/i18n/en.ts +247 -0
  63. package/src/tool/printQualityCalculator/i18n/es.ts +245 -0
  64. package/src/tool/printQualityCalculator/i18n/fr.ts +245 -0
  65. package/src/tool/printQualityCalculator/index.ts +56 -0
  66. package/src/tool/printQualityCalculator/logic.ts +53 -0
  67. package/src/tool/printQualityCalculator/seo.astro +18 -0
  68. package/src/tool/printQualityCalculator/style.css +491 -0
  69. package/src/tool/printQualityCalculator/ui.ts +122 -0
  70. package/src/tool/privacyBlur/bibliography.astro +17 -0
  71. package/src/tool/privacyBlur/component.astro +230 -0
  72. package/src/tool/privacyBlur/i18n/en.ts +238 -0
  73. package/src/tool/privacyBlur/i18n/es.ts +236 -0
  74. package/src/tool/privacyBlur/i18n/fr.ts +236 -0
  75. package/src/tool/privacyBlur/index.ts +49 -0
  76. package/src/tool/privacyBlur/logic.ts +249 -0
  77. package/src/tool/privacyBlur/seo.astro +15 -0
  78. package/src/tool/privacyBlur/style.css +332 -0
  79. package/src/tool/privacyBlur/ui.ts +124 -0
  80. package/src/tool/subtitleSync/bibliography.astro +17 -0
  81. package/src/tool/subtitleSync/component.astro +187 -0
  82. package/src/tool/subtitleSync/i18n/en.ts +241 -0
  83. package/src/tool/subtitleSync/i18n/es.ts +241 -0
  84. package/src/tool/subtitleSync/i18n/fr.ts +241 -0
  85. package/src/tool/subtitleSync/index.ts +49 -0
  86. package/src/tool/subtitleSync/logic.ts +91 -0
  87. package/src/tool/subtitleSync/seo.astro +15 -0
  88. package/src/tool/subtitleSync/style.css +325 -0
  89. package/src/tool/subtitleSync/ui.ts +152 -0
  90. package/src/tool/timelapseCalculator/bibliography.astro +15 -0
  91. package/src/tool/timelapseCalculator/component.astro +148 -0
  92. package/src/tool/timelapseCalculator/i18n/en.ts +169 -0
  93. package/src/tool/timelapseCalculator/i18n/es.ts +169 -0
  94. package/src/tool/timelapseCalculator/i18n/fr.ts +169 -0
  95. package/src/tool/timelapseCalculator/index.ts +52 -0
  96. package/src/tool/timelapseCalculator/logic.ts +46 -0
  97. package/src/tool/timelapseCalculator/seo.astro +18 -0
  98. package/src/tool/timelapseCalculator/style.css +285 -0
  99. package/src/tool/tvDistance/bibliography.astro +17 -0
  100. package/src/tool/tvDistance/component.astro +178 -0
  101. package/src/tool/tvDistance/i18n/en.ts +223 -0
  102. package/src/tool/tvDistance/i18n/es.ts +223 -0
  103. package/src/tool/tvDistance/i18n/fr.ts +223 -0
  104. package/src/tool/tvDistance/index.ts +49 -0
  105. package/src/tool/tvDistance/logic.ts +47 -0
  106. package/src/tool/tvDistance/seo.astro +15 -0
  107. package/src/tool/tvDistance/style.css +435 -0
  108. package/src/tool/tvDistance/ui.ts +66 -0
  109. package/src/tool/videoFrameExtractor/bibliography.astro +17 -0
  110. package/src/tool/videoFrameExtractor/component.astro +285 -0
  111. package/src/tool/videoFrameExtractor/i18n/en.ts +235 -0
  112. package/src/tool/videoFrameExtractor/i18n/es.ts +235 -0
  113. package/src/tool/videoFrameExtractor/i18n/fr.ts +235 -0
  114. package/src/tool/videoFrameExtractor/index.ts +53 -0
  115. package/src/tool/videoFrameExtractor/logic.ts +49 -0
  116. package/src/tool/videoFrameExtractor/seo.astro +15 -0
  117. package/src/tool/videoFrameExtractor/style.css +426 -0
  118. package/src/tool/videoFrameExtractor/ui.ts +179 -0
  119. package/src/tools.ts +25 -0
  120. package/src/types.ts +72 -0
@@ -0,0 +1,244 @@
1
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
+ import type { ChromaticLensUI, ChromaticLensLocaleContent } from '../index';
3
+
4
+ const slug = 'lentille-chromatique-extraction-palette-couleurs-ligne';
5
+ const title = 'Lentille Chromatique - Extraction de Palette de Couleurs en Ligne';
6
+ const description = 'Extrayez gratuitement des palettes de couleurs professionnelles à partir de n\'importe quelle image. Identifiez les couleurs dominantes à l\'aide d\'algorithmes mathématiques.';
7
+
8
+ const ui: ChromaticLensUI = {
9
+ dropTitle: "Analyser les Couleurs",
10
+ dropSubtitle: "Faites glisser une image pour extraire son ADN chromatique.",
11
+ processingLabel: "Extraction de la palette...",
12
+ paletteTitle: "Palette Extraite",
13
+ copyLabel: "Copier HEX",
14
+ copiedLabel: "Copié !",
15
+ colorCountLabel: "Nombre de couleurs",
16
+ changeImage: "Changer d'image"
17
+ };
18
+
19
+ const faq: ChromaticLensLocaleContent['faq'] = [
20
+ {
21
+ question: "Comment fonctionne l'extraction des couleurs ?",
22
+ answer: "Nous utilisons l'algorithme 'Median Cut', qui regroupe les pixels de l'image en fonction de leur proximité dans l'espace colorimétrique RVB pour trouver les tons les plus représentatifs.",
23
+ },
24
+ {
25
+ question: "Puis-je copier les couleurs dans mon éditeur de design ?",
26
+ answer: "Oui, en cliquant sur chaque couleur, le code HEX sera automatiquement copié dans votre presse-papiers, prêt à être collé dans Photoshop, Figma ou CSS.",
27
+ },
28
+ {
29
+ question: "Quels types d'images puis-je analyser ?",
30
+ answer: "N'importe quel fichier JPG, PNG ou WebP. Le traitement est local, les images lourdes seront donc analysées rapidement sans consommer de données réseau.",
31
+ },
32
+ ];
33
+
34
+ const howTo: ChromaticLensLocaleContent['howTo'] = [
35
+ {
36
+ name: "Charger l'image",
37
+ text: "Téléchargez la photographie dont vous souhaitez extraire l'inspiration chromatique.",
38
+ },
39
+ {
40
+ name: "Ajuster la précision",
41
+ text: "Sélectionnez le nombre de couleurs dominantes que l'outil doit identifier (de 3 à 12).",
42
+ },
43
+ {
44
+ name: "Analyser le résultat",
45
+ text: "La palette apparaîtra instantanément avec ses codes hexadécimaux correspondants.",
46
+ },
47
+ {
48
+ name: "Copier et utiliser",
49
+ text: "Cliquez sur les tons pour les enregistrer et les appliquer à votre projet de conception.",
50
+ },
51
+ ];
52
+
53
+ const bibliography: ChromaticLensLocaleContent['bibliography'] = [
54
+ {
55
+ name: "Median Cut Algorithm - Wikipédia",
56
+ url: "https://fr.wikipedia.org/wiki/Coupure_m%C3%A9diane",
57
+ },
58
+ {
59
+ name: "Théorie des couleurs pour les designers",
60
+ url: "https://www.smashingmagazine.com/2010/01/color-theory-for-designers-part-1-the-meaning-of-color/",
61
+ },
62
+ ];
63
+
64
+ const seo: ChromaticLensLocaleContent['seo'] = [
65
+ {
66
+ type: 'summary',
67
+ title: 'Extraction Intelligente de Palette de Couleurs',
68
+ items: [
69
+ 'Algorithme Median Cut professionnel pour l\'analyse des couleurs',
70
+ 'Extrayez 3 à 12 couleurs dominantes de n\'importe quelle image',
71
+ 'Codes HEX cliquables directement vers le presse-papiers',
72
+ 'Traitement 100 % local - idéal pour les créatifs'
73
+ ]
74
+ },
75
+ { type: 'title', text: 'Extraction de Palettes de Couleurs : Science et Design', level: 2 },
76
+ { type: 'paragraph', html: 'Vous êtes-vous déjà demandé pourquoi une photographie de film semble si harmonieuse ? Ce n\'est pas un hasard ; c\'est la théorie des couleurs en action. Lentille Chromatique vous permet d\'extraire cette harmonie directement du pixel, en la transformant en codes HEX utilisables dans vos projets de design.' },
77
+
78
+ { type: 'stats', items: [
79
+ { value: 'Instantané', label: 'Analyse de Couleur', icon: 'mdi:lightning-bolt' },
80
+ { value: '100%', label: 'Confidentialité Locale', icon: 'mdi:lock' },
81
+ { value: 'RVB', label: 'Espace Colorimétrique Précis', icon: 'mdi:palette' }
82
+ ], columns: 3 },
83
+
84
+ { type: 'title', text: 'L\'Algorithme Median Cut Expliqué', level: 3 },
85
+ { type: 'paragraph', html: 'L\'extraction intelligente de palettes n\'est pas un simple échantillonnage aléatoire de pixels. Elle utilise l\'algorithme Median Cut, une technique de partitionnement récursif qui garantit une représentation fidèle :' },
86
+ { type: 'list', items: [
87
+ '<strong>Division Récursive :</strong> Le "cube de couleur" RVB de l\'image est divisé récursivement en boîtes plus petites.',
88
+ '<strong>Équilibre de Volume :</strong> Chaque partition cherche à regrouper des pixels du même espace colorimétrique avec des volumes similaires.',
89
+ '<strong>Moyenne Pondérée :</strong> La couleur résultante de chaque boîte est la moyenne de tous les pixels qu\'elle contient.',
90
+ '<strong>Représentation Fidèle :</strong> Les couleurs dominantes reflètent l\'atmosphère visuelle réelle de l\'image, pas un simple échantillon.'
91
+ ], icon: 'mdi:check' },
92
+
93
+ { type: 'card', title: 'Flux de Travail Créatif', html: 'Idéal pour les développeurs web, les designers UX/UI, les artistes numériques et les créatifs cherchant à capturer instantanément l\'essence visuelle d\'une photographie, d\'un film ou d\'une référence visuelle pour l\'appliquer dans leurs interfaces, illustrations ou palettes de marque.' },
94
+
95
+ { type: 'title', text: 'Cas d\'Utilisation en Design Numérique', level: 3 },
96
+ { type: 'comparative', items: [
97
+ {
98
+ title: 'Designers UX/UI',
99
+ description: 'Extrayez des palettes d\'images hero pour créer des interfaces cohérentes',
100
+ icon: 'mdi:palette',
101
+ points: [
102
+ 'Couleurs de fond harmonieuses',
103
+ 'Boutons et éléments secondaires',
104
+ 'Contraste calculé automatiquement'
105
+ ]
106
+ },
107
+ {
108
+ title: 'Développeurs Web',
109
+ description: 'Créez des feuilles de style CSS directement depuis des références visuelles',
110
+ icon: 'mdi:code-braces',
111
+ points: [
112
+ 'Copier HEX directement vers CSS',
113
+ 'Variables de couleur en SCSS/CSS',
114
+ 'Thèmes cohérents sans conception préalable'
115
+ ],
116
+ highlight: true
117
+ },
118
+ {
119
+ title: 'Artistes Numériques et Illustrateurs',
120
+ description: 'Capturez des références chromatiques de films, de la nature ou de l\'art',
121
+ icon: 'mdi:brush',
122
+ points: [
123
+ 'Palettes de référence de chefs-d\'œuvre',
124
+ 'Études de couleurs cinématographiques',
125
+ 'Inspiration visuelle immédiate'
126
+ ]
127
+ },
128
+ {
129
+ title: 'Spécialistes en Branding',
130
+ description: 'Développez des identités visuelles basées sur des photographies de référence',
131
+ icon: 'mdi:tag-multiple',
132
+ points: [
133
+ 'Extraire les couleurs de marque des images',
134
+ 'Créer des guides chromatiques professionnels',
135
+ 'Assurer la cohérence visuelle'
136
+ ]
137
+ }
138
+ ], columns: 2 },
139
+
140
+ { type: 'title', text: 'Théorie des Couleurs Appliquée', level: 3 },
141
+ { type: 'table', headers: ['Concept de Couleur', 'Définition', 'Application Pratique'], rows: [
142
+ ['Harmonie Chromatique', 'Combinaison de couleurs visuellement équilibrée', 'Identité visuelle cohérente en UI'],
143
+ ['Contraste', 'Différence de luminosité entre les couleurs', 'Lisibilité et hiérarchie visuelle'],
144
+ ['Saturation', 'Intensité colorée d\'un ton', 'Professionnalisme (basse) vs Énergie (haute)'],
145
+ ['Température de Couleur', 'Couleurs chaudes (rouges) vs froides (bleues)', 'Psychologie émotionnelle du design'],
146
+ ['Palette Monochromatique', 'Variations d\'un seul ton', 'Élégance et minimalisme']
147
+ ] },
148
+
149
+ { type: 'proscons', items: [
150
+ {
151
+ pro: 'Précision mathématique dans l\'extraction - pas une sélection visuelle approximative',
152
+ con: 'Des couleurs peu visibles peuvent être incluses si elles ont beaucoup de pixels'
153
+ },
154
+ {
155
+ pro: 'Copie instantanée dans le presse-papiers - intégration parfaite avec le flux de travail',
156
+ con: 'Nécessite un navigateur moderne compatible avec l\'API Canvas'
157
+ },
158
+ {
159
+ pro: 'Confidentialité totale - analyse 100 % locale sans envoi de données',
160
+ con: 'Aucun enregistrement d\'historique des analyses précédentes'
161
+ },
162
+ {
163
+ pro: 'Compatible avec n\'importe quel format d\'image numérique',
164
+ con: 'Les couleurs finales dépendent de la compression et de la qualité de l\'image'
165
+ }
166
+ ], proTitle: 'Avantages', conTitle: 'Limitations' },
167
+
168
+ { type: 'diagnostic', variant: 'success', title: 'Représentation Réaliste de la Couleur', icon: 'mdi:check-circle-outline', badge: 'Algorithme Avancé', html: 'Contrairement aux outils qui échantillonnent simplement des pixels aléatoires, notre système utilise l\'algorithme Median Cut qui pondère l\'ensemble du nombre de pixels de chaque ton, garantissant que la palette résultante reflète fidèlement l\'atmosphère visuelle et la psychologie des couleurs de l\'image originale.' },
169
+
170
+ { type: 'glossary', items: [
171
+ {
172
+ term: 'Median Cut',
173
+ definition: 'Algorithme de quantification de couleur qui divise récursivement l\'espace RVB en boîtes, assurant une distribution uniforme. Utilisé historiquement dans les GIF.'
174
+ },
175
+ {
176
+ term: 'Espace Colorimétrique RVB',
177
+ definition: 'Modèle de couleur basé sur le rouge, le vert et le bleu. Chaque couleur est représentée comme une combinaison de ces trois valeurs (0-255).'
178
+ },
179
+ {
180
+ term: 'Code HEX',
181
+ definition: 'Notation hexadécimale à 6 chiffres (#RRGGBB) représentant la couleur sur le web. Universel en CSS, Figma et Adobe.'
182
+ },
183
+ {
184
+ term: 'Saturation de la Couleur',
185
+ definition: 'Intensité ou pureté de la couleur. Une saturation élevée = couleur vive ; une saturation basse = couleur grisâtre.'
186
+ },
187
+ {
188
+ term: 'Harmonie Chromatique',
189
+ definition: 'Sélection et combinaison de couleurs résultant en un ensemble visuellement agréable.'
190
+ }
191
+ ] },
192
+
193
+ { type: 'message', title: 'Analyse Chromatique Professionnelle', ariaLabel: 'Informations techniques sur l\'analyse des couleurs', html: 'Lentille Chromatique transforme l\'analyse visuelle manuelle en précision algorithmique. Elle ne se contente pas d\'extraire des couleurs : elle capture l\'essence émotionnelle et visuelle de toute image, la plaçant directement dans votre presse-papiers sous forme de codes HEX prêts à l\'emploi. Confidentialité totale, analyses illimitées.' },
194
+
195
+ { type: 'title', text: 'Concevez Depuis l\'Inspiration Visuelle', level: 3 },
196
+ { type: 'paragraph', html: 'La meilleure palette de couleurs est celle qui capture l\'intention visuelle de votre référence. Lentille Chromatique automatise ce qui était auparavant un processus manuel : observer, analyser, noter. Désormais, faites simplement glisser une image et obtenez des codes HEX professionnels en quelques secondes.' }
197
+ ];
198
+
199
+ const faqSchema: WithContext<FAQPage> = {
200
+ '@context': 'https://schema.org',
201
+ '@type': 'FAQPage',
202
+ mainEntity: faq.map((item) => ({
203
+ '@type': 'Question',
204
+ name: item.question,
205
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
206
+ })),
207
+ };
208
+
209
+ const howToSchema: WithContext<HowTo> = {
210
+ '@context': 'https://schema.org',
211
+ '@type': 'HowTo',
212
+ name: title,
213
+ description,
214
+ step: howTo.map((step) => ({
215
+ '@type': 'HowToStep',
216
+ name: step.name,
217
+ text: step.text,
218
+ })),
219
+ };
220
+
221
+ const appSchema: WithContext<SoftwareApplication> = {
222
+ '@context': 'https://schema.org',
223
+ '@type': 'SoftwareApplication',
224
+ name: title,
225
+ description,
226
+ applicationCategory: 'UtilitiesApplication',
227
+ operatingSystem: 'Web',
228
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' },
229
+ inLanguage: 'fr',
230
+ };
231
+
232
+ export const content: ChromaticLensLocaleContent = {
233
+ slug,
234
+ title,
235
+ description,
236
+ ui,
237
+ seo,
238
+ faqTitle: "Frequently Asked Questions",
239
+ faq,
240
+ bibliography,
241
+ bibliographyTitle: "References",
242
+ howTo,
243
+ schemas: [faqSchema as any, howToSchema as any, appSchema],
244
+ };
@@ -0,0 +1,43 @@
1
+ import type { AudiovisualToolEntry, ToolLocaleContent, ToolDefinition } from '../../types';
2
+ import ChromaticLens from './component.astro';
3
+ import ChromaticLensSEO from './seo.astro';
4
+ import ChromaticLensBibliography from './bibliography.astro';
5
+
6
+ export interface ChromaticLensUI {
7
+ dropTitle: string;
8
+ dropSubtitle: string;
9
+ processingLabel: string;
10
+ paletteTitle: string;
11
+ copyLabel: string;
12
+ copiedLabel: string;
13
+ colorCountLabel: string;
14
+ [key: string]: string;
15
+ }
16
+
17
+ export type ChromaticLensLocaleContent = ToolLocaleContent<ChromaticLensUI>;
18
+
19
+ import { content as es } from './i18n/es';
20
+ import { content as en } from './i18n/en';
21
+ import { content as fr } from './i18n/fr';
22
+
23
+ export const chromaticLens: AudiovisualToolEntry<ChromaticLensUI> = {
24
+ id: 'lente-cromatica',
25
+ icons: {
26
+ bg: 'mdi:palette-swatch',
27
+ fg: 'mdi:eye-outline',
28
+ },
29
+ i18n: {
30
+ es: async () => es as unknown as ChromaticLensLocaleContent,
31
+ en: async () => en as unknown as ChromaticLensLocaleContent,
32
+ fr: async () => fr as unknown as ChromaticLensLocaleContent,
33
+ },
34
+ };
35
+
36
+ export { ChromaticLens, ChromaticLensSEO, ChromaticLensBibliography };
37
+
38
+ export const CHROMATIC_LENS_TOOL: ToolDefinition = {
39
+ entry: chromaticLens as unknown as AudiovisualToolEntry,
40
+ Component: ChromaticLens,
41
+ SEOComponent: ChromaticLensSEO,
42
+ BibliographyComponent: ChromaticLensBibliography,
43
+ };
@@ -0,0 +1,87 @@
1
+ export type RGB = [number, number, number];
2
+
3
+ export interface ColorSwatch {
4
+ rgb: RGB;
5
+ hex: string;
6
+ }
7
+
8
+ function rgbToHex(r: number, g: number, b: number): string {
9
+ return (
10
+ "#" +
11
+ [r, g, b]
12
+ .map((x) => {
13
+ const hex = x.toString(16);
14
+ return hex.length === 1 ? "0" + hex : hex;
15
+ })
16
+ .join("")
17
+ ).toUpperCase();
18
+ }
19
+
20
+ function findBucketRanges(bucket: RGB[]): { ranges: [number, number, number]; channel: 0 | 1 | 2 } {
21
+ let minR = 255, maxR = 0, minG = 255, maxG = 0, minB = 255, maxB = 0;
22
+ for (const p of bucket) {
23
+ minR = Math.min(minR, p[0]); maxR = Math.max(maxR, p[0]);
24
+ minG = Math.min(minG, p[1]); maxG = Math.max(maxG, p[1]);
25
+ minB = Math.min(minB, p[2]); maxB = Math.max(maxB, p[2]);
26
+ }
27
+ const rangeR = maxR - minR, rangeG = maxG - minG, rangeB = maxB - minB;
28
+ const ranges = [rangeR, rangeG, rangeB] as const;
29
+ const maxIdx = ranges.indexOf(Math.max(...ranges)) as 0 | 1 | 2;
30
+ return { ranges, channel: maxIdx };
31
+ }
32
+
33
+ function samplePixels(imageData: Uint8ClampedArray): RGB[] {
34
+ const pixels: RGB[] = [];
35
+ const skip = imageData.length > 500000 ? 40 : 4;
36
+ for (let i = 0; i < imageData.length; i += skip) {
37
+ const r = imageData[i], g = imageData[i + 1], b = imageData[i + 2], a = imageData[i + 3];
38
+ if (typeof r === 'number' && typeof g === 'number' && typeof b === 'number' && typeof a === 'number' && a >= 128) {
39
+ pixels.push([r, g, b]);
40
+ }
41
+ }
42
+ return pixels;
43
+ }
44
+
45
+ function quantizeBuckets(buckets: RGB[][], colorCount: number): void {
46
+ while (buckets.length < colorCount) {
47
+ let maxRange = -1, splitIndex = -1, bestChannel: 0 | 1 | 2 = 0;
48
+ for (let i = 0; i < buckets.length; i++) {
49
+ const bucket = buckets[i];
50
+ if (!bucket?.length) continue;
51
+ const { channel } = findBucketRanges(bucket);
52
+ const maxDimension = Math.max(...findBucketRanges(bucket).ranges);
53
+ if (maxDimension > maxRange) {
54
+ maxRange = maxDimension; splitIndex = i; bestChannel = channel;
55
+ }
56
+ }
57
+ if (splitIndex === -1) break;
58
+ const bucketToSplit = buckets[splitIndex];
59
+ if (!bucketToSplit) break;
60
+ bucketToSplit.sort((a, b) => (a[bestChannel] ?? 0) - (b[bestChannel] ?? 0));
61
+ const mid = Math.floor(bucketToSplit.length / 2);
62
+ buckets.splice(splitIndex, 1, bucketToSplit.slice(0, mid), bucketToSplit.slice(mid));
63
+ }
64
+ }
65
+
66
+ function bucketToSwatch(bucket: RGB[]): ColorSwatch {
67
+ let r = 0, g = 0, b = 0;
68
+ for (const p of bucket) {
69
+ r += p[0]; g += p[1]; b += p[2];
70
+ }
71
+ const count = bucket.length || 1;
72
+ return {
73
+ rgb: [Math.round(r / count), Math.round(g / count), Math.round(b / count)],
74
+ hex: rgbToHex(Math.round(r / count), Math.round(g / count), Math.round(b / count)),
75
+ };
76
+ }
77
+
78
+ export function extractPalette(
79
+ imageData: Uint8ClampedArray,
80
+ colorCount: number = 5
81
+ ): ColorSwatch[] {
82
+ const pixels = samplePixels(imageData);
83
+ if (!pixels.length) return [];
84
+ const buckets: RGB[][] = [pixels];
85
+ quantizeBuckets(buckets, colorCount);
86
+ return buckets.map(bucketToSwatch);
87
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { chromaticLens } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await chromaticLens.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+
15
+ <SEORenderer content={{ locale, sections: content.seo || [] }} />
@@ -0,0 +1,308 @@
1
+ .cl-root {
2
+ --cl-bg: #fff;
3
+ --cl-bg-elevated: #f8fafc;
4
+ --cl-border: #e2e8f0;
5
+ --cl-text: #0f172a;
6
+ --cl-text-muted: #64748b;
7
+ --cl-accent: #6366f1;
8
+ --cl-accent-alpha: rgba(99, 102, 241, 0.08);
9
+ --cl-accent-alpha-hover: rgba(99, 102, 241, 0.04);
10
+ --cl-emerald: #10b981;
11
+ --cl-emerald-alpha: rgba(16, 185, 129, 0.06);
12
+ --cl-shadow: rgba(0, 0, 0, 0.15);
13
+
14
+ padding: 2.5rem 1.5rem;
15
+ max-width: 1000px;
16
+ margin: 0 auto;
17
+ }
18
+
19
+ .theme-dark .cl-root {
20
+ --cl-bg: #18181b;
21
+ --cl-bg-elevated: #27272a;
22
+ --cl-border: #3f3f46;
23
+ --cl-text: #f4f4f5;
24
+ --cl-text-muted: #71717a;
25
+ --cl-accent: #818cf8;
26
+ --cl-accent-alpha: rgba(129, 140, 248, 0.12);
27
+ --cl-accent-alpha-hover: rgba(129, 140, 248, 0.06);
28
+ --cl-emerald: #34d399;
29
+ --cl-emerald-alpha: rgba(52, 211, 153, 0.08);
30
+ --cl-shadow: rgba(0, 0, 0, 0.5);
31
+ }
32
+
33
+ .cl-card {
34
+ background: var(--cl-bg);
35
+ border: 1px solid var(--cl-border);
36
+ border-radius: 3rem;
37
+ padding: 1.5rem;
38
+ box-shadow: 0 45px 120px -30px var(--cl-shadow);
39
+ position: relative;
40
+ overflow: hidden;
41
+ }
42
+
43
+ .cl-drop {
44
+ padding: 5rem 2rem;
45
+ display: flex;
46
+ flex-direction: column;
47
+ align-items: center;
48
+ border: 3px dashed var(--cl-border);
49
+ border-radius: 2.5rem;
50
+ cursor: pointer;
51
+ transition: all 0.2s ease;
52
+ text-align: center;
53
+ gap: 0.5rem;
54
+ }
55
+
56
+ .cl-drop:hover,
57
+ .cl-drop-active {
58
+ background: var(--cl-accent-alpha-hover);
59
+ border-color: var(--cl-accent);
60
+ }
61
+
62
+ .cl-drop-icon {
63
+ width: 5rem;
64
+ height: 5rem;
65
+ background: var(--cl-accent-alpha);
66
+ border-radius: 50%;
67
+ display: flex;
68
+ align-items: center;
69
+ justify-content: center;
70
+ margin-bottom: 1rem;
71
+ color: var(--cl-accent);
72
+ }
73
+
74
+ .cl-drop-icon svg {
75
+ width: 2.5rem;
76
+ height: 2.5rem;
77
+ }
78
+
79
+ .cl-drop-title {
80
+ font-size: 2rem;
81
+ font-weight: 950;
82
+ color: var(--cl-text);
83
+ margin: 0;
84
+ }
85
+
86
+ .cl-drop-sub {
87
+ font-size: 1.1rem;
88
+ color: var(--cl-text-muted);
89
+ margin: 0;
90
+ font-weight: 600;
91
+ }
92
+
93
+ .cl-workspace {
94
+ padding: 1.5rem;
95
+ }
96
+
97
+ .cl-mini-drop {
98
+ display: inline-flex;
99
+ align-items: center;
100
+ gap: 0.75rem;
101
+ padding: 0.75rem 1.25rem;
102
+ background: var(--cl-bg-elevated);
103
+ border: 1px solid var(--cl-border);
104
+ border-radius: 1rem;
105
+ font-size: 0.75rem;
106
+ font-weight: 800;
107
+ color: var(--cl-text-muted);
108
+ cursor: pointer;
109
+ margin-bottom: 2rem;
110
+ transition: border-color 0.2s, color 0.2s;
111
+ }
112
+
113
+ .cl-mini-drop:hover {
114
+ border-color: var(--cl-accent);
115
+ color: var(--cl-accent);
116
+ }
117
+
118
+ .cl-mini-drop svg {
119
+ width: 1.1rem;
120
+ height: 1.1rem;
121
+ }
122
+
123
+ .cl-config-bar {
124
+ padding: 1rem 0;
125
+ margin-bottom: 2rem;
126
+ border-bottom: 1px solid var(--cl-border);
127
+ display: flex;
128
+ justify-content: flex-end;
129
+ }
130
+
131
+ .cl-config-item {
132
+ display: flex;
133
+ align-items: center;
134
+ gap: 1rem;
135
+ }
136
+
137
+ .cl-config-label {
138
+ font-size: 0.75rem;
139
+ font-weight: 900;
140
+ text-transform: uppercase;
141
+ color: var(--cl-text-muted);
142
+ letter-spacing: 0.1em;
143
+ }
144
+
145
+ .cl-count-select {
146
+ padding: 0.5rem 1rem;
147
+ border-radius: 0.75rem;
148
+ background: var(--cl-bg-elevated);
149
+ border: 1px solid var(--cl-border);
150
+ color: var(--cl-text);
151
+ font-weight: 800;
152
+ cursor: pointer;
153
+ }
154
+
155
+ .cl-result-layout {
156
+ display: grid;
157
+ grid-template-columns: 1fr 1.25fr;
158
+ gap: 3rem;
159
+ }
160
+
161
+ @media (max-width: 800px) {
162
+ .cl-result-layout {
163
+ grid-template-columns: 1fr;
164
+ }
165
+ }
166
+
167
+ .cl-preview-col {
168
+ display: flex;
169
+ flex-direction: column;
170
+ gap: 1rem;
171
+ }
172
+
173
+ .cl-preview-img {
174
+ width: 100%;
175
+ border-radius: 1.5rem;
176
+ box-shadow: 0 20px 40px var(--cl-shadow);
177
+ display: block;
178
+ }
179
+
180
+ .cl-palette-col {
181
+ display: flex;
182
+ flex-direction: column;
183
+ gap: 1.5rem;
184
+ }
185
+
186
+ .cl-palette-header {
187
+ display: flex;
188
+ align-items: center;
189
+ gap: 1rem;
190
+ color: var(--cl-accent);
191
+ }
192
+
193
+ .cl-palette-header svg {
194
+ width: 1.25rem;
195
+ height: 1.25rem;
196
+ flex-shrink: 0;
197
+ }
198
+
199
+ .cl-palette-header h4 {
200
+ font-size: 1.25rem;
201
+ font-weight: 950;
202
+ color: var(--cl-text);
203
+ margin: 0;
204
+ }
205
+
206
+ .cl-loader {
207
+ display: flex;
208
+ flex-direction: column;
209
+ align-items: center;
210
+ gap: 1rem;
211
+ padding: 2rem 0;
212
+ }
213
+
214
+ .cl-spinner {
215
+ width: 3rem;
216
+ height: 3rem;
217
+ border: 3px solid var(--cl-accent-alpha);
218
+ border-top-color: var(--cl-accent);
219
+ border-radius: 50%;
220
+ animation: cl-spin 0.8s linear infinite;
221
+ }
222
+
223
+ .cl-loader-text {
224
+ font-size: 0.75rem;
225
+ font-weight: 900;
226
+ text-transform: uppercase;
227
+ color: var(--cl-text-muted);
228
+ letter-spacing: 0.1em;
229
+ margin: 0;
230
+ }
231
+
232
+ .cl-swatches {
233
+ display: flex;
234
+ flex-direction: column;
235
+ gap: 0.75rem;
236
+ animation: cl-fade-up 0.5s ease;
237
+ }
238
+
239
+ .cl-swatch {
240
+ display: flex;
241
+ align-items: center;
242
+ gap: 1.25rem;
243
+ background: var(--cl-bg-elevated);
244
+ border-radius: 1.25rem;
245
+ padding: 1rem;
246
+ border: 1px solid var(--cl-border);
247
+ cursor: pointer;
248
+ transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275), border-color 0.2s;
249
+ }
250
+
251
+ .cl-swatch:hover {
252
+ transform: scale(1.02);
253
+ border-color: var(--cl-accent);
254
+ }
255
+
256
+ .cl-swatch-copied {
257
+ background: var(--cl-emerald-alpha);
258
+ border-color: var(--cl-emerald);
259
+ }
260
+
261
+ .cl-swatch-color {
262
+ width: 3.5rem;
263
+ height: 3.5rem;
264
+ border-radius: 0.75rem;
265
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.05);
266
+ flex-shrink: 0;
267
+ }
268
+
269
+ .cl-swatch-info {
270
+ display: flex;
271
+ flex-direction: column;
272
+ gap: 0.2rem;
273
+ }
274
+
275
+ .cl-swatch-hex {
276
+ font-weight: 950;
277
+ color: var(--cl-text);
278
+ font-size: 1.25rem;
279
+ }
280
+
281
+ .cl-swatch-action {
282
+ font-size: 0.7rem;
283
+ font-weight: 900;
284
+ color: var(--cl-text-muted);
285
+ text-transform: uppercase;
286
+ letter-spacing: 0.05em;
287
+ }
288
+
289
+ .cl-hidden {
290
+ display: none;
291
+ }
292
+
293
+ @keyframes cl-spin {
294
+ to {
295
+ transform: rotate(360deg);
296
+ }
297
+ }
298
+
299
+ @keyframes cl-fade-up {
300
+ from {
301
+ opacity: 0;
302
+ transform: translateY(10px);
303
+ }
304
+ to {
305
+ opacity: 1;
306
+ transform: translateY(0);
307
+ }
308
+ }