From d0c5212449b4fd4d6cc7802b92e25a364299b039 Mon Sep 17 00:00:00 2001 From: mster Date: Tue, 13 Apr 2021 23:35:39 -0700 Subject: [PATCH] v0.1.0 Adds effects builders and QOL changes to utils --- README.md | 88 ++++++++++++++++++++++++++++- index.js | 3 +- lib/effects.js | 150 +++++++++++++++++++++++++++++++++++++++++++++++++ lib/utils.js | 89 ++++++++++++++++++----------- package.json | 9 ++- 5 files changed, 302 insertions(+), 37 deletions(-) create mode 100644 lib/effects.js diff --git a/README.md b/README.md index 3fdb0e2..6ebd056 100644 --- a/README.md +++ b/README.md @@ -44,13 +44,99 @@ scheme = scheme.map(e => HSV2RGB(e)) ] */ -schemes = schemes.map(e => HSV2Hex(e)) +scheme = scheme.map(e => RGB2Hex(e)) /* [ 'FF6745', 'FFC445', 'DDFF45', '80FF45' ] */ ``` +# Effects +Prismaek includes an effect builder for generating **tints**, **tones**, and **shades**. Effects have one required argument and three optional arguments. + +
+ +### shade(base[, format][, step][, count ]) +* #### **base** \ | \ | \ Base color object (HSV/RGB), hex value as a string, or an array containing any combination of either. +* #### **format** \ Output format. Optional, **Default:** `hex` + * Supported types: `hex`, `rgb`, `hsv` +* #### **step** \ | \ Step size. Optional, **Default:** `0.10` +* #### **count** \ | \ Iteration count (base color included). Optional, **Default:** `5` + +
+ +```js + const { shade, tint, tone } = require('prismaek').effects; + + const color = { r: 102, g: 55, b: 69 }; + const shades = shade(color, "hex", 0.05, 5); + // [ '663745', '613442', '5C323E', '572F3B', '522C37' ] +``` + +Works with schemes too. Pretty cool, huh? + +```js +const color = "#289866"; +const tetradicScheme = tetradic(hex2HSV(color)); +/* [{ h: 153, s: 0.737, v: 0.596 }, + { h: 243, s: 0.737, v: 0.596 }, + { h: 333, s: 0.737, v: 0.596 }, + { h: 63, s: 0.737, v: 0.596 }] */ + +const tetradicTones = tone(tetradicScheme, "rgb", 0.1, 3); +/* { + '0': [ + { r: 60, g: 228, b: 153, shift: 0 }, + { r: 69, g: 60, b: 228, shift: 0 }, + { r: 228, g: 60, b: 135, shift: 0 }, + { r: 219, g: 228, b: 60, shift: 0 } + ], + '0.1': [ + { r: 56, g: 213, b: 143, shift: 0.1 }, + { r: 64, g: 56, b: 213, shift: 0.1 }, + { r: 213, g: 56, b: 126, shift: 0.1 }, + { r: 204, g: 213, b: 56, shift: 0.1 } + ], + '0.2': [ + { r: 52, g: 198, b: 133, shift: 0.2 }, + { r: 60, g: 52, b: 198, shift: 0.2 }, + { r: 198, g: 52, b: 117, shift: 0.2 }, + { r: 190, g: 198, b: 52, shift: 0.2 } + ] +} */ +``` + +Finally, something a bit funky. + +```js +let woah = RGBs.map(rgb => tone(triadic(RGB2HSV(rgb)))); +/*[ + { + '0': [ '950269', '699502', '026995' ], + '0.1': [ '8B0162', '628B01', '01628B' ], + '0.2': [ '81015B', '5B8101', '015B81' ], + '0.3': [ '770154', '547701', '015477' ], + '0.4': [ '6D014D', '4D6D01', '014D6D' ] + }, + { + '0': [ 'B972C2', 'C2B972', '72C2B9' ], + '0.1': [ 'AC6AB5', 'B5AC6A', '6AB5AC' ], + '0.2': [ 'A063A8', 'A8A063', '63A8A0' ], + '0.3': [ '945B9B', '9B945B', '5B9B94' ], + '0.4': [ '87548E', '8E8754', '548E87' ] + }, + { + '0': [ '995368', '689953', '536899' ], + '0.1': [ '8F4D61', '618F4D', '4D618F' ], + '0.2': [ '85485A', '5A8548', '485A85' ], + '0.3': [ '7A4253', '537A42', '42537A' ], + '0.4': [ '703D4C', '4C703D', '3D4C70' ] + } +] */ +``` + +# Contributing +Thanks for checking out my package! Contribution guidelines will be coming soon. diff --git a/index.js b/index.js index 49b0375..31589e8 100644 --- a/index.js +++ b/index.js @@ -2,5 +2,6 @@ module.exports = { utils: require('./lib/utils'), - harmonies: require('./lib/harmonies') + harmonies: require('./lib/harmonies'), + effects: require('./lib/effects') } \ No newline at end of file diff --git a/lib/effects.js b/lib/effects.js new file mode 100644 index 0000000..b4d05e7 --- /dev/null +++ b/lib/effects.js @@ -0,0 +1,150 @@ +'use strict' + +const { + isHex, + isRGB, + isHSV, + hex2RGB, + HSV2RGB, + RGB2Hex, + RGB2HSV +} = require('./utils'); + +module.exports = { + shade, + tint, + tone +}; + +function shade (base, format, step, count) { + if (!base) throw new Error("Error: arg `base` is required!") + + if (!format) format = "hex"; + if (!step) step = 0.10; + if (!count) count = 5; + + return effectsBuilder(base, format, step, count, (rgb, shift) => { + return { + r: Math.min(Math.round((+rgb.r) * (1 - shift)), 255), + g: Math.min(Math.round((+rgb.g) * (1 - shift)), 255), + b: Math.min(Math.round((+rgb.b) * (1 - shift)), 255), + shift + } + }); +} + +function tint (base, format, step, count) { + if (!base) throw new Error("Error: arg `base` is required!") + + if (!format) format = "hex"; + if (!step) step = 0.10; + if (!count) count = 5; + + return effectsBuilder(base, format, step, count, (rgb, shift) => { + return { + r: Math.min(Math.round((255 - (+rgb.r)) * +shift), 255), + g: Math.min(Math.round((255 - (+rgb.g)) * +shift), 255), + b: Math.min(Math.round((255 - (+rgb.b)) * +shift), 255), + shift + } + }); +} + +function tone (base, format, step, count) { + if (!base) throw new Error("Error: arg `base` is required!") + + if (!format) format = "hex"; + if (!step) step = 0.10; + if (!count) count = 5; + + return effectsBuilder(base, format, step, count, (rgb, shift) => { + return { + r: Math.min(Math.round((+rgb.r) + (+rgb.r) * (0.5 - shift)), 255), + g: Math.min(Math.round((+rgb.g) + (+rgb.g) * (0.5 - shift)), 255), + b: Math.min(Math.round((+rgb.b) + (+rgb.b) * (0.5 - shift)), 255), + shift + } + }); +} + + +function effectsBuilder (base, format="hex", step=0.05, count=5, applyEffect) { + if (isNaN(step) || isNaN(count)) { + throw new Error(`Arguments step and count must be of type number (or coercible). step=${step} count=${count}`) + } + + if (step < 0.01) { + throw new Error(`Shade step percision must be >= 0.01 (1%). step=${step}`); + } + + const formatMap = { + "hex": RGB2Hex, + "hsv": RGB2HSV, + "rgb": ((e) => e) + } + + const constructor = base?.constructor?.name + + if (constructor === "Array") { + const _results = {}; + + let i = 0; + for(; i < count; i++) { + const shift = Math.round(i * step * 100) / 100; + + _results[String(shift)] = base.map((color, index) => { + let hex, hsv; + if ( + (color?.constructor?.name === "Object" || color?.constructor?.name === "String") && + (isRGB(color) || (hex = isHex(color)) || (hsv = isHSV(color))) + ) { + if (hex) color = hex2RGB(color); + if (hsv) color = HSV2RGB(color); + return formatMap[format](applyEffect(color, shift)); + } + + throw new Error(`Invalid color format; unable to generate shades. base, index=${index} color=${color}`) + }); + } + + return _results; + } + + // valid for hsv or rgb + let hsv; + if (constructor === "Object" && (isRGB(base) || (hsv = isHSV(base)))) { + if (hsv) base = HSV2RGB(base); + + const _results = []; + + let i = 0; + for(; i < count; i++) { + const shift = Math.round(i * step * 100) / 100; + _results.push(formatMap[format](applyEffect(base, shift))); + } + + return _results; + } + + // only valid for singular hex color + if (base.constructor.name === "String") { + if (base.includes('#')) base = base.replace('#', ''); + + if (isHex(base)) { + const rgb = hex2RGB(base); + const _results = []; + + let i = 0; + for(; i < count; i++) { + const shift = Math.round(i * step * 100) / 100; + _results.push(formatMap[format](applyEffect(rgb, shift))); + } + + return _results; + } + + throw new Error(`Invalid color format; unable to generate shades. base=${base}`) + } + + throw new Error(`Shade generator requires a valid base color, or array of colors. \nbase.constructor.name=${base?.constructor?.name}`) +} diff --git a/lib/utils.js b/lib/utils.js index 3c2dc74..cb409c9 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -2,12 +2,14 @@ module.exports = { hex2RGB, + hex2HSV, RGB2Hex, RGB2HSV, HSV2RGB, - hex2HSV, HSV2Hex, isHex, + isRGB, + isHSV, shiftDegrees } @@ -16,6 +18,8 @@ function hex2RGB(hex) { throw new Error(`Argument is not a String: ${hex.constructor.name }`) } + if (hex.includes('#')) hex = hex.replace('#', ''); + if (hex.length !== 6) { throw new Error(`Argument is malformed String: ${hex}`) } @@ -24,7 +28,7 @@ function hex2RGB(hex) { const g = parseInt((hex[2] + hex[3]), 16) const b = parseInt((hex[4] + hex[5]), 16) - return {r,g,b} + return { r, g, b } } function RGB2Hex(rgb) { @@ -61,27 +65,23 @@ function RGB2HSV(rgb) { throw new Error(`Argument is not an Object: ${rgb.constructor.name}`) } - if ( - isNaN(+(rgb?.r)) || - isNaN(+(rgb?.g)) || - isNaN(+(rgb?.b)) - ) { + const { r, g, b, ...rest } = rgb; + + if (isNaN(+r) || isNaN(+g) || isNaN(+b)) { throw new Error(`Argument properties are malformed: ${Object.values(rgb)}`) } if ( - 0 > (+(rgb?.r)) || (+(rgb?.r)) > 255 || - 0 > (+(rgb?.g)) || (+(rgb?.g)) > 255 || - 0 > (+(rgb?.b)) || (+(rgb?.b)) > 255 + 0 > (+r) || (+r) > 255 || + 0 > (+g) || (+g) > 255 || + 0 > (+b) || (+b) > 255 ) { throw new Error(`RGB values must be between 0-255: ${Object.values(rgb)}`) } - // formulas from https://www.rapidtables.com/convert/color/rgb-to-hsv.html - - const rPrime = (+(rgb.r))/255 - const gPrime = (+(rgb.g))/255 - const bPrime = (+(rgb.b))/255 + const rPrime = (r)/255 + const gPrime = (g)/255 + const bPrime = (b)/255 const cMax = Math.max(rPrime, gPrime, bPrime) const cMin = Math.min(rPrime, gPrime, bPrime) @@ -107,7 +107,7 @@ function RGB2HSV(rgb) { const s = cMax === 0 ? 0 : Math.round((delta/cMax) * 1000) / 1000 const v = Math.round(cMax * 1000) / 1000 - return {h,s,v} + return { h, s, v, ...rest } } function HSV2RGB(hsv) { @@ -115,28 +115,23 @@ function HSV2RGB(hsv) { throw new Error(`Argument is not an Object: ${hsv.constructor.name}`) } - if ( - isNaN(+(hsv?.h)) || - isNaN(+(hsv?.s)) || - isNaN(+(hsv?.v)) - ) { + const { h, s, v, ...rest } = hsv; + + if (isNaN(+h) || isNaN(+s) || isNaN(+v)) { throw new Error(`Argument properties are malformed: ${Object.values(hsv)}`) } if ( - 0 > (+(hsv?.h)) || (+(hsv?.h)) > 360 || - 0 > (+(hsv?.s)) || (+(hsv?.s)) > 1 || - 0 > (+(hsv?.v)) || (+(hsv?.v)) > 1 + 0 > (+h) || (+h) > 360 || + 0 > (+s) || (+s) > 1 || + 0 > (+v) || (+v) > 1 ) { throw new Error(`Hue (h) value must be between 0-360;\nSaturation (s) & Value (v) values must be between 0-1: ${Object.values(hsv)}`) } - // formulas from https://www.rapidtables.com/convert/color/hsv-to-rgb.html - - const h = (+(hsv.h)) - const c = (+(hsv.v)) * (+(hsv.s)) - const x = c * (1 - Math.abs((h/60) % 2 - 1)) - const m = (+(hsv.v)) - c + const c = (+v) * (+s) + const x = c * (1 - Math.abs((+h/60) % 2 - 1)) + const m = (+v) - c let rPrime = 0, gPrime = 0, bPrime = 0 @@ -177,7 +172,7 @@ function HSV2RGB(hsv) { const g = Math.round((gPrime + m) * 255) const b = Math.round((bPrime + m) * 255) - return {r,g,b} + return {r, g, b, ...rest } } @@ -194,10 +189,40 @@ function HSV2Hex(hsv) { } function isHex(hexString) { + if (hexString?.constructor?.name !== "String") return false; + if (hexString.includes('#')) hexString = hexString.replace('#', ''); return /[\da-f]{6}/i.test(hexString) && hexString.length === 6 } +function isRGB(rgbObj) { + if ( + rgbObj?.constructor?.name === "Object" && + !isNaN(rgbObj?.r) && !isNaN(rgbObj?.g) && !isNaN(rgbObj?.b) && + (+rgbObj?.r) < 256 && (+rgbObj?.r) >= 0 && + (+rgbObj?.g) < 256 && (+rgbObj?.g) >= 0 && + (+rgbObj?.b) < 256 && (+rgbObj?.b) >= 0 + ) { + return true; + } + + return false; +} + +function isHSV(hsvObj) { + if ( + hsvObj?.constructor?.name === "Object" && + !isNaN(hsvObj?.h) && !isNaN(hsvObj?.s) && !isNaN(hsvObj?.v) && + (+hsvObj?.h) < 360 && (+hsvObj?.h) >= 0 && + (+hsvObj?.s) <= 1 && (+hsvObj?.s) >= 0 && + (+hsvObj?.v) <= 1 && (+hsvObj?.v) >= 0 + ) { + return true; + } + + return false; +} + function shiftDegrees(degree, shift) { if (degree + shift < 360) return degree + shift - return Math.abs((degree + shift) - 360) + return (degree + shift - 360) } \ No newline at end of file diff --git a/package.json b/package.json index d43914c..9838860 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,14 @@ { "name": "prismaek", - "version": "0.0.0", - "description": "Generate custom color schemes using maths.", + "version": "0.1.0", + "description": "Generate colors and color schemes using maths.", "main": "index.js", "scripts": { "test": "exit 0" }, "author": "Michael Sterpka ", - "license": "MIT" + "license": "MIT", + "keywords": [ + "color", "scheme", "tint", "tone", "shade", "harmony", "complementary", "generate" + ] }