I'm writing a simple drawing "thing" for our users. It uses SVG. All of the users have iPads, either 4s or Air 2s. I'm testing on a 2 (2nd gen, not Air 2). All iPads are running iOS 9.2 and using Safari.
When free-hand drawing on the SVG element, there is substantial lag. On the 2nd gen it's "almost tolerable", on the 4s it's "incredibly painful". Not sure about the Air 2s, but if it's scaling the way the 4s did, I'd say it's "kill me now". Interesting how the better hardware lags more, but I suppose it could have something to do with the increased resolution, but still...
Is there anything I can do to improve the performance? The lag is also noticible when drawing ready shapes (rectangle, line (path), and ellipse), but it's something we can live with.
Here's the code I'm using to bind the event handlers (using jQuery) for the free-hand drawing.
Floor.bindFreeHand = function (e) {
e.preventDefault();
var sPB, d, p1, segments;
Floor._jElement.off(EventTypes.pointerDown + " " + EventTypes.pointerUp).on(EventTypes.pointerDown, function (e) {
var pI1 = PointerInfo.parse(e);
sPB = new SvgPathsBuilder();
d = sPB.moveTo(pI1.x, pI1.y).d();
p1 = Svg.path(d).attr({
"fill": "none",
"stroke": Floor._color,
"stroke-linecap": "round",
"stroke-linejoin": "round",
"stroke-opacity": Floor._opacity,
"stroke-width": Floor._width
});
segments = p1.element.pathSegList;
Floor._sElement.add(p1);
Floor._jElement.on(EventTypes.pointerMove, function (e) {
e.preventDefault();
e.stopPropagation();
segments.appendItem(p1.element.createSVGPathSegLinetoAbs(e.offsetX, e.offsetY));
//var pI2 = PointerInfo.parse(e);
//d = sPB.lineTo(pI2.x, pI2.y).d();
//p1.attr("d", d);
});
}).on(EventTypes.pointerUp, function (e) {
Floor._jElement.off(EventTypes.pointerMove);
});
};
And here's the PointerInfo and EventTypes objects:
var PointerInfo = (function () {
function PointerInfo(x, y) {
this.x = x;
this.y = y;
}
PointerInfo.parse = function (e) {
var x = e.offsetX - 2, y = e.offsetY - 1;
return new PointerInfo(x, y);
};
return PointerInfo;
})();
var EventTypes = (function () {
function EventTypes() {
}
EventTypes.touch = EventTypes.touch || ("ontouchstart" in window);
EventTypes.pointerDown = EventTypes.touch ? "touchstart" : "mousedown";
EventTypes.pointerMove = EventTypes.touch ? "touchmove" : "mousemove";
EventTypes.pointerUp = EventTypes.touch ? "touchend" : "mouseup";
EventTypes.pointerLeave = EventTypes.touch ? "touchleave" : "mouseout";
return EventTypes;
})();
All of these are compiled down from TypeScript.
I figured it out, after spending two days on it and disabling everything else on the page and every other script being loaded. It turned out that it was CSS that was causing the lag. The SVG element that was being drawn on had a background pattern created via gradients to look like a grid. Apparently as path was being updated on touchmove the element's layout was invalidated and Safari was force redrawing it constantly.
So, the moral of the story is that the performance issues in iOS Safari aren't always due to JavaScript. Fancy CSS effects also contribute to it especially if the element they're on is being manipulated constantly.
UPDATE
With Chrome 48, you can no longer set the path's segments because that API was deprecated and removed. The solution now is to constantly update the d attribute as the pointer moves. From my testing, there's no noticeable performance penalty, even on older devices such as the iPad 2nd gen.
Related
I'm running into a bit of an odd issue with Paper.js - I'm using the library to scale the "petals" of a randomly generated flower while audio plays.
The issue crops up if the flower is "growing" and the user navigates to a different tab in the browser. Even though it appears that the onFrame event is not firing when the window is out of view, whichever petal is currently scaling at the time will continue to scale indefinitely.
I even tried using a special js library to determine if the window is in view and still wasn't able to get the petals to stop scaling.
You can view a demo here, as I was not even able to replicate this in a Paper sketch: https://demos2.paperbeatsscissors.com/
Also including my onFrame code here in case the problem is obvious to someone:
view.onFrame = function(event) {
// See if something is playing
if (playing > -1) {
// Get the active flower
var activeFlower = garden[playing],
activeData = activeFlower.data;
// Active layer and petal
var activeLayer = getEl(activeFlower, activeData.lIndex),
activePetal = getEl(activeLayer, activeData.pIndex);
// Variables
var time = activeData.audio.seek(),
scaleAmount = (1 / (activeData.timing / event.delta.toFixed(3))) * 2;
// Petal progression
if (!activeData.completed) {
if (activePetal.scaling.x < 1 && activePetal.scaling.y < 1) {
activePetal.pivot = {x: 0, y: activePetal.height / 2};
activePetal.scaling.x = activePetal.scaling.x + scaleAmount;
activePetal.scaling.y = activePetal.scaling.y + scaleAmount;
} else {
if (activeData.pIndex < (activeLayer.children.length - 1)) {
// If the petal is scaled, jump to a new petal
activeData.pIndex += 1;
} else {
if (activeData.lIndex > 0) {
// When all petals are bloomed, jump to a new layer
activeData.pIndex = 0;
activeData.lIndex -= 1;
} else {
// Set the flower as completed
activeData.completed = true;
}
}
}
}
activeFlower.rotate(.125, activeData.center);
// Reset the playing variable if the audio clip is complete and the flower has completed
if (!activeData.audio.playing() && time === 0 && activeData.completed) {
playing = -1;
}
}
}
Really stumped on this one so any help is greatly appreciated!
I think that your problem is coming from the fact that you base your scaling calculation on event.delta which represents the time elapsed since the last event fired.
The thing is that, if I'm not mistaken, under the hood, Paper.js onFrame event relies on requestAnimationFrame which does not fire when the tab if inactive.
So when you switch tab, wait for a while and get back to your tab event.delta value is big and your scaling value too, hence the size of your petals. This basic sketch showcase this behavior.
So in my opinion, you should simply check event.delta value and limit it if it's too high.
I have a Fabric.js canvas and I want to implement the full-canvas panning that software packages usually do with a "hand" tool. It's when you press one of the mouse buttons, then move over the canvas while holding the mouse button and the visible portion of the canvas changes accordingly.
You can see in this video what I want to achieve.
In order to implement this functionality I wrote the following code:
$(canvas.wrapperEl).on('mousemove', function(evt) {
if (evt.button == 2) { // 2 is the right mouse button
canvas.absolutePan({
x: evt.clientX,
y: evt.clientY
});
}
});
But it doesn't work. You can see in this video what happens.
How can I modify my code in order:
For panning to work like in the first video?
For the event handler to consume the event? It should prevent the context menu from appearing when the user presses or releases the right mouse button.
An easy way to pan a Fabric canvas in response to mouse movement is to calculate the cursor displacement between mouse events and pass it to relativePan.
Observe how we can use the screenX and screenY properties of the previous mouse event to calculate the relative position of the current mouse event:
function startPan(event) {
if (event.button != 2) {
return;
}
var x0 = event.screenX,
y0 = event.screenY;
function continuePan(event) {
var x = event.screenX,
y = event.screenY;
fc.relativePan({ x: x - x0, y: y - y0 });
x0 = x;
y0 = y;
}
function stopPan(event) {
$(window).off('mousemove', continuePan);
$(window).off('mouseup', stopPan);
};
$(window).mousemove(continuePan);
$(window).mouseup(stopPan);
$(window).contextmenu(cancelMenu);
};
function cancelMenu() {
$(window).off('contextmenu', cancelMenu);
return false;
}
$(canvasWrapper).mousedown(startPan);
We start panning on mousedown and continue panning on mousemove. On mouseup, we cancel the panning; we also cancel the mouseup-cancelling function itself.
The right-click menu, also known as the context menu, is cancelled by returning false. The menu-cancelling function also cancels itself. Thus, the context menu will work if you subsequently click outside the canvas wrapper.
Here is a page demonstrating this approach:
http://michaellaszlo.com/so/fabric-pan/
You will see three images on a Fabric canvas (it may take a moment or two for the images to load). You'll be able to use the standard Fabric functionality. You can left-click on the images to move them around, stretch them, and rotate them. But when you right-click within the canvas container, you pan the whole Fabric canvas with the mouse.
I have an example on Github using fabric.js Canvas panning: https://sabatinomasala.github.io/fabric-clipping-demo/
The code responsible for the panning behaviour is the following: https://github.com/SabatinoMasala/fabric-clipping-demo/blob/master/src/classes/Panning.js
It's a simple extension on the fabric.Canvas.prototype, which enables you to toggle 'drag mode' on the canvas as follows:
canvas.toggleDragMode(true); // Start panning
canvas.toggleDragMode(false); // Stop panning
Take a look at the following snippet, documentation is available throughout the code.
const STATE_IDLE = 'idle';
const STATE_PANNING = 'panning';
fabric.Canvas.prototype.toggleDragMode = function(dragMode) {
// Remember the previous X and Y coordinates for delta calculations
let lastClientX;
let lastClientY;
// Keep track of the state
let state = STATE_IDLE;
// We're entering dragmode
if (dragMode) {
// Discard any active object
this.discardActiveObject();
// Set the cursor to 'move'
this.defaultCursor = 'move';
// Loop over all objects and disable events / selectable. We remember its value in a temp variable stored on each object
this.forEachObject(function(object) {
object.prevEvented = object.evented;
object.prevSelectable = object.selectable;
object.evented = false;
object.selectable = false;
});
// Remove selection ability on the canvas
this.selection = false;
// When MouseUp fires, we set the state to idle
this.on('mouse:up', function(e) {
state = STATE_IDLE;
});
// When MouseDown fires, we set the state to panning
this.on('mouse:down', (e) => {
state = STATE_PANNING;
lastClientX = e.e.clientX;
lastClientY = e.e.clientY;
});
// When the mouse moves, and we're panning (mouse down), we continue
this.on('mouse:move', (e) => {
if (state === STATE_PANNING && e && e.e) {
// let delta = new fabric.Point(e.e.movementX, e.e.movementY); // No Safari support for movementX and movementY
// For cross-browser compatibility, I had to manually keep track of the delta
// Calculate deltas
let deltaX = 0;
let deltaY = 0;
if (lastClientX) {
deltaX = e.e.clientX - lastClientX;
}
if (lastClientY) {
deltaY = e.e.clientY - lastClientY;
}
// Update the last X and Y values
lastClientX = e.e.clientX;
lastClientY = e.e.clientY;
let delta = new fabric.Point(deltaX, deltaY);
this.relativePan(delta);
this.trigger('moved');
}
});
} else {
// When we exit dragmode, we restore the previous values on all objects
this.forEachObject(function(object) {
object.evented = (object.prevEvented !== undefined) ? object.prevEvented : object.evented;
object.selectable = (object.prevSelectable !== undefined) ? object.prevSelectable : object.selectable;
});
// Reset the cursor
this.defaultCursor = 'default';
// Remove the event listeners
this.off('mouse:up');
this.off('mouse:down');
this.off('mouse:move');
// Restore selection ability on the canvas
this.selection = true;
}
};
// Create the canvas
let canvas = new fabric.Canvas('fabric')
canvas.backgroundColor = '#f1f1f1';
// Add a couple of rects
let rect = new fabric.Rect({
width: 100,
height: 100,
fill: '#f00'
});
canvas.add(rect)
rect = new fabric.Rect({
width: 200,
height: 200,
top: 200,
left: 200,
fill: '#f00'
});
canvas.add(rect)
// Handle dragmode change
let dragMode = false;
$('#dragmode').change(_ => {
dragMode = !dragMode;
canvas.toggleDragMode(dragMode);
});
<div>
<label for="dragmode">
Enable panning
<input type="checkbox" id="dragmode" name="dragmode" />
</label>
</div>
<canvas width="300" height="300" id="fabric"></canvas>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.15/fabric.min.js"></script>
Not sure about FabricJS, but it could be in such a way:
for it to work like in the first video:
By making use of CSS cursor property, toggling it on mousedown and mouseup events using javascript.
the event handler consume the event (prevent the context menu from appearing, when the user releases the right mouse button):
Using javascript we return false on contextmenu event
CODE: with a little problem ( * )
using jQuery JS Fiddle 1
$('#test').on('mousedown', function(e){
if (e.button == 2) {
// if right-click, set cursor shape to grabbing
$(this).css({'cursor':'grabbing'});
}
}).on('mouseup', function(){
// set cursor shape to default
$(this).css({'cursor':'default'});
}).on('contextmenu', function(){
//disable context menu on right click
return false;
});
Using raw javascript JS Fiddle 2
var test = document.getElementById('test');
test.addEventListener('mousedown', function(e){
if (e.button == 2) {
// if right-click, set cursor shape to grabbing
this.style.cursor = 'grabbing';
}
});
test.addEventListener('mouseup', function(){
// set cursor shape to default
this.style.cursor = 'default';
});
test.oncontextmenu = function(){
//disable context menu on right click
return false;
}
( * ) Problem:
The above snippets works as it should but there's a cross-browser issue, if you check the above fiddles in Firefox - or Opera -you'll see the correct behavior, when checked in Chrome and IE11 - didn't checked it with Edge or Safari - I found that Chrome and IE don't support the grabbing cursor shape, so in the above code snippets, change the cursor lines into this:
jQuery: $(this).css({'cursor':'move'}); JS Fiddle 3
Raw javascript: this.style.cursor = 'move'; JS Fiddle 4
Now we have a working code but without the hand cursor. but there is the following solution:-
SOLUTIONS:
Chrome and Safari support grab and grabbing with the -webkit- prefix like:
$(this).css({'cursor': '-webkit-grabbing'});
but you need to make browser sniffing first, if Firefox then the default and standard code, if Chrome and Safari then with the -webkit- prefix, and this still makes IE out of the game.
Have a look at this example, test it with Chrome, Safari, Firefox, Opera and IE you can see that cursor: url(foo.bar) works and supported in ALL browsers.
Chrome, Safari, Firefox and Opera shows the yellow smile image smiley.gif, but IE shows the red ball cursor url(myBall.cur).
So I think you can make use of this, and a grabbing hand image like this
Or this:
You can use an image like the above, a png or gif format with all browsers except IE which supports .cur, so you need to find a way to convert it into a .cur. Google search shows many result of convert image to cur
Note that, although this cursor:url(smiley.gif),url(myBall.cur),auto; - with fallback support separated by comma, works well in the W3Schools example shown above, I couldn't get it to work the same way in javascript, I tried $(this).css({'cursor': 'grabbing, move'}); but it didn't work.
I also tried doing it as CSS class
.myCursor{ cursor: grabbing, -webkit-grabbing, move; }
Then with jQuery $(this).addClass('myCursor'); but no avail either.
So you still need to make browser sniffing whether you are going the second solution or a hybrid fix of the both solutions, this is my code which I've used couple times to detect browser and it worked well at the time of this post but you porbabely won't need the "Mobile" and "Kindle" parts.
// Detecting browsers
$UA = navigator.userAgent;
if ($UA.match(/firefox/i)) {
$browser = 'Firefox';
} else if ($UA.indexOf('Trident') != -1 && $UA.indexOf('MSIE') == -1) {
$browser = 'MSIE';
} else if ($UA.indexOf('MSIE') != -1) {
$browser = 'MSIE';
} else if ($UA.indexOf('OPR/') != -1) {
$browser = 'Opera';
} else if ($UA.indexOf("Chrome") != -1) {
$browser = 'Chrome';
} else if ($UA.indexOf("Safari")!=-1) {
$browser = 'Safari';
}
if($UA.match(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Nokia|Mobile|Opera Mini/i)) {
$browser = 'Mobile';
}else if($UA.match(/KFAPWI/i)){
$browser = 'Kindle';
}
console.log($browser);
Resources:
https://developer.mozilla.org/en-US/docs/Web/CSS/cursor
http://www.w3schools.com/cssref/pr_class_cursor.asp
https://css-tricks.com/almanac/properties/c/cursor/
Google images search of grabbing hand cursor
i have made an example on jsfiddle , that we can actually drag the whole canvas with all its objects, into a parent div, like the picture,and i will try to explain it step by step.
First of all i download the drag library jquery.dradscroll.js, you can find it on the net. This is a small js file that with little changes it can helps us complete the task.
download link: http://www.java2s.com/Open-Source/Javascript_Free_Code/jQuery_Scroll/Download_jquery_dragscroll_Free_Java_Code.htm
create the container that holds our canvas.
<div class="content">
<canvas id="c" width="600" height="700" ></canvas>
</div>
little css
.content{
overflow:auto;
width:400px;
height:400px;
}
javascript:
a. create the canvas.
b. make default cursor , when it is over canvas , open hand
canvas.defaultCursor = 'url(" http://maps.gstatic.com/intl/en_us/mapfiles/openhand_8_8.cur") 15 15, crosshair';
c. override the __onMouseDown function , for change to the closedhand cursor( at end).
fabric.Canvas.prototype.__onMouseDown = function(e){
// accept only left clicks
var isLeftClick = 'which' in e ? e.which === 1 : e.button === 1;
if (!isLeftClick && !fabric.isTouchSupported) {
return;
}
if (this.isDrawingMode) {
this._onMouseDownInDrawingMode(e);
return;
}
// ignore if some object is being transformed at this moment
if (this._currentTransform) {
return;
}
var target = this.findTarget(e),
pointer = this.getPointer(e, true);
// save pointer for check in __onMouseUp event
this._previousPointer = pointer;
var shouldRender = this._shouldRender(target, pointer),
shouldGroup = this._shouldGroup(e, target);
if (this._shouldClearSelection(e, target)) {
this._clearSelection(e, target, pointer);
}
else if (shouldGroup) {
this._handleGrouping(e, target);
target = this.getActiveGroup();
}
if (target && target.selectable && !shouldGroup) {
this._beforeTransform(e, target);
this._setupCurrentTransform(e, target);
}
// we must renderAll so that active image is placed on the top canvas
shouldRender && this.renderAll();
this.fire('mouse:down', { target: target, e: e });
target && target.fire('mousedown', { e: e });
if(!canvas.getActiveObject() || !canvas.getActiveGroup()){
flag=true;
//change cursor to closedhand.cur
canvas.defaultCursor = 'url("http://maps.gstatic.com/intl/en_us/mapfiles/closedhand_8_8.cur") 15 15, crosshair';
}//end if
override the __onMouseUp event ,to change back the cursor to openhand.
fabric.Canvas.prototype.__onMouseUp = function(e){
if(flag){
canvas.defaultCursor = 'url(" http://maps.gstatic.com/intl/en_us/mapfiles/openhand_8_8.cur") 15 15, crosshair';
flag=false;
}
};
You initialize the dragScroll() to work on the content that hosts the canvas:
$('.content').dragScroll({});
Some small changes on the jquery.dragScroll.js file, so as to understand when to drag the canvas and when not. On mousedown() event we add an if statement to check whether we have an active object or group.If true ,no canvas drag.
$($scrollArea).mousedown(function (e) {
if (canvas.getActiveObject() || canvas.getActiveGroup()) {
console.log('no drag');return;
} else {
console.log($('body'));
if (typeof options.limitTo == "object") {
for (var i = 0; i < options.limitTo.length; i++) {
if ($(e.target).hasClass(options.limitTo[i])) {
doMousedown(e);
}
}
} else {
doMousedown(e);
}
}
});
on mousedown event we grab the DOM element (.content) and get the top & left position
function doMousedown(e) {
e.preventDefault();
down = true;
x = e.pageX;
y = e.pageY;
top = e.target.parentElement.parentElement.scrollTop; // .content
left = e.target.parentElement.parentElement.scrollLeft;// .content
}
If we dont want to have the scrollbars visible:
.content{
overflow:hidden;
width:400px;
height:400px;
}
There is a small problem though,jsfiddle, accepts only https libraries ,so it blocks fabricjs, except if you add it from 'https://rawgit.com/kangax/fabric.js/master/dist/fabric.js', but again it still blocks it some times (at least on my chrome and mozilla).
jsfiddle example : https://jsfiddle.net/tornado1979/up48rxLs/
you may have better luck ,than me, on your browser , but it would definitelly work on your live app.
Anyway,
i hope helps ,good luck.
I know this has already been answered, but I redid the pen created in this answer using the new version of fabricjs (4.5.0)
Pen: https://codepen.io/201flaviosilva/pen/GROLbQa
In this case, I used the middle button in the mouse to pan :)
// Enable mouse middle button
canvas.fireMiddleClick = true;
// Mouse Up Event
if (e.button === 2) currentState = STATE_IDLE;
// Mouse Down Event
if (e.button === 2) currentState = STATE_PANNING;
:)
I cannot understund, why IE and EDGE has strange behavior.
If you rapidly move the mouse over and out of pentagram -- animation is distancing, but in other NORMAL web-browsers all in order.
http://jsfiddle.net/b1Lhjo4k/
var svgElement = Snap("#svg");
var pent = svgElement.select('#pentagram-one');
var hoverover = function() {
pent.stop().animate({transform: 'r180,500,515'}, 400);
};
var hoverout = function() {
pent.stop().animate({transform: 'r0,500,515'}, 400);
};
pent.hover(hoverover, hoverout);
Solved by prototype function; ie and edge works now correctly.
I don't have Edge to test, but I suspect its because its doing something funky with Matrix/transform interpolation when starting an animation before the previous one has finished (this may be wrong, but just what initial thoughts would be to look at), or it could be something odd with very small number strings sometime as I've seen those before with IE.
One way around this, would be to not let Snap do the interpolation using the browsers matrix values, but control it yourself. You could try a small plugin for it, if you will use it a lot, something like this...
Snap.plugin( function( Snap, Element, Paper, global ) {
Element.prototype.animRotate = function( from, to, dur ) {
var el = this;
this.stop();
Snap.animate(from, to, function( val ) {
el.transform('r' + val + ',500,515')
},dur)
return this;
};
});
var pent = svgElement.select('#pentagram-one');
var hoverover = function() {
this.animRotate(0, 180, 400)
};
var hoverout = function() {
this.animRotate(180, 0, 400)
};
jsfiddle
I am developing a simple HTML5 game presently which you can play test at the following URL:
http://frankcaron.com/simplegame/game.html
The code is here:
https://github.com/frankcaron/Personal_HTML5_GamePrototype/blob/master/js/game.js
In order to animate elements, I'm using a few setIntervals. The main game loop has a setInterval which works fine. Some fading elements use them, as well, without issue. The one I'm having an issue with is one for an on-click animation.
The issue I'm having is that, in Safari and also to some extent in Chrome, the click event and animation is not responsive. It doesn't fire every time. I think there is some conflict with the concurrent intervals running. I'm not sure why.
I've done some research on the other method of HTML5 animation using requestAnimFrame but I don't think that'll solve my problem here.
Here is the code of interest:
//Track mousedown
addEventListener('mousedown', function (e) {
//Safari Fix
e.preventDefault();
//If you click while the game is going, do your special move
performSpecial();
}
//Perform a special move
var performSpecial = function () {
switch(classType)
{
case 1:
//Class 1
break;
default:
//No class
renderExplosion();
}
}
//Render explosion special move
var renderExplosion = function () {
//Temp vars
var alpha = 1.0; // full opacity
var circleMaxRadius = 32;
fadeCircle = setInterval(function () {
ctx.beginPath();
ctx.arc(hero.x + 16, hero.y + 16, circleMaxRadius, 0, 2 * Math.PI, false);
ctx.lineWidth = 5;
ctx.strokeStyle = "rgba(255, 0, 0, " + alpha + ")";
ctx.stroke();
circleMaxRadius = circleMaxRadius + 5;
alpha = alpha - 0.05 ; // decrease opacity (fade out)
if (alpha < 0) {
clearInterval(fadeCircle);
}
}, 1);
};
Welp, I rewrote the application using window.requestAnimationFrame and I no longer have this problem. The code is up on my Github if anyone wants to see the full solution.
In brief, I removed all of the intervals and replaced the entire game loop. When an animation effect is needed, I turn a flag on corresponding with the event. The renderer checks this conditional and renders the effect if the flag is on, turning the flag off again when it's done.
I'm developing some page when I use Raphael liblary to draw some items.
my App
So my problem is in that when I'm moving to some rect it growing up but when my mouse is on text which is positioning on my rect, it loss his hover. You can see it on my app example.
var paper = new Raphael(document.getElementById('holder'), 500, object.length * 100);
drawLine(paper, aType.length, bType.length, cType.length, cellSize, padding);
process = function(i,label)
{
txt = paper.text(390,((i+1)* cellSize) - 10,label.devRepo)
.attr({ stroke: "none", opacity: 0, "font-size": 20});
var a = paper.rect(200, ((i+1)* cellSize) - 25, rectWidth, rectHeight)
.hover(function()
{
this.animate({ transform : "s2"}, 1000, "elastic");
this.prev.animate({opacity: 1}, 500, "elastic");
this.next.attr({"font-size" : 30});
},
function()
{
this.animate({ transform : "s1" }, 1000, "elastic");
this.prev.animate({opacity: 0}, 500);
this.next.attr({"font-size" : 15});
});
}
I have tried e.preventDefault(); on hover of this.next and some other solutions but it's doesn't work.
Any help would be appreciated.
Most people will suggest you place a transparent rectangle over the box and the labels and attach the hover functions to that instead. (If memory serves, you have to make the opacity 0.01 instead of 0 to prevent the object from losing its attached events.) This works fine, but I don't love this solution; it feels hacky and clutters the page with unnecessary objects.
Instead, I recommend this: Remove the second function from the hover, making it functionally a mouseover function only. Before you draw any of the rectangles and labels, make a rectangular "mat" the size of the paper. Then, attach the function that minimizes the label as a mouseover on the mat. In other words, you're changing the trigger from mousing out of the box to mousing over the area outside of it.
I left a tiny bit of opacity and color on the mat to be sure it's working. You can just change the color to your background color.
var mat = paper.rect(0, 0, paper.width, paper.height).attr({fill: "#F00", opacity: 0.1});
Now, you want to make a container for all the rectangles so you can loop through them to see which need to be minimized. I made an object called "rectangles" that contains the objects we're concerned with. Then:
mat.mouseover(function () {
for (var c = 0; c < rectangles.length; c += 1) {
//some measure to tell if rectangle is presently expanded
if (rectangles[c].next.attr("font-size")) {
rectangles[c].animate({
transform : "s1"
}, 1000, "elastic");
rectangles[c].prev.animate({opacity: 0}, 500);
rectangles[c].next.attr({"font-size" : 15});
}
}
});
Then I just removed the mouseout function from the individual rectangles.
jsBin
To be clear, this will have some downsides: If people run the mouse around really fast, they can expand several rectangles at the same time. This is remedied as soon as the mouse touches the mat. I think the functionality looks pretty nice. But the invisible mats is always an option.
I wrote a small extension to Raphael - called hoverInBounds - that resolves this limitation.
Demo: http://jsfiddle.net/amustill/Bh276/1
Raphael.el.hoverInBounds = function(inFunc, outFunc) {
var inBounds = false;
// Mouseover function. Only execute if `inBounds` is false.
this.mouseover(function() {
if (!inBounds) {
inBounds = true;
inFunc.call(this);
}
});
// Mouseout function
this.mouseout(function(e) {
var x = e.offsetX || e.clientX,
y = e.offsetY || e.clientY;
// Return `false` if we're still inside the element's bounds
if (this.isPointInside(x, y)) return false;
inBounds = false;
outFunc.call(this);
});
return this;
}