510 lines · 12.8 KB
Raw Download
1
<?php
2
// functions.php
3
function hslToRgb($h, $s, $l) {
4
	$h /= 360;
5
	$s /= 100;
6
	$l /= 100;
7
8
	if ($s == 0) {
9
		$r = $g = $b = $l;
10
	} else {
11
		$q = $l < 0.5 ? $l * (1 + $s) : $l + $s - $l * $s;
12
		$p = 2 * $l - $q;
13
		
14
		$r = hueToRgb($p, $q, $h + 1/3);
15
		$g = hueToRgb($p, $q, $h);
16
		$b = hueToRgb($p, $q, $h - 1/3);
17
	}
18
19
	return [
20
		round($r * 255),
21
		round($g * 255),
22
		round($b * 255)
23
	];
24
}
25
26
function lchToRgb($l, $c, $h, $alpha = 1) {
27
	$a = $c * cos(deg2rad($h));
28
	$b = $c * sin(deg2rad($h));
29
	return array_merge(labToRgb($l, $a, $b), [$alpha]);
30
}
31
32
function hwbToRgb($h, $w, $b) {
33
	$h = $h % 360;
34
	if ($h < 0) {
35
		$h += 360;
36
	}
37
	
38
	$w = min(100, max(0, $w)) / 100;
39
	$b = min(100, max(0, $b)) / 100;
40
	
41
	if ($w + $b >= 1) {
42
		$gray = $w / ($w + $b) * 255;
43
		return [round($gray), round($gray), round($gray)];
44
	}
45
	
46
	$h = $h / 360;
47
	
48
	$i = floor($h * 6);
49
	$f = $h * 6 - $i;
50
	$p = 1 * (1 - $f);
51
	$q = 1 * $f;
52
	
53
	switch ($i % 6) {
54
		case 0: $r = 1; $g = $q; $b_val = 0; break;
55
		case 1: $r = $p; $g = 1; $b_val = 0; break;
56
		case 2: $r = 0; $g = 1; $b_val = $q; break;
57
		case 3: $r = 0; $g = $p; $b_val = 1; break;
58
		case 4: $r = $q; $g = 0; $b_val = 1; break;
59
		case 5: $r = 1; $g = 0; $b_val = $p; break;
60
	}
61
	
62
	$factor = (1 - $w - $b);
63
	$r = $r * $factor + $w;
64
	$g = $g * $factor + $w;
65
	$b_val = $b_val * $factor + $w;
66
	
67
	return [
68
		round($r * 255),
69
		round($g * 255),
70
		round($b_val * 255)
71
	];
72
}
73
74
function hueToRgb($p, $q, $t) {
75
	if ($t < 0) $t += 1;
76
	if ($t > 1) $t -= 1;
77
	if ($t < 1/6) return $p + ($q - $p) * 6 * $t;
78
	if ($t < 1/2) return $q;
79
	if ($t < 2/3) return $p + ($q - $p) * (2/3 - $t) * 6;
80
	return $p;
81
}
82
83
function cmykToRgb($c, $m, $y, $k) {
84
	$c = $c / 100;
85
	$m = $m / 100;
86
	$y = $y / 100;
87
	$k = $k / 100;
88
	
89
	$r = 255 * (1 - $c) * (1 - $k);
90
	$g = 255 * (1 - $m) * (1 - $k);
91
	$b = 255 * (1 - $y) * (1 - $k);
92
	
93
	return [round($r), round($g), round($b)];
94
}
95
96
function labToRgb($l, $a, $b) {
97
	$y = ($l + 16) / 116;
98
	$x = $a / 500 + $y;
99
	$z = $y - $b / 200;
100
	
101
	$lab2xyz = function($v) {
102
		$v3 = $v * $v * $v;
103
		return $v3 > 0.008856 ? $v3 : ($v - 16/116) / 7.787;
104
	};
105
	
106
	$x = 0.95047 * $lab2xyz($x);
107
	$y = 1.00000 * $lab2xyz($y);
108
	$z = 1.08883 * $lab2xyz($z);
109
	
110
	$r = $x *  3.2406 + $y * -1.5372 + $z * -0.4986;
111
	$g = $x * -0.9689 + $y *  1.8758 + $z *  0.0415;
112
	$b = $x *  0.0557 + $y * -0.2040 + $z *  1.0570;
113
	
114
	$clip = function($v) {
115
		$v = $v > 0.0031308 ? 1.055 * pow($v, 1/2.4) - 0.055 : 12.92 * $v;
116
		return round(max(0, min(1, $v)) * 255);
117
	};
118
	
119
	return [$clip($r), $clip($g), $clip($b)];
120
}
121
122
function hsbToRgb($h, $s, $v) {
123
	$s = $s / 100;
124
	$v = $v / 100;
125
	$h = $h / 60;
126
	
127
	$i = floor($h);
128
	$f = $h - $i;
129
	$p = $v * (1 - $s);
130
	$q = $v * (1 - $s * $f);
131
	$t = $v * (1 - $s * (1 - $f));
132
	
133
	switch ($i) {
134
		case 0: $r = $v; $g = $t; $b = $p; break;
135
		case 1: $r = $q; $g = $v; $b = $p; break;
136
		case 2: $r = $p; $g = $v; $b = $t; break;
137
		case 3: $r = $p; $g = $q; $b = $v; break;
138
		case 4: $r = $t; $g = $p; $b = $v; break;
139
		default: $r = $v; $g = $p; $b = $q; break;
140
	}
141
	
142
	return [
143
		round($r * 255),
144
		round($g * 255),
145
		round($b * 255)
146
	];
147
}
148
149
function oklabToRgb($L, $a, $b, $alpha = 1) {
150
	$l = $L + 0.3963377774 * $a + 0.2158037573 * $b;
151
	$m = $L - 0.1055613458 * $a - 0.0638541728 * $b;
152
	$s = $L - 0.0894841775 * $a - 1.2914855480 * $b;
153
154
	$l = $l * $l * $l;
155
	$m = $m * $m * $m;
156
	$s = $s * $s * $s;
157
158
	$r = +4.0767416621 * $l - 3.3077115913 * $m + 0.2309699292 * $s;
159
	$g = -1.2684380046 * $l + 2.6097574011 * $m - 0.3413193965 * $s;
160
	$b = -0.0041960863 * $l - 0.7034186147 * $m + 1.7076147010 * $s;
161
162
	$toSRGB = function($x) {
163
		if ($x <= 0) return 0;
164
		if ($x >= 1) return 255;
165
		return round(($x >= 0.0031308 ? 1.055 * pow($x, 1/2.4) - 0.055 : 12.92 * $x) * 255);
166
	};
167
168
	return [
169
		$toSRGB($r),
170
		$toSRGB($g),
171
		$toSRGB($b),
172
		$alpha
173
	];
174
}
175
176
function oklchToRgb($L, $c, $h, $alpha = 1) {
177
	$a = $c * cos(deg2rad($h));
178
	$b = $c * sin(deg2rad($h));
179
	return oklabToRgb($L, $a, $b, $alpha);
180
}
181
182
function blendColors($fg, $bg, $alpha) {
183
	return [
184
		($fg[0] * $alpha) + ($bg[0] * (1 - $alpha)),
185
		($fg[1] * $alpha) + ($bg[1] * (1 - $alpha)),
186
		($fg[2] * $alpha) + ($bg[2] * (1 - $alpha))
187
	];
188
}
189
190
function getLuminance($rgb, $alpha = 1, $bg = null) {
191
	if ($alpha < 1 && $bg !== null) {
192
		$rgb = blendColors($rgb, $bg, $alpha);
193
	}
194
	
195
	$rgb = array_map(function($val) {
196
		$val = $val / 255;
197
		return $val <= 0.03928 ? $val / 12.92 : pow(($val + 0.055) / 1.055, 2.4);
198
	}, $rgb);
199
	
200
	return $rgb[0] * 0.2126 + $rgb[1] * 0.7152 + $rgb[2] * 0.0722;
201
}
202
203
function getCleanColorName($color) {
204
	$color = preg_replace('/^(color|background-color|border-color):\s*/i', '', $color);
205
	$color = preg_replace('/[;{}].*$/', '', $color);
206
	return trim($color);
207
}
208
209
function getContrastRatio($l1, $l2) {
210
	$lighter = max($l1, $l2);
211
	$darker = min($l1, $l2);
212
	return ($lighter + 0.05) / ($darker + 0.05);
213
}
214
215
function getWCAGLevel($ratio) {
216
	if ($ratio >= 7) return 'AAA';
217
	if ($ratio >= 4.5) return 'AA';
218
	if ($ratio >= 3) return 'AA Large';
219
	return 'Fail';
220
}
221
222
function getCssColor($color) {
223
	$rgba = parseColor($color);
224
	if ($rgba === false) {
225
		return '#000000';
226
	}
227
	
228
	if (!isset($rgba[3]) || $rgba[3] >= 0.999) {
229
		return sprintf('rgb(%d, %d, %d)', $rgba[0], $rgba[1], $rgba[2]);
230
	}
231
	
232
	return sprintf('rgba(%d, %d, %d, %.3f)', $rgba[0], $rgba[1], $rgba[2], $rgba[3]);
233
}
234
235
/**
236
 * Convert sRGB to Y (luminance) for APCA
237
 * Based on the reference JavaScript implementation
238
 */
239
function sRGBtoY($rgb) {
240
	$mainTRC = 2.4;
241
	$sRco = 0.2126729;
242
	$sGco = 0.7151522;
243
	$sBco = 0.0721750;
244
	
245
	$simpleExp = function($chan) use ($mainTRC) {
246
		return pow($chan / 255.0, $mainTRC);
247
	};
248
	
249
	return $sRco * $simpleExp($rgb[0]) + $sGco * $simpleExp($rgb[1]) + $sBco * $simpleExp($rgb[2]);
250
}
251
252
/**
253
 * Calculate APCA contrast value between two colors
254
 * Based on the APCA algorithm by Andrew Somers (version 0.1.9 for WCAG 3)
255
 * 
256
 * @param array $bg RGB background color array [r,g,b]
257
 * @param array $text RGB text color array [r,g,b]
258
 * @param float $bgAlpha Background alpha (0-1)
259
 * @param float $textAlpha Text alpha (0-1)
260
 * @return float APCA contrast value (can be negative or positive)
261
 */
262
function getAPCAContrast($bg, $text, $bgAlpha = 1, $textAlpha = 1) {
263
	// Handle alpha blending if needed
264
	if ($bgAlpha < 1 || $textAlpha < 1) {
265
		// For simplicity, assume white backdrop for alpha blending
266
		$backdrop = [255, 255, 255];
267
		if ($bgAlpha < 1) {
268
			$bg = blendColors($bg, $backdrop, $bgAlpha);
269
		}
270
		if ($textAlpha < 1) {
271
			$text = blendColors($text, $bg, $textAlpha);
272
		}
273
	}
274
	
275
	// APCA constants from the JavaScript reference
276
	$normBG = 0.56;
277
	$normTXT = 0.57;
278
	$revTXT = 0.62;
279
	$revBG = 0.65;
280
	$blkThrs = 0.022;
281
	$blkClmp = 1.414;
282
	$scaleBoW = 1.14;
283
	$scaleWoB = 1.14;
284
	$loBoWoffset = 0.027;
285
	$loWoBoffset = 0.027;
286
	$deltaYmin = 0.0005;
287
	$loClip = 0.1;
288
	
289
	// Convert sRGB to Y (luminance)
290
	$txtY = sRGBtoY($text);
291
	$bgY = sRGBtoY($bg);
292
	
293
	// Validate input range
294
	$icp = [0.0, 1.1];
295
	if (is_nan($txtY) || is_nan($bgY) || min($txtY, $bgY) < $icp[0] || max($txtY, $bgY) > $icp[1]) {
296
		return 0.0;
297
	}
298
	
299
	// Apply soft clip and clamp black levels
300
	$txtY = ($txtY > $blkThrs) ? $txtY : $txtY + pow($blkThrs - $txtY, $blkClmp);
301
	$bgY = ($bgY > $blkThrs) ? $bgY : $bgY + pow($blkThrs - $bgY, $blkClmp);
302
	
303
	// Check if the difference is too small (same or nearly same colors)
304
	if (abs($bgY - $txtY) < $deltaYmin) {
305
		return 0.0;
306
	}
307
	
308
	$SAPC = 0.0;
309
	$outputContrast = 0.0;
310
	
311
	if ($bgY > $txtY) { // Normal polarity (dark text on light background)
312
		$SAPC = (pow($bgY, $normBG) - pow($txtY, $normTXT)) * $scaleBoW;
313
		$outputContrast = ($SAPC < $loClip) ? 0.0 : $SAPC - $loBoWoffset;
314
	} else { // Reverse polarity (light text on dark background)
315
		$SAPC = (pow($bgY, $revBG) - pow($txtY, $revTXT)) * $scaleWoB;
316
		$outputContrast = ($SAPC > -$loClip) ? 0.0 : $SAPC + $loWoBoffset;
317
	}
318
	
319
	// Return as percentage
320
	return $outputContrast * 100.0;
321
}
322
323
/**
324
 * Get APCA contrast level
325
 * Based on APCA guidelines
326
 * 
327
 * @param float $contrast APCA contrast value
328
 * @return string APCA compliance level
329
 */
330
function getAPCALevel($contrast) {
331
	$absContrast = abs($contrast);
332
	
333
	if ($absContrast >= 90) {
334
		return 'Perfect - Lc 90+ (All text)';
335
	} elseif ($absContrast >= 75) {
336
		return 'Excellent - Lc 75+ (Body text)';
337
	} elseif ($absContrast >= 60) {
338
		return 'Good - Lc 60+ (Large text)';
339
	} elseif ($absContrast >= 45) {
340
		return 'Fair - Lc 45+ (Large bold text)';
341
	} elseif ($absContrast >= 30) {
342
		return 'Poor - Lc 30+ (Spot text only)';
343
	} else {
344
		return 'Fail - Insufficient contrast';
345
	}
346
}
347
348
/**
349
 * Calculate APCA contrast (convenience function)
350
 * 
351
 * @param array $textColor RGB text color array
352
 * @param array $bgColor RGB background color array
353
 * @param float $textAlpha Text alpha (0-1)
354
 * @param float $bgAlpha Background alpha (0-1)
355
 * @return float APCA contrast value
356
 */
357
function calcAPCA($textColor, $bgColor, $textAlpha = 1, $bgAlpha = 1) {
358
	return getAPCAContrast($bgColor, $textColor, $bgAlpha, $textAlpha);
359
}
360
361
/**
362
 * Alpha blend function for APCA calculations
363
 * 
364
 * @param array $rgbaFG Foreground color with alpha [r,g,b,a]
365
 * @param array $rgbBG Background color [r,g,b]
366
 * @param bool $round Whether to round the result
367
 * @return array Blended RGB color
368
 */
369
function alphaBlend($rgbaFG, $rgbBG, $round = true) {
370
	$alpha = max(min($rgbaFG[3], 1.0), 0.0);
371
	$compBlend = 1.0 - $alpha;
372
	$rgbOut = [0, 0, 0];
373
	
374
	for ($i = 0; $i < 3; $i++) {
375
		$rgbOut[$i] = $rgbBG[$i] * $compBlend + $rgbaFG[$i] * $alpha;
376
		if ($round) {
377
			$rgbOut[$i] = min(round($rgbOut[$i]), 255);
378
		}
379
	}
380
	
381
	return $rgbOut;
382
}
383
384
/**
385
 * Reverse APCA - find unknown color given contrast and known color
386
 * 
387
 * @param float $contrast Target APCA contrast
388
 * @param float $knownY Known color luminance
389
 * @param string $knownType Either 'bg' or 'text'
390
 * @param string $returnAs Return format: 'hex', 'rgb', or 'Y'
391
 * @return mixed The calculated color in requested format, or false if impossible
392
 */
393
function reverseAPCA($contrast = 0, $knownY = 1.0, $knownType = 'bg', $returnAs = 'hex') {
394
	if (abs($contrast) < 9) {
395
		return false;
396
	}
397
	
398
	$normBG = 0.56;
399
	$normTXT = 0.57;
400
	$revTXT = 0.62;
401
	$revBG = 0.65;
402
	$blkThrs = 0.022;
403
	$blkClmp = 1.414;
404
	$scaleBoW = 1.14;
405
	$scaleWoB = 1.14;
406
	$loBoWoffset = 0.027;
407
	$loWoBoffset = 0.027;
408
	$mainTRCencode = 1 / 2.4;
409
	
410
	// Constants for reverse calculation
411
	$mFactor = 1.94685544331710;
412
	$mFactInv = 1 / $mFactor;
413
	$mOffsetIn = 0.03873938165714010;
414
	$mExpAdj = 0.2833433964208690;
415
	$mExp = $mExpAdj / $blkClmp;
416
	$mOffsetOut = 0.3128657958707580;
417
	
418
	$unknownY = $knownY;
419
	
420
	$scale = $contrast > 0 ? $scaleBoW : $scaleWoB;
421
	$offset = $contrast > 0 ? $loBoWoffset : -$loWoBoffset;
422
	$contrast = ($contrast * 0.01 + $offset) / $scale;
423
	
424
	$knownY = ($knownY > $blkThrs) ? $knownY : $knownY + pow($blkThrs - $knownY, $blkClmp);
425
	
426
	if ($knownType == 'bg' || $knownType == 'background') {
427
		$knownExp = $contrast > 0 ? $normBG : $revBG;
428
		$unknownExp = $contrast > 0 ? $normTXT : $revTXT;
429
		$unknownY = pow(pow($knownY, $knownExp) - $contrast, 1 / $unknownExp);
430
		if (is_nan($unknownY)) return false;
431
	} elseif ($knownType == 'txt' || $knownType == 'text') {
432
		$knownExp = $contrast > 0 ? $normTXT : $revTXT;
433
		$unknownExp = $contrast > 0 ? $normBG : $revBG;
434
		$unknownY = pow($contrast + pow($knownY, $knownExp), 1 / $unknownExp);
435
		if (is_nan($unknownY)) return false;
436
	} else {
437
		return false;
438
	}
439
	
440
	if ($unknownY > 1.06 || $unknownY < 0) {
441
		return false;
442
	}
443
	
444
	$unknownY = ($unknownY > $blkThrs) ? $unknownY : (pow((($unknownY + $mOffsetIn) * $mFactor), $mExp) * $mFactInv) - $mOffsetOut;
445
	
446
	$unknownY = max(min($unknownY, 1.0), 0.0);
447
	
448
	if ($returnAs === 'hex') {
449
		$hexB = dechex(round(pow($unknownY, $mainTRCencode) * 255));
450
		$hexB = str_pad($hexB, 2, '0', STR_PAD_LEFT);
451
		return '#' . $hexB . $hexB . $hexB;
452
	} elseif ($returnAs === 'rgb') {
453
		$colorB = round(pow($unknownY, $mainTRCencode) * 255);
454
		return [$colorB, $colorB, $colorB];
455
	} elseif ($returnAs === 'Y' || $returnAs === 'y') {
456
		return max(0.0, $unknownY);
457
	} else {
458
		return false;
459
	}
460
}
461
462
/**
463
 * Get combined WCAG and APCA level for "Best of Both" mode
464
 * Uses contrast values with mapped thresholds instead of named categories
465
 */
466
function getCombinedLevel($wcag_contrast, $apca_contrast) {
467
	$apca_abs = abs($apca_contrast);
468
	
469
	// Tier 1: Perfect (APCA ≥90 + WCAG ≥10)
470
	if ($apca_abs >= 90 && $wcag_contrast >= 10) {
471
		return 'Perfect';
472
	}
473
	
474
	// Tier 1: Excellent (APCA ≥75 + WCAG ≥7)
475
	if ($apca_abs >= 75 && $wcag_contrast >= 7) {
476
		return 'Excellent';
477
	}
478
	
479
	// Tier 2: Good (APCA ≥60 + WCAG ≥4.5)
480
	if ($apca_abs >= 60 && $wcag_contrast >= 4.5) {
481
		return 'Good';
482
	}
483
	
484
	// Tier 3: Fair (APCA ≥45 + WCAG ≥3)
485
	if ($apca_abs >= 45 && $wcag_contrast >= 3) {
486
		return 'Fair';
487
	}
488
	
489
	// Tier 4: Poor (APCA ≥30 + WCAG ≥1.7)
490
	if ($apca_abs >= 30 && $wcag_contrast >= 1.7) {
491
		return 'Poor';
492
	}
493
494
	return 'Fail';
495
}
496
497
function getCopyrightYears($foundedYear) {
498
	$currentYear = date('Y');
499
	$site = "&copy; <a href=\"https://jefftml.com\">Jeff Lange</a>";
500
	if ($foundedYear > $currentYear) {
501
		return $site;
502
	}
503
	
504
	if ($foundedYear == $currentYear) {
505
		return $site . " " . $currentYear;
506
	}
507
	
508
	return $site . " " . $foundedYear . " - " . $currentYear;
509
}
510
?>