From c0a0a980363989f83963c28fbfd336dc3ac73af4 Mon Sep 17 00:00:00 2001 From: shihui Date: Mon, 26 Dec 2022 23:17:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20rampcolors=20?= =?UTF-8?q?=E6=9E=9A=E4=B8=BE=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev-demos/features/line/demos/linearline.tsx | 15 +- .../features/tile/rasterData/loadTiff.tsx | 68 +----- packages/layers/src/core/TextureService.ts | 19 +- packages/site/docs/api/experiment/wind.zh.md | 35 +-- .../site/docs/api/heatmap_layer/style.zh.md | 78 ++++++- .../docs/api/raster_layer/common/style.md | 69 +++++- .../api/raster_layer/common/style_single.md | 69 +++++- .../site/docs/api/tile/common/style.zh.md | 69 +++++- .../site/docs/tutorial/heatmap/normal.zh.md | 65 +++++- .../site/examples/tile/raster/demo/tiff.js | 34 +-- packages/utils/__tests__/color.spec.ts | 111 +++++++++ packages/utils/src/color.ts | 217 +++++++++++++++--- 12 files changed, 672 insertions(+), 177 deletions(-) create mode 100644 packages/utils/__tests__/color.spec.ts diff --git a/dev-demos/features/line/demos/linearline.tsx b/dev-demos/features/line/demos/linearline.tsx index 241c895668..a8babd1a83 100644 --- a/dev-demos/features/line/demos/linearline.tsx +++ b/dev-demos/features/line/demos/linearline.tsx @@ -52,15 +52,12 @@ export default () => { .shape('linearline') .style({ rampColors: { - colors: [ - '#FF4818', - '#F7B74A', - '#FFF598', - '#91EABC', - '#2EA9A1', - '#206C7C', - ], - weights: [0.1, 0.1, 0.1, 0.1, 0.1, 0.5], + c1: '#FF4818', + c2: '#F7B74A', + c3: '#FFF598', + c4: '#91EABC', + c5: '#2EA9A1', + c6: [0.5, 1, '#206C7C'] }, }); diff --git a/dev-demos/features/tile/rasterData/loadTiff.tsx b/dev-demos/features/tile/rasterData/loadTiff.tsx index 39261ae7a5..0c6c6ad11a 100644 --- a/dev-demos/features/tile/rasterData/loadTiff.tsx +++ b/dev-demos/features/tile/rasterData/loadTiff.tsx @@ -3,59 +3,7 @@ import { GaodeMap } from '@antv/l7-maps'; import React, { useEffect } from 'react'; import * as GeoTIFF from 'geotiff'; -const colorList = [ - '#419bdf', // Water - '#419bdf', - '#397d49', // Tree - '#397d49', - - '#88b053', // Grass - '#88b053', - - '#7a87c6', // vegetation - '#7a87c6', - - '#e49635', // Crops - '#e49635', - - '#dfc35a', // shrub - '#dfc35a', - - '#c4281b', // Built Area - '#c4281b', - - '#a59b8f', // Bare ground - '#a59b8f', - - '#a8ebff', // Snow - '#a8ebff', - - '#616161', // Clouds - '#616161' -]; -const positions = [ - 0.0, - 0.1, - 0.1, - 0.2, - 0.2, - 0.3, - 0.3, - 0.4, - 0.4, - 0.5, - 0.5, - 0.6, - 0.6, - 0.7, - 0.7, - 0.8, - 0.8, - 0.9, - 0.9, - 1.0 -]; export default () => { useEffect(() => { @@ -103,10 +51,18 @@ export default () => { domain: [ 0.001, 11.001 ], clampLow: false, rampColors: { - colors: colorList, - positions - // colors: ['#f00', '#f00'], - // positions: [0, 1] + // colors: colorList, + // positions + // 'Water': '#419bdf', + // 'Tree': '#397d49', + // 'Grass': '#88b053', + // 'vegetation': '#7a87c6', + // 'Crops': '#e49635', + // 'shrub': '#dfc35a', + // 'Built Area': '#c4281b', + // 'Bare ground': '#a59b8f', + // 'Snow': '#a8ebff', + // 'Clouds': '#616161', } }); diff --git a/packages/layers/src/core/TextureService.ts b/packages/layers/src/core/TextureService.ts index c13609483c..b7a9f1d695 100644 --- a/packages/layers/src/core/TextureService.ts +++ b/packages/layers/src/core/TextureService.ts @@ -6,7 +6,11 @@ import { TYPES, } from '@antv/l7-core'; -import { generateColorRamp, IColorRamp } from '@antv/l7-utils'; +import { + generateColorRamp, + generateColorRampKey, + IColorRamp, +} from '@antv/l7-utils'; export default class TextureService implements ITextureService { private layer: ILayer; @@ -22,13 +26,14 @@ export default class TextureService implements ITextureService { } public getColorTexture(colorRamp: IColorRamp) { // TODO 支持传入图片 - const currentkey = this.getTextureKey(colorRamp); - if (this.key === currentkey) { + const currentKey = generateColorRampKey(colorRamp); + + if (this.key === currentKey) { return this.colorTexture; } else { this.createColorTexture(colorRamp); } - this.key = currentkey; + this.key = currentKey; return this.colorTexture; } @@ -46,15 +51,11 @@ export default class TextureService implements ITextureService { } public setColorTexture(texture: ITexture2D, colorRamp: IColorRamp) { - this.key = this.getTextureKey(colorRamp); + this.key = generateColorRampKey(colorRamp); this.colorTexture = texture; } public destroy() { this.colorTexture?.destroy(); } - - private getTextureKey(colorRamp: IColorRamp): string { - return `${colorRamp.colors.join('_')}_${colorRamp.positions.join('_')}`; - } } diff --git a/packages/site/docs/api/experiment/wind.zh.md b/packages/site/docs/api/experiment/wind.zh.md index f7475cdfb8..cb4e9bf854 100644 --- a/packages/site/docs/api/experiment/wind.zh.md +++ b/packages/site/docs/api/experiment/wind.zh.md @@ -51,36 +51,13 @@ layer.animate(true); ```js const rampColors = { - colors: [ - '#3288bd', - '#66c2a5', - '#abdda4', - '#e6f598', - '#fee08b', - '#fdae61', - '#f46d43', - '#d53e4f', - ], - positions: [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 1.0], + 0.0: '#c6dbef', + 0.1: '#9ecae1', + 0.2: '#6baed6', + 0.3: '#4292c6', + 0.4: '#2171b5', + 0.5: '#084594', }; ``` -#### rampColors - -- colors  颜色数组 -- positions 数据区间 - -配置值域映射颜色的色带,值域的范围为 `[0 - 1]`, 对应的我们需要为每一个 `position` 位置设置一个颜色值。 - -⚠️ colors, positions 的长度要相同 - -```javascript -layer.style({ - rampColors: { - colors: ['#FF4818', '#F7B74A', '#FFF598', '#91EABC', '#2EA9A1', '#206C7C'], - positions: [0, 0.2, 0.4, 0.6, 0.8, 1.0], - }, -}); -``` - [在线案例](/examples/wind/basic#wind) diff --git a/packages/site/docs/api/heatmap_layer/style.zh.md b/packages/site/docs/api/heatmap_layer/style.zh.md index 08d4ff934c..2059cce4e2 100644 --- a/packages/site/docs/api/heatmap_layer/style.zh.md +++ b/packages/site/docs/api/heatmap_layer/style.zh.md @@ -35,12 +35,14 @@ layer.style({ #### rampColors -- colors  颜色数组 -- positions 数据区间 +1. 连续色带,根据 `colors`  和 `positions` 设置色带。 + +- `colors`  颜色数组 +- `positions` 数据区间 配置值域映射颜色的色带,值域的范围为 `[0 - 1]`, 对应的我们需要为每一个 `position` 位置设置一个颜色值。 -⚠️ colors, positions 的长度要相同 +⚠️ `colors`, `positions` 的长度要相同。 ```javascript layer.style({ @@ -51,6 +53,76 @@ layer.style({ }); ``` +2. 枚举色带 + 枚举模式下色带不再是连续的,而是分段的,同时用户可以选择直接传入颜色或者指定具体的分布。 + +- 直接传入 + +```js +layer.style({ + rampColors: { + c1: '#f00', + c2: '#ff0', + }, +}); +``` + +c1 和 c2 平均分布,前一半色带为 #f00,后一半为 #ff0。 + +- 指定具体的分布 + +```js +layer.style({ + rampColors: { + c1: [0, 0.3 '#f00'], + c2: [0.3, 1.0, '#ff0'] + }, +}); +``` + +c1 和 c2 三七分布,前三色带为 #f00,后七为 #ff0。 + +- 混合使用 + +```js +layer.style({ + rampColors: { + c1: [0, 0.3 '#f00'], + c2: '#0f0', + c3: [0.7, 1.0, '#ff0'] + }, +}); +``` + +在混合使用的情况下,使用直接传入方式指定的色值会填满空隙,上面的分布 c1:c2:c3 为 3:4:3 + +- 默认色 + 当用户仅指定部分色值分布或者使用错误的颜色值时,其余色值用默认色进行填充,可以动过 default 指定默认色,未指定时默认色为 #fff。 + +```js +layer.style({ + rampColors: { + c1: [0, 0.3 '#f00'], + }, +}); + +// => 上面的写法在内部会被默认填充 +layer.style({ + rampColors: { + c1: [0, 0.3 '#f00'], + defaultFill: '#fff' // 等价写法 + }, +}); + +layer.style({ + rampColors: { + default: '#ff0', // 指定默认填充色 + c1: [0, 0.3 '#f00'], + defaultFill: '#ff0' // 等价写法 + }, +}); +``` + ### hexagon 绘制蜂窝热力图。 diff --git a/packages/site/docs/api/raster_layer/common/style.md b/packages/site/docs/api/raster_layer/common/style.md index dab6507b37..f76e308e49 100644 --- a/packages/site/docs/api/raster_layer/common/style.md +++ b/packages/site/docs/api/raster_layer/common/style.md @@ -21,12 +21,13 @@ layer.style({ #### rampColors -- colors  颜色数组 -- positions 数据区间 +1. 连续色带,根据 `colors` 和 `positions` 设置色带。 +- `colors`  颜色数组 +- `positions` 数据区间 配置值域映射颜色的色带,值域的范围为 `[0 - 1]`, 对应的我们需要为每一个 `position` 位置设置一个颜色值。 -⚠️ colors, positions 的长度要相同 +⚠️ `colors`, `positions` 的长度要相同。 ```javascript layer.style({ @@ -35,4 +36,66 @@ layer.style({ positions: [0, 0.2, 0.4, 0.6, 0.8, 1.0], }, }); +``` +2. 枚举色带 +枚举模式下色带不再是连续的,而是分段的,同时用户可以选择直接传入颜色或者指定具体的分布。 + +- 直接传入 +```js +layer.style({ + rampColors: { + c1: '#f00', + c2: '#ff0' + }, +}); +``` +c1 和 c2 平均分布,前一半色带为 #f00,后一半为 #ff0。 + +- 指定具体的分布 +```js +layer.style({ + rampColors: { + c1: [0, 0.3 '#f00'], + c2: [0.3, 1.0, '#ff0'] + }, +}); +``` +c1 和 c2 三七分布,前三色带为 #f00,后七为 #ff0。 + +- 混合使用 +```js +layer.style({ + rampColors: { + c1: [0, 0.3 '#f00'], + c2: '#0f0', + c3: [0.7, 1.0, '#ff0'] + }, +}); +``` +在混合使用的情况下,使用直接传入方式指定的色值会填满空隙,上面的分布 c1:c2:c3 为 3:4:3 + +- 默认色 +当用户仅指定部分色值分布或者使用错误的颜色值时,其余色值用默认色进行填充,可以动过 default 指定默认色,未指定时默认色为 #fff。 +```js +layer.style({ + rampColors: { + c1: [0, 0.3 '#f00'], + }, +}); + +// => 上面的写法在内部会被默认填充 +layer.style({ + rampColors: { + c1: [0, 0.3 '#f00'], + defaultFill: '#fff' // 等价写法 + }, +}); + +layer.style({ + rampColors: { + default: '#ff0', // 指定默认填充色 + c1: [0, 0.3 '#f00'], + defaultFill: '#ff0' // 等价写法 + }, +}); ``` \ No newline at end of file diff --git a/packages/site/docs/api/raster_layer/common/style_single.md b/packages/site/docs/api/raster_layer/common/style_single.md index d906946e79..e1d4348217 100644 --- a/packages/site/docs/api/raster_layer/common/style_single.md +++ b/packages/site/docs/api/raster_layer/common/style_single.md @@ -19,12 +19,13 @@ layer.style({ #### rampColors -- colors  颜色数组 -- positions 数据区间 +1. 连续色带,根据 `colors` 和 `positions` 设置色带。 +- `colors`  颜色数组 +- `positions` 数据区间 配置值域映射颜色的色带,值域的范围为 `[0 - 1]`, 对应的我们需要为每一个 `position` 位置设置一个颜色值。 -⚠️ colors, positions 的长度要相同 +⚠️ `colors`, `positions` 的长度要相同。 ```javascript layer.style({ @@ -34,3 +35,65 @@ layer.style({ }, }); ``` +2. 枚举色带 +枚举模式下色带不再是连续的,而是分段的,同时用户可以选择直接传入颜色或者指定具体的分布。 + +- 直接传入 +```js +layer.style({ + rampColors: { + c1: '#f00', + c2: '#ff0' + }, +}); +``` +c1 和 c2 平均分布,前一半色带为 #f00,后一半为 #ff0。 + +- 指定具体的分布 +```js +layer.style({ + rampColors: { + c1: [0, 0.3 '#f00'], + c2: [0.3, 1.0, '#ff0'] + }, +}); +``` +c1 和 c2 三七分布,前三色带为 #f00,后七为 #ff0。 + +- 混合使用 +```js +layer.style({ + rampColors: { + c1: [0, 0.3 '#f00'], + c2: '#0f0', + c3: [0.7, 1.0, '#ff0'] + }, +}); +``` +在混合使用的情况下,使用直接传入方式指定的色值会填满空隙,上面的分布 c1:c2:c3 为 3:4:3 + +- 默认色 +当用户仅指定部分色值分布或者使用错误的颜色值时,其余色值用默认色进行填充,可以动过 default 指定默认色,未指定时默认色为 #fff。 +```js +layer.style({ + rampColors: { + c1: [0, 0.3 '#f00'], + }, +}); + +// => 上面的写法在内部会被默认填充 +layer.style({ + rampColors: { + c1: [0, 0.3 '#f00'], + defaultFill: '#fff' // 等价写法 + }, +}); + +layer.style({ + rampColors: { + default: '#ff0', // 指定默认填充色 + c1: [0, 0.3 '#f00'], + defaultFill: '#ff0' // 等价写法 + }, +}); +``` diff --git a/packages/site/docs/api/tile/common/style.zh.md b/packages/site/docs/api/tile/common/style.zh.md index bbd78c8886..44ab1c5154 100644 --- a/packages/site/docs/api/tile/common/style.zh.md +++ b/packages/site/docs/api/tile/common/style.zh.md @@ -23,12 +23,13 @@ ps:rgb 多通道栅格不支持 #### rampColors -- colors  颜色数组 -- positions 数据区间 +1. 连续色带,根据 `colors` 和 `positions` 设置色带。 +- `colors`  颜色数组 +- `positions` 数据区间 配置值域映射颜色的色带,值域的范围为 `[0 - 1]`, 对应的我们需要为每一个 `position` 位置设置一个颜色值。 -⚠️ colors, positions 的长度要相同 +⚠️ `colors`, `positions` 的长度要相同。 ```javascript layer.style({ @@ -38,6 +39,68 @@ layer.style({ }, }); ``` +2. 枚举色带 +枚举模式下色带不再是连续的,而是分段的,同时用户可以选择直接传入颜色或者指定具体的分布。 + +- 直接传入 +```js +layer.style({ + rampColors: { + c1: '#f00', + c2: '#ff0' + }, +}); +``` +c1 和 c2 平均分布,前一半色带为 #f00,后一半为 #ff0。 + +- 指定具体的分布 +```js +layer.style({ + rampColors: { + c1: [0, 0.3 '#f00'], + c2: [0.3, 1.0, '#ff0'] + }, +}); +``` +c1 和 c2 三七分布,前三色带为 #f00,后七为 #ff0。 + +- 混合使用 +```js +layer.style({ + rampColors: { + c1: [0, 0.3 '#f00'], + c2: '#0f0', + c3: [0.7, 1.0, '#ff0'] + }, +}); +``` +在混合使用的情况下,使用直接传入方式指定的色值会填满空隙,上面的分布 c1:c2:c3 为 3:4:3 + +- 默认色 +当用户仅指定部分色值分布或者使用错误的颜色值时,其余色值用默认色进行填充,可以动过 default 指定默认色,未指定时默认色为 #fff。 +```js +layer.style({ + rampColors: { + c1: [0, 0.3 '#f00'], + }, +}); + +// => 上面的写法在内部会被默认填充 +layer.style({ + rampColors: { + c1: [0, 0.3 '#f00'], + defaultFill: '#fff' // 等价写法 + }, +}); + +layer.style({ + rampColors: { + default: '#ff0', // 指定默认填充色 + c1: [0, 0.3 '#f00'], + defaultFill: '#ff0' // 等价写法 + }, +}); +``` ps:⚠️ color, position 的长度要相同,rgb 多通道栅格不支持 diff --git a/packages/site/docs/tutorial/heatmap/normal.zh.md b/packages/site/docs/tutorial/heatmap/normal.zh.md index f4b5e70267..197d0390e7 100644 --- a/packages/site/docs/tutorial/heatmap/normal.zh.md +++ b/packages/site/docs/tutorial/heatmap/normal.zh.md @@ -100,12 +100,13 @@ layer.size('weight', [0, 1]); #### rampColors +1. 连续色带,根据 `colors` 和 `positions` 设置色带。 - `colors`  颜色数组 - `positions` 数据区间 配置值域映射颜色的色带,值域的范围为 `[0 - 1]`, 对应的我们需要为每一个 `position` 位置设置一个颜色值。 -⚠️ colors, positions 的长度要相同 +⚠️ `colors`, `positions` 的长度要相同。 ```javascript layer.style({ @@ -115,3 +116,65 @@ layer.style({ }, }); ``` +2. 枚举色带 +枚举模式下色带不再是连续的,而是分段的,同时用户可以选择直接传入颜色或者指定具体的分布。 + +- 直接传入 +```js +layer.style({ + rampColors: { + c1: '#f00', + c2: '#ff0' + }, +}); +``` +c1 和 c2 平均分布,前一半色带为 #f00,后一半为 #ff0。 + +- 指定具体的分布 +```js +layer.style({ + rampColors: { + c1: [0, 0.3 '#f00'], + c2: [0.3, 1.0, '#ff0'] + }, +}); +``` +c1 和 c2 三七分布,前三色带为 #f00,后七为 #ff0。 + +- 混合使用 +```js +layer.style({ + rampColors: { + c1: [0, 0.3 '#f00'], + c2: '#0f0', + c3: [0.7, 1.0, '#ff0'] + }, +}); +``` +在混合使用的情况下,使用直接传入方式指定的色值会填满空隙,上面的分布 c1:c2:c3 为 3:4:3 + +- 默认色 +当用户仅指定部分色值分布或者使用错误的颜色值时,其余色值用默认色进行填充,可以动过 default 指定默认色,未指定时默认色为 #fff。 +```js +layer.style({ + rampColors: { + c1: [0, 0.3 '#f00'], + }, +}); + +// => 上面的写法在内部会被默认填充 +layer.style({ + rampColors: { + c1: [0, 0.3 '#f00'], + defaultFill: '#fff' // 等价写法 + }, +}); + +layer.style({ + rampColors: { + default: '#ff0', // 指定默认填充色 + c1: [0, 0.3 '#f00'], + defaultFill: '#ff0' // 等价写法 + }, +}); +``` diff --git a/packages/site/examples/tile/raster/demo/tiff.js b/packages/site/examples/tile/raster/demo/tiff.js index c300ab3eff..0bf77f8168 100644 --- a/packages/site/examples/tile/raster/demo/tiff.js +++ b/packages/site/examples/tile/raster/demo/tiff.js @@ -33,28 +33,6 @@ const colorList = [ '#616161', // Clouds '#616161' ]; -const positions = [ - 0.0, - 0.1, - 0.1, - 0.2, - 0.2, - 0.3, - 0.3, - 0.4, - 0.4, - 0.5, - 0.5, - 0.6, - 0.6, - 0.7, - 0.7, - 0.8, - 0.8, - 0.9, - 0.9, - 1.0 -]; const scene = new Scene({ id: 'map', @@ -100,8 +78,16 @@ scene.on('loaded', () => { domain: [ 0.001, 11.001 ], clampLow: false, rampColors: { - colors: colorList, - positions + 'Water': '#419bdf', + 'Tree': '#397d49', + 'Grass': '#88b053', + 'vegetation': '#7a87c6', + 'Crops': '#e49635', + 'shrub': '#dfc35a', + 'Built Area': '#c4281b', + 'Bare ground': '#a59b8f', + 'Snow': '#a8ebff', + 'Clouds': '#616161', } }); diff --git a/packages/utils/__tests__/color.spec.ts b/packages/utils/__tests__/color.spec.ts new file mode 100644 index 0000000000..80ee9fbcf3 --- /dev/null +++ b/packages/utils/__tests__/color.spec.ts @@ -0,0 +1,111 @@ +import { + generateColorRamp, + generateColorRampKey, + formatCategory, + IColorRamp, +} from '../src/color'; +const colors = [ + '#419bdf', // Water + '#419bdf', + + '#397d49', // Tree + '#397d49', + + '#88b053', // Grass + '#88b053', + + '#7a87c6', // vegetation + '#7a87c6', + + '#e49635', // Crops + '#e49635', + + '#dfc35a', // shrub + '#dfc35a', + + '#c4281b', // Built Area + '#c4281b', + + '#a59b8f', // Bare ground + '#a59b8f', + + '#a8ebff', // Snow + '#a8ebff', + + '#616161', // Clouds + '#616161' +]; +const positions = [ + 0.0, + 0.1, + 0.1, + 0.2, + 0.2, + 0.3, + 0.3, + 0.4, + 0.4, + 0.5, + 0.5, + 0.6, + 0.6, + 0.7, + 0.7, + 0.8, + 0.8, + 0.9, + 0.9, + 1.0 +]; +const rampColors = { + 'Water': '#419bdf', + 'Tree': '#397d49', + 'Grass': '#88b053', + 'vegetation': '#7a87c6', + 'Crops': '#e49635', + 'shrub': '#dfc35a', + 'Built Area': '#c4281b', + 'Bare ground': '#a59b8f', + 'Snow': '#a8ebff', + 'Clouds': '#616161', +} as IColorRamp; +describe('generateColorTexture', () => { + it('key', () => { + const key1 = generateColorRampKey(rampColors); + const key2 = generateColorRampKey({ + colors, + positions + }) + + expect(key1).toEqual('#419bdf_#397d49_#88b053_#7a87c6_#e49635_#dfc35a_#c4281b_#a59b8f_#a8ebff_#616161_#616161_0_0.1_0.1_0.2_0.2_0.30000000000000004_0.30000000000000004_0.4_0.4_0.5_0.5_0.6000000000000001_0.6000000000000001_0.7000000000000001_0.7000000000000001_0.8_0.8_0.9_0.9_1_0.9_1'); + expect(key2).toEqual('#419bdf_#419bdf_#397d49_#397d49_#88b053_#88b053_#7a87c6_#7a87c6_#e49635_#e49635_#dfc35a_#dfc35a_#c4281b_#c4281b_#a59b8f_#a59b8f_#a8ebff_#a8ebff_#616161_#616161_0_0.1_0.1_0.2_0.2_0.3_0.3_0.4_0.4_0.5_0.5_0.6_0.6_0.7_0.7_0.8_0.8_0.9_0.9_1'); + }); + + it('texture', () => { + const d1 = generateColorRamp({ + c1: '#f00', + c2: '#ff0' + }) + expect(d1.data.length).toEqual(1024); + }) + + it('cat', () => { + const list = formatCategory({ + water: '#f00', + land: [0.2, 0.3, '#ff0'], + land2: [0.35, 0.4, '#ddd'], + wood: '#0f0', + city: '#ccc', + city2: [0.9, 0.95, '#0ff'] + }) + expect(list).toEqual([ + [ 0, 0.2, '#f00' ], + [ 0.2, 0.3, '#ff0' ], + [ 0.3, 0.35, null ], + [ 0.35, 0.4, '#ddd' ], + [ 0.4, 0.65, '#0f0' ], + [ 0.65, 0.9, '#ccc' ], + [ 0.9, 1, '#0ff' ] + ]) + }) +}); diff --git a/packages/utils/src/color.ts b/packages/utils/src/color.ts index b917fbf26b..3bd8b802ef 100644 --- a/packages/utils/src/color.ts +++ b/packages/utils/src/color.ts @@ -1,9 +1,10 @@ import * as d3 from 'd3-color'; import { $window, isMini } from './mini-adapter'; export interface IColorRamp { - positions: number[]; - colors: string[]; + positions?: number[]; + colors?: string[]; weights?: number[]; + [key: string]: any; } export function isColor(str: any) { @@ -51,39 +52,129 @@ export interface IImagedata { height: number; } -export function generateColorRamp( - colorRamp: IColorRamp, -): ImageData | IImagedata { - let canvas = $window.document.createElement('canvas'); - let ctx = canvas.getContext('2d') as CanvasRenderingContext2D; - canvas.width = 256; - canvas.height = 1; - let data = null; +enum ColorRampType { + LINEAR = 'linear', + CAT = 'cat' +} - if (colorRamp.weights) { - // draw enum color - let count = 0; - colorRamp.weights.map((w, index) => { - const color = colorRamp.colors[index] || 'rgba(0, 0, 0, 0)'; - const stop = count + w; - ctx.fillStyle = color; - ctx.fillRect(count * 256, 0, stop * 256, 1); - count = stop; - }); +type ColorCategory = [number, number, string]; + +function getColorRampType(colorRamp: IColorRamp) { + if(colorRamp.colors && colorRamp.positions) { + return ColorRampType.LINEAR; } else { - // draw linear color - const gradient = ctx.createLinearGradient(0, 0, 256, 1); + return ColorRampType.CAT; + } +} - const min = colorRamp.positions[0]; - const max = colorRamp.positions[colorRamp.positions.length - 1]; - for (let i = 0; i < colorRamp.colors.length; ++i) { - const value = (colorRamp.positions[i] - min) / (max - min); - gradient.addColorStop(value, colorRamp.colors[i]); +function isValid(category: any) { + // valid category - [number, number, string?] || string + if(typeof category === 'string') return true; + if(Array.isArray(category) && category.length === 3) { + return typeof category[0] === 'number' && + typeof category[1] === 'number' && + typeof category[2] === 'string' + } + return false; +} + +export function formatCategory(colorRamp: IColorRamp) { + /** + * { + * a: '#f00', + * b: [0.2, 0.3, #ff0], + * c: [0.3, 0.4, #0f0], + * d: '#fff', + * e: #0ff + * } + */ + + const keywords = ['colors', 'position', 'default']; + + /** categories + * [ + * [0, 0.2 #f00], + * [0.2, 0.3, #ff0], + * [0.3, 0.4, #ff0], + * [0.4, 1.0, #fff, #0ff] + * ] + */ + const categories: any[]= []; + let range = 0; + Object.keys(colorRamp) + .filter(key => keywords.indexOf(key) < 0) + .filter(key => isValid(colorRamp[key])) + .forEach((key) => { + const category = colorRamp[key]; + const last = categories[categories.length - 1]; + if(Array.isArray(category)) { + // category === [number, number, color] + if(last) { + if(last[1] === -1) { + last[1] = category[0]; + } else if(last[1] < category[0]){ + categories.push([last[1], category[0], null]) + } } - ctx.fillStyle = gradient; - ctx.fillRect(0, 0, 256, 1); + range = category[1]; + categories.push(category); + } else { + // category === color + if(last && last[1] === -1) { + last.push(category) + } else { + categories.push([range, -1, category]) + } + } + return category + }) + + // incase all raw color + if(categories.length > 0) { + categories[categories.length - 1][1] = 1; } + const validCategories: ColorCategory[] = []; + categories.forEach(category => { + const [start, end, ...colors] = category; + const step = (end - start)/colors.length; + colors.forEach((color: string, index: number) => { + validCategories.push([start + index * step, start + (index + 1) * step, color]) + }) + }) + return validCategories; +} + +// draw cat color +function drawCat(colorRamp: IColorRamp, ctx: CanvasRenderingContext2D) { + // 色带未指定部分的默认颜色 + const defaultColor = colorRamp.default || '#fff'; + const canvasWidth = 256; + const categories = formatCategory(colorRamp); + categories.forEach(([start, end, color]) => { + const drawColor = isColor(color) ? color : defaultColor; + ctx.fillStyle = drawColor; + ctx.fillRect(start * canvasWidth, 0, end * canvasWidth, 1); + }) +} + +// draw linear color +function drawLinear(colorRamp: IColorRamp, ctx: CanvasRenderingContext2D) { + const gradient = ctx.createLinearGradient(0, 0, 256, 1); + const positions = colorRamp.positions as number[]; + const colors = colorRamp.colors as string[]; + const min = positions[0]; + const max = positions[positions.length - 1]; + for (let i = 0; i < colors.length; ++i) { + const value = (positions[i] - min) / (max - min); + gradient.addColorStop(value, colors[i]); + } + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, 256, 1); +} + +function getColorData(ctx: CanvasRenderingContext2D) { + let data = null; if (!isMini) { data = ctx.getImageData(0, 0, 256, 1).data; // 使用 createImageData 替代 new ImageData、兼容 IE11 @@ -94,17 +185,69 @@ export function generateColorRamp( imageData.data[i + 2] = data[i + 2]; imageData.data[i + 3] = data[i + 3]; } - // @ts-ignore - canvas = null; - // @ts-ignore - ctx = null; return imageData; } else { data = new Uint8ClampedArray(ctx.getImageData(0, 0, 256, 1).data); - // @ts-ignore - canvas = null; - // @ts-ignore - ctx = null; return { data, width: 256, height: 1 }; } } + +export function generateColorRampKey(colorRamp: IColorRamp) { + const type = getColorRampType(colorRamp); + switch(type) { + case ColorRampType.CAT: + const defaultColor = colorRamp.default || '#fff'; + const categories = formatCategory(colorRamp); + const fields: string[] = []; + const values: string[] = []; + categories.forEach(category => { + const start = category[0]; + const end = category[1]; + const color = category[2]; + fields.push(String(start)); + fields.push(String(end)); + const drawColor = isColor(color) ? color : defaultColor; + values.push(drawColor) + }) + if(categories.length > 0) { + fields.push(String(categories[categories.length - 1][0])); + fields.push(String(categories[categories.length - 1][1])); + values.push(categories[categories.length - 1][2]); + } + return [...values, ...fields].join('_'); + case ColorRampType.LINEAR: + return `${colorRamp?.colors?.join('_')}_${colorRamp?.positions?.join('_')}`; + } + +} + +/** + * init color texture for data range + * @param colorRamp + * @returns + */ +export function generateColorRamp( + colorRamp: IColorRamp, +): ImageData | IImagedata { + let canvas = $window.document.createElement('canvas'); + const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; + canvas.width = 256; + canvas.height = 1; + + const type = getColorRampType(colorRamp); + switch(type) { + case ColorRampType.CAT: + drawCat(colorRamp, ctx); + break; + case ColorRampType.LINEAR: + drawLinear(colorRamp, ctx); + break; + } + + const colorData = getColorData(ctx); + + canvas.width = 0; + canvas.height = 0; + canvas = null; + return colorData; +}