Color Palette Generator — PHP source
Generate harmonious color palettes - complementary, analogous, triadic, tetradic, and monochromatic - from any base color. Export to CSS, Tailwind, or JSON.
This is the PHP implementation — the same logic the interactive tool runs, in a shareable, citable form.
<?php
/**
* palette-generator — polyglot showcase port (PHP).
*
* Pure color-theory helpers for the Color Palette Generator. No I/O, no
* external dependencies — standard library only. All inputs are clamped; the
* functions never throw (they fall back to black on bad input).
*
* Ported from src/lib/colorPalette.ts (TypeScript, the canonical
* implementation). This is display source — part of CosmoDev's polyglot tool
* pages (dev.cosmolabs.org), where each tool's pure logic is shown side-by-side
* in six languages.
*/
namespace CosmoDev\Palette;
// Scheme identifiers selecting which color-harmony rule generate_palette applies.
const COMPLEMENT = 'complement';
const SPLIT_COMPLEMENT = 'split-complement';
const ANALOGOUS = 'analogous';
const TRIADIC = 'triadic';
const TETRADIC = 'tetradic';
const MONOCHROMATIC = 'monochromatic';
/**
* Parse any reasonable hex string (#rgb / #rrggbb, with or without a leading #)
* into an [r, g, b] byte array. Unparseable input falls back to black so the
* generator never throws.
*
* @param string $hex
* @return array{0:int,1:int,2:int}
*/
function hex_to_rgb($hex) {
$h = preg_replace('/^#/', '', trim((string) $hex));
if (preg_match('/^[0-9a-fA-F]{3}$/', $h)) {
// Expand CSS shorthand: each digit doubles (#abc -> #aabbcc).
$doubled = '';
foreach (str_split($h) as $c) {
$doubled .= $c . $c;
}
$h = $doubled;
}
if (!preg_match('/^[0-9a-fA-F]{6}$/', $h)) {
return [0, 0, 0];
}
return [
(int) hexdec(substr($h, 0, 2)),
(int) hexdec(substr($h, 2, 2)),
(int) hexdec(substr($h, 4, 2)),
];
}
/**
* Round to the nearest integer (half away from zero — matching TypeScript's
* Math.round on the non-negative color values these helpers produce) and clamp
* to [0, 255].
*
* @param float|int $n
* @return int
*/
function clamp_byte($n) {
return (int) min(255, max(0, round($n)));
}
/**
* Pack three float channels into a lowercase #rrggbb string.
*
* @param float|int $r
* @param float|int $g
* @param float|int $b
* @return string
*/
function rgb_to_hex($r, $g, $b) {
return sprintf('#%02x%02x%02x', clamp_byte($r), clamp_byte($g), clamp_byte($b));
}
/**
* Convert hex → HSL. h ∈ [0, 360), s/l ∈ [0, 100]. Achromatic colors (gray,
* white, black) collapse to h = 0, s = 0.
*
* @param string $hex
* @return array{0:float,1:float,2:float}
*/
function hex_to_hsl($hex) {
[$r8, $g8, $b8] = hex_to_rgb($hex);
$r = $r8 / 255;
$g = $g8 / 255;
$b = $b8 / 255;
$max = max($r, $g, $b);
$min = min($r, $g, $b);
$l = ($max + $min) / 2;
$h = 0.0;
$s = 0.0;
if ($max !== $min) {
$d = $max - $min;
$s = $l > 0.5 ? $d / (2 - $max - $min) : $d / ($max + $min);
// Which channel is max determines the hue sextant.
if ($max === $r) {
$h = ($g - $b) / $d + ($g < $b ? 6 : 0);
} elseif ($max === $g) {
$h = ($b - $r) / $d + 2;
} else {
$h = ($r - $g) / $d + 4;
}
$h /= 6;
}
return [$h * 360, $s * 100, $l * 100];
}
/**
* Convert HSL → hex. h wraps modulo 360 (so h + 150 etc. stay in range); s/l
* clamp to [0, 100].
*
* @param float|int $h
* @param float|int $s
* @param float|int $l
* @return string
*/
function hsl_to_hex($h, $s, $l) {
// fmod keeps the dividend's sign (like JS %), so add 360 and mod again to
// land negative rotations in [0, 360).
$H = fmod(fmod($h, 360) + 360, 360);
$S = max(0, min(100, $s)) / 100;
$L = max(0, min(100, $l)) / 100;
$c = (1 - abs(2 * $L - 1)) * $S;
$x = $c * (1 - abs(fmod($H / 60, 2) - 1));
$m = $L - $c / 2;
if ($H < 60) {
[$r, $g, $b] = [$c, $x, 0];
} elseif ($H < 120) {
[$r, $g, $b] = [$x, $c, 0];
} elseif ($H < 180) {
[$r, $g, $b] = [0, $c, $x];
} elseif ($H < 240) {
[$r, $g, $b] = [0, $x, $c];
} elseif ($H < 300) {
[$r, $g, $b] = [$x, 0, $c];
} else {
[$r, $g, $b] = [$c, 0, $x];
}
return rgb_to_hex(($r + $m) * 255, ($g + $m) * 255, ($b + $m) * 255);
}
/**
* Generate a harmonious palette from a base color.
*
* Counts: complement = 2, split-complement = 3, analogous = 3, triadic = 3,
* tetradic = 4. $count is honored by monochromatic (default 5): the base hue
* and saturation are held while lightness spreads across $count steps.
*
* @param string $base_hex
* @param string $scheme One of the SCHEME_* constants above.
* @param int $count
* @return string[]
*/
function generate_palette($base_hex, $scheme, $count = 5) {
[$h, $s, $l] = hex_to_hsl($base_hex);
$base = hsl_to_hex($h, $s, $l);
// Rotate the hue by $deg degrees, holding saturation and lightness fixed.
// The closure captures h/s/l by value, matching the TypeScript closure.
$rot = function ($deg) use ($h, $s, $l) {
return hsl_to_hex($h + $deg, $s, $l);
};
switch ($scheme) {
case COMPLEMENT:
return [$base, $rot(180)];
case SPLIT_COMPLEMENT:
return [$base, $rot(150), $rot(210)];
case ANALOGOUS:
return [$rot(-30), $base, $rot(30)];
case TRIADIC:
return [$base, $rot(120), $rot(240)];
case TETRADIC:
return [$base, $rot(90), $rot(180), $rot(270)];
case MONOCHROMATIC:
$n = (int) max(1, floor($count));
// Pin the lightness window to [10, 90] so swatches never fully
// wash out or go black, regardless of the base color's lightness.
$lo = max(10, $l - 32);
$hi = min(90, $l + 32);
$out = [];
for ($i = 0; $i < $n; $i++) {
$ll = ($n === 1) ? $l : $lo + (($hi - $lo) * $i) / ($n - 1);
$out[] = hsl_to_hex($h, $s, $ll);
}
return $out;
default:
return [$base];
}
}
/**
* n shades — the base color mixed progressively toward black (RGB lerp). $i
* runs 1..n so the base itself is never returned, only intermediate steps.
*
* @param string $base_hex
* @param int $n
* @return string[]
*/
function shades($base_hex, $n) {
[$r, $g, $b] = hex_to_rgb($base_hex);
$steps = (int) max(1, floor($n));
$out = [];
for ($i = 1; $i <= $steps; $i++) {
$f = $i / ($steps + 1);
$out[] = rgb_to_hex($r * (1 - $f), $g * (1 - $f), $b * (1 - $f));
}
return $out;
}
/**
* n tints — the base color mixed progressively toward white (RGB lerp). $i
* runs 1..n so the base itself is never returned.
*
* @param string $base_hex
* @param int $n
* @return string[]
*/
function tints($base_hex, $n) {
[$r, $g, $b] = hex_to_rgb($base_hex);
$steps = (int) max(1, floor($n));
$out = [];
for ($i = 1; $i <= $steps; $i++) {
$f = $i / ($steps + 1);
$out[] = rgb_to_hex($r + (255 - $r) * $f, $g + (255 - $g) * $f, $b + (255 - $b) * $f);
}
return $out;
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →