Consider a graph like the one shown below:
I would like to be able to display/hide the red edges (forget that they are hand drawn) shown below when the user clicks a button or similar:
I don't want the red edges to participate in the layout but instead for them to be shown as a kind of overlay. It would be nice if the edges could try to avoid overlapping any nodes in their path, but its definitely not required.
I think if I could set a boolean flag on the edges telling the layout engine to either include or exclude them from the layout setup, it could work. There is a physics parameter on the edge that I can override, but it doesn't seem to help - the edge still participates in the layout.
I could probably also write some scripting which tracks the nodes and draw the red edges in another graph above, but that is specifically what I want to avoid.
When using a hierarchical layout in vis network (options.layout.hierarchical.enabled = true) there doesn't appear to be an option which achieves this. This could however be achieved with an overlay. The question mentions that this isn't desired, but adding it as an option. An example is incorporated into the post below and also at https://jsfiddle.net/7abovhtu/.
In summary the solution places an overlay canvas on top of the vis network canvas. Clicks on the overlay canvas are passed through to the vis network canvas due to the CSS pointer-events: none;. Extra edges are drawn onto the overlay canvas using the positioning of the nodes. Updates to the overlay canvas are triggered by the vis network event afterDrawing which triggers whenever the network changes (dragging, zooming, etc.).
This answer makes use of the closest point to an ellipse calculation provided in the answer https://stackoverflow.com/a/18363333/1620449 to end the lines at the edge of the nodes. This answer also makes use of the function in the answer https://stackoverflow.com/a/6333775/1620449 to draw an arrow on a canvas.
// create an array with nodes
var nodes = new vis.DataSet([
{ id: 1, label: "Node 1" },
{ id: 2, label: "Node 2" },
{ id: 3, label: "Node 3" },
{ id: 4, label: "Node 4" },
{ id: 5, label: "Node 5" },
{ id: 6, label: "Node 6" },
{ id: 7, label: "Node 7" },
]);
// create an array with edges
var edges = new vis.DataSet([
{ from: 1, to: 2 },
{ from: 2, to: 3 },
{ from: 3, to: 4 },
{ from: 3, to: 5 },
{ from: 3, to: 6 },
{ from: 6, to: 7 }
]);
// create an array with extra edges displayed on button press
var extraEdges = [
{ from: 7, to: 5 },
{ from: 6, to: 1 }
];
// create a network
var container = document.getElementById("network");
var data = {
nodes: nodes,
edges: edges,
};
var options = {
layout: {
hierarchical: {
enabled: true,
direction: 'LR',
sortMethod: 'directed',
shakeTowards: 'roots'
}
}
};
var network = new vis.Network(container, data, options);
// Create an overlay for displaying extra edges
var overlayCanvas = document.getElementById("overlay");
var overlayContext = overlayCanvas.getContext("2d");
// Function called to draw the extra edges, called on initial display and
// when the network completes each draw (due to drag, zoom etc.)
function drawExtraEdges(){
// Resize overlay canvas in case the continer has changed
overlayCanvas.height = container.clientHeight;
overlayCanvas.width = container.clientWidth;
// Begin drawing path on overlay canvas
overlayContext.beginPath();
// Clear any existing lines from overlay canvas
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
// Loop through extra edges to draw them
extraEdges.forEach(edge => {
// Gather the necessary coordinates for the start and end shapres
const startPos = network.canvasToDOM(network.getPosition(edge.from));
const endPos = network.canvasToDOM(network.getPosition(edge.to));
const endBox = network.getBoundingBox(edge.to);
// Determine the radius of the ellipse based on the scale of network
// Start and end ellipse are presumed to be the same size
const scale = network.getScale();
const radiusX = ((endBox.right * scale) - (endBox.left * scale)) / 2;
const radiusY = ((endBox.bottom * scale) - (endBox.top * scale)) / 2;
// Get the closest point on the end ellipse to the start point
const endClosest = getEllipsePt(endPos.x, endPos.y, radiusX, radiusY, startPos.x, startPos.y);
// Now we have an end point get the point on the ellipse for the start
const startClosest = getEllipsePt(startPos.x, startPos.y, radiusX, radiusY, endClosest.x, endClosest.y);
// Draw arrow on diagram
drawArrow(overlayContext, startClosest.x, startClosest.y, endClosest.x, endClosest.y);
});
// Apply red color to overlay canvas context
overlayContext.strokeStyle = '#ff0000';
// Make the line dashed
overlayContext.setLineDash([10, 3]);
// Apply lines to overlay canvas
overlayContext.stroke();
}
// Adjust the positioning of the lines each time the network is redrawn
network.on("afterDrawing", function (event) {
// Only draw the lines if they have been toggled on with the button
if(extraEdgesShown){
drawExtraEdges();
}
});
// Add button event to show / hide extra edges
var extraEdgesShown = false;
document.getElementById('extraEdges').onclick = function() {
if(!extraEdgesShown){
if(extraEdges.length > 0){
// Call function to draw extra lines
drawExtraEdges();
extraEdgesShown = true;
}
} else {
// Remove extra edges
// Clear the overlay canvas
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
extraEdgesShown = false;
}
}
//////////////////////////////////////////////////////////////////////
// Elllipse closest point calculation
// https://stackoverflow.com/a/18363333/1620449
//////////////////////////////////////////////////////////////////////
var halfPI = Math.PI / 2;
var steps = 8; // larger == greater accuracy
// calc a point on the ellipse that is "near-ish" the target point
// uses "brute force"
function getEllipsePt(cx, cy, radiusX, radiusY, targetPtX, targetPtY) {
// calculate which ellipse quadrant the targetPt is in
var q;
if (targetPtX > cx) {
q = (targetPtY > cy) ? 0 : 3;
} else {
q = (targetPtY > cy) ? 1 : 2;
}
// calc beginning and ending radian angles to check
var r1 = q * halfPI;
var r2 = (q + 1) * halfPI;
var dr = halfPI / steps;
var minLengthSquared = 200000000;
var minX, minY;
// walk the ellipse quadrant and find a near-point
for (var r = r1; r < r2; r += dr) {
// get a point on the ellipse at radian angle == r
var ellipseX = cx + radiusX * Math.cos(r);
var ellipseY = cy + radiusY * Math.sin(r);
// calc distance from ellipsePt to targetPt
var dx = targetPtX - ellipseX;
var dy = targetPtY - ellipseY;
var lengthSquared = dx * dx + dy * dy;
// if new length is shortest, save this ellipse point
if (lengthSquared < minLengthSquared) {
minX = ellipseX;
minY = ellipseY;
minLengthSquared = lengthSquared;
}
}
return ({
x: minX,
y: minY
});
}
//////////////////////////////////////////////////////////////////////
// Draw Arrow on Canvas Function
// https://stackoverflow.com/a/6333775/1620449
//////////////////////////////////////////////////////////////////////
function drawArrow(ctx, fromX, fromY, toX, toY) {
var headLength = 10; // length of head in pixels
var dX = toX - fromX;
var dY = toY - fromY;
var angle = Math.atan2(dY, dX);
ctx.fillStyle = "red";
ctx.moveTo(fromX, fromY);
ctx.lineTo(toX, toY);
ctx.lineTo(toX - headLength * Math.cos(angle - Math.PI / 6), toY - headLength * Math.sin(angle - Math.PI / 6));
ctx.moveTo(toX, toY);
ctx.lineTo(toX - headLength * Math.cos(angle + Math.PI / 6), toY - headLength * Math.sin(angle + Math.PI / 6));
}
#container {
width: 100%;
height: 80vh;
border: 1px solid lightgray;
position: relative;
}
#network, #overlay {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
#overlay {
z-index: 100;
pointer-events: none;
}
<script src="https://visjs.github.io/vis-network/standalone/umd/vis-network.min.js"></script>
<button id="extraEdges">Toggle Extra Edges</button>
<div id="container">
<div id="network"></div>
<canvas width="600" height="400" id="overlay"></canvas>
</div>
This can be achieved using either the physics or hidden options on the extra edges (those in red). For reference, these options are described in more detail at https://visjs.github.io/vis-network/docs/network/edges.html.
Please note the below options do not work when hierarchical layout is used as set in the Vis Network options options.layout.hierarchical.enabled = true.
Physics - An example of using the physics option is https://jsfiddle.net/6oac73p0. However as you mentioned this may cause overlaps with nodes which have physics enabled. The extra edges are set to dashed in this example to ensure everything is still visible.
Hidden - An example of using the hidden option is https://jsfiddle.net/xfcuvtgk/ and also incorporated into this post below. Edges set to hidden are still part of the physics calculation when the layout is generated, which you mentioned wasn't desired, however this does mean they fit nicely when later displayed.
// create an array with nodes
var nodes = new vis.DataSet([
{ id: 1, label: "Node 1" },
{ id: 2, label: "Node 2" },
{ id: 3, label: "Node 3" },
{ id: 4, label: "Node 4" },
{ id: 5, label: "Node 5" },
]);
// create an array with edges
var edges = new vis.DataSet([
{ from: 1, to: 3 },
{ from: 1, to: 2 },
{ from: 2, to: 4 },
{ from: 2, to: 5 },
{ from: 3, to: 3 },
{ from: 4, to: 5, color: 'red', hidden: true, arrows: 'to', extra: true },
{ from: 3, to: 5, color: 'red', hidden: true, arrows: 'to', extra: true },
{ from: 1, to: 5, color: 'red', hidden: true, arrows: 'to', extra: true }
]);
// create a network
var container = document.getElementById("mynetwork");
var data = {
nodes: nodes,
edges: edges,
};
var options = {};
var network = new vis.Network(container, data, options);
document.getElementById('extraEdges').onclick = function() {
// Extract the list of extra edges
edges.forEach(function(edge){
if(edge.extra){
// Toggle the hidden value
edge.hidden = !edge.hidden;
// Update edge back onto data set
edges.update(edge);
}
});
}
#mynetwork {
width: 600px;
/* Height adjusted for Stack Overflow inline demo */
height: 160px;
border: 1px solid lightgray;
}
<script src="https://visjs.github.io/vis-network/standalone/umd/vis-network.min.js"></script>
<button id="extraEdges">Show/Hide Extra Edges</button>
<div id="mynetwork"></div>
Related
So I have been using the Chebyshev Distance example from the Phaser labs, and while this example was using one layer, I happen to be using two, and when i set transparency on them, the colors start leaking into each other, especially on light colors.
Is there any way to circumvent or get rid of this effect
If the problem is that you have two layers, one ontop of the other and you are making both transparent (or only the top one), and you don't want that color to pass through, the solution could be to hide the tiles on the bottom layer.
Just check in the map-tile-loop, if the tile, where you want to change the alpha, has a tile beneath it, and if so make that background tile transparent.
Here a small working demo:
(The main magic is in the updateMap function)
document.body.style = 'margin:0;';
var config = {
type: Phaser.AUTO,
width: 536,
height: 183,
scene: {
preload,
create
}
};
var player;
var bgLayer;
var point1 = {x: 250, y: 31};
var isLeaking = false;
new Phaser.Game(config);
function preload (){
this.load.image('tiles', 'https://labs.phaser.io/assets/tilemaps/tiles/catastrophi_tiles_16.png');
this.load.tilemapCSV('map', 'https://labs.phaser.io/assets/tilemaps/csv/catastrophi_level2.csv');
}
function create () {
this.add.text(50, 1, ' <- Background is visible, if no tiles are ontop')
.setOrigin(0)
.setDepth(100)
.setStyle({fontFamily: 'Arial'});
this.infoText = this.add.text(10, 20, 'Click to toggle leaking: on')
.setOrigin(0)
.setDepth(100)
.setStyle({fontFamily: 'Arial'});
// Just creating image for second layer tiles //
let graphics = this.make.graphics();
graphics.fillStyle(0xff0000);
graphics.fillRect(0, 0, 16, 16);
graphics.generateTexture('tiles2', 16, 16);
// Just creating image for second layer tiles //
let map = this.make.tilemap({ key: 'map', tileWidth: 16, tileHeight: 16 });
let tileset = map.addTilesetImage('tiles');
let tileset2 = map.addTilesetImage('tiles2');
bgLayer = map.createBlankLayer('background', tileset2);
bgLayer.fill(0);
let fgLayer = map.createLayer(0, tileset, 0, 0);
// Just to show that the Background is still show if not Tile is covering
fgLayer.removeTileAt(0, 0);
fgLayer.removeTileAt(1, 0);
fgLayer.removeTileAt(2, 0);
player = this.add.rectangle(point1.x, point1.y, 5, 5, 0xffffff, .5)
.setOrigin(.5);
this.input.on('pointerdown', () => {
isLeaking = !isLeaking;
this.infoText.setText( `Click to toggle leaking: ${isLeaking?'off':'on'}` )
updateMap(map);
});
updateMap(map);
}
function updateMap (map) {
let originPoint1 = map.getTileAtWorldXY(point1.x, point1.y);
console.info(map.layers.sort((a,b) => b.depth - a.depth))
map.forEachTile(function (tile) {
var dist = Phaser.Math.Distance.Chebyshev(
originPoint1.x,
originPoint1.y,
tile.x,
tile.y
);
let bgTile = bgLayer.getTileAt(tile.x, tile.y, false)
let hideOnlyTheseTiles = [ 0, 1, 2, 3, 4]; // Indexes to hide
if( !isLeaking ){
if(hideOnlyTheseTiles.indexOf(bgTile.index) > -1){ // here yopu can select the
bgTile.setAlpha(0);
}
} else{
bgTile.setAlpha(1);
}
tile.setAlpha(1 - 0.09 * dist);
});
}
<script src="//cdn.jsdelivr.net/npm/phaser#3.55.2/dist/phaser.js"></script>
I'm trying to build a screen with circular bodies (bubbles). The bubbles should be able to change the scale with some interval, e.g. every 3 seconds and random value (from 1 to 3). The scale should change the size of a bubble and its mass.
Calling
Matter.Body.scale
doesn't make any effect. Bubbles keep staying the same. Maybe because I'm not using sprites with textures, but the Game-Engine entities with renderer as React.PureComponent instead. Not sure
Bubble node:
export interface BubbleProps {
body: Matter.Body;
item: Item;
}
class BubbleNode extends React.Component<BubbleProps> {
render() {
const {body, item} = this.props;
const x = body.position.x - body.circleRadius;
const y = body.position.y - body.circleRadius;
const style: ViewStyle = {
position: 'absolute',
left: x,
top: y,
width: body.circleRadius * 2,
height: body.circleRadius * 2,
backgroundColor: item.color,
borderRadius: body.circleRadius,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
padding: 3,
};
return <View style={style}>{item.content}</View>;
}
}
Bubble Entity:
const body = Matter.Bodies.circle(
randomX,
randomY,
RADIUS, //default radius 25
{
mass: 30,
}
);
return {
body,
item,
renderer: BubbleNode,
};
Update method:
// find out if the entity was already added to the world
if (Object.prototype.hasOwnProperty.call(entities, id)) {
//update scale
const scale = bubble.item.scale
Matter.Body.scale(bubble.body, scale, scale)
} else {
//add new entity
entities[id] = bubble;
Matter.World.add(physics.world, [bubble.body]);
}
Also, I need to make this scaling smooth
Edit:
Interesting thing. I was able to scale the bubbles using Matter.Body.scale but right before adding to the world. I wonder if there is a method to update the bodies after adding to the world.
Final edit
this is the method I'm using to set up the world and specify entities for the GameEngine framework
private setupWorld = (layout: LayoutRectangle) => {
const engine = Matter.Engine.create({enableSleeping: false});
const world = engine.world;
world.gravity.x = 0;
world.gravity.y = 0;
//center gravity body
Matter.use(MatterAttractors);
var attractiveBody = Matter.Bodies.circle(
layout.width / 2,
layout.height / 2,
1,
{
isSensor: true,
plugin: {
attractors: [centerGravity],
},
},
);
Matter.World.add(world, attractiveBody);
//walls
const wallThickness = 5;
let floor = Matter.Bodies.rectangle(
layout.width / 2,
layout.height - wallThickness / 2,
layout.width,
wallThickness,
{isStatic: true},
);
let ceiling = Matter.Bodies.rectangle(
layout.width / 2,
wallThickness / 2,
layout.width,
wallThickness,
{isStatic: true},
);
let left = Matter.Bodies.rectangle(
wallThickness / 2,
layout.height / 2,
wallThickness,
layout.height,
{isStatic: true},
);
let right = Matter.Bodies.rectangle(
layout.width - wallThickness / 2,
layout.height / 2,
wallThickness,
layout.height,
{isStatic: true},
);
Matter.World.add(world, [floor, ceiling, left, right]);
//basic entitites
this.entities = {
physics: {engine, world},
floor: {body: floor, color: 'green', renderer: Wall},
ceiling: {body: ceiling, color: 'green', renderer: Wall},
left: {body: left, color: 'green', renderer: Wall},
right: {body: right, color: 'green', renderer: Wall},
};
};
And here is the method that will be triggered by a parent Component with some interval
public updateNodes = (items: Item[]) => {
if (!this.state.mounted || !this.entities || !this.entities.physics || !this.layout) {
console.log('Missiing required data');
return;
}
const layout = this.layout
const entities = this.entities
const bubbles: BubbleEntity[] = items.map((item) => {
const randomX = randomPositionValue(layout.width);
const randomY = randomPositionValue(layout.height);
const body = Matter.Bodies.circle(
randomX,
randomY,
RADIUS, {
mass: 30,
}
);
body.label = item.id
return {
body,
item,
renderer: BubbleNode,
};
});
const physics = this.entities.physics as PhysicsEntity;
const allBodies = Matter.Composite.allBodies(physics.world)
bubbles.forEach((bubble) => {
//update existing or add new
const id = `bubble#${bubble.item.id}`;
if (Object.prototype.hasOwnProperty.call(entities, id)) {
//do physical node update here
//update scale and mass
const scale = bubble.item.scale
console.log('Updating item', id, scale);
//right her there used to be an issue because I used **bubble.body** which was not a correct reference to the world's body.
//so when I started to use allBodies array to find a proper reference of the body, everything started to work
let body = allBodies.find(item => item.label === bubble.item.id)
if (!!body) {
const scaledRadius = RADIUS*scale
const current = body.circleRadius || RADIUS
const scaleValue = scaledRadius/current
Matter.Body.scale(body, scaleValue, scaleValue)
}else{
console.warn('Physycal body not found, while the entity does exist');
}
} else {
console.log('Adding entity to the world');
entities[id] = bubble;
Matter.World.add(physics.world, [bubble.body]);
}
});
this.entities = entities
};
In the future, I'm going to improve that code, I will use some variables for the body and will create a matter.js plugin that will allow me to scale the body smoothly and not instant as it works right now. Also, the method above requires some clean, short implementation instead of that garbage I made attempting to make it work
Your example isn't exactly complete; it's not clear how (or if) the MJS engine is running. The first step is to make sure you have an actual rendering loop using calls to Matter.Engine.update(engine); in a requestAnimationFrame loop.
React doesn't seem critical here. It shouldn't affect the result since x/y coordinates and radii for each body in the MJS engine are extracted and handed to the View component, so the data flows in one direction. I'll leave it out for the rest of this example but it should be easy to reintroduce once you have the MJS side working to your satisfaction.
The way to scale a body in MJS is to call Matter.body.scale(body, scaleX, scaleY). This function recomputes other physical properties such as mass for the body.
There's an annoying caveat with this function: instead of setting an absolute scale as a JS canvas context or CSS transformation might, it sets a relative scale. This means each call to this function changes the baseline scaling for future calls. The problem with this is that rounding errors can accumulate and drift can occur. It also makes applying custom tweens difficult.
Workarounds are likely to be dependent on what the actual animation you hope to achieve is (and may not even be necessary), so I'll avoid prescribing anything too specific other than suggesting writing logic relative to the radius as the point of reference to ensure it stays within bounds. Other workarounds can include re-creating and scaling circles per frame.
Another gotcha when working with circles is realizing that MJS circles are n-sided polygons, so small circles lose resolution when scaled up. Again, this is use-case dependent, but you may wish to create a Bodies.polygon with more sides than would be created by Bodies.circle if you experience unusual behavior.
That said, here's a minimal, complete example of naive scaling that shows you how you can run an animation loop and call scale to adjust it dynamically over time. Consider it a proof of concept and will require adaptation to work for your use case (whatever that may be).
const engine = Matter.Engine.create();
const circles = [...Array(15)].map((_, i) => {
const elem = document.createElement("div");
elem.classList.add("circle");
document.body.append(elem);
const body = Matter.Bodies.circle(
// x, y, radius
10 * i + 60, 0, Math.random() * 5 + 20
);
return {
elem,
body,
offset: i,
scaleAmt: 0.05,
speed: 0.25,
};
});
const mouseConstraint = Matter.MouseConstraint.create(
engine, {element: document.body}
);
const walls = [
Matter.Bodies.rectangle(
// x, y, width, height
innerWidth / 2, 0, innerWidth, 40, {isStatic: true}
),
Matter.Bodies.rectangle(
innerWidth / 2, 180, innerWidth, 40, {isStatic: true}
),
Matter.Bodies.rectangle(
0, innerHeight / 2, 40, innerHeight, {isStatic: true}
),
Matter.Bodies.rectangle(
300, innerHeight / 2, 40, innerHeight, {isStatic: true}
),
];
Matter.Composite.add(
engine.world,
[mouseConstraint, ...walls, ...circles.map(e => e.body)]
);
/*
naive method to mitigate scaling drift over time.
a better approach would be to scale proportional to radius.
*/
const baseScale = 1.00063;
(function update() {
requestAnimationFrame(update);
circles.forEach(e => {
const {body, elem} = e;
const {x, y} = body.position;
const {circleRadius: radius} = body;
const scale = baseScale + e.scaleAmt * Math.sin(e.offset);
Matter.Body.scale(body, scale, scale);
e.offset += e.speed;
elem.style.top = `${y - radius / 2}px`;
elem.style.left = `${x - radius / 2}px`;
elem.style.width = `${2 * radius}px`;
elem.style.height = `${2 * radius}px`;
elem.style.transform = `rotate(${body.angle}rad)`;
});
Matter.Engine.update(engine);
})();
* {
margin: 0;
padding: 0;
}
body, html {
height: 100%;
}
.circle {
border-radius: 50%;
position: absolute;
cursor: move;
background: rgb(23, 0, 36, 1);
background: linear-gradient(
90deg,
rgba(23, 0, 36, 1) 0%,
rgba(0, 212, 255, 1) 100%
);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
I am trying to do something like paint with KineticJS. I am trying to draw the color with circles that originate from the mouse position. However the eventlistener of the mouse position seems too slow and when I move the mouse too fast the circles drawn are far from each other resulting this:
I have seen people filling array with points drawing lines between them, but I thought thats very bad for optimization because after dubbing the screen too much the canvas starts lagging because it has too much lines that it redraws every frame. I decided to cancel the cleaning of the layer and I am adding new circle at the current mouse position and I remove the old one for optimization. However since Im not drawing lines on fast mouse movement it leaves huge gaps. I would be very grateful if anyone can help me with this.
Here is my code:
(function() {
var stage = new Kinetic.Stage({
container: 'main-drawing-window',
width: 920,
height: 750
}),
workplace = document.getElementById('main-drawing-window'),
layer = new Kinetic.Layer({
clearBeforeDraw: false
}),
border = new Kinetic.Rect({
stroke: "black",
strokeWidth: 2,
x: 0,
y: 0,
width: stage.getWidth(),
height: stage.getHeight()
}),
brush = new Kinetic.Circle({
radius: 20,
fill: 'red',
strokeWidth: 2,
x: 100,
y: 300
});
Input = function() {
this.mouseIsDown = false;
this.mouseX = 0;
this.mouseY = 0;
this.offsetX = 0;
this.offsetY = 0;
};
var input = new Input();
document.documentElement.onmousedown = function(ev) {
input.mouseIsDown = true;
};
document.documentElement.onmouseup = function(ev) {
input.mouseIsDown = false;
};
document.documentElement.onmousemove = function(ev) {
ev = ev || window.event;
// input.mouseX = (ev.clientX - workplace.offsetLeft);
// input.mouseY = (ev.clientY - workplace.offsetTop);
input.mouseX = (ev.offsetX);
input.mouseY = (ev.offsetY);
};
function DistanceBetweenPoints(x1, y1, x2, y2) {
return Math.sqrt(((x2 - x1) * (x2 - x1)) + ((y2 - y1) * (y2 - y1)));
}
var canvasDraw = setInterval(function() {
// console.log(input);
if (input.mouseIsDown) {
workplace.style.cursor = "crosshair";
var currentBrushPosition = brush.clone();
currentBrushPosition.setX(input.mouseX);
currentBrushPosition.setY(input.mouseY);
// var distance = DistanceBetweenPoints(brush.getX(), brush.getY(), currentBrushPosition.getX(), currentBrushPosition.getY());
// if (distance > brush.getRadius() * 2) {
// var fillingLine = new Kinetic.Line({
// points: [brush.getX(), brush.getY(), currentBrushPosition.getX(), currentBrushPosition.getY()],
// stroke: 'yellow',
// strokeWidth: brush.getRadius()*2,
// lineJoin: 'round'
// });
// // layer.add(fillingLine);
// }
layer.add(currentBrushPosition);
brush.remove();
brush = currentBrushPosition;
layer.draw();
// if (fillingLine) {
// fillingLine.remove();
// }
}
if (!input.mouseIsDown) {
workplace.style.cursor = 'default';
}
}, 16);
layer.add(border);
stage.add(layer);
})();
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Coloring Game</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/kineticjs/5.2.0/kinetic.min.js"></script>
</head>
<body>
<div id="main-drawing-window"></div>
<script type="text/javascript" src="./JS files/canvas-draw.js"></script>
</body>
</html>
Don't use individual Kinetic.Circles for each mousemove. Every Kinetic object is a "managed" object and that management takes up a lot of resources. KineticJS will slow to a crawl as the number of circles increases with every mousemove.
Instead, use a Kinetic.Shape and draw you circles onto the canvas with
// This is Pseudo-code since I haven't worked with KineticJS in a while
shapeContext.beginPath();
shapeContext.arc(mouseX,mouseY,20,0,Math.PI*2);
shapeContext.fillStrokeShape(this);
This will probably clear your problem, but if the mouse is moved very far in a single mousemove then you might have to draw a lineTo (instead of arc) between the last mouse point and the current far-away mouse point.
For the links - in a JointJS diagram - I tried to implement this tutorial (http://jointjs.com/tutorial/constraint-move-to-circle) to move the labels on the link, but I don't understand where to put the ConstraintElementView.
I would like to make the label of a link moveable over the link. So how can I define the link as the 'path' for the moveable label?
ConstraintElementView
var constraint = label; // ???
var ConstraintElementView = joint.dia.ElementView.extend({
pointerdown: function(evt, x, y) {
var position = this.model.get('position');
var size = this.model.get('size');
var center = g.rect(position.x, position.y, size.width, size.height).center();
var intersection = constraint.intersectionWithLineFromCenterToPoint(center);
joint.dia.ElementView.prototype.pointerdown.apply(this, [evt, intersection.x, intersection.y]);
},
pointermove: function(evt, x, y) {
var intersection = constraint.intersectionWithLineFromCenterToPoint(g.point(x, y));
joint.dia.ElementView.prototype.pointermove.apply(this, [evt, intersection.x, intersection.y]);
}
});
link label
paper.on({
/**
* Doubleclick on link: Add label for link
*/
'cell:pointerdblclick': function(cellView, event, x, y){
if (cellView.model.isLink()) {
cellView.model.label(0, {
position: .5,
attrs: {
rect: { fill: '#eeeeee' },
text: { text: 'text', 'font-size': 12, ref: 'rect' }
}
});
}
}
});
paper
var paper = new joint.dia.Paper({
el: $('#canvas'),
width: 801,
height: 496,
model: graph,
gridSize: 10,
elementView: ConstraintElementView,
defaultLink: new joint.dia.Link({
router: { name: 'manhattan' },
connector: { name: 'rounded' },
attrs: {
'.marker-target': { d: 'M 10 0 L 0 5 L 10 10 z', fill: '#6a6c8a', stroke: '#6a6c8a' },
'.connection': { stroke: '#6a6c8a', 'stroke-width': 2 }
}
})
});
As it is moveable over the link, it should be snap to the center of each segment of the manhattan-style link. But I don't see any chance to get the value of the center of each segment.
You do not need to create any path. Just change the position of label by calculating its relative value - of course can also use absolute values.
'cell:pointermove': function(event, x, y) {
if (event.model.isLink()) {
var clickPoint = { x: event._dx, y: event._dy },
lengthTotal = event.sourcePoint.manhattanDistance(event.targetPoint),
length = event.sourcePoint.manhattanDistance(clickPoint),
position = round(length / lengthTotal, 2);
event.model.label(0, { position: position });
}
}
Enabling labels to be movable along links can be done via the labelMove option of the interactive object on the paper:
var paper = new joint.dia.Paper({
// ...
interactive: { labelMove: true }
// ...
})
This flag defaults to false.
I have five rectangles placed at different points along a circle like this - http://imgur.com/uVYkwl7.
Upon clicking any rectangle i want the circle to move to the left of the screen, gradually scaling down it's radius until the circle's center reaches x=0. I'd like the five rectangles to move along with the circle while its being scaled down and also adjust their own positions and scale on the circle so that they are within the view's bounds, like this - http://imgur.com/acDG0Aw
I'd appreciate any help on how to go about doing this. Heres my code for getting to the 1st image and animating the circle:
var radius = 300;
var center = view.center;
var circle = new Path.Circle({
center: view.center,
radius: radius,
strokeColor: 'black',
name: 'circle'
});
var path = new Path.Rectangle({
size: [230, 100],
fillColor: '#1565C0'
});
var rectText = ['Text 1',
'Text 2',
'Text 3',
'Text 4',
'Text 5'
];
var symbol = new Symbol(path);
var corners = [
new Point(center.x, center.y - radius),
new Point(center.x - radius, center.y - radius / 2),
new Point(center.x + radius, center.y - radius / 2),
new Point(center.x - radius, center.y + radius / 2),
new Point(center.x + radius, center.y + radius / 2)
];
var rectClicked = false;
var clickedRect = null;
var rectClick = function(event) {
rectClicked = true;
clickedRect = this;
};
function onFrame(event) {
// Your animation code goes in here
if (rectClicked) {
for (var i = 0; i < 1; i++) {
var item = project.activeLayer.children[i];
if (item.name == 'circle') {
if (item.position.x < 0) {
rectClicked = false;
} else {
item.position.x -= 10;
item.scale(1/1.01);
}
}
}
}
}
// Place the instances of the symbol:
for (var i = 0; i < corners.length; i++) {
var placedSymbol = symbol.place(corners[i]);
placedSymbol.onMouseDown = rectClick;
var rText = new PointText({
point: placedSymbol.bounds.topLeft + 20,
content: rectText[i],
fontSize: '20',
fillColor: 'white'
});
}
Paper.js provides rotations around a pivot out of the box.
var pivotPoint = new Point(10, 5);
circle.rotate(30,pivotPoint);
Here is the docs reference for this behaviour and here is a very basic Sketch example to illustrate this
The above snippet will rotate a circle(you can change this to rectangle in your case) by 30 degrees around a pivot point at coordinates 10,5 on the x/y axis.
Thus what you describe is certainly doable as long as the path that your elements will follow is always circular.
Bear in mind that in order for the pivot rotation to work the way you want them to you need to update the pivotPoint and reinitiate the rotation again.
Note: In case you want to move along an arbitrary shape instead of circular path, you should search for Paper.js animation-along-a-path which is something that I've seen been done before without much difficulty - e.g this simple Sketch by the creator of Paper.js himself.
The sketch I provided above is a basic example of rotation around a pivot point.
I'm dumping the Sketch code here in case the link goes dead:
//Create a center point
var centerCircle = new Path.Circle(paper.view.center, 100);
centerCircle.strokeColor = 'black';
centerCircle.dashArray = [10, 12];
//Create the circles
var circle1Radius = 30;
var circle1 = new Path.Circle((centerCircle.position-centerCircle.bounds.width/2)+circle1Radius, circle1Radius);
circle1.fillColor = '#2196F3';
var circle2Radius = 40;
var circle2 = new Path.Circle((centerCircle.position-centerCircle.bounds.width/2)+circle2Radius, circle2Radius);
circle2.fillColor = '#E91E63';
var circle3Radius = 40;
var circle3 = new Path.Circle((centerCircle.position-centerCircle.bounds.width/2)+circle2Radius, circle2Radius);
circle3.fillColor = '#009688';
var i=0;
var animationGap = 125; //how long to move before animating the next circle
var rotationSpeed = 2;
function onFrame(event) {
circle1.rotate(rotationSpeed,centerCircle.position);
if(i>animationGap)
circle2.rotate(rotationSpeed,centerCircle.position);
if(i>animationGap*2)
circle3.rotate(rotationSpeed,centerCircle.position);
i++;
}