├── .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 |
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 ExampleMember Computers
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 |
60 |
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,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',
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 |
--------------------------------------------------------------------------------