├── .browserslistrc ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── README.md ├── assets ├── img │ └── how-to-work-v3.png └── tools │ ├── crayon-texture-origin.png │ └── crayon-texture.png ├── config └── webpack │ ├── base.js │ ├── dev-server.js │ ├── index.js │ ├── module.js │ └── plugins.js ├── dist └── boardy-1.0.0.min.js ├── docs ├── development.md └── examples │ ├── add-tools.html │ ├── add-tools.min.js │ ├── app.html │ ├── app.min.js │ ├── extend-tools.html │ ├── extend-tools.min.js │ ├── real-time-communication.html │ ├── real-time-communication.min.js │ ├── resize.html │ ├── resize.min.js │ ├── simple.html │ └── simple.min.js ├── examples ├── add-tools │ ├── index.html │ └── index.ts ├── app │ ├── index.html │ └── index.ts ├── extend-tools │ ├── index.html │ └── index.ts ├── real-time-communication │ ├── index.html │ └── index.ts ├── resize │ ├── index.html │ └── index.ts └── simple │ ├── index.html │ └── index.ts ├── package.json ├── src ├── @types │ └── base.d.ts ├── Boardy.ts ├── constants │ ├── crayonTexture.ts │ └── index.ts ├── index.ts ├── modules │ ├── Painter │ │ ├── Painter.ts │ │ ├── index.ts │ │ └── tools │ │ │ ├── blackcrayon.ts │ │ │ ├── blackline.ts │ │ │ ├── eraser.ts │ │ │ └── index.ts │ ├── Rasterizer │ │ ├── Rasterizer.ts │ │ └── index.ts │ ├── Renderer │ │ ├── Renderer.ts │ │ └── index.ts │ ├── Tool │ │ ├── Tool.ts │ │ └── index.ts │ └── Trigger │ │ ├── Trigger.ts │ │ └── index.ts └── utils │ ├── EventChannel │ ├── EventChannel.ts │ └── index.ts │ ├── IdGenerator │ ├── IdGenerator.ts │ └── index.ts │ ├── ResizeObserver │ ├── ResizeObserver.ts │ └── index.ts │ ├── debounce.ts │ └── index.ts ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 version 2 | > 1% 3 | IE 10 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.js 2 | **/*.html 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2020": true 5 | }, 6 | "extends": [ 7 | "google" 8 | ], 9 | "parser": "@typescript-eslint/parser", 10 | "parserOptions": { 11 | "ecmaVersion": 11, 12 | "sourceType": "module" 13 | }, 14 | "plugins": [ 15 | "@typescript-eslint" 16 | ], 17 | "rules": { 18 | "require-jsdoc": "off", 19 | "indent": ["error", 2], 20 | "no-unused-vars": "off", 21 | "valid-jsdoc": "off", 22 | "no-invalid-this": "off", 23 | "max-len": ["error", { "code": 100 }], 24 | "guard-for-in": "off" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .env 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Boardy 2 | 3 | **Boardy** is a Javascript library for drawing on web browsers. 4 | it supports following three features, basically. 5 | 6 | 1. **Responsive** : boardy keeps clear draw output in responsive environment. if image is handled as bitmap, there must be a limit at variouse resolutions. so we solve this problem using **vector-based state**. 7 | 2. **Persist** : boardy is designed to trigger draw actions (20 bytes), and paint by inject it. in other words, you can maintain painted states by storing the actions. besides, you can also convert these actions to static svg image. 8 | 3. **Fast realtime communication** : we developed this module in consideration of realtime communication beginning. so boardy allows you to share your blackboard handwriting on the internet with minimal payload. (not protocol payload, only painting payload) 9 | 10 |
11 | 12 | ## How to work 13 | 14 | how-to-work 15 | 16 |
17 | 18 | ## Cross Browsing 19 | 20 | - `last 2 version` 21 | - `> 1%` 22 | - `IE 10` 23 | -------------------------------------------------------------------------------- /assets/img/how-to-work-v3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devphilip21/boardy/496132ffbacaaa197ac565328fd730f78ef8259b/assets/img/how-to-work-v3.png -------------------------------------------------------------------------------- /assets/tools/crayon-texture-origin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devphilip21/boardy/496132ffbacaaa197ac565328fd730f78ef8259b/assets/tools/crayon-texture-origin.png -------------------------------------------------------------------------------- /assets/tools/crayon-texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devphilip21/boardy/496132ffbacaaa197ac565328fd730f78ef8259b/assets/tools/crayon-texture.png -------------------------------------------------------------------------------- /config/webpack/base.js: -------------------------------------------------------------------------------- 1 | const Path = require('path'); 2 | const PackageJson = require('../../package.json'); 3 | 4 | module.exports = (mode, example) => { 5 | const isDev = mode === 'development'; 6 | const isExample = mode === 'example'; 7 | 8 | const {version} = PackageJson; 9 | const entry = (isDev || isExample) ? 10 | Path.resolve(__dirname, `../../examples/${example}/index.ts`) : 11 | Path.resolve(__dirname, '../../src/index.ts'); 12 | const output = (isDev || isExample) ? { 13 | path: Path.resolve(__dirname, '../../docs/examples'), 14 | filename: `${example}.min.js`, 15 | } : { 16 | path: Path.resolve(__dirname, '../../dist'), 17 | filename: `boardy-${version}.min.js`, 18 | }; 19 | 20 | console.log(`[[ Path ]]\n entry: ${entry}\n output: ${output.path}\n filename: ${output.filename}\n`) 21 | 22 | return { 23 | mode: isExample ? 'production' : mode, 24 | entry, 25 | output, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /config/webpack/dev-server.js: -------------------------------------------------------------------------------- 1 | module.exports = (mode) => { 2 | const isDev = mode === 'development'; 3 | 4 | if (isDev) { 5 | return { 6 | devServer: { 7 | port: 8000, 8 | }, 9 | }; 10 | } 11 | 12 | return {}; 13 | }; 14 | -------------------------------------------------------------------------------- /config/webpack/index.js: -------------------------------------------------------------------------------- 1 | const configBase = require('./base'); 2 | const configDevServer = require('./dev-server'); 3 | const configModule = require('./module'); 4 | const configPlugins = require('./plugins'); 5 | 6 | /** 7 | * config merge pipe 8 | * @param {string} mode 9 | * @return {any} config 10 | */ 11 | module.exports = (mode, example) => { 12 | const config = { 13 | ...configBase(mode, example), 14 | ...configDevServer(mode), 15 | ...configModule(mode), 16 | ...configPlugins(mode, example), 17 | }; 18 | 19 | return config; 20 | }; 21 | -------------------------------------------------------------------------------- /config/webpack/module.js: -------------------------------------------------------------------------------- 1 | const Path = require('path'); 2 | const Fs = require('fs'); 3 | 4 | module.exports = () => { 5 | const browsers = Fs 6 | .readFileSync(Path.resolve(__dirname, '../../.browserslistrc'), { 7 | encoding: 'utf8', 8 | }) 9 | .split('\n') 10 | .map((str) => str.trim()) 11 | .filter((str) => str); 12 | 13 | const logBrowsers = `${browsers.map(browser => ` - ${browser}`).join('\n')}`; 14 | const logMessage = `[[ Supported Browsers ]]\n${logBrowsers}\n`; 15 | 16 | console.log(logMessage); 17 | 18 | return { 19 | resolve: { 20 | alias: { 21 | '@': Path.resolve(__dirname, '../../src'), 22 | }, 23 | extensions: ['.mjs', '.js', '.mts', '.ts'], 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.(ts)/, 29 | exclude: /node_modules/, 30 | loader: 'babel-loader', 31 | options: { 32 | presets: [ 33 | '@babel/preset-typescript', 34 | [ 35 | '@babel/preset-env', { 36 | targets: {browsers}, 37 | }, 38 | ], 39 | ], 40 | plugins: [ 41 | '@babel/plugin-proposal-class-properties', 42 | ], 43 | }, 44 | }, 45 | ], 46 | }, 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /config/webpack/plugins.js: -------------------------------------------------------------------------------- 1 | const Path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 4 | 5 | module.exports = (mode, example) => { 6 | const config = {}; 7 | const isDev = mode === 'development'; 8 | const isExample = mode === 'example'; 9 | 10 | if (isDev) { 11 | config.plugins = [ 12 | new HtmlWebpackPlugin({ 13 | template: Path.resolve(__dirname, `../../examples/${example}/index.html`), 14 | }), 15 | ]; 16 | } else if (isExample) { 17 | config.plugins = [ 18 | new HtmlWebpackPlugin({ 19 | filename: `${example}.html`, 20 | template: Path.resolve(__dirname, `../../examples/${example}/index.html`), 21 | }), 22 | ]; 23 | } 24 | 25 | if (!isDev) { 26 | config.optimization = { 27 | minimizer: [ 28 | new UglifyJsPlugin(), 29 | ], 30 | } 31 | } 32 | 33 | return config; 34 | }; 35 | -------------------------------------------------------------------------------- /dist/boardy-1.0.0.min.js: -------------------------------------------------------------------------------- 1 | !function(n){var r={};function o(e){if(r[e])return r[e].exports;var t=r[e]={i:e,l:!1,exports:{}};return n[e].call(t.exports,t,t.exports,o),t.l=!0,t.exports}o.m=n,o.c=r,o.d=function(e,t,n){o.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(t,e){if(1&e&&(t=o(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(o.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)o.d(n,r,function(e){return t[e]}.bind(null,r));return n},o.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(t,"a",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p="",o(o.s=0)}([function(e,t,n){"use strict";n.r(t),n.d(t,"default",function(){return r});var r=function e(){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),console.log("create boardy instance")}}]); -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Boardy Development Guide 2 | 3 | ## Scripts 4 | 5 | - `npm start` : start app on developer mode 6 | - `npm run build` : build source codes to javascript module 7 | 8 | ## Environment 9 | 10 | - **Language** : `typescript` 11 | - **Code Lint** : `eslint :: google guide` 12 | - **Bundle**: `webpack` 13 | - **Dev Server**: `webpack-dev-server` 14 | -------------------------------------------------------------------------------- /docs/examples/add-tools.html: -------------------------------------------------------------------------------- 1 | Add Tools :: Boardy Example -------------------------------------------------------------------------------- /docs/examples/add-tools.min.js: -------------------------------------------------------------------------------- 1 | !function(n){var r={};function o(e){if(r[e])return r[e].exports;var t=r[e]={i:e,l:!1,exports:{}};return n[e].call(t.exports,t,t.exports,o),t.l=!0,t.exports}o.m=n,o.c=r,o.d=function(e,t,n){o.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(t,e){if(1&e&&(t=o(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(o.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)o.d(n,r,function(e){return t[e]}.bind(null,r));return n},o.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(t,"a",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p="",o(o.s=0)}([function(e,t,n){"use strict";function o(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);nthis.context.size.width&&(e=this.context.size.width),e}},{key:"checkPointYOverflow",value:function(e){return e<0?e=0:e>this.context.size.height&&(e=this.context.size.height),e}}])&&m(e.prototype,t),n&&m(e,n),i}();function M(e,t){for(var n=0;ne.height?n.height:n.width,n}}])&&ie(e.prototype,n),r&&ie(e,r),t}();function se(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}ae(ce,"Tool",z),ae(ce,"ActionType",g);var le=new ce({canvas:document.querySelector("canvas")});le.on(function(e){le.render(e)});function fe(e,t){var n=t.pointX,r=t.pointY,o=t.unit;e.beginPath(),e.moveTo(n,r),e.lineJoin="round",e.lineWidth=2*o,e.strokeStyle="#f00"}function he(e){e.closePath()}var de=ce.Tool.create((se(ue={},g.MouseDown,fe),se(ue,g.DragIn,fe),se(ue,g.Drag,function(e,t){var n=t.pointX,r=t.pointY;e.lineTo(n,r),e.stroke()}),se(ue,g.MouseUp,he),se(ue,g.DragOut,he),ue));le.addTool("redLine",de),le.useTool("redLine")}]); -------------------------------------------------------------------------------- /docs/examples/app.html: -------------------------------------------------------------------------------- 1 | RTC :: Boardy Example

Leader Computer

Pen

Member Computers

1. it is drawn at a constant resolution, without being affected by the screen size.

2. members and leaders exchange only 20 bytes, no image. => cool network performance.

Greater Resolution

Smaller Resolution

-------------------------------------------------------------------------------- /docs/examples/app.min.js: -------------------------------------------------------------------------------- 1 | !function(n){var r={};function o(e){if(r[e])return r[e].exports;var t=r[e]={i:e,l:!1,exports:{}};return n[e].call(t.exports,t,t.exports,o),t.l=!0,t.exports}o.m=n,o.c=r,o.d=function(e,t,n){o.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(t,e){if(1&e&&(t=o(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(o.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)o.d(n,r,function(e){return t[e]}.bind(null,r));return n},o.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(t,"a",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p="",o(o.s=0)}([function(e,t,n){"use strict";function o(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);nthis.context.size.width&&(e=this.context.size.width),e}},{key:"checkPointYOverflow",value:function(e){return e<0?e=0:e>this.context.size.height&&(e=this.context.size.height),e}}])&&w(e.prototype,t),n&&w(e,n),i}();function M(e,t){for(var n=0;ne.height?n.height:n.width,n}}])&&ie(e.prototype,n),r&&ie(e,r),t}();function se(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}ae(ce,"Tool",z),ae(ce,"ActionType",g);var le,fe={send:function(e){le(e)},on:function(e){le=e}},he=new ce({canvas:document.querySelector("#canvas-leader")});he.useTool("blackline"),he.on(function(e){he.render(e),fe.send(e)});var de=new ce({canvas:document.querySelector("#canvas-member-1")}),ve=new ce({canvas:document.querySelector("#canvas-member-2")});fe.on(function(e){de.render(e),ve.render(e)});function pe(e,t){var n=t.unit;e.strokeStyle=be,e.lineWidth=3*n}var be="#000",ge=ce.Tool.extend(Q,(se(ue={},g.MouseDown,pe),se(ue,g.DragIn,pe),ue)),ye="globalColorLine";he.addTool(ye,ge),de.addTool(ye,ge),ve.addTool(ye,ge),we(ye);var Ae=[document.querySelector("#inp-pen"),document.querySelector("#btn-eraser")];function me(n){Ae.forEach(function(e,t){t===n?e.parentElement.classList.add("is-selected"):e.parentElement.classList.remove("is-selected")})}function we(e){he.useTool(e),de.useTool(e),ve.useTool(e)}me(0),Ae[0].addEventListener("input",function(e){be=e.target.value,we(ye),me(0)}),Ae[1].addEventListener("click",function(){we("eraser"),me(1)})}]); -------------------------------------------------------------------------------- /docs/examples/extend-tools.html: -------------------------------------------------------------------------------- 1 | Extend Tools :: Boardy Example -------------------------------------------------------------------------------- /docs/examples/extend-tools.min.js: -------------------------------------------------------------------------------- 1 | !function(n){var o={};function r(e){if(o[e])return o[e].exports;var t=o[e]={i:e,l:!1,exports:{}};return n[e].call(t.exports,t,t.exports,r),t.l=!0,t.exports}r.m=n,r.c=o,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(t,e){if(1&e&&(t=r(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)r.d(n,o,function(e){return t[e]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,n){"use strict";function r(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,o=new Array(t);nthis.context.size.width&&(e=this.context.size.width),e}},{key:"checkPointYOverflow",value:function(e){return e<0?e=0:e>this.context.size.height&&(e=this.context.size.height),e}}])&&m(e.prototype,t),n&&m(e,n),i}();function M(e,t){for(var n=0;ne.height?n.height:n.width,n}}])&&ie(e.prototype,n),o&&ie(e,o),t}();function le(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}ae(se,"Tool",z),ae(se,"ActionType",g);var fe=new se({canvas:document.querySelector("canvas")});fe.on(function(e){fe.render(e)});function he(e,t){var n=t.pointX,o=t.pointY,r=t.unit;e.beginPath(),e.moveTo(n,o),e.lineJoin="round",e.lineWidth=2*r,e.strokeStyle="#f00"}function de(e){e.closePath()}function ve(e,t){var n=t.unit;e.lineWidth=10*n}var pe=se.Tool.create((le(ue={},g.MouseDown,he),le(ue,g.DragIn,he),le(ue,g.Drag,function(e,t){var n=t.pointX,o=t.pointY;e.lineTo(n,o),e.stroke()}),le(ue,g.MouseUp,de),le(ue,g.DragOut,de),ue)),be=se.Tool.extend(pe,(le(ce={},g.MouseDown,ve),le(ce,g.DragIn,ve),ce));fe.addTool("thickRedLine",be),fe.useTool("thickRedLine")}]); -------------------------------------------------------------------------------- /docs/examples/real-time-communication.html: -------------------------------------------------------------------------------- 1 | RTC :: Boardy Example -------------------------------------------------------------------------------- /docs/examples/real-time-communication.min.js: -------------------------------------------------------------------------------- 1 | !function(n){var r={};function o(e){if(r[e])return r[e].exports;var t=r[e]={i:e,l:!1,exports:{}};return n[e].call(t.exports,t,t.exports,o),t.l=!0,t.exports}o.m=n,o.c=r,o.d=function(e,t,n){o.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(t,e){if(1&e&&(t=o(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(o.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)o.d(n,r,function(e){return t[e]}.bind(null,r));return n},o.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(t,"a",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p="",o(o.s=0)}([function(e,t,n){"use strict";function o(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);nthis.context.size.width&&(e=this.context.size.width),e}},{key:"checkPointYOverflow",value:function(e){return e<0?e=0:e>this.context.size.height&&(e=this.context.size.height),e}}])&&w(e.prototype,t),n&&w(e,n),i}();function M(e,t){for(var n=0;ne.height?n.height:n.width,n}}])&&ie(e.prototype,n),r&&ie(e,r),t}();ae(ue,"Tool",D),ae(ue,"ActionType",g);var ce,se={send:function(e){ce(e)},on:function(e){ce=e}},le=new ue({canvas:document.querySelector("#leader-canvas")});le.useTool("blackline"),le.on(function(e){le.render(e),se.send(e)});var fe=new ue({canvas:document.querySelector("#member-canvas-1")}),he=new ue({canvas:document.querySelector("#member-canvas-2")});se.on(function(e){fe.render(e),he.render(e)})}]); -------------------------------------------------------------------------------- /docs/examples/resize.html: -------------------------------------------------------------------------------- 1 | Resize :: Boardy Example -------------------------------------------------------------------------------- /docs/examples/resize.min.js: -------------------------------------------------------------------------------- 1 | !function(n){var r={};function o(e){if(r[e])return r[e].exports;var t=r[e]={i:e,l:!1,exports:{}};return n[e].call(t.exports,t,t.exports,o),t.l=!0,t.exports}o.m=n,o.c=r,o.d=function(e,t,n){o.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(t,e){if(1&e&&(t=o(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(o.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)o.d(n,r,function(e){return t[e]}.bind(null,r));return n},o.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(t,"a",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p="",o(o.s=0)}([function(e,t,n){"use strict";function o(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);nthis.context.size.width&&(e=this.context.size.width),e}},{key:"checkPointYOverflow",value:function(e){return e<0?e=0:e>this.context.size.height&&(e=this.context.size.height),e}}])&&m(e.prototype,t),n&&m(e,n),i}();function M(e,t){for(var n=0;ne.height?n.height:n.width,n}}])&&ie(e.prototype,n),r&&ie(e,r),t}();ae(ue,"Tool",D),ae(ue,"ActionType",g);var ce=new ue({canvas:document.querySelector("canvas")});ce.useTool("blackline"),ce.on(function(e){ce.render(e)})}]); -------------------------------------------------------------------------------- /docs/examples/simple.html: -------------------------------------------------------------------------------- 1 | Simple :: Boardy Example -------------------------------------------------------------------------------- /docs/examples/simple.min.js: -------------------------------------------------------------------------------- 1 | !function(n){var r={};function o(e){if(r[e])return r[e].exports;var t=r[e]={i:e,l:!1,exports:{}};return n[e].call(t.exports,t,t.exports,o),t.l=!0,t.exports}o.m=n,o.c=r,o.d=function(e,t,n){o.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(t,e){if(1&e&&(t=o(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(o.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)o.d(n,r,function(e){return t[e]}.bind(null,r));return n},o.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(t,"a",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p="",o(o.s=0)}([function(e,t,n){"use strict";function o(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);nthis.context.size.width&&(e=this.context.size.width),e}},{key:"checkPointYOverflow",value:function(e){return e<0?e=0:e>this.context.size.height&&(e=this.context.size.height),e}}])&&m(e.prototype,t),n&&m(e,n),i}();function M(e,t){for(var n=0;ne.height?n.height:n.width,n}}])&&ie(e.prototype,n),r&&ie(e,r),t}();ae(ue,"Tool",D),ae(ue,"ActionType",g);var ce=new ue({canvas:document.querySelector("canvas")});ce.useTool("blackline"),ce.on(function(e){ce.render(e)})}]); -------------------------------------------------------------------------------- /examples/add-tools/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Add Tools :: Boardy Example 7 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/add-tools/index.ts: -------------------------------------------------------------------------------- 1 | import Boardy, {ActionType} from '@/Boardy'; 2 | import {Drawing} from '@/modules/Tool'; 3 | 4 | const boardy = new Boardy({ 5 | canvas: document.querySelector('canvas'), 6 | }); 7 | 8 | boardy.on((action) => { 9 | boardy.render(action); 10 | }); 11 | 12 | // 1. create your custom tools! 13 | // - args: { [actionType]: DrawingFunction } 14 | // - actionType: declared in Boardy.ActionType 15 | // - DrawingFunction: ( 16 | // ctx: CanvasContext, 17 | // values: { 18 | // pointX: CoordinateX, 19 | // pointY: CoordinateY, 20 | // unit: OnePixelByResolution 21 | // } 22 | // ) => void 23 | const startRedLine: Drawing = (ctx, {pointX, pointY, unit}) => { 24 | ctx.beginPath(); 25 | ctx.moveTo(pointX, pointY); 26 | ctx.lineJoin = 'round'; 27 | // if you want to set it to 1px, 28 | // use 1 * unit (multiply unit). 29 | // it will be drawn in 1px that can correspond to various resolutions 30 | ctx.lineWidth = 2 * unit; 31 | ctx.strokeStyle = '#f00'; 32 | }; 33 | const drawRedLine: Drawing = (ctx, {pointX, pointY}) => { 34 | ctx.lineTo(pointX, pointY); 35 | ctx.stroke(); 36 | }; 37 | const endRedLine: Drawing = (ctx) => { 38 | ctx.closePath(); 39 | }; 40 | 41 | const redLine = Boardy.Tool.create({ 42 | // if mouse down or entering the canvas, beginPath and set path style. 43 | [ActionType.MouseDown]: startRedLine, 44 | [ActionType.DragIn]: startRedLine, 45 | // if dragging(mouse down and move), draw line 46 | [ActionType.Drag]: drawRedLine, 47 | // if mouse up or exit the canvas, close path. 48 | [ActionType.MouseUp]: endRedLine, 49 | [ActionType.DragOut]: endRedLine, 50 | }); 51 | 52 | 53 | // 2. add tool with name. 54 | // - you can use this tool by name 55 | boardy.addTool('redLine', redLine); 56 | 57 | // 3. use tool by name 58 | boardy.useTool('redLine'); 59 | -------------------------------------------------------------------------------- /examples/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | RTC :: Boardy Example 7 | 8 | 34 | 35 | 36 |
37 |

Leader Computer

38 |
39 |
40 | 41 |
42 |
43 |
44 | Pen 45 | 46 |
47 | 51 |
52 | 53 |
54 |
55 |
56 |
57 |
58 |

Member Computers

59 |

1. it is drawn at a constant resolution, without being affected by the screen size.

60 |

2. members and leaders exchange only 20 bytes, no image. => cool network performance.

61 |
62 |

Greater Resolution

63 | 64 |
65 |
66 |

Smaller Resolution

67 | 68 |
69 |
70 | 71 | 72 | -------------------------------------------------------------------------------- /examples/app/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable spaced-comment */ 2 | import Boardy, {ActionType} from '@/Boardy'; 3 | import blackline from '@/modules/Painter/tools/blackline'; 4 | import {Drawing} from '@/modules/Tool'; 5 | 6 | ///////////////////////////////////// 7 | /////////// Server ////////////////// 8 | ///////////////////////////////////// 9 | 10 | // virtual socket module for example 11 | // - to show like real-time communication 12 | const socket = createVirtualSocket(); 13 | 14 | ///////////////////////////////////// 15 | /////////// Leader Client /////////// 16 | ///////////////////////////////////// 17 | 18 | // 1. initialize boardy module 19 | const leader = new Boardy({ 20 | canvas: document.querySelector('#canvas-leader'), 21 | }); 22 | 23 | // 2. set drawing tool 24 | // - blackline is default tool 25 | leader.useTool('blackline'); 26 | 27 | // 3. if action is triggered, 28 | // 3.1. render on self canvas. 29 | // 3.2. send action using socket. 30 | leader.on((action) => { 31 | leader.render(action); 32 | socket.send(action); 33 | }); 34 | 35 | 36 | //////////////////////////////////// 37 | /////////// Member Clients ///////// 38 | //////////////////////////////////// 39 | 40 | const firstMember = new Boardy({ 41 | canvas: document.querySelector('#canvas-member-1'), 42 | }); 43 | const secondMember = new Boardy({ 44 | canvas: document.querySelector('#canvas-member-2'), 45 | }); 46 | 47 | socket.on((action) => { 48 | firstMember.render(action); 49 | secondMember.render(action); 50 | }); 51 | 52 | 53 | //////////////////////////////////// 54 | ///////// Custom Tool ////////////// 55 | //////////////////////////////////// 56 | 57 | // create line tool (to use global color) 58 | // - see example[add-tools] for details. 59 | // - see example[extend-tools] for details. 60 | let penColor = '#000'; 61 | 62 | /** 63 | * line using global color 64 | */ 65 | const startLine: Drawing = (ctx, {unit}) => { 66 | ctx.strokeStyle = penColor; 67 | ctx.lineWidth = 3 * unit; 68 | }; 69 | const globalColorLine = Boardy.Tool.extend(blackline, { 70 | [ActionType.MouseDown]: startLine, 71 | [ActionType.DragIn]: startLine, 72 | }); 73 | 74 | /** 75 | * wave using global color 76 | */ 77 | const lineToolName = 'globalColorLine'; 78 | 79 | (function initTool() { 80 | leader.addTool(lineToolName, globalColorLine); 81 | firstMember.addTool(lineToolName, globalColorLine); 82 | secondMember.addTool(lineToolName, globalColorLine); 83 | allClientsUseTool(lineToolName); 84 | })(); 85 | 86 | //////////////////////////////////// 87 | /////////// View /////////////////// 88 | //////////////////////////////////// 89 | 90 | const toolSelectElements = [ 91 | document.querySelector('#inp-pen'), 92 | document.querySelector('#btn-eraser'), 93 | ]; 94 | 95 | highlightTool(0); 96 | 97 | toolSelectElements[0].addEventListener('input', (e: any) => { 98 | penColor = e.target.value; 99 | allClientsUseTool(lineToolName); 100 | highlightTool(0); 101 | }); 102 | 103 | toolSelectElements[1].addEventListener('click', () => { 104 | allClientsUseTool('eraser'); 105 | highlightTool(1); 106 | }); 107 | 108 | function highlightTool(index) { 109 | toolSelectElements.forEach((el, i) => { 110 | if (i === index) el.parentElement.classList.add('is-selected'); 111 | else el.parentElement.classList.remove('is-selected'); 112 | }); 113 | } 114 | 115 | //////////////////////////////////// 116 | /////////// Util /////////////////// 117 | //////////////////////////////////// 118 | 119 | function allClientsUseTool(toolName) { 120 | leader.useTool(toolName); 121 | firstMember.useTool(toolName); 122 | secondMember.useTool(toolName); 123 | } 124 | 125 | function createVirtualSocket() { 126 | let communication; 127 | 128 | return { 129 | send(action) { 130 | communication(action); 131 | }, 132 | on(fn) { 133 | communication = fn; 134 | }, 135 | }; 136 | } 137 | -------------------------------------------------------------------------------- /examples/extend-tools/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Extend Tools :: Boardy Example 7 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/extend-tools/index.ts: -------------------------------------------------------------------------------- 1 | import Boardy, {ActionType} from '@/Boardy'; 2 | import {Drawing} from '@/modules/Tool'; 3 | 4 | const boardy = new Boardy({ 5 | canvas: document.querySelector('canvas'), 6 | }); 7 | 8 | boardy.on((action) => { 9 | boardy.render(action); 10 | }); 11 | 12 | // 1. maybe parent tool exist. 13 | // - see example[add-tools] for details. 14 | const startRedLine: Drawing = (ctx, {pointX, pointY, unit}) => { 15 | ctx.beginPath(); 16 | ctx.moveTo(pointX, pointY); 17 | ctx.lineJoin = 'round'; 18 | ctx.lineWidth = 2 * unit; 19 | ctx.strokeStyle = '#f00'; 20 | }; 21 | const drawRedLine: Drawing = (ctx, {pointX, pointY}) => { 22 | ctx.lineTo(pointX, pointY); 23 | ctx.stroke(); 24 | }; 25 | const endRedLine: Drawing = (ctx) => { 26 | ctx.closePath(); 27 | }; 28 | 29 | const redLine = Boardy.Tool.create({ 30 | [ActionType.MouseDown]: startRedLine, 31 | [ActionType.DragIn]: startRedLine, 32 | [ActionType.Drag]: drawRedLine, 33 | [ActionType.MouseUp]: endRedLine, 34 | [ActionType.DragOut]: endRedLine, 35 | }); 36 | 37 | 38 | // 2. extends your custom tools! 39 | // - args[0]: parent tool 40 | // - args[1]: { [actionType]: DrawingFunction } 41 | // - actionType: declared in Boardy.ActionType 42 | // - DrawingFunction: ( 43 | // ctx: CanvasContext, 44 | // pointX: CoordinateX, 45 | // pointY: CoordinateY, 46 | // unit: OnePixelByResolution 47 | // ) => void 48 | const startThickRedLine = (ctx, {unit}) => { 49 | ctx.lineWidth = 10 * unit; 50 | }; 51 | const thickRedLine = Boardy.Tool.extend(redLine, { 52 | // if mouse down, beginPath and set path style. 53 | [ActionType.MouseDown]: startThickRedLine, 54 | [ActionType.DragIn]: startThickRedLine, 55 | }); 56 | 57 | 58 | // 2. add tool with name. 59 | // - you can use this tool by name 60 | boardy.addTool('thickRedLine', thickRedLine); 61 | 62 | // 3. use tool by name 63 | boardy.useTool('thickRedLine'); 64 | -------------------------------------------------------------------------------- /examples/real-time-communication/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | RTC :: Boardy Example 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/real-time-communication/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable spaced-comment */ 2 | import Boardy from '@/Boardy'; 3 | 4 | ///////////////////////////////////// 5 | /////////// Server ////////////////// 6 | ///////////////////////////////////// 7 | 8 | // virtual socket module 9 | // to show like real-time communication 10 | const socket = createVirtualSocket(); 11 | 12 | 13 | ///////////////////////////////////// 14 | /////////// Leader Client /////////// 15 | ///////////////////////////////////// 16 | 17 | // 1. initialize boardy module 18 | const leader = new Boardy({ 19 | canvas: document.querySelector('#leader-canvas'), 20 | }); 21 | 22 | // 2. set drawing tool 23 | // - blackline is default tool 24 | leader.useTool('blackline'); 25 | 26 | // 3. if action is triggered, 27 | // 3.1. render on self canvas. 28 | // 3.2. send action using socket. 29 | leader.on((action) => { 30 | leader.render(action); 31 | socket.send(action); 32 | }); 33 | 34 | 35 | //////////////////////////////////// 36 | /////////// Member Clients ///////// 37 | //////////////////////////////////// 38 | 39 | const firstMember = new Boardy({ 40 | canvas: document.querySelector('#member-canvas-1'), 41 | }); 42 | const secondMember = new Boardy({ 43 | canvas: document.querySelector('#member-canvas-2'), 44 | }); 45 | 46 | socket.on((action) => { 47 | firstMember.render(action); 48 | secondMember.render(action); 49 | }); 50 | 51 | 52 | //////////////////////////////////// 53 | /////////// Util /////////////////// 54 | //////////////////////////////////// 55 | 56 | function createVirtualSocket() { 57 | let communication; 58 | 59 | return { 60 | send(action) { 61 | communication(action); 62 | }, 63 | on(fn) { 64 | communication = fn; 65 | }, 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /examples/resize/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Resize :: Boardy Example 7 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/resize/index.ts: -------------------------------------------------------------------------------- 1 | import Boardy from '@/Boardy'; 2 | 3 | const boardy = new Boardy({ 4 | canvas: document.querySelector('canvas'), 5 | }); 6 | 7 | boardy.useTool('blackline'); 8 | boardy.on((action) => { 9 | boardy.render(action); 10 | }); 11 | -------------------------------------------------------------------------------- /examples/simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simple :: Boardy Example 7 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/simple/index.ts: -------------------------------------------------------------------------------- 1 | import Boardy from '@/Boardy'; 2 | 3 | // 1. initialize boardy module 4 | const boardy = new Boardy({ 5 | canvas: document.querySelector('canvas'), 6 | }); 7 | 8 | // 2. set drawing tool 9 | // - blackline is default tool 10 | boardy.useTool('blackline'); 11 | 12 | // 3. if action is triggered, 13 | // you should be able to render it 14 | // by connecting it to renderer module (same in real-time model!) 15 | boardy.on((action) => { 16 | boardy.render(action); 17 | }); 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boardy", 3 | "version": "1.0.0", 4 | "description": "responsive, persist, reactive drawing tools on web browser", 5 | "main": "src/index.ts", 6 | "scripts": { 7 | "<< COMMENT_1 >>": "example=[example name(dirname)] yarn start", 8 | "<< COMMENT_2 >>": "ex) example=simple yarn start", 9 | "start": "NODE_ENV=development webpack-dev-server", 10 | "<< COMMENT_3 >>": "example=[example name(dirname)] yarn build:example", 11 | "<< COMMENT_4 >>": "ex) example=simple yarn build:example", 12 | "build:example": "NODE_ENV=example webpack", 13 | "build": "NODE_ENV=production webpack" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/load0ne/boardy.git" 18 | }, 19 | "keywords": [ 20 | "board", 21 | "drawing", 22 | "drawing tool", 23 | "responsive", 24 | "vector based", 25 | "stateful", 26 | "context", 27 | "canvas", 28 | "persist" 29 | ], 30 | "author": "load0ne", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/load0ne/boardy/issues" 34 | }, 35 | "homepage": "https://github.com/load0ne/boardy#readme", 36 | "devDependencies": { 37 | "@babel/core": "^7.10.4", 38 | "@babel/plugin-proposal-class-properties": "^7.10.4", 39 | "@babel/preset-env": "^7.10.4", 40 | "@babel/preset-typescript": "^7.10.4", 41 | "@typescript-eslint/eslint-plugin": "^3.5.0", 42 | "@typescript-eslint/parser": "^3.5.0", 43 | "babel-loader": "^8.1.0", 44 | "eslint": "^7.4.0", 45 | "eslint-config-google": "^0.14.0", 46 | "html-webpack-plugin": "^4.3.0", 47 | "typescript": "^3.9.6", 48 | "uglifyjs-webpack-plugin": "^2.2.0", 49 | "webpack": "^4.43.0", 50 | "webpack-cli": "^3.3.12", 51 | "webpack-dev-server": "^3.11.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/@types/base.d.ts: -------------------------------------------------------------------------------- 1 | export type ContextCanvas = { 2 | screen: HTMLCanvasElement, 3 | offscreen: HTMLCanvasElement, 4 | } 5 | 6 | export type ContextCtx = { 7 | screen: CanvasRenderingContext2D, 8 | offscreen: CanvasRenderingContext2D, 9 | } 10 | 11 | export type ContextSize = { 12 | width: number, 13 | height: number, 14 | } 15 | 16 | export type ContextResolution = { 17 | width: number, 18 | height: number, 19 | } 20 | 21 | export type ContextUnit = { 22 | width: number, 23 | height: number, 24 | contents: number, 25 | } 26 | 27 | export type Context = { 28 | canvas: ContextCanvas, 29 | ctx: ContextCtx, 30 | size: ContextSize, 31 | resolution: ContextResolution, 32 | unit: ContextUnit, 33 | } 34 | 35 | /** 36 | * @description 37 | * [ActionType, PointX, PointY, Unit, toolId] 38 | */ 39 | export type Action = Uint32Array; 40 | -------------------------------------------------------------------------------- /src/Boardy.ts: -------------------------------------------------------------------------------- 1 | import Trigger from '@/modules/Trigger'; 2 | import Renderer from '@/modules/Renderer'; 3 | import Painter from '@/modules/Painter'; 4 | import Rasterizer from '@/modules/Rasterizer'; 5 | import Tool from '@/modules/Tool'; 6 | import {Context, Action, ContextSize, ContextResolution, ContextUnit} from '@/@types/base'; 7 | import {ActionType} from '@/constants'; 8 | import {ResizeObserver} from '@/utils'; 9 | 10 | type IntializeOptions = { 11 | canvas?: HTMLCanvasElement, 12 | resolution?: { 13 | width?: number, 14 | height?: number, 15 | } 16 | painter?: Painter 17 | } 18 | 19 | /** 20 | * Entry Module 21 | */ 22 | export default class Boardy { 23 | private context: Context; 24 | private painter: Painter; 25 | private renderer: Renderer; 26 | private trigger: Trigger; 27 | private resizeObserver: ResizeObserver; 28 | 29 | constructor(options: IntializeOptions) { 30 | if (!options.canvas) { 31 | throw new Error('canvas element is required!'); 32 | } 33 | 34 | // global context of all modules. 35 | this.context = this.initalizeContext(options); 36 | 37 | // manage drawing tools 38 | this.painter = options.painter || new Painter(this.context); 39 | 40 | // dispatcher role in flux architecture. 41 | this.renderer = new Renderer( 42 | this.painter, 43 | new Rasterizer(this.context), 44 | ); 45 | 46 | // trigger events (ex. mouse event) 47 | this.trigger = new Trigger(this.context); 48 | 49 | // on resize 50 | this.resizeObserver = new ResizeObserver(this.context.canvas.screen, { 51 | debounceTime: 300, 52 | }); 53 | this.resizeObserver.on(this.handleResize); 54 | } 55 | 56 | public on(handler: (action: Action) => void) { 57 | this.trigger.on(handler); 58 | } 59 | 60 | public render(action: Action) { 61 | this.renderer.render(action); 62 | } 63 | 64 | public addTool(toolName: string, tool: Tool) { 65 | this.painter.addTool(toolName, tool); 66 | } 67 | 68 | public useTool(toolName: string) { 69 | this.trigger.setTool(toolName); 70 | } 71 | 72 | /** 73 | * options => context 74 | * + offscreen canvas 75 | * + unit 76 | */ 77 | private initalizeContext(options?: IntializeOptions): Context { 78 | const screenElement: HTMLCanvasElement = options.canvas; 79 | const offscreenElement: HTMLCanvasElement = document.createElement('canvas'); 80 | const size: ContextSize = this.intializeSize(options); 81 | const resolution: ContextResolution = this.initializeResolution(options, size); 82 | const unit: ContextUnit = this.calculateUnit(size, resolution); 83 | 84 | screenElement.width = size.width; 85 | screenElement.height = size.height; 86 | offscreenElement.width = resolution.width; 87 | offscreenElement.height = resolution.height; 88 | 89 | return { 90 | canvas: { 91 | screen: screenElement, 92 | offscreen: offscreenElement, 93 | }, 94 | ctx: { 95 | screen: screenElement.getContext('2d'), 96 | offscreen: offscreenElement.getContext('2d'), 97 | }, 98 | size, 99 | resolution, 100 | unit, 101 | }; 102 | } 103 | 104 | private intializeSize(options: IntializeOptions): ContextSize { 105 | const size: ContextSize = options.canvas?.offsetWidth ? { 106 | width: options.canvas.offsetWidth, 107 | height: options.canvas.offsetHeight, 108 | } : { 109 | width: 500, 110 | height: 500, 111 | }; 112 | 113 | return size; 114 | } 115 | 116 | private initializeResolution( 117 | options: IntializeOptions, 118 | size: ContextSize, 119 | ): ContextResolution { 120 | const resolution: ContextResolution = { 121 | width: 0, 122 | height: 0, 123 | }; 124 | 125 | if (options.resolution) { 126 | resolution.width = options.resolution.width || 127 | (options.resolution.height / size.height) * size.width; 128 | resolution.height = options.resolution.height || 129 | (options.resolution.width / size.width) * size.height; 130 | } else { 131 | resolution.width = 2048; 132 | resolution.height = (resolution.width / size.width * size.height); 133 | } 134 | 135 | return resolution; 136 | } 137 | 138 | private calculateUnit( 139 | size: ContextSize, 140 | resolution: ContextResolution, 141 | ): ContextUnit { 142 | const unit: ContextUnit = { 143 | width: 0, 144 | height: 0, 145 | contents: 0, 146 | }; 147 | 148 | unit.width = resolution.width / size.width; 149 | unit.height = resolution.height / size.height; 150 | unit.contents = size.width > size.height ? unit.height : unit.width; 151 | 152 | return unit; 153 | } 154 | 155 | private readonly handleResize = ({width, height}) => { 156 | const nextUnit = this.calculateUnit({width, height}, this.context.resolution); 157 | 158 | this.context.size.width = width; 159 | this.context.size.height = height; 160 | this.context.canvas.screen.width = width; 161 | this.context.canvas.screen.height = height; 162 | this.context.unit.width = nextUnit.width; 163 | this.context.unit.height = nextUnit.height; 164 | this.renderer.render(null); 165 | } 166 | 167 | static Tool = Tool; 168 | static ActionType = ActionType; 169 | } 170 | 171 | export {ActionType} from './constants'; 172 | -------------------------------------------------------------------------------- /src/constants/crayonTexture.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | width: 34, 4 | height: 34, 5 | src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAAiCAYAAAA6RwvCAAAAAXNSR0IArs4c6QAAAIRlWElmTU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAABgAAAAAQAAAGAAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAACKgAwAEAAAAAQAAACIAAAAAHh0gkAAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KTMInWQAACr9JREFUWAk9l3lvG+cRxke8T4miRN335dix0zhBjDgpkLZ23K+l75T/W8AojDR2HNlK7fiodR8USVGkeItHn98I6gLE7nLfnXfmmWeemR364v6DwfBIyvb2P9rC7LRVKlX74os/2avfXtv83IpNTc3Y1qvXNjE9bns7ezY7O2PFUsnCQ0EbmFm3e2VDQ0MWDUdtZXXZqnq/UruwVCpl9Xrdvvr6S3vx/IWtra3b27d/2EW5qrcCdmvjju3u7dhnn63Z/MKsBaqXNSsWz2xOTlxUyjY8krTSecFGR0dtenrGdnf2/cXTkzMbHk5bJBKxcChsSW00MjKizRoWlFMLiwt2lj+zdrtt3U5Pa0f0ntnrV9t+DoVC1mw1LZFIaPM7dnZWtEa9Zfx/fn5uwS/vf7NZLOUtM5qxYGBIhjo2mZuydGrYMiNjirhvp/m8xRJhazVb1mw2hcRACESs02lbNBq1y0uiNOt1e2ZDZjUFF9LzTCaj5zGrVqv24sVzW1patmgkbqenxxaJRiwYCtrBwb5FYxELfP75HXmfsaOjI3nYUCqmLH92JjRmZXog+HZtYmLckegPBnKsa+1W21qtlqIqeFri8bgjEQwGrHpRtXgiLQcSSnPNtre35bCCm5xWEHU7K5za/Py8TcpmLBaScws2Pj5ugXz+VPlMWi43qd+EJeIp+/67732TgTbOZjNKQ0I+DRTsQCnL6LKvaAK2vLJoV1dt6/d7zhXOuVzOIuLL0eGpoj22pJzq9YSUjouLCwsGQb1pY7msBXRdLBX0ft8CvR5kC+iFpDYI2O3bd2zj1mfyNmYPHjxwXjx58qOlxY9isaCNOxYOhyygNCYScQtHwpZOp/x6bGzMrrodOXHiKZbfjiRcmJyccD5ks6Me2NbWbyqKe/b48WPZLVrwh78+2oTheNsR0ZriwcryqqpjzuLxpG1sbNit27fsvFxSSlpuGCL3lKJwGCeG7UrQO/wTk8p3VE5GRPoRodSx6uWFTU1PyX7ZkSTAavVS76XdWVLtSN3/+qvNsbGsCFcz65udnuS9zNJyTkCJVGGxumQvX/6q6EP/j75Rr9nJybHukyL1sNVql0pf32Zm5qygigCxVrshx8Mi8EA8yFq5fC5OrNh3D//sv1gsbvsHe+5I6OTkxCEjT0Q4Pp6z4+Mje/78uZy7VGqGxYeQneSPlPuIa8PMzIzQijvJ0Iq9nV2PsKr1v/zys2VHJ6zWqGntpQJR2Yq0OLy6umpPnvzdcuOTdnh4qJSUrNloC6GKBaj7SqXinICoeWkBnAEu6vtMFUSF8B+w4iy/Wq3mxoA2ozSgKX2RkooAyUQiJqGasWlVYUD3BPXo0Y+e9qurrr1980FlXLBkEm6ahe7duycjo57jrV9feqRDqo/sqFgtC+12yw6O9nQdtI31decITpwen2htwtecnuZtWiglhFJWaY7GGlZWIGZBdwISk/qffvrJ/vX0mQK7EukHej+mtF1nIlQQAidHx/JqIASKfiZNqXTCjo67vlk0DgGH7OPHj16Kva4pZVkZu7JSqWKDQMj+++mTiL1ui0uLdniUF6KXVqsjdCp1lakkRs5cuMhNTI5ZKKg0N1r2evulyJyz0Ln6Rrlc9g2AjwOniqWi9eRQWhwpX5RU6wNHrdfry0hUZdq3MfHJBU66QBp3Rby2KqWYr7o2jGSGrdGoe/Uk03Epc1t8DCiIlPhxbgG1BnTn4kIcQWwwRgkvLS25HOMIaaGvcDQaDSc0EHNQekOSct5F8h1BiSLrUGgqBQfgF2lEoQf9gKObV7v4IGS3trbsWFWXm8g5v0L0AQzRfDC0sLDgkHPPRpD2hqA4xzUEg7wQlKpCXVtqaIfi0qEcmZ/uKR3Yq6s9sFFKlXjsQWgrFzzO6M/Wb1s2MztloYmJCV/ABgcHB15iREU0RJsZyVi3F/BuTFoSiZQg7uic8EoANUYBzvQMnOyqKkC41a77WME1mlEuV9TDprXfkEs/yENWqjIEdMwTGEFFcWJyctLTxbORjMryomvffvutq25CreCf/3jqhkGMjQuFmlDtOarJZMrHgJKITw8LS3tKpXNVX9sdwAk6cletpdW8srgcDAaDFux2e5ugAgKIG15yTYTIdjqVVkV8dA1B/l9pSAoFNRIoCkYAnI2qpdM8c7kxD6pRq6tZoqRl59qgf735ysqqyJ9yhxkRUOor8ZOWEYCg8IADEqKUkBJkyH9XtbqysmJv3ryxp0+f2s7OJ49gbW3dU0cJI4ioLcgiXqhppXIhlAaWEkKgBCp7e3uupNBgYWHOJQHegWyIjYkeD31SEkwYBi5IDPTNUt2hnZ2ddaMo7Pv374SCBiZdoxUgWRHxJTcugiFNcVE5REdf1PSGozs7O/ZJeoNGtVoNBV1Vc52Vwp7a0A9/+duA/EFUuIF3eAmsoMUM22jSc9I+OnJ/1el77XsX1vqYJqxms+HkpHTbSmEkEpWtvo0Mk66g84LUMdXt7e94H8pIZ1qd7rW+kGNSQuSgQ96JjqqghHFybm5Oa1JuvFGXbmhz0gCHQPLysuKpBUEqLak+EwnHndDra7ft99/fOno0N4Yq1vFeX4Sl4piBQ0CGM2zKQ8QNtV1eXtaw27JbtzYc/kIhL0fPpE3X0Y2LRxB2TKRsafPs2KhSGxIqUk9pSGYk6+Pm/fv3vdqwfypCH+zv2Xjuurd1FdCUBvSANCnUlIg1hERMSHRkmE8E8g5fOIJKU0kTVK2qoVkPBwOhEb2evAaKrFgoWJ0yF7GT8bTNzy6pKx8ptZdCt2yHB4f2aeejI4jojWTSrj9kIaRJr925sp5QCgExgkM6QIb0oJjMG9dENFfTxcVFf3Yj26gwz0EFo9hw+f7wQVybcsJTBO8/vNe3TNli6rRoFSTlgIfFgkbESAyqW1CD0CZOsDFpoVpuNASeUGrc//j4id29e9c35B5H2BxOAftN9cEbxgNIyWZwZlQDOLy4tssnScuDGNX8GpdAdoRKUD1jEwTQChzB8LUeXBuBrOhMQvMrGz579sx7EWvhEURGbxiAEUY+R6isZDLhpcr7s3Mz9u7dO++0Mc20zKxra2vWV0pONIaQmuDdu/c2iQx4QaYkolLCtGdKGIS4/8/vb1wHSB8p4TmIoQtEjgCyjjbBOyWNEaQCTWK8ABEaKnMr76G8f/zxTgPVrI+iQSnkJgjgOflGKTEE1CCAMVRxIJVkDc7SsXGY+xvxu0np9va2O3WNrDnCh0cHnnr+o0p5n+tLBTWmgPh89ZkVj4GW/GKchUSHU8gy3zI4xDO4xAF3QAGHKXtQ5RpuZLNjIvywiyT9i+c4gNrCrd3dXV87r7SyR1wIhyAQN2zCAezAyJln8IO0gRpOYIgfB4iBEBJ9PZdeev6ZgRmQWU8aGs2a2yHQ9fVVH7BxjnfSGTU8fQMH8JR8I9ccbMg1zmEE8oEG0z0bwxF+oMi7Dx8+9GtQZP26BmzSBTL0Ef6rqxvfSAIfWuyxv7+v3lTRpL/gnxXBb755sIlRCMfmGOFlDPMfcAM7HLnhAfdcs4b+RNWQc9bCM4joHJBdJ6pEEKduNIi0shf2n/38bwmeCF/UV9yZ1BHYbtICPyIy3NHiQ414DNE8QxNwFEM4AfRwiBTSq0AOuCljNmEtn6SkFiepGtaSWs6UOWtw/H+EWYZM4TktrwAAAABJRU5ErkJggg==', 6 | } 7 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export enum ActionType { 2 | MouseDown = 0, 3 | MouseUp = 1, 4 | MouseIn = 2, 5 | MouseOut = 3, 6 | MouseMove = 4, 7 | Drag = 5, 8 | DragIn = 6, 9 | DragOut = 7, 10 | } 11 | 12 | export const NS_SVG = 'http://www.w3.org/2000/svg'; 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {default} from './Boardy'; 2 | -------------------------------------------------------------------------------- /src/modules/Painter/Painter.ts: -------------------------------------------------------------------------------- 1 | import Tool, {Drawing} from '@/modules/Tool'; 2 | import {Context, Action} from '@/@types/base'; 3 | import {IdGenerator} from '@/utils'; 4 | import * as tools from './tools'; 5 | 6 | export type Tools = { 7 | [toolId: number]: Tool, 8 | } 9 | 10 | export default class Painter { 11 | private tools: Tools; 12 | private context: Context; 13 | 14 | constructor(context: Context) { 15 | this.context = context; 16 | this.context.ctx.offscreen.globalCompositeOperation = 'source-over'; 17 | this.tools = { 18 | [IdGenerator.hashStringToNumber('blackline')]: tools.blackline, 19 | [IdGenerator.hashStringToNumber('blackcrayon')]: tools.blackcrayon, 20 | [IdGenerator.hashStringToNumber('eraser')]: tools.eraser, 21 | }; 22 | } 23 | 24 | public paint(action: Action) { 25 | const actionType: number = action[0]; 26 | const pointX: number = action[1]; 27 | const pointY: number = action[2]; 28 | const unit: number = action[3]; 29 | const toolId: number = action[4]; 30 | const drawing: Drawing = this.tools[toolId] && this.tools[toolId].get(actionType); 31 | 32 | if (drawing) { 33 | drawing( 34 | this.context.ctx.offscreen, 35 | { 36 | pointX, 37 | pointY, 38 | unit, 39 | }, 40 | ); 41 | } 42 | } 43 | 44 | public addTool(toolName: string, tool: Tool) { 45 | const toolId: number = IdGenerator.hashStringToNumber(toolName); 46 | 47 | this.tools[toolId] = tool; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/modules/Painter/index.ts: -------------------------------------------------------------------------------- 1 | export {default} from './Painter'; 2 | export * from './Painter'; 3 | -------------------------------------------------------------------------------- /src/modules/Painter/tools/blackcrayon.ts: -------------------------------------------------------------------------------- 1 | import Tool, {Drawing} from '@/modules/Tool'; 2 | import {ActionType} from '@/constants'; 3 | import data from '@/constants/crayonTexture'; 4 | import blackline from './blackline'; 5 | 6 | /** 7 | * Crayon Texture Image 8 | * TODO: async resource handling architecture 9 | */ 10 | const image = (function(): HTMLImageElement { 11 | const imageSource: HTMLImageElement = document.createElement('img') as HTMLImageElement; 12 | imageSource.width = data.width; 13 | imageSource.height = data.height; 14 | imageSource.src = data.src; 15 | return imageSource; 16 | })(); 17 | const cache: { [unit: number]: HTMLCanvasElement } = {}; 18 | const createCrayonTexture = (unit: number): HTMLCanvasElement => { 19 | if (cache[unit]) { 20 | return cache[unit]; 21 | } 22 | 23 | const canvas: HTMLCanvasElement = document.createElement('canvas'); 24 | const ctx: CanvasRenderingContext2D = canvas.getContext('2d'); 25 | const canvasWidth: number = data.width * unit; 26 | const canvasHeight: number = data.height * unit; 27 | 28 | canvas.width = canvasWidth; 29 | canvas.height = canvasHeight; 30 | ctx.drawImage( 31 | image, 32 | 0, 0, image.width, image.height, 33 | 0, 0, canvasWidth, canvasHeight, 34 | ); 35 | cache[unit] = canvas; 36 | 37 | return canvas; 38 | }; 39 | 40 | /** 41 | * DrawingFunctions 42 | */ 43 | const setCrayonLineStyle: Drawing = (ctx, {unit}) => { 44 | const texture: HTMLCanvasElement = createCrayonTexture(unit); 45 | 46 | ctx.strokeStyle = ctx.createPattern(texture, 'repeat'); 47 | ctx.lineCap = 'round'; 48 | ctx.lineWidth = unit * 10; 49 | }; 50 | 51 | export default Tool.extend(blackline, { 52 | [ActionType.MouseDown]: setCrayonLineStyle, 53 | [ActionType.DragIn]: setCrayonLineStyle, 54 | }); 55 | -------------------------------------------------------------------------------- /src/modules/Painter/tools/blackline.ts: -------------------------------------------------------------------------------- 1 | import Tool, {Drawing} from '@/modules/Tool'; 2 | import {ActionType} from '@/constants'; 3 | 4 | /** 5 | * Drawing Functions 6 | */ 7 | const startLine: Drawing = (ctx, {pointX, pointY, unit}) => { 8 | ctx.beginPath(); 9 | ctx.moveTo(pointX, pointY); 10 | ctx.lineJoin = 'round'; 11 | ctx.lineWidth = 1 * unit; 12 | ctx.strokeStyle = '#000'; 13 | }; 14 | const drawLine: Drawing = (ctx, {pointX, pointY}) => { 15 | ctx.lineTo(pointX, pointY); 16 | ctx.stroke(); 17 | }; 18 | const endLine: Drawing = (ctx, values) => { 19 | drawLine(ctx, values); 20 | ctx.closePath(); 21 | }; 22 | 23 | /** 24 | * Tool Object { [Event]: DrawingFunction } 25 | */ 26 | export default Tool.create({ 27 | [ActionType.MouseDown]: startLine, 28 | [ActionType.DragIn]: startLine, 29 | [ActionType.Drag]: drawLine, 30 | [ActionType.DragOut]: endLine, 31 | [ActionType.MouseUp]: endLine, 32 | }); 33 | -------------------------------------------------------------------------------- /src/modules/Painter/tools/eraser.ts: -------------------------------------------------------------------------------- 1 | import Tool from '@/modules/Tool'; 2 | import {ActionType} from '@/constants'; 3 | 4 | export default Tool.create({ 5 | [ActionType.MouseDown]: (ctx) => { 6 | ctx.globalCompositeOperation = 'destination-out'; 7 | }, 8 | [ActionType.Drag]: (ctx, {pointX, pointY, unit}) => { 9 | ctx.beginPath(); 10 | ctx.arc(pointX, pointY, unit * 20, 0, Math.PI*2, false); 11 | ctx.fill(); 12 | ctx.closePath(); 13 | }, 14 | [ActionType.MouseOut]: (ctx) => { 15 | ctx.globalCompositeOperation = 'source-over'; 16 | }, 17 | [ActionType.MouseUp]: (ctx) => { 18 | ctx.globalCompositeOperation = 'source-over'; 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /src/modules/Painter/tools/index.ts: -------------------------------------------------------------------------------- 1 | export {default as blackline} from './blackline'; 2 | export {default as blackcrayon} from './blackcrayon'; 3 | export {default as eraser} from './eraser'; 4 | -------------------------------------------------------------------------------- /src/modules/Rasterizer/Rasterizer.ts: -------------------------------------------------------------------------------- 1 | import {Context} from '@/@types/base'; 2 | 3 | // TODO: use Web Worker in browsers that support OffscreenCanvas 4 | export default class Rasterizer { 5 | private context: Context; 6 | 7 | constructor(context: Context) { 8 | this.context = context; 9 | } 10 | 11 | public rasterize() { 12 | this.clear(); 13 | this.context.ctx.screen.drawImage( 14 | this.context.canvas.offscreen, 15 | 0, 16 | 0, 17 | this.context.resolution.width, 18 | this.context.resolution.height, 19 | 0, 20 | 0, 21 | this.context.size.width, 22 | this.context.size.height, 23 | ); 24 | } 25 | 26 | public clear() { 27 | this.context.ctx.screen.clearRect( 28 | 0, 29 | 0, 30 | this.context.resolution.width, 31 | this.context.resolution.height, 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/Rasterizer/index.ts: -------------------------------------------------------------------------------- 1 | export {default} from './Rasterizer'; 2 | export * from './Rasterizer'; 3 | -------------------------------------------------------------------------------- /src/modules/Renderer/Renderer.ts: -------------------------------------------------------------------------------- 1 | import {Action} from '@/@types/base'; 2 | import Painter from '@/modules/Painter'; 3 | import Rasterizer from '@/modules/Rasterizer'; 4 | 5 | /** 6 | * Render Loop per vsync 7 | */ 8 | export default class Renderer { 9 | private painter: Painter; 10 | private rasterizer: Rasterizer; 11 | private actionQueue: Action[]; 12 | private isRunning: boolean; 13 | 14 | constructor( 15 | painter: Painter, 16 | rasterizer: Rasterizer, 17 | ) { 18 | this.painter = painter; 19 | this.rasterizer = rasterizer; 20 | this.actionQueue = []; 21 | this.isRunning = false; 22 | } 23 | 24 | public render(action: Action): void { 25 | this.actionQueue.unshift(action); 26 | if (!this.isRunning) { 27 | window.requestAnimationFrame(this.animate); 28 | } 29 | } 30 | 31 | private readonly animate = () => { 32 | // if actionQueue is empty, stop animation 33 | if (this.actionQueue.length === 0) { 34 | this.isRunning = false; 35 | return; 36 | } 37 | 38 | // do animation 39 | while (this.actionQueue.length > 0) { 40 | const action = this.actionQueue.pop(); 41 | 42 | if (action) { 43 | this.painter.paint(action); 44 | } 45 | } 46 | this.rasterizer.rasterize(); 47 | 48 | // next vsync tick 49 | window.requestAnimationFrame(this.animate); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/modules/Renderer/index.ts: -------------------------------------------------------------------------------- 1 | export {default} from './Renderer'; 2 | export * from './Renderer'; 3 | -------------------------------------------------------------------------------- /src/modules/Tool/Tool.ts: -------------------------------------------------------------------------------- 1 | import {ActionType} from '@/constants'; 2 | 3 | export type Drawing = ( 4 | ctx: CanvasRenderingContext2D, 5 | values: { 6 | pointX: number, 7 | pointY: number, 8 | unit: number, 9 | } 10 | ) => void; 11 | 12 | export type DrawingMap = { 13 | [actionType: number]: Drawing 14 | }; 15 | 16 | export default class Tool { 17 | private drawingMap: DrawingMap; 18 | 19 | constructor(drawingMap: DrawingMap) { 20 | this.drawingMap = drawingMap || {}; 21 | } 22 | 23 | public set(actionType: ActionType, drawing: Drawing): void { 24 | this.drawingMap[actionType] = drawing; 25 | } 26 | 27 | public get(actionType: ActionType): Drawing { 28 | return this.drawingMap[actionType]; 29 | } 30 | 31 | public getActionTypes(): number[] { 32 | const keys: string[] = Object.keys(this.drawingMap); 33 | const actionTypes: number[] = []; 34 | 35 | keys.forEach((key) => { 36 | const actionType: number = Number.parseInt(key, 10); 37 | 38 | if (!Number.isNaN(actionType)) { 39 | actionTypes.push(actionType); 40 | } 41 | }); 42 | 43 | return actionTypes; 44 | } 45 | 46 | static create(drawingMap: DrawingMap): Tool { 47 | return new Tool(drawingMap); 48 | } 49 | 50 | static extend(parentTools: Tool, childDrawingMap: DrawingMap): Tool { 51 | const curried: DrawingMap = {...childDrawingMap}; 52 | 53 | parentTools.getActionTypes().forEach((actionType: number) => { 54 | const drawingOfParent: Drawing = parentTools.get(actionType); 55 | const drawingOfChild: Drawing = curried[actionType]; 56 | const curriedDrawing: Drawing = !drawingOfChild ? 57 | drawingOfParent : 58 | (...args) => { 59 | drawingOfParent(...args); 60 | drawingOfChild(...args); 61 | }; 62 | 63 | curried[actionType] = curriedDrawing; 64 | }); 65 | 66 | return new Tool(curried); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/modules/Tool/index.ts: -------------------------------------------------------------------------------- 1 | export {default} from './Tool'; 2 | export * from './Tool'; 3 | -------------------------------------------------------------------------------- /src/modules/Trigger/Trigger.ts: -------------------------------------------------------------------------------- 1 | import {Context, Action} from '@/@types/base'; 2 | import {EventChannel, IdGenerator} from '@/utils'; 3 | import {ActionType} from '@/constants'; 4 | 5 | type EventHandler = (e: Event) => void; 6 | 7 | // [TODO] addWindowEventListeners(), addCanvasEventListener() 8 | // If multiple Boardy objects are created in one app, 9 | // optimization is needed to recycle one window event. 10 | // + singleton 11 | // [Question] Why did seperate the "window events" from the "canvas events"? 12 | // if canvas events is used only, we cannot offer UX connectivity - feeling. 13 | // if window events is used only, there are performance issue. 14 | // (browser-reflow, position calculation) 15 | export default class Trigger extends EventChannel { 16 | private context: Context; 17 | private currentToolId: number; 18 | private isMouseDown: boolean; 19 | private toolId: { [toolName: string]: number }; 20 | 21 | constructor(context: Context) { 22 | super(); 23 | 24 | this.toolId = { 25 | behavior: IdGenerator.hashStringToNumber('behavior'), 26 | }; 27 | this.context = context; 28 | this.currentToolId = this.toolId.behavior; 29 | this.addDocumentEventListeners(); 30 | this.isMouseDown = false; 31 | } 32 | 33 | public setTool(toolName: string) { 34 | if (!this.toolId[toolName]) { 35 | this.toolId[toolName] = IdGenerator.hashStringToNumber(toolName); 36 | } 37 | 38 | this.currentToolId = this.toolId[toolName]; 39 | } 40 | 41 | public destroy() { 42 | this.removeDocumentEventListeners(); 43 | } 44 | 45 | private addDocumentEventListeners() { 46 | const canvas: HTMLCanvasElement = this.context.canvas.screen; 47 | 48 | // canvas events: trigger with poisition. 49 | canvas.addEventListener('mouseover', this.handleMouseIn); 50 | canvas.addEventListener('mouseout', this.handleMouseOut); 51 | canvas.addEventListener('mousemove', this.handleMouseMove); 52 | // window events: check only mouse down/up. 53 | window.addEventListener('mousedown', this.handleMouseDownOnWindow); 54 | window.addEventListener('mouseup', this.handleMouseUpOnWindow); 55 | } 56 | 57 | private removeDocumentEventListeners() { 58 | const canvas: HTMLCanvasElement = this.context.canvas.screen; 59 | 60 | // canvas events 61 | canvas.removeEventListener('mouseover', this.handleMouseIn); 62 | canvas.removeEventListener('mouseout', this.handleMouseOut); 63 | canvas.removeEventListener('mousemove', this.handleMouseMove); 64 | // window events 65 | window.removeEventListener('mousedown', this.handleMouseDownOnWindow); 66 | window.removeEventListener('mouseup', this.handleMouseUpOnWindow); 67 | } 68 | 69 | private createAction( 70 | pointX: number, 71 | pointY: number, 72 | actionType: ActionType, 73 | ): Action { 74 | const action: Action = Uint32Array ? new Uint32Array(5) : ([] as any); 75 | 76 | action[0] = actionType; 77 | action[1] = this.checkPointXOverflow(pointX) * this.context.unit.width; 78 | action[2] = this.checkPointYOverflow(pointY) * this.context.unit.height; 79 | action[3] = this.context.unit.contents; 80 | action[4] = this.currentToolId; 81 | 82 | return action; 83 | } 84 | 85 | private checkPointXOverflow(offsetX: number): number { 86 | if (offsetX < 0) { 87 | offsetX = 0; 88 | } else if (offsetX > this.context.size.width) { 89 | offsetX = this.context.size.width; 90 | } 91 | 92 | return offsetX; 93 | } 94 | 95 | private checkPointYOverflow(offsetY: number): number { 96 | if (offsetY < 0) { 97 | offsetY = 0; 98 | } else if (offsetY > this.context.size.height) { 99 | offsetY = this.context.size.height; 100 | } 101 | 102 | return offsetY; 103 | } 104 | 105 | private readonly handleCommon = (handler: EventHandler): EventHandler => { 106 | return (e) => { 107 | if (this.currentToolId === this.toolId.behavior) { 108 | return; 109 | } 110 | handler(e); 111 | }; 112 | } 113 | 114 | private readonly handleMouseIn = this.handleCommon((e: MouseEvent) => { 115 | const actionType: ActionType = this.isMouseDown ? 116 | ActionType.DragIn : 117 | ActionType.MouseIn; 118 | const action: Action = this.createAction(e.offsetX, e.offsetY, actionType); 119 | 120 | this.emit(action); 121 | }); 122 | 123 | private readonly handleMouseOut = this.handleCommon((e: MouseEvent) => { 124 | const actionType: ActionType = this.isMouseDown ? 125 | ActionType.DragOut : 126 | ActionType.MouseOut; 127 | const action: Action = this.createAction(e.offsetX, e.offsetY, actionType); 128 | 129 | this.emit(action); 130 | }) 131 | 132 | private readonly handleMouseMove = this.handleCommon((e: MouseEvent) => { 133 | const actionType: ActionType = this.isMouseDown ? 134 | ActionType.Drag : 135 | ActionType.MouseMove; 136 | const action: Action = this.createAction(e.offsetX, e.offsetY, actionType); 137 | 138 | this.emit(action); 139 | }) 140 | 141 | private readonly handleMouseDownOnWindow = this.handleCommon((e: MouseEvent) => { 142 | this.isMouseDown = true; 143 | 144 | if (e.target !== this.context.canvas.screen) return; 145 | const actionType: ActionType = ActionType.MouseDown; 146 | const action: Action = this.createAction(e.offsetX, e.offsetY, actionType); 147 | 148 | this.emit(action); 149 | }) 150 | 151 | private readonly handleMouseUpOnWindow = this.handleCommon((e: MouseEvent) => { 152 | this.isMouseDown = false; 153 | 154 | if (e.target !== this.context.canvas.screen) return; 155 | const actionType: ActionType = ActionType.MouseUp; 156 | const action: Action = this.createAction(e.offsetX, e.offsetY, actionType); 157 | 158 | this.emit(action); 159 | }) 160 | } 161 | -------------------------------------------------------------------------------- /src/modules/Trigger/index.ts: -------------------------------------------------------------------------------- 1 | export {default} from './Trigger'; 2 | -------------------------------------------------------------------------------- /src/utils/EventChannel/EventChannel.ts: -------------------------------------------------------------------------------- 1 | export default class EventChannel { 2 | protected handlers: ((e: EventPayload) => void)[]; 3 | 4 | constructor() { 5 | this.handlers = []; 6 | } 7 | 8 | public on(handler: (e: EventPayload) => void) { 9 | this.handlers.push(handler); 10 | } 11 | 12 | public off(handler: (e: EventPayload) => void) { 13 | const handlerIndex = this.handlers.indexOf(handler); 14 | 15 | if (handlerIndex !== -1) { 16 | this.handlers.splice(handlerIndex, 1); 17 | } 18 | } 19 | 20 | public offAll() { 21 | this.handlers = []; 22 | } 23 | 24 | public emit(payload: EventPayload) { 25 | this.handlers.forEach((handler) => handler(payload)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/EventChannel/index.ts: -------------------------------------------------------------------------------- 1 | export {default} from './EventChannel'; 2 | -------------------------------------------------------------------------------- /src/utils/IdGenerator/IdGenerator.ts: -------------------------------------------------------------------------------- 1 | export default class IdGenerator { 2 | private id: number; 3 | 4 | constructor() { 5 | this.id = 0; 6 | } 7 | 8 | public get() { 9 | return ++this.id; 10 | } 11 | 12 | static hashStringToNumber(str: String): number { 13 | let h = 0; 14 | for (let i = 0; i < str.length; i++) { 15 | h = Math.imul(31, h) + str.charCodeAt(i) | 0; 16 | } 17 | return Math.abs(h); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/IdGenerator/index.ts: -------------------------------------------------------------------------------- 1 | export {default} from './IdGenerator'; 2 | -------------------------------------------------------------------------------- /src/utils/ResizeObserver/ResizeObserver.ts: -------------------------------------------------------------------------------- 1 | import {EventChannel, debounce} from '@/utils'; 2 | 3 | export type IntializeOption = { 4 | debounceTime: number // millisecond 5 | } 6 | 7 | export type ResizePayload = { 8 | width: number, 9 | height: number, 10 | } 11 | 12 | export default class ResizeObserver extends EventChannel { 13 | private element: HTMLElement; 14 | 15 | constructor(elementToBeObserved: HTMLElement, {debounceTime}: IntializeOption) { 16 | super(); 17 | 18 | this.handleResize = debounce(this.handleResize, debounceTime); 19 | this.element = elementToBeObserved; 20 | window.addEventListener('resize', this.handleResize); 21 | } 22 | 23 | public destroy() { 24 | window.removeEventListener('resize', this.handleResize); 25 | } 26 | 27 | private readonly handleResize = () => { 28 | const width: number = this.element.offsetWidth; 29 | const height: number = this.element.offsetHeight; 30 | 31 | this.emit({width, height}); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/ResizeObserver/index.ts: -------------------------------------------------------------------------------- 1 | export {default} from './ResizeObserver'; 2 | -------------------------------------------------------------------------------- /src/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | const debounce = >( 2 | job: (...argv: Argv) => void, 3 | debounceTime: number, 4 | ): (...argv: Argv) => void => { 5 | if (!debounceTime) { 6 | return job; 7 | } 8 | 9 | let lastArgv: Argv; 10 | let timerId: number = 0; 11 | 12 | const debouncedJob = () => { 13 | clearTimeout(timerId); 14 | timerId = 0; 15 | job(...lastArgv); 16 | }; 17 | 18 | return (...argv: Argv) => { 19 | lastArgv = argv; 20 | 21 | if (!timerId) { 22 | timerId = window.setTimeout(debouncedJob, debounceTime); 23 | job(...lastArgv); 24 | } 25 | }; 26 | }; 27 | 28 | export default debounce; 29 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export {default as EventChannel} from './EventChannel'; 2 | export {default as IdGenerator} from './IdGenerator'; 3 | export {default as debounce} from './debounce'; 4 | export {default as ResizeObserver} from './ResizeObserver'; 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "umd", 5 | "moduleResolution": "node", 6 | "baseUrl": ".", 7 | "outDir": "./dist", 8 | "noImplicitAny": false, 9 | "removeComments": true, 10 | "preserveConstEnums": true, 11 | "sourceMap": true, 12 | "allowJs": true, 13 | "paths": { 14 | "@/*": ["src/*"] 15 | }, 16 | "typeRoots": [ 17 | "./node_modules/@types", 18 | "./src/@types" 19 | ] 20 | }, 21 | "include": [ 22 | "src/**/*", "examples/**/*" 23 | ], 24 | "exclude": [ 25 | "node_modules", 26 | "**/*.spec.ts" 27 | ] 28 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const generateConfig = require('./config/webpack'); 2 | const exampleAbbreviation = { 3 | rtc: 'real-time-communication', 4 | } 5 | 6 | module.exports = () =>{ 7 | const exampleArg = process.env.example; 8 | const exampleStr = exampleArg || 'simple'; 9 | const example = exampleAbbreviation[exampleStr] || exampleStr; 10 | 11 | console.log(`[[ Example ]]\n - ${example}\n`); 12 | 13 | return generateConfig( 14 | process.env.NODE_ENV || 'production', 15 | example 16 | ) 17 | } ; 18 | --------------------------------------------------------------------------------