@antv/g-plugin-canvas-renderer
Version:
A G plugin of renderer implementation with Canvas2D API
1,199 lines (1,141 loc) • 64.4 kB
JavaScript
/*!
* @antv/g-plugin-canvas-renderer
* @description A G plugin of renderer implementation with Canvas2D API
* @version 2.2.19
* @date 2/27/2025, 8:28:07 AM
* @author AntVis
* @docs https://g.antv.antgroup.com/
*/
'use strict';
var _defineProperty = require('@babel/runtime/helpers/defineProperty');
var _objectSpread = require('@babel/runtime/helpers/objectSpread2');
var _classCallCheck = require('@babel/runtime/helpers/classCallCheck');
var _createClass = require('@babel/runtime/helpers/createClass');
var _callSuper = require('@babel/runtime/helpers/callSuper');
var _inherits = require('@babel/runtime/helpers/inherits');
var gLite = require('@antv/g-lite');
var _slicedToArray = require('@babel/runtime/helpers/slicedToArray');
var _toConsumableArray = require('@babel/runtime/helpers/toConsumableArray');
var _classPrivateFieldLooseBase = require('@babel/runtime/helpers/classPrivateFieldLooseBase');
var _classPrivateFieldLooseKey = require('@babel/runtime/helpers/classPrivateFieldLooseKey');
var glMatrix = require('gl-matrix');
var util = require('@antv/util');
var gPluginImageLoader = require('@antv/g-plugin-image-loader');
var _renderState = /*#__PURE__*/_classPrivateFieldLooseKey("renderState");
/**
* support 2 modes in rendering:
* * immediate
* * delayed: render at the end of frame with dirty-rectangle
*/
var CanvasRendererPlugin = /*#__PURE__*/function () {
/**
* RBush used in dirty rectangle rendering
*/
function CanvasRendererPlugin(canvasRendererPluginOptions // private styleRendererFactory: Record<Shape, StyleRenderer>,
) {
_classCallCheck(this, CanvasRendererPlugin);
this.removedRBushNodeAABBs = [];
this.renderQueue = [];
Object.defineProperty(this, _renderState, {
writable: true,
value: {
restoreStack: [],
prevObject: null,
currentContext: new Map()
}
});
this.clearFullScreenLastFrame = false;
this.clearFullScreen = false;
/**
* view projection matrix
*/
this.vpMatrix = glMatrix.mat4.create();
this.dprMatrix = glMatrix.mat4.create();
this.tmpMat4 = glMatrix.mat4.create();
this.vec3a = glMatrix.vec3.create();
this.vec3b = glMatrix.vec3.create();
this.vec3c = glMatrix.vec3.create();
this.vec3d = glMatrix.vec3.create();
this.canvasRendererPluginOptions = canvasRendererPluginOptions;
}
return _createClass(CanvasRendererPlugin, [{
key: "apply",
value: function apply(context, runtime) {
var _this = this;
this.context = context;
var _this$context = this.context,
config = _this$context.config,
camera = _this$context.camera,
renderingService = _this$context.renderingService,
renderingContext = _this$context.renderingContext,
rBushRoot = _this$context.rBushRoot,
pathGeneratorFactory = _this$context.pathGeneratorFactory;
var enableRenderingOptimization = config.renderer.getConfig().enableRenderingOptimization;
config.renderer.getConfig().enableDirtyCheck = false;
config.renderer.getConfig().enableDirtyRectangleRendering = false;
this.rBush = rBushRoot;
this.pathGeneratorFactory = pathGeneratorFactory;
var contextService = context.contextService;
var canvas = renderingContext.root.ownerDocument.defaultView;
var handleUnmounted = function handleUnmounted(e) {
var object = e.target;
// remove r-bush node
// @ts-ignore
var rBushNode = object.rBushNode;
if (rBushNode.aabb) {
// save removed aabbs for dirty-rectangle rendering later
_this.removedRBushNodeAABBs.push(rBushNode.aabb);
}
};
var handleCulled = function handleCulled(e) {
var object = e.target;
// @ts-ignore
var rBushNode = object.rBushNode;
if (rBushNode.aabb) {
// save removed aabbs for dirty-rectangle rendering later
_this.removedRBushNodeAABBs.push(rBushNode.aabb);
}
};
renderingService.hooks.init.tap(CanvasRendererPlugin.tag, function () {
canvas.addEventListener(gLite.ElementEvent.UNMOUNTED, handleUnmounted);
canvas.addEventListener(gLite.ElementEvent.CULLED, handleCulled);
// clear fullscreen
var dpr = contextService.getDPR();
var width = config.width,
height = config.height;
var context = contextService.getContext();
_this.clearRect(context, 0, 0, width * dpr, height * dpr, config.background);
});
renderingService.hooks.destroy.tap(CanvasRendererPlugin.tag, function () {
canvas.removeEventListener(gLite.ElementEvent.UNMOUNTED, handleUnmounted);
canvas.removeEventListener(gLite.ElementEvent.CULLED, handleCulled);
_this.renderQueue = [];
_this.removedRBushNodeAABBs = [];
_classPrivateFieldLooseBase(_this, _renderState)[_renderState] = {
restoreStack: [],
prevObject: null,
currentContext: null
};
});
renderingService.hooks.beginFrame.tap(CanvasRendererPlugin.tag, function () {
var _canvas$context$rende;
var context = contextService.getContext();
var dpr = contextService.getDPR();
var width = config.width,
height = config.height;
var _this$canvasRendererP = _this.canvasRendererPluginOptions,
dirtyObjectNumThreshold = _this$canvasRendererP.dirtyObjectNumThreshold,
dirtyObjectRatioThreshold = _this$canvasRendererP.dirtyObjectRatioThreshold;
// some heuristic conditions such as 80% object changed
var _renderingService$get = renderingService.getStats(),
total = _renderingService$get.total,
rendered = _renderingService$get.rendered;
var ratio = rendered / total;
_this.clearFullScreen = _this.clearFullScreenLastFrame ||
// @ts-ignore
!((_canvas$context$rende = canvas.context.renderingPlugins[1]) !== null && _canvas$context$rende !== void 0 && _canvas$context$rende.isFirstTimeRenderingFinished) || renderingService.disableDirtyRectangleRendering() || rendered > dirtyObjectNumThreshold && ratio > dirtyObjectRatioThreshold;
if (context) {
if (typeof context.resetTransform === 'function') {
context.resetTransform();
} else {
context.setTransform(1, 0, 0, 1, 0, 0);
}
if (_this.clearFullScreen) {
_this.clearRect(context, 0, 0, width * dpr, height * dpr, config.background);
}
}
});
/**
* render objects by z-index
*
* - The level of the child node will be affected by the level of the parent node
*/
var renderByZIndex = function renderByZIndex(object, context) {
var stack = [object];
while (stack.length > 0) {
var currentObject = stack.pop();
if (currentObject.isVisible() && !currentObject.isCulled()) {
if (enableRenderingOptimization) {
_this.renderDisplayObjectOptimized(currentObject, context, _this.context, _classPrivateFieldLooseBase(_this, _renderState)[_renderState], runtime);
} else {
_this.renderDisplayObject(currentObject, context, _this.context, _classPrivateFieldLooseBase(_this, _renderState)[_renderState], runtime);
}
}
var objects = currentObject.sortable.sorted || currentObject.childNodes;
// should account for z-index
for (var i = objects.length - 1; i >= 0; i--) {
stack.push(objects[i]);
}
}
};
// render at the end of frame
renderingService.hooks.endFrame.tap(CanvasRendererPlugin.tag, function () {
// Skip rendering.
if (renderingContext.root.childNodes.length === 0) {
_this.clearFullScreenLastFrame = true;
return;
}
enableRenderingOptimization = config.renderer.getConfig().enableRenderingOptimization;
// init
_classPrivateFieldLooseBase(_this, _renderState)[_renderState] = {
restoreStack: [],
prevObject: null,
currentContext: _classPrivateFieldLooseBase(_this, _renderState)[_renderState].currentContext
};
_classPrivateFieldLooseBase(_this, _renderState)[_renderState].currentContext.clear();
_this.clearFullScreenLastFrame = false;
var context = contextService.getContext();
// clear & clip dirty rectangle
var dpr = contextService.getDPR();
glMatrix.mat4.fromScaling(_this.dprMatrix, [dpr, dpr, 1]);
glMatrix.mat4.multiply(_this.vpMatrix, _this.dprMatrix, camera.getOrthoMatrix());
if (_this.clearFullScreen) {
// console.time('renderByZIndex');
if (enableRenderingOptimization) {
context.save();
renderByZIndex(renderingContext.root, context);
context.restore();
} else {
renderByZIndex(renderingContext.root, context);
}
// console.timeEnd('renderByZIndex');
_this.removedRBushNodeAABBs = [];
} else {
// console.log('canvas renderer next...', this.renderQueue);
// merge removed AABB
var dirtyRenderBounds = _this.safeMergeAABB.apply(_this, [_this.mergeDirtyAABBs(_this.renderQueue)].concat(_toConsumableArray(_this.removedRBushNodeAABBs.map(function (_ref) {
var minX = _ref.minX,
minY = _ref.minY,
maxX = _ref.maxX,
maxY = _ref.maxY;
var aabb = new gLite.AABB();
aabb.setMinMax(
// vec3.fromValues(minX, minY, 0),
// vec3.fromValues(maxX, maxY, 0),
[minX, minY, 0], [maxX, maxY, 0]);
return aabb;
}))));
_this.removedRBushNodeAABBs = [];
if (gLite.AABB.isEmpty(dirtyRenderBounds)) {
_this.renderQueue = [];
return;
}
var dirtyRect = _this.convertAABB2Rect(dirtyRenderBounds);
var x = dirtyRect.x,
y = dirtyRect.y,
width = dirtyRect.width,
height = dirtyRect.height;
var tl = glMatrix.vec3.transformMat4(_this.vec3a, [x, y, 0], _this.vpMatrix);
var tr = glMatrix.vec3.transformMat4(_this.vec3b, [x + width, y, 0], _this.vpMatrix);
var bl = glMatrix.vec3.transformMat4(_this.vec3c, [x, y + height, 0], _this.vpMatrix);
var br = glMatrix.vec3.transformMat4(_this.vec3d, [x + width, y + height, 0], _this.vpMatrix);
var minx = Math.min(tl[0], tr[0], br[0], bl[0]);
var miny = Math.min(tl[1], tr[1], br[1], bl[1]);
var maxx = Math.max(tl[0], tr[0], br[0], bl[0]);
var maxy = Math.max(tl[1], tr[1], br[1], bl[1]);
var ix = Math.floor(minx);
var iy = Math.floor(miny);
var iwidth = Math.ceil(maxx - minx);
var iheight = Math.ceil(maxy - miny);
context.save();
_this.clearRect(context, ix, iy, iwidth, iheight, config.background);
context.beginPath();
context.rect(ix, iy, iwidth, iheight);
context.clip();
// @see https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Transformations
context.setTransform(_this.vpMatrix[0], _this.vpMatrix[1], _this.vpMatrix[4], _this.vpMatrix[5], _this.vpMatrix[12], _this.vpMatrix[13]);
// draw dirty rectangle
var _config$renderer$getC = config.renderer.getConfig(),
enableDirtyRectangleRenderingDebug = _config$renderer$getC.enableDirtyRectangleRenderingDebug;
if (enableDirtyRectangleRenderingDebug) {
canvas.dispatchEvent(new gLite.CustomEvent(gLite.CanvasEvent.DIRTY_RECTANGLE, {
dirtyRect: {
x: ix,
y: iy,
width: iwidth,
height: iheight
}
}));
}
// search objects intersect with dirty rectangle
var dirtyObjects = _this.searchDirtyObjects(dirtyRenderBounds);
// do rendering
dirtyObjects
// sort by z-index
.sort(function (a, b) {
return a.sortable.renderOrder - b.sortable.renderOrder;
}).forEach(function (object) {
// culled object should not be rendered
if (object && object.isVisible() && !object.isCulled()) {
_this.renderDisplayObject(object, context, _this.context, _classPrivateFieldLooseBase(_this, _renderState)[_renderState], runtime);
}
});
context.restore();
// save dirty AABBs in last frame
_this.renderQueue.forEach(function (object) {
_this.saveDirtyAABB(object);
});
// clear queue
_this.renderQueue = [];
}
// pop restore stack, eg. root -> parent -> child
_classPrivateFieldLooseBase(_this, _renderState)[_renderState].restoreStack.forEach(function () {
context.restore();
});
// clear restore stack
_classPrivateFieldLooseBase(_this, _renderState)[_renderState].restoreStack = [];
});
renderingService.hooks.render.tap(CanvasRendererPlugin.tag, function (object) {
if (!_this.clearFullScreen) {
// render at the end of frame
_this.renderQueue.push(object);
}
});
}
}, {
key: "clearRect",
value: function clearRect(context, x, y, width, height, background) {
// clearRect is faster than fillRect @see https://stackoverflow.com/a/30830253
context.clearRect(x, y, width, height);
if (background) {
context.fillStyle = background;
context.fillRect(x, y, width, height);
}
}
}, {
key: "renderDisplayObjectOptimized",
value: function renderDisplayObjectOptimized(object, context, canvasContext, renderState, runtime) {
var nodeName = object.nodeName;
var updateTransform = false;
var clipDraw = false;
// @ts-ignore
var styleRenderer = this.context.styleRendererFactory[nodeName];
var generatePath = this.pathGeneratorFactory[nodeName];
// clip path
var clipPath = object.parsedStyle.clipPath;
if (clipPath) {
updateTransform = !renderState.prevObject || !glMatrix.mat4.exactEquals(clipPath.getWorldTransform(), renderState.prevObject.getWorldTransform());
if (updateTransform) {
this.applyWorldTransform(context, clipPath);
renderState.prevObject = null;
}
// generate path in local space
var _generatePath = this.pathGeneratorFactory[clipPath.nodeName];
if (_generatePath) {
context.save();
clipDraw = true;
context.beginPath();
_generatePath(context, clipPath.parsedStyle);
context.closePath();
context.clip();
}
}
// fill & stroke
if (styleRenderer) {
updateTransform = !renderState.prevObject || !glMatrix.mat4.exactEquals(object.getWorldTransform(), renderState.prevObject.getWorldTransform());
if (updateTransform) {
this.applyWorldTransform(context, object);
}
var forceUpdateStyle = !renderState.prevObject;
if (!forceUpdateStyle) {
var prevNodeName = renderState.prevObject.nodeName;
if (nodeName === gLite.Shape.TEXT) {
forceUpdateStyle = prevNodeName !== gLite.Shape.TEXT;
} else if (nodeName === gLite.Shape.IMAGE) {
forceUpdateStyle = prevNodeName !== gLite.Shape.IMAGE;
} else {
forceUpdateStyle = prevNodeName === gLite.Shape.TEXT || prevNodeName === gLite.Shape.IMAGE;
}
}
styleRenderer.applyStyleToContext(context, object, forceUpdateStyle, renderState);
renderState.prevObject = object;
}
if (generatePath) {
context.beginPath();
generatePath(context, object.parsedStyle);
if (nodeName !== gLite.Shape.LINE && nodeName !== gLite.Shape.PATH && nodeName !== gLite.Shape.POLYLINE) {
context.closePath();
}
}
// fill & stroke
if (styleRenderer) {
styleRenderer.drawToContext(context, object, _classPrivateFieldLooseBase(this, _renderState)[_renderState], this, runtime);
}
if (clipDraw) {
context.restore();
}
// finish rendering, clear dirty flag
object.renderable.dirty = false;
}
}, {
key: "renderDisplayObject",
value: function renderDisplayObject(object, context, canvasContext, renderState, runtime) {
var nodeName = object.nodeName;
// restore to its ancestor
var parent = renderState.restoreStack[renderState.restoreStack.length - 1];
if (parent && !(object.compareDocumentPosition(parent) & gLite.Node.DOCUMENT_POSITION_CONTAINS)) {
context.restore();
renderState.restoreStack.pop();
}
// @ts-ignore
var styleRenderer = this.context.styleRendererFactory[nodeName];
var generatePath = this.pathGeneratorFactory[nodeName];
// clip path
var clipPath = object.parsedStyle.clipPath;
if (clipPath) {
this.applyWorldTransform(context, clipPath);
// generate path in local space
var _generatePath2 = this.pathGeneratorFactory[clipPath.nodeName];
if (_generatePath2) {
context.save();
// save clip
renderState.restoreStack.push(object);
context.beginPath();
_generatePath2(context, clipPath.parsedStyle);
context.closePath();
context.clip();
}
}
// fill & stroke
if (styleRenderer) {
this.applyWorldTransform(context, object);
context.save();
// apply attributes to context
this.applyAttributesToContext(context, object);
}
if (generatePath) {
context.beginPath();
generatePath(context, object.parsedStyle);
if (nodeName !== gLite.Shape.LINE && nodeName !== gLite.Shape.PATH && nodeName !== gLite.Shape.POLYLINE) {
context.closePath();
}
}
// fill & stroke
if (styleRenderer) {
styleRenderer.render(context, object.parsedStyle, object, canvasContext, this, runtime);
// restore applied attributes, eg. shadowBlur shadowColor...
context.restore();
}
// finish rendering, clear dirty flag
object.renderable.dirty = false;
}
}, {
key: "applyAttributesToContext",
value: function applyAttributesToContext(context, object) {
var _ref2 = object.parsedStyle,
stroke = _ref2.stroke,
fill = _ref2.fill,
opacity = _ref2.opacity,
lineDash = _ref2.lineDash,
lineDashOffset = _ref2.lineDashOffset;
// @see https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/setLineDash
if (lineDash) {
context.setLineDash(lineDash);
}
// @see https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/lineDashOffset
if (!util.isNil(lineDashOffset)) {
context.lineDashOffset = lineDashOffset;
}
if (!util.isNil(opacity)) {
context.globalAlpha *= opacity;
}
if (!util.isNil(stroke) && !Array.isArray(stroke) && !stroke.isNone) {
context.strokeStyle = object.attributes.stroke;
}
if (!util.isNil(fill) && !Array.isArray(fill) && !fill.isNone) {
context.fillStyle = object.attributes.fill;
}
}
}, {
key: "convertAABB2Rect",
value: function convertAABB2Rect(aabb) {
var min = aabb.getMin();
var max = aabb.getMax();
// expand the rectangle a bit to avoid artifacts
// @see https://www.yuque.com/antv/ou292n/bi8nix#ExvCu
var minX = Math.floor(min[0]);
var minY = Math.floor(min[1]);
var maxX = Math.ceil(max[0]);
var maxY = Math.ceil(max[1]);
var width = maxX - minX;
var height = maxY - minY;
return {
x: minX,
y: minY,
width: width,
height: height
};
}
/**
* TODO: merge dirty rectangles with some strategies.
* For now, we just simply merge all the rectangles into one.
* @see https://idom.me/articles/841.html
*/
}, {
key: "mergeDirtyAABBs",
value: function mergeDirtyAABBs(dirtyObjects) {
// merge into a big AABB
// TODO: skip descendant if ancestor is caculated, but compareNodePosition is really slow
var aabb = new gLite.AABB();
dirtyObjects.forEach(function (object) {
var renderBounds = object.getRenderBounds();
aabb.add(renderBounds);
var dirtyRenderBounds = object.renderable.dirtyRenderBounds;
if (dirtyRenderBounds) {
aabb.add(dirtyRenderBounds);
}
});
return aabb;
}
}, {
key: "searchDirtyObjects",
value: function searchDirtyObjects(dirtyRectangle) {
// search in r-tree, get all affected nodes
var _dirtyRectangle$getMi = dirtyRectangle.getMin(),
_dirtyRectangle$getMi2 = _slicedToArray(_dirtyRectangle$getMi, 2),
minX = _dirtyRectangle$getMi2[0],
minY = _dirtyRectangle$getMi2[1];
var _dirtyRectangle$getMa = dirtyRectangle.getMax(),
_dirtyRectangle$getMa2 = _slicedToArray(_dirtyRectangle$getMa, 2),
maxX = _dirtyRectangle$getMa2[0],
maxY = _dirtyRectangle$getMa2[1];
var rBushNodes = this.rBush.search({
minX: minX,
minY: minY,
maxX: maxX,
maxY: maxY
});
return rBushNodes.map(function (_ref3) {
var displayObject = _ref3.displayObject;
return displayObject;
});
}
}, {
key: "saveDirtyAABB",
value: function saveDirtyAABB(object) {
var renderable = object.renderable;
if (!renderable.dirtyRenderBounds) {
renderable.dirtyRenderBounds = new gLite.AABB();
}
var renderBounds = object.getRenderBounds();
if (renderBounds) {
// save last dirty aabb
renderable.dirtyRenderBounds.update(renderBounds.center, renderBounds.halfExtents);
}
}
}, {
key: "applyWorldTransform",
value: function applyWorldTransform(context, object, matrix) {
// apply clip shape's RTS
if (matrix) {
glMatrix.mat4.copy(this.tmpMat4, object.getLocalTransform());
glMatrix.mat4.multiply(this.tmpMat4, matrix, this.tmpMat4);
glMatrix.mat4.multiply(this.tmpMat4, this.vpMatrix, this.tmpMat4);
} else {
// apply RTS transformation in world space
glMatrix.mat4.copy(this.tmpMat4, object.getWorldTransform());
glMatrix.mat4.multiply(this.tmpMat4, this.vpMatrix, this.tmpMat4);
}
// @see https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Transformations
context.setTransform(this.tmpMat4[0], this.tmpMat4[1], this.tmpMat4[4], this.tmpMat4[5], this.tmpMat4[12], this.tmpMat4[13]);
}
}, {
key: "safeMergeAABB",
value: function safeMergeAABB() {
var merged = new gLite.AABB();
for (var _len = arguments.length, aabbs = new Array(_len), _key = 0; _key < _len; _key++) {
aabbs[_key] = arguments[_key];
}
aabbs.forEach(function (aabb) {
merged.add(aabb);
});
return merged;
}
}]);
}();
CanvasRendererPlugin.tag = 'CanvasRenderer';
function getPattern(pattern, object, context, canvasContext, plugin, runtime, imagePool) {
var $offscreenCanvas;
var dpr;
if (pattern.image.nodeName === 'rect') {
var _parsedStyle = pattern.image.parsedStyle,
width = _parsedStyle.width,
height = _parsedStyle.height;
dpr = canvasContext.contextService.getDPR();
var offscreenCanvas = canvasContext.config.offscreenCanvas;
$offscreenCanvas = runtime.offscreenCanvasCreator.getOrCreateCanvas(offscreenCanvas);
$offscreenCanvas.width = width * dpr;
$offscreenCanvas.height = height * dpr;
var offscreenCanvasContext = runtime.offscreenCanvasCreator.getOrCreateContext(offscreenCanvas);
var renderState = {
restoreStack: [],
prevObject: null,
currentContext: new Map()
};
// offscreenCanvasContext.scale(1 / dpr, 1 / dpr);
pattern.image.forEach(function (object) {
plugin.renderDisplayObject(object, offscreenCanvasContext, canvasContext, renderState, runtime);
});
renderState.restoreStack.forEach(function () {
offscreenCanvasContext.restore();
});
}
var canvasPattern = imagePool.getOrCreatePatternSync(object, pattern, context, $offscreenCanvas, dpr, object.getGeometryBounds().min, function () {
// set dirty rectangle flag
object.renderable.dirty = true;
canvasContext.renderingService.dirtify();
});
return canvasPattern;
}
function getColor(parsedColor, object, context, imagePool) {
var color;
if (parsedColor.type === gLite.GradientType.LinearGradient || parsedColor.type === gLite.GradientType.RadialGradient) {
var bounds = object.getGeometryBounds();
var width = bounds && bounds.halfExtents[0] * 2 || 1;
var height = bounds && bounds.halfExtents[1] * 2 || 1;
var min = bounds && bounds.min || [0, 0];
color = imagePool.getOrCreateGradient(_objectSpread(_objectSpread({
type: parsedColor.type
}, parsedColor.value), {}, {
min: min,
width: width,
height: height
}), context);
}
return color;
}
var SHADOW_NUMBER_STYLE = ['shadowBlur', 'shadowOffsetX', 'shadowOffsetY'];
var STROKE_STYLE = ['lineCap', 'lineJoin', 'miterLimit'];
var DEFAULT_STYLE = {
// common
globalAlpha: 1,
shadowBlur: 0,
shadowOffsetX: 0,
shadowOffsetY: 0,
shadowColor: '#000',
filter: 'none',
globalCompositeOperation: 'source-over',
// stroke/fill
strokeStyle: '#000',
strokeOpacity: 1,
lineWidth: 1,
lineDash: [],
lineDashOffset: 0,
lineCap: 'butt',
lineJoin: 'miter',
miterLimit: 10,
fillStyle: '#000',
fillOpacity: 1
// image
};
var defaultParsedStyle = {};
/**
* Updating the canvas context is an expensive operation. The state of the context is cached and the actual update operation is performed only when the cache is not hit.
*
* In any case, the previous value is returned, which is convenient for temporarily updating the context and restoring it later.
*/
function updateContextIfNotHitCache(context, key, value, cache) {
var prevValue = cache.has(key) ? cache.get(key) : DEFAULT_STYLE[key];
if (prevValue !== value) {
// console.log('not hit cache', key, value, prevValue, cache);
if (key === 'lineDash') {
context.setLineDash(value);
} else {
// @ts-ignore
context[key] = value;
}
cache.set(key, value);
}
return prevValue;
}
var OptimizedDefaultRenderer = /*#__PURE__*/function () {
function OptimizedDefaultRenderer(imagePool) {
_classCallCheck(this, OptimizedDefaultRenderer);
this.imagePool = imagePool;
}
return _createClass(OptimizedDefaultRenderer, [{
key: "applyAttributesToContext",
value: function applyAttributesToContext(context, object) {}
}, {
key: "render",
value: function render(context, parsedStyle, object, canvasContext, plugin, runtime) {}
// #region common style
}, {
key: "applyCommonStyleToContext",
value: function applyCommonStyleToContext(context, object, forceUpdate, renderState) {
// const dpr = object.ownerDocument.defaultView.getContextService().getDPR();
var prevStyle = forceUpdate ? defaultParsedStyle : renderState.prevObject.parsedStyle;
var style = object.parsedStyle;
if (forceUpdate || style.opacity !== prevStyle.opacity) {
updateContextIfNotHitCache(context, 'globalAlpha', !util.isNil(style.opacity) ? style.opacity : DEFAULT_STYLE.globalAlpha, renderState.currentContext);
}
// TODO blend prop
// @ts-ignore
if (forceUpdate || style.blend !== prevStyle.blend) {
updateContextIfNotHitCache(context, 'globalCompositeOperation',
// @ts-ignore
!util.isNil(style.blend) ?
// @ts-ignore
style.blend : DEFAULT_STYLE.globalCompositeOperation, renderState.currentContext);
}
}
// #endregion common style
// #region stroke/fill style
}, {
key: "applyStrokeFillStyleToContext",
value: function applyStrokeFillStyleToContext(context, object, forceUpdate, renderState) {
var prevStyle = forceUpdate ? defaultParsedStyle : renderState.prevObject.parsedStyle;
var style = object.parsedStyle;
var _style$lineWidth = style.lineWidth,
lineWidth = _style$lineWidth === void 0 ? DEFAULT_STYLE.lineWidth : _style$lineWidth;
var hasFill = style.fill && !style.fill.isNone;
var hasStroke = style.stroke && !style.stroke.isNone && lineWidth > 0;
if (hasStroke) {
if (forceUpdate || object.attributes.stroke !== renderState.prevObject.attributes.stroke) {
var value = !util.isNil(style.stroke) && !Array.isArray(style.stroke) && !style.stroke.isNone ? object.attributes.stroke : DEFAULT_STYLE.strokeStyle;
updateContextIfNotHitCache(context, 'strokeStyle', value, renderState.currentContext);
}
if (forceUpdate || style.lineWidth !== prevStyle.lineWidth) {
updateContextIfNotHitCache(context, 'lineWidth', !util.isNil(style.lineWidth) ? style.lineWidth : DEFAULT_STYLE.lineWidth, renderState.currentContext);
}
if (forceUpdate || style.lineDash !== prevStyle.lineDash) {
updateContextIfNotHitCache(context, 'lineDash', style.lineDash || DEFAULT_STYLE.lineDash, renderState.currentContext);
}
if (forceUpdate || style.lineDashOffset !== prevStyle.lineDashOffset) {
updateContextIfNotHitCache(context, 'lineDashOffset', !util.isNil(style.lineDashOffset) ? style.lineDashOffset : DEFAULT_STYLE.lineDashOffset, renderState.currentContext);
}
for (var i = 0; i < STROKE_STYLE.length; i++) {
var styleName = STROKE_STYLE[i];
if (forceUpdate || style[styleName] !== prevStyle[styleName]) {
updateContextIfNotHitCache(context, styleName, !util.isNil(style[styleName]) ? style[styleName] : DEFAULT_STYLE[styleName], renderState.currentContext);
}
}
}
if (hasFill && (forceUpdate || object.attributes.fill !== renderState.prevObject.attributes.fill)) {
var _value = !util.isNil(style.fill) && !Array.isArray(style.fill) && !style.fill.isNone ? object.attributes.fill : DEFAULT_STYLE.fillStyle;
updateContextIfNotHitCache(context, 'fillStyle', _value, renderState.currentContext);
}
}
// #endregion stroke/fill style
}, {
key: "applyStyleToContext",
value: function applyStyleToContext(context, object, forceUpdate, renderState) {
var nodeName = object.nodeName;
this.applyCommonStyleToContext(context, object, forceUpdate, renderState);
if (nodeName === gLite.Shape.IMAGE) ; else {
this.applyStrokeFillStyleToContext(context, object, forceUpdate, renderState);
}
}
}, {
key: "applyShadowAndFilterStyleToContext",
value: function applyShadowAndFilterStyleToContext(context, object, hasShadow, renderState) {
var style = object.parsedStyle;
if (hasShadow) {
updateContextIfNotHitCache(context, 'shadowColor', style.shadowColor.toString(), renderState.currentContext);
for (var i = 0; i < SHADOW_NUMBER_STYLE.length; i++) {
var styleName = SHADOW_NUMBER_STYLE[i];
updateContextIfNotHitCache(context, styleName, style[styleName] || DEFAULT_STYLE[styleName], renderState.currentContext);
}
}
if (style.filter && style.filter.length) {
updateContextIfNotHitCache(context, 'filter',
// use raw filter string
object.attributes.filter, renderState.currentContext);
}
}
}, {
key: "clearShadowAndFilterStyleForContext",
value: function clearShadowAndFilterStyleForContext(context, hasShadow, hasFilter, renderState) {
var onlyClearShadowFilter = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
if (hasShadow) {
updateContextIfNotHitCache(context, 'shadowColor', DEFAULT_STYLE.shadowColor, renderState.currentContext);
for (var i = 0; i < SHADOW_NUMBER_STYLE.length; i++) {
var styleName = SHADOW_NUMBER_STYLE[i];
updateContextIfNotHitCache(context, styleName, DEFAULT_STYLE[styleName], renderState.currentContext);
}
}
if (hasFilter) {
if (hasShadow && onlyClearShadowFilter) {
// save drop-shadow filter
var oldFilter = context.filter;
if (!util.isNil(oldFilter) && oldFilter.indexOf('drop-shadow') > -1) {
updateContextIfNotHitCache(context, 'filter', oldFilter.replace(/drop-shadow\([^)]*\)/, '').trim() || DEFAULT_STYLE.filter, renderState.currentContext);
}
} else {
updateContextIfNotHitCache(context, 'filter', DEFAULT_STYLE.filter, renderState.currentContext);
}
}
}
}, {
key: "fillToContext",
value: function fillToContext(context, object, renderState, plugin, runtime) {
var _this = this;
var _object$parsedStyle = object.parsedStyle,
fill = _object$parsedStyle.fill,
fillRule = _object$parsedStyle.fillRule;
var resetStyle = null;
if (Array.isArray(fill) && fill.length > 0) {
fill.forEach(function (gradient) {
var prevStyle = updateContextIfNotHitCache(context, 'fillStyle', getColor(gradient, object, context, _this.imagePool), renderState.currentContext);
resetStyle = resetStyle !== null && resetStyle !== void 0 ? resetStyle : prevStyle;
if (fillRule) {
context.fill(fillRule);
} else {
context.fill();
}
});
} else {
if (gLite.isPattern(fill)) {
var pattern = getPattern(fill, object, context, object.ownerDocument.defaultView.context, plugin, runtime, this.imagePool);
if (pattern) {
context.fillStyle = pattern;
resetStyle = true;
}
}
if (fillRule) {
context.fill(fillRule);
} else {
context.fill();
}
}
if (resetStyle !== null) {
updateContextIfNotHitCache(context, 'fillStyle', resetStyle, renderState.currentContext);
}
}
}, {
key: "strokeToContext",
value: function strokeToContext(context, object, renderState, plugin, runtime) {
var _this2 = this;
var stroke = object.parsedStyle.stroke;
var resetStyle = null;
if (Array.isArray(stroke) && stroke.length > 0) {
stroke.forEach(function (gradient) {
var prevStyle = updateContextIfNotHitCache(context, 'strokeStyle', getColor(gradient, object, context, _this2.imagePool), renderState.currentContext);
resetStyle = resetStyle !== null && resetStyle !== void 0 ? resetStyle : prevStyle;
context.stroke();
});
} else {
if (gLite.isPattern(stroke)) {
var pattern = getPattern(stroke, object, context, object.ownerDocument.defaultView.context, plugin, runtime, this.imagePool);
if (pattern) {
var prevStyle = updateContextIfNotHitCache(context, 'strokeStyle', pattern, renderState.currentContext);
resetStyle = resetStyle !== null && resetStyle !== void 0 ? resetStyle : prevStyle;
}
}
context.stroke();
}
if (resetStyle !== null) {
updateContextIfNotHitCache(context, 'strokeStyle', resetStyle, renderState.currentContext);
}
}
}, {
key: "drawToContext",
value: function drawToContext(context, object, renderState, plugin, runtime) {
var _style$fill;
var nodeName = object.nodeName;
var style = object.parsedStyle;
var _style$opacity = style.opacity,
opacity = _style$opacity === void 0 ? DEFAULT_STYLE.globalAlpha : _style$opacity,
_style$fillOpacity = style.fillOpacity,
fillOpacity = _style$fillOpacity === void 0 ? DEFAULT_STYLE.fillOpacity : _style$fillOpacity,
_style$strokeOpacity = style.strokeOpacity,
strokeOpacity = _style$strokeOpacity === void 0 ? DEFAULT_STYLE.strokeOpacity : _style$strokeOpacity,
_style$lineWidth2 = style.lineWidth,
lineWidth = _style$lineWidth2 === void 0 ? DEFAULT_STYLE.lineWidth : _style$lineWidth2;
var hasFill = style.fill && !style.fill.isNone;
var hasStroke = style.stroke && !style.stroke.isNone && lineWidth > 0;
if (!hasFill && !hasStroke) {
return;
}
var hasShadow = !util.isNil(style.shadowColor) && style.shadowBlur > 0;
var isInnerShadow = style.shadowType === 'inner';
var isFillTransparent = ((_style$fill = style.fill) === null || _style$fill === void 0 ? void 0 : _style$fill.alpha) === 0;
var hasFilter = !!(style.filter && style.filter.length);
// Shadows can only be applied to fill() or stroke(), the default is fill()
var shouldDrawShadowWithStroke = hasShadow && hasStroke && (nodeName === gLite.Shape.PATH || nodeName === gLite.Shape.LINE || nodeName === gLite.Shape.POLYLINE || isFillTransparent || isInnerShadow);
// TODO https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/paint-order
var originGlobalAlpha = null;
if (hasFill) {
if (!shouldDrawShadowWithStroke) {
this.applyShadowAndFilterStyleToContext(context, object, hasShadow, renderState);
}
var updateOpacity = opacity * fillOpacity;
originGlobalAlpha = updateContextIfNotHitCache(context, 'globalAlpha', updateOpacity, renderState.currentContext);
this.fillToContext(context, object, renderState, plugin, runtime);
if (!shouldDrawShadowWithStroke) {
this.clearShadowAndFilterStyleForContext(context, hasShadow, hasFilter, renderState);
}
}
if (hasStroke) {
var clearShadowAndFilter = false;
var _updateOpacity = opacity * strokeOpacity;
var prevOpacity = updateContextIfNotHitCache(context, 'globalAlpha', _updateOpacity, renderState.currentContext);
originGlobalAlpha = hasFill ? originGlobalAlpha : prevOpacity;
if (shouldDrawShadowWithStroke) {
this.applyShadowAndFilterStyleToContext(context, object, hasShadow, renderState);
clearShadowAndFilter = true;
if (isInnerShadow) {
var originBlend = context.globalCompositeOperation;
context.globalCompositeOperation = 'source-atop';
this.strokeToContext(context, object, renderState, plugin, runtime);
context.globalCompositeOperation = originBlend;
this.clearShadowAndFilterStyleForContext(context, hasShadow, hasFilter, renderState, true);
}
}
this.strokeToContext(context, object, renderState, plugin, runtime);
if (clearShadowAndFilter) {
this.clearShadowAndFilterStyleForContext(context, hasShadow, hasFilter, renderState);
}
}
// clear
if (originGlobalAlpha !== null) {
updateContextIfNotHitCache(context, 'globalAlpha', originGlobalAlpha, renderState.currentContext);
}
}
}]);
}();
var DefaultRenderer = /*#__PURE__*/function (_OptimizedDefaultRend) {
function DefaultRenderer() {
_classCallCheck(this, DefaultRenderer);
return _callSuper(this, DefaultRenderer, arguments);
}
_inherits(DefaultRenderer, _OptimizedDefaultRend);
return _createClass(DefaultRenderer, [{
key: "render",
value: function render(context, parsedStyle, object, canvasContext, plugin, runtime) {
var fill = parsedStyle.fill,
fillRule = parsedStyle.fillRule,
_parsedStyle$opacity = parsedStyle.opacity,
opacity = _parsedStyle$opacity === void 0 ? 1 : _parsedStyle$opacity,
_parsedStyle$fillOpac = parsedStyle.fillOpacity,
fillOpacity = _parsedStyle$fillOpac === void 0 ? 1 : _parsedStyle$fillOpac,
stroke = parsedStyle.stroke,
_parsedStyle$strokeOp = parsedStyle.strokeOpacity,
strokeOpacity = _parsedStyle$strokeOp === void 0 ? 1 : _parsedStyle$strokeOp,
_parsedStyle$lineWidt = parsedStyle.lineWidth,
lineWidth = _parsedStyle$lineWidt === void 0 ? 1 : _parsedStyle$lineWidt,
lineCap = parsedStyle.lineCap,
lineJoin = parsedStyle.lineJoin,
shadowType = parsedStyle.shadowType,
shadowColor = parsedStyle.shadowColor,
shadowBlur = parsedStyle.shadowBlur,
filter = parsedStyle.filter,
miterLimit = parsedStyle.miterLimit;
var hasFill = fill && !fill.isNone;
var hasStroke = stroke && !stroke.isNone && lineWidth > 0;
var isFillTransparent = (fill === null || fill === void 0 ? void 0 : fill.alpha) === 0;
var hasFilter = !!(filter && filter.length);
var hasShadow = !util.isNil(shadowColor) && shadowBlur > 0;
var nodeName = object.nodeName;
var isInnerShadow = shadowType === 'inner';
var shouldDrawShadowWithStroke = hasStroke && hasShadow && (nodeName === gLite.Shape.PATH || nodeName === gLite.Shape.LINE || nodeName === gLite.Shape.POLYLINE || isFillTransparent || isInnerShadow);
if (hasFill) {
context.globalAlpha = opacity * fillOpacity;
if (!shouldDrawShadowWithStroke) {
setShadowAndFilter(object, context, hasShadow);
}
applyFill(context, object, fill, fillRule, canvasContext, plugin, runtime, this.imagePool);
if (!shouldDrawShadowWithStroke) {
this.clearShadowAndFilter(context, hasFilter, hasShadow);
}
}
if (hasStroke) {
context.globalAlpha = opacity * strokeOpacity;
context.lineWidth = lineWidth;
if (!util.isNil(miterLimit)) {
context.miterLimit = miterLimit;
}
if (!util.isNil(lineCap)) {
context.lineCap = lineCap;
}
if (!util.isNil(lineJoin)) {
context.lineJoin = lineJoin;
}
if (shouldDrawShadowWithStroke) {
if (isInnerShadow) {
context.globalCompositeOperation = 'source-atop';
}
setShadowAndFilter(object, context, true);
if (isInnerShadow) {
applyStroke(context, object, stroke, canvasContext, plugin, runtime, this.imagePool);
context.globalCompositeOperation = DEFAULT_STYLE.globalCompositeOperation;
this.clearShadowAndFilter(context, hasFilter, true);
}
}
applyStroke(context, object, stroke, canvasContext, plugin, runtime, this.imagePool);
}
}
}, {
key: "clearShadowAndFilter",
value: function clearShadowAndFilter(context, hasFilter, hasShadow) {
if (hasShadow) {
context.shadowColor = 'transparent';
context.shadowBlur = 0;
}
if (hasFilter) {
// save drop-shadow filter
var oldFilter = context.filter;
if (!util.isNil(oldFilter) && oldFilter.indexOf('drop-shadow') > -1) {
context.filter = oldFilter.replace(/drop-shadow\([^)]*\)/, '').trim() || 'none';
}
}
}
}]);
}(OptimizedDefaultRenderer);
/**
* apply before fill and stroke but only once
*/
function setShadowAndFilter(object, context, hasShadow) {
var _object$parsedStyle = object.parsedStyle,
filter = _object$parsedStyle.filter,
shadowColor = _object$parsedStyle.shadowColor,
shadowBlur = _object$parsedStyle.shadowBlur,
shadowOffsetX = _object$parsedStyle.shadowOffsetX,
shadowOffsetY = _object$parsedStyle.shadowOffsetY;
if (filter && filter.length) {
// use raw filter string
context.filter = object.style.filter;
}
if (hasShadow) {
context.shadowColor = shadowColor.toString();
context.shadowBlur = shadowBlur || 0;
context.shadowOffsetX = shadowOffsetX || 0;
context.shadowOffsetY = shadowOffsetY || 0;
}
}
function applyFill(context, object, fill, fillRule, canvasContext, plugin, runtime, imagePool) {
var skipFill = arguments.length > 8 && arguments[8] !== undefined ? arguments[8] : false;
if (Array.isArray(fill)) {
fill.forEach(function (gradient) {
context.fillStyle = getColor(gradient, object, context, imagePool);
if (!skipFill) {
fillRule ? context.fill(fillRule) : context.fill();
}
});
} else {
if (gLite.isPattern(fill)) {
context.fillStyle = getPattern(fill, object, context, canvasContext, plugin, runtime, imagePool);
}
if (!skipFill) {
fillRule ? context.fill(fillRule) : context.fill();
}
}
}
function applyStroke(context, object, stroke, canvasContext, plugin, runtime, imagePool) {
var skipStroke = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : false;
if (Array.isArray(stroke)) {
stroke.forEach(function (gradient) {
context.strokeStyle = getColor(gradient, object, context, imagePool);
if (!skipStroke) {
context.stroke();
}
});
} else {
if (gLite.isPattern(stroke)) {
context.strokeStyle = getPattern(stroke, object, context, canvasContext, plugin, runtime, imagePool);
}
if (!skipStroke) {
context.stroke();
}
}
}
function calculateOverlapRect(rect1, rect2) {
var _rect = _slicedToArray(rect1, 4),
x1 = _rect[0],
y1 = _rect[1],
w1 = _rect[2],
h1 = _rect[3];
var _rect2 = _slicedToArray(rect2, 4),
x2 = _rect2[0],
y2 = _rect2[1],
w2 = _rect2[2],
h2 = _rect2[3];
// 计算重叠区域的左上角和右下角
var overlapLeft = Math.max(x1, x2);
var overlapTop = Math.max(y1, y2);
var overlapRight = Math.min(x1 + w1, x2 + w2);
var overlapBottom = Math.min(y1 + h1, y2 + h2);
if (overlapRight <= overlapLeft || overlapBottom <= overlapTop) {
return null;
}
return [overlapLeft, overlapTop, overlapRight - overlapLeft, overlapBottom - overlapTop];
}
function transformRect(rect, matrix) {
var tl = glMatrix.vec3.transformMat4(glMatrix.vec3.create(), [rect[0], rect[1], 0], matrix);
var tr = glMatrix.vec3.transformMat4(glMatrix.vec3.create(), [rect[0] + rect[2], rect[1], 0], matrix);
var bl = glMatrix.vec3.transformMat4(glMatrix.vec3.create(), [rect[0], rect[1] + rect[3], 0], matrix);
var br = glMatrix.vec3.transformMat4(glMatrix.vec3.create(), [rect[0] + rect[2], rect[1] + rect[3], 0], matrix);
return [Math.min(tl[0], tr[0], bl[0], br[0]), Math.min(tl[1], tr[1], bl[1], br[1]), Math.max(tl[0], tr[0], bl[0], br[0]) - Math.min(tl[0], tr[0], bl[0], br[0]), Math.max(tl[1], tr[1], bl[1], br[1]) - Math.min(tl[1], tr[1], bl[1], br[1])];
}
var ImageRenderer = /*#__PURE__*/function (_DefaultRenderer) {
function ImageRenderer() {
_classCallCheck(this, ImageRenderer);
return _callSuper(this, ImageRenderer, arguments);
}
_inherits(ImageRenderer, _DefaultRenderer);
return _createClass(ImageRenderer, [{
key: "renderDownSampled",
value: function renderDownSampled(context, parsedStyle, object, data) {
var src = data.src,
imageCache = data.imageCache;
if (!imageCache.downSampled) {
this.imagePool.createDownSampledImage(src, object).then(function () {
// be removed from dom tree
if (!object.ownerDocument) {
return;
}
// rerender
// object.dirty();
object.renderable.dirty = true;
object.ownerDocument.defaultView.context.renderingService.dirtify();
})["catch"](function (reason) {
console.error(reason);
});
return;
}
context.drawImage(imageCache.downSampled, Math.floor(data.drawRect[0]), Math.floor(data.drawRect[1]), Math.ceil(data.drawRect[2]), Math.ceil(data.drawRect[3]));
}
}, {
key: "renderTile",
value: function renderTile(context, parsedStyle, object, data) {
var src = data.src,
imageCache = data.imageCache,
imageRect = data.imageRect,
drawRect = data.drawRect;
var originalSize = imageCache.size;
var _context$getTransform = context.getTransform(),
a = _context$getTransform.a,
b = _context$getTransform.b,
c = _context$getTransform.c,
d = _context$getTransform.d,
e = _context$getTransform.e,
f = _context$getTransform.f;
context.resetTransform();
if (!(imageCache !== null && imageCache !== void 0 && imageCache.gridSize)) {
this.imagePool.createImageTiles(src, [], function () {
// be removed from dom tree
if (!object.ownerDocument) {
return;
}
// rerender
// object.dirty();
object.renderable.dirty = true;
object.ownerDocument.defaultView.context.renderingService.dirtify();
}, object)["catch"](function (reason) {
console.error(reason);
});
return;
}
var scaleToOrigin = [originalSize[0] / imageRect[2], originalSize[1] / imageRect[3]];
var scaledTileSize = [imageCache.tileSize[0] / scaleToOrigin[0], imageCache.tileSize[1] / scaleToOrigin[1]];
var _ref = [Math.floor((drawRect[0] - imageRect[0]) / scaledTileSize[0]), Math.ceil((drawRect[0] + drawRect[2] - imageRect[0]) / scaledTileSize[0])],
startTileX = _ref[0],
endTileX = _ref[1];
var _ref2 = [Math.floor((drawRect[1] - imageRect[1]) / scaledTileSize[1]), Math.ceil((drawRect[1] + drawRect[3] - imageRect[1]) / scaledTileSize[1])],
startTileY = _ref2[0],
endTileY = _ref2[1];
for (var tileY = startTileY; tileY <= endTileY; tileY++) {
for (var tileX = startTileX; tileX <= endTileX; tileX++) {
var item = imageCache.tiles[tileY][tileX];
if (item) {
var tileRect = [Math.floor(imageRect[0] + item.tileX * scaledTileSize[0]), Math.floor(imageRect[1] + item.tileY * scaledTileSize[1]), Math.ceil