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

Reply via email to