我正在尝试使用mouseMove事件围绕原点旋转三 Angular 形。
我使用touchstart
和touchmove
事件获取触摸起点和当前触摸点,然后使用以下方法轻松找到方向 Angular :
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且 = 0.5会导致追赶值在所需值附近反弹,随着阻力值移向1,反弹会变得越来越明显。
加速度值的范围大于0且
拖动和加速都相互影响。
为了
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;
通过减少其变形来将拖动添加到三 Angular 洲。
rotateDelta *= rotateDrag;
然后将deltaV添加到跟踪值中,
rotateChase += rotateDelta;
现在可以显示追踪值了
myTriangle.rotation = rotateChase;
添加边界
那是一个无限的追逐者。要将边界设置为该值,需要一些额外的代码。首先通过设置最小值和最大值来描述边界。
var rotateMin = 0;
var rotateMax = Math.PI*2;
然后,通过定义反射或反射来描述值越界时的行为。 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));
简化
所有的接缝都是为了一项值(value)而进行的大量工作。但是我们是程序员,本质上是懒惰的,所以让我们将其划分为一个简单的惯性类
// 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);
更新
我添加了一些代码来显示各种设置以及它们如何影响惯性。该代码并非旨在作为代码风格或DOM接口(interface)最佳实践的示例,因为在这两个方面都远远不够。它使用了我上面介绍的惯性对象。您可以在演示代码的顶部找到该对象。
最好观看该演示全屏
//------------------------------------------------------
// 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;
}
<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>
希望这可以帮助。