initial: bootstrap from BukidBountyApp base
This commit is contained in:
145
app/Support/PaletteExtractor.php
Normal file
145
app/Support/PaletteExtractor.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
class PaletteExtractor
|
||||
{
|
||||
/**
|
||||
* Extract a usable brand palette from an image file.
|
||||
* Returns ['primary' => '#rrggbb', 'accent' => '#rrggbb', 'tint' => '#rrggbb']
|
||||
* or null if extraction fails (e.g. GD missing, unreadable file).
|
||||
*/
|
||||
public static function extract(string $path): ?array
|
||||
{
|
||||
if (!extension_loaded('gd') || !is_readable($path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$info = @getimagesize($path);
|
||||
if (!$info) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$img = match ($info[2]) {
|
||||
IMAGETYPE_JPEG => @imagecreatefromjpeg($path),
|
||||
IMAGETYPE_PNG => @imagecreatefrompng($path),
|
||||
IMAGETYPE_GIF => @imagecreatefromgif($path),
|
||||
IMAGETYPE_WEBP => function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($path) : null,
|
||||
default => null,
|
||||
};
|
||||
if (!$img) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$w = imagesx($img);
|
||||
$h = imagesy($img);
|
||||
$stepX = max(1, (int) floor($w / 60));
|
||||
$stepY = max(1, (int) floor($h / 60));
|
||||
|
||||
$buckets = [];
|
||||
for ($y = 0; $y < $h; $y += $stepY) {
|
||||
for ($x = 0; $x < $w; $x += $stepX) {
|
||||
$rgba = imagecolorat($img, $x, $y);
|
||||
$a = ($rgba >> 24) & 0x7F;
|
||||
if ($a > 90) {
|
||||
continue; // mostly transparent
|
||||
}
|
||||
$r = ($rgba >> 16) & 0xFF;
|
||||
$g = ($rgba >> 8) & 0xFF;
|
||||
$b = $rgba & 0xFF;
|
||||
|
||||
$sum = $r + $g + $b;
|
||||
if ($sum < 60 || $sum > 720) {
|
||||
continue; // too dark or too light
|
||||
}
|
||||
|
||||
$max = max($r, $g, $b);
|
||||
$min = min($r, $g, $b);
|
||||
if (($max - $min) < 25) {
|
||||
continue; // too gray
|
||||
}
|
||||
|
||||
// quantize to 32-step buckets
|
||||
$key = (($r >> 5) << 10) | (($g >> 5) << 5) | ($b >> 5);
|
||||
if (!isset($buckets[$key])) {
|
||||
$buckets[$key] = ['r' => 0, 'g' => 0, 'b' => 0, 'n' => 0];
|
||||
}
|
||||
$buckets[$key]['r'] += $r;
|
||||
$buckets[$key]['g'] += $g;
|
||||
$buckets[$key]['b'] += $b;
|
||||
$buckets[$key]['n']++;
|
||||
}
|
||||
}
|
||||
imagedestroy($img);
|
||||
|
||||
if (empty($buckets)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
uasort($buckets, fn ($a, $b) => $b['n'] <=> $a['n']);
|
||||
|
||||
$candidates = [];
|
||||
foreach ($buckets as $bucket) {
|
||||
$candidates[] = [
|
||||
'r' => (int) round($bucket['r'] / $bucket['n']),
|
||||
'g' => (int) round($bucket['g'] / $bucket['n']),
|
||||
'b' => (int) round($bucket['b'] / $bucket['n']),
|
||||
'n' => $bucket['n'],
|
||||
];
|
||||
if (count($candidates) >= 8) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$primary = $candidates[0];
|
||||
$accent = null;
|
||||
foreach (array_slice($candidates, 1) as $c) {
|
||||
$dr = $c['r'] - $primary['r'];
|
||||
$dg = $c['g'] - $primary['g'];
|
||||
$db = $c['b'] - $primary['b'];
|
||||
if (sqrt($dr * $dr + $dg * $dg + $db * $db) > 60) {
|
||||
$accent = $c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$accent) {
|
||||
$accent = self::shift($primary, 30);
|
||||
}
|
||||
|
||||
$tint = self::mixWithWhite($primary, 0.92);
|
||||
|
||||
return [
|
||||
'primary' => self::toHex($primary),
|
||||
'accent' => self::toHex($accent),
|
||||
'tint' => self::toHex($tint),
|
||||
];
|
||||
}
|
||||
|
||||
private static function toHex(array $c): string
|
||||
{
|
||||
return sprintf('#%02x%02x%02x',
|
||||
max(0, min(255, $c['r'])),
|
||||
max(0, min(255, $c['g'])),
|
||||
max(0, min(255, $c['b']))
|
||||
);
|
||||
}
|
||||
|
||||
private static function shift(array $c, int $delta): array
|
||||
{
|
||||
return [
|
||||
'r' => ($c['r'] + $delta) % 256,
|
||||
'g' => ($c['g'] + $delta * 2) % 256,
|
||||
'b' => ($c['b'] + $delta * 3) % 256,
|
||||
];
|
||||
}
|
||||
|
||||
private static function mixWithWhite(array $c, float $whiteRatio): array
|
||||
{
|
||||
$w = max(0.0, min(1.0, $whiteRatio));
|
||||
return [
|
||||
'r' => (int) round($c['r'] * (1 - $w) + 255 * $w),
|
||||
'g' => (int) round($c['g'] * (1 - $w) + 255 * $w),
|
||||
'b' => (int) round($c['b'] * (1 - $w) + 255 * $w),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user