diff --git a/package.json b/package.json
index d65ca75..14efb9b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "cytoscape-layers",
"description": "Cytoscape.js plugin for rendering layers in SVG, DOM, or Canvas",
- "version": "2.4.1",
+ "version": "2.4.2",
"author": {
"name": "Samuel Gratzl",
"email": "sam@sgratzl.com",
diff --git a/samples/annotations.html b/samples/annotations.html
index 5a8594d..1fcd9d1 100644
--- a/samples/annotations.html
+++ b/samples/annotations.html
@@ -12,6 +12,7 @@
+
diff --git a/samples/annotations.ts b/samples/annotations.ts
index 9bd3368..0c32f98 100644
--- a/samples/annotations.ts
+++ b/samples/annotations.ts
@@ -74,4 +74,18 @@ namespace Annotations {
a.click();
});
});
+ document.getElementById('png2')?.addEventListener('click', () => {
+ layers
+ .png({
+ output: 'blob-promise',
+ ignoreUnsupportedLayerOrder: true,
+ full: true,
+ })
+ .then((r) => {
+ const url = URL.createObjectURL(r);
+ const a = document.getElementById('url') as HTMLAnchorElement;
+ a.href = url;
+ a.click();
+ });
+ });
}
diff --git a/samples/edge.html b/samples/edge.html
new file mode 100644
index 0000000..79366d8
--- /dev/null
+++ b/samples/edge.html
@@ -0,0 +1,21 @@
+
+
+
+ Sample
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/edge.ts b/samples/edge.ts
new file mode 100644
index 0000000..a7d1f81
--- /dev/null
+++ b/samples/edge.ts
@@ -0,0 +1,45 @@
+/* eslint-disable @typescript-eslint/no-namespace */
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+namespace AnimatedEdges {
+ declare const cytoscape: typeof import('cytoscape');
+ declare const CytoscapeLayers: typeof import('../build');
+
+ const cy = cytoscape({
+ container: document.getElementById('app'),
+ elements: fetch('./grid-data.json').then((r) => r.json()),
+ // elements: Promise.resolve([
+ // { data: { id: 'a' } },
+ // { data: { id: 'b' } },
+ // {
+ // data: {
+ // id: 'ab',
+ // source: 'a',
+ // target: 'b',
+ // },
+ // },
+ // ]),
+ layout: {
+ name: 'grid',
+ },
+ style: [
+ {
+ selector: 'edge',
+ style: {
+ 'line-color': 'white',
+ opacity: 0.01,
+ },
+ },
+ ],
+ });
+
+ const layers = CytoscapeLayers.layers(cy);
+
+ const layer = layers.nodeLayer.insertBefore('canvas');
+
+ layers.renderPerEdge(layer, (ctx, _, path) => {
+ ctx.strokeStyle = 'red';
+ ctx.lineWidth = 5;
+ ctx.lineCap = 'round';
+ ctx.stroke(path);
+ });
+}
diff --git a/src/LayersPlugin.ts b/src/LayersPlugin.ts
index c892f45..5e7fe4f 100644
--- a/src/LayersPlugin.ts
+++ b/src/LayersPlugin.ts
@@ -337,24 +337,6 @@ export default class LayersPlugin {
);
}
- const renderer = (
- this.cy as unknown as {
- renderer(): {
- bufferCanvasImage(
- o: cy.ExportJpgStringOptions | cy.ExportJpgBlobOptions | cy.ExportJpgBlobPromiseOptions
- ): HTMLCanvasElement;
- };
- }
- ).renderer();
-
- const bg = options.bg;
-
- const canvas = renderer.bufferCanvasImage({ ...options, bg: undefined });
-
- const width = canvas.width;
- const height = canvas.height;
- const ctx = canvas.getContext('2d')!;
-
const before = layers
.slice(0, nodeIndex)
.reverse()
@@ -364,27 +346,30 @@ export default class LayersPlugin {
.slice(nodeIndex + 1)
.filter((d) => d.supportsRender() && d !== this.dragLayer && d !== this.selectBoxLayer);
- const scale = options.scale ?? 1;
-
- const hint = { scale, width, height, full: options.full ?? false };
-
- ctx.globalCompositeOperation = 'destination-over';
- for (const l of before) {
- l.renderInto(ctx, hint);
- }
-
- ctx.globalCompositeOperation = 'source-over';
- for (const l of after) {
- l.renderInto(ctx, hint);
- }
-
- if (bg) {
- ctx.globalCompositeOperation = 'destination-over';
- ctx.fillStyle = bg;
- ctx.rect(0, 0, width, height);
- ctx.fill();
- }
+ const renderer = (
+ this.cy as unknown as {
+ renderer(): {
+ bufferCanvasImage(
+ o: cy.ExportJpgStringOptions | cy.ExportJpgBlobOptions | cy.ExportJpgBlobPromiseOptions
+ ): HTMLCanvasElement;
+ drawElements(ctx: CanvasRenderingContext2D, elems: cy.Collection): void;
+ };
+ }
+ ).renderer();
+ const drawElements = renderer.drawElements;
+ // patch with all levels
+ renderer.drawElements = function (ctx, elems) {
+ for (const l of before) {
+ l.renderInto(ctx);
+ }
+ drawElements.call(this, ctx, elems);
+ for (const l of after) {
+ l.renderInto(ctx);
+ }
+ };
+ const canvas = renderer.bufferCanvasImage(options);
+ renderer.drawElements = drawElements;
return canvas;
}
diff --git a/src/elements/edges.ts b/src/elements/edges.ts
index dd0ed02..d7a2961 100644
--- a/src/elements/edges.ts
+++ b/src/elements/edges.ts
@@ -1,5 +1,5 @@
import type cy from 'cytoscape';
-import type { ICanvasLayer, IPoint } from '../layers';
+import type { ICanvasLayer, IPoint, IRenderHint } from '../layers';
import { ICallbackRemover, registerCallback } from './utils';
import { IElementLayerOptions, defaultElementLayerOptions } from './common';
@@ -66,12 +66,13 @@ export function renderPerEdge(
if (o.updateOn === 'render') {
layer.updateOnRender = true;
- } else {
+ }
+ if (!o.queryEachTime) {
edges = reevaluateCollection(edges);
layer.cy.on('add remove', o.selector, revaluateAndUpdateOnce);
}
- const renderer = (ctx: CanvasRenderingContext2D) => {
+ const renderer = (ctx: CanvasRenderingContext2D, hint: IRenderHint) => {
if (o.queryEachTime) {
edges = reevaluateCollection(edges);
}
@@ -90,7 +91,12 @@ export function renderPerEdge(
impl && impl.startX != null && impl.startY != null ? { x: impl.startX, y: impl.startY } : edge.sourceEndpoint();
const t = impl && impl.endX != null && impl.endY != null ? { x: impl.endX, y: impl.endY } : edge.targetEndpoint();
- if (o.checkBounds && o.checkBoundsPointCount > 0 && !anyVisible(layer, s, t, o.checkBoundsPointCount)) {
+ if (
+ !hint.forExport &&
+ o.checkBounds &&
+ o.checkBoundsPointCount > 0 &&
+ !anyVisible(layer, s, t, o.checkBoundsPointCount)
+ ) {
return;
}
if (impl && impl.pathCache) {
diff --git a/src/elements/nodes.ts b/src/elements/nodes.ts
index d064517..9988671 100644
--- a/src/elements/nodes.ts
+++ b/src/elements/nodes.ts
@@ -1,5 +1,5 @@
import type cy from 'cytoscape';
-import type { ICanvasLayer, IHTMLLayer, ISVGLayer, ILayer } from '../layers';
+import type { ICanvasLayer, IHTMLLayer, ISVGLayer, ILayer, IRenderHint } from '../layers';
import { SVG_NS } from '../layers/SVGLayer';
import { matchNodes, registerCallback, ICallbackRemover, IMatchOptions } from './utils';
import { IElementLayerOptions, defaultElementLayerOptions } from './common';
@@ -140,7 +140,7 @@ export function renderPerNode(
if (layer.type === 'canvas') {
const oCanvas = o as INodeCanvasLayerOption;
- const renderer = (ctx: CanvasRenderingContext2D) => {
+ const renderer = (ctx: CanvasRenderingContext2D, hint: IRenderHint) => {
const t = ctx.getTransform();
if (o.queryEachTime) {
nodes = reevaluateCollection(nodes);
@@ -150,7 +150,7 @@ export function renderPerNode(
return;
}
const bb = node.boundingBox(o.boundingBox);
- if (oCanvas.checkBounds && !layer.inVisibleBounds(bb)) {
+ if (!hint.forExport && oCanvas.checkBounds && !layer.inVisibleBounds(bb)) {
return;
}
if (oCanvas.position === 'top-left') {
diff --git a/src/layers/ABaseLayer.ts b/src/layers/ABaseLayer.ts
index f83761d..ffc0e7a 100644
--- a/src/layers/ABaseLayer.ts
+++ b/src/layers/ABaseLayer.ts
@@ -7,7 +7,6 @@ import type {
IHTMLStaticLayer,
ISVGStaticLayer,
ICanvasStaticLayer,
- IRenderHint,
} from './interfaces';
import type cy from 'cytoscape';
import type { ICanvasLayerOptions, ISVGLayerOptions, IHTMLLayerOptions } from './public';
@@ -39,7 +38,7 @@ export abstract class ABaseLayer implements IMoveAbleLayer {
return false;
}
- renderInto(_ctx: CanvasRenderingContext2D, _hint: IRenderHint): void {
+ renderInto(_ctx: CanvasRenderingContext2D): void {
// dummy
}
diff --git a/src/layers/CanvasLayer.ts b/src/layers/CanvasLayer.ts
index e8a81f3..6bab3e3 100644
--- a/src/layers/CanvasLayer.ts
+++ b/src/layers/CanvasLayer.ts
@@ -1,11 +1,4 @@
-import type {
- ICanvasLayer,
- ILayerElement,
- ILayerImpl,
- IRenderFunction,
- ICanvasStaticLayer,
- IRenderHint,
-} from './interfaces';
+import type { ICanvasLayer, ILayerElement, ILayerImpl, IRenderFunction, ICanvasStaticLayer } from './interfaces';
import { layerStyle, stopClicks } from './utils';
import { ABaseLayer, ILayerAdapter } from './ABaseLayer';
import type { ICanvasLayerOptions } from './public';
@@ -86,7 +79,7 @@ export class CanvasBaseLayer extends ABaseLayer implements ILayerImpl {
ctx.scale(this.transform.zoom * scale, this.transform.zoom * scale);
for (const r of this.callbacks) {
- r(ctx);
+ r(ctx, {});
}
ctx.restore();
@@ -96,8 +89,10 @@ export class CanvasBaseLayer extends ABaseLayer implements ILayerImpl {
return true;
}
- renderInto(ctx: CanvasRenderingContext2D, hint: IRenderHint): void {
- this.drawImpl(ctx, hint.scale);
+ renderInto(ctx: CanvasRenderingContext2D): void {
+ for (const r of this.callbacks) {
+ r(ctx, { forExport: true });
+ }
}
resize(width: number, height: number) {
diff --git a/src/layers/interfaces.ts b/src/layers/interfaces.ts
index abfc5fc..069be51 100644
--- a/src/layers/interfaces.ts
+++ b/src/layers/interfaces.ts
@@ -6,13 +6,6 @@ export interface ILayerElement {
__cy_layer: ILayer & ILayerImpl;
}
-export interface IRenderHint {
- scale: number;
- width: number;
- height: number;
- full: boolean;
-}
-
export interface ILayerImpl {
readonly root: HTMLElement | SVGElement;
resize(width: number, height: number): void;
@@ -20,5 +13,5 @@ export interface ILayerImpl {
setViewport(tx: number, ty: number, zoom: number): void;
supportsRender(): boolean;
- renderInto(ctx: CanvasRenderingContext2D, hint: IRenderHint): void;
+ renderInto(ctx: CanvasRenderingContext2D): void;
}
diff --git a/src/layers/public.ts b/src/layers/public.ts
index fe86887..54828a9 100644
--- a/src/layers/public.ts
+++ b/src/layers/public.ts
@@ -5,8 +5,12 @@ export interface IPoint {
y: number;
}
+export interface IRenderHint {
+ forExport?: boolean;
+}
+
export interface IRenderFunction {
- (ctx: CanvasRenderingContext2D): void;
+ (ctx: CanvasRenderingContext2D, hint: IRenderHint): void;
}
export interface IDOMUpdateFunction {