=m?p=0:(-1===p||m component higher in the tree to provide a loading indicator or placeholder to display."+ut(c))}Ra=!0,f=ia(f,c),u=s;do{switch(u.tag){case 3:u.effectTag|=2048,u.expirationTime=l,Ji(u,l=wa(u,f,l));break e;case 1:if(p=f,h=u.type,c=u.stateNode,0==(64&u.effectTag)&&("function"==typeof h.getDerivedStateFromError||null!==c&&"function"==typeof c.componentDidCatch&&(null===Aa||!Aa.has(c)))){u.effectTag|=2048,u.expirationTime=l,Ji(u,l=Ea(u,p,l));break e}}u=u.return}while(null!==u)}Pa=Ba(i);continue}o=!0,Rl(t)}}break}if(Ca=!1,xa.current=n,Ai=ji=Di=null,Xo(),o)Ia=null,e.finishedWork=null;else if(null!==Pa)e.finishedWork=null;else{if(null===(n=e.current.alternate)&&a("281"),Ia=null,Ra){if(o=e.latestPendingTime,i=e.latestSuspendedTime,l=e.latestPingedTime,0!==o&&ot?0:t)):(e.pendingCommitExpirationTime=r,e.finishedWork=n)}}function qa(e,t){for(var n=e.return;null!==n;){switch(n.tag){case 1:var r=n.stateNode;if("function"==typeof n.type.getDerivedStateFromError||"function"==typeof r.componentDidCatch&&(null===Aa||!Aa.has(r)))return Xi(n,e=Ea(n,e=ia(t,e),1073741823)),void Xa(n,1073741823);break;case 3:return Xi(n,e=wa(n,e=ia(t,e),1073741823)),void Xa(n,1073741823)}n=n.return}3===e.tag&&(Xi(e,n=wa(e,n=ia(t,e),1073741823)),Xa(e,1073741823))}function Ka(e,t){var n=i.unstable_getCurrentPriorityLevel(),r=void 0;if(0==(1&t.mode))r=1073741823;else if(Ca&&!za)r=Oa;else{switch(n){case i.unstable_ImmediatePriority:r=1073741823;break;case i.unstable_UserBlockingPriority:r=1073741822-10*(1+((1073741822-e+15)/10|0));break;case i.unstable_NormalPriority:r=1073741822-25*(1+((1073741822-e+500)/25|0));break;case i.unstable_LowPriority:case i.unstable_IdlePriority:r=1;break;default:a("313")}null!==Ia&&r===Oa&&--r}return n===i.unstable_UserBlockingPriority&&(0===ll||r=r&&(e.didError=!1,(0===(t=e.latestPingedTime)||t>n)&&(e.latestPingedTime=n),no(n,e),0!==(n=e.expirationTime)&&Sl(e,n)))}function Za(e,t){e.expirationTimeOa&&Ua(),Jr(e,t),Ca&&!za&&Ia===e||Sl(e,e.expirationTime),yl>vl&&(yl=0,a("185")))}function Ja(e,t,n,r,o){return i.unstable_runWithPriority(i.unstable_ImmediatePriority,function(){return e(t,n,r,o)})}var el=null,tl=null,nl=0,rl=void 0,ol=!1,il=null,al=0,ll=0,ul=!1,sl=null,cl=!1,fl=!1,dl=null,pl=i.unstable_now(),hl=1073741822-(pl/10|0),ml=hl,vl=50,yl=0,gl=null;function bl(){hl=1073741822-((i.unstable_now()-pl)/10|0)}function kl(e,t){if(0!==nl){if(te.expirationTime&&(e.expirationTime=t),ol||(cl?fl&&(il=e,al=1073741823,Ol(e,1073741823,!1)):1073741823===t?Pl(1073741823,!1):kl(e,t))}function xl(){var e=0,t=null;if(null!==tl)for(var n=tl,r=el;null!==r;){var o=r.expirationTime;if(0===o){if((null===n||null===tl)&&a("244"),r===r.nextScheduledRoot){el=tl=r.nextScheduledRoot=null;break}if(r===el)el=o=r.nextScheduledRoot,tl.nextScheduledRoot=o,r.nextScheduledRoot=null;else{if(r===tl){(tl=n).nextScheduledRoot=el,r.nextScheduledRoot=null;break}n.nextScheduledRoot=r.nextScheduledRoot,r.nextScheduledRoot=null}r=n.nextScheduledRoot}else{if(o>e&&(e=o,t=r),r===tl)break;if(1073741823===e)break;n=r,r=r.nextScheduledRoot}}il=t,al=e}var _l=!1;function Tl(){return!!_l||!!i.unstable_shouldYield()&&(_l=!0)}function Cl(){try{if(!Tl()&&null!==el){bl();var e=el;do{var t=e.expirationTime;0!==t&&hl<=t&&(e.nextExpirationTimeToWorkOn=hl),e=e.nextScheduledRoot}while(e!==el)}Pl(0,!0)}finally{_l=!1}}function Pl(e,t){if(xl(),t)for(bl(),ml=hl;null!==il&&0!==al&&e<=al&&!(_l&&hl>al);)Ol(il,al,hl>al),xl(),bl(),ml=hl;else for(;null!==il&&0!==al&&e<=al;)Ol(il,al,!1),xl();if(t&&(nl=0,rl=null),0!==al&&kl(il,al),yl=0,gl=null,null!==dl)for(e=dl,dl=null,t=0;t=n&&(null===dl?dl=[r]:dl.push(r),r._defer))return e.finishedWork=t,void(e.expirationTime=0);e.finishedWork=null,e===gl?yl++:(gl=e,yl=0),i.unstable_runWithPriority(i.unstable_ImmediatePriority,function(){$a(e,t)})}function Rl(e){null===il&&a("246"),il.expirationTime=0,ul||(ul=!0,sl=e)}function Ml(e,t){var n=cl;cl=!0;try{return e(t)}finally{(cl=n)||ol||Pl(1073741823,!1)}}function zl(e,t){if(cl&&!fl){fl=!0;try{return e(t)}finally{fl=!1}}return e(t)}function Ll(e,t,n){cl||ol||0===ll||(Pl(ll,!1),ll=0);var r=cl;cl=!0;try{return i.unstable_runWithPriority(i.unstable_UserBlockingPriority,function(){return e(t,n)})}finally{(cl=r)||ol||Pl(1073741823,!1)}}function Dl(e,t,n,r,o){var i=t.current;e:if(n){t:{2===tn(n=n._reactInternalFiber)&&1===n.tag||a("170");var l=n;do{switch(l.tag){case 3:l=l.stateNode.context;break t;case 1:if(zr(l.type)){l=l.stateNode.__reactInternalMemoizedMergedChildContext;break t}}l=l.return}while(null!==l);a("171"),l=void 0}if(1===n.tag){var u=n.type;if(zr(u)){n=Ar(n,u,l);break e}}n=l}else n=Ir;return null===t.context?t.context=n:t.pendingContext=n,t=o,(o=Yi(r)).payload={element:e},null!==(t=void 0===t?null:t)&&(o.callback=t),Va(),Xi(i,o),Xa(i,r),r}function jl(e,t,n,r){var o=t.current;return Dl(e,t,n,o=Ka(El(),o),r)}function Al(e){if(!(e=e.current).child)return null;switch(e.child.tag){case 5:default:return e.child.stateNode}}function Ul(e){var t=1073741822-25*(1+((1073741822-El()+500)/25|0));t>=Ta&&(t=Ta-1),this._expirationTime=Ta=t,this._root=e,this._callbacks=this._next=null,this._hasChildren=this._didComplete=!1,this._children=null,this._defer=!0}function Fl(){this._callbacks=null,this._didCommit=!1,this._onCommit=this._onCommit.bind(this)}function Wl(e,t,n){e={current:t=Br(3,null,null,t?3:0),containerInfo:e,pendingChildren:null,pingCache:null,earliestPendingTime:0,latestPendingTime:0,earliestSuspendedTime:0,latestSuspendedTime:0,latestPingedTime:0,didError:!1,pendingCommitExpirationTime:0,finishedWork:null,timeoutHandle:-1,context:null,pendingContext:null,hydrate:n,nextExpirationTimeToWorkOn:0,expirationTime:0,firstBatch:null,nextScheduledRoot:null},this._internalRoot=t.stateNode=e}function Hl(e){return!(!e||1!==e.nodeType&&9!==e.nodeType&&11!==e.nodeType&&(8!==e.nodeType||" react-mount-point-unstable "!==e.nodeValue))}function Vl(e,t,n,r,o){var i=n._reactRootContainer;if(i){if("function"==typeof o){var a=o;o=function(){var e=Al(i._internalRoot);a.call(e)}}null!=e?i.legacy_renderSubtreeIntoContainer(e,t,o):i.render(t,o)}else{if(i=n._reactRootContainer=function(e,t){if(t||(t=!(!(t=e?9===e.nodeType?e.documentElement:e.firstChild:null)||1!==t.nodeType||!t.hasAttribute("data-reactroot"))),!t)for(var n;n=e.lastChild;)e.removeChild(n);return new Wl(e,!1,t)}(n,r),"function"==typeof o){var l=o;o=function(){var e=Al(i._internalRoot);l.call(e)}}zl(function(){null!=e?i.legacy_renderSubtreeIntoContainer(e,t,o):i.render(t,o)})}return Al(i._internalRoot)}function $l(e,t){var n=2=t;)n=r,r=r._next;e._next=r,null!==n&&(n._next=e)}return e},Re=Ml,Me=Ll,ze=function(){ol||0===ll||(Pl(ll,!1),ll=0)};var Bl={createPortal:$l,findDOMNode:function(e){if(null==e)return null;if(1===e.nodeType)return e;var t=e._reactInternalFiber;return void 0===t&&("function"==typeof e.render?a("188"):a("268",Object.keys(e))),e=null===(e=rn(t))?null:e.stateNode},hydrate:function(e,t,n){return Hl(t)||a("200"),Vl(null,e,t,!0,n)},render:function(e,t,n){return Hl(t)||a("200"),Vl(null,e,t,!1,n)},unstable_renderSubtreeIntoContainer:function(e,t,n,r){return Hl(n)||a("200"),(null==e||void 0===e._reactInternalFiber)&&a("38"),Vl(e,t,n,!1,r)},unmountComponentAtNode:function(e){return Hl(e)||a("40"),!!e._reactRootContainer&&(zl(function(){Vl(null,null,e,!1,function(){e._reactRootContainer=null})}),!0)},unstable_createPortal:function(){return $l.apply(void 0,arguments)},unstable_batchedUpdates:Ml,unstable_interactiveUpdates:Ll,flushSync:function(e,t){ol&&a("187");var n=cl;cl=!0;try{return Ja(e,t)}finally{cl=n,Pl(1073741823,!1)}},unstable_createRoot:function(e,t){return Hl(e)||a("299","unstable_createRoot"),new Wl(e,!0,null!=t&&!0===t.hydrate)},unstable_flushControlled:function(e){var t=cl;cl=!0;try{Ja(e)}finally{(cl=t)||ol||Pl(1073741823,!1)}},__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED:{Events:[D,j,A,I.injectEventPluginsByName,g,$,function(e){T(e,V)},Oe,Ne,Pn,N]}};!function(e){var t=e.findFiberByHostInstance;(function(e){if("undefined"==typeof __REACT_DEVTOOLS_GLOBAL_HOOK__)return!1;var t=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(t.isDisabled||!t.supportsFiber)return!0;try{var n=t.inject(e);Wr=Vr(function(e){return t.onCommitFiberRoot(n,e)}),Hr=Vr(function(e){return t.onCommitFiberUnmount(n,e)})}catch(e){}})(o({},e,{overrideProps:null,currentDispatcherRef:$e.ReactCurrentDispatcher,findHostInstanceByFiber:function(e){return null===(e=rn(e))?null:e.stateNode},findFiberByHostInstance:function(e){return t?t(e):null}}))}({findFiberByHostInstance:L,bundleType:0,version:"16.8.5",rendererPackageName:"react-dom"});var Gl={default:Bl},Ql=Gl&&Bl||Gl;e.exports=Ql.default||Ql},function(e,t,n){"use strict";e.exports=n(8)},function(e,t,n){"use strict";(function(e){
23 | /** @license React v0.13.5
24 | * scheduler.production.min.js
25 | *
26 | * Copyright (c) Facebook, Inc. and its affiliates.
27 | *
28 | * This source code is licensed under the MIT license found in the
29 | * LICENSE file in the root directory of this source tree.
30 | */
31 | Object.defineProperty(t,"__esModule",{value:!0});var n=null,r=!1,o=3,i=-1,a=-1,l=!1,u=!1;function s(){if(!l){var e=n.expirationTime;u?S():u=!0,E(d,e)}}function c(){var e=n,t=n.next;if(n===t)n=null;else{var r=n.previous;n=r.next=t,t.previous=r}e.next=e.previous=null,r=e.callback,t=e.expirationTime,e=e.priorityLevel;var i=o,l=a;o=e,a=t;try{var u=r()}finally{o=i,a=l}if("function"==typeof u)if(u={callback:u,priorityLevel:e,expirationTime:t,next:null,previous:null},null===n)n=u.next=u.previous=u;else{r=null,e=n;do{if(e.expirationTime>=t){r=e;break}e=e.next}while(e!==n);null===r?r=n:r===n&&(n=u,s()),(t=r.previous).next=r.previous=u,u.next=r,u.previous=t}}function f(){if(-1===i&&null!==n&&1===n.priorityLevel){l=!0;try{do{c()}while(null!==n&&1===n.priorityLevel)}finally{l=!1,null!==n?s():u=!1}}}function d(e){l=!0;var o=r;r=e;try{if(e)for(;null!==n;){var i=t.unstable_now();if(!(n.expirationTime<=i))break;do{c()}while(null!==n&&n.expirationTime<=i)}else if(null!==n)do{c()}while(null!==n&&!x())}finally{l=!1,r=o,null!==n?s():u=!1,f()}}var p,h,m=Date,v="function"==typeof setTimeout?setTimeout:void 0,y="function"==typeof clearTimeout?clearTimeout:void 0,g="function"==typeof requestAnimationFrame?requestAnimationFrame:void 0,b="function"==typeof cancelAnimationFrame?cancelAnimationFrame:void 0;function k(e){p=g(function(t){y(h),e(t)}),h=v(function(){b(p),e(t.unstable_now())},100)}if("object"==typeof performance&&"function"==typeof performance.now){var w=performance;t.unstable_now=function(){return w.now()}}else t.unstable_now=function(){return m.now()};var E,S,x,_=null;if("undefined"!=typeof window?_=window:void 0!==e&&(_=e),_&&_._schedMock){var T=_._schedMock;E=T[0],S=T[1],x=T[2],t.unstable_now=T[3]}else if("undefined"==typeof window||"function"!=typeof MessageChannel){var C=null,P=function(e){if(null!==C)try{C(e)}finally{C=null}};E=function(e){null!==C?setTimeout(E,0,e):(C=e,setTimeout(P,0,!1))},S=function(){C=null},x=function(){return!1}}else{"undefined"!=typeof console&&("function"!=typeof g&&console.error("This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills"),"function"!=typeof b&&console.error("This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills"));var I=null,O=!1,N=-1,R=!1,M=!1,z=0,L=33,D=33;x=function(){return z<=t.unstable_now()};var j=new MessageChannel,A=j.port2;j.port1.onmessage=function(){O=!1;var e=I,n=N;I=null,N=-1;var r=t.unstable_now(),o=!1;if(0>=z-r){if(!(-1!==n&&n<=r))return R||(R=!0,k(U)),I=e,void(N=n);o=!0}if(null!==e){M=!0;try{e(o)}finally{M=!1}}};var U=function(e){if(null!==I){k(U);var t=e-z+D;tt&&(t=8),D=tt?A.postMessage(void 0):R||(R=!0,k(U))},S=function(){I=null,O=!1,N=-1}}t.unstable_ImmediatePriority=1,t.unstable_UserBlockingPriority=2,t.unstable_NormalPriority=3,t.unstable_IdlePriority=5,t.unstable_LowPriority=4,t.unstable_runWithPriority=function(e,n){switch(e){case 1:case 2:case 3:case 4:case 5:break;default:e=3}var r=o,a=i;o=e,i=t.unstable_now();try{return n()}finally{o=r,i=a,f()}},t.unstable_next=function(e){switch(o){case 1:case 2:case 3:var n=3;break;default:n=o}var r=o,a=i;o=n,i=t.unstable_now();try{return e()}finally{o=r,i=a,f()}},t.unstable_scheduleCallback=function(e,r){var a=-1!==i?i:t.unstable_now();if("object"==typeof r&&null!==r&&"number"==typeof r.timeout)r=a+r.timeout;else switch(o){case 1:r=a+-1;break;case 2:r=a+250;break;case 5:r=a+1073741823;break;case 4:r=a+1e4;break;default:r=a+5e3}if(e={callback:e,priorityLevel:o,expirationTime:r,next:null,previous:null},null===n)n=e.next=e.previous=e,s();else{a=null;var l=n;do{if(l.expirationTime>r){a=l;break}l=l.next}while(l!==n);null===a?a=n:a===n&&(n=e,s()),(r=a.previous).next=a.previous=e,e.next=a,e.previous=r}return e},t.unstable_cancelCallback=function(e){var t=e.next;if(null!==t){if(t===e)n=null;else{e===n&&(n=t);var r=e.previous;r.next=t,t.previous=r}e.next=e.previous=null}},t.unstable_wrapCallback=function(e){var n=o;return function(){var r=o,a=i;o=n,i=t.unstable_now();try{return e.apply(this,arguments)}finally{o=r,i=a,f()}}},t.unstable_getCurrentPriorityLevel=function(){return o},t.unstable_shouldYield=function(){return!r&&(null!==n&&n.expirationTimeR.length&&R.push(e)}function L(e,t,n){return null==e?0:function e(t,n,r,o){var l=typeof t;"undefined"!==l&&"boolean"!==l||(t=null);var u=!1;if(null===t)u=!0;else switch(l){case"string":case"number":u=!0;break;case"object":switch(t.$$typeof){case i:case a:u=!0}}if(u)return r(o,t,""===n?"."+D(t,0):n),1;if(u=0,n=""===n?".":n+":",Array.isArray(t))for(var s=0;s {
25 | this.events.changeStrokeWidth(this.props.widthEl.value);
26 | };
27 | this.props.colorEl.onchange = () => {
28 | this.events.changeStrokeColor(this.props.colorEl.value);
29 | };
30 | this.props.pasteEl.onclick = () => {
31 | const url = this.props.imageEl.value;
32 | if (url.endsWith('.png')) {
33 | SvgConverter.fromPngImage(url)
34 | .then(image => {
35 | this.events.pasteImage(0, 0, image.width, image.height, image.dataUrl);
36 | });
37 | } else if (url.endsWith('.jpeg') || url.endsWith('.jpg')) {
38 | SvgConverter.fromJpegImage(url)
39 | .then(image => {
40 | this.events.pasteImage(0, 0, image.width, image.height, image.dataUrl);
41 | });
42 | } else if (url.endsWith('.gif')) {
43 | SvgConverter.fromGifImage(url)
44 | .then(image => {
45 | this.events.pasteImage(0, 0, image.width, image.height, image.dataUrl);
46 | });
47 | }
48 | };
49 | }
50 |
51 | render() {
52 | return (
53 |
55 | );
56 | }
57 | }
58 |
59 |
60 | const containerEl = document.getElementById('root');
61 | const widthEl = document.getElementById('width');
62 | const colorEl = document.getElementById('color');
63 | const imageEl = document.getElementById('image');
64 | const pasteEl = document.getElementById('paste');
65 |
66 | ReactDOM.render(, containerEl);
67 |
--------------------------------------------------------------------------------
/docs/js/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ohtomi/react-whiteboard-demo",
3 | "version": "1.0.0",
4 | "description": "A demo for @ohtomi/react-whiteboard.",
5 | "license": "MIT",
6 | "author": "Kenichi Ohtomi",
7 | "scripts": {
8 | "compile": "webpack --progress --profile"
9 | },
10 | "devDependencies": {
11 | "@babel/core": "^7.4.0",
12 | "@babel/plugin-proposal-export-default-from": "^7.2.0",
13 | "@babel/plugin-proposal-export-namespace-from": "^7.2.0",
14 | "@babel/preset-env": "^7.4.2",
15 | "@babel/preset-react": "^7.0.0",
16 | "@ohtomi/react-whiteboard": "^0.1.2",
17 | "babel-loader": "^8.0.5",
18 | "react": "^16.8.5",
19 | "react-dom": "^16.8.5",
20 | "webpack": "^4.37.0",
21 | "webpack-cli": "^3.3.4"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/docs/js/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | entry: './index.js',
5 | output: {
6 | path: path.resolve(__dirname),
7 | filename: 'demo.js'
8 | },
9 | module: {
10 | rules: [{
11 | test: /\.jsx?$/,
12 | exclude: /node_modules/,
13 | use: {
14 | loader: 'babel-loader'
15 | }
16 | }]
17 | },
18 | mode: process.env.WEBPACK_SERVE ? 'development' : 'production'
19 | };
20 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 'roots': [
3 | '/src'
4 | ],
5 | 'transform': {
6 | '^.+\\.tsx?$': 'ts-jest'
7 | },
8 | 'testRegex': '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
9 | 'moduleFileExtensions': [
10 | 'ts',
11 | 'tsx',
12 | 'js',
13 | 'jsx',
14 | 'json',
15 | 'node'
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ohtomi/react-whiteboard",
3 | "version": "0.1.2",
4 | "description": "A whiteboard React component using SVG",
5 | "keywords": [
6 | "react",
7 | "svg",
8 | "whiteboard"
9 | ],
10 | "homepage": "https://github.com/ohtomi/react-whiteboard",
11 | "bugs": {
12 | "url": "https://github.com/ohtomi/react-whiteboard/issues"
13 | },
14 | "license": "MIT",
15 | "author": "Kenichi Ohtomi",
16 | "files": [
17 | "dist"
18 | ],
19 | "main": "dist/index.js",
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/ohtomi/react-whiteboard.git"
23 | },
24 | "scripts": {
25 | "build": "run-s compile test",
26 | "clear": "rimraf dist coverage",
27 | "compile": "webpack --progress --profile",
28 | "start": "run-s storybook",
29 | "storybook": "start-storybook -p 9080",
30 | "test": "jest --coverage",
31 | "watch": "run-p watch:*",
32 | "watch:src": "webpack --watch",
33 | "watch:test": "jest --watch --onlyChanged --notify --notifyMode failure"
34 | },
35 | "dependencies": {},
36 | "devDependencies": {
37 | "@storybook/react": "^5.1.8",
38 | "@types/jest": "^24.0.18",
39 | "@types/node": "^12.11.2",
40 | "@types/react": "^16.9.2",
41 | "@types/react-dom": "^16.9.0",
42 | "jest": "^24.9.0",
43 | "npm-run-all": "^4.1.5",
44 | "react": "^16.8.5",
45 | "react-dom": "^16.8.5",
46 | "rimraf": "^3.0.0",
47 | "ts-jest": "^24.1.0",
48 | "ts-loader": "^6.1.0",
49 | "typescript": "^3.6.3",
50 | "webpack": "^4.37.0",
51 | "webpack-cli": "^3.3.4"
52 | },
53 | "peerDependencies": {
54 | "react": "^16.8.5",
55 | "react-dom": "^16.8.5"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/CanvasPane.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import {AnyReducedEvent, EventStore, ImageData, isReducedImageEvent, isReducedLineEvent, PointData} from './EventStore'
4 |
5 |
6 | type Props = {
7 | eventStore: EventStore,
8 | width: number,
9 | height: number,
10 | style: {
11 | backgroundColor: string
12 | }
13 | }
14 |
15 | type State = {}
16 |
17 | export class CanvasPane extends React.Component {
18 |
19 | props: Props
20 | state: State
21 |
22 | svgElement?: SVGSVGElement
23 |
24 | constructor(props: Props) {
25 | super(props)
26 |
27 | this.svgElement = null
28 | }
29 |
30 | getSvgElement(): SVGSVGElement | undefined {
31 | return this.svgElement
32 | }
33 |
34 | render() {
35 | const canvasLayerStyle = {
36 | position: 'absolute' as 'absolute',
37 | width: this.props.width,
38 | height: this.props.height
39 | }
40 |
41 | return (
42 |
43 |
50 |
51 | )
52 | }
53 |
54 | drawWhiteboardCanvas(): Array | undefined> {
55 | return this.props.eventStore.reduceEvents().map((element: AnyReducedEvent, index: number): React.ComponentElement | undefined => {
56 | if (isReducedLineEvent(element)) {
57 | const key = index
58 | const d = element.values.map((point: PointData, index: number) => {
59 | if (index === 0) {
60 | return 'M ' + point.x + ' ' + point.y
61 | } else {
62 | return 'L ' + point.x + ' ' + point.y
63 | }
64 | })
65 |
66 | return (
67 |
68 | )
69 |
70 | } else if (isReducedImageEvent(element)) {
71 | const key = index
72 | const image: ImageData = element.image
73 |
74 | return (
75 |
76 | )
77 |
78 | } else {
79 | return null
80 | }
81 | })
82 | }
83 |
84 | drawImageBorder(): React.ComponentElement | undefined {
85 | const lastImage = this.props.eventStore.lastImage()
86 | if (!lastImage) {
87 | return null
88 | }
89 |
90 | return (
91 |
93 | )
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/Constants.ts:
--------------------------------------------------------------------------------
1 | export enum ModeEnum {
2 | HAND = 'HAND',
3 | DRAW_LINE = 'DRAW_LINE',
4 | DRAG_IMAGE = 'DRAG_IMAGE',
5 | NW_RESIZE_IMAGE = 'NW_RESIZE_IMAGE',
6 | NE_RESIZE_IMAGE = 'NE_RESIZE_IMAGE',
7 | SE_RESIZE_IMAGE = 'SE_RESIZE_IMAGE',
8 | SW_RESIZE_IMAGE = 'SW_RESIZE_IMAGE'
9 | }
10 |
11 | export enum SvgElementEnum {
12 | LINE = 'LINE',
13 | IMAGE = 'IMAGE'
14 | }
15 |
16 | export type ResizeImageDirection =
17 | typeof ModeEnum.NW_RESIZE_IMAGE |
18 | typeof ModeEnum.NE_RESIZE_IMAGE |
19 | typeof ModeEnum.SE_RESIZE_IMAGE |
20 | typeof ModeEnum.SW_RESIZE_IMAGE
21 |
--------------------------------------------------------------------------------
/src/CursorPane.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import {ModeEnum, ResizeImageDirection} from './Constants'
4 | import {EventStream} from './EventStream'
5 | import {EventStore, PointData} from './EventStore'
6 |
7 |
8 | type Props = {
9 | events: EventStream,
10 | eventStore: EventStore,
11 | width: number,
12 | height: number,
13 | mode: ModeEnum,
14 | strokeWidth: number,
15 | strokeColor: string
16 | }
17 |
18 | type State = {
19 | dragStart?: PointData,
20 | resizeStart?: PointData
21 | }
22 |
23 | export class CursorPane extends React.Component {
24 |
25 | props: Props
26 | state: State
27 |
28 | dragHandle?: HTMLElement
29 |
30 | constructor(props: Props) {
31 | super(props)
32 |
33 | this.state = {
34 | dragStart: null,
35 | resizeStart: null
36 | }
37 | }
38 |
39 | onClickCursorLayer(ev: React.MouseEvent) {
40 | let eventToPoint = (ev: React.MouseEvent): Array => {
41 | const x = ev.nativeEvent.offsetX - 2
42 | const y = ev.nativeEvent.offsetY + (2 * (this.props.strokeWidth / 3))
43 | return [x, y]
44 | }
45 |
46 | if (this.props.mode === ModeEnum.HAND) {
47 | const [x, y] = eventToPoint(ev)
48 | this.props.events.startDrawing(x, y)
49 | } else {
50 | this.props.events.stopDrawing()
51 | }
52 | }
53 |
54 | onMouseMoveCursorLayer(ev: React.MouseEvent) {
55 | let eventToPoint = (ev: React.MouseEvent): Array => {
56 | const x = ev.nativeEvent.offsetX - 2
57 | const y = ev.nativeEvent.offsetY + (2 * (this.props.strokeWidth / 3))
58 | return [x, y]
59 | }
60 |
61 | if (this.props.mode === ModeEnum.DRAW_LINE) {
62 | const [x, y] = eventToPoint(ev)
63 | this.props.events.pushPoint(x, y)
64 | } else if (this.props.mode === ModeEnum.DRAG_IMAGE) {
65 | if (ev.target !== this.dragHandle) {
66 | return
67 | }
68 |
69 | const lastImage = this.props.eventStore.lastImage()
70 | if (!lastImage) {
71 | return
72 | }
73 |
74 | if (this.state.dragStart) {
75 | const base = (lastImage.width < lastImage.height) ? lastImage.width : lastImage.height
76 | const unit = (base / 8) < 20 ? Math.ceil(base / 8) : 20
77 |
78 | const moveX = lastImage.x + unit < 0 ? ev.nativeEvent.offsetX - this.state.dragStart.x - (lastImage.x + unit) : ev.nativeEvent.offsetX - this.state.dragStart.x
79 | const moveY = lastImage.y + unit < 0 ? ev.nativeEvent.offsetY - this.state.dragStart.y - (lastImage.y + unit) : ev.nativeEvent.offsetY - this.state.dragStart.y
80 |
81 | this.props.events.dragImage(moveX, moveY)
82 | }
83 | } else if (this.props.mode === ModeEnum.NW_RESIZE_IMAGE || this.props.mode === ModeEnum.NE_RESIZE_IMAGE || this.props.mode === ModeEnum.SE_RESIZE_IMAGE || this.props.mode === ModeEnum.SW_RESIZE_IMAGE) {
84 | const lastImage = this.props.eventStore.lastImage()
85 | if (!lastImage) {
86 | return
87 | }
88 |
89 | if (this.state.resizeStart) {
90 | const moveX = ev.pageX - this.state.resizeStart.x
91 | const moveY = ev.pageY - this.state.resizeStart.y
92 |
93 | // do nothing if cannot resize image
94 | if (this.props.mode === ModeEnum.NW_RESIZE_IMAGE && (lastImage.width - moveX < 0 || lastImage.height - moveY < 0)) {
95 | return
96 | } else if (this.props.mode === ModeEnum.NE_RESIZE_IMAGE && (lastImage.width + moveX < 0 || lastImage.height - moveY < 0)) {
97 | return
98 | } else if (this.props.mode === ModeEnum.SE_RESIZE_IMAGE && (lastImage.width + moveX < 0 || lastImage.height + moveY < 0)) {
99 | return
100 | } else if (this.props.mode === ModeEnum.SW_RESIZE_IMAGE && (lastImage.width - moveX < 0 || lastImage.height + moveY < 0)) {
101 | return
102 | }
103 |
104 | this.setState({resizeStart: {x: ev.pageX, y: ev.pageY}})
105 | this.props.events.resizeImage(moveX, moveY)
106 | }
107 | }
108 | }
109 |
110 | onClickDragHandle(ev: React.MouseEvent) {
111 | if (this.props.mode === ModeEnum.HAND) {
112 | this.setState({dragStart: {x: ev.nativeEvent.offsetX, y: ev.nativeEvent.offsetY}})
113 | this.props.events.startDragging()
114 | } else {
115 | this.setState({dragStart: null})
116 | this.props.events.stopDragging()
117 | }
118 | ev.preventDefault()
119 | ev.stopPropagation()
120 | }
121 |
122 | onClickResizeHandle(direction: ResizeImageDirection, ev: React.MouseEvent) {
123 | if (this.props.mode === ModeEnum.HAND) {
124 | this.setState({resizeStart: {x: ev.pageX, y: ev.pageY}})
125 | this.props.events.startResizing(direction)
126 | } else {
127 | this.setState({resizeStart: null})
128 | this.props.events.stopResizing()
129 | }
130 | ev.preventDefault()
131 | ev.stopPropagation()
132 | }
133 |
134 | render() {
135 | const cursorLayerStyle = {
136 | position: 'absolute' as 'absolute',
137 | zIndex: 2000,
138 | width: this.props.width,
139 | height: this.props.height,
140 | borderStyle: 'none none solid none',
141 | borderColor: this.props.strokeColor
142 | }
143 |
144 | return (
145 |
147 | {this.renderImageHandle()}
148 |
149 | )
150 | }
151 |
152 | renderImageHandle(): Array> | undefined {
153 | const lastImage = this.props.eventStore.lastImage()
154 | if (!lastImage) {
155 | return null
156 | }
157 |
158 | const base = (lastImage.width < lastImage.height) ? lastImage.width : lastImage.height
159 | const unit = (base / 8) < 20 ? Math.ceil(base / 8) : 20
160 |
161 | const mathMinOrMax = (min: number, max: number, value: number): number => {
162 | if (value < min) {
163 | return min
164 | } else if (value > max) {
165 | return max
166 | } else {
167 | return value
168 | }
169 | }
170 |
171 | const top = mathMinOrMax(0, this.props.height, lastImage.y + unit)
172 | const bottom = mathMinOrMax(0, this.props.height, lastImage.y + lastImage.height - unit)
173 | const left = mathMinOrMax(0, this.props.width, lastImage.x + unit)
174 | const right = mathMinOrMax(0, this.props.width, lastImage.x + lastImage.width - unit)
175 |
176 | const dragHandleStyle = {
177 | position: 'absolute' as 'absolute',
178 | zIndex: 2500,
179 | top: top,
180 | left: left,
181 | width: right - left,
182 | height: bottom - top,
183 | cursor: 'move'
184 | }
185 |
186 | const nwResizeHandleStyle = {
187 | position: 'absolute' as 'absolute',
188 | zIndex: 2500,
189 | top: mathMinOrMax(0, this.props.height, top - unit),
190 | left: mathMinOrMax(0, this.props.width, left - unit),
191 | width: left - mathMinOrMax(0, this.props.width, left - unit),
192 | height: top - mathMinOrMax(0, this.props.height, top - unit),
193 | cursor: 'nw-resize'
194 | }
195 |
196 | const neResizeHandleStyle = {
197 | position: 'absolute' as 'absolute',
198 | zIndex: 2500,
199 | top: mathMinOrMax(0, this.props.height, top - unit),
200 | left: right,
201 | width: mathMinOrMax(0, this.props.width, right + unit) - right,
202 | height: top - mathMinOrMax(0, this.props.height, top - unit),
203 | cursor: 'ne-resize'
204 | }
205 |
206 | const seResizeHandleStyle = {
207 | position: 'absolute' as 'absolute',
208 | zIndex: 2500,
209 | top: bottom,
210 | left: right,
211 | width: mathMinOrMax(0, this.props.width, right + unit) - right,
212 | height: mathMinOrMax(0, this.props.height, bottom + unit) - bottom,
213 | cursor: 'se-resize'
214 | }
215 |
216 | const swResizeHandleStyle = {
217 | position: 'absolute' as 'absolute',
218 | zIndex: 2500,
219 | top: bottom,
220 | left: mathMinOrMax(0, this.props.width, left - unit),
221 | width: left - mathMinOrMax(0, this.props.width, left - unit),
222 | height: mathMinOrMax(0, this.props.height, bottom + unit) - bottom,
223 | cursor: 'sw-resize'
224 | }
225 |
226 | return ([
227 | this.dragHandle = dragHandle} onClick={this.onClickDragHandle.bind(this)}/>,
229 |
,
231 |
,
233 |
,
235 |
237 | ])
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/src/EventStore.ts:
--------------------------------------------------------------------------------
1 | import {SvgElementEnum} from './Constants'
2 |
3 |
4 | export type PointData = {
5 | x: number,
6 | y: number
7 | }
8 |
9 | export type ImageData = {
10 | x?: number,
11 | y?: number,
12 | width: number,
13 | height: number,
14 | dataUrl: string
15 | }
16 |
17 | export type MouseMoveData = {
18 | x: number,
19 | y: number
20 | }
21 |
22 | export type PointEvent = {
23 | type: typeof SvgElementEnum.LINE,
24 | layer: number,
25 | strokeWidth: number,
26 | strokeColor: string,
27 | point: PointData
28 | }
29 |
30 | export type ImageEvent = {
31 | type: typeof SvgElementEnum.IMAGE,
32 | layer: number,
33 | image: ImageData
34 | }
35 |
36 | export type StopEvent = {
37 | type: null
38 | }
39 |
40 | export type AnyEvent = PointEvent | ImageEvent | StopEvent
41 |
42 | export type ReducedLineEvent = {
43 | type: typeof SvgElementEnum.LINE,
44 | strokeWidth: number,
45 | strokeColor: string,
46 | values: Array
47 | }
48 |
49 | export type ReducedImageEvent = {
50 | type: typeof SvgElementEnum.IMAGE,
51 | image: ImageData
52 | }
53 |
54 | export type ReducedStopEvent = {
55 | type?: null
56 | }
57 |
58 | export type AnyReducedEvent = ReducedLineEvent | ReducedImageEvent | ReducedStopEvent
59 |
60 | export const isPointEvent = (arg: AnyEvent): arg is PointEvent => {
61 | return arg.type === SvgElementEnum.LINE
62 | }
63 |
64 | export const isImageEvent = (arg: AnyEvent): arg is ImageEvent => {
65 | return arg.type === SvgElementEnum.IMAGE
66 | }
67 |
68 | export const isReducedLineEvent = (arg: AnyReducedEvent): arg is ReducedLineEvent => {
69 | return arg.type === SvgElementEnum.LINE
70 | }
71 |
72 | export const isReducedImageEvent = (arg: AnyReducedEvent): arg is ReducedImageEvent => {
73 | return arg.type === SvgElementEnum.IMAGE
74 | }
75 |
76 | export interface EventStoreProtocol {
77 |
78 | lastImage(): ImageData | undefined
79 |
80 | reduceEvents(): Array
81 |
82 | selectLayer(layer: number): void
83 |
84 | addLayer(): void
85 |
86 | startDrawing(strokeWidth: number, strokeColor: string, point: PointData): void
87 |
88 | stopDrawing(): void
89 |
90 | pushPoint(strokeWidth: number, strokeColor: string, point: PointData): void
91 |
92 | pasteImage(image: ImageData): void
93 |
94 | dragImage(move: MouseMoveData): void
95 |
96 | nwResizeImage(move: MouseMoveData): void
97 |
98 | neResizeImage(move: MouseMoveData): void
99 |
100 | seResizeImage(move: MouseMoveData): void
101 |
102 | swResizeImage(move: MouseMoveData): void
103 |
104 | undo(): void
105 |
106 | redo(): void
107 |
108 | clear(): void
109 | }
110 |
111 |
112 | export class EventStore implements EventStoreProtocol {
113 |
114 | selectedLayer: number
115 | renderableLayers: Array
116 | goodEvents: Array
117 | undoEvents: Array
118 |
119 | constructor() {
120 | this.selectedLayer = 0
121 | this.renderableLayers = [true]
122 | this.goodEvents = []
123 | this.undoEvents = []
124 | }
125 |
126 | lastImage(): ImageData | undefined {
127 | const last = this.goodEvents[this.goodEvents.length - 1]
128 | if (last && isImageEvent(last)) {
129 | return last.image
130 | } else {
131 | return null
132 | }
133 | }
134 |
135 | reduceEvents(): Array {
136 | return this.goodEvents.reduce((prev: Array>, element: AnyEvent): Array> => {
137 | if (!element.type) {
138 | prev.forEach(p => {
139 | p.push({})
140 | })
141 | return prev
142 | }
143 |
144 | if (isPointEvent(element)) {
145 | let last = prev[element.layer][prev[element.layer].length - 1]
146 | if (last && isReducedLineEvent(last) && last.strokeWidth === element.strokeWidth && last.strokeColor === element.strokeColor) {
147 | last.values.push(element.point)
148 | } else {
149 | const event: ReducedLineEvent = {
150 | type: element.type,
151 | strokeWidth: element.strokeWidth,
152 | strokeColor: element.strokeColor,
153 | values: [element.point]
154 | }
155 | prev[element.layer].push(event)
156 | }
157 | return prev
158 |
159 | } else if (isImageEvent(element)) {
160 | const event: ReducedImageEvent = {
161 | type: element.type,
162 | image: element.image
163 | }
164 | prev[element.layer].push(event)
165 | return prev
166 |
167 | } else {
168 | return prev
169 | }
170 |
171 | }, this.renderableLayers.map(() => [])
172 | ).filter((element: Array, index: number): boolean => {
173 | return this.renderableLayers[index]
174 |
175 | }).reduce((prev: Array, element: Array): Array => {
176 | return prev.concat(element)
177 |
178 | }, []
179 | ).filter((element: AnyReducedEvent): boolean => {
180 | if (isReducedLineEvent(element)) {
181 | return element.values.length > 1
182 | } else if (isReducedImageEvent(element)) {
183 | return true
184 | } else {
185 | return true
186 | }
187 | })
188 | }
189 |
190 | selectLayer(layer: number) {
191 | this.goodEvents.push({type: null})
192 | this.selectedLayer = layer
193 | }
194 |
195 | addLayer() {
196 | this.renderableLayers.push(true)
197 | }
198 |
199 | startDrawing(strokeWidth: number, strokeColor: string, point: PointData) {
200 | this.goodEvents.push({
201 | type: SvgElementEnum.LINE,
202 | layer: this.selectedLayer,
203 | strokeWidth: strokeWidth,
204 | strokeColor: strokeColor,
205 | point: point
206 | })
207 | }
208 |
209 | stopDrawing() {
210 | this.goodEvents.push({type: null})
211 | }
212 |
213 | pushPoint(strokeWidth: number, strokeColor: string, point: PointData) {
214 | const event: PointEvent = {
215 | type: SvgElementEnum.LINE,
216 | layer: this.selectedLayer,
217 | strokeWidth: strokeWidth,
218 | strokeColor: strokeColor,
219 | point: point
220 | }
221 | this.goodEvents.push(event)
222 | this.undoEvents = []
223 | }
224 |
225 | pasteImage(image: ImageData) {
226 | const event: ImageEvent = {
227 | type: SvgElementEnum.IMAGE,
228 | layer: this.selectedLayer,
229 | image: image
230 | }
231 | this.goodEvents.push(event)
232 | this.undoEvents = []
233 | }
234 |
235 | dragImage(move: MouseMoveData) {
236 | const lastImage = this.lastImage()
237 | if (lastImage) {
238 | lastImage.x = lastImage.x + move.x
239 | lastImage.y = lastImage.y + move.y
240 | }
241 | }
242 |
243 | nwResizeImage(move: MouseMoveData) {
244 | const lastImage = this.lastImage()
245 | if (lastImage) {
246 | lastImage.x = lastImage.x + move.x
247 | lastImage.y = lastImage.y + move.y
248 | lastImage.width = lastImage.width - move.x
249 | lastImage.height = lastImage.height - move.y
250 | }
251 | }
252 |
253 | neResizeImage(move: MouseMoveData) {
254 | const lastImage = this.lastImage()
255 | if (lastImage) {
256 | // lastImage.x = lastImage.x + move.x;
257 | lastImage.y = lastImage.y + move.y
258 | lastImage.width = lastImage.width + move.x
259 | lastImage.height = lastImage.height - move.y
260 | }
261 | }
262 |
263 | seResizeImage(move: MouseMoveData) {
264 | const lastImage = this.lastImage()
265 | if (lastImage) {
266 | // lastImage.x = lastImage.x + move.x;
267 | // lastImage.y = lastImage.y + move.y;
268 | lastImage.width = lastImage.width + move.x
269 | lastImage.height = lastImage.height + move.y
270 | }
271 | }
272 |
273 | swResizeImage(move: MouseMoveData) {
274 | const lastImage = this.lastImage()
275 | if (lastImage) {
276 | lastImage.x = lastImage.x + move.x
277 | // lastImage.y = lastImage.y + move.y;
278 | lastImage.width = lastImage.width - move.x
279 | lastImage.height = lastImage.height + move.y
280 | }
281 | }
282 |
283 | undo() {
284 | if (this.goodEvents.length) {
285 | this.undoEvents.push(this.goodEvents.pop()) // {}
286 | this.undoEvents.push(this.goodEvents.pop()) // {type: 'line', point: [...], ...} or {type: 'image', image: {...}, ...}
287 | this.goodEvents.push({type: null})
288 | }
289 | }
290 |
291 | redo() {
292 | if (this.undoEvents.length) {
293 | this.goodEvents.pop()
294 | this.goodEvents.push(this.undoEvents.pop()) // {type: 'line', point: [...], ...} or {type: 'image', image: {...}, ...}
295 | this.goodEvents.push(this.undoEvents.pop()) // {}
296 | }
297 | }
298 |
299 | clear() {
300 | this.goodEvents = []
301 | this.undoEvents = []
302 | }
303 | }
304 |
--------------------------------------------------------------------------------
/src/EventStream.ts:
--------------------------------------------------------------------------------
1 | import EventEmitter from 'events'
2 |
3 | import {ResizeImageDirection} from './Constants'
4 | import {ImageData, MouseMoveData, PointData} from './EventStore'
5 |
6 |
7 | export type EventListener = (value: any) => void
8 |
9 | export type ChangeStrokeWidth = {
10 | key: 'strokeWidth',
11 | value: number
12 | }
13 |
14 | export type ChangeStrokeColor = {
15 | key: 'strokeColor',
16 | value: string
17 | }
18 |
19 | export enum EventNameEnum {
20 | SELECT_LAYER = 'selectLayer',
21 | ADD_LAYER = 'addLayer',
22 | START = 'start',
23 | STOP = 'stop',
24 | SET = 'set',
25 | PUSH = 'push',
26 | PASTE = 'paste',
27 | START_DRAGGING = 'startDragging',
28 | STOP_DRAGGING = 'stopDragging',
29 | DRAG = 'drag',
30 | START_RESIZING = 'startResizing',
31 | STOP_RESIZING = 'stopResizing',
32 | RESIZE = 'resize',
33 | UNDO = 'undo',
34 | REDO = 'redo',
35 | CLEAR = 'clear'
36 | }
37 |
38 | export interface EventStreamProtocol {
39 |
40 | on(name: EventNameEnum, listener: EventListener): void
41 |
42 | selectLayer(layer: number): void
43 |
44 | addLayer(): void
45 |
46 | startDrawing(x: number, y: number): void
47 |
48 | stopDrawing(): void
49 |
50 | changeStrokeWidth(width: number): void
51 |
52 | changeStrokeColor(color: string): void
53 |
54 | pushPoint(x: number, y: number): void
55 |
56 | pasteImage(x: number, y: number, width: number, height: number, dataUrl: string): void
57 |
58 | startDragging(): void
59 |
60 | stopDragging(): void
61 |
62 | dragImage(x: number, y: number): void
63 |
64 | startResizing(direction: ResizeImageDirection): void
65 |
66 | stopResizing(): void
67 |
68 | resizeImage(x: number, y: number): void
69 |
70 | undo(): void
71 |
72 | redo(): void
73 |
74 | clear(): void
75 |
76 | }
77 |
78 |
79 | export class EventStream implements EventStreamProtocol {
80 |
81 | emitter: EventEmitter
82 |
83 | constructor() {
84 | this.emitter = new EventEmitter()
85 | }
86 |
87 | on(name: EventNameEnum, listener: EventListener) {
88 | this.emitter.on(name, listener)
89 | }
90 |
91 | selectLayer(layer: number) {
92 | this.emitter.emit('selectLayer', layer)
93 | }
94 |
95 | addLayer() {
96 | this.emitter.emit('addLayer')
97 | }
98 |
99 | startDrawing(x: number, y: number) {
100 | this.emitter.emit('start', {x, y})
101 | }
102 |
103 | stopDrawing() {
104 | this.emitter.emit('stop')
105 | }
106 |
107 | changeStrokeWidth(width: number) {
108 | const change: ChangeStrokeWidth = {key: 'strokeWidth', value: width}
109 | this.emitter.emit('set', change)
110 | }
111 |
112 | changeStrokeColor(color: string) {
113 | const change: ChangeStrokeColor = {key: 'strokeColor', value: color}
114 | this.emitter.emit('set', change)
115 | }
116 |
117 | pushPoint(x: number, y: number) {
118 | const point: PointData = {x, y}
119 | this.emitter.emit('push', point)
120 | }
121 |
122 | pasteImage(x: number, y: number, width: number, height: number, dataUrl: string) {
123 | const image: ImageData = {x, y, width, height, dataUrl}
124 | this.emitter.emit('paste', image)
125 | }
126 |
127 | startDragging() {
128 | this.emitter.emit('startDragging')
129 | }
130 |
131 | stopDragging() {
132 | this.emitter.emit('stopDragging')
133 | }
134 |
135 | dragImage(x: number, y: number) {
136 | const move: MouseMoveData = {x, y}
137 | this.emitter.emit('drag', move)
138 | }
139 |
140 | startResizing(direction: ResizeImageDirection) {
141 | this.emitter.emit('startResizing', direction)
142 | }
143 |
144 | stopResizing() {
145 | this.emitter.emit('stopResizing')
146 | }
147 |
148 | resizeImage(x: number, y: number) {
149 | const move: MouseMoveData = {x, y}
150 | this.emitter.emit('resize', move)
151 | }
152 |
153 | undo() {
154 | this.emitter.emit('undo')
155 | }
156 |
157 | redo() {
158 | this.emitter.emit('redo')
159 | }
160 |
161 | clear() {
162 | this.emitter.emit('clear')
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/src/SvgConverter.ts:
--------------------------------------------------------------------------------
1 | import {ImageData} from './EventStore'
2 |
3 |
4 | export class SvgConverter {
5 |
6 | static toSvgData(sourceNode: SVGSVGElement): Promise {
7 | let htmlText = sourceNode.outerHTML
8 | let base64EncodedText = window.btoa(
9 | window.encodeURIComponent(htmlText)
10 | .replace(/%([0-9A-F]{2})/g, (match: string, p1: string): string => String.fromCharCode(window.parseInt('0x' + p1))))
11 |
12 | return new Promise(resolve => {
13 | resolve('data:image/svg+xml;charset=utf-8;base64,' + base64EncodedText)
14 | })
15 | }
16 |
17 | static toPngData(sourceNode: SVGSVGElement): Promise {
18 | return SvgConverter.toDataUrl(sourceNode, 'image/png')
19 | }
20 |
21 | static toJpegData(sourceNode: SVGSVGElement): Promise {
22 | return SvgConverter.toDataUrl(sourceNode, 'image/jpeg')
23 | }
24 |
25 | static toDataUrl(sourceNode: SVGSVGElement, imageType: string): Promise {
26 | return new Promise(resolve => {
27 | SvgConverter.toSvgData(sourceNode).then((svgdata: string) => {
28 | let {width, height} = sourceNode.getBoundingClientRect()
29 |
30 | let imageNode = new window.Image()
31 | imageNode.onload = () => {
32 | let canvasNode = document.createElement('canvas')
33 | canvasNode.width = width
34 | canvasNode.height = height
35 |
36 | let graphicsContext = canvasNode.getContext('2d')
37 | if (graphicsContext) {
38 | graphicsContext.drawImage(imageNode, 0, 0)
39 | } else {
40 | throw new Error('got no rendering context')
41 | }
42 |
43 | resolve(canvasNode.toDataURL(imageType))
44 | }
45 | imageNode.src = svgdata
46 | })
47 | })
48 | }
49 |
50 | static fromPngImage(imageUrl: string): Promise {
51 | return SvgConverter.fromImageUrl(imageUrl, 'image/png')
52 | }
53 |
54 | static fromJpegImage(imageUrl: string): Promise {
55 | return SvgConverter.fromImageUrl(imageUrl, 'image/jpeg')
56 | }
57 |
58 | static fromGifImage(imageUrl: string): Promise {
59 | return SvgConverter.fromImageUrl(imageUrl, 'image/gif')
60 | }
61 |
62 | static fromImageUrl(imageUrl: string, imageType: string): Promise {
63 | return new Promise(resolve => {
64 | let imageNode = new window.Image()
65 | imageNode.onload = () => {
66 | let canvasNode = document.createElement('canvas')
67 | canvasNode.width = imageNode.width
68 | canvasNode.height = imageNode.height
69 |
70 | let graphicsContext = canvasNode.getContext('2d')
71 | if (graphicsContext) {
72 | graphicsContext.drawImage(imageNode, 0, 0)
73 | } else {
74 | throw new Error('got no rendering context')
75 | }
76 |
77 | resolve({width: imageNode.width, height: imageNode.height, dataUrl: canvasNode.toDataURL(imageType)})
78 | }
79 | imageNode.crossOrigin = 'anonymous'
80 | imageNode.src = imageUrl
81 | })
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/Whiteboard.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import {ModeEnum, ResizeImageDirection} from './Constants'
4 | import {ChangeStrokeColor, ChangeStrokeWidth, EventNameEnum, EventStream} from './EventStream'
5 | import {EventStore, ImageData, MouseMoveData, PointData} from './EventStore'
6 | import {CursorPane} from './CursorPane'
7 | import {CanvasPane} from './CanvasPane'
8 |
9 |
10 | type Props = {
11 | events: EventStream,
12 | eventStore: EventStore,
13 | width: number,
14 | height: number,
15 | style: {
16 | backgroundColor: string
17 | }
18 | }
19 |
20 | type State = {
21 | eventStore: EventStore,
22 | mode: ModeEnum,
23 | layer: number,
24 | strokeWidth: number,
25 | strokeColor: string
26 | }
27 |
28 | export class Whiteboard extends React.Component {
29 |
30 | static defaultProps = {
31 | events: new EventStream(),
32 | eventStore: new EventStore(),
33 | width: 400,
34 | height: 400,
35 | style: {
36 | backgroundColor: 'none'
37 | }
38 | }
39 | props: Props
40 | state: State
41 |
42 | canvas?: CanvasPane
43 |
44 | constructor(props: Props) {
45 | super(props)
46 |
47 | this.state = {
48 | eventStore: props.eventStore,
49 | mode: ModeEnum.HAND,
50 | layer: 0,
51 | strokeWidth: 5,
52 | strokeColor: 'black'
53 | }
54 |
55 | this.canvas = null
56 | }
57 |
58 | getSvgElement(): SVGSVGElement | undefined {
59 | if (this.canvas) {
60 | return this.canvas.getSvgElement()
61 | }
62 | }
63 |
64 | componentDidMount() {
65 | this.setupEventHandler()
66 | }
67 |
68 | setupEventHandler() {
69 | this.props.events.on(EventNameEnum.SELECT_LAYER, this.selectLayer.bind(this))
70 | this.props.events.on(EventNameEnum.ADD_LAYER, this.addLayer.bind(this))
71 |
72 | this.props.events.on(EventNameEnum.START, this.startDrawing.bind(this))
73 | this.props.events.on(EventNameEnum.STOP, this.stopDrawing.bind(this))
74 |
75 | this.props.events.on(EventNameEnum.SET, (event: ChangeStrokeWidth | ChangeStrokeColor) => {
76 | if (event.key === 'strokeWidth') {
77 | this.changeStrokeWidth(event.value)
78 | }
79 | if (event.key === 'strokeColor') {
80 | this.changeStrokeColor(event.value)
81 | }
82 | })
83 |
84 | this.props.events.on(EventNameEnum.PUSH, this.pushPoint.bind(this))
85 |
86 | this.props.events.on(EventNameEnum.PASTE, this.pasteImage.bind(this))
87 | this.props.events.on(EventNameEnum.START_DRAGGING, this.startDragging.bind(this))
88 | this.props.events.on(EventNameEnum.STOP_DRAGGING, this.stopDragging.bind(this))
89 | this.props.events.on(EventNameEnum.DRAG, this.dragImage.bind(this))
90 | this.props.events.on(EventNameEnum.START_RESIZING, this.startResizing.bind(this))
91 | this.props.events.on(EventNameEnum.STOP_RESIZING, this.stopResizing.bind(this))
92 | this.props.events.on(EventNameEnum.RESIZE, this.resizeImage.bind(this))
93 |
94 | this.props.events.on(EventNameEnum.UNDO, this.undo.bind(this))
95 | this.props.events.on(EventNameEnum.REDO, this.redo.bind(this))
96 | this.props.events.on(EventNameEnum.CLEAR, this.clear.bind(this))
97 | }
98 |
99 | selectLayer(layer: number) {
100 | this.state.eventStore.selectLayer(layer)
101 | this.setState({
102 | layer: layer,
103 | eventStore: this.state.eventStore
104 | })
105 | }
106 |
107 | addLayer() {
108 | this.state.eventStore.addLayer()
109 | this.setState({eventStore: this.state.eventStore})
110 | }
111 |
112 | startDrawing(point: PointData) {
113 | this.state.eventStore.startDrawing(this.state.strokeWidth, this.state.strokeColor, point)
114 | this.setState({
115 | mode: ModeEnum.DRAW_LINE,
116 | eventStore: this.state.eventStore
117 | })
118 | }
119 |
120 | stopDrawing() {
121 | this.state.eventStore.stopDrawing()
122 | this.setState({
123 | mode: ModeEnum.HAND,
124 | eventStore: this.state.eventStore
125 | })
126 | }
127 |
128 | changeStrokeWidth(width: number) {
129 | this.state.eventStore.stopDrawing()
130 | this.setState({
131 | strokeWidth: width,
132 | eventStore: this.state.eventStore
133 | })
134 | }
135 |
136 | changeStrokeColor(color: string) {
137 | this.state.eventStore.stopDrawing()
138 | this.setState({
139 | strokeColor: color,
140 | eventStore: this.state.eventStore
141 | })
142 | }
143 |
144 | pushPoint(point: PointData) {
145 | this.state.eventStore.pushPoint(this.state.strokeWidth, this.state.strokeColor, point)
146 | this.setState({eventStore: this.state.eventStore})
147 | }
148 |
149 | pasteImage(image: ImageData) {
150 | this.state.eventStore.pasteImage(image)
151 | this.setState({eventStore: this.state.eventStore})
152 | }
153 |
154 | startDragging() {
155 | this.setState({mode: ModeEnum.DRAG_IMAGE})
156 | }
157 |
158 | stopDragging() {
159 | this.setState({mode: ModeEnum.HAND})
160 | }
161 |
162 | dragImage(move: MouseMoveData) {
163 | if (this.state.mode !== ModeEnum.DRAG_IMAGE) {
164 | return
165 | }
166 |
167 | this.state.eventStore.dragImage(move)
168 | this.setState({eventStore: this.state.eventStore})
169 | }
170 |
171 | startResizing(direction: ResizeImageDirection) {
172 | this.setState({mode: direction})
173 | }
174 |
175 | stopResizing() {
176 | this.setState({mode: ModeEnum.HAND})
177 | }
178 |
179 | resizeImage(move: MouseMoveData) {
180 | if (this.state.mode === ModeEnum.NW_RESIZE_IMAGE) {
181 | this.state.eventStore.nwResizeImage(move)
182 | this.setState({eventStore: this.state.eventStore})
183 | } else if (this.state.mode === ModeEnum.NE_RESIZE_IMAGE) {
184 | this.state.eventStore.neResizeImage(move)
185 | this.setState({eventStore: this.state.eventStore})
186 | } else if (this.state.mode === ModeEnum.SE_RESIZE_IMAGE) {
187 | this.state.eventStore.seResizeImage(move)
188 | this.setState({eventStore: this.state.eventStore})
189 | } else if (this.state.mode === ModeEnum.SW_RESIZE_IMAGE) {
190 | this.state.eventStore.swResizeImage(move)
191 | this.setState({eventStore: this.state.eventStore})
192 | }
193 | }
194 |
195 | undo() {
196 | this.state.eventStore.undo()
197 | this.setState({eventStore: this.state.eventStore})
198 | }
199 |
200 | redo() {
201 | this.state.eventStore.redo()
202 | this.setState({eventStore: this.state.eventStore})
203 | }
204 |
205 | clear() {
206 | this.state.eventStore.clear()
207 | this.setState({eventStore: this.state.eventStore})
208 | }
209 |
210 | render() {
211 | const wrapperStyle = {
212 | position: 'relative' as 'relative',
213 | width: this.props.width,
214 | height: this.props.height
215 | }
216 |
217 | const props = {...this.props, ...this.state}
218 |
219 | return (
220 |
221 |
222 | this.canvas = canvas} {...props}/>
223 |
224 | )
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/src/dummy.test.ts:
--------------------------------------------------------------------------------
1 | test('dummy', () => {
2 | expect(true).toBeTruthy()
3 | })
4 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import {ModeEnum, SvgElementEnum} from './Constants'
2 |
3 | export {Whiteboard} from './Whiteboard'
4 | export {ChangeStrokeColor, ChangeStrokeWidth, EventListener, EventNameEnum, EventStream, EventStreamProtocol} from './EventStream'
5 | export {
6 | EventStore,
7 | EventStoreProtocol,
8 | ImageData,
9 | ImageEvent,
10 | isPointEvent,
11 | isImageEvent,
12 | isReducedImageEvent,
13 | isReducedLineEvent,
14 | MouseMoveData,
15 | PointData,
16 | PointEvent,
17 | StopEvent
18 | } from './EventStore'
19 | export {SvgConverter} from './SvgConverter'
20 | export {ModeEnum, SvgElementEnum}
21 |
--------------------------------------------------------------------------------
/stories/Basic.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import {storiesOf} from '@storybook/react'
3 |
4 | import {EventStore, EventStream, Whiteboard} from '../src/index'
5 |
6 |
7 | storiesOf('Basic', module)
8 | .add('plain', () => {
9 | const events = new EventStream()
10 | const eventStore = new EventStore()
11 |
12 | return (
13 |
14 | )
15 | })
16 | .add('background color', () => {
17 | const events = new EventStream()
18 | const eventStore = new EventStore()
19 | const style = {
20 | backgroundColor: 'lightyellow'
21 | }
22 |
23 | return (
24 |
25 | )
26 | })
27 | .add('container div', () => {
28 | const events = new EventStream()
29 | const eventStore = new EventStore()
30 | const style = {
31 | backgroundColor: 'lightyellow'
32 | }
33 |
34 | return (
35 |
36 |
37 |
38 | )
39 | })
40 | .add('draw lines', () => {
41 | const events = new EventStream()
42 | const eventStore = new EventStore()
43 | const style = {
44 | backgroundColor: 'lightyellow'
45 | }
46 |
47 | requestAnimationFrame(() => {
48 | events.selectLayer(0)
49 |
50 | events.changeStrokeColor('blue')
51 | events.changeStrokeWidth(5)
52 | events.startDrawing(100, 100)
53 | events.pushPoint(100, 200)
54 | events.pushPoint(200, 200)
55 | events.stopDrawing()
56 |
57 | events.changeStrokeColor('green')
58 | events.changeStrokeWidth(7)
59 | events.startDrawing(200, 200)
60 | events.pushPoint(200, 100)
61 | events.pushPoint(100, 100)
62 | events.stopDrawing()
63 | })
64 |
65 | return (
66 |
67 |
68 |
69 | )
70 | })
71 |
--------------------------------------------------------------------------------
/stories/Image.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import {storiesOf} from '@storybook/react'
3 |
4 | import {EventStore, EventStream, ModeEnum, SvgConverter, Whiteboard} from '../src/index'
5 |
6 |
7 | storiesOf('Image', module)
8 | .add('paste image', () => {
9 | const events = new EventStream()
10 | const eventStore = new EventStore()
11 | const style = {
12 | backgroundColor: 'lightyellow'
13 | }
14 |
15 | requestAnimationFrame(() => {
16 | SvgConverter.fromPngImage('https://4.bp.blogspot.com/-add9p_TiRII/V9PE77mE44I/AAAAAAAA9kk/1JeafQlm6-QFxt--xA5gMWrPl2EZyKTMgCLcB/s400/kjhou_board.png')
17 | .then(image => {
18 | events.pasteImage(50, 50, image.width, image.height, image.dataUrl)
19 | })
20 | })
21 |
22 | return (
23 |
24 |
25 |
26 | )
27 | })
28 | .add('resize image', () => {
29 | const events = new EventStream()
30 | const eventStore = new EventStore()
31 | const style = {
32 | backgroundColor: 'lightyellow'
33 | }
34 |
35 | requestAnimationFrame(() => {
36 | SvgConverter.fromPngImage('https://4.bp.blogspot.com/-add9p_TiRII/V9PE77mE44I/AAAAAAAA9kk/1JeafQlm6-QFxt--xA5gMWrPl2EZyKTMgCLcB/s400/kjhou_board.png')
37 | .then(image => {
38 | events.pasteImage(50, 50, image.width, image.height, image.dataUrl)
39 | events.startResizing(ModeEnum.NW_RESIZE_IMAGE)
40 | events.resizeImage(150, 150)
41 | events.stopResizing()
42 | })
43 | })
44 |
45 | return (
46 |
47 |
48 |
49 | )
50 | })
51 | .add('drag image', () => {
52 | const events = new EventStream()
53 | const eventStore = new EventStore()
54 | const style = {
55 | backgroundColor: 'lightyellow'
56 | }
57 |
58 | requestAnimationFrame(() => {
59 | SvgConverter.fromPngImage('https://4.bp.blogspot.com/-add9p_TiRII/V9PE77mE44I/AAAAAAAA9kk/1JeafQlm6-QFxt--xA5gMWrPl2EZyKTMgCLcB/s400/kjhou_board.png')
60 | .then(image => {
61 | events.pasteImage(50, 50, image.width, image.height, image.dataUrl)
62 | events.startResizing(ModeEnum.NW_RESIZE_IMAGE)
63 | events.resizeImage(150, 150)
64 | events.stopResizing()
65 | events.startDragging()
66 | events.dragImage(-150, -150)
67 | events.stopDragging()
68 | })
69 | })
70 |
71 | return (
72 |
73 |
74 |
75 | )
76 | })
77 | .add('download whiteboard', () => {
78 | const events = new EventStream()
79 | const eventStore = new EventStore()
80 | const style = {
81 | backgroundColor: 'lightyellow'
82 | }
83 |
84 | let whiteboardEl: Whiteboard, linkEl: HTMLAnchorElement
85 |
86 | requestAnimationFrame(() => {
87 | SvgConverter.fromPngImage('https://4.bp.blogspot.com/-add9p_TiRII/V9PE77mE44I/AAAAAAAA9kk/1JeafQlm6-QFxt--xA5gMWrPl2EZyKTMgCLcB/s400/kjhou_board.png')
88 | .then(image => {
89 | events.pasteImage(50, 50, image.width, image.height, image.dataUrl)
90 | events.startResizing(ModeEnum.NW_RESIZE_IMAGE)
91 | events.resizeImage(150, 150)
92 | events.stopResizing()
93 | events.startDragging()
94 | events.dragImage(-150, -150)
95 | events.stopDragging()
96 | events.stopDrawing()
97 |
98 | const svgEl = whiteboardEl.getSvgElement()
99 | SvgConverter.toPngData(svgEl)
100 | .then(data => {
101 | linkEl.href = data
102 | })
103 | })
104 | })
105 |
106 | return (
107 |
114 | )
115 | })
116 |
--------------------------------------------------------------------------------
/stories/Layer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import {storiesOf} from '@storybook/react'
3 |
4 | import {EventStore, EventStream, Whiteboard} from '../src/index'
5 |
6 |
7 | storiesOf('Layer', module)
8 | .add('draw lines on an upper layer', () => {
9 | const events = new EventStream()
10 | const eventStore = new EventStore()
11 | const style = {
12 | backgroundColor: 'lightyellow'
13 | }
14 |
15 | requestAnimationFrame(() => {
16 | events.addLayer()
17 | events.selectLayer(1)
18 |
19 | events.changeStrokeColor('blue')
20 | events.changeStrokeWidth(10)
21 | events.startDrawing(100, 100)
22 | events.pushPoint(110, 200)
23 | events.pushPoint(120, 100)
24 | events.pushPoint(130, 200)
25 | events.pushPoint(140, 100)
26 | events.pushPoint(150, 200)
27 | events.pushPoint(160, 100)
28 | events.pushPoint(170, 200)
29 | events.pushPoint(180, 100)
30 | events.pushPoint(190, 200)
31 | events.pushPoint(200, 100)
32 | events.stopDrawing()
33 |
34 | events.changeStrokeColor('yellow')
35 | events.startDrawing(50, 130)
36 | events.pushPoint(250, 130)
37 | events.stopDrawing()
38 | events.startDrawing(50, 150)
39 | events.pushPoint(250, 150)
40 | events.stopDrawing()
41 | events.startDrawing(50, 170)
42 | events.pushPoint(250, 170)
43 | events.stopDrawing()
44 | })
45 |
46 | return (
47 |
48 |
49 |
50 | )
51 | })
52 | .add('draw lines on two layers', () => {
53 | const events = new EventStream()
54 | const eventStore = new EventStore()
55 | const style = {
56 | backgroundColor: 'lightyellow'
57 | }
58 |
59 | requestAnimationFrame(() => {
60 | events.addLayer()
61 | events.selectLayer(1)
62 |
63 | events.changeStrokeColor('blue')
64 | events.changeStrokeWidth(10)
65 | events.startDrawing(100, 100)
66 | events.pushPoint(110, 200)
67 | events.pushPoint(120, 100)
68 | events.pushPoint(130, 200)
69 | events.pushPoint(140, 100)
70 | events.pushPoint(150, 200)
71 | events.pushPoint(160, 100)
72 | events.pushPoint(170, 200)
73 | events.pushPoint(180, 100)
74 | events.pushPoint(190, 200)
75 | events.pushPoint(200, 100)
76 | events.stopDrawing()
77 |
78 | events.selectLayer(0)
79 |
80 | events.changeStrokeColor('yellow')
81 | events.startDrawing(50, 130)
82 | events.pushPoint(250, 130)
83 | events.stopDrawing()
84 | events.startDrawing(50, 150)
85 | events.pushPoint(250, 150)
86 | events.stopDrawing()
87 | events.startDrawing(50, 170)
88 | events.pushPoint(250, 170)
89 | events.stopDrawing()
90 | })
91 |
92 | return (
93 |
94 |
95 |
96 | )
97 | })
98 |
--------------------------------------------------------------------------------
/stories/UndoRedo.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import {storiesOf} from '@storybook/react'
3 |
4 | import {EventStore, EventStream, Whiteboard} from '../src/index'
5 |
6 |
7 | storiesOf('Undo & Redo', module)
8 | .add('draw lines', () => {
9 | const events = new EventStream()
10 | const eventStore = new EventStore()
11 | const style = {
12 | backgroundColor: 'lightyellow'
13 | }
14 |
15 | requestAnimationFrame(() => {
16 | events.selectLayer(0)
17 |
18 | events.changeStrokeColor('blue')
19 | events.changeStrokeWidth(5)
20 | events.startDrawing(100, 100)
21 | events.pushPoint(100, 200)
22 | events.pushPoint(200, 200)
23 | events.pushPoint(200, 100)
24 | events.pushPoint(110, 100)
25 | events.stopDrawing()
26 | })
27 |
28 | return (
29 |
30 |
31 |
32 | )
33 | })
34 | .add('undo', () => {
35 | const events = new EventStream()
36 | const eventStore = new EventStore()
37 | const style = {
38 | backgroundColor: 'lightyellow'
39 | }
40 |
41 | requestAnimationFrame(() => {
42 | events.selectLayer(0)
43 |
44 | events.changeStrokeColor('blue')
45 | events.changeStrokeWidth(5)
46 | events.startDrawing(100, 100)
47 | events.pushPoint(100, 200)
48 | events.pushPoint(200, 200)
49 | events.pushPoint(200, 100)
50 | events.pushPoint(110, 100)
51 | events.stopDrawing()
52 |
53 | events.undo()
54 | events.undo()
55 | })
56 |
57 | return (
58 |
59 |
60 |
61 | )
62 | })
63 | .add('redo', () => {
64 | const events = new EventStream()
65 | const eventStore = new EventStore()
66 | const style = {
67 | backgroundColor: 'lightyellow'
68 | }
69 |
70 | requestAnimationFrame(() => {
71 | events.selectLayer(0)
72 |
73 | events.changeStrokeColor('blue')
74 | events.changeStrokeWidth(5)
75 | events.startDrawing(100, 100)
76 | events.pushPoint(100, 200)
77 | events.pushPoint(200, 200)
78 | events.pushPoint(200, 100)
79 | events.pushPoint(110, 100)
80 | events.stopDrawing()
81 |
82 | events.undo()
83 | events.undo()
84 |
85 | events.redo()
86 | })
87 |
88 | return (
89 |
90 |
91 |
92 | )
93 | })
94 | .add('redo then draw line', () => {
95 | const events = new EventStream()
96 | const eventStore = new EventStore()
97 | const style = {
98 | backgroundColor: 'lightyellow'
99 | }
100 |
101 | requestAnimationFrame(() => {
102 | events.selectLayer(0)
103 |
104 | events.changeStrokeColor('blue')
105 | events.changeStrokeWidth(5)
106 | events.startDrawing(100, 100)
107 | events.pushPoint(100, 200)
108 | events.pushPoint(200, 200)
109 | events.pushPoint(200, 100)
110 | events.pushPoint(110, 100)
111 | events.stopDrawing()
112 |
113 | events.undo()
114 | events.undo()
115 |
116 | events.redo()
117 |
118 | events.startDrawing(200, 100) // then delete undo stack
119 | events.pushPoint(300, 100)
120 | events.stopDrawing()
121 |
122 | events.redo() // nothing to draw because undo stack is empty
123 | })
124 |
125 | return (
126 |
127 |
128 |
129 | )
130 | })
131 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist/",
4 | "sourceMap": true,
5 | "noImplicitAny": true,
6 | "module": "es6",
7 | "target": "es5",
8 | "jsx": "react",
9 | "moduleResolution": "node",
10 | "rootDirs": ["src", "stories"],
11 | "baseUrl": "src",
12 | "allowJs": true,
13 | "allowSyntheticDefaultImports": true
14 | },
15 | "include": [
16 | "./src/**/*"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | entry: './src/index.ts',
5 | output: {
6 | path: path.resolve(__dirname + '/dist'),
7 | filename: 'index.js',
8 | library: 'ReactWhiteboard',
9 | libraryTarget: 'umd'
10 | },
11 | module: {
12 | rules: [{
13 | test: /\.tsx?$/,
14 | exclude: /node_modules/,
15 | use: {
16 | loader: 'ts-loader'
17 | }
18 | }]
19 | },
20 | resolve: {
21 | extensions: [ '.tsx', '.ts', '.js' ]
22 | },
23 | externals: {
24 | 'react': 'react',
25 | 'react-dom': 'react-dom'
26 | },
27 | mode: 'production',
28 | devtool: 'source-map'
29 | };
30 |
--------------------------------------------------------------------------------