Dojo connect doesn't work with dojo declare - javascript

I have built a square and a rectangle. I use dojo.declare to overwrite the onMouseMove event, so that the square only is dnd inside the surface.
But i have problem to connect the square with a function.
The rectangle doesn't use the custom mover and is connected to a function. It prints out to the console every time it moves.
What am i doing wrong ?
http://jsfiddle.net/Baboly/jh6dnb49/
HTML:
<body>
<div id='drawing_surface'></div>
</body>
Javascript:
djConfig='parseOnLoad: true'>
dojo.require('dojox.gfx'); // omit this and the rectangle doesn't draw
dojo.require('dojox.gfx.move'); // omit this and the rectangle doesn't move
dojo.require('dojo.parser'); // parser and dojo.css aren't required
dojo.ready(function() {
surface = dojox.gfx.createSurface(dojo.byId('drawing_surface'),'640px', '550px');
// draw rectangle on the surface
// createRect({}) parameters
// x: horizontal offset from left side of surface
// y: vertical offset from top of surface
// height, width: dimensions of rectangle
rectangle = surface.createRect({ x: 20, y: 5, height: 100, width: 150})
.setFill([0,0,255,0.3])
.setStroke({ color: "blue", width: 2})
;
var symbol = surface.createGroup();
square = surface.createRect({ x: 20, y: 10, height: 100, width: 100})
.setFill([0,0,255,0.3])
.setStroke({ color: "blue", width: 2})
;
// define limits to motion - in this case, the entire drawing surface.
var limits = { xmin: 0, xmax: 640, ymin: 0, ymax: 550};
// Extend mover with a class that overwites onMouseMove.
// We will call the class net.aa3sd.surfaceMover to prevent naming
// conflicts with any existing dojo classes
dojo.declare("net.aa3sd.surfaceMover",dojox.gfx.Mover, {
// Mover has several methods, we only need to overwrite onMouseMove.
onMouseMove: function(event) {
// getTransform() will tell us how far the shape has been moved
// from its initial position
transform = this.shape.getTransform();
// If this.shape hasn't been transformed yet
// getTransform() will return null, so we
// need to handle the special case of the first mouse movement.
if (transform==null) {
// We aren't dealing with rotations here, so we will
// just define initial values for the translations dx and dy.
transform = { dx: 0, dy: 0 };
}
// event.layerX is the mouse position in the coordinate system of the
// layer that was clicked on, the rectangle in this case
var layerx = event.layerX;
var layery = event.layerY;
if (this.click_on_rectangle==null) {
// we need to find where in the rectangle the mouse pointer is
// otherwise the rectangle will drag untill the mouse pointer
// hits the limits, rather than when the rectangel hits
// the limits.
this.click_on_rectangle = {
x: layerx - this.shape.shape.x - transform.dx,
y: layery - this.shape.shape.y - transform.dy
} // x,y of initial mouse click in coordinate system of rectangle
}
var click_on_surface = {
x: layerx - this.click_on_rectangle.x,
y: layery - this.click_on_rectangle.y
}; // x,y of mouse click in coordinate system of drawing surface
var x = event.clientX;
var y = event.clientY;
// check to see if the edges of the rectangle are within the
// limits of movement, if so, allow the rectangle to be moved
if (click_on_surface.x > limits.xmin
& click_on_surface.y > limits.ymin
& click_on_surface.x < limits.xmax - square.shape.width
& click_on_surface.y < limits.ymax - square.shape.height
) {
// move the rectangle by applying a translation
this.shape.applyLeftTransform({
dx: x - this.lastX,
dy: y - this.lastY
});
// store the last position of the rectangle
this.lastX = x;
this.lastY = y;
}
dojo.stopEvent(event);
}
});
// now we simply make the rectangle movable using the custom mover.
var recHandler = new dojox.gfx.Moveable(rectangle);
console.log(recHandler);
dojo.connect(recHandler, "onMove", function(mover) {
console.log("recHandler start moving with mover:", mover);
});
var sqHandler = new dojox.gfx.Moveable(square, { mover: net.aa3sd.surfaceMover })
console.log(sqHandler);
dojo.connect(sqHandler, "onMove", function(mover) {
console.log("sqHandler start moving with mover:", mover);
});
});

Related

Can I use an image (.svg) or svg path(?) in Konva.Group()'s clipFunc?

Suppose I have a map of the world.
And I'd like each continent to be an area where I could attach shapes to and drag/reshape them, while always being clipped by the continent's shape borders/limits.
Here's what I have so far:
const stage = new Konva.Stage({
container: 'stage',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
const group = new Konva.Group({
clipFunc: function (ctx) {
ctx.arc(250, 120, 50, 0, Math.PI * 2, false);
ctx.arc(150, 120, 60, 0, Math.PI * 2, false);
},
});
const shape = new Konva.Rect({
x: 150,
y: 70,
width: 100,
height: 50,
fill: "green",
stroke: "black",
strokeWidth: 4,
draggable: true,
});
group.add(shape);
layer.add(group);
stage.add(layer);
body {
margin: 0;
padding: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/konva/8.4.0/konva.min.js"></script>
<div id="stage"></div>
My question is, how could I use the clipFunc to draw a continent's limits? Could I use and image? svg path? I can't seem to find the answer in the docs.
[Edit: Added a new option 2, added demos for options 2 & 3 codepen + snippets.]
TLDR: Nothing totally automatic but two possible options.
Just to confirm - based on
And I'd like each continent to be an area where I could attach shapes
to and drag/reshape them, while always being clipped by the
continent's shape borders/limits.
I think you are asking how to limit the boundaries for dragging a shape to an 'arbitrary' region. I say arbitrary because it is a non-geometric region (not a square, circle, pentagon etc).
It would be fabulous to have a baked-in function to achieve this but sadly I am not aware that it is possible. Here's why:
Dragbounds limits: In terms of what you get 'out of the box', how Konva handles constraining drag position is via the node.dragBoundFunc(). Here is the example from the Konva docs which is straightforward.
// get drag bound function
var dragBoundFunc = node.dragBoundFunc();
// create vertical drag and drop
node.dragBoundFunc(function(pos){
// important pos - is absolute position of the node
// you should return absolute position too
return {
x: this.absolutePosition().x,
y: pos.y
};
});
The gist of this is that we are able to use the code in the dragBoundFunc function to decide if we like the position the shape is being dragged to, or not. If not we can override that 'next' position with our own.
Ok - so that is how dragging is constrained via dragBoundFunc. You can also use the node.on('dragmove') to achieve the same effect - the code would be very similar.
Hit testing
To decide in the dragBoundFunc whether to accept the proposed position of the shape being dragged, we need to carry out 'hit testing'.
[Aside: An important consideration is that, to make a pleasing UI, we should be hit testing at the boundary of the shape that is being dragged - not where the mouse pointer or finger are positioned. Example - think of a circle being dragged with the mouse pointer at its center - we want to show the user the 'hit' UI when the perimeter of the circle goes 'out of bounds' from the perspective of the dragBoundFunc, not when the center hits that point. What this means in effect is that our logic should check the perimeter of the shape for collision with the boundary - that might be simple or more difficult, depending on the shape.]
So we know we want to hit test our dragging shape against an arbitrary, enclosing boundary (the country border).
Option #1: Konva built-in method.
[Update] On developing the demo for this option I discovered that its mainstay, getIntersection(pt), is deliberately disabled (will always return null) when used in a dragmove situation. This is by design and done for performance because the overhead for the process is so costly.
What getIntersection does is to look at a given pixel, from topmost shape down, of the shapes that might overlap the given x, y point. It stops at the first hit. To do this is draws in an off-screen canvas each shape, checks the pixel, and repeats until no shapes remain. As you can tell, quite a costly process to run in-between mousemove steps.
The proposal for this option was to check a bunch of static border points on the stage via getIntersection - if the dragging shape came up as the hit then we would know the border was being crossed.
What point do we give it to check ? So here's the rub - you would have to predefine points on your map that were on the borders. How many points? Enough so that your draggable shapes can't stray very far over the mesh of border points without the hit-test firing. Do it correctly and this would be a very efficient method of hit testing. And it's not as if the borders will be changing regularly.
I made a simple point creator here. This is the view after I created the points around Wombania.
** Option #2: The concept is to create an off-screen canvas the same size as the client rect of the shape being dragged and create a clone of the drag shape therein. Space around the shape would be transparent, shape itself would be colored. We now use a set of pre-defined points along the country boundary, example above. We filter the points inside the shape's clientRect - so we only have a handful to test. We then 'translate' those points to the appropriate location in the off-screen hit canvas and check the color of the pixels at those points - any color whatsoever indicates the dragging shape is 'over' the point we are testing. Any hit means we can break out of the loop and report a boundary collision.
This is optimised in the following ways:
1 - we only make the offscreen canvas once at the dragstart.
2 - we only test the minimum number of boundary points - only those falling in the bounding box of the dragging shape.
Demo here at codepen. Snippet below - best consumed full screen.
const scale = 1,
stage = new Konva.Stage({
container: "container",
width: 500,
height: 400,
draggable: false
}),
layer = new Konva.Layer({
draggable: false
}),
imageShape = new Konva.Image({
x: 0,
y: 0,
draggable: false
}),
// Rect drawn to show client rect of dragging shape
theShapeRect = new Konva.Rect({
stroke: "silver",
strokeWidth: 1,
listening: false
}),
// small dots to show check points
pointCircle = new Konva.Circle({
radius: 30,
fill: "silver",
draggable: false
}),
// the three draggable shape defs - select by button
dragShapes = {
circle: new Konva.Circle({
radius: 30,
fill: "lime",
draggable: true,
visible: false
}),
rectangle: new Konva.Rect({
width: 60,
height: 60,
fill: "lime",
draggable: true,
visible: false
}),
star: new Konva.Star({
numPoints: 6,
innerRadius: 40,
outerRadius: 70,
fill: "lime",
draggable: true,
visible: false
})
},
// data for the check points.
data = `{"pt0":{"x":85.5,"y":44.5},"pt1":{"x":76,"y":62},"pt2":{"x":60,"y":78},"pt3":{"x":47,"y":94},"pt4":{"x":33,"y":115},"pt5":{"x":26,"y":133},"pt6":{"x":17,"y":149},"pt7":{"x":27,"y":171},"pt8":{"x":45,"y":186},"pt9":{"x":69,"y":187},"pt10":{"x":87,"y":191},"pt11":{"x":104,"y":194},"pt12":{"x":123,"y":214},"pt13":{"x":124,"y":238},"pt14":{"x":120,"y":260},"pt15":{"x":94,"y":265},"pt16":{"x":92,"y":275},"pt17":{"x":113,"y":281},"pt18":{"x":130,"y":280},"pt19":{"x":148,"y":280},"pt20":{"x":156,"y":261},"pt21":{"x":169,"y":248},"pt22":{"x":188,"y":251},"pt23":{"x":201,"y":263},"pt24":{"x":207,"y":274},"pt25":{"x":195,"y":281},"pt26":{"x":181,"y":285},"pt27":{"x":183,"y":291},"pt28":{"x":194,"y":293},"pt29":{"x":222,"y":293},"pt30":{"x":242,"y":284},"pt31":{"x":245,"y":257},"pt32":{"x":247,"y":238},"pt33":{"x":263,"y":236},"pt34":{"x":278,"y":240},"pt35":{"x":293,"y":239},"pt36":{"x":305,"y":238},"pt37":{"x":315,"y":237},"pt38":{"x":333,"y":236},"pt39":{"x":337,"y":248},"pt40":{"x":324,"y":258},"pt41":{"x":303,"y":263},"pt42":{"x":314,"y":267},"pt43":{"x":326,"y":273},"pt44":{"x":347,"y":273},"pt45":{"x":364,"y":273},"pt46":{"x":378,"y":260},"pt47":{"x":401,"y":263},"pt48":{"x":422,"y":272},"pt49":{"x":429,"y":278},"pt50":{"x":414,"y":281},"pt51":{"x":400,"y":287},"pt52":{"x":411,"y":294},"pt53":{"x":434,"y":292},"pt54":{"x":462,"y":287},"pt55":{"x":478,"y":275},"pt56":{"x":474,"y":259},"pt57":{"x":466,"y":233},"pt58":{"x":470,"y":208},"pt59":{"x":483,"y":189},"pt60":{"x":484,"y":169},"pt61":{"x":494,"y":153},"pt62":{"x":496,"y":129},"pt63":{"x":489,"y":106},"pt64":{"x":472,"y":91},"pt65":{"x":458,"y":78},"pt66":{"x":443,"y":65},"pt67":{"x":428,"y":54},"pt68":{"x":412,"y":41},"pt69":{"x":394,"y":31},"pt70":{"x":369,"y":23},"pt71":{"x":346,"y":22},"pt72":{"x":323,"y":22},"pt73":{"x":300,"y":23},"pt74":{"x":278,"y":24},"pt75":{"x":265,"y":26},"pt76":{"x":251,"y":30},"pt77":{"x":235,"y":32},"pt78":{"x":220,"y":38},"pt79":{"x":203,"y":44},"pt80":{"x":189,"y":53},"pt81":{"x":174,"y":57},"pt82":{"x":163,"y":51},"pt83":{"x":148,"y":53},"pt84":{"x":128,"y":52},"pt85":{"x":100,"y":51}}`,
// load the data into an object.
pointsList = JSON.parse(data);
// shape is set when the shape-type button is clicked.
let theShape = undefined;
// Add shapes to the layer and layer to stage
layer.add(
imageShape,
dragShapes.circle, // not visible at this point
dragShapes.rectangle, // not visible at this point
dragShapes.star, // not visible at this point
theShapeRect
);
stage.add(layer);
// Make the hit stage where we will do color sampling
const hitStage = new Konva.Stage({
container: "container2",
width: 300,
height: 300,
draggable: true
}),
hitLayer = new Konva.Layer(),
ctx = hitLayer.getCanvas().getContext(); // Get the convas context for access to pixel data
hitStage.add(hitLayer);
// Make an HTML image variable to act as the image loader, load the image
const img = new Image();
img.crossOrigin = "Anonymous";
img.onload = function () {
imageShape.image(img); // when loaded give the image to the Konva image shape
};
img.src = "https://assets.codepen.io/255591/map_of_wombania2.svg"; // start image loading - fires onload above.
// draw a small grey dot centered on each test point
for (const [key, pt] of Object.entries(pointsList)) {
layer.add(
pointCircle.clone({
name: key + " point",
radius: 5,
x: pt.x,
y: pt.y
})
);
}
// Function to get the color data for given point on a given canvas context
function getRGBAInfo(ctx, point) {
// get the image data for one pixel at the computed point
const pixel = ctx.getImageData(point.x, point.y, 1, 1);
const data = pixel.data;
// for fun, we show the rgba value at the pixel
const rgba =
"pt " +
JSON.stringify(point) +
` rgba(${data[0]}, ${data[1]}, ${data[2]}, ${data[3] / 255})`;
// console.log(rgba);
return data;
}
// function to reset collided point colors
function clearPoints() {
// clear the collision point colors
const points = stage.find(".point");
for (const point of points) {
point.fill("silver");
}
}
// variable to track whether we collided or not.
let hit = false;
// user clicks a shape-select button
$(".shapeButton").on("click", function () {
setShape($(this).data("shape"));
});
// Set the active shape.
function setShape(shapeName) {
clearPoints();
if (theShape) {
theShape.visible(false);
}
theShape = dragShapes[shapeName];
// Somewhere in Wombania....
theShape.position({
x: 300,
y: 120
});
// finally we see the shape !
theShape.visible(true);
// and set the bounding rect visualising rect
theShapeRect.position(theShape.getClientRect());
theShapeRect.size(theShape.getClientRect());
// better clear any listeners on the shape just in case
theShape.off();
// fires once as the drag commences
theShape.on("dragstart", function (evt) {
// clear the hitLayer for color testing
hitLayer.destroyChildren();
// make a copy of the dragging shape, positioned at top-left of hit canvas
// Note I fill shape with solid color - if you drag a Konva.Group then make a filled rect
// the pos & size of the group.getClientRect and add that into the group after cloning.
const clone = evt.target.clone({ fill: "red", stroke: "red" });
clone.position({
x: clone.width() / 2,
y: clone.height() / 2
});
hitLayer.add(clone);
// cloning copies some events so better clear them as they are not needed on the clone.
clone.off();
// reset the boundary point color
clearPoints();
// position the client rect visulaiser
theShapeRect.position(theShape.getClientRect());
theShapeRect.size(theShape.getClientRect());
});
// Will run on each drag move event
theShape.on("dragmove", function (evt) {
// assume no collisions - we will know by the end of the event
hit = false;
// position the client rect visulaiser
theShapeRect.position(theShape.getClientRect());
// Get the translation vector from the drag shape in the main canvas to the location
// in the hit canvas. We use thit to translate the check points in the main canvas
// to their positions in the hit canvas
const translateDist = {
x: -this.position().x + this.width() / 2,
y: -this.position().y + this.width() / 2
};
// get a rect around the current pos of the draggging shape, use to check if points
// are within this rect. If YES then process them, otherwise ignore.
const checkRect = this.getClientRect();
// Walk the set of check points...
for (const [key, pt] of Object.entries(pointsList)) {
// Is this point in the client rect of the dragging shape ?...
if (
checkRect.x < pt.x &&
checkRect.y < pt.y &&
checkRect.x + checkRect.width > pt.x &&
checkRect.y + checkRect.height > pt.y
) {
//...yes - so we pocess it
// translate the point to its position in the hit canvas.
let pointTranslated = {
x: pt.x + translateDist.x,
y: pt.y + translateDist.y
};
// get the color info of the point
const colorInfo = getRGBAInfo(ctx, pointTranslated);
// Is there any color there, anything, at all, maybe ?
if (colorInfo[0] + colorInfo[1] + colorInfo[2] + colorInfo[3] > 0) {
// if we find color then we have a collision!
hit = true;
// set the color of the collided point to visualise it
stage.findOne("." + key).fill("black");
// !Important: In live code we could 'break' here because it is not
// important to know _all_ the hits. I will process them all for demo purposes.
// break;
}
}
}
// Phew - after all that point fettling, if we got a hit then say so !
if (hit) {
$("#alarm").html("Boundary collision");
evt.target.fill("red");
} else {
evt.target.fill("lime");
$("#alarm").html("Still good");
}
});
}
body {
margin: 10px;
background-color: #f0f0f0;
}
.container {
border: 1px solid black;
display: inline-block;
}
alarm {
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/konva#8/konva.min.js"></script>
<p><span id='info'>Pick a shape, drag it around the country without hitting the edges!</span></p>
<p><span id='alarm'>.</span></p>
<p>
<button class="shapeButton" data-shape='circle'>Circle</button>
<button class="shapeButton" data-shape='rectangle'>Rectangle</button>
<button class="shapeButton" data-shape='star'>Star</button>
</span></p>
<div id="container" class='container'></div>
<div id="container2" class='container'></div>
Option #3: Alpha value checking.
The gist of this method is to have the color fill of each country have a specific alpha value in its RGBA setting. You can then check the colors at specific points on the perimeter of your dragging shape. Lets say we set the alpha for France to 250,
the Channel is 249, Spain 248, Italy 247, etc. If you are dragging your shape around 'inside' France, you expect an alpha value of 250. If you see anything else under any of those perimeter points then some part of your shape has crossed the border. [In practice, the HTML canvas will add some antialiasing along the border line so you will see some values outside those that you set but these have a low impact affect and can be ignored.]
One point is that you can't test the color on the main canvas if the shape being dragged is visible - because you will be getting the fill, stroke, or antialised pixel color of the shape!
To solve this you need a second stage - this can be memory only, so not visible on the page - where you load either a copy of the main stage with the dragging shape invisible, or you load the image of the map only. Let's call this the hit-stage. Assuming you keep the position of the hit-stage in line with the main-stage, then everything will work. Based on the location of the dragging shape and its perimeter points, you check the pixel colors on the hit-canvas. If the values match the country you are expecting then no hit, but if you see a different alpha value then you hit or passed the border. Actually you don't even need to know the color for the starting country - just note the color under the mouse point when the drag commences and look out for a different alpha value under the perimeter points.
There's a working demo of the 2-stage approach at codePen here. The demo just uses a country boundary and 'elsewhere' but you would use the same technique to construct an atlas of countries with different alpha values for your needs.
This is the JavaScript from the codepen demo. Best seen in full screen though when I checked it after copying from codepen some of the detections on right hand side did not fire, so maybe view the codepen if you can.
const countries = [
{ name: "wombania", alpha: 252 },
// add more countries as required
{ name: "Elsewhere", alpha: 0 }
],
scale = 1,
stage = new Konva.Stage({
container: "container",
width: 500,
height: 400,
draggable: false,
scale: {
x: scale,
y: scale
}
}),
layer = new Konva.Layer({
draggable: false
}),
imageShape = new Konva.Image({
x: 0,
y: 0,
draggable: false
}),
circle = new Konva.Circle({
radius: 30,
fill: "lime",
draggable: true,
x: 300,
y: 120,
scale: {
x: scale,
y: scale
}
});
let currentCountry = undefined;
const hitStage = new Konva.Stage({
container: "container2",
width: 500,
height: 400,
draggable: false
}),
hitLayer = new Konva.Layer(),
hitImage = new Konva.Image(),
ctx = hitLayer.getCanvas().getContext(); // Get the convas context for access to pixel data
layer.add(imageShape, circle);
stage.add(layer);
hitLayer.add(hitImage);
hitStage.add(hitLayer);
// Make an HTML image variable to act as the image loader, load the image
const img = new Image();
img.crossOrigin = "Anonymous";
img.onload = function () {
imageShape.image(img); // when loaded give the image to the Konva image shape
hitImage.image(img); // and to the hit canvas
const hitImageObj = new Image();
};
img.src = "https://assets.codepen.io/255591/map_of_wombania2.svg"; // start image loading - fires onload above.
// Will run on each drag move event
circle.on("dragmove", function () {
// get 20 points on the perimeter to check.
let hitCountry = currentCountry;
for (let angle = 0; angle < 360; angle = angle + 18) {
const angleRadians = (angle * Math.PI) / 180;
let point = {
x: parseInt(
circle.position().x + Math.cos(angleRadians) * circle.radius(),
10
),
y: parseInt(
circle.position().y + Math.sin(angleRadians) * circle.radius(),
10
)
};
// get the image data for one pixel at the computed point
const pixel = ctx.getImageData(point.x, point.y, 1, 1);
const data = pixel.data;
// for fun, we show the rgba value at the pixel
const rgba = `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${data[3] / 255})`;
// console.log("color at (" + point.x + ", " + point.y + "):", rgba);
// Here comes the good part.
// We know the alpha value for the current country - any other value means
// we crossed the border!
let country = getCountryAtPoint(point);
if (country && country.name !== currentCountry.name) {
hitCountry = country;
break; // jump out of the loop now because we know we got a hit.
}
}
// After checking the points what did the hit indicator show ?
if (hitCountry.alpha !== currentCountry.alpha) {
circle.fill("magenta");
$("#alarm").html("You crossed the border into " + hitCountry.name);
} else {
circle.fill("lime");
$("#alarm").html("Still inside " + hitCountry.name);
}
});
function getRGBAInfo(ctx, point) {
// get the image data for one pixel at the computed point
const pixel = ctx.getImageData(point.x, point.y, 1, 1);
const data = pixel.data;
// for fun, we show the rgba value at the pixel
const rgba = `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${data[3] / 255})`;
return data;
}
imageShape.on("mousemove", function () {
const point = stage.getPointerPosition();
getRGBAInfo(ctx, point);
});
function getCountryAtPoint(point) {
const colorInfo = getRGBAInfo(ctx, point);
for (const country of countries) {
if (country.alpha === colorInfo[3]) {
$("#info2").html("Selected: " + country.name);
return country;
}
}
}
imageShape.on("mousedown", function () {
currentCountry = getCountryAtPoint(stage.getPointerPosition());
});
circle.on("mousedown", function () {
currentCountry = getCountryAtPoint(stage.getPointerPosition());
});
body {
margin: 10px;
background-color: #f0f0f0;
}
.container {
border: 1px solid black;
display: inline-block;
}
alarm {
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/konva#8/konva.min.js"></script>
<p><span id='info'>Drag the circle around the country without hitting the edges!</span></p>
<p><span id='info2'>Selected: none</span> <span id='alarm'></span></p>
<div id="container" class='container'></div>
<div id="container2" class='container'></div>
PS. As a bonus, knowing the alpha values of the countries gives you an instant way to know which country the user clicks on. See the mousedown event.
To use an image, you can use the drawImage method of the canvas context in the clipFunc
const image = new Image();
image.src = 'image.png';
const group = new Konva.Group({
clipFunc: function (ctx) {
ctx.drawImage(image, 0, 0);
},
});
To use an SVG path, you can use the clip method of the canvas context in the clipFunc
const group = new Konva.Group({
clipFunc: function (ctx) {
ctx.clip('M10,10 h80 v80 h-80 Z');
},
});

How can I create a tooltip on mouse over in p5JS?

I have started playing with P5JS, but I wanted to create a tooltip on mouse over for each of the data points when the pointer is over it.
How can I do this? I have seen some examples using the map function, but unsure how this would work here.
Appreciate any help! I am not a JS dev just yet!
Generally speaking displaying tooltips that are specific to graphics displayed on a canvas involves "hit testing," which is to say: checking if the mouse is hovering over the specific graphics, and then some mechanism for displaying the tooltip.
Since your graphics are all circles, hit testing is quite simple. For each circle that you draw, check the distance to the mouse position. If the distance is less than the radius of the circle than the mouse is hovering over that circle:
let mouseDist = dist(posX, posY, mouseX, mouseY);
if (mouseDist < earthquakeMag * 5) {
// display tooltip, or set a flag to display the
// tooltip after the rest of the graphics have been
// displayed
}
As for displaying the tooltip you have two options: 1) you can rely on the native behavior of browser elements by setting the title attribute of the canvas element, or 2) you can display your own tooltip. The advantage of the former is that it is very trivial, but the advantage of the latter is that you have more control over where/when/how the tooltip is rendered.
Option 1:
let c;
function setup() {
c = createCanvas(500,500);
// ...
}
function draw() {
// ...
for (var i = 0; i < this.data.getRowCount(); i++) {
// ...
let mouseDist = dist(posX, posY, mouseX, mouseY);
if (mouseDist < earthquakeMag * 5) {
c.elt.title = 'hit!';
}
}
}
Option 2:
let tooltipText;
for (var i = 0; i < this.data.getRowCount(); i++) {
// ...
let mouseDist = dist(posX, posY, mouseX, mouseY);
if (mouseDist < earthquakeMag * 5) {
// If we displayed the tooltip at this point then
// some of the circles would overlap it.
tooltipText = date_[i];
}
// ...
}
if (tooltipText) {
// measure the width of the tooltip
let w = textWidth(tooltipText);
// save the current fill/stroke/textAlign state
push();
// draw a lightgray rectangle with a dimgray border
fill('lightgray');
stroke('dimgray');
strokeWeight(1);
// draw this rectangle slightly below and to the
// right of the mouse
rect(mouseX + 10, mouseY + 10, w + 20, 24, 4);
textAlign(LEFT, TOP);
noStroke();
fill('black');
text(tooltipText, mouseX + 20, mouseY + 16);
// restore the previous fill/stroke/textAlign state
pop();
}

Get the real mouse location on a canvas with double context

I have been asked to get the mouse coordinates in a game made in html5 with canvas.
As a first test, try reading the mouse position with the function below. But this function only reads the mouse position taking into account the dimensions of the canvas.
What happens is that the game has a larger stage than the canvas and this function does not show me the real location of the character on the stage.
I was doing a search and noticed that "behind" the canvas exists on a map (.png) with pixel dimensions already established. The canvas works like the camera to see a portion of the map.
Will it be possible to adapt my function to read the dimensions of the map and then locate the actual coordinates of the player?
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext("2d");
canvas.addEventListener("click", function(e) {
var cRect = canvas.getBoundingClientRect();
var scaleX = canvas.width / cRect.width;
var scaleY = canvas.height / cRect.height;
var canvasX = Math.round((e.clientX - cRect.left) * scaleX);
var canvasY = Math.round((e.clientY - cRect.top) * scaleY);
console.log("X: "+canvasX+", Y: "+canvasY);
});
This function will only give me the position of the mouse based on the size of the canvas but the map is larger, I leave here an explanatory image.
I hope you have understood me. Thanks in advance.
World <=> View
To establish the vernacular, the terms used are
World: the coordinate system (in pixels) of world / playfield / (red box).
View: The coordinate system (in canvas pixels) of canvas / camera / (blue box).
As pointed out in the comments. You need the view origin. That is the coordinates that the top left of the canvas in world space.
You also need to know the view scale. That is the size of the canvas in relationship to the world.
Required information
const world = {width: 2048, height: 1024}; // Red box in pixels
const view = { // blue box
origin: {x: 500, y: 20}, // in world scale (pixels on world)
scale: {width: 1, height: 1}, // scale of pixels (from view to world)
}
Without this information you can not do the conversion. It must exist as it is required to render world content to the canvas.
Note that if the scales are 1 they may only be inferred in the canvas rendering system. If you can not find a scale then use 1.
Note This answer assumes there is no rotation of the view.
View => World
The following function will convert from view coordinates to world coordinates.
function viewToWorld(x, y) { // x,y pixel coordinates on canvas
return {
x: x * view.scale.width + view.origin.x,
y: y * view.scale.height + view.origin.y
}; // return x,y pixel coordinates in world
}
To use in a mouse event where the client is the canvas
function mouseEvent(event) {
// get world (red box) coords
const worldCoord = viewToWorld(event.clientX, event.clientY);
// normalize
worldCoord.x /= world.width;
worldCoord.y /= world.height;
}
World => View
You can reverse the conversion. That is move from world coordinates to view coordinates with the following functions.
function normalWorldToView(x, y) { // x,y normalized world coordinates
return {
x: (x * world.width - view.origin.x) / view.scale.width,
y: (y * world.height - view.origin.y) / view.scale.height
}; // return x,y pixel on canvas (view)
}
and in pixels
function worldToView(x, y) { // x,y world coordinates in pixels
return {
x: (x - view.origin.x) / view.scale.width,
y: (y - view.origin.y) / view.scale.height
}; // return x,y pixel on canvas (view)
}

Dragging a rotated element within a certain boundary

In the following fiddle, you can click and drag around the image, and it will not be able to exit the blue border. By clicking the red and green rectangles, you can rotate the image. However when you click and drag a rotated object, the image does not follow the mouse. I would like the image to follow the mouse even if it is rotated.
http://jsfiddle.net/n3Sn5/
I think the issue occurs within my move function
move = function (dx, dy)
{
nowX = Math.min(boundary.attr("x")+boundary.attr("width")-this.attr("width"), this.ox + dx);
nowY = Math.min(boundary.attr("y")+boundary.attr("height")-this.attr("height"), this.oy + dy);
nowX = Math.max(boundary.attr("x"), nowX);
nowY = Math.max(boundary.attr("y"), nowY);
this.attr({x: nowX, y: nowY });
}
One thing to notice is that when you click and drag a rotated object, after you release your mouse click, if you rotate the image, it snaps to where your mouse was when you released the mouse click, even obeying the boundary.
I was able to get the rotated image to drag with the mouse previously, but by adding the boundary rectangle, i had to use a more complex approach.
If anyone has an idea of what I need to change, I would be very grateful!
Thanks!
The required output can be achieved in a bit different way. Please check the fiddle at http://jsfiddle.net/6BbRL/. I have trimmed to code to keep the basic parts for demo.
var paper = Raphael(0, 0, 475, 475),
boxX = 100,
boxY = 100,
boxWidth = 300,
boxHeight = 200,
// EDITED
imgWidth = 50,
imgHeight = 50,
box = paper.rect(boxX, boxY, boxWidth, boxHeight).attr({fill:"#ffffff"}),
// EDITED
html5 = paper.image("http://www.w3.org/html/logo/downloads/HTML5_Badge_512.png",boxX+boxWidth-imgWidth,boxY+boxHeight-imgHeight,imgWidth,imgHeight)
.attr({cursor: "move"}),
elementCounterClockwise = paper.rect(180, 0, 50, 50).attr({fill:"#ff5555", cursor:"pointer"}),
elementClockwise = paper.rect(250, 0, 50, 50).attr({ fill: "#55ff55", cursor: "pointer" }),
boundary = paper.rect(50,50,400,300).attr({stroke: '#3333FF'}),
transform,
// EDITED
xBound = {min: 50 + imgWidth/2, max: 450 - imgWidth/2},
yBound = {min: 50 + imgHeight/2, max: 350 - imgHeight/2};
start = function (x, y) {
// Find min and max values of dx and dy for "html5" element and store them for validating dx and dy in move()
// This is required to impose a rectagular bound on drag movement of "html5" element.
transform = html5.transform();
}
move = function (dx, dy, x, y) {
// To restrict movement of the dragged element, Validate dx and dy before applying below.
// Here, dx and dy are shifts along x and y axes, with respect to drag start position.
// EDITED
var deltaX = x > xBound.max && xBound.max - x || x < xBound.min && xBound.min - x || 0;
deltaY = y > yBound.max && yBound.max - y || y < yBound.min && yBound.min - y || 0;
this.attr({transform: transform + 'T'+ [dx + deltaX, dy + deltaY]});
}
up = function () {
};
html5.drag(move, start, up);
elementClockwise.click(function() {
html5.animate({transform: '...r90'}, 100);
})
elementCounterClockwise.click(function() {
html5.animate({transform: '...r-90'}, 100);
})
Use of '...' to append a transformation to the pre-existing transformation state (Raphael API) is important for the rotational issue. While, for translating the element on drag requires absolute translation, which neglects the rotational state of the element while translating the element.
//EDIT NOTE
Drag bounding is worked on and updated. However, there remains an issue with incorporating the difference between mouse position and image center.
I can help you with your rotation and drag problem, you need to store the rotation and apply it after you have moved the object.
elementClockwise.node.onclick = function()
{
html5.animate({'transform': html5.transform() +'r90'}, 100, onAnimComplete);
}
elementCounterClockwise.node.onclick = function()
{
html5.animate({'transform': html5.transform() +'r-90'}, 100, onAnimComplete);
}
function onAnimComplete(){
default_transform = html5.transform();
}
At present I can't get the boundary to work, but will have a try later.
http://jsfiddle.net/n3Sn5/2/

Rectangle selection of SVG elements (for Raphael)

I bumped into the following problem, hope someone will know how to help me:
I work with the JavaScript library Raphael. Now, what I want to do is, when I have many Raphael SVG elements, to simply select more elements with "rectangle selection", i.e. by dragging the mouse starting from the graph's background to create a selection rectangle (I hope I was clear enough), and move the elements which are in this rectangle.
For now, I've found something like this (someone posted it from a previous question of mine):
var paper = Raphael(0, 0, '100%', '100%');
var circle = paper.circle(75, 75, 50);
var rect = paper.rect(150, 150, 50, 50);
var set = paper.set();
set.push(circle, rect);
set.attr({
fill: 'red',
stroke: 0
});
var ox = 0;
var oy = 0;
var dragging = false;
set.mousedown(function(event) {
ox = event.screenX;
oy = event.screenY;
set.attr({
opacity: .5
});
dragging = true;
});
set.mousemove(function(event) {
if (dragging) {
set.translate(event.screenX - ox, event.screenY - oy);
ox = event.screenX;
oy = event.screenY;
}
});
set.mouseup(function(event) {
dragging = false;
set.attr({
opacity: 1
});
});
This code can be executed on jsfiddle. But, as you can see, this selects ALL the elements, by simply adding them to a Raphael set.
Now, I think that my problem will be solved by:
Making a rectangle selection
Adding the nodes which are in the rectangle to a Raphael set
Move only the selected items (i.e. move only the items which are in the Raphael set with set.mousemove)
My problem for now would be the first two issues.
Any ideas how to do this?
Thank you in advance!
Fun problem. You can do this by placing a rectangular "mat" the size of the canvas behind all of your other objects and attaching a drag event to it for selecting other elements. (Note this solution uses the newer version of Raphael, 2.1.0:
var paper = Raphael(0, 0, '100%', '100%');
//make an object in the background on which to attach drag events
var mat = paper.rect(0, 0, paper.width, paper.height).attr("fill", "#FFF");
var circle = paper.circle(75, 75, 50);
var rect = paper.rect(150, 150, 50, 50);
var set = paper.set();
set.push(circle, rect);
set.attr({
fill: 'red',
stroke: 0
});
//the box we're going to draw to track the selection
var box;
//set that will receive the selected items
var selections = paper.set();
Now, we add a drag event -- similar to the mouseover events but with three functions (see documentation), and draw a box to track the selection space:
//DRAG FUNCTIONS
//when mouse goes down over background, start drawing selection box
function dragstart (x, y, event) {
box = paper.rect(x, y, 0, 0).attr("stroke", "#9999FF");
}
// When mouse moves during drag, adjust box.
// If the drag is to the left or above original point,
// you have to translate the whole box and invert the dx
// or dy values since .rect() doesn't take negative width or height
function dragmove (dx, dy, x, y, event) {
var xoffset = 0,
yoffset = 0;
if (dx < 0) {
xoffset = dx;
dx = -1 * dx;
}
if (dy < 0) {
yoffset = dy;
dy = -1 * dy;
}
box.transform("T" + xoffset + "," + yoffset);
box.attr("width", dx);
box.attr("height", dy);
}
function dragend (event) {
//get the bounds of the selections
var bounds = box.getBBox();
box.remove();
reset();
console.log(bounds);
for (var c in set.items) {
// Here, we want to get the x,y vales of each object
// regardless of what sort of shape it is.
// But rect uses rx and ry, circle uses cx and cy, etc
// So we'll see if the bounding boxes intercept instead
var mybounds = set[c].getBBox();
//do bounding boxes overlap?
//is one of this object's x extremes between the selection's xe xtremes?
if (mybounds.x >= bounds.x && mybounds.x <= bounds.x2 || mybounds.x2 >= bounds.x && mybounds.x2 <= bounds.x2) {
//same for y
if (mybounds.y >= bounds.y && mybounds.y <= bounds.y2 || mybounds.y2 >= bounds.y && mybounds.y2 <= bounds.y2) {
selections.push(set[c]);
}
}
selections.attr("opacity", 0.5);
}
}
function reset () {
//empty selections and reset opacity;
selections = paper.set();
set.attr("opacity", 1);
}
mat.drag(dragmove, dragstart, dragend);
mat.click(function(e) {
reset();
});
Just like that, you have a new set (selections) that contains every object that was selected by the mouse drag. You can then apply your mouseover events from the original to that set.
Note that this will select circle objects if you nick the corner of their bounding box with your selection box, even if it doesn't overlap with the area of the circle. You could make a special case for circles if this is a problem.
jsFiddle

Categories

Resources