动画和用户界面(UI)
当你在画布上拖动任何东西时,实际上是在给画布添加动画效果。为了获得最佳的效果,您需要停止使用DOM方式思考,而是开始像编写游戏那样编写代码。
一种函数管理所有
为了协调所有的操作,您需要一个函数来处理所有的动画效果。有时在游戏圈中称为mainLoop,它使得处理动态内容变得更加容易。
主循环
function mainLoop(time){
ctx.clearRect(0,0,canvas.width,canvas.height);
updateDisplay();
requestAnimationFrame(mainLoop);
}
requestAnimationFrame(mainLoop);
从主循环中,您可以检查当前应用程序状态并调用适当的功能。在这个例子中,它只是简单地调用updateDisplay()
。
KISS IO 事件。
鼠标、键盘、触摸等事件处理程序不应执行任何功能逻辑。这些事件可能会非常频繁地触发,但显示器每秒只更新60次。当用户只能看到CPU正在执行的10%左右的工作时,从可能每秒触发500次的事件进行渲染是没有意义的。
编写IO事件的最佳方法就是将其视为数据记录器。尽快获取尽可能多的信息并退出,不要为每种类型的事件编写不同的事件侦听器,保持代码简单,只编写一个事件处理程序来处理尽可能多的内容。如果需要键盘,请将其添加到同一侦听器中。
var mouse = (function(){
var bounds;
var m = {x:0,y:0,button:false};
function mouseEvent(event){
bounds = event.currentTarget.getBoundingClientRect();
m.x = event.pageX - bounds.left + scrollX;
m.y = event.pageY - bounds.top + scrollY;
if(event.type === "mousedown"){
m.button = true;
}else if(event.type === "mouseup"){
m.button = false;
}
}
m.start = function(element){
["mousemove","mousedown","mouseup"].forEach(eventType => element.addEventListener(eventType, mouseEvent));
}
return m;
}())
mouse.start(canvas);
现在,您可以通过简单的鼠标界面随时访问鼠标。
圆形
如果没有东西可以移动,添加漂亮的界面就没有意义。以下是一个帮助管理圆形的对象。它可以创建、绘制和定位圆形。
其中包括findClosest
函数,它可以获取鼠标下方的圆形。它将返回鼠标下的最小圆形。如果鼠标下没有任何东西,它将返回未定义。
var circles = {
items [],
drawCircle(){
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.stroke();
},
each(callback){
var i;
for(i = 0; i < this.items.length; i++){
callBack(this.items[i],i);
}
},
drawCircles(){
this.each(c => {
c.draw();
})
},
addCircle(x,y,radius){
var circle = {x, y, radius, draw : this.drawCircle};
this.items.push(circle);
return circle;
},
getClosest(pos){
var minDist, i, dist, x, y, foundCircle;
minDist = Infinity;
this.each(c =>{
x = pos.x - c.x;
y = pos.y - c.y;
dist = Math.sqrt(x * x + y * y);
if(dist <= c.radius && dist < minDist){
minDist = dist;
foundCircle = c;
}
})
return foundCircle;
}
}
如何进行拖放操作
在拖放对象时需要注意许多细节。你需要一个函数来找到鼠标最接近的对象,还需要提供反馈信息,以便用户可以看到哪些对象可以被拖动。
你可能会遇到许多不同的拖放类型事件。例如:拖动创建、拖动移动、拖动将某物体放置到画布上或者操纵自定义呈现的 UI 对象。如果你从一个对象管理拖放状态,就可以确保当拖动圆形时不会意外点击 UI 项。
当开始拖动时,检查拖动将要执行的操作。然后标记正在进行拖动,并指示拖动要做什么(参见 updateDisplay 函数)。
当拖动处于活动状态时,执行该操作。当鼠标松开时,只需停止拖动。
var dragging = {
started : false,
type : null,
currentObj : null,
startX : 0,
startY : 0,
start(type, obj){
this.startX = mouse.x;
this.startY = mouse.y;
this.started = true;
this.type = type;
this.currentObj = obj;
}
}
接下来是渲染函数。它在每一帧中被调用。它会检查鼠标按钮是否被按下,如果是,则执行相应操作,并设置光标以便人们知道该做什么,然后绘制圆形。
var cursor = "default";
var overCircle = null;
function updateDisplay(){
var x,y, c;
cursor = "default";
if(mouse.x >= 0 && mouse.x < canvas.width && mouse.y >= 0 && mouse.y < canvas.height){
cursor = "crosshair";
}
if(mouse.button){
if(!dragging.started){
if(overCircle){
dragging.start("move",overCircle)
overCircle = null;
}else{
dragging.start("create",circles.addCircle(mouse.x, mouse.y, 1));
}
}
c = dragging.currentObj;
if(dragging.type === "create"){
x = c.x - mouse.x;
y = c.y - mouse.y;
c.radius = Math.sqrt(x * x + y * y);
}else if(dragging.type === "move"){
x = dragging.startX - mouse.x;
y = dragging.startY - mouse.y;
c.x -= x;
c.y -= y;
dragging.startX = mouse.x;
dragging.startY = mouse.y;
}
cursor = "none";
} else {
if(dragging.started){
dragging.started = false;
}
}
ctx.strokeStyle = "black";
circles.draw();
if(!dragging.started){
c = circles.getClosest(mouse);
if(c !== undefined){
cursor = "move";
ctx.strokeStyle = "red";
ctx.fillStyle = "rgba(0,255,255,0.1)";
c.draw();
ctx.fill();
overCircle = c;
}else{
overCircle = null;
}
}
canvas.style.cursor = cursor;
}
作为一个工作示例。
var canvas = document.createElement("canvas");
canvas.style.border = "1px black solid";
canvas.width = 512;
canvas.height = 200;
var ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
function mainLoop(time){
ctx.clearRect(0,0,canvas.width,canvas.height);
updateDisplay();
requestAnimationFrame(mainLoop);
}
requestAnimationFrame(mainLoop);
var mouse = (function(){
var bounds;
var m = {x:0,y:0,button:false};
function mouseEvent(event){
bounds = event.currentTarget.getBoundingClientRect();
m.x = event.pageX - bounds.left + scrollX;
m.y = event.pageY - bounds.top + scrollY;
if(event.type === "mousedown"){
m.button = true;
}else if(event.type === "mouseup"){
m.button = false;
}
}
m.start = function(element){
["mousemove","mousedown","mouseup"].forEach(eventType => element.addEventListener(eventType, mouseEvent));
}
return m;
}())
mouse.start(canvas);
var circles = {
items : [],
drawCircle(){
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.stroke();
},
each(callback){
var i;
for(i = 0; i < this.items.length; i++){
callback(this.items[i],i);
}
},
draw(){
this.each(c => {
c.draw();
})
},
addCircle(x,y,radius){
var circle = {x, y, radius, draw : this.drawCircle};
this.items.push(circle);
return circle;
},
getClosest(pos){
var minDist, i, dist, x, y, foundCircle;
minDist = Infinity;
this.each(c =>{
x = pos.x - c.x;
y = pos.y - c.y;
dist = Math.sqrt(x * x + y * y);
if(dist <= c.radius){
if(foundCircle === undefined || (foundCircle && c.radius < foundCircle.radius)){
minDist = dist;
foundCircle = c;
}
}
})
return foundCircle;
}
}
var dragging = {
started : false,
type : null,
currentObj : null,
startX : 0,
startY : 0,
start(type, obj){
this.startX = mouse.x;
this.startY = mouse.y;
this.started = true;
this.type = type;
this.currentObj = obj;
}
}
var cursor = "default";
var overCircle = null;
function updateDisplay(){
var x,y, c;
cursor = "default"
if(mouse.x >= 0 && mouse.x < canvas.width && mouse.y >= 0 && mouse.y < canvas.height){
cursor = "crosshair";
}
if(mouse.button){
if(!dragging.started){
if(overCircle){
dragging.start("move",overCircle)
overCircle = null;
}else{
dragging.start("create",circles.addCircle(mouse.x, mouse.y, 1));
}
}
c = dragging.currentObj;
if(dragging.type === "create"){
x = c.x - mouse.x;
y = c.y - mouse.y;
c.radius = Math.sqrt(x * x + y * y);
}else if(dragging.type === "move"){
x = dragging.startX - mouse.x;
y = dragging.startY - mouse.y;
c.x -= x;
c.y -= y;
dragging.startX = mouse.x;
dragging.startY = mouse.y;
}
cursor = "none";
} else {
if(dragging.started){
dragging.started = false;
}
}
ctx.strokeStyle = "black";
circles.draw();
if(!dragging.started){
c = circles.getClosest(mouse);
if(c !== undefined){
cursor = "move";
ctx.strokeStyle = "red";
ctx.fillStyle = "rgba(0,255,255,0.1)";
c.draw();
ctx.fill();
overCircle = c;
}else{
overCircle = null;
}
}
canvas.style.cursor = cursor;
}
<div style="font-family:12px arial;">Click drag to create circle</div>
请注意,这是用ES6编写的,如果没有某种类型的编译器,它将无法在IE上运行。