diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..cd8eb86 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..bb6265e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +/.gitattributes export-ignore +/.gitignore export-ignore +/.travis.yml export-ignore +/phpunit.xml.dist export-ignore +/.scrutinizer.yml export-ignore +/tests export-ignore +/.editorconfig export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e02cac5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea +build +composer.lock +docs +vendor +coverage diff --git a/.phpunit.result.cache b/.phpunit.result.cache new file mode 100644 index 0000000..f2bb06e --- /dev/null +++ b/.phpunit.result.cache @@ -0,0 +1 @@ +C:30:"PHPUnit\Runner\TestResultCache":1802:{a:2:{s:7:"defects";a:7:{s:121:"Breadthe\PhpContrast\Tests\ContrastTest::it_returns_random_color_pairs_of_minimum_specified_ratio_but_no_less_than_3_to_1";i:3;s:111:"Breadthe\PhpContrast\Tests\ContrastTest::it_returns_the_correct_contrast_ratio with data set "maximum contrast"";i:4;s:123:"Breadthe\PhpContrast\Tests\ContrastTest::it_returns_the_correct_contrast_ratio with data set "no contrast - black on black"";i:4;s:123:"Breadthe\PhpContrast\Tests\ContrastTest::it_returns_the_correct_contrast_ratio with data set "no contrast - white on white"";i:4;s:108:"Breadthe\PhpContrast\Tests\ContrastTest::it_returns_the_correct_contrast_ratio with data set "high contrast"";i:4;s:107:"Breadthe\PhpContrast\Tests\ContrastTest::it_returns_the_correct_contrast_ratio with data set "low contrast"";i:4;s:94:"Breadthe\PhpContrast\Tests\ContrastTest::it_returns_random_color_pairs_of_minimum_3_to_1_ratio";i:3;}s:5:"times";a:7:{s:111:"Breadthe\PhpContrast\Tests\ContrastTest::it_returns_the_correct_contrast_ratio with data set "maximum contrast"";d:0.001;s:123:"Breadthe\PhpContrast\Tests\ContrastTest::it_returns_the_correct_contrast_ratio with data set "no contrast - black on black"";d:0;s:123:"Breadthe\PhpContrast\Tests\ContrastTest::it_returns_the_correct_contrast_ratio with data set "no contrast - white on white"";d:0;s:108:"Breadthe\PhpContrast\Tests\ContrastTest::it_returns_the_correct_contrast_ratio with data set "high contrast"";d:0;s:107:"Breadthe\PhpContrast\Tests\ContrastTest::it_returns_the_correct_contrast_ratio with data set "low contrast"";d:0;s:94:"Breadthe\PhpContrast\Tests\ContrastTest::it_returns_random_color_pairs_of_minimum_3_to_1_ratio";d:0;s:121:"Breadthe\PhpContrast\Tests\ContrastTest::it_returns_random_color_pairs_of_minimum_specified_ratio_but_no_less_than_3_to_1";d:0;}}} \ No newline at end of file diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..df16b68 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,19 @@ +filter: + excluded_paths: [tests/*] + +checks: + php: + remove_extra_empty_lines: true + remove_php_closing_tag: true + remove_trailing_whitespace: true + fix_use_statements: + remove_unused: true + preserve_multiple: false + preserve_blanklines: true + order_alphabetically: true + fix_php_opening_tag: true + fix_linefeed: true + fix_line_ending: true + fix_identation_4spaces: true + fix_doc_comments: true + diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..f4d3cbc --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,4 @@ +preset: laravel + +disabled: + - single_class_element_per_statement diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7a96344 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +language: php + +php: + - 7.1 + - 7.2 + - 7.3 + - 7.4 + +env: + matrix: + - COMPOSER_FLAGS="--prefer-lowest" + - COMPOSER_FLAGS="" + +before_script: + - travis_retry composer self-update + - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-source + +script: + - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover + +after_script: + - php vendor/bin/ocular code-coverage:upload --format=php-clover coverage.clover diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b5a2cd5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +All notable changes to `php-contrast` will be documented in this file + +## 1.0.0 - 201X-XX-XX + +- initial release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b4ae1c4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +Please read and understand the contribution guide before creating an issue or pull request. + +## Etiquette + +This project is open source, and as such, the maintainers give their free time to build and maintain the source code +held within. They make the code freely available in the hope that it will be of use to other developers. It would be +extremely unfair for them to suffer abuse or anger for their hard work. + +Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the +world that developers are civilized and selfless people. + +It's the duty of the maintainer to ensure that all submissions to the project are of sufficient +quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. + +## Viability + +When requesting or submitting new features, first consider whether it might be useful to others. Open +source projects are used by many developers, who may have entirely different needs to your own. Think about +whether or not your feature is likely to be used by other users of the project. + +## Procedure + +Before filing an issue: + +- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. +- Check to make sure your feature suggestion isn't already present within the project. +- Check the pull requests tab to ensure that the bug doesn't have a fix in progress. +- Check the pull requests tab to ensure that the feature isn't already in progress. + +Before submitting a pull request: + +- Check the codebase to ensure that your feature doesn't already exist. +- Check the pull requests to ensure that another person hasn't already submitted the feature or fix. + +## Requirements + +If the project maintainer has any additional requirements, you will find them listed here. + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + +**Happy coding**! diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..a3fef6f --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Omigosh Dev + +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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1b69665 --- /dev/null +++ b/README.md @@ -0,0 +1,157 @@ +# Very short description of the package + +[![Latest Version on Packagist](https://img.shields.io/packagist/v/breadthe/php-contrast.svg?style=flat-square)](https://packagist.org/packages/breadthe/php-contrast) +[![Build Status](https://img.shields.io/travis/breadthe/php-contrast/master.svg?style=flat-square)](https://travis-ci.org/breadthe/php-contrast) +[![Quality Score](https://img.shields.io/scrutinizer/g/breadthe/php-contrast.svg?style=flat-square)](https://scrutinizer-ci.com/g/breadthe/php-contrast) +[![Total Downloads](https://img.shields.io/packagist/dt/breadthe/php-contrast.svg?style=flat-square)](https://packagist.org/packages/breadthe/php-contrast) + +Provides various utilities for working with color contrast. + +## Installation + +You can install the package via composer: + +```bash +composer require breadthe/php-contrast +``` + +## Usage + +Import the class. + +```php +//use Breadthe\PhpContrast\Contrast; +use Breadthe\PhpContrast\HexColor; +use Breadthe\PhpContrast\HexColorPair; +use Breadthe\PhpContrast\TailwindColor; +``` + +## Check the contrast ratio between 2 colors + +```php +$hexColorPair = HexColor::make(HexColor::make('000000'), HexColor::make('ffffff')); +$hexColorPair->ratio; // 21 +$hexColorPair->fg; // '#000000' +$hexColorPair->bg; // '#ffffff' +``` + +## Get a random color pair (with the resulting ratio), with minimum 3:1 contrast ratio + +```php +$hexColorPair = HexColorPair::random(); +$hexColorPair->ratio; // 3.8 +$hexColorPair->fg->hex; // '#36097e' +$hexColorPair->fg->name; // null +$hexColorPair->bg->hex; // '#ed4847' +$hexColorPair->bg->name; // null +``` + +## Get a random color pair (with the resulting ratio), with minimum specified contrast ratio (but no less than 3:1) + +**⚠️ Warning** For performance reasons, the minimum requested contrast ratio is capped at 4.5, although the generated pairs can go up to the theoretical maximum 21:1 ratio. + +**⚠️ Caution** When chaining with `minContrast()`, make sure to use `getRandom()` instead of `random()`. + +```php +$hexColorPair = HexColorPair::minContrast(4.5)->getRandom(); +$hexColorPair->ratio; // 7.6 +$hexColorPair->fg->hex; // '#0c402f' +$hexColorPair->fg->name; // null +$hexColorPair->bg->hex; // '#badd73' +$hexColorPair->bg->name; // null +``` + +## Get a random accessible sibling for the given color, with minimum specified contrast ratio (but no less than 3:1) + +```php +// Minimum 3:1 contrast ratio +HexColorPair::sibling('0000000'); // [21, '0000000', 'ffffff'] + +// Minimum specified contrast ratio (no less than 3:1) +HexColorPair::minContrast(4.5)->getSibling('0000000'); // [21, '0000000', 'ffffff'] +``` + +## Generate a random TailwindCSS color + +```php +$twColor = TailwindColor::random(); +$twColor->hex; // '#e2e8f0' +$twColor->name; // 'gray-300' +``` + +## Generate a pair of random accessible TailwindCSS colors + +```php +$twColorpair = TailwindColor::randomPair(); +$twColorpair->ratio; // 3.3 +$twColorpair->fg->hex; // '#63b3ed' +$twColorpair->fg->name; // 'blue-400' +$twColorpair->bg->hex; // '#4a5568' +$twColorpair->bg->name; // 'green-700' +``` + +## Generate a pair of random accessible TailwindCSS colors, with minimum specified contrast ratio (but no less than 3:1) + +**⚠️ Warning** For performance reasons, the minimum requested contrast ratio is capped at 4.5, although the generated pairs can go up to the theoretical maximum 21:1 ratio. + +**⚠️ Caution** When chaining with `minContrast()`, make sure to use `getRandomPair()` instead of `randomPair()`. + +```php +$twColorpair = TailwindColor::minContrast(4.5)->getRandomPair(); +$twColorpair->ratio; // 7.0 +$twColorpair->fg->hex; // '#faf5ff' +$twColorpair->fg->name; // 'purple-100' +$twColorpair->bg->hex; // '#9b2c2c' +$twColorpair->bg->name; // 'red-800' +``` + +## Merge the default Tailwind colors with a custom palette + +You may extend the default Tailwind colors with your own custom palette. Here's an example of how to import a custom palette from a JSON file. + +```json +{ + "background": "#FFFFFF", + "headline": "#1f1235", + "sub-headline": "#1b1425", + "button": "#ff6e6c", + "button-text": "#1f1235" +} +``` + +```php +$customPalette = json_decode(file_get_contents('custom-palette.json'), true); + +$colors = TailwindColor::merge($customPalette)->getColors(); +``` + +### Testing + +``` bash +composer test +``` + +### Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) for details. + +### Security + +If you discover any security related issues, please email omigoshdev@protonmail.com instead of using the issue tracker. + +## Credits + +- [Omigosh Dev](https://github.com/breadthe) +- [All Contributors](../../contributors) + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. + +## PHP Package Boilerplate + +This package was generated using the [PHP Package Boilerplate](https://laravelpackageboilerplate.com). diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..24c9ef5 --- /dev/null +++ b/composer.json @@ -0,0 +1,44 @@ +{ + "name": "breadthe/php-contrast", + "description": "Color and contrast tools", + "keywords": [ + "breadthe", + "php-contrast" + ], + "homepage": "https://github.com/breadthe/php-contrast", + "license": "MIT", + "type": "library", + "authors": [ + { + "name": "Omigosh Dev", + "email": "omigoshdev@protonmail.com", + "role": "Developer" + } + ], + "require": { + "php": "7.*", + "ext-json": "*", + "tightenco/collect": "^6.14" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "autoload": { + "psr-4": { + "Breadthe\\PhpContrast\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Breadthe\\PhpContrast\\Tests\\": "tests" + } + }, + "scripts": { + "test": "vendor/bin/phpunit", + "test-coverage": "vendor/bin/phpunit --coverage-html coverage" + + }, + "config": { + "sort-packages": true + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..22fe879 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + tests + + + + + src/ + + + + + + + + + + diff --git a/src/Color.php b/src/Color.php new file mode 100644 index 0000000..6d5c1da --- /dev/null +++ b/src/Color.php @@ -0,0 +1,8 @@ +ratio = $ratio; + $this->color1 = $color1; + $this->color2 = $color2; + } + + /** + * '000' => '#000000' + * '000000' => '#000000' + */ +// protected function normalize($color) +// { +// return $color; // +// } +} diff --git a/src/HexColor.php b/src/HexColor.php new file mode 100644 index 0000000..f60ee33 --- /dev/null +++ b/src/HexColor.php @@ -0,0 +1,58 @@ +hex = $this->normalize($hex); + $this->name = $name; + } + + public static function make(string $hex, string $name = null): self + { + return new static($hex, $name); + } + + public static function random(): self + { + return new static(self::randomColor()); + } + + /** + * '#abc' or 'abc' => '#aabbcc' + * '#abcdef' or 'abcdef' => '#abcdef' + */ + protected function normalize(string $hexColor) + { + // '#abcdef' or 'abcdef' + if (preg_match('/^\s*#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})\s*$/i', $hexColor, $matches)) { + return sprintf("#%s%s%s", $matches[1], $matches[2], $matches[3]); + } + + // '#abc' or 'abc' + if (preg_match('/^\s*#?([0-9a-f])([0-9a-f])([0-9a-f])\s*$/i', $hexColor, $matches)) { + return sprintf("#%s%s%s", str_repeat($matches[1], 2), str_repeat($matches[2], 2), + str_repeat($matches[3], 2)); + } + + return null; // TODO throw + } + + /** + * @see https://stackoverflow.com/questions/5614530/generating-a-random-hex-color-code-with-php + */ + protected static function randomColor() + { + return static::randomColorPart() . static::randomColorPart() . static::randomColorPart(); + } + + protected static function randomColorPart() + { + return str_pad(dechex(mt_rand(0, 255)), 2, '0', STR_PAD_LEFT); + } +} diff --git a/src/HexColorPair.php b/src/HexColorPair.php new file mode 100644 index 0000000..ed22be3 --- /dev/null +++ b/src/HexColorPair.php @@ -0,0 +1,137 @@ +fg = $fg; + $this->bg = $bg; + $this->ratio = static::calculateRatio($fg, $bg); + } + + public static function make(HexColor $fg, HexColor $bg) + { + return new static($fg, $bg); + } + + public static function ratio(HexColor $fg, HexColor $bg) + { + return (new static($fg, $bg))->ratio; + } + + public static function random(): self + { + return (new static)->getRandom(); + } + + public static function sibling(string $hexValue): HexColor + { + return (new static)->getSibling($hexValue); + } + + public static function minContrast($ratio): self + { + if ($ratio < self::MIN_RATIO) { + $ratio = self::MIN_RATIO; + } + + if ($ratio > self::MAX_RATIO) { + $ratio = self::MAX_RATIO; + } + + $pair = new static; + $pair->minRatio = $ratio; + + return $pair; + } + + public function getRandom(): self + { + $minRatio = $this->minRatio ?? self::MIN_RATIO; + + $fg = HexColor::random(); + + return $this->getRandomPairMinRatio($fg, $minRatio); + } + + public function getSibling(string $hexValue): HexColor + { + $minRatio = $this->minRatio ?? self::MIN_RATIO; + + $pair = $this->getRandomPairMinRatio(HexColor::make($hexValue), $minRatio); + + return $pair->bg; + } + + protected static function calculateRatio(HexColor $fg = null, HexColor $bg = null) + { + if (!$fg || !$bg) { + return null; + } + + $fgLuminance = static::luminance($fg); + $bgLuminance = static::luminance($bg); + + return round((max($fgLuminance, $bgLuminance) + 0.05) / (min($fgLuminance, $bgLuminance) + 0.05) * 10) / 10; + } + + protected static function luminance(HexColor $color) + { + [$rHex, $gHex, $bHex] = static::rgbHexChannels($color); + + // Get decimal values + $r8bit = base_convert($rHex, 16, 10); + $g8bit = base_convert($gHex, 16, 10); + $b8bit = base_convert($bHex, 16, 10); + + // Get sRGB values + $rSrgb = $r8bit / 255; + $gSrgb = $g8bit / 255; + $bSrgb = $b8bit / 255; + + // Calculate luminance + $r = ($rSrgb <= 0.03928) ? $rSrgb / 12.92 : pow((($rSrgb + 0.055) / 1.055), 2.4); + $g = ($gSrgb <= 0.03928) ? $gSrgb / 12.92 : pow((($gSrgb + 0.055) / 1.055), 2.4); + $b = ($bSrgb <= 0.03928) ? $bSrgb / 12.92 : pow((($bSrgb + 0.055) / 1.055), 2.4); + + return (0.2126 * $r + 0.7152 * $g + 0.0722 * $b); + } + + /** + * '#abcdef' => ['ab', 'cd', 'ef'] + */ + protected static function rgbHexChannels(HexColor $hexColor): array + { + $hex = substr($hexColor->hex, 1); // strip the # + + return [ + substr($hex, 0, 2), + substr($hex, 2, 2), + substr($hex, 4, 2), + ]; + } + + protected function getRandomPairMinRatio($fg, $minRatio): self + { + do { + $bg = HexColor::random(); + $pair = new static($fg, $bg); + } while ($pair->ratio <= $minRatio); + + return $pair; + } +} diff --git a/src/TailwindColor.php b/src/TailwindColor.php new file mode 100644 index 0000000..b2fbdf9 --- /dev/null +++ b/src/TailwindColor.php @@ -0,0 +1,127 @@ +colors = $this->getDefaultTailwindColors(); + } + + /** + * Returns all the default Tailwind colors + */ + public static function colors(): array + { + return (new static)->colors->toArray(); + } + + public function getColors(): array + { + return $this->colors->toArray(); + } + + /** + * Returns a random Tailwind color + */ + public static function random(): HexColor + { + $tailwindColors = (new static)->colors; + + return $tailwindColors[rand(0, count($tailwindColors) - 1)]; + } + + public static function minContrast($ratio): self + { + if ($ratio < HexColorPair::MIN_RATIO) { + $ratio = HexColorPair::MIN_RATIO; + } + + if ($ratio > HexColorPair::MAX_RATIO) { + $ratio = HexColorPair::MAX_RATIO; + } + + $twColor = new static; + $twColor->minRatio = $ratio; + + return $twColor; + } + + public static function merge(array $customPalette): self + { + $twColor = new static; + + $customPalette = collect($customPalette) + ->map(function ($hex, $name) { + return HexColor::make($hex, $name); + }); + + $twColor->colors = $twColor->colors->merge($customPalette->flatten()); + + return $twColor; + } + + /** + * Returns a random pair of accessible (min. contrast 3:1) Tailwind colors + */ + public static function randomPair(): HexColorPair + { + return (new static)->getRandomPair(); + } + + public function getRandomPair(): HexColorPair + { + $minRatio = $this->minRatio ?? HexColorPair::MIN_RATIO; + + $fg = static::random(); + + return $this->getRandomPairMinRatio($fg, $minRatio); + } + + protected function getRandomPairMinRatio($fg, $minRatio): HexColorPair + { + do { + $bg = static::random(); + $pair = HexColorPair::make($fg, $bg); + } while ($pair->ratio <= $minRatio); + + return $pair; + } + + protected function getDefaultTailwindColors(): Collection + { + $twColorsArr = $this->parseDefaultTailwindColors(); + + return $this->flatten($twColorsArr['colors']); + } + + protected function flatten(array $twColorsArr): Collection + { + $colors = new Collection(); + + collect($twColorsArr) + ->each(function ($color, $label) use ($colors) { + if (is_array($color)) { + foreach ($color as $shade => $value) { + $colors->push(HexColor::make($value, "{$label}-{$shade}")); + } + } else { + $colors->push(HexColor::make($color, $label)); + } + }); + + return $colors; + } + + protected function parseDefaultTailwindColors() + { + return json_decode(file_get_contents(__DIR__ . '/../stubs/twcolors.json'), true); + } +} diff --git a/stubs/custom-palette.json b/stubs/custom-palette.json new file mode 100644 index 0000000..502a684 --- /dev/null +++ b/stubs/custom-palette.json @@ -0,0 +1,7 @@ +{ + "background": "#FFFFFF", + "headline": "#1f1235", + "sub-headline": "#1b1425", + "button": "#ff6e6c", + "button-text": "#1f1235" +} diff --git a/stubs/twcolors.json b/stubs/twcolors.json new file mode 100644 index 0000000..5afabd8 --- /dev/null +++ b/stubs/twcolors.json @@ -0,0 +1,116 @@ +{ + "colors": { + "black": "#000000", + "white": "#ffffff", + "gray": { + "100": "#f7fafc", + "200": "#edf2f7", + "300": "#e2e8f0", + "400": "#cbd5e0", + "500": "#a0aec0", + "600": "#718096", + "700": "#4a5568", + "800": "#2d3748", + "900": "#1a202c" + }, + "red": { + "100": "#fff5f5", + "200": "#fed7d7", + "300": "#feb2b2", + "400": "#fc8181", + "500": "#f56565", + "600": "#e53e3e", + "700": "#c53030", + "800": "#9b2c2c", + "900": "#742a2a" + }, + "orange": { + "100": "#fffaf0", + "200": "#feebc8", + "300": "#fbd38d", + "400": "#f6ad55", + "500": "#ed8936", + "600": "#dd6b20", + "700": "#c05621", + "800": "#9c4221", + "900": "#7b341e" + }, + "yellow": { + "100": "#fffff0", + "200": "#fefcbf", + "300": "#faf089", + "400": "#f6e05e", + "500": "#ecc94b", + "600": "#d69e2e", + "700": "#b7791f", + "800": "#975a16", + "900": "#744210" + }, + "green": { + "100": "#f0fff4", + "200": "#c6f6d5", + "300": "#9ae6b4", + "400": "#68d391", + "500": "#48bb78", + "600": "#38a169", + "700": "#2f855a", + "800": "#276749", + "900": "#22543d" + }, + "teal": { + "100": "#e6fffa", + "200": "#b2f5ea", + "300": "#81e6d9", + "400": "#4fd1c5", + "500": "#38b2ac", + "600": "#319795", + "700": "#2c7a7b", + "800": "#285e61", + "900": "#234e52" + }, + "blue": { + "100": "#ebf8ff", + "200": "#bee3f8", + "300": "#90cdf4", + "400": "#63b3ed", + "500": "#4299e1", + "600": "#3182ce", + "700": "#2b6cb0", + "800": "#2c5282", + "900": "#2a4365" + }, + "indigo": { + "100": "#ebf4ff", + "200": "#c3dafe", + "300": "#a3bffa", + "400": "#7f9cf5", + "500": "#667eea", + "600": "#5a67d8", + "700": "#4c51bf", + "800": "#434190", + "900": "#3c366b" + }, + "purple": { + "100": "#faf5ff", + "200": "#e9d8fd", + "300": "#d6bcfa", + "400": "#b794f4", + "500": "#9f7aea", + "600": "#805ad5", + "700": "#6b46c1", + "800": "#553c9a", + "900": "#44337a" + }, + "pink": { + "100": "#fff5f7", + "200": "#fed7e2", + "300": "#fbb6ce", + "400": "#f687b3", + "500": "#ed64a6", + "600": "#d53f8c", + "700": "#b83280", + "800": "#97266d", + "900": "#702459" + } + } +} diff --git a/tests/HexColorPairTest.php b/tests/HexColorPairTest.php new file mode 100644 index 0000000..2e196a1 --- /dev/null +++ b/tests/HexColorPairTest.php @@ -0,0 +1,92 @@ +ratio); + + // Test the static constructor + self::assertEquals($contrastRatio, HexColorPair::ratio($fg, $bg)); + } + + /** + * @test + * + * For more assurance, call the following assertions inside a loop + */ + public function it_returns_random_color_pairs_of_minimum_3_to_1_ratio() + { + $minRatio = 3; + + $pair = HexColorPair::random(); + self::assertGreaterThanOrEqual($minRatio, $pair->ratio); + + // Color pairs with a desired min ratio < 3 will default to 3 + $desiredMinRatio = 2; + $pair = HexColorPair::minContrast($desiredMinRatio)->getRandom(); + self::assertGreaterThanOrEqual($minRatio, $pair->ratio); + + // Color pairs are generated with the minimum specified ratio + $desiredMinRatio = 4.5; + $pair = HexColorPair::minContrast($desiredMinRatio)->getRandom(); + self::assertGreaterThanOrEqual($desiredMinRatio, $pair->ratio); + } + + /** @test */ + public function a_sibling_of_minimum_contrast_is_generated_correctly() + { + $minRatio = 3; + $hexColor = '#cccccc'; + + // Sibling is generated with a minimum contrast ratio of 3.0 + $sibling = HexColorPair::sibling($hexColor); + $pair = HexColorPair::make(HexColor::make($hexColor), $sibling); + self::assertGreaterThanOrEqual($minRatio, $pair->ratio); + + // Sibling is generated with the minimum specified ratio + $desiredMinRatio = 4.5; + $sibling = HexColorPair::minContrast($desiredMinRatio)->getSibling($hexColor); + $pair = HexColorPair::make(HexColor::make($hexColor), $sibling); + self::assertGreaterThanOrEqual($desiredMinRatio, $pair->ratio); + } + + public function provideContrastData() + { + yield 'maximum contrast' => [ + 21, + ['000', 'fff',], + ]; + yield 'no contrast - black on black' => [ + 1, + ['000', '000',], + ]; + yield 'no contrast - white on white' => [ + 1, + ['fff', 'fff',], + ]; + yield 'high contrast' => [ + 18.4, + ['300', 'fff',], + ]; + yield 'low contrast' => [ + 1.1, + ['300', '000',], + ]; + } +} diff --git a/tests/HexColorTest.php b/tests/HexColorTest.php new file mode 100644 index 0000000..ca97d5a --- /dev/null +++ b/tests/HexColorTest.php @@ -0,0 +1,44 @@ +name); + self::assertEquals('#aabbcc', $hexColor->hex); + + $hexColor = HexColor::make('#abc'); + self::assertNull($hexColor->name); + self::assertEquals('#aabbcc', $hexColor->hex); + + $hexColor = HexColor::make('abcdef'); + self::assertNull($hexColor->name); + self::assertEquals('#abcdef', $hexColor->hex); + + $hexColor = HexColor::make('#abcdef'); + self::assertNull($hexColor->name); + self::assertEquals('#abcdef', $hexColor->hex); + + $name = 'custom-color-name'; + $hexColor = HexColor::make('#abcdef', $name); + self::assertEquals($name, $hexColor->name); + self::assertEquals('#abcdef', $hexColor->hex); + } + + /** @test */ + public function a_random_hex_color_is_generated_correctly() + { + $randomHexColor = HexColor::random(); + + self::assertRegExp('/^#[0-9a-f]{6}$/i', $randomHexColor->hex); + self::assertNull($randomHexColor->name); + } +} diff --git a/tests/TailwindTest.php b/tests/TailwindTest.php new file mode 100644 index 0000000..7791d6b --- /dev/null +++ b/tests/TailwindTest.php @@ -0,0 +1,64 @@ +fg, HexColor::class)); + self::assertTrue(is_a($randomPair->bg, HexColor::class)); + } + + /** @test */ + public function it_returns_random_color_pairs_of_minimum_3_to_1_ratio() + { + $minRatio = 3; + + $randomPair = TailwindColor::randomPair(); + self::assertGreaterThanOrEqual($minRatio, $randomPair->ratio); + + // Color pairs with a desired min ratio < 3 will default to 3 + $desiredMinRatio = 2; + $randomPair = TailwindColor::minContrast($desiredMinRatio)->getRandomPair(); + self::assertGreaterThanOrEqual($minRatio, $randomPair->ratio); + + // Color pairs are generated with the minimum specified ratio + $desiredMinRatio = 4.5; + $randomPair = TailwindColor::minContrast($desiredMinRatio)->getRandomPair(); + self::assertGreaterThanOrEqual($minRatio, $randomPair->ratio); + } + + /** @test */ + public function it_can_merge_default_and_custom_colors() + { + $customPalette = json_decode(file_get_contents(__DIR__ . '/../stubs/custom-palette.json'), true); + + $colors = TailwindColor::merge($customPalette)->getColors(); + $customPalette = collect($customPalette) + ->map(function ($hex, $name) { + return HexColor::make($hex, $name); + }) + ->flatten() + ->toArray(); + + $randomCustomPaletteElement = $customPalette[rand(0, count($customPalette) - 1)]; + + self::assertContains($randomCustomPaletteElement, $colors, false, true, false); + } +}