当前位置:  开发笔记 > 编程语言 > 正文

如何为跟踪鼠标移动的动画添加惯性?

如何解决《如何为跟踪鼠标移动的动画添加惯性?》经验,为你挑选了1个好方法。

我正在尝试使用mouseMove事件围绕原点旋转三角形.
我得到的触摸起始点当前触摸点的使用touchstarttouchmove事件,然后我发现方向的角度很容易使用:

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循环管理我的画作.

问题是动画不稳定,我需要旋转惯性.怎么能修复这个功能?



1> Blindman67..:

Inertia, acceleration, and drag

A method I use is to create a chasing value that chases the desired value using a deltaV that simulates acceleration and drag (resistance) values. Note this is a simple simulation.

Step by step

Defining required values

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

The drag is a value greater than 0 and <= 1 and is more like a damping spring in function. Values less than 0.5 provide a smooth stop to the counting value. Values >= 0.5 cause the chase value to bounce around the required value, as the drag value moves towards 1 the bounce become more and more pronounced.

The acceleration value is a range greater than 0 and <= 1. This is how responsive to change the chase value is. Low values will make the control seem sluggish or heavy. Higher values make the chase quick to respond and seem light to the touch.

拖动和加速都相互影响.对于

快速响应accel = 0.9,drag = 0.49

反应迟钝accel = 0.1,drag = 0.49

沉重的响应速度慢accel = 0.02,drag = 0.49

坚硬的反应accel = 0.7,drag = 0.7

缓慢的弹性反应accel = 0.1,drag = 0.7

更新

然后一旦一帧,通过添加到deltaV加速朝向输入值,

rotateDelta += (rotate - rotateChase) * rotateAcceleration;

通过降低其强度将拖动添加到增量.

rotateDelta *= rotateDrag;

然后将deltaV添加到追逐值,

rotateChase += rotateDelta;

现在它已准备好显示追逐值

myTriangle.rotation = rotateChase;

添加边界

那是一个无约束的追逐者.要设置值的边界需要一些额外的代码.首先通过设置最小值和最大值来描述边界.

var rotateMin = 0;
var rotateMax = Math.PI*2;

Then describe the behaviour if the value crosses the bounds by defining the reflection or bounce. 0 makes it stop dead at the bounds, < 0 and >= -1 will give a small bounce from the ends. Other values create interesting FX

var rotateReflect = -0.5;  

and then the code to control that behaviour

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;
   }
}

Display and use

You are now just left with the choice of which value to use as the input of the control. rotateChase and rotate can both be used but rotateChase can take a little time to settle down to. What I do is get a rounded value from the rotateChase value which removes the fine detail of the chase.

For example if the control is for a volume

device.volume = Number(rotateChase.toFixed(3));

Making it easy

That all seams like a lot of work for one value. But we are programmers and inherently lazy, so lets compartmentalise it into a simple inertia class

// 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;
     }
 }

To use the code above

// 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

I have added some code to show the various settings and how they effect inertia. The code is not intended as an example of code style or DOM interfacing best practice, for it falls very far short on both counts. It uses the Inertia object I presented above. You can find that object at the top of the demo code.

The demo is best viewed Full Screen

//------------------------------------------------------
// 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, 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;
    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,y,1,x,y,r);
    g.addColorStop(0,col);
    g.addColorStop(1,colB);
    c.fillStyle = g;
    c.beginPath();
    c.arc(x,y,r-width*3,0,Math.PI*2);
    c.fill();

    c.strokeStyle = col1;
    c.fillStyle = col1;
    c.fillRect(x,y-width,r,width*2)
    c.fillStyle = col;
    c.fillRect(x+width,y-width/2,r,width)
}
var drawCircleO= function(img,x,y,r,col,colB,col1,width){
    var c = img.ctx;
    var g = c.createRadialGradient(x+r*0.21,y+r*0.21,r*0.7,x+r*0.21,y+r*0.21,r);
    g.addColorStop(0,"black");
    g.addColorStop(1,"rgba(0,0,0,0)");
    c.fillStyle = g;
    c.globalAlpha = 0.5;
    c.beginPath();
    c.arc(x+r*0.21,y+r*0.21,r,0,Math.PI*2);
    c.fill();
    c.globalAlpha = 1;
    var g = c.createRadialGradient(x*0.3,y*0.3,r*0.5,x*0.3,y*0.3,r);
    g.addColorStop(0,col);
    g.addColorStop(1,colB);
    c.lineWidth = width;
    c.strokeStyle = col1;
    c.fillStyle = g;
    c.beginPath();
    c.arc(x,y,r-width,0,Math.PI*2);
    c.stroke();
    c.fill();
}
// draws radial marks with miner markes 
// len,col and width are arrays
var drawCircleMarks= function(img,x,y,r,start,end,col,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,0.4);
        }
    },
    "value":{
        validate:function(val){
            return defaultMap.number(val,0);
        }
    },
    "tick-color":{
        validate:function(val){
            
        }
    },
    "decimals":{
        validate:function(val){
            return defaultMap.number(val,0);
        }
    },
    "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,drag,accel;
    var dialUpdate = function(){
        var unitPos = (this.value-this.min)/(this.max-this.min);
        canvas.ctx.setTransform(1,0,0,1,0,0);
        canvas.ctx.clearRect(0,0,w,h);
        canvas.ctx.drawImage(back,0,0);
        canvas.ctx.setTransform(1,0,0,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,drag,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),minSize*(3/4)*(1/2),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/2,
            minSize/3,
            startP,
            endP,
            ["black","#666"],
            [2,1],
            [minSize*(1/4),minSize*(1/9)],
            16,
            4
        );
        drawCircleO(back,minSize*(1/2),minSize*(1/2),minSize*(3/4)*(1/2),"white","#aaa","black",3);        
        drawFunc = dialUpdate.bind(this);
    }
    element.appendChild(canvas);
    this.active = true;
    this.resetChaser = function(min,max,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,this.max,accel,drag,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;
}

Examples of variouse Drag and Acceleration settings

Click on the control to change the setting. Click drag to adjust setting. The first two rows are preset. D and A above the control are the Drag and Acceleration settings for the control under it.

D:0.1 A:0.9 D:0.2 A:0.8 D:0.3 A:0.7 D:0.4 A:0.6 D:0.5 A:0.5 D:0.6 A:0.4 D:0.7 A:0.3 D:0.8 A:0.2 D:0.9 A:0.1
D:0.9 A:0.9 D:0.8 A:0.8 D:0.7 A:0.7 D:0.6 A:0.6 D:0.5 A:0.5 D:0.4 A:0.4 D:0.3 A:0.3 D:0.2 A:0.2 D:0.1 A:0.1

The following 3 dials control the inertia setting of the large dial

Drag 0.000 Accel 0.000 Reflect 0.000
Letf click drag to change

推荐阅读
帆侮听我悄悄说星星
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有