|
/*
|
* 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.
|
*/
|
|
import WeakMap from 'zrender/src/core/WeakMap';
|
import { PatternObject } from 'zrender/src/graphic/Pattern';
|
import LRU from 'zrender/src/core/LRU';
|
import {defaults, createCanvas, map, isArray} from 'zrender/src/core/util';
|
import {getLeastCommonMultiple} from './number';
|
import {createSymbol} from './symbol';
|
import ExtensionAPI from '../core/ExtensionAPI';
|
import type SVGPainter from 'zrender/src/svg/Painter';
|
import { brushSingle } from 'zrender/src/canvas/graphic';
|
import {DecalDashArrayX, DecalDashArrayY, InnerDecalObject, DecalObject} from './types';
|
|
const decalMap = new WeakMap<DecalObject, PatternObject>();
|
|
const decalCache = new LRU<HTMLCanvasElement | SVGElement>(100);
|
|
const decalKeys = [
|
'symbol', 'symbolSize', 'symbolKeepAspect',
|
'color', 'backgroundColor',
|
'dashArrayX', 'dashArrayY',
|
'maxTileWidth', 'maxTileHeight'
|
];
|
|
/**
|
* Create or update pattern image from decal options
|
*
|
* @param {InnerDecalObject | 'none'} decalObject decal options, 'none' if no decal
|
* @return {Pattern} pattern with generated image, null if no decal
|
*/
|
export function createOrUpdatePatternFromDecal(
|
decalObject: InnerDecalObject | 'none',
|
api: ExtensionAPI
|
): PatternObject {
|
if (decalObject === 'none') {
|
return null;
|
}
|
|
const dpr = api.getDevicePixelRatio();
|
const zr = api.getZr();
|
const isSVG = zr.painter.type === 'svg';
|
|
if (decalObject.dirty) {
|
decalMap.delete(decalObject);
|
}
|
|
const oldPattern = decalMap.get(decalObject);
|
if (oldPattern) {
|
return oldPattern;
|
}
|
|
const decalOpt = defaults(decalObject, {
|
symbol: 'rect',
|
symbolSize: 1,
|
symbolKeepAspect: true,
|
color: 'rgba(0, 0, 0, 0.2)',
|
backgroundColor: null,
|
dashArrayX: 5,
|
dashArrayY: 5,
|
rotation: 0,
|
maxTileWidth: 512,
|
maxTileHeight: 512
|
} as DecalObject);
|
if (decalOpt.backgroundColor === 'none') {
|
decalOpt.backgroundColor = null;
|
}
|
|
const pattern: PatternObject = { repeat: 'repeat' } as PatternObject;
|
setPatternnSource(pattern);
|
pattern.rotation = decalOpt.rotation;
|
pattern.scaleX = pattern.scaleY = isSVG ? 1 : 1 / dpr;
|
|
decalMap.set(decalObject, pattern);
|
|
decalObject.dirty = false;
|
|
return pattern;
|
|
function setPatternnSource(pattern: PatternObject) {
|
const keys = [dpr];
|
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);
|
}
|
|
let cacheKey;
|
if (isValidKey) {
|
cacheKey = keys.join(',') + (isSVG ? '-svg' : '');
|
const cache = decalCache.get(cacheKey);
|
if (cache) {
|
isSVG ? pattern.svgElement = cache as SVGElement
|
: pattern.image = cache as HTMLCanvasElement;
|
}
|
}
|
|
const dashArrayX = normalizeDashArrayX(decalOpt.dashArrayX);
|
const dashArrayY = normalizeDashArrayY(decalOpt.dashArrayY);
|
const symbolArray = normalizeSymbolArray(decalOpt.symbol);
|
const lineBlockLengthsX = getLineBlockLengthX(dashArrayX);
|
const lineBlockLengthY = getLineBlockLengthY(dashArrayY);
|
|
const canvas = !isSVG && createCanvas();
|
const svgRoot = isSVG && (zr.painter as SVGPainter).createSVGElement('g');
|
const pSize = getPatternSize();
|
let ctx: CanvasRenderingContext2D;
|
if (canvas) {
|
canvas.width = pSize.width * dpr;
|
canvas.height = pSize.height * dpr;
|
ctx = canvas.getContext('2d');
|
}
|
brushDecal();
|
|
if (isValidKey) {
|
decalCache.put(cacheKey, canvas || svgRoot);
|
}
|
|
pattern.image = canvas;
|
pattern.svgElement = svgRoot;
|
pattern.svgWidth = pSize.width;
|
pattern.svgHeight = pSize.height;
|
|
/**
|
* Get minumum length that can make a repeatable pattern.
|
*
|
* @return {Object} pattern width and height
|
*/
|
function getPatternSize(): {
|
width: number,
|
height: 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`
|
* |--- --- --- |--- --- ...
|
* |-- -- -- -- -- |-- -- -- ...
|
*/
|
let width = 1;
|
for (let i = 0, xlen = lineBlockLengthsX.length; i < xlen; ++i) {
|
width = getLeastCommonMultiple(width, lineBlockLengthsX[i]);
|
}
|
|
let symbolRepeats = 1;
|
for (let i = 0, xlen = symbolArray.length; i < xlen; ++i) {
|
symbolRepeats = getLeastCommonMultiple(symbolRepeats, symbolArray[i].length);
|
}
|
width *= symbolRepeats;
|
|
const height = lineBlockLengthY * lineBlockLengthsX.length * symbolArray.length;
|
|
if (__DEV__) {
|
const warn = (attrName: string) => {
|
/* eslint-disable-next-line */
|
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');
|
}
|
}
|
|
return {
|
width: Math.max(1, Math.min(width, decalOpt.maxTileWidth)),
|
height: Math.max(1, Math.min(height, decalOpt.maxTileHeight))
|
};
|
}
|
|
function brushDecal() {
|
if (ctx) {
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
if (decalOpt.backgroundColor) {
|
ctx.fillStyle = decalOpt.backgroundColor;
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
}
|
}
|
|
let ySum = 0;
|
for (let i = 0; i < dashArrayY.length; ++i) {
|
ySum += dashArrayY[i];
|
}
|
if (ySum <= 0) {
|
// dashArrayY is 0, draw nothing
|
return;
|
}
|
|
let y = -lineBlockLengthY;
|
let yId = 0;
|
let yIdTotal = 0;
|
let xId0 = 0;
|
while (y < pSize.height) {
|
if (yId % 2 === 0) {
|
const symbolYId = (yIdTotal / 2) % symbolArray.length;
|
let x = 0;
|
let xId1 = 0;
|
let xId1Total = 0;
|
while (x < pSize.width * 2) {
|
let xSum = 0;
|
for (let i = 0; i < dashArrayX[xId0].length; ++i) {
|
xSum += dashArrayX[xId0][i];
|
}
|
if (xSum <= 0) {
|
// Skip empty line
|
break;
|
}
|
|
// 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;
|
const symbolXId = (xId1Total / 2) % symbolArray[symbolYId].length;
|
|
brushSymbol(left, top, width, height, symbolArray[symbolYId][symbolXId]);
|
}
|
|
x += dashArrayX[xId0][xId1];
|
++xId1Total;
|
++xId1;
|
if (xId1 === dashArrayX[xId0].length) {
|
xId1 = 0;
|
}
|
}
|
|
++xId0;
|
if (xId0 === dashArrayX.length) {
|
xId0 = 0;
|
}
|
}
|
y += dashArrayY[yId];
|
|
++yIdTotal;
|
++yId;
|
if (yId === dashArrayY.length) {
|
yId = 0;
|
}
|
}
|
|
function brushSymbol(x: number, y: number, width: number, height: number, symbolType: string) {
|
const scale = isSVG ? 1 : dpr;
|
const symbol = createSymbol(
|
symbolType,
|
x * scale,
|
y * scale,
|
width * scale,
|
height * scale,
|
decalOpt.color,
|
decalOpt.symbolKeepAspect
|
);
|
if (isSVG) {
|
svgRoot.appendChild((zr.painter as SVGPainter).paintOne(symbol));
|
}
|
else {
|
// Paint to canvas for all other renderers.
|
brushSingle(ctx, symbol);
|
}
|
}
|
}
|
}
|
|
}
|
|
/**
|
* Convert symbol array into normalized array
|
*
|
* @param {string | (string | string[])[]} symbol symbol input
|
* @return {string[][]} normolized symbol array
|
*/
|
function normalizeSymbolArray(symbol: string | (string | string[])[]): string[][] {
|
if (!symbol || (symbol as string[]).length === 0) {
|
return [['rect']];
|
}
|
if (typeof symbol === 'string') {
|
return [[symbol]];
|
}
|
|
let isAllString = true;
|
for (let i = 0; i < symbol.length; ++i) {
|
if (typeof symbol[i] !== 'string') {
|
isAllString = false;
|
break;
|
}
|
}
|
if (isAllString) {
|
return normalizeSymbolArray([symbol as string[]]);
|
}
|
|
const result: string[][] = [];
|
for (let i = 0; i < symbol.length; ++i) {
|
if (typeof symbol[i] === 'string') {
|
result.push([symbol[i] as string]);
|
}
|
else {
|
result.push(symbol[i] as string[]);
|
}
|
}
|
return result;
|
}
|
|
/**
|
* Convert dash input into dashArray
|
*
|
* @param {DecalDashArrayX} dash dash input
|
* @return {number[][]} normolized dash array
|
*/
|
function normalizeDashArrayX(dash: DecalDashArrayX): number[][] {
|
if (!dash || (dash as number[]).length === 0) {
|
return [[0, 0]];
|
}
|
if (typeof dash === 'number') {
|
const dashValue = Math.ceil(dash);
|
return [[dashValue, dashValue]];
|
}
|
|
/**
|
* [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') {
|
const dashValue = Math.ceil(dash[i] as number);
|
result.push([dashValue, dashValue]);
|
}
|
else {
|
const dashValue = 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);
|
}
|
}
|
}
|
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') {
|
const dashValue = Math.ceil(dash);
|
return [dashValue, dashValue];
|
}
|
|
const dashValue = map(dash as number[], n => Math.ceil(n));
|
return dash.length % 2 ? dashValue.concat(dashValue) : dashValue;
|
}
|
|
/**
|
* 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;
|
}
|