diff --git a/.gitignore b/.gitignore index 752b9500..5343b86c 100644 --- a/.gitignore +++ b/.gitignore @@ -180,3 +180,4 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk +/nbproject/private/ \ No newline at end of file diff --git a/examples/main.jsx b/examples/main.jsx index 6d242d54..8d971882 100644 --- a/examples/main.jsx +++ b/examples/main.jsx @@ -32,6 +32,8 @@ import RemoveIcon from '@material-ui/icons/Remove'; import DownloadIcon from '@material-ui/icons/CloudDownload'; import ZoomInIcon from '@material-ui/icons/ZoomIn'; import ZoomOutIcon from '@material-ui/icons/ZoomOut'; +import UploadIcon from '@material-ui/icons/Backup'; + import dataJson from './data.json'; import dataJsonControlled from './data.json.controlled'; import {SketchField, Tools} from '../src'; @@ -40,6 +42,10 @@ import DropZone from 'react-dropzone'; import Toolbar from '@material-ui/core/Toolbar/Toolbar'; import Typography from '@material-ui/core/Typography/Typography'; +import { makeStyles } from '@material-ui/core/styles'; +import Modal from '@material-ui/core/Modal'; + + const styles = { root: { padding: '3px', @@ -91,6 +97,11 @@ const styles = { }, card: { margin: '10px 10px 5px 0' + }, + modal: { + top: '40%', + left: '50%', + transform: 'translate(50%, 50%)' } }; @@ -101,6 +112,7 @@ const styles = { * @param etype the event type */ function eventFire(el, etype) { + if (el.fireEvent) { el.fireEvent('on' + etype); } else { @@ -110,7 +122,11 @@ function eventFire(el, etype) { } } + + + class SketchFieldDemo extends React.Component { + constructor(props) { super(props); @@ -121,7 +137,7 @@ class SketchFieldDemo extends React.Component { backgroundColor: 'transparent', shadowWidth: 0, shadowOffset: 0, - tool: Tools.Pencil, + tool: Tools.Select, enableRemoveSelected: false, fillWithColor: false, fillWithBackgroundColor: false, @@ -129,8 +145,8 @@ class SketchFieldDemo extends React.Component { canUndo: false, canRedo: false, controlledSize: false, - sketchWidth: 600, - sketchHeight: 600, + sketchWidth: (window.innerWidth - 400), + sketchHeight: (window.innerHeight - 200), stretched: true, stretchedX: false, stretchedY: false, @@ -143,8 +159,13 @@ class SketchFieldDemo extends React.Component { expandBack: false, expandImages: false, expandControlled: false, - text: 'a text, cool!', + text: 'Testing!', enableCopyPaste: false, + isOpen: true, + dataJson: dataJson, + url: dataUrl, + maxHeight: (window.innerHeight + 800), + isOpen: true }; } @@ -157,24 +178,107 @@ class SketchFieldDemo extends React.Component { }; _save = () => { - let drawings = this.state.drawings; - drawings.push(this._sketch.toDataURL()); - this.setState({ drawings: drawings }); + var array = [] + let canvas = this._sketch._fc; + var stObject = canvas._objects; + for (var key in stObject) { + var obArray = [] + var orgState = stObject[key].__originalState; + if(orgState){ + if(orgState.objects){ + for(var j = 0; j < orgState.objects.length; j++){ + var newHash = this._getHash(orgState.objects[j]); + obArray.push(newHash); + } + } + var hash = this._getHash(stObject[key].__originalState, obArray); + array.push(hash) + } + } + + var objects = { "objects": array , background: canvas.backgroundColor } + + if(canvas.backgroundImage){ + var url = canvas.backgroundImage._element.src; + this.setState({url: url}) + } + + this.setState({isOpen: true , objects: objects}); }; + _open = () => { + this._sketch.changeBackground(this.state.url) + if(this.state.objects.objects.length > 0){ + this._sketch.fromJSON(this.state.objects); + } + } + _download = () => { - console.save(this._sketch.toDataURL(), 'toDataURL.txt'); - console.save(JSON.stringify(this._sketch.toJSON()), 'toDataJSON.txt'); + this._sketch.download(); + }; - /*eslint-enable no-console*/ - let { imgDown } = this.refs; - let event = new Event('click', {}); + _getHash = (obj, obArray) => { - imgDown.href = this._sketch.toDataURL(); - imgDown.download = 'toPNG.png'; - imgDown.dispatchEvent(event); - }; + var hash = { + type: obj.type, + originX: obj.originX, + originY: obj.originY, + left: obj.left, + top: obj.top, + width: obj.width, + height: obj.height, + fill: obj.fill, + stroke: obj.stroke, + strokeWidth: obj.strokeWidth, + strokeDashArray: obj.strokeDashArray, + strokeLineCap: obj.strokeLineCap, + strokeLineJoin: obj.strokeLineJoin, + strokeMiterLimit: obj.strokeMiterLimit, + scaleX: obj.scaleX, + scaleY: obj.scaleY, + angle: obj.angle, + flipX: obj.flipX, + flipY: obj.flipY, + opacity: obj.opacity, + shadow: obj.shadow, + visible: obj.visible, + clipTo: obj.clipTo, + backgroundColor: obj.backgroundColor, + fillRule: obj.fillRule, + paintFirst: obj.paintFirst, + globalCompositeOperation: obj.globalCompositeOperation, + transformMatrix: obj.transformMatrix, + skewX: obj.skewX, + skewY: obj.skewY, + path: obj.path, + x1: obj.x1, + x2: obj.x2, + y1: obj.y1, + y2: obj.y2, + objects: obArray, + radius: obj.radius, + startAngle: obj.startAngle, + endAngle: obj.endAngle, + text: obj.text, + fontSize: obj.fontSize, + fontWeight: obj.fontWeight, + fontFamily: obj.fontFamily, + fontStyle: obj.fontStyle, + lineHeight: obj.lineHeight, + underline: obj.underline, + overline: obj.overline, + linethrough: obj.linethrough, + textAlign: obj.textAlign, + textBackgroundColor: obj.textBackgroundColor, + charSpacing: obj.charSpacing, + crossOrigin: "", + cropX: obj.cropX, + cropY: obj.cropY, + src: obj.src, + } + return hash; + } _renderTile = (drawing, index) => { return ( @@ -243,21 +347,22 @@ class SketchFieldDemo extends React.Component { } }; - _onBackgroundImageDrop = (accepted /*, rejected*/) => { + _onBackgroundImageDrop = (accepted) => { if (accepted && accepted.length > 0) { let sketch = this._sketch; let reader = new FileReader(); + let { stretched, stretchedX, stretchedY, originX, originY } = this.state; reader.addEventListener( 'load', - () => - sketch.setBackgroundFromDataUrl(reader.result, { + () => sketch.setBackgroundFromDataUrl(reader.result, { stretched: stretched, stretchedX: stretchedX, stretchedY: stretchedY, originX: originX, originY: originY, }), + false, ); reader.readAsDataURL(accepted[0]); @@ -266,11 +371,37 @@ class SketchFieldDemo extends React.Component { _addText = () => this._sketch.addText(this.state.text); + _readFile = (event) => { + + this._clear(); + let file = event.target.files[0]; + + if(file){ + let reader = new FileReader(); + reader.addEventListener( + 'load', + () => this._loadData(reader.result), false, + ); + reader.readAsText(file); + } + } + + _loadData = (data) =>{ + data = JSON.parse(data); + this.setState({height: data.height, width: data.width}) + let sketch = this._sketch; + sketch.fromJSON(data); + sketch._backgroundColor(data.backgroundColor); + if(data.backImg){ + sketch.changeBackground(data.backImg) + } + } + componentDidMount = () => { (function(console) { console.save = function(data, filename) { + if (!data) { - console.error('Console.save: No data'); return; } if (!filename) filename = 'console.json'; @@ -287,8 +418,29 @@ class SketchFieldDemo extends React.Component { a.dispatchEvent(e); }; })(console); + }; + _handleClose = () =>{ + this.setState({isOpen: false}) + } + + _setInput1 = (event) =>{ + this.setState({newHeight: event.target.value}) + } + _setInput2 = (event) =>{ + this.setState({newWidth: event.target.value}) + } + + _saveClick = () =>{ + + if(this.state.newWidth && this.state.newHeight){ + this.setState({ height: parseInt(this.state.newHeight), width: parseInt(this.state.newWidth)}) + } + this.setState({isOpen: false}) + console.log(this.state.height) + } + render = () => { let { controlledValue } = this.state; const theme = createMuiTheme({ @@ -300,37 +452,66 @@ class SketchFieldDemo extends React.Component { secondary: { main: '#11cb5f' }, // This is just green.A700 as hex. }, }); + + return (
- + + Sketch Tool + + + + + + + + @@ -338,6 +519,38 @@ class SketchFieldDemo extends React.Component {
+ +
+ this._handleClose()} + aria-labelledby="simple-modal-title" + aria-describedby="simple-modal-description"> +
+
+

Select Size

+
+ +
+ + +
+
+ +
+ + +
+ +
+ +
+
+
+
+
(this._sketch = c)} lineColor={this.state.lineColor} lineWidth={this.state.lineWidth} + removeItem={this._removeSelected} fillColor={ this.state.fillWithColor ? this.state.fillColor @@ -357,18 +571,19 @@ class SketchFieldDemo extends React.Component { : 'transparent' } width={ - this.state.controlledSize ? this.state.sketchWidth : null + this.state.controlledSize ? this.state.sketchWidth : this.state.width } height={ - this.state.controlledSize ? this.state.sketchHeight : null + this.state.controlledSize ? this.state.sketchHeight : this.state.height } - defaultValue={dataJson} + defaultValue={this.state.dataJson} value={controlledValue} forceValue onChange={this._onSketchChange} tool={this.state.tool} />
+
this.setState({ expandImages: !this.state.expandImages })}> + this.setState({ expandImages: !this.state.expandImages })}> }/> +
@@ -627,9 +842,7 @@ class SketchFieldDemo extends React.Component { value={this.state.imageUrl}/>
@@ -642,6 +855,8 @@ class SketchFieldDemo extends React.Component {
+ +
- {/* Sketch area */} -
-
diff --git a/src/SketchField.jsx b/src/SketchField.jsx index 5e8b6f7c..2caf000e 100644 --- a/src/SketchField.jsx +++ b/src/SketchField.jsx @@ -1,8 +1,10 @@ -/*eslint no-unused-vars: 0*/ + /*eslint no-unused-vars: 0*/ -import React, {PureComponent} from 'react'; +import React, {PureComponent, useEffect} from 'react'; import PropTypes from 'prop-types'; + import History from './history'; + import {uuid4} from './utils'; import Select from './select'; import Pencil from './pencil'; @@ -18,6 +20,7 @@ const fabric = require('fabric').fabric; /** * Sketch Tool based on FabricJS for React Applications */ + class SketchField extends PureComponent { static propTypes = { @@ -60,6 +63,8 @@ class SketchField extends PureComponent { style: PropTypes.object, }; + + static defaultProps = { lineColor: 'black', lineWidth: 10, @@ -67,7 +72,7 @@ class SketchField extends PureComponent { backgroundColor: 'transparent', opacity: 1.0, undoSteps: 25, - tool: Tool.Pencil, + tool: Tool.Circle, widthCorrection: 2, heightCorrection: 0, forceValue: false @@ -75,8 +80,10 @@ class SketchField extends PureComponent { state = { parentWidth: 550, - action: true + action: true, + save: null, }; + _initTools = (fabricCanvas) => { this._tools = {}; this._tools[Tool.Select] = new Select(fabricCanvas); @@ -120,7 +127,9 @@ class SketchField extends PureComponent { * } */ addImg = (dataUrl, options = {}) => { + let canvas = this._fc; + fabric.Image.fromURL(dataUrl, (oImg) => { let opts = { left: Math.random() * (canvas.getWidth() - oImg.width * 0.5), @@ -179,11 +188,13 @@ class SketchField extends PureComponent { _onObjectModified = (e) => { let obj = e.target; obj.__version += 1; + let prevState = JSON.stringify(obj.__originalState); let objState = obj.toJSON(); - // record current object state as json and update to originalState + obj.__originalState = objState; let currState = JSON.stringify(objState); + this._history.keep([obj, prevState, currState]); }; @@ -229,8 +240,7 @@ class SketchField extends PureComponent { _onMouseUp = (e) => { this._selectedTool.doMouseUp(e); // Update the final state to new-generated object - // Ignore Path object since it would be created after mouseUp - // Assumed the last object in canvas.getObjects() in the newest object + if (this.props.tool !== Tool.Pencil) { const canvas = this._fc; const objects = canvas.getObjects(); @@ -294,6 +304,14 @@ class SketchField extends PureComponent { canvas.calcOffset(); }; + _canvansSize = () =>{ + let canvas = this._fc; + console.log(canvas); + canvas.height = 500; + canvas.renderAll() + console.log(canvas) + } + /** * Sets the background color for this sketch * @param color in rgba or hex format @@ -315,6 +333,8 @@ class SketchField extends PureComponent { zoom = (factor) => { let canvas = this._fc; let objects = canvas.getObjects(); + + for (let i in objects) { objects[i].scaleX = objects[i].scaleX * factor; objects[i].scaleY = objects[i].scaleY * factor; @@ -356,10 +376,10 @@ class SketchField extends PureComponent { * Perform a redo operation on canvas, if it cannot redo it will leave the canvas intact */ redo = () => { + let history = this._history; if (history.canRedo()) { let canvas = this._fc; - //noinspection Eslint let [obj, prevState, currState] = history.redo(); if (obj.__version === 0) { this.setState({ action: false }, () => { @@ -396,62 +416,26 @@ class SketchField extends PureComponent { return this._history.canRedo() }; - /** - * Exports canvas element to a dataurl image. Note that when multiplier is used, cropping is scaled appropriately - * - * Available Options are - * - * - * - * - * - * - * - * - * - * - * - *
NameTypeArgumentDefaultDescription
format String pngThe format of the output image. Either "jpeg" or "png"
qualityNumber1Quality level (0..1). Only used for jpeg.
multiplierNumber1Multiplier to scale by
leftNumberCropping left offset. Introduced in v1.2.14
topNumberCropping top offset. Introduced in v1.2.14
widthNumberCropping width. Introduced in v1.2.14
heightNumberCropping height. Introduced in v1.2.14
- * - * @returns {String} URL containing a representation of the object in the format specified by options.format - */ - toDataURL = (options) => this._fc.toDataURL(options); + toDataURL = (options) => { + this._fc.toDataURL(options); + } - /** - * Returns JSON representation of canvas - * - * @param propertiesToInclude Array Any properties that you might want to additionally include in the output - * @returns {string} JSON string - */ toJSON = (propertiesToInclude) => this._fc.toJSON(propertiesToInclude); - /** - * Populates canvas with data from the specified JSON. - * - * JSON format must conform to the one of fabric.Canvas#toDatalessJSON - * - * @param json JSON string or object - */ fromJSON = (json) => { + var canvas = this._fc; if (!json) return; - let canvas = this._fc; - setTimeout(() => { + //setTimeout(() => { canvas.loadFromJSON(json, () => { canvas.renderAll(); if (this.props.onChange) { - this.props.onChange() + // this.props.onChange() } }) - }, 100) + // }, 100) + }; - /** - * Clear the content of the canvas, this will also clear history but will return the canvas content as JSON to be - * used as needed in order to undo the clear if possible - * - * @param propertiesToInclude Array Any properties that you might want to additionally include in the output - * @returns {string} JSON string of the canvas just cleared - */ clear = (propertiesToInclude) => { let discarded = this.toJSON(propertiesToInclude); this._fc.clear(); @@ -459,9 +443,6 @@ class SketchField extends PureComponent { return discarded }; - /** - * Remove selected object from the canvas - */ removeSelected = () => { let canvas = this._fc; let activeObj = canvas.getActiveObject(); @@ -487,7 +468,6 @@ class SketchField extends PureComponent { copy = () => { let canvas = this._fc; - canvas.getActiveObject().clone(cloned => this._clipboard = cloned); }; paste = () => { @@ -526,7 +506,7 @@ class SketchField extends PureComponent { if (options.stretched) { delete options.stretched; Object.assign(options, { - width: canvas.width, + width: canvas.width , height: canvas.height }) } @@ -549,14 +529,30 @@ class SketchField extends PureComponent { img.src = dataUrl }; + changeBackground = (imgData) =>{ + let canvas = this._fc; + setTimeout(() => { + var img = new Image(); + img.onload = function() { + // this is syncronous + var f_img = new fabric.Image(img); + canvas.setBackgroundImage(f_img); + canvas.renderAll(); + }; + img.src = imgData; + }, 3000) + } + addText = (text, options = {}) => { let canvas = this._fc; let iText = new fabric.IText(text, options); + let opts = { left: (canvas.getWidth() - iText.width) * 0.5, top: (canvas.getHeight() - iText.height) * 0.5, }; Object.assign(options, opts); + iText.set({ 'left': options.left, 'top': options.top @@ -566,32 +562,17 @@ class SketchField extends PureComponent { }; componentDidMount = () => { - let { - tool, - value, - undoSteps, - defaultValue, - backgroundColor - } = this.props; - - let canvas = this._fc = new fabric.Canvas(this._canvas/*, { - preserveObjectStacking: false, - renderOnAddRemove: false, - skipTargetFind: true - }*/); - + let { tool, value, undoSteps, defaultValue, backgroundColor } = this.props; + let canvas = this._fc = new fabric.Canvas(this._canvas); this._initTools(canvas); - - // set initial backgroundColor this._backgroundColor(backgroundColor) - let selectedTool = this._tools[tool]; + selectedTool.configureCanvas(this.props); this._selectedTool = selectedTool; - - // Control resize window.addEventListener('resize', this._resize, false); - + window.addEventListener('keydown', this.deleteKey, false) + // Initialize History, with maximum number of undo steps this._history = new History(undoSteps); @@ -606,22 +587,18 @@ class SketchField extends PureComponent { canvas.on('object:moving', this._onObjectMoving); canvas.on('object:scaling', this._onObjectScaling); canvas.on('object:rotating', this._onObjectRotating); - // IText Events fired on Adding Text - // canvas.on("text:event:changed", console.log) - // canvas.on("text:selection:changed", console.log) - // canvas.on("text:editing:entered", console.log) - // canvas.on("text:editing:exited", console.log) - this.disableTouchScroll(); + this.disableTouchScroll(); this._resize(); - - // initialize canvas with controlled value if exists (value || defaultValue) && this.fromJSON(value || defaultValue); - }; - componentWillUnmount = () => window.removeEventListener('resize', this._resize); + + componentWillUnmount = () => { + window.removeEventListener('resize', this._resize); + window.removeEventListener('keydown', this.deleteKey, false) + } componentDidUpdate = (prevProps, prevState) => { if (this.state.parentWidth !== prevState.parentWidth @@ -630,11 +607,9 @@ class SketchField extends PureComponent { this._resize() } - if (this.props.tool !== prevProps.tool) { this._selectedTool = this._tools[this.props.tool] || this._tools[Tool.Pencil] } - //Bring the cursor back to default if it is changed by a tool this._fc.defaultCursor = 'default'; this._selectedTool.configureCanvas(this.props); @@ -648,32 +623,54 @@ class SketchField extends PureComponent { } }; + /** + * Delete key Commands for selection deleted. + * @event params + */ + deleteKey = (event) =>{ + if(event.code == "Delete" || event.code == "Backspace"){ + this.props.removeItem() + } + } + + /** + * Download Content + */ + download = () => { + var url = null; + if(this._fc.backgroundImage){ + url = this._fc.backgroundImage._element.src; + } + let hash = { height: this._fc.height, width: this._fc.width, + backgroundColor: this._fc.backgroundColor, viewportTransform: this._fc.viewportTransform, + backImg: url, + } + this._history.saveToFile(hash); + } + + + render = () => { - let { - className, - style, - width, - height - } = this.props; + let { className, style, width, height } = this.props; + let canvasDivStyle = Object.assign({}, style ? style : {}, width ? { width: width } : {}, height ? { height: height } : { height: 512 }); - return ( -
this._container = c} - style={canvasDivStyle}> - this._canvas = c}> - Sorry, Canvas HTML5 element is not supported by your browser - :( - -
+
this._container = c} + style={canvasDivStyle}> + this._canvas = c}> + Sorry, Canvas HTML5 element is not supported by your browser + :( + +
) } } -export default SketchField \ No newline at end of file +export default SketchField diff --git a/src/fabrictool.js b/src/fabrictool.js index 05125b98..94d66996 100644 --- a/src/fabrictool.js +++ b/src/fabrictool.js @@ -29,4 +29,4 @@ class FabricCanvasTool { } } -export default FabricCanvasTool; \ No newline at end of file +export default FabricCanvasTool; diff --git a/src/history.js b/src/history.js index e0e54f8b..2bed80a4 100644 --- a/src/history.js +++ b/src/history.js @@ -2,7 +2,7 @@ * Maintains the history of an object */ class History { - constructor(undoLimit = 10, debug = false) { + constructor(undoLimit = 1000, debug = false) { this.undoLimit = undoLimit; this.undoList = []; this.redoList = []; @@ -35,18 +35,20 @@ class History { * * @param obj */ + + keep(obj) { + try { - this.redoList = []; - if (this.current) { - this.undoList.push(this.current); - } - if (this.undoList.length > this.undoLimit) { - this.undoList.shift(); - } - this.current = obj; + if (this.current) { + this.undoList.push(this.current); + } + if (this.undoList.length > this.undoLimit) { + this.undoList.shift(); + } + this.current = obj; } finally { - this.print(); + this.print(); } } @@ -80,6 +82,7 @@ class History { * @returns the new current value after the redo operation, or null if no redo operation was possible */ redo() { + try { if (this.redoList.length > 0) { if (this.current) this.undoList.push(this.current); @@ -120,6 +123,42 @@ class History { this.print(); } + /** + * Save actions to a json file + * + * + */ + saveToFile(hash) { + let dataArray = []; + + if(this.current){ + if(this.undoList.length == 0){ + allElement = this.current[0] + dataArray.push(this.current[0]) + } else { + for(let i = 0; i < this.undoList.length; i++){ + dataArray.push(this.undoList[i][0]) + dataArray.push(this.current[0]) + } + } + } + + var hash1 = {"objects": dataArray} + hash["objects"] = dataArray; + const a = document.createElement("a"); + a.href = URL.createObjectURL(new Blob([JSON.stringify(hash, null, 4)], { + type: "text/plain" + })); + a.setAttribute("download", "image.txt"); + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } + + /** + * print object + */ + print() { if (this.debug) { /* eslint-disable no-console */ diff --git a/src/index.js b/src/index.js index d075d846..cf158267 100644 --- a/src/index.js +++ b/src/index.js @@ -1,10 +1,10 @@ import SketchField from './SketchField' import Tools from './tools' -export {SketchField} +export { SketchField } export {Tools} export default { SketchField, Tools -}; \ No newline at end of file +};