zrender
Version:
A lightweight graphic library providing 2d draw for Apache ECharts
816 lines (726 loc) • 26.4 kB
text/typescript
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();
}