Как анимировать несколько HTML5 объектов холста один за другим? - PullRequest
1 голос
/ 10 января 2020

Я хочу сделать анимацию, используя HTML5 canvas и 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()

...

Есть ли простой способ для этого (без необходимости иметь дело с обещаниями и вложенными функциями обратного вызова), например, с помощью некоторой библиотеки?

Ответы [ 2 ]

1 голос
/ 10 января 2020

Ключевые кадры

Вы можете использовать ключевые кадры, чтобы эффектно анимировать практически все.

Пример ниже (собирался сделать больше записи, но я опоздал, вы принял ответ) показывает, как очень простая c ключевая рамка может создавать анимации.

Ключевой кадр - это просто time и value

Добавлены ключевые кадры. на дорожки, которые дают имя значению.

Таким образом, имя x (позиция) и ключи {time: 0, value: 100}, {time: 1000, value: 900} изменят x свойство от 100 до 900 в течение времени от 0 до 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();

// properties to animate
circleTracks.addTrack("x");
circleTracks.addTrack("y");
circleTracks.addTrack("r");
circleTracks.addTrack("col");

Затем добавьте ключевые кадры с указанными c отметками времени.

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)

. Это самая основная c анимация ключевого кадра.

И главное в ключевых кадрах то, что они отделяют анимацию от кода, позволяя импортировать и экспортировать анимации в виде простых структур данных.

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>
1 голос
/ 10 января 2020

Хороший вопрос, учитывая, что то, что вы не хотите делать (использовать обещания и / или обратные вызовы), фактически означало бы жесткое кодирование анимации в сценарии с ограниченным потенциалом для повторного использования и, возможно, создавая трудности при внесении изменений в будущее.

Решением, которое я использовал, является создание сборника рассказов о функциях, которые рисуют кадры, так что вы бы поместили

()=>line1.draw()

в книгу, а не

* 1007. *

, который немедленно нарисовал бы его и попытался бы добавить его возвращаемое значение в книгу!

Следующая часть (без определенного порядка) - это игрок, который использует 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>
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...