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/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..385326c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +All notable changes to `pet-avatar` 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..2886c83 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Nefy + +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..6aaa9ce --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +![Banner](art/banner.png) + +# Pet Avatar + +Generate a random cute avatar using the GD Library + +## Preview + +![Preview](art/preview.png) + +## Requirements + +- PHP 7.4 or later +- PHP GD extension must be enabled + +## Installation + +You can install the package via composer: + +```bash +composer require nefydev/pet-avatar +``` +To setup the config file: + +```bash +php artisan vendor:publish --provider="Nefydev\PetAvatar\PetAvatarServiceProvider" +``` + +## Usage + +```php + use Nefydev\PetAvatar\Facades\PetAvatar; + ... + + // generate a cute random avatar + $avatar = PetAvatar::generate(); + + // shows the file name + echo $avatar; +``` + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) for details. + +## Credits + +- [Nefy](https://github.com/nefydev) +- [All Contributors](https://github.com/nefydev/pet-avatar/contributors) + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. + +## Support + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/E1E5BQUXC) \ No newline at end of file diff --git a/art/banner.png b/art/banner.png new file mode 100644 index 0000000..fafd567 Binary files /dev/null and b/art/banner.png differ diff --git a/art/preview.png b/art/preview.png new file mode 100644 index 0000000..a492f93 Binary files /dev/null and b/art/preview.png differ diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7aecd96 --- /dev/null +++ b/composer.json @@ -0,0 +1,41 @@ +{ + "name": "nefydev/pet-avatar", + "description": "Cute avatar generator.", + "keywords": [ + "laravel", + "laravel-package", + "avatar", + "pet-avatar" + ], + "homepage": "https://github.com/nefydev/pet-avatar", + "license": "MIT", + "authors": [ + { + "name": "Nefy", + "email": "info@nefy.dev", + "role": "Developer" + } + ], + "require": { + "php": "^7.4|^8.0", + "illuminate/support": "^8.0" + }, + "autoload": { + "psr-4": { + "Nefydev\\PetAvatar\\": "src" + } + }, + "config": { + "sort-packages": true + }, + "extra": { + "laravel": { + "providers": [ + "Nefydev\\PetAvatar\\PetAvatarServiceProvider" + ], + "aliases": { + "PetAvatar": "Nefydev\\PetAvatar\\Facades\\PetAvatar" + } + } + } +} diff --git a/config/cat.php b/config/cat.php new file mode 100644 index 0000000..cd8c14d --- /dev/null +++ b/config/cat.php @@ -0,0 +1,117 @@ + [ + 'head' => [ + 'colors' => [ + 'format' => 'rgb', + 'luminosity' => 'light', + 'hue' => ['monochrome', 'orange', 'yellow'] + ], + 'variations' => [ + [ + [11, 1, 2, 1], + [10, 2, 3, 1], + [4, 1, 2, 1], + [4, 2, 3, 1], + [4, 8, 4, 1], + [3, 10, 5, 2], + [2, 12, 7, 3] + ] + ] + ], + 'spot' => [ + 'colors' => [ + 'format' => 'rgb', + 'luminosity' => 'light', + 'hue' => 'monochrome' + ], + 'variations' => [ + [ + [3, 10, 10, 1], + [4, 8, 11, 1], + [5, 6, 12, 1], + [5, 6, 9, 1] + ], + [ + [3, 10, 10, 1], + [4, 8, 11, 1], + [5, 6, 12, 1], + [5, 6, 9, 1], + [3, 1, 5, 2], + [4, 1, 2, 4], + [5, 1, 3, 3], + [6, 1, 4, 1] + ], + [ + [3, 10, 10, 1], + [4, 8, 11, 1], + [5, 6, 12, 1], + [5, 6, 9, 1], + [12, 1, 5, 2], + [11, 1, 2, 4], + [10, 1, 3, 3], + [9, 1, 4, 1] + ], + ] + ], + 'ears' => [ + 'colors' => [ + 'format' => 'rgb', + 'luminosity' => 'light', + 'hue' => ['monochrome', 'pink'] + ], + 'variations' => [[ + [4, 2, 4, 2], + [11, 2, 4, 2] + ]] + ], + 'collar' => [ + 'colors' => [ + 'format' => 'rgb', + 'hue' => ['random'] + ], + 'variations' => [[ + [5, 6, 13, 1] + ]] + ], + 'eyes' => [ + 'colors' => [ + 'format' => 'rgb', + 'luminosity' => 'dark', + 'hue' => 'monochrome' + ], + 'variations' => [ + [ + [6, 1, 8, 2], + [6, 2, 9, 1], + [10, 1, 8, 2], + [10, 2, 9, 1] + ], + [ + [5, 3, 9, 1], + [10, 3, 9, 1] + ], + ] + ], + 'nose' => [ + 'colors' => [ + 'format' => 'rgb', + 'luminosity' => 'light', + 'hue' => 'pink' + ], + 'variations' => [ + [ + [8, 2, 10, 2], + [7, 2, 11, 1], + [9, 2, 11, 1] + ], + [ + [9, 2, 10, 2], + [8, 2, 11, 1] + ] + ] + ] + ], + 'small_parts' => ['eyes', 'nose', 'ears'] +]; diff --git a/config/config.php b/config/config.php new file mode 100644 index 0000000..3a539b2 --- /dev/null +++ b/config/config.php @@ -0,0 +1,40 @@ + 'cat', + + /* + |-------------------------------------------------------------------------- + | File type + |-------------------------------------------------------------------------- + | + | The image file type. + | Possible values: jpeg, png, gif + | Default: png + | + */ + 'file' => 'png', + + /* + |-------------------------------------------------------------------------- + | Path + |-------------------------------------------------------------------------- + | + | The path where the files will be stored. relative to public. + | Default: public/avatars + | + */ + 'path' => 'avatars', + +]; diff --git a/config/dog.php b/config/dog.php new file mode 100644 index 0000000..c6a4ce7 --- /dev/null +++ b/config/dog.php @@ -0,0 +1,116 @@ + [ + 'head' => [ + 'colors' => [ + 'format' => 'rgb', + 'luminosity' => 'light', + 'hue' => ['monochrome', 'orange'] + ], + 'variations' => [[ + [5, 6, 2, 1], + [4, 8, 3, 7] + ]] + ], + 'spot' => [ + 'colors' => [ + 'format' => 'rgb', + 'luminosity' => 'light', + 'hue' => 'monochrome' + ], + 'variations' => [ + [ + [7, 2, 8, 1], + [6, 4, 9, 1], + [4, 8, 10, 3] + ], + [ + [7, 2, 8, 1], + [6, 4, 9, 1], + [4, 8, 10, 3], + [4, 1, 3, 2], + [5, 2, 2, 2], + [7, 1, 2, 1] + ], + [ + [7, 2, 8, 1], + [6, 4, 9, 1], + [4, 8, 10, 3], + [11, 1, 3, 2], + [9, 2, 2, 2], + [8, 1, 2, 1] + + ], + ] + ], + 'ears' => [ + 'colors' => [ + 'format' => 'rgb', + 'luminosity' => 'dark', + 'hue' => ['orange', 'monochrome'] + ], + 'variations' => [[ + [3, 1, 4, 9], + [2, 1, 5, 7], + [12, 1, 4, 9], + [13, 1, 5, 7] + ]] + ], + 'collar' => [ + 'colors' => [ + 'format' => 'rgb', + 'hue' => ['random'] + ], + 'variations' => [[ + [5, 6, 13, 1] + ]] + ], + 'eyes' => [ + 'colors' => [ + 'format' => 'rgb', + 'luminosity' => 'dark', + 'hue' => 'monochrome' + ], + 'variations' => [ + [ + [5, 1, 7, 3], + [5, 2, 8, 1], + [10, 1, 7, 3], + [10, 2, 8, 1] + ], + [ + [5, 3, 8, 1], + [10, 3, 8, 1] + ], + ] + ], + 'nose' => [ + 'colors' => [ + 'format' => 'rgb', + 'luminosity' => 'dark', + 'hue' => 'monochrome' + ], + 'variations' => [ + [[7, 2, 10, 1]] + ] + ], + 'mouth' => [ + 'colors' => [ + 'format' => 'rgb', + 'luminosity' => 'dark', + 'hue' => 'red' + ], + 'variations' => [ + [ + [6, 2, 11, 2] + ], + [ + [9, 2, 11, 2] + ] + + ] + ] + ], + 'small_parts' => ['eyes', 'mouth'] +]; diff --git a/src/Facades/PetAvatar.php b/src/Facades/PetAvatar.php new file mode 100644 index 0000000..0a31880 --- /dev/null +++ b/src/Facades/PetAvatar.php @@ -0,0 +1,18 @@ +config = $this->validateConfig(config('pet-avatar')); + } + + public function generate() + { + $pet = $this->config['pet']; + + $parts = []; + foreach (array_keys(config("{$pet}.parts")) as $part) { + $pos = array_rand(config("{$pet}.parts.{$part}.variations")); + $parts[$part] = config("{$pet}.parts.{$part}.variations.{$pos}"); + } + + $image = imagecreate(320, 320); + $background = RandomColor::one(array('format' => 'rgb')); + imagefilltoborder($image, 0, 0, imagecolorallocate($image, $background['r'], $background['g'], $background['b']), imagecolorallocate($image, $background['r'], $background['g'], $background['b'])); + + $unit1 = 20; + $unit2 = 10; + + foreach ($parts as $name => $part) { + $color = RandomColor::one(config("{$pet}.parts.{$name}.colors")); + foreach ($part as $col) { + $unit = in_array($name, config("{$pet}.small_parts")) ? $unit2 : $unit1; + $x1 = $col[0] * $unit1; + $x2 = $x1 + ($col[1] * $unit); + $y1 = $col[2] * $unit1; + $y2 = $y1 + ($col[3] * $unit); + imagefilledrectangle($image, $x1, $y1, $x2, $y2, imagecolorallocate($image, $color['r'], $color['g'], $color['b'])); + } + } + + $name = uniqid() . '.' . $this->config['file']; + $path = $this->config['path'] . '/' . $name; + + $this->config['function']($image, $path); + imagedestroy($image); + + return $name; + } + + public function validateConfig(array $config) + { + $config['pet'] = in_array($config['pet'], ['cat', 'dog']) ? $config['pet'] : 'cat'; + $config['file'] = in_array($config['file'], ['jpeg', 'png', 'gif']) ? $config['file'] : 'png'; + $config['path'] = $config['path'] == '' ? public_path('avatars') : public_path($config['path']); + $config['function'] = "image{$config['file']}"; + + if (!is_dir($config['path']) && !mkdir($config['path'], 0777, true)) { + throw new Exception( + "PetAvatar - Export folder could not be instantiated ({$config['path']})" + ); + } + + return $config; + } +} diff --git a/src/PetAvatarServiceProvider.php b/src/PetAvatarServiceProvider.php new file mode 100644 index 0000000..19f2612 --- /dev/null +++ b/src/PetAvatarServiceProvider.php @@ -0,0 +1,36 @@ +app->runningInConsole()) { + $this->publishes([ + __DIR__.'/../config/config.php' => config_path('pet-avatar.php'), + ], 'config'); + } + } + + /** + * Register the application services. + */ + public function register() + { + // Automatically apply the package configuration + $this->mergeConfigFrom(__DIR__.'/../config/config.php', 'pet-avatar'); + $this->mergeConfigFrom(__DIR__.'/../config/cat.php', 'cat'); + $this->mergeConfigFrom(__DIR__.'/../config/dog.php', 'dog'); + + // Register the main class to use with the facade + $this->app->singleton('pet-avatar', function () { + return new PetAvatar; + }); + } +} diff --git a/src/Utilities/RandomColor.php b/src/Utilities/RandomColor.php new file mode 100644 index 0000000..e582d58 --- /dev/null +++ b/src/Utilities/RandomColor.php @@ -0,0 +1,360 @@ += 0) { + $ranges[] = array($hue, $hue); + } + } + } + } + + if (($l = count($ranges)) === 0) { + return array(0, 360); + } else if ($l === 1) { + return $ranges[0]; + } else { + return $ranges[self::_rand(array(0, $l - 1), $options)]; + } + } + + static private function _getMinimumBrightness($h, $s) + { + $colorInfo = self::_getColorInfo($h); + $bounds = $colorInfo['bounds']; + + for ($i = 0, $l = count($bounds); $i < $l - 1; $i++) { + $s1 = $bounds[$i][0]; + $v1 = $bounds[$i][1]; + $s2 = $bounds[$i + 1][0]; + $v2 = $bounds[$i + 1][1]; + + if ($s >= $s1 && $s <= $s2) { + $m = ($v2 - $v1) / ($s2 - $s1); + $b = $v1 - $m * $s1; + return round($m * $s + $b); + } + } + + return 0; + } + + static private function _getColorInfo($h) + { + // Maps red colors to make picking hue easier + if ($h >= 334 && $h <= 360) { + $h -= 360; + } + + foreach (self::$dictionary as $color) { + if ($color['h'] !== null && $h >= $color['h'][0] && $h <= $color['h'][1]) { + return $color; + } + } + } + + static private function _rand($bounds, $options) + { + if (isset($options['prng'])) { + return $options['prng']($bounds[0], $bounds[1]); + } else { + return mt_rand($bounds[0], $bounds[1]); + } + } + + static public function hsv2hex($hsv) + { + $rgb = self::hsv2rgb($hsv); + $hex = '#'; + + foreach ($rgb as $c) { + $hex .= str_pad(dechex($c), 2, '0', STR_PAD_LEFT); + } + + return $hex; + } + + static public function hsv2hsl($hsv) + { + extract($hsv); + + $s /= 100; + $v /= 100; + $k = (2 - $s) * $v; + + return array( + 'h' => $h, + 's' => round($s * $v / ($k < 1 ? $k : 2 - $k), 4) * 100, + 'l' => $k / 2 * 100, + ); + } + + static public function hsv2rgb($hsv) + { + extract($hsv); + + $h /= 360; + $s /= 100; + $v /= 100; + + $i = floor($h * 6); + $f = $h * 6 - $i; + + $m = $v * (1 - $s); + $n = $v * (1 - $s * $f); + $k = $v * (1 - $s * (1 - $f)); + + $r = 1; + $g = 1; + $b = 1; + + switch ($i) { + case 0: + list($r, $g, $b) = array($v, $k, $m); + break; + case 1: + list($r, $g, $b) = array($n, $v, $m); + break; + case 2: + list($r, $g, $b) = array($m, $v, $k); + break; + case 3: + list($r, $g, $b) = array($m, $n, $v); + break; + case 4: + list($r, $g, $b) = array($k, $m, $v); + break; + case 5: + case 6: + list($r, $g, $b) = array($v, $m, $n); + break; + } + + return array( + 'r' => floor($r * 255), + 'g' => floor($g * 255), + 'b' => floor($b * 255), + ); + } +} + +/* + * h=hueRange + * s=saturationRange : bounds[0][0] ; bounds[-][0] + */ +RandomColor::$dictionary = array( + 'monochrome' => array( + 'bounds' => array(array(0, 0), array(100, 0)), + 'h' => NULL, + 's' => array(0, 100) + ), + 'red' => array( + 'bounds' => array(array(20, 100), array(30, 92), array(40, 89), array(50, 85), array(60, 78), array(70, 70), array(80, 60), array(90, 55), array(100, 50)), + 'h' => array(-26, 18), + 's' => array(20, 100) + ), + 'orange' => array( + 'bounds' => array(array(20, 100), array(30, 93), array(40, 88), array(50, 86), array(60, 85), array(70, 70), array(100, 70)), + 'h' => array(19, 46), + 's' => array(20, 100) + ), + 'yellow' => array( + 'bounds' => array(array(25, 100), array(40, 94), array(50, 89), array(60, 86), array(70, 84), array(80, 82), array(90, 80), array(100, 75)), + 'h' => array(47, 62), + 's' => array(25, 100) + ), + 'green' => array( + 'bounds' => array(array(30, 100), array(40, 90), array(50, 85), array(60, 81), array(70, 74), array(80, 64), array(90, 50), array(100, 40)), + 'h' => array(63, 178), + 's' => array(30, 100) + ), + 'blue' => array( + 'bounds' => array(array(20, 100), array(30, 86), array(40, 80), array(50, 74), array(60, 60), array(70, 52), array(80, 44), array(90, 39), array(100, 35)), + 'h' => array(179, 257), + 's' => array(20, 100) + ), + 'purple' => array( + 'bounds' => array(array(20, 100), array(30, 87), array(40, 79), array(50, 70), array(60, 65), array(70, 59), array(80, 52), array(90, 45), array(100, 42)), + 'h' => array(258, 282), + 's' => array(20, 100) + ), + 'pink' => array( + 'bounds' => array(array(20, 100), array(30, 90), array(40, 86), array(60, 84), array(80, 80), array(90, 75), array(100, 73)), + 'h' => array(283, 334), + 's' => array(20, 100) + ) +);