UNPKG

@rbmweb/canvas

Version:

react-native-sketch-canvas allows you to draw / sketch on both iOS and Android devices and sync the drawing data between users. Of course you can save as image.

416 lines (378 loc) 16.1 kB
"use strict"; import React from "react"; import PropTypes from "prop-types"; import ReactNative, { requireNativeComponent, NativeModules, UIManager, PanResponder, PixelRatio, Platform, ViewPropTypes, processColor } from "react-native"; import { requestPermissions } from "./handlePermissions"; const RNImageEditor = requireNativeComponent("RNImageEditor", ImageEditor, { nativeOnly: { nativeID: true, onChange: true } }); const ImageEditorManager = NativeModules.RNImageEditorManager || {}; class ImageEditor extends React.Component { static propTypes = { style: ViewPropTypes.style, strokeColor: PropTypes.string, strokeWidth: PropTypes.number, onPathsChange: PropTypes.func, onStrokeStart: PropTypes.func, onStrokeChanged: PropTypes.func, onStrokeEnd: PropTypes.func, onSketchSaved: PropTypes.func, onShapeSelectionChanged: PropTypes.func, shapeConfiguration: PropTypes.shape({ shapeBorderColor: PropTypes.string, shapeBorderStyle: PropTypes.string, shapeBorderStrokeWidth: PropTypes.number, shapeColor: PropTypes.string, shapeStrokeWidth: PropTypes.number }), user: PropTypes.string, scale: PropTypes.number, touchEnabled: PropTypes.bool, text: PropTypes.arrayOf( PropTypes.shape({ text: PropTypes.string, font: PropTypes.string, fontSize: PropTypes.number, fontColor: PropTypes.string, overlay: PropTypes.oneOf(["TextOnSketch", "SketchOnText"]), anchor: PropTypes.shape({ x: PropTypes.number, y: PropTypes.number }), position: PropTypes.shape({ x: PropTypes.number, y: PropTypes.number }), coordinate: PropTypes.oneOf(["Absolute", "Ratio"]), alignment: PropTypes.oneOf(["Left", "Center", "Right"]), lineHeightMultiple: PropTypes.number }) ), localSourceImage: PropTypes.shape({ filename: PropTypes.string, directory: PropTypes.string, mode: PropTypes.oneOf(["AspectFill", "AspectFit", "ScaleToFill"]) }), permissionDialogTitle: PropTypes.string, permissionDialogMessage: PropTypes.string }; static defaultProps = { style: null, strokeColor: "#000000", strokeWidth: 3, onPathsChange: () => {}, onStrokeStart: () => {}, onStrokeChanged: () => {}, onStrokeEnd: () => {}, onSketchSaved: () => {}, onShapeSelectionChanged: () => {}, shapeConfiguration: { shapeBorderColor: "transparent", shapeBorderStyle: "Dashed", shapeBorderStrokeWidth: 1, shapeColor: "#000000", shapeStrokeWidth: 3 }, user: null, scale: 1, touchEnabled: true, text: null, localSourceImage: null, permissionDialogTitle: "", permissionDialogMessage: "", defaultPaths: [], }; state = { text: null, hasPanResponder: false }; constructor(props) { super(props); this._pathsToProcess = this.props.defaultPaths || []; this._paths = []; this._path = null; this._handle = null; this._screenScale = Platform.OS === "ios" ? 1 : PixelRatio.get(); this._offset = { x: 0, y: 0 }; this._size = { width: 0, height: 0 }; this._initialized = false; this.state = { text: ImageEditor.processText(props.text ? props.text.map((t) => Object.assign({}, t)) : null), hasPanResponder: false }; } static getDerivedStateFromProps(nextProps, prevState) { if (nextProps.text) { return { text: ImageEditor.processText(nextProps.text ? nextProps.text.map((t) => Object.assign({}, t)) : null) }; } else { return null; } } static processText(text) { text && text.forEach((t) => (t.fontColor = processColor(t.fontColor))); return text; } componentDidUpdate(prevProps, prevState) { if (prevState.text !== this.state.text) { this.setState({ text: this.state.text }); } } clear() { this._paths = []; this._path = null; UIManager.dispatchViewManagerCommand( this._handle, UIManager.getViewManagerConfig(RNImageEditor).Commands.clear, [] ); } undo() { let lastId = -1; this._paths.forEach((d) => (lastId = d.drawer === this.props.user ? d.path.id : lastId)); if (lastId >= 0) this.deletePath(lastId); return lastId; } addPath(data) { if (this._initialized) { if (this._paths.filter((p) => p.path.id === data.path.id).length === 0) this._paths.push(data); const pathData = data.path.data.map((p) => { const coor = p.split(",").map((pp) => parseFloat(pp).toFixed(2)); return `${(coor[0] * this._screenScale * this._size.width) / data.size.width},${(coor[1] * this._screenScale * this._size.height) / data.size.height}`; }); UIManager.dispatchViewManagerCommand( this._handle, UIManager.getViewManagerConfig(RNImageEditor).Commands.addPath, [data.path.id, processColor(data.path.color), data.path.width * this._screenScale, pathData] ); } else { this._pathsToProcess.filter((p) => p.path.id === data.path.id).length === 0 && this._pathsToProcess.push(data); } } deletePath(id) { this._paths = this._paths.filter((p) => p.path.id !== id); UIManager.dispatchViewManagerCommand( this._handle, UIManager.getViewManagerConfig(RNImageEditor).Commands.deletePath, [id] ); } addShape(config) { if (config) { let fontSize = config.textShapeFontSize ? config.textShapeFontSize : 0; UIManager.dispatchViewManagerCommand( this._handle, UIManager.getViewManagerConfig(RNImageEditor).Commands.addShape, [config.shapeType, config.textShapeFontType, fontSize, config.textShapeText, config.imageShapeAsset] ); } } deleteSelectedShape() { UIManager.dispatchViewManagerCommand( this._handle, UIManager.getViewManagerConfig(RNImageEditor).Commands.deleteSelectedShape, [] ); } unselectShape() { UIManager.dispatchViewManagerCommand( this._handle, UIManager.getViewManagerConfig(RNImageEditor).Commands.unselectShape, [] ); } increaseSelectedShapeFontsize() { UIManager.dispatchViewManagerCommand( this._handle, UIManager.getViewManagerConfig(RNImageEditor).Commands.increaseShapeFontsize, [] ); } decreaseSelectedShapeFontsize() { UIManager.dispatchViewManagerCommand( this._handle, UIManager.getViewManagerConfig(RNImageEditor).Commands.decreaseShapeFontsize, [] ); } changeSelectedShapeText(newText) { UIManager.dispatchViewManagerCommand( this._handle, UIManager.getViewManagerConfig(RNImageEditor).Commands.changeShapeText, [newText] ); } save(imageType, transparent, folder, filename, includeImage, includeText, cropToImageSize) { UIManager.dispatchViewManagerCommand( this._handle, UIManager.getViewManagerConfig(RNImageEditor).Commands.save, [imageType, folder, filename, transparent, includeImage, includeText, cropToImageSize] ); } getPaths() { return this._paths; } getBase64(imageType, transparent, includeImage, includeText, cropToImageSize, callback) { if (Platform.OS === "ios") { ImageEditorManager.transferToBase64( this._handle, imageType, transparent, includeImage, includeText, cropToImageSize, callback ); } else { NativeModules.ImageEditorModule.transferToBase64( this._handle, imageType, transparent, includeImage, includeText, cropToImageSize, callback ); } } async componentDidMount() { const isStoragePermissionAuthorized = await requestPermissions( this.props.permissionDialogTitle, this.props.permissionDialogMessage ); this.panResponder = PanResponder.create({ // Ask to be the responder: onStartShouldSetPanResponder: (evt, gestureState) => true, onStartShouldSetPanResponderCapture: (evt, gestureState) => true, onMoveShouldSetPanResponder: (evt, gestureState) => true, onMoveShouldSetPanResponderCapture: (evt, gestureState) => true, onPanResponderGrant: (evt, gestureState) => { if (!this.props.touchEnabled) return; const e = evt.nativeEvent; this._offset = { x: e.pageX - e.locationX, y: e.pageY - e.locationY }; this._path = { id: parseInt(Math.random() * 100000000), color: this.props.strokeColor, width: this.props.strokeWidth, data: [] }; UIManager.dispatchViewManagerCommand( this._handle, UIManager.getViewManagerConfig(RNImageEditor).Commands.newPath, [this._path.id, processColor(this._path.color), this._path.width * this._screenScale] ); UIManager.dispatchViewManagerCommand( this._handle, UIManager.getViewManagerConfig(RNImageEditor).Commands.addPoint, [ parseFloat((gestureState.x0 - this._offset.x).toFixed(2) * this._screenScale), parseFloat((gestureState.y0 - this._offset.y).toFixed(2) * this._screenScale), false ] ); const x = parseFloat((gestureState.x0 - this._offset.x).toFixed(2)), y = parseFloat((gestureState.y0 - this._offset.y).toFixed(2)); this._path.data.push(`${x},${y}`); this.props.onStrokeStart(x, y); }, onPanResponderMove: (evt, gestureState) => { if (!this.props.touchEnabled) return; if (Math.abs(gestureState.dx) < 2.5 || Math.abs(gestureState.dy) < 2.5) return; if (this._path) { const x = parseFloat( (gestureState.x0 + gestureState.dx / this.props.scale - this._offset.x).toFixed(2) ), y = parseFloat( (gestureState.y0 + gestureState.dy / this.props.scale - this._offset.y).toFixed(2) ); UIManager.dispatchViewManagerCommand( this._handle, UIManager.getViewManagerConfig(RNImageEditor).Commands.addPoint, [parseFloat(x * this._screenScale), parseFloat(y * this._screenScale), true] ); this._path.data.push(`${x},${y}`); this.props.onStrokeChanged(x, y); } }, onPanResponderRelease: (evt, gestureState) => { if (!this.props.touchEnabled) return; if (this._path) { this.props.onStrokeEnd({ path: this._path, size: this._size, drawer: this.props.user }); this._paths.push({ path: this._path, size: this._size, drawer: this.props.user }); } UIManager.dispatchViewManagerCommand( this._handle, UIManager.getViewManagerConfig(RNImageEditor).Commands.endPath, [] ); }, onShouldBlockNativeResponder: (evt, gestureState) => { return this.props.touchEnabled; } }); this.setState({ hasPanResponder: true }); } render() { return ( <RNImageEditor ref={(ref) => { this._handle = ReactNative.findNodeHandle(ref); }} style={this.props.style} onLayout={(e) => { this._size = { width: e.nativeEvent.layout.width, height: e.nativeEvent.layout.height }; this._initialized = true; this._pathsToProcess.length > 0 && this._pathsToProcess.forEach((p) => this.addPath(p)); }} {...(this.state.hasPanResponder ? this.panResponder.panHandlers : undefined)} {...this.panResponder?.panHandlers} onChange={(e) => { if (e.nativeEvent.hasOwnProperty("pathsUpdate")) { this.props.onPathsChange(e.nativeEvent.pathsUpdate); } else if (e.nativeEvent.hasOwnProperty("success") && e.nativeEvent.hasOwnProperty("path")) { this.props.onSketchSaved(e.nativeEvent.success, e.nativeEvent.path); } else if (e.nativeEvent.hasOwnProperty("success")) { this.props.onSketchSaved(e.nativeEvent.success); } else if (e.nativeEvent.hasOwnProperty("isShapeSelected")) { this.props.onShapeSelectionChanged(e.nativeEvent.isShapeSelected); } }} localSourceImage={this.props.localSourceImage} permissionDialogTitle={this.props.permissionDialogTitle} permissionDialogMessage={this.props.permissionDialogMessage} shapeConfiguration={{ shapeBorderColor: processColor(this.props.shapeConfiguration.shapeBorderColor), shapeBorderStyle: this.props.shapeConfiguration.shapeBorderStyle, shapeBorderStrokeWidth: this.props.shapeConfiguration.shapeBorderStrokeWidth, shapeColor: processColor(this.props.strokeColor), shapeStrokeWidth: this.props.strokeWidth }} text={this.state.text} /> ); } } ImageEditor.MAIN_BUNDLE = Platform.OS === "ios" ? UIManager.getViewManagerConfig(RNImageEditor).Constants.MainBundlePath : ""; ImageEditor.DOCUMENT = Platform.OS === "ios" ? UIManager.getViewManagerConfig(RNImageEditor).Constants.NSDocumentDirectory : ""; ImageEditor.LIBRARY = Platform.OS === "ios" ? UIManager.getViewManagerConfig(RNImageEditor).Constants.NSLibraryDirectory : ""; ImageEditor.CACHES = Platform.OS === "ios" ? UIManager.getViewManagerConfig(RNImageEditor).Constants.NSCachesDirectory : ""; module.exports = ImageEditor;