react-native-lucky-wheel
Version:
Lucky Wheel for React Native.
432 lines (380 loc) • 17 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = _interopRequireWildcard(require("react"));
var _reactNative = require("react-native");
var d3Shape = _interopRequireWildcard(require("d3-shape"));
var _randomcolor = _interopRequireDefault(require("randomcolor"));
var _reactNativeSvg = _interopRequireWildcard(require("react-native-svg"));
var _Knob = _interopRequireDefault(require("./Knob"));
var _utils = _interopRequireDefault(require("../utils"));
var _index = require("../index");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
const AnimatedSvg = _reactNative.Animated.createAnimatedComponent(_reactNativeSvg.default);
const {
width
} = _reactNative.Dimensions.get('screen');
const ONE_TURN = 360;
const LuckyWheel = /*#__PURE__*/(0, _react.forwardRef)((props, ref) => {
const rotate = (0, _react.useRef)(new _reactNative.Animated.Value(0)).current;
const spinVelocity = (0, _react.useRef)(new _reactNative.Animated.Value(0)).current;
const containerRef = (0, _react.useRef)(null);
const [isSpinning, setIsSpinning] = (0, _react.useState)(false);
const [spinCount, setSpinCount] = (0, _react.useState)(1);
const [winnerLastOffset, setWinnerLastOffset] = (0, _react.useState)(0);
const [px, setPx] = (0, _react.useState)(0);
const [py, setPy] = (0, _react.useState)(0);
const rotateInterpolated = rotate.interpolate({
inputRange: [-ONE_TURN, 0, ONE_TURN],
outputRange: [`-${ONE_TURN}deg`, '0deg', `${ONE_TURN}deg`]
});
const outerRadius = (0, _react.useMemo)(() => props.size / 2 - props.outerRadius, [props.size, props.outerRadius]);
const DURATION_IN_MS = (0, _react.useMemo)(() => props.duration * 1000, [props.duration]);
const SLICE_COUNT = (0, _react.useMemo)(() => props.slices.length, [props.slices]);
const SLICE_ANGLE = (0, _react.useMemo)(() => ONE_TURN / SLICE_COUNT, [SLICE_COUNT]);
const SLICE_ANGLE_CENTER = (0, _react.useMemo)(() => SLICE_ANGLE / 2, [SLICE_ANGLE]);
const SLICE_PAYLOAD = (0, _react.useMemo)(() => {
const data = Array.from({
length: SLICE_COUNT
}).fill(1);
const arcs = d3Shape.pie()(data);
const instance = d3Shape.arc().padAngle(props.padAngle).outerRadius(outerRadius).innerRadius(props.innerRadius);
const colors = (0, _randomcolor.default)({ ...props.backgroundColorOptions,
count: SLICE_COUNT
});
return arcs.map((arc, index) => {
return {
path: instance(arc),
color: props.slices[index].color ?? colors[index % colors.length],
text: props.slices[index].text,
textStyle: props.slices[index].textStyle,
centroid: instance.centroid(arc)
};
});
}, [props.padAngle, props.innerRadius, outerRadius, props.backgroundColorOptions, props.slices, SLICE_COUNT]);
const knobAnim = _reactNative.Animated.modulo(_reactNative.Animated.divide(_reactNative.Animated.modulo(_reactNative.Animated.subtract(rotate, SLICE_ANGLE_CENTER), ONE_TURN), new _reactNative.Animated.Value(SLICE_ANGLE)), 1);
const knobInterpolated = knobAnim.interpolate({
inputRange: [-1, -0.5, -0.0001, 0.0001, 0.5, 1],
outputRange: ['0deg', '0deg', '35deg', '-35deg', '0deg', '0deg']
});
const startSpinning = (0, _react.useCallback)(() => {
if (!isSpinning) setIsSpinning(true);
const isGestureSpinning = spinVelocity._value !== 0;
const isSpinningValid = Math.abs(spinVelocity._value) >= 1;
const velocity = isGestureSpinning && props.enablePhysics && isSpinningValid ? Math.round(Math.abs(spinVelocity._value)) : 1;
const WINNER_INDEX = props.winnerIndex ?? Math.floor(Math.random() * SLICE_COUNT);
const WINNER = props.slices[WINNER_INDEX];
const WINNER_ANGLE = WINNER_INDEX * (ONE_TURN / SLICE_COUNT);
const WINNER_ANGLE_OFFSET = _utils.default.randomNumber(-SLICE_ANGLE_CENTER + SLICE_ANGLE / 3, SLICE_ANGLE_CENTER - SLICE_ANGLE / 3);
const EXTRA_SPIN_DEGREE = ONE_TURN * props.duration * spinCount * velocity;
const TARGET_ANGLE = ONE_TURN - WINNER_ANGLE + EXTRA_SPIN_DEGREE - winnerLastOffset + WINNER_ANGLE_OFFSET;
const isEndlessSpinning = props.waitWinner && !props.winnerIndex;
if (isEndlessSpinning) {
_reactNative.Animated.loop(_reactNative.Animated.timing(rotate, {
toValue: EXTRA_SPIN_DEGREE * 4,
duration: DURATION_IN_MS,
useNativeDriver: true,
easing: _reactNative.Easing.linear
})).start();
} else {
_reactNative.Animated.timing(rotate, {
toValue: TARGET_ANGLE,
duration: DURATION_IN_MS,
easing: props.easing === 'out' ? _reactNative.Easing.out(_reactNative.Easing.cubic) : _reactNative.Easing.inOut(_reactNative.Easing.cubic),
useNativeDriver: true
}).start(() => {
setIsSpinning(false);
spinVelocity.setValue(0);
if (props.onSpinningEnd) props.onSpinningEnd(WINNER);
});
}
if (props.onSpinningStart) props.onSpinningStart();
setWinnerLastOffset(WINNER_ANGLE_OFFSET);
setSpinCount(spinCount + 1);
}, [DURATION_IN_MS, SLICE_ANGLE, SLICE_ANGLE_CENTER, SLICE_COUNT, isSpinning, props, rotate, spinCount, spinVelocity, winnerLastOffset]);
(0, _react.useEffect)(() => {
if (isSpinning && props.waitWinner && props.winnerIndex) {
rotate.resetAnimation(() => {
startSpinning();
});
}
}, [isSpinning, props.waitWinner, props.winnerIndex, rotate, startSpinning]);
(0, _react.useEffect)(() => {
if (props.onKnobTick) {
knobAnim.addListener(_ref => {
let {
value
} = _ref;
if (value > 0.7) {
if (props.onKnobTick) props.onKnobTick();
}
});
} else {
knobAnim.removeAllListeners();
}
}, [knobAnim, props]);
(0, _react.useImperativeHandle)(ref, () => ({
start: () => {
startSpinning();
},
stop: () => {
rotate.stopAnimation();
},
reset: () => {
rotate.resetAnimation();
}
}));
const _panResponder = _reactNative.PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onPanResponderMove: (_, gestureState) => {
if (!props.enableGesture || isSpinning) {
return;
}
const mappedX = gestureState.moveX - px;
const mappedY = gestureState.moveY - py;
const x = mappedX < 0 ? 0 : mappedX > 300 ? props.size : mappedX;
const y = mappedY < 0 ? 0 : mappedY > 300 ? props.size : mappedY;
const top = y < props.size / 2;
const bottom = y > props.size / 2;
const left = x < props.size / 2;
const right = x > props.size / 2; // console.log({x, y, top, bottom, left, right});
const isMoveRight = gestureState.vx > 0;
const isMoveLeft = gestureState.vx < 0;
const isMoveUp = gestureState.vy < 0;
const isMoveBottom = gestureState.vy > 0; // console.log({isMoveRight, isMoveLeft, isMoveUp, isMoveBottom});
const isSpinRight = top && isMoveRight || bottom && isMoveLeft || top && right && isMoveBottom || bottom && left && isMoveUp;
const isSpinLeft = (top && isMoveLeft || bottom && isMoveRight || top && left && isMoveBottom || bottom && right && isMoveUp) && !isSpinRight; // console.log({isSpinRight, isSpinLeft});
const isSingleTouch = gestureState.numberActiveTouches === 1; // const isRightMove = gestureState.vx < 0;
const isClockwiseEnabled = props.gestureType === _index.GestureTypes.CLOCKWISE || props.gestureType === _index.GestureTypes.MULTIDIRECTIONAL;
const isAntiClockwiseEnabled = props.gestureType === _index.GestureTypes.ANTI_CLOCKWISE || props.gestureType === _index.GestureTypes.MULTIDIRECTIONAL; // set spin velocity
if (isSpinRight && isClockwiseEnabled || isSpinLeft && isAntiClockwiseEnabled) {
if (rotate._value === 0) {
spinVelocity.setValue(gestureState.vx);
} else {
spinVelocity.setValue((gestureState.vx + spinVelocity._value) / 2);
}
}
const slowDivider = 15;
if (isClockwiseEnabled && isSingleTouch && isSpinRight) {
rotate.setValue((rotate._value + x / slowDivider) % 360);
}
if (isAntiClockwiseEnabled && isSingleTouch && isSpinLeft) {
rotate.setValue((rotate._value - x / slowDivider) % 360);
}
},
onPanResponderRelease: () => {
if (!props.enableGesture || isSpinning) {
return;
}
const isVelocityFastEnough = Math.abs(spinVelocity._value) > props.minimumSpinVelocity;
if (isVelocityFastEnough) {
startSpinning();
}
}
});
const _renderKnob = () => {
return /*#__PURE__*/_react.default.createElement(_reactNative.Animated.View, {
style: [styles.knob, {
transform: [{
rotate: knobInterpolated
}]
}]
}, props.customKnob ? props.customKnob({
size: props.knobSize,
color: props.knobColor
}) : /*#__PURE__*/_react.default.createElement(_Knob.default, {
size: props.knobSize,
color: props.knobColor
}));
};
const _renderText = params => {
var _params$payload$textS2, _props$textStyle2;
if (props.source) return null;
if (props.customText) {
return props.customText(params);
}
if (props.textAngle === _index.TextAngles.VERTICAL) {
var _params$payload$textS, _props$textStyle;
return /*#__PURE__*/_react.default.createElement(_reactNativeSvg.G, {
rotation: params.i * ONE_TURN / SLICE_COUNT + SLICE_ANGLE_CENTER + 90,
origin: `${params.x}, ${params.y}`
}, /*#__PURE__*/_react.default.createElement(_reactNativeSvg.Text, _extends({
x: params.x - props.size / 7,
y: params.y,
fill: ((_params$payload$textS = params.payload.textStyle) === null || _params$payload$textS === void 0 ? void 0 : _params$payload$textS.color) ?? ((_props$textStyle = props.textStyle) === null || _props$textStyle === void 0 ? void 0 : _props$textStyle.color) ?? styles.text.color
}, props.textStyle, params.payload.textStyle), params.payload.text));
}
return /*#__PURE__*/_react.default.createElement(_reactNativeSvg.G, {
rotation: params.i * ONE_TURN / SLICE_COUNT + SLICE_ANGLE_CENTER,
origin: `${params.x}, ${params.y}`
}, /*#__PURE__*/_react.default.createElement(_reactNativeSvg.Text, _extends({
x: params.x - params.payload.text.length * 5,
y: params.y - props.size / 8,
fill: ((_params$payload$textS2 = params.payload.textStyle) === null || _params$payload$textS2 === void 0 ? void 0 : _params$payload$textS2.color) ?? ((_props$textStyle2 = props.textStyle) === null || _props$textStyle2 === void 0 ? void 0 : _props$textStyle2.color) ?? styles.text.color
}, props.textStyle, params.payload.textStyle), params.payload.text));
};
const _renderOuterDots = (x, y, i) => {
if (!props.enableOuterDots) {
return null;
}
return /*#__PURE__*/_react.default.createElement(_reactNativeSvg.Circle, {
origin: `${x}, ${y}`,
rotation: i * ONE_TURN / SLICE_COUNT + SLICE_ANGLE_CENTER,
cx: x + outerRadius + 2.5,
cy: y + SLICE_ANGLE - SLICE_COUNT * 2 + props.innerRadius / 2 + props.innerRadius / 100,
r: "4",
fill: props.dotColor
});
};
const _renderSlice = (path, color) => {
return /*#__PURE__*/_react.default.createElement(_reactNativeSvg.Path, {
d: path,
strokeWidth: 2,
fill: color
});
};
const _renderCircle = () => {
if (!props.enableInnerShadow) return false;
return /*#__PURE__*/_react.default.createElement(_reactNative.Animated.View, {
style: { ...styles.wheel,
...styles.circle,
width: props.size - (props.size - outerRadius * 2),
height: props.size - (props.size - outerRadius * 2),
borderRadius: props.size / 2
}
});
};
const _renderWheel = () => {
if (props.source) {
return /*#__PURE__*/_react.default.createElement(_reactNative.Animated.Image, _extends({
ref: containerRef,
onLayout: () => {
var _containerRef$current;
containerRef === null || containerRef === void 0 ? void 0 : (_containerRef$current = containerRef.current) === null || _containerRef$current === void 0 ? void 0 : _containerRef$current.measure((_fx, _fy, _wheelWidth, _wheelHeight, pxSize, pySize) => {
setPx(pxSize);
setPy(pySize);
});
},
style: { ...styles.wheel,
width: props.size,
height: props.size,
transform: [{
rotate: rotateInterpolated
}]
},
source: props.source
}, _panResponder.panHandlers));
}
return /*#__PURE__*/_react.default.createElement(_reactNative.Animated.View, _extends({
ref: containerRef,
onLayout: () => {
if (containerRef.current) {
containerRef.current.measure((_fx, _fy, _wheelWidth, _wheelHeight, pxSize, pySize) => {
setPx(pxSize);
setPy(pySize);
});
}
},
style: { ...styles.wheel,
transform: [{
rotate: rotateInterpolated
}],
backgroundColor: props.backgroundColor,
width: props.size,
height: props.size,
borderRadius: props.size / 2
}
}, _panResponder.panHandlers), /*#__PURE__*/_react.default.createElement(AnimatedSvg, {
style: {
transform: [{
rotate: `-${SLICE_ANGLE_CENTER}deg`
}]
}
}, /*#__PURE__*/_react.default.createElement(_reactNativeSvg.G, {
y: props.size / 2,
x: props.size / 2
}, SLICE_PAYLOAD.map((payload, i) => {
const [x, y] = payload.centroid;
return /*#__PURE__*/_react.default.createElement(_reactNativeSvg.G, {
key: `arc-${i}`
}, _renderSlice(payload.path, payload.color), _renderText({
x,
y,
payload,
i
}), _renderOuterDots(x, y, i));
}))), _renderCircle());
};
return /*#__PURE__*/_react.default.createElement(_reactNative.View, {
style: styles.container
}, _renderKnob(), /*#__PURE__*/_react.default.createElement(_reactNative.View, {
style: {
transform: [{
rotate: `${props.offset}deg`
}]
}
}, _renderWheel()));
});
const defaultProps = {
duration: 4,
innerRadius: 30,
outerRadius: 13,
padAngle: 0.01,
backgroundColor: '#FFF',
size: width - 40,
textAngle: _index.TextAngles.VERTICAL,
backgroundColorOptions: {
luminosity: 'dark',
hue: 'random'
},
knobSize: 30,
knobColor: '#FF0000',
textStyle: {},
easing: _index.EasingTypes.OUT,
dotColor: '#000',
minimumSpinVelocity: 1,
enableGesture: false,
enableOuterDots: true,
enablePhysics: false,
gestureType: _index.GestureTypes.CLOCKWISE,
offset: 0,
waitWinner: false,
enableInnerShadow: true
};
LuckyWheel.defaultProps = defaultProps;
const styles = _reactNative.StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center',
zIndex: 1
},
wheel: {
justifyContent: 'center',
alignItems: 'center'
},
circle: {
position: 'absolute',
backgroundColor: 'transparent',
borderColor: '#000',
borderWidth: 15,
opacity: 0.3
},
knob: {
justifyContent: 'flex-end',
zIndex: 1
},
text: {
fontSize: 20,
fontWeight: 'bold',
color: '#FFF'
}
});
var _default = LuckyWheel;
exports.default = _default;
//# sourceMappingURL=LuckyWheel.js.map