├── .editorconfig ├── .eslintrc.yml ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── color-blend.svg ├── explanation.png ├── package-lock.json ├── package.json ├── src ├── helpers.ts ├── index.ts ├── non-separable-blend.ts ├── non-separable-modes.ts ├── separable-blend.ts ├── separable-modes.ts ├── types.ts └── unit.ts ├── test └── test.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [**.{ts,js}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.{json,svg}] 13 | insert_final_newline = false 14 | 15 | [package.json] 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | parser: '@typescript-eslint/parser' 3 | plugins: 4 | - '@typescript-eslint' 5 | extends: 6 | - eslint:recommended 7 | - plugin:@typescript-eslint/recommended 8 | - prettier 9 | rules: 10 | '@typescript-eslint/no-explicit-any': off 11 | '@typescript-eslint/ban-ts-comment': off 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: '*' 11 | update-types: 12 | - 'version-update:semver-patch' 13 | - 'version-update:semver-minor' 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | paths-ignore: 9 | - "*.md" 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ${{ matrix.os }} 15 | 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, windows-latest] 19 | node-version: ['16', '18', '20'] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | with: 24 | persist-credentials: false 25 | - uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: npm install 29 | - run: npm test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node,windows,linux,macos 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (http://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directories 35 | node_modules 36 | jspm_packages 37 | 38 | # Optional npm cache directory 39 | .npm 40 | 41 | # Optional eslint cache 42 | .eslintcache 43 | 44 | # Optional REPL history 45 | .node_repl_history 46 | 47 | # Output of 'npm pack' 48 | *.tgz 49 | 50 | # Yarn Integrity file 51 | .yarn-integrity 52 | 53 | 54 | 55 | ### Windows ### 56 | # Windows image file caches 57 | Thumbs.db 58 | ehthumbs.db 59 | 60 | # Folder config file 61 | Desktop.ini 62 | 63 | # Recycle Bin used on file shares 64 | $RECYCLE.BIN/ 65 | 66 | # Windows Installer files 67 | *.cab 68 | *.msi 69 | *.msm 70 | *.msp 71 | 72 | # Windows shortcuts 73 | *.lnk 74 | 75 | 76 | ### Linux ### 77 | *~ 78 | 79 | # temporary files which can be created if a process still has a handle open of a deleted file 80 | .fuse_hidden* 81 | 82 | # KDE directory preferences 83 | .directory 84 | 85 | # Linux trash folder which might appear on any partition or disk 86 | .Trash-* 87 | 88 | # .nfs files are created when an open file is removed but is still being accessed 89 | .nfs* 90 | 91 | 92 | ### macOS ### 93 | *.DS_Store 94 | .AppleDouble 95 | .LSOverride 96 | 97 | # Icon must end with two \r 98 | Icon 99 | # Thumbnails 100 | ._* 101 | # Files that might appear in the root of a volume 102 | .DocumentRevisions-V100 103 | .fseventsd 104 | .Spotlight-V100 105 | .TemporaryItems 106 | .Trashes 107 | .VolumeIcon.icns 108 | .com.apple.timemachine.donotpresent 109 | # Directories potentially created on remote AFP share 110 | .AppleDB 111 | .AppleDesktop 112 | Network Trash Folder 113 | Temporary Items 114 | .apdisk 115 | 116 | # Custom 117 | /.rts2_cache_* 118 | /dist 119 | /unit 120 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | arrowParens: avoid 2 | bracketSpacing: true 3 | htmlWhitespaceSensitivity: css 4 | insertPragma: false 5 | jsxBracketSameLine: false 6 | jsxSingleQuote: false 7 | printWidth: 80 8 | proseWrap: preserve 9 | requirePragma: false 10 | semi: false 11 | singleQuote: true 12 | tabWidth: 2 13 | trailingComma: none 14 | useTabs: false 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Florian Reuschel 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | ![color-blend logo showing two half-transparent, overlapping circles](https://cdn.jsdelivr.net/gh/Loilo/color-blend@61bf569ab93e02df2291f47585d3e554acc0c9a1/color-blend.svg) 6 | 7 |
8 |
9 | 10 | # color-blend 11 | 12 | [![Tests](https://badgen.net/github/checks/loilo/color-blend/master)](https://github.com/loilo/color-blend/actions) 13 | [![Version on npm](https://badgen.net/npm/v/color-blend)](https://www.npmjs.com/package/color-blend) 14 | 15 | > Blends RGBA colors with different blend modes 16 | 17 | This is a zero-dependency JavaScript implementation of the blend modes introduced in the [W3C Compositing and Blending spec](https://www.w3.org/TR/compositing-1/). 18 | 19 | Altogether it's a whopping 1.1 KB small (minified & gzipped), going down to as far as 0.4 KB if you use just one blending method and a [tree-shaking](https://en.wikipedia.org/wiki/Tree_shaking) bundler. 20 | 21 | ## Install 22 | 23 | ```console 24 | $ npm install --save color-blend 25 | ``` 26 | 27 | ## Usage 28 | 29 | ### Example 30 | 31 | It's really easy to wrap your head around. Consider the following simple example: 32 | 33 | ```js 34 | // Using vanilla Node.js 35 | const { normal } = require('color-blend') 36 | 37 | // Using a bundler? It will automatically pick up a 38 | // tree-shakeable ES modules version of color-blend: 39 | import { normal } from 'color-blend' 40 | 41 | // Mix some green and pink 42 | const pinkBackground = { r: 255, g: 0, b: 87, a: 0.42 } 43 | const greenForeground = { r: 70, g: 217, b: 98, a: 0.6 } 44 | 45 | normal(pinkBackground, greenForeground) 46 | // returns { r: 110, g: 170, b: 96, a: 0.768 } 47 | ``` 48 | 49 | By the way, those are the colors from the logo above. See? 50 | 51 | ![Visual representation of the example code](explanation.png) 52 | 53 | ### Explanation 54 | 55 | This module provides an implementation for all blend modes listed in the aforementioned W3C document, which are: 56 | 57 | - `normal` 58 | - `multiply` 59 | - `screen` 60 | - `overlay` 61 | - `darken` 62 | - `lighten` 63 | - `colorDodge` 64 | - `colorBurn` 65 | - `hardLight` 66 | - `softLight` 67 | - `difference` 68 | - `exclusion` 69 | - `hue` 70 | - `saturation` 71 | - `color` 72 | - `luminosity` 73 | 74 | All those methods have the same API: they take a `background` and a `foreground` color as arguments. 75 | Those are expected to be RGBA colors, similar to how they appear in CSS — represented as plain objects containing the keys 76 | 77 | - `r`, `g`, `b` (each ranging from 0 to 255) 78 | - `a` (ranging from 0 to 1) 79 | 80 | The result of the blending operation will be returned as such an RGBA object as well. 81 | 82 | ### Unit Colors 83 | 84 | If you need higher precision (resulting RGB channels will be rounded to integers!) or just have a different flavor, this package offers the `/unit` entry point, where all accepted and returned color channels are values between 0 and 1: 85 | 86 | ```javascript 87 | import { normal } from 'color-blend/unit' 88 | 89 | // Still mix some green and pink 90 | const pinkBackground = { r: 1, g: 0, b: 0.34, a: 0.42 } 91 | const greenForeground = { r: 0.27, g: 0.85, b: 0.38, a: 0.6 } 92 | 93 | normal(pinkBackground, greenForeground) 94 | // returns { r: 0.43, g: 0.665, b: 0.372, a: 0.768 } (rounded to 3 decimals for brevity) 95 | ``` 96 | 97 | ## Thanks 98 | 99 | A special "thank you" goes to [Christos Lytras](https://github.com/clytras) who helped me [digging deep](https://stackoverflow.com/questions/40796852/mix-two-non-opaque-colors-with-hue-blend-mode) into the rabbit hole of color blending. 100 | -------------------------------------------------------------------------------- /color-blend.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /explanation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loilo/color-blend/015b055a69c5a5e64e528621558ded006177eb9b/explanation.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "color-blend", 3 | "version": "4.0.0", 4 | "description": "Blends RGBA colors with different blend modes", 5 | "keywords": [ 6 | "blend", 7 | "color", 8 | "colour" 9 | ], 10 | "repository": "Loilo/color-blend", 11 | "license": "MIT", 12 | "author": "Florian Reuschel ", 13 | "files": [ 14 | "dist", 15 | "unit", 16 | "explanation.png" 17 | ], 18 | "main": "dist/index.js", 19 | "module": "dist/index.modern.js", 20 | "source": "src/index.ts", 21 | "types": "dist/index.d.ts", 22 | "umd:main": "dist/index.umd.js", 23 | "unpkg": "dist/index.umd.js", 24 | "scripts": { 25 | "build": "microbundle --entry src/index.ts --output dist/index.js && microbundle --entry src/unit.ts --output unit/index.js --format modern,cjs,umd", 26 | "pretest": "eslint \"src/*.ts\" && npm run build", 27 | "test": "jest" 28 | }, 29 | "devDependencies": { 30 | "@types/jest": "^29.5.3", 31 | "@typescript-eslint/eslint-plugin": "^6.0.0", 32 | "@typescript-eslint/parser": "^6.0.0", 33 | "eslint": "^8.14.0", 34 | "eslint-config-prettier": "^9.0.0", 35 | "jest": "^29.6.1", 36 | "microbundle": "^0.15.1", 37 | "typescript": "^5.1.6" 38 | }, 39 | "engines": { 40 | "node": ">=10.0.0" 41 | }, 42 | "sideEffects": false 43 | } 44 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { ChannelBlender, NoAlphaBlender, RGB, RGBA } from './types' 2 | 3 | // Some utility (no actual blend-related algorithms) for color handling 4 | 5 | /** 6 | * Restricts a number to given boundaries 7 | * @param value The number to restrict 8 | * @param from The lower boundary 9 | * @param to The upper boundary 10 | * @return The restricted value 11 | */ 12 | function restrictNumber(value: number, from: number, to: number) { 13 | return Math.min(Math.max(value || 0, from), to) 14 | } 15 | 16 | /** 17 | * Restricts an { r,g,b,a } color to its boundaries (0..255 color channels, 0..1 alpha channel) 18 | * @param color The { r,g,b,a } color to restrict 19 | * @return The restricted color 20 | */ 21 | function restrictColor(color: RGBA): RGBA { 22 | return { 23 | r: restrictNumber(color.r, 0, 255), 24 | g: restrictNumber(color.g, 0, 255), 25 | b: restrictNumber(color.b, 0, 255), 26 | a: restrictNumber(color.a, 0, 1) 27 | } 28 | } 29 | 30 | /** 31 | * Converts a color from unit color channels [0..1] to 8-bit color channels [0..255] 32 | * @param color The { r,g,b,a } color to convert 33 | * @return The { r,g,b,a } with 8-bit color channels 34 | */ 35 | export function convertFromUnit(color: RGBA): RGBA 36 | export function convertFromUnit(color: RGB): RGB 37 | export function convertFromUnit(color: any): RGB | RGBA { 38 | return { 39 | r: color.r * 255, 40 | g: color.g * 255, 41 | b: color.b * 255, 42 | a: color.a 43 | } 44 | } 45 | 46 | /** 47 | * Converts a color from 8-bit color channels [0..255] to unit color channels [0..1] 48 | * @param color The { r,g,b,a } color to convert 49 | * @return The { r,g,b,a } with unit color channels 50 | */ 51 | export function convertToUnit(color: RGBA): RGBA 52 | export function convertToUnit(color: RGB): RGB 53 | export function convertToUnit(color: any): RGB | RGBA { 54 | return { 55 | r: color.r / 255, 56 | g: color.g / 255, 57 | b: color.b / 255, 58 | a: color.a 59 | } 60 | } 61 | 62 | /** 63 | * Rounds the color channels of an RGBA color 64 | * @param color The { r,g,b,a } color to handle 65 | * @param precision How many decimals? Defaults to 0 66 | * @return The { r,g,b,a } with rounded color channels 67 | */ 68 | function roundChannels(color: RGBA, precision?: number): RGBA 69 | function roundChannels(color: RGB, precision?: number): RGB 70 | function roundChannels(color: any, precision = 0): RGB | RGBA { 71 | const multiplier = Math.pow(10, precision) 72 | 73 | return { 74 | r: Math.round(color.r * multiplier) / multiplier, 75 | g: Math.round(color.g * multiplier) / multiplier, 76 | b: Math.round(color.b * multiplier) / multiplier, 77 | a: color.a 78 | } 79 | } 80 | 81 | /** 82 | * Rounds the color channels of an RGBA color with high precision to aviod IEEE 754 related issues 83 | * @param color The { r,g,b,a } color to handle 84 | * @return The { r,g,b,a } with rounded color channels 85 | */ 86 | function roundChannelsBinaryFloat(color: RGBA): RGBA { 87 | return roundChannels(color, 9) 88 | } 89 | 90 | /** 91 | * Applies the appropriate alpha blending to a blend process. 92 | * @see https://www.w3.org/TR/compositing-1/#blending 93 | * @param backdropAlpha The alpha channel of the backdrop color [0..1] 94 | * @param sourceAlpha The alpha channel of the source color [0..1] 95 | * @param compositeAlpha The alpha channel of the composite color [0..1] 96 | * @param backdropColor A color channel (R, G or B) of the backdrop color [0..255] 97 | * @param sourceColor A color channel (R, G or B) of the source color [0..255] 98 | * @param compositeColor A color channel (R, G or B) of the composite color [0..255] 99 | * @return The resulting color channel 100 | */ 101 | function alphaCompose( 102 | backdropAlpha: number, 103 | sourceAlpha: number, 104 | compositeAlpha: number, 105 | backdropColor: number, 106 | sourceColor: number, 107 | compositeColor: number 108 | ) { 109 | return ( 110 | (1 - sourceAlpha / compositeAlpha) * backdropColor + 111 | (sourceAlpha / compositeAlpha) * 112 | Math.round( 113 | (1 - backdropAlpha) * sourceColor + backdropAlpha * compositeColor 114 | ) 115 | ) 116 | } 117 | 118 | export interface BlendOptions { 119 | unitInput: boolean 120 | unitOutput: boolean 121 | roundOutput: boolean 122 | } 123 | 124 | /** 125 | * Blend two colors 126 | * All RGBA objects are { r,g,b,a } with [0..255] for RGB and [0..1] for alpha 127 | * @param source The { r,g,b,a } color to be put on top 128 | * @param backdrop The { r,g,b,a } color to be put below the source 129 | * @param abstractModeCallback The abstract blend mode function (separable vs. non-separable) 130 | * @param concreteModeCallback The concrete blend mode function (normal, multiply, ...) 131 | * @param options The options to apply 132 | * @return The { r,g,b,a } result object, channel values are not rounded 133 | */ 134 | export function performBlend( 135 | backdrop: RGBA, 136 | source: RGBA, 137 | abstractModeCallback: ( 138 | backdrop: RGBA, 139 | source: RGBA, 140 | concreteModeCallback: ChannelBlender 141 | ) => RGB, 142 | concreteModeCallback: ChannelBlender, 143 | options?: Partial 144 | ): RGBA 145 | export function performBlend( 146 | backdrop: RGBA, 147 | source: RGBA, 148 | abstractModeCallback: ( 149 | backdrop: RGBA, 150 | source: RGBA, 151 | concreteModeCallback: NoAlphaBlender 152 | ) => RGB, 153 | concreteModeCallback: NoAlphaBlender, 154 | options?: Partial 155 | ): RGBA 156 | export function performBlend( 157 | backdrop: RGBA, 158 | source: RGBA, 159 | abstractModeCallback: ( 160 | backdrop: RGBA, 161 | source: RGBA, 162 | concreteModeCallback: any 163 | ) => RGB, 164 | concreteModeCallback: ChannelBlender | NoAlphaBlender, 165 | options: Partial = { 166 | unitInput: false, 167 | unitOutput: false, 168 | roundOutput: true 169 | } 170 | ) { 171 | // Handle unit input if needed 172 | if (options.unitInput) { 173 | backdrop = convertFromUnit(backdrop) 174 | source = convertFromUnit(source) 175 | } 176 | 177 | // Remove out-of-bounds values 178 | backdrop = restrictColor(backdrop) 179 | source = restrictColor(source) 180 | 181 | // Calculate resulting alpha 182 | const a = source.a + backdrop.a - source.a * backdrop.a 183 | 184 | // Calculate resulting RGB 185 | const resultRGB = abstractModeCallback(backdrop, source, concreteModeCallback) 186 | 187 | // Calculate actual RGBs from backdrop, source and result + alpha values 188 | // Since blending may result in out-of-bounds color channels, cut those 189 | let resultRGBA = restrictColor({ 190 | r: alphaCompose(backdrop.a, source.a, a, backdrop.r, source.r, resultRGB.r), 191 | g: alphaCompose(backdrop.a, source.a, a, backdrop.g, source.g, resultRGB.g), 192 | b: alphaCompose(backdrop.a, source.a, a, backdrop.b, source.b, resultRGB.b), 193 | a: a 194 | }) 195 | 196 | // Convert color channels to unit values if needed 197 | if (options.unitOutput) { 198 | resultRGBA = convertToUnit(resultRGBA) 199 | 200 | // Round 8-bit color channels if needed 201 | } else if (options.roundOutput) { 202 | resultRGBA = roundChannels(resultRGBA) 203 | 204 | // Round anyways to get rid of JavaScript floating point issues 205 | } else { 206 | resultRGBA = roundChannelsBinaryFloat(resultRGBA) 207 | } 208 | 209 | return resultRGBA 210 | } 211 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import separableBlend from './separable-blend' 2 | import * as separableBlendModes from './separable-modes' 3 | 4 | import nonSeparableBlend from './non-separable-blend' 5 | import * as nonSeparableBlendModes from './non-separable-modes' 6 | 7 | import { performBlend } from './helpers' 8 | import { RGBA } from './types' 9 | 10 | /** 11 | * Blend two colors with the "normal" blend mode 12 | * 13 | * @param backdrop The background color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 14 | * @param source The foreground color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 15 | * @return The blended color 16 | */ 17 | export function normal(backdrop: RGBA, source: RGBA) { 18 | return performBlend( 19 | backdrop, 20 | source, 21 | separableBlend, 22 | separableBlendModes.normal 23 | ) 24 | } 25 | 26 | /** 27 | * Blend two colors with the "multiply" blend mode 28 | * 29 | * @param backdrop The background color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 30 | * @param source The foreground color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 31 | * @return The blended color 32 | */ 33 | export function multiply(backdrop: RGBA, source: RGBA) { 34 | return performBlend( 35 | backdrop, 36 | source, 37 | separableBlend, 38 | separableBlendModes.multiply 39 | ) 40 | } 41 | 42 | /** 43 | * Blend two colors with the "screen" blend mode 44 | * 45 | * @param backdrop The background color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 46 | * @param source The foreground color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 47 | * @return The blended color 48 | */ 49 | export function screen(backdrop: RGBA, source: RGBA) { 50 | return performBlend( 51 | backdrop, 52 | source, 53 | separableBlend, 54 | separableBlendModes.screen 55 | ) 56 | } 57 | 58 | /** 59 | * Blend two colors with the "overlay" blend mode 60 | * 61 | * @param backdrop The background color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 62 | * @param source The foreground color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 63 | * @return The blended color 64 | */ 65 | export function overlay(backdrop: RGBA, source: RGBA) { 66 | return performBlend( 67 | backdrop, 68 | source, 69 | separableBlend, 70 | separableBlendModes.overlay 71 | ) 72 | } 73 | 74 | /** 75 | * Blend two colors with the "darken" blend mode 76 | * 77 | * @param backdrop The background color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 78 | * @param source The foreground color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 79 | * @return The blended color 80 | */ 81 | export function darken(backdrop: RGBA, source: RGBA) { 82 | return performBlend( 83 | backdrop, 84 | source, 85 | separableBlend, 86 | separableBlendModes.darken 87 | ) 88 | } 89 | 90 | /** 91 | * Blend two colors with the "lighten" blend mode 92 | * 93 | * @param backdrop The background color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 94 | * @param source The foreground color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 95 | * @return The blended color 96 | */ 97 | export function lighten(backdrop: RGBA, source: RGBA) { 98 | return performBlend( 99 | backdrop, 100 | source, 101 | separableBlend, 102 | separableBlendModes.lighten 103 | ) 104 | } 105 | 106 | /** 107 | * Blend two colors with the "color dodge" blend mode 108 | * 109 | * @param backdrop The background color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 110 | * @param source The foreground color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 111 | * @return The blended color 112 | */ 113 | export function colorDodge(backdrop: RGBA, source: RGBA) { 114 | return performBlend( 115 | backdrop, 116 | source, 117 | separableBlend, 118 | separableBlendModes.colorDodge 119 | ) 120 | } 121 | 122 | /** 123 | * Blend two colors with the "color burn" blend mode 124 | * 125 | * @param backdrop The background color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 126 | * @param source The foreground color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 127 | * @return The blended color 128 | */ 129 | export function colorBurn(backdrop: RGBA, source: RGBA) { 130 | return performBlend( 131 | backdrop, 132 | source, 133 | separableBlend, 134 | separableBlendModes.colorBurn 135 | ) 136 | } 137 | 138 | /** 139 | * Blend two colors with the "hard light" blend mode 140 | * 141 | * @param backdrop The background color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 142 | * @param source The foreground color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 143 | * @return The blended color 144 | */ 145 | export function hardLight(backdrop: RGBA, source: RGBA) { 146 | return performBlend( 147 | backdrop, 148 | source, 149 | separableBlend, 150 | separableBlendModes.hardLight 151 | ) 152 | } 153 | 154 | /** 155 | * Blend two colors with the "soft light" blend mode 156 | * 157 | * @param backdrop The background color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 158 | * @param source The foreground color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 159 | * @return The blended color 160 | */ 161 | export function softLight(backdrop: RGBA, source: RGBA) { 162 | return performBlend( 163 | backdrop, 164 | source, 165 | separableBlend, 166 | separableBlendModes.softLight 167 | ) 168 | } 169 | 170 | /** 171 | * Blend two colors with the "difference" blend mode 172 | * 173 | * @param backdrop The background color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 174 | * @param source The foreground color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 175 | * @return The blended color 176 | */ 177 | export function difference(backdrop: RGBA, source: RGBA) { 178 | return performBlend( 179 | backdrop, 180 | source, 181 | separableBlend, 182 | separableBlendModes.difference 183 | ) 184 | } 185 | 186 | /** 187 | * Blend two colors with the "exclusion" blend mode 188 | * 189 | * @param backdrop The background color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 190 | * @param source The foreground color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 191 | * @return The blended color 192 | */ 193 | export function exclusion(backdrop: RGBA, source: RGBA) { 194 | return performBlend( 195 | backdrop, 196 | source, 197 | separableBlend, 198 | separableBlendModes.exclusion 199 | ) 200 | } 201 | 202 | /** 203 | * Blend two colors with the "hue" blend mode 204 | * 205 | * @param backdrop The background color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 206 | * @param source The foreground color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 207 | * @return The blended color 208 | */ 209 | export function hue(backdrop: RGBA, source: RGBA) { 210 | return performBlend( 211 | backdrop, 212 | source, 213 | nonSeparableBlend, 214 | nonSeparableBlendModes.hue 215 | ) 216 | } 217 | 218 | /** 219 | * Blend two colors with the "saturation" blend mode 220 | * 221 | * @param backdrop The background color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 222 | * @param source The foreground color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 223 | * @return The blended color 224 | */ 225 | export function saturation(backdrop: RGBA, source: RGBA) { 226 | return performBlend( 227 | backdrop, 228 | source, 229 | nonSeparableBlend, 230 | nonSeparableBlendModes.saturation 231 | ) 232 | } 233 | 234 | /** 235 | * Blend two colors with the "color" blend mode 236 | * 237 | * @param backdrop The background color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 238 | * @param source The foreground color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 239 | * @return The blended color 240 | */ 241 | export function color(backdrop: RGBA, source: RGBA) { 242 | return performBlend( 243 | backdrop, 244 | source, 245 | nonSeparableBlend, 246 | nonSeparableBlendModes.color 247 | ) 248 | } 249 | 250 | /** 251 | * Blend two colors with the "luminosity" blend mode 252 | * 253 | * @param backdrop The background color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 254 | * @param source The foreground color object { r,g,b,a } with the color channels being integers in the [0..255] range and the alpha channel being a fraction in [0..1] 255 | * @return The blended color 256 | */ 257 | export function luminosity(backdrop: RGBA, source: RGBA) { 258 | return performBlend( 259 | backdrop, 260 | source, 261 | nonSeparableBlend, 262 | nonSeparableBlendModes.luminosity 263 | ) 264 | } 265 | -------------------------------------------------------------------------------- /src/non-separable-blend.ts: -------------------------------------------------------------------------------- 1 | import { convertFromUnit, convertToUnit } from './helpers' 2 | import { NoAlphaBlender, RGBA } from './types' 3 | 4 | /** 5 | * Blend two colors in a non-separable way 6 | * 7 | * @param backdrop The background color as an { r,g,b,a } object 8 | * @param source The foreground color as an { r,g,b,a } object 9 | * @param callback The blend mode callback to apply 10 | */ 11 | export default function nonSeparableBlend( 12 | backdrop: RGBA, 13 | source: RGBA, 14 | callback: NoAlphaBlender 15 | ) { 16 | return convertFromUnit( 17 | callback(convertToUnit(backdrop), convertToUnit(source)) 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/non-separable-modes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Algorithms for non-separable blend modes (based on HSV/HSL color space) 3 | * @see https://www.w3.org/TR/compositing-1/#blendingnonseparable 4 | */ 5 | 6 | import { RGB } from './types' 7 | 8 | /** 9 | * Get the luminosity of a color 10 | * 11 | * @param rgb The color as an { r,g,b } object with each channel as a fraction 12 | */ 13 | function getLuminosity(rgb: RGB) { 14 | return 0.3 * rgb.r + 0.59 * rgb.g + 0.11 * rgb.b 15 | } 16 | 17 | /** 18 | * Clip the channels of a color 19 | * 20 | * @param rgb The color as an { r,g,b } object with each channel as a fraction 21 | */ 22 | function clipColor(rgb: RGB): RGB { 23 | const luminosity = getLuminosity(rgb) 24 | let { r, g, b } = rgb 25 | 26 | const lowestChannel = Math.min(r, g, b) 27 | const highestChannel = Math.max(r, g, b) 28 | 29 | function clipLowest(channel: number) { 30 | return ( 31 | luminosity + 32 | ((channel - luminosity) * luminosity) / (luminosity - lowestChannel) 33 | ) 34 | } 35 | 36 | function clipHighest(channel: number) { 37 | return ( 38 | luminosity + 39 | ((channel - luminosity) * (1 - luminosity)) / 40 | (highestChannel - luminosity) 41 | ) 42 | } 43 | 44 | if (lowestChannel < 0) { 45 | r = clipLowest(r) 46 | g = clipLowest(g) 47 | b = clipLowest(b) 48 | } 49 | 50 | if (highestChannel > 1) { 51 | r = clipHighest(r) 52 | g = clipHighest(g) 53 | b = clipHighest(b) 54 | } 55 | 56 | return { r, g, b } 57 | } 58 | 59 | /** 60 | * Set luminosity on a color 61 | * 62 | * @param rgb The color as an { r,g,b } object with each channel as a fraction 63 | * @param luminosity The luminosity to apply 64 | */ 65 | function setLuminosity(rgb: RGB, luminosity: number) { 66 | const delta = luminosity - getLuminosity(rgb) 67 | 68 | return clipColor({ 69 | r: rgb.r + delta, 70 | g: rgb.g + delta, 71 | b: rgb.b + delta 72 | }) 73 | } 74 | 75 | /** 76 | * Get the saturation of a color 77 | * 78 | * @param rgb The color as an { r,g,b } object with each channel as a fraction 79 | */ 80 | function getSaturation(rgb: RGB) { 81 | return Math.max(rgb.r, rgb.g, rgb.b) - Math.min(rgb.r, rgb.g, rgb.b) 82 | } 83 | 84 | /** 85 | * Set saturation on a color 86 | * 87 | * @param rgb The color as an { r,g,b } object with each channel as a fraction 88 | * @param saturation The saturation to apply 89 | */ 90 | function setSaturation(rgb: RGB, saturation: number) { 91 | const sortedChannels = ['r', 'g', 'b'].sort( 92 | (a, b) => rgb[a as keyof RGB] - rgb[b as keyof RGB] 93 | ) as [keyof RGB, keyof RGB, keyof RGB] 94 | const channelMin = sortedChannels[0] 95 | const channelMid = sortedChannels[1] 96 | const channelMax = sortedChannels[2] 97 | 98 | const result = { 99 | r: rgb.r, 100 | g: rgb.g, 101 | b: rgb.b 102 | } 103 | 104 | if (result[channelMax] > result[channelMin]) { 105 | result[channelMid] = 106 | ((result[channelMid] - result[channelMin]) * saturation) / 107 | (result[channelMax] - result[channelMin]) 108 | result[channelMax] = saturation 109 | } else { 110 | result[channelMid] = result[channelMax] = 0 111 | } 112 | 113 | result[channelMin] = 0 114 | 115 | return result 116 | } 117 | 118 | /** 119 | * Blend two colors with the "hue" blend mode 120 | * 121 | * @param backdrop The background color channel as an { r,g,b } object with each channel represented as a fraction 122 | * @param source The foreground color channel as an { r,g,b } object with each channel represented as a fraction 123 | * @return The blended color 124 | */ 125 | export function hue(backdrop: RGB, source: RGB) { 126 | return setLuminosity( 127 | setSaturation(source, getSaturation(backdrop)), 128 | getLuminosity(backdrop) 129 | ) 130 | } 131 | 132 | /** 133 | * Blend two colors with the "saturation" blend mode 134 | * 135 | * @param backdrop The background color channel as an { r,g,b } object with each channel represented as a fraction 136 | * @param source The foreground color channel as an { r,g,b } object with each channel represented as a fraction 137 | * @return The blended color 138 | */ 139 | export function saturation(backdrop: RGB, source: RGB) { 140 | return setLuminosity( 141 | setSaturation(backdrop, getSaturation(source)), 142 | getLuminosity(backdrop) 143 | ) 144 | } 145 | 146 | /** 147 | * Blend two colors with the "color" blend mode 148 | * 149 | * @param backdrop The background color channel as an { r,g,b } object with each channel represented as a fraction 150 | * @param source The foreground color channel as an { r,g,b } object with each channel represented as a fraction 151 | * @return The blended color 152 | */ 153 | export function color(backdrop: RGB, source: RGB) { 154 | return setLuminosity(source, getLuminosity(backdrop)) 155 | } 156 | 157 | /** 158 | * Blend two colors with the "luminosity" blend mode 159 | * 160 | * @param backdrop The background color channel as an { r,g,b } object with each channel represented as a fraction 161 | * @param source The foreground color channel as an { r,g,b } object with each channel represented as a fraction 162 | * @return The blended color 163 | */ 164 | export function luminosity(backdrop: RGB, source: RGB) { 165 | return setLuminosity(backdrop, getLuminosity(source)) 166 | } 167 | -------------------------------------------------------------------------------- /src/separable-blend.ts: -------------------------------------------------------------------------------- 1 | import { ChannelBlender, RGB, RGBA } from './types' 2 | 3 | /** 4 | * Blend two colors in a separable way (i.e. each color channel individually) 5 | * 6 | * @param backdrop The RGBA backdrop color 7 | * @param source The RGBA source color 8 | * @param callback The blend mode callback to apply 9 | */ 10 | export default function separableBlend( 11 | backdrop: RGBA, 12 | source: RGBA, 13 | callback: ChannelBlender 14 | ): RGB { 15 | return { 16 | r: callback(backdrop.r / 255, source.r / 255) * 255, 17 | g: callback(backdrop.g / 255, source.g / 255) * 255, 18 | b: callback(backdrop.b / 255, source.b / 255) * 255 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/separable-modes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Algorithms for separable blend modes (i.e. where the same algorithms is applied to each color channel) 3 | * @see https://www.w3.org/TR/compositing-1/#blendingseparable 4 | */ 5 | 6 | /** 7 | * Blend two color channels with the "normal" blend mode 8 | * 9 | * @param backdrop The background color channel as an integer from 0 to 255 10 | * @param source The foreground color channel as an integer from 0 to 255 11 | * @return The blended channel value 12 | */ 13 | // @ts-ignore the unused first parameter to comply with the interface 14 | export function normal(backdrop: number, source: number) { 15 | return source 16 | } 17 | 18 | /** 19 | * Blend two color channels with the "multiply" blend mode 20 | * 21 | * @param backdrop The background color channel as an integer from 0 to 255 22 | * @param source The foreground color channel as an integer from 0 to 255 23 | * @return The blended channel value 24 | */ 25 | export function multiply(backdrop: number, source: number) { 26 | return backdrop * source 27 | } 28 | 29 | /** 30 | * Blend two color channels with the "screen" blend mode 31 | * 32 | * @param backdrop The background color channel as an integer from 0 to 255 33 | * @param source The foreground color channel as an integer from 0 to 255 34 | * @return The blended channel value 35 | */ 36 | export function screen(backdrop: number, source: number) { 37 | return backdrop + source - backdrop * source 38 | } 39 | 40 | /** 41 | * Blend two color channels with the "overlay" blend mode 42 | * 43 | * @param backdrop The background color channel as an integer from 0 to 255 44 | * @param source The foreground color channel as an integer from 0 to 255 45 | * @return The blended channel value 46 | */ 47 | export function overlay(backdrop: number, source: number) { 48 | return hardLight(source, backdrop) 49 | } 50 | 51 | /** 52 | * Blend two color channels with the "darken" blend mode 53 | * 54 | * @param backdrop The background color channel as an integer from 0 to 255 55 | * @param source The foreground color channel as an integer from 0 to 255 56 | * @return The blended channel value 57 | */ 58 | export function darken(backdrop: number, source: number) { 59 | return Math.min(backdrop, source) 60 | } 61 | 62 | /** 63 | * Blend two color channels with the "lighten" blend mode 64 | * 65 | * @param backdrop The background color channel as an integer from 0 to 255 66 | * @param source The foreground color channel as an integer from 0 to 255 67 | * @return The blended channel value 68 | */ 69 | export function lighten(backdrop: number, source: number) { 70 | return Math.min(Math.max(backdrop, source), 1) 71 | } 72 | 73 | /** 74 | * Blend two color channels with the "color dodge" blend mode 75 | * 76 | * @param backdrop The background color channel as an integer from 0 to 255 77 | * @param source The foreground color channel as an integer from 0 to 255 78 | * @return The blended channel value 79 | */ 80 | export function colorDodge(backdrop: number, source: number) { 81 | return backdrop === 0 82 | ? 0 83 | : source === 1 84 | ? 1 85 | : Math.min(1, backdrop / (1 - source)) 86 | } 87 | 88 | /** 89 | * Blend two color channels with the "color burn" blend mode 90 | * 91 | * @param backdrop The background color channel as an integer from 0 to 255 92 | * @param source The foreground color channel as an integer from 0 to 255 93 | * @return The blended channel value 94 | */ 95 | export function colorBurn(backdrop: number, source: number) { 96 | return backdrop === 1 97 | ? 1 98 | : source === 0 99 | ? 0 100 | : 1 - Math.min(1, (1 - backdrop) / source) 101 | } 102 | 103 | /** 104 | * Blend two color channels with the "hard light" blend mode 105 | * 106 | * @param backdrop The background color channel as an integer from 0 to 255 107 | * @param source The foreground color channel as an integer from 0 to 255 108 | * @return The blended channel value 109 | */ 110 | export function hardLight(backdrop: number, source: number) { 111 | return source <= 0.5 112 | ? multiply(backdrop, 2 * source) 113 | : screen(backdrop, 2 * source - 1) 114 | } 115 | 116 | /** 117 | * Blend two color channels with the "soft light" blend mode 118 | * 119 | * @param backdrop The background color channel as an integer from 0 to 255 120 | * @param source The foreground color channel as an integer from 0 to 255 121 | * @return The blended channel value 122 | */ 123 | export function softLight(backdrop: number, source: number) { 124 | return source <= 0.5 125 | ? backdrop - (1 - 2 * source) * backdrop * (1 - backdrop) 126 | : backdrop + 127 | (2 * source - 1) * 128 | ((backdrop <= 0.25 129 | ? ((16 * backdrop - 12) * backdrop + 4) * backdrop 130 | : Math.sqrt(backdrop)) - 131 | backdrop) 132 | } 133 | 134 | /** 135 | * Blend two color channels with the "difference" blend mode 136 | * 137 | * @param backdrop The background color channel as an integer from 0 to 255 138 | * @param source The foreground color channel as an integer from 0 to 255 139 | * @return The blended channel value 140 | */ 141 | export function difference(backdrop: number, source: number) { 142 | return Math.abs(backdrop - source) 143 | } 144 | 145 | /** 146 | * Blend two color channels with the "exclusion" blend mode 147 | * 148 | * @param backdrop The background color channel as an integer from 0 to 255 149 | * @param source The foreground color channel as an integer from 0 to 255 150 | * @return The blended channel value 151 | */ 152 | export function exclusion(backdrop: number, source: number) { 153 | return backdrop + source - 2 * backdrop * source 154 | } 155 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An object representing an RGB color 3 | */ 4 | export interface RGB { 5 | /** 6 | * Red channel as an integer from 0 to 255 7 | */ 8 | r: number 9 | 10 | /** 11 | * Green channel as an integer from 0 to 255 12 | */ 13 | g: number 14 | 15 | /** 16 | * Blue channel as an integer from 0 to 255 17 | */ 18 | b: number 19 | } 20 | 21 | /** 22 | * An object representing an RGB color with an additional alpha channel 23 | */ 24 | export interface RGBA extends RGB { 25 | /** 26 | * Alpha channel as a fraction from 0 to 1 27 | */ 28 | a: number 29 | } 30 | 31 | /** 32 | * Blends a single color channel of two colors 33 | */ 34 | export type ChannelBlender = ( 35 | backdropChannel: number, 36 | sourceChannel: number 37 | ) => number 38 | 39 | /** 40 | * Blends two RGBA colors into RGBA 41 | */ 42 | export type AlphaBlender = (backdrop: RGBA, source: RGBA) => RGBA 43 | 44 | /** 45 | * Blends two RGB(A) colors into RGB 46 | */ 47 | export type NoAlphaBlender = (backdrop: RGB, source: RGB) => RGB 48 | -------------------------------------------------------------------------------- /src/unit.ts: -------------------------------------------------------------------------------- 1 | import separableBlend from './separable-blend' 2 | import * as separableBlendModes from './separable-modes' 3 | 4 | import nonSeparableBlend from './non-separable-blend' 5 | import * as nonSeparableBlendModes from './non-separable-modes' 6 | 7 | import { performBlend } from './helpers' 8 | import { RGBA } from './types' 9 | 10 | /** 11 | * Blend two colors with the "normal" blend mode 12 | * 13 | * @param backdrop The background color object { r,g,b,a } with all channels in the [0..1] range 14 | * @param source The foreground color object { r,g,b,a } with all channels in the [0..1] range 15 | * @return The blended color 16 | */ 17 | export function normal(backdrop: RGBA, source: RGBA) { 18 | return performBlend( 19 | backdrop, 20 | source, 21 | separableBlend, 22 | separableBlendModes.normal, 23 | { unitInput: true, unitOutput: true } 24 | ) 25 | } 26 | 27 | /** 28 | * Blend two colors with the "multiply" blend mode 29 | * 30 | * @param backdrop The background color object { r,g,b,a } with all channels in the [0..1] range 31 | * @param source The foreground color object { r,g,b,a } with all channels in the [0..1] range 32 | * @return The blended color 33 | */ 34 | export function multiply(backdrop: RGBA, source: RGBA) { 35 | return performBlend( 36 | backdrop, 37 | source, 38 | separableBlend, 39 | separableBlendModes.multiply, 40 | { unitInput: true, unitOutput: true } 41 | ) 42 | } 43 | 44 | /** 45 | * Blend two colors with the "screen" blend mode 46 | * 47 | * @param backdrop The background color object { r,g,b,a } with all channels in the [0..1] range 48 | * @param source The foreground color object { r,g,b,a } with all channels in the [0..1] range 49 | * @return The blended color 50 | */ 51 | export function screen(backdrop: RGBA, source: RGBA) { 52 | return performBlend( 53 | backdrop, 54 | source, 55 | separableBlend, 56 | separableBlendModes.screen, 57 | { unitInput: true, unitOutput: true } 58 | ) 59 | } 60 | 61 | /** 62 | * Blend two colors with the "overlay" blend mode 63 | * 64 | * @param backdrop The background color object { r,g,b,a } with all channels in the [0..1] range 65 | * @param source The foreground color object { r,g,b,a } with all channels in the [0..1] range 66 | * @return The blended color 67 | */ 68 | export function overlay(backdrop: RGBA, source: RGBA) { 69 | return performBlend( 70 | backdrop, 71 | source, 72 | separableBlend, 73 | separableBlendModes.overlay, 74 | { unitInput: true, unitOutput: true } 75 | ) 76 | } 77 | 78 | /** 79 | * Blend two colors with the "darken" blend mode 80 | * 81 | * @param backdrop The background color object { r,g,b,a } with all channels in the [0..1] range 82 | * @param source The foreground color object { r,g,b,a } with all channels in the [0..1] range 83 | * @return The blended color 84 | */ 85 | export function darken(backdrop: RGBA, source: RGBA) { 86 | return performBlend( 87 | backdrop, 88 | source, 89 | separableBlend, 90 | separableBlendModes.darken, 91 | { unitInput: true, unitOutput: true } 92 | ) 93 | } 94 | 95 | /** 96 | * Blend two colors with the "lighten" blend mode 97 | * 98 | * @param backdrop The background color object { r,g,b,a } with all channels in the [0..1] range 99 | * @param source The foreground color object { r,g,b,a } with all channels in the [0..1] range 100 | * @return The blended color 101 | */ 102 | export function lighten(backdrop: RGBA, source: RGBA) { 103 | return performBlend( 104 | backdrop, 105 | source, 106 | separableBlend, 107 | separableBlendModes.lighten, 108 | { unitInput: true, unitOutput: true } 109 | ) 110 | } 111 | 112 | /** 113 | * Blend two colors with the "color dodge" blend mode 114 | * 115 | * @param backdrop The background color object { r,g,b,a } with all channels in the [0..1] range 116 | * @param source The foreground color object { r,g,b,a } with all channels in the [0..1] range 117 | * @return The blended color 118 | */ 119 | export function colorDodge(backdrop: RGBA, source: RGBA) { 120 | return performBlend( 121 | backdrop, 122 | source, 123 | separableBlend, 124 | separableBlendModes.colorDodge, 125 | { unitInput: true, unitOutput: true } 126 | ) 127 | } 128 | 129 | /** 130 | * Blend two colors with the "color burn" blend mode 131 | * 132 | * @param backdrop The background color object { r,g,b,a } with all channels in the [0..1] range 133 | * @param source The foreground color object { r,g,b,a } with all channels in the [0..1] range 134 | * @return The blended color 135 | */ 136 | export function colorBurn(backdrop: RGBA, source: RGBA) { 137 | return performBlend( 138 | backdrop, 139 | source, 140 | separableBlend, 141 | separableBlendModes.colorBurn, 142 | { unitInput: true, unitOutput: true } 143 | ) 144 | } 145 | 146 | /** 147 | * Blend two colors with the "hard light" blend mode 148 | * 149 | * @param backdrop The background color object { r,g,b,a } with all channels in the [0..1] range 150 | * @param source The foreground color object { r,g,b,a } with all channels in the [0..1] range 151 | * @return The blended color 152 | */ 153 | export function hardLight(backdrop: RGBA, source: RGBA) { 154 | return performBlend( 155 | backdrop, 156 | source, 157 | separableBlend, 158 | separableBlendModes.hardLight, 159 | { unitInput: true, unitOutput: true } 160 | ) 161 | } 162 | 163 | /** 164 | * Blend two colors with the "soft light" blend mode 165 | * 166 | * @param backdrop The background color object { r,g,b,a } with all channels in the [0..1] range 167 | * @param source The foreground color object { r,g,b,a } with all channels in the [0..1] range 168 | * @return The blended color 169 | */ 170 | export function softLight(backdrop: RGBA, source: RGBA) { 171 | return performBlend( 172 | backdrop, 173 | source, 174 | separableBlend, 175 | separableBlendModes.softLight, 176 | { unitInput: true, unitOutput: true } 177 | ) 178 | } 179 | 180 | /** 181 | * Blend two colors with the "difference" blend mode 182 | * 183 | * @param backdrop The background color object { r,g,b,a } with all channels in the [0..1] range 184 | * @param source The foreground color object { r,g,b,a } with all channels in the [0..1] range 185 | * @return The blended color 186 | */ 187 | export function difference(backdrop: RGBA, source: RGBA) { 188 | return performBlend( 189 | backdrop, 190 | source, 191 | separableBlend, 192 | separableBlendModes.difference, 193 | { unitInput: true, unitOutput: true } 194 | ) 195 | } 196 | 197 | /** 198 | * Blend two colors with the "exclusion" blend mode 199 | * 200 | * @param backdrop The background color object { r,g,b,a } with all channels in the [0..1] range 201 | * @param source The foreground color object { r,g,b,a } with all channels in the [0..1] range 202 | * @return The blended color 203 | */ 204 | export function exclusion(backdrop: RGBA, source: RGBA) { 205 | return performBlend( 206 | backdrop, 207 | source, 208 | separableBlend, 209 | separableBlendModes.exclusion, 210 | { unitInput: true, unitOutput: true } 211 | ) 212 | } 213 | 214 | /** 215 | * Blend two colors with the "hue" blend mode 216 | * 217 | * @param backdrop The background color object { r,g,b,a } with all channels in the [0..1] range 218 | * @param source The foreground color object { r,g,b,a } with all channels in the [0..1] range 219 | * @return The blended color 220 | */ 221 | export function hue(backdrop: RGBA, source: RGBA) { 222 | return performBlend( 223 | backdrop, 224 | source, 225 | nonSeparableBlend, 226 | nonSeparableBlendModes.hue, 227 | { unitInput: true, unitOutput: true } 228 | ) 229 | } 230 | 231 | /** 232 | * Blend two colors with the "saturation" blend mode 233 | * 234 | * @param backdrop The background color object { r,g,b,a } with all channels in the [0..1] range 235 | * @param source The foreground color object { r,g,b,a } with all channels in the [0..1] range 236 | * @return The blended color 237 | */ 238 | export function saturation(backdrop: RGBA, source: RGBA) { 239 | return performBlend( 240 | backdrop, 241 | source, 242 | nonSeparableBlend, 243 | nonSeparableBlendModes.saturation, 244 | { unitInput: true, unitOutput: true } 245 | ) 246 | } 247 | 248 | /** 249 | * Blend two colors with the "color" blend mode 250 | * 251 | * @param backdrop The background color object { r,g,b,a } with all channels in the [0..1] range 252 | * @param source The foreground color object { r,g,b,a } with all channels in the [0..1] range 253 | * @return The blended color 254 | */ 255 | export function color(backdrop: RGBA, source: RGBA) { 256 | return performBlend( 257 | backdrop, 258 | source, 259 | nonSeparableBlend, 260 | nonSeparableBlendModes.color, 261 | { unitInput: true, unitOutput: true } 262 | ) 263 | } 264 | 265 | /** 266 | * Blend two colors with the "luminosity" blend mode 267 | * 268 | * @param backdrop The background color object { r,g,b,a } with all channels in the [0..1] range 269 | * @param source The foreground color object { r,g,b,a } with all channels in the [0..1] range 270 | * @return The blended color 271 | */ 272 | export function luminosity(backdrop: RGBA, source: RGBA) { 273 | return performBlend( 274 | backdrop, 275 | source, 276 | nonSeparableBlend, 277 | nonSeparableBlendModes.luminosity, 278 | { unitInput: true, unitOutput: true } 279 | ) 280 | } 281 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | describe('Basics: blend { r: 250, g: 200, b: 0, a: 0.6 } with { r: 50, g: 150, b: 75, a: 0.4 }', () => { 2 | const blender = require('../dist') 3 | 4 | test('normal should return { r: 145, g: 174, b: 39, a: 0.76 }', () => { 5 | expect( 6 | blender.normal( 7 | { r: 250, g: 200, b: 0, a: 0.6 }, 8 | { r: 50, g: 150, b: 75, a: 0.4 } 9 | ) 10 | ).toStrictEqual({ r: 145, g: 174, b: 39, a: 0.76 }) 11 | }) 12 | 13 | test('multiply should return { r: 144, g: 164, b: 16, a: 0.76 }', () => { 14 | expect( 15 | blender.multiply( 16 | { r: 250, g: 200, b: 0, a: 0.6 }, 17 | { r: 50, g: 150, b: 75, a: 0.4 } 18 | ) 19 | ).toStrictEqual({ r: 144, g: 164, b: 16, a: 0.76 }) 20 | }) 21 | 22 | test('screen should return { r: 208, g: 199, b: 39, a: 0.76 }', () => { 23 | expect( 24 | blender.screen( 25 | { r: 250, g: 200, b: 0, a: 0.6 }, 26 | { r: 50, g: 150, b: 75, a: 0.4 } 27 | ) 28 | ).toStrictEqual({ r: 208, g: 199, b: 39, a: 0.76 }) 29 | }) 30 | 31 | test('overlay should return { r: 207, g: 193, b: 16, a: 0.76 }', () => { 32 | expect( 33 | blender.overlay( 34 | { r: 250, g: 200, b: 0, a: 0.6 }, 35 | { r: 50, g: 150, b: 75, a: 0.4 } 36 | ) 37 | ).toStrictEqual({ r: 207, g: 193, b: 16, a: 0.76 }) 38 | }) 39 | 40 | test('darken should return { r: 145, g: 174, b: 16, a: 0.76 }', () => { 41 | expect( 42 | blender.darken( 43 | { r: 250, g: 200, b: 0, a: 0.6 }, 44 | { r: 50, g: 150, b: 75, a: 0.4 } 45 | ) 46 | ).toStrictEqual({ r: 145, g: 174, b: 16, a: 0.76 }) 47 | }) 48 | 49 | test('lighten should return { r: 208, g: 189, b: 39, a: 0.76 }', () => { 50 | expect( 51 | blender.lighten( 52 | { r: 250, g: 200, b: 0, a: 0.6 }, 53 | { r: 50, g: 150, b: 75, a: 0.4 } 54 | ) 55 | ).toStrictEqual({ r: 208, g: 189, b: 39, a: 0.76 }) 56 | }) 57 | 58 | test('colorDodge should return { r: 209, g: 207, b: 16, a: 0.76 }', () => { 59 | expect( 60 | blender.colorDodge( 61 | { r: 250, g: 200, b: 0, a: 0.6 }, 62 | { r: 50, g: 150, b: 75, a: 0.4 } 63 | ) 64 | ).toStrictEqual({ r: 209, g: 207, b: 16, a: 0.76 }) 65 | }) 66 | 67 | test('colorBurn should return { r: 202, g: 177, b: 16, a: 0.76 }', () => { 68 | expect( 69 | blender.colorBurn( 70 | { r: 250, g: 200, b: 0, a: 0.6 }, 71 | { r: 50, g: 150, b: 75, a: 0.4 } 72 | ) 73 | ).toStrictEqual({ r: 202, g: 177, b: 16, a: 0.76 }) 74 | }) 75 | 76 | test('hardLight should return { r: 160, g: 193, b: 16, a: 0.76 }', () => { 77 | expect( 78 | blender.hardLight( 79 | { r: 250, g: 200, b: 0, a: 0.6 }, 80 | { r: 50, g: 150, b: 75, a: 0.4 } 81 | ) 82 | ).toStrictEqual({ r: 160, g: 193, b: 16, a: 0.76 }) 83 | }) 84 | 85 | test('softLight should return { r: 207, g: 191, b: 16, a: 0.76 }', () => { 86 | expect( 87 | blender.softLight( 88 | { r: 250, g: 200, b: 0, a: 0.6 }, 89 | { r: 50, g: 150, b: 75, a: 0.4 } 90 | ) 91 | ).toStrictEqual({ r: 207, g: 191, b: 16, a: 0.76 }) 92 | }) 93 | 94 | test('difference should return { r: 192, g: 142, b: 39, a: 0.76 }', () => { 95 | expect( 96 | blender.difference( 97 | { r: 250, g: 200, b: 0, a: 0.6 }, 98 | { r: 50, g: 150, b: 75, a: 0.4 } 99 | ) 100 | ).toStrictEqual({ r: 192, g: 142, b: 39, a: 0.76 }) 101 | }) 102 | 103 | test('exclusion should return { r: 193, g: 163, b: 39, a: 0.76 }', () => { 104 | expect( 105 | blender.exclusion( 106 | { r: 250, g: 200, b: 0, a: 0.6 }, 107 | { r: 50, g: 150, b: 75, a: 0.4 } 108 | ) 109 | ).toStrictEqual({ r: 193, g: 163, b: 39, a: 0.76 }) 110 | }) 111 | 112 | test('hue should return { r: 162, g: 207, b: 49, a: 0.76 }', () => { 113 | expect( 114 | blender.hue( 115 | { r: 250, g: 200, b: 0, a: 0.6 }, 116 | { r: 50, g: 150, b: 75, a: 0.4 } 117 | ) 118 | ).toStrictEqual({ r: 158, g: 207, b: 58, a: 0.76 }) 119 | }) 120 | 121 | test('saturation should return { r: 209, g: 182, b: 54, a: 0.76 }', () => { 122 | expect( 123 | blender.saturation( 124 | { r: 250, g: 200, b: 0, a: 0.6 }, 125 | { r: 50, g: 150, b: 75, a: 0.4 } 126 | ) 127 | ).toStrictEqual({ r: 197, g: 188, b: 52, a: 0.76 }) 128 | }) 129 | 130 | test('color should return { r: 171, g: 199, b: 65, a: 0.76 }', () => { 131 | expect( 132 | blender.color( 133 | { r: 250, g: 200, b: 0, a: 0.6 }, 134 | { r: 50, g: 150, b: 75, a: 0.4 } 135 | ) 136 | ).toStrictEqual({ r: 171, g: 199, b: 65, a: 0.76 }) 137 | }) 138 | 139 | test('luminosity should return { r: 175, g: 163, b: 16, a: 0.76 }', () => { 140 | expect( 141 | blender.luminosity( 142 | { r: 250, g: 200, b: 0, a: 0.6 }, 143 | { r: 50, g: 150, b: 75, a: 0.4 } 144 | ) 145 | ).toStrictEqual({ r: 175, g: 163, b: 16, a: 0.76 }) 146 | }) 147 | }) 148 | 149 | function approximateChannels(color) { 150 | return { 151 | r: Math.round(color.r * 1000) / 1000, 152 | g: Math.round(color.g * 1000) / 1000, 153 | b: Math.round(color.b * 1000) / 1000, 154 | a: color.a 155 | } 156 | } 157 | 158 | describe('Test basic unit functionality', () => { 159 | const blender = require('../unit') 160 | 161 | test('blend { r: 0, g: 0, b: 0, a: 0 } and { r: 0, g: 0, b: 0, a: 0 } should return { r: 0, g: 0, b: 0, a: 0 }', () => { 162 | expect( 163 | blender.normal( 164 | { r: 0, g: 0, b: 0, a: 0 }, 165 | { r: 0, g: 0, b: 0, a: 0 } 166 | ) 167 | ).toStrictEqual({ r: 0, g: 0, b: 0, a: 0 }) 168 | }) 169 | 170 | test('blend { r: 1, g: 0, b: 0, a: 0.5 } and { r: 0, g: 1, b: 0, a: 0.5 } should return { r: 1/3, g: 2/3, b: 0, a: 0.75 }', () => { 171 | expect( 172 | approximateChannels( 173 | blender.normal( 174 | { r: 1, g: 0, b: 0, a: 0.5 }, 175 | { r: 0, g: 1, b: 0, a: 0.5 } 176 | ) 177 | ) 178 | ).toStrictEqual(approximateChannels({ r: 1 / 3, g: 2 / 3, b: 0, a: 0.75 })) 179 | }) 180 | }) 181 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2015", 5 | "declaration": true, 6 | "declarationMap": true, 7 | 8 | /* Strict Type-Checking Options */ 9 | "strict": true, 10 | "alwaysStrict": true, 11 | 12 | /* Additional Checks */ 13 | "noUnusedLocals": true, /* Report errors on unused locals. */ 14 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 15 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 16 | 17 | /* Module Resolution Options */ 18 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 19 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 20 | 21 | "outDir": "tmp" 22 | }, 23 | 24 | "files": [ 25 | "src/index.ts", 26 | "src/types.ts", 27 | "src/unit.ts", 28 | ] 29 | } --------------------------------------------------------------------------------