I am using kineticjs and want to zoom into a layer when I click on it. The zooming should centered around the point of the click.
My code works but only if I keep the click point for every subsequent click at the same spot.
When I change the spot the layer translates in some direction.
image_part.on('click', function(evt){
zoom += 0.1;
var offset = layer.getOffset();
layer.setOffset(evt.pageX, evt.pageY);
layer.setScale(zoom);
layer.setX(evt.pageX);
layer.setY(evt.pageY);
layer.draw();
});
Does anyone know a solution that works?
I do it like this. I'm zooming the whole layer but it works for any image or shape. Just let setPosition do all the work and adjust for the new scale factor. Note1: if the canvas isn't in the top corner of the page, you also need to get the canvas position and remove it from the pageX,Y position. This is what the function getPos() does. I pulled it straight from another stackoverflow topic. Note2: Use the zP point to control where the zooming takes place (i.e. at the mouse click position, or center than zoom, etc.)
The kineticJS part...
layer.on("dblclick",function(ev){
var d=document.getElementById('photoCnvs');
var cnvsPos=getPos(d);
var R={ //(canvas space)
x: ev.pageX,
y: ev.pageY
};
var off0=this.getPosition();
var scl0=this.getScale().x;
var w=stageM.getWidth();
var h=stageM.getHeight();
//desired zoom point (e.g. mouse position, canvas center)
var zP={
//use these first two lines to center the image on the clicked point while zooming
//x: w/2,
//y: h/2
//use these next two lines to zoom the image around the clicked point
x: R.x-cnvsPos.x,
y: R.y-cnvsPos.y
}
//actual pixel value clicked (image space)
var xA={
x:(R.x-off0.x-cnvsPos.x)/scl0,
y:(R.y-off0.y-cnvsPos.y)/scl0
}
//rescale image
var sclf=scl0*1.10;
this.setScale(sclf);
//move clicked pixel to the desired zoom point
var newR={
x: zP.x-sclf*xA.x,
y: zP.y-sclf*xA.y
}
this.setPosition(newR.x, newR.y)
this.draw();
})
Then the canvas position part
function getPos(el) {
for (var lx=0, ly=0;
el != null;
lx += el.offsetLeft, ly += el.offsetTop, el = el.offsetParent);
return {x: lx,y: ly};
}
Related
I have this code below to add a rectangle to a canvas, I have some questions regarding this.
Is it possible to move the added rectangle after it has been created?
Is it possible to delete a rectangle that has been added?
Can I retrieve all added rectangles positions (x, y, width and height) after I have added a bunch of rectangles and lets say by click of a button?
<script>
function rect()
{
var canvas = document.getElementById('drawing'),
ctx = canvas.getContext('2d'),
rect = {},
drag = false;
function init() {
canvas.addEventListener('mousedown', mouseDown, false);
canvas.addEventListener('mouseup', mouseUp, false);
canvas.addEventListener('mousemove', mouseMove, false);
}
function mouseDown(e) {
rect.startX = e.pageX - this.offsetLeft;
rect.startY = e.pageY - this.offsetTop;
drag = true;
}
function mouseUp() {
drag = false;
}
function mouseMove(e) {
if (drag) {
rect.w = (e.pageX - this.offsetLeft) - rect.startX;
rect.h = (e.pageY - this.offsetTop) - rect.startY ;
//ctx.clearRect(0,0,canvas.width,canvas.height);
draw();
}
}
function draw() {
ctx.globalAlpha=0.5; // Half opacity
ctx.fillStyle= "#b0c2f7";
//ctx.fillStyle = "rgba(255, 255, 255, 0.05)";
ctx.fillRect(rect.startX, rect.startY, rect.w, rect.h);
}
init();
}
</script>
<div id="canvasDiv">
<canvas id="drawing" width="580px" height="788px" style="border:2px solid #000; background: #FFF;"></canvas>
</div>
<script>
var canvas = document.getElementById('drawing');
var context = canvas.getContext('2d');
var imageObj = new Image();
imageObj.onload = function() {
context.drawImage(imageObj, 69, 50);
};
imageObj.src = 'http://localhost/test/Images/DSC0273446.jpg';
</script>
<div id="rect">
<p><button onclick="rect();">Rectangle</button></p>
</div>
Really appreciate all help I can get in this matter, thanks!
Is it possible to move the added rectangle after it has been created?
No, once drawn it is just pixels, there is no 'rectangle object' in the canvas. The usual approach to 'moving' a shape in canvas is to clearRect() (or the whole canvas) and then fillRect() in a slightly different position in a requestAnimationFrame controlled loop.
Is it possible to delete a rectangle that has been added?
As long as you've stored the location where you drew it, you can clearRect(). Note that this clears an area of pixels, not an object - the results of previous drawing operations will not automatically reappear.
Can I retrieve all added rectangles positions (x, y, width and height) after I have added a bunch of rectangles and lets say by click of a button?
No. The canvas does not store drawn objects, only pixel image data. If you want to keep track of the objects that have been drawn then you have to do that yourself. If you want to manipulate shapes instead of pixels then there are libraries like fabric.js which add an object manipulation layer on top of canvas, or you can use an svg element instead which will let you create graphics with normal DOM methods.
Can any one help in suggesting a solution for the following:
i have a large image, consider it as a map, i want to put this image in a viewer that is smaller than the image and i have to be able to scroll the image by clicking and dragging it.
and i want to put in this image a clickable spots in a specified x and y coordinated, and be able to click the spots.
when clicking any spot in the image, the image will be changed with a new spots.. and so on..
can you help in suggesting what is the best object to load the image in and be able to do all the mentioned points.
Thanks in advance.
This is easier than you think. You have a few goals to consider:
"i want to put this image in a viewer that is smaller than the image": You dont need anything special to do this. The concept of this is simply that you have a mask overlay where you want the large image visible.
var viewer:Sprite = new Sprite; //200x200
var imageMask:Sprite = new Sprite; //200x200
var imageContainer:Sprite = new Sprite; //400x500
imageContainer.mask = imageMask;
viewer.addChild(imageContainer);
//this will allow you to visibly see only 200x200 of the
//imageContainer at any time
"i have to be able to scroll the image by clicking and dragging it": This is a little more logic as the imageContainer will have to move in the -(negative) direction of the mouse. Add some listeners to check for mouse actions, and drag as required.
var allowDrag:Boolean = false;
imageContainer.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown);
imageContainer.addEventListener(MouseEvent.MOUSE_UP, onMouseUp);
imageContainer.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);
function onMouseDown(e:Event):void{
allowDrag = true;
}
function onMouseUp(e:Event):void{
allowDrag = false;
}
function onMouseMove(e:Event):void{
//return if not dragging
if(!allowDrag) return;
//move the imageContainer in a -(negative) direction of the mouse
//use an index relative to the size of the viewer and imageContainer
var speed:Number = 0.5;
imageContainer.x -= (viewer.width/imageContainer.width)*speed;
imageContainer.y -= (viewer.height/imageContainer.height)*speed;
//clean the positions so the image remains within the viewer
if(imageContainer.x > 0) imageContainer.x = 0;
if(imageContainer.x < -viewer.width) imageContainer.x = -viewer.width;
if(imageContainer.y > 0) imageContainer.y = 0;
if(imageContainer.y < -viewer.height) imageContainer.y = -viewer.height;
}
"i want to put in this image a clickable spots in a specified x and y coordinated, and be able to click the spots": This also requires a little more thinking. In this case what you want to do is create [hotspots] on the image that are clickable, when clicked = do actions.
//USAGE
//define the click area coords
var clickCoords:Rectangle = new Rectangle();
clickCoords.x = 10; //starts at x 10
clickCoords.y = 10; //starts at y 10
clickCoords.width = 100; //100 wide
clickCoords.height = 100; //100 tall
//add the click listener
var clickArea:Sprite = hotSpot(imageContainer,clickCoords);
clickArea.addEventListener(MouseEvent.CLICK, onHotSoptClick);
//hot spot factory
function hotSpot(target:Sprite,coords:Rectangle):Sprite{
//create the hotspot
var hs:Sprite = new Sprite;
hs.graphics.beginFill(0,0);
hs.graphics.drawRect(0,0,coords.width,coords.height);
hs.graphics.endFill();
//add the hotspot to the target
hs.x = coords.x;
hs.y = coords.y;
target.addChild(hs);
}
function onHotSoptClick(e:MouseEvent):void{
//do something
}
IMPORTANT:
You may want to keep a list of hot spots you create so you can do garbage cleanup, and you plan on dynamically generating hotspots per image... then YOU MUST keep an active list of hot spots and remove when not in use.
You can catch the events MouseDown, MouseUp, MouseMove, MouseOut, on your viewing window, this way you can control exactly what do you want to do.
Here is the pseudo-code:
reset()
{
isDown=false;
downPointX=0;
downPointY=0;
distanceX=0;
distanceY=0;
}
onMouseDown()
{
isDown=true;
downPointX=mouseX;
downPointY=mouseY;
}
onMouseUp()
{
if(distanceX+distanceY==0 and isDown)
click(downPointX,downPointY);
reset();
}
onMouseMove()
{
if isDown then
distanceX=mouseX-downPointX;
distanceY=mouseY-downPointY;
drag(distanceX,distanceY);
endif;
}
onMouseOut()
{
reset();
}
drag(distanceX,distanceY)
{
change your map coordinates
}
click(downPointX,downPointY)
{
if(inSpot(downPointX,downPointY)==true)
changeMap();
endif;
}
changeMap()
{
change your maps and spots
}
avoid implementing any event for your spots sprites or you can get unexpected results.
You can check these for more information
http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/display/Sprite.html#eventSummary
I mean, I'd like to center a map to a fixed position :
map.setCenter(markerCliccato.getPosition());
and less (Y-axis) 100px down. Is it possible? Or I need the Lat/Lng of this "virtual" point?
This is because I have a InfoWindow very height, and I need to put it 100px from top of the map! Can't use "margin" on infowindow because can make troubles with other marker in the map.
Example
I have this point as Lat/Lng : https://maps.google.com/?q=54.572062,-3.038818
But I want to center the map at https://maps.google.com/?q=54.486793,-3.042046 (which is more or less 100px down of my point, in the map)
ok, i need to post pictures :)
this is before the click
and this is after
there is no gray area, the map is all rendered.
also, to see the marker you must allow the site to track your location.
function offsetCenter(latlng, offsetx, offsety) {
var scale = Math.pow(2, map.getZoom());
var nw = new google.maps.LatLng(
map.getBounds().getNorthEast().lat(), map.getBounds().getSouthWest().lng());
var worldCoordinateCenter = map.getProjection().fromLatLngToPoint(latlng);
var pixelOffset = new google.maps.Point((offsetx / scale) || 0, (offsety / scale) || 0)
var worldCoordinateNewCenter = new google.maps.Point(
worldCoordinateCenter.x - pixelOffset.x, worldCoordinateCenter.y + pixelOffset.y);
var newCenter = map.getProjection().fromPointToLatLng(worldCoordinateNewCenter);
map.setCenter(newCenter);
}
see it working here: http://jsfiddle.net/RASG/vA4eQ/
tested with FF 15, IE 9 and Chrome 21
I have a dynamic tile set where I do NOT want to allow panning outside of its bounds.
The below code gets me close, but the user can still scroll horizontally outside of strict bounds because it uses the map center for comparison
var strictBounds = new google.maps.LatLngBounds(
new google.maps.LatLng(sw_lat, sw_lon),
new google.maps.LatLng(ne_lat, ne_lon)
);
google.maps.event.addListener(map, 'drag', function()
{
if (strictBounds.contains(map.getCenter())) return;
// We're out of bounds - Move the map back within the bounds
var c = map.getCenter(),
x = c.lng(),
y = c.lat(),
maxX = strictBounds.getNorthEast().lng(),
maxY = strictBounds.getNorthEast().lat(),
minX = strictBounds.getSouthWest().lng(),
minY = strictBounds.getSouthWest().lat();
if (x < minX) x = minX;
if (x > maxX) x = maxX;
if (y < minY) y = minY;
if (y > maxY) y = maxY;
map.setCenter(new google.maps.LatLng(y, x));
});
For quick access, here is the jsfiddle for this solution: http://jsfiddle.net/nYz6k/
Supposed you're happy with the current solution of detecting bounds against the center, all you have to do is to restrict the bounds based on the current map canvas size.
The result that you're getting now is that every corner of the restricting bounds appears on the center of the map when it's full restricting (i.e. both latitude and longitude exceed the bounds corners).
The solution is to actually remove the x and y offset from the bounds, and still be able to check against the center (which is the most efficient solution, compared to other bounds-based checking solutions).
Also, you have to restrict the bounds only when you initialize the map and when the window is resizing, which means that when you pan there is no extra processing overhead besides the check method that you have already provided.
Don't forget to set the maxZoom property for the map (tweak it as needed) because above a determined zoom level the restricting bounds will by themselves fit in the viewport and there's no solution for that.
Important! Use 'center_changed' instead of 'drag' because 'drag' has that sliding behavior that when you finished dragging and set the center, the maps still slides in the direction of the panning.
Before you implement the solution I recommend you to check out how the Maps API coordinate system and projection works, because this solution is heavily based on it, and if you want to tweak it out, it's useful to know this information.
https://developers.google.com/maps/documentation/javascript/maptypes and check out the Custom Map Types -> Map Coordinates section.
Here's what you need to do. First, you need to implement 2 short methods of converting between geographic coordinates (LatLng) and pixel coordinates.
var fromLatLngToPixel = function (latLng) {
var point = map.getProjection().fromLatLngToPoint(latLng);
var zoom = map.getZoom();
return new google.maps.Point(
Math.floor(point.x * Math.pow(2, zoom)),
Math.floor(point.y * Math.pow(2, zoom))
);
}
var fromPixelToLatLng = function (pixel) {
var zoom = map.getZoom();
var point = new google.maps.Point(
pixel.x / Math.pow(2, zoom),
pixel.y / Math.pow(2, zoom)
);
return map.getProjection().fromPointToLatLng(point);
}
Next, implement the method that effectively restricts the bounds. Note that you always have to keep a variable that stores the original bounds, because each time the map canvas is resized, the resulting bounds will change.
For this, let's say originalBounds keeps the bounds you provided with sw_lat, sw_long etc, and shrinkedBounds is modified by this method. You can rename it to strictBounds to still work in your method, but it's up to you. This uses jQuery for getting the width and height of the canvas object.
var shrinkBounds = function () {
zoom = map.getZoom();
// The x and y offset will always be half the current map canvas
// width and height respectively
xoffset = $('#map_canvas').width() / 2;
yoffset = $('#map_canvas').height() / 2;
// Convert the bounds extremities to global pixel coordinates
var pixswOriginal = fromLatLngToPixel(originalBounds.getSouthWest());
var pixneOriginal = fromLatLngToPixel(originalBounds.getNorthEast());
// Shrink the original bounds with the x and y offset
var pixswShrinked = new google.maps.Point(pixswOriginal.x + xoffset, pixswOriginal.y - yoffset);
var pixneShrinked = new google.maps.Point(pixneOriginal.x - xoffset, pixneOriginal.y + yoffset);
// Rebuild the shrinkedBounds object with the modified
shrinkedBounds = new google.maps.LatLngBounds(
fromPixelToLatLng(pixswShrinked),
fromPixelToLatLng(pixneShrinked));
}
Next, all you have to do is to call this method:
once when the map is initialized. Be aware that depending on when you call the method, you may be getting some weird errors because the map might not have yet initialized all it's properties. Best way is to use the projection_changed event.
google.maps.event.addListener(map, 'projection_changed', function (e) {
shrinkBounds();
});
every time the map canvas is resized
google.maps.event.addListener(map, 'resize', function (e) {
shrinkBounds();
});
But the most important part of this is that the 'resize' event is never triggered for programmatic resizes of the map container, so you have to trigger it manually every time you resize the canvas programmatically (if you do, but I doubt you do).
The most common way is to trigger it when the window is resized:
$(window).resize(function () {
google.maps.event.trigger(map, 'resize');
});
After all of this, you can now safely use your method, but as a handler for 'center_changed' and not for 'drag' as I've mentioned earlier.
I set up a jsfiddle with the full working code.
http://jsfiddle.net/nYz6k/
You can see the restricting at the bottom left corner with that little half-marker that shows up, which is positioned in the SW corner of the bounds. There is also one at the NW corner but naturally you can't see it because it's displayed above the position.
Instead of using map.getCenter() as a check, use map.getBounds()
Untested, but a simple way to accomplish this would be to check if the union of both bounds were identical (if not, your map bounds is outside your strict bounds):
var union = strictBounds.union(map.getBounds());
if (strictBounds.equals(union)) { //new map bounds is within strict bounds
Tiborg version above is the best version so far in keeping the bounds...
I noticed a bug that occurs when zoom level and width/height are larger than original bounds
I have modified his code and added a rectangle to see it in action.
var buildBounds = function(sw, ne) {
var swY = sw.lat();
var swX = sw.lng();
var neY = ne.lat();
var neX = ne.lng();
if (swY > neY) {
var cY = (swY + neY) / 2;
swY = cY;
neY = cY;
}
if (swX > neX) {
var cX = (swX + neX) / 2;
swX = cX;
neX = cX;
}
return new google.maps.LatLngBounds(
new google.maps.LatLng(swY, swX),
new google.maps.LatLng(neY, neX));
}
http://jsfiddle.net/p4wgjs6s/
I would like to resize the canvas to a smaller dimension than what it is. Here is my code:
var modelPane = document.getElementById('model_pane');
//create canvasclass="modelview"
var canvas_ = document.createElement('canvas');
canvas_.setAttribute('width', 1160);
canvas_.setAttribute('height', 500);
canvas_.setAttribute('id', 'canvas');
canvas_.setAttribute('class', 'modelview');
var ctx_ = canvas_.getContext('2d');
//draw
for (i=0;i<nodes.length;i++)
{
ctx_.strokeRect(nodes[i].x, nodes[i].y, nodes[i].width, nodes[i].height);
}
//scale
ctx_.scale(0.3,0.3);
modelPane.appendChild(canvas_);
Yet this doesn't work. Anyone know why? The canvas doesn't resize to the new dimensions.
You need to call scale first:
var ctx_ = canvas_.getContext('2d');
ctx_.scale(0.3,0.3);
//draw
for (i=0;i<nodes.length;i++)
ctx_.strokeRect(nodes[i].x, nodes[i].y, nodes[i].width, nodes[i].height);
}
Think about it, like you need to scale you canvas down. Then draw on the smaller canvas.
Example