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 {