javascript - 使用 requestAnim 或 TimeOut 在动画中滞后 FPS

标签 javascript html animation three.js webgl

我正在尝试制作具有恒定 fps 的优质动画,但没有任何效果。我正在使用 threejs、webgl 来渲染场景和动画循环,我找到了两种方法(有第三种吗?),即 requestAnimationFrame(...) 或 setTimeOut()。两者都不保证 fps 是恒定的,但我通过 window.performance.now() 的 timedelta 更新对象位置来修复它。但是我仍然可以清楚地看到滞后峰值。那么我该如何解决呢?这显然可能是因为有像《毁灭战士》这样的游戏不会滞后。

我的示例和完整的 src 代码可以在这里找到:

http://sc2tube.com:8080/test/three.html

相关代码:

function animate() {

requestAnimationFrame( animate );

// calculate how long the last frame was 
var timefix = (window.performance.now() - last)/(1000/30);

last = window.performance.now();

var oldX = object.position.x;
// calculate updateX including the timefix
var updateX = oldX + (10 / 30 * 100) * dx * timefix;

// update the position of the object
object.position.x = updateX;  
// render the scene
renderer.render(scene, camera);

}

worker.js:

self.addEventListener('message', function(e) {

setInterval(function(){ 

    now = self.performance.now()
    timefix = (now - last)/(1000/100);
    last = now;

    x += 5*timefix*dx;

    self.postMessage(x);

}, 1000/100);

}, false);

var test;  
	var dx = 1, dy = 0;
	var speed = 0.5;

	var activeKey = 0;
    // Set up the scene, camera, and renderer as global variables.
    var scene, camera, renderer;

    init();
    animate();

    // Sets up the scene.
    function init() {

      // Create the scene and set the scene size.
      scene = new THREE.Scene();
      var WIDTH = window.innerWidth - 50,
          HEIGHT = 500;

      // Create a renderer and add it to the DOM.
      renderer = new THREE.WebGLRenderer({antialias:true});
      renderer.setSize(WIDTH, HEIGHT);
      document.body.appendChild(renderer.domElement);

      camera = new THREE.OrthographicCamera( 0, WIDTH, 200, -HEIGHT, 1, 1000 );
      
      camera.position.set(0,0,100);
      scene.add(camera);
      console.log(WIDTH);
      
      
      window.addEventListener('resize', function() {
    	  var WIDTH = window.innerWidth - 50,
          HEIGHT = window.innerHeight - 50;
        renderer.setSize(WIDTH, HEIGHT);
        camera.aspect = WIDTH / HEIGHT;
        camera.updateProjectionMatrix();
        
      });
      renderer.setClearColor();

      var loader = new THREE.ObjectLoader();
  	  
      loader.parse({
    "metadata" : {
        "type" : "Object",
        "version" : 4.3,
        "generator" : "Blender Script"
    },
    "object" : {
        "name" : "red_cube.Material",
        "type" : "Mesh",
        "uuid" : "6071e8f2-79ae-5660-8d2b-aa675c566703",
        "matrix" : [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],
        "geometry" : "5d6cbd93-cf58-58a9-b0a7-5be9e5794547",
        "material" : "5e847bd4-84a9-5d4b-a8fb-c567e27f7561"
    },
    "geometries" : [{
            "name" : "red_cube.Material",
            "type" : "BufferGeometry",
            "uuid" : "5d6cbd93-cf58-58a9-b0a7-5be9e5794547",
            "data" : {
                "attributes" : {
                    "position" : {
                        "type" : "Float32Array",
                        "itemSize" : 3,
                        "array" : [0.79906648,-0.73424673,-0.87263167,0.79906648,-0.73424661,1.1273682,-1.2009337,-0.73424661,1.1273681,-1.2009332,-0.73424673,-0.87263215,0.79906696,1.2657533,-0.87263131,-1.2009335,1.2657533,-0.87263179,-1.2009339,1.2657533,1.1273677,0.79906583,1.2657533,1.1273688,0.79906648,-0.73424673,-0.87263167,0.79906696,1.2657533,-0.87263131,0.79906583,1.2657533,1.1273688,0.79906648,-0.73424661,1.1273682,0.79906648,-0.73424661,1.1273682,0.79906583,1.2657533,1.1273688,-1.2009339,1.2657533,1.1273677,-1.2009337,-0.73424661,1.1273681,-1.2009337,-0.73424661,1.1273681,-1.2009339,1.2657533,1.1273677,-1.2009335,1.2657533,-0.87263179,-1.2009332,-0.73424673,-0.87263215,0.79906696,1.2657533,-0.87263131,0.79906648,-0.73424673,-0.87263167,-1.2009332,-0.73424673,-0.87263215,-1.2009335,1.2657533,-0.87263179]
                    },
                    "normal" : {
                        "type" : "Float32Array",
                        "itemSize" : 3,
                        "array" : [-1.0658141e-14,-1,5.9604645e-08,-1.0658141e-14,-1,5.9604645e-08,-1.0658141e-14,-1,5.9604645e-08,-1.0658141e-14,-1,5.9604645e-08,0,1,0,0,1,0,0,1,0,0,1,0,1,4.4703416e-08,2.8312209e-07,1,4.4703416e-08,2.8312209e-07,1,4.4703416e-08,2.8312209e-07,1,4.4703416e-08,2.8312209e-07,-2.9802322e-07,-5.9604723e-08,1,-2.9802322e-07,-5.9604723e-08,1,-2.9802322e-07,-5.9604723e-08,1,-2.9802322e-07,-5.9604723e-08,1,-1,-1.1920929e-07,-2.3841858e-07,-1,-1.1920929e-07,-2.3841858e-07,-1,-1.1920929e-07,-2.3841858e-07,-1,-1.1920929e-07,-2.3841858e-07,2.3841858e-07,1.7881393e-07,-1,2.3841858e-07,1.7881393e-07,-1,2.3841858e-07,1.7881393e-07,-1,2.3841858e-07,1.7881393e-07,-1]
                    },
                    "index" : {
                        "type" : "Uint32Array",
                        "itemSize" : 1,
                        "array" : [0,1,2,2,3,0,4,5,6,6,7,4,8,9,10,10,11,8,12,13,14,14,15,12,16,17,18,18,19,16,20,21,22,22,23,20]
                    }
                }
            }
        }],
    "materials" : [{
            "name" : "Material",
            "type" : "MeshBasicMaterial",
            "uuid" : "5e847bd4-84a9-5d4b-a8fb-c567e27f7561",
            "transparent" : false,
            "opacity" : 1,
            "color" : 10682379
        }]
}, function(object){
       		test = object;
       		object.scale.set(50,50,50);
       		scene.add(object)
       		
      });
      
  	document.addEventListener('keydown', function(e) {
  	    if (activeKey == e.keyCode) return;
  	    activeKey = e.keyCode;
  	    
  	    //left
  	    if (e.keyCode == 37) {
  	        dx = -1;
  	    }
  	    //top
  	    else if (e.keyCode == 38) {
  	        dy = 1;
  	    }
  	    //right
  	    else if (e.keyCode == 39) {
  	        dx = 1;
  	    }
  	    //bottom
  	    else if (e.keyCode == 40) {
  	        dy = -1;
  	    }
  	});
  	document.addEventListener('keyup', function(e) {
  	    switch (e.keyCode) {
  	        case 37: // left
  	        case 39: // right
  	            dx = 0;
  	            break;
  	            
  	        case 38: // up
  	        case 40: // down
  	            dy = 0;
  	            break;
  	    }
  	    
  	    activeKey = 0;
  	});

    }
    
    var start;
    var last;

    function animate() {

        requestAnimationFrame( animate );
                
    	if(start == null) {
    		start = window.performance.now();
    		last = start;
    	}
    	
    	var timefix = (window.performance.now() - last)/(1000/30);
    	
    	last = window.performance.now();
    	
        if(test != null) {
       	    var oldX = test.position.x;
       	    var oldY = test.position.y;
       	    
       	    var updateX = oldX + (10 / 30 * 100) * dx * speed * timefix;
       	    var updateY = oldY + (10 / 30 * 100) * dy * speed * timefix;  
       	    
       	    if(updateX > 1800 ) {
       	    	dx = -1;
       	    } else if(updateX < 100) {       	    	
       	    	dx = 1;
       	    }
       	    
       	    test.position.x = updateX;  
       	    test.position.y =  oldY + (10 / 30 * 100) * dy * speed * timefix;  
       	    
       	 	var text = document.getElementById('panel');
       	 	text.innerHTML =  timefix;
       		renderer.render(scene, camera);
       	}

    }
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/84/three.js"></script>
<body style="margin: 0;">

<div id="panel">TEST
</div>
<br>

</body>

最佳答案

问题是所谓的游戏滴答声。

您需要的是线程。 一个渲染线程。 一个游戏线程。

对于游戏线程,我建议网络 worker :

https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers

您让游戏线程每 50 毫秒运行一次以更新游戏逻辑。它应该在两者之间“ sleep ”。 您将内容发回渲染线程,该线程会更新所有内容并在 50 毫秒内将当前位置的轨迹内插到下一个位置。

  • 勾选 1. 0MS

    • 游戏线程:
      • 在 0.0 生成方 block
      • 将游戏状态推送到渲染线程
    • 渲染线程
      • 收集实体。
      • 移动时更新位置实体
      • 在pos处绘制实体
  • 滴答 2. 50ms

    • 游戏线程:
      • 获取实体
      • 触发更新函数
        • 红色方 block 向左移动 20
        • 将游戏状态推送到渲染线程
    • 渲染线程
      • 收集实体。
      • 移动时更新位置实体
        • 红色方 block 从 0 移动到 20
        • 平均每刻帧数 4
        • 每个刻度的内插移动,5 像素。
        • 更新红色为5
      • 在pos处绘制实体

编辑 添加了如何利用渲染线程的示例代码。

基本上,游戏线程(webworker)中的对象与渲染线程中的对象相同。 唯一不同的是,渲染线程有渲染指令(onRender),游戏循环有更新指令(on update)

所以它们既相同又不同。

看看。

function getInlineJS() {
    var js = document.querySelector('[type="javascript/worker"]').textContent;
    var blob = new Blob([js], {"type": "text\/plain"});
    return URL.createObjectURL(blob);
}



var RedCube = function(id) {
    this.cube = null;
    this.type = 'redcube';
    if(typeof id === undefined) {
       this.entityId = generateId();
    }
    else {
        this.entityId = id;
    }
    this.lastX = 0;
    this.x = 0;
}
RedCube.prototype.getType = function() {
   return this.type;
}
RedCube.prototype.onUpdate = function() {
    this.x += 20;
}
RedCube.prototype.loadCube = function(scene, renderer) {
var that = this;
       
        var loader = new THREE.ObjectLoader();
  	        loader.parse({
    "metadata" : {
        "type" : "Object",
        "version" : 4.3,
        "generator" : "Blender Script"
    },
    "object" : {
        "name" : "red_cube.Material",
        "type" : "Mesh",
        "uuid" : "6071e8f2-79ae-5660-8d2b-aa675c566703",
        "matrix" : [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],
        "geometry" : "5d6cbd93-cf58-58a9-b0a7-5be9e5794547",
        "material" : "5e847bd4-84a9-5d4b-a8fb-c567e27f7561"
    },
    "geometries" : [{
            "name" : "red_cube.Material",
            "type" : "BufferGeometry",
            "uuid" : "5d6cbd93-cf58-58a9-b0a7-5be9e5794547",
            "data" : {
                "attributes" : {
                    "position" : {
                        "type" : "Float32Array",
                        "itemSize" : 3,
                        "array" : [0.79906648,-0.73424673,-0.87263167,0.79906648,-0.73424661,1.1273682,-1.2009337,-0.73424661,1.1273681,-1.2009332,-0.73424673,-0.87263215,0.79906696,1.2657533,-0.87263131,-1.2009335,1.2657533,-0.87263179,-1.2009339,1.2657533,1.1273677,0.79906583,1.2657533,1.1273688,0.79906648,-0.73424673,-0.87263167,0.79906696,1.2657533,-0.87263131,0.79906583,1.2657533,1.1273688,0.79906648,-0.73424661,1.1273682,0.79906648,-0.73424661,1.1273682,0.79906583,1.2657533,1.1273688,-1.2009339,1.2657533,1.1273677,-1.2009337,-0.73424661,1.1273681,-1.2009337,-0.73424661,1.1273681,-1.2009339,1.2657533,1.1273677,-1.2009335,1.2657533,-0.87263179,-1.2009332,-0.73424673,-0.87263215,0.79906696,1.2657533,-0.87263131,0.79906648,-0.73424673,-0.87263167,-1.2009332,-0.73424673,-0.87263215,-1.2009335,1.2657533,-0.87263179]
                    },
                    "normal" : {
                        "type" : "Float32Array",
                        "itemSize" : 3,
                        "array" : [-1.0658141e-14,-1,5.9604645e-08,-1.0658141e-14,-1,5.9604645e-08,-1.0658141e-14,-1,5.9604645e-08,-1.0658141e-14,-1,5.9604645e-08,0,1,0,0,1,0,0,1,0,0,1,0,1,4.4703416e-08,2.8312209e-07,1,4.4703416e-08,2.8312209e-07,1,4.4703416e-08,2.8312209e-07,1,4.4703416e-08,2.8312209e-07,-2.9802322e-07,-5.9604723e-08,1,-2.9802322e-07,-5.9604723e-08,1,-2.9802322e-07,-5.9604723e-08,1,-2.9802322e-07,-5.9604723e-08,1,-1,-1.1920929e-07,-2.3841858e-07,-1,-1.1920929e-07,-2.3841858e-07,-1,-1.1920929e-07,-2.3841858e-07,-1,-1.1920929e-07,-2.3841858e-07,2.3841858e-07,1.7881393e-07,-1,2.3841858e-07,1.7881393e-07,-1,2.3841858e-07,1.7881393e-07,-1,2.3841858e-07,1.7881393e-07,-1]
                    },
                    "index" : {
                        "type" : "Uint32Array",
                        "itemSize" : 1,
                        "array" : [0,1,2,2,3,0,4,5,6,6,7,4,8,9,10,10,11,8,12,13,14,14,15,12,16,17,18,18,19,16,20,21,22,22,23,20]
                    }
                }
            }
        }],
    "materials" : [{
            "name" : "Material",
            "type" : "MeshBasicMaterial",
            "uuid" : "5e847bd4-84a9-5d4b-a8fb-c567e27f7561",
            "transparent" : false,
            "opacity" : 1,
            "color" : 10682379
        }]
}, function(object){
       		that.cube = object;
       		object.scale.set(50,50,50);
       		scene.add(object)
       		
      });
}
RedCube.prototype.onRender = function(scene, renderer) {
   if(this.cube === null) {
       this.loadCube(scene, renderer);
   }
   
   // Some interprolation logic here to move from lastpos to next pos in average frames
   // per tick.
   this.cube.position.x = this.x;
}

RedCube.prototype.getType = function() {
   return type;
}

RedCube.prototype.generateSyncPacket = function() {
   return {
            type: this.getType(),
            x : this.x
          };
}

RedCube.prototype.parseSyncPacket = function(syncpacket) {
   this.setPosition(syncpacket.x);
}

RedCube.prototype.generateId = function() {
    var d = new Date().getTime();
    if (typeof performance !== 'undefined' && typeof performance.now === 'function'){
        d += performance.now(); //use high-precision timer if available
    }
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        var r = (d + Math.random() * 16) % 16 | 0;
        d = Math.floor(d / 16);
        return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
    });

}


RedCube.prototype.getEntityId = function() {
    return this.entityId;
}
RedCube.prototype.beforeDeath = function() {
}

RedCube.prototype.die = function() {
}

RedCube.prototype.setPosition = function(newpos) {
    this.lastX = this.x;
    this.x = newpos;
}

RedCube.prototype.getPosition = function() {
    return this.x;
}
RedCube.prototype.getLastX = function() {
    return this.lastX;
}

var EntityRegistry = function() {
   this.entities = {};
   this.types = {};
}

EntityRegistry.prototype.register = function(entity) {
   this.entities[entity.getEntityId()] = entity;
   
}
EntityRegistry.prototype.callUpdate = function() {
  for(entityId in this.entities) {
      if(this.entities.hasOwnProperty(entityId)) {
          this.entities[entityId].onUpdate();
      }
   }
}
EntityRegistry.prototype.callOnRender = function(scene, renderer) {
  for(entityId in this.entities) {
      if(this.entities.hasOwnProperty(entityId)) {
          this.entities[entityId].onRender(scene, renderer);
      }
   }
}
EntityRegistry.prototype.remove = function(entity) {
   entity.beforeDeath();
   delete this.entities[entity.getEntityId()]
   entity.die();
}

EntityRegistry.prototype.registerType = function(name, entityClass) {
    this.types[name] = entityClass;
}
EntityRegistry.prototype.startEntity = function(syncpacket, entityId) {
    var entity = new this.types[syncpacket.type](entityId);
    entity.parseSyncPacket(syncpacket);
    this.register(entity);
}

EntityRegistry.prototype.getSyncData = function() {
   var syncpacket = {};
   for(entityId in this.entities) {
      if(this.entities.hasOwnProperty(entityId)) {
          syncpacket[entityId] = this.entities[entityId].generateSyncPacket();
      }
   }
   return syncpacket;
}

EntityRegistry.prototype.parseSyncData = function(syncpacket) {
   for(entityId in syncpacket) {
      
      if(this.entities.hasOwnProperty(entityId)) {
          this.entities[entityId].parseSyncPacket(syncpacket[entityId]);
      }
      else {
          this.startEntity(syncpacket[entityId], entityId);
      }
   }
   return syncpacket;
}
var REGISTRY = new EntityRegistry();
REGISTRY.registerType('redcube', RedCube);

 	var test = "d";  
	var dx = 1, dy = 0;
	var speed = 0.5;

	var activeKey = 0;
    // Set up the scene, camera, and renderer as global variables.
    var scene, camera, renderer;

    var worker = new Worker(getInlineJS());
    worker.postMessage("dasd");
  
    worker.addEventListener('message', function(e) {
    	  REGISTRY.parseSyncData(e.data);
    	}, false);
    
    
    console.log("asd " + test);
    init();
    animate();

    // Sets up the scene.
    function init() {

      // Create the scene and set the scene size.
      scene = new THREE.Scene();
      var WIDTH = window.innerWidth - 50,
          HEIGHT = 500;

      // Create a renderer and add it to the DOM.
      renderer = new THREE.WebGLRenderer({antialias:true});
      renderer.setSize(WIDTH, HEIGHT);
      document.body.appendChild(renderer.domElement);

      camera = new THREE.OrthographicCamera( 0, WIDTH, 200, -HEIGHT, 1, 1000 );
      
      camera.position.set(0,0,100);
      scene.add(camera);
      console.log(WIDTH);
      
      
      window.addEventListener('resize', function() {
    	  var WIDTH = window.innerWidth - 50,
          HEIGHT = window.innerHeight - 50;
        renderer.setSize(WIDTH, HEIGHT);
        camera.aspect = WIDTH / HEIGHT;
        camera.updateProjectionMatrix();
        
      });
      renderer.setClearColor();

     
      
  	document.addEventListener('keydown', function(e) {
  	    if (activeKey == e.keyCode) return;
  	    activeKey = e.keyCode;
  	    
  	    //left
  	    if (e.keyCode == 37) {
  	        dx = -1;
  	    }
  	    //top
  	    else if (e.keyCode == 38) {
  	        dy = 1;
  	    }
  	    //right
  	    else if (e.keyCode == 39) {
  	        dx = 1;
  	    }
  	    //bottom
  	    else if (e.keyCode == 40) {
  	        dy = -1;
  	    }
  	});
  	document.addEventListener('keyup', function(e) {
  	    switch (e.keyCode) {
  	        case 37: // left
  	        case 39: // right
  	            dx = 0;
  	            break;
  	            
  	        case 38: // up
  	        case 40: // down
  	            dy = 0;
  	            break;
  	    }
  	    
  	    activeKey = 0;
  	});

    }
    
    var start;
    var last;

    var timefix, oldX,oldY, updateX,updateY,text;
    
    function animate() {
    	
    	  REGISTRY.callOnRender(scene, renderer);
        renderer.render(scene, camera);   	   
        requestAnimationFrame( animate );

    }
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/84/three.js"></script>
<body style="margin: 0;">

<div id="panel">TEST
</div>
<br>
<script type="javascript/worker">
var RedCube = function(id) {
    this.direction = false;
    this.type = 'redcube';
    if(typeof id === 'undefined') {
       this.entityId = this.generateId();
    }
    else {
        this.entityId = id;
    }
    this.lastX = 0;
    this.x = 0;
}
RedCube.prototype.getType = function() {
   return this.type;
}

RedCube.prototype.generateSyncPacket = function() {
   return {
            type: this.getType(),
            x : this.x
          };
}

RedCube.prototype.parseSyncPacket = function(syncpacket) {
   this.setPosition(syncpacket.x);
}

RedCube.prototype.generateId = function() {
    var d = new Date().getTime();
    if (typeof performance !== 'undefined' && typeof performance.now === 'function'){
        d += performance.now(); //use high-precision timer if available
    }
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        var r = (d + Math.random() * 16) % 16 | 0;
        d = Math.floor(d / 16);
        return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
    });

}

RedCube.prototype.getEntityId = function() {
    return this.entityId;
}

RedCube.prototype.beforeDeath = function() {
}

RedCube.prototype.die = function() {
}

RedCube.prototype.setPosition = function(newpos) {
    this.lastX = this.x;
    this.x = newpos;
}

RedCube.prototype.getPosition = function() {
    return this.x;
}
RedCube.prototype.getLastX = function() {
    return this.lastX;
}

var EntityRegistry = function() {
   this.entities = {};
   this.types = {};
}

RedCube.prototype.onUpdate = function() {
    if(this.x > 500) {
       this.direction = true;
    }
    if(this.x <= 0) {
       this.direction = false;
    }
    this.x += !this.direction ? 20 : -20;
}
RedCube.prototype.onRender = function(scene, renderer) {
/// this is not a rendering thread. leave it empty
}
EntityRegistry.prototype.register = function(entity) {   
   this.entities[entity.getEntityId()] = entity;
   
}

EntityRegistry.prototype.remove = function(entity) {
   entity.beforeDeath();
   delete this.entities[entity.getEntityId()]
   entity.die();
}

EntityRegistry.prototype.registerType = function(name, entityClass) {
    this.types[name] = entityClass;
}
EntityRegistry.prototype.startEntity = function(entityId, syncpacket) {
    var entity = this.types[syncpacket.type](entityId);
    entity.parseSyncPacket(syncpacket);
    this.register(entity);
}

EntityRegistry.prototype.getSyncData = function() {
   var syncpacket = {};
   
   for(entityId in this.entities) {
      
      if(this.entities.hasOwnProperty(entityId)) {
          syncpacket[entityId] = this.entities[entityId].generateSyncPacket();
      }
   }
   return syncpacket;
}
EntityRegistry.prototype.callUpdate = function() {
  for(entityId in this.entities) {
      if(this.entities.hasOwnProperty(entityId)) {
          this.entities[entityId].onUpdate();
      }
   }
}
EntityRegistry.prototype.callOnRender = function(scene, renderer) {
  for(entityId in this.entities) {
      if(this.entities.hasOwnProperty(entityId)) {
          this.entities[entityId].onRender(scene, renderer);
      }
   }
}
EntityRegistry.prototype.parseSyncData = function(syncpacket) {
   for(entityId in syncpacket) {
      if(this.entities.hasOwnProperty(entityId)) {
          this.entities[entityId].parseSyncPacket(syncpacket[entityid]);
      }
      else {
          this.startEntity(syncpacket, entityId);
      }
   }
   return syncpacket;
}
var REGISTRY = new EntityRegistry();
var little_red = new RedCube();

REGISTRY.register(little_red);    

var x = 0;
var timefix = 0;
var last = 0;
var dx = 1;
var loopInterval = 0;
loopInterval = setInterval(function(){ 			
    REGISTRY.callUpdate()
		var msg = REGISTRY.getSyncData();
		self.postMessage(msg);
		
	}, 1000/60);
  
self.addEventListener('message', function(e) {
	  
	

}, false);
</script>
</body>

关于javascript - 使用 requestAnim 或 TimeOut 在动画中滞后 FPS,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42575873/

相关文章:

html - 将 HTML 代码作为输入的 Angular 组件

ios - swift 4 : Mask not working for UIImage?

javascript - 有没有办法让两个 jQuery 动画同时(正确地)运行?

javascript - .setValue() 对 Google Sheets 脚本不一致

javascript - 如何在ie8中自动​​打开bootstrap模式?

jquery - 使用 jQuery 添加列表项后删除它们

jquery - HTML5 textarea 占位符未出现

javascript - native 基本列表项单击事件不起作用

javascript - JavaScript Hook 的 HTML 元素

javascript - 如何将动画 div 限制到其父 div