Pokemon Cards with Skia – Summary

CONTENTS:

Summary

This was my first project where I have dug deeper into react-native-skia. I think this is a wonderful technology. The goal was to demonstrate that you can actually render 60 FPS animation with a composable view that responds to user and device input while having multiple images, layers and transformations at the same time.

The examples that I have mentioned at the beginning of this article are very nice. I love them. The thing was that I wanted to push the bar even further – it worked.

Honestly I think there are some more optimisations that would make this example run smoothly even on older devices. I simply just don’t have time to work on this project anymore. It’s just good enough.

The most important part of the work was optimisation. Since React Native is built around a virtual tree, avoiding unnecessary re-renders was the first major priority. My initial implementation caused too many updates from gesture handling and motion changes, so I focused on removing React from the hot path wherever possible. The main idea was simple: keep rendering stable, move animations and motion to shared values, and let Skia and Reanimated do their work on the UI thread.

On the canvas side, I wrapped the main rendering surface in memo(…) with a custom comparator and moved expensive image and shader loading higher in the component tree. Resources such as useImage(…) and animated image values are created once and passed down as references, instead of being recreated inside the canvas. I also kept the canvas dimensions stable and positioned it absolutely, which helped reduce layout-related work and made the renderer focus only on repainting.

Another important decision was to unify rendering into a single canvas while still splitting the visual output into separate internal layers. This made it possible to cache static parts such as the background or card image, while recomputing only dynamic layers like gloss, hologram and outline effects. Each layer could be memoized independently and toggled without disturbing the rest of the render tree. Instead of conditionally mounting and unmounting large sections with {visible && <Component />}, I used early returns inside components so the overall structure stayed stable and invisible parts simply returned null.

Feature gating also helped reduce unnecessary work. Some calculations, like hologram gradients, are only needed when specific effects are enabled. By checking those requirements early, I could skip entire chunks of math and return cheap default values instead. The same pattern was used for mask transforms and motion handling. Shared zero-value defaults allowed the render pipeline to stay stable even when gestures or sensors were inactive, which removed repeated null checks and kept lower-level logic simple.

A big performance improvement came from using useDerivedValue and shared values instead of React state. Motion, gradients, transforms and shader uniforms all update without causing React re-renders. This is especially important for animation-heavy components, because visual updates can happen continuously while React stays idle. Combined motion values were precomputed once, scalar values like half-width and inverse angles were cached, and style arrays were memoized to avoid unnecessary object creation.

Shader performance followed the same philosophy. Instead of driving animation time with requestAnimationFrame and useState, I used Skia’s useClock() together with derived uniforms. This keeps shader animation off the JS thread and lets Skia consume shared references directly. In practice, the shader component mounts once and continues animating through referenced values, without touching the virtual tree every frame. This also makes memoization much more effective, since changing time no longer changes React props in a meaningful way.

Gestures and motion were treated as a completely separate concern from rendering. The gesture container sits above the canvas and passes motion into it as shared references. This separation means the renderer only renders, while user input and sensor logic happen elsewhere. Gesture rotation, sensor rotation and translation are all stored in useSharedValue, so touch and device updates never trigger component re-renders.

To keep the code maintainable, I split gesture logic and animated styles into dedicated hooks. Reusable worklet helpers such as clamp, mapToAngle, applyDeadZone, smoothValue and shouldUpdateValue were extracted into utilities so they could run directly on the UI thread. I also used a memoized render callback pattern for the canvas child, which made it possible to pass motion into render-heavy components without causing unnecessary child execution.

Sensor input needed extra filtering to feel smooth and not overload the pipeline. I limited update frequency, added dead zones for tiny movements, clamped the output range, smoothed noisy values and ignored updates below an epsilon threshold. This reduced jitter, prevented constant micro-updates and made the overall tilt feel more natural. At the end of gestures, values return to zero with withTiming(…), which gives the card a smooth snap-back animation instead of an abrupt reset.

Beyond project-specific changes, I also applied some general optimisations. Shared constants, default values and reusable color arrays were moved outside of components so they could occupy stable memory and be reused aggressively by the JS engine. The same thinking applies to style composition: whenever possible, styles should be cached or composed once instead of rebuilt on every render.

Overall, this project was an experiment in pushing react-native-skia beyond a simple visual demo and into something more production-minded. The goal was not only to make the card effects look good, but to prove that multiple layered effects, shader animation, gesture input and sensor-driven motion can coexist while still targeting smooth 60 FPS rendering. There is definitely still room for improvement, especially for older devices, but the result showed that with the right architecture, React Native, Reanimated and Skia can work together very efficiently.

Full Video Play button

▶ Full Video
Reel Play button

▶ Reel

📟 Pokédex GIF Gallery

Click any card to open the file.

Pikachu
#001 Pikachu
Bulbasaur
#002 Bulbasaur
Jolteon
#003 Jolteon
Charizard Big
#004 Charizard Big
Pikachu
#005 Pikachu
Chardizard
#006 Chardizard
Pikachu
#007 Pikachu
Squirtle
#008 Squirtle
Marill
#009 Marill
Mew
#010 Mew
Mew Controls
#011 Mew Controls
🟡 ???
Coming soon

🤝 Sponsors

WORKDEI

Generalne wykonawstwo inwestycji
Podwykonawstwo budowlane
Usługi budowlane Poznań
Nadzór budowlany
MILWICZ ARCHITEKCI

Gotowy projekt domu
Architekt Poznań
Projekt domu na zamówienie
Nowoczesna stodoła
DRL-CLINIC

Dentysta Kraków
Ortodonta Kraków
Implanty Kraków
Metamorfoza uśmiechu
MAXDENT

Leczenie laserowe Wrocław
Leczenie pod mikroskopem
Ortodonta Wrocław
Implanty Wrocław

💛 Thanks to our partners for supporting this project

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top