PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
fluent-cart / app / Services / Theme / ColorMath.php

ColorMath.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.5, at app/Services/Theme/ColorMath.php

244 lines 6.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Services\Theme;
4
5 /**
6 * Colour maths for the storefront palette.
7 *
8 * A block theme hands us a handful of palette colours. The storefront needs a
9 * coherent set of surfaces, borders, muted text and accent tints. Rather than
10 * invent those, we derive them from the colours the theme actually declares —
11 * a border is the body text mixed most of the way toward the surface, a muted
12 * text tone is the same mix stopped earlier, and button text is whichever of
13 * light/dark reads better on the button it sits on.
14 *
15 * All maths is sRGB. Luminance follows the WCAG 2.x relative-luminance
16 * definition so the contrast picks match what an accessibility checker sees.
17 */
18 class ColorMath
19 {
20 /**
21 * Parse a colour into an RGB triplet.
22 *
23 * Accepts #rgb, #rrggbb, #rrggbbaa and rgb()/rgba(). Returns null for
24 * anything unreadable so callers can fall back instead of guessing.
25 *
26 * @param string $color
27 * @return array|null [r, g, b] or null
28 */
29 public static function parse($color): ?array
30 {
31 $color = trim((string)$color);
32
33 if ($color === '') {
34 return null;
35 }
36
37 if (preg_match('/^#([0-9a-f]{3})$/i', $color, $matches)) {
38 $shorthand = $matches[1];
39
40 return [
41 hexdec(str_repeat($shorthand[0], 2)),
42 hexdec(str_repeat($shorthand[1], 2)),
43 hexdec(str_repeat($shorthand[2], 2)),
44 ];
45 }
46
47 if (preg_match('/^#([0-9a-f]{6})(?:[0-9a-f]{2})?$/i', $color, $matches)) {
48 $sixDigit = $matches[1];
49
50 return [
51 hexdec(substr($sixDigit, 0, 2)),
52 hexdec(substr($sixDigit, 2, 2)),
53 hexdec(substr($sixDigit, 4, 2)),
54 ];
55 }
56
57 if (preg_match('/^rgba?\(\s*([0-9.]+)[\s,]+([0-9.]+)[\s,]+([0-9.]+)/i', $color, $matches)) {
58 return [
59 (int)round((float)$matches[1]),
60 (int)round((float)$matches[2]),
61 (int)round((float)$matches[3]),
62 ];
63 }
64
65 return null;
66 }
67
68 /**
69 * Convert an RGB triplet to #rrggbb.
70 *
71 * @param array $rgb
72 * @return string
73 */
74 public static function toHex(array $rgb): string
75 {
76 $hex = '#';
77
78 foreach (array_slice($rgb, 0, 3) as $channel) {
79 $channel = max(0, min(255, (int)round($channel)));
80 $hex .= str_pad(dechex($channel), 2, '0', STR_PAD_LEFT);
81 }
82
83 return $hex;
84 }
85
86 /**
87 * Normalise any readable colour to #rrggbb.
88 *
89 * @param string $color
90 * @param string $fallback Returned when the colour cannot be parsed.
91 * @return string
92 */
93 public static function hex($color, string $fallback = ''): string
94 {
95 $rgb = self::parse($color);
96
97 return $rgb ? self::toHex($rgb) : $fallback;
98 }
99
100 /**
101 * Mix two colours.
102 *
103 * @param string $first
104 * @param string $second
105 * @param int $percent How much of $first to keep, 0-100.
106 * @return string Hex, or '' when neither colour is readable.
107 */
108 public static function mix($first, $second, int $percent = 50): string
109 {
110 $firstRgb = self::parse($first);
111 $secondRgb = self::parse($second);
112
113 if (!$firstRgb || !$secondRgb) {
114 return self::hex($firstRgb ? $first : $second);
115 }
116
117 $ratio = max(0, min(100, $percent)) / 100;
118 $mixed = [];
119
120 for ($channel = 0; $channel < 3; $channel++) {
121 $mixed[$channel] = ($firstRgb[$channel] * $ratio) + ($secondRgb[$channel] * (1 - $ratio));
122 }
123
124 return self::toHex($mixed);
125 }
126
127 /**
128 * WCAG relative luminance.
129 *
130 * @param string $color
131 * @return float 0-1, or -1 when the colour cannot be read.
132 */
133 public static function luminance($color): float
134 {
135 $rgb = self::parse($color);
136
137 if (!$rgb) {
138 return -1.0;
139 }
140
141 $linear = [];
142
143 foreach (array_slice($rgb, 0, 3) as $channel) {
144 $channel = $channel / 255;
145 $linear[] = $channel <= 0.03928
146 ? $channel / 12.92
147 : pow(($channel + 0.055) / 1.055, 2.4);
148 }
149
150 return (0.2126 * $linear[0]) + (0.7152 * $linear[1]) + (0.0722 * $linear[2]);
151 }
152
153 /**
154 * Whether a colour reads as dark.
155 *
156 * The 0.45 threshold sits above the midpoint on purpose: mid-tone brand
157 * colours read as "dark" to the eye well before their luminance halves.
158 *
159 * @param string $color
160 * @return bool
161 */
162 public static function isDark($color): bool
163 {
164 $luminance = self::luminance($color);
165
166 return $luminance >= 0 && $luminance < 0.45;
167 }
168
169 /**
170 * WCAG contrast ratio between two colours.
171 *
172 * @param string $first
173 * @param string $second
174 * @return float 1-21, or 0 when either colour cannot be read.
175 */
176 public static function contrast($first, $second): float
177 {
178 $firstLuminance = self::luminance($first);
179 $secondLuminance = self::luminance($second);
180
181 if ($firstLuminance < 0 || $secondLuminance < 0) {
182 return 0.0;
183 }
184
185 $lighter = max($firstLuminance, $secondLuminance);
186 $darker = min($firstLuminance, $secondLuminance);
187
188 return ($lighter + 0.05) / ($darker + 0.05);
189 }
190
191 /**
192 * Pick whichever of two text colours reads better on a background.
193 *
194 * @param string $background
195 * @param string $light
196 * @param string $dark
197 * @return string
198 */
199 public static function readableOn($background, string $light = '#ffffff', string $dark = '#1f2937'): string
200 {
201 return self::contrast($background, $light) >= self::contrast($background, $dark) ? $light : $dark;
202 }
203
204 /**
205 * Nudge a colour toward white.
206 *
207 * @param string $color
208 * @param int $percent
209 * @return string
210 */
211 public static function lighten($color, int $percent): string
212 {
213 return self::mix('#ffffff', $color, $percent);
214 }
215
216 /**
217 * Nudge a colour toward black.
218 *
219 * @param string $color
220 * @param int $percent
221 * @return string
222 */
223 public static function darken($color, int $percent): string
224 {
225 return self::mix('#000000', $color, $percent);
226 }
227
228 /**
229 * Move a colour away from the surface it sits on, so it stays visible on
230 * a light or a dark background without the caller knowing which it has.
231 *
232 * @param string $color
233 * @param string $background
234 * @param int $percent
235 * @return string
236 */
237 public static function awayFrom($color, $background, int $percent): string
238 {
239 return self::isDark($background)
240 ? self::lighten($color, $percent)
241 : self::darken($color, $percent);
242 }
243 }
244