image-label-ts
Version:
基于ts的前端图片标注组件,不依赖vue和react
818 lines (810 loc) • 33 kB
JavaScript
import { SVG } from '@svgdotjs/svg.js';
import '@svgdotjs/svg.panzoom.js';
import '@svgdotjs/svg.draggable.js';
import '@svgdotjs/svg.topoly.js';
import { merge } from 'lodash-es';
var LayerTypeEnum;
(function (LayerTypeEnum) {
LayerTypeEnum["polygon"] = "polygon";
// rect = 'rect',
})(LayerTypeEnum || (LayerTypeEnum = {}));
class Layer {
containerCanvas;
type;
dom;
name;
id;
constructor() { }
}
class LayerCanvas {
constructor() { }
}
var img = "data:image/png;base64,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";
class SelectLayer {
containerCanvas;
layer;
selectDom;
cloneDom;
constructor(layer) {
this.layer = layer;
this.containerCanvas = layer.containerCanvas;
this.removeSelect();
this.selectDom = this.containerCanvas.canvas.group().size().attr({ id: 'select' });
this.cloneDom = this.layer.dom
.clone()
.attr({
stroke: this.layer.style.color,
fill: this.layer.style.selectFillColor,
name: 'select-polygon',
'stroke-dasharray': '10,10',
})
.on('click', function (e) {
e.stopPropagation();
})
.addTo(this.containerCanvas.canvas.findOne('#select'));
if (this.layer.able.remove) {
this.createCloseBtn();
}
if (this.layer.able.drag) {
this.selectDom.draggable().on('dragstart', () => {
// 拖动遮罩区域时,隐藏选中图形
layer.dom.hide();
});
this.createLayerDraggableCircle();
this.selectDom.on('dragend', this.layerMaskDragendHandler.bind(this));
}
}
static init(layer) {
return new SelectLayer(layer);
}
removeSelect() {
this.containerCanvas.canvas.find('#select').remove();
}
removeLayer() {
this.removeSelect();
this.layer.dom.remove();
this.containerCanvas.event.remove && this.containerCanvas.event.remove(this.layer);
}
createCloseBtn() {
let minIndex = 0;
let left = 5;
let top = -40;
const points = this.layer.dom.plot();
points.forEach((item, index) => {
if (item[1] < points[minIndex][1]) {
minIndex = index;
}
});
if (points[minIndex][0] + 45 > this.containerCanvas.width) {
left = -45;
}
if (points[minIndex][1] - 40 < 0) {
top = 0;
}
if (this.selectDom.closeBtn) {
this.selectDom.closeBtn.remove();
}
this.selectDom.closeBtn = this.containerCanvas.canvas
.image(new URL(img, import.meta.url).href)
.size(40, 40)
.attr({
x: points[minIndex][0] + left,
y: points[minIndex][1] + top,
style: 'cursor: pointer',
})
.on('click', () => {
this.removeLayer();
this.containerCanvas.removeLayerByLayer(this.layer);
})
.addTo(this.containerCanvas.canvas.findOne('#select'));
}
createLayerDraggableCircle() {
const points = [...this.cloneDom.array()];
points.pop();
points.map(([cx, cy], index) => {
const circle = this.selectDom.circle();
return circle
.radius(this.layer.style.circleRadius / this.containerCanvas.root.zoomNum)
.attr({
cx,
cy,
fill: this.layer.style.color,
index,
})
.draggable()
.on('click', (e) => {
e.stopPropagation();
})
.on('dragstart', () => {
this.layer.dom.hide();
})
.on('dragmove', this.layerMaskCircleDragmoveHandler.bind(this, circle))
.on('dragend', this.layerMaskDragendCircleHandler.bind(this));
});
}
layerMaskCircleDragmoveHandler(circle) {
const { cx, cy, index } = circle.attr(['cx', 'cy', 'index']);
const pointsArray = this.cloneDom.array();
pointsArray[index] = [cx, cy];
if (index === 0) {
pointsArray[pointsArray.length - 1] = [cx, cy];
}
this.cloneDom.plot(pointsArray.join().replace(/,/g, ' '));
}
layerMaskDragendHandler() {
this.layer.dom._array = this.cloneDom.array();
this.layer.dom.plot(this.cloneDom.array().join().replace(/,/g, ' ')).show();
this.dragEnd();
}
dragEnd() {
// 多边形
if (this.layer.type === LayerTypeEnum.polygon) {
const points = Polygon.transformSVGPointsToOrigin({
points: this.cloneDom.array(),
scale: this.containerCanvas.transform.scale,
offsetX: this.containerCanvas.transform.offsetX,
offsetY: this.containerCanvas.transform.offsetY,
});
this.layer.setOption({
points: points,
});
}
if (this.containerCanvas.event.dragEnd) {
this.containerCanvas.event.dragEnd(this.layer);
}
}
layerMaskDragendCircleHandler() {
this.layer.dom.plot(this.cloneDom.array().join().replace(/,/g, ' ')).show();
this.selectDom.closeBtn.remove();
this.selectDom.closeBtn = null;
this.createCloseBtn();
this.dragEnd();
}
}
const defaultIPolygonOption = {
name: '',
points: [],
style: {
color: '#E1600A',
fillColor: 'rgba(0, 0, 0, 0)',
selectFillColor: 'rgba(0, 0, 0, 0)',
strokeWidth: 2,
circleRadius: 4,
},
able: {
click: false,
drag: true,
remove: true,
},
};
class Polygon extends Layer {
points;
style;
able;
canvasOnce;
constructor(canvas, option) {
super();
this.containerCanvas = canvas;
const { x, y, width, height } = this.containerCanvas.labelImage.attr([
'x',
'y',
'width',
'height',
]);
this.canvasOnce = this.containerCanvas.canvas
.group()
.attr({ id: 'polygonCanvas-polygon-once', width, height });
this.canvasOnce.rect(width, height).attr({ x, y });
this.setOption(option);
if (!this.id) {
this.id = Math.floor(Math.random() * 1e10).toString();
}
this.draw();
}
static init(canvas, option) {
return new Polygon(canvas, option);
}
static transformOriginPointsToSVG(params) {
const { points, scale, offsetX, offsetY } = params;
return points.map((item) => [item.x / scale + offsetX, item.y / scale + offsetY]);
}
static transformSVGPointsToOrigin(params) {
const { points, scale, offsetX, offsetY } = params;
const res = points.map((item) => ({
x: Math.round((item[0] - offsetX) * scale),
y: Math.round((item[1] - offsetY) * scale),
}));
res.pop();
return res;
}
transformOriginPointsToSVG(points) {
const { scale = 1, offsetX = 0, offsetY = 0 } = this.containerCanvas.transform;
return points.map((item) => [item.x / scale + offsetX, item.y / scale + offsetY]);
}
transformSVGPointsToOrigin(points) {
const { scale = 1, offsetX = 0, offsetY = 0 } = this.containerCanvas.transform;
const res = points.map((item) => ({
x: Math.round((item[0] - offsetX) * scale),
y: Math.round((item[1] - offsetY) * scale),
}));
res.pop();
return res;
}
remove() {
this.dom.remove();
}
select() {
this.containerCanvas.event.select(this);
this.containerCanvas.selectLayer = SelectLayer.init(this);
}
// todo 此api出错率高,可做单元测试
setOption(option) {
this.type = LayerTypeEnum.polygon;
const _option = merge({}, defaultIPolygonOption, this, JSON.parse(JSON.stringify(option))); // 深度合并
Object.keys(_option).forEach((key) => {
this[key] = option[key] ? _option[key] : this[key] || defaultIPolygonOption[key];
});
}
draw() {
const pointsFormat = this.transformOriginPointsToSVG(this.points).map((item) => {
return ['L', item[0], item[1]];
});
pointsFormat.unshift([
'M',
pointsFormat[pointsFormat.length - 1][1],
pointsFormat[pointsFormat.length - 1][2],
]);
const polyPath = this.containerCanvas.canvas
.path()
.plot(['M', pointsFormat[0][1], pointsFormat[0][2]].join().replace(/,/g, ' '))
.attr({
stroke: this.style.color,
fill: 'none',
'stroke-width': this.style.strokeWidth / this.containerCanvas.root.zoomNum,
});
try {
this.dom = polyPath
.plot(pointsFormat.join().replace(/,/g, ' '))
.toPoly()
.attr({
color: this.style.color,
stroke: this.style.color,
'stroke-width': this.style.strokeWidth / this.containerCanvas.root.zoomNum,
fill: this.style.fillColor,
type: 'polygon',
})
.addTo(this.containerCanvas.canvas);
}
catch (e) {
console.error('draw error', e);
this.canvasOnce.remove();
this.canvasOnce = null;
}
this.dom.attr({ name: this.name });
if (this.able.click) {
this.dom.on('click', (e) => {
this.select();
e.stopPropagation();
});
}
this.canvasOnce.remove();
this.canvasOnce = null;
return this;
}
// todo
reDraw(option) {
this.setOption(option);
// ...
}
}
class PolygonCanvas extends LayerCanvas {
containerCanvas;
polygonCanvas;
tempPath; // 描述手动绘图的dom对象
polyPath; // 描述手动绘图的dom对象
closePoint; // 手动绘图时首尾连通的那个点
constructor(canvas) {
super();
this.containerCanvas = canvas;
const { x, y, width, height } = this.containerCanvas.labelImage.attr([
'x',
'y',
'width',
'height',
]);
this.polygonCanvas = this.containerCanvas.canvas
.group()
.attr({ id: 'drawLayer', width, height });
this.polygonCanvas
.rect(width, height)
.attr({ x, y, 'fill-opacity': 0 })
.on('mousemove', this.mouseMoveFollow.bind(this));
this.polygonCanvas.pathPoints = [];
this.tempPath = null;
this.polygonCanvas.on('mousemove', this.drawTempPath.bind(this));
this.polygonCanvas.on('mousemove', this.polygonCountIsClose.bind(this));
this.polygonCanvas.on('click', this.drawPolyPathHandler.bind(this));
}
static init(canvas) {
return new PolygonCanvas(canvas);
}
drawTempPath(e) {
if (this.polygonCanvas.pathPoints.length > 0) {
const [x, y] = this.currentNodeMovePosition(e);
const tempPathArray = this.polyPath.array();
tempPathArray[this.polygonCanvas.pathPoints.length] = ['L', x, y];
if (!this.tempPath) {
this.tempPath = this.polygonCanvas
.path()
.plot(tempPathArray.join().replace(/,/g, ' '))
.attr({
stroke: this.containerCanvas.style.color,
fill: this.containerCanvas.style.fillColor,
'fill-opacity': 0.6,
'stroke-width': this.containerCanvas.style.strokeWidth / this.containerCanvas.root.zoomNum,
'stroke-dasharray': '10,10',
});
}
else {
this.tempPath.plot(tempPathArray.join().replace(/,/g, ' '));
}
}
}
// 点击回调事件,绘点
drawPolyPathHandler(e) {
const [x, y] = this.currentNodeMovePosition(e);
if (this.polygonCanvas.pathPoints.length === 0) {
this.polygonCanvas.pathPoints.push(this.polygonCanvas
.circle()
.radius(this.containerCanvas.style.circleRadius / this.containerCanvas.root.zoomNum)
.attr({ cx: x, cy: y, fill: this.containerCanvas.style.color, id: 'begin' }));
this.polyPath = this.polygonCanvas
.path()
.plot(['M', x, y].join().replace(/,/g, ' '))
.attr({
stroke: this.containerCanvas.style.color,
fill: 'none',
'stroke-width': this.containerCanvas.style.strokeWidth / this.containerCanvas.root.zoomNum,
});
}
else {
this.polygonCanvas.pathPoints.push(this.polygonCanvas
.circle()
.radius(this.containerCanvas.style.circleRadius / this.containerCanvas.root.zoomNum)
.attr({ cx: x, cy: y, fill: this.containerCanvas.style.color }));
// pathArray 为构成图形的点位
const pathArray = this.polyPath.array();
pathArray.push(['L', x, y]);
this.polyPath.plot(pathArray.join().replace(/,/g, ' '));
}
}
// 检测鼠标是否移动到了多边形的第一个点(是否形成封闭多边形)
polygonCountIsClose(e) {
if (this.polygonCanvas.pathPoints.length > 2) {
const [x, y] = this.currentNodeMovePosition(e);
const { cx, cy } = this.polygonCanvas.pathPoints[0].attr(['cx', 'cy']);
const a = Math.abs(cx - x);
const b = Math.abs(cy - y);
if (Math.sqrt(a * a + b * b) < 15 / this.containerCanvas.root.zoomNum) {
if (!this.closePoint) {
this.closePoint = this.polygonCanvas
.circle()
.radius(15 / this.containerCanvas.root.zoomNum)
.attr({
cx,
cy,
fill: this.containerCanvas.style.color,
'fill-opacity': 0.5,
})
.on('click', (e) => {
this.pathToPolygon();
e.stopPropagation();
});
}
this.polygonCanvas.followCircle && this.polygonCanvas.followCircle.hide();
this.closePoint?.show();
const tempPathArray = this.polyPath.array();
tempPathArray[this.polygonCanvas.pathPoints.length] = ['L', cx, cy];
this.tempPath.plot(tempPathArray.join().replace(/,/g, ' '));
}
else {
if (this.closePoint) {
this.closePoint.remove();
this.closePoint = null;
}
this.polygonCanvas.followCircle && this.polygonCanvas.followCircle.show();
}
}
}
pathToPolygon() {
const points = this.polyPath.array().map((item) => {
return [item[1], item[2]];
});
const formatPoints = Polygon.transformSVGPointsToOrigin({
points,
scale: this.containerCanvas.transform.scale,
offsetX: this.containerCanvas.transform.offsetX,
offsetY: this.containerCanvas.transform.offsetY,
});
this.containerCanvas.drawLayer(LayerTypeEnum.polygon, {
name: this.containerCanvas.layerName,
points: formatPoints,
style: {
color: this.containerCanvas.style.color,
fillColor: this.containerCanvas.style.fillColor,
selectFillColor: this.containerCanvas.style.selectFillColor,
strokeWidth: this.containerCanvas.style.strokeWidth,
circleRadius: this.containerCanvas.style.circleRadius,
},
able: {
click: true,
drag: true,
remove: true,
},
});
this.remove();
this.containerCanvas.drawDone && this.containerCanvas.drawDone();
}
remove() {
if (this.containerCanvas.canvas.findOne('#drawLayer')) {
this.containerCanvas.canvas.findOne('#drawLayer').remove();
}
this.polygonCanvas = null;
this.tempPath = null;
this.polyPath = null;
this.closePoint = null;
}
currentNodeMovePosition(e) {
const bgNode = this.containerCanvas.root.findOne('#labelImage-background').node;
const { x, y } = bgNode === null ? { x: 0, y: 0 } : bgNode.getClientRects()[0];
return [
(1 / this.containerCanvas.root.zoomNum) *
(e.clientX - x + this.containerCanvas.transform.offsetX),
(1 / this.containerCanvas.root.zoomNum) *
(e.clientY - y + this.containerCanvas.transform.offsetY),
];
}
mouseMoveFollow(e) {
const [cx, cy] = this.currentNodeMovePosition(e);
this.containerCanvas.root.on('mousemove', this.removeFollower.bind(this));
const polygonCanvas = this.polygonCanvas;
if (!polygonCanvas.followCircle) {
polygonCanvas.followCircle = polygonCanvas
.circle()
.radius(this.containerCanvas.style.circleRadius / this.containerCanvas.root.zoomNum)
.attr({ cx, cy, fill: this.containerCanvas.style.color })
.on('mousemove', (e) => {
const [x, y] = this.currentNodeMovePosition(e);
polygonCanvas.followCircle.attr({ cx: x, cy: y });
});
}
else {
polygonCanvas.followCircle.attr({ cx, cy });
}
}
removeFollower(e) {
if (this.containerCanvas.root) {
const polygonCanvas = this.polygonCanvas;
const [cx, cy] = this.currentNodeMovePosition(e);
const { width, height } = polygonCanvas.attr(['width', 'height']);
if (cx > width || cy > height || cx < 0 || cy < 0) {
if (polygonCanvas.followCircle) {
polygonCanvas.followCircle.remove();
polygonCanvas.followCircle = null;
this.containerCanvas.root.off('mousemove');
}
}
}
}
}
const loadImage = (url) => {
return new Promise((res) => {
const image = new Image();
image.onload = () => {
res(image);
};
image.src = url;
});
};
var ModeEnum;
(function (ModeEnum) {
ModeEnum["default"] = "default";
ModeEnum["drag"] = "drag";
ModeEnum["poly"] = "poly";
})(ModeEnum || (ModeEnum = {}));
const defaultContainerCanvasOption = {
width: 0,
height: 0,
zoom: {
zoomFactor: 0.5,
zoomMin: 0.5,
zoomMax: 20,
},
style: {
color: '#E1600A',
fillColor: 'rgba(0, 0, 0, 0)',
selectFillColor: 'rgba(0, 0, 0, 0)',
strokeWidth: 2,
circleRadius: 4,
},
layerName: 'default-layer',
mode: ModeEnum.default,
event: {
draw: () => { },
select: () => { },
remove: () => { },
dragEnd: () => { },
clickCanvas: () => { },
beforeLoadImage: () => { },
loadedImage: () => { },
},
transform: {
scale: 1,
fixedOffset: false,
offsetX: 0,
offsetY: 0,
},
};
class ContainerCanvas {
root;
canvas;
layerCanvas; // layer画布, 手动画图时使用
layers = []; // 标注的图形
labelImage; // 标注的图
selectLayer;
polygonCanvas;
width;
height;
zoom;
style;
layerName;
mode;
event;
transform;
imageUrl = '';
drawImageWidth = 0;
drawImageHeight = 0;
constructor(el, option) {
this.setOption(option);
this.root = SVG().addTo(el).attr({ style: 'vertical-align: bottom' });
this.root.zoomNum = 1;
this.canvas = this.root.group().attr({ id: 'canvas' });
this.root.size(this.width, this.height).viewbox(0, 0, this.width, this.height);
if (this.mode !== ModeEnum.default) {
this.root = this.root.panZoom(this.zoom);
}
this.root.on('zoom', (lvl) => {
this.root.zoomNum = lvl.detail.level;
this.canvas.find('path').forEach((path) => {
path.attr({
'stroke-width': this.style.strokeWidth / lvl.detail.level,
});
});
this.canvas.find('polyline').forEach((polyline) => {
polyline.attr({
'stroke-width': this.style.strokeWidth / lvl.detail.level,
});
});
this.canvas.find('circle').forEach((circle) => {
circle.radius(this.style.circleRadius / lvl.detail.level);
});
});
this.canvas.on('click', () => {
this.selectLayer && this.selectLayer.removeSelect();
this.selectLayer = undefined;
this.event.clickCanvas && this.event.clickCanvas();
});
}
static init(el, option) {
return new ContainerCanvas(el, option);
}
setOption(option) {
const _option = merge({}, defaultContainerCanvasOption, option);
Object.keys(_option).forEach((key) => {
// this[key] = _option[key];
this[key] = this[key] || _option[key];
});
}
setMode(mode) {
this.mode = mode;
switch (mode) {
case ModeEnum.drag:
this.polygonCanvas && this.polygonCanvas.remove();
this.polygonCanvas = null;
break;
case ModeEnum.poly:
// 创建layer画布
this.polygonCanvas = PolygonCanvas.init(this);
break;
case ModeEnum.default:
break;
}
}
toggleMode(mode1, mode2) {
if (this.mode === mode1) {
this.setMode(mode2);
}
else {
this.setMode(mode1);
}
}
// 清除整个画布
clear() {
this.canvas.children().forEach((child) => {
child.remove();
});
this.layers = [];
}
// 获取所有的图形 (ContainerCanvas实例是个ref时, containerCanvas.layers也会变成响应式)
getLayers(type) {
return this.layers.filter((layer) => layer.type === type);
}
// 获取所有的图形
getLayersByName(type, name) {
return this.getLayers(type).filter((layer) => layer.name === name);
}
// 根据多边形删除多边形
removeLayerByLayer(layer) {
layer.remove();
for (let i = 0; i < this.layers.length; i++) {
if (this.layers[i] === layer) {
this.layers.splice(i, 1);
return;
}
}
}
// 根据多边形删除多边形
removeLayerById(id) {
const layer = this.layers.find(item => item.id === id);
if (layer) {
layer.remove();
const index = this.layers.indexOf(layer);
this.layers.splice(index, 1);
}
}
drawDone() {
this.root.off('mousemove');
this.setMode(ModeEnum.drag);
this.event.draw && this.event.draw(this.layers[this.layers.length - 1]);
}
loadImageStack = [];
loadImagePool = (imageUrl, fn) => {
return new Promise((resolve) => {
const p = loadImage(imageUrl);
this.loadImageStack.push(p);
p.then((image) => {
if (this.loadImageStack[this.loadImageStack.length - 1] === p) {
fn.call(this, image);
}
this.loadImageStack.splice(0, this.loadImageStack.indexOf(p));
resolve(null);
});
});
};
// 加载图片
async loadImage(imageUrl, imageWidth, imageHeight) {
this.imageUrl = imageUrl;
if (this.mode !== ModeEnum.default) {
this.setMode(ModeEnum.drag);
}
this.root.zoom(1);
this.root.zoomNum = 1;
if (this.mode !== ModeEnum.default) {
this.root = this.root.panZoom(this.zoom);
}
this.root.viewbox(0, 0, this.width, this.height);
this.clear();
this.layers = [];
if (!imageUrl) {
return;
}
this.event.beforeLoadImage && this.event.beforeLoadImage();
await this.loadImagePool(imageUrl, (image) => {
this.event.loadedImage && this.event.loadedImage();
imageWidth = imageWidth || image.width;
imageHeight = imageHeight || image.height;
this.transform.scale = Math.max(imageWidth / this.width, imageHeight / this.height);
this.drawImageWidth = imageWidth / this.transform.scale;
this.drawImageHeight = imageHeight / this.transform.scale;
if (!this.transform.fixedOffset) {
this.transform.offsetX = (this.width - this.drawImageWidth) / 2;
this.transform.offsetY = (this.height - this.drawImageHeight) / 2;
}
this._loadImage(imageUrl).size(this.drawImageWidth, this.drawImageHeight).attr({
x: this.transform.offsetX,
y: this.transform.offsetY,
});
});
}
_loadImage(imageUrl) {
const image = this.canvas.findOne('#labelImage-background');
if (image) {
image.load(imageUrl);
return image;
}
else {
this.labelImage = this.canvas.image(imageUrl).attr({ id: 'labelImage-background' });
return this.labelImage;
}
}
// 画蒙层
drawMask(color) {
this.canvas
.rect(this.drawImageWidth, this.drawImageHeight)
.fill(color)
.attr({ x: this.transform.offsetX, y: this.transform.offsetY });
}
// 画一个图片的多边形可使区域
drawPolygonImages(points, imageUrl = this.imageUrl) {
if (!points || !points.length)
return;
const pattern = this.root
.defs()
.pattern()
.attr({ x: this.transform.offsetX, y: this.transform.offsetY });
pattern.image(imageUrl).size(this.drawImageWidth, this.drawImageHeight);
points.forEach(point => {
const _points = Polygon.transformOriginPointsToSVG({
points: point,
scale: this.transform.scale,
offsetX: this.transform.offsetX,
offsetY: this.transform.offsetY,
});
const str = _points.map((item) => `${item[0]},${item[1]}`).join(' ');
this.canvas.polygon(str).fill(pattern);
});
}
// 绘制一个图形
drawLayer(type, option) {
if (type === LayerTypeEnum.polygon) {
try {
this.layers.push(Polygon.init(this, option));
}
catch (e) {
console.error('drawLayer error', e);
}
}
}
// 绘制多个图形
drawLayers(type, options) {
options.forEach((option) => {
option.points.forEach((point) => {
this.drawLayer(type, {
...option,
points: point,
});
});
});
}
hideLayers(type) {
this.getLayers(type).forEach((item) => {
item.dom.style.display = 'none';
});
}
showLayers(type) {
this.getLayers(type).forEach((item) => {
item.dom.style.display = 'block';
});
}
hideLayersByName(type, name) {
this.getLayersByName(type, name).forEach((item) => {
item.dom.node.style.display = 'none';
});
}
showLayersByName(type, name) {
this.getLayersByName(type, name).forEach((item) => {
item.dom.node.style.display = 'block';
});
}
// 切换图片并且绘制多边形
async loadImageAndDrawLayers(imageUrl, options) {
await this.loadImage(imageUrl);
(options || []).forEach((option) => {
if (option.type === LayerTypeEnum.polygon) {
this.drawLayers(option.type, option.option);
}
});
}
}
export { ContainerCanvas, Layer, LayerCanvas, LayerTypeEnum, ModeEnum, Polygon, PolygonCanvas, SelectLayer, defaultContainerCanvasOption };
//# sourceMappingURL=index.js.map