How to create a Konva-React context menu - javascript

To the best of my knowledge there isn't an easy/built in way with Konva to create a context menu for right clicking on objects. I am busy working on a project which requires the use of context menus, so I thought I'd just create my own.
Needless to say I am fairly new to Konva, so I was hoping someone on SO might have more experience to help me get over the last hurdles.
I have create a sandbox, located HERE
The requirements are:
An object should be draggable. (I copied a working example off the Konva sandbox.)
An object should show a context menu when right clicked upon.
The context menu should be dynamic, thus allow for multiple items, each executing its own callback when clicked upon.
Once a selection has been made, the context menu should be closed.
Thus far I have gotten most of it right, but the things I am struggling with are:
I cannot figure out how to hover over one context menu item, have it highlighted, then move to the next which should be highlighted and the old one restored to original settings.
Moving out of the context menu repaints the whole object. I don't understand why.
Clicking on one items fires both item's callbacks. Why? I a targeting the specific menu item which was clicked on, but getting both?
This point is less of a bug and more that I am unsure as how to proceed: How would I prevent multiple context menus to be create if a user right clicks multiple times on the object? Conceptually I understand that I could search for any items in a layer(?) with the name of the context menu and close it, however I have no idea how to do this.
I would appreciate any help. Thanks in advance.

Not sure if I'm late but I would use React Portals, theres a example about it on the react-konva page: https://konvajs.github.io/docs/react/DOM_Portal.html
I forked your sandbox with how this would be done: https://codesandbox.io/s/km0n1x8367

Not in react but plain JS I am afraid, but it shines a light on some of what you will have to do.
Click the pink circle, then take option 2 and click sub-option 2.
Areas requiring more work:
deliver the menu config data via JSON
make adding callbacks a method within the class
add a timeout on the hide to allow shaky mouse hands
how to handle hiding sub-menus when user mouse-outs or clicks another option
add reveal & hide animations
// Set up the canvas / stage
var stage = new Konva.Stage({container: 'container1', width: 600, height: 300});
// Add a layer some sample shapes
var layer = new Konva.Layer({draggable: false});
stage.add(layer);
// draw some shapes.
var circle = new Konva.Circle({ x: 80, y: 80, radius: 30, fill: 'Magenta'});
layer.add(circle);
var rect = new Konva.Rect({ x: 80, y: 80, width: 60, height: 40, fill: 'Cyan'});
layer.add(rect);
stage.draw();
// that is the boring bit over - now menu fun
// I decided to set up a plain JS object to define my menu structure - could easily receive from async in JSON format. [Homework #1]
var menuData = { options: [
{key: 'opt1', text: 'Option 1', callBack: null},
{key: 'opt2', text: 'Option 2', callBack: null,
options: [
{key: 'opt2-1', text: 'Sub 1', callBack: null},
{key: 'opt2-2', text: 'Sub 2', callBack: null}
]
},
{key: 'opt3', text: 'Option 3', callBack: null},
{key: 'opt4', text: 'Option 4', callBack: null}
]};
// Define a menu 'class' object.
var menu = function(menuData) {
var optHeight = 20; // couple of dimension constants.
var optWidth = 100;
var colors = ['white','gold'];
this.options = {}; // prepare an associative list accessible by key - will put key into the shape as the name so we can can get from click event to this entry
this.menuGroup = new Konva.Group({}); // prepare a canvas group to hold the option rects for this level. Make it accessible externally by this-prefix
var _this = this; // put a ref for this-this to overcome this-confusion later.
// recursive func to add a menu level and assign its option components.
var addHost = function(menuData, hostGroup, level, pos){ // params are the data for the level, the parent group, the level counter, and an offset position counter
var menuHost = new Konva.Group({ visible: false}); // make a canvas group to contain new options
hostGroup.add(menuHost); // add to the parent group
// for every option at this level
for (var i = 0; i < menuData.options.length; i = i + 1 ){
var option = menuData.options[i]; // get the option into a var for readability
// Add a rect as the background for the visible option in the menu.
option.optionRect = new Konva.Rect({x: (level * optWidth), y: (pos + i) * optHeight, width: optWidth, height: optHeight, fill: colors[0], stroke: 'silver', name: option.key });
option.optionText = new Konva.Text({x: (level * optWidth), y: (pos + i) * optHeight, width: optWidth, height: optHeight, text: ' ' + option.text, listening: false, verticalAlign: 'middle'})
console.log(option.optionText.height())
option.optionRect
.on('mouseover', function(){
this.fill(colors[1])
layer.draw();
})
.on('mouseleave', function(){
this.fill(colors[0])
layer.draw();
})
// click event listener for the menu option
option.optionRect.on('click', function(e){
var key = this.name(); // get back the key we stashed in the rect so we can get the options object from the lookup list
if (_this.options[key] && (typeof _this.options[key].callback == 'function')){ // is we found an option and it has a real function as a callback then call it.
_this.options[key].callback();
}
else {
console.log('No callback for ' + key)
}
})
menuHost.add(option.optionRect); // better add the rect and text to the canvas or we will not see it
menuHost.add(option.optionText);
_this.options[option.key] = option; // stash the option in the lookup list for later retrieval in click handlers.
// pay attention Bond - if this menu level has a sub-level then we call into this function again.
if (option.options){
var optionGroup = addHost(option, menuHost, level + 1, i) // params 3 & 4 are menu depth and popout depth for positioning the rects.
// make an onclick listener to show the sub-options
option.callback = function(e){
optionGroup.visible(true);
layer.draw();
}
}
}
return menuHost; // return the konva group
}
// so - now we can call out addHost function for the top level of the menu and it will recurse as needed down the sub-options.
var mainGroup = addHost(menuData, this.menuGroup, 0, 0);
// lets be nice and make a show() method that takes a position x,y too.
this.show = function(location){
location.x = location.x - 10; // little offset to get the group under the mouse
location.y = location.y - 10;
mainGroup.position(location);
mainGroup.show(); // notice we do not draw the layer here - leave that to the caller to avoid too much redraw.
}
// and if we have a show we better have a hide.
this.hide = function(){
mainGroup.hide();
}
// and a top-level group listener for mouse-out to hide the menu. You might want to put a timer on this [Homework #3]
mainGroup.on('mouseleave', function(){
this.hide();
layer.draw();
})
// end of the menu class object.
return this;
}
// ok - now we can get our menu data turned into a menu
var theMenu = new menu(menuData);
layer.add(theMenu.menuGroup); // add the returned canvas group to the layer
layer.draw(); // and never forget to draw the layer when it is time!
//
// now we can add some arbitrary callbacks to some of the options.
//
// make a trivial function to pop a message when we click option 1
var helloFunc = function(){
alert('hello')
}
// make this the callback for opt1 - you can move this inside the menu class object as a setCallback(name, function()) method if you prefer [homework #2]
theMenu.options['opt1'].callback = helloFunc;
// put a function on sub2 just to show it works.
theMenu.options['opt2-2'].callback = function(){ alert('click on sub-2') };
// and the original reason for this - make it a context menu on a shape.
circle.on('click', function(e){
theMenu.show({x: e.evt.offsetX, y: e.evt.offsetY});
layer.draw();
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/2.5.1/konva.min.js"></script>
<div id='container1' style="width: 300px, height: 200px; background-color: silver;"></div>

Related

Phaser 3: Change "Hitbox"/Interactive area of sprite without physics

The game I'm creating doesn't require any physics, however you are able to interact when hovering over/clicking on the sprite by using sprite.setInteractive({cursor: "pointer"});, sprite.on('pointermove', function(activePointer) {...}); and similar. However I noticed two issues with that:
The sprite has some area which are transparent. The interactive functions will still trigger when clicking on those transparent areas, which is unideal.
When playing a sprite animation, the interactive area doesn't seem to entirely (at all?) change, thus if the sprite ends on a frame bigger than the previous, there end up being small areas I can't interact with.
One option I thought of was to create a polygon over my sprite, which covers the area I want to be interactive. However before I do that, I simply wanted to ask if there are simpler ways to fix these issues.
Was trying to find an answer for this myself just now..
Think Make Pixel Perfect is what you're looking for.
this.add.sprite(x, y, key).setInteractive(this.input.makePixelPerfect());
https://newdocs.phaser.io/docs/3.54.0/focus/Phaser.Input.InputPlugin-makePixelPerfect
This might not be the best solution, but I would solve this problem like this. (If I don't want to use physics, and if it doesn't impact the performance too much)
I would check in the event-handler, if at the mouse-position the pixel is transparent or so, this is more exact and less work, than using bounding-boxes.
You would have to do some minor calculations, but it should work well.
btw.: if the origin is not 0, you would would have to compensate in the calculations for this. (in this example, the origin offset is implemented)
Here is a demo, for the click event:
let Scene = {
preload ()
{
this.load.spritesheet('brawler', 'https://labs.phaser.io/assets/animations/brawler48x48.png', { frameWidth: 48, frameHeight: 48 });
},
create ()
{
// Animation set
this.anims.create({
key: 'walk',
frames: this.anims.generateFrameNumbers('brawler', { frames: [ 0, 1, 2, 3 ] }),
frameRate: 8,
repeat: -1
});
// create sprite
const cody = this.add.sprite(200, 100).setOrigin(0);
cody.play('walk');
cody.setInteractive();
// just info text
this.mytext = this.add.text(10, 10, 'Click the Sprite, or close to it ...', { fontFamily: 'Arial' });
// event to watch
cody.on('pointerdown', function (pointer) {
// calculate x,y position of the sprite to check
let x = (pointer.x - cody.x) / (cody.displayWidth / cody.width)
let y = (pointer.y - cody.y) / (cody.displayHeight / cody.height);
// just checking if the properties are set
if(cody.anims && cody.anims.currentFrame){
let currentFrame = cody.anims.currentFrame;
let pixelColor = this.textures.getPixel(x, y, currentFrame.textureKey, currentFrame.textureFrame);
// alpha > 0 a visible pixel of the sprite, is clicked
if(pixelColor.a > 0) {
this.mytext.text = 'Hit';
} else {
this.mytext.text = 'No Hit';
}
// just reset the textmessage
setTimeout(_ => this.mytext.text = 'Click the Sprite, or close to it ...' , 1000);
}
}, this);
}
};
const config = {
type: Phaser.AUTO,
width: 400,
height: 200,
scene: Scene
};
const game = new Phaser.Game(config);
<script src="https://cdn.jsdelivr.net/npm/phaser#3.55.2/dist/phaser.js"></script>

How to make resizable Text on canvas using javascript

I'm pretty much new to canvas. What I'm trying to make is that I can write text in canvas using input and can be able to resize it by dragging it's corners. Also I should be able to drag text position within the canvas.
Following is the screen shot of what I want!
Canvas is raster, not vector. By simply drawing and resizing text you would expect it to get blurry or pixelated. And redrawing the whole canvas each time user moves the cursor while resizing will not result in the best performance. Consider using svg instead. In case you do need canvas and don't want to implement all the functions yourself, you can use the paperjs library.
http://paperjs.org/reference/pointtext/
As #hirasawa-yui mentioned, you can use Paper.js to greatly facilitate the implementation of what you want in a canvas.
Here is a simplified sketch showing a possible implementation of dragging/resizing interactions.
// create item
var item = new PointText({
content: 'Custom text content',
point: view.center,
justification: 'center',
fontSize: 30,
selected: true
});
// init variables so they can be shared by event handlers
var resizeVector;
var moving;
// on mouse down...
function onMouseDown(event) {
// ...do a hit test on item bounds with a small tolerance for better UX
var cornerHit = item.hitTest(event.point, {
bounds: true,
tolerance: 5
});
// if a hit is detected on one of the corners...
if (cornerHit && ['top-left', 'top-right', 'bottom-left', 'bottom-right'].indexOf(cornerHit.name) >= 0) {
// ...store current vector from item center to point
resizeVector = event.point - item.bounds.center;
// ...else if hit is detected inside item...
} else if (item.hitTest(event.point, { fill: true })) {
// ...store moving state
moving = true;
}
}
// on mouse drag...
function onMouseDrag(event) {
// ...if a corner was previously hit...
if (resizeVector) {
// ...calculate new vector from item center to point
var newVector = event.point - item.bounds.center;
// scale item so current mouse position is corner position
item.scale(newVector / resizeVector);
// store vector for next event
resizeVector = newVector;
// ...if item fill was previously hit...
} else {
// ...move item
item.position += event.delta;
}
}
// on mouse up...
function onMouseUp(event) {
// ... reset state
resizeVector = null;
moving = null;
}
// draw instructions
new PointText({
content: 'Drag rectangle to move, drag corners to resize.',
point: view.center + [0, -50],
justification: 'center'
});

fabricjs change cursor for every object

I created some objects with fabric js and added different cursors for them if the user hovers with his mouse over the object. i want to add a function which changes the cursor for the whole canvas and all its objects and then changes it back to its default values (so the cursor settings for the objects and the canvas).
is there a way to do this simple? or do i have to set the cursor for every object manually?
I got a small jsFiddle for this: https://jsfiddle.net/bxgox7cr/14/
If you click on the button "change Cursor", the cursor is changed to a crosshair, but when you move your mose over the lines or circles, it is changed to their settings. I want the cursor to stay as a crosshair. After pressing "default Cursor" all the settings should be set back, so the cursor should use the special settings for lines and circles.
the main functionality is in this 2 functions:
$('#button').click(function() {
canvas.defaultCursor = 'crosshair';
});
$('#button2').click(function() {
canvas.defaultCursor = 'default';
});
I might save the default values for every object and then change them back after the click of button2, but this seems to be a difficult solution, so I'm hoping there might be an easier way.
Just add to your code this event:
canvas.on('mouse:over', function(event){
if (event.target != null)
event.target.hoverCursor = canvas.defaultCursor;
}
});
On each object you will reset 'hover' cursor what you set for whole canvas.
Updated:
If you need reset to default you have to keep track of original cursors for each object. You need something like this:
$('#button').click(function() {
changeCursor('crosshair');
});
$('#button2').click(function() {
resetCursor();
});
var cursors = {canvasDefault: canvas.defaultCursor,
canvasDefault: 'default',
object:[
{objectType: 'circle',
hoverCursor: 'move'},
{objectType: 'line',
hoverCursor: 'ns-resize'}
]
};
function changeCursor(cursor){
canvas.forEachObject(function(obj){
for (var i in cursors.object){
if (cursors.object[i].objectType == obj.type){
obj.hoverCursor = cursor;
}
}
canvas.defaultCursor = cursor;
});
}
function resetCursor(){
canvas.forEachObject(function(obj){
for (var i in cursors.object){
if (cursors.object[i].objectType == obj.type){
obj.hoverCursor = cursors.object[i].hoverCursor;
}
}
canvas.defaultCursor = cursors.canvasDefault;
});
}

How to lock slider and prevent updating of values with mouse into dat.GUI menu

I try to implement a way to prevent the updating of values with mouse (actually when the three.js animation has started, launched with a click on button).
For the moment, I have the following dat.GUI menu:
Once "start" button is clicked, I would like to prevent user from modifying with mouse the parameters "Rotation x" and "Rotation y".
Here is the concerned part of code for this menu:
// Create GUI
var gui = new dat.GUI({
autoplace: false,
width: 350,
height: 9 * 32 - 1
});
var params = {
GreatCircle : '',
Rotationx : torusRotationInitX,
Rotationy : torusRotationInitY,
StartingVector : '',
ComponentVectorTheta : 15.0,
ComponentVectorPhi : 15.0,
CovariantDerivativeVector : '',
ComponentCovariantDerivativeTheta : 15.0,
ComponentCovariantDerivativePhi : 15.0
};
// Set parameters for GUI
gui.add(params, 'GreatCircle').name('Great Circle ');
controllerRotationx = gui.add(params, 'Rotationx', 0, 2*Math.PI, 0.001).name('Rotation x ');
controllerRotationy = gui.add(params, 'Rotationy', 0, 2*Math.PI, 0.001).name('Rotation y ');
...
When I click on reset button, I call the following function:
// Reset Button
resetButton.onclick = function ResetParameters() {
...
// Reinitialize parameters into gui
params.Rotationx = torusRotationInitX;
params.Rotationy = torusRotationInitY;
for (var i in gui.__controllers) {
gui.__controllers[i].updateDisplay();
}
render();
}
I don't know if there is an option for controller to lock these sliders which usually change their values. Is it possible?
Update 1
Maybe I could wrapper the dat.GUI menu into a div and make this div not clickable, is it a solution?
Update 2
I tried to apply the method used on Method for disabling a button in dat.gui?
Following this solution, I have added the extension into dat.gui, just after:
dat.controllers.FunctionController = (function (Controller, dom, common) {
...
});
The following added code snippet is:
function blockEvent(event)
{
event.stopPropagation();
}
Object.defineProperty(dat.controllers.FunctionController.prototype, "disabled", {
get: function()
{
return this.domElement.hasAttribute("disabled");
},
set: function(value)
{
if (value)
{
this.domElement.setAttribute("disabled", "disabled");
this.domElement.addEventListener("click", blockEvent, true);
}
else
{
this.domElement.removeAttribute("disabled");
this.domElement.removeEventListener("click", blockEvent, true);
}
},
enumerable: true
});
Is extension code well located into dat.GUI source?
Then, I set the property "disabled" into my code to prevent user from sliding "controllerRotationx" with mouse (once start button is pressed):
if (animation)
controllerRotationx.__li.disabled = true;
Unfortunately, my method doesn't work : when animation is started, I can still move the slider contained into "controllerRotationx".
I saw that above link (Method for disabling a button in dat.gui?), this was about a button and not for a slider, does it change anything for my case?
I didn't find an explicit controller for the slider.
I would do this. The slider is not a form element, there's nothing to disable in the traditional w3c sense. Luckily we can use pointer-events and disable it properly as if it were a form element using just public dat.gui properties.
var speeder = menu.add(text, 'speed', -5, 5);
speeder.domElement.style.pointerEvents = "none"
speeder.domElement.style.opacity = .5;
The solution given by #Radio works pretty well. But, with sliders, the slider is a sibling of the text box's DOM element. We need to disable pointer events on the div which contains all the controls (and which is not exposed directly by dat.gui). So,
var speeder = menu.add(text, 'speed', -5, 5);
// disables the text box
speeder.domElement.style.pointerEvents = "none"
// disables all controller elements related to "speeder"
speeder.domElement.parentElement.style.pointerEvents = 'none'
When the Start button is pressed, set:
controllerRotationx.__li.setAttribute( "style", "display: none" );
thanks for tips
on my side i hack the Common controller
so able to chainning.
gui.add(this, '_screenW').disable(true);
Common.extend(controller, {
disable: function disable(v) {
this.domElement.style.pointerEvents = v?"none":"auto";
this.domElement.style.opacity = v?.5:1;
return controller;
},

Event delegation in KineticJS

I'm wondering if possible do create a event delegation from an object reference inside another object, this is the scenario.
I have 2 circles and 1 line between those two circles, every time any circle changes its own postition it have to update the line's position as well, by this time I have created a custom line that is going to have a reference to the two circles as a properties (origin, target)
but isn't binding the event in that way.
what I'm tryting to do is something like>
this.origin.on('dragstart dragmove',function(){
console.log("origin mouse move");
this.setPoints([origin.getX(),origin.getY(), target.getX(), target.getY()]);
this.draw();
});
this is the fiddle
buildArrow: function(origin,target){
var arrowInstance = new Shape.Arrow({
points: [origin.getX(),origin.getY(), target.getX(), target.getY()],
stroke: '#629DD1',
tension: 1
});
arrowInstance.origin=origin;
origin.line = arrowInstance;
arrowInstance.target=target;
target.line = arrowInstance;
return arrowInstance;
},
updatePosition : function() {
this.points([this.origin.getX(),this.origin.getY(), this.target.getX(), this.target.getY()]);
},
Then:
this.on('dragstart dragmove',function(){
var centerpoint = (this.cellSize/2)
this.setX(((Math.round(this.getX() / centerpoint) * centerpoint)));
this.setY(((Math.round(this.getY() / centerpoint) * centerpoint)));
this.line.updatePosition();
});
Demo: http://jsfiddle.net/lavrton/5Wkph/28/

Categories

Resources