I want to start using Konva.js library. It looks really great in my browser (chrome) on my PC and has awesome functionality. I created code to make some really easy animations (move and rotate). But when I tried to run my webpage on a mobile device or even in Safari browser, it started to be really laggy. Moreover, when I set four objects(Images) into a movement, the browser crashed.
I did some tests and realized that even draggable objects are laggy on mobile devices. (When I tried to move them, they had really slow and jerky movement).
Is there some way to optimize it. (I have tried the recommended batchDraw() function, but it didn't help)? How can I make the movement fluent? If this is not currently possible, will there be some optimization possibilities in the future?
Here is my code... but obviously you have to run it on a mobile phone to see the effect. To activate the effect click(touch) on images.
http://bannerteam.tode.cz/konvaAnim/
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Konva animace</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #F0F0F0;
}
</style>
<script src="https://cdn.rawgit.com/konvajs/konva/2.0.3/konva.min.js"></script>
</head>
<body>
<div id="container"></div>
<script>
var width = window.innerWidth;
var height = window.innerHeight;
var lengthOfAnim = 60;
var pos = [{x: 50, y: 50, p: 'A'}, {x: 250, y: 50, p: 'B'}, {x: 450, y: 50, p: 'C'},
{x: 50, y: 250, p: 'D'}, {x: 250, y: 250, p: 'E'}, {x: 450, y: 250, p: 'F'},
{x: 50, y: 450, p: 'G'}, {x: 250, y: 450, p: 'I'}, {x: 450, y: 450, p: 'J'},
{x: 50, y: 650, p: 'K'}, {x: 250, y: 650, p: 'F'}];
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height
});
var imageObj = [];
var img = new Image();
var doAnimations = function(i){
switch(i){
case 0:
imageObj[i].img.opacity(1 - imageObj[i].time/lengthOfAnim);
break;
case 1:
if(imageObj[i].time === lengthOfAnim -1)
imageObj[i].img.y(imageObj[i].img.y() + (lengthOfAnim-1)*2);
else
imageObj[i].img.y(imageObj[i].img.y() - 2);
break;
case 2:
imageObj[i].img.opacity(1 - imageObj[i].time/lengthOfAnim);
if(imageObj[i].time === lengthOfAnim -1)
imageObj[i].img.y(imageObj[i].img.y() + (lengthOfAnim-1)*2);
else
imageObj[i].img.y(imageObj[i].img.y() - 2);
break;
case 3:
var parent = imageObj[i].img.getParent();
parent.clipFunc(function(ctx) {
ctx.rect(imageObj[i].img.x() - 75, imageObj[i].img.y() - 75, 75*(1 - imageObj[i].time/lengthOfAnim), 150);
ctx.rect(imageObj[i].img.x() + 75 - 75*(1 - imageObj[i].time/lengthOfAnim) , imageObj[i].img.y() - 75, 75*(1 - imageObj[i].time/lengthOfAnim), 150);
});
break;
case 4:
var parent = imageObj[i].img.getParent();
parent.clipHeight(150*(1 - imageObj[i].time/lengthOfAnim));
break;
case 5:
imageObj[i].img.opacity(1 - imageObj[i].time/lengthOfAnim);
imageObj[i].img.rotation(90*(imageObj[i].time/lengthOfAnim));
break;
case 6:
imageObj[i].img.opacity(1 - imageObj[i].time/lengthOfAnim);
var pom = (1 - imageObj[i].time/lengthOfAnim);
imageObj[i].img.scale({x: pom, y: pom});
break;
case 7:
if(imageObj[i].time === lengthOfAnim -1)
imageObj[i].img.x(imageObj[i].img.x() - (lengthOfAnim-1)*2);
else
imageObj[i].img.x(imageObj[i].img.x() + 2);
imageObj[i].img.move({x: 0, y: 10*(Math.sin(6*Math.PI*(imageObj[i].time/lengthOfAnim)))});
break;
case 8:
imageObj[i].img.rotation(20*Math.sin(6*Math.PI*(imageObj[i].time/lengthOfAnim)));
break;
case 9:
imageObj[i].img.opacity(0.5 + Math.abs((imageObj[i].time/lengthOfAnim-0.5)));
break;
case 10:
imageObj[i].img.draggable(true);
break;
}
}
img.onload = function() {
for(let i = 0; i < pos.length; i++){
var layer = new Konva.Layer();
var yoda = new Konva.Image({
x: pos[i].x + 75,
y: pos[i].y + 75,
image: img,
width: 150,
height: 150,
offset: {
x: 75,
y: 75
}
});
imageObj.push({img: yoda, layer: layer, time: 0});
var charac = new Konva.Text({
x: pos[i].x + 50,
y: pos[i].y + 160,
text: pos[i].p,
fontSize: 30,
fontFamily: 'Calibri',
fill: 'black'
});
if(i === 3){
var group = new Konva.Group({clipFunc:function(ctx) {
ctx.rect(pos[i].x, pos[i].y, 150, 150)
},});
group.add(yoda);
layer.add(group);
}else if(i === 4){
var group = new Konva.Group({clip: {
x : pos[i].x,
y : pos[i].y,
width : 150,
height : 150
},});
group.add(yoda);
layer.add(group);
}else
layer.add(yoda);
layer.add(charac);
stage.add(layer);
yoda.on('click tap', function() {
if(imageObj[i].time === 0)
imageObj[i].time = lengthOfAnim;
});
}
setInterval(function(){
for(var i = 0; i < pos.length; i++){
if(imageObj[i].time > 0){
imageObj[i].time--;
doAnimations(i);
imageObj[i].layer.draw();
}
}
}, 40);
}
img.src = './castle.png';
</script>
<body>
</html>
There are a lot of ways to improve the performance.
You can read a lot of tips here: https://konvajs.github.io/docs/performance/All_Performance_Tips.html
Your stage looks simple, so performance should be very good on mobile.
Some tips:
Do not use setInterval. Use requestAnimationFrame. The animation will be much smoother
use layer.batchDraw()
If possible try to move animated objects into another layer, so you don't need to redraw ALL nodes
Related
In my application, I need to detect the mouse events on the edge / border / stroked part of a shape - but not on the filled part. I have not found a way to do this.
I do not know how to get started with this but here is pseudo code of what I am trying to do.
shape.on('mousemove', function () {
if (mouse on the edge of the shape) {
// change the cursor to a star
} else {
// do nothing
}
});
To detect mouse hits on the edge of a shape only, use the fillEnabled:false property. What this does is tell Konva to disregard fill - meaning that any event-listening on the fill-part of the shape will be switched off. However, with great power great responsibility comes and the fillEnabled property also stops any visual fill you might want to appear.
Putting that together, if you want to hit-test the stroke part of a shape only you will need another transparent shape drawn on top of the visual shape to detect mouse events.
As a bonus, you can use the hitStrokeWidth property to make the hit-detecting region of the stroke wider - as if you set the stroke 'thicker' for purposes of mouse detection.
Snippet below shows this approach on a rect and random polygon.
// Set up a stage
stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
}),
// add a layer to draw on
layer = new Konva.Layer(),
rect = new Konva.Rect({
name: 'r1',
x: 220,
y: 20,
width: 100,
height: 40,
stroke: 'cyan',
fill: 'transparent',
fillEnabled: false
}),
poly = new Konva.Line({
points: [23, 20, 23, 160, 70, 93, 150, 109, 290, 139, 270, 93],
fill: '#00D2FF',
stroke: 'black',
strokeWidth: 5,
closed: true,
fillEnabled: false,
hitStrokeWidth: 10
});
// Add the layer to the stage
stage.add(layer);
layer.add(rect, poly)
stage.draw();
rect.on('mouseover', function() {
$('#info').html('Rect MouseEnter')
})
rect.on('mouseout', function() {
$('#info').html('Rect mouseOut')
})
poly.on('mouseover', function() {
$('#info').html('Poly MouseEnter')
})
poly.on('mouseout', function() {
$('#info').html('Poly mouseOut')
})
body {
margin: 10;
padding: 10;
overflow: hidden;
background-color: #f0f0f0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/konva#^3/konva.min.js"></script>
<p>Move mouse over the shapes </p>
<p id='info'>Events show here</p>
<div id="container"></div>
It is easy to clone a shape to make an edge-event detecting version and place the clone over the original shape so that you can detect the edge-events specifically. See the following working snippet - enable the console to view the events sequence.
// Set up a stage
stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
}),
// add a layer to draw on
layer = new Konva.Layer(),
rect = new Konva.Rect({
name: 'r1',
x: 220,
y: 20,
width: 100,
height: 40,
stroke: 'cyan',
fill: 'magenta'
}),
poly = new Konva.Line({
points: [23, 20, 23, 160, 70, 93, 150, 109, 290, 139, 270, 93],
fill: '#00D2FF',
stroke: 'black',
strokeWidth: 5,
closed: true,
hitStrokeWidth: 10
}),
// this is a clone of rect with fillEnabled set to false, placed 'above' rect in the z-order.
rect2 = rect.clone({
fillEnabled: false
}),
poly2 = poly.clone({
fillEnabled: false
}),
// Add the layer to the stage
stage.add(layer);
layer.add(rect, rect2, poly, poly2)
stage.draw();
rect.on('mouseover', function() {
showMsg('Rect MouseEnter');
})
rect2.on('mouseover', function() {
showMsg('Rect2 Edge MouseEnter');
})
rect2.on('mouseout', function() {
showMsg('Rect2 Edge mouseOut');
})
poly.on('mouseover', function() {
showMsg('Poly MouseEnter');
})
poly.on('mouseout', function() {
showMsg('Poly MouseOut');
})
poly2.on('mouseover', function() {
showMsg('Poly2 Edge MouseEnter');
})
poly2.on('mouseout', function() {
showMsg('Poly2 Edge MouseOut');
})
function showMsg(msg) {
console.log(msg)
$('#info').html(msg)
}
body {
margin: 10;
padding: 10;
overflow: hidden;
background-color: #f0f0f0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/konva#^3/konva.min.js"></script>
<p>Move mouse over the shapes </p>
<p id='info'>Events show here</p>
<div id="container"></div>
It's not a precise approach ,just a approximate way which detect if the cursor just near the out edge of the object.
stage.on('mousemove', function (e) {
var deta = 3;
var node8 = stage.getIntersection({x: e.evt.clientX, y: e.evt.clientY});
if(node8){
console.log(node8.getClassName()+"====mouse on object=====");
return;
}
var node = stage.getIntersection({x: e.evt.clientX+deta, y: e.evt.clientY});
if(node){
console.log(node.getClassName()+"====mouse on edge=====");
return;
}
var node1 = stage.getIntersection({x: e.evt.clientX, y: e.evt.clientY+deta});
if(node1){
console.log(node1.getClassName()+"====mouse on edge=====");
return;
}
var node2 = stage.getIntersection({x: e.evt.clientX+deta, y: e.evt.clientY+deta});
if(node2){
console.log(node2.getClassName()+"====mouse on edge=====");
return;
}
var node3 = stage.getIntersection({x: e.evt.clientX-deta, y: e.evt.clientY});
if(node3){
console.log(node3.getClassName()+"====mouse on edge=====");
return;
}
var node4 = stage.getIntersection({x: e.evt.clientX, y: e.evt.clientY-deta});
if(node4){
console.log(node4.getClassName()+"====mouse on edge=====");
return;
}
var node5 = stage.getIntersection({x: e.evt.clientX-deta, y: e.evt.clientY-deta});
if(node5){
console.log(node5.getClassName()+"====mouse on edge=====");
return;
}
var node6 = stage.getIntersection({x: e.evt.clientX-deta, y: e.evt.clientY+deta});
if(node6){
console.log(node6.getClassName()+"====mouse on edge=====");
return;
}
var node7 = stage.getIntersection({x: e.evt.clientX+deta, y: e.evt.clientY-deta});
if(node7){
console.log(node7.getClassName()+"====mouse on edge=====");
return;
}
});
I am using Fabric.js and I have the following code for drawing a hexagon:
makeObject(originPoint, newPoint, canvasObject, properties) {
let width = Math.abs(newPoint.x - originPoint.x);
let height = 0;
if(this.shouldKeepProportion){
height=width;
}else{
height=Math.abs(newPoint.y - originPoint.y);
}
width = (this.minWidth!=null && width<this.minWidth ? this.minWidth: width);
height = (this.minHeight!=null && height<this.minHeight ? this.minHeight : height);
let sweep=Math.PI*2/6;
let points=[];
//generate points for 6 angles
for(let i=0;i<6;i++){
let x=width*Math.cos(i*sweep);
let y=height*Math.sin(i*sweep);
points.push({x:x/2,y:y/1.75});
}
properties = {
objectType: 'octagon',
left: originPoint.x,
top: originPoint.y,
originX: 'left',
originY: 'top',
fill: 'rgba(0, 0, 0, 1.0)',
width: width,
height: height,
originX: originPoint.x > newPoint.x ? 'right' : 'left',
originY: originPoint.y > newPoint.y ? 'bottom' : 'top',
flipX: originPoint.x > newPoint.x,
stroke: 'rgba(0, 0, 0, 1.0)',
strokeWidth: 1,
strokeLineJoin: 'round',
...properties
};
return new fabric.fabric.Polygon(points, properties);
}
What I want to get is a regular octagon, same as the hexagon. If i try to change the number of corners/angles, I am getting the following type of octagon:
What I actually need is this:
PLEASE NOTE: I do not need it rotated or flipped or something like that, I need it drawn like on the picture.
Thank you for the help!
EDIT: Working JSFiddle: https://jsfiddle.net/42fb716n/
This should do it, using the Polygon() function instead of Line() so it's like a single object, not a group of objects, and so it works in your drawing App as you have different properties for single and group objects:
var canvas = new fabric.Canvas('canvas');
var size = 150;
var side = Math.round((size * Math.sqrt(2)) / (2 + Math.sqrt(2)));
var octagon = new fabric.Polygon([
{x:-side / 2, y:size / 2},
{x:side / 2, y:size / 2},
{x:size / 2, y:side / 2},
{x:size / 2, y:-side / 2},
{x:side / 2, y:-size / 2},
{x:-side / 2, y:-size / 2},
{x:-size / 2, y:-side / 2},
{x:-size / 2, y:side / 2}], {
stroke: 'red',
left: 10,
top: 10,
strokeWidth: 2,
strokeLineJoin: 'bevil'
},false);
canvas.add(octagon);
<script src="//cdnjs.cloudflare.com/ajax/libs/fabric.js/1.4.0/fabric.min.js"></script>
<canvas id="canvas" width=200 height=200></canvas>
Let me know if you need anything else. Happy to help!
Say i have the following code the event only seems to be firing every now and again take the two following examples.
First example this works great and the event fires everytime as expected
for (var i = 0; i < items; i++) {
var rect = new Kinetic.Rect({
x: 0,
y: 1+i,
width: 100,
height: 2,
fill: 'green'
});
rect.on('mouseover', function(evt) {
$('#console').text(evt.shape.index);
});
layer.add(rect);
}
view in action http://jsfiddle.net/6aTNn/5/
Here is my issue when adding a rotate value on the event doesn't seem to be firing correctly.
for (var i = 0; i < items; i++) {
var rect = new Kinetic.Rect({
x: stage.getWidth() / 2,
y: stage.getHeight() / 2,
width: 100,
height: 1,
fill: 'green'
});
rect.on('mouseover', function(evt) {
console.log(evt.shape.index);
$('#console').text(evt.shape.index);
});
rect.rotate(i * angularSpeed / items);
// add the shape to the layer
layer.add(rect);
}
view in action http://jsfiddle.net/6aTNn/8/
Any help on this would be great been at this for hours and cant find a solution that works?
The nodes are too 'thin' and too overlapped for the event to fire properly.
Try increasing the height of each node and adding fewer of them.
For example, replace the i++ with i += 3 and increase the height to 2.
for (var i = 0; i < 800; i += 3) {
var rect = new Kinetic.Rect({
x: stage.getWidth() / 2,
y: stage.getHeight() / 2,
width: 100,
height: 2,
fill: 'green'
});
// ...
}
I am trying to change my variables inside my items variable and not sure how to do it.
var items = [
{name: '#builder', x: 0, y:15},
{name: '#image', x: 0, y:15},
{name: '#button', x: -100, y: -55}
];
//I only want to change builder and image but not button...How to do this?
if(i>5){
items = [
{name: '#builder', x: 50, y:105},
{name: '#image', x: 110, y:115}
];
}
Thanks for the help.
Have you considered using an object hash table instead?
var items = {
'#builder' : { x: 0, y:15},
'#image': { x: 0, y:15},
'#button': {x: -100, y: -55}
};
Then you can change them like so:
items['#builder'].x = 50;
If it's not an option, that's cool, but it seems like if you want to look things up by name, this might be a better route for you.
Outside of that, you'd have to loop through each record to find the name you wanted and set the values appropriately.
var items = [
{name: '#builder', x: 0, y:15},
{name: '#image', x: 0, y:15},
{name: '#button', x: -100, y: -55}
];
function updateValues(name, x, y) {
for(var i = 0; i < items.length; i++) {
var item = items[i];
if(item.name == name) {
item.x = x;
item.y = y;
return;
}
}
}
updateValues('#builder', 50, 105);
If you know the position, then you could do:
if (i > 5) {
items[0].x = 50;
items[0].y = 105;
items[1].x = 110;
items[1].y = 115;
}
I've created an intro animation to a page I'm working on using jQuery and Raphael, a javascript library. The animation works the way I'd like it to, but is oftentimes jumpy. Usually refreshing will cause it to animate much smoother than on its first page load. I'm wondering if this has anything to do with load times or if it's just the efficiency of my code.
You can see the page at: http://developer.crawford.com as well as the animation code below.
Is there any way to increase efficiency when it comes to javascript animations, or specifically with my code? Am I doing anything to cause the script to be very inefficient? Is there any good way to give the code a few seconds to load before executing to maybe make it run smoother other than simply setTimeout()?
function introAnimation() {
// creating the canvas
var paper = new Raphael(document.getElementById('mainCanvas'), '100%', '100%');
var canvasWidth = 500;
var canvasHeight = 500;
var offset = .6;
// speed of circle getting bigger
var speed1 = 1000;
// speed of circles diverging
var speed2 = 1200;
var hide = Raphael.animation({'opacity': 0});
// ellipse variable instantiation
var cRadius = 105;
var diam = cRadius*2;
// centerpoint
var cX = canvasWidth/2;
var cY = canvasHeight/2;
var circ1 = paper.ellipse(cX, cY, 10, 10);
circ1.attr({opacity: 1, stroke: '#777'});
var circRed = paper.ellipse(cX, cY, cRadius, cRadius).attr({opacity: 0, stroke: '#777'});
var circGreen = paper.ellipse(cX, cY, cRadius, cRadius).attr({opacity: 0, stroke: '#777'});
var circBlue = paper.ellipse(cX, cY, cRadius, cRadius).attr({opacity: 0, stroke: '#777'});
//red, green, blue watermarks, and logo
var redWatermark = paper.image('images/circle_red.png', cX-50, cY-50, 100, 100).attr({opacity: 0});
var greenWatermark = paper.image('images/circle_green.png', cX-50, cY, 100, 100).attr({opacity: 0});
var blueWatermark = paper.image('images/circle_blue.png', cX-50, cY, 100, 100).attr({opacity: 0});
var logoWidth = 60;
var logoHeight = 30;
var logo = paper.image('images/CMS_logo_only.png', cX-(logoWidth/2), cY*1.04, logoWidth*.95, logoHeight*.95).attr({opacity: 0});
var letterOffset = cRadius*1.2;
// circle centerpoints xR, yR: center of red; xG, yG: center of green; xB, yB: center of blue
var xR = cX; var yR = cY-cRadius*offset;
var xG = cX-cRadius*offset; var yG = cY+cRadius*offset;
var xB = cX+cRadius*offset; var yB = cY+cRadius*offset;
// insert CMS letter text
var c = paper.text(xR-Math.cos(.8)*letterOffset, yR-Math.sin(.8)*letterOffset, "c.").attr({fill: '#737373', 'font-size': '25px', 'font-family': 'IMFELLDWPicaItalic', opacity: 0});
var m = paper.text(xG+Math.cos(5*Math.PI/4)*letterOffset, yG-Math.sin(5*Math.PI/4)*letterOffset, "m.").attr({fill: '#737373', 'font-size': '25px', 'font-family': 'IMFELLDWPicaItalic', opacity: 0});
var s = paper.text(xB+Math.cos(0)*letterOffset, yB-Math.sin(0)*letterOffset, "s.").attr({fill: '#737373', 'font-size': '25px', 'font-family': 'IMFELLDWPicaItalic', opacity: 0});
// white overlap
// Three points of overlap:
var pointTopX = cX; var pointTopY = cY-(cRadius*.2);
var pointLeftX = cX-(cRadius*.365); var pointLeftY = cY+(cRadius*.33);
var pointRightX = cX+(cRadius*.365); var pointRightY = cY+(cRadius*.33);
var pathString = 'M'+pointTopX+' '+pointTopY+'A'+cRadius+' '+cRadius+' '+xG+' '+yG;
var pathString = "M"+pointTopX+" "+pointTopY+','
+"A"+cRadius+","+cRadius+",0,0,0,"+pointLeftX+","+pointLeftY+','
+"A"+cRadius+","+cRadius+",0,0,0,"+pointRightX+","+pointRightY+','
+"A"+cRadius+","+cRadius+",0,0,0,"+pointTopX+","+pointTopY;
var overlapFill = paper.path(pathString).attr({'stroke-width': 0, fill: '#fff', opacity: 0});
var overlapPath = paper.path(pathString).attr({opacity: 0});
//resize circle
circ1.animate({ 'rx': cRadius, 'ry': cRadius }, speed1, function() {
//hide it once it's done
circ1.animate({opacity: 0}, 0);
//show other circles
circRed.animate({opacity: 1}, 0);
circGreen.animate({opacity: 1}, 0);
circBlue.animate({opacity: 1}, 0);
//move other circles
circRed.animate({cy: cY-cRadius*offset, rx: cRadius, ry: cRadius}, speed2);
circGreen.animate({cx: cX-cRadius*offset, cy: cY+cRadius*offset, rx: cRadius, ry: cRadius}, speed2);
circBlue.animate({cx: cX+cRadius*offset, cy: cY+cRadius*offset, rx: cRadius, ry: cRadius}, speed2);
logo.animate({opacity: 1}, speed2);
//move to center
redWatermark.attr({width: diam, height: diam, x: imgX(cX, diam), y: imgY(cY, diam)});
greenWatermark.attr({width: diam, height: diam, x: imgX(cX, diam), y: imgY(cY, diam)});
blueWatermark.attr({width: diam, height: diam, x: imgX(cX, diam), y: imgY(cY, diam)});
//animate out
redWatermark.animate({y: imgY(cY-cRadius*offset, diam), opacity: .35}, speed2);
greenWatermark.animate({x: imgX(cX-cRadius*offset, diam), y: imgY(cY+cRadius*offset, diam), opacity: .35}, speed2);
blueWatermark.animate({x: imgX(cX+cRadius*offset, diam), y: imgY(cY+cRadius*offset, diam), opacity: .35}, speed2, function() {
logo.toFront();
c.animate({opacity: 1}, 1000); m.animate({opacity: 1}, 1000); s.animate({opacity: 1}, 1000);
overlapFill.animate({opacity: 1}, 1000); overlapPath.animate({opacity: .3}, 1000);
//nav slide in
nav();
});
});
redWatermark.hover(function() {
$('#createSub').slideDown(300);
});
redWatermark.mouseout(function() {
$('#createSub').slideUp(300);
});
greenWatermark.hover(function() {
$('#storeSub').slideDown('fast');
});
greenWatermark.mouseout(function() {
$('#storeSub').slideUp('fast');
});
blueWatermark.hover(function() {
$('#manageSub').slideDown('fast');
});
blueWatermark.mouseout(function() {
$('#manageSub').slideUp('fast');
});
}
Your PNG's are 400+k http://developer.crawford.com/images/circle_blue.png
You're forcing users to download over a meg of image data while trying to animate it at the same time. This will not be smooth for most visitors. I'd recommend either compressing/shrinking your images, or preloading them.
Replace repeated calculations with the result:
Variations of cY-cRadius*offset appears often, so calculate it ahead of time.
It looks jumpy to me too. Have you considered pre-caching your images before you call any of the animation?