Created
September 17, 2018 22:58
-
-
Save sanealytics/eae89a5397e6f036d5af3938b0953288 to your computer and use it in GitHub Desktop.
ImageViewer pinch generate
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { Component } from 'react'; | |
import { StyleSheet, View, Image } from 'react-native'; | |
import { | |
PanGestureHandler, | |
State, | |
PinchGestureHandler, | |
TapGestureHandler, | |
} from 'react-native-gesture-handler'; | |
import Animated, { Easing } from 'react-native-reanimated'; | |
// setInterval(() => { | |
// let iters = 1e8, | |
// sum = 0; | |
// while (iters-- > 0) sum += iters; | |
// }, 300); | |
const { | |
set, | |
cond, | |
eq, | |
or, | |
add, | |
sub, | |
pow, | |
min, | |
max, | |
debug, | |
multiply, | |
divide, | |
lessThan, | |
spring, | |
defined, | |
decay, | |
timing, | |
call, | |
diff, | |
acc, | |
not, | |
abs, | |
block, | |
startClock, | |
stopClock, | |
clockRunning, | |
Value, | |
Clock, | |
event, | |
} = Animated; | |
function randomUniform(min, max) { | |
return Math.random() * (max - min) + min | |
} | |
function generatePhoto( | |
xMin, xMax, | |
yMin, yMax) | |
{ | |
console.log('Pulling random photo') | |
return { | |
x: randomUniform(xMin, xMax), | |
y: randomUniform(yMin, yMax), | |
szX: (xMax - xMin), | |
szY: (yMax - yMin), | |
} | |
} | |
function scaleDiff(value) { | |
const tmp = new Value(1); | |
const prev = new Value(1); | |
return [set(tmp, divide(value, prev)), set(prev, value), tmp]; | |
} | |
function dragDiff(value, updating) { | |
const tmp = new Value(0); | |
const prev = new Value(0); | |
return cond( | |
updating, | |
[set(tmp, sub(value, prev)), set(prev, value), tmp], | |
set(prev, 0) | |
); | |
} | |
// returns linear friction coeff. When `value` is 0 coeff is 1 (no friction), then | |
// it grows linearly until it reaches `MAX_FRICTION` when `value` is equal | |
// to `MAX_VALUE` | |
function friction(value) { | |
const MAX_FRICTION = 5; | |
const MAX_VALUE = 100; | |
return max( | |
1, | |
min(MAX_FRICTION, add(1, multiply(value, (MAX_FRICTION - 1) / MAX_VALUE))) | |
); | |
} | |
function speed(value) { | |
const clock = new Clock(); | |
const dt = diff(clock); | |
return cond(lessThan(dt, 1), 0, multiply(1000, divide(diff(value), dt))); | |
} | |
const MIN_SCALE = 1; | |
const MAX_SCALE = 5; | |
function scaleRest(value) { | |
return cond( | |
lessThan(value, MIN_SCALE), | |
MIN_SCALE, | |
cond(lessThan(MAX_SCALE, value), MAX_SCALE, value) | |
); | |
} | |
function scaleFriction(value, rest, delta) { | |
const MAX_FRICTION = 20; | |
const MAX_VALUE = 0.5; | |
const res = multiply(value, delta); | |
const howFar = abs(sub(rest, value)); | |
const friction = max( | |
1, | |
min(MAX_FRICTION, add(1, multiply(howFar, (MAX_FRICTION - 1) / MAX_VALUE))) | |
); | |
return cond( | |
lessThan(0, howFar), | |
multiply(value, add(1, divide(add(delta, -1), friction))), | |
res | |
); | |
} | |
function runTiming(clock, value, dest, startStopClock = true) { | |
const state = { | |
finished: new Value(0), | |
position: new Value(0), | |
frameTime: new Value(0), | |
time: new Value(0), | |
}; | |
const config = { | |
toValue: new Value(0), | |
duration: 300, | |
easing: Easing.inOut(Easing.cubic), | |
}; | |
return [ | |
cond(clockRunning(clock), 0, [ | |
set(state.finished, 0), | |
set(state.frameTime, 0), | |
set(state.time, 0), | |
set(state.position, value), | |
set(config.toValue, dest), | |
startStopClock && startClock(clock), | |
]), | |
timing(clock, state, config), | |
cond(state.finished, startStopClock && stopClock(clock)), | |
state.position, | |
]; | |
} | |
function runDecay(clock, value, velocity) { | |
const state = { | |
finished: new Value(0), | |
velocity: new Value(0), | |
position: new Value(0), | |
time: new Value(0), | |
}; | |
const config = { deceleration: 0.99 }; | |
return [ | |
cond(clockRunning(clock), 0, [ | |
set(state.finished, 0), | |
set(state.velocity, velocity), | |
set(state.position, value), | |
set(state.time, 0), | |
startClock(clock), | |
]), | |
set(state.position, value), | |
decay(clock, state, config), | |
cond(state.finished, stopClock(clock)), | |
state.position, | |
]; | |
} | |
function bouncyPinch( | |
value, | |
gesture, | |
gestureActive, | |
focalX, | |
displacementX, | |
focalY, | |
displacementY | |
) { | |
const clock = new Clock(); | |
const delta = scaleDiff(gesture); | |
const rest = scaleRest(value); | |
const focalXRest = cond( | |
lessThan(value, 1), | |
0, | |
sub(displacementX, multiply(focalX, add(-1, divide(rest, value)))) | |
); | |
const focalYRest = cond( | |
lessThan(value, 1), | |
0, | |
sub(displacementY, multiply(focalY, add(-1, divide(rest, value)))) | |
); | |
const nextScale = new Value(1); | |
return cond( | |
[delta, gestureActive], | |
[ | |
stopClock(clock), | |
set(nextScale, scaleFriction(value, rest, delta)), | |
set( | |
displacementX, | |
sub(displacementX, multiply(focalX, add(-1, divide(nextScale, value)))) | |
), | |
set( | |
displacementY, | |
sub(displacementY, multiply(focalY, add(-1, divide(nextScale, value)))) | |
), | |
nextScale, | |
], | |
cond( | |
or(clockRunning(clock), not(eq(rest, value))), | |
[ | |
set(displacementX, runTiming(clock, displacementX, focalXRest, false)), | |
set(displacementY, runTiming(clock, displacementY, focalYRest, false)), | |
runTiming(clock, value, rest), | |
], | |
value | |
) | |
); | |
} | |
function bouncy( | |
value, | |
gestureDiv, | |
gestureActive, | |
lowerBound, | |
upperBound, | |
friction | |
) { | |
const timingClock = new Clock(); | |
const decayClock = new Clock(); | |
const velocity = speed(value); | |
// did value go beyond the limits (lower, upper) | |
const isOutOfBounds = or( | |
lessThan(value, lowerBound), | |
lessThan(upperBound, value) | |
); | |
// position to snap to (upper or lower is beyond or the current value elsewhere) | |
const rest = cond( | |
lessThan(value, lowerBound), | |
lowerBound, | |
cond(lessThan(upperBound, value), upperBound, value) | |
); | |
// how much the value exceeds the bounds, this is used to calculate friction | |
const outOfBounds = abs(sub(rest, value)); | |
return cond( | |
[gestureDiv, velocity, gestureActive], | |
[ | |
stopClock(timingClock), | |
stopClock(decayClock), | |
add(value, divide(gestureDiv, friction(outOfBounds))), | |
], | |
cond( | |
or(clockRunning(timingClock), isOutOfBounds), | |
[stopClock(decayClock), runTiming(timingClock, value, rest)], | |
cond( | |
or(clockRunning(decayClock), lessThan(5, abs(velocity))), | |
runDecay(decayClock, value, velocity), | |
value | |
) | |
) | |
); | |
} | |
const WIDTH = 300; | |
const HEIGHT = 300; | |
class Viewer extends Component { | |
pinchRef = React.createRef(); | |
panRef = React.createRef(); | |
constructor(props) { | |
super(props); | |
// DECLARE TRANSX | |
const panTransX = new Value(0); | |
const panTransY = new Value(0); | |
// PINCH | |
const pinchScale = new Value(1); | |
const pinchFocalX = new Value(0); | |
const pinchFocalY = new Value(0); | |
const pinchState = new Value(-1); | |
this._onPinchEvent = event([ | |
{ | |
nativeEvent: { | |
state: pinchState, | |
scale: pinchScale, | |
focalX: pinchFocalX, | |
focalY: pinchFocalY, | |
}, | |
}, | |
]); | |
// SCALE | |
const scale = new Value(1); | |
const pinchActive = eq(pinchState, State.ACTIVE); | |
this._focalDisplacementX = new Value(0); | |
const relativeFocalX = sub( | |
pinchFocalX, | |
add(panTransX, this._focalDisplacementX) | |
); | |
this._focalDisplacementY = new Value(0); | |
const relativeFocalY = sub( | |
pinchFocalY, | |
add(panTransY, this._focalDisplacementY) | |
); | |
this._scale = set( | |
scale, | |
bouncyPinch( | |
scale, | |
pinchScale, | |
pinchActive, | |
relativeFocalX, | |
this._focalDisplacementX, | |
relativeFocalY, | |
this._focalDisplacementY | |
) | |
); | |
// PAN | |
const dragX = new Value(0); | |
const dragY = new Value(0); | |
const panState = new Value(-1); | |
this._onPanEvent = event([ | |
{ | |
nativeEvent: { | |
translationX: dragX, | |
translationY: dragY, | |
state: panState, | |
}, | |
}, | |
]); | |
const panActive = eq(panState, State.ACTIVE); | |
const panFriction = value => friction(value); | |
// X | |
const panUpX = cond( | |
lessThan(this._scale, 1), | |
0, | |
multiply(-1, this._focalDisplacementX) | |
); | |
const panLowX = add(panUpX, multiply(-WIDTH, add(max(1, this._scale), -1))); | |
this._panTransX = set( | |
panTransX, | |
bouncy( | |
panTransX, | |
dragDiff(dragX, panActive), | |
or(panActive, pinchActive), | |
panLowX, | |
panUpX, | |
panFriction | |
) | |
); | |
// Y | |
const panUpY = cond( | |
lessThan(this._scale, 1), | |
0, | |
multiply(-1, this._focalDisplacementY) | |
); | |
const panLowY = add( | |
panUpY, | |
multiply(-HEIGHT, add(max(1, this._scale), -1)) | |
); | |
this._panTransY = set( | |
panTransY, | |
bouncy( | |
panTransY, | |
dragDiff(dragY, panActive), | |
or(panActive, pinchActive), | |
panLowY, | |
panUpY, | |
panFriction | |
) | |
); | |
} | |
state = { | |
photos: Array(3) | |
.fill(null) | |
.map((r, key) => generatePhoto(0, 1, 0, 1)), | |
} | |
render() { | |
// The below two animated values makes it so that scale appears to be done | |
// from the top left corner of the image view instead of its center. This | |
// is required for the "scale focal point" math to work correctly | |
const scaleTopLeftFixX = divide(multiply(WIDTH, add(this._scale, -1)), 2); | |
const scaleTopLeftFixY = divide(multiply(HEIGHT, add(this._scale, -1)), 2); | |
return ( | |
<View style={styles.wrapper}> | |
<PinchGestureHandler | |
ref={this.pinchRef} | |
simultaneousHandlers={this.panRef} | |
onGestureEvent={this._onPinchEvent} | |
onHandlerStateChange={this._onPinchEvent}> | |
<Animated.View> | |
<PanGestureHandler | |
ref={this.panRef} | |
avgTouches | |
simultaneousHandlers={this.pinchRef} | |
onGestureEvent={this._onPanEvent} | |
onHandlerStateChange={this._onPanEvent}> | |
<Animated.View | |
style={[ | |
{ | |
transform: [ | |
{ translateX: this._panTransX }, | |
{ translateY: this._panTransY }, | |
{ translateX: this._focalDisplacementX }, | |
{ translateY: this._focalDisplacementY }, | |
{ translateX: scaleTopLeftFixX }, | |
{ translateY: scaleTopLeftFixY }, | |
{ scale: this._scale }, | |
], | |
}, | |
]} | |
> | |
<Animated.View | |
style={{ width: WIDTH, height: HEIGHT}} | |
> | |
<TapGestureHandler | |
style={{ | |
position: 'absolute', | |
left: 10, | |
top : 0, | |
}} | |
onHandlerStateChange={(event) => { | |
if (event.nativeEvent.state === State.ACTIVE) { | |
console.log('button 2 pressed, adding new photo') | |
const photos = this.state.photos.slice() | |
console.log(this.state.photos.length) | |
// Corners of the current viewfinder | |
// (panTranX + focalDisplacementX) / scale / -1 | |
const leftX = divide(add(this._panTransX, this._focalDisplacementX), -1, this._scale ) | |
const topY = divide(add(this._panTransY, this._focalDisplacementY), -1, this._scale ) | |
const rightX = add(leftX, this._scale) | |
const bottomY = add(topY, this._scale) | |
newPhoto = generatePhoto( | |
leftX, | |
rightX, | |
topY, | |
bottomY, | |
) | |
photos.push(newPhoto) | |
this.setState({ photos: photos }) | |
} | |
}} | |
> | |
<Image | |
style={{ width: 300, height: 300 }} | |
source={this.props.source} | |
/> | |
</TapGestureHandler> | |
{this.state.photos && | |
this.state | |
.photos | |
.map((r, key) => ( | |
<Animated.View | |
style={{ | |
position: 'absolute', | |
left: r.x * WIDTH , | |
top : r.y * HEIGHT, | |
width: WIDTH/4, | |
height: HEIGHT/4, | |
backgroundColor: 'red', | |
opacity: 0.5, | |
}} | |
key={ key } | |
/> | |
))} | |
</Animated.View> | |
</Animated.View> | |
</PanGestureHandler> | |
</Animated.View> | |
</PinchGestureHandler> | |
</View> | |
); | |
} | |
} | |
export default class Example extends Component { | |
static navigationOptions = { | |
title: 'Image Viewer Example', | |
}; | |
render() { | |
return ( | |
<View style={styles.container}> | |
<Viewer source={require('./grid.png')} /> | |
</View> | |
); | |
} | |
} | |
const styles = StyleSheet.create({ | |
container: { | |
flex: 1, | |
justifyContent: 'center', | |
alignItems: 'center', | |
backgroundColor: '#F5FCFF', | |
}, | |
wrapper: { | |
borderColor: 'green', | |
borderWidth: 2, | |
overflow: 'hidden', | |
}, | |
image: { | |
width: 300, | |
height: 300, | |
backgroundColor: 'black', | |
}, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment