📄 BrandPalette.php
📁 Path: /home/skhata/new.chandrapharma.com/app/Support/BrandPalette.php
📊 Size: 4.92 KB
🔒 Perm: 0600
📝 MIME: text/x-php
<?php
namespace App\Support;
/**
* Derives a full Tailwind-style 50–950 color palette from a single hex
* color, so the admin's "Brand Color" setting (one color picker) can drive
* every `primary-*` shade used throughout the storefront (buttons, links,
* badges, focus rings, etc.) rather than requiring 11 separate pickers.
*
* Output is RGB triplets as space-separated strings ("13 148 136"), not
* hex or rgb(...) — that's the format tailwind.config.js's
* `rgb(var(--color-primary-600) / <alpha-value>)` pattern expects when
* read from a CSS custom property.
*
* Approach: convert to HSL, anchor shade 600 to the exact input color
* (matching this project's existing convention where 600 IS the brand
* color), and interpolate lightness outward from there — lighter toward
* white for 50–500, darker toward black for 700–950. Hue and saturation
* are held constant; this won't perfectly match hand-tuned palettes like
* Tailwind's own, but it's more than close enough for admin-picked brand
* colors and never produces a broken/unreadable palette.
*/
class BrandPalette
{
/** Target lightness (0–100) for every shade except 600, which uses the input's own lightness. */
private const LIGHTNESS_TARGETS = [
'50' => 97.0,
'100' => 93.0,
'200' => 86.0,
'300' => 75.0,
'400' => 62.0,
'500' => 50.0,
'700' => null, // computed relative to the input — see shades()
'800' => null,
'900' => null,
'950' => null,
];
/**
* @return array<string, string> shade name ("50".."950") => "r g b"
*/
public static function shades(string $hex): array
{
[$h, $s, $l] = self::hexToHsl($hex);
$result = ['600' => self::hslToRgbTriplet($h, $s, $l)];
foreach (self::LIGHTNESS_TARGETS as $shade => $targetL) {
if ($targetL !== null) {
$result[$shade] = self::hslToRgbTriplet($h, $s, $targetL);
}
}
// Darker shades (700-950) are computed as a fraction of the input's
// own lightness rather than fixed targets — e.g. a light input
// color still darkens sensibly instead of clipping straight to
// near-black at every dark stop.
$result['700'] = self::hslToRgbTriplet($h, $s, max(0, $l * 0.80));
$result['800'] = self::hslToRgbTriplet($h, $s, max(0, $l * 0.62));
$result['900'] = self::hslToRgbTriplet($h, $s, max(0, $l * 0.48));
$result['950'] = self::hslToRgbTriplet($h, $s, max(0, $l * 0.30));
ksort($result, SORT_NUMERIC);
return $result;
}
/**
* @return array{0: float, 1: float, 2: float} [hue 0-360, saturation 0-100, lightness 0-100]
*/
private static function hexToHsl(string $hex): array
{
$hex = ltrim($hex, '#');
if (strlen($hex) === 3) {
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
}
if (! preg_match('/^[0-9a-fA-F]{6}$/', $hex)) {
// Malformed input (e.g. picker cleared mid-edit) — fall back
// to the project's original teal-600 rather than crashing a
// page render over a bad color value.
$hex = '0d9488';
}
$r = hexdec(substr($hex, 0, 2)) / 255;
$g = hexdec(substr($hex, 2, 2)) / 255;
$b = hexdec(substr($hex, 4, 2)) / 255;
$max = max($r, $g, $b);
$min = min($r, $g, $b);
$l = ($max + $min) / 2;
if ($max === $min) {
$h = $s = 0.0;
} else {
$d = $max - $min;
$s = $l > 0.5 ? $d / (2 - $max - $min) : $d / ($max + $min);
$h = match ($max) {
$r => ($g - $b) / $d + ($g < $b ? 6 : 0),
$g => ($b - $r) / $d + 2,
default => ($r - $g) / $d + 4,
};
$h /= 6;
}
return [$h * 360, $s * 100, $l * 100];
}
private static function hslToRgbTriplet(float $h, float $s, float $l): string
{
$h /= 360;
$s /= 100;
$l /= 100;
if ($s === 0.0) {
$r = $g = $b = $l;
} else {
$q = $l < 0.5 ? $l * (1 + $s) : $l + $s - $l * $s;
$p = 2 * $l - $q;
$r = self::hueToRgb($p, $q, $h + 1 / 3);
$g = self::hueToRgb($p, $q, $h);
$b = self::hueToRgb($p, $q, $h - 1 / 3);
}
return implode(' ', [
(int) round($r * 255),
(int) round($g * 255),
(int) round($b * 255),
]);
}
private static function hueToRgb(float $p, float $q, float $t): float
{
if ($t < 0) {
$t += 1;
}
if ($t > 1) {
$t -= 1;
}
if ($t < 1 / 6) {
return $p + ($q - $p) * 6 * $t;
}
if ($t < 1 / 2) {
return $q;
}
if ($t < 2 / 3) {
return $p + ($q - $p) * (2 / 3 - $t) * 6;
}
return $p;
}
}