From 99c2420a08edb4490d57e155741c642a00294ec3 Mon Sep 17 00:00:00 2001 From: Ovilia Date: Wed, 14 Aug 2024 16:00:43 +0800 Subject: [PATCH 1/3] feat(dark): support auto computing dark mode --- src/canvas/Painter.ts | 20 ++++++++++++++------ src/canvas/graphic.ts | 26 +++++++++++++++++++++----- src/tool/color.ts | 36 ++++++++++++++++++++++++++++++++++++ src/zrender.ts | 13 ++++++++++++- 4 files changed, 83 insertions(+), 12 deletions(-) diff --git a/src/canvas/Painter.ts b/src/canvas/Painter.ts index 568885692..b22097633 100644 --- a/src/canvas/Painter.ts +++ b/src/canvas/Painter.ts @@ -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,8 @@ interface CanvasPainterOption { devicePixelRatio?: number width?: number | string // Can be 10 / 10px / auto height?: number | string, - useDirtyRect?: boolean + useDirtyRect?: boolean, + darkMode?: boolean } export default class CanvasPainter implements PainterBase { @@ -274,7 +276,8 @@ export default class CanvasPainter implements PainterBase { const scope: BrushScope = { inHover: true, viewWidth: this._width, - viewHeight: this._height + viewHeight: this._height, + darkMode: this._opts.darkMode }; let ctx; @@ -305,7 +308,7 @@ export default class CanvasPainter implements PainterBase { } paintOne(ctx: CanvasRenderingContext2D, el: Displayable) { - brushSingle(ctx, el); + brushSingle(ctx, el, this._opts.darkMode); } private _paintList(list: Displayable[], prevList: Displayable[], paintAll: boolean, redrawId?: number) { @@ -416,7 +419,8 @@ export default class CanvasPainter implements PainterBase { allClipped: false, prevEl: null, viewWidth: this._width, - viewHeight: this._height + viewHeight: this._height, + darkMode: this._opts.darkMode }; for (i = start; i < layer.__endIndex; i++) { @@ -785,7 +789,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') + : backgroundColor; util.each(this._layers, layer => { layer.setUnpainted(); @@ -950,7 +957,8 @@ export default class CanvasPainter implements PainterBase { const scope = { inHover: false, viewWidth: this._width, - viewHeight: this._height + viewHeight: this._height, + darkMode: this._opts.darkMode }; 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 fb3a145eb..f939d3dff 100644 --- a/src/canvas/graphic.ts +++ b/src/canvas/graphic.ts @@ -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') + : 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') + : style.stroke + ); } if (forceSetAll || style.opacity !== prevStyle.opacity) { if (!styleChanged) { @@ -566,6 +573,8 @@ export type BrushScope = { batchStroke?: string lastDrawType?: number + + darkMode?: boolean } // If path can be batched @@ -602,8 +611,14 @@ 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) { + const scope: BrushScope = { + inHover: false, + viewWidth: 0, + viewHeight: 0, + darkMode + }; + brush(ctx, el, scope, true); } // Brush different type of elements. @@ -785,7 +800,8 @@ function brushIncremental( allClipped: false, viewWidth: scope.viewWidth, viewHeight: scope.viewHeight, - inHover: scope.inHover + inHover: scope.inHover, + darkMode: scope.darkMode }; let i; let len; diff --git a/src/tool/color.ts b/src/tool/color.ts index ae912e3fe..18160e223 100644 --- a/src/tool/color.ts +++ b/src/tool/color.ts @@ -593,3 +593,39 @@ 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): string { + let colorArr = parse(lightColor); + + if (colorArr) { + 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'); + } +} diff --git a/src/zrender.ts b/src/zrender.ts index 4a460df12..f69160fd9 100644 --- a/src/zrender.ts +++ b/src/zrender.ts @@ -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,16 @@ 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); + + const painter = new painterCtors[rendererType](dom, storage, + { + darkMode, + ...opts + }, id); const ssrMode = opts.ssr || painter.ssrOnly; this.storage = storage; @@ -491,6 +501,7 @@ export interface ZRenderInitOpt { devicePixelRatio?: number width?: number | string // 10, 10px, 'auto' height?: number | string + darkMode?: 'auto' | 'light' | 'dark' useDirtyRect?: boolean useCoarsePointer?: 'auto' | boolean pointerSize?: number From 5c86209e8ff19bf2dd0cdb2cf928a10c920269f4 Mon Sep 17 00:00:00 2001 From: Ovilia Date: Mon, 19 Aug 2024 15:35:54 +0800 Subject: [PATCH 2/3] feat(dark): enable setting customized dark mapping --- src/canvas/Painter.ts | 16 ++++++++++------ src/canvas/graphic.ts | 20 ++++++++++++++------ src/tool/color.ts | 28 +++++++++++++++++++++++++++- src/zrender.ts | 6 +++++- 4 files changed, 56 insertions(+), 14 deletions(-) diff --git a/src/canvas/Painter.ts b/src/canvas/Painter.ts index b22097633..1054905eb 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'; @@ -69,7 +69,8 @@ interface CanvasPainterOption { width?: number | string // Can be 10 / 10px / auto height?: number | string, useDirtyRect?: boolean, - darkMode?: boolean + darkMode?: boolean, + darkColorMap?: Dictionary } export default class CanvasPainter implements PainterBase { @@ -277,7 +278,8 @@ export default class CanvasPainter implements PainterBase { inHover: true, viewWidth: this._width, viewHeight: this._height, - darkMode: this._opts.darkMode + darkMode: this._opts.darkMode, + darkColorMap: this._opts.darkColorMap }; let ctx; @@ -420,7 +422,8 @@ export default class CanvasPainter implements PainterBase { prevEl: null, viewWidth: this._width, viewHeight: this._height, - darkMode: this._opts.darkMode + darkMode: this._opts.darkMode, + darkColorMap: this._opts.darkColorMap }; for (i = start; i < layer.__endIndex; i++) { @@ -791,7 +794,7 @@ export default class CanvasPainter implements PainterBase { setBackgroundColor(backgroundColor: string | GradientObject | ImagePatternObject) { // TODO: fix when is gradient or pattern this._backgroundColor = this._opts.darkMode - ? convertToDark(backgroundColor as string, 'fill') + ? convertToDark(backgroundColor as string, 'fill', this._opts.darkColorMap) : backgroundColor; util.each(this._layers, layer => { @@ -958,7 +961,8 @@ export default class CanvasPainter implements PainterBase { inHover: false, viewWidth: this._width, viewHeight: this._height, - darkMode: this._opts.darkMode + 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 f939d3dff..1092ca0ed 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'; @@ -441,7 +441,7 @@ function bindPathAndTextCommonStyle( styleChanged = true; } isValidStrokeFillStyle(style.fill) && (ctx.fillStyle = scope.darkMode - ? convertToDark(style.fill, 'fill') + ? convertToDark(style.fill, 'fill', scope.darkColorMap) : style.fill ); } @@ -451,7 +451,7 @@ function bindPathAndTextCommonStyle( styleChanged = true; } isValidStrokeFillStyle(style.stroke) && (ctx.strokeStyle = scope.darkMode - ? convertToDark(style.stroke, 'stroke') + ? convertToDark(style.stroke, 'stroke', scope.darkColorMap) : style.stroke ); } @@ -575,6 +575,7 @@ export type BrushScope = { lastDrawType?: number darkMode?: boolean + darkColorMap?: Dictionary } // If path can be batched @@ -611,12 +612,18 @@ function getStyle(el: Displayable, inHover?: boolean) { return inHover ? (el.__hoverStyle || el.style) : el.style; } -export function brushSingle(ctx: CanvasRenderingContext2D, el: Displayable, darkMode: boolean) { +export function brushSingle( + ctx: CanvasRenderingContext2D, + el: Displayable, + darkMode: boolean, + darkColorMap: Dictionary +) { const scope: BrushScope = { inHover: false, viewWidth: 0, viewHeight: 0, - darkMode + darkMode, + darkColorMap }; brush(ctx, el, scope, true); } @@ -801,7 +808,8 @@ function brushIncremental( viewWidth: scope.viewWidth, viewHeight: scope.viewHeight, inHover: scope.inHover, - darkMode: scope.darkMode + darkMode: scope.darkMode, + darkColorMap: scope.darkColorMap }; let i; let len; diff --git a/src/tool/color.ts b/src/tool/color.ts index 18160e223..f6d7ee9c1 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'; @@ -604,11 +605,21 @@ export type ColorAttributeType = 'fill' | 'stroke' | 'textFill'; * @param lightColor color in light mode * @return color in dark mode, in rgba format */ -export function convertToDark(lightColor: string, type: ColorAttributeType): string { +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. @@ -629,3 +640,18 @@ export function convertToDark(lightColor: string, type: ColorAttributeType): str 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 f69160fd9..1fdd8f04f 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'; @@ -123,6 +123,9 @@ class ZRender { ? false : (opts.darkMode === 'dark' ? true : isDark); + opts.darkColorMap = normalizeColorMap(opts.darkColorMap); + console.log(opts.darkColorMap) + const painter = new painterCtors[rendererType](dom, storage, { darkMode, @@ -502,6 +505,7 @@ export interface ZRenderInitOpt { width?: number | string // 10, 10px, 'auto' height?: number | string darkMode?: 'auto' | 'light' | 'dark' + darkColorMap?: Dictionary, useDirtyRect?: boolean useCoarsePointer?: 'auto' | boolean pointerSize?: number From a47077f3a1032c6964b158f8961c30ad0197f564 Mon Sep 17 00:00:00 2001 From: Ovilia Date: Wed, 21 Aug 2024 16:04:43 +0800 Subject: [PATCH 3/3] feat(dark): support custom dark map --- src/canvas/Painter.ts | 2 +- src/canvas/graphic.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/canvas/Painter.ts b/src/canvas/Painter.ts index 1054905eb..b5ae9c6f6 100644 --- a/src/canvas/Painter.ts +++ b/src/canvas/Painter.ts @@ -310,7 +310,7 @@ export default class CanvasPainter implements PainterBase { } paintOne(ctx: CanvasRenderingContext2D, el: Displayable) { - brushSingle(ctx, el, this._opts.darkMode); + brushSingle(ctx, el, this._opts.darkMode, this._opts.darkColorMap); } private _paintList(list: Displayable[], prevList: Displayable[], paintAll: boolean, redrawId?: number) { diff --git a/src/canvas/graphic.ts b/src/canvas/graphic.ts index 1092ca0ed..b355391ac 100644 --- a/src/canvas/graphic.ts +++ b/src/canvas/graphic.ts @@ -615,8 +615,8 @@ function getStyle(el: Displayable, inHover?: boolean) { export function brushSingle( ctx: CanvasRenderingContext2D, el: Displayable, - darkMode: boolean, - darkColorMap: Dictionary + darkMode?: boolean, + darkColorMap?: Dictionary ) { const scope: BrushScope = { inHover: false,