This is an automated email from the ASF dual-hosted git repository. ovilia pushed a commit to branch feat-decal in repository https://gitbox.apache.org/repos/asf/incubator-echarts.git
commit 9ec0aaad6552b0ed434ac8bfde7519801332fc5e Author: Ovilia <zwl.s...@gmail.com> AuthorDate: Fri Sep 18 13:38:17 2020 +0800 feat(decal): decal for basic charts --- src/model/mixin/itemStyle.ts | 2 + src/model/mixin/makeStyleMapper.ts | 10 +- src/util/decal.ts | 285 +++++++++++++++++++++++++++++++++++++ src/util/number.ts | 29 ++++ test/decal.html | 89 ++++++++++++ 5 files changed, 413 insertions(+), 2 deletions(-) diff --git a/src/model/mixin/itemStyle.ts b/src/model/mixin/itemStyle.ts index 959ecc4..14f3c1d 100644 --- a/src/model/mixin/itemStyle.ts +++ b/src/model/mixin/itemStyle.ts @@ -25,6 +25,7 @@ import { PathStyleProps } from 'zrender/src/graphic/Path'; export const ITEM_STYLE_KEY_MAP = [ ['fill', 'color'], ['stroke', 'borderColor'], + ['decal'], ['lineWidth', 'borderWidth'], ['opacity'], ['shadowBlur'], @@ -42,6 +43,7 @@ const getItemStyle = makeStyleMapper(ITEM_STYLE_KEY_MAP); type ItemStyleKeys = 'fill' | 'stroke' + | 'decal' | 'lineWidth' | 'opacity' | 'shadowBlur' diff --git a/src/model/mixin/makeStyleMapper.ts b/src/model/mixin/makeStyleMapper.ts index e08e42a..4ed0f5e 100644 --- a/src/model/mixin/makeStyleMapper.ts +++ b/src/model/mixin/makeStyleMapper.ts @@ -20,9 +20,10 @@ // TODO Parse shadow style // TODO Only shallow path support import * as zrUtil from 'zrender/src/core/util'; +import {Dictionary} from 'zrender/src/core/types'; +import {PathStyleProps} from 'zrender/src/graphic/Path'; import Model from '../Model'; -import { Dictionary } from 'zrender/src/core/types'; -import { PathStyleProps } from 'zrender/src/graphic/Path'; +import {createOrUpdatePatternFromDecal} from '../../util/decal'; export default function (properties: readonly string[][], ignoreParent?: boolean) { // Normalize @@ -48,6 +49,11 @@ export default function (properties: readonly string[][], ignoreParent?: boolean style[properties[i][0]] = val; } } + + if (style.decal) { + createOrUpdatePatternFromDecal(style.decal); + } + // TODO Text or image? return style as PathStyleProps; }; diff --git a/src/util/decal.ts b/src/util/decal.ts new file mode 100644 index 0000000..4c77de7 --- /dev/null +++ b/src/util/decal.ts @@ -0,0 +1,285 @@ +import {DecalObject, DecalDashArrayX, DecalDashArrayY} from 'zrender/src/graphic/Decal'; +import Pattern, {PatternObject} from 'zrender/src/graphic/Pattern'; +import {defaults, createCanvas, map} from 'zrender/src/core/util'; +import {getLeastCommonMultiple} from './number'; + +/** + * Create or update pattern image from decal options + * + * @param {DecalObject} decalObject decal options + * @return {Pattern} pattern with generated image + */ +export function createOrUpdatePatternFromDecal( + decalObject: DecalObject +): PatternObject { + if (decalObject.__pattern) { + return decalObject.__pattern; + } + + const decalOpt = defaults({ + shape: 'rect', + symbolSize: 1, + symbolKeepAspect: true, + color: 'rgba(255, 255, 255, 0.4)', + backgroundColor: null, + dashArrayX: 10, + dashArrayY: 10, + dashLineOffset: 0, + rotation: Math.PI / 4, + maxTileWidth: 512, + maxTileHeight: 512 + } as DecalObject, decalObject); + if (decalOpt.backgroundColor === 'none') { + decalOpt.backgroundColor = null; + } + + const dashArrayX = normalizeDashArrayX(decalOpt.dashArrayX); + const dashArrayY = normalizeDashArrayY(decalOpt.dashArrayY); + + const lineBlockLengthsX = getLineBlockLengthX(dashArrayX); + const lineBlockLengthY = getLineBlockLengthY(dashArrayY); + + const canvas = createCanvas(); + const pSize = getPatternSize(); + + canvas.width = pSize.width; + canvas.height = pSize.height; + canvas.style.width = canvas.width + 'px'; + canvas.style.height = canvas.height + 'px'; + + const ctx = canvas.getContext('2d'); + + brush(); + + const base64 = canvas.toDataURL(); + + const pattern = new Pattern(base64, 'repeat', decalOpt.rotation); + decalObject.__pattern = pattern; + + decalObject.dirty = function () { + console.log('dirty'); + }; + + return pattern; + + /** + * Get minumum length that can make a repeatable pattern. + * + * @return {Object} pattern width and height + */ + function getPatternSize() + : { + width: number, + height: number, + lines: number + } + { + /** + * For example, if dash is [[3, 2], [2, 1]] for X, it looks like + * |--- --- --- --- --- ... + * |-- -- -- -- -- -- -- -- ... + * |--- --- --- --- --- ... + * |-- -- -- -- -- -- -- -- ... + * So the minumum length of X is 15, + * which is the least common multiple of `3 + 2` and `2 + 1` + * |--- --- --- |--- --- ... + * |-- -- -- -- -- |-- -- -- ... + * + * When consider with dashLineOffset, it means the `n`th line has the offset + * of `n * dashLineOffset`. + * For example, if dash is [[3, 1], [1, 1]] and dashLineOffset is 3, + * and use `=` for the start to make it clear, it looks like + * |=-- --- --- --- --- -... + * | - = - - - - - - - - ... + * |- --- =-- --- --- -- ... + * | - - - - = - - - - - ... + * |--- --- --- =-- --- -... + * | - - - - - - - = - - ... + * In this case, the minumum length is 12, which is the least common + * multiple of `3 + 1`, `1 + 1` and `3 * 2` where `2` is xlen + * |=-- --- --- |--- --- -... + * | - = - - - -| - - - - ... + * |- --- =-- --|- --- -- ... + * | - - - - = -| - - - - ... + */ + const offsetMultipleX = decalOpt.dashLineOffset || 1; + let width = 1; + for (let i = 0, xlen = lineBlockLengthsX.length; i < xlen; ++i) { + const x = getLeastCommonMultiple(offsetMultipleX * xlen, lineBlockLengthsX[i]); + width = getLeastCommonMultiple(width, x); + } + const columns = decalOpt.dashLineOffset + ? width / offsetMultipleX + : 2; + let height = lineBlockLengthY * columns; + + return { + width: Math.max(1, Math.min(width, decalOpt.maxTileWidth)), + height: Math.max(1, Math.min(height, decalOpt.maxTileHeight)), + lines: columns + }; + } + + function fixStartPosition(lineOffset: number, blockLength: number) { + let start = lineOffset || 0; + while (start > 0) { + start -= blockLength; + } + return start; + } + + function brush() { + ctx.clearRect(0, 0, pSize.width, pSize.height); + if (decalOpt.backgroundColor) { + ctx.fillStyle = decalOpt.backgroundColor; + ctx.fillRect(0, 0, pSize.width, pSize.height); + } + + ctx.fillStyle = decalOpt.color; + + let yCnt = 0; + let y = -pSize.lines * lineBlockLengthY; + let yId = 0; + let xId0 = 0; + while (y < pSize.height) { + if (yId % 2 === 0) { + let x = fixStartPosition( + decalOpt.dashLineOffset * (yCnt - pSize.lines) / 2, + lineBlockLengthsX[0] + ); + let xId1 = 0; + while (x < pSize.width * 2) { + if (xId1 % 2 === 0) { + // E.g., [15, 5, 20, 5] draws only for 15 and 20 + // brushShape(x, y, dashArrayX[xId0][xId1], dashArrayY[yId]); + ctx.fillRect(x, y, dashArrayX[xId0][xId1], dashArrayY[yId]); + } + + x += dashArrayX[xId0][xId1]; + ++xId1; + if (xId1 === dashArrayX[xId0].length) { + xId1 = 0; + } + } + + ++xId0; + if (xId0 === dashArrayX.length) { + xId0 = 0; + } + } + + ++yCnt; + y += dashArrayY[yId]; + + ++yId; + if (yId === dashArrayY.length) { + yId = 0; + } + } + console.log(ctx.canvas.toDataURL()) + + // ctx.strokeStyle = 'red'; + // ctx.strokeRect(0, 0, pSize.width, pSize.height); + } + + function brushShape(x: number, y: number, width: number, height: number) { + if (decalOpt.image) { + + return; + } + + // switch (decalOpt.shape) { + // case '' + // } + } + +} + +/** + * Convert dash input into dashArray + * + * @param {DecalDashArrayX} dash dash input + * @return {number[][]} normolized dash array + */ +function normalizeDashArrayX(dash: DecalDashArrayX): number[][] { + if (!dash || typeof dash === 'object' && dash.length === 0) { + return [[0, 0]]; + } + if (typeof dash === 'number') { + return [[dash, dash]]; + } + + /** + * [20, 5] should be normalized into [[20, 5]], + * while [20, [5, 10]] should be normalized into [[20, 20], [5, 10]] + */ + let isAllNumber = true; + for (let i = 0; i < dash.length; ++i) { + if (typeof dash[i] !== 'number') { + isAllNumber = false; + break; + } + } + if (isAllNumber) { + return normalizeDashArrayX([dash as number[]]); + } + + const result: number[][] = []; + for (let i = 0; i < dash.length; ++i) { + if (typeof dash[i] === 'number') { + result.push([dash[i] as number, dash[i] as number]); + } + else if ((dash[i] as number[]).length % 2 === 1) { + // [4, 2, 1] means |---- - -- |---- - -- | + // so normalize it to be [4, 2, 1, 4, 2, 1] + result.push((dash[i] as number[]).concat(dash[i])); + } + else { + result.push((dash[i] as number[]).slice()); + } + } + return result; +} + +/** + * Convert dash input into dashArray + * + * @param {DecalDashArrayY} dash dash input + * @return {number[]} normolized dash array + */ +function normalizeDashArrayY(dash: DecalDashArrayY): number[] { + if (!dash || typeof dash === 'object' && dash.length === 0) { + return [0, 0]; + } + if (typeof dash === 'number') { + return [dash, dash]; + } + return dash.length % 2 ? dash.concat(dash) : dash.slice(); +} + +/** + * Get block length of each line. A block is the length of dash line and space. + * For example, a line with [4, 1] has a dash line of 4 and a space of 1 after + * that, so the block length of this line is 5. + * + * @param {number[][]} dash dash arrary of X or Y + * @return {number[]} block length of each line + */ +function getLineBlockLengthX(dash: number[][]): number[] { + return map(dash, function (line) { + return getLineBlockLengthY(line); + }); +} + +function getLineBlockLengthY(dash: number[]): number { + let blockLength = 0; + for (let i = 0; i < dash.length; ++i) { + blockLength += dash[i]; + } + if (dash.length % 2 === 1) { + // [4, 2, 1] means |---- - -- |---- - -- | + // So total length is (4 + 2 + 1) * 2 + return blockLength * 2; + } + return blockLength; +} diff --git a/src/util/number.ts b/src/util/number.ts index 865c4c7..735c5be 100644 --- a/src/util/number.ts +++ b/src/util/number.ts @@ -574,3 +574,32 @@ export function isNumeric(val: unknown): val is number { export function getRandomIdBase(): number { return Math.round(Math.random() * 9); } + +/** + * Get the greatest common dividor + * + * @param {number} a one number + * @param {number} b the other number + */ +export function getGreatestCommonDividor(a: number, b: number): number { + if (b === 0) { + return a; + } + return getGreatestCommonDividor(b, a % b); +} + +/** + * Get the least common multiple + * + * @param {number} a one number + * @param {number} b the other number + */ +export function getLeastCommonMultiple(a: number, b: number) { + if (a == null) { + return b; + } + if (b == null) { + return a; + } + return a * b / getGreatestCommonDividor(a, b); +} diff --git a/test/decal.html b/test/decal.html new file mode 100644 index 0000000..6740c8b --- /dev/null +++ b/test/decal.html @@ -0,0 +1,89 @@ +<!DOCTYPE html> +<!-- +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +--> + + +<html> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <script src="lib/esl.js"></script> + <script src="lib/config.js"></script> + <script src="lib/jquery.min.js"></script> + <script src="lib/facePrint.js"></script> + <script src="lib/testHelper.js"></script> + <!-- <script src="ut/lib/canteen.js"></script> --> + <link rel="stylesheet" href="lib/reset.css" /> + </head> + <body> + <style> + </style> + + + + <div id="main0"></div> + + + + + + + + + + <script> + require(['echarts'/*, 'map/js/china' */], function (echarts) { + var option; + + option = { + xAxis: { + type: 'category', + data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + }, + yAxis: { + type: 'value' + }, + series: [{ + data: [120, 200, 150, 80, 70, 110, 130], + type: 'bar', + itemStyle: { + decal: { + } + } + }] + }; + + + var chart = testHelper.create(echarts, 'main0', { + title: [ + 'Test Case Description of main0', + '(Muliple lines and **emphasis** are supported in description)' + ], + option: option + // height: 300, + // buttons: [{text: 'btn-txt', onclick: function () {}}], + // recordCanvas: true, + }); + }); + </script> + + + </body> +</html> + --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@echarts.apache.org For additional commands, e-mail: commits-h...@echarts.apache.org