我正在使用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因子。这是因为动画出现的背景可能会变小或变大。因此,我还要缩放每个动画的开始和结束x / y坐标的位置,以及缩放动画图像本身。
时间和暂停时间
时间表示动画应花费的时间(以毫秒为单位)。暂停时间表示在移至下一个动画之前要暂停的毫秒数。
问题
该代码不能平稳地移动动画,并且它们似乎偶尔会跳来跳去。
此外,当我在页面上的任意位置单击鼠标时,会使动画跳到另一个位置。为什么单击鼠标会影响动画?
我注意到的一件事是,如果我出于调试目的而打开控制台,这确实会降低动画的速度。
我该如何对代码进行处理,以使动画按预期工作?
答案 0 :(得分:8)
您正尝试使用setInterval
进行setState
坐标和absolute
位置的动画。所有这些都无法实现出色的性能。
首先,setInterval
永远不要用于动画,而您应该更喜欢requestAnimationFrame,因为它会允许60fps的动画,因为动画将在浏览器下一次重绘之前运行。
第二,执行setState
将重新渲染您的整个组件,这可能会对渲染时间产生影响,因为我认为您的组件不会仅渲染图像。您应该尽量避免重新渲染未更改的内容,因此请尝试为动画隔离图像。
最后,当您使用left
和top
属性对元素进行 position 定位时,但是您应该坚持该定位,定位并且不进行动画处理,因为浏览器会制作动画逐像素,将无法创造出良好的性能。相反,您应该使用CSS translate()
,因为它可以进行亚像素计算,并且可以在GPU上运行,从而可以实现60fps的动画。保罗·爱尔兰(Paul Irish)对此有一个good article。
话虽如此,您可能应该使用react-motion,这样可以使您获得流畅的动画效果:
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 animations文档here。
值得一试的是React Pose,它非常易于使用,并且在使用干净的API时也表现良好。 Here是React的入门页面。
这里是一个使用您的概念进行坐姿/行走/跑步周期的快速演示。请注意,react-motion是处理帧之间的过渡而不用硬编码过渡持续时间的唯一方法,而状态go against a fluid UI只能处理经过不同步骤的状态。
引用反应动作Readme:
对于95%的动画组件用例,我们不必求助于使用硬编码的缓动曲线和持续时间。为您的UI元素设置刚度和阻尼,然后让物理魔术处理其余的工作。这样,您就不必担心诸如动画行为中断之类的小问题。这也大大简化了API。
如果您对默认弹簧不满意,可以更改dampling
和stiffness
参数。 app可以帮助您获得最满足您的需求。
答案 1 :(得分:2)
反应并非完全旨在用于动画。我并不是说您不能为React组件设置动画,但这不是React尝试解决的问题的一部分。它的作用是为您提供一个不错的框架,使多个UI彼此交互。即例如,在创建游戏时,您将使用react和redux来创建和管理屏幕,按钮等。但是游戏本身将被单独包含而不使用react。
一种长篇大论的说法是,如果您想使用动画反应不够用,那么最好使用诸如greensock的动画库之类的方法:https://greensock.com/ 他们提供了有关如何与react结合使用的教程:https://greensock.com/react
答案 2 :(得分:1)
让CSS执行转换。使用变换:翻译而不是上下。
使用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));
答案 3 :(得分:0)
我相信您的根本问题在于React / Redux处理状态的方式。 React可以将多个更新请求分批处理,以提高渲染效率。如果没有进一步的诊断措施,我的猜测是setState
之后的状态处理将过于僵化。
该解决方案将使用现成的框架或简单地自己动手维护动画来更新状态系统以外的动画。获取对该元素的引用并进行更新,而不是每次状态更新时都重新渲染该元素。
答案 4 :(得分:0)
在不深入探讨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中呈现。如果在div上具有css过渡效果,则这一点尤其重要。
TLDR:使用一些标识符作为key
而不是变量i
(在创建动画时可以生成一个标识符吗?)
另外,如果div上有css过渡,则应将其删除,因为与setInterval中的更改一起,过渡计算将无法跟上更改。