├── .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 |
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 | 
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 |
--------------------------------------------------------------------------------