iddan / react-native-canvas

A Canvas component for React Native
MIT License
992 stars 172 forks source link

Problem with resizing canvas #134

Open edbrito opened 5 years ago

edbrito commented 5 years ago

I've been having the same problem. This is my code:

import React, { Component } from 'react';
import {
  AppRegistry,
  View,
  PanResponder,
} from 'react-native';

import Canvas, {Image as CanvasImage} from 'react-native-canvas';

export default class ImagePainter extends Component {
  constructor(props) {
    super(props);
  }

  state = {
      image: this.props.image,
      originalImage: this.props.image,
      width: this.props.width,
      height: this.props.height,
      positionX: undefined,
      positionY: undefined,
      canvas: undefined,
  };

  panResponder = PanResponder.create({
    onStartShouldSetPanResponder: (evt, gestureState) => true,
    onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
    onMoveShouldSetPanResponder: (evt, gestureState) => true,
    onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,

    onPanResponderGrant: (evt, gestureState) => {
      const x = Math.floor(evt.nativeEvent.locationX);
      const y = Math.floor(evt.nativeEvent.locationY);
      this.positionX = x;
      this.positionY = y;
    },
    onPanResponderMove: (evt, gestureState) => {
      if(!this.canvas){
        return;
      }

      const x0 = this.positionX;
      const y0 = this.positionY;
      const x1 = Math.floor(evt.nativeEvent.locationX);
      const y1 = Math.floor(evt.nativeEvent.locationY);
      const ctx = this.canvas.getContext('2d');
      ctx.moveTo(x0, y0);
      ctx.lineTo(x1, y1);
      ctx.stroke();
      this.positionX=x1
      this.positionY=y1;
    },
    onPanResponderTerminationRequest: (evt, gestureState) => true,
    onPanResponderRelease: (evt, gestureState) => {
    },
    onPanResponderTerminate: (evt, gestureState) => {
    },
    onShouldBlockNativeResponder: (evt, gestureState) => {
      return true;
    },
  });

  componentDidMount() {
    const canvas = this.canvas;
    if(canvas){
      canvas.width = this.props.width;
      canvas.height = this.props.height;
      this.renderCanvas();
    }
  }

  renderCanvas = async () => {
    if (this.canvas) {
      const canvas = this.canvas;
      const ctx = await canvas.getContext('2d');
      const image = new CanvasImage(canvas, this.props.height, this.props.width);
      image.addEventListener('load', function() {
        ctx.drawImage(image, 0, 0);
      });
      image.addEventListener('error', err => console.log(err))
      image.src = `data:image/jpeg;base64,${this.state.image}`;
    } else {
      console.log('No canvas?')
    }
  }

  componentDidUpdate() {
    this.renderCanvas();
  }

  render() {
    return (
          <View {...this.panResponder.panHandlers} style={{width: this.props.width, height: this.props.height}}>
            <Canvas ref={ canvas => this.canvas = canvas } 
                    style={{borderWidth: 3, borderStyle: 'dashed', width: this.props.width, height: this.props.height}}
                    width={this.props.width}
                    height={this.props.height}
                  />
          </View>
    );
  }
}

AppRegistry.registerComponent('ImagePainter', () => ImagePainter);

From what is written here, it should work.

I narrowed down the problem to the resizing. It looks like resizing changes the reference to the canvas?

On my componentDidMount if I leave the canvas.width and height assignments, nothing shows up. If I comment it how, I get a canvas that's too small (300x150px instead of 300x400) but everything works.

I installed react-native-canvas 2 days ago so it should be the most up-to-date...

Originally posted by @edbrito in https://github.com/iddan/react-native-canvas/issues/94#issuecomment-504378489

edbrito commented 5 years ago

Posted as a new issue since the previous thread was already closed when I posted the comment... Sorry.

iddan commented 5 years ago

Do you pass width and height to your component?

edbrito commented 5 years ago

Yes. I've confirmed it. The width and height are being passed.

I've also added code to restore the original image and it restores but as soon as I press to draw, it goes back to the altered canvas that I had before I restored the image to the original one.

edbrito commented 5 years ago

I removed the need to draw the image on the react native canvas by drawing it with a regular Image component and overlaying the react-native-canvas component on top of the image by using absolute positioning.

However, I can't resize the component without facing the same problems. As in, it doesn't draw anything on the canvas after having been resized.

Azus5 commented 4 years ago

i'm having the same issue, any solutions?

Vannevelj commented 4 years ago

I'm facing the same problem as well. @iddan do you perhaps have an example where a Canvas has dimensions X, resizes to dimensions Y, draws something and it shows up? Just to rule out us doing something wrong.

Edit: I looked into it more and it appears that this is expected behaviour. When the canvas its width or height changes, it will clear everything that was rendered. I've verified this in non-native React and sources corroborate this: https://stackoverflow.com/a/5517885/1864167 & https://stackoverflow.com/questions/56120082/how-to-get-correct-width-and-height-for-a-canvas-in-react#comment98873492_56120235

Note that this might just be me and the others their problem is slightly different. I'll update this if I find myself in the same situation as they are. That being said, this example draws a rectangle on the canvas, resizes the canvas and draws a new rectangle: https://gist.github.com/Vannevelj/eca9ab7da0d543c84964ecdbcad00879

dandan-drori commented 4 years ago

Found a possible fix since it worked for me: inside the handleCanvas function (as in the documentation), i set canvas.width to equal Dimensions.get('window').width (as in the react native documentation for getting device height and width). That was the only way i managed to set a height and a width to the canvas element that is not 150X300. I'm using a bare react native project, with very minimal dependencies which include react, react native, react native canvas, react native webview, react redux, redux, react router native and styled components. My react native version is 0.63.3 according to my package.json and react native canvas is 0.1.37.

James-Firth commented 3 years ago

(sorry if this is hijacking this issue but I think this is related)

The problem with the technique @dandan-drori mentioned is that the transformation matrix is no longer the identity matrix.

I confirmed this by manually exposing the getTransform function for CanvasRenderingContext2D in my node_modules.

isIdentity is true if I leave my Canvas component untouched, or add a height+width style or if I add the height and width props. However, if I modify the canvas object itself in the handler to make the canvas bigger it changes the transformation matrix (in my case where a and d are 2 instead of 1)

It seems this is related to my current issue where scaling/translating isn't working as expected.

I think ideally the Canvas would accept a height and width prop that would pass down and also apply both the WebView and the RNCWebView so they would size correctly? (as even

Example function passed to <Canvas ref={handleCanvas} />

function handleCanvas(canvas) {
    canvas.width = myCalculatedWidth;
    canvas.height = myCalculatedHeight;
}

I'm testing this on an iPad using Expo Go with React Native and Expo for reference.

EDIT: Oh, I've just noticed autoScaleCanvas so it seems the default matrix being non-identity is intended. I'll have to see if I can work around this but it seems to still be causing issues.

blwinters commented 2 years ago

Similar to @dandan-drori , I used the onLayout handler to get the size of my canvas container (which varies a bit based on the device). From that handler I set the width and height of the ref.

Also, TypeScript was showing an error when I tried to pass in the width and height as props for some reason, so I skipped those.

const onContainerLayout = (event: LayoutChangeEvent) => {
  const { layout } = event.nativeEvent
  if (canvasRef?.current) {
    canvasRef.current.height = layout.height * 0.6
    canvasRef.current.width = layout.width
  }
}
<View onLayout={onContainerLayout}>
  <Canvas ref={canvasRef} />
</View>
Pingou commented 6 months ago

@James-Firth Hi, I am experiencing similar issues with scaling, did you manage to find a work around?