Related
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>
A beginner question. I'm trying to put up a famo.us app. So far, I had nice results on a simple experiment.
Now that my app is growing, I want to allow moving between "screens" : landing screen -> info screen -> back to landing screen -> etc.
My "screens" are View() with surfaces, modifiers, etc. added to the View()
I use RenderController() to switch between screens, with its hide() and show() functions.
The problem is that when going back to a previously used screen, surfaces no longer show up !
Code :
var app_render_controller = new RenderController();
app_render_controller.show(landing_screen); // ok
app_render_controller.hide(landing_screen); // ok
app_render_controller.show(info_screen); // ok
app_render_controller.hide(info_screen); // ok
app_render_controller.show(landing_screen); // NOK -> I only see my background surface !
When examining the DOM, I'm able to see the outline of my surfaces at the place they should be, but they are hidden. It seems the background surface is now in front of the content surfaces (a ScrollView). If I hide the background, I see the Scrollview again.
I read that the overlap of elements was corresponding to the order they were added to the rendering tree. So since the tree under my View()-based "screen" hasn't changed, the Scrollview should still be drawn on top of the background like the 1st time ?
Is this expected ? Am I doing something wrong ? Am I missinq something ?
------- edit ------
my code (roughly) :
var main_context = Engine.createContext();
var root_render_controller = new RenderController();
main_context.add(root_render_controller);
var landing_screen = new View();
landing_screen.add(new Surface(...background texture...));
var scrollview = new Scrollview();
landing_screen.add(scrollview);
// (add stuff to the scrollview)
var info_screen = new View();
info_screen.add(new Surface(...background texture...));
info_screen.add(new Surface(...some message...));
root_render_controller.show(landing_screen);
// display is fine
root_render_controller.hide(landing_screen);
root_render_controller.show(info_screen);
// display is fine
root_render_controller.hide(info_screen);
root_render_controller.show(landing_screen); // again
// display is wrecked : the background surface is now hiding the scrollview !
When looking at the DOM, we can see the flip :
1st display of the landing screen : background then the scrollview and its 6 elements
2nd display of the landing screen : scrollview and its 6 elements then the background !
I read that the overlap of elements was corresponding to the order they were added to the rendering tree
From what I've seen, overlap of elements is set by the inverse of the order of creation (e.g., last one on top), not addition to the render tree.
When a Surface is created, it is registered with Entity:
https://github.com/Famous/core/blob/master/Surface.js#L60
function Surface(options) {
...
this.id = Entity.register(this);
...
}
When changes on a RenderNode are committed, they are applied based on the creation order (as Object.keys returns indices from an array back in numerical order):
https://github.com/Famous/core/blob/master/RenderNode.js#L106
function _applyCommit(spec, context, cacheStorage) {
var result = SpecParser.parse(spec, context);
var keys = Object.keys(result);
for (var i = 0; i < keys.length; i++) {
var id = keys[i];
var childNode = Entity.get(id);
....
}
You can test this out by creating two Surfaces and adding the one you created second to a Context before the first. It will still be on top:
define(function(require, exports, module) {
var Engine = require("famous/core/Engine");
var Surface = require("famous/core/Surface");
var mainContext = Engine.createContext();
var surface1 = new Surface({
size: [200, 200],
content: "Red",
properties: {
lineHeight: "200px",
textAlign: "center",
backgroundColor: "#f00"
}
});
var surface2 = new Surface({
size: [200, 200],
content: "Green",
properties: {
lineHeight: "200px",
textAlign: "center",
backgroundColor: "#0f0"
}
});
// Doesn't matter what order you add these - surface2 will be on top
mainContext.add(surface2);
mainContext.add(surface1);
});
Hopefully that gives you a good place to start looking. It would help if you shared where your background is added to the Context to get a sense of why it is now in front.
It looks like you've found a bug in RenderController. The simple example below is exhibiting the same behavior you described. I've filed it against their repo at https://github.com/Famous/views/issues/42.
Note: If you don't hide the current view before showing the next, you get the same behavior. Interestingly, if you wait between the hide/show operations, you see different incorrect behavior.
define(function(require, exports, module) {
var Engine = require("famous/core/Engine");
var Surface = require("famous/core/Surface");
var View = require("famous/core/View");
var RenderController = require("famous/views/RenderController");
var Timer = require("famous/utilities/Timer");
var mainContext = Engine.createContext();
var renderController = new RenderController();
mainContext.add(renderController);
var view1 = new View();
var view2 = new View();
var surface1 = new Surface({
size: [200, 200],
content: "Red Background",
properties: {
lineHeight: "200px",
textAlign: "center",
backgroundColor: "#f00"
}
});
var surface2 = new Surface({
size: [200, 200],
content: "Green Foreground",
properties: {
lineHeight: "200px",
textAlign: "center",
backgroundColor: "#0f0"
}
});
var surface3 = new Surface({
size: [200, 200],
content: "Blue Background",
properties: {
lineHeight: "200px",
textAlign: "center",
backgroundColor: "#00f"
}
});
var surface4 = new Surface({
size: [200, 200],
content: "Black Foreground",
properties: {
lineHeight: "200px",
textAlign: "center",
backgroundColor: "#000",
color: "white"
}
});
view1.add(surface1);
view1.add(surface2);
view2.add(surface3);
view2.add(surface4);
// If performing hide/show on same tick (or not hiding before show),
// you get green-fg, black-fg, red-bg (wrong)
renderController.show(view1);
Timer.setTimeout(function() {
renderController.hide(view1); // <- commenting this out results in same behavior
renderController.show(view2);
Timer.setTimeout(function() {
renderController.hide(view2); // <- commenting this out results in same behavior
renderController.show(view1);
}, 1000);
}, 1000);
// If waiting between each hide/show
// you get green-fg, blue-bg (wrong), green-fg
// renderController.show(view1);
// Timer.setTimeout(function() {
// renderController.hide(view1);
// Timer.setTimeout(function() {
// renderController.show(view2);
// Timer.setTimeout(function() {
// renderController.hide(view2);
// Timer.setTimeout(function() {
// renderController.show(view1);
// }, 1000)
// }, 1000)
// }, 1000)
// }, 1000)
});
A View has no idea the order in which Surfaces should be layered in. When something is hidden with a RenderController it frees up the document fragment that the Surface is managing. When that View is brought back into the scene graph, the Surfaces get a document fragment to fill with its content from the pool of unused elements if any exist, if the pool is empty, it creates new ones.
Due to the way that browsers implement layering, newly created DOM nodes exist on top of old DOM nodes if they exist at the same transform in Z and have the same CSS zIndex. You should not be relying on the creation order if you need explicit layering. Try getting around this by either adding Modifiers to transform them in z-space or add explicit CSS zIndexing to denote what your layers should be.
I am new to client-side programming. Thus far I've been writing only asp and php based solutions. But now I need to retrieve data from json and plot on a map (I don't know how to do that yet, but this is later).
After days of searching, I think OpenLayers can give me what I need.
I have gone through the Examples on dev.openlayers site, (such as this one http://dev.openlayers.org/releases/OpenLayers-2.13.1/examples/vector-features-with-text.html), and also searched (and found some) solutions on stackoverflow, but they don't offer solutions to my problems).
Please view what I've done so far:
http://www.nusantech.com/bangkuujian/openlayer.html
The canvas.js is as follows:
// create some sample features
var Feature = OpenLayers.Feature.Vector;
var Geometry = OpenLayers.Geometry;
var features = [
new Feature(new Geometry.Point(-220, -60),attributes = { name: "Mercury",align: "cm",xOffset:10,yOffset:50 }),
new Feature(new Geometry.Point(-70, 120),attributes = { name: "Venus" }),
new Feature(new Geometry.Point(0, 0),attributes = { name: "Earth" }),
new Feature(new Geometry.Point(160, -100),attributes = { name: "Mars",align: "cm",xOffset:10,yOffset:50 })];
// create rule based styles
var Rule = OpenLayers.Rule;
var Filter = OpenLayers.Filter;
var style = new OpenLayers.Style({
pointRadius: 10,
strokeWidth: 3,
strokeOpacity: 0.7,
strokeColor: "#ffdd77",
fillColor: "#eecc66",
fillOpacity: 1,
label : "${name}",
fontColor: "#f0f0f0",
fontSize: "12px",
fontFamily: "Calibri, monospace",
labelAlign: "${align}",
labelXOffset: "${xOffset}",
labelYOffset: "${yOffset}",
labelOutlineWidth : 1
},
{
rules: [
new Rule({
elseFilter: true,
symbolizer: {graphicName: "circle"}
})
]
});
var layer = new OpenLayers.Layer.Vector(null, {
styleMap: new OpenLayers.StyleMap({'default': style,
select: {
pointRadius: 14,
strokeColor: "#e0e0e0",
strokeWidth: 5
}
}),
isBaseLayer: true,
renderers: ["Canvas"]
});
layer.addFeatures(features);
var map = new OpenLayers.Map({
div: "map",
layers: [layer],
center: new OpenLayers.LonLat(50, 45),
zoom: 0
});
var select = new OpenLayers.Control.SelectFeature(layer);
map.addControl(select);
select.activate();
What I have problems with:
Label offset
In the samples, the labels should offset from the centre by labelXOffset: "(xvalue)", labelYOffset: "(yvalue)", but this is not happening in my page. Is there something I forgot?
Zoom-in
When I click the + button on the map, all the features look like they are zoomed in, however, the sizes of the features stay the same. How do I enlarge the features (circles) too?
Hit Detection
i) When I click on a circle, it is selected as designed. However, is it possible when I select a circle, I also change the right side (now there is a red "text here") and fill it up with html? Can you show me an example how to change the red "text here" to the label-name of the selected circle with a different colour?
ii) Secondly, after I select a circle, how do I add a label under all the other circles denoting the distance between each circle and the selected circle?
Thank you in advance, hopefully these questions are not too much.
I have another question about retrieving an array of coordinates from json to plot the circles, but I will do more research on that. If you can point me in the right direction with regards to this, it would be much appreciated too.
I know how to do them server-side asp or php, but client side is very new to me. However client-side can do all of this much-much faster and can reduce a lot of load.
Cheers,
masCh
I think I have managed to most of it.
Labels not offsetting
Not sure what I did, but I declared a WMS layer and made a few changes to offset and now it is offsetting correctly.
var wms = new OpenLayers.Layer.WMS("NASA Global Mosaic",
"http://hendak.seribudaya.com/starmap.jpg",
{
layers: "modis,global_mosaic",
}, {
opacity: 0.5,
singleTile: true
});
var context = {
getSize: function(feature) {
return feature.attributes["jejari"] / map.getResolution() * .703125;
}
};
var template = {
pointRadius: "${getSize}", // using context.getSize(feature)
label : "\n\n\n\n${name}\n${jarak}",
labelAlign: "left",
labelXOffset: "${xoff}",
labelYOffset: "${yoff}",
labelOutlineWidth : 0
};
var style = new OpenLayers.Style(template, {context: context});
And I declared xoff & yoff under new OpenLayers.Geometry.Point(x,y), { jejari:5, xoff: -10, yoff: -15 }
2) Zoom in on point features.
This was a weird problem. Anyway, I declared a radius called jejari as in the code above next to xoff and yoff. Then modified pointRadius from a static number to "${getSize}" And then added the getSize function to var template which retrieves the current radius. I think that was all I did for that. But the labels were running all over the place, I still haven't solved that.
3) Hit detection and changing another in html
This adds what happens to the once a point feature has been selected
layer.addFeatures(features);
layer.events.on({ "featureselected": function(e) {
kemasMaklumat('maklumat', "<FONT FACE='Calibri' color='#f0f0f0' size=5><center>"+
e.feature.attributes.name+
"<p>This is displayed text when a feature has been selected";
maklumat.style.color="black";
layer.redraw();
}
});
map.addLayers([layer]);
And in the html the and the kemasMaklumat function is declared as
<script type="text/javascript">
function kemasMaklumat(id,content) {
var container = document.getElementById(id);
container.innerHTML = content;
}
</script>
<td valign="top"><div id="maklumat" style="border-radius:25px; background-color:#000000;box-shadow: 8px 8px 4px #686868;">
Write Something Here<P>
</div></td>
The second part of this question was changing the labels of all the UNselected features, i.e. modifying attributes of all features that weren't the selected one. To do this, I added a for loop through all the features and check if it has the same label as the feature that was selected, this was done under the layer.events.on "featureselected" as was done in the above part 1 of this question.
layer.addFeatures(features);
layer.events.on({ "featureselected": function(e) {
kemasMaklumat('maklumat', "<FONT FACE='Calibri' color='#f0f0f0' size=5><center>"+
e.feature.attributes.name+
"<p>This is displayed text when a feature has been selected";
maklumat.style.color="black";
for (var i = 0, l = layer.features.length; i < l; i++) {
var feature = layer.features[i];
if (feature.attributes.name!=e.feature.attributes.name) {
feature.attributes.name="I was not selected"; }}
layer.redraw();
}
});
map.addLayers([layer]);
I'm trying a simple test where I have a base image with draggable features on top. These point features have associated external graphics (I'd prefer to use polygons with associated graphics but that's a whole other question).
I have a hover control that works perfectly. I also have a select control that almost works. However I cannot reselect a feature once I have clicked out once. Similarly, I cannot select features if I click anywhere outside of the features before trying to select one.
I hope that's clear - but if not then this example should be (and you can also see the whole code) http://sigfrid.co.uk/oltest/simple.html
I'll put what I think are the key bits of code below....
Create a map
var map = new OpenLayers.Map({
div:'map',
});
Add a base layer
var base = new OpenLayers.Layer.Image(
'Base level',
'img/base.png',
new OpenLayers.Bounds(-1000, -1000, 1000, 1000),
new OpenLayers.Size(864,864),
options
);
map.addLayer(base);
Add styles
var markerStyleMap = new OpenLayers.StyleMap
({
"default": new OpenLayers.Style(template, {context: context}),
"hover": new OpenLayers.Style ({graphicOpacity:0.5}),
"select": new OpenLayers.Style ({graphicOpacity:0.1})});
Add points
pt1 = new OpenLayers.Geometry.Point(0,0);
pt1Feature = new OpenLayers.Feature.Vector(pt1);
...
markerLayer.addFeatures([pt1Feature,pt2Feature,pt3Feature]);
Add unique lookup for the different marker images
var lookup = {
"f1": {externalGraphic:"img/f1.png"},
"f2": {externalGraphic:"img/f2.png"},
"f3": {externalGraphic:"img/f3.png"},
}
markerStyleMap.addUniqueValueRules("default", "type", lookup);
markerLayer.styleMap = markerStyleMap;
markerLayer.features[0].attributes.type = "f1";
...
Add controls
var dragControl = new OpenLayers.Control.DragFeature(markerLayer)
map.addControl(dragControl);
dragControl.activate();
var highlightCtrl = new OpenLayers.Control.SelectFeature(markerLayer, {
hover: true,
highlightOnly: true,
renderIntent: "hover",
});
map.addControl(highlightCtrl);
highlightCtrl.activate();
var selectCtrl = new OpenLayers.Control.SelectFeature(markerLayer, {
clickout: true,
renderIntent: "select",
});
map.addControl(selectCtrl);
selectCtrl.activate();
Thanks for any help,
Nick
Turns out drag and select are not compatible. However, there are some work arounds such as
http://fastr.wordpress.com/2012/04/17/openlayers-selectfeature-where-did-i-clicked-2/
although this does add some event problems...
I'm new to JS and KineticJS, and was wondering if someone could help me understand how the Kinetic.Stage node works. I've used this tutorial as a starting place -- but would like to be able to append objects (such as new Kinetic.Text() or new Kinetic.Group()) to the existing stage on click of a button.
An example of what I'm trying to do is have a form next to the canvas that allows a user to enter text, click "go", and have the "text that they entered" appear inside the canvas.
I've tried to declare "var stage" inside the document.ready and in global scope, thinking that I could access the stage from within a function, for instance:
function writeTextToStage(stage) {
var text = new Kinetic.Text({
x: 190,
y: 15,
text: 'Simple Text',
fontSize: 30,
fontFamily: 'Calibri',
textFill: 'green'
});
var layer = new Kinetic.Layer();
layer.add(text);
stage.add(layer);
stage.draw();
}
window.onload = function(){
var stage = new Kinetic.Stage({
container: "designer",
width: 578,
height: 520
});
var go = document.getElementById('go');
go.addEventListener('mousedown', writeTextToCanvas(stage);
};
Anyways, if the "stage" object is declared within another function -- "initStage()", is it possible to get the stage node after it has been initialized in order to append things to it? I've tried accessing it via the DOM element, $('kineticjs-content').children()[0].
Please assist. The documentation on kineticjs.com is not very verbose.
Thanks!
You need to create the layer outside your function, then in your function just add the text to the layer...
function AddText() {
var text = new Kinetic.Text({
...
});
layer.add(text);
}