-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor
tailwind-lerp-colors
as a function rather than a plugin
- Loading branch information
1 parent
911800c
commit d750270
Showing
3 changed files
with
156 additions
and
131 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,120 +1,117 @@ | ||
const plugin = require('tailwindcss/plugin'); | ||
const interpolateColors = (colorsObj, options = {}) => { | ||
const chroma = require('chroma-js'); | ||
const builtInColors = require('tailwindcss/colors'); | ||
const legacyNames = ['lightBlue', 'warmGray', 'trueGray', 'coolGray', 'blueGray']; | ||
|
||
const interpolateColors = plugin.withOptions( | ||
() => {}, (options = {}) => { | ||
function generateLerpedColors({ theme }) { | ||
const chroma = require('chroma-js'); | ||
const baseColors = require('tailwindcss/colors'); | ||
|
||
const defaultOptions = { | ||
includeBaseColors: true, | ||
includeEnds: true, | ||
interval: 25, | ||
mode: 'rgb', | ||
}; | ||
|
||
const validColorModes = [ | ||
'rgb', 'lab', 'lch', 'lrgb', | ||
'hcl', 'num', 'hcg', 'oklch', | ||
'hsi', 'hsl', 'hsv', 'oklab', | ||
]; | ||
|
||
const sortByNumericFirstIndex = ([numericKeyA], [numericKeyB]) => { | ||
return numericKeyA - numericKeyB; | ||
}; | ||
|
||
const isOptionInvalid = (optionName, test) => { | ||
return options.hasOwnProperty(optionName) && !test(options[optionName]); | ||
} | ||
const defaultOptions = { | ||
includeBase: true, | ||
includeLegacy: false, | ||
lerpEnds: true, | ||
interval: 25, | ||
mode: 'rgb', | ||
}; | ||
|
||
if (isOptionInvalid('includeBaseColors', v => typeof v === 'boolean')) | ||
throw new Error('tailwind-lerp-colors option `includeBaseColors` must be a boolean.'); | ||
if (isOptionInvalid('includeEnds', v => typeof v === 'boolean')) | ||
throw new Error('tailwind-lerp-colors option `includeEnds` must be a boolean.'); | ||
if (isOptionInvalid('interval', v => Number.isInteger(v) && v > 0)) | ||
throw new Error('tailwind-lerp-colors option `interval` must be a positive integer greater than 0.'); | ||
if (isOptionInvalid('mode', v => validColorModes.includes(v))) | ||
throw new Error(`tailwind-lerp-colors option \`mode\` must be one of the following values: ${validColorModes.map(modeName => '`modeName`').join(', ')}.`); | ||
const validColorModes = [ | ||
'rgb', 'lab', 'lch', 'lrgb', | ||
'hcl', 'num', 'hcg', 'oklch', | ||
'hsi', 'hsl', 'hsv', 'oklab', | ||
]; | ||
|
||
const { includeBaseColors, includeEnds, interval, mode } = { | ||
...defaultOptions, | ||
...options, | ||
}; | ||
const initialColors = Object.entries({ | ||
...(includeBaseColors ? baseColors : {}), | ||
...theme.colors, | ||
...theme.extend.colors, | ||
}); | ||
|
||
const finalColors = {}; | ||
const sortByNumericFirstIndex = ([numericKeyA], [numericKeyB]) => { | ||
return numericKeyA - numericKeyB; | ||
}; | ||
|
||
for (const [name, shades] of initialColors) { | ||
if ( | ||
['null', 'undefined'].includes(typeof shades) || | ||
!shades.toString | ||
) { | ||
continue; | ||
} | ||
finalColors[name] = shades; | ||
if ( | ||
typeof shades === 'string' || | ||
Array.isArray(shades) || | ||
shades.toString() !== '[object Object]' || | ||
!Object.keys(shades).every(key => { | ||
return !isNaN(key); | ||
}) | ||
) { | ||
continue; | ||
} | ||
const shadesArray = ( | ||
Object.entries(shades) | ||
.map(([numericStringKey, color]) => { | ||
return [Number(numericStringKey), color]; | ||
}) | ||
.sort(sortByNumericFirstIndex) | ||
); | ||
if (includeEnds) { | ||
shadesArray.unshift([0, '#ffffff']); | ||
shadesArray.push([1000, '#000000']); | ||
} | ||
const finalShades = [...shadesArray]; | ||
for (let i = 0; i < shadesArray.length - 1; i++) { | ||
const [shade, color] = shadesArray[i]; | ||
const [nextShade, nextColor] = shadesArray[i + 1]; | ||
const isOptionInvalid = (optionName, test) => { | ||
return options.hasOwnProperty(optionName) && !test(options[optionName]); | ||
} | ||
|
||
// check to make sure both shades being compared | ||
// are evenly divisible by the set interval | ||
let interpolations = (nextShade - shade) / interval - 1; | ||
if ( | ||
interpolations <= 0 || | ||
!Number.isInteger(interpolations) | ||
) continue; | ||
if (isOptionInvalid('includeBase', v => typeof v === 'boolean')) | ||
throw new Error('tailwind-lerp-colors option `includeBase` must be a boolean.'); | ||
if (isOptionInvalid('includeLegacy', v => typeof v === 'boolean')) | ||
throw new Error('tailwind-lerp-colors option `includeLegacy` must be a boolean.'); | ||
if (isOptionInvalid('includeEnds', v => typeof v === 'boolean')) | ||
throw new Error('tailwind-lerp-colors option `includeEnds` must be a boolean.'); | ||
if (isOptionInvalid('interval', v => Number.isInteger(v) && v > 0)) | ||
throw new Error('tailwind-lerp-colors option `interval` must be a positive integer greater than 0.'); | ||
if (isOptionInvalid('mode', v => validColorModes.includes(v))) | ||
throw new Error(`tailwind-lerp-colors option \`mode\` must be one of the following values: ${validColorModes.map(modeName => '`modeName`').join(', ')}.`); | ||
|
||
const scale = chroma.scale([color, nextColor]).mode(mode); | ||
const getColorAt = percent => scale(percent).hex(); | ||
for (let run = 1; run <= interpolations; run++) { | ||
const percent = run / (interpolations + 1); | ||
finalShades.push([ | ||
shade + (interval * run), | ||
getColorAt(percent) | ||
]); | ||
} | ||
} | ||
finalShades.sort(sortByNumericFirstIndex); | ||
finalColors[name] = Object.fromEntries(finalShades) | ||
} | ||
const [baseColors, legacyColors] = Object.entries(builtInColors).reduce( | ||
([base, legacy], [name, values]) => { | ||
if (legacyNames.includes(name)) legacy[name] = values; | ||
else base[name] = values; | ||
return [base, legacy]; | ||
}, [{}, {}] | ||
); | ||
const { includeBase, includeLegacy, includeEnds, interval, mode } = { | ||
...defaultOptions, | ||
...options, | ||
}; | ||
const initialColors = Object.entries({ | ||
...(includeBase ? baseColors : {}), | ||
...(includeLegacy ? legacyColors : {}), | ||
...colorsObj, | ||
}); | ||
|
||
return finalColors; | ||
const finalColors = {}; | ||
|
||
for (const [name, shades] of initialColors) { | ||
if ( | ||
['null', 'undefined'].includes(typeof shades) || | ||
!shades.toString | ||
) { | ||
continue; | ||
} | ||
finalColors[name] = shades; | ||
if ( | ||
typeof shades === 'string' || | ||
Array.isArray(shades) || | ||
shades.toString() !== '[object Object]' || | ||
!Object.keys(shades).every(key => { | ||
return !isNaN(key); | ||
}) | ||
) { | ||
continue; | ||
} | ||
|
||
return { | ||
theme: { | ||
extend: { | ||
colors: generateLerpedColors | ||
} | ||
const shadesArray = ( | ||
Object.entries(shades) | ||
.map(([numericStringKey, color]) => { | ||
return [Number(numericStringKey), color]; | ||
}) | ||
.sort(sortByNumericFirstIndex) | ||
); | ||
if (includeEnds) { | ||
shadesArray.unshift([0, '#ffffff']); | ||
shadesArray.push([1000, '#000000']); | ||
} | ||
const finalShades = [...shadesArray]; | ||
for (let i = 0; i < shadesArray.length - 1; i++) { | ||
const [shade, color] = shadesArray[i]; | ||
const [nextShade, nextColor] = shadesArray[i + 1]; | ||
|
||
// check to make sure both shades being compared | ||
// are evenly divisible by the set interval | ||
let interpolations = (nextShade - shade) / interval - 1; | ||
if ( | ||
interpolations <= 0 || | ||
!Number.isInteger(interpolations) | ||
) continue; | ||
|
||
const scale = chroma.scale([color, nextColor]).mode(mode); | ||
const getColorAt = percent => scale(percent).hex(); | ||
for (let run = 1; run <= interpolations; run++) { | ||
const percent = run / (interpolations + 1); | ||
finalShades.push([ | ||
shade + (interval * run), | ||
getColorAt(percent) | ||
]); | ||
} | ||
}; | ||
} | ||
finalShades.sort(sortByNumericFirstIndex); | ||
finalColors[name] = Object.fromEntries(finalShades) | ||
} | ||
); | ||
|
||
return finalColors; | ||
}; | ||
|
||
module.exports = interpolateColors; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters