WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Canvas Scaling Results in Blurry Image Rendering (No Resampling Applied) #3464

@Anu666

Description

@Anu666

Description

Summary
When zooming into a canvas containing Image components, the images become pixelated because Skia doesn't automatically resample the image to match the new scale. The image sampling appears to be based on the original dimensions rather than the current transformed dimensions.

Environment
React Native Skia: 2.2.12
React Native: 0.81.4
Expo: ^54.0.8
Platform: Android/iOS (React Native)
React Native Reanimated: ~4.1.0

Expected Behavior
When zooming into a canvas, images should automatically resample to maintain quality at the new scale, similar to how vector graphics scale smoothly.

Actual Behavior
Images become pixelated when zoomed in because they retain their original sampling resolution and don't adapt to the current transformation matrix scale.

React Native Skia Version

2.2.12

React Native Version

0.81.4

Using New Architecture

  • Enabled

Steps to Reproduce

  1. Create a new React Native project and install @shopify/react-native-skia, react-native-gesture-handler, and react-native-reanimated.
  2. Add the sample component (MinimalPlanogramDrawing) from the code snippet below.
  3. Place a high-resolution image (for example, the one attached below) inside the project’s assets/images folder. The image should contain text and fine details for better visual comparison.
  4. Run the app on a device or simulator (Android or iOS).
  5. Observe the canvas at zoom level 1 — the image appears crisp and clear.
  6. Use pinch-to-zoom to enlarge the canvas until it occupies the full screen.
  7. Compare the zoomed-in image (on the Skia canvas) with the same image opened in a native image viewer.

Image used -

Image

The image provided has some images and text which are crisp and detailed when opened in any image viewer.
In the sample component that I have attached now, I have created a canvas of size - 800x600. Inside that, I have rendered the attached image with a width of 200*200 at the center.
At zoom level of 1, this would look something like this. To the left, I have added the actual image opened in a image viewer.

Image

But when I zoom in to occupy the entire screen, this is how it looks. Comparing it to the actual image on the left, we can see that the image has become pixelated and lost the details because of the sampling. (Focus on the text. It's not legible once zoomed. But on the actual left side image, it is legible)

Image

Also attaching a video showing the same behavior.

SkiaZoomIssue.webm

We would be really helpful if there is a way to control the sampling to retain the image quality and the details when it is zoomed.
Please let me know if you need any details.

Snack, Code Example, Screenshot, or Link to Repository

Sample component code -

import { Canvas, Image, useImage } from '@shopify/react-native-skia';
import React, { useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
  runOnJS,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
} from 'react-native-reanimated';

/**
 * Minimal Planogram Drawing Component with Zoom
 * 
 * This demonstrates a simple zoomable canvas with a single image.
 * Shows how Skia canvas quality is affected when zooming.
 */
const MinimalPlanogramDrawing: React.FC = () => {
  const [zoomLevel, setZoomLevel] = useState(1);
  const scale = useSharedValue(1);
  const translateX = useSharedValue(0);
  const translateY = useSharedValue(0);

  // Load image from assets
  const image = useImage(require('../assets/images/high-quality-image.jpeg'));

  const pinchGesture = Gesture.Pinch()
    .onUpdate((event) => {
      scale.value = Math.max(0.5, Math.min(10, event.scale));
      runOnJS(setZoomLevel)(scale.value);
    })
    .onEnd(() => {
      scale.value = withSpring(scale.value);
    });

  const panGesture = Gesture.Pan()
    .onUpdate((event) => {
      translateX.value = event.translationX;
      translateY.value = event.translationY;
    })
    .onEnd(() => {
      translateX.value = withSpring(0);
      translateY.value = withSpring(0);
    });

  const composedGesture = Gesture.Simultaneous(pinchGesture, panGesture);

  const animatedStyle = useAnimatedStyle(() => {
    return {
      transform: [
        { translateX: translateX.value },
        { translateY: translateY.value },
        { scale: scale.value },
      ],
    };
  });

  if (!image) {
    return (
      <View style={styles.container}>
        <Text>Loading image...</Text>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <View style={styles.controlsContainer}>
        <Text style={styles.zoomText}>Zoom: {zoomLevel.toFixed(2)}x</Text>
      </View>

      <GestureDetector gesture={composedGesture}>
        <Animated.View style={[styles.canvasContainer, animatedStyle]}>
          <Canvas style={styles.canvas}>
            <Image
              image={image}
              x={800 / 2 - 100} // center the image
              y={600 / 2 - 100}
              width={200}
              height={200}
              fit="contain"
            />
          </Canvas>
        </Animated.View>
      </GestureDetector>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    padding: 20,
  },
  controlsContainer: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    alignItems: 'center',
    marginBottom: 20,
    paddingHorizontal: 10,
    flexWrap: 'wrap',
    gap: 10,
  },
  zoomText: {
    fontSize: 18,
    fontWeight: '600',
    color: '#333',
  },
  canvasContainer: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  canvas: {
    width: 800,
    height: 600,
    borderWidth: 2,
    borderColor: '#007AFF',
    borderRadius: 8,
    backgroundColor: '#f8f9fa',
  },
});

export default MinimalPlanogramDrawing;

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions