So, I used react-native-canvas library to use canvas on my React Native project, everything went very good but when it comes to selecting of font family's, like for example pacifico or anything like that, it was not applying to my canvas, but on my _layout.tsx, I actually specifically installed a library called as:
import { Pacifico_400Regular } from "@expo-google-fonts/pacifico";
and I loaded fonts as well in Expo, here is my code:
_layout.tsx:
import { SplashScreen, Stack } from "expo-router";
import "./globals.css";
import { useFonts } from "expo-font";
import { useEffect } from "react";
import { ClerkProvider, ClerkLoaded, ClerkLoading } from "@clerk/clerk-expo";
import { tokenCache } from "@/lib/auth";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { Alert, Text } from "react-native";
import CustomLoader from "@/components/CustomLoader";
import { StripeProvider } from "@stripe/stripe-react-native";
import { Pacifico_400Regular } from "@expo-google-fonts/pacifico";
export default function RootLayout() {
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!;
if (!publishableKey) {
throw new Error("Add EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY to your .env file");
}
const [fontsLoaded] = useFonts({
Pacifico: Pacifico_400Regular,
"Rubik-Bold": require("../assets/fonts/Rubik-Bold.ttf"),
"Rubik-ExtraBold": require("../assets/fonts/Rubik-ExtraBold.ttf"),
"Rubik-Medium": require("../assets/fonts/Rubik-Medium.ttf"),
"Rubik-Light": require("../assets/fonts/Rubik-Light.ttf"),
"Rubik-Regular": require("../assets/fonts/Rubik-Regular.ttf"),
"Rubik-SemiBold": require("../assets/fonts/Rubik-SemiBold.ttf"),
});
useEffect(() => {
if (!process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY) {
console.error("Missing Clerk Publishable Key!");
Alert.alert("Configuration Error", "Missing Clerk credentials");
}
}, []);
useEffect(() => {
if (fontsLoaded) {
SplashScreen.hideAsync();
}
}, [fontsLoaded]);
if (!fontsLoaded) {
return null;
}
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<StripeProvider
publishableKey={process.env.EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY!}
>
<ClerkProvider tokenCache={tokenCache} publishableKey={publishableKey}>
<ClerkLoaded>
<Stack screenOptions={{ headerShown: false }} />
</ClerkLoaded>
<ClerkLoading>
{/* Optional: Loading indicator or splash screen alternative */}
<CustomLoader loading={true} />
</ClerkLoading>
</ClerkProvider>
</StripeProvider>
</GestureHandlerRootView>
);
}
Yes, it worked when I used it in my Expo screens, but not in canvas!, why? here is my code:
constants/fonts.ts:
export const fontConfig: any = {
Arial: { isPremium: false },
Helvetica: { isPremium: false },
Pacifico: { isPremium: false },
Montserrat: { isPremium: true },
"Playfair Display": { isPremium: true },
// Add other fonts...
};
export const freeFonts = Object.keys(fontConfig).filter(
(font) => !fontConfig[font].isPremium
);
export const premiumFonts = Object.keys(fontConfig).filter(
(font) => fontConfig[font].isPremium
);
Font Picker Modal:
<Modal
visible={showFontPicker}
transparent={true}
animationType="slide"
>
<View className="flex-1 justify-center items-center bg-black/50">
<View className="bg-white p-4 rounded-lg w-80 max-h-[80%]">
<Text className="text-lg font-bold mb-4">
Select Font
</Text>
<ScrollView>
{/* Free Fonts Section */}
<Text className="text-gray-700 font-semibold mb-2">
Free Fonts
</Text>
{freeFonts.map((font) => (
<TouchableOpacity
key={font}
onPress={() => {
updateLayer(selectedLayer.id, {
fontFamily: font,
});
setSelectedFont(font);
setShowFontPicker(false);
}}
className="p-2"
>
<Text style={{ fontFamily: font }}>{font}</Text>
</TouchableOpacity>
))}
{/* Premium Fonts Section */}
<Text className="text-gray-700 font-semibold mt-4 mb-2">
Premium Fonts
</Text>
{premiumFonts.map((font) => (
<TouchableOpacity
key={font}
onPress={() => {
if (plan !== "PRO") {
Alert.alert(
"Premium Feature",
"Upgrade to access premium fonts"
);
return;
}
updateLayer(selectedLayer.id, {
fontFamily: font,
});
setSelectedFont(font);
setShowFontPicker(false);
}}
className="p-2"
disabled={plan !== "PRO"} // Disable for free users
>
<Text
style={{
fontFamily: font,
color: plan !== "PRO" ? "#ccc" : "#000", // Gray out for free users
}}
>
{font}
</Text>
</TouchableOpacity>
))}
</ScrollView>
{/* Close Button */}
<TouchableOpacity
onPress={() => setShowFontPicker(false)}
className="mt-4 p-2 bg-gray-200 rounded"
>
<Text>Close</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
Canvas Rendering Logic:
const drawOnCanvas = useCallback(
async (layers: TextLayer[]) => {
if (!canvasRef.current || !imageUrl || !processedImageurl) return;
try {
const ctx = canvasRef.current.getContext("2d");
const img = new CanvasImage(canvasRef.current);
const img_2 = new CanvasImage(canvasRef.current);
canvasRef.current.width = screenWidth;
canvasRef.current.height = canvasHeight;
const [originalBase64, processedBase64] = await Promise.all([
FileSystem.readAsStringAsync(imageUrl, {
encoding: FileSystem.EncodingType.Base64,
}),
FileSystem.readAsStringAsync(processedImageurl, {
encoding: FileSystem.EncodingType.Base64,
}),
]);
const imgLoaded = new Promise((resolve) =>
img.addEventListener("load", resolve)
);
const img2Loaded = new Promise((resolve) =>
img_2.addEventListener("load", resolve)
);
img.src = `data:image/png;base64,${processedBase64}`;
img_2.src = `data:image/png;base64,${originalBase64}`;
await Promise.all([imgLoaded, img2Loaded]);
const ratio = Math.min(
screenWidth / img_2.width,
canvasHeight / img_2.height
);
const displayWidth = img_2.width * ratio;
const displayHeight = img_2.height * ratio;
ctx.clearRect(0, 0, screenWidth, canvasHeight);
// Draw original image
ctx.drawImage(
img_2,
(screenWidth - displayWidth) / 2,
(canvasHeight - displayHeight) / 2,
displayWidth,
displayHeight
);
// Draw text layers
layers.forEach((layer) => {
ctx.save();
ctx.translate(layer.x, layer.y);
ctx.rotate((layer.rotation * Math.PI) / 180);
ctx.font = `${layer.fontWeight} ${layer.fontSize}px "${layer.fontFamily}"`; // Ensure fontFamily is wrapped in quotes
ctx.fillStyle = layer.color;
ctx.globalAlpha = layer.opacity;
ctx.fillText(layer.text, 0, 0);
ctx.restore();
});
// Draw processed image
ctx.drawImage(
img,
(screenWidth - displayWidth) / 2,
(canvasHeight - displayHeight) / 2,
displayWidth,
displayHeight
);
} catch (error) {
console.error("Error drawing image:", error);
}
},
[imageUrl, processedImageurl, screenWidth, canvasHeight]
);
please help me!
So I tried to reproduce your problem by making a Snack on expo, and I reached the same results, I'm not able to render the proper fontFamily inside the canvas, which I think is a thing of the webview inside the react-native-canvas
package. You cannot render the font family because expo-font
does not reach the webview context.
Also looking for the pull requests of the canvas repo, I saw a contributor trying to implement a Font API, but it didn't merge: https://github.com/iddan/react-native-canvas/pull/294.
Just to my conclusion, the plugin of expo-font
doesn't have the flexibility to add fonts inside webview contexts, at least doing it this way.
This is the code that I made to reproduce this issue:
import { Text, SafeAreaView, StyleSheet } from 'react-native';
import { useEffect } from 'react';
import { useFonts } from 'expo-font';
import Canvas from 'react-native-canvas';
const WaitFont = (props) => {
const [loaded, error] = useFonts({
'eightBit': require('./assets/fonts/eightBit.ttf'),
});
useEffect(() => {
if (loaded || error) {
console.log('Working?')
}
}, [loaded, error]);
if (!loaded && !error) {
return null;
}
return props.children
}
export default function App() {
const handleCanvas = (canvas) => {
if (!canvas) return;
const ctx = canvas.getContext('2d');
ctx.font = '500 26px "eightBit"';
ctx.fillText("Hello from <Canvas />", 8, 28) ;
};
return (
<SafeAreaView style={styles.container}>
<WaitFont>
<Text style={styles.paragraph}>
{`Hello from <Text />`}
</Text>
<Canvas style={styles.canvas} ref={handleCanvas} />
</WaitFont>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
backgroundColor: '#ecf0f1',
padding: 8,
},
paragraph: {
fontSize: 50,
fontWeight: '500',
fontFamily: 'eightBit'
},
canvas: {
borderWidth: 1,
borderColor: 'red',
width: '100%'
}
});