decal.ts 12.3 KB
Newer Older
O
Ovilia 已提交
1 2 3
import WeakMap from 'zrender/src/core/WeakMap';
import {DecalObject, DecalDashArrayX, DecalDashArrayY} from 'zrender/src/graphic/Decal';
import Pattern from 'zrender/src/graphic/Pattern';
O
Ovilia 已提交
4
import LRU from 'zrender/src/core/LRU';
O
Ovilia 已提交
5
import {brushSingle} from 'zrender/src/canvas/graphic';
O
Ovilia 已提交
6
import {defaults, createCanvas, map, isArray} from 'zrender/src/core/util';
O
Ovilia 已提交
7
import {getLeastCommonMultiple} from './number';
O
Ovilia 已提交
8
import {createSymbol} from './symbol';
O
Ovilia 已提交
9
import {util} from 'zrender/src/export';
O
Ovilia 已提交
10 11 12

const decalMap = new WeakMap<DecalObject, Pattern>();

O
Ovilia 已提交
13 14 15 16 17 18 19 20 21
const decalCache = new LRU<HTMLCanvasElement>(100);

const decalKeys = [
    'symbol', 'symbolSize', 'symbolKeepAspect',
    'color', 'backgroundColor',
    'dashArrayX', 'dashArrayY', 'dashLineOffset',
    'maxTileWidth', 'maxTileHeight'
];

O
Ovilia 已提交
22 23 24 25 26 27 28
/**
 * Create or update pattern image from decal options
 *
 * @param {DecalObject} decalObject decal options
 * @return {Pattern} pattern with generated image
 */
export function createOrUpdatePatternFromDecal(
O
Ovilia 已提交
29 30
    decalObject: DecalObject,
    dpr: number
O
Ovilia 已提交
31
): Pattern {
O
Ovilia 已提交
32 33
    dpr = dpr || 1;

O
Ovilia 已提交
34 35 36 37
    if (decalObject.dirty) {
        decalMap.delete(decalObject);
    }

O
Ovilia 已提交
38 39 40 41 42 43
    const oldPattern = decalMap.get(decalObject);
    if (oldPattern) {
        return oldPattern;
    }

    const decalOpt = defaults(decalObject, {
O
Ovilia 已提交
44
        symbol: 'rect',
O
Ovilia 已提交
45 46
        symbolSize: 1,
        symbolKeepAspect: true,
O
Ovilia 已提交
47
        color: 'rgba(0, 0, 0, 0.2)',
O
Ovilia 已提交
48 49 50 51
        backgroundColor: null,
        dashArrayX: 5,
        dashArrayY: 5,
        dashLineOffset: 0,
O
Ovilia 已提交
52
        rotation: 0,
O
Ovilia 已提交
53 54 55 56 57 58 59
        maxTileWidth: 512,
        maxTileHeight: 512
    } as DecalObject);
    if (decalOpt.backgroundColor === 'none') {
        decalOpt.backgroundColor = null;
    }

O
Ovilia 已提交
60
    const canvas = getPatternCanvas();
O
Ovilia 已提交
61 62 63 64
    const pattern = new Pattern(canvas, 'repeat');
    pattern.rotation = decalOpt.rotation;
    pattern.scaleX = pattern.scaleY = 1 / dpr;

O
Ovilia 已提交
65
    decalMap.set(decalObject, pattern);
O
Ovilia 已提交
66

O
Ovilia 已提交
67
    decalObject.dirty = false;
O
Ovilia 已提交
68

O
Ovilia 已提交
69
    return pattern;
O
Ovilia 已提交
70

O
Ovilia 已提交
71
    function getPatternCanvas(): HTMLCanvasElement {
O
Ovilia 已提交
72
        const keys = [dpr];
O
Ovilia 已提交
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
        let isValidKey = true;
        for (let i = 0; i < decalKeys.length; ++i) {
            const value = (decalOpt as any)[decalKeys[i]];
            const valueType = typeof value;
            if (value != null
                && !isArray(value)
                && valueType !== 'string'
                && valueType !== 'number'
                && valueType !== 'boolean'
            ) {
                isValidKey = false;
                break;
            }
            keys.push(value);
        }
O
Ovilia 已提交
88

O
Ovilia 已提交
89 90 91 92 93 94 95 96
        let cacheKey;
        if (isValidKey) {
            cacheKey = keys.join(',');
            const cache = decalCache.get(cacheKey);
            if (cache) {
                return cache;
            }
        }
O
Ovilia 已提交
97

O
Ovilia 已提交
98 99 100 101
        const dashArrayX = normalizeDashArrayX(decalOpt.dashArrayX);
        const dashArrayY = normalizeDashArrayY(decalOpt.dashArrayY);
        const lineBlockLengthsX = getLineBlockLengthX(dashArrayX);
        const lineBlockLengthY = getLineBlockLengthY(dashArrayY);
O
Ovilia 已提交
102

O
Ovilia 已提交
103 104
        const canvas = createCanvas();
        const pSize = getPatternSize();
O
Ovilia 已提交
105 106
        canvas.width = pSize.width * dpr;
        canvas.height = pSize.height * dpr;
O
Ovilia 已提交
107
        brushDecal();
O
Ovilia 已提交
108

O
Ovilia 已提交
109 110
        if (isValidKey) {
            decalCache.put(cacheKey, canvas);
O
Ovilia 已提交
111
        }
O
Ovilia 已提交
112 113
        return canvas;

O
Ovilia 已提交
114
        /**
O
Ovilia 已提交
115
         * Get minumum length that can make a repeatable pattern.
O
Ovilia 已提交
116
         *
O
Ovilia 已提交
117
         * @return {Object} pattern width and height
O
Ovilia 已提交
118
         */
O
Ovilia 已提交
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
        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;
O
Ovilia 已提交
164

O
Ovilia 已提交
165 166 167 168 169 170 171 172 173 174 175
            if (__DEV__) {
                const warn = (attrName: string) => {
                    console.warn(`Calculated decal size is greater than ${attrName} due to decal option settings so ${attrName} is used for the decal size. Please consider changing the decal option to make a smaller decal or set ${attrName} to be larger to avoid incontinuity.`);
                };
                if (width > decalOpt.maxTileWidth) {
                    warn('maxTileWidth');
                }
                if (height > decalOpt.maxTileHeight) {
                    warn('maxTileHeight');
                }
            }
O
Ovilia 已提交
176

O
Ovilia 已提交
177 178 179 180 181
            return {
                width: Math.max(1, Math.min(width, decalOpt.maxTileWidth)),
                height: Math.max(1, Math.min(height, decalOpt.maxTileHeight)),
                lines: columns
            };
O
Ovilia 已提交
182 183
        }

O
Ovilia 已提交
184 185
        function brushDecal() {
            const ctx = canvas.getContext('2d');
O
Ovilia 已提交
186
            ctx.clearRect(0, 0, canvas.width, canvas.height);
O
Ovilia 已提交
187 188
            if (decalOpt.backgroundColor) {
                ctx.fillStyle = decalOpt.backgroundColor;
O
Ovilia 已提交
189
                ctx.fillRect(0, 0, canvas.width, canvas.height);
O
Ovilia 已提交
190 191 192 193
            }

            ctx.fillStyle = decalOpt.color;

O
Ovilia 已提交
194 195 196 197 198 199 200 201 202
            let ySum = 0;
            for (let i = 0; i < dashArrayY.length; ++i) {
                ySum += dashArrayY[i];
            }
            if (ySum <= 0) {
                // dashArrayY is 0, draw nothing
                return;
            }

O
Ovilia 已提交
203 204 205 206 207 208 209 210 211 212 213 214
            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) {
O
Ovilia 已提交
215 216 217 218 219 220 221 222 223
                        let xSum = 0;
                        for (let i = 0; i < dashArrayX[xId0].length; ++i) {
                            xSum += dashArrayX[xId0][i];
                        }
                        if (xSum <= 0) {
                            // Skip empty line
                            break;
                        }

O
Ovilia 已提交
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
                        // E.g., [15, 5, 20, 5] draws only for 15 and 20
                        if (xId1 % 2 === 0) {
                            const size = (1 - decalOpt.symbolSize) * 0.5;
                            const left = x + dashArrayX[xId0][xId1] * size;
                            const top = y + dashArrayY[yId] * size;
                            const width = dashArrayX[xId0][xId1] * decalOpt.symbolSize;
                            const height = dashArrayY[yId] * decalOpt.symbolSize;
                            brushSymbol(left, top, width, height);
                        }

                        x += dashArrayX[xId0][xId1];
                        ++xId1;
                        if (xId1 === dashArrayX[xId0].length) {
                            xId1 = 0;
                        }
O
Ovilia 已提交
239 240
                    }

O
Ovilia 已提交
241 242 243
                    ++xId0;
                    if (xId0 === dashArrayX.length) {
                        xId0 = 0;
O
Ovilia 已提交
244 245 246
                    }
                }

O
Ovilia 已提交
247 248 249 250 251 252
                ++yCnt;
                y += dashArrayY[yId];

                ++yId;
                if (yId === dashArrayY.length) {
                    yId = 0;
O
Ovilia 已提交
253 254 255
                }
            }

O
Ovilia 已提交
256
            function brushSymbol(x: number, y: number, width: number, height: number) {
O
Ovilia 已提交
257 258 259 260 261 262 263
                const symbol = createSymbol(
                    decalOpt.symbol,
                    x * dpr,
                    y * dpr,
                    width * dpr,
                    height * dpr
                );
O
Ovilia 已提交
264 265
                symbol.style.fill = decalOpt.color;
                brushSingle(ctx, symbol);
O
Ovilia 已提交
266 267 268 269 270 271 272 273 274 275 276 277 278
            }
        }
    }

}

/**
 * Convert dash input into dashArray
 *
 * @param {DecalDashArrayX} dash dash input
 * @return {number[][]} normolized dash array
 */
function normalizeDashArrayX(dash: DecalDashArrayX): number[][] {
O
Ovilia 已提交
279
    if (!dash || (dash as number[]).length === 0) {
O
Ovilia 已提交
280 281 282
        return [[0, 0]];
    }
    if (typeof dash === 'number') {
O
Ovilia 已提交
283 284
        const dashValue = Math.ceil(dash);
        return [[dashValue, dashValue]];
O
Ovilia 已提交
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
    }

    /**
     * [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') {
O
Ovilia 已提交
305 306
            const dashValue = Math.ceil(dash[i] as number);
            result.push([dashValue, dashValue]);
O
Ovilia 已提交
307 308
        }
        else {
O
Ovilia 已提交
309 310 311 312 313 314 315 316 317
            const dashValue = util.map(dash[i] as number[], n => Math.ceil(n));
            if (dashValue.length % 2 === 1) {
                // [4, 2, 1] means |----  -    -- |----  -    -- |
                // so normalize it to be [4, 2, 1, 4, 2, 1]
                result.push(dashValue.concat(dashValue));
            }
            else {
                result.push(dashValue);
            }
O
Ovilia 已提交
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
        }
    }
    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') {
O
Ovilia 已提交
334 335
        const dashValue = Math.ceil(dash);
        return [dashValue, dashValue];
O
Ovilia 已提交
336
    }
O
Ovilia 已提交
337 338 339

    const dashValue = util.map(dash as number[], n => Math.ceil(n));
    return dash.length % 2 ? dashValue.concat(dashValue) : dashValue;
O
Ovilia 已提交
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
}

/**
 * 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;
}
O
Ovilia 已提交
368 369 370 371 372 373 374 375

function fixStartPosition(lineOffset: number, blockLength: number) {
    let start = lineOffset || 0;
    while (start > 0) {
        start -= blockLength;
    }
    return start;
}