Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ZHB committed Jul 23, 2021
0 parents commit a274c94
Show file tree
Hide file tree
Showing 13 changed files with 580 additions and 0 deletions.
20 changes: 20 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Test

on: [push, pull_request]

jobs:
tests:
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
php-versions: [ '7.4', '8.0' ]
steps:
- uses: actions/checkout@master
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
- name: Install dependencies
run: composer install --prefer-dist --no-interaction --no-ansi --no-progress
- name: PHPUnit
run: vendor/bin/phpunit
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/vendor/
/composer.lock
19 changes: 19 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Copyright (c) Vincent Huck

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
Weather Gradient
==================

Weather gradient is a small library allowing to determine the RGB color at a given value in an interval bounded by
a combination of a minimum value and a color and a combination of a maximum value and a color.

In addition to the minimum and maximum limits, it is possible to add as many thresholds as desired.

<p align="center">
<img src="./doc/temperature-gradient.png" title="Weather Gradient example">
</p>

## Documentation

### Installation

Use [Composer](http://getcomposer.org/) to install Weather Gradient in your project :

```shell
composer require "zhb/weather-gradient"
```

### Usage

```php
$colors = [
0 => [59, 130, 246], // blue
30 => [239, 68, 68], // red
];

// create a gradient from given thresholds
$gradient = Gradient::fromColors($colors);

// get the RGB color at a specific gradient position
$color = $gradient->colorAtGradientPosition(18);

// print the color
echo $color;

// or get r, g, b values
$r = $color->getR();
$g = $color->getG();
$b = $color->getB();
```

In addition to the Gradient class, you can use the Contrast::darkOrLight(array $rgb) to determine if a dark or light text fit the best with the given rgb color.

```php
// $bestColor will contain [255, 255, 255] (white)
$bestColor = Contrast::darkOrLight($darkBlue = [85, 101, 242]);

// $bestColor will contain [0, 0, 0] (black)
$bestColor = Contrast::darkOrLight($lightBlue = [59, 130, 246]);
```

### Examples

A usage example can be found in [example](./example) folder.
23 changes: 23 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "zhb/weather-gradient",
"description": "Library for determining the color at a specific position in a bounded color gradient.",
"keywords": ["colors", "gradient", "weather"],
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Vincent Huck",
"email": "[email protected]",
"homepage": "https://github.com/ZHB"
}
],
"require": {
"php": "^7.4 || ^8.0"
},
"autoload": {
"psr-4": { "Zhb\\WeatherGradient\\": "src/WeatherGradient"}
},
"require-dev": {
"phpunit/phpunit": "^9.5"
}
}
Binary file added doc/temperature-gradient.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
60 changes: 60 additions & 0 deletions example/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

/*
* (c) ZHB <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

use Zhb\WeatherGradient\Gradient;

require_once __DIR__.'/../vendor/autoload.php';

?>

<html>
<head>
<title>Color gradient</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.6/tailwind.min.css" integrity="sha512-EYVjvPqURgm6pqtZxeqvlbZtnWjYmecnLS0QEedL51IUdaH0HXmSHjTKK7X1yWmiB3/5U1fwIv06ZDwLoo1LdA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body>
<div class="my-5 mx-5">
<div class="flex flex-row text-xs space-y-0 space-x-4">
<div class="w-40 flex-shrink-0">
<div class="h-10 flex flex-col justify-center">
<div class="text-sm font-semibold text-gray-900">Temperature</div>
<div>
<code class="text-xs text-gray-500">-20 °C to 40 °C</code>
</div>
</div>
</div>
<div class="min-w-0 flex-1 grid grid-cols-10 gap-x-1 gap-y-3">
<?php
$colors = [
-20 => [124, 58, 237], // purple
-10 => [59, 130, 246], // blue
0 => [239, 246, 255], // white
20 => [252, 211, 77], // yellow
35 => [239, 68, 68], // red
10 => [16, 185, 129], // green
40 => [236, 72, 153], // pink
];

$gradient = Gradient::fromColors($colors);

for ($temperature = -20; $temperature <= 40; ++$temperature) {
$color = $gradient->colorAtGradientPosition($temperature); ?>
<div class="space-y-1.5">
<div class="h-10 w-full rounded ring-1 ring-inset ring-black ring-opacity-0" style="background-color:<?php echo $color; ?>;"></div>
<div class="px-0.5 md:flex md:justify-between md:space-x-2 2xl:space-x-0 2xl:block">
<div class="w-20 font-medium text-gray-900"><?php echo $temperature; ?> °C</div><div><?php echo $color; ?></div>
</div>
</div>
<?php
} ?>
</div>
</div>
</div>
</body>
</html>
14 changes: 14 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>

<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="./vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="all">
<directory>./tests</directory>
</testsuite>
</testsuites>
</phpunit>
47 changes: 47 additions & 0 deletions src/WeatherGradient/Contrast.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

/*
* This file is part of the Weather Gradient package.
*
* (c) Vincent Huck <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Zhb\WeatherGradient;

class Contrast
{
/**
* Determines the brightness contrast ratio between a background and a foreground color.
*/
private static function brightnessContrastRatio(array $backgroundColor, array $foregroundColor): string
{
list($r1, $g1, $b1) = $backgroundColor;
list($r2, $g2, $b2) = $foregroundColor;

$l1 = 0.2126 * (($r1 / 255) ** 2.2) + 0.7152 * (($g1 / 255) ** 2.2) + 0.0722 * (($b1 / 255) ** 2.2);
$l2 = 0.2126 * (($r2 / 255) ** 2.2) + 0.7152 * (($g2 / 255) ** 2.2) + 0.0722 * (($b2 / 255) ** 2.2);

if ($l1 > $l2) {
$ratio = ($l1 + 0.05) / ($l2 + 0.05);
} else {
$ratio = ($l2 + 0.05) / ($l1 + 0.05);
}

return $ratio;
}

/**
* Return black or white hexadecimal color that fit best (best contrast ratio) with a given background color.
*/
public static function darkOrLight(array $backgroundColor, array $color1 = [255, 255, 255], array $color2 = [0, 0, 0], int $ratioBreak = 5): string
{
$ratio = self::brightnessContrastRatio($backgroundColor, $color2);

$preferredColor = $ratio < $ratioBreak ? $color1 : $color2;

return implode(',', $preferredColor);
}
}
27 changes: 27 additions & 0 deletions src/WeatherGradient/Exception/WeatherGradientException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

/*
* This file is part of the Weather Gradient package.
*
* (c) Vincent Huck <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Zhb\WeatherGradient\Exception;

use Throwable;

class WeatherGradientException extends \Exception
{
public static function missingOrInvalidNumberOfColors(Throwable $previous = null): self
{
return new self('Gradient colors must contain at least two RGB colors.', 0, $previous);
}

public static function arrayColorsNotValid(Throwable $previous = null): self
{
return new self('Colors must be an associative array with numeric keys only.', 0, $previous);
}
}
108 changes: 108 additions & 0 deletions src/WeatherGradient/Gradient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

/*
* This file is part of the Weather Gradient package.
*
* (c) Vincent Huck <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Zhb\WeatherGradient;

use Zhb\WeatherGradient\Exception\WeatherGradientException;

class Gradient
{
private array $rgbColors = [];

public function __construct(array $colors)
{
$this->rgbColors = $colors;

if (2 > $this->countColors()) {
throw WeatherGradientException::missingOrInvalidNumberOfColors();
}

if (!$this->isColorsValid($colors)) {
throw WeatherGradientException::arrayColorsNotValid();
}

ksort($this->rgbColors);
}

private function getRgbColors(): array
{
return array_values($this->rgbColors);
}

private function getThresholds(): array
{
return array_keys($this->rgbColors);
}

private function countColors(): int
{
return \count($this->rgbColors);
}

private function isColorsValid(array $array): bool
{
if ([] === $array) {
return false;
}

$keys = $this->getThresholds();

// numeric keys only
if ($keys !== array_filter($keys, 'is_int')) {
return false;
}

return $keys !== range(0, \count($array) - 1);
}

/**
* Retrieves the color at the given position of the gradient.
*/
public function colorAtGradientPosition(int $position): RgbColor
{
$gradientThresholds = $this->getThresholds();
$gradientRgbs = $this->getRgbColors();
$colorCount = $this->countColors();

$rgbColor = [];
for ($i = 0; $i < $colorCount; ++$i) {
if ($position >= $gradientThresholds[$i] && $position < ($gradientThresholds[$i + 1] ?? $gradientThresholds[$i])) {
$rgb1 = $gradientRgbs[$i];
$rgb2 = $gradientRgbs[$i + 1];

for ($j = 0; $j < 3; ++$j) {
$c = (max($rgb1[$j], $rgb2[$j]) - min($rgb1[$j], $rgb2[$j])) / (max($gradientThresholds[$i], $gradientThresholds[$i + 1]) - min($gradientThresholds[$i], $gradientThresholds[$i + 1]));

if ($rgb1[$j] < $rgb2[$j]) {
$rgbColor[] = (int) (max($rgb1[$j], $rgb2[$j]) - ((max($gradientThresholds[$i], $gradientThresholds[$i + 1]) - $position) * $c));
} else {
$rgbColor[] = (int) (min($rgb1[$j], $rgb2[$j]) + ((max($gradientThresholds[$i], $gradientThresholds[$i + 1]) - $position) * $c));
}
}
}
}

if ($position <= $gradientThresholds[0]) {
$rgbColor = $gradientRgbs[0];
}

if ($position >= $gradientThresholds[$colorCount - 1]) {
$rgbColor = $gradientRgbs[$colorCount - 1];
}

return RgbColor::fromRgb($rgbColor);
}

public static function fromColors(array $colors): self
{
return new self($colors);
}
}
Loading

0 comments on commit a274c94

Please sign in to comment.