[英]How to animate multiple HTML5 canvas objects one after another?
我想使用 HTML5 画布和 JavaScript 制作动画。 这个想法是为不同的对象编写类,如下所示:
class Line {
constructor(x1, y1, x2, y2) {
this.x1 = x1;
this.y1 = y2;
...
}
draw() {
}
}
class Circle {
constructor(x, y, radius) {
this.x = x;
...
}
draw() {}
}
...
然后你在主代码中要做的就是一个接一个地绘制形状,中间有停顿:
let line1 = new Line(x1, y1, x2, y2);
let circle = new Circle(x, y, r);
let line2 = new Line(x1, y1, x2, y2);
line1.draw()
pause()
circle.draw()
pause()
line2.draw()
...
有没有一种简单的方法(无需处理 Promise 和嵌套的回调函数),例如使用一些库?
一个很好的问题,因为您不想做的事情(使用承诺和/或回调)实际上意味着在脚本中对动画进行硬编码,重用潜力有限,并且可能会在将来进行修改时造成困难。
我使用的一个解决方案是创建一个绘制框架的函数的故事书,所以你可以把
()=>line1.draw()
进入书而不是
line1.draw()
这将立即绘制它并尝试将其返回值添加到书中!
下一部分(无特定顺序)是一个播放器,它使用requestAnimationFrame对故事书进行时间步进并调用函数来绘制框架。 至少它需要脚本的方法
让延迟函数在调用故事书中的下一个条目之前等待许多帧可以保持简单,但会根据帧速率创建计时,这可能不是恒定的。
这是纯 JavaScript 中的一个简化示例,它更改背景颜色(不是画布操作)以进行演示 - 如果您无法使其正常工作,请查看参考。
"use strict"; class AnimePlayer { constructor() { this.storyBook = []; this.pause = 0; this.drawFrame = this.drawFrame.bind( this); this.frameNum = 0; } addFrame( frameDrawer) { this.storyBook.push( frameDrawer); } pauseFrames(n) { this.storyBook.push ( ()=>this.pause = n); } play() { this.frameNum = 0; this.drawFrame(); } drawFrame() { if( this.pause > 0) { --this.pause; requestAnimationFrame( this.drawFrame); } else if( this.frameNum < this.storyBook.length) { this.storyBook[this.frameNum](); ++this.frameNum; requestAnimationFrame( this.drawFrame); } } } let player = new AnimePlayer(); let style = document.body.style; player.addFrame( ()=> style.backgroundColor = "green"); player.pauseFrames(60); player.addFrame( ()=> style.backgroundColor = "yellow"); player.pauseFrames(5); player.addFrame( ()=>style.backgroundColor = "orange"); player.pauseFrames(60); player.addFrame( ()=> style.backgroundColor = "red"); player.pauseFrames(60); player.addFrame( ()=> style.backgroundColor = ""); function tryMe() { console.clear(); player.play(); }
<button type="button" onclick="tryMe()">try me</button>
您可以使用关键帧来制作几乎任何东西的动画效果。
下面的示例(本来打算写更多,但我为时已晚,您已经接受了答案)显示了一个非常基本的关键帧实用程序如何创建动画。
关键帧只是一个time
和一个value
将关键帧添加到为值命名的轨道。
因此,名称x
(位置)和键 {time:0, value:100}, {time:1000, value:900} 将在 0 到 1 秒的时间内将x
属性从100
更改为900
例如一个圆圈
const circle = {
x: 0,
y: 0,
r: 10,
col : "",
draw() {
ctx.fillStyle = this.col;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.fill()
}
};
可以随时间改变其任何属性。
首先创建一个tracks对象并定义keys
const circleTracks = createTracks();
// properties to animate
circleTracks.addTrack("x");
circleTracks.addTrack("y");
circleTracks.addTrack("r");
circleTracks.addTrack("col");
然后在特定时间戳添加关键帧。
circleTracks.addKeysAtTime(0, {x: 220, y :85, r: 20, col: "#F00"});
circleTracks.addKeysAtTime(1000, {x: 220, y :50, r: 50, col: "#0F0"});
circleTracks.addKeysAtTime(2000, {x: 420, y :100, r: 20, col: "#00F"});
circleTracks.addKeysAtTime(3000, {x: 180, y :160, r: 10, col: "#444"});
circleTracks.addKeysAtTime(4000, {x: 20, y :100, r: 20});
circleTracks.addKeysAtTime(5000, {x: 220, y :85, r: 10, col: "#888"});
circleTracks.addKeysAtTime(5500, {r: 10, col: "#08F"});
circleTracks.addKeysAtTime(6000, {r: 340, col: "#00F"});
准备好后清理密钥(您可以不按时间顺序添加它们)
circleTracks.clean();
寻找起点
circleTracks.seek(0);
并更新对象
circleTracks.update(circle);
要制作动画,只需调用刻度和更新函数,然后绘制圆圈
circleTracks.tick();
circleTracks.update(circle);
circle.draw();
单击以开始动画。 当它结束时,您可以使用tracks.seek(time)
擦洗动画
这是最基本的关键帧动画。
关键帧的最佳之处在于它们将动画与代码分开,让您可以将动画作为简单的数据结构导入和导出。
const ctx = canvas.getContext("2d"); requestAnimationFrame(mainLoop); const allTracks = []; function addKeyframedObject(tracks, object) { tracks.clean(); tracks.seek(0); tracks.update(object); allTracks.push({tracks, object}); } const FRAMES_PER_SEC = 60, TICK = 1000 / FRAMES_PER_SEC; // const key = (time, value) => ({time, value}); var playing = false; var showScrubber = false; var currentTime = 0; function mainLoop() { ctx.clearRect(0 ,0 ,ctx.canvas.width, ctx.canvas.height); if(playing) { for (const animated of allTracks) { animated.tracks.tick(); animated.tracks.update(animated.object); } } for (const animated of allTracks) { animated.object.draw(); } if(showScrubber) { slide.update(); slide.draw(); if(slide.value !== currentTime) { currentTime = slide.value; for (const animated of allTracks) { animated.tracks.seek(currentTime); animated.tracks.update(animated.object); } } } else { if(mouse.button) { playing = true } } if(allTracks[0].tracks.time > 6300) { showScrubber = true playing = false; } requestAnimationFrame(mainLoop); } const text = { x: canvas.width / 2, y: canvas.height / 2, alpha: 1, text: "", draw() { ctx.font = "24px arial"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillStyle = "#000"; ctx.globalAlpha = this.alpha; ctx.fillText(this.text, this.x, this.y); ctx.globalAlpha = 1; } } const circle = { x: 0, y: 0, r: 10, col : "", draw() { ctx.fillStyle = this.col; ctx.beginPath(); ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2); ctx.fill() } } const circleTracks = createTracks(); circleTracks.addTrack("x"); circleTracks.addTrack("y"); circleTracks.addTrack("r"); circleTracks.addTrack("col"); circleTracks.addKeysAtTime(0, {x: 220, y :85, r: 20, col: "#F00"}); circleTracks.addKeysAtTime(1000, {x: 220, y :50, r: 50, col: "#0F0"}); circleTracks.addKeysAtTime(2000, {x: 420, y :100, r: 20, col: "#00F"}); circleTracks.addKeysAtTime(3000, {x: 180, y :160, r: 10, col: "#444"}); circleTracks.addKeysAtTime(4000, {x: 20, y :100, r: 20}); circleTracks.addKeysAtTime(5000, {x: 220, y :85, r: 10, col: "#888"}); circleTracks.addKeysAtTime(5500, {r: 10, col: "#08F"}); circleTracks.addKeysAtTime(6000, {r: 340, col: "#00F"}); addKeyframedObject(circleTracks, circle); const textTracks = createTracks(); textTracks.addTrack("alpha"); textTracks.addTrack("text"); textTracks.addKeysAtTime(0, {alpha: 1, text: "Click to start"}); textTracks.addKeysAtTime(1, {alpha: 0}); textTracks.addKeysAtTime(20, {alpha: 0, text: "Simple keyframed animation"}); textTracks.addKeysAtTime(1000, {alpha: 1}); textTracks.addKeysAtTime(2000, {alpha: 0}); textTracks.addKeysAtTime(3500, {alpha: 0, text: "The END!" }); textTracks.addKeysAtTime(3500, {alpha: 1}); textTracks.addKeysAtTime(5500, {alpha: 1}); textTracks.addKeysAtTime(6000, {alpha: 0, text: "Use slider to scrub"}); textTracks.addKeysAtTime(6300, {alpha: 1}); addKeyframedObject(textTracks, text); function createTracks() { return { tracks: {}, addTrack(name, keys = [], value) { this.tracks[name] = {name, keys, idx: -1, value} }, addKeysAtTime(time, keys) { for(const name of Object.keys(keys)) { this.tracks[name].keys.push(key(time, keys[name])); } }, clean() { for(const track of Object.values(this.tracks)) { track.keys.sort((a,b) => a.time - b.time); } }, seek(time) { // seek to random time this.time = time; for(const track of Object.values(this.tracks)) { if (track.keys[0].time > time) { track.idx = -1; // befor first key }else { let idx = 1; while(idx < track.keys.length) { if(track.keys[idx].time > time && track.keys[idx-1].time <= time) { track.idx = idx - 1; break; } idx += 1; } } } this.tick(0); }, tick(timeStep = TICK) { const time = this.time += timeStep; for(const track of Object.values(this.tracks)) { if(track.keys[track.idx + 1] && track.keys[track.idx + 1].time <= time) { track.idx += 1; } if(track.idx === -1) { track.value = track.keys[0].value; } else { const k1 = track.keys[track.idx]; const k2 = track.keys[track.idx + 1]; if (typeof k1.value !== "number" || !k2) { track.value = k1.value; } else if (k2) { const unitTime = (time - k1.time) / (k2.time - k1.time); track.value = (k2.value - k1.value) * unitTime + k1.value; } } } }, update(obj) { for(const track of Object.values(this.tracks)) { obj[track.name] = track.value; } } }; }; const slide = { min: 0, max: 6300, value: 6300, top: 160, left: 1, height: 9, width: 438, slide: 10, slideX: 0, draw() { ctx.fillStyle = "#000"; ctx.fillRect(this.left-1, this.top-1, this.width+ 2, this.height+ 2); ctx.fillStyle = "#888"; ctx.fillRect(this.left, this.top, this.width, this.height); ctx.fillStyle = "#DDD"; this.slideX = (this.value - this.min) / (this.max - this.min) * (this.width - this.slide) + this.left; ctx.fillRect(this.slideX, this.top + 1, this.slide, this.height - 2); }, update() { if(mouse.x > this.left && mouse.x < this.left + this.width && mouse.y > this.top && mouse.y < this.top + this.height) { if (mouse.button && !this.captured) { this.captured = true; } else { canvas.style.cursor = "ew-resize"; } } if (this.captured) { if (!mouse.button) { this.captured = false; canvas.style.cursor = "default"; } else { this.value = ((mouse.x - this.left) / this.width) * (this.max - this.min) + this.min; canvas.style.cursor = "none"; this.value = this.value < this.min ? this.min : this.value > this.max ? this.max : this.value; } } } }; const mouse = {x : 0, y : 0, button : false}; function mouseEvents(e){ const bounds = canvas.getBoundingClientRect(); mouse.x = e.pageX - bounds.left - scrollX; mouse.y = e.pageY - bounds.top - scrollY; mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button; } ["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
canvas { border: 1px solid black; }
<canvas id="canvas" width="440" height="170"><canvas>
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.