img-to-text-computational
Version:
High-performance image-to-text analyzer using pure computational methods. Convert images to structured text descriptions with 99.9% accuracy, zero AI dependencies, and complete offline processing.
858 lines (721 loc) • 28 kB
JavaScript
const { Stats } = require('fast-stats');
class DesignSystemAnalyzer {
constructor(options = {}) {
this.options = {
colorToleranceHSL: options.colorToleranceHSL || 10,
spacingTolerance: options.spacingTolerance || 5,
fontSizeTolerance: options.fontSizeTolerance || 2,
minimumOccurrences: options.minimumOccurrences || 2,
...options
};
// Common design system patterns
this.designSystemPatterns = this.initializeDesignSystemPatterns();
}
/**
* Initialize common design system patterns and rules
*/
initializeDesignSystemPatterns() {
return {
spacing: {
common_scales: [4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96],
fibonacci: [8, 13, 21, 34, 55, 89],
powers_of_two: [4, 8, 16, 32, 64, 128],
golden_ratio: [8, 13, 21, 34, 55]
},
typography: {
common_scales: [12, 14, 16, 18, 20, 24, 28, 32, 36, 48, 60, 72],
modular_scales: {
minor_second: 1.067,
major_second: 1.125,
minor_third: 1.2,
major_third: 1.25,
perfect_fourth: 1.333,
golden_ratio: 1.618
}
},
colors: {
common_palettes: [
'monochromatic',
'analogous',
'complementary',
'triadic',
'tetradic',
'split_complementary'
],
brand_patterns: [
'primary_secondary_accent',
'primary_variants',
'semantic_colors'
]
}
};
}
/**
* Analyze design system compliance
* @param {Object} analysisResult - Complete analysis result
* @returns {Promise<Object>} Design system compliance analysis
*/
async analyzeDesignSystemCompliance(analysisResult) {
try {
const compliance = {
overall_score: 0,
color_system: await this.analyzeColorSystem(analysisResult),
typography_system: await this.analyzeTypographySystem(analysisResult),
spacing_system: await this.analyzeSpacingSystem(analysisResult),
component_system: await this.analyzeComponentSystem(analysisResult),
layout_system: await this.analyzeLayoutSystem(analysisResult),
recommendations: []
};
// Calculate overall compliance score
compliance.overall_score = this.calculateOverallScore(compliance);
// Generate recommendations
compliance.recommendations = this.generateRecommendations(compliance);
return compliance;
} catch (error) {
throw new Error(`Design system analysis failed: ${error.message}`);
}
}
/**
* Analyze color system compliance
*/
async analyzeColorSystem(analysisResult) {
const colors = analysisResult.color_analysis?.color_palette || [];
const dominantColors = analysisResult.color_analysis?.dominant_colors || {};
const colorSystem = {
palette_size: colors.length,
color_harmony: analysisResult.color_analysis?.color_harmony || {},
consistency_score: 0,
palette_type: 'unknown',
brand_compliance: {},
issues: [],
strengths: []
};
// Analyze palette size
if (colors.length <= 8) {
colorSystem.consistency_score += 0.3;
colorSystem.strengths.push('Restrained color palette promotes consistency');
} else if (colors.length > 15) {
colorSystem.issues.push('Large color palette may indicate inconsistency');
} else {
colorSystem.consistency_score += 0.1;
}
// Analyze color harmony
const harmony = colorSystem.color_harmony;
if (harmony.scheme_type && harmony.scheme_type !== 'none') {
colorSystem.consistency_score += 0.4;
colorSystem.palette_type = harmony.scheme_type;
colorSystem.strengths.push(`Follows ${harmony.scheme_type} color harmony`);
}
// Check for semantic color usage
const semanticColors = this.identifySemanticColors(colors);
if (semanticColors.length > 0) {
colorSystem.consistency_score += 0.2;
colorSystem.strengths.push(`${semanticColors.length} semantic colors identified`);
colorSystem.semantic_colors = semanticColors;
}
// Analyze color distribution
const colorDistribution = this.analyzeColorDistribution(colors);
colorSystem.distribution = colorDistribution;
if (colorDistribution.primary_dominance > 0.4) {
colorSystem.consistency_score += 0.1;
colorSystem.strengths.push('Strong primary color dominance');
}
return colorSystem;
}
/**
* Analyze typography system compliance
*/
async analyzeTypographySystem(analysisResult) {
const textElements = analysisResult.text_extraction?.structured_text || [];
const typographySystem = {
font_sizes: [],
size_scale: 'unknown',
hierarchy_clarity: 0,
consistency_score: 0,
issues: [],
strengths: []
};
if (textElements.length === 0) {
typographySystem.issues.push('No text elements found for analysis');
return typographySystem;
}
// Extract font sizes
const fontSizes = textElements
.map(text => text.font_info?.estimated_size)
.filter(size => size && size > 0)
.sort((a, b) => a - b);
typographySystem.font_sizes = [...new Set(fontSizes)];
if (typographySystem.font_sizes.length === 0) {
typographySystem.issues.push('No font size information available');
return typographySystem;
}
// Analyze type scale
const scaleAnalysis = this.analyzeTypeScale(typographySystem.font_sizes);
typographySystem.size_scale = scaleAnalysis.scale_type;
typographySystem.scale_ratio = scaleAnalysis.ratio;
typographySystem.consistency_score += scaleAnalysis.consistency;
if (scaleAnalysis.consistency > 0.7) {
typographySystem.strengths.push(`Follows ${scaleAnalysis.scale_type} scale`);
} else if (scaleAnalysis.consistency < 0.3) {
typographySystem.issues.push('Inconsistent font size scale');
}
// Analyze hierarchy
const hierarchyAnalysis = this.analyzeTypographyHierarchy(textElements);
typographySystem.hierarchy_clarity = hierarchyAnalysis.clarity;
typographySystem.hierarchy_levels = hierarchyAnalysis.levels;
if (hierarchyAnalysis.clarity > 0.7) {
typographySystem.strengths.push('Clear typographic hierarchy');
typographySystem.consistency_score += 0.3;
} else {
typographySystem.issues.push('Unclear typographic hierarchy');
}
// Check for reasonable font size range
const minSize = Math.min(...typographySystem.font_sizes);
const maxSize = Math.max(...typographySystem.font_sizes);
const sizeRange = maxSize / minSize;
if (sizeRange >= 2 && sizeRange <= 6) {
typographySystem.strengths.push('Appropriate font size range');
typographySystem.consistency_score += 0.2;
} else if (sizeRange > 8) {
typographySystem.issues.push('Very large font size range may impact consistency');
}
return typographySystem;
}
/**
* Analyze spacing system compliance
*/
async analyzeSpacingSystem(analysisResult) {
const components = analysisResult.components || [];
const layoutAnalysis = analysisResult.layout_analysis || {};
const spacingSystem = {
spacing_values: [],
scale_type: 'unknown',
consistency_score: 0,
grid_compliance: false,
issues: [],
strengths: []
};
if (components.length < 2) {
spacingSystem.issues.push('Insufficient components for spacing analysis');
return spacingSystem;
}
// Extract spacing values
const spacingValues = this.extractSpacingValues(components);
spacingSystem.spacing_values = spacingValues.unique;
// Analyze spacing scale
const scaleAnalysis = this.analyzeSpacingScale(spacingValues.all);
spacingSystem.scale_type = scaleAnalysis.scale_type;
spacingSystem.consistency_score = scaleAnalysis.consistency;
if (scaleAnalysis.consistency > 0.7) {
spacingSystem.strengths.push(`Follows ${scaleAnalysis.scale_type} spacing scale`);
} else if (scaleAnalysis.consistency < 0.4) {
spacingSystem.issues.push('Inconsistent spacing values');
}
// Check grid compliance
if (layoutAnalysis.grid_analysis?.detected) {
spacingSystem.grid_compliance = true;
spacingSystem.strengths.push('Grid-based layout promotes consistent spacing');
spacingSystem.consistency_score += 0.2;
}
// Analyze spacing distribution
const distribution = this.analyzeSpacingDistribution(spacingValues.all);
spacingSystem.distribution = distribution;
if (distribution.most_common_usage > 0.4) {
spacingSystem.strengths.push('Consistent use of primary spacing values');
spacingSystem.consistency_score += 0.1;
}
return spacingSystem;
}
/**
* Analyze component system compliance
*/
async analyzeComponentSystem(analysisResult) {
const components = analysisResult.components || [];
const componentSystem = {
component_types: {},
consistency_score: 0,
reusability_score: 0,
standardization: {},
issues: [],
strengths: []
};
if (components.length === 0) {
componentSystem.issues.push('No components found for analysis');
return componentSystem;
}
// Group components by type
components.forEach(comp => {
if (!componentSystem.component_types[comp.type]) {
componentSystem.component_types[comp.type] = [];
}
componentSystem.component_types[comp.type].push(comp);
});
// Analyze each component type
for (const [type, typeComponents] of Object.entries(componentSystem.component_types)) {
if (typeComponents.length >= 2) {
const typeAnalysis = this.analyzeComponentType(type, typeComponents);
componentSystem.standardization[type] = typeAnalysis;
if (typeAnalysis.consistency > 0.7) {
componentSystem.strengths.push(`Consistent ${type} components`);
componentSystem.consistency_score += 0.1;
} else if (typeAnalysis.consistency < 0.4) {
componentSystem.issues.push(`Inconsistent ${type} components`);
}
}
}
// Calculate reusability score
const reusableTypes = Object.values(componentSystem.component_types)
.filter(typeComponents => typeComponents.length >= 2);
componentSystem.reusability_score = reusableTypes.length / Object.keys(componentSystem.component_types).length;
if (componentSystem.reusability_score > 0.6) {
componentSystem.strengths.push('High component reusability');
} else if (componentSystem.reusability_score < 0.3) {
componentSystem.issues.push('Low component reusability');
}
return componentSystem;
}
/**
* Analyze layout system compliance
*/
async analyzeLayoutSystem(analysisResult) {
const layoutAnalysis = analysisResult.layout_analysis || {};
const components = analysisResult.components || [];
const layoutSystem = {
layout_type: layoutAnalysis.layout_type || 'unknown',
consistency_score: 0,
alignment_quality: 0,
responsive_indicators: [],
issues: [],
strengths: []
};
// Analyze alignment quality
const alignmentAnalysis = layoutAnalysis.alignment_analysis || {};
if (alignmentAnalysis.horizontal_groups || alignmentAnalysis.vertical_groups) {
const totalElements = components.length;
const alignedElements = (alignmentAnalysis.horizontal_groups?.reduce((sum, group) => sum + group.elements.length, 0) || 0) +
(alignmentAnalysis.vertical_groups?.reduce((sum, group) => sum + group.elements.length, 0) || 0);
layoutSystem.alignment_quality = Math.min(alignedElements / totalElements, 1);
if (layoutSystem.alignment_quality > 0.7) {
layoutSystem.strengths.push('Strong element alignment');
layoutSystem.consistency_score += 0.3;
} else if (layoutSystem.alignment_quality < 0.4) {
layoutSystem.issues.push('Poor element alignment');
}
}
// Check for grid system usage
if (layoutAnalysis.grid_analysis?.detected) {
layoutSystem.strengths.push('Grid-based layout system');
layoutSystem.consistency_score += 0.4;
const grid = layoutAnalysis.grid_analysis;
if (grid.regularity > 0.8) {
layoutSystem.strengths.push('Highly regular grid structure');
layoutSystem.consistency_score += 0.2;
}
}
// Analyze spacing consistency
if (layoutAnalysis.spacing_analysis) {
const hSpacing = layoutAnalysis.spacing_analysis.horizontal_spacing;
const vSpacing = layoutAnalysis.spacing_analysis.vertical_spacing;
const avgConsistency = ((hSpacing?.consistency || 0) + (vSpacing?.consistency || 0)) / 2;
if (avgConsistency > 0.7) {
layoutSystem.strengths.push('Consistent spacing throughout layout');
layoutSystem.consistency_score += 0.1;
} else if (avgConsistency < 0.4) {
layoutSystem.issues.push('Inconsistent spacing in layout');
}
}
// Check for responsive design indicators
const responsiveIndicators = this.detectResponsiveIndicators(analysisResult);
layoutSystem.responsive_indicators = responsiveIndicators;
if (responsiveIndicators.length > 0) {
layoutSystem.strengths.push(`${responsiveIndicators.length} responsive design indicators`);
layoutSystem.consistency_score += 0.1;
}
return layoutSystem;
}
/**
* Identify semantic colors (success, error, warning, info)
*/
identifySemanticColors(colors) {
const semanticColors = [];
colors.forEach(color => {
const hsl = this.hexToHSL(color.hex);
// Green range - success
if (hsl.h >= 90 && hsl.h <= 150 && hsl.s > 0.3) {
semanticColors.push({ ...color, semantic: 'success' });
}
// Red range - error/danger
else if ((hsl.h >= 0 && hsl.h <= 20) || (hsl.h >= 340 && hsl.h <= 360)) {
if (hsl.s > 0.3) {
semanticColors.push({ ...color, semantic: 'error' });
}
}
// Yellow/Orange range - warning
else if (hsl.h >= 35 && hsl.h <= 65 && hsl.s > 0.4) {
semanticColors.push({ ...color, semantic: 'warning' });
}
// Blue range - info
else if (hsl.h >= 190 && hsl.h <= 250 && hsl.s > 0.3) {
semanticColors.push({ ...color, semantic: 'info' });
}
});
return semanticColors;
}
/**
* Analyze color distribution patterns
*/
analyzeColorDistribution(colors) {
if (colors.length === 0) return { primary_dominance: 0 };
const totalPercentage = colors.reduce((sum, color) => sum + color.percentage, 0);
const primaryColor = colors[0];
return {
primary_dominance: primaryColor.percentage / totalPercentage,
top_3_dominance: colors.slice(0, 3).reduce((sum, color) => sum + color.percentage, 0) / totalPercentage,
distribution_evenness: this.calculateDistributionEvenness(colors)
};
}
/**
* Calculate distribution evenness (lower values = more even distribution)
*/
calculateDistributionEvenness(colors) {
const percentages = colors.map(color => color.percentage);
const avg = percentages.reduce((a, b) => a + b, 0) / percentages.length;
const variance = percentages.reduce((sum, p) => sum + Math.pow(p - avg, 2), 0) / percentages.length;
return Math.sqrt(variance) / avg;
}
/**
* Analyze typography scale
*/
analyzeTypeScale(fontSizes) {
if (fontSizes.length < 2) {
return { scale_type: 'insufficient_data', consistency: 0, ratio: 0 };
}
const ratios = [];
for (let i = 1; i < fontSizes.length; i++) {
ratios.push(fontSizes[i] / fontSizes[i - 1]);
}
// Check against known modular scales
const scaleChecks = [];
for (const [scaleName, expectedRatio] of Object.entries(this.designSystemPatterns.typography.modular_scales)) {
const avgRatio = ratios.reduce((a, b) => a + b, 0) / ratios.length;
const ratioConsistency = 1 - Math.abs(avgRatio - expectedRatio) / expectedRatio;
if (ratioConsistency > 0.7) {
scaleChecks.push({ scale: scaleName, consistency: ratioConsistency, ratio: expectedRatio });
}
}
if (scaleChecks.length > 0) {
const bestScale = scaleChecks.reduce((best, current) =>
current.consistency > best.consistency ? current : best
);
return {
scale_type: bestScale.scale,
consistency: bestScale.consistency,
ratio: bestScale.ratio
};
}
// Check against common font sizes
const commonSizeMatches = fontSizes.filter(size =>
this.designSystemPatterns.typography.common_scales.includes(size)
);
const commonSizeConsistency = commonSizeMatches.length / fontSizes.length;
return {
scale_type: commonSizeConsistency > 0.6 ? 'common_sizes' : 'custom',
consistency: commonSizeConsistency,
ratio: ratios.reduce((a, b) => a + b, 0) / ratios.length
};
}
/**
* Analyze typography hierarchy
*/
analyzeTypographyHierarchy(textElements) {
const sizeGroups = {};
textElements.forEach(text => {
const size = text.font_info?.estimated_size;
if (size) {
const roundedSize = Math.round(size / 2) * 2; // Group similar sizes
if (!sizeGroups[roundedSize]) {
sizeGroups[roundedSize] = [];
}
sizeGroups[roundedSize].push(text);
}
});
const levels = Object.keys(sizeGroups).map(Number).sort((a, b) => b - a);
// Check if hierarchy makes sense (larger sizes for headers, etc.)
let hierarchyClarity = 0;
if (levels.length >= 2) {
// Check if largest sizes are used for headers
const largestSizeTexts = sizeGroups[levels[0]];
const headerCount = largestSizeTexts.filter(text => text.type === 'header').length;
if (headerCount / largestSizeTexts.length > 0.5) {
hierarchyClarity += 0.4;
}
// Check for reasonable size differences between levels
const sizeDifferences = [];
for (let i = 1; i < levels.length; i++) {
sizeDifferences.push(levels[i - 1] - levels[i]);
}
const avgDifference = sizeDifferences.reduce((a, b) => a + b, 0) / sizeDifferences.length;
if (avgDifference >= 4 && avgDifference <= 12) {
hierarchyClarity += 0.4;
}
// Check for consistent level usage
const levelUsage = levels.map(level => sizeGroups[level].length);
const usageVariance = this.calculateVariance(levelUsage);
if (usageVariance < 0.5) {
hierarchyClarity += 0.2;
}
}
return {
clarity: hierarchyClarity,
levels: levels.length,
size_groups: sizeGroups
};
}
/**
* Extract spacing values from components
*/
extractSpacingValues(components) {
const spacingValues = [];
// Calculate distances between components
for (let i = 0; i < components.length; i++) {
for (let j = i + 1; j < components.length; j++) {
const comp1 = components[i];
const comp2 = components[j];
// Horizontal spacing
const horizontalSpacing = Math.abs(comp1.position.x - (comp2.position.x + comp2.position.width));
if (horizontalSpacing < 200) {
spacingValues.push(Math.round(horizontalSpacing));
}
// Vertical spacing
const verticalSpacing = Math.abs(comp1.position.y - (comp2.position.y + comp2.position.height));
if (verticalSpacing < 200) {
spacingValues.push(Math.round(verticalSpacing));
}
}
}
// Also include margins from edges (if available)
components.forEach(comp => {
if (comp.position.x < 50) spacingValues.push(comp.position.x);
if (comp.position.y < 50) spacingValues.push(comp.position.y);
});
return {
all: spacingValues,
unique: [...new Set(spacingValues)].sort((a, b) => a - b)
};
}
/**
* Analyze spacing scale
*/
analyzeSpacingScale(spacingValues) {
if (spacingValues.length === 0) {
return { scale_type: 'no_data', consistency: 0 };
}
const uniqueValues = [...new Set(spacingValues)].sort((a, b) => a - b);
// Check against common spacing scales
const scaleMatches = {};
for (const [scaleName, scaleValues] of Object.entries(this.designSystemPatterns.spacing)) {
const matches = uniqueValues.filter(value =>
scaleValues.some(scaleValue => Math.abs(value - scaleValue) <= this.options.spacingTolerance)
);
scaleMatches[scaleName] = matches.length / uniqueValues.length;
}
const bestMatch = Object.entries(scaleMatches).reduce((best, [scale, score]) =>
score > best.score ? { scale, score } : best
, { scale: 'custom', score: 0 });
return {
scale_type: bestMatch.score > 0.5 ? bestMatch.scale : 'custom',
consistency: bestMatch.score,
matches: scaleMatches
};
}
/**
* Analyze spacing distribution
*/
analyzeSpacingDistribution(spacingValues) {
const counts = {};
spacingValues.forEach(value => {
counts[value] = (counts[value] || 0) + 1;
});
const sortedCounts = Object.entries(counts).sort(([, a], [, b]) => b - a);
const totalValues = spacingValues.length;
return {
most_common_value: parseInt(sortedCounts[0]?.[0] || 0),
most_common_usage: (sortedCounts[0]?.[1] || 0) / totalValues,
top_3_usage: sortedCounts.slice(0, 3).reduce((sum, [, count]) => sum + count, 0) / totalValues
};
}
/**
* Analyze component type consistency
*/
analyzeComponentType(type, components) {
const analysis = {
count: components.length,
consistency: 0,
variations: {},
issues: []
};
// Analyze size consistency
const widths = components.map(comp => comp.position.width);
const heights = components.map(comp => comp.position.height);
const widthConsistency = 1 - this.calculateVariance(widths);
const heightConsistency = 1 - this.calculateVariance(heights);
analysis.size_consistency = (widthConsistency + heightConsistency) / 2;
// Analyze text content consistency (for buttons, etc.)
if (type === 'button') {
const textLengths = components
.map(comp => comp.text_content?.length || 0)
.filter(length => length > 0);
if (textLengths.length > 0) {
const textConsistency = 1 - this.calculateVariance(textLengths);
analysis.text_consistency = textConsistency;
}
}
// Calculate overall consistency
analysis.consistency = analysis.size_consistency;
if (analysis.text_consistency) {
analysis.consistency = (analysis.consistency + analysis.text_consistency) / 2;
}
return analysis;
}
/**
* Detect responsive design indicators
*/
detectResponsiveIndicators(analysisResult) {
const indicators = [];
const components = analysisResult.components || [];
const layoutAnalysis = analysisResult.layout_analysis || {};
// Check for flexible layouts
if (layoutAnalysis.layout_type === 'flexbox') {
indicators.push('Flexbox layout suggests responsive design');
}
if (layoutAnalysis.grid_analysis?.detected) {
indicators.push('Grid layout can adapt to different screen sizes');
}
// Check for relative sizing
const imageWidth = analysisResult.image_metadata?.width || 1000;
const relativeComponents = components.filter(comp =>
comp.position.width / imageWidth > 0.8 || comp.position.width / imageWidth < 0.2
);
if (relativeComponents.length > components.length * 0.3) {
indicators.push('Mix of full-width and narrow components suggests responsive design');
}
// Check for stacked layouts (mobile-first indicator)
const verticalGroups = layoutAnalysis.alignment_analysis?.vertical_groups || [];
if (verticalGroups.length === 1 && verticalGroups[0].elements.length > 3) {
indicators.push('Single column layout suggests mobile-first design');
}
return indicators;
}
/**
* Calculate overall design system compliance score
*/
calculateOverallScore(compliance) {
const scores = [
compliance.color_system.consistency_score,
compliance.typography_system.consistency_score,
compliance.spacing_system.consistency_score,
compliance.component_system.consistency_score,
compliance.layout_system.consistency_score
];
const validScores = scores.filter(score => score > 0);
if (validScores.length === 0) return 0;
return validScores.reduce((sum, score) => sum + score, 0) / validScores.length;
}
/**
* Generate design system recommendations
*/
generateRecommendations(compliance) {
const recommendations = [];
// Color system recommendations
if (compliance.color_system.consistency_score < 0.5) {
recommendations.push({
category: 'colors',
priority: 'high',
issue: 'Inconsistent color usage',
recommendation: 'Define a limited color palette with primary, secondary, and accent colors'
});
}
if (compliance.color_system.palette_size > 12) {
recommendations.push({
category: 'colors',
priority: 'medium',
issue: 'Large color palette',
recommendation: 'Reduce color palette to 8-12 colors for better consistency'
});
}
// Typography recommendations
if (compliance.typography_system.consistency_score < 0.5) {
recommendations.push({
category: 'typography',
priority: 'high',
issue: 'Inconsistent typography scale',
recommendation: 'Implement a modular typography scale (e.g., 1.25 ratio)'
});
}
if (compliance.typography_system.hierarchy_clarity < 0.5) {
recommendations.push({
category: 'typography',
priority: 'medium',
issue: 'Unclear typographic hierarchy',
recommendation: 'Establish clear size differences between heading levels'
});
}
// Spacing recommendations
if (compliance.spacing_system.consistency_score < 0.5) {
recommendations.push({
category: 'spacing',
priority: 'high',
issue: 'Inconsistent spacing values',
recommendation: 'Define a spacing scale (e.g., 4px, 8px, 16px, 24px, 32px)'
});
}
// Component recommendations
if (compliance.component_system.reusability_score < 0.4) {
recommendations.push({
category: 'components',
priority: 'medium',
issue: 'Low component reusability',
recommendation: 'Create reusable component variants instead of unique components'
});
}
// Layout recommendations
if (compliance.layout_system.alignment_quality < 0.5) {
recommendations.push({
category: 'layout',
priority: 'medium',
issue: 'Poor element alignment',
recommendation: 'Use grid or flexbox layouts for better alignment'
});
}
return recommendations;
}
// Helper methods
hexToHSL(hex) {
const r = parseInt(hex.slice(1, 3), 16) / 255;
const g = parseInt(hex.slice(3, 5), 16) / 255;
const b = parseInt(hex.slice(5, 7), 16) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return { h: h * 360, s, l };
}
calculateVariance(values) {
if (values.length === 0) return 0;
const avg = values.reduce((a, b) => a + b, 0) / values.length;
const variance = values.reduce((sum, val) => sum + Math.pow(val - avg, 2), 0) / values.length;
return variance / (avg * avg + 1);
}
}
module.exports = DesignSystemAnalyzer;