462 lines
10 KiB
JavaScript
462 lines
10 KiB
JavaScript
import React from 'react';
|
|
import { StyleSheet, View } from 'react-native';
|
|
import PropTypes from 'prop-types';
|
|
import {
|
|
PanGestureHandler,
|
|
State,
|
|
PinchGestureHandler
|
|
} from 'react-native-gesture-handler';
|
|
import Animated, { Easing } from 'react-native-reanimated';
|
|
import { ImageComponent } from './ImageComponent';
|
|
import { themes } from '../../constants/colors';
|
|
|
|
const styles = StyleSheet.create({
|
|
flex: {
|
|
flex: 1
|
|
},
|
|
image: {
|
|
backgroundColor: 'transparent'
|
|
}
|
|
});
|
|
|
|
const {
|
|
set,
|
|
cond,
|
|
eq,
|
|
or,
|
|
add,
|
|
sub,
|
|
min,
|
|
max,
|
|
multiply,
|
|
divide,
|
|
lessThan,
|
|
decay,
|
|
timing,
|
|
diff,
|
|
not,
|
|
abs,
|
|
startClock,
|
|
stopClock,
|
|
clockRunning,
|
|
Value,
|
|
Clock,
|
|
event
|
|
} = Animated;
|
|
|
|
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 = 2;
|
|
|
|
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 f = 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), f))),
|
|
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,
|
|
f
|
|
) {
|
|
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, f(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 Image extends React.PureComponent {
|
|
static propTypes = {
|
|
imageComponentType: PropTypes.string
|
|
}
|
|
|
|
render() {
|
|
const { imageComponentType } = this.props;
|
|
|
|
const Component = ImageComponent(imageComponentType);
|
|
|
|
return <Component {...this.props} />;
|
|
}
|
|
}
|
|
const AnimatedImage = Animated.createAnimatedComponent(Image);
|
|
|
|
// it was picked from https://github.com/software-mansion/react-native-reanimated/tree/master/Example/imageViewer
|
|
// and changed to use FastImage animated component
|
|
export class ImageViewer extends React.Component {
|
|
static propTypes = {
|
|
uri: PropTypes.string,
|
|
width: PropTypes.number,
|
|
height: PropTypes.number,
|
|
theme: PropTypes.string,
|
|
imageComponentType: PropTypes.string
|
|
}
|
|
|
|
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
|
|
)
|
|
);
|
|
}
|
|
|
|
pinchRef = React.createRef();
|
|
|
|
panRef = React.createRef();
|
|
|
|
render() {
|
|
const {
|
|
uri, width, height, imageComponentType, theme, ...props
|
|
} = this.props;
|
|
|
|
// 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);
|
|
const backgroundColor = themes[theme].previewBackground;
|
|
|
|
return (
|
|
<View style={[styles.flex, { width, height, backgroundColor }]}>
|
|
<PinchGestureHandler
|
|
ref={this.pinchRef}
|
|
simultaneousHandlers={this.panRef}
|
|
onGestureEvent={this._onPinchEvent}
|
|
onHandlerStateChange={this._onPinchEvent}
|
|
>
|
|
<Animated.View>
|
|
<PanGestureHandler
|
|
ref={this.panRef}
|
|
minDist={10}
|
|
avgTouches
|
|
simultaneousHandlers={this.pinchRef}
|
|
onGestureEvent={this._onPanEvent}
|
|
onHandlerStateChange={this._onPanEvent}
|
|
>
|
|
<AnimatedImage
|
|
style={[
|
|
styles.image,
|
|
{
|
|
width,
|
|
height: '100%'
|
|
},
|
|
{
|
|
transform: [
|
|
{ translateX: this._panTransX },
|
|
{ translateY: this._panTransY },
|
|
{ translateX: this._focalDisplacementX },
|
|
{ translateY: this._focalDisplacementY },
|
|
{ translateX: scaleTopLeftFixX },
|
|
{ translateY: scaleTopLeftFixY },
|
|
{ scale: this._scale }
|
|
]
|
|
}
|
|
]}
|
|
imageComponentType={imageComponentType}
|
|
resizeMode='contain'
|
|
source={{ uri }}
|
|
{...props}
|
|
/>
|
|
</PanGestureHandler>
|
|
</Animated.View>
|
|
</PinchGestureHandler>
|
|
</View>
|
|
);
|
|
}
|
|
}
|