未加星标

Declarative `canvas` Animation with React and Konva

字体大小 | |
[前端(javascript) 所属分类 前端(javascript) | 发布者 店小二04 | 时间 2017 | 作者 红领巾 ] 0人收藏点击收藏

Today we're looking at declarative animation that renders on <canvas> : React for declarativeness, Konva as a canvas abstraction layer, and react-konva to make them work together.

In theory, this combination has better performance than SVG+React but worse performance than raw canvas because of the additional abstraction layers. We have canvas as the rendering layer, then Konva gives us basic shapes and interactions, react-konva turns those into React components, and our own React code makes it work together.

If that sounds complicated… it probably is. I barely know how to use it, and I have no idea how it actually works.


Declarative `canvas` Animation with React and Konva

We're building a marble simulation. You can pick up a marble and throw it, and it bounces around until it stops. I wanted to add collision detection as well, but N-body collisions are hard . Next time!

You can see the code on Github and play with marbles on the live demo . The live demo looks better than the gif, I promise.

We have two components:

Marble , which renders each marble and deals with drag events Collisions , which renders all the marbles and deals with the game loop logic

Yes, the game loop logic should totally be a Redux or MobX store instead of shoved into a component. This is fine. Small example :stuck_out_tongue_winking_eye:

Marble

The Marble component uses react-konva to render a <Circle> and listen for a dragend event. That's how you "throw".

You can think of react-konva as a very thin abstraction layer on Konva. I looked at the source code once, and it just uses a bit of magic to translate all of Konva's classes into React components. Props are passed through unchanged as Konva attributes.

That means you don't have to think about using react-konva. Focus on the Konva docs, it's all the same.

class Marble extends Component { onDragEnd() { const { x, y } = this.props, circle = this.refs.circle; this.props.onShoot({ x: circle.attrs.x, y: circle.attrs.y, vx: (circle.attrs.x-x)/7, vy: (circle.attrs.y-y)/7 }); } render() { const { x, y, sprite, type, draggable } = this.props; return ( <Circle x={x} y={y} radius={MarbleR} fillPatternImage={sprite} fillPatternOffset={Marbles[type]} fillPatternScale={{ x: MarbleR*2/111, y: MarbleR*2/111 }} shadowColor={Marbles[type].c} shadowBlur="15" shadowOpacity="1" draggable={draggable} onDragEnd={this.onDragEnd.bind(this)} ref="circle" /> ); } }

We're rendering a <Circle> element at position (x, y) and giving it a radius of 15 . Very similar to SVG, right?

Here's where it gets crazy. To get the marble look, we use fillPattern* props and use a sprite for the background. We reposition and scale it to get the sprite to fit and make marbles look different.


Declarative `canvas` Animation with React and Konva

For the shine effect, we use shadow* props. Shadows get a color that matches each marble (I used Photoshop), some blur, and an opacity. This gives each marble a glow that makes the marbles look shiny.

They're still shadows though, so a bunch of things are wrong. Especially when the marbles get close together, you can see that the shadows look darker when combined. Real shines would look brighter.

To get draggability, we turn it on. Konva handles the rest for us. onDragEnd we call the onShoot callback with the marble's new position and movement vector. This part is janky. I'll explain why later.

Collisions

The <Collisions> component is called collisions because this was meant to be a simulation of inelastic N-body collisions . High school physics stuff.

But that's hard to do, so you get just the bouncing off of walls.

This component has three major parts:

calculating the initial positions to make a triangle the game loop that drives animation declaratively rendering the marbles

class Collisions extends Component { constructor(props) { // setting up this.state } get initialPositions() { // calculating initial positions } componentDidMount() { // loading sprite } shoot(newPos, i) { // updating a thrown marble } gameLoop() { // moving } render() { // rendering }

Initial positions and sprite loading

componentDidMount() { const sprite = new Image(); sprite.src = MarbleSprite; sprite.onload = () => { this.setState({ sprite: sprite }); this.timer = timer(() => this.gameLoop()); }; }

Konva takes sprites as ES6 Image objects. We load one up, wait for the onload event to fire, add it to state, which triggers a re-render, and start the game loop timer.

If you're not familiar with the Image object, it's basically an in-memory representation of the image bytestream. Unless you're doing something very particular, you don't need to know the details. It loads an image into memory

get initialPositions() { const { width, height } = this.props, center = width/2; let marbles = range(3, 0, -1).map(y => { if (y === 3) return [{ x: center, y: 200, vx: 0, vy: 0}]; const left = center - y*(MarbleR+5), right = center + y*(MarbleR+5); return range(left, right, MarbleR*2+5).map(x => ({ x: x, y: 200-y*(MarbleR*2+5), vx: 0, vy: 0 })); }).reduce((acc, pos) => acc.concat(pos), []); marbles = marbles.concat({ x: width/2, y: height-150, vx: 0, vy: 0 }); return marbles; }

This… this took me embarrassingly long to code. It's one of those interview question things: Render stuff in a triangle. Then you fumble for an hour, and they're like "LoL you're an idiot, pass" .

That was going through my mind the entire time. How the hell am I struggling this hard to put marbles in a triangle?

Here's how it works:

loop from 3 to 0 to create the rows in each row. go from the left edge to the right edge with a step of "marble size" add position to array

You get the left edge is y marble halves to the left, and the right is y marble halves to the right. This nested loop approach returns a nested array so you flatten it with a .reduce .

Oh, and those range() functions are actually d3.range . I got them with import { range } from 'd3-array' .

Game loop

Our game loop is a function that d3.timer calls on every requestAnimationFrame . It goes through our array of marbles, updates their positions, and triggers a re-render.

Like this:

shoot(newPos, i) { let marbles = this.state.marbles; marbles[i] = newPos; this.setState({ marbles: marbles }); } gameLoop() { const { width, height } = this.props; const moveMarble = ({x, y, vx, vy}) => ({ x: x+vx, y: y+vy, vx: ((x+vx < MarbleR) ? -vx : (x+vx > width-MarbleR) ? -vx : vx)*.99, vy: ((y+vy < MarbleR) ? -vy : (y+vy > height-MarbleR) ? -vy : vy)*.99 }); this.setState({ marbles: this.state.marbles.map(moveMarble) }); }

See? Loop through marbles and update their positions by adding the speed vector to the position. We invert the speed vector when a marble is about to hit a wall in the next step.

Nested ternary expressions are hard to read. I should refactor that. If x+vx is smaller than left edge, invert vx . Otherwise if x+vx is bigger than right edge, invert vx . Otherwise, leave it alone.

The shoot() function is that dragend callback that <Marble> calls. It updates the particular marble with the new position and the new speed vector.

Rendering

After all that logic, rendering is the easy part. We loop through the marbles and declaratively add them to the Stage . Stage is what Konva calls the canvas element. I don't fully understand why, but I'm sure there's a reason.

render() { const { sprite } = this.state, { width, height } = this.props, marbleTypes = Object.keys(Marbles); if (!sprite) { return (<h2>Loading sprites ...</h2>); } return ( <Stage width={width} height={height}> <Layer> <Group> {this.state.marbles.map(({x: x, y: y}, i) => ( <Marble x={x} y={y} type={marbleTypes[i%marbleTypes.length]} sprite={sprite} draggable="true" onShoot={(newPos) => this.shoot(newPos, i)} key={`marble-${i}`} /> ))} </Group> </Layer> </Stage> ) }

See? Loop through marbles , put down Marble components. All this inside a Stage , which is the canvas, and Layer , which I think makes more sense when you have more than one, and Group which is the same concept as SVG's <g> element. It helps you think of groups of shapes as a single thing.

A Stage must always have at least one Layer. So that part is important albeit seemingly useless.

Why is your demo so janky, Swizec?

Did you guess it yet? Why am I having so much trouble throwing marbles at 0:14 in the video ?

It's the game loop and Konva fighting each other. The game loop re-renders all our marbles every 16 milliseconds. Konva isn't telling React that they've moved, so the position is reset.

That means you have to complete your throw within 16ms or it won't work.

Now, while this looks really bad, it's not a fundamental limitation of the React-Konva-Canvas stack. Just an extra step to take care of before I tackle the N-body collisions.

Gotta add a dragmove listener to Marble and make sure it updates React state. Should be easy

本文前端(javascript)相关术语:javascript是什么意思 javascript下载 javascript权威指南 javascript基础教程 javascript 正则表达式 javascript设计模式 javascript高级程序设计 精通javascript javascript教程

主题: ReactGitPhotoshop
分页:12
转载请注明
本文标题:Declarative `canvas` Animation with React and Konva
本站链接:http://www.codesec.net/view/535043.html
分享请点击:


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 前端(javascript) | 评论(0) | 阅读(100)