Scaling bug with multi-touch Canvas scale with pinch zoom - javascript

Using the sample code from Konvajs.org as a base (https://konvajs.org/docs/sandbox/Multi-touch_Scale_Stage.html), I have added a large SVG to a layer (4096 x 3444) to experiment with zoom / pan of a vector-based map, base64 encoded SVG in this instance. Initial impressions are good however during testing I experience an odd bug where during a pinch the view of the map would snap to a different location on the map not the area that I centred on.
Here is the code (map base64 code removed due to length):
// by default Konva prevent some events when node is dragging
// it improve the performance and work well for 95% of cases
// we need to enable all events on Konva, even when we are dragging a node
// so it triggers touchmove correctly
Konva.hitOnDragEnabled = true;
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
draggable: true,
});
var layer = new Konva.Layer();
var triangle = new Konva.RegularPolygon({
x: 190,
y: stage.height() / 2,
sides: 3,
radius: 80,
fill: 'green',
stroke: 'black',
strokeWidth: 4,
});
var circle = new Konva.Circle({
x: 380,
y: stage.height() / 2,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
let bg = new Konva.Image({
width: 4096,
height: 3444
});
layer.add(bg);
var image = new Image();
image.onload = function() {
bg.image(image);
layer.draw();
};
image.src = 'data:image/svg+xml;base64,...';
function getDistance(p1, p2) {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
}
function getCenter(p1, p2) {
return {
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2,
};
}
var lastCenter = null;
var lastDist = 0;
stage.on('touchmove', function (e) {
e.evt.preventDefault();
var touch1 = e.evt.touches[0];
var touch2 = e.evt.touches[1];
if (touch1 && touch2) {
// if the stage was under Konva's drag&drop
// we need to stop it, and implement our own pan logic with two pointers
if (stage.isDragging()) {
stage.stopDrag();
}
var p1 = {
x: touch1.clientX,
y: touch1.clientY,
};
var p2 = {
x: touch2.clientX,
y: touch2.clientY,
};
if (!lastCenter) {
lastCenter = getCenter(p1, p2);
return;
}
var newCenter = getCenter(p1, p2);
var dist = getDistance(p1, p2);
if (!lastDist) {
lastDist = dist;
}
// local coordinates of center point
var pointTo = {
x: (newCenter.x - stage.x()) / stage.scaleX(),
y: (newCenter.y - stage.y()) / stage.scaleX(),
};
var scale = stage.scaleX() * (dist / lastDist);
stage.scaleX(scale);
stage.scaleY(scale);
// calculate new position of the stage
var dx = newCenter.x - lastCenter.x;
var dy = newCenter.y - lastCenter.y;
var newPos = {
x: newCenter.x - pointTo.x * scale + dx,
y: newCenter.y - pointTo.y * scale + dy,
};
stage.position(newPos);
lastDist = dist;
lastCenter = newCenter;
}
});
stage.on('touchend', function () {
lastDist = 0;
lastCenter = null;
});
layer.add(triangle);
layer.add(circle);
stage.add(layer);
I am unsure if this is due to the large size of the image and / or canvas or an inherent flaw in the example code from Konvas.js. This has been tested, with the same results, on 2 models of iPad Pro, iPhone X & 11, Android Pixel 3, 5 and 6 Pro.
Here is the code on codepen as an example: https://codepen.io/mr-jose/pen/WNXgbdG
Any help would be appreciated, thanks!

I faced the same issue and discovered that it was caused by the dragging functionality of the stage. Everytime if (stage.isDragging()) evaluated to true, the jump happened.
For me what worked was setting draggable to false while pinch zooming and back to true on touch end.
stage.on('touchmove', function (e) {
...
if (touch1 && touch2) {
stage.draggable(false);
....
}
});
stage.on('touchend', function (e) {
...
stage.draggable(true);
});

Related

How to get the correct tile position in Phaser.js with a zoomed camera?

I am using Phaser.js 3.55 to build a tile-based game. I have a tilemap, a layer, and a player sprite. I want to be able to click on a tile and have the player sprite move to that tile.
However, I'm having trouble getting the correct tile position when the camera is zoomed in or out. I've tried using layer.worldToTileX and layer.worldToTileY to calculate the tile position based on the pointer position, but it gives me different results depending on the zoom level.
Here's my code for the pointerdown event in the update-function:
function update (time, delta) {
controls.update(delta);
zoomLevel = this.cameras.main.zoom;
this.input.on("pointerdown", function (pointer) {
var x = layer.worldToTileX(pointer.x / zoomLevel );
var y = layer.worldToTileY(pointer.y / zoomLevel );
if (map.getTileAt(x, y)) {
var goalX = map.getTileAt(x, y).x * 32 + 16;
var goalY = map.getTileAt(x, y).y * 32 + 16;
this.tweens.add({
targets: player,
x: goalX,
y: goalY,
duration: 1000,
ease: 'Power4',
});
}
}, this);
}
What is the correct way to get the tile position based on the pointer position and the zoom level in Phaser.js?
At first I wrote the pointerdown-event in the create() function, which didn't update the zoom-scale I got by zoomLevel = this.cameras.main.zoom;. I took it to the update-function where zoomLevel would be updated, then trying to divide the current zoomLevel with the pointer.x and pointer.y. I expected the pointer so give me the currently clicked tile. Outcome: It shifted, depending on the zoomLevel.
My other code besides the update()-function
var game = new Phaser.Game(config);
var map;
var layer;
var player;
function preload () {
this.load.tilemapTiledJSON('tilemap', '/img/phaser/map.json');
this.load.image('base_tiles', '/img/phaser/outdoors.png');
this.load.spritesheet('player', '/img/phaser/dude.jpg', { frameWidth: 32, frameHeight: 48 });
}
function create () {
map = this.make.tilemap({ key: 'tilemap' });
const tileset = map.addTilesetImage('outdoors', 'base_tiles');
layer = map.createLayer(0, tileset, 0, 0);
player = this.add.sprite(500, 100, 'player', 4);
var cursors = this.input.keyboard.createCursorKeys();
var controlConfig = {
camera: this.cameras.main,
left: cursors.left,
right: cursors.right,
up: cursors.up,
down: cursors.down,
zoomIn: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q),
zoomOut: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E),
acceleration: 0.06,
drag: 0.0005,
maxSpeed: 1.0
};
controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);
}
You can get the position with the function getWorldPoint of the camera (link to the documentation), to get the exact position. No calculation needed
Here a demo showcasing this:
(use the Arrow UP/DOWN to zoom in or out, and click to select tile)
document.body.style = 'margin:0;';
var config = {
type: Phaser.AUTO,
width: 5 * 16,
height: 3 * 16,
zoom: 4,
pixelart: true,
scene: {
preload,
create
},
banner: false
};
function preload (){
this.load.image('tiles', 'https://labs.phaser.io/assets/tilemaps/tiles/super-mario.png');
}
function create () {
let infoText = this.add.text(2, 2, )
.setOrigin(0)
.setScale(.5)
.setDepth(100)
.setStyle({fontStyle: 'bold', fontFamily: 'Arial'});
// Load a map from a 2D array of tile indices
let level = [ [ 0, 0, 0, 0, 0],[ 0, 14, 13, 14, 0],[ 0, 0, 0, 0, 0]];
let map = this.make.tilemap({ data: level, tileWidth: 16, tileHeight: 16 });
let tiles = map.addTilesetImage('tiles');
let layer = map.createLayer(0, tiles, 0, 0);
const cam = this.cameras.main;
let tileSelector = this.add.rectangle(0, 0, 16, 16, 0xffffff, .5).setOrigin(0);
this.input.keyboard.on('keyup-UP', function (event) {
cam.zoom+=.25;
});
this.input.keyboard.on('keyup-DOWN', function (event) {
if(cam.zoom > 1){
cam.zoom-=.25;
}
});
this.input.on('pointerdown', ({x, y}) => {
let {x:worldX, y:worldY} = cam.getWorldPoint(x, y)
let tile = layer.getTileAtWorldXY(worldX, worldY);
tileSelector.setPosition(tile.pixelX, tile.pixelY);
infoText.setText(`selected TileID:${tile.index}`);
});
}
new Phaser.Game(config);
<script src="//cdn.jsdelivr.net/npm/phaser/dist/phaser.min.js"></script>

Resize Konva Label on Zoom

I have a simple setup where use clicks location and creates a label with text.
What i would like to be able to do is resize on zoom, so that label is relative size always. Currently label is huge when i zoom in covering key areas.
code:
createTag() {
return new Konva.Tag({
// omitted for brevity
})
}
createLabel(x: number, y: number) {
return new Konva.Label({
x: x,
y: y,
opacity: 0.75
});
}
createText() {
return new Konva.Text({
// omitted for brevity
});
}
createMarker(point: any) {
var label = createLabel(point.x, point.y);
var tag = createTag();
var text = createText();
label.add(tag);
label.add(text);
this.layer.add(label);
this.stage.add(this.layer);
}
Mouse click event listener
this.stage.on('click', function (e) {
// omitted for brevity
var pointer = this.getRelativePointerPosition();
createMarker(pointer);
// omitted for brevity
}
this.stage.on('wheel', function (e) {
// zoom code
});
How can i resize the labels to be relative size of the canvas?
There are many possible solutions. If you are changing scale of the whole stage, then you can just apply reversed scale to the label, so its absolute scale will be always 1.
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
});
var layer = new Konva.Layer();
stage.add(layer);
var circle = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 50,
fill: 'green',
});
layer.add(circle);
// tooltip
var tooltip = new Konva.Label({
x: circle.x(),
y: circle.y(),
opacity: 0.75,
});
layer.add(tooltip);
tooltip.add(
new Konva.Tag({
fill: 'black',
pointerDirection: 'down',
pointerWidth: 10,
pointerHeight: 10,
lineJoin: 'round',
shadowColor: 'black',
shadowBlur: 10,
shadowOffsetX: 10,
shadowOffsetY: 10,
shadowOpacity: 0.5,
})
);
tooltip.add(
new Konva.Text({
text: 'Try to zoom with wheel',
fontFamily: 'Calibri',
fontSize: 18,
padding: 5,
fill: 'white',
})
);
var scaleBy = 1.01;
stage.on('wheel', (e) => {
e.evt.preventDefault();
var oldScale = stage.scaleX();
var pointer = stage.getPointerPosition();
var mousePointTo = {
x: (pointer.x - stage.x()) / oldScale,
y: (pointer.y - stage.y()) / oldScale,
};
var newScale =
e.evt.deltaY > 0 ? oldScale * scaleBy : oldScale / scaleBy;
stage.scale({ x: newScale, y: newScale });
// apply reversed scale to label
// so its absolte scale is still 1
tooltip.scale({ x: 1 / newScale, y: 1 / newScale })
var newPos = {
x: pointer.x - mousePointTo.x * newScale,
y: pointer.y - mousePointTo.y * newScale,
};
stage.position(newPos);
});
<script src="https://unpkg.com/konva#8.3.0/konva.min.js"></script>
<div id="container"></div>

paperjs draw 0 to 360º arc using mouse events

I´m trying to draw an arc using mouse events with paper.js
The user must be able to draw an arc from 0 degree to 360 degree.
The issue that I´m facing is that I can draw 270 degree arc but more than 270, the arc flips to another quadrant.
Start point must be located anywhere
A sketch can be found here:
http://sketch.paperjs.org/#V/0.12.7/S/nVTBjpswEP2VEZeQXYcG2l6IOFTpZQ+RVruHHkpVeR1nQSE2MoZNFPHvtbHdeCGtorWEsOf5zTyex5wDhg80SIPnPZWkCFBA+FavOywAC/KbNHSVs5zptcDbsm2yZLlcmQChTFKRMfoGj7xkMvyyXCL1zC3eSCzkCP5qYJeTxJDBsAPLIlqXglQ0POcM1DDpU/tGJmhEpJDY9a6sqjWvuNo3e6kw2c9y1s9XoAvYD/AquNR6NFLwPXVcQbczNAZ/lFtZpBBPgDWuNYe3TLEADKzLAgyV4TJyJjmvIs42vG3ohndaz65lRJachbRTHzeHsyNpT2rPsgGPaj2PjshfnbSNjtLF2WD2wnjlI0lWT6OYvVY0C7skGmYP7EnZilmz6OJRxFXxuIJ0uMq0ueun7+GEgSZZ0bBE9hzNCb7Pa08qEvSgDAodaMNeh3wTJDQC9J5e2+a8BKcIV3WBYzS8kqu1TRfYhqKyFQy8xtgJfkj9gB5H14fREe5tF95ttCTCG1tyjt5zTn85pxGnKZnjXCi9R5eFaq7X4iYZcAcjIQoys2Rhq3xKbhHnMl3kXc30D8n8Q6bdj/J/xMRJjusKb7/xn/974+11t03Um7+Z+ne+CIr3w+1sgvTnr/4P
and this is the implemented code:
var arc_cse;
var radius=200;
var center=new Point(400,400);
var start=new Point(400,500);
var c1 = new Path.Circle({
center: center,
radius: 2,
fillColor: 'black'
});
arc_cse = new Path({
strokeColor: 'red',
strokeWidth: 1,
strokeCap: 'round',
});
tool.onMouseMove = function(event) {
var p=new Point(event.point.x,event.point.y);
var v1=start-center;
var v2=p-center;
var angle=(v2.angleInRadians-v1.angleInRadians);
var arcval=arc_CRD(v1.angleInRadians,v2.angleInRadians,angle,center,radius);
arc_cse.remove();
arc_cse= new Path.Arc(arcval);
}
function arc_CRD(alpha1,alpha2,angle,center,radius){
return {
from: {
x: center.x + radius*Math.cos(alpha1),
y: center.y + radius*Math.sin(alpha1)
},
through: {
x: center.x + radius * Math.cos(alpha1 + (alpha2-alpha1)/2),
y: center.y + radius * Math.sin(alpha1 + (alpha2-alpha1)/2)
},
to: {
x: center.x + radius*Math.cos(alpha1+(alpha2-alpha1)),
y: center.y + radius*Math.sin(alpha1+(alpha2-alpha1))
},
strokeColor: 'red',
strokeWidth: 3,
strokeCap: 'round'
}
}
Thanks in advance
There are certainly tons of ways to do this but here's how I would do it: sketch.
This should help you finding the proper solution to your own use case.
function dot(point, color) {
const item = new Path.Circle({ center: point, radius: 5, fillColor: color });
item.removeOnMove();
}
function drawArc(from, center, mousePoint) {
const radius = (from - center).length;
const circle = new Path.Circle(center, radius);
const to = circle.getNearestPoint(mousePoint);
const middle = (from + to) / 2;
const throughVector = (middle - center).normalize(radius);
const angle = (from - center).getDirectedAngle(to - center);
const through = angle <= 0
? center + throughVector
: center - throughVector;
const arc = new Path.Arc({
from,
through,
to,
strokeColor: 'red',
strokeWidth: 2
});
circle.removeOnMove();
arc.removeOnMove();
// Visual helpers
dot(from, 'orange');
dot(center, 'black');
dot(mousePoint, 'red');
dot(to, 'blue');
dot(middle, 'lime');
dot(through, 'purple');
circle.strokeColor = 'black';
return arc;
}
function onMouseMove(event) {
drawArc(view.center + 100, view.center, event.point);
}
Edit
In answer to your comment, here is a more mathematical approach: sketch.
This is based on your code and has exactly the same behavior so you should have no difficulty using it.
The key of both implementation is the Point.getDirectedAngle() method which allow you to adapt the behavior depending on the through point side.
const center = new Point(400, 400);
const start = new Point(400, 500);
const c1 = new Path.Circle({
center: center,
radius: 2,
fillColor: 'black'
});
let arc;
function getArcPoint(from, center, angle) {
return center + (from - center).rotate(angle);
}
function drawArc(from, center, mousePoint) {
const directedAngle = (from - center).getDirectedAngle(mousePoint - center);
const counterClockwiseAngle = directedAngle < 0
? directedAngle
: directedAngle - 360;
const through = getArcPoint(from, center, counterClockwiseAngle / 2);
const to = getArcPoint(from, center, counterClockwiseAngle);
return new Path.Arc({
from,
through,
to,
strokeColor: 'red',
strokeWidth: 1,
strokeCap: 'round'
});
}
function onMouseMove(event) {
if (arc) {
arc.remove();
}
arc = drawArc(start, center, event.point);
}

Rotating Shape with KineticJS

I'm struggling to implement a little things on canvas with KineticJS.
I want to create a circle + a line which form a group (plane).
The next step is to allow the group to rotate around itself with a button that appears when you click on the group.
My issue is that when I click on the rotate button, it does not rotate near the button but elsewhere. Have a look :
My rotation atm : http://hpics.li/b46b73a
I want the rotate button to be near the end of the line. Not far away..
I tried to implement it on jsfiddle but I'm kinda new and I didn't manage to put it correctly , if you could help me on that, I would be thankful !
http://jsfiddle.net/49nn0ydh/1/
function radians (degrees) {return degrees * (Math.PI/180)}
function degrees (radians) {return radians * (180/Math.PI)}
function angle (cx, cy, px, py) {var x = cx - px; var y = cy - py; return Math.atan2 (-y, -x)}
function distance (p1x, p1y, p2x, p2y) {return Math.sqrt (Math.pow ((p2x - p1x), 2) + Math.pow ((p2y - p1y), 2))}
jQuery (function(){
var stage = new Kinetic.Stage ({container: 'kineticDiv', width: 1200, height:600})
var layer = new Kinetic.Layer(); stage.add (layer)
// group avion1
var groupPlane1 = new Kinetic.Group ({
x: 150, y: 150,
draggable:true
}); layer.add (groupPlane1)
// avion 1
var plane1 = new Kinetic.Circle({
radius: 10,
stroke: "darkgreen",
strokeWidth: 3,
}); groupPlane1.add(plane1);
var trackPlane1 = new Kinetic.Line({
points: [10, 0, 110, 0],
stroke: "darkgreen",
strokeWidth: 2
}); groupPlane1.add(trackPlane1);
groupPlane1.on('click', function() {
controlGroup.show();
});
groupPlane1.setOffset (plane1.getWidth() * plane1.getScale().x / 2, plane1.getHeight() * plane1.getScale().y / 2)
var controlGroup = new Kinetic.Group ({
x: groupPlane1.getPosition().x + 120,
y: groupPlane1.getPosition().y ,
opacity: 1, draggable: true,
}); layer.add (controlGroup)
var signRect2 = new Kinetic.Rect({
x:-8,y: -6,
width: 20,
height: 20,
fill: 'white',
opacity:0
});
controlGroup.add(signRect2);
var sign = new Kinetic.Path({
x: -10, y: -10,
data: 'M12.582,9.551C3.251,16.237,0.921,29.021,7.08,38.564l-2.36,1.689l4.893,2.262l4.893,2.262l-0.568-5.36l-0.567-5.359l-2.365,1.694c-4.657-7.375-2.83-17.185,4.352-22.33c7.451-5.338,17.817-3.625,23.156,3.824c5.337,7.449,3.625,17.813-3.821,23.152l2.857,3.988c9.617-6.893,11.827-20.277,4.935-29.896C35.591,4.87,22.204,2.658,12.582,9.551z',
scale: {x:0.5, y:0.5}, fill: 'black'
}); controlGroup.add (sign)
controlGroup.setDragBoundFunc (function (pos) {
var groupPos = groupPlane1.getPosition();
var rotation = degrees (angle (groupPos.x, groupPos.y, pos.x, pos.y));
var dis = distance (groupPos.x, groupPos.y, pos.x, pos.y);
groupPlane1.setRotationDeg (rotation);
layer.draw()
return pos
})
controlGroup.on ('dragend', function() {
controlGroup.hide();
layer.draw()
})
controlGroup.hide();
layer.draw()
})
You can adjust the rotation point by setting the offsetX and offsetY of the group.

JQuery Spot of light with Sparkles, How to achieve it, is there something ready similar?

I have t his effect on this website
http://www.immersive-garden.com/
there's this point of light sparking, on hover you get the background, I want something similar without using flash
this is the script I'm using right now
/*
Particle Emitter JavaScript Library
Version 0.3
by Erik Friend
Creates a circular particle emitter of specified radius centered and offset at specified screen location. Particles appear outside of emitter and travel outward at specified velocity while fading until disappearing in specified decay time. Particle size is specified in pixels. Particles reduce in size toward 1px as they decay. A custom image(s) may be used to represent particles. Multiple images will be cycled randomly to create a mix of particle types.
example:
var emitter = new particle_emitter({
image: ['resources/particle.white.gif', 'resources/particle.black.gif'],
center: ['50%', '50%'], offset: [0, 0], radius: 0,
size: 6, velocity: 40, decay: 1000, rate: 10
}).start();
*/
particle_emitter = function (opts) {
// DEFAULT VALUES
var defaults = {
center: ['50%', '50%'], // center of emitter (x / y coordinates)
offset: [0, 0], // offset emitter relative to center
radius: 0, // radius of emitter circle
image: 'particle.gif', // image or array of images to use as particles
size: 1, // particle diameter in pixels
velocity: 10, // particle speed in pixels per second
decay: 500, // evaporation rate in milliseconds
rate: 10 // emission rate in particles per second
};
// PASSED PARAMETER VALUES
var _options = $.extend({}, defaults, opts);
// CONSTRUCTOR
var _timer, _margin, _distance, _interval, _is_chrome = false;
(function () {
// Detect Google Chrome to avoid alpha transparency clipping bug when adjusting opacity
if (navigator.userAgent.indexOf('Chrome') >= 0) _is_chrome = true;
// Convert particle size into emitter surface margin (particles appear outside of emitter)
_margin = _options.size / 2;
// Convert emission velocity into distance traveled
_distance = _options.velocity * (_options.decay / 1000);
// Convert emission rate into callback interval
_interval = 1000 / _options.rate;
})();
// PRIVATE METHODS
var _sparkle = function () {
// Pick a random angle and convert to radians
var rads = (Math.random() * 360) * (Math.PI / 180);
// Starting coordinates
var sx = parseInt((Math.cos(rads) * (_options.radius + _margin)) + _options.offset[0] - _margin);
var sy = parseInt((Math.sin(rads) * (_options.radius + _margin)) + _options.offset[1] - _margin);
// Ending Coordinates
var ex = parseInt((Math.cos(rads) * (_options.radius + _distance + _margin + 0.5)) + _options.offset[0] - 0.5);
var ey = parseInt((Math.sin(rads) * (_options.radius + _distance + _margin + 0.5)) + _options.offset[1] - 0.5);
// Pick from available particle images
var image;
if (typeof(_options.image) == 'object') image = _options.image[Math.floor(Math.random() * _options.image.length)];
else image = _options.image;
// Attach sparkle to page, then animate movement and evaporation
var s = $('<img>')
.attr('src', image)
.css({
zIndex: 10,
position: 'absolute',
width: _options.size + 'px',
height: _options.size + 'px',
left: _options.center[0],
top: _options.center[1],
marginLeft: sx + 'px',
marginTop: sy + 'px'
})
.appendTo('body')
.animate({
width: '1px',
height: '1px',
marginLeft: ex + 'px',
marginTop: ey + 'px',
opacity: _is_chrome ? 1 : 0
}, _options.decay, 'linear', function () { $(this).remove(); });
// Spawn another sparkle
_timer = setTimeout(function () { _sparkle(); }, _interval);
};
// PUBLIC INTERFACE
// This is what gets returned by "new particle_emitter();"
// Everything above this point behaves as private thanks to closure
return {
start:function () {
clearTimeout(_timer);
_timer = setTimeout(function () { _sparkle(); }, 0);
return(this);
},
stop:function () {
clearTimeout(_timer);
return(this);
},
centerTo:function (x, y) {
_options.center[0] = x;
_options.center[1] = y;
},
offsetTo:function (x, y) {
if ((typeof(x) == 'number') && (typeof(y) == 'number')) {
_options.center[0] = x;
_options.center[1] = y;
}
}
}
};
you probably need something like this: http://www.realcombiz.com/2012/09/customize-blackquote-with-light-bulb.html

Categories

Resources