react-native-animated-glow
Version:
A performant, highly-customizable animated glow effect for React Native, powered by Skia and Reanimated.
175 lines (174 loc) • 8.97 kB
JavaScript
// src/AnimatedGlow.tsx
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const react_1 = __importStar(require("react"));
const react_native_1 = require("react-native");
const react_native_reanimated_1 = require("react-native-reanimated");
const react_native_gesture_handler_1 = require("react-native-gesture-handler");
const LazyUnifiedSkiaGlow_1 = require("./animated-glow/LazyUnifiedSkiaGlow");
const SkiaWebLoader_1 = require("./animated-glow/SkiaWebLoader");
const isObject = (item) => (item && typeof item === 'object' && !Array.isArray(item));
const mergeDeep = (target, source) => {
const output = { ...target };
if (isObject(target) && isObject(source)) {
Object.keys(source).forEach(key => {
if (key === 'glowLayers' && Array.isArray(source[key]) && Array.isArray(target[key])) {
const mergedLayers = target[key].map((layer, index) => source[key][index] ? { ...layer, ...source[key][index] } : layer);
if (source[key].length > target[key].length) {
mergedLayers.push(...source[key].slice(target[key].length));
}
output[key] = mergedLayers;
}
else if (isObject(source[key]) && key in target && isObject(target[key])) {
output[key] = mergeDeep(target[key], source[key]);
}
else {
output[key] = source[key];
}
});
}
return output;
};
const AnimatedGlow = (props) => {
const { preset = {}, states: statesProp, initialState = 'default', children, style, isVisible = true, ...overrideProps } = props;
const [isSkiaReady, setIsSkiaReady] = (0, react_1.useState)(SkiaWebLoader_1.skiaWebState.status === 'ready');
const [layout, setLayout] = (0, react_1.useState)({ width: 0, height: 0 });
const [hasLaidOut, setHasLaidOut] = (0, react_1.useState)(false);
const [activeState, setActiveState] = (0, react_1.useState)(initialState);
const prevActiveState = (0, react_1.useRef)(initialState);
const skiaOpacity = (0, react_native_reanimated_1.useSharedValue)(0);
const animationProgress = (0, react_native_reanimated_1.useSharedValue)(0);
const fromConfigSV = (0, react_native_reanimated_1.useSharedValue)({});
const toConfigSV = (0, react_native_reanimated_1.useSharedValue)({});
(0, react_1.useEffect)(() => {
if (SkiaWebLoader_1.skiaWebState.status === 'ready') {
if (!isSkiaReady)
setIsSkiaReady(true);
return;
}
const onReady = () => setIsSkiaReady(true);
SkiaWebLoader_1.skiaWebState.subscribers.add(onReady);
(0, SkiaWebLoader_1.ensureSkiaWebLoaded)();
return () => { SkiaWebLoader_1.skiaWebState.subscribers.delete(onReady); };
}, [isSkiaReady]);
const states = (0, react_1.useMemo)(() => statesProp || preset.states || [], [statesProp, preset.states]);
const targetConfig = (0, react_1.useMemo)(() => {
const allStates = statesProp || preset.states || [];
const defaultState = allStates.find(s => s.name === 'default')?.preset || {};
const legacyBase = { ...preset, ...overrideProps };
delete legacyBase.metadata;
delete legacyBase.states;
const baseConfig = mergeDeep(defaultState, legacyBase);
const stateOverride = allStates.find(s => s.name === activeState)?.preset || {};
return mergeDeep(baseConfig, stateOverride);
}, [preset, overrideProps, statesProp, activeState]);
(0, react_1.useEffect)(() => {
if (animationProgress.value === 0 && !fromConfigSV.value.cornerRadius) {
fromConfigSV.value = targetConfig;
toConfigSV.value = targetConfig;
animationProgress.value = 1;
prevActiveState.current = activeState;
return;
}
let transition = states.find(s => s.name === activeState)?.transition ?? 0;
if (activeState === 'default' && prevActiveState.current !== 'default') {
transition = states.find(s => s.name === prevActiveState.current)?.transition ?? transition;
}
fromConfigSV.value = toConfigSV.value;
toConfigSV.value = targetConfig;
if (transition > 0) {
animationProgress.value = 0;
animationProgress.value = (0, react_native_reanimated_1.withTiming)(1, { duration: transition, easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.quad) });
}
else {
animationProgress.value = 1;
}
prevActiveState.current = activeState;
}, [targetConfig, activeState, states]);
const pressGesture = react_native_gesture_handler_1.Gesture.LongPress()
.minDuration(1)
.onStart(() => {
if (states.some(s => s.name === 'press')) {
setActiveState('press');
}
})
.onEnd(() => {
setActiveState(current => (current === 'press' ? initialState : current));
});
const hoverGesture = react_native_gesture_handler_1.Gesture.Hover()
.onStart(() => {
if (states.some(s => s.name === 'hover')) {
setActiveState('hover');
}
})
.onEnd(() => {
setActiveState(current => (current === 'hover' ? initialState : current));
});
const gesture = react_native_gesture_handler_1.Gesture.Race(react_native_1.Platform.OS === 'web' ? hoverGesture : react_native_gesture_handler_1.Gesture.Manual(), pressGesture);
const { cornerRadius = 10, outlineWidth = 2, borderColor = 'white', backgroundColor } = targetConfig;
const hasAnimatedBorder = Array.isArray(borderColor) && borderColor.length > 1;
const hasGlowLayers = (targetConfig.glowLayers?.length ?? 0) > 0;
const useSkiaRenderer = (0, react_1.useMemo)(() => hasGlowLayers || hasAnimatedBorder, [hasGlowLayers, hasAnimatedBorder]);
const wrapperStyle = (0, react_1.useMemo)(() => ({
backgroundColor: useSkiaRenderer ? 'transparent' : backgroundColor,
borderWidth: useSkiaRenderer ? 0 : outlineWidth,
borderColor: useSkiaRenderer ? 'transparent' : (Array.isArray(borderColor) ? borderColor[0] : borderColor),
borderRadius: cornerRadius,
overflow: 'hidden',
}), [useSkiaRenderer, outlineWidth, borderColor, cornerRadius, backgroundColor]);
const shouldRenderSkia = useSkiaRenderer && hasLaidOut && isVisible && isSkiaReady;
(0, react_1.useEffect)(() => {
skiaOpacity.value = (0, react_native_reanimated_1.withTiming)(shouldRenderSkia ? 1 : 0, { duration: 300 });
}, [shouldRenderSkia]);
return (<react_native_gesture_handler_1.GestureDetector gesture={gesture}>
<react_native_1.View style={[style]} onLayout={(e) => {
const l = e.nativeEvent.layout;
if (l.width !== layout.width || l.height !== layout.height) {
setLayout({ width: l.width, height: l.height });
}
if (!hasLaidOut)
setHasLaidOut(true);
}}>
{shouldRenderSkia && (<react_1.Suspense fallback={null}>
<LazyUnifiedSkiaGlow_1.LazyUnifiedSkiaGlow layout={layout} masterOpacity={skiaOpacity} progress={animationProgress} fromConfig={fromConfigSV} toConfig={toConfigSV}/>
</react_1.Suspense>)}
<react_native_1.View style={wrapperStyle}>
{children}
</react_native_1.View>
</react_native_1.View>
</react_native_gesture_handler_1.GestureDetector>);
};
exports.default = AnimatedGlow;
;