UNPKG

expo-pixel-perfect

Version:

Nearest-Neighbor scaling for React Native and Expo

146 lines 5.31 kB
import { requireNativeView } from "expo"; import { Asset } from "expo-asset"; import { StyleSheet, View, Platform } from "react-native"; import { useEffect, useState } from "react"; const NativeView = requireNativeView("ExpoPixelPerfect"); const calculateScale = (scale, dimensions) => { if (!scale) return 1; if (scale.scale) return scale.scale; if (scale.targetHeight) { if (scale.targetWidth) { return Math.min(scale.targetWidth / dimensions.width, scale.targetHeight / dimensions.height); } return scale.targetHeight / dimensions.height; } else if (scale.targetWidth) { return scale.targetWidth / dimensions.width; } return 1; }; /** * A component for rendering pixel art with perfect scaling. * Ensures crisp, sharp pixels without blurring or anti-aliasing. * * @example * <ExpoPixelPerfectView * source={require('./assets/character.png')} * scale={4} * android_renderMode="software" * ios_renderMode="software" * /> */ export default function ExpoPixelPerfectView({ source, style, scale, onError, onLoad, fallback, loadingComponent, android_renderMode, ios_renderMode, }) { const [state, setState] = useState({ status: "loading", width: null, height: null, }); useEffect(() => { let mounted = true; async function loadAsset() { try { // Handle base64 source directly if (typeof source === "object" && "base64" in source) { if (mounted) { setState({ status: "loaded", base64Data: source.base64, width: source.width, height: source.height, }); onLoad?.(); } return; } // Original asset loading logic for URI and require() sources let asset = null; if (typeof source === "number") { if (Asset.fromModule(source).localUri) { asset = Asset.fromModule(source); } else { asset = await Asset.fromModule(source).downloadAsync(); } } else if ("uri" in source && typeof source.uri === "string") { asset = await Asset.fromURI(source.uri).downloadAsync(); } if (!mounted) return; if (!asset) { throw new Error("Asset not found"); } setState({ status: "loaded", localUri: asset.localUri, width: asset.width, height: asset.height, }); onLoad?.(); } catch (error) { if (!mounted) return; const assetError = error instanceof Error ? error : new Error("Failed to load asset"); setState((prev) => ({ ...prev, status: "error", error: assetError, })); onError?.(assetError); } } setState((prev) => ({ ...prev, status: "loading" })); loadAsset(); return () => { mounted = false; }; }, [source, onError, onLoad]); if (state.status === "loading") { return (<View style={[ styles.container, style, { width: scale?.targetWidth ?? scale?.targetHeight, height: scale?.targetHeight ?? scale?.targetWidth, }, ]}> {loadingComponent ?? null} </View>); } if (state.status === "error" || (!state.localUri && !state.base64Data)) { return fallback ? (<View style={[styles.container, style]}>{fallback}</View>) : null; } if (state.width === null || state.height === null) { return (<View style={[ styles.container, style, { width: scale?.targetWidth ?? scale?.targetHeight, height: scale?.targetHeight ?? scale?.targetWidth, }, ]}> {loadingComponent ?? null} </View>); } const targetScale = Math.max(calculateScale(scale, { width: state.width, height: state.height, }), 1); const combinedStyle = StyleSheet.compose({ width: state.width * targetScale, height: state.height * targetScale }, style); // Pass platform-specific props to the native view const platformProps = Platform.select({ android: { android_renderMode }, ios: { ios_renderMode }, }); return (<NativeView path={state.localUri || undefined} base64={state.base64Data} scale={targetScale} style={combinedStyle} {...platformProps}/>); } const styles = StyleSheet.create({ container: { justifyContent: "center", alignItems: "center", }, }); //# sourceMappingURL=ExpoPixelPerfectView.js.map