diff --git a/README.md b/README.md index 4d4f48d..8a8d7f1 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ It supports non-px units like `rem`, 3 shadow types, `inset` shadows, and any co --- - ## Usage **Step 1:** Install plugin: @@ -60,7 +59,7 @@ module.exports = { ### CSS API -The plugins supports 3 shadows types. You can try them on [`smoothshadows.com`](https://smoothshadows.com). +The plugins supports 3 shadows types from [`smoothshadows.com`](https://smoothshadows.com). ```css .soft { @@ -82,6 +81,25 @@ It also supports `inset` shadows: } ``` +It supports lush shadows generation from [`joshwcomeau.com/shadow-palette`](https://www.joshwcomeau.com/shadow-palette/) + +```css +.low { + box-shadow: --lush-shadow(low -0.25 -0.5 0.5 0.5 0.75 oklch(0 0 0 / 15%)); +} +.medium { + box-shadow: --lush-shadow(medium -0.25 -0.5 0.5 0.5 0.75 oklch(0 0 0 / 15%)); +} +.high { + box-shadow: --lush-shadow(high -0.25 -0.5 0.5 0.5 0.75 oklch(0 0 0 / 15%)); +} +.inset { + box-shadow: --lush-shadow( + medium inset -0.25 -0.5 0.5 0.5 0.75 oklch(0 0 0 / 15%) + ); +} +``` + ### JS API There is low-level JS API: @@ -92,3 +110,43 @@ import { renderShadows } from 'postcss-smooth-shadow' renderShadows('soft', false, '0', '0.5rem', '1rem', 'oklch(0 0 0 / 10%)') // => ["calc(0.111 * 0.5rem) calc(0.111 * 1rem) …", …] ``` + +API for lush shadows + +```ts +import { renderLushShadows } from 'postcss-smooth-shadow' + +const [low, medium, high] = renderLushShadows( + false, // inset + -0.25, // light-x + -0.5, // light-y + 0.5, // oomph + 0.5, // crispy + 0.75, // resolution + 'oklch(0 0 0 / 15%)' // color +) + +low = [ + ['0.3px 0.5px 0.7px hsl(from oklch(0 0 0 / 15%) h s l / 0.1)'], + ['0.4px 0.8px 1px -1.2px hsl(from oklch(0 0 0 / 15%) h s l / 0.1)'], + ['1px 2px 2.5px -2.5px hsl(from oklch(0 0 0 / 15%) h s l / 0.1)'] +] + +medium = [ + ['0.3px 0.5px 0.7px hsl(from oklch(0 0 0 / 15%) h s l / 0.11)'], + ['0.8px 1.6px 2px -0.8px hsl(from oklch(0 0 0 / 15%) h s l / 0.11)'], + ['2.1px 4.1px 5.2px -1.7px hsl(from oklch(0 0 0 / 15%) h s l / 0.11)'], + ['5px 10px 12.6px -2.5px hsl(from oklch(0 0 0 / 15%) h s l / 0.11)'] +] + +high = [ + ['0.3px 0.5px 0.7px hsl(from oklch(0 0 0 / 15%) h s l / 0.1)'], + ['1.5px 2.9px 3.7px -0.4px hsl(from oklch(0 0 0 / 15%) h s l / 0.1)'], + ['2.7px 5.4px 6.8px -0.7px hsl(from oklch(0 0 0 / 15%) h s l / 0.1)'], + ['4.5px 8.9px 11.2px -1.1px hsl(from oklch(0 0 0 / 15%) h s l / 0.1)'], + ['7.1px 14.3px 18px -1.4px hsl(from oklch(0 0 0 / 15%) h s l / 0.1)'], + ['11.2px 22.3px 28.1px -1.8px hsl(from oklch(0 0 0 / 15%) h s l / 0.1)'], + ['17px 33.9px 42.7px -2.1px hsl(from oklch(0 0 0 / 15%) h s l / 0.1)'], + ['25px 50px 62.9px -2.5px hsl(from oklch(0 0 0 / 15%) h s l / 0.1)'] +] +``` diff --git a/index.d.ts b/index.d.ts index 154197a..c0b398a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -9,6 +9,16 @@ export function renderShadows( color: string ): string +export function renderLushShadows( + inset: boolean, + lightX: number, + lightY: number, + oomph: number, + crispy: number, + resolution: number, + color: string +): [string[], string[], string[]] + declare const plugin: PluginCreator<{}> export default plugin diff --git a/index.js b/index.js index cd18781..ca4a65c 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,5 @@ +import { renderLushShadows, replaceLushFunctions } from './src/lush.js' + function easeInQuad(x) { return x * x } @@ -121,10 +123,6 @@ function replaceFunctions(decl, func) { let result = decl.value let currentIndex = 0 - if (!result.includes(searchPattern)) { - return - } - let startIndex = result.indexOf(searchPattern, currentIndex) while (startIndex !== -1) { let openParenIndex = startIndex + searchPattern.length - 1 @@ -193,11 +191,21 @@ function findClosingParenAndParseParams(text, openParenIndex) { let plugin = () => { return { Declaration(decl) { - if (decl.value.includes('-shadow(')) { + if (decl.value.includes('--sharp-shadow(')) { replaceFunctions(decl, 'sharp') + } + + if (decl.value.includes('--soft-shadow(')) { replaceFunctions(decl, 'soft') + } + + if (decl.value.includes('--linear-shadow(')) { replaceFunctions(decl, 'linear') } + + if (decl.value.includes('--lush-shadow(')) { + replaceLushFunctions(decl) + } }, postcssPlugin: 'postcss-smooth-shadow' } @@ -205,3 +213,5 @@ let plugin = () => { plugin.postcss = true export default plugin + +export { renderLushShadows } diff --git a/package.json b/package.json index d5165de..13a3a64 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "eslint": "^9.37.0", "multiocular": "^0.8.1", "postcss": "^8.5.6", + "postcss-value-parser": "^4.2.0", "vite": "^7.1.10" }, "prettier": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4c31a2..8ef5af8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: postcss: specifier: ^8.5.6 version: 8.5.6 + postcss-value-parser: + specifier: ^4.2.0 + version: 4.2.0 vite: specifier: ^7.1.10 version: 7.1.10(yaml@2.8.1) @@ -341,67 +344,56 @@ packages: resolution: {integrity: sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.52.4': resolution: {integrity: sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.52.4': resolution: {integrity: sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.52.4': resolution: {integrity: sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.52.4': resolution: {integrity: sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.52.4': resolution: {integrity: sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.52.4': resolution: {integrity: sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.52.4': resolution: {integrity: sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.52.4': resolution: {integrity: sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.52.4': resolution: {integrity: sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.52.4': resolution: {integrity: sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openharmony-arm64@4.52.4': resolution: {integrity: sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==} @@ -538,49 +530,41 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -1125,6 +1109,9 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -2308,6 +2295,8 @@ snapshots: picomatch@4.0.3: {} + postcss-value-parser@4.2.0: {} + postcss@8.5.6: dependencies: nanoid: 3.3.11 diff --git a/src/lush.js b/src/lush.js new file mode 100644 index 0000000..1b59c84 --- /dev/null +++ b/src/lush.js @@ -0,0 +1,401 @@ +import valueParser from 'postcss-value-parser' + +import { + clamp, + getValuesForBezierCurve, + normalize, + range, + roundTo +} from './utils.js' + +/** + * @param {{ value: string }} decl + */ +export function replaceLushFunctions(decl) { + let [variant, inset, x, y, oomph, crispy, resolution, color] = + parseLushShadow(decl) + + let [low, medium, high] = renderLushShadows( + inset, + x, + y, + oomph, + crispy, + resolution, + color + ) + + let before = + decl.raws.between && decl.raws.before ? `${decl.raws.before} ` : '' + let between = + decl.raws.between && decl.raws.before ? `,${decl.raws.before} ` : ', ' + + switch (variant) { + case 'high': + decl.value = before + high.flat().join(between) + break + case 'low': + decl.value = before + low.flat().join(between) + break + case 'medium': + decl.value = before + medium.flat().join(between) + break + } + + return decl +} + +/** + * @param {{ value: string }} decl + * @returns {[string, boolean, number, number, number, number, number, string]} + */ +export function parseLushShadow(decl) { + let parsed = valueParser(decl.value) + + let args = [ + 'variant', + 'inset', + 'light-x', + 'light-y', + 'oomph', + 'crispy', + 'resolution', + 'color' + ] + + let node = parsed.nodes.find( + ({ type, value }) => type === 'function' && value === '--lush-shadow' + ) + + if (node === undefined) { + return + } + + let nodes = node.nodes.filter( + fnNode => + fnNode.type !== 'space' && + fnNode.type !== 'comment' && + fnNode.type !== 'div' + ) + + if (nodes.length !== 7 && nodes.length !== 8) { + throw decl.error( + `'--lush-shadow(variant inset? light-x light-y oomph crispy resolution color) requires 7-8 params got ${nodes.length}`, + { index: node.startIndex } + ) + } + + return nodes.reduce((previous, valueNode, index) => { + if (index === 0 && valueNode.type === 'word') { + if (['high', 'low', 'medium'].includes(valueNode.value.toLowerCase())) { + return [...previous, valueNode.value] + } + + throw decl.error( + `'--lush-shadow(variant inset? light-x light-y oomph crispy resolution color) variant to be "low", "medium" or "high" got ${valueNode.value}`, + { index: valueNode.sourceIndex } + ) + } + + if (index === 1 && valueNode.type === 'word') { + if (valueNode.value === 'inset') { + return [...previous, true] + } else if (/^-?\d+(\.\d+)?$/.test(valueNode.value)) { + previous.push(false) + } + } + + if (index < nodes.length - 1) { + if (/^-?\d+(\.\d+)?$/.test(valueNode.value)) { + return [...previous, Number(valueNode.value)] + } else { + let argIndex = previous.length + Math.abs(args.length - nodes.length) + + throw decl.error( + `'--lush-shadow(variant inset? light-x light-y oomph crispy resolution color) ${args[argIndex]} to be number got ${valueNode.value}`, + { index: node.sourceIndex + valueNode.sourceIndex } + ) + } + } + + if (valueNode.type === 'word' && index === nodes.length - 1) { + return [...previous, valueNode.value] + } + + return [ + ...previous, + decl.value.substring(valueNode.sourceIndex, valueNode.sourceEndIndex) + ] + }, []) +} + +/** + * We'll generate a set of 3 shadows: small, medium, large. + * Each shadow will have multiple layers, depending on the size. + * A small shadow might only have 2 shadows, a large might have 6. + * Though, this is affected by the `layers` property + * + * @param {boolean} inset + * @param {number} lightX + * @param {number} lightY + * @param {number} oomph + * @param {number} crispy + * @param {number} resolution + * @param {string} color + * @returns {[string[], string[], string[]]} + */ +export function renderLushShadows( + inset, + lightX, + lightY, + oomph, + crispy, + resolution, + color +) { + let output = [] + + let SHADOW_LAYER_LIMITS = { + large: { + max: 10, + min: 3 + }, + medium: { + max: 5, + min: 2 + }, + small: { + max: 3, + min: 2 + } + } + + for (let size of ['small', 'medium', 'large']) { + let numOfLayers = Math.round( + normalize( + resolution, + 0, + 1, + SHADOW_LAYER_LIMITS[size].min, + SHADOW_LAYER_LIMITS[size].max + ) + ) + + let layersForSize = [] + + range(numOfLayers).forEach(layerIndex => { + let opacity = calculateShadowOpacity({ + crispy, + layerIndex, + maxLayers: SHADOW_LAYER_LIMITS[size].max, + minLayers: SHADOW_LAYER_LIMITS[size].min, + numOfLayers, + oomph + }) + + let { x, y } = calculateShadowOffsets({ + crispy, + layerIndex, + lightSource: { + x: lightX, + y: lightY + }, + numOfLayers, + oomph, + size + }) + + let blurRadius = calculateBlurRadius({ + crispy, + layerIndex, + numOfLayers, + oomph, + size, + x, + y + }) + + let spread = calculateSpread({ + crispy, + layerIndex, + numOfLayers, + oomph + }) + + let spreadString = spread !== 0 ? `${spread}px ` : '' + + let insetPrefix = inset ? 'inset ' : '' + + layersForSize.push([ + `${x}px ${y}px ${blurRadius}px ${spreadString}${insetPrefix}hsl(from ${color} h s l / ${opacity})` + ]) + }) + + output.push(layersForSize) + } + + return output +} + +/** + * @param {{ + * crispy: number; + * layerIndex: number; + * maxLayers: number; + * minLayers: number; + * numOfLayers: number; + * oomph: number; + * }} props + * @returns + */ +function calculateShadowOpacity({ + crispy, + layerIndex, + maxLayers, + minLayers, + numOfLayers, + oomph +}) { + let baseOpacity = normalize(oomph, 0, 1, 0.4, 1.25) + + let initialOpacityMultiplier = normalize(crispy, 0, 1, 0, 1) + let finalOpacityMultiplier = normalize(crispy, 0, 1, 1, 0) + + // Crispy determines which shadows are more visible, and + // which shadows are less visible. + let layerOpacityMultiplier = normalize( + layerIndex, + 0, + numOfLayers, + initialOpacityMultiplier, + finalOpacityMultiplier + ) + + let opacity = baseOpacity * layerOpacityMultiplier + + // So, here's the problem. + // The `resolution` param lets us change how many layers are + // generated. Every additional layer should reduce the opacity + // of all layers, so that "resolution" doesn't change the + // perceived opacity. + let averageLayers = (minLayers + maxLayers) / 2 + let ratio = averageLayers / numOfLayers + + let layerOpacity = opacity * ratio + + layerOpacity *= 0.3 + + return clamp(roundTo(layerOpacity, 2), 0, 1) +} + +/** + * @param {{ + * crispy: number; + * layerIndex: number; + * lightSource: { + * x: number; + * y: number; + * }; + * numOfLayers: number; + * oomph: number; + * size: number; + * }} param0 + * @returns + */ +function calculateShadowOffsets({ + crispy, + layerIndex, + lightSource, + numOfLayers, + oomph, + size +}) { + let maxOffsetBySize = { + large: normalize(oomph, 0, 1, 50, 150), + medium: normalize(oomph, 0, 1, 15, 25), + small: normalize(oomph, 0, 1, 3, 5) + } + + // We don't want to use linear interpolation here because we want + // the shadows to cluster near the front and fall off. Otherwise, + // the most opaque part of the shadow is in the middle of the + // group, rather than being near the element. + // We'll use a bezier curve and pluck points along it. + let curve = { + controlPoint1: [ + normalize(crispy, 0, 1, 0.25, 0), + normalize(crispy, 0, 1, 0.25, 0) + ], + controlPoint2: [ + normalize(crispy, 0, 1, 0.25, 0), + normalize(crispy, 0, 1, 0.25, 0) + ], + endPoint: [1, 0], + startPoint: [0, 1] + } + let t = layerIndex / (numOfLayers - 1) + let [ratio] = getValuesForBezierCurve(curve, t) + + let max = maxOffsetBySize[size] + + // Now, for x/y offset... we have this lightSource value, with + // X and Y from -1 to 1. + let xOffsetMin = normalize(lightSource.x, -1, 1, 1, -1) + let xOffsetMax = normalize(lightSource.x, -1, 1, max, max * -1) + let yOffsetMin = normalize(lightSource.y, -1, 1, 1, -1) + let yOffsetMax = normalize(lightSource.y, -1, 1, max, max * -1) + + let x = roundTo(normalize(ratio, 0, 1, xOffsetMin, xOffsetMax), 1) + let y = roundTo(normalize(ratio, 0, 1, yOffsetMin, yOffsetMax), 1) + + return { x, y } +} + +/** + * @param {{ + * crispy: number; + * layerIndex: number; + * numOfLayers: number; + * oomph: number; + * size: number; + * x: number; + * y: number; + * }} props + * @returns + */ +function calculateBlurRadius({ crispy, x, y }) { + // The blur radius should depend on the x/y offset. + // Calculate the hypothenuse length and use it as the blur radius? + let hypothenuse = (x ** 2 + y ** 2) ** 0.5 + + let radius = normalize(crispy, 0, 1, hypothenuse * 1.5, hypothenuse * 0.75) + + return roundTo(radius, 1) +} + +/** + * @param {{ + * crispy: number; + * layerIndex: number; + * numOfLayers: number; + * oomph: number; + * }} props + * @returns + */ +function calculateSpread({ crispy, layerIndex, numOfLayers }) { + if (layerIndex === 0) { + return 0 + } + + let maxReduction = normalize(crispy, 0, 1, 0, -5) + + let actualReduction = normalize( + layerIndex + 1, + 1, + numOfLayers, + 0, + maxReduction + ) + + return roundTo(actualReduction, 1) +} diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..dae937f --- /dev/null +++ b/src/utils.js @@ -0,0 +1,114 @@ +/** + * @param {number} value + * @param {number} currentScaleMin + * @param {number} currentScaleMax + * @param {number} newScaleMin + * @param {number} newScaleMax + * @returns {number} + */ +export function normalize( + value, + currentScaleMin, + currentScaleMax, + newScaleMin = 0, + newScaleMax = 1 +) { + // First, normalize the value between 0 and 1. + let standardNormalization = + (value - currentScaleMin) / (currentScaleMax - currentScaleMin) + + // Next, transpose that value to our desired scale. + return (newScaleMax - newScaleMin) * standardNormalization + newScaleMin +} + +/** + * @param {number} value + * @param {number} min + * @param {number} max + * @returns {number} + */ +export function clamp(value, min = 0, max = 1) { + // We might be passing in "inverted" values, eg: + // clamp(someVal, 10, 5); + // + // This is especially common with `clampedNormalize`. + // In these cases, we'll flip the min/max so that the function works as expected. + let actualMin = Math.min(min, max) + let actualMax = Math.max(min, max) + + return Math.max(actualMin, Math.min(actualMax, value)) +} + +/** + * @param {number} start + * @param {number} end + * @param {number} step + * @returns {number} + */ +export function range(start, end, step = 1) { + let output = [] + + if (typeof end === 'undefined') { + end = start + start = 0 + } + + for (let i = start; i < end; i += step) { + output.push(i) + } + + return output +} + +/** + * @param {number} value + * @param {number} places + * @returns {number} + */ +export function roundTo(value, places = 0) { + return Math.round(value * 10 ** places) / 10 ** places +} + +/** + * + * @param {{ + * controlPoint1: number, + * controlPoint2: number, + * endPoint: number, + * startPoint: number }} points + * @param {number} t + * @returns {[number, number]} + */ +export function getValuesForBezierCurve( + { controlPoint1, controlPoint2, endPoint, startPoint }, + t +) { + let x, y + + if (controlPoint2) { + // Cubic Bezier curve + x = + (1 - t) ** 3 * startPoint[0] + + 3 * (1 - t) ** 2 * t * controlPoint1[0] + + 3 * (1 - t) * t ** 2 * controlPoint2[0] + + t ** 3 * endPoint[0] + + y = + (1 - t) ** 3 * startPoint[1] + + 3 * (1 - t) ** 2 * t * controlPoint1[1] + + 3 * (1 - t) * t ** 2 * controlPoint2[1] + + t ** 3 * endPoint[1] + } else { + // Quadratic Bezier curve + x = + (1 - t) * (1 - t) * startPoint[0] + + 2 * (1 - t) * t * controlPoint1[0] + + t * t * endPoint[0] + y = + (1 - t) * (1 - t) * startPoint[1] + + 2 * (1 - t) * t * controlPoint1[1] + + t * t * endPoint[1] + } + + return [x, y] +} diff --git a/test/demo/index.html b/test/demo/index.html index 0c316db..c1c320a 100644 --- a/test/demo/index.html +++ b/test/demo/index.html @@ -51,6 +51,34 @@ &.is-6 { box-shadow: --sharp-shadow(inset 0 2px 5px oklch(0 0 0 / 40%)); } + + &.is-lush-low { + width: 25rem; + box-shadow: --lush-shadow( + low -0.25 -0.5 0.5 0.5 0.75 oklch(0 0 0 / 15%) + ); + } + + &.is-lush-medium { + width: 25rem; + box-shadow: --lush-shadow( + medium -0.25 -0.5 0.5 0.5 0.75 oklch(0 0 0 / 15%) + ); + } + + &.is-lush-high { + width: 25rem; + box-shadow: --lush-shadow( + high -0.25 -0.5 0.5 0.5 0.75 oklch(0 0 0 / 15%) + ); + } + + &.is-lush-high-inset { + width: 25rem; + box-shadow: --lush-shadow( + medium inset -0.25 -0.5 0.5 0.5 0.75 oklch(0 0 0 / 15%) + ); + } } @@ -90,5 +118,29 @@ inset 0 1px 2px oklch(0 0 0 / 40%) ) + +
+--lush-shadow( + low -0.25 -0.5 0.5 0.5 0.75 oklch(0 0 0 / 15%) +)+
+--lush-shadow( + medium -0.25 -0.5 0.5 0.5 0.75 oklch(0 0 0 / 15%) +)+
+--lush-shadow( + high -0.25 -0.5 0.5 0.5 0.75 oklch(0 0 0 / 15%) +)+
+--lush-shadow( + medium inset -0.25 -0.5 0.5 0.5 0.75 oklch(0 0 0 / 15%) +)+