Реагировать анимацию JS на основе данных JSON

Я использую React/Redux и сохраняю данные анимации в JSON и пытаюсь отобразить его на странице React.

Я использую setTimeout (для пауз) и setInterval (для движения анимации). Тем не менее, мне кажется, у меня проблемы с пониманием того, как правильно реализовать анимацию, и думаю, что я иду совсем по-другому.

Данные JSON:

"objects": [

    {
        "title": "puppy",
        "image_set": [
            {
                "image": "images/puppy_sitting.png",
                "startx": 520,
                "starty": 28,
                "pause": 1000

            },
            {
                "image": "images/puppy_walking.png",
                "startx": 520,
                "starty": 28,
                "endx": 1,
                "endy": 1,
                "time": 1000
            },
            {
                "image": "images/puppy_crouching.png",
                "startx": 1,
                "starty": 1,
                "endx": 500,
                "endy": 400,
                "time": 2000
            }

        ]
    },
    {
        "title": "scorpion",
        "image_set": [
            {
                "image": "images/scorping_sleeping.png",
                "startx": 100,
                "starty": 400,
                "pause": 5000

            },
            {
                "image": "images/scorpion_walking.png",
                "startx": 100,
                "starty": 400,
                "endx": 500,
                "endy": 400,
                "time": 7000
            },
            {
                "image": "images/scorpion_walking.png",
                "startx": 500,
                "starty": 400,
                "endx": 100,
                "endy": 400,
                "time": 2000
            },
            {
                "image": "images/scorpion_walking.png",
                "startx": 100,
                "starty": 400,
                "endx": 200,
                "endy": 400,
                "time": 7000
            },
            {
                "image": "images/scorpion_walking.png",
                "startx": 200,
                "starty": 400,
                "endx": 100,
                "endy": 400,
                "time": 1000
            }
        ]
    }
]

Каждый объект может иметь несколько связанных с ними изображений. Анимация будет продолжать повторяться без остановок. Каждый объект должен перемещаться одновременно с каждым из других объектов, чтобы я мог создать сцену различных животных и объектов, перемещающихся вокруг нее.

Анимационный код:

Я почти уверен, что я ругаю неправильное дерево здесь, но мой код выглядит примерно так:

  // image_set is the list of images for a specific object
  // object_num is the array index corresponding to the JSON objects array
  // selected is the array index corresponding to which image in the image_set will be displayed
  runAnimation(image_set, object_num, selected){

        // Uses prevState so that we keep state immutable
        this.setState(prevState => {
            let images = [...prevState.images];

            if (!images[object_num]){
                images.push({image: null, x: 0, y: 0})

            }

            images[object_num].image = image_set[selected].image;
            images[object_num].x = this.getFactoredX(image_set[selected].startx);
            images[object_num].y = this.getFactoredY(image_set[selected].starty);
            return {images: images};
        })

        if (image_set[selected].endx && image_set[selected].endy && image_set[selected].time){
                let x = this.getFactoredX(image_set[selected].startx)
                let y = this.getFactoredY(image_set[selected].starty)
                let startx = x
                let starty = y
                let endx = this.getFactoredX(image_set[selected].endx)
                let endy = this.getFactoredY(image_set[selected].endy)
                let time = image_set[selected].time

                let x_increment = (endx - x) / (time / 50)
                let y_increment = (endy - y) / (time / 50)



                let int = setInterval(function(){

                        x += x_increment
                        y += y_increment


                        if (x > endx || y > endy){
                                clearInterval(int)
                        }

                        this.setState(prevState => {
                                let images = [...prevState.images]

                                if (images[object_num]){
                                        images[object_num].x = x
                                        images[object_num].y = y
                                }


                                return {images: images};


                        })


                }.bind(this),
                 50
                )

        }

        if (image_set[selected].pause && image_set[selected].pause > 0){
                selected++

                if (selected == image_set.length){
                        selected = 0
                }

                setTimeout(function() {
                        this.runAnimation(image_set, object_num, selected)

                }.bind(this),
                  image_set[selected].pause
                )

        }
        else {
                selected++

                if (selected == image_set.length){
                        selected = 0
                }
                setTimeout(function() {
                        this.runAnimation(image_set, object_num, selected)

                }.bind(this),
                        50
                )
        }


  }

Redux и this.props.data

Redux вводит данные в качестве реквизита. Итак, у меня есть функция, вызываемая из моих функций componentDidMount и componentWillReceiveProps, которая передает исходное изображение, установленное в loadAnimationFunction.

Мой рендер()

В моей функции render() меня есть что-то вроде этого:

if (this.state.images.length > 1){
    animated = this.state.images.map((image, i) => {
            let x_coord = image.x
            let y_coord = image.y
            return (
                     <div key={i} style={{transform: "scale(" + this.state.x_factor + ")", transformOrigin: "top left", position: "absolute", left: x_coord, top: y_coord}}>
                            <img src={'/api/get_image.php?image=${image.image}'} />
                    </div>
            )

    })
}

x_factor/y_factor

Во всем моем коде есть ссылка на х и у фактор. Это связано с тем, что фон, в котором появляются анимации, может быть меньше или меньше. Поэтому я также масштабирую положение начальных и конечных координат x/y для каждой анимации, а также масштабирует сами анимированные изображения.

время и время паузы

Время указывает время в мс, которое должна принимать анимация. Время паузы указывает, как долго в секундах приостанавливаться до перехода к следующей анимации.

Эта проблема

Код не перемещает анимацию плавно, и они, похоже, периодически спотыкаются.

Кроме того, когда я нажимаю кнопку мыши на любой странице, это заставляет анимацию переходить в другую позицию. Почему щелчок мышью влияет на анимацию?

Одна вещь, которую я заметил, это то, что если у меня консоль открыта для целей отладки, это действительно замедляет анимацию.

Что я могу сделать с моим кодом, чтобы анимация работала так, как ожидалось?

Ответы

Ответ 1

Вы пытаетесь анимировать свой элемент, используя setInterval делая setState из координат и с absolute позицией. Все это не может достичь высокой производительности.

Во-первых, setInterval никогда не должен использоваться для анимации, и вы должны предпочесть requestAnimationFrame, так как он позволит анимацию со скоростью 60 кадров в секунду, так как анимация будет запущена до следующей перерисовки браузера.

Во-вторых, выполнение setState приведет к повторной визуализации всего компонента, что потенциально может повлиять на время рендеринга, поскольку я предполагаю, что ваш компонент не отображает только ваши изображения. Вы должны стараться избегать повторного рендеринга вещей, которые не изменились, поэтому постарайтесь изолировать ваши изображения для анимации.

Наконец, когда вы позиционируете свой элемент с left и top свойствами, но вы должны придерживаться этого, позиционируя, а не анимируя, так как браузер будет делать анимацию попиксельно и не сможет создавать хорошие характеристики. Вместо этого вы должны использовать CSS translate(), так как он может выполнять вычисления на уровне субпикселей и вместо этого будет работать на графическом процессоре, позволяя достигать анимации 60 кадров в секунду. На это есть хорошая статья Пола Айриша.


Тем не менее, вы, вероятно, должны использовать реагирующее движение, которое позволит вам плавную анимацию:

import { Motion, spring } from 'react-motion'

<Motion defaultStyle={{ x: 0 }} style={{ x: spring(image.x), y: spring(image.y) }}>
  {({ x, y }) => (
    <div style={{
       transform: 'translate(${x}px, ${y}px)'
    }}>
      <img src={'/api/get_image.php?image=${image.image}'} />
    </div>
  )}
</Motion>

Существует также React Transition Group, Transition может перемещать ваши элементы с помощью анимации translate как описано выше. Вы также должны взглянуть на документы по анимации реакции здесь.

Также стоит попробовать React Pose, который довольно прост в использовании и также хорошо работает с чистым API. Вот начальная страница React.


Вот краткая демонстрация с использованием вашей концепции с циклом сидя/ходьбы/бега. Обратите внимание, что реакция-движение - единственная, которая обрабатывает переход между кадрами без жесткого кодирования продолжительности перехода, которая будет идти против плавного пользовательского интерфейса, состояние только обрабатывает прохождение различных этапов.

Цитирую Readme:

В 95% случаев использования компонентов анимации нам не нужно прибегать к использованию жестко закодированных кривых замедления и продолжительности. Установите жесткость и демпфирование для вашего элемента пользовательского интерфейса, и пусть магия физики позаботится обо всем остальном. Таким образом, вам не нужно беспокоиться о мелких ситуациях, таких как прерывание анимации. Это также значительно упрощает API.

Если вас не устраивает пружина по умолчанию, вы можете изменить параметры dampling и stiffness. Есть приложение, которое может помочь вам получить то, что удовлетворяет вас больше всего.

demo

Source

Ответ 2

Реакт не предназначен для анимации. Я не говорю, что вы не можете анимировать реагирующие компоненты, но это не часть проблемной области, которую пытается реагировать. То, что он делает, это предоставить вам хорошую структуру, чтобы несколько частей пользовательского интерфейса взаимодействовали друг с другом. Т.е. при создании игры, например, вы будете использовать реакцию и сокращение для создания и управления экранами, кнопками и т.д., Однако сама игра будет содержаться отдельно и не использовать реакцию.

Просто длинный способ сказать, что если вы хотите использовать анимацию, реакция будет недостаточной, лучше использовать что-то вроде библиотеки анимации грейзеров: https://greensock.com/ Они предоставляют учебник о том, как использовать его в сочетании с реагировать: https://greensock.com/react

Ответ 3

Я подхожу к этому под другим углом: анимации, которые есть в вашем образце, очень легко выразить с помощью css transition, transition-delay и transform. Я приложил бы свои усилия для преобразования JSON в css (используя решение cssInJs, которое позволяет генерировать классы на лету) и применил эти классы к изображениям.

как-то так (рабочий пример с вашим примером JSON): https://stackblitz.com/edit/react-animate-json

const App = () =>
  <div>
    {objects.map(object =>
      <Item item={object} />)
    }
  </div>

Item.js:

class Item extends React.Component {
  state = { selected: 0, classNames: {} }
  componentDidMount() {
    this.nextImage();
    this.generateClassNames();
  }

  generateClassNames = () => {
    const stylesArray = this.props.item.image_set.flatMap((image, index) => {
      const { startx, starty, endx = startx, endy = starty, time } = image;
      return [{
        ['image${index}_start']: {
          transform: 'translate(${startx}px,${starty}px)',
          transition: 'all ${time || 0}ms linear'
        }
      }, {
        ['image${index}_end']: { transform: 'translate(${endx}px,${endy}px)' }
      }]
    });

    const styles = stylesArray.reduce((res, style) => ({ ...res, ...style }), {})

    const { classes: classNames } = jss.createStyleSheet(styles).attach();
    this.setState({ classNames });
  }

  nextImage = async () => {
    const { image_set } = this.props.item;
    let currentImage = image_set[this.state.selected];
    await wait(currentImage.pause);
    await wait(currentImage.time);

    this.setState(({ selected }) =>
      ({ selected: (selected + 1) % image_set.length }), this.nextImage)
  }

  render() {
    const { selected, classNames } = this.state;
    const startClassName = classNames['image${selected}_start'];
    const endClassName = classNames['image${selected}_end'];
    return <img
      className={'${startClassName} ${endClassName}'}
      src={this.props.item.image_set[selected].image}
    />
  }
}

const wait = (ms) => new Promise(res => setTimeout(res, ms));

Ответ 4

О проблеме: "Код не перемещает анимацию гладко"

Проблема заключается в настройке времени, а именно в setTimeout и setInterval и времени в данных.

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

Чтобы исправить это, добавьте настройку временного интервала следующим образом:

let int = setInterval(function(){}.bind(this),100)
setTimeout(function() {
     this.runAnimation(image_set, object_num, selected)
}.bind(this), 5000)

О проблеме: "консоль открыта для целей отладки"

Анимация в javascript будет занимать много воспоминаний. Утешение слишком большой информации также будет занимать воспоминания.

Другое предложение

Использование js-анимации фактически не является хорошим вариантом. Если ваша анимация не слишком сложна, анимация css лучше. Если ваша анимация будет намного сложнее в будущем, вы также можете попробовать другую стороннюю анимационную библиотеку, такую как React Motion, Velocity Реагировать и так далее.

Ответ 5

Я считаю, что ваша основная проблема заключается в том, как реагировать на React/Redux. React может объединить несколько запросов обновления, чтобы сделать рендеринг более эффективным. Без дальнейших диагностических мер, я предполагаю, что обработка состояния после setState будет просто слишком жестко реагировать.

Это решение должно было бы обновить вашу анимацию вне государственной системы, либо используя готовые рамки, либо просто самостоятельно заботясь о анимации; получить ссылку на элемент и обновить его вместо повторного рендеринга элемента при каждом обновлении состояния.

Ответ 6

не задумываясь о анимации в JS (здесь уже есть достоверные ответы), вы должны подумать о том, как вы визуализируете свои изображения:

<div key={i} style={{transform: "scale(" + this.state.x_factor + ")", transformOrigin: "top left", position: "absolute", left: x_coord, top: y_coord}}>
    <img src={'/api/get_image.php?image=${image.image}'} />
</div>

Вы должны увидеть предупреждение при компиляции (или это было в документах?), Потому что вы используете индекс цикла как ключ. Это должно привести к тому, что объект изображения будет отображаться в разных div, так как добавляются и удаляются дополнительные изображения. Это особенно критично, если у вас есть эффект css-перехода на div. TL;DR: используйте какой-то идентификатор в качестве key вместо переменной i (может быть сгенерирован при создании анимации?) Также, если у вас есть переход css на div, вы должны удалить его, потому что вместе с изменениями из setInterval переход расчет не сможет идти в ногу с изменениями.