diff --git a/src/canvas/Painter.ts b/src/canvas/Painter.ts index 56888569..b5ae9c6f 100644 --- a/src/canvas/Painter.ts +++ b/src/canvas/Painter.ts @@ -4,7 +4,7 @@ import Layer, { LayerConfig } from './Layer'; import requestAnimationFrame from '../animation/requestAnimationFrame'; import env from '../core/env'; import Displayable from '../graphic/Displayable'; -import { WXCanvasRenderingContext } from '../core/types'; +import { Dictionary, WXCanvasRenderingContext } from '../core/types'; import { GradientObject } from '../graphic/Gradient'; import { ImagePatternObject } from '../graphic/Pattern'; import Storage from '../Storage'; @@ -14,6 +14,7 @@ import BoundingRect from '../core/BoundingRect'; import { REDRAW_BIT } from '../graphic/constants'; import { getSize } from './helper'; import type IncrementalDisplayable from '../graphic/IncrementalDisplayable'; +import { convertToDark } from '../tool/color'; const HOVER_LAYER_ZLEVEL = 1e5; const CANVAS_ZLEVEL = 314159; @@ -67,7 +68,9 @@ interface CanvasPainterOption { devicePixelRatio?: number width?: number | string // Can be 10 / 10px / auto height?: number | string, - useDirtyRect?: boolean + useDirtyRect?: boolean, + darkMode?: boolean, + darkColorMap?: Dictionary } export default class CanvasPainter implements PainterBase { @@ -274,7 +277,9 @@ export default class CanvasPainter implements PainterBase { const scope: BrushScope = { inHover: true, viewWidth: this._width, - viewHeight: this._height + viewHeight: this._height, + darkMode: this._opts.darkMode, + darkColorMap: this._opts.darkColorMap }; let ctx; @@ -305,7 +310,7 @@ export default class CanvasPainter implements PainterBase { } paintOne(ctx: CanvasRenderingContext2D, el: Displayable) { - brushSingle(ctx, el); + brushSingle(ctx, el, this._opts.darkMode, this._opts.darkColorMap); } private _paintList(list: Displayable[], prevList: Displayable[], paintAll: boolean, redrawId?: number) { @@ -416,7 +421,9 @@ export default class CanvasPainter implements PainterBase { allClipped: false, prevEl: null, viewWidth: this._width, - viewHeight: this._height + viewHeight: this._height, + darkMode: this._opts.darkMode, + darkColorMap: this._opts.darkColorMap }; for (i = start; i < layer.__endIndex; i++) { @@ -785,7 +792,10 @@ export default class CanvasPainter implements PainterBase { } setBackgroundColor(backgroundColor: string | GradientObject | ImagePatternObject) { - this._backgroundColor = backgroundColor; + // TODO: fix when is gradient or pattern + this._backgroundColor = this._opts.darkMode + ? convertToDark(backgroundColor as string, 'fill', this._opts.darkColorMap) + : backgroundColor; util.each(this._layers, layer => { layer.setUnpainted(); @@ -950,7 +960,9 @@ export default class CanvasPainter implements PainterBase { const scope = { inHover: false, viewWidth: this._width, - viewHeight: this._height + viewHeight: this._height, + darkMode: this._opts.darkMode, + darkColorMap: this._opts.darkColorMap }; const displayList = this.storage.getDisplayList(true); for (let i = 0, len = displayList.length; i < len; i++) { diff --git a/src/canvas/graphic.ts b/src/canvas/graphic.ts index fb3a145e..b355391a 100644 --- a/src/canvas/graphic.ts +++ b/src/canvas/graphic.ts @@ -4,7 +4,7 @@ import { GradientObject } from '../graphic/Gradient'; import { ImagePatternObject, InnerImagePatternObject } from '../graphic/Pattern'; import { LinearGradientObject } from '../graphic/LinearGradient'; import { RadialGradientObject } from '../graphic/RadialGradient'; -import { ZRCanvasRenderingContext } from '../core/types'; +import { Dictionary, ZRCanvasRenderingContext } from '../core/types'; import { createOrUpdateImage, isImageReady } from '../graphic/helper/image'; import { getCanvasGradient, isClipPathChanged } from './helper'; import Path, { PathStyleProps } from '../graphic/Path'; @@ -16,6 +16,7 @@ import { getLineDash } from './dashStyle'; import { REDRAW_BIT, SHAPE_CHANGED_BIT } from '../graphic/constants'; import type IncrementalDisplayable from '../graphic/IncrementalDisplayable'; import { DEFAULT_FONT } from '../core/platform'; +import { convertToDark } from '../tool/color'; const pathProxyForDraw = new PathProxy(true); @@ -439,14 +440,20 @@ function bindPathAndTextCommonStyle( flushPathDrawn(ctx, scope); styleChanged = true; } - isValidStrokeFillStyle(style.fill) && (ctx.fillStyle = style.fill); + isValidStrokeFillStyle(style.fill) && (ctx.fillStyle = scope.darkMode + ? convertToDark(style.fill, 'fill', scope.darkColorMap) + : style.fill + ); } if (forceSetAll || style.stroke !== prevStyle.stroke) { if (!styleChanged) { flushPathDrawn(ctx, scope); styleChanged = true; } - isValidStrokeFillStyle(style.stroke) && (ctx.strokeStyle = style.stroke); + isValidStrokeFillStyle(style.stroke) && (ctx.strokeStyle = scope.darkMode + ? convertToDark(style.stroke, 'stroke', scope.darkColorMap) + : style.stroke + ); } if (forceSetAll || style.opacity !== prevStyle.opacity) { if (!styleChanged) { @@ -566,6 +573,9 @@ export type BrushScope = { batchStroke?: string lastDrawType?: number + + darkMode?: boolean + darkColorMap?: Dictionary } // If path can be batched @@ -602,8 +612,20 @@ function getStyle(el: Displayable, inHover?: boolean) { return inHover ? (el.__hoverStyle || el.style) : el.style; } -export function brushSingle(ctx: CanvasRenderingContext2D, el: Displayable) { - brush(ctx, el, { inHover: false, viewWidth: 0, viewHeight: 0 }, true); +export function brushSingle( + ctx: CanvasRenderingContext2D, + el: Displayable, + darkMode?: boolean, + darkColorMap?: Dictionary +) { + const scope: BrushScope = { + inHover: false, + viewWidth: 0, + viewHeight: 0, + darkMode, + darkColorMap + }; + brush(ctx, el, scope, true); } // Brush different type of elements. @@ -785,7 +807,9 @@ function brushIncremental( allClipped: false, viewWidth: scope.viewWidth, viewHeight: scope.viewHeight, - inHover: scope.inHover + inHover: scope.inHover, + darkMode: scope.darkMode, + darkColorMap: scope.darkColorMap }; let i; let len; diff --git a/src/tool/color.ts b/src/tool/color.ts index ae912e3f..f6d7ee9c 100644 --- a/src/tool/color.ts +++ b/src/tool/color.ts @@ -1,4 +1,5 @@ import LRU from '../core/LRU'; +import { Dictionary } from '../core/types'; import { extend, isGradientObject, isString, map } from '../core/util'; import { GradientObject } from '../graphic/Gradient'; @@ -593,3 +594,64 @@ export function liftColor(color: string | GradientObject): string | GradientObje // Change nothing. return color; } + +/** + * text stroke is treated as 'stroke' + */ +export type ColorAttributeType = 'fill' | 'stroke' | 'textFill'; + +/** + * Convert color to dark mode. + * @param lightColor color in light mode + * @return color in dark mode, in rgba format + */ +export function convertToDark( + lightColor: string, + type: ColorAttributeType, + darkColorMap: Dictionary +): string { + let colorArr = parse(lightColor); + + if (colorArr) { + const colorStr = stringify(colorArr, 'rgba'); + if (darkColorMap && darkColorMap[colorStr]) { + return darkColorMap[colorStr]; + } + + colorArr = rgba2hsla(colorArr); + + switch (type) { + // TODO: Probably using other color space to enhance the result. + // Just a quick demo for now. + case 'stroke': + case 'textFill': + // Text color needs more contrast luminance? + colorArr[2] = 1 - colorArr[2]; + break; + case 'fill': + default: + colorArr[2] = Math.min(1, (1 - colorArr[2]) * 1.1); + break; + } + + // Desaturate a little. + colorArr[1] *= 0.9; + + return stringify(hsla2rgba(colorArr), 'rgba'); + } +} + +export function normalizeColorMap(map: Dictionary): Dictionary { + if (!map) { + return {}; + } + + const normalizedMap: Dictionary = {}; + for (let key in map) { + const normalizedKey = stringify(parse(key), 'rgba'); + if (normalizedKey) { + normalizedMap[normalizedKey] = stringify(parse(map[key]), 'rgba'); + } + } + return normalizedMap; +} diff --git a/src/zrender.ts b/src/zrender.ts index 4a460df1..1fdd8f04 100644 --- a/src/zrender.ts +++ b/src/zrender.ts @@ -22,7 +22,7 @@ import { GradientObject } from './graphic/Gradient'; import { PatternObject } from './graphic/Pattern'; import { EventCallback } from './core/Eventful'; import Displayable from './graphic/Displayable'; -import { lum } from './tool/color'; +import { lum, normalizeColorMap } from './tool/color'; import { DARK_MODE_THRESHOLD } from './config'; import Group from './graphic/Group'; @@ -83,6 +83,7 @@ class ZRender { private _needsRefreshHover = true private _disposed: boolean; /** + * TODO: probably should be removed in the future * If theme is dark mode. It will determine the color strategy for labels. */ private _darkMode = false; @@ -117,7 +118,19 @@ class ZRender { ? false : opts.useDirtyRect; - const painter = new painterCtors[rendererType](dom, storage, opts, id); + const isDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + const darkMode = opts.darkMode === 'light' + ? false + : (opts.darkMode === 'dark' ? true : isDark); + + opts.darkColorMap = normalizeColorMap(opts.darkColorMap); + console.log(opts.darkColorMap) + + const painter = new painterCtors[rendererType](dom, storage, + { + darkMode, + ...opts + }, id); const ssrMode = opts.ssr || painter.ssrOnly; this.storage = storage; @@ -491,6 +504,8 @@ export interface ZRenderInitOpt { devicePixelRatio?: number width?: number | string // 10, 10px, 'auto' height?: number | string + darkMode?: 'auto' | 'light' | 'dark' + darkColorMap?: Dictionary, useDirtyRect?: boolean useCoarsePointer?: 'auto' | boolean pointerSize?: number