完成世界书、骰子、apiconfig页面处理

This commit is contained in:
2026-04-30 01:35:10 +08:00
parent a3e3711b2b
commit ba9b925c32
4602 changed files with 785225 additions and 23 deletions

View File

@@ -0,0 +1,105 @@
import * as util from '../../util/index.mjs';
import * as math from '../../math.mjs';
import * as is from '../../is.mjs';
let defaults = {
fit: true, // whether to fit the viewport to the graph
padding: 30, // the padding on fit
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
avoidOverlap: true, // prevents node overlap, may overflow boundingBox and radius if not enough space
nodeDimensionsIncludeLabels: false, // Excludes the label when calculating node bounding boxes for the layout algorithm
spacingFactor: undefined, // Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up
radius: undefined, // the radius of the circle
startAngle: 3 / 2 * Math.PI, // where nodes start in radians
sweep: undefined, // how many radians should be between the first and last node (defaults to full circle)
clockwise: true, // whether the layout should go clockwise (true) or counterclockwise/anticlockwise (false)
sort: undefined, // a sorting function to order the nodes; e.g. function(a, b){ return a.data('weight') - b.data('weight') }
animate: false, // whether to transition the node positions
animationDuration: 500, // duration of animation in ms if enabled
animationEasing: undefined, // easing of animation if enabled
animateFilter: function ( node, i ){ return true; }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
ready: undefined, // callback on layoutready
stop: undefined, // callback on layoutstop
transform: function (node, position ){ return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
};
function CircleLayout( options ){
this.options = util.extend( {}, defaults, options );
}
CircleLayout.prototype.run = function(){
let params = this.options;
let options = params;
let cy = params.cy;
let eles = options.eles;
let clockwise = options.counterclockwise !== undefined ? !options.counterclockwise : options.clockwise;
let nodes = eles.nodes().not( ':parent' );
if( options.sort ){
nodes = nodes.sort( options.sort );
}
let bb = math.makeBoundingBox( options.boundingBox ? options.boundingBox : {
x1: 0, y1: 0, w: cy.width(), h: cy.height()
} );
let center = {
x: bb.x1 + bb.w / 2,
y: bb.y1 + bb.h / 2
};
let sweep = options.sweep === undefined ? 2 * Math.PI - 2 * Math.PI / nodes.length : options.sweep;
let dTheta = sweep / ( Math.max( 1, nodes.length - 1 ) );
let r;
let minDistance = 0;
for( let i = 0; i < nodes.length; i++ ){
let n = nodes[ i ];
let nbb = n.layoutDimensions( options );
let w = nbb.w;
let h = nbb.h;
minDistance = Math.max( minDistance, w, h );
}
if( is.number( options.radius ) ){
r = options.radius;
} else if( nodes.length <= 1 ){
r = 0;
} else {
r = Math.min( bb.h, bb.w ) / 2 - minDistance;
}
// calculate the radius
if( nodes.length > 1 && options.avoidOverlap ){ // but only if more than one node (can't overlap)
minDistance *= 1.75; // just to have some nice spacing
let dcos = Math.cos( dTheta ) - Math.cos( 0 );
let dsin = Math.sin( dTheta ) - Math.sin( 0 );
let rMin = Math.sqrt( minDistance * minDistance / ( dcos * dcos + dsin * dsin ) ); // s.t. no nodes overlapping
r = Math.max( rMin, r );
}
let getPos = function( ele, i ){
let theta = options.startAngle + i * dTheta * ( clockwise ? 1 : -1 );
let rx = r * Math.cos( theta );
let ry = r * Math.sin( theta );
let pos = {
x: center.x + rx,
y: center.y + ry
};
return pos;
};
eles.nodes().layoutPositions( this, options, getPos );
return this; // chaining
};
export default CircleLayout;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
import * as util from '../../util/index.mjs';
import * as math from '../../math.mjs';
let defaults = {
fit: true, // whether to fit to viewport
padding: 30, // fit padding
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
animate: false, // whether to transition the node positions
animationDuration: 500, // duration of animation in ms if enabled
animationEasing: undefined, // easing of animation if enabled
animateFilter: function ( node, i ){ return true; }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
ready: undefined, // callback on layoutready
stop: undefined, // callback on layoutstop
transform: function (node, position ){ return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
};
function RandomLayout( options ){
this.options = util.extend( {}, defaults, options );
}
RandomLayout.prototype.run = function(){
let options = this.options;
let cy = options.cy;
let eles = options.eles;
let bb = math.makeBoundingBox( options.boundingBox ? options.boundingBox : {
x1: 0, y1: 0, w: cy.width(), h: cy.height()
} );
let getPos = function( node, i ){
return {
x: bb.x1 + Math.round( Math.random() * bb.w ),
y: bb.y1 + Math.round( Math.random() * bb.h )
};
};
eles.nodes().layoutPositions( this, options, getPos );
return this; // chaining
};
export default RandomLayout;

View File

@@ -0,0 +1,29 @@
import * as util from '../../../../util/index.mjs';
import coords from './coords.mjs';
import edgeArrows from './edge-arrows.mjs';
import edgeControlPoints from './edge-control-points.mjs';
import edgeEndpoints from './edge-endpoints.mjs';
import edgeProjection from './edge-projection.mjs';
import labels from './labels.mjs';
import nodes from './nodes.mjs';
import renderedStyle from './rendered-style.mjs';
import zOrdering from './z-ordering.mjs';
var BRp = {};
[
coords,
edgeArrows,
edgeControlPoints,
edgeEndpoints,
edgeProjection,
labels,
nodes,
renderedStyle,
zOrdering
].forEach(function( props ){
util.extend( BRp, props );
});
export default BRp;

View File

@@ -0,0 +1,38 @@
var BRp = {};
BRp.getCachedImage = function( url, crossOrigin, onLoad ){
var r = this;
var imageCache = r.imageCache = r.imageCache || {};
var cache = imageCache[ url ];
if( cache ){
if( !cache.image.complete ){
cache.image.addEventListener('load', onLoad);
}
return cache.image;
} else {
cache = imageCache[ url ] = imageCache[ url ] || {};
var image = cache.image = new Image(); // eslint-disable-line no-undef
image.addEventListener('load', onLoad);
image.addEventListener('error', function(){ image.error = true; });
// #1582 safari doesn't load data uris with crossOrigin properly
// https://bugs.webkit.org/show_bug.cgi?id=123978
var dataUriPrefix = 'data:';
var isDataUri = url.substring( 0, dataUriPrefix.length ).toLowerCase() === dataUriPrefix;
if( !isDataUri ){
// if crossorigin is 'null'(stringified), then manually set it to null
crossOrigin = crossOrigin === 'null' ? null : crossOrigin;
image.crossOrigin = crossOrigin; // prevent tainted canvas
}
image.src = url;
return image;
}
};
export default BRp;

View File

@@ -0,0 +1,224 @@
import * as util from '../../../util/index.mjs';
import * as is from '../../../is.mjs';
import arrowShapes from './arrow-shapes.mjs';
import coordEleMath from './coord-ele-math/index.mjs';
import images from './images.mjs';
import loadListeners from './load-listeners.mjs';
import nodeShapes from './node-shapes.mjs';
import redraw from './redraw.mjs';
var BaseRenderer = function( options ){ this.init( options ); };
var BR = BaseRenderer;
var BRp = BR.prototype;
BRp.clientFunctions = [ 'redrawHint', 'render', 'renderTo', 'matchCanvasSize', 'nodeShapeImpl', 'arrowShapeImpl' ];
BRp.init = function( options ){
var r = this;
r.options = options;
r.cy = options.cy;
var ctr = r.container = options.cy.container();
var containerWindow = r.cy.window();
// prepend a stylesheet in the head such that
if( containerWindow ){
var document = containerWindow.document;
var head = document.head;
var stylesheetId = '__________cytoscape_stylesheet';
var className = '__________cytoscape_container';
var stylesheetAlreadyExists = document.getElementById( stylesheetId ) != null;
if( ctr.className.indexOf( className ) < 0 ){
ctr.className = ( ctr.className || '' ) + ' ' + className;
}
if( !stylesheetAlreadyExists ){
var stylesheet = document.createElement('style');
stylesheet.id = stylesheetId;
stylesheet.textContent = '.'+className+' { position: relative; }';
head.insertBefore( stylesheet, head.children[0] ); // first so lowest priority
}
var computedStyle = containerWindow.getComputedStyle( ctr );
var position = computedStyle.getPropertyValue('position');
if( position === 'static' ){
util.warn('A Cytoscape container has style position:static and so can not use UI extensions properly');
}
}
r.selection = [ undefined, undefined, undefined, undefined, 0]; // Coordinates for selection box, plus enabled flag
r.bezierProjPcts = [ 0.05, 0.225, 0.4, 0.5, 0.6, 0.775, 0.95 ];
//--Pointer-related data
r.hoverData = {down: null, last: null,
downTime: null, triggerMode: null,
dragging: false,
initialPan: [ null, null ], capture: false};
r.dragData = {possibleDragElements: []};
r.touchData = {
start: null, capture: false,
// These 3 fields related to tap, taphold events
startPosition: [ null, null, null, null, null, null ],
singleTouchStartTime: null,
singleTouchMoved: true,
now: [ null, null, null, null, null, null ],
earlier: [ null, null, null, null, null, null ]
};
r.redraws = 0;
r.showFps = options.showFps;
r.debug = options.debug;
r.webgl = options.webgl;
r.hideEdgesOnViewport = options.hideEdgesOnViewport;
r.textureOnViewport = options.textureOnViewport;
r.wheelSensitivity = options.wheelSensitivity;
r.motionBlurEnabled = options.motionBlur; // on by default
r.forcedPixelRatio = is.number(options.pixelRatio) ? options.pixelRatio : null;
r.motionBlur = options.motionBlur; // for initial kick off
r.motionBlurOpacity = options.motionBlurOpacity;
r.motionBlurTransparency = 1 - r.motionBlurOpacity;
r.motionBlurPxRatio = 1;
r.mbPxRBlurry = 1; //0.8;
r.minMbLowQualFrames = 4;
r.fullQualityMb = false;
r.clearedForMotionBlur = [];
r.desktopTapThreshold = options.desktopTapThreshold;
r.desktopTapThreshold2 = options.desktopTapThreshold * options.desktopTapThreshold;
r.touchTapThreshold = options.touchTapThreshold;
r.touchTapThreshold2 = options.touchTapThreshold * options.touchTapThreshold;
r.tapholdDuration = 500;
r.bindings = [];
r.beforeRenderCallbacks = [];
r.beforeRenderPriorities = { // higher priority execs before lower one
animations: 400,
eleCalcs: 300,
eleTxrDeq: 200,
lyrTxrDeq: 150,
lyrTxrSkip: 100,
};
r.registerNodeShapes();
r.registerArrowShapes();
r.registerCalculationListeners();
};
BRp.notify = function( eventName, eles ) {
var r = this;
var cy = r.cy;
// the renderer can't be notified after it's destroyed
if( this.destroyed ){ return; }
if( eventName === 'init' ){
r.load();
return;
}
if( eventName === 'destroy' ){
r.destroy();
return;
}
if(
eventName === 'add'
|| eventName === 'remove'
|| (eventName === 'move' && cy.hasCompoundNodes())
|| eventName === 'load'
|| eventName === 'zorder'
|| eventName === 'mount'
){
r.invalidateCachedZSortedEles();
}
if( eventName === 'viewport' ){
r.redrawHint( 'select', true );
}
if( eventName === 'gc' ){
r.redrawHint( 'gc', true );
}
if( eventName === 'load' || eventName === 'resize' || eventName === 'mount' ){
r.invalidateContainerClientCoordsCache();
r.matchCanvasSize( r.container );
}
r.redrawHint( 'eles', true );
r.redrawHint( 'drag', true );
this.startRenderLoop();
this.redraw();
};
BRp.destroy = function(){
var r = this;
r.destroyed = true;
r.cy.stopAnimationLoop();
for( var i = 0; i < r.bindings.length; i++ ){
var binding = r.bindings[ i ];
var b = binding;
var tgt = b.target;
( tgt.off || tgt.removeEventListener ).apply( tgt, b.args );
}
r.bindings = [];
r.beforeRenderCallbacks = [];
r.onUpdateEleCalcsFns = [];
if( r.removeObserver ){
r.removeObserver.disconnect();
}
if( r.styleObserver ){
r.styleObserver.disconnect();
}
if( r.resizeObserver ){
r.resizeObserver.disconnect();
}
if( r.labelCalcDiv ){
try {
document.body.removeChild( r.labelCalcDiv ); // eslint-disable-line no-undef
} catch( e ){
// ie10 issue #1014
}
}
};
BRp.isHeadless = function(){
return false;
};
[
arrowShapes,
coordEleMath,
images,
loadListeners,
nodeShapes,
redraw
].forEach( function( props ){
util.extend( BRp, props );
} );
export default BR;

View File

@@ -0,0 +1,431 @@
/* global Path2D */
import * as util from '../../../util/index.mjs';
import {drawPreparedRoundCorner} from "../../../round.mjs";
let CRp = {};
CRp.drawEdge = function( context, edge, shiftToOriginWithBb, drawLabel = true, shouldDrawOverlay = true, shouldDrawOpacity = true ){
let r = this;
let rs = edge._private.rscratch;
if( shouldDrawOpacity && !edge.visible() ){ return; }
// if bezier ctrl pts can not be calculated, then die
if( rs.badLine || rs.allpts == null || isNaN(rs.allpts[0]) ){ // isNaN in case edge is impossible and browser bugs (e.g. safari)
return;
}
let bb;
if( shiftToOriginWithBb ){
bb = shiftToOriginWithBb;
context.translate( -bb.x1, -bb.y1 );
}
let opacity = shouldDrawOpacity ? edge.pstyle('opacity').value : 1;
let lineOpacity = shouldDrawOpacity ? edge.pstyle('line-opacity').value : 1;
let curveStyle = edge.pstyle('curve-style').value;
let lineStyle = edge.pstyle('line-style').value;
let edgeWidth = edge.pstyle('width').pfValue;
let lineCap = edge.pstyle('line-cap').value;
let lineOutlineWidth = edge.pstyle('line-outline-width').value;
let lineOutlineColor = edge.pstyle('line-outline-color').value;
let effectiveLineOpacity = opacity * lineOpacity;
// separate arrow opacity would require arrow-opacity property
let effectiveArrowOpacity = opacity * lineOpacity;
let drawLine = ( strokeOpacity = effectiveLineOpacity) => {
if (curveStyle === 'straight-triangle') {
r.eleStrokeStyle( context, edge, strokeOpacity );
r.drawEdgeTrianglePath(
edge,
context,
rs.allpts
);
} else {
context.lineWidth = edgeWidth;
context.lineCap = lineCap;
r.eleStrokeStyle( context, edge, strokeOpacity );
r.drawEdgePath(
edge,
context,
rs.allpts,
lineStyle
);
context.lineCap = 'butt'; // reset for other drawing functions
}
};
let drawLineOutline = ( strokeOpacity = effectiveLineOpacity) => {
context.lineWidth = edgeWidth + lineOutlineWidth;
context.lineCap = lineCap;
if (lineOutlineWidth > 0) {
r.colorStrokeStyle( context, lineOutlineColor[0], lineOutlineColor[1], lineOutlineColor[2], strokeOpacity );
} else {
// do not draw any lineOutline
context.lineCap = 'butt'; // reset for other drawing functions
return;
}
if (curveStyle === 'straight-triangle') {
r.drawEdgeTrianglePath(
edge,
context,
rs.allpts
);
} else {
r.drawEdgePath(
edge,
context,
rs.allpts,
lineStyle
);
context.lineCap = 'butt'; // reset for other drawing functions
}
};
let drawOverlay = () => {
if( !shouldDrawOverlay ){ return; }
r.drawEdgeOverlay( context, edge );
};
let drawUnderlay = () => {
if( !shouldDrawOverlay ){ return; }
r.drawEdgeUnderlay( context, edge );
};
let drawArrows = ( arrowOpacity = effectiveArrowOpacity) => {
r.drawArrowheads( context, edge, arrowOpacity );
};
let drawText = () => {
r.drawElementText( context, edge, null, drawLabel );
};
context.lineJoin = 'round';
let ghost = edge.pstyle('ghost').value === 'yes';
if( ghost ){
let gx = edge.pstyle('ghost-offset-x').pfValue;
let gy = edge.pstyle('ghost-offset-y').pfValue;
let ghostOpacity = edge.pstyle('ghost-opacity').value;
let effectiveGhostOpacity = effectiveLineOpacity * ghostOpacity;
context.translate( gx, gy );
drawLine( effectiveGhostOpacity );
drawArrows( effectiveGhostOpacity );
context.translate( -gx, -gy );
} else {
drawLineOutline();
}
drawUnderlay();
drawLine();
drawArrows();
drawOverlay();
drawText();
if( shiftToOriginWithBb ){
context.translate( bb.x1, bb.y1 );
}
};
const drawEdgeOverlayUnderlay = function( overlayOrUnderlay ) {
if (!['overlay', 'underlay'].includes(overlayOrUnderlay)) {
throw new Error('Invalid state');
}
return function( context, edge ){
if( !edge.visible() ){ return; }
let opacity = edge.pstyle(`${overlayOrUnderlay}-opacity`).value;
if( opacity === 0 ){ return; }
let r = this;
let usePaths = r.usePaths();
let rs = edge._private.rscratch;
let padding = edge.pstyle(`${overlayOrUnderlay}-padding`).pfValue;
let width = 2 * padding;
let color = edge.pstyle(`${overlayOrUnderlay}-color`).value;
context.lineWidth = width;
if( rs.edgeType === 'self' && !usePaths ){
context.lineCap = 'butt';
} else {
context.lineCap = 'round';
}
r.colorStrokeStyle( context, color[0], color[1], color[2], opacity );
r.drawEdgePath(
edge,
context,
rs.allpts,
'solid'
);
};
};
CRp.drawEdgeOverlay = drawEdgeOverlayUnderlay('overlay');
CRp.drawEdgeUnderlay = drawEdgeOverlayUnderlay('underlay');
CRp.drawEdgePath = function( edge, context, pts, type ){
let rs = edge._private.rscratch;
let canvasCxt = context;
let path;
let pathCacheHit = false;
let usePaths = this.usePaths();
let lineDashPattern = edge.pstyle('line-dash-pattern').pfValue;
let lineDashOffset = edge.pstyle('line-dash-offset').pfValue;
if( usePaths ){
let pathCacheKey = pts.join( '$' );
let keyMatches = rs.pathCacheKey && rs.pathCacheKey === pathCacheKey;
if( keyMatches ){
path = context = rs.pathCache;
pathCacheHit = true;
} else {
path = context = new Path2D();
rs.pathCacheKey = pathCacheKey;
rs.pathCache = path;
}
}
if( canvasCxt.setLineDash ){ // for very outofdate browsers
switch( type ){
case 'dotted':
canvasCxt.setLineDash( [ 1, 1 ] );
break;
case 'dashed':
canvasCxt.setLineDash( lineDashPattern );
canvasCxt.lineDashOffset = lineDashOffset;
break;
case 'solid':
canvasCxt.setLineDash( [ ] );
break;
}
}
if( !pathCacheHit && !rs.badLine ){
if( context.beginPath ){ context.beginPath(); }
context.moveTo( pts[0], pts[1] );
switch( rs.edgeType ){
case 'bezier':
case 'self':
case 'compound':
case 'multibezier':
for( let i = 2; i + 3 < pts.length; i += 4 ){
context.quadraticCurveTo( pts[ i ], pts[ i + 1], pts[ i + 2], pts[ i + 3] );
}
break;
case 'straight':
case 'haystack':
for( let i = 2; i + 1 < pts.length; i += 2 ) {
context.lineTo( pts[ i ], pts[ i + 1] );
}
break;
case 'segments':
if (rs.isRound) {
for( let corner of rs.roundCorners ){
drawPreparedRoundCorner(context, corner);
}
context.lineTo( pts[ pts.length - 2 ], pts[ pts.length - 1] );
} else {
for( let i = 2; i + 1 < pts.length; i += 2 ) {
context.lineTo( pts[ i ], pts[ i + 1] );
}
}
break;
}
}
context = canvasCxt;
if( usePaths ){
context.stroke( path );
} else {
context.stroke();
}
// reset any line dashes
if( context.setLineDash ){ // for very outofdate browsers
context.setLineDash( [ ] );
}
};
CRp.drawEdgeTrianglePath = function( edge, context, pts ){
// use line stroke style for triangle fill style
context.fillStyle = context.strokeStyle;
let edgeWidth = edge.pstyle('width').pfValue;
for( let i = 0; i + 1 < pts.length; i += 2 ){
const vector = [ pts[ i + 2 ] - pts[ i ], pts[ i + 3 ] - pts[ i + 1 ] ];
const length = Math.sqrt( vector[0] * vector[0] + vector[1] * vector[1] );
const normal = [ vector[1] / length, -vector[0] / length ];
const triangleHead = [ normal[0] * edgeWidth / 2, normal[1] * edgeWidth / 2 ];
context.beginPath();
context.moveTo( pts[ i ] - triangleHead[0], pts[ i + 1 ] - triangleHead[1] );
context.lineTo( pts[ i ] + triangleHead[0], pts[ i + 1 ] + triangleHead[1] );
context.lineTo( pts[ i + 2 ], pts[ i + 3 ] );
context.closePath();
context.fill();
}
};
CRp.drawArrowheads = function( context, edge, opacity ){
let rs = edge._private.rscratch;
let isHaystack = rs.edgeType === 'haystack';
if( !isHaystack ){
this.drawArrowhead( context, edge, 'source', rs.arrowStartX, rs.arrowStartY, rs.srcArrowAngle, opacity );
}
this.drawArrowhead( context, edge, 'mid-target', rs.midX, rs.midY, rs.midtgtArrowAngle, opacity );
this.drawArrowhead( context, edge, 'mid-source', rs.midX, rs.midY, rs.midsrcArrowAngle, opacity );
if( !isHaystack ){
this.drawArrowhead( context, edge, 'target', rs.arrowEndX, rs.arrowEndY, rs.tgtArrowAngle, opacity );
}
};
CRp.drawArrowhead = function( context, edge, prefix, x, y, angle, opacity ){
if( isNaN( x ) || x == null || isNaN( y ) || y == null || isNaN( angle ) || angle == null ){ return; }
let self = this;
let arrowShape = edge.pstyle( prefix + '-arrow-shape' ).value;
if( arrowShape === 'none' ) { return; }
let arrowClearFill = edge.pstyle( prefix + '-arrow-fill' ).value === 'hollow' ? 'both' : 'filled';
let arrowFill = edge.pstyle( prefix + '-arrow-fill' ).value;
let edgeWidth = edge.pstyle( 'width' ).pfValue;
let pArrowWidth = edge.pstyle( prefix + '-arrow-width' );
let arrowWidth = pArrowWidth.value === 'match-line' ? edgeWidth : pArrowWidth.pfValue;
if (pArrowWidth.units === '%') arrowWidth *= edgeWidth;
let edgeOpacity = edge.pstyle( 'opacity' ).value;
if( opacity === undefined ){
opacity = edgeOpacity;
}
let gco = context.globalCompositeOperation;
if( opacity !== 1 || arrowFill === 'hollow' ){ // then extra clear is needed
context.globalCompositeOperation = 'destination-out';
self.colorFillStyle( context, 255, 255, 255, 1 );
self.colorStrokeStyle( context, 255, 255, 255, 1 );
self.drawArrowShape( edge, context,
arrowClearFill, edgeWidth, arrowShape, arrowWidth, x, y, angle
);
context.globalCompositeOperation = gco;
} // otherwise, the opaque arrow clears it for free :)
let color = edge.pstyle( prefix + '-arrow-color' ).value;
self.colorFillStyle( context, color[0], color[1], color[2], opacity );
self.colorStrokeStyle( context, color[0], color[1], color[2], opacity );
self.drawArrowShape( edge, context,
arrowFill, edgeWidth, arrowShape, arrowWidth, x, y, angle
);
};
CRp.drawArrowShape = function( edge, context, fill, edgeWidth, shape, shapeWidth, x, y, angle ){
let r = this;
let usePaths = this.usePaths() && shape !== 'triangle-cross';
let pathCacheHit = false;
let path;
let canvasContext = context;
let translation = { x, y };
let scale = edge.pstyle( 'arrow-scale' ).value;
let size = this.getArrowWidth( edgeWidth, scale );
let shapeImpl = r.arrowShapes[ shape ];
if( usePaths ){
let cache = r.arrowPathCache = r.arrowPathCache || [];
let key = util.hashString(shape);
let cachedPath = cache[ key ];
if( cachedPath != null ){
path = context = cachedPath;
pathCacheHit = true;
} else {
path = context = new Path2D();
cache[ key ] = path;
}
}
if( !pathCacheHit ){
if( context.beginPath ){ context.beginPath(); }
if( usePaths ){ // store in the path cache with values easily manipulated later
shapeImpl.draw( context, 1, 0, { x: 0, y: 0 }, 1 );
} else {
shapeImpl.draw( context, size, angle, translation, edgeWidth );
}
if( context.closePath ){ context.closePath(); }
}
context = canvasContext;
if( usePaths ){ // set transform to arrow position/orientation
context.translate( x, y );
context.rotate( angle );
context.scale( size, size );
}
if( fill === 'filled' || fill === 'both' ){
if( usePaths ){
context.fill( path );
} else {
context.fill();
}
}
if( fill === 'hollow' || fill === 'both' ){
context.lineWidth = shapeWidth / (usePaths ? size : 1);
context.lineJoin = 'miter';
if( usePaths ){
context.stroke( path );
} else {
context.stroke();
}
}
if( usePaths ){ // reset transform by applying inverse
context.scale( 1/size, 1/size );
context.rotate( -angle );
context.translate( -x, -y );
}
};
export default CRp;

View File

@@ -0,0 +1,218 @@
import * as math from '../../../math.mjs';
let CRp = {};
CRp.drawElement = function( context, ele, shiftToOriginWithBb, showLabel, showOverlay, showOpacity ){
let r = this;
if( ele.isNode() ){
r.drawNode( context, ele, shiftToOriginWithBb, showLabel, showOverlay, showOpacity );
} else {
r.drawEdge( context, ele, shiftToOriginWithBb, showLabel, showOverlay, showOpacity );
}
};
CRp.drawElementOverlay = function( context, ele ){
let r = this;
if( ele.isNode() ){
r.drawNodeOverlay( context, ele );
} else {
r.drawEdgeOverlay( context, ele );
}
};
CRp.drawElementUnderlay = function( context, ele ){
let r = this;
if( ele.isNode() ){
r.drawNodeUnderlay( context, ele );
} else {
r.drawEdgeUnderlay( context, ele );
}
};
CRp.drawCachedElementPortion = function( context, ele, eleTxrCache, pxRatio, lvl, reason, getRotation, getOpacity ){
let r = this;
let bb = eleTxrCache.getBoundingBox(ele);
if( bb.w === 0 || bb.h === 0 ){ return; } // ignore zero size case
let eleCache = eleTxrCache.getElement( ele, bb, pxRatio, lvl, reason );
if( eleCache != null ){
let opacity = getOpacity(r, ele);
if( opacity === 0 ){ return; }
let theta = getRotation(r, ele);
let { x1, y1, w, h } = bb;
let x, y, sx, sy, smooth;
if( theta !== 0 ){
let rotPt = eleTxrCache.getRotationPoint(ele);
sx = rotPt.x;
sy = rotPt.y;
context.translate(sx, sy);
context.rotate(theta);
smooth = r.getImgSmoothing(context);
if( !smooth ){ r.setImgSmoothing(context, true); }
let off = eleTxrCache.getRotationOffset(ele);
x = off.x;
y = off.y;
} else {
x = x1;
y = y1;
}
let oldGlobalAlpha;
if( opacity !== 1 ){
oldGlobalAlpha = context.globalAlpha;
context.globalAlpha = oldGlobalAlpha * opacity;
}
context.drawImage( eleCache.texture.canvas, eleCache.x, 0, eleCache.width, eleCache.height, x, y, w, h );
if( opacity !== 1 ){
context.globalAlpha = oldGlobalAlpha;
}
if( theta !== 0 ){
context.rotate(-theta);
context.translate(-sx, -sy);
if( !smooth ){ r.setImgSmoothing(context, false); }
}
} else {
eleTxrCache.drawElement( context, ele ); // direct draw fallback
}
};
const getZeroRotation = () => 0;
const getLabelRotation = (r, ele) => r.getTextAngle(ele, null);
const getSourceLabelRotation = (r, ele) => r.getTextAngle(ele, 'source');
const getTargetLabelRotation = (r, ele) => r.getTextAngle(ele, 'target');
const getOpacity = (r, ele) => ele.effectiveOpacity();
const getTextOpacity = (e, ele) => ele.pstyle('text-opacity').pfValue * ele.effectiveOpacity();
CRp.drawCachedElement = function( context, ele, pxRatio, extent, lvl, requestHighQuality ){
let r = this;
let { eleTxrCache, lblTxrCache, slbTxrCache, tlbTxrCache } = r.data;
let bb = ele.boundingBox();
let reason = requestHighQuality === true ? eleTxrCache.reasons.highQuality : null;
if( bb.w === 0 || bb.h === 0 || !ele.visible() ){ return; }
if( !extent || math.boundingBoxesIntersect( bb, extent ) ){
let isEdge = ele.isEdge();
let badLine = ele.element()._private.rscratch.badLine;
r.drawElementUnderlay( context, ele );
r.drawCachedElementPortion( context, ele, eleTxrCache, pxRatio, lvl, reason, getZeroRotation, getOpacity );
if( !isEdge || !badLine ){
r.drawCachedElementPortion( context, ele, lblTxrCache, pxRatio, lvl, reason, getLabelRotation, getTextOpacity );
}
if( isEdge && !badLine ){
r.drawCachedElementPortion( context, ele, slbTxrCache, pxRatio, lvl, reason, getSourceLabelRotation, getTextOpacity );
r.drawCachedElementPortion( context, ele, tlbTxrCache, pxRatio, lvl, reason, getTargetLabelRotation, getTextOpacity );
}
r.drawElementOverlay( context, ele );
}
};
CRp.drawElements = function( context, eles ){
let r = this;
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
r.drawElement( context, ele );
}
};
CRp.drawCachedElements = function( context, eles, pxRatio, extent ){
let r = this;
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
r.drawCachedElement( context, ele, pxRatio, extent );
}
};
CRp.drawCachedNodes = function( context, eles, pxRatio, extent ){
let r = this;
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
if( !ele.isNode() ){ continue; }
r.drawCachedElement( context, ele, pxRatio, extent );
}
};
CRp.drawLayeredElements = function( context, eles, pxRatio, extent ){
let r = this;
let layers = r.data.lyrTxrCache.getLayers( eles, pxRatio );
if( layers ){
for( let i = 0; i < layers.length; i++ ){
let layer = layers[i];
let bb = layer.bb;
if( bb.w === 0 || bb.h === 0 ){ continue; }
context.drawImage( layer.canvas, bb.x1, bb.y1, bb.w, bb.h );
}
} else { // fall back on plain caching if no layers
r.drawCachedElements( context, eles, pxRatio, extent );
}
};
if( process.env.NODE_ENV !== 'production' ){
CRp.drawDebugPoints = function( context, eles ){
let draw = function( x, y, color ){
context.fillStyle = color;
context.fillRect( x - 1, y - 1, 3, 3 );
};
for( let i = 0; i < eles.length; i++ ){
let ele = eles[i];
let rs = ele._private.rscratch;
if( ele.isNode() ){
let p = ele.position();
draw( rs.labelX, rs.labelY, 'red' );
draw( p.x, p.y, 'magenta' );
} else {
let pts = rs.allpts;
for( let j = 0; j + 1 < pts.length; j += 2 ){
let x = pts[ j ];
let y = pts[ j + 1 ];
draw( x, y, 'cyan' );
}
draw( rs.midX, rs.midY, 'yellow' );
}
}
};
}
export default CRp;

View File

@@ -0,0 +1,804 @@
/* global Path2D */
import * as is from '../../../is.mjs';
import {expandPolygon, joinLines} from '../../../math.mjs';
import * as util from '../../../util/index.mjs';
import * as round from "../../../round.mjs";
import * as math from "../../../math.mjs";
let CRp = {};
CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, shouldDrawOverlay = true, shouldDrawOpacity = true ){
let r = this;
let nodeWidth, nodeHeight;
let _p = node._private;
let rs = _p.rscratch;
let pos = node.position();
if( !is.number( pos.x ) || !is.number( pos.y ) ){
return; // can't draw node with undefined position
}
if( shouldDrawOpacity && !node.visible() ){ return; }
let eleOpacity = shouldDrawOpacity ? node.effectiveOpacity() : 1;
let usePaths = r.usePaths();
let path;
let pathCacheHit = false;
let padding = node.padding();
nodeWidth = node.width() + 2 * padding;
nodeHeight = node.height() + 2 * padding;
//
// setup shift
let bb;
if( shiftToOriginWithBb ){
bb = shiftToOriginWithBb;
context.translate( -bb.x1, -bb.y1 );
}
//
// load bg image
let bgImgProp = node.pstyle( 'background-image' );
let urls = bgImgProp.value;
let urlDefined = new Array( urls.length );
let image = new Array( urls.length );
let numImages = 0;
for( let i = 0; i < urls.length; i++ ){
let url = urls[i];
let defd = urlDefined[i] = url != null && url !== 'none';
if( defd ){
let bgImgCrossOrigin = node.cy().style().getIndexedStyle(node, 'background-image-crossorigin', 'value', i);
numImages++;
// get image, and if not loaded then ask to redraw when later loaded
image[i] = r.getCachedImage( url, bgImgCrossOrigin, function(){
_p.backgroundTimestamp = Date.now();
node.emitAndNotify('background');
} );
}
}
//
// setup styles
let darkness = node.pstyle('background-blacken').value;
let borderWidth = node.pstyle('border-width').pfValue;
let bgOpacity = node.pstyle('background-opacity').value * eleOpacity;
let borderColor = node.pstyle('border-color').value;
let borderStyle = node.pstyle('border-style').value;
let borderJoin = node.pstyle('border-join').value;
let borderCap = node.pstyle('border-cap').value;
let borderPosition = node.pstyle('border-position').value;
let borderPattern = node.pstyle('border-dash-pattern').pfValue;
let borderOffset = node.pstyle('border-dash-offset').pfValue;
let borderOpacity = node.pstyle('border-opacity').value * eleOpacity;
let outlineWidth = node.pstyle('outline-width').pfValue;
let outlineColor = node.pstyle('outline-color').value;
let outlineStyle = node.pstyle('outline-style').value;
let outlineOpacity = node.pstyle('outline-opacity').value * eleOpacity;
let outlineOffset = node.pstyle('outline-offset').value;
let cornerRadius = node.pstyle('corner-radius').value;
if (cornerRadius !== 'auto') cornerRadius = node.pstyle('corner-radius').pfValue;
let setupShapeColor = ( bgOpy = bgOpacity ) => {
r.eleFillStyle( context, node, bgOpy );
};
let setupBorderColor = ( bdrOpy = borderOpacity ) => {
r.colorStrokeStyle( context, borderColor[0], borderColor[1], borderColor[2], bdrOpy );
};
let setupOutlineColor = ( otlnOpy = outlineOpacity ) => {
r.colorStrokeStyle( context, outlineColor[0], outlineColor[1], outlineColor[2], otlnOpy );
};
//
// setup shape
let getPath = (width, height, shape, points) => {
let pathCache = r.nodePathCache = r.nodePathCache || [];
let key = util.hashStrings(
shape === 'polygon' ? shape + ',' + points.join(',') : shape,
'' + height,
'' + width,
'' + cornerRadius
);
let cachedPath = pathCache[ key ];
let path;
let cacheHit = false;
if( cachedPath != null ){
path = cachedPath;
cacheHit = true;
rs.pathCache = path;
} else {
path = new Path2D();
pathCache[ key ] = rs.pathCache = path;
}
return {
path,
cacheHit
};
};
let styleShape = node.pstyle('shape').strValue;
let shapePts = node.pstyle('shape-polygon-points').pfValue;
if( usePaths ){
context.translate( pos.x, pos.y );
const shapePath = getPath(nodeWidth, nodeHeight, styleShape, shapePts);
path = shapePath.path;
pathCacheHit = shapePath.cacheHit;
}
let drawShape = () => {
if( !pathCacheHit ){
let npos = pos;
if( usePaths ){
npos = {
x: 0,
y: 0
};
}
r.nodeShapes[ r.getNodeShape( node ) ].draw(
( path || context ),
npos.x,
npos.y,
nodeWidth,
nodeHeight, cornerRadius, rs );
}
if( usePaths ){
context.fill( path );
} else {
context.fill();
}
};
let drawImages = ( nodeOpacity = eleOpacity, inside = true ) => {
let prevBging = _p.backgrounding;
let totalCompleted = 0;
for( let i = 0; i < image.length; i++ ){
const bgContainment = node.cy().style().getIndexedStyle(node, 'background-image-containment', 'value', i);
if( inside && bgContainment === 'over' || !inside && bgContainment === 'inside' ){
totalCompleted++;
continue;
}
if( urlDefined[i] && image[i].complete && !image[i].error ){
totalCompleted++;
r.drawInscribedImage( context, image[i], node, i, nodeOpacity );
}
}
_p.backgrounding = !(totalCompleted === numImages);
if( prevBging !== _p.backgrounding ){ // update style b/c :backgrounding state changed
node.updateStyle( false );
}
};
let drawPie = ( redrawShape = false, pieOpacity = eleOpacity ) => {
if( r.hasPie( node ) ){
r.drawPie( context, node, pieOpacity );
// redraw/restore path if steps after pie need it
if( redrawShape ){
if( !usePaths ){
r.nodeShapes[ r.getNodeShape( node ) ].draw(
context,
pos.x,
pos.y,
nodeWidth,
nodeHeight, cornerRadius, rs );
}
}
}
};
let drawStripe = (redrawShape = false, stripeOpacity = eleOpacity) => {
if( r.hasStripe( node ) ){
context.save();
if (usePaths) {
context.clip( rs.pathCache );
} else {
r.nodeShapes[ r.getNodeShape( node ) ].draw(
context,
pos.x,
pos.y,
nodeWidth,
nodeHeight, cornerRadius, rs );
context.clip();
}
r.drawStripe( context, node, stripeOpacity );
context.restore();
// redraw/restore path if steps after stripes need it
if( redrawShape ){
if( !usePaths ){
r.nodeShapes[ r.getNodeShape( node ) ].draw(
context,
pos.x,
pos.y,
nodeWidth,
nodeHeight, cornerRadius, rs );
}
}
}
};
let darken = ( darkenOpacity = eleOpacity ) => {
let opacity = ( darkness > 0 ? darkness : -darkness ) * darkenOpacity;
let c = darkness > 0 ? 0 : 255;
if( darkness !== 0 ){
r.colorFillStyle( context, c, c, c, opacity );
if( usePaths ){
context.fill( path );
} else {
context.fill();
}
}
};
let drawBorder = () => {
if( borderWidth > 0 ){
context.lineWidth = borderWidth;
context.lineCap = borderCap;
context.lineJoin = borderJoin;
if( context.setLineDash ){ // for very outofdate browsers
switch( borderStyle ){
case 'dotted':
context.setLineDash( [ 1, 1 ] );
break;
case 'dashed':
context.setLineDash( borderPattern );
context.lineDashOffset = borderOffset;
break;
case 'solid':
case 'double':
context.setLineDash( [ ] );
break;
}
}
if ( borderPosition !== 'center') {
context.save();
context.lineWidth *= 2;
if (borderPosition === 'inside') {
usePaths ? context.clip(path) : context.clip();
} else {
const region = new Path2D();
region.rect(
-nodeWidth / 2 - borderWidth,
-nodeHeight / 2 - borderWidth,
nodeWidth + 2 * borderWidth,
nodeHeight + 2 * borderWidth
);
region.addPath(path);
context.clip(region, 'evenodd');
}
usePaths ? context.stroke(path) : context.stroke();
context.restore();
} else {
usePaths ? context.stroke(path) : context.stroke();
}
if( borderStyle === 'double' ){
context.lineWidth = borderWidth / 3;
let gco = context.globalCompositeOperation;
context.globalCompositeOperation = 'destination-out';
if( usePaths ){
context.stroke( path );
} else {
context.stroke();
}
context.globalCompositeOperation = gco;
}
// reset in case we changed the border style
if( context.setLineDash ){ // for very outofdate browsers
context.setLineDash( [ ] );
}
}
};
let drawOutline = () => {
if( outlineWidth > 0 ){
context.lineWidth = outlineWidth;
context.lineCap = 'butt';
if( context.setLineDash ){ // for very outofdate browsers
switch( outlineStyle ){
case 'dotted':
context.setLineDash( [ 1, 1 ] );
break;
case 'dashed':
context.setLineDash( [ 4, 2 ] );
break;
case 'solid':
case 'double':
context.setLineDash( [ ] );
break;
}
}
let npos = pos;
if( usePaths ){
npos = {
x: 0,
y: 0
};
}
let shape = r.getNodeShape( node );
let bWidth = borderWidth;
if( borderPosition === 'inside' ) bWidth = 0;
if( borderPosition === 'outside' ) bWidth *= 2;
let scaleX = (nodeWidth + bWidth + (outlineWidth + outlineOffset)) / nodeWidth;
let scaleY = (nodeHeight + bWidth + (outlineWidth + outlineOffset)) / nodeHeight;
let sWidth = nodeWidth * scaleX;
let sHeight = nodeHeight * scaleY;
let points = r.nodeShapes[ shape ].points;
let path;
if (usePaths) {
let outlinePath = getPath(sWidth, sHeight, shape, points);
path = outlinePath.path;
}
// draw the outline path, either by using expanded points or by scaling
// the dimensions, depending on shape
if (shape === "ellipse") {
r.drawEllipsePath(path || context, npos.x, npos.y, sWidth, sHeight);
} else if ([
'round-diamond', 'round-heptagon', 'round-hexagon', 'round-octagon',
'round-pentagon', 'round-polygon', 'round-triangle', 'round-tag'
].includes(shape)) {
let sMult = 0;
let offsetX = 0;
let offsetY = 0;
if (shape === 'round-diamond') {
sMult = (bWidth + outlineOffset + outlineWidth) * 1.4;
} else if (shape === 'round-heptagon') {
sMult = (bWidth + outlineOffset + outlineWidth) * 1.075;
offsetY = -(bWidth/2 + outlineOffset + outlineWidth) / 35;
} else if (shape === 'round-hexagon') {
sMult = (bWidth + outlineOffset + outlineWidth) * 1.12;
} else if (shape === 'round-pentagon') {
sMult = (bWidth + outlineOffset + outlineWidth) * 1.13;
offsetY = -(bWidth/2 + outlineOffset + outlineWidth) / 15;
} else if (shape === 'round-tag') {
sMult = (bWidth + outlineOffset + outlineWidth) * 1.12;
offsetX = (bWidth/2 + outlineWidth + outlineOffset) * .07;
} else if (shape === 'round-triangle') {
sMult = (bWidth + outlineOffset + outlineWidth) * (Math.PI/2);
offsetY = -(bWidth + outlineOffset/2 + outlineWidth) / Math.PI;
}
if (sMult !== 0) {
scaleX = (nodeWidth + sMult)/nodeWidth;
sWidth = nodeWidth * scaleX;
if ( ! ['round-hexagon', 'round-tag'].includes(shape) ) {
scaleY = (nodeHeight + sMult)/nodeHeight;
sHeight = nodeHeight * scaleY;
}
}
cornerRadius = cornerRadius === 'auto' ? math.getRoundPolygonRadius( sWidth, sHeight ) : cornerRadius;
const halfW = sWidth / 2;
const halfH = sHeight / 2;
const radius = cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 2;
const p = new Array( points.length / 2 );
const corners = new Array( points.length / 2 );
for ( let i = 0; i < points.length / 2; i++ ){
p[i] = {
x: npos.x + offsetX + halfW * points[ i * 2 ],
y: npos.y + offsetY + halfH * points[ i * 2 + 1 ]
};
}
let i, p1, p2, p3, len = p.length;
p1 = p[ len - 1 ];
// for each point
for( i = 0; i < len; i++ ){
p2 = p[ (i) % len ];
p3 = p[ (i + 1) % len ];
corners[ i ] = round.getRoundCorner( p1, p2, p3, radius );
p1 = p2;
p2 = p3;
}
r.drawRoundPolygonPath(path || context, npos.x + offsetX, npos.y + offsetY, nodeWidth * scaleX, nodeHeight * scaleY, points, corners );
} else if (['roundrectangle', 'round-rectangle'].includes(shape)) {
cornerRadius = cornerRadius === 'auto' ? math.getRoundRectangleRadius( sWidth, sHeight ) : cornerRadius;
r.drawRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 2 );
} else if (['cutrectangle', 'cut-rectangle'].includes(shape)) {
cornerRadius = cornerRadius === 'auto' ? math.getCutRectangleCornerLength( sWidth, sHeight ) : cornerRadius;
r.drawCutRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, null ,cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 4 );
} else if (['bottomroundrectangle', 'bottom-round-rectangle'].includes(shape)) {
cornerRadius = cornerRadius === 'auto' ? math.getRoundRectangleRadius( sWidth, sHeight ) : cornerRadius;
r.drawBottomRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 2 );
} else if (shape === "barrel") {
r.drawBarrelPath(path || context, npos.x, npos.y, sWidth, sHeight);
} else if (shape.startsWith("polygon") || ['rhomboid', 'right-rhomboid', 'round-tag', 'tag', 'vee'].includes(shape)) {
let pad = (bWidth + outlineWidth + outlineOffset) / nodeWidth;
points = joinLines(expandPolygon(points, pad));
r.drawPolygonPath(path || context, npos.x, npos.y, nodeWidth, nodeHeight, points);
} else {
let pad = (bWidth + outlineWidth + outlineOffset) / nodeWidth;
points = joinLines(expandPolygon(points, -pad));
r.drawPolygonPath(path || context, npos.x, npos.y, nodeWidth, nodeHeight, points);
}
if( usePaths ){
context.stroke( path );
} else {
context.stroke();
}
if( outlineStyle === 'double' ){
context.lineWidth = bWidth / 3;
let gco = context.globalCompositeOperation;
context.globalCompositeOperation = 'destination-out';
if( usePaths ){
context.stroke( path );
} else {
context.stroke();
}
context.globalCompositeOperation = gco;
}
// reset in case we changed the border style
if( context.setLineDash ){ // for very outofdate browsers
context.setLineDash( [ ] );
}
}
};
let drawOverlay = () => {
if( shouldDrawOverlay ){
r.drawNodeOverlay( context, node, pos, nodeWidth, nodeHeight );
}
};
let drawUnderlay = () => {
if( shouldDrawOverlay ){
r.drawNodeUnderlay( context, node, pos, nodeWidth, nodeHeight );
}
};
let drawText = () => {
r.drawElementText( context, node, null, drawLabel );
};
let ghost = node.pstyle('ghost').value === 'yes';
if( ghost ){
let gx = node.pstyle('ghost-offset-x').pfValue;
let gy = node.pstyle('ghost-offset-y').pfValue;
let ghostOpacity = node.pstyle('ghost-opacity').value;
let effGhostOpacity = ghostOpacity * eleOpacity;
context.translate( gx, gy );
setupOutlineColor();
drawOutline();
setupShapeColor( ghostOpacity * bgOpacity );
drawShape();
drawImages( effGhostOpacity, true );
setupBorderColor( ghostOpacity * borderOpacity );
drawBorder();
drawPie( darkness !== 0 || borderWidth !== 0 );
drawStripe( darkness !== 0 || borderWidth !== 0 );
drawImages( effGhostOpacity, false );
darken( effGhostOpacity );
context.translate( -gx, -gy );
}
if( usePaths ){
context.translate( -pos.x, -pos.y );
}
drawUnderlay();
if( usePaths ){
context.translate( pos.x, pos.y );
}
setupOutlineColor();
drawOutline();
setupShapeColor();
drawShape();
drawImages(eleOpacity, true);
setupBorderColor();
drawBorder();
drawPie( darkness !== 0 || borderWidth !== 0 );
drawStripe( darkness !== 0 || borderWidth !== 0 );
drawImages(eleOpacity, false);
darken();
if( usePaths ){
context.translate( -pos.x, -pos.y );
}
drawText();
drawOverlay();
//
// clean up shift
if( shiftToOriginWithBb ){
context.translate( bb.x1, bb.y1 );
}
};
const drawNodeOverlayUnderlay = function( overlayOrUnderlay ) {
if (!['overlay', 'underlay'].includes(overlayOrUnderlay)) {
throw new Error('Invalid state');
}
return function( context, node, pos, nodeWidth, nodeHeight ){
let r = this;
if( !node.visible() ){ return; }
let padding = node.pstyle( `${overlayOrUnderlay}-padding` ).pfValue;
let opacity = node.pstyle( `${overlayOrUnderlay}-opacity` ).value;
let color = node.pstyle( `${overlayOrUnderlay}-color` ).value;
let shape = node.pstyle( `${overlayOrUnderlay}-shape` ).value;
let radius = node.pstyle( `${overlayOrUnderlay}-corner-radius` ).value;
if( opacity > 0 ){
pos = pos || node.position();
if( nodeWidth == null || nodeHeight == null ){
let padding = node.padding();
nodeWidth = node.width() + 2 * padding;
nodeHeight = node.height() + 2 * padding;
}
r.colorFillStyle( context, color[0], color[1], color[2], opacity );
r.nodeShapes[shape].draw(
context,
pos.x,
pos.y,
nodeWidth + padding * 2,
nodeHeight + padding * 2,
radius
);
context.fill();
}
};
};
CRp.drawNodeOverlay = drawNodeOverlayUnderlay('overlay');
CRp.drawNodeUnderlay = drawNodeOverlayUnderlay('underlay');
// does the node have at least one pie piece?
CRp.hasPie = function( node ){
node = node[0]; // ensure ele ref
return node._private.hasPie;
};
CRp.hasStripe = function( node ){
node = node[0]; // ensure ele ref
return node._private.hasStripe;
};
CRp.drawPie = function( context, node, nodeOpacity, pos ){
node = node[0]; // ensure ele ref
pos = pos || node.position();
let cyStyle = node.cy().style();
let pieSize = node.pstyle( 'pie-size' );
let hole = node.pstyle('pie-hole');
let overallStartAngle = node.pstyle('pie-start-angle').pfValue;
let x = pos.x;
let y = pos.y;
let nodeW = node.width();
let nodeH = node.height();
let radius = Math.min( nodeW, nodeH ) / 2; // must fit in node
let holeRadius;
let lastPercent = 0; // what % to continue drawing pie slices from on [0, 1]
let usePaths = this.usePaths();
if( usePaths ){
x = 0;
y = 0;
}
if( pieSize.units === '%' ){
radius = radius * pieSize.pfValue;
} else if( pieSize.pfValue !== undefined ){
radius = pieSize.pfValue / 2; // diameter in pixels => radius
}
if (hole.units === '%') {
holeRadius = radius * hole.pfValue;
} else if (hole.pfValue !== undefined) {
holeRadius = hole.pfValue / 2; // diameter in pixels => radius
}
if (holeRadius >= radius) {
return; // the pie would be invisible anyway
}
for( let i = 1; i <= cyStyle.pieBackgroundN; i++ ){ // 1..N
let size = node.pstyle( 'pie-' + i + '-background-size' ).value;
let color = node.pstyle( 'pie-' + i + '-background-color' ).value;
let opacity = node.pstyle( 'pie-' + i + '-background-opacity' ).value * nodeOpacity;
let percent = size / 100; // map integer range [0, 100] to [0, 1]
// percent can't push beyond 1
if( percent + lastPercent > 1 ){
percent = 1 - lastPercent;
}
let angleStart = (1.5 * Math.PI) + (2 * Math.PI * lastPercent); // start at 12 o'clock and go clockwise
angleStart += overallStartAngle; // shift by the overall pie start angle
let angleDelta = 2 * Math.PI * percent;
let angleEnd = angleStart + angleDelta;
// ignore if
// - zero size
// - we're already beyond the full circle
// - adding the current slice would go beyond the full circle
if( size === 0 || lastPercent >= 1 || lastPercent + percent > 1 ){
continue;
}
if (holeRadius === 0) { // make a pie slice
context.beginPath();
context.moveTo( x, y );
context.arc( x, y, radius, angleStart, angleEnd );
context.closePath();
} else { // make a pie slice that's like the above but with a hole in the middle
context.beginPath();
context.arc(x, y, radius, angleStart, angleEnd);
context.arc(x, y, holeRadius, angleEnd, angleStart, true); // true for anticlockwise
context.closePath();
}
this.colorFillStyle( context, color[0], color[1], color[2], opacity );
context.fill();
lastPercent += percent;
}
};
CRp.drawStripe = function( context, node, nodeOpacity, pos ){
node = node[0]; // ensure ele ref
pos = pos || node.position();
let cyStyle = node.cy().style();
let x = pos.x;
let y = pos.y;
let nodeW = node.width();
let nodeH = node.height();
let lastPercent = 0; // what % to continue drawing pie slices from on [0, 1]
let usePaths = this.usePaths();
context.save();
let direction = node.pstyle('stripe-direction').value;
let stripeSize = node.pstyle('stripe-size');
switch (direction) {
case 'vertical':
break; // default
case 'righward':
context.rotate(-Math.PI / 2);
break;
}
let stripeW = nodeW;
let stripeH = nodeH;
if( stripeSize.units === '%' ){
stripeW = stripeW * stripeSize.pfValue;
stripeH = stripeH * stripeSize.pfValue;
} else if( stripeSize.pfValue !== undefined ){
stripeW = stripeSize.pfValue;
stripeH = stripeSize.pfValue;
}
if( usePaths ){
x = 0;
y = 0;
}
// shift up from the centre of the node to the top-left corner
y -= stripeW / 2;
x -= stripeH / 2;
for( let i = 1; i <= cyStyle.stripeBackgroundN; i++ ){ // 1..N
let size = node.pstyle( 'stripe-' + i + '-background-size' ).value;
let color = node.pstyle( 'stripe-' + i + '-background-color' ).value;
let opacity = node.pstyle( 'stripe-' + i + '-background-opacity' ).value * nodeOpacity;
let percent = size / 100; // map integer range [0, 100] to [0, 1]
// percent can't push beyond 1
if( percent + lastPercent > 1 ){
percent = 1 - lastPercent;
}
// ignore if
// - zero size
// - we're already beyond the full chart
// - adding the current slice would go beyond the full chart
if( size === 0 || lastPercent >= 1 || lastPercent + percent > 1 ){
continue;
}
// draw rect for the current stripe
context.beginPath();
context.rect( x, y + stripeH * lastPercent, stripeW, stripeH * percent );
context.closePath();
this.colorFillStyle( context, color[0], color[1], color[2], opacity );
context.fill();
lastPercent += percent;
}
context.restore();
};
export default CRp;

View File

@@ -0,0 +1,555 @@
import * as math from '../../../math.mjs';
import { trueify, falsify, removeFromArray, clearArray, MAX_INT, assign, defaults } from '../../../util/index.mjs';
import Heap from '../../../heap.mjs';
import defs from './texture-cache-defs.mjs';
import ElementTextureCacheLookup from './ele-texture-cache-lookup.mjs';
const minTxrH = 25; // the size of the texture cache for small height eles (special case)
const txrStepH = 50; // the min size of the regular cache, and the size it increases with each step up
const minLvl = -4; // when scaling smaller than that we don't need to re-render
export const maxLvl = 3; // when larger than this scale just render directly (caching is not helpful)
export const maxZoom = 7.99; // beyond this zoom level, layered textures are not used
const eleTxrSpacing = 8; // spacing between elements on textures to avoid blitting overlaps
const defTxrWidth = 1024; // default/minimum texture width
const maxTxrW = 1024; // the maximum width of a texture
const maxTxrH = 1024; // the maximum height of a texture
const minUtility = 0.2; // if usage of texture is less than this, it is retired
const maxFullness = 0.8; // fullness of texture after which queue removal is checked
const maxFullnessChecks = 10; // dequeued after this many checks
const deqCost = 0.15; // % of add'l rendering cost allowed for dequeuing ele caches each frame
const deqAvgCost = 0.1; // % of add'l rendering cost compared to average overall redraw time
const deqNoDrawCost = 0.9; // % of avg frame time that can be used for dequeueing when not drawing
const deqFastCost = 0.9; // % of frame time to be used when >60fps
const deqRedrawThreshold = 100; // time to batch redraws together from dequeueing to allow more dequeueing calcs to happen in the meanwhile
const maxDeqSize = 1; // number of eles to dequeue and render at higher texture in each batch
const getTxrReasons = {
dequeue: 'dequeue',
downscale: 'downscale',
highQuality: 'highQuality'
};
const initDefaults = defaults({
getKey: null,
doesEleInvalidateKey: falsify,
drawElement: null,
getBoundingBox: null,
getRotationPoint: null,
getRotationOffset: null,
isVisible: trueify,
allowEdgeTxrCaching: true,
allowParentTxrCaching: true
});
const ElementTextureCache = function( renderer, initOptions ){
let self = this;
self.renderer = renderer;
self.onDequeues = [];
let opts = initDefaults(initOptions);
assign(self, opts);
self.lookup = new ElementTextureCacheLookup(opts.getKey, opts.doesEleInvalidateKey);
self.setupDequeueing();
};
const ETCp = ElementTextureCache.prototype;
ETCp.reasons = getTxrReasons;
// the list of textures in which new subtextures for elements can be placed
ETCp.getTextureQueue = function( txrH ){
let self = this;
self.eleImgCaches = self.eleImgCaches || {};
return ( self.eleImgCaches[ txrH ] = self.eleImgCaches[ txrH ] || [] );
};
// the list of usused textures which can be recycled (in use in texture queue)
ETCp.getRetiredTextureQueue = function( txrH ){
let self = this;
let rtxtrQs = self.eleImgCaches.retired = self.eleImgCaches.retired || {};
let rtxtrQ = rtxtrQs[ txrH ] = rtxtrQs[ txrH ] || [];
return rtxtrQ;
};
// queue of element draw requests at different scale levels
ETCp.getElementQueue = function(){
let self = this;
let q = self.eleCacheQueue = self.eleCacheQueue || new Heap(function( a, b ){
return b.reqs - a.reqs;
});
return q;
};
// queue of element draw requests at different scale levels (element id lookup)
ETCp.getElementKeyToQueue = function(){
let self = this;
let k2q = self.eleKeyToCacheQueue = self.eleKeyToCacheQueue || {};
return k2q;
};
ETCp.getElement = function( ele, bb, pxRatio, lvl, reason ){
let self = this;
let r = this.renderer;
let zoom = r.cy.zoom();
let lookup = this.lookup;
if( !bb || bb.w === 0 || bb.h === 0 || isNaN(bb.w) || isNaN(bb.h) || !ele.visible() || ele.removed() ){ return null; }
if(
( !self.allowEdgeTxrCaching && ele.isEdge() )
|| ( !self.allowParentTxrCaching && ele.isParent() )
){
return null;
}
if( lvl == null ){
lvl = Math.ceil( math.log2( zoom * pxRatio ) );
}
if( lvl < minLvl ){
lvl = minLvl;
} else if( zoom >= maxZoom || lvl > maxLvl ){
return null;
}
let scale = Math.pow( 2, lvl );
let eleScaledH = bb.h * scale;
let eleScaledW = bb.w * scale;
let scaledLabelShown = r.eleTextBiggerThanMin( ele, scale );
if( !this.isVisible(ele, scaledLabelShown) ){ return null; }
let eleCache = lookup.get( ele, lvl );
// if this get was on an unused/invalidated cache, then restore the texture usage metric
if( eleCache && eleCache.invalidated ){
eleCache.invalidated = false;
eleCache.texture.invalidatedWidth -= eleCache.width;
}
if( eleCache ){
return eleCache;
}
let txrH; // which texture height this ele belongs to
if( eleScaledH <= minTxrH ){
txrH = minTxrH;
} else if( eleScaledH <= txrStepH ){
txrH = txrStepH;
} else {
txrH = Math.ceil( eleScaledH / txrStepH ) * txrStepH;
}
if( eleScaledH > maxTxrH || eleScaledW > maxTxrW ){
return null; // caching large elements is not efficient
}
let txrQ = self.getTextureQueue( txrH );
// first try the second last one in case it has space at the end
let txr = txrQ[ txrQ.length - 2 ];
let addNewTxr = function(){
return self.recycleTexture( txrH, eleScaledW ) || self.addTexture( txrH, eleScaledW );
};
// try the last one if there is no second last one
if( !txr ){
txr = txrQ[ txrQ.length - 1 ];
}
// if the last one doesn't exist, we need a first one
if( !txr ){
txr = addNewTxr();
}
// if there's no room in the current texture, we need a new one
if( txr.width - txr.usedWidth < eleScaledW ){
txr = addNewTxr();
}
let scalableFrom = function( otherCache ){
return otherCache && otherCache.scaledLabelShown === scaledLabelShown;
};
let deqing = reason && reason === getTxrReasons.dequeue;
let highQualityReq = reason && reason === getTxrReasons.highQuality;
let downscaleReq = reason && reason === getTxrReasons.downscale;
let higherCache; // the nearest cache with a higher level
for( let l = lvl + 1; l <= maxLvl; l++ ){
let c = lookup.get( ele, l );
if( c ){ higherCache = c; break; }
}
let oneUpCache = higherCache && higherCache.level === lvl + 1 ? higherCache : null;
let downscale = function(){
txr.context.drawImage(
oneUpCache.texture.canvas,
oneUpCache.x, 0,
oneUpCache.width, oneUpCache.height,
txr.usedWidth, 0,
eleScaledW, eleScaledH
);
};
// reset ele area in texture
txr.context.setTransform( 1, 0, 0, 1, 0, 0 );
txr.context.clearRect( txr.usedWidth, 0, eleScaledW, txrH );
if( scalableFrom(oneUpCache) ){
// then we can relatively cheaply rescale the existing image w/o rerendering
downscale();
} else if( scalableFrom(higherCache) ){
// then use the higher cache for now and queue the next level down
// to cheaply scale towards the smaller level
if( highQualityReq ){
for( let l = higherCache.level; l > lvl; l-- ){
oneUpCache = self.getElement( ele, bb, pxRatio, l, getTxrReasons.downscale );
}
downscale();
} else {
self.queueElement( ele, higherCache.level - 1 );
return higherCache;
}
} else {
let lowerCache; // the nearest cache with a lower level
if( !deqing && !highQualityReq && !downscaleReq ){
for( let l = lvl - 1; l >= minLvl; l-- ){
let c = lookup.get( ele, l );
if( c ){ lowerCache = c; break; }
}
}
if( scalableFrom(lowerCache) ){
// then use the lower quality cache for now and queue the better one for later
self.queueElement( ele, lvl );
return lowerCache;
}
txr.context.translate( txr.usedWidth, 0 );
txr.context.scale( scale, scale );
this.drawElement( txr.context, ele, bb, scaledLabelShown, false );
txr.context.scale( 1/scale, 1/scale );
txr.context.translate( -txr.usedWidth, 0 );
}
eleCache = {
x: txr.usedWidth,
texture: txr,
level: lvl,
scale: scale,
width: eleScaledW,
height: eleScaledH,
scaledLabelShown: scaledLabelShown
};
txr.usedWidth += Math.ceil( eleScaledW + eleTxrSpacing );
txr.eleCaches.push( eleCache );
lookup.set( ele, lvl, eleCache );
self.checkTextureFullness( txr );
return eleCache;
};
ETCp.invalidateElements = function( eles ){
for( let i = 0; i < eles.length; i++ ){
this.invalidateElement(eles[i]);
}
};
ETCp.invalidateElement = function( ele ){
let self = this;
let lookup = self.lookup;
let caches = [];
let invalid = lookup.isInvalid(ele);
if( !invalid ){
return; // override the invalidation request if the element key has not changed
}
for( let lvl = minLvl; lvl <= maxLvl; lvl++ ){
let cache = lookup.getForCachedKey( ele, lvl );
if( cache ){
caches.push( cache );
}
}
let noOtherElesUseCache = lookup.invalidate(ele);
if( noOtherElesUseCache ){
for( let i = 0; i < caches.length; i++ ){
let cache = caches[i];
let txr = cache.texture;
// remove space from the texture it belongs to
txr.invalidatedWidth += cache.width;
// mark the cache as invalidated
cache.invalidated = true;
// retire the texture if its utility is low
self.checkTextureUtility( txr );
}
}
// remove from queue since the old req was for the old state
self.removeFromQueue( ele );
};
ETCp.checkTextureUtility = function( txr ){
// invalidate all entries in the cache if the cache size is small
if( txr.invalidatedWidth >= minUtility * txr.width ){
this.retireTexture( txr );
}
};
ETCp.checkTextureFullness = function( txr ){
// if texture has been mostly filled and passed over several times, remove
// it from the queue so we don't need to waste time looking at it to put new things
let self = this;
let txrQ = self.getTextureQueue( txr.height );
if( txr.usedWidth / txr.width > maxFullness && txr.fullnessChecks >= maxFullnessChecks ){
removeFromArray( txrQ, txr );
} else {
txr.fullnessChecks++;
}
};
ETCp.retireTexture = function( txr ){
let self = this;
let txrH = txr.height;
let txrQ = self.getTextureQueue( txrH );
let lookup = this.lookup;
// retire the texture from the active / searchable queue:
removeFromArray( txrQ, txr );
txr.retired = true;
// remove the refs from the eles to the caches:
let eleCaches = txr.eleCaches;
for( let i = 0; i < eleCaches.length; i++ ){
let eleCache = eleCaches[i];
lookup.deleteCache( eleCache.key, eleCache.level );
}
clearArray( eleCaches );
// add the texture to a retired queue so it can be recycled in future:
let rtxtrQ = self.getRetiredTextureQueue( txrH );
rtxtrQ.push( txr );
};
ETCp.addTexture = function( txrH, minW ){
let self = this;
let txrQ = self.getTextureQueue( txrH );
let txr = {};
txrQ.push( txr );
txr.eleCaches = [];
txr.height = txrH;
txr.width = Math.max( defTxrWidth, minW );
txr.usedWidth = 0;
txr.invalidatedWidth = 0;
txr.fullnessChecks = 0;
txr.canvas = self.renderer.makeOffscreenCanvas(txr.width, txr.height);
txr.context = txr.canvas.getContext('2d');
return txr;
};
ETCp.recycleTexture = function( txrH, minW ){
let self = this;
let txrQ = self.getTextureQueue( txrH );
let rtxtrQ = self.getRetiredTextureQueue( txrH );
for( let i = 0; i < rtxtrQ.length; i++ ){
let txr = rtxtrQ[i];
if( txr.width >= minW ){
txr.retired = false;
txr.usedWidth = 0;
txr.invalidatedWidth = 0;
txr.fullnessChecks = 0;
clearArray( txr.eleCaches );
txr.context.setTransform( 1, 0, 0, 1, 0, 0 );
txr.context.clearRect( 0, 0, txr.width, txr.height );
removeFromArray( rtxtrQ, txr );
txrQ.push( txr );
return txr;
}
}
};
ETCp.queueElement = function( ele, lvl ){
let self = this;
let q = self.getElementQueue();
let k2q = self.getElementKeyToQueue();
let key = this.getKey(ele);
let existingReq = k2q[key];
if( existingReq ){
// use the max lvl b/c in between lvls are cheap to make
existingReq.level = Math.max( existingReq.level, lvl );
existingReq.eles.merge(ele);
existingReq.reqs++;
q.updateItem( existingReq );
} else {
let req = {
eles: ele.spawn().merge(ele),
level: lvl,
reqs: 1,
key
};
q.push( req );
k2q[key] = req;
}
};
ETCp.dequeue = function( pxRatio /*, extent*/ ){
let self = this;
let q = self.getElementQueue();
let k2q = self.getElementKeyToQueue();
let dequeued = [];
let lookup = self.lookup;
for( let i = 0; i < maxDeqSize; i++ ){
if( q.size() > 0 ){
let req = q.pop();
let key = req.key;
let ele = req.eles[0]; // all eles have the same key
let cacheExists = lookup.hasCache(ele, req.level);
// clear out the key to req lookup
k2q[key] = null;
// dequeueing isn't necessary with an existing cache
if( cacheExists ){ continue; }
dequeued.push( req );
let bb = self.getBoundingBox( ele );
self.getElement( ele, bb, pxRatio, req.level, getTxrReasons.dequeue );
} else {
break;
}
}
return dequeued;
};
ETCp.removeFromQueue = function( ele ){
let self = this;
let q = self.getElementQueue();
let k2q = self.getElementKeyToQueue();
let key = this.getKey(ele);
let req = k2q[key];
if( req != null ){
if( req.eles.length === 1 ){ // remove if last ele in the req
// bring to front of queue
req.reqs = MAX_INT;
q.updateItem(req);
q.pop(); // remove from queue
k2q[key] = null; // remove from lookup map
} else { // otherwise just remove ele from req
req.eles.unmerge(ele);
}
}
};
ETCp.onDequeue = function( fn ){ this.onDequeues.push( fn ); };
ETCp.offDequeue = function( fn ){ removeFromArray( this.onDequeues, fn ); };
ETCp.setupDequeueing = defs.setupDequeueing({
deqRedrawThreshold: deqRedrawThreshold,
deqCost: deqCost,
deqAvgCost: deqAvgCost,
deqNoDrawCost: deqNoDrawCost,
deqFastCost: deqFastCost,
deq: function( self, pxRatio, extent ){
return self.dequeue( pxRatio, extent );
},
onDeqd: function( self, deqd ){
for( let i = 0; i < self.onDequeues.length; i++ ){
let fn = self.onDequeues[i];
fn( deqd );
}
},
shouldRedraw: function( self, deqd, pxRatio, extent ){
for( let i = 0; i < deqd.length; i++ ){
let eles = deqd[i].eles;
for( let j = 0; j < eles.length; j++ ){
let bb = eles[j].boundingBox();
if( math.boundingBoxesIntersect( bb, extent ) ){
return true;
}
}
}
return false;
},
priority: function( self ){
return self.renderer.beforeRenderPriorities.eleTxrDeq;
}
});
export default ElementTextureCache;

View File

@@ -0,0 +1,390 @@
/*
The canvas renderer was written by Yue Dong.
Modifications tracked on Github.
*/
/* global OffscreenCanvas */
import * as util from '../../../util/index.mjs';
import * as is from '../../../is.mjs';
import { makeBoundingBox } from '../../../math.mjs';
import ElementTextureCache from './ele-texture-cache.mjs';
import LayeredTextureCache from './layered-texture-cache.mjs';
import arrowShapes from './arrow-shapes.mjs';
import drawingElements from './drawing-elements.mjs';
import drawingEdges from './drawing-edges.mjs';
import drawingImages from './drawing-images.mjs';
import drawingLabelText from './drawing-label-text.mjs';
import drawingNodes from './drawing-nodes.mjs';
import drawingRedraw from './drawing-redraw.mjs';
import drawingRedrawWebGL from './webgl/drawing-redraw-webgl.mjs';
import drawingShapes from './drawing-shapes.mjs';
import exportImage from './export-image.mjs';
import nodeShapes from './node-shapes.mjs';
var CR = CanvasRenderer;
var CRp = CanvasRenderer.prototype;
CRp.CANVAS_LAYERS = 3;
//
CRp.SELECT_BOX = 0;
CRp.DRAG = 1;
CRp.NODE = 2;
CRp.WEBGL = 3;
CRp.CANVAS_TYPES = [ '2d', '2d', '2d', 'webgl2' ];
CRp.BUFFER_COUNT = 3;
//
CRp.TEXTURE_BUFFER = 0;
CRp.MOTIONBLUR_BUFFER_NODE = 1;
CRp.MOTIONBLUR_BUFFER_DRAG = 2;
function CanvasRenderer( options ){
var r = this;
var containerWindow = r.cy.window();
var document = containerWindow.document;
if( options.webgl ){
CRp.CANVAS_LAYERS = r.CANVAS_LAYERS = 4;
console.log('webgl rendering enabled');
}
r.data = {
canvases: new Array( CRp.CANVAS_LAYERS ),
contexts: new Array( CRp.CANVAS_LAYERS ),
canvasNeedsRedraw: new Array( CRp.CANVAS_LAYERS ),
bufferCanvases: new Array( CRp.BUFFER_COUNT ),
bufferContexts: new Array( CRp.CANVAS_LAYERS ),
};
var tapHlOffAttr = '-webkit-tap-highlight-color';
var tapHlOffStyle = 'rgba(0,0,0,0)';
r.data.canvasContainer = document.createElement( 'div' ); // eslint-disable-line no-undef
var containerStyle = r.data.canvasContainer.style;
r.data.canvasContainer.style[tapHlOffAttr] = tapHlOffStyle;
containerStyle.position = 'relative';
containerStyle.zIndex = '0';
containerStyle.overflow = 'hidden';
var container = options.cy.container();
container.appendChild( r.data.canvasContainer );
container.style[tapHlOffAttr] = tapHlOffStyle;
var styleMap = {
'-webkit-user-select': 'none',
'-moz-user-select': '-moz-none',
'user-select': 'none',
'-webkit-tap-highlight-color': 'rgba(0,0,0,0)',
'outline-style': 'none',
};
if(is.ms()) {
styleMap['-ms-touch-action'] = 'none';
styleMap['touch-action'] = 'none';
}
for( var i = 0; i < CRp.CANVAS_LAYERS; i++ ){
var canvas = r.data.canvases[ i ] = document.createElement( 'canvas' ); // eslint-disable-line no-undef
var type = CRp.CANVAS_TYPES[ i ];
r.data.contexts[ i ] = canvas.getContext( type );
if( !r.data.contexts[ i ] ) {
util.error( 'Could not create canvas of type ' + type );
}
Object.keys(styleMap).forEach((k) => {
canvas.style[k] = styleMap[k];
});
canvas.style.position = 'absolute';
canvas.setAttribute( 'data-id', 'layer' + i );
canvas.style.zIndex = String( CRp.CANVAS_LAYERS - i );
r.data.canvasContainer.appendChild( canvas );
r.data.canvasNeedsRedraw[ i ] = false;
}
r.data.topCanvas = r.data.canvases[0];
r.data.canvases[ CRp.NODE ].setAttribute( 'data-id', 'layer' + CRp.NODE + '-node' );
r.data.canvases[ CRp.SELECT_BOX ].setAttribute( 'data-id', 'layer' + CRp.SELECT_BOX + '-selectbox' );
r.data.canvases[ CRp.DRAG ].setAttribute( 'data-id', 'layer' + CRp.DRAG + '-drag' );
if( r.data.canvases[ CRp.WEBGL ] ) {
r.data.canvases[ CRp.WEBGL ].setAttribute( 'data-id', 'layer' + CRp.WEBGL + '-webgl' );
}
for( var i = 0; i < CRp.BUFFER_COUNT; i++ ){
r.data.bufferCanvases[ i ] = document.createElement( 'canvas' ); // eslint-disable-line no-undef
r.data.bufferContexts[ i ] = r.data.bufferCanvases[ i ].getContext( '2d' );
r.data.bufferCanvases[ i ].style.position = 'absolute';
r.data.bufferCanvases[ i ].setAttribute( 'data-id', 'buffer' + i );
r.data.bufferCanvases[ i ].style.zIndex = String( -i - 1 );
r.data.bufferCanvases[ i ].style.visibility = 'hidden';
//r.data.canvasContainer.appendChild(r.data.bufferCanvases[i]);
}
r.pathsEnabled = true;
let emptyBb = makeBoundingBox();
let getBoxCenter = bb => ({ x: (bb.x1 + bb.x2)/2, y: (bb.y1 + bb.y2)/2 });
let getCenterOffset = bb => ({ x: -bb.w/2, y: -bb.h/2 });
let backgroundTimestampHasChanged = ele => {
let _p = ele[0]._private;
let same = _p.oldBackgroundTimestamp === _p.backgroundTimestamp;
return !same;
};
let getStyleKey = ele => ele[0]._private.nodeKey;
let getLabelKey = ele => ele[0]._private.labelStyleKey;
let getSourceLabelKey = ele => ele[0]._private.sourceLabelStyleKey;
let getTargetLabelKey = ele => ele[0]._private.targetLabelStyleKey;
let drawElement = (context, ele, bb, scaledLabelShown, useEleOpacity) => r.drawElement( context, ele, bb, false, false, useEleOpacity );
let drawLabel = (context, ele, bb, scaledLabelShown, useEleOpacity) => r.drawElementText( context, ele, bb, scaledLabelShown, 'main', useEleOpacity );
let drawSourceLabel = (context, ele, bb, scaledLabelShown, useEleOpacity) => r.drawElementText( context, ele, bb, scaledLabelShown, 'source', useEleOpacity );
let drawTargetLabel = (context, ele, bb, scaledLabelShown, useEleOpacity) => r.drawElementText( context, ele, bb, scaledLabelShown, 'target', useEleOpacity );
let getElementBox = ele => { ele.boundingBox(); return ele[0]._private.bodyBounds; };
let getLabelBox = ele => { ele.boundingBox(); return ele[0]._private.labelBounds.main || emptyBb; };
let getSourceLabelBox = ele => { ele.boundingBox(); return ele[0]._private.labelBounds.source || emptyBb; };
let getTargetLabelBox = ele => { ele.boundingBox(); return ele[0]._private.labelBounds.target || emptyBb; };
let isLabelVisibleAtScale = (ele, scaledLabelShown) => scaledLabelShown;
let getElementRotationPoint = ele => getBoxCenter( getElementBox(ele) );
let addTextMargin = (prefix, pt, ele) => {
let pre = prefix ? prefix + '-' : '';
return {
x: pt.x + ele.pstyle(pre + 'text-margin-x').pfValue,
y: pt.y + ele.pstyle(pre + 'text-margin-y').pfValue
};
};
let getRsPt = (ele, x, y) => {
let rs = ele[0]._private.rscratch;
return { x: rs[x], y: rs[y] };
};
let getLabelRotationPoint = ele => addTextMargin('', getRsPt(ele, 'labelX', 'labelY'), ele);
let getSourceLabelRotationPoint = ele => addTextMargin('source', getRsPt(ele, 'sourceLabelX', 'sourceLabelY'), ele);
let getTargetLabelRotationPoint = ele => addTextMargin('target', getRsPt(ele, 'targetLabelX', 'targetLabelY'), ele);
let getElementRotationOffset = ele => getCenterOffset( getElementBox(ele) );
let getSourceLabelRotationOffset = ele => getCenterOffset( getSourceLabelBox(ele) );
let getTargetLabelRotationOffset = ele => getCenterOffset( getTargetLabelBox(ele) );
let getLabelRotationOffset = ele => {
let bb = getLabelBox(ele);
let p = getCenterOffset( getLabelBox(ele) );
if( ele.isNode() ){
switch( ele.pstyle('text-halign').value ){
case 'left':
p.x = -bb.w - (bb.leftPad || 0);
break;
case 'right':
p.x = -(bb.rightPad || 0);
break;
}
switch( ele.pstyle('text-valign').value ){
case 'top':
p.y = -bb.h - (bb.topPad || 0);
break;
case 'bottom':
p.y = -(bb.botPad || 0);
break;
}
}
return p;
};
let eleTxrCache = r.data.eleTxrCache = new ElementTextureCache( r, {
getKey: getStyleKey,
doesEleInvalidateKey: backgroundTimestampHasChanged,
drawElement: drawElement,
getBoundingBox: getElementBox,
getRotationPoint: getElementRotationPoint,
getRotationOffset: getElementRotationOffset,
allowEdgeTxrCaching: false,
allowParentTxrCaching: false
} );
let lblTxrCache = r.data.lblTxrCache = new ElementTextureCache( r, {
getKey: getLabelKey,
drawElement: drawLabel,
getBoundingBox: getLabelBox,
getRotationPoint: getLabelRotationPoint,
getRotationOffset: getLabelRotationOffset,
isVisible: isLabelVisibleAtScale
} );
let slbTxrCache = r.data.slbTxrCache = new ElementTextureCache( r, {
getKey: getSourceLabelKey,
drawElement: drawSourceLabel,
getBoundingBox: getSourceLabelBox,
getRotationPoint: getSourceLabelRotationPoint,
getRotationOffset: getSourceLabelRotationOffset,
isVisible: isLabelVisibleAtScale
} );
let tlbTxrCache = r.data.tlbTxrCache = new ElementTextureCache( r, {
getKey: getTargetLabelKey,
drawElement: drawTargetLabel,
getBoundingBox: getTargetLabelBox,
getRotationPoint: getTargetLabelRotationPoint,
getRotationOffset: getTargetLabelRotationOffset,
isVisible: isLabelVisibleAtScale
} );
let lyrTxrCache = r.data.lyrTxrCache = new LayeredTextureCache( r );
r.onUpdateEleCalcs(function invalidateTextureCaches( willDraw, eles ){
// each cache should check for sub-key diff to see that the update affects that cache particularly
eleTxrCache.invalidateElements( eles );
lblTxrCache.invalidateElements( eles );
slbTxrCache.invalidateElements( eles );
tlbTxrCache.invalidateElements( eles );
// any change invalidates the layers
lyrTxrCache.invalidateElements( eles );
// update the old bg timestamp so diffs can be done in the ele txr caches
for( let i = 0; i < eles.length; i++ ){
let _p = eles[i]._private;
_p.oldBackgroundTimestamp = _p.backgroundTimestamp;
}
});
let refineInLayers = reqs => {
for( var i = 0; i < reqs.length; i++ ){
lyrTxrCache.enqueueElementRefinement( reqs[i].ele );
}
};
eleTxrCache.onDequeue(refineInLayers);
lblTxrCache.onDequeue(refineInLayers);
slbTxrCache.onDequeue(refineInLayers);
tlbTxrCache.onDequeue(refineInLayers);
if( options.webgl ) {
r.initWebgl( options, {
getStyleKey,
getLabelKey,
getSourceLabelKey,
getTargetLabelKey,
drawElement,
drawLabel,
drawSourceLabel,
drawTargetLabel,
getElementBox,
getLabelBox,
getSourceLabelBox,
getTargetLabelBox,
getElementRotationPoint,
getElementRotationOffset,
getLabelRotationPoint,
getSourceLabelRotationPoint,
getTargetLabelRotationPoint,
getLabelRotationOffset,
getSourceLabelRotationOffset,
getTargetLabelRotationOffset
} );
}
}
CRp.redrawHint = function( group, bool ){
var r = this;
switch( group ){
case 'eles':
r.data.canvasNeedsRedraw[ CRp.NODE ] = bool;
break;
case 'drag':
r.data.canvasNeedsRedraw[ CRp.DRAG ] = bool;
break;
case 'select':
r.data.canvasNeedsRedraw[ CRp.SELECT_BOX ] = bool;
break;
case 'gc':
r.data.gc = true;
break;
}
};
// whether to use Path2D caching for drawing
var pathsImpld = typeof Path2D !== 'undefined';
CRp.path2dEnabled = function( on ){
if( on === undefined ){
return this.pathsEnabled;
}
this.pathsEnabled = on ? true : false;
};
CRp.usePaths = function(){
return pathsImpld && this.pathsEnabled;
};
CRp.setImgSmoothing = function( context, bool ){
if( context.imageSmoothingEnabled != null ){
context.imageSmoothingEnabled = bool;
} else {
context.webkitImageSmoothingEnabled = bool;
context.mozImageSmoothingEnabled = bool;
context.msImageSmoothingEnabled = bool;
}
};
CRp.getImgSmoothing = function( context ){
if( context.imageSmoothingEnabled != null ){
return context.imageSmoothingEnabled;
} else {
return context.webkitImageSmoothingEnabled || context.mozImageSmoothingEnabled || context.msImageSmoothingEnabled;
}
};
CRp.makeOffscreenCanvas = function(width, height){
let canvas;
if( typeof OffscreenCanvas !== typeof undefined ){
canvas = new OffscreenCanvas(width, height);
} else {
var containerWindow = this.cy.window();
var document = containerWindow.document;
canvas = document.createElement('canvas'); // eslint-disable-line no-undef
canvas.width = width;
canvas.height = height;
}
return canvas;
};
[
arrowShapes,
drawingElements,
drawingEdges,
drawingImages,
drawingLabelText,
drawingNodes,
drawingRedraw,
drawingRedrawWebGL,
drawingShapes,
exportImage,
nodeShapes
].forEach( function( props ){
util.extend( CRp, props );
} );
export default CR;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,208 @@
// Canvas Upscaling Plugin
class MiscUpscaler {
constructor(options = {}) {
// Plugin Settings
this.options = Object.assign({
useEdgeDetection: true, // Edge Detection
scaleFactor: 2.0, // Upscaling Value
}, options);
this.gl = null;
this.program = null;
this.frameBuffer = null;
this.texture = null;
this.outputTexture = null;
}
init(inputCanvas, outputCanvas) {
this.inputCanvas = inputCanvas;
this.outputCanvas = outputCanvas;
// Set output canvas size based on scale factor
this.outputCanvas.width = this.inputCanvas.width * this.options.scaleFactor;
this.outputCanvas.height = this.inputCanvas.height * this.options.scaleFactor;
// Initialize WebGL context
this.gl = this.outputCanvas.getContext('webgl2');
if (!this.gl) {
console.error('WebGL not supported');
return false;
}
// Initialize shaders and buffers
this.initShaders();
this.initBuffers();
this.initTextures();
return true;
}
initShaders() {
// Vertex shader
const vertexShaderSource = `
attribute vec2 a_position;
attribute vec2 a_texCoord;
varying vec2 vUv;
void main() {
vUv = a_texCoord;
gl_Position = vec4(a_position, 0.0, 1.0);
}
`;
// Fragment shader
const fragmentShaderSource = `
precision mediump float;
uniform sampler2D tDiffuse;
uniform vec2 resolution;
varying vec2 vUv;
void main() {
vec2 texelSize = 1.0 / resolution;
// Get Neighbor Pixels
vec4 color = texture2D(tDiffuse, vUv);
vec4 colorUp = texture2D(tDiffuse, vUv + vec2(0.0, texelSize.y));
vec4 colorDown = texture2D(tDiffuse, vUv - vec2(0.0, texelSize.y));
vec4 colorLeft = texture2D(tDiffuse, vUv - vec2(texelSize.x, 0.0));
vec4 colorRight = texture2D(tDiffuse, vUv + vec2(texelSize.x, 0.0));
// Work with edges
float edgeStrength = 1.0 - smoothstep(0.1, 0.3, length(color.rgb - colorUp.rgb));
edgeStrength += 1.0 - smoothstep(0.1, 0.3, length(color.rgb - colorDown.rgb));
edgeStrength += 1.0 - smoothstep(0.1, 0.3, length(color.rgb - colorLeft.rgb));
edgeStrength += 1.0 - smoothstep(0.1, 0.3, length(color.rgb - colorRight.rgb));
edgeStrength = clamp(edgeStrength, 0.0, 1.0);
// Applying edges incresing and filtering
vec3 enhancedColor = mix(color.rgb, vec3(1.0) - (1.0 - color.rgb) * edgeStrength, 0.5);
gl_FragColor = vec4(enhancedColor, color.a);
}
`;
// Create shader program
const vertexShader = this.createShader(this.gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, fragmentShaderSource);
this.program = this.createProgram(vertexShader, fragmentShader);
// Get attribute and uniform locations
this.positionLocation = this.gl.getAttribLocation(this.program, 'a_position');
this.texCoordLocation = this.gl.getAttribLocation(this.program, 'a_texCoord');
this.resolutionLocation = this.gl.getUniformLocation(this.program, 'resolution');
this.textureLocation = this.gl.getUniformLocation(this.program, 'tDiffuse');
}
createShader(type, source) {
const shader = this.gl.createShader(type);
this.gl.shaderSource(shader, source);
this.gl.compileShader(shader);
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
console.error('Shader compile error:', this.gl.getShaderInfoLog(shader));
this.gl.deleteShader(shader);
return null;
}
return shader;
}
createProgram(vertexShader, fragmentShader) {
const program = this.gl.createProgram();
this.gl.attachShader(program, vertexShader);
this.gl.attachShader(program, fragmentShader);
this.gl.linkProgram(program);
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
console.error('Program link error:', this.gl.getProgramInfoLog(program));
return null;
}
return program;
}
initBuffers() {
// Create a buffer for positions
this.positionBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
// Full screen quad (2 triangles)
const positions = [
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1
];
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(positions), this.gl.STATIC_DRAW);
// Create a buffer for texture coordinates
this.texCoordBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer);
// Texture coordinates for the quad with Y-flipped to match canvas coordinates
const texCoords = [
0, 1, // top-left (flipped from 0,0)
1, 1, // top-right (flipped from 1,0)
0, 0, // bottom-left (flipped from 0,1)
0, 0, // bottom-left (flipped from 0,1)
1, 1, // top-right (flipped from 1,0)
1, 0 // bottom-right (flipped from 1,1)
];
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(texCoords), this.gl.STATIC_DRAW);
}
initTextures() {
// Create texture for input canvas
this.texture = this.gl.createTexture();
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
// Set parameters for texture
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
}
render() {
if (!this.gl) return;
// Update texture from input canvas
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, this.inputCanvas);
// Set viewport and clear
this.gl.viewport(0, 0, this.outputCanvas.width, this.outputCanvas.height);
this.gl.clearColor(0, 0, 0, 0);
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
// Use shader program
this.gl.useProgram(this.program);
// Set uniforms
this.gl.uniform2f(this.resolutionLocation, this.inputCanvas.width, this.inputCanvas.height);
this.gl.uniform1i(this.textureLocation, 0);
// Set position attribute
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
this.gl.enableVertexAttribArray(this.positionLocation);
this.gl.vertexAttribPointer(this.positionLocation, 2, this.gl.FLOAT, false, 0, 0);
// Set texture coordinate attribute
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer);
this.gl.enableVertexAttribArray(this.texCoordLocation);
this.gl.vertexAttribPointer(this.texCoordLocation, 2, this.gl.FLOAT, false, 0, 0);
// Draw
this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);
}
resize() {
if (this.inputCanvas && this.outputCanvas) {
this.outputCanvas.width = this.inputCanvas.width * this.options.scaleFactor;
this.outputCanvas.height = this.inputCanvas.height * this.options.scaleFactor;
}
}
}
export { MiscUpscaler as UpscalerPlugin };

View File

@@ -0,0 +1,9 @@
import nullRenderer from './null/index.mjs';
import baseRenderer from './base/index.mjs';
import canvasRenderer from './canvas/index.mjs';
export default [
{ name: 'null', impl: nullRenderer },
{ name: 'base', impl: baseRenderer },
{ name: 'canvas', impl: canvasRenderer },
];