├── .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 | 
6 |
7 |
8 |
9 |
10 | # color-blend
11 |
12 | [](https://github.com/loilo/color-blend/actions)
13 | [](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 | 
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 | }
--------------------------------------------------------------------------------