What is the proper way how to fire event on start and finish of the complex drawing using KanvaJS? - konvajs

I am drawing a complex graphic on canvas using KonvaJS library. Execution takes few seconds and I want to monitor progress on user screen, but I can't make work functions like:
layer.on('draw', () => {
console.log('Draw finished')
})
What is the proper way how to fire event on start and finish of the drawing using KanvaJS?
Here is a test code where console.log('Draw finished') is not executed
var stage = new Konva.Stage({
container: 'canvas-container', // id of HTML div container
width: 1000,
height: 500,
});
function drawGrid(){
let layerMPE = new Konva.Layer();
let pxMax = stage.width();
let pyMax = stage.height();
let res = 10;
for (let px=0; px < pxMax; px += res){
for (let py=0; py < pyMax; py += res){
let rect = new Konva.Rect({
x: px,
y: py,
width: res,
height: res,
fill: 'red'
});
layerMPE.add(rect);
}
}
stage.add(layerMPE);
layerMPE.draw();
layerMPE.on('draw', () => {
console.log('Draw finished')
});
}
drawGrid();

Related

Konva converting stage toDataUrl() do not work in chrome browser,but work well in safari browser

I have Konva Stage with few layers, when I try convert to image all stage - result is OK in safari browser , when I try convert stage in chrome ,firefox etc. - result is failed. I think that toDataUrl() method does
not work well in chrome browser ,firefox browser etc except for safari browser
<button id="save" type="button">save</button>
<div id="container"></div>
<script>
var width = window.innerWidth;
var height = window.innerHeight;
function downloadURI(uri, name) {
var link = document.createElement('a');
link.download = name;
link.href = uri;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
delete link;
}
function drawImage(imageObj) {
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height
});
var layer = new Konva.Layer();
// darth vader
var darthVaderImg = new Konva.Image({
image: imageObj,
x: stage.getWidth() / 2 - 200 / 2,
y: stage.getHeight() / 2 - 137 / 2,
width: 200,
height: 137,
name: 'myimg',
draggable: true
});
// add cursor styling
darthVaderImg.on('mouseover', function () {
document.body.style.cursor = 'pointer';
});
darthVaderImg.on('mouseout', function () {
document.body.style.cursor = 'default';
});
layer.add(darthVaderImg);
stage.add(layer);
stage.on('click tap', function (e) {
// if click on empty area - remove all transformers
if (e.target === stage) {
stage.find('Transformer').destroy();
layer.draw();
return;
}
// do nothing if clicked NOT on our rectangles
if (!e.target.hasName('myimg')) {
return;
}
// remove old transformers
// TODO: we can skip it if current rect is already selected
stage.find('Transformer').destroy();
// create new transformer
var tr = new Konva.Transformer();
layer.add(tr);
tr.attachTo(e.target);
layer.draw();
})
document.getElementById('save').addEventListener(
'click',
function () {
var dataURL = stage.toDataURL({ pixelRatio: 3 });
downloadURI(dataURL, 'stage.png');
},
false
);
}
var imageObj = new Image();
imageObj.onload = function () {
drawImage(this);
};
imageObj.src = 'https://www.decanterchina.com/assets/images/article/550/136031_decanter-cava-tasting-1.jpg';
</script>
Any possible reasons or solutions ? Thanks!!
If you look into the console you will see a message:
Konva warning: Unable to get data URL. Failed to execute 'toDataURL'
on 'HTMLCanvasElement': Tainted canvases may not be exported.
You have CORS issue. Take a look here for solutions:
Tainted canvases may not be exported
https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image

Why is the zIndex sequence of my objects not what I expected?

How can I get a list of all objects with all params (x, y, width, etc.), including zIndex param on the stage after completing resizing? And how can I set an zIndex for each object when creating a stage?
I have this code, but setZIndex not working correctly. Images are not set correctly.
const oKonvaStage = new Konva.Stage({
container: 'dropzone'
});
const oKonvaLayer = new Konva.Layer();
oKonvaStage.add(oKonvaLayer);
const oKonvaImage1 = new Konva.Image({
x: 624,
y: 433,
width: 1920,
height: 1280
});
const oImage1 = new Image();
oImage1.onload = function() {
oKonvaImage1.image(oImage1);
oKonvaLayer.add(oKonvaImage1);
oKonvaImage1.setZIndex(2);
oKonvaLayer.draw();
};
oImage1.src = 'image1.jpg';
oKonvaImage1.on('transformend', function(e) {
UpdateAttrs();
});
const oKonvaImage2 = new Konva.Image({
x: 9,
y: 254,
width: 1024,
height: 1024
});
const oImage2 = new Image();
oImage2.onload = function() {
oKonvaImage2.image(oImage2);
oKonvaLayer.add(oKonvaImage2);
oKonvaImage2.setZIndex(0);
oKonvaLayer.draw();
};
oImage2.src = 'image2.jpg';
oKonvaImage2.on('transformend', function(e) {
UpdateAttrs();
});
const oKonvaImage3 = new Konva.Image({
x: -586,
y: -315,
width: 1920,
height: 1199
});
const oImage3 = new Image();
oImage3.onload = function() {
oKonvaImage3.image(oImage3);
oKonvaLayer.add(oKonvaImage3);
oKonvaImage3.setZIndex(1);
oKonvaLayer.draw();
};
oImage3.src = 'image3.jpg';
Image3 has index = 1 but is over Image2 which has index = 2.
First off, prompted by #lavrton's comment, you should add the konva.Images to the canvas as soon as you have instantiated them - not in the image onload event. The image objects are no overhead to the canvas, and you can then be sure of the initial z-index sequence. You may change the sequence after that, but at least you start with a known layout.
And as a general rule, you need to take care when using any commands inside the onload event of an image. Image loading is asynchronous - meaning it does not happen in the sequence you might anticipate when you write the code. A large image coming from a slow server will load more slowly than a small image from a quick server, but you cannot make any assumptions about the sequence. The ONLY way you can ensure the sequence is to have the load of the second image initiated from the onload event of the first, but this is generally going to give bad UX.
Back to the code you posted, the code in my snippet below would appear to work as you intended. I switched the ECMA2015 const to plain old vars, and removed the unnecessary on-transforms.
I also added some code to analyse the results, showing the hoped-for zIndex value and the achieved zIndex values.
Note that the zIndex value in Konva is relative to the parent container and not absolute.
So, for example, when I set zondex=999 I actually get a value of 4.
Summary:
avoid calling code for which sequence is critical in onload events.
do not expect to get exactly the zindex you ask for.
var div = $('#dropzone');
var oKonvaStage = new Konva.Stage({container: 'dropzone', width: div.width(), height: div.height()});
var indexWanted = [];
var oKonvaLayer = new Konva.Layer();
oKonvaStage.add(oKonvaLayer);
var oKonvaImage1 = new Konva.Image({
name: 'image-1',
x: 20,
y: 20,
width: 300,
height: 100
});
var oImage1 = new Image();
oImage1.onload = function() {
oKonvaLayer.add(oKonvaImage1);
oKonvaImage1.image(oImage1);
oKonvaImage1.setZIndex(2);
indexWanted[0] = 2;
oKonvaLayer.draw();
sayPos();
};
oImage1.src = 'https://dummyimage.com/300x100/666/fff.png?text=Image-1'
var oKonvaImage2 = new Konva.Image({
name: 'image-2',
x: 10,
y: 100,
width: 300,
height: 100
});
var oImage2 = new Image();
oImage2.onload = function() {
oKonvaImage2.image(oImage2);
oKonvaLayer.add(oKonvaImage2);
oKonvaImage2.setZIndex(0);
indexWanted[1] = 0;
oKonvaLayer.draw();
sayPos();
};
oImage2.src = 'https://dummyimage.com/300x100/333/fff.png?text=Image-2';
var oKonvaImage3 = new Konva.Image({
name: 'image-3',
x: 280,
y: 80,
width: 300,
height: 100
});
var oImage3 = new Image();
oImage3.onload = function() {
oKonvaImage3.image(oImage3);
oKonvaLayer.add(oKonvaImage3);
oKonvaImage3.setZIndex(999); // <<<< notice this is set to 99. Compare to console output !!
indexWanted[2] = 999;
oKonvaLayer.draw();
sayPos();
};
oImage3.src = 'https://dummyimage.com/300x100/ccc/fff.png?text=Image-3';
oKonvaLayer.draw();
oKonvaStage.draw();
var picCnt = 0, s= '', imgNo = 0;
function sayPos(){
picCnt = picCnt + 1;
if (picCnt === 3){
for (var i = 0; i < indexWanted.length; i = i + 1){
imgNo = i + 1;
s = s + '<br/>Image-' + imgNo + ' zindex wanted = ' + indexWanted[i] + ' actual zIndex=' + oKonvaLayer.findOne('.image-' + imgNo).getAbsoluteZIndex();
}
$('#info').html(s)
}
}
#info {
font-size: 10pt;
height: 100px;
}
<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>
<p id='info' >
</p>
<div id='dropzone' style="position: absolute; top: 90px; z-index: -1; display: inline-block; left: 0px; width: 600px; height: 400px; background-color: silver;"></div>

Drag and Drop limitation in Konva js

I recently began to learn Konva-JS... please help me :)
<script>
var width = window.innerWidth;
var height = window.innerHeight;
function loadImages(sources, callback) {
var assetDir = '/assets/';
var images = {};
var loadedImages = 0;
var numImages = 0;
for(var src in sources) {
numImages++;
}
for(var src in sources) {
images[src] = new Image();
images[src].onload = function() {
if(++loadedImages >= numImages) {
callback(images);
}
};
images[src].src = assetDir + sources[src];
}
}
function isNearOutline(animal, outline) {
var a = animal;
var o = outline;
var ax = a.getX();
var ay = a.getY();
if(ax > o.x - 20 && ax < o.x + 20 && ay > o.y - 20 && ay < o.y + 20) {
return true;
}
else {
return false;
}
}
function drawBackground(background, beachImg, text) {
var context = background.getContext();
context.drawImage(beachImg, 0, 0);
context.setAttr('font', '20pt Calibri');
context.setAttr('textAlign', 'center');
context.setAttr('fillStyle', 'white');
context.fillText(text, background.getStage().getWidth() / 2, 40);
}
function initStage(images) {
var stage = new Konva.Stage({
container: 'container',
width: 578,
height: 530
});
var background = new Konva.Layer();
var animalLayer = new Konva.Layer();
var animalShapes = [];
var score = 0;
// image positions
var animals = {
snake: {
x: 10,
y: 70
},
giraffe: {
x: 90,
y: 70
},
monkey: {
x: 275,
y: 70
},
lion: {
x: 400,
y: 70
}
};
var outlines = {
snake_black: {
x: 275,
y: 350
},
giraffe_black: {
x: 390,
y: 250
},
monkey_black: {
x: 300,
y: 420
},
lion_black: {
x: 100,
y: 390
}
};
// create draggable animals
for(var key in animals) {
// anonymous function to induce scope
(function() {
var privKey = key;
var anim = animals[key];
var animal = new Konva.Image({
image: images[key],
x: anim.x,
y: anim.y,
draggable: true
});
animal.on('dragstart', function() {
this.moveToTop();
animalLayer.draw();
});
/*
* check if animal is in the right spot and
* snap into place if it is
*/
animal.on('dragend', function() {
var outline = outlines[privKey + '_black'];
if(!animal.inRightPlace && isNearOutline(animal, outline)) {
animal.position({
x : outline.x,
y : outline.y
});
animalLayer.draw();
animal.inRightPlace = true;
if(++score >= 4) {
var text = 'You win! Enjoy your booty!';
drawBackground(background, images.beach, text);
}
// disable drag and drop
setTimeout(function() {
animal.draggable(false);
}, 50);
}
});
// make animal glow on mouseover
animal.on('mouseover', function() {
animal.image(images[privKey + '_glow']);
animalLayer.draw();
document.body.style.cursor = 'pointer';
});
// return animal on mouseout
animal.on('mouseout', function() {
animal.image(images[privKey]);
animalLayer.draw();
document.body.style.cursor = 'default';
});
animal.on('dragmove', function() {
document.body.style.cursor = 'pointer';
});
animalLayer.add(animal);
animalShapes.push(animal);
})();
}
// create animal outlines
for(var key in outlines) {
// anonymous function to induce scope
(function() {
var imageObj = images[key];
var out = outlines[key];
var outline = new Konva.Image({
image: imageObj,
x: out.x,
y: out.y
});
animalLayer.add(outline);
})();
}
stage.add(background);
stage.add(animalLayer);
drawBackground(background, images.beach, 'Ahoy! Put the animals on the beach!');
}
var sources = {
beach: 'beach.png',
snake: 'snake.png',
snake_glow: 'snake-glow.png',
snake_black: 'snake-black.png',
lion: 'lion.png',
lion_glow: 'lion-glow.png',
lion_black: 'lion-black.png',
monkey: 'monkey.png',
monkey_glow: 'monkey-glow.png',
monkey_black: 'monkey-black.png',
giraffe: 'giraffe.png',
giraffe_glow: 'giraffe-glow.png',
giraffe_black: 'giraffe-black.png'
};
loadImages(sources, initStage);
</script>
as we can see in this example Animals_on_the_Beach_Game the animal's images are drag-able and can be drop ever where.... but I want to change it in the way that it just can drop on the specific place ... what can I do ?
thank you :)
This is more of a design question, as letting go of the mouse button isn't something you can prevent. It would also be non-intuitive to keep the image attached to the mouse position as you would then need a new mouse event to associate with dropping it. What I've done for a drag and drop UI was to either (1) destroy the dropped shape, or if that wasn't an option, (2) animate the shape back (i.e. snap back) to its original position. Alternatively, you might (3) find the closest likely valid drop target and snap to that location.
First you define lionOrigin, that maybe you already have.
You have to implement the call on the dragend event of the object dragged, so let's say the lion. You have to check position of the lion in relation to the end desired position, let's call it lionDestiny. That can be done with a simple grometry: calculate the distance between to point. We do that with distanceA2B() function.
Now you can establish an offset inside wich you can snap the object, as it is close enough. If the minimal offset is not achieved, then you place the lion back on lionOrigin.
Al last, in konvajs you can use .x() and .y() to easily get or set position to lion.
Something like this:
var lionOrigin = [50,50];
var lionDestiny = [200,200];
var offset = 20;
distanceA2B(a,b) {
return Math.sqrt( ((a[0]-b[0])*(a[0]-b[0])) + ((a[1]-b[1])*(a[1]-b[1])) );
}
lion.on('dragend', (e) => {
var d = distanceA2B([lion.x(),lion.y()],lionDestiny);
if(d<offset){
lion.x(lionDestiny[0]);
lion.y(lionDestiny[1]);
}else{
lion.x(lionOrigin[0]);
lion.y(lionOrigin[1]);
}
});
Hope this helps!
It would have been better if you could explain your question more when you say you want to move any shape to a specific position. Though konva.js provides you with various events through which you can do this. For example, suppose you want to interchange the location of two shapes when you drag and move the first shape to the second and drop it there. In this case, you can use dragend event of konva. So when you move the target element to another element and drop it there, check if they are intersecting each other or not and then interchange their coordinates with each other.
Here is the function to find the intersection between two elements:
haveIntersection(r1, r2) {
return !(
r2.x > r1.x + r1.width ||
r2.x + r2.width < r1.x ||
r2.y > r1.y + r1.height ||
r2.y + r2.height < r1.y
);
}
And from here, you can try to understand the functionality. Though it's in nuxt.js but the events and scripts would be almost same if you are using only javascript. You can find sample code with an explanation for replacing the location of two shapes with each other. So even if you don't want to replace the locations but you want to move your target element to any position this will make you understand how to do this.

Openlayers 3: Drawing grid lines (graticule) with predefined units on the custom static image

I am trying to draw custom x-y axes grid lines on top of a static image, i.e. image pixels rather than lattitude and longitudes. Ideally, the grid lines should be redrawn dynamically when I drag/zoom/scroll the image, just like the x-y ruler bars in Photoshop.
I came across the following code example, which provides a custom projection function to directly map image pixel coordinates to map coordinates.
http://openlayers.org/en/latest/examples/static-image.html
// Map views always need a projection. Here we just want to map image
// coordinates directly to map coordinates, so we create a projection that uses
// the image extent in pixels.
var extent = [0, 0, 1024, 968];
var projection = new ol.proj.Projection({
code: 'xkcd-image',
units: 'pixels',
extent: extent
});
I tried to append the following code to the script. However, the ol.Graticule class seems to be incompatible with the custom ol.proj.Projection definition.
http://openlayers.org/en/latest/examples/graticule.html
// Create the graticule component
var graticule = new ol.Graticule({
// the style to use for the lines, optional.
strokeStyle: new ol.style.Stroke({
color: 'rgba(255,120,0,0.9)',
width: 2,
lineDash: [0.5, 4]
})
});
graticule.setMap(map);
What's wrong with the above code?
P.S. I am aware of the Openseadragon API which provides a dynamic scalebar. However, I wish to stick to Openlayers API because I also have an extra map layer of anchor points at predefined locations on the static image.
I had the same problem. For this to work I created a Vector Layer, (where axis are drawn).
To draw the axis, I need to listen to View changes.
Whenever the view changes, calculate the actual extent for the view.
With extent information and ([width, height] of the image, you can then draw axis)
let listenerAxis = null,
w = 0,
h = 0
const xAxisStyle = new ol.style.Style({
stroke: new ol.style.Stroke({
color: 'red',
width: 2
})
})
const yAxisStyle = new ol.style.Style({
stroke: new ol.style.Stroke({
color: 'green',
width: 2
})
})
const ImageLayer = new ol.layer.Image()
const AxisLayer = new ol.layer.Vector({ source: new ol.source.Vector() })
AxisLayer.setStyle((feature, resolution) => {
if(feature.getProperties().axis == 'x') {
return xAxisStyle
}
return yAxisStyle
})
const renderer = new ol.Map({
target: 'map',
layers: [ImageLayer]
})
AxisLayer.setMap(renderer)
processFile('https://i2.wp.com/beebom.com/wp-content/uploads/2016/01/Reverse-Image-Search-Engines-Apps-And-Its-Uses-2016.jpg?resize=640%2C426')
function removeAxis() {
AxisLayer.getSource().clear()
ol.Observable.unByKey(listenerAxis)
listenerAxis = null
}
function drawAxis() {
function draw(){
AxisLayer.getSource().clear()
const extent = renderer.getView().calculateExtent()
const [xmin, ymin, xmax, ymax] = extent
// Eje X
const axisX = new ol.geom.LineString([ [xmin, h / 2], [xmax, h / 2] ])
const axisY = new ol.geom.LineString([ [w / 2, ymin], [w / 2, ymax] ])
const featureX = new ol.Feature({ geometry: axisX, axis: 'x' })
const featureY = new ol.Feature({ geometry: axisY, axis: 'y' })
AxisLayer.getSource().addFeatures([featureX, featureY])
}
listenerAxis = renderer.getView().on('change', draw)
draw()
}
async function processFile(path) {
ImageLayer.setSource()
removeAxis()
if(!path) {
return
}
const [wi, hi] = await readImage(path)
w = wi
h = hi
const source = getImageStatic(path, w, h)
const view = getViewForImage(w, h)
ImageLayer.setSource(source)
renderer.setView(view)
drawAxis()
}
// Some helpers
function readImage(localPath) {
const img = document.createElement('img')
return new Promise((res, rej) => {
img.src = localPath
img.addEventListener('load', (event) => {
const { naturalWidth, naturalHeight } = img
console.log('img', naturalWidth, naturalHeight)
res([naturalWidth, naturalHeight])
})
})
}
function getViewForImage(w, h) {
return new ol.View({
center: [w / 2, h / 2],
zoom: 2,
projection: new ol.proj.Projection({
extent: [0, 0, w, h],
units: 'pixels'
}),
extent: [0, 0, w, h]
})
}
function getImageStatic(path, w, h) {
return new ol.source.ImageStatic({
url: path,
imageExtent: [0, 0, w, h]
})
}
#map {
width: 100%;
height: 100%;
background: grey;
}
<link href="https://openlayers.org/en/v4.6.5/css/ol.css" rel="stylesheet"/>
<script src="https://openlayers.org/en/v4.6.5/build/ol.js"></script>
<div id="map"></div>

dojox.gfx - Maintaining gfx references for event connection

I have a simple program where I am GETing an svg file and then creating gfx shapes from rect elements within the svg as follows
function superCallback(shape){
shape.setFill("red");
}
dojo.ready(function() {
// Load image
dojo.xhrGet({
//handleAs: 'xml',
url: 'svg/demo.svg',
load: function(data) {
// Create the surface
var surface = dojox.gfx.createSurface("logoHolder", 2000, 1500);
var group = surface.createGroup();
var dom = dojox.xml.parser.parse(data);
rects = dom.getElementsByTagName("rect");
for (var idx = 0 ; idx < rects.length ; idx++) {
//alert('Coordinates ' + rects[idx].getAttribute("x") + ', ' + rects[idx].getAttribute("y"));
var rectangle = group.createRect({ x: parseFloat(rects[idx].getAttribute("x")),
y: parseFloat(rects[idx].getAttribute("y")),
width: parseFloat(rects[idx].getAttribute("width")),
height: parseFloat(rects[idx].getAttribute("height")) })
.setStroke({ style: "Solid" , width: "7" , color :"black"});
rectangle.connect("onclick",dojo.hitch(rectangle,function(e) {
superCallback(rectangle);
}));
}
});
});
As can be seen, I am using a local variable rectangle to create and store the gfx shape and then calling connect to attach an event to it.
The problem is that the event is attached to only the last gfx rect which is created in teh for loop because I guess the closure is executing in the context of the last rectangle reference always.
Need some help in fixing this so that all rects can be triggered by the event
Thanks in advance
Regards
Shyam

Resources