我正在使用touchstart和touchmove事件获得触摸起点和当前触摸点,然后我轻松地使用以下方法找到方向角:
alpha = y2 - y1 / x2 - x1; // alpha is the tangent of the angle beta= atan(alpha); // beta is the angle in radians
然后我在PIXI中旋转元素:
function animTriangle (beta) { // initially clear the prevIoUs shape myTriangle.clear(); // draw a new shape in new positions myTriangle.beginFill(0x000000,0.1); myTriangle.moveTo(origin.x,origin.y); myTriangle.lineTo(origin.x - 100,0); myTriangle.lineTo(origin.x + 100,0); myTriangle.lineTo(origin.x,origin.y); myTriangle.endFill(); myTriangle.rotation = beta; }
我正在使用RequestAnimationFrame循环管理我的绘画.
问题是动画不稳定,我需要旋转惯性.怎么能修复这个功能?
解决方法
我使用的方法是创建一个追逐值,该值使用模拟加速度和阻力(阻力)值的deltaV来追踪所需的值.请注意,这是一个简单的模拟.
一步步
定义所需的值
var rotate = ?; // the value input by the user var rotateChase; // this is the chasing value representing the displayed control var rotateDelta; // this is the change in chase per frame const rotateDrag = 0.4; // this is the friction or drag const rotateAcceleration = 0.9; // this is how quickly the display responds
阻力是大于0且<= 1的值,并且更像功能中的阻尼弹簧.小于0.5的值可以平稳地停止计数值.值> = 0.5导致追逐值在所需值附近反弹,因为拖动值向1移动,反弹变得越来越明显.
加速度值是大于0且<= 1的范围.这是响应于改变追踪值的方式.较低的值会使控制看起来缓慢或沉重.较高的值使追逐快速响应并且看起来很轻松. 拖动和加速都相互影响.
对于
>快速光响应accel = 0.9,阻力= 0.49
>轻缓慢响应accel = 0.1,阻力= 0.49
>重慢响应accel = 0.02,阻力= 0.49
>硬弹性响应accel = 0.7,阻力= 0.7
>慢弹性响应accel = 0.1,阻力= 0.7
更新
然后一旦一帧,通过添加到deltaV加速朝向输入值,
rotateDelta += (rotate - rotateChase) * rotateAcceleration;
通过降低其强度将拖动添加到增量.
rotateDelta *= rotateDrag;
然后将deltaV添加到追逐值,
rotateChase += rotateDelta;
现在它已准备好显示追逐值
myTriangle.rotation = rotateChase;
添加边界
那是一个无约束的追逐者.要设置值的边界需要一些额外的代码.首先通过设置最小值和最大值来描述边界.
var rotateMin = 0; var rotateMax = Math.PI*2;
然后通过定义反射或反弹来描述值是否超过边界的行为. 0使它在界限处停止死亡,< 0和> = -1将从末端给出小的反弹.其他价值创造有趣的外汇
var rotateReflect = -0.5;
然后是控制该行为的代码
if (rotateChase < rotateMin) { rotateChase = rotateMin; // set to the min val if(rotateDelta < 0){ // only if delta is negative rotateDelta *= rotateReflect; } }else if (rotateChase > rotateMax) { rotateChase = rotateMax; // set to the max if(rotateDelta > 0){ // only if delta is positive rotateDelta *= rotateReflect; } }
显示和使用
您现在可以选择使用哪个值作为控件的输入. rotateChase和rotate都可以使用但是rotateChase可能需要一点时间来安顿下来.我所做的是从rotateChase值中获取一个舍入值,该值将删除追踪的精细细节.
例如,如果控件是针对卷
device.volume = Number(rotateChase.toFixed(3));
让它变得简单
所有的接缝都像一个值很多的工作.但是我们是程序员并且本质上是懒惰的,所以我们将它划分为一个简单的惯性类
// Define a Inertia object. Set Answer for details. // Has methods // update(input); Called once pre animation frame with input being the value to chase // setValue(input); Hard sets the chasing value. Not drag or inertia // Has properties // value; The chasing value bounds checked function Inertia (min,max,acceleration,drag,reflect) { // some may question why the constants,why not use the closure on arguments // Reason: Some JS environments will fail to optimise code if the input // arguments change. It may be tempting to extend this Object to // change the min,max or others. I put this here to highlight the // fact that argument closure variables should not be modified // if performance is important. const ac = acceleration; // set constants const dr = drag; const minV = min; const maxV = max; const ref = -Math.abs(reflect); // ensure a negative. Why? because I always forget this is a negative. this.value = min; var delta = 0; this.update = function (input) { delta += (input - this.value) * ac; delta *= dr; this.value += delta; if (this.value < minV) { this.value = minV; if(delta < 0){ delta *= ref; } } else if (this.value > maxV) { this.value = maxV; if(delta > 0){ delta *= ref; } } return this.value; }; // this move the value to the required value without any inertial or drag // is bound checked this.setValue = function (input) { delta = 0; this.value = Math.min(maxV,Math.min(minV,input)); return this.value; } }
要使用上面的代码
// in init var rotater = new Inertia(0,Math.PI*2,0.9,0.4,-0.1); // in the animation frame myTriange = rotater.update(beta);
UPDATE
我添加了一些代码来显示各种设置以及它们如何影响惯性.该代码不是作为代码样式或DOM接口最佳实践的示例,因为它在两个方面都非常短.它使用我上面提到的惯性对象.您可以在演示代码的顶部找到该对象.
该演示最佳浏览全屏
//------------------------------------------------------ // Function from answer Inertia // Define a Inertia object. Set Answer for details. // Has methods // update(input); Called once pre animation frame with input being the value to chase // set(input); Hard sets the chasing value. Not drag or inertia // Has properties // value; The chasing value bounds checked function Inertia (min,max or others. I put this here to highlight the // fact that argument closure variables should not be modified // if performance is important. const ac = acceleration; // set constants const dr = drag; const minV = min; const maxV = max; const ref = -Math.abs(reflect); // ensure a negative. Why? because I always forget this is a negative. this.value = min; this.quiet = true; var delta = 0; this.update = function (input) { delta += (input - this.value) * ac; delta *= dr; this.value += delta; if (this.value < minV) { this.value = minV; if(delta < 0){ delta *= ref; } } else if (this.value > maxV) { this.value = maxV; if(delta > 0){ delta *= ref; } } if(Math.abs(delta) < (maxV-minV)*0.001 && Math.abs(this.value-input) < 0.1 ){ this.quiet = true; }else{ this.quiet = false; } return this.value; }; // this move the value to the required value without any inertial or drag // is bound checked this.setValue = function (input) { delta = 0; this.quiet = true; this.value = Math.min(maxV,Math.max(minV,input)); return this.value; } } // End of answer //-------------------------------------------------------- // All the following code is not part of the answer. // I have not formatted,commented,and thoroughly tested it /** MouseFullDemo.js begin **/ var canvasMouseCallBack = undefined; // if needed function createMouse(element){ var demoMouse = (function(){ var mouse = { x : 0,y : 0,w : 0,alt : false,shift : false,ctrl : false,lx:0,ly:0,interfaceId : 0,buttonLastRaw : 0,buttonRaw : 0,over : false,// mouse is over the element bm : [1,2,4,6,5,3],// masks for setting and clearing button raw bits; getInterfaceId : function () { return this.interfaceId++; },// For UI functions startMouse:undefined,}; function mouseMove(e) { //console.log(e) var t = e.type,m = mouse; m.lx = e.offsetX; m.ly = e.offsetY; m.x = e.clientX; m.y = e.clientY; m.alt = e.altKey;m.shift = e.shiftKey;m.ctrl = e.ctrlKey; if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1]; } else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2]; } else if (t === "mouSEOut") {m.over = false; } else if (t === "mouSEOver") { m.over = true; } else if (t === "mousewheel") { m.w = e.wheelDelta; } else if (t === "DOMMouseScroll") { m.w = -e.detail;} if (canvasMouseCallBack) { canvasMouseCallBack(m.x,m.y); } e.preventDefault(); } function startMouse(element){ if(element === undefined){ element = document; } "mousemove,mousedown,mouseup,mouSEOut,mouSEOver,mousewheel,DOMMouseScroll".split(",").forEach( function(n){element.addEventListener(n,mouseMove);}); element.addEventListener("contextmenu",function (e) {e.preventDefault();},false); } mouse.mouseStart = startMouse; return mouse; })(); demoMouse.mouseStart(element); return demoMouse; } /** MouseFullDemo.js end **/ var cellSize = 70; var createImage=function(w,h){ var i=document.createElement("canvas"); i.width=w; i.height=h; i.ctx=i.getContext("2d"); return i; } var drawCircle= function(img,x,y,r,col,colB,col1,width){ var c = img.ctx; var g; c.lineWidth = width; c.strokeStyle = col1; g = c.createRadialGradient(x,1,r); g.addColorStop(0,col); g.addColorStop(1,colB); c.fillStyle = g; c.beginPath(); c.arc(x,r-width*3,Math.PI*2); c.fill(); c.strokeStyle = col1; c.fillStyle = col1; c.fillRect(x,y-width,width*2) c.fillStyle = col; c.fillRect(x+width,y-width/2,width) } var drawCircleO= function(img,width){ var c = img.ctx; var g = c.createRadialGradient(x+r*0.21,y+r*0.21,r*0.7,x+r*0.21,"black"); g.addColorStop(1,"rgba(0,0)"); c.fillStyle = g; c.globalAlpha = 0.5; c.beginPath(); c.arc(x+r*0.21,Math.PI*2); c.fill(); c.globalAlpha = 1; var g = c.createRadialGradient(x*0.3,y*0.3,r*0.5,x*0.3,colB); c.lineWidth = width; c.strokeStyle = col1; c.fillStyle = g; c.beginPath(); c.arc(x,r-width,Math.PI*2); c.stroke(); c.fill(); } // draws radial marks with miner markes // len,col and width are arrays var drawCircleMarks= function(img,start,end,width,length,number,miner){ var i,vx,vy,count,style,len; var c = img.ctx; var step = (end-start)/number; count = 0; end += step/2; // add to end to account for floating point rounding error for(i = start; i <= end; i+= step){ vx = Math.cos(i); vy = Math.sin(i); if(count % miner === 0){ style = 0; }else{ style = 1; } c.strokeStyle = col[style]; c.lineWidth = width[style]; len = length[style]; c.beginPath(); c.moveTo(vx*r+x,vy*r+y); c.lineTo(vx*(r+len)+x,vy*(r+len)+y); c.stroke(); count += 1; } } var defaultMap = { number:function(num,def){ if( isNaN(num) ){ return def } return Number(num); },colour:function(col,def){ // no much code for demo so removed if(col === undefined || typeof col !== "string"){ return def; } return col; },"ticks":{ validate:function(val){ return val===undefined?true:val?true:false; },},"type":{ validate:function(val){ switch (val) { case "dial": case "horizontal-slider": return val; } return undefined } },"min":{ validate:function(val){ return defaultMap.number(val,0); } },"max":{ validate:function(val){ return defaultMap.number(val,100); } },"drag":{ validate:function(val){ return defaultMap.number(val,0.4); } },"reflect":{ validate:function(val){ return defaultMap.number(val,0.2); } },"accel":{ validate:function(val){ return defaultMap.number(val,"value":{ validate:function(val){ return defaultMap.number(val,"tick-color":{ validate:function(val){ } },"decimals":{ validate:function(val){ return defaultMap.number(val,"display":{ validate:function(val){ if(val === null || val === undefined || typeof val !== "string"){ return undefined; } return document.querySelector(val); } } } // validates user defined DOM attribute function getSafeAttribute(element,name){ var val,def; if(name === undefined){ return undefined; } def = defaultMap[name]; if(def === undefined){ // unknown attribute if(element.attributes["data-"+name]){ return element.attributes["data-"+name].value; } return undefined } if(element.attributes["data-"+name]){ val = element.attributes["data-"+name].value; } return def.validate(val); } // Custom user control // Warning this can return a undefined control function Control(element,owner){ var dialUpdate,drawFunc,w,h,nob,back,mouse,minSize,canvas,chaser,dragging,dragX,dragY,dragV,realValue,startP,endP,lastVal,reflect,accel; var dialUpdate = function(){ var unitPos = (this.value-this.min)/(this.max-this.min); canvas.ctx.setTransform(1,0); canvas.ctx.clearRect(0,h); canvas.ctx.drawImage(back,0); canvas.ctx.setTransform(1,back.width/2,back.height/2); canvas.ctx.rotate(unitPos *(endP-startP)+startP); canvas.ctx.drawImage(nob,-nob.width/2,-nob.height/2); } if(element === undefined){ // To my UNI mentor with love.. LOL return undefined; } this.type = getSafeAttribute(element,"type"); if(this.type === undefined){ return undefined; // this is a non standared contrutor return } this.owner = owner; // expose owner // exposed properties this.min = getSafeAttribute(element,"min"); this.max = getSafeAttribute(element,"max"); this.ticks = getSafeAttribute(element,"ticks"); this.tickColor = getSafeAttribute(element,"tick-color"); this.value = realValue = getSafeAttribute(element,"value"); this.display = getSafeAttribute(element,"display"); if(this.display){ var decimals = getSafeAttribute(element,"decimals"); } drag = getSafeAttribute(element,"drag"); accel = getSafeAttribute(element,"accel"); reflect = getSafeAttribute(element,"reflect");; chaser = new Inertia(this.min,this.max,accel,reflect); w = element.offsetWidth; h = element.offsetHeight; canvas = createImage(w,h); minSize = Math.min(w,h); mouse = createMouse(element); if(this.type === "dial"){ nob = createImage(minSize*(3/4),minSize*(3/4)); drawCircle(nob,minSize*(3/4)*(1/2),"white","#CCC","black",3); back = createImage(minSize,minSize); startP = Math.PI*(3/4); endP = Math.PI*(9/4); drawCircleMarks( back,minSize/2,minSize/3,["black","#666"],[2,1],[minSize*(1/4),minSize*(1/9)],16,4 ); drawCircleO(back,minSize*(1/2),"#aaa",3); drawFunc = dialUpdate.bind(this); } element.appendChild(canvas); this.active = true; this.resetChaser = function(min,accel1,drag1,reflect1){ this.min = min===null?this.min:min; this.max = max===null?this.max:max; drag = drag1===null?drag:drag1; accel = accel1===null?accel:accel1; reflect = reflect1===null?reflect:reflect1; chaser = new Inertia(this.min,reflect); chaser.setValue(this.value); drawFunc(); } this.update = function(){ var inVal; if(mouse.over){ element.style.cursor = "drag_ew"; } if((this.owner.mouse.buttonRaw&1) === 1 && !dragging && mouse.over && this.owner.draggingID === -1){ dragX = this.owner.mouse.x - (mouse.lx-w/2); dragY = this.owner.mouse.y - (mouse.ly-h/2); dragging = true; this.owner.draggingID = this.ID; }else if(this.owner.draggingID === this.ID && ((this.owner.mouse.buttonRaw&1) === 1 || (this.owner.mouse.buttonRaw&1) === 0) && dragging){ inVal = (Math.atan2(this.owner.mouse.y-dragY,this.owner.mouse.x-dragX)+Math.PI*2); if(inVal > Math.PI*0.5+Math.PI*2){ inVal -= Math.PI*2; } realValue = inVal; realValue = ((realValue-startP)/(endP-startP))*(this.max-this.min)+this.min; if((this.owner.mouse.buttonRaw&1) === 0){ dragging = false; this.owner.draggingID = -1; } } realValue = Math.min(this.max,Math.max(this.min,realValue)); this.value = chaser.update(realValue); if(!chaser.quiet){ drawFunc(); if(this.display){ this.display.textContent = realValue.toFixed(decimals); } if(this.onchange !== undefined && typeof this.onchange === "function"){ this.onchange({value:realValue,target:element,control:this}); } } } // force chaser to wake up chaser.setValue(this.value); drawFunc(); element.control = this; } // find and create controllers function Controllers(name){ var controls,elems,i,control,e; var ID = 0; controls = []; elems = document.querySelectorAll("."+name); for(i = 0; i < elems.length; i++){ e = elems[i]; control = new Control(e,this); control.ID = ID++; if(control !== undefined){ controls.push(control); } } this.update = function(){ controls.forEach(function(cont){ cont.update(); }) } this.mouse = createMouse(document); this.draggingID = -1; } // get elements to play with the large control var c = new Controllers("testControl"); var drag = document.getElementById("dragSetting"); var accel = document.getElementById("accelSetting"); var reflect = document.getElementById("reflectSetting"); var bigDial = document.getElementById("bigDial"); var bigDialt = document.getElementById("bigDialText"); // callback for large controller function changeBigDial(e){ bigDial.control.resetChaser(null,null,drag.control.value,accel.control.value,reflect.control.value); if(accel.control.value === 0 || drag.control.value === 0){ var str = "Can no move as Drag and/or Acceleration is Zero"; }else{ var str = "A:"+ accel.control.value.toFixed(3); str += "D:"+ drag.control.value.toFixed(3); str += "R:-"+ reflect.control.value.toFixed(3); } bigDialt.textContent = str; } // set callbacks drag.control.onchange = changeBigDial; accel.control.onchange = changeBigDial; reflect.control.onchange = changeBigDial; // Update all controls function update(){ c.update(); requestAnimationFrame(update); } update();
.testControl { width:110px; height:110px; display: inline-block; text-align:center; } .big { width:200px; height:200px; } .demo { text-align:center; }
<div class="demo"><h3> Examples of varIoUse Drag and Acceleration settings</h3> <p>Click on the control to change the setting. Click drag to adjust setting. The first two rows are preset. <b>D</b> and <b>A</b> above the control are the <b>D</b>rag and <b>A</b>cceleration settings for the control under it.</p> <span class="testControl" data-drag="0.1" data-accel="0.9" data-value="0" data-type = "dial" ><b>D</b>:0.1 <b>A</b>:0.9</span> <span class="testControl" data-drag="0.2" data-accel="0.8" data-value="0" data-type = "dial" ><b>D</b>:0.2 <b>A</b>:0.8</span> <span class="testControl" data-drag="0.3" data-accel="0.7" data-value="0" data-type = "dial" ><b>D</b>:0.3 <b>A</b>:0.7</span> <span class="testControl" data-drag="0.4" data-accel="0.6" data-value="0" data-type = "dial" ><b>D</b>:0.4 <b>A</b>:0.6</span> <span class="testControl" data-drag="0.5" data-accel="0.5" data-value="0" data-type = "dial" ><b>D</b>:0.5 <b>A</b>:0.5</span> <span class="testControl" data-drag="0.6" data-accel="0.4" data-value="0" data-type = "dial" ><b>D</b>:0.6 <b>A</b>:0.4</span> <span class="testControl" data-drag="0.7" data-accel="0.3" data-value="0" data-type = "dial" ><b>D</b>:0.7 <b>A</b>:0.3</span> <span class="testControl" data-drag="0.8" data-accel="0.2" data-value="0" data-type = "dial" ><b>D</b>:0.8 <b>A</b>:0.2</span> <span class="testControl" data-drag="0.9" data-accel="0.1" data-value="0" data-type = "dial" ><b>D</b>:0.9 <b>A</b>:0.1</span><br> <span class="testControl" data-drag="0.9" data-accel="0.9" data-value="0" data-type = "dial" ><b>D</b>:0.9 <b>A</b>:0.9</span> <span class="testControl" data-drag="0.8" data-accel="0.8" data-value="0" data-type = "dial" ><b>D</b>:0.8 <b>A</b>:0.8</span> <span class="testControl" data-drag="0.7" data-accel="0.7" data-value="0" data-type = "dial" ><b>D</b>:0.7 <b>A</b>:0.7</span> <span class="testControl" data-drag="0.6" data-accel="0.6" data-value="0" data-type = "dial" ><b>D</b>:0.6 <b>A</b>:0.6</span> <span class="testControl" data-drag="0.5" data-accel="0.5" data-value="0" data-type = "dial" ><b>D</b>:0.5 <b>A</b>:0.5</span> <span class="testControl" data-drag="0.4" data-accel="0.4" data-value="0" data-type = "dial" ><b>D</b>:0.4 <b>A</b>:0.4</span> <span class="testControl" data-drag="0.3" data-accel="0.3" data-value="0" data-type = "dial" ><b>D</b>:0.3 <b>A</b>:0.3</span> <span class="testControl" data-drag="0.2" data-accel="0.2" data-value="0" data-type = "dial" ><b>D</b>:0.2 <b>A</b>:0.2</span> <span class="testControl" data-drag="0.1" data-accel="0.1" data-value="0" data-type = "dial" ><b>D</b>:0.1 <b>A</b>:0.1</span><br> <h3>The following 3 dials control the inertia setting of the large dial</h3> <span class="testControl" id="dragSetting" data-value="0" data-display="#display-drag" data-decimals="3" data-min="0" data-max="1" data-type = "dial" >Drag <span id="display-drag">0.000</span></span> <span class="testControl" id="accelSetting" data-value="0" data-display="#display-accel" data-decimals="3" data-min="0" data-max="1" data-type = "dial" >Accel <span id="display-accel">0.000</span></span> <span class="testControl" id="reflectSetting" data-value="0" data-display="#display-reflect" data-decimals="3" data-min="0" data-max="1" data-type = "dial" >Reflect <span id="display-reflect">0.000</span></span> </div> <div class="demo"> <div class="testControl big" id="bigDial" data-drag="0.1" data-accel="0.1" data-value="0" data-type = "dial" ><span id="bigDialText">Letf click drag to change</span></div><br> </div>
希望这可以帮助.