Skip to content

Commit 6b19cde

Browse files
authored
Add round and color nodes (#15)
This PR adds new "round" and "color" nodes. Color nodes can be used to map to color props in view (e.g. backgroundColor) Round is required for the color math to function properly (each color component needs to be an integer). Added demo app where you can pan view around that changes color depending on the position on HSV palette.
1 parent b9ba694 commit 6b19cde

File tree

8 files changed

+232
-4
lines changed

8 files changed

+232
-4
lines changed

Example/App.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Snappable from './snappable';
77
import ImageViewer from './imageViewer';
88
import Test from './test';
99
import Interpolate from './src/interpolate';
10+
import Colors from './colors';
1011

1112
YellowBox.ignoreWarnings([
1213
'Warning: isMounted(...) is deprecated',
@@ -20,6 +21,7 @@ const SCREENS = {
2021
Test: { screen: Test, title: 'Test' },
2122
ImageViewer: { screen: ImageViewer, title: 'Image Viewer' },
2223
Interpolate: { screen: Interpolate, title: 'Interpolate' },
24+
Colors: { screen: Colors, title: 'Colors' },
2325
};
2426

2527
class MainScreen extends React.Component {

Example/colors/index.js

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import React, { Component } from 'react';
2+
import { StyleSheet, View, Dimensions } from 'react-native';
3+
import { PanGestureHandler, State } from 'react-native-gesture-handler';
4+
import Animated from 'react-native-reanimated';
5+
6+
// setInterval(() => {
7+
// let iters = 1e8,
8+
// sum = 0;
9+
// while (iters-- > 0) sum += iters;
10+
// }, 300);
11+
12+
const {
13+
set,
14+
cond,
15+
eq,
16+
add,
17+
multiply,
18+
lessThan,
19+
abs,
20+
modulo,
21+
round,
22+
interpolate,
23+
divide,
24+
sub,
25+
color,
26+
Value,
27+
event,
28+
} = Animated;
29+
30+
const PICKER_WIDTH = Dimensions.get('window').width;
31+
const PICKER_HEIGHT = Dimensions.get('window').height;
32+
33+
function match(condsAndResPairs, offset = 0) {
34+
if (condsAndResPairs.length - offset === 1) {
35+
return condsAndResPairs[offset];
36+
} else if (condsAndResPairs.length - offset === 0) {
37+
return undefined;
38+
}
39+
return cond(
40+
condsAndResPairs[offset],
41+
condsAndResPairs[offset + 1],
42+
match(condsAndResPairs, offset + 2)
43+
);
44+
}
45+
46+
function colorHSV(h /* 0 - 360 */, s /* 0 - 1 */, v /* 0 - 1 */) {
47+
// Converts color from HSV format into RGB
48+
// Formula explained here: https://www.rapidtables.com/convert/color/hsv-to-rgb.html
49+
const c = multiply(v, s);
50+
const hh = divide(h, 60);
51+
const x = multiply(c, sub(1, abs(sub(modulo(hh, 2), 1))));
52+
53+
const m = sub(v, c);
54+
55+
const colorRGB = (r, g, b) =>
56+
color(
57+
round(multiply(255, add(r, m))),
58+
round(multiply(255, add(g, m))),
59+
round(multiply(255, add(b, m)))
60+
);
61+
62+
return match([
63+
lessThan(h, 60),
64+
colorRGB(c, x, 0),
65+
lessThan(h, 120),
66+
colorRGB(x, c, 0),
67+
lessThan(h, 180),
68+
colorRGB(0, c, x),
69+
lessThan(h, 240),
70+
colorRGB(0, x, c),
71+
lessThan(h, 300),
72+
colorRGB(x, 0, c),
73+
colorRGB(c, 0, x) /* else */,
74+
]);
75+
}
76+
77+
export default class Example extends Component {
78+
static navigationOptions = {
79+
title: 'Colors Example',
80+
};
81+
constructor(props) {
82+
super(props);
83+
84+
const dragX = new Value(0);
85+
const dragY = new Value(0);
86+
const state = new Value(-1);
87+
88+
this._onGestureEvent = event([
89+
{
90+
nativeEvent: { translationX: dragX, translationY: dragY, state: state },
91+
},
92+
]);
93+
94+
const offsetX = new Value(PICKER_WIDTH / 2);
95+
this._transX = cond(
96+
eq(state, State.ACTIVE),
97+
add(offsetX, dragX),
98+
set(offsetX, add(offsetX, dragX))
99+
);
100+
101+
const offsetY = new Value(200);
102+
this._transY = cond(
103+
eq(state, State.ACTIVE),
104+
add(offsetY, dragY),
105+
set(offsetY, add(offsetY, dragY))
106+
);
107+
108+
const h = interpolate(this._transX, {
109+
inputRange: [0, PICKER_WIDTH],
110+
outputRange: [0, 360],
111+
extrapolate: 'clamp',
112+
});
113+
const s = interpolate(this._transY, {
114+
inputRange: [0, PICKER_HEIGHT],
115+
outputRange: [1, 0],
116+
extrapolate: 'clamp',
117+
});
118+
const v = 1;
119+
this._color = colorHSV(h, s, v);
120+
}
121+
render() {
122+
return (
123+
<View style={styles.container}>
124+
<PanGestureHandler
125+
maxPointers={1}
126+
onGestureEvent={this._onGestureEvent}
127+
onHandlerStateChange={this._onGestureEvent}>
128+
<Animated.View
129+
style={[
130+
styles.box,
131+
{
132+
backgroundColor: this._color,
133+
transform: [
134+
{ translateX: this._transX, translateY: this._transY },
135+
],
136+
},
137+
]}
138+
/>
139+
</PanGestureHandler>
140+
</View>
141+
);
142+
}
143+
}
144+
145+
const CIRCLE_SIZE = 70;
146+
147+
const styles = StyleSheet.create({
148+
container: {
149+
flex: 1,
150+
backgroundColor: '#F5FCFF',
151+
},
152+
box: {
153+
width: CIRCLE_SIZE,
154+
height: CIRCLE_SIZE,
155+
marginLeft: -CIRCLE_SIZE / 2,
156+
marginTop: -CIRCLE_SIZE / 2,
157+
borderRadius: CIRCLE_SIZE / 2,
158+
borderColor: 'black',
159+
borderWidth: 1,
160+
},
161+
});

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,14 @@ exp(node)
278278

279279
Returns an exponent of the value of the given node.
280280

281+
### `round`
282+
283+
```js
284+
round(node)
285+
```
286+
287+
Returns node that rounds input value to the nearest integer.
288+
281289
---
282290
### `lessThan`
283291

@@ -444,6 +452,17 @@ Extrapolate.IDENTITY; // Will return the input value if the input value is out o
444452

445453
Maps an input value within a range to an output value within a range. Also supports different types of extrapolation for when the value falls outside the range.
446454

455+
---
456+
### `color`
457+
458+
```js
459+
color(red, green, blue, alpha)
460+
```
461+
462+
Creates a color node in RGBA format. Where first three input nodes should have integer values in range 0-255 and corresponds to color components Red, Green and Blue respectively. Last input node should have value between 0 and 1 and represents alpha channel (value `1` means fully opaque and `0` completely transparent). Alpha parameter can be ommited, then `1` (fully opaque) is used as a default.
463+
464+
The returned node can be mapped to view properties that represents color (e.g. [`backgroundColor`](https://facebook.github.io/react-native/docs/view-style-props.html#backgroundcolor)).
465+
447466
<!-- Anims -->
448467

449468
---

android/src/main/java/com/swmansion/reanimated/nodes/OperatorNode.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,12 @@ public double eval(Double x) {
107107
return Math.exp(x);
108108
}
109109
};
110+
private static final Operator ROUND = new SingleOperator() {
111+
@Override
112+
public double eval(Double x) {
113+
return Math.round(x);
114+
}
115+
};
110116

111117
// logical
112118
private static final Operator AND = new Operator() {
@@ -159,7 +165,7 @@ public boolean eval(Double x, Double y) {
159165
private static final Operator GREATER_THAN = new CompOperator() {
160166
@Override
161167
public boolean eval(Double x, Double y) {
162-
return x < y;
168+
return x > y;
163169
}
164170
};
165171
private static final Operator LESS_OR_EQ = new CompOperator() {
@@ -211,6 +217,8 @@ public OperatorNode(int nodeID, ReadableMap config, NodesManager nodesManager) {
211217
mOperator = COS;
212218
} else if ("exp".equals(op)) {
213219
mOperator = EXP;
220+
} else if ("round".equals(op)) {
221+
mOperator = ROUND;
214222
} else if ("and".equals(op)) {
215223
mOperator = AND;
216224
} else if ("or".equals(op)) {

ios/Nodes/REAOperatorNode.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ - (instancetype)initWithID:(REANodeID)nodeID config:(NSDictionary<NSString *,id>
4848
@"sin": REA_SINGLE(sin(a)),
4949
@"cos": REA_SINGLE(cos(a)),
5050
@"exp": REA_SINGLE(exp(a)),
51+
@"round": REA_SINGLE(round(a)),
5152

5253
// logical
5354
@"and": ^(NSArray<REANode *> *inputNodes) {

src/ConfigHelper.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ const { ReanimatedModule } = NativeModules;
88
let NATIVE_PROPS_WHITELIST = {
99
opacity: true,
1010
transform: true,
11+
/* colors */
12+
backgroundColor: true,
13+
borderRightColor: true,
14+
borderBottomColor: true,
15+
borderColor: true,
16+
borderEndColor: true,
17+
borderLeftColor: true,
18+
backgroundColor: true,
19+
borderStartColor: true,
20+
borderTopColor: true,
1121
/* ios styles */
1222
shadowOpacity: true,
1323
shadowRadius: true,

src/base.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const sqrt = operator('sqrt');
2525
export const sin = operator('sin');
2626
export const cos = operator('cos');
2727
export const exp = operator('exp');
28+
export const round = operator('round');
2829
export const lessThan = operator('lessThan');
2930
export const eq = operator('eq');
3031
export const greaterThan = operator('greaterThan');

src/derived.js

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1+
import { Platform } from 'react-native';
12
import {
23
cond,
34
lessThan,
5+
greaterThan,
46
multiply,
57
block,
68
defined,
79
sub,
810
set,
911
add,
1012
divide,
13+
round,
1114
} from './base';
1215
import AnimatedValue from './core/AnimatedValue';
1316
import { adapt } from './utils';
@@ -38,6 +41,29 @@ export const diff = function(v) {
3841
]);
3942
};
4043

44+
export const color = function(r, g, b, a = 1) {
45+
if (a instanceof AnimatedValue) {
46+
a = round(multiply(a, 255));
47+
} else {
48+
a = Math.round(a * 255);
49+
}
50+
const color = add(
51+
multiply(a, 1 << 24),
52+
multiply(r, 1 << 16),
53+
multiply(g, 1 << 8),
54+
b
55+
);
56+
if (Platform.OS === 'android') {
57+
// on Android color is represented as signed 32 bit int
58+
return cond(
59+
lessThan(color, (1 << 31) >>> 0),
60+
color,
61+
sub(color, Math.pow(2, 32))
62+
);
63+
}
64+
return color;
65+
};
66+
4167
export const acc = function(v) {
4268
const acc = new AnimatedValue(0);
4369
return set(acc, add(acc, v));
@@ -82,9 +108,9 @@ const interpolateInternal = function(
82108
};
83109

84110
export const Extrapolate = {
85-
EXTEND: 'EXTEND',
86-
CLAMP: 'CLAMP',
87-
IDENTITY: 'IDENTITY',
111+
EXTEND: 'extend',
112+
CLAMP: 'clamp',
113+
IDENTITY: 'identity',
88114
};
89115

90116
export const interpolate = function(value, config) {

0 commit comments

Comments
 (0)