I was trying to maintain the object size while zooming, i tried to get inspired by this answer in which the guy who wrote it didn't solve the controls issue in such as case, as a consequence you can see them not sticking to the object while zooming as in this screenshot.
But i came with this solution to maintain the object position and controls by updating its left and top after calculating them based on the inverted viewportTransform by calculating a new fabric.Point using the fabric.util.transformPoint function
fabric.Object.prototype.transform = function(ctx) {
const obj = this;
const {
ignoreZoom,
group,
canvas,
left,
top
} = obj;
const {
contextTop,
viewportTransform,
getZoom,
requestRenderAll,
} = canvas;
var needFullTransform = (group && !group._transformDone) || (group && canvas && ctx === contextTop);
if (ignoreZoom) {
const oldP = new fabric.Point(left, top);
const newP = fabric.util.transformPoint(oldP, fabric.util.invertTransform(viewportTransform));
var zoom = 1 / getZoom();
/* // here i tried to refresh the whole canvas with requestRenderAll()
this.set({
left: newP.x,
top: newP.y,
scaleX: zoom,
scaleY: zoom,
});
this.setCoords();
requestRenderAll();
*/
// but here i try refresh the object only which is better i think
this.left = newP.x;
this.top = newP.y;
this.scaleX = zoom;
this.scaleY = zoom;
this.drawObject(ctx);
}
var m = this.calcTransformMatrix(!needFullTransform);
ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
}
I have made this codesandbox as a demo for my code. As you can see in this screenshot, controls stick around the object but the whole of them doesn't maintain their position relatively to the background and sometimes they disappear completely.
I need the object to keep its position relatively to the background.
How to make it better ?
// EDIT
I tried to understand better what happens while zooming, i found the fabric.Canvas.zoomToPoint() which is used for zooming (as in their tutorial)
zoomToPoint: function (point, value) {
// TODO: just change the scale, preserve other transformations
var before = point, vpt = this.viewportTransform.slice(0);
point = transformPoint(point, invertTransform(this.viewportTransform));
vpt[0] = value;
vpt[3] = value;
var after = transformPoint(point, vpt);
vpt[4] += before.x - after.x;
vpt[5] += before.y - after.y;
return this.setViewportTransform(vpt);
},
i guess the best way to fix the object position relatively to the background will be to apply the inverse transformation of the one applied to the canvas for the zoom to the object.
So i wrote this function
function getNewVpt(point, value) {
var before = point,
vpt = canvas.viewportTransform.slice(0);
point = fabric.util.transformPoint(point, fabric.util.invertTransform(canvas.viewportTransform));
vpt[0] = value;
vpt[3] = value;
var after = fabric.util.transformPoint(point, vpt);
vpt[4] += before.x - after.x;
vpt[5] += before.y - after.y;
return vpt;
}
and i used it to rewrite the fabric.Object.prototype.transform
fabric.Object.prototype.transform = function (ctx) {
const obj = this;
const { ignoreZoom, group, canvas: objCanvas, left, top } = obj;
const {
contextTop,
viewportTransform,
} = objCanvas;
var needFullTransform =
(group && !group._transformDone) ||
(group && objCanvas && ctx === contextTop);
if (ignoreZoom && zoomingIsOn) {
zoomingIsOn = false;
var zoom = 1 / objCanvas.getZoom();
const oldP = new fabric.Point(left, top);
console.log('transform : oldP : ', oldP);
const newVpt = getNewVpt(oldP, zoom)
const newP = fabric.util.transformPoint(oldP, newVpt);
console.log('transform : newP : ', newP);
// here i tried to refresh the whole canvas with requestRenderAll()
this.set({
left: newP.x,
top: newP.y,
scaleX: zoom,
scaleY: zoom
});
this.setCoords();
console.log('transform : CALLING objCanvas.requestRenderAll() ');
objCanvas.requestRenderAll();
// but here i try refresh the object only which is better i think
// this.left = newP.x;
// this.top = newP.y;
// this.scaleX = zoom;
// this.scaleY = zoom;
// this.drawObject(ctx);
}
var m = this.calcTransformMatrix(!needFullTransform);
ctx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
};
And here i forked this new codesandbox for this second solution , the result seems to be better than the former solution but it still not perfect. What i may still be doing wrong ?!
// EDIT 2
I tried to pass objCanvas.getZoom() instead of zoom as second parameter to the getNewVpt() function. It seems there is some more improovement but still not perfect again
// Edit 3
In This codesandbox probably i got the best result i could get using another function which returns directly the new point:
function getNewPt(point, value) {
// TODO: just change the scale, preserve other transformations
var vpt = canvas.viewportTransform.slice(0);
point = fabric.util.transformPoint(point, fabric.util.invertTransform(canvas.viewportTransform));
vpt[0] = value;
vpt[3] = value;
return fabric.util.transformPoint(point, vpt);;
}
I still wish anybody who can tell me if there is a way to improove it more. As you can see the triangle returns back to its initial position after zooming/ dezooming and getting back to the same initial zoom value which is good but between those initial and final states , it still seems not to be in the right spot..
You just have to call zoomToPoint where it zooms and the objects will keep their position and scale relative to the background.
Try the following
canvas.on('mouse:wheel', function(opt) {
// console.log(opt.e.deltaY)
let zoomLevel = canvas.getZoom();
// console.log('zoom Level: ', (zoomLevel * 100).toFixed(0), '%');
zoomLevel += opt.e.deltaY * -0.01;
// Restrict scale
zoomLevel = Math.min(Math.max(.125, zoomLevel), 20);
canvas.zoomToPoint(
new fabric.Point(opt.e.offsetX, opt.e.offsetY),
zoomLevel,
);
canvas.renderAll();
})
Related
I want to drag and resize a rectangle in paperjs, I also want to rotate the rectangle and resize it while maintaining its relative dimensions.
Ideally I'd like to do so with my mouse by dragging one of its corners (anchors). What mathematics or feature is helpful in doing this in paperjs?
I have tried this by using scaling and modifying the corners but it doesn't work as I want it to. Could someone point me to a solution?
Thanks in advance.
Here's a simple solution that should get you started. It doesn't handle rotation because I'm not sure how you envision the UI working, but by modifying the bounding box to resize the rectangle you should be able to rotate it without problems.
paperjs sketch
I decided to make up my own UI and go ahead and make the example more complicated to address as much of you question as I can without more information. Here's the new sketch:
new sketch
The UI is
click in rectangle to move it by dragging
click on a corner and drag to resize it
control-click on a corner to rotate it
It's a bit tricky to click the corners, but that's an exercise left to the reader. They are colored circles just to emphasize where each segment point of the Path is located.
Key points of the code:
Use the rectangle's bounds to scale. Path.Rectangle is not a rectangle as far as paper is concerned. It is four curves (which happen to be straight) connecting four segment points. When you need to work with a rectangle to get its center, top left, etc., you need a Rectangle. Scale the visible rectangle by using the rectangle's bounds (Path.Rectangle.bounds). The code illustrates the bounds with an additional aqua rectangle so it's visible (it's easiest to see when rotating).
onMouseDown() sets the state for onMouseDrag() and sets up data needed for each state, e.g., saving the scale base for resizing.
onMouseDrag() implements moving, resizing, and rotating.
tool.onMouseDrag = function(e) {
if (rect.data.state === 'moving') {
rect.position = rect.position + e.point - e.lastPoint;
adjustRect(rect);
} else if (rect.data.state === 'resizing') {
// scale by distance from down point
var bounds = rect.data.bounds;
var scale = e.point.subtract(bounds.center).length /
rect.data.scaleBase.length;
var tlVec = bounds.topLeft.subtract(bounds.center).multiply(scale);
var brVec = bounds.bottomRight.subtract(bounds.center).multiply(scale);
var newBounds = new Rectangle(tlVec + bounds.center, brVec + bounds.center);
rect.bounds = newBounds;
adjustRect(rect);
} else if (rect.data.state === 'rotating') {
// rotate by difference of angles, relative to center, of
// the last two points.
var center = rect.bounds.center;
var baseVec = center - e.lastPoint;
var nowVec = center - e.point;
var angle = nowVec.angle - baseVec.angle;
rect.rotate(angle);
adjustRect(rect);
}
}
Moving is pretty easy - just calculate the difference between the current and last points from the event and change the position of the rectangle by that much.
Resizing is not as obvious. The strategy is to adjust the x and y bounds based on the original distance (scaleBase.length) between the mousedown point and the center of the rectangle. Note that while paper-full.js allows using operators ("+", "-", "*", "/") with points, I used the raw subtract() and multiply() methods a few times - I find it natural to chain the calculations that way.
Rotating uses the very nice paper concept that a point also defines a vector and a vector has an angle. It just notes the difference in the angles between the event lastPoint and point relative to the rectangle's center and rotates the rectangle by that difference.
moveCircles() and adjustRect() are just bookkeeping functions to update the corner circles and aqua rectangle.
Consider the following. I just went through the process of figuring this out, based on lots of examples.
My Goals:
use my own bounding box when selecting an item
Move, Resize, and Rotate (with snap to rotation [45 degrees]) the selected item
Show a title / name of the item
Example Sketch
Paper.js Code
var hitOptions = {
segments: true,
stroke: true,
fill: true,
tolerance: 5
};
function drawHex(w, c, n){
var h = new Path.RegularPolygon(new Point(100, 100), 6, w / 2);
h.selectedColor = 'transparent';
c = c != undefined ? c : "#e9e9ff";
n = n != undefined ? n : "Hexayurt";
h.name = n;
h.fillColor = c;
h.data.highlight = new Group({
children: [makeBounds(h), makeCorners(h), makeTitle(h)],
strokeColor: '#a2a2ff',
visible: false
});
return h;
}
function makeCorners(o, s){
s = s != undefined ? s : 5;
var g = new Group();
var corners = [
o.bounds.topLeft,
o.bounds.topRight,
o.bounds.bottomLeft,
o.bounds.bottomRight
];
corners.forEach(function(corner, i) {
var h = new Path.Rectangle({
center: corner,
size: s
});
g.addChild(h);
});
return g;
}
function makeBounds(o){
return new Path.Rectangle({
rectangle: o.bounds
});
}
function makeTitle(o, n, c){
c = c != undefined ? c : 'black';
var t = new PointText({
fillColor: c,
content: n != undefined ? n : o.name,
strokeWidth: 0
});
t.bounds.center = o.bounds.center;
return t;
}
function selectItem(o){
console.log("Select Item", o.name);
o.selected = true;
o.data.highlight.visible = true;
o.data.highlight.bringToFront();
}
function clearSelected(){
project.selectedItems.forEach(function(o, i){
console.log("Unselect Item", o.name);
o.data.highlight.visible = false;
});
project.activeLayer.selected = false;
}
function moveBoxes(o){
var boxes = o.data.highlight.children[1].children;
boxes[0].position = o.bounds.topLeft;
boxes[1].position = o.bounds.topRight;
boxes[2].position = o.bounds.bottomLeft;
boxes[3].position = o.bounds.bottomRight;
}
function moveTitle(o){
var t = o.data.highlight.children[2];
t.bounds.center = o.bounds.center;
}
function adjustBounds(o){
if(o.data.state == "moving"){
o.data.highlight.position = o.position;
} else {
o.data.highlight.children[0].bounds = o.bounds;
moveBoxes(o);
}
}
var hex1 = drawHex(200);
console.log(hex1.data, hex1.data.highlight);
var segment, path;
var movePath = false;
var tool = new Tool();
tool.minDistance = 10;
tool.onMouseDown = function(event) {
segment = path = null;
var hitResult = project.hitTest(event.point, hitOptions);
if (!hitResult){
clearSelected();
return;
}
if(hitResult && hitResult.type == "fill"){
path = hitResult.item;
}
if (hitResult && hitResult.type == "segment") {
path = project.selectedItems[0];
segment = hitResult.segment;
if(event.modifiers.control){
path.data.state = "rotating";
} else {
path.data.state = "resizing";
path.data.bounds = path.bounds.clone();
path.data.scaleBase = event.point - path.bounds.center;
}
console.log(path.data);
}
movePath = hitResult.type == 'fill';
if (movePath){
project.activeLayer.addChild(hitResult.item);
path.data.state = "moving";
selectItem(path);
console.log("Init Event", path.data.state);
}
};
tool.onMouseDrag = function(event) {
console.log(path, segment, path.data.state);
if (segment && path.data.state == "resizing") {
var bounds = path.data.bounds;
var scale = event.point.subtract(bounds.center).length / path.data.scaleBase.length;
var tlVec = bounds.topLeft.subtract(bounds.center).multiply(scale);
var brVec = bounds.bottomRight.subtract(bounds.center).multiply(scale);
var newBounds = new Rectangle(tlVec + bounds.center, brVec + bounds.center);
path.bounds = newBounds;
adjustBounds(path);
} else if(segment && path.data.state == "rotating") {
var center = path.bounds.center;
var baseVec = center - event.lastPoint;
var nowVec = center - event.point;
var angle = nowVec.angle - baseVec.angle;
if(angle < 0){
path.rotate(-45);
} else {
path.rotate(45);
}
adjustBounds(path);
} else if (path && path.data.state == "moving") {
path.position += event.delta;
adjustBounds(path);
}
};
This makes use of .data to store references of the bounding box, handles, and title as a Group. This way, they are always there, they can just visible true or false. This makes it easy to show and hide them as needed.
drawHex( width , color, name )
Width - Required, number of pixels wide
Color - Optional, string that defines the Fill Color. Default: #e9e9ff
Name - Optional, string to be used as the Name and Title. Default: "Hexayurt"
Interactions
click - Select item (show bounding box & Handles)
click + drag - Move item
click + drag handle - Resize item
Ctrl + click + drag handle - Rotate item
This is my first pass at it and I may cleanup a lot of the code. For example, I could bind events to the handles specifically instead of looking at more global events.
I ran XQueryTree on all my windows and I got them in z-order from topmost at top of the array to bottom-most at bottom of array.
I then filtered out only what is visible by doing XGetWindowAttributes on each and removing it if it is not map_state of IsVisible
For each visible window, I check and get the _NET_WM_PID, _NET_WM_NAME, x, y, height (as height + border_width), and width (as width + border_width).
My data is at this gist and also at bottom: https://gist.github.com/Noitidart/94562d08f243cd7ca7ec
My setup is of two monitors. And this is a fullscreenshot of them both:
There is one transparent window over each monitor, that is the height and width of the monitor, their titles are "nativeshot_canvas".
So looking through the data I see that windows are broken up into multiple entries by XQueryTree. My manual analysis tells me this:
"nativeshot_canvas" on left monitor
"nativeshot_canvas" on right monitor
UNKNOWN WINDOW 1
UNKNOWN WINDOW 2
horizontal menu bar on right monitor
horizontal menu bar on left monitor
vertical dock on right monitor
vertical dock on left monitor
UNKNOWN WINDOW 3
"Javascript Application" window
"Browser Console" window
"Mozilla Firefox" window with two tabs
"Desktop"
A graphic of the data and my manual analysis is below.
THE QUESTION
Is there a way to programmatically identify which entries correlate to a single window? In my manual analysis I used some patterns I came up with in that each window group starts with either a _NET_WM_PID (pid key in data below) or a _NET_WM_NAME (title key in data below). I don't think this is a good pattern because we see at top things have a PID but their width and height are 1.
edit: still stuck on how finding a gurantteed way to correlate these divisions into groups of windows. i basically need to get all x, y, width, and height of all the windows out there, if anyone has any input i would be very greatful
My code to list out the XQueryTree data:
var xqRoot = ostypes.TYPE.Window();
var xqParent = ostypes.TYPE.Window();
var xqChildArr = ostypes.TYPE.Window.ptr();
var nChilds = ostypes.TYPE.unsigned_int();
var gpTypeReturned = ostypes.TYPE.Atom();
var gpFormatReturned = ostypes.TYPE.int();
var gpNItemsReturned = ostypes.TYPE.unsigned_long();
var gpBytesAfterReturn = ostypes.TYPE.unsigned_long();
var gpItemsArr = ostypes.TYPE.unsigned_char.ptr();
var geoRoot = ostypes.TYPE.Window();
var geoX = ostypes.TYPE.int();
var geoY = ostypes.TYPE.int();
var geoW = ostypes.TYPE.unsigned_int();
var geoH = ostypes.TYPE.unsigned_int();
var geoBorderWidth = ostypes.TYPE.unsigned_int();
var geoDepth = ostypes.TYPE.unsigned_int();
var wAttr = ostypes.TYPE.XWindowAttributes();
var processWin = function(w) {
if (aOptions.filterVisible) {
var rez_WA = ostypes.API('XGetWindowAttributes')(ostypes.HELPER.cachedXOpenDisplay(), w, wAttr.address());
console.info('wAttr.map_state:', wAttr.map_state.toString());
if (!cutils.jscEqual(wAttr.map_state, ostypes.CONST.IsViewable)) {
return; // continue as this is a hidden window, do not list features, do not dig this window
}
}
var thisWin = {};
// fetch props on thisWin
thisWin.hwndXid = parseInt(cutils.jscGetDeepest(w));
if (aOptions.getPid) {
var rez_pid = ostypes.API('XGetWindowProperty')(ostypes.HELPER.cachedXOpenDisplay(), w, ostypes.HELPER.cachedAtom('_NET_WM_PID'), 0, 1, ostypes.CONST.False, ostypes.CONST.XA_CARDINAL, gpTypeReturned.address(), gpFormatReturned.address(), gpNItemsReturned.address(), gpBytesAfterReturn.address(), gpItemsArr.address());
if (ostypes.HELPER.getWinProp_ReturnStatus(ostypes.CONST.XA_CARDINAL, gpTypeReturned, gpFormatReturned, gpBytesAfterReturn) == 1) {
var jsN = parseInt(cutils.jscGetDeepest(gpNItemsReturned));
if (jsN == 0) {
thisWin.pid = null; // set to null as this window did not have a pid, but i add the key indicating i tested for it and the window had the proerty
} else {
//console.info('gpItemsArr:', gpItemsArr.toString(), 'casted:', ctypes.cast(gpItemsArr, ostypes.TYPE.CARD32).toString(), 'casted to single el:', ctypes.cast(gpItemsArr, ostypes.TYPE.CARD32.array(1).ptr).contents.toString()); // "gpItemsArr:" "ctypes.unsigned_char.ptr(ctypes.UInt64("0x7f229409d710"))" "casted:" "ctypes.unsigned_int(2483672848)" "casted to single el:" "ctypes.unsigned_int.array(1)([2212])" // showing that it must be cast and not just to type cuz its single element, but to array of 1 element
thisWin.pid = parseInt(cutils.jscGetDeepest(ctypes.cast(gpItemsArr, ostypes.TYPE.CARD32.array(1).ptr).contents[0]));
}
ostypes.API('XFree')(gpItemsArr);
} else {
thisWin.pid = undefined; // window didnt even have property
}
}
if (aOptions.getTitle) {
var rez_title = ostypes.API('XGetWindowProperty')(ostypes.HELPER.cachedXOpenDisplay(), w, ostypes.HELPER.cachedAtom('_NET_WM_NAME'), 0, 256 /* this number times 4 is maximum ctypes.char that can be returned*/, ostypes.CONST.False, ostypes.HELPER.cachedAtom('UTF8_STRING'), gpTypeReturned.address(), gpFormatReturned.address(), gpNItemsReturned.address(), gpBytesAfterReturn.address(), gpItemsArr.address());
if (ostypes.HELPER.getWinProp_ReturnStatus(ostypes.HELPER.cachedAtom('UTF8_STRING'), gpTypeReturned, gpFormatReturned, gpBytesAfterReturn) == 1) {
var jsN = parseInt(cutils.jscGetDeepest(gpNItemsReturned));
if (jsN == 0) {
thisWin.title = ''; // window had property but not title
} else {
thisWin.title = ctypes.cast(gpItemsArr, ostypes.TYPE.char.array(jsN).ptr).contents.readString();
}
ostypes.API('XFree')(gpItemsArr);
} else {
thisWin.title = undefined; // window didnt even have property
}
}
if (aOptions.getBounds) {
if (aOptions.filterVisible) {
// then get the info from wAttr as its already available
thisWin.left = parseInt(cutils.jscGetDeepest(wAttr.x));
thisWin.top = parseInt(cutils.jscGetDeepest(wAttr.y));
var borderWidth = parseInt(cutils.jscGetDeepest(wAttr.border_width));
thisWin.borderWidth = borderWidth;
thisWin.width = parseInt(cutils.jscGetDeepest(wAttr.width))/* + borderWidth*/;
thisWin.height = parseInt(cutils.jscGetDeepest(wAttr.height))/* + borderWidth*/;
thisWin.right = thisWin.left + thisWin.width;
thisWin.bottom = thisWin.top + thisWin.height;
} else {
var rez_bounds = ostypes.API('XGetGeometry')(ostypes.HELPER.cachedXOpenDisplay(), w, geoRoot.address(), geoX.address(), geoY.address(), geoW.address(), geoH.address(), geoBorderWidth.address(), geoDepth.address());
thisWin.left = parseInt(cutils.jscGetDeepest(geoX));
thisWin.top = parseInt(cutils.jscGetDeepest(geoY));
var borderWidth = parseInt(cutils.jscGetDeepest(wAttr.border_width));
thisWin.borderWidth = borderWidth;
thisWin.width = parseInt(cutils.jscGetDeepest(wAttr.width))/* + borderWidth*/;
thisWin.height = parseInt(cutils.jscGetDeepest(wAttr.height))/* + borderWidth*/;
thisWin.right = thisWin.left + thisWin.width;
thisWin.bottom = thisWin.top + thisWin.height;
}
}
rezWinArr.splice(0, 0, thisWin);
// dig the win even if it doesnt qualify
var rez_XQ = ostypes.API('XQueryTree')(ostypes.HELPER.cachedXOpenDisplay(), w, xqRoot.address(), xqParent.address(), xqChildArr.address(), nChilds.address()); // interesting note about XQueryTree and workspaces: "The problem with this approach is that it will only return windows on the same virtual desktop. In the case of multiple virtual desktops, windows on other virtual desktops will be ignored." source: http://www.experts-exchange.com/Programming/System/Q_21443252.html
var jsNC = parseInt(cutils.jscGetDeepest(nChilds));
if (jsNC > 0) {
var jsChildArr = ctypes.cast(xqChildArr, ostypes.TYPE.Window.array(jsNC).ptr).contents;
// for (var i=jsNC-1; i>-1; i--) {
for (var i=0; i<jsNC; i++) {
var wChild = jsChildArr[i];
processWin(wChild);
}
ostypes.API('XFree')(xqChildArr);
}
}
processWin(ostypes.HELPER.cachedDefaultRootWindow());
I am having trouble with pixi.js I am creating a page like http://www.wolverineunleashed.com/#muscles I have created a big stage which the user can use their drag their way around, it all works fine apart from when the user is dragging, the images shake on the screen, I am thinking that it might be a rendering issue? But at the moment I am pulling my hair out so any help would be most grateful. The code I have is:
var w = window.innerWidth;
var h = window.innerHeight;
var images = [];
var stage = new PIXI.Container();
var renderer = new PIXI.autoDetectRenderer(w, h,{transparent:true},true);
document.body.appendChild(renderer.view);
var background = new PIXI.Container();
background.parent = background;
background.interactive = true;
background.on('mousedown', onDragStart)
.on('touchstart', onDragStart)
.on('mouseup', onDragEnd)
.on('mouseupoutside', onDragEnd)
.on('touchend', onDragEnd)
.on('touchendoutside', onDragEnd)
.on('mousemove', onDragMove)
.on('touchmove', onDragMove);
loadImages();
requestAnimationFrame(animate);
function animate() {
requestAnimationFrame(animate);
renderer.render(background);
}
function onDragStart(event)
{
this.data = event.data;
this.mousePressPoint = [];
this.dragging = true;
this.mousePressPoint[0] = this.data.getLocalPosition(this.parent).x - this.position.x;
this.mousePressPoint[1] = this.data.getLocalPosition(this.parent).y - this.position.y;
}
function onDragEnd()
{
this.dragging = false;
this.data = null;
}
function onDragMove()
{
if (this.dragging)
{
var position = this.data.getLocalPosition(this.parent);
this.position.x = parseInt(position.x - this.mousePressPoint[0]);
this.position.y = parseInt(position.y - this.mousePressPoint[1]);
}
}
This is a very old question, and the PIXI library has changed since the OP, but I was having problems with this myself, and I don't feel the question yet has a good answer...
Some of the PIXI API has changed since the OP, but see the PIXI documentation for the dragging bunnies example.
The jumpiness of the sprite when it first starts to drag is because the sprite is always positioned relative to the anchor/pivot point. Because of this, the position of the anchor point follows the position of the mouse when dragging (at least in the code above). If you click in the center of the bunnies, you don't notice, but clicking on the very edge, produces a very noticeable jump. When using significantly larger sprites (as in the example linked in the OP), this jump can become quite glaring.
Here is how I fixed it:
Very little needs to be changed from the PIXI demo. onDragEnd and onDragMove remain identical:
function onDragEnd(){
this.dragging=false;
this.data = null;
}
function onDragMove(){
if (this.dragging){
let newPosition = this.data.getLocalPosition(this.parent);
this.x = newPosition.x;
this.y = newPosition.y;
}
}
However, we need to update the pivot point to the location of the click event within the onDragStart function like so:
function onDragStart(event){
this.data = event.data;
//store this variable for convenience
let position = this.data.getLocalPosition(this);
// Set the pivot point to the new position
this.pivot.set(position.x, position.y)
// update the new position of the sprite to the position obtained through
// the global data. This ensures the position lines up with the location of
// the mouse on the screen. I'm not certain why, but this is necessary.
this.position.set(this.data.global.x, this.data.global.y)
this.dragging = true;
}
With this set up, dragging sprites will be smooth, regardless of size. Excellent for creating a click-and-drag-to-explore kind of environment, as linked in the OP.
Hope this helps someone else two years in the future.
I do hope that you have already figured out how to fix this issue, but since there is no answer on here, I'm going to guide you in the right direction.
The code you give us is missing the loadImages() function, but I believe that we should be able to solve it anyway.
The problem seems to lie in this code snippet:
var background = new PIXI.Container();
background.parent = background;
background.interactive = true;
background.on('mousedown', onDragStart)
.on('touchstart', onDragStart)
.on('mouseup', onDragEnd)
.on('mouseupoutside', onDragEnd)
.on('touchend', onDragEnd)
.on('touchendoutside', onDragEnd)
.on('mousemove', onDragMove)
.on('touchmove', onDragMove);
Here you are making the background container interactive and giving it all the event handlers.
What you should be doing instead is making each individual sprite/image interactive and giving them the eventhandlers that move it around.
I added the following code to your code:
// Create a sprite from some image
var sprite = new PIXI.Sprite.fromImage('some_image.png');
// Make the sprite interactive. should be done to each individual sprite
sprite.interactive = true;
// Set the anchor in the center of our sprite
sprite.anchor.x = 0.5;
sprite.anchor.y = 0.5;
// Position our sprite in the center of the renderer
sprite.position.x = renderer.width / 2;
sprite.position.y = renderer.height / 2;
sprite.on('mousedown', onDragStart)
.on('touchstart', onDragStart)
.on('mouseup', onDragEnd)
.on('mouseupoutside', onDragEnd)
.on('touchend', onDragEnd)
.on('touchendoutside', onDragEnd)
.on('mousemove', onDragMove)
.on('touchmove', onDragMove);
background.addChild(sprite);
And you should put this inside your loadImages function. And then make that function iterate through each image, giving them the event handlers and options they need for this to work.
Here is the code, based on yours, that works.
var w = window.innerWidth;
var h = window.innerHeight;
var images = [];
var stage = new PIXI.Container();
var renderer = new PIXI.autoDetectRenderer(w, h,{transparent:true},true);
document.body.appendChild(renderer.view);
var background = new PIXI.Container();
// Create a sprite from some image
var sprite = new PIXI.Sprite.fromImage('some_image.png');
// Make the sprite interactive. should be done to each individual sprite
sprite.interactive = true;
// Set the anchor in the center of our sprite
sprite.anchor.x = 0.5;
sprite.anchor.y = 0.5;
// Position our sprite in the center of the renderer
sprite.position.x = renderer.width / 2;
sprite.position.y = renderer.height / 2;
sprite.on('mousedown', onDragStart)
.on('touchstart', onDragStart)
.on('mouseup', onDragEnd)
.on('mouseupoutside', onDragEnd)
.on('touchend', onDragEnd)
.on('touchendoutside', onDragEnd)
.on('mousemove', onDragMove)
.on('touchmove', onDragMove);
background.addChild(sprite);
requestAnimationFrame(animate);
function animate() {
requestAnimationFrame(animate);
renderer.render(background);
}
function onDragStart(event)
{
this.data = event.data;
this.mousePressPoint = [];
this.dragging = true;
}
function onDragEnd()
{
this.dragging = false;
this.data = null;
}
function onDragMove()
{
if (this.dragging)
{
var position = this.data.getLocalPosition(this.parent);
this.position.x = position.x;
this.position.y = position.y;
}
}
Update
Since the problem has been found I've also find out that Box2D for web is leaking on every side :/
To show this I made a simple circle moving in a static polygon and here is the result after some time.
Notice how the following items are leaking as I'm not creating any body or changing the world in any way:
b2Vec2
Features
b2ManifoldPoint
b2ContactID
b2Manifold
b2ContactEdge
b2PolyAndCircleContact
Array
...
Original post
I have a problem because I'm profiling my game and the garbage collector doesnt' delete my bodies, contacts and other stuff. Then I've looked at what are they keeping from the GC and was the Box2D itself. This might lead to 2 options: I'm doing it bad or Box2D is leaking. I consider is my cause.
What exactly is keeping it?
contact.m_nodeA.other was appearing to be the most used to keep it from GC.
other times: m_fixtureB in a contact... see image
You can see that the body has a __destroyed property. That is set manually before deleting it with world.DestroyBody(body)
When I destroy a body I call it after I call the step method on the world.
As you can see from the box2d method it doesn't get rid of the other variable nor it changes it to another body and my body is not GC.
Any idea of what I'm missing here?
Now I can fix the problem only if the world.Step is not ran:
var gravity = new Box2D.Vec2(0, 0);
var doSleep = true;
var world = new Box2D.World(gravity, doSleep);
var step = false;
var fixtureDef = new Box2D.FixtureDef();
fixtureDef.density = 1.0;
fixtureDef.friction = 0.5;
fixtureDef.restitution = 0.2;
fixtureDef.shape = new Box2D.PolygonShape();
fixtureDef.shape.SetAsBox(1, 1);
var bodyDef = new Box2D.BodyDef;
bodyDef.type = Box2D.Body.b2_dynamicBody;
bodyDef.position.x = 0.4;
bodyDef.position.y = 0.4;
var bodies = []
var fix = [];
window.c = function(){
for(var i = 0; i < 100; i++){
var body = world.CreateBody(bodyDef);
body._id = i;
fix.push(body.CreateFixture(fixtureDef));
bodies.push(body);
}
if(step){world.Step(1/60, 3, 3); world.ClearForces();}
console.log('Created', bodies)
fixtureDef = null;
bodyDef = null;
}
window.d = function(){
_.each(bodies, function(body, i){
body.DestroyFixture(fix[i]);
world.DestroyBody(body);
fix[i] = null;
bodies[i] = null;
})
if(step){world.Step(1/60, 3, 3); world.ClearForces();}
bodies = null;
fix = null;
}
Change the step to true and the memory leak problem appears again.
Reproduce the memory leak problem:
Code in your file:
var gravity = new Box2D.Vec2(0, 0);
var doSleep = true;
var world = new Box2D.World(gravity, doSleep);
var bodies = []
window.c = function(){
for(var i = 0; i < 100; i++){
var bodyDef = new Box2D.BodyDef();
bodyDef.type = 2;
var shape = new Box2D.PolygonShape();
shape.SetAsBox(1, 1);
var fixtureDef = new Box2D.FixtureDef();
fixtureDef.shape = shape;
var body = world.CreateBody(bodyDef);
body._id = i;
body.CreateFixture(fixtureDef);
bodies.push(body);
}
world.Step(0.3, 3, 3);
console.log('Created', bodies)
}
window.d = function(){
_.each(bodies, function(body, i){
world.DestroyBody(body);
bodies[i] = null;
})
world.Step(0.3, 3, 3);
bodies = null;
}
Open google chrome:
Then open your profile and make a snapshot.
Now run the c() method in your console to create 100 bodies
Now snapshot 2
Search in snapshot for b2Body and you'll find 100 Object count
Now run d() to delete all your bodies;
Force Garbage collection by clicking on the garbage can
Make a snapshot 3
Search for b2Body and you'll also find 100 Object count
At the last step should only be 0 objects as they have been destroyed. Instead of this you'll find this:
Now you can see there are a lot of references from b2ContactEdge. Now if you remove the world.Step part of the code you will only see 2 references to the body.
If you remove this line
body.CreateFixture(fixtureDef);
or making the body static is not leaking anymore.
My game loop
...gameLoop = function(o){
// used a lot here
var world = o.world;
// calculate the new positions
var worldStepSeconds = o.worldStepMs / 1000;
// step world
world.Step(worldStepSeconds, o.velocityIterations, o.positionIterations)
// render debug
if(o.renderDebug){
world.DrawDebugData();
}
// always to not accumulate forces, maybe some bug occurs
world.ClearForces();
// tick all ticking entities
_.each(o.getTickEntitiesFn(), function(actor){
if(!actor) return;
actor.tick(o.worldStepMs, o.lastFrameMs);
})
// update PIXI entities
var body = world.GetBodyList();
var worldScale = world.SCALE;
var destroyBody = world.DestroyBody.bind(world);
while(body){
var actor = null;
var visualEntity = null;
var box2DEntity = o.getBox2DEntityByIdFn(body.GetUserData());
if(box2DEntity){
visualEntity = o.getVisualEntityByIdFn(box2DEntity.getVisualEntityId());
if(box2DEntity.isDestroying()){
// optimization
body.__destroyed = true;
world.DestroyBody(body);
box2DEntity.completeDestroy();
}
}
if(visualEntity){
if(visualEntity.isDestroying()){
visualEntity.completeDestroy();
}else{
var inverseY = true;
var bodyDetails = Utils.getScreenPositionAndRotationOfBody(world, body, inverseY);
visualEntity.updateSprite(bodyDetails.x, bodyDetails.y, bodyDetails.rotation);
}
}
// this delegates out functionality for each body processed
if(o.triggersFn.eachBody) o.triggersFn.eachBody(world, body, visualEntity);
body = body.GetNext();
}
// when a joint is created is then also created it's visual counterpart and then set to userData.
var joint = world.GetJointList();
while(joint){
var pixiGraphics = joint.GetUserData();
if(pixiGraphics){
// In order to draw a distance joint we need to know the start and end positions.
// The joint saves the global (yes) anchor positions for each body.
// After that we need to scale to our screen and invert y axis.
var anchorA = joint.GetAnchorA();
var anchorB = joint.GetAnchorB();
var screenPositionA = anchorA.Copy();
var screenPositionB = anchorB.Copy();
// scale
screenPositionA.Multiply(world.SCALE);
screenPositionB.Multiply(world.SCALE);
// invert y
screenPositionA.y = world.CANVAS_HEIGHT - screenPositionA.y
screenPositionB.y = world.CANVAS_HEIGHT - screenPositionB.y
// draw a black line
pixiGraphics.clear();
pixiGraphics.lineStyle(1, 0x000000, 0.7);
pixiGraphics.moveTo(screenPositionA.x, screenPositionA.y);
pixiGraphics.lineTo(screenPositionB.x, screenPositionB.y);
}
joint = joint.GetNext();
}
// render the PIXI scene
if(o.renderPixi){
o.renderer.render(o.stage)
}
// render next frame
requestAnimFrame(o.requestAnimFrameFn);
}
Code from Box2d:
b2ContactManager.prototype.Destroy = function (c) {
var fixtureA = c.GetFixtureA();
var fixtureB = c.GetFixtureB();
var bodyA = fixtureA.GetBody();
var bodyB = fixtureB.GetBody();
if (c.IsTouching()) {
this.m_contactListener.EndContact(c);
}
if (c.m_prev) {
c.m_prev.m_next = c.m_next;
}
if (c.m_next) {
c.m_next.m_prev = c.m_prev;
}
if (c == this.m_world.m_contactList) {
this.m_world.m_contactList = c.m_next;
}
if (c.m_nodeA.prev) {
c.m_nodeA.prev.next = c.m_nodeA.next;
}
if (c.m_nodeA.next) {
c.m_nodeA.next.prev = c.m_nodeA.prev;
}
if (c.m_nodeA == bodyA.m_contactList) {
bodyA.m_contactList = c.m_nodeA.next;
}
if (c.m_nodeB.prev) {
c.m_nodeB.prev.next = c.m_nodeB.next;
}
if (c.m_nodeB.next) {
c.m_nodeB.next.prev = c.m_nodeB.prev;
}
if (c.m_nodeB == bodyB.m_contactList) {
bodyB.m_contactList = c.m_nodeB.next;
}
this.m_contactFactory.Destroy(c);
--this.m_contactCount;
}
b2ContactFactory.prototype.Destroy = function (contact) {
if (contact.m_manifold.m_pointCount > 0) {
contact.m_fixtureA.m_body.SetAwake(true);
contact.m_fixtureB.m_body.SetAwake(true);
}
var type1 = parseInt(contact.m_fixtureA.GetType());
var type2 = parseInt(contact.m_fixtureB.GetType());
var reg = this.m_registers[type1][type2];
if (true) {
reg.poolCount++;
contact.m_next = reg.pool;
reg.pool = contact;
}
var destroyFcn = reg.destroyFcn;
destroyFcn(contact, this.m_allocator);
}
I have the same problem, but I think I find out from where it comes.
Instead of m_* try functions, like GetFixtureA() instead of m_fixtureA.
Totti did you ever figure this out? It looks like box2dweb requires manual destruction and memory management.
I think I have found your leaks, un-implemented ( static class ) destruction functions:
b2Joint.Destroy = function (joint, allocator) {}
b2CircleContact.Destroy = function (contact, allocator) {}<
b2PolygonContact.Destroy = function (contact, allocator) {}
b2EdgeAndCircleContact.Destroy = function (contact, allocator) {}<
b2PolyAndCircleContact.Destroy = function (contact, allocator) {}
b2PolyAndEdgeContact.Destroy = function (contact, allocator) {}
[UPDATE...]
b2DestructionListener.b2DestructionListener = function () {};
b2DestructionListener.prototype.SayGoodbyeJoint = function (joint) {}
b2DestructionListener.prototype.SayGoodbyeFixture = function (fixture) {}
b2Contact.prototype.Reset(fixtureA, fixtureB)
called with with one/both fixture arguments resets passed in fixture/s BUT ALSO pass in NO arguments and it 'nulls' all the the b2Contact properties! (UNTESTED:) but I suggest set your YOURcontactListener class up to handle all contact callbacks EVERY call with Reset(??) dynamically configureable as logic requies EVERY call (there are more than you'd imagine each and every world step).
Also take Colt McAnlis clever advice and strategically pre allocate all the memory the life of your game will need (by creating game and box2d object pools now you know objects can be reset) so the garbage collector NEVER runs until, you destroy object pools at times of your own convenience.... i.e when you close the tab, or your device needs recharging! ;D [...UPDATE]
// you can define and assign your own contact listener ...via...
YOUR.b2world.b2ContactManager.m_world.m_contactList = new YOURcontactlistener();<br>[edit]...if you dont it actually does have Box2D.Dynamics.b2ContactListener.b2_defaultListener.
// box2d in the worldStep calls YOURcontactlistener.update() via:
this.b2world.b2ContactManager.m_world.m_contactList.Update(this.m_contactListener) // this.m_contactListener being YOURS || b2_defaultListener;
// which instantiates ALL your listed leaking object like so:
{b2Contact which instantiates {b2ContactEdge} and {b2Manifold which instantiates {b2ManifoldPoint{which instantiates m_id.key == ContactID{which instantiates Features}}}} along with {B2Vec2} are instantiated in b2ContactResult ...which I can not actually find but assume it must be instantiated in the Solver.
// There is a Contacts.destroyFcn callback is CREATED in....
b2ContactFactory.prototype.Destroy = function (contact) {...}
// then Contacts.destroyFcn callback(s) are privately REGISTERED in....
b2ContactFactory.prototype.InitializeRegisters() {...}
...via...
this.AddType = function (createFcn, destroyFcn, type1, type2) {...}
...BUT... THOSE privately registered ARE four of the un-implimented static class function from above...
b2PolygonContact.Destroy = function (contact, allocator) {}
b2EdgeAndCircleContact.Destroy = function (contact, allocator) {}
b2PolyAndCircleContact.Destroy = function (contact, allocator) {}
b2PolyAndEdgeContact.Destroy = function (contact, allocator) {}
So I havn't tested it yet but it looks like box2dweb just gives you the Destroy callback/handler functions and you have to read the source to find all the properties you need to null. [Edit] In combination with b2Contact.prototype.Reset(fixtureA, fixtureB)
But either way pretty confident the functions above(possibly incomplete) are callback/handlers, and can be used to null your way back to performance for anyone else who stumbles across this problem. Pretty sure Totti's moved on(dont forget to handle your 'this' scope in callbacks).
I know that I can easily allow a user to select multiple Features/Geometries in OpenLayers but I then want enable the user to easily drag/move all of the selected features at the same time.
With the ModifyFeature control it only moves one feature at a time ... is there a way to easily extend this control (or whatever works) to move all of the selected features on that layer?
Okay, skip the ModifyFeature control and just hook into the SelectFeature control to keep track of the selected features and then use the DragControl to manipulate the selected points at the same time.
Example of the control instantiation:
var drag = new OpenLayers.Control.DragFeature(vectors, {
onStart: startDrag,
onDrag: doDrag,
onComplete: endDrag
});
var select = new OpenLayers.Control.SelectFeature(vectors, {
box: true,
multiple: true,
onSelect: addSelected,
onUnselect: clearSelected
});
Example of the event handling functions:
/* Keep track of the selected features */
function addSelected(feature) {
selectedFeatures.push(feature);
}
/* Clear the list of selected features */
function clearSelected(feature) {
selectedFeatures = [];
}
/* Feature starting to move */
function startDrag(feature, pixel) {
lastPixel = pixel;
}
/* Feature moving */
function doDrag(feature, pixel) {
for (f in selectedFeatures) {
if (feature != selectedFeatures[f]) {
var res = map.getResolution();
selectedFeatures[f].geometry.move(res * (pixel.x - lastPixel.x), res * (lastPixel.y - pixel.y));
vectors.drawFeature(selectedFeatures[f]);
}
}
lastPixel = pixel;
}
/* Featrue stopped moving */
function endDrag(feature, pixel) {
for (f in selectedFeatures) {
f.state = OpenLayers.State.UPDATE;
}
}
Hmm...
I tried the code above, and couldn't make it work. Two issues:
1) To move each feature, you need to use the original position of that feature, and add the "drag vector" from whatever feature the DragControl is moving around by itself (i.e. the feature-parameter to doDrag).
2) Since DragFeatures own code sets lastPixel=pixel before calling onDrag, the line calling move() will move the feature to (0,0).
My code looks something like this:
var lastPixels;
function startDrag(feature, pixel) {
// save hash with selected features start position
lastPixels = [];
for( var f=0; f<wfs.selectedFeatures.length; f++){
lastPixels.push({ fid: layer.selectedFeatures[f].fid,
lastPixel: map.getPixelFromLonLat( layer.selectedFeatures[f].geometry.getBounds().getCenterLonLat() )
});
}
}
function doDrag(feature, pixel) {
/* because DragFeatures own handler overwrites dragSelected.lastPixel with pixel before this is called, calculate drag vector from movement of "feature" */
var g = 0;
while( lastPixels[g].fid != feature.fid ){ g++; }
var lastPixel = lastPixels[g].lastPixel;
var currentCenter = map.getPixelFromLonLat( feature.geometry.getBounds().getCenterLonLat() );
var dragVector = { dx: currentCenter.x - lastPixel.x, dy: lastPixel.y - currentCenter.y };
for( var f=0; f<layer.selectedFeatures.length; f++){
if (feature != layer.selectedFeatures[f]) {
// get lastpixel of this feature
lastPixel = null;
var h = 0;
while( lastPixels[h].fid != layer.selectedFeatures[f].fid ){ h++; }
lastPixel = lastPixels[h].lastPixel;
var newPixel = new OpenLayers.Pixel( lastPixel.x + dragVector.dx, lastPixel.y - dragVector.dy );
// move() moves polygon feature so that centre is at location given as parameter
layer.selectedFeatures[f].move(newPixel);
}
}
}
I had a similar problem and solved it by overriding DragFeature's moveFeature function and putting this.lastPixel = pixel inside the for loop that applies the move to all features within my layer vector. Until I moved this.lastPixel = pixel inside the loop, all features except the one being dragged got crazily distorted.
`OpenLayers.Control.DragFeature.prototype.moveFeature = function (pixel) {
var res = this.map.getResolution();
for (var i = 0; i < vector.features.length; i++) {
var feature = vector.features[i];
feature .geometry.move(res * (pixel.x - this.lastPixel.x),
res * (this.lastPixel.y - pixel.y));
this.layer.drawFeature(feature );
this.lastPixel = pixel;
}
this.onDrag(this.feature, pixel);
};
`