import Displayable, { DEFAULT_COMMON_STYLE } from '../graphic/Displayable'; 
 | 
import PathProxy from '../core/PathProxy'; 
 | 
import { GradientObject } from '../graphic/Gradient'; 
 | 
import { ImagePatternObject, InnerImagePatternObject } from '../graphic/Pattern'; 
 | 
import { LinearGradientObject } from '../graphic/LinearGradient'; 
 | 
import { RadialGradientObject } from '../graphic/RadialGradient'; 
 | 
import { ZRCanvasRenderingContext } from '../core/types'; 
 | 
import { createOrUpdateImage, isImageReady } from '../graphic/helper/image'; 
 | 
import { getCanvasGradient, isClipPathChanged } from './helper'; 
 | 
import Path, { PathStyleProps } from '../graphic/Path'; 
 | 
import ZRImage, { ImageStyleProps } from '../graphic/Image'; 
 | 
import TSpan, {TSpanStyleProps} from '../graphic/TSpan'; 
 | 
import { MatrixArray } from '../core/matrix'; 
 | 
import { RADIAN_TO_DEGREE } from '../core/util'; 
 | 
import { getLineDash } from './dashStyle'; 
 | 
import { REDRAW_BIT, SHAPE_CHANGED_BIT } from '../graphic/constants'; 
 | 
import type IncrementalDisplayable from '../graphic/IncrementalDisplayable'; 
 | 
import { DEFAULT_FONT } from '../core/platform'; 
 | 
  
 | 
const pathProxyForDraw = new PathProxy(true); 
 | 
  
 | 
// Not use el#hasStroke because style may be different. 
 | 
function styleHasStroke(style: PathStyleProps) { 
 | 
    const stroke = style.stroke; 
 | 
    return !(stroke == null || stroke === 'none' || !(style.lineWidth > 0)); 
 | 
} 
 | 
  
 | 
// ignore lineWidth and must be string 
 | 
// Expected color but found '[' when color is gradient 
 | 
function isValidStrokeFillStyle( 
 | 
    strokeOrFill: PathStyleProps['stroke'] | PathStyleProps['fill'] 
 | 
): strokeOrFill is string { 
 | 
    return typeof strokeOrFill === 'string' && strokeOrFill !== 'none'; 
 | 
} 
 | 
  
 | 
function styleHasFill(style: PathStyleProps) { 
 | 
    const fill = style.fill; 
 | 
    return fill != null && fill !== 'none'; 
 | 
} 
 | 
function doFillPath(ctx: CanvasRenderingContext2D, style: PathStyleProps) { 
 | 
    if (style.fillOpacity != null && style.fillOpacity !== 1) { 
 | 
        const originalGlobalAlpha = ctx.globalAlpha; 
 | 
        ctx.globalAlpha = style.fillOpacity * style.opacity; 
 | 
        ctx.fill(); 
 | 
        // Set back globalAlpha 
 | 
        ctx.globalAlpha = originalGlobalAlpha; 
 | 
    } 
 | 
    else { 
 | 
        ctx.fill(); 
 | 
    } 
 | 
} 
 | 
  
 | 
function doStrokePath(ctx: CanvasRenderingContext2D, style: PathStyleProps) { 
 | 
    if (style.strokeOpacity != null && style.strokeOpacity !== 1) { 
 | 
        const originalGlobalAlpha = ctx.globalAlpha; 
 | 
        ctx.globalAlpha = style.strokeOpacity * style.opacity; 
 | 
        ctx.stroke(); 
 | 
        // Set back globalAlpha 
 | 
        ctx.globalAlpha = originalGlobalAlpha; 
 | 
    } 
 | 
    else { 
 | 
        ctx.stroke(); 
 | 
    } 
 | 
} 
 | 
  
 | 
export function createCanvasPattern( 
 | 
    this: void, 
 | 
    ctx: CanvasRenderingContext2D, 
 | 
    pattern: ImagePatternObject, 
 | 
    el: {dirty: () => void} 
 | 
): CanvasPattern { 
 | 
    const image = createOrUpdateImage(pattern.image, (pattern as InnerImagePatternObject).__image, el); 
 | 
    if (isImageReady(image)) { 
 | 
        const canvasPattern = ctx.createPattern(image, pattern.repeat || 'repeat'); 
 | 
        if ( 
 | 
            typeof DOMMatrix === 'function' 
 | 
            && canvasPattern                // image may be not ready 
 | 
            && canvasPattern.setTransform   // setTransform may not be supported in some old devices. 
 | 
        ) { 
 | 
            const matrix = new DOMMatrix(); 
 | 
            matrix.translateSelf((pattern.x || 0), (pattern.y || 0)); 
 | 
            matrix.rotateSelf(0, 0, (pattern.rotation || 0) * RADIAN_TO_DEGREE); 
 | 
            matrix.scaleSelf((pattern.scaleX || 1), (pattern.scaleY || 1)); 
 | 
            canvasPattern.setTransform(matrix); 
 | 
        } 
 | 
        return canvasPattern; 
 | 
    } 
 | 
} 
 | 
  
 | 
// Draw Path Elements 
 | 
function brushPath(ctx: CanvasRenderingContext2D, el: Path, style: PathStyleProps, inBatch: boolean) { 
 | 
    let hasStroke = styleHasStroke(style); 
 | 
    let hasFill = styleHasFill(style); 
 | 
  
 | 
    const strokePercent = style.strokePercent; 
 | 
    const strokePart = strokePercent < 1; 
 | 
  
 | 
    // TODO Reduce path memory cost. 
 | 
    const firstDraw = !el.path; 
 | 
    // Create path for each element when: 
 | 
    // 1. Element has interactions. 
 | 
    // 2. Element draw part of the line. 
 | 
    if ((!el.silent || strokePart) && firstDraw) { 
 | 
        el.createPathProxy(); 
 | 
    } 
 | 
  
 | 
    const path = el.path || pathProxyForDraw; 
 | 
    const dirtyFlag = el.__dirty; 
 | 
  
 | 
    if (!inBatch) { 
 | 
        const fill = style.fill; 
 | 
        const stroke = style.stroke; 
 | 
  
 | 
        const hasFillGradient = hasFill && !!(fill as GradientObject).colorStops; 
 | 
        const hasStrokeGradient = hasStroke && !!(stroke as GradientObject).colorStops; 
 | 
        const hasFillPattern = hasFill && !!(fill as ImagePatternObject).image; 
 | 
        const hasStrokePattern = hasStroke && !!(stroke as ImagePatternObject).image; 
 | 
  
 | 
        let fillGradient; 
 | 
        let strokeGradient; 
 | 
        let fillPattern; 
 | 
        let strokePattern; 
 | 
        let rect; 
 | 
        if (hasFillGradient || hasStrokeGradient) { 
 | 
            rect = el.getBoundingRect(); 
 | 
        } 
 | 
  
 | 
        // Update gradient because bounding rect may changed 
 | 
        if (hasFillGradient) { 
 | 
            fillGradient = dirtyFlag 
 | 
                ? getCanvasGradient(ctx, fill as (LinearGradientObject | RadialGradientObject), rect) 
 | 
                : el.__canvasFillGradient; 
 | 
            // No need to clear cache when fill is not gradient. 
 | 
            // It will always been updated when fill changed back to gradient. 
 | 
            el.__canvasFillGradient = fillGradient; 
 | 
        } 
 | 
        if (hasStrokeGradient) { 
 | 
            strokeGradient = dirtyFlag 
 | 
                ? getCanvasGradient(ctx, stroke as (LinearGradientObject | RadialGradientObject), rect) 
 | 
                : el.__canvasStrokeGradient; 
 | 
            el.__canvasStrokeGradient = strokeGradient; 
 | 
        } 
 | 
        if (hasFillPattern) { 
 | 
            // Pattern might be null if image not ready (even created from dataURI) 
 | 
            fillPattern = (dirtyFlag || !el.__canvasFillPattern) 
 | 
                ? createCanvasPattern(ctx, fill as ImagePatternObject, el) 
 | 
                : el.__canvasFillPattern; 
 | 
            el.__canvasFillPattern = fillPattern; 
 | 
        } 
 | 
        if (hasStrokePattern) { 
 | 
            // Pattern might be null if image not ready (even created from dataURI) 
 | 
            strokePattern = (dirtyFlag || !el.__canvasStrokePattern) 
 | 
                ? createCanvasPattern(ctx, stroke as ImagePatternObject, el) 
 | 
                : el.__canvasStrokePattern; 
 | 
            el.__canvasStrokePattern = fillPattern; 
 | 
        } 
 | 
        // Use the gradient or pattern 
 | 
        if (hasFillGradient) { 
 | 
            // PENDING If may have affect the state 
 | 
            ctx.fillStyle = fillGradient; 
 | 
        } 
 | 
        else if (hasFillPattern) { 
 | 
            if (fillPattern) {  // createCanvasPattern may return false if image is not ready. 
 | 
                ctx.fillStyle = fillPattern; 
 | 
            } 
 | 
            else { 
 | 
                // Don't fill if image is not ready 
 | 
                hasFill = false; 
 | 
            } 
 | 
        } 
 | 
        if (hasStrokeGradient) { 
 | 
            ctx.strokeStyle = strokeGradient; 
 | 
        } 
 | 
        else if (hasStrokePattern) { 
 | 
            if (strokePattern) { 
 | 
                ctx.strokeStyle = strokePattern; 
 | 
            } 
 | 
            else { 
 | 
                // Don't stroke if image is not ready 
 | 
                hasStroke = false; 
 | 
            } 
 | 
        } 
 | 
    } 
 | 
  
 | 
    // Update path sx, sy 
 | 
    const scale = el.getGlobalScale(); 
 | 
    path.setScale(scale[0], scale[1], el.segmentIgnoreThreshold); 
 | 
  
 | 
    let lineDash; 
 | 
    let lineDashOffset; 
 | 
    if (ctx.setLineDash && style.lineDash) { 
 | 
        [lineDash, lineDashOffset] = getLineDash(el); 
 | 
    } 
 | 
  
 | 
    let needsRebuild = true; 
 | 
  
 | 
    if (firstDraw || (dirtyFlag & SHAPE_CHANGED_BIT)) { 
 | 
        path.setDPR((ctx as any).dpr); 
 | 
        if (strokePart) { 
 | 
            // Use rebuildPath for percent stroke, so no context. 
 | 
            path.setContext(null); 
 | 
        } 
 | 
        else { 
 | 
            path.setContext(ctx); 
 | 
            needsRebuild = false; 
 | 
        } 
 | 
        path.reset(); 
 | 
  
 | 
        el.buildPath(path, el.shape, inBatch); 
 | 
        path.toStatic(); 
 | 
  
 | 
        // Clear path dirty flag 
 | 
        el.pathUpdated(); 
 | 
    } 
 | 
  
 | 
    // Not support separate fill and stroke. For the compatibility of SVG 
 | 
    if (needsRebuild) { 
 | 
        path.rebuildPath(ctx, strokePart ? strokePercent : 1); 
 | 
    } 
 | 
  
 | 
    if (lineDash) { 
 | 
        ctx.setLineDash(lineDash); 
 | 
        ctx.lineDashOffset = lineDashOffset; 
 | 
    } 
 | 
  
 | 
    if (!inBatch) { 
 | 
        if (style.strokeFirst) { 
 | 
            if (hasStroke) { 
 | 
                doStrokePath(ctx, style); 
 | 
            } 
 | 
            if (hasFill) { 
 | 
                doFillPath(ctx, style); 
 | 
            } 
 | 
        } 
 | 
        else { 
 | 
            if (hasFill) { 
 | 
                doFillPath(ctx, style); 
 | 
            } 
 | 
            if (hasStroke) { 
 | 
                doStrokePath(ctx, style); 
 | 
            } 
 | 
        } 
 | 
    } 
 | 
  
 | 
    if (lineDash) { 
 | 
        // PENDING 
 | 
        // Remove lineDash 
 | 
        ctx.setLineDash([]); 
 | 
    } 
 | 
} 
 | 
  
 | 
// Draw Image Elements 
 | 
function brushImage(ctx: CanvasRenderingContext2D, el: ZRImage, style: ImageStyleProps) { 
 | 
    const image = el.__image = createOrUpdateImage( 
 | 
        style.image, 
 | 
        el.__image, 
 | 
        el, 
 | 
        el.onload 
 | 
    ); 
 | 
  
 | 
    if (!image || !isImageReady(image)) { 
 | 
        return; 
 | 
    } 
 | 
  
 | 
    const x = style.x || 0; 
 | 
    const y = style.y || 0; 
 | 
    let width = el.getWidth(); 
 | 
    let height = el.getHeight(); 
 | 
    const aspect = image.width / image.height; 
 | 
    if (width == null && height != null) { 
 | 
        // Keep image/height ratio 
 | 
        width = height * aspect; 
 | 
    } 
 | 
    else if (height == null && width != null) { 
 | 
        height = width / aspect; 
 | 
    } 
 | 
    else if (width == null && height == null) { 
 | 
        width = image.width; 
 | 
        height = image.height; 
 | 
    } 
 | 
  
 | 
    if (style.sWidth && style.sHeight) { 
 | 
        const sx = style.sx || 0; 
 | 
        const sy = style.sy || 0; 
 | 
        ctx.drawImage( 
 | 
            image, 
 | 
            sx, sy, style.sWidth, style.sHeight, 
 | 
            x, y, width, height 
 | 
        ); 
 | 
    } 
 | 
    else if (style.sx && style.sy) { 
 | 
        const sx = style.sx; 
 | 
        const sy = style.sy; 
 | 
        const sWidth = width - sx; 
 | 
        const sHeight = height - sy; 
 | 
        ctx.drawImage( 
 | 
            image, 
 | 
            sx, sy, sWidth, sHeight, 
 | 
            x, y, width, height 
 | 
        ); 
 | 
    } 
 | 
    else { 
 | 
        ctx.drawImage(image, x, y, width, height); 
 | 
    } 
 | 
} 
 | 
  
 | 
// Draw Text Elements 
 | 
function brushText(ctx: CanvasRenderingContext2D, el: TSpan, style: TSpanStyleProps) { 
 | 
  
 | 
    let text = style.text; 
 | 
    // Convert to string 
 | 
    text != null && (text += ''); 
 | 
  
 | 
    if (text) { 
 | 
        ctx.font = style.font || DEFAULT_FONT; 
 | 
        ctx.textAlign = style.textAlign; 
 | 
        ctx.textBaseline = style.textBaseline; 
 | 
  
 | 
        let lineDash; 
 | 
        let lineDashOffset; 
 | 
        if (ctx.setLineDash && style.lineDash) { 
 | 
            [lineDash, lineDashOffset] = getLineDash(el); 
 | 
        } 
 | 
  
 | 
        if (lineDash) { 
 | 
            ctx.setLineDash(lineDash); 
 | 
            ctx.lineDashOffset = lineDashOffset; 
 | 
        } 
 | 
  
 | 
        if (style.strokeFirst) { 
 | 
            if (styleHasStroke(style)) { 
 | 
                ctx.strokeText(text, style.x, style.y); 
 | 
            } 
 | 
            if (styleHasFill(style)) { 
 | 
                ctx.fillText(text, style.x, style.y); 
 | 
            } 
 | 
        } 
 | 
        else { 
 | 
            if (styleHasFill(style)) { 
 | 
                ctx.fillText(text, style.x, style.y); 
 | 
            } 
 | 
            if (styleHasStroke(style)) { 
 | 
                ctx.strokeText(text, style.x, style.y); 
 | 
            } 
 | 
        } 
 | 
  
 | 
        if (lineDash) { 
 | 
            // Remove lineDash 
 | 
            ctx.setLineDash([]); 
 | 
        } 
 | 
    } 
 | 
  
 | 
} 
 | 
  
 | 
const SHADOW_NUMBER_PROPS = ['shadowBlur', 'shadowOffsetX', 'shadowOffsetY'] as const; 
 | 
const STROKE_PROPS = [ 
 | 
    ['lineCap', 'butt'], ['lineJoin', 'miter'], ['miterLimit', 10] 
 | 
] as const; 
 | 
  
 | 
type AllStyleOption = PathStyleProps | TSpanStyleProps | ImageStyleProps; 
 | 
// type ShadowPropNames = typeof SHADOW_PROPS[number][0]; 
 | 
// type StrokePropNames = typeof STROKE_PROPS[number][0]; 
 | 
// type DrawPropNames = typeof DRAW_PROPS[number][0]; 
 | 
  
 | 
function bindCommonProps( 
 | 
    ctx: CanvasRenderingContext2D, 
 | 
    style: AllStyleOption, 
 | 
    prevStyle: AllStyleOption, 
 | 
    forceSetAll: boolean, 
 | 
    scope: BrushScope 
 | 
): boolean { 
 | 
    let styleChanged = false; 
 | 
  
 | 
    if (!forceSetAll) { 
 | 
        prevStyle = prevStyle || {}; 
 | 
  
 | 
        // Shared same style. 
 | 
        if (style === prevStyle) { 
 | 
            return false; 
 | 
        } 
 | 
    } 
 | 
    if (forceSetAll || style.opacity !== prevStyle.opacity) { 
 | 
        flushPathDrawn(ctx, scope); 
 | 
        styleChanged = true; 
 | 
        // Ensure opacity is between 0 ~ 1. Invalid opacity will lead to a failure set and use the leaked opacity from the previous. 
 | 
        const opacity = Math.max(Math.min(style.opacity, 1), 0); 
 | 
        ctx.globalAlpha = isNaN(opacity) ? DEFAULT_COMMON_STYLE.opacity : opacity; 
 | 
    } 
 | 
  
 | 
    if (forceSetAll || style.blend !== prevStyle.blend) { 
 | 
        if (!styleChanged) { 
 | 
            flushPathDrawn(ctx, scope); 
 | 
            styleChanged = true; 
 | 
        } 
 | 
        ctx.globalCompositeOperation = style.blend || DEFAULT_COMMON_STYLE.blend; 
 | 
    } 
 | 
    for (let i = 0; i < SHADOW_NUMBER_PROPS.length; i++) { 
 | 
        const propName = SHADOW_NUMBER_PROPS[i]; 
 | 
        if (forceSetAll || style[propName] !== prevStyle[propName]) { 
 | 
            if (!styleChanged) { 
 | 
                flushPathDrawn(ctx, scope); 
 | 
                styleChanged = true; 
 | 
            } 
 | 
            // FIXME Invalid property value will cause style leak from previous element. 
 | 
            ctx[propName] = (ctx as ZRCanvasRenderingContext).dpr * (style[propName] || 0); 
 | 
        } 
 | 
    } 
 | 
    if (forceSetAll || style.shadowColor !== prevStyle.shadowColor) { 
 | 
        if (!styleChanged) { 
 | 
            flushPathDrawn(ctx, scope); 
 | 
            styleChanged = true; 
 | 
        } 
 | 
        ctx.shadowColor = style.shadowColor || DEFAULT_COMMON_STYLE.shadowColor; 
 | 
    } 
 | 
    return styleChanged; 
 | 
} 
 | 
  
 | 
function bindPathAndTextCommonStyle( 
 | 
    ctx: CanvasRenderingContext2D, 
 | 
    el: TSpan | Path, 
 | 
    prevEl: TSpan | Path, 
 | 
    forceSetAll: boolean, 
 | 
    scope: BrushScope 
 | 
) { 
 | 
    const style = getStyle(el, scope.inHover); 
 | 
    const prevStyle = forceSetAll 
 | 
        ? null 
 | 
        : (prevEl && getStyle(prevEl, scope.inHover) || {}); 
 | 
    // Shared same style. prevStyle will be null if forceSetAll. 
 | 
    if (style === prevStyle) { 
 | 
        return false; 
 | 
    } 
 | 
  
 | 
    let styleChanged = bindCommonProps(ctx, style, prevStyle, forceSetAll, scope); 
 | 
  
 | 
    if (forceSetAll || style.fill !== prevStyle.fill) { 
 | 
        if (!styleChanged) { 
 | 
            // Flush before set 
 | 
            flushPathDrawn(ctx, scope); 
 | 
            styleChanged = true; 
 | 
        } 
 | 
        isValidStrokeFillStyle(style.fill) && (ctx.fillStyle = style.fill); 
 | 
    } 
 | 
    if (forceSetAll || style.stroke !== prevStyle.stroke) { 
 | 
        if (!styleChanged) { 
 | 
            flushPathDrawn(ctx, scope); 
 | 
            styleChanged = true; 
 | 
        } 
 | 
        isValidStrokeFillStyle(style.stroke) && (ctx.strokeStyle = style.stroke); 
 | 
    } 
 | 
    if (forceSetAll || style.opacity !== prevStyle.opacity) { 
 | 
        if (!styleChanged) { 
 | 
            flushPathDrawn(ctx, scope); 
 | 
            styleChanged = true; 
 | 
        } 
 | 
        ctx.globalAlpha = style.opacity == null ? 1 : style.opacity; 
 | 
    } 
 | 
    if (el.hasStroke()) { 
 | 
        const lineWidth = style.lineWidth; 
 | 
        const newLineWidth = lineWidth / ( 
 | 
            (style.strokeNoScale && el.getLineScale) ? el.getLineScale() : 1 
 | 
        ); 
 | 
        if (ctx.lineWidth !== newLineWidth) { 
 | 
            if (!styleChanged) { 
 | 
                flushPathDrawn(ctx, scope); 
 | 
                styleChanged = true; 
 | 
            } 
 | 
            ctx.lineWidth = newLineWidth; 
 | 
        } 
 | 
    } 
 | 
  
 | 
    for (let i = 0; i < STROKE_PROPS.length; i++) { 
 | 
        const prop = STROKE_PROPS[i]; 
 | 
        const propName = prop[0]; 
 | 
        if (forceSetAll || style[propName] !== prevStyle[propName]) { 
 | 
            if (!styleChanged) { 
 | 
                flushPathDrawn(ctx, scope); 
 | 
                styleChanged = true; 
 | 
            } 
 | 
            // FIXME Invalid property value will cause style leak from previous element. 
 | 
            (ctx as any)[propName] = style[propName] || prop[1]; 
 | 
        } 
 | 
    } 
 | 
  
 | 
    return styleChanged; 
 | 
} 
 | 
  
 | 
function bindImageStyle( 
 | 
    ctx: CanvasRenderingContext2D, 
 | 
    el: ZRImage, 
 | 
    prevEl: ZRImage, 
 | 
    // forceSetAll must be true if prevEl is null 
 | 
    forceSetAll: boolean, 
 | 
    scope: BrushScope 
 | 
) { 
 | 
    return bindCommonProps( 
 | 
        ctx, 
 | 
        getStyle(el, scope.inHover), 
 | 
        prevEl && getStyle(prevEl, scope.inHover), 
 | 
        forceSetAll, 
 | 
        scope 
 | 
    ); 
 | 
} 
 | 
  
 | 
function setContextTransform(ctx: CanvasRenderingContext2D, el: Displayable) { 
 | 
    const m = el.transform; 
 | 
    const dpr = (ctx as ZRCanvasRenderingContext).dpr || 1; 
 | 
    if (m) { 
 | 
        ctx.setTransform(dpr * m[0], dpr * m[1], dpr * m[2], dpr * m[3], dpr * m[4], dpr * m[5]); 
 | 
    } 
 | 
    else { 
 | 
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 
 | 
    } 
 | 
} 
 | 
  
 | 
function updateClipStatus(clipPaths: Path[], ctx: CanvasRenderingContext2D, scope: BrushScope) { 
 | 
    let allClipped = false; 
 | 
    for (let i = 0; i < clipPaths.length; i++) { 
 | 
        const clipPath = clipPaths[i]; 
 | 
        // Ignore draw following elements if clipPath has zero area. 
 | 
        allClipped = allClipped || clipPath.isZeroArea(); 
 | 
  
 | 
        setContextTransform(ctx, clipPath); 
 | 
        ctx.beginPath(); 
 | 
        clipPath.buildPath(ctx, clipPath.shape); 
 | 
        ctx.clip(); 
 | 
    } 
 | 
    scope.allClipped = allClipped; 
 | 
} 
 | 
  
 | 
function isTransformChanged(m0: MatrixArray, m1: MatrixArray): boolean { 
 | 
    if (m0 && m1) { 
 | 
        return m0[0] !== m1[0] 
 | 
            || m0[1] !== m1[1] 
 | 
            || m0[2] !== m1[2] 
 | 
            || m0[3] !== m1[3] 
 | 
            || m0[4] !== m1[4] 
 | 
            || m0[5] !== m1[5]; 
 | 
    } 
 | 
    else if (!m0 && !m1) {  // All identity matrix. 
 | 
        return false; 
 | 
    } 
 | 
  
 | 
    return true; 
 | 
} 
 | 
  
 | 
const DRAW_TYPE_PATH = 1; 
 | 
const DRAW_TYPE_IMAGE = 2; 
 | 
const DRAW_TYPE_TEXT = 3; 
 | 
const DRAW_TYPE_INCREMENTAL = 4; 
 | 
  
 | 
export type BrushScope = { 
 | 
    inHover: boolean 
 | 
  
 | 
    // width / height of viewport 
 | 
    viewWidth: number 
 | 
    viewHeight: number 
 | 
  
 | 
    // Status for clipping 
 | 
    prevElClipPaths?: Path[] 
 | 
    prevEl?: Displayable 
 | 
    allClipped?: boolean    // If the whole element can be clipped 
 | 
  
 | 
    // Status for batching 
 | 
    batchFill?: string 
 | 
    batchStroke?: string 
 | 
  
 | 
    lastDrawType?: number 
 | 
} 
 | 
  
 | 
// If path can be batched 
 | 
function canPathBatch(style: PathStyleProps) { 
 | 
  
 | 
    const hasFill = styleHasFill(style); 
 | 
    const hasStroke = styleHasStroke(style); 
 | 
  
 | 
    return !( 
 | 
        // Line dash is dynamically set in brush function. 
 | 
        style.lineDash 
 | 
        // Can't batch if element is both set fill and stroke. Or both not set 
 | 
        || !(+hasFill ^ +hasStroke) 
 | 
        // Can't batch if element is drawn with gradient or pattern. 
 | 
        || (hasFill && typeof style.fill !== 'string') 
 | 
        || (hasStroke && typeof style.stroke !== 'string') 
 | 
        // Can't batch if element only stroke part of line. 
 | 
        || style.strokePercent < 1 
 | 
        // Has stroke or fill opacity 
 | 
        || style.strokeOpacity < 1 
 | 
        || style.fillOpacity < 1 
 | 
    ); 
 | 
} 
 | 
  
 | 
function flushPathDrawn(ctx: CanvasRenderingContext2D, scope: BrushScope) { 
 | 
    // Force flush all after drawn last element 
 | 
    scope.batchFill && ctx.fill(); 
 | 
    scope.batchStroke && ctx.stroke(); 
 | 
    scope.batchFill = ''; 
 | 
    scope.batchStroke = ''; 
 | 
} 
 | 
  
 | 
function getStyle(el: Displayable, inHover?: boolean) { 
 | 
    return inHover ? (el.__hoverStyle || el.style) : el.style; 
 | 
} 
 | 
  
 | 
export function brushSingle(ctx: CanvasRenderingContext2D, el: Displayable) { 
 | 
    brush(ctx, el, { inHover: false, viewWidth: 0, viewHeight: 0 }, true); 
 | 
} 
 | 
  
 | 
// Brush different type of elements. 
 | 
export function brush( 
 | 
    ctx: CanvasRenderingContext2D, 
 | 
    el: Displayable, 
 | 
    scope: BrushScope, 
 | 
    isLast: boolean 
 | 
) { 
 | 
    const m = el.transform; 
 | 
  
 | 
    if (!el.shouldBePainted(scope.viewWidth, scope.viewHeight, false, false)) { 
 | 
        // Needs to mark el rendered. 
 | 
        // Or this element will always been rendered in progressive rendering. 
 | 
        // But other dirty bit should not be cleared, otherwise it cause the shape 
 | 
        // can not be updated in this case. 
 | 
        el.__dirty &= ~REDRAW_BIT; 
 | 
        el.__isRendered = false; 
 | 
        return; 
 | 
    } 
 | 
  
 | 
    // HANDLE CLIPPING 
 | 
    const clipPaths = el.__clipPaths; 
 | 
    const prevElClipPaths = scope.prevElClipPaths; 
 | 
  
 | 
    let forceSetTransform = false; 
 | 
    let forceSetStyle = false; 
 | 
    // Optimize when clipping on group with several elements 
 | 
    if (!prevElClipPaths || isClipPathChanged(clipPaths, prevElClipPaths)) { 
 | 
        // If has previous clipping state, restore from it 
 | 
        if (prevElClipPaths && prevElClipPaths.length) { 
 | 
            // Flush restore 
 | 
            flushPathDrawn(ctx, scope); 
 | 
  
 | 
            ctx.restore(); 
 | 
            // Must set all style and transform because context changed by restore 
 | 
            forceSetStyle = forceSetTransform = true; 
 | 
  
 | 
            scope.prevElClipPaths = null; 
 | 
            scope.allClipped = false; 
 | 
            // Reset prevEl since context has been restored 
 | 
            scope.prevEl = null; 
 | 
        } 
 | 
        // New clipping state 
 | 
        if (clipPaths && clipPaths.length) { 
 | 
            // Flush before clip 
 | 
            flushPathDrawn(ctx, scope); 
 | 
  
 | 
            ctx.save(); 
 | 
            updateClipStatus(clipPaths, ctx, scope); 
 | 
            // Must set transform because it's changed when clip. 
 | 
            forceSetTransform = true; 
 | 
        } 
 | 
        scope.prevElClipPaths = clipPaths; 
 | 
    } 
 | 
  
 | 
    // Not rendering elements if it's clipped by a zero area path. 
 | 
    // Or it may cause bug on some version of IE11 (like 11.0.9600.178**), 
 | 
    // where exception "unexpected call to method or property access" 
 | 
    // might be thrown when calling ctx.fill or ctx.stroke after a path 
 | 
    // whose area size is zero is drawn and ctx.clip() is called and 
 | 
    // shadowBlur is set. See #4572, #3112, #5777. 
 | 
    // (e.g., 
 | 
    //  ctx.moveTo(10, 10); 
 | 
    //  ctx.lineTo(20, 10); 
 | 
    //  ctx.closePath(); 
 | 
    //  ctx.clip(); 
 | 
    //  ctx.shadowBlur = 10; 
 | 
    //  ... 
 | 
    //  ctx.fill(); 
 | 
    // ) 
 | 
    if (scope.allClipped) { 
 | 
        el.__isRendered = false; 
 | 
        return; 
 | 
    } 
 | 
  
 | 
    // START BRUSH 
 | 
    el.beforeBrush && el.beforeBrush(); 
 | 
    el.innerBeforeBrush(); 
 | 
  
 | 
    const prevEl = scope.prevEl; 
 | 
    // TODO el type changed. 
 | 
    if (!prevEl) { 
 | 
        forceSetStyle = forceSetTransform = true; 
 | 
    } 
 | 
  
 | 
    let canBatchPath = el instanceof Path   // Only path supports batch 
 | 
        && el.autoBatch 
 | 
        && canPathBatch(el.style); 
 | 
  
 | 
    if (forceSetTransform || isTransformChanged(m, prevEl.transform)) { 
 | 
        // Flush 
 | 
        flushPathDrawn(ctx, scope); 
 | 
        setContextTransform(ctx, el); 
 | 
    } 
 | 
    else if (!canBatchPath) { 
 | 
        // Flush 
 | 
        flushPathDrawn(ctx, scope); 
 | 
    } 
 | 
  
 | 
    const style = getStyle(el, scope.inHover); 
 | 
    if (el instanceof Path) { 
 | 
        // PENDING do we need to rebind all style if displayable type changed? 
 | 
        if (scope.lastDrawType !== DRAW_TYPE_PATH) { 
 | 
            forceSetStyle = true; 
 | 
            scope.lastDrawType = DRAW_TYPE_PATH; 
 | 
        } 
 | 
  
 | 
        bindPathAndTextCommonStyle(ctx, el as Path, prevEl as Path, forceSetStyle, scope); 
 | 
        // Begin path at start 
 | 
        if (!canBatchPath || (!scope.batchFill && !scope.batchStroke)) { 
 | 
            ctx.beginPath(); 
 | 
        } 
 | 
        brushPath(ctx, el as Path, style, canBatchPath); 
 | 
  
 | 
        if (canBatchPath) { 
 | 
            scope.batchFill = style.fill as string || ''; 
 | 
            scope.batchStroke = style.stroke as string || ''; 
 | 
        } 
 | 
    } 
 | 
    else { 
 | 
        if (el instanceof TSpan) { 
 | 
            if (scope.lastDrawType !== DRAW_TYPE_TEXT) { 
 | 
                forceSetStyle = true; 
 | 
                scope.lastDrawType = DRAW_TYPE_TEXT; 
 | 
            } 
 | 
  
 | 
            bindPathAndTextCommonStyle(ctx, el as TSpan, prevEl as TSpan, forceSetStyle, scope); 
 | 
            brushText(ctx, el as TSpan, style); 
 | 
        } 
 | 
        else if (el instanceof ZRImage) { 
 | 
            if (scope.lastDrawType !== DRAW_TYPE_IMAGE) { 
 | 
                forceSetStyle = true; 
 | 
                scope.lastDrawType = DRAW_TYPE_IMAGE; 
 | 
            } 
 | 
  
 | 
            bindImageStyle(ctx, el as ZRImage, prevEl as ZRImage, forceSetStyle, scope); 
 | 
            brushImage(ctx, el as ZRImage, style); 
 | 
        } 
 | 
        // Assume it's a IncrementalDisplayable 
 | 
        else if ((el as IncrementalDisplayable).getTemporalDisplayables) { 
 | 
            if (scope.lastDrawType !== DRAW_TYPE_INCREMENTAL) { 
 | 
                forceSetStyle = true; 
 | 
                scope.lastDrawType = DRAW_TYPE_INCREMENTAL; 
 | 
            } 
 | 
  
 | 
            brushIncremental(ctx, el as IncrementalDisplayable, scope); 
 | 
        } 
 | 
  
 | 
    } 
 | 
  
 | 
    if (canBatchPath && isLast) { 
 | 
        flushPathDrawn(ctx, scope); 
 | 
    } 
 | 
  
 | 
    el.innerAfterBrush(); 
 | 
    el.afterBrush && el.afterBrush(); 
 | 
  
 | 
    scope.prevEl = el; 
 | 
  
 | 
    // Mark as painted. 
 | 
    el.__dirty = 0; 
 | 
    el.__isRendered = true; 
 | 
} 
 | 
  
 | 
function brushIncremental( 
 | 
    ctx: CanvasRenderingContext2D, 
 | 
    el: IncrementalDisplayable, 
 | 
    scope: BrushScope 
 | 
) { 
 | 
    let displayables = el.getDisplayables(); 
 | 
    let temporalDisplayables = el.getTemporalDisplayables(); 
 | 
    // Provide an inner scope. 
 | 
    // Save current context and restore after brushed. 
 | 
    ctx.save(); 
 | 
    let innerScope: BrushScope = { 
 | 
        prevElClipPaths: null, 
 | 
        prevEl: null, 
 | 
        allClipped: false, 
 | 
        viewWidth: scope.viewWidth, 
 | 
        viewHeight: scope.viewHeight, 
 | 
        inHover: scope.inHover 
 | 
    }; 
 | 
    let i; 
 | 
    let len; 
 | 
    // Render persistant displayables. 
 | 
    for (i = el.getCursor(), len = displayables.length; i < len; i++) { 
 | 
        const displayable = displayables[i]; 
 | 
        displayable.beforeBrush && displayable.beforeBrush(); 
 | 
        displayable.innerBeforeBrush(); 
 | 
        brush(ctx, displayable, innerScope, i === len - 1); 
 | 
        displayable.innerAfterBrush(); 
 | 
        displayable.afterBrush && displayable.afterBrush(); 
 | 
        innerScope.prevEl = displayable; 
 | 
    } 
 | 
    // Render temporary displayables. 
 | 
    for (let i = 0, len = temporalDisplayables.length; i < len; i++) { 
 | 
        const displayable = temporalDisplayables[i]; 
 | 
        displayable.beforeBrush && displayable.beforeBrush(); 
 | 
        displayable.innerBeforeBrush(); 
 | 
        brush(ctx, displayable, innerScope, i === len - 1); 
 | 
        displayable.innerAfterBrush(); 
 | 
        displayable.afterBrush && displayable.afterBrush(); 
 | 
        innerScope.prevEl = displayable; 
 | 
    } 
 | 
    el.clearTemporalDisplayables(); 
 | 
    el.notClear = true; 
 | 
  
 | 
    ctx.restore(); 
 | 
} 
 |