From 568aa759c93e86ad3431ddced0fabd063c453358 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Wed, 25 Jan 2023 14:01:17 -0500 Subject: [PATCH] Converts to use colorjs.io for color manipulation --- package.json | 3 +- src/annotations/annotations.ts | 13 +-- src/annotations/blameAnnotationProvider.ts | 4 +- src/annotations/gutterBlameAnnotationProvider.ts | 2 +- .../gutterHeatmapBlameAnnotationProvider.ts | 2 +- src/system/color.ts | 98 ++++++++++++++++++++++ src/webviews/apps/plus/graph/graph.tsx | 2 +- src/webviews/apps/shared/colors.ts | 80 ------------------ src/webviews/apps/shared/theme.ts | 2 +- yarn.lock | 15 ++-- 10 files changed, 118 insertions(+), 103 deletions(-) create mode 100644 src/system/color.ts delete mode 100644 src/webviews/apps/shared/colors.ts diff --git a/package.json b/package.json index 2c6dbb2..357db2a 100644 --- a/package.json +++ b/package.json @@ -12742,7 +12742,7 @@ "@vscode/webview-ui-toolkit": "1.2.1", "ansi-regex": "6.0.1", "billboard.js": "3.7.2", - "chroma-js": "2.4.2", + "colorjs.io": "0.4.2", "https-proxy-agent": "5.0.1", "iconv-lite": "0.6.3", "lit": "2.3.1", @@ -12756,7 +12756,6 @@ "sortablejs": "1.15.0" }, "devDependencies": { - "@types/chroma-js": "2.1.4", "@types/glob": "8.0.1", "@types/lodash-es": "4.17.6", "@types/mocha": "10.0.1", diff --git a/src/annotations/annotations.ts b/src/annotations/annotations.ts index 8a11936..fc0bcf4 100644 --- a/src/annotations/annotations.ts +++ b/src/annotations/annotations.ts @@ -14,8 +14,8 @@ import { Colors, GlyphChars } from '../constants'; import type { CommitFormatOptions } from '../git/formatters/commitFormatter'; import { CommitFormatter } from '../git/formatters/commitFormatter'; import type { GitCommit } from '../git/models/commit'; +import { steps, toRgba } from '../system/color'; import { getWidth, interpolate, pad } from '../system/string'; -import { toRgba } from '../webviews/apps/shared/colors'; export interface ComputedHeatmap { coldThresholdTimestamp: number; @@ -58,7 +58,7 @@ const defaultHeatmapColors = [ ]; let heatmapColors: { hot: string[]; cold: string[] } | undefined; -export async function getHeatmapColors() { +export function getHeatmapColors() { if (heatmapColors == null) { const { coldColor, hotColor } = configuration.get('heatmap'); @@ -66,10 +66,13 @@ export async function getHeatmapColors() { if (coldColor === defaultHeatmapColdColor && hotColor === defaultHeatmapHotColor) { colors = defaultHeatmapColors; } else { - const chroma = (await import(/* webpackChunkName: "heatmap-chroma" */ 'chroma-js')).default; - colors = chroma.scale([hotColor, coldColor]).mode('lrgb').classes(20).colors(20); + colors = steps(hotColor, coldColor, { + space: 'xyz', + outputSpace: 'srgb', + steps: 20, + maxSteps: 20, + }).map(c => c.toString({ format: 'hex' })); } - heatmapColors = { hot: colors.slice(0, 10), cold: colors.slice(10, 20), diff --git a/src/annotations/blameAnnotationProvider.ts b/src/annotations/blameAnnotationProvider.ts index 9f64bf0..09678f4 100644 --- a/src/annotations/blameAnnotationProvider.ts +++ b/src/annotations/blameAnnotationProvider.ts @@ -55,7 +55,7 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase } @log({ args: false }) - protected async getComputedHeatmap(blame: GitBlame): Promise { + protected getComputedHeatmap(blame: GitBlame): ComputedHeatmap { const dates: Date[] = []; let commit; @@ -124,7 +124,7 @@ export abstract class BlameAnnotationProviderBase extends AnnotationProviderBase return { coldThresholdTimestamp: coldThresholdTimestamp, - colors: await getHeatmapColors(), + colors: getHeatmapColors(), computeRelativeAge: (date: Date) => computeRelativeAge(date, getLookupTable(date)), computeOpacity: (date: Date) => { const lookup = getLookupTable(date, true); diff --git a/src/annotations/gutterBlameAnnotationProvider.ts b/src/annotations/gutterBlameAnnotationProvider.ts index 0fd7b1e..2aa7c85 100644 --- a/src/annotations/gutterBlameAnnotationProvider.ts +++ b/src/annotations/gutterBlameAnnotationProvider.ts @@ -86,7 +86,7 @@ export class GutterBlameAnnotationProvider extends BlameAnnotationProviderBase { let computedHeatmap; if (cfg.heatmap.enabled) { - computedHeatmap = await this.getComputedHeatmap(blame); + computedHeatmap = this.getComputedHeatmap(blame); } for (const l of blame.lines) { diff --git a/src/annotations/gutterHeatmapBlameAnnotationProvider.ts b/src/annotations/gutterHeatmapBlameAnnotationProvider.ts index c778b12..1be8007 100644 --- a/src/annotations/gutterHeatmapBlameAnnotationProvider.ts +++ b/src/annotations/gutterHeatmapBlameAnnotationProvider.ts @@ -31,7 +31,7 @@ export class GutterHeatmapBlameAnnotationProvider extends BlameAnnotationProvide string, { decorationType: TextEditorDecorationType; rangesOrOptions: Range[] } >(); - const computedHeatmap = await this.getComputedHeatmap(blame); + const computedHeatmap = this.getComputedHeatmap(blame); let commit: GitCommit | undefined; for (const l of blame.lines) { diff --git a/src/system/color.ts b/src/system/color.ts new file mode 100644 index 0000000..b999958 --- /dev/null +++ b/src/system/color.ts @@ -0,0 +1,98 @@ +import type Color from 'colorjs.io'; +import { steps as _steps } from 'colorjs.io/fn'; +import type { ColorTypes } from 'colorjs.io/types/src/color'; +import type { Methods } from 'colorjs.io/types/src/index-fn'; +import type { RangeOptions } from 'colorjs.io/types/src/interpolation'; + +const cssColorRegex = + /^(?:(#?)([0-9a-f]{3}|[0-9a-f]{6})|((?:rgb|hsl)a?)\((-?\d+%?)[,\s]+(-?\d+%?)[,\s]+(-?\d+%?)[,\s]*(-?[\d.]+%?)?\))$/i; + +function adjustLight(color: number, amount: number) { + const cc = color + amount; + const c = amount < 0 ? (cc < 0 ? 0 : cc) : cc > 255 ? 255 : cc; + + return Math.round(c); +} + +export function darken(color: string, percentage: number) { + return lighten(color, -percentage); +} + +export function lighten(color: string, percentage: number) { + const rgba = toRgba(color); + if (rgba == null) return color; + + const [r, g, b, a] = rgba; + const amount = (255 * percentage) / 100; + return `rgba(${adjustLight(r, amount)}, ${adjustLight(g, amount)}, ${adjustLight(b, amount)}, ${a})`; +} + +export function opacity(color: string, percentage: number) { + const rgba = toRgba(color); + if (rgba == null) return color; + + const [r, g, b, a] = rgba; + return `rgba(${r}, ${g}, ${b}, ${a * (percentage / 100)})`; +} + +export function mix(color1: string, color2: string, percentage: number) { + const rgba1 = toRgba(color1); + const rgba2 = toRgba(color2); + if (rgba1 == null || rgba2 == null) return color1; + const [r1, g1, b1, a1] = rgba1; + const [r2, g2, b2, a2] = rgba2; + return `rgba(${mixChannel(r1, r2, percentage)}, ${mixChannel(g1, g2, percentage)}, ${mixChannel( + b1, + b2, + percentage, + )}, ${mixChannel(a1, a2, percentage)})`; +} + +const mixChannel = (channel1: number, channel2: number, percentage: number) => { + return channel1 + ((channel2 - channel1) * percentage) / 100; +}; + +interface StepsOptions extends RangeOptions { + maxDeltaE?: number | undefined; + deltaEMethod?: Methods | undefined; + steps?: number | undefined; + maxSteps?: number | undefined; +} + +export function steps(color1: ColorTypes, color2: ColorTypes, options?: StepsOptions): Color[] { + type Steps = (color1: ColorTypes, color2: ColorTypes, options?: StepsOptions) => Color[]; + return (_steps as Steps)(color1, color2, options); +} + +export function toRgba(color: string) { + color = color.trim(); + + const result = cssColorRegex.exec(color); + if (result == null) return null; + + if (result[1] === '#') { + const hex = result[2]; + switch (hex.length) { + case 3: + return [parseInt(hex[0] + hex[0], 16), parseInt(hex[1] + hex[1], 16), parseInt(hex[2] + hex[2], 16), 1]; + case 6: + return [ + parseInt(hex.substring(0, 2), 16), + parseInt(hex.substring(2, 4), 16), + parseInt(hex.substring(4, 6), 16), + 1, + ]; + } + + return null; + } + + switch (result[3]) { + case 'rgb': + return [parseInt(result[4], 10), parseInt(result[5], 10), parseInt(result[6], 10), 1]; + case 'rgba': + return [parseInt(result[4], 10), parseInt(result[5], 10), parseInt(result[6], 10), parseFloat(result[7])]; + default: + return null; + } +} diff --git a/src/webviews/apps/plus/graph/graph.tsx b/src/webviews/apps/plus/graph/graph.tsx index a900b65..d18a70d 100644 --- a/src/webviews/apps/plus/graph/graph.tsx +++ b/src/webviews/apps/plus/graph/graph.tsx @@ -46,11 +46,11 @@ import { UpdateRefsVisibilityCommandType, UpdateSelectionCommandType, } from '../../../../plus/webviews/graph/protocol'; +import { mix, opacity } from '../../../../system/color'; import { debounce } from '../../../../system/function'; import type { IpcMessage, IpcNotificationType } from '../../../protocol'; import { onIpc } from '../../../protocol'; import { App } from '../../shared/appBase'; -import { mix, opacity } from '../../shared/colors'; import { GraphWrapper } from './GraphWrapper'; import './graph.scss'; diff --git a/src/webviews/apps/shared/colors.ts b/src/webviews/apps/shared/colors.ts deleted file mode 100644 index 554f0e5..0000000 --- a/src/webviews/apps/shared/colors.ts +++ /dev/null @@ -1,80 +0,0 @@ -const cssColorRegex = - /^(?:(#?)([0-9a-f]{3}|[0-9a-f]{6})|((?:rgb|hsl)a?)\((-?\d+%?)[,\s]+(-?\d+%?)[,\s]+(-?\d+%?)[,\s]*(-?[\d.]+%?)?\))$/i; - -function adjustLight(color: number, amount: number) { - const cc = color + amount; - const c = amount < 0 ? (cc < 0 ? 0 : cc) : cc > 255 ? 255 : cc; - - return Math.round(c); -} - -export function darken(color: string, percentage: number) { - return lighten(color, -percentage); -} - -export function lighten(color: string, percentage: number) { - const rgba = toRgba(color); - if (rgba == null) return color; - - const [r, g, b, a] = rgba; - const amount = (255 * percentage) / 100; - return `rgba(${adjustLight(r, amount)}, ${adjustLight(g, amount)}, ${adjustLight(b, amount)}, ${a})`; -} - -export function opacity(color: string, percentage: number) { - const rgba = toRgba(color); - if (rgba == null) return color; - - const [r, g, b, a] = rgba; - return `rgba(${r}, ${g}, ${b}, ${a * (percentage / 100)})`; -} - -export function mix(color1: string, color2: string, percentage: number) { - const rgba1 = toRgba(color1); - const rgba2 = toRgba(color2); - if (rgba1 == null || rgba2 == null) return color1; - const [r1, g1, b1, a1] = rgba1; - const [r2, g2, b2, a2] = rgba2; - return `rgba(${mixChannel(r1, r2, percentage)}, ${mixChannel(g1, g2, percentage)}, ${mixChannel( - b1, - b2, - percentage, - )}, ${mixChannel(a1, a2, percentage)})`; -} - -const mixChannel = (channel1: number, channel2: number, percentage: number) => { - return channel1 + ((channel2 - channel1) * percentage) / 100; -}; - -export function toRgba(color: string) { - color = color.trim(); - - const result = cssColorRegex.exec(color); - if (result == null) return null; - - if (result[1] === '#') { - const hex = result[2]; - switch (hex.length) { - case 3: - return [parseInt(hex[0] + hex[0], 16), parseInt(hex[1] + hex[1], 16), parseInt(hex[2] + hex[2], 16), 1]; - case 6: - return [ - parseInt(hex.substring(0, 2), 16), - parseInt(hex.substring(2, 4), 16), - parseInt(hex.substring(4, 6), 16), - 1, - ]; - } - - return null; - } - - switch (result[3]) { - case 'rgb': - return [parseInt(result[4], 10), parseInt(result[5], 10), parseInt(result[6], 10), 1]; - case 'rgba': - return [parseInt(result[4], 10), parseInt(result[5], 10), parseInt(result[6], 10), parseFloat(result[7])]; - default: - return null; - } -} diff --git a/src/webviews/apps/shared/theme.ts b/src/webviews/apps/shared/theme.ts index df6b44f..b6da142 100644 --- a/src/webviews/apps/shared/theme.ts +++ b/src/webviews/apps/shared/theme.ts @@ -1,5 +1,5 @@ /*global window document MutationObserver*/ -import { darken, lighten, opacity } from './colors'; +import { darken, lighten, opacity } from '../../../system/color'; import type { Event } from './events'; import { Emitter } from './events'; diff --git a/yarn.lock b/yarn.lock index b9a2480..6709306 100644 --- a/yarn.lock +++ b/yarn.lock @@ -681,11 +681,6 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== -"@types/chroma-js@2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-2.1.4.tgz#52e3a8453000cdb9ad76357c2c47dbed702d136f" - integrity sha512-l9hWzP7cp7yleJUI7P2acmpllTJNYf5uU6wh50JzSIZt3fFHe+w2FM6w9oZGBTYzjjm2qHdnQvI+fF/JF/E5jQ== - "@types/d3-selection@*", "@types/d3-selection@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.4.tgz#923d7f8985718116de56f55307d26e5f00728dc5" @@ -1836,11 +1831,6 @@ chownr@^2.0.0: resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== -chroma-js@2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.4.2.tgz#dffc214ed0c11fa8eefca2c36651d8e57cbfb2b0" - integrity sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A== - chrome-trace-event@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" @@ -1990,6 +1980,11 @@ colorette@^2.0.14: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== +colorjs.io@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/colorjs.io/-/colorjs.io-0.4.2.tgz#5338fc185a8be3b46674420cd2be88389a18145b" + integrity sha512-vtpiH+BTzZtzs4Yno0GyoC05Z20fTeLwNJ7lQzjxi8GJJb1SZO2o5yUBAUXzgvrO2JNuyIqur4gb1Z6HBjpd9A== + commander@7, commander@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"