Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Chat example #2756

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion apps/paper/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"react-native-safe-area-context": "^4.10.9",
"react-native-screens": "^4.3.0",
"react-native-svg": "^15.6.0",
"react-native-wgpu": "0.1.19",
"react-native-wgpu": "0.1.20",
"typescript": "^5.2.2"
},
"devDependencies": {
Expand Down
9 changes: 9 additions & 0 deletions apps/paper/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
SpeedTest,
Video,
WebGPU,
Chat,
} from "./Examples";
import { CI, Tests } from "./Tests";
import { HomeScreen } from "./Home";
Expand Down Expand Up @@ -65,6 +66,7 @@ const linking: LinkingOptions<StackParamList> = {
FrostedCard: "frosted-card",
SpeedTest: "speedtest",
Video: "video",
Chat: "chat",
},
},
prefixes: ["rnskia://"],
Expand Down Expand Up @@ -215,6 +217,13 @@ const App = () => {
header: () => null,
}}
/>
<Stack.Screen
name="Chat"
component={Chat}
options={{
headerShown: false,
}}
/>
<Stack.Screen
name="Performance"
component={PerformanceDrawingTest}
Expand Down
44 changes: 44 additions & 0 deletions apps/paper/src/Examples/Chat/ChatExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from "react";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { GestureHandlerRootView } from "react-native-gesture-handler";

import { HomeScreen } from "./HomeScreen";
import { ChatScreen } from "./ChatScreen/ChatScreen";

export type RootStackParamList = {
Home: undefined;
ChatIndex: { chatId: string };
};

export type RouteProps<
T extends keyof RootStackParamList = keyof RootStackParamList
> = NativeStackNavigationProp<RootStackParamList, T>;

const Stack = createNativeStackNavigator<RootStackParamList>();

export const Chat = () => {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<Stack.Navigator
screenOptions={{ headerTransparent: true }}
initialRouteName="Home"
>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: "Home", headerBackTitle: "" }}
/>
<Stack.Screen
name="ChatIndex"
component={ChatScreen}
options={{
animation: "ios_from_right",
headerBackTitleVisible: false,
headerTintColor: "black",
}}
/>
</Stack.Navigator>
</GestureHandlerRootView>
);
};
159 changes: 159 additions & 0 deletions apps/paper/src/Examples/Chat/ChatScreen/Background.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import React, { useEffect } from "react";
import type { SkCanvas, SkImage, SkPaint } from "@shopify/react-native-skia";
import { Canvas, Image, Skia } from "@shopify/react-native-skia";
import { makeMutable, runOnUI } from "react-native-reanimated";
import { StyleSheet } from "react-native";

import { WINDOW_HEIGHT, WINDOW_WIDTH } from "../constants";
import { renderToTexture } from "../utils/renderToTexture";

import { RENDER_BACKGROUND } from "./config";

const { Paint } = Skia;

const backgroundTexture = makeMutable<SkImage | null>(null);

// this shader produces a color variation as you scroll
// mostly generated by chatgpt
const backgroundShader = Skia.RuntimeEffect.Make(`
uniform vec2 iResolution; // Canvas width and height
uniform float offset; // Offset to animate the gradient

// Smooth random noise function for organic transitions
float random(vec2 st) {
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
}

// Interpolated noise for smoother blending
float smoothNoise(vec2 st) {
vec2 i = floor(st);
vec2 f = fract(st);

float a = random(i);
float b = random(i + vec2(1.0, 0.0));
float c = random(i + vec2(0.0, 1.0));
float d = random(i + vec2(1.0, 1.0));

vec2 u = f * f * (3.0 - 2.0 * f);
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}

half4 main(vec2 fragCoord) {
// Normalize coordinates to [0, 1]
vec2 uv = fragCoord / iResolution / 3;

// Apply smooth UV distortion based on the offset value
uv.x += sin(uv.y * 3.0 + offset * 0.002) * 0.5;
uv.y += cos(uv.x * 3.0 + offset * 0.002) * 0.5;

// Generate smooth noise to add texture to the gradient
float n = smoothNoise(uv * 5.0);

// Define soft, pastel-like colors
vec3 color1 = vec3(0.95, 0.84, 0.76); // Soft peach
vec3 color2 = vec3(0.76, 0.87, 0.93); // Light blue
vec3 color3 = vec3(0.85, 0.78, 0.9); // Lavender
vec3 color4 = vec3(0.9, 0.85, 0.75); // Beige

// Blend the colors smoothly based on UV and noise
vec3 color = mix(
mix(color1, color2, smoothstep(0.0, 1.0, uv.x)),
mix(color3, color4, smoothstep(0.0, 1.0, uv.y)),
n
);

return half4(color, 1.0); // Return the final blended color
}

`);

export function renderGradientIntoTexture(
scrollOffset: number,
paint: SkPaint | null
) {
"worklet";

if (!paint) {
paint = Paint();
}

const texture = renderToTexture(
WINDOW_WIDTH,
WINDOW_HEIGHT,
0,
(canvas) => {
paint.setShader(
backgroundShader!.makeShader([
WINDOW_WIDTH,
WINDOW_HEIGHT,
scrollOffset,
])
);

canvas.drawPaint(paint);
},
1
);

backgroundTexture.value = texture!.image;
}

export function createGradient() {
"worklet";

if (!RENDER_BACKGROUND) {
return {
onFrame() {},
};
}

const paint = Paint();

return {
onFrame(ctx: SkCanvas, scrollOffset: number) {
const prevTexture = backgroundTexture.value;

renderGradientIntoTexture(scrollOffset, paint);

if (!backgroundTexture.value) {
return;
}

ctx.save();

ctx.drawImage(backgroundTexture.value, 0, 0);

ctx.restore();

if (prevTexture) {
prevTexture.dispose();
}
},
};
}

export function Background() {
useEffect(() => {
if (!RENDER_BACKGROUND) {
return;
}

runOnUI(() => {
renderGradientIntoTexture(0, null);
})();
}, []);

if (!RENDER_BACKGROUND) {
return null;
}

return (
<Canvas style={StyleSheet.absoluteFill}>
<Image
image={backgroundTexture}
width={WINDOW_WIDTH}
height={WINDOW_HEIGHT}
/>
</Canvas>
);
}
127 changes: 127 additions & 0 deletions apps/paper/src/Examples/Chat/ChatScreen/ChatInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// import { BlurView } from "expo-blur";
import React from "react";
import { Image, StyleSheet, TouchableOpacity, View } from "react-native";
import { TextInput } from "react-native-gesture-handler";
import { useSafeAreaInsets } from "react-native-safe-area-context";

type ChatInputProps = {
chatId: string;
onSend: (text: string) => void;
onDraw: () => void;
};

export const INPUT_HEIGHT = 50;

export function ChatInput({ onSend, onDraw }: ChatInputProps) {
const [value, setValue] = React.useState("");

const handleSend = () => {
onSend(value);
setValue("");
};

const insets = useSafeAreaInsets();
return (
<View
style={[
styles.container,
{
paddingBottom: insets.bottom,
},
]}
>
<TouchableOpacity
style={styles.drawButton}
activeOpacity={0.8}
onPress={onDraw}
>
<Image
source={require("../../../assets/pencil.png")}
style={styles.icon}
/>
</TouchableOpacity>
{/* <BlurView
tint="systemThickMaterial"
style={styles.inputContainer}
intensity={40}
> */}
<View style={styles.inputContainer}>
<TextInput
placeholderTextColor="gray"
style={styles.input}
value={value}
onChangeText={setValue}
placeholder="Type a message..."
/>
</View>
{/* </BlurView> */}
<TouchableOpacity
style={styles.sendButton}
activeOpacity={0.8}
onPress={handleSend}
>
<Image
source={require("../../../assets/send.png")}
style={styles.icon}
/>
</TouchableOpacity>
</View>
);
}

const styles = StyleSheet.create({
container: {
position: "absolute",
left: 0,
right: 0,
bottom: 0,
paddingHorizontal: 12,
backgroundColor: "transparent",
flexDirection: "row",
},

inputContainer: {
flex: 1,
height: INPUT_HEIGHT,
borderRadius: 50,
overflow: "hidden",
borderWidth: StyleSheet.hairlineWidth,
borderColor: "rgb(242, 242, 247)",
},

input: {
height: INPUT_HEIGHT,
fontSize: 16,
paddingHorizontal: 12,
color: "black",
backgroundColor: "rgba(0,0,0,0.05)",
},
sendButton: {
position: "absolute",
right: 16,
justifyContent: "center",
alignItems: "center",
alignSelf: "center",
height: INPUT_HEIGHT - 8,
width: INPUT_HEIGHT - 8,
top: 4,
backgroundColor: "#007AFF",
borderRadius: 50,
},
drawButton: {
justifyContent: "center",
alignItems: "center",
alignSelf: "center",
height: INPUT_HEIGHT - 8,
width: INPUT_HEIGHT - 8,
backgroundColor: "black",
borderRadius: 50,
marginRight: 8,
},
icon: {
width: 20,
height: 20,
right: 1,
tintColor: "white",
},
});
Loading