| |
| /* |
| * 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 { ImagePatternObject, PatternObject, SVGPatternObject } from 'zrender/src/graphic/Pattern'; |
| import LRU from 'zrender/src/core/LRU'; |
| import {defaults, map, isArray, isString, isNumber} 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'; |
| import { SVGVNode } from 'zrender/src/svg/core'; |
| import { platformApi } from 'zrender/src/core/platform'; |
| |
| const decalMap = new WeakMap<DecalObject, PatternObject>(); |
| |
| const decalCache = new LRU<HTMLCanvasElement | SVGVNode>(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]]; |
| if (value != null |
| && !isArray(value) |
| && !isString(value) |
| && !isNumber(value) |
| && typeof value !== '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 as SVGPatternObject).svgElement = cache as SVGVNode |
| : (pattern as ImagePatternObject).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 && platformApi.createCanvas(); |
| const svgRoot: SVGVNode = isSVG && { |
| tag: 'g', |
| attrs: {}, |
| key: 'dcl', |
| children: [] |
| }; |
| 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 as ImagePatternObject).image = canvas; |
| (pattern as SVGPatternObject).svgElement = svgRoot; |
| (pattern as SVGPatternObject).svgWidth = pSize.width; |
| (pattern as SVGPatternObject).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) { |
| const symbolVNode = (zr.painter as SVGPainter).renderOneToVNode(symbol); |
| if (symbolVNode) { |
| svgRoot.children.push(symbolVNode); |
| } |
| } |
| 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 (isString(symbol)) { |
| return [[symbol]]; |
| } |
| |
| let isAllString = true; |
| for (let i = 0; i < symbol.length; ++i) { |
| if (!isString(symbol[i])) { |
| isAllString = false; |
| break; |
| } |
| } |
| if (isAllString) { |
| return normalizeSymbolArray([symbol as string[]]); |
| } |
| |
| const result: string[][] = []; |
| for (let i = 0; i < symbol.length; ++i) { |
| if (isString(symbol[i])) { |
| 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 (isNumber(dash)) { |
| 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 (!isNumber(dash[i])) { |
| isAllNumber = false; |
| break; |
| } |
| } |
| if (isAllNumber) { |
| return normalizeDashArrayX([dash as number[]]); |
| } |
| |
| const result: number[][] = []; |
| for (let i = 0; i < dash.length; ++i) { |
| if (isNumber(dash[i])) { |
| 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 (isNumber(dash)) { |
| 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; |
| } |