├── .gitignore ├── App.tsx ├── README.md ├── app.json ├── assets ├── adaptive-icon.png ├── favicon.png ├── icon.png └── splash.png ├── babel.config.js ├── bubble.png ├── package.json ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | 19 | # Metro 20 | .metro-health-check* 21 | 22 | # debug 23 | npm-debug.* 24 | yarn-debug.* 25 | yarn-error.* 26 | 27 | # macOS 28 | .DS_Store 29 | *.pem 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import { Image } from 'expo-image'; 2 | import { Dimensions, StyleSheet, View } from 'react-native'; 3 | import Animated, { useAnimatedScrollHandler, useDerivedValue, useSharedValue } from 'react-native-reanimated'; 4 | import { Canvas, Image as SkiaImage, useImage, Group, Rect, Paint, DisplacementMap, RadialGradient, vec, LinearGradient, ImageShader } from '@shopify/react-native-skia'; 5 | 6 | const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); 7 | 8 | const images = [ 9 | { 10 | url: 'https://images.unsplash.com/photo-1433086966358-54859d0ed716?q=80&w=2187&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', 11 | title: 'Image 1', 12 | }, 13 | { 14 | url: 'https://plus.unsplash.com/premium_photo-1675368244123-082a84cf3072?q=80&w=2300&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', 15 | title: 'Image 2', 16 | }, 17 | { 18 | url: 'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?q=80&w=2174&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', 19 | title: 'Image 3', 20 | }, 21 | { 22 | url: 'https://images.unsplash.com/photo-1426604966848-d7adac402bff?q=80&w=2340&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', 23 | title: 'Image 4', 24 | }, 25 | { 26 | url: 'https://images.unsplash.com/photo-1715128083452-065d5045bac1?q=80&w=2187&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', 27 | title: 'Image 5', 28 | }, 29 | { 30 | url: 'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?q=80&w=2174&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', 31 | title: 'Image 6', 32 | }, 33 | { 34 | url: 'https://plus.unsplash.com/premium_photo-1689596509629-bc6f6a455fa1?q=80&w=2187&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', 35 | title: 'Image 7', 36 | }, 37 | { 38 | url: 'https://images.unsplash.com/photo-1715090488848-4d05af86009b?q=80&w=2342&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', 39 | title: 'Image 8', 40 | }, 41 | { 42 | url: 'https://plus.unsplash.com/premium_photo-1668989820410-6c02365835af?q=80&w=2340&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', 43 | title: 'Image 9', 44 | }, 45 | { 46 | url: 'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?q=80&w=2174&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', 47 | title: 'Image 10', 48 | }, 49 | ]; 50 | const itemHeight = 150; 51 | const itemWidth = 150; 52 | const gap = 10; 53 | const topOffset = (screenHeight - itemHeight) / 2; 54 | 55 | //you can play around with these values to stretch the displacement map and achieve different distortions 56 | const displacementMapWidth = 1236; 57 | const displacementMapHeight = 800; 58 | 59 | export default function App() { 60 | const scrollX = useSharedValue(0); 61 | const displacementMap = useImage('http://tavmjong.free.fr/INKSCAPE/MANUAL/images/FILTERS/bubble.png'); 62 | 63 | const scrollHandler = useAnimatedScrollHandler({ 64 | onScroll: (event) => { 65 | console.log(event.contentOffset.x); 66 | scrollX.value = event.contentOffset.x; 67 | }, 68 | }); 69 | 70 | const renderItem = ({ item }: any) => ; 71 | 72 | return ( 73 | 74 | 75 | 78 | 79 | {/* workaround for "DisplacementMap expects a shader as child" */} 80 | 81 | 82 | 83 | 84 | } 85 | > 86 | 87 | {images.map((image, index) => { 88 | const x = useDerivedValue(() => { 89 | return index * itemWidth + index * gap - scrollX.value; 90 | }); 91 | return ; 92 | })} 93 | 94 | 95 | 96 | {/* edge gradients */} 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | ); 108 | } 109 | 110 | const styles = StyleSheet.create({ 111 | container: { 112 | flex: 1, 113 | backgroundColor: 'black', 114 | justifyContent: 'center', 115 | }, 116 | canvas: { 117 | width: '100%', 118 | height: '100%', 119 | position: 'absolute', 120 | }, 121 | flatList: { 122 | flexGrow: 0, 123 | opacity: 0, 124 | }, 125 | contentContainer: { 126 | gap: gap, 127 | justifyContent: 'center', 128 | }, 129 | }); 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## React Native Skia Displacement Map Demo 2 | ![image](https://github.com/lauridskern/skia-scroll-view-shader/assets/8788856/b1ed8c72-8221-4a9c-8ddd-1530023382f1) 3 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "skia-scroll-view-experiment", 4 | "slug": "skia-scroll-view-experiment", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "assetBundlePatterns": [ 15 | "**/*" 16 | ], 17 | "ios": { 18 | "supportsTablet": true 19 | }, 20 | "android": { 21 | "adaptiveIcon": { 22 | "foregroundImage": "./assets/adaptive-icon.png", 23 | "backgroundColor": "#ffffff" 24 | } 25 | }, 26 | "web": { 27 | "favicon": "./assets/favicon.png" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lauridskern/skia-scroll-view-shader/8421b97a11d004fc03b6df72ce3e8531cc219c79/assets/adaptive-icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lauridskern/skia-scroll-view-shader/8421b97a11d004fc03b6df72ce3e8531cc219c79/assets/favicon.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lauridskern/skia-scroll-view-shader/8421b97a11d004fc03b6df72ce3e8531cc219c79/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lauridskern/skia-scroll-view-shader/8421b97a11d004fc03b6df72ce3e8531cc219c79/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /bubble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lauridskern/skia-scroll-view-shader/8421b97a11d004fc03b6df72ce3e8531cc219c79/bubble.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "skia-scroll-view-experiment", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "@shopify/react-native-skia": "1.2.3", 13 | "expo": "~51.0.4", 14 | "expo-image": "~1.12.9", 15 | "expo-status-bar": "~1.12.1", 16 | "react": "18.2.0", 17 | "react-native": "0.74.1", 18 | "react-native-reanimated": "~3.10.1" 19 | }, 20 | "devDependencies": { 21 | "@babel/core": "^7.20.0", 22 | "@types/react": "~18.2.45", 23 | "typescript": "~5.3.3" 24 | }, 25 | "private": true 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | } 6 | } 7 | --------------------------------------------------------------------------------