Creating a game like Pivvot seems to be an easy task, until you really understand whats happening and how to achieve the same effect in a html5 manner. Certain things need to be done, accurate collision detection, create a random path trough a number of points, move along path with constant speed and place/replace Obstacles along path. Since this is just a prototype it does not contain all of the features of the original Pivvot game, such as dynamic background, path pattern, path start point, animated path width, etc.. The code as well is not optimized in any way, but it’s already a good starting point to create something like Pivvot.
concept
This Pivvot clone prototype is made as easy as possible, instead of crating a endless random path which generates new path segments on the fly, we create a simple distorted circle which is naturally an endless path. There is an Obstacle pool which holds all the obstacles, once an obstacle is displayed at the screen, the objects shown flag is set to true. If an obstacle is not shown on the screen, but its shown flag is set, the object will be replaced.
Instead of moving the Player along the path, we move the path and keep the player at the center. Checking each frame the distance to the colliders that are currently on the screen, those which are flagged as shown. Once a collision appears the level gets regenerated, and the game restarts.
overview
Once you have downloaded the project, these are all the project files from the “/js” folder inside the project folder. The rest of the project files are fairly simple and basic, you should check them as well. The game is created an controlled entirely with these javascript files.
/js
Initializes the game and keeps the render alive. Add a simple GUI, and get delta time for animations.
main.js
var SVG; //MAIN SVG ELEMENT var elapsed = 0; //GAME RUNNING TIME var time; //TIME var duration; //DURATION TO TRAVEL THE PATH var play = false; //GAME IS RUNNING var w; //SCREEN WIDTH var h; //SCREEN HEIGHT var startText; //START TEXT var score; //SCORE var highscore = 0; //HIGHSCORE var GUI; function init() { SVG = document.getElementById("SVG_scene"); //GET SVG ELEMENT w = window.innerWidth/2; //GET X CENTER h = window.innerHeight/2; //GET Y CENTER addGUI(); //ADD GUI ELEMENT initPath(); //ADD PATH initPlayer(); //ADD PLAYER initObjstacles(); //ADD OBSTACLES initControll(); //ADD CONTROLL render(); //START RENDER SCENE } function addGUI() { var svgNS = "http://www.w3.org/2000/svg"; //DEFINE THE namespaceURI GUI = document.createElementNS(svgNS,"g"); //CREATE A GROUP FOR ALL THE PLAYER ELEMENTS startText = document.createElementNS(svgNS,"text"); //CREATE A TEXT NODE startText.setAttributeNS(null,"x",w+50); //START X startText.setAttributeNS(null,"y",h); //START Y startText.setAttributeNS(null,"fill","white"); //FILLCOLOR startText.setAttributeNS(null,"font-family","helvetica"); //FONT startText.setAttributeNS(null,"font-weight","bold"); //FONT-WEIGHT startText.setAttributeNS(null,"font-size",w/4); //FONT-WEIGHT startText.setAttributeNS(null,"opacity",1); //FONT-WEIGHT startText.textContent ="ready"; //TEXT controllText = document.createElementNS(svgNS,"text"); //CREATE A RTEXT NODE controllText.setAttributeNS(null,"id","controllText"); //START X controllText.setAttributeNS(null,"x",w+100); //START X controllText.setAttributeNS(null,"y",h+50); //START Y controllText.setAttributeNS(null,"fill","white"); //FILLCOLOR controllText.setAttributeNS(null,"font-family","helvetica"); //FONT controllText.setAttributeNS(null,"font-size",20); //FONT-WEIGHT controllText.setAttributeNS(null,"opacity",1); //FONT-WEIGHT controllText.textContent ="move with arrow keys (left/right)"; //TEXT GUI.appendChild(controllText) //APPEND TEXT TO THE GUI ELEMENT GUI.appendChild(startText) //APPEND TEXT TO THE GUI ELEMENT score = document.createElementNS(svgNS,"text"); //CREATE A TEXT NODE score.setAttributeNS(null,"x",w*2-20); //START X score.setAttributeNS(null,"y",20); //START Y score.setAttributeNS(null,"fill","white"); //FILLCOLOR score.setAttributeNS(null,"font-family","helvetica"); //FONT score.setAttributeNS(null,"font-weight","bold"); //FONT-WEIGHT score.setAttributeNS(null,"font-size",20); //FONT-WEIGHT score.setAttributeNS(null,"text-anchor","end"); //FONT-WEIGHT score.textContent ="0"; //TEXT score.current = 0; //CURRENT SCORE GUI.appendChild(score) //APPEND TEXT TO THE GUI ELEMENT SVG.appendChild(GUI) //APPEND GUI TO THE SVG ELEMENT } function updateScore(dt) { score.current += dt/1000 //ADD TO SCORE score.textContent =Math.round(score.current); //UPDATE SCORE } function render() { var now = new Date().getTime(), //GET CURRENT TIME dt = now - (time || now); //FRAME TIME elapsed += dt/1000; //ELAPSED TIME SINCE ANIMATION START time = now; if(elapsed > 2 && !play) { if(!collide) { play = true; elapsed = 0; startText.textContent ="go"; //TEXT } } if(play) { if(startText.getAttribute("opacity") > 0) { startText.setAttributeNS(null,"opacity",startText.getAttribute("opacity") - 0.05); //FONT-WEIGHT document.getElementById("controllText").style.display = "none"; } updateScore(dt); updatePlayer(dt); updatePath(dt); updateObstacle(dt); } requestAnimationFrame(render); }
Creates a random path. Takes care of the path position and updates the path.
path.js
var pathWrapper; //GROUP THAT CONTAINS ALL PATH SEGMENTS var pathColor = "white"; //PATH COLOR var splineArray = []; //ARRAY OF X AND Y POINTS TO CREATE A SPLINE var precision = 20; //SUBDIVISION LEVEL OF THE PATH var size = 2000; //SIZE OF THE PATH var t = 0.5; //TENSION var length; //LENGTH OF THE PATH var path; //PATH ELEMENT function initPath() { createPath(); //CREATE THE PATH }; function createPath() { var svgNS = "http://www.w3.org/2000/svg"; //DEFINE THE namespaceURI pathWrapper = document.createElementNS(svgNS,"g") //CREATE A GROUP WHICH CONTAINS ALL PATH SEGMENTS splineArray = []; //EMPTY SPLINE ARRAY SVG.appendChild(pathWrapper) //CALCULATE OUTLINE POINTS OF A CIRCLE for (var i = 0; i < precision; i++) { var xValues =( w + size * Math.cos(2 * Math.PI * i / precision))+Math.random()*size/4;//ADD A RANDOM X VALUE TO THE CIRCLE var yValues = (h + size * Math.sin(2 * Math.PI * i / precision))+Math.random()*size/4;//ADD A RANDOM Y VALUE TO THE CIRCLE //PUSH POINTS TO THE ARRAY splineArray.push(xValues); splineArray.push(yValues); }; drawSpline(splineArray,t);//DRAW SPLINE WITH TENSION }; function getControlPoints(x0,y0,x1,y1,x2,y2,t){ // x0,y0,x1,y1 are the coordinates of the end (knot) pts of this segment // x2,y2 is the next knot -- not connected here but needed to calculate p2 // p1 is the control point calculated here, from x1 back toward x0. // p2 is the next control point, calculated here and returned to become the // next segment's p1. // t is the 'tension' which controls how far the control points spread. // Scaling factors: distances from this knot to the previous and following knots. var d01=Math.sqrt(Math.pow(x1-x0,2)+Math.pow(y1-y0,2)); var d12=Math.sqrt(Math.pow(x2-x1,2)+Math.pow(y2-y1,2)); var fa=t*d01/(d01+d12); var fb=t-fa; var p1x=x1+fa*(x0-x2); var p1y=y1+fa*(y0-y2); var p2x=x1-fb*(x0-x2); var p2y=y1-fb*(y0-y2); return [p1x,p1y,p2x,p2y] //Copyright 2010 by Robin W. Spencer //http://scaledinnovation.com/analytics/splines/aboutSplines.html }; function drawSpline(pts,t){ var cp=[]; // ARRAY OF CONTROL POINTS, AS x0,y0,x1,y1,... var n=pts.length; //NUMBER OF POINTS path = document.createElementNS("http://www.w3.org/2000/svg","path"); //CREATE A SVG PATH ELEMENT var pathSegment = ""; //THE PATH FOR THE SVG ELEMENT //APPEND AND PREPEND KNOTS AND CONTROL POINTS TO CLOSE THE CURVE pts.push(pts[0],pts[1],pts[2],pts[3]); pts.unshift(pts[n-1]); pts.unshift(pts[n-1]); //CALCULATE PATH SEGMENTS for(var i=0;i<n;i+=2){ cp=cp.concat(getControlPoints(pts[i],pts[i+1],pts[i+2],pts[i+3],pts[i+4],pts[i+5],t)); }; cp=cp.concat(cp[0],cp[1]); for(var i=2;i<n+2;i+=2) { pathSegment += "M"+pts[i]+" "+pts[i+1]+" C"+cp[2*i-2]+" "+cp[2*i-1]+" "+cp[2*i]+" "+cp[2*i+1]+" "+pts[i+2]+" "+pts[i+3]; //// SUBDIVIDED PATH TO MULTIPLE SEGMENTS FOR FASTER RENDERING?? // var pathSeg = document.createElementNS("http://www.w3.org/2000/svg","path"); // pathSeg.setAttributeNS(null,"d","M"+pts[i]+","+pts[i+1]+",C"+cp[2*i-2]+","+cp[2*i-1]+","+cp[2*i]+","+cp[2*i+1]+","+pts[i+2]+","+pts[i+3]); // pathSeg.setAttributeNS(null,"fill","none"); //PATH // pathSeg.setAttributeNS(null,"stroke","blue"); //PATH // pathSeg.setAttributeNS(null,"stroke-width","2px"); //PATH // pathWrapper.appendChild(pathSeg); }; path.setAttributeNS(null,"d",pathSegment); //PATH path.setAttributeNS(null,"fill","none"); //FILLCOLOR = NONE path.setAttributeNS(null,"stroke",pathColor); //PATHCOLOR path.setAttributeNS(null,"stroke-width","14px"); //PATH WIDTH path.pos = {x:0,y:0} //PATH POSITION pathWrapper.appendChild(path);//ADD PLAYER TO THE PATH GROUP SVG.appendChild(pathWrapper);//ADD PATH TO THE MAIN SVG ELEMENT length = path.getTotalLength(); //GET TOTAL LENGHT OF THE PATH duration = length/400; //DURATION TO CYCLE THE WHOLE PATH ONE TIME var pos = path.getPointAtLength( 0 ); //GET POSITION AT THE CURRENT FRAME pathWrapper.setAttribute("transform", "translate("+ (pos.x-w)*-1 +" "+ (pos.y-h)*-1 +")");//SET PATH TO START POSITION path.pos = {x:(pos.x-w)*-1,y:(pos.y-h)*-1} //STORE PATH POSITION }; function updatePath(dt) { if(!explode){ var t = elapsed / duration; //RUNNING TIME OF THE FULL ANIMATION var percent = t; //CURRENT PERCENTAGE OF THE PATH if(t >= 1){elapsed = 0;} //IF PLAYER REACH THE END RESET COUNTER var pos = path.getPointAtLength( length * percent ); //GET POSITION A CURRENT PERCENTAGE pathWrapper.setAttribute("transform", "translate("+ (pos.x-w)*-1 +" "+ (pos.y-h)*-1 +")");// MOVE PATH TO POSITION path.pos = {x:(pos.x)*-1,y:(pos.y)*-1} //UPDATE PATH POSITION } };
Creates the player, checks for collision on each frame, respond to collision.
player.js
var playerColor = "blue"; //PLAYER COLOR var explode = false; //IS EXPLODING var playerG; //PLAYERGROUP var ParticleG; //PLAYERGROUP var explosionParticle; //ARRAY OF PAARTICLES function initPlayer() { explosionParticle = []; //EMPTY PARTICLES createPlayer(); //CREATE THE PLAYER } function createPlayer() { var svgNS = "http://www.w3.org/2000/svg"; //DEFINE THE namespaceURI var center = document.createElementNS(svgNS,"circle"); //CREATE BASIC SVG ELEMENT(RECT, CIRCLE, ELLIPSE, LINE, POLYLINE, POLYGON) center.setAttributeNS(null,"cx",w); //CENTER OF THE CIRCLE X center.setAttributeNS(null,"cy",h); //CENTER OF THE CIRCLE Y center.setAttributeNS(null,"r",5); //RADIUS OF THE CIRCLE center.setAttributeNS(null,"fill",playerColor); //FILLCOLOR playerG = document.createElementNS(svgNS,"g"); //CREATE A GROUP FOR ALL THE PLAYER ELEMENTS ParticleG = document.createElementNS(svgNS,"g"); //CREATE A GROUP FOR ALL THE PARTICLES playerG.rotation = 0; //SET ROTATION playerG.speed = 20; //SET ROTATION SPEED var player = document.createElementNS(svgNS,"circle"); //CREATE BASIC SVG ELEMENT(RECT, CIRCLE, ELLIPSE, LINE, POLYLINE, POLYGON) player.setAttributeNS(null,"cx",w-100); //CENTER OF THE CIRCLE X player.setAttributeNS(null,"cy",h-100); //CENTER OF THE CIRCLE Y player.setAttributeNS(null,"r",20); //RADIUS OF THE CIRCLE player.setAttributeNS(null,"fill",playerColor); //FILLCOLOR var connect = document.createElementNS(svgNS,"path"); //CREATE BASIC SVG ELEMENT(RECT, CIRCLE, ELLIPSE, LINE, POLYLINE, POLYGON) connect.setAttributeNS(null,"d","M"+(w)+" "+(h+5)+" L"+(w+5)+" "+(h)+" L"+(w-90)+" "+(h-115)+" L"+(w-115) +" "+(h-90)+" Z");//DRAW A PATH connect.setAttributeNS(null,"fill",playerColor); //FILLCOLOR connect.setAttributeNS(null,"opacity","0.3"); //OPACITY //EXPOSION PARTICLES for (var i = 0; i < 5; i++) { var particle = document.createElementNS(svgNS,"rect"); //CREATE BASIC SVG ELEMENT(RECT, CIRCLE, ELLIPSE, LINE, POLYLINE, POLYGON) particle.setAttributeNS(null,"x",-50); //CENTER OF THE CIRCLE X particle.setAttributeNS(null,"y",-50); //CENTER OF THE CIRCLE Y particle.setAttributeNS(null,"width",20); //RADIUS OF THE CIRCLE particle.setAttributeNS(null,"height",20); //RADIUS OF THE CIRCLE particle.setAttributeNS(null,"fill","blue"); //FILLCOLOR particle.setAttributeNS(null,"stroke","none"); //OUTLINE COLOR particle.setAttributeNS(null,"opacity",1); //OPACITY particle.pos = {x:0,y:0}; //POS particle.vel = {x:Math.random()-0.5,y:Math.random()-0.5}; //VELOCITY explosionParticle.push(particle) //ADD OBSTACLES TO THE ARRAY ParticleG.appendChild(particle) //ADD PARTICLE TO THE GROUP } SVG.appendChild(ParticleG); //ADD PARTICLE TO THE MAIN SVG ELEMENT playerG.appendChild(connect); //ADD CONNECT TO THE PLAYER GROUP playerG.appendChild(player); //ADD PLAYER TO THE PLAYER GROUP playerG.appendChild(center); //ADD PLAYER TO THE PLAYER GROUP SVG.appendChild(playerG); //ADD GROUP TO THE MAIN SVG ELEMENT } function updatePlayer(dt) { if(right && !explode) { playerG.rotation +=playerG.speed/60*dt; //UPDATE PLAYER ROTATION playerG.setAttribute("transform", "rotate(" + playerG.rotation + " "+w+" "+ h+")"); //SET CURRENT ROTATION AND PIVOT } if(left && !explode) { playerG.rotation -=playerG.speed/60*dt; //UPDATE PLAYER ROTATION playerG.setAttribute("transform", "rotate(" + playerG.rotation + " "+w+" "+ h+")"); //SET CURRENT ROTATION AND PIVOT } //IF PLAYER EXPLODE if(explode) { playerG.setAttributeNS(null,"opacity",0); //FONT-WEIGHT var offsetX = playerG.childNodes[1].getBoundingClientRect().left;//GET PLAYER POSITION X var offsetY = playerG.childNodes[1].getBoundingClientRect().top; //GET PLAYER POSITION Y for (var i = 0; i < 5; i++) { explosionParticle[i].pos.x +=explosionParticle[i].vel.x*dt; //GET NEW X POSITION explosionParticle[i].pos.y +=explosionParticle[i].vel.y*dt; //GET NEW Y POSITION explosionParticle[i].setAttributeNS(null,"x",offsetX+explosionParticle[i].pos.x); //SET PARTICLE TO X POSITION explosionParticle[i].setAttributeNS(null,"y",offsetY+explosionParticle[i].pos.x); //SET PARTICLE TO Y POSITION //LOWER OPACITY OF PARTICLES if(explosionParticle[i].getAttribute("opacity") > 0) { explosionParticle[i].setAttributeNS(null,"opacity",explosionParticle[i].getAttribute("opacity") - dt/500); //SET OPACITY } else { explode = false; //SET EXPLODE BACK TO FALSE if(score.current > highscore) //CHECK IF CURRENT SCORE IS HIGHER AS HIGHSCORE { highscore = Math.round(score.current); } document.getElementById("controllText").style.display = "block"; //DISPLAY HIGHSCORE document.getElementById("controllText").textContent ="HIGHSCORE: " + highscore; //UPDATE HIGHSCORE score.current = 0; //RESET SCORE score.textContent =Math.round(score.current); //UPDATE SCORE pathWrapper.remove(); //REMOVE OLD PATH playerG.remove(); //REMOVE OLD PATH ParticleG.remove(); //REMOVE OLD PARTICLES elapsed = 0; //RESET TIMER play = false; //SET PLAY TO FALSE } } } if(!explode && !play) { initPath(); //INIT NEW PATH initPlayer(); //INIT NEW PLAYER initObjstacles(); //INIT NEW OBSTACLES startText.textContent ="ready"; //RESET TEST startText.setAttributeNS(null,"opacity",1); //RESET OPACITY } }
Creates the obstacles and update them if necessarily.
obstacle.js
var ObstacleArray = [];//pool var maxObstacles = 7; //MAX OBSTACLES IN THE POOL var resetTimer = 0; // var collide = false; //CURRENTLY COLLIDES var minDistance = 100; //MIN DISTANCE BETWEEN OBSTACLES function initObjstacles() { createObstacles(); //CREATE OBSTACLES AN PUT THEM INTO THE POOL } function createObstacles() { var svgNS = "http://www.w3.org/2000/svg"; //DEFINE THE namespaceURI ObstacleArray = []; //EMPTY THE ARRAY FIRST for (var i = 0; i < maxObstacles; i++) //CREATE "maxObstacles" OBJECTS { var Obstacle = document.createElementNS(svgNS,"circle"); //CREATE BASIC SVG ELEMENT(RECT, CIRCLE, ELLIPSE, LINE, POLYLINE, POLYGON) Obstacle.setAttributeNS(null,"class","circle"); //CLASS OF THE CIRCLE, OBJECT CAN BE MODIFIED VIA CSS Obstacle.setAttributeNS(null,"cx",0); //CENTER OF THE CIRCLE X Obstacle.setAttributeNS(null,"cy",0); //CENTER OF THE CIRCLE Y Obstacle.setAttributeNS(null,"r",50); //RADIUS OF THE CIRCLE Obstacle.setAttributeNS(null,"fill","black"); //FILLCOLOR Obstacle.setAttributeNS(null,"stroke","none"); //OUTLINE COLOR Obstacle.pos = {x:0,y:0}; //POS Obstacle.shown = true; //SET SHOW FLAG TO TRUE -> IT WILL BE SET AUTOMATICLY ObstacleArray.push(Obstacle) //ADD OBSTACLES TO THE ARRAY pathWrapper.appendChild(Obstacle); //ADD OBSTACLES TO THE MAIN SVG ELEMENT } } function placeObstacle(o) { var percent = elapsed / duration; //CURRENT PERCENTAGE OF THE PATH var t = (length * percent)+h*2; //CURRENT t OF THE PATH var pos = path.getPointAtLength(t); //GET POINT var p0 = path.getPointAtLength(t-1) //GET POINT BEFORE var p1 = path.getPointAtLength(t+1) //GET POINT AFTER var angle = Math.atan2(p1.y-p0.y,p1.x-p0.x)*180 / Math.PI; //GET ANGLE var rot = (angle) * (Math.PI/180); // CONVERT TO RADIANS var random = Math.random() *200 -100; //GET A RANDOM VALUE var rotatedX = Math.cos(rot) * ((pos.x + random) - pos.x) - Math.sin(rot) * ((pos.y + random)-pos.y) + pos.x; //GET ROTATED X VALUE var rotatedY = Math.sin(rot) * ((pos.x + random) - pos.x) + Math.cos(rot) * ((pos.y + random) - pos.y) + pos.y; //GET ROTATED Y VALUE o.setAttributeNS(null,"cx",rotatedX); //SET OBSTACLE TO X POSITION o.setAttributeNS(null,"cy",rotatedY); //SET OBSTACLE TO Y POSITION o.pos = {x:rotatedX,y:rotatedY}; //UPDATE OBSTACLE POSITION } function updateObstacle(dt) { resetTimer += dt/1000; //UPDATE RESET TIMER var offsetX = playerG.childNodes[1].getBoundingClientRect().left - w-20;//GET PLAYER POSITION X var offsetY = playerG.childNodes[1].getBoundingClientRect().top - h-20; //GET PLAYER POSITION Y // //CHECK FOR COLLISION for (var i = 0; i < ObstacleArray.length; i++) { if(ObstacleArray[i].pos.y-path.pos.y*-1 <= 0-h || ObstacleArray[i].pos.y-path.pos.y*-1 >= h || ObstacleArray[i].pos.x-path.pos.x*-1 < = 0-w || ObstacleArray[i].pos.x-path.pos.x*-1 >= w) //IS OUT OF SCREEN TOP/BOTTOM/LEFT/RIGHT { if(ObstacleArray[i].shown && resetTimer > 1)//IF THE OBSTACLE HAS ALREADY SHOWN UP -> REPLACE { placeObstacle(ObstacleArray[i]) //REPLACE OBJECT ObstacleArray[i].shown = false; //SET SHOWN FLAG TO FALSE resetTimer = Math.random(); //SET A RANDOM TIMER FOR MORE PATH VARIATION } } else //IF OBJECT IS CURRENTLY ON THE SCREEN { ObstacleArray[i].shown = true; //SET SHOW FLAG TO TRUE //GET DISTANCE BETWEEN OBSTACLE AND PLAYER var distance_x =((ObstacleArray[i].pos.x-50)-(path.pos.x*-1+offsetX)); var distance_y =((ObstacleArray[i].pos.y-50)-(path.pos.y*-1+offsetY)); var distance = Math.sqrt(distance_x*distance_x+distance_y*distance_y); if(distance < = 70) //IF DISTANCE IS SHORTER THE RADIUS1 + RADIUS2 ->COLLISION { //RESET GAME explode = true; return; } } } }
Check for user input, apply input correct to player.
controll.js
var left = false; var right = false; function initControll() { document.onkeydown=function(e) { if(e.keyCode == 37 && left==false)//LEFT { e.cancelBubble = true; e.returnValue = false; left = true; return; } if(e.keyCode == 39 && right==false)//RIGHT { e.cancelBubble = true; e.returnValue = false; right = true; return; } return; }; document.onkeyup=function(e){left = false;right = false;} document.addEventListener('touchstart', function(e) { if(e.touches[0].pageX < w)//LEFT { left = true; } else { right = true; }; },false); document.addEventListener('touchend', function(e) { left = false;right = false; },false); }
Basic render tick with requestAnimationFrame.
requestAnimationFrame.js
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/ // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel // MIT license (function() { var lastTime = 0; var vendors = ['ms', 'moz', 'webkit', 'o']; for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; } if (!window.requestAnimationFrame) window.requestAnimationFrame = function(callback, element) { var currTime = new Date().getTime(); var timeToCall = Math.max(0, 16 - (currTime - lastTime)); var id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; }; if (!window.cancelAnimationFrame) window.cancelAnimationFrame = function(id) { clearTimeout(id); }; }());
(click files to open)
conclusion
A very decent html5/SVG Pivvot clone is the Result. Thankfully the SVG element supports the “getPointAtLength” function, which saves us a lot of time and power. Almost any part of the game is based or related to this function. It could go really tricky if this function would not exist, with a whole lot of curve math, parametrized curves and path subdivision, to get a clean and controlled animation. Even if the prototype just creates a circle path, it should be an easy task to create a endless path or spiral along a number of points.
Anyway, the prototype works quite well, even on slower mobile devices it runs around 30 fps. Which is not perfect in any way, but it’s not that bad for SVG animated elements.