簡體   English   中英

如何一個接一個地為多個 HTML5 畫布對象設置動畫?

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM