├── .gitignore
├── assets
├── src
│ ├── width-1.afdesign
│ ├── width-2.afdesign
│ ├── width-3.afdesign
│ ├── width-4.afdesign
│ ├── icon-pen.afdesign
│ └── icon-eraser.afdesign
└── icons
│ ├── width-1.svg
│ ├── width-2.svg
│ ├── width-3.svg
│ ├── width-4.svg
│ ├── icon-eraser.svg
│ ├── icon-pen.svg
│ └── trashcan.svg
├── README.md
├── LICENSE
├── index.html
├── style.css
└── core.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /.vscode
2 |
--------------------------------------------------------------------------------
/assets/src/width-1.afdesign:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FastMirror-MC/douBoard/HEAD/assets/src/width-1.afdesign
--------------------------------------------------------------------------------
/assets/src/width-2.afdesign:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FastMirror-MC/douBoard/HEAD/assets/src/width-2.afdesign
--------------------------------------------------------------------------------
/assets/src/width-3.afdesign:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FastMirror-MC/douBoard/HEAD/assets/src/width-3.afdesign
--------------------------------------------------------------------------------
/assets/src/width-4.afdesign:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FastMirror-MC/douBoard/HEAD/assets/src/width-4.afdesign
--------------------------------------------------------------------------------
/assets/src/icon-pen.afdesign:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FastMirror-MC/douBoard/HEAD/assets/src/icon-pen.afdesign
--------------------------------------------------------------------------------
/assets/src/icon-eraser.afdesign:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FastMirror-MC/douBoard/HEAD/assets/src/icon-eraser.afdesign
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # douBoard
2 |
3 | > 一个支持压感的在线白板
4 |
5 | ## 快捷键
6 |
7 | - Ctrl+S 保存
8 | - Ctrl+Z 撤回
9 | - E 橡皮擦
10 | - B 画笔
11 |
12 | ## 在线demo
13 |
14 | [https://fastmirror-mc.github.io/douBoard/](https://fastmirror-mc.github.io/douBoard/ "demo")
15 |
--------------------------------------------------------------------------------
/assets/icons/width-1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/icons/width-2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/icons/width-3.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/icons/width-4.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 戴兜
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/assets/icons/icon-eraser.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/icons/icon-pen.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/assets/icons/trashcan.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | douBoard
9 |
10 |
11 |
12 |
13 |
14 |
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | width: 100%;
4 | height: 100%;
5 | padding: 0;
6 | margin: 0;
7 | }
8 |
9 | * {
10 | -webkit-user-select: none;
11 | user-select: none;
12 | }
13 |
14 | canvas {
15 | position: fixed;
16 | top: 0;
17 | left: 0;
18 | touch-action: none;
19 | }
20 |
21 | .eraser {
22 | border-radius: 100%;
23 | width: 60px;
24 | height: 60px;
25 | border: 2px #d3d3d3 solid;
26 | background: rgba(207, 207, 207, 0.2);
27 | opacity: 0.7;
28 | display: none;
29 | position: fixed;
30 | pointer-events: none;
31 | z-index: 10;
32 | box-sizing: border-box;
33 | }
34 |
35 | .toolbar {
36 | height: 50px;
37 | width: 100px;
38 | position: fixed;
39 | bottom: 0px;
40 | left: 50%;
41 | z-index: 12;
42 | transform: translateX(-50%);
43 | box-shadow: 0 0 10px 0px rgba(0, 0, 0, 0.2);
44 | user-select: none;
45 | background: #fff;
46 | }
47 |
48 | [class^="toolbar-"] {
49 | height: 50px;
50 | width: 50px;
51 | line-height: 50px;
52 | text-align: center;
53 | display: inline-block;
54 | float: left;
55 | cursor: pointer;
56 | transition: background-color 0.2s ease-out;
57 | }
58 |
59 | [class^="toolbar-"]:hover {
60 | background: #f1f1f2;
61 | }
62 |
63 | [class^="toolbar-"].active {
64 | background: #f1f1f2;
65 | }
66 |
67 | [class^="toolbar-"] svg {
68 | transform: translateY(12px);
69 | transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
70 | }
71 | [class^="toolbar-"].active svg {
72 | transform: translateY(3px);
73 | }
74 |
75 | .toolbar-pen {
76 | position: relative;
77 | }
78 |
79 | .toolbar-pen span {
80 | opacity: 0;
81 | transition: opacity 0.1s ease-out;
82 | }
83 |
84 | .toolbar-pen.active span {
85 | opacity: 1;
86 | position: absolute;
87 | background: #000;
88 | top: 3px;
89 | right: 3px;
90 | border-radius: 100%;
91 | }
92 |
93 | .toolbar-pen-viewer-1 {
94 | width: 3px;
95 | height: 3px;
96 | }
97 | .toolbar-pen-viewer-2 {
98 | width: 5px;
99 | height: 5px;
100 | }
101 | .toolbar-pen-viewer-3 {
102 | width: 7px;
103 | height: 7px;
104 | }
105 | .toolbar-pen-viewer-4 {
106 | width: 9px;
107 | height: 9px;
108 | }
109 |
110 | .toolbar-eraser {
111 | }
112 |
113 | .toolbarmenu {
114 | height: 50px;
115 | width: 100px;
116 | position: fixed;
117 | bottom: 50px;
118 | left: 50%;
119 | transform: translateX(-50%);
120 | user-select: none;
121 | z-index: 11;
122 | }
123 |
124 | [class^="toolbarmenu-"] {
125 | height: auto;
126 | width: 160px;
127 | background: #fff;
128 | box-shadow: 0 0 10px 0px rgba(0, 0, 0, 0.15);
129 | pointer-events: none;
130 | }
131 |
132 | [class^="toolbarmenu-"].active {
133 | pointer-events: initial;
134 | }
135 |
136 | .toolbarmenu-pen {
137 | opacity: 0;
138 | width: 260px;
139 | transition: opacity 0.2s linear;
140 | pointer-events: none;
141 | animation-fill-mode: forwards;
142 | animation: fadelogOut 0.4s;
143 | position: fixed;
144 | bottom: 5px;
145 | padding: 5px 5px 5px 0;
146 | }
147 |
148 | .width-viewer {
149 | height: 200px;
150 | width: 50px;
151 | float: left;
152 | }
153 |
154 | .switcher-container {
155 | height: 200px;
156 | float: left;
157 | display: flex;
158 | flex-direction: column;
159 | justify-content: space-around;
160 | }
161 |
162 | .switcher-container[type="width"] {
163 | width: 50px;
164 | }
165 |
166 | .switcher-container [class^="width-switcher-"] {
167 | height: 25px;
168 | width: 25px;
169 | border-radius: 100%;
170 | border: 2px solid rgba(179, 179, 179, 0.8);
171 | box-shadow: 0 0 0px 3px transparent;
172 | transition: box-shadow 0.1s ease-out;
173 | cursor: pointer;
174 | display: flex;
175 | justify-content: center;
176 | align-items: center;
177 | }
178 |
179 | .switcher-container [class^="width-switcher-"]:hover {
180 | box-shadow: 0 0 0px 3px rgba(221, 221, 221, 0.8);
181 | }
182 |
183 | .switcher-container [class^="width-switcher-"].active {
184 | box-shadow: 0 0 0px 3px rgba(221, 221, 221, 0.8);
185 | }
186 |
187 | .switcher-container [class^="width-switcher-"] span {
188 | background: #000;
189 | display: block;
190 | border-radius: 100%;
191 | }
192 |
193 | .switcher-container .width-switcher-4 span {
194 | height: 13px;
195 | width: 13px;
196 | }
197 |
198 | .switcher-container .width-switcher-3 span {
199 | height: 11px;
200 | width: 11px;
201 | }
202 |
203 | .switcher-container .width-switcher-2 span {
204 | height: 9px;
205 | width: 9px;
206 | }
207 |
208 | .switcher-container .width-switcher-1 span {
209 | height: 5px;
210 | width: 5px;
211 | }
212 |
213 | .switcher-container[type="color"] {
214 | width: 150px;
215 | flex-flow: column wrap;
216 | }
217 |
218 | .switcher-container .color-switcher {
219 | height: 25px;
220 | width: 25px;
221 | border-radius: 100%;
222 | border: 2px solid rgba(179, 179, 179, 0.8);
223 | box-shadow: 0 0 0px 3px transparent;
224 | transition: box-shadow 0.1s ease-out;
225 | cursor: pointer;
226 | flex-shrink: 0;
227 | background-color: white;
228 | margin: 10px 10px;
229 | }
230 |
231 | .switcher-container .color-switcher:hover {
232 | box-shadow: 0 0 0px 3px rgba(221, 221, 221, 0.8);
233 | }
234 |
235 | .switcher-container .color-switcher.active {
236 | box-shadow: 0 0 0px 3px rgba(221, 221, 221, 0.8);
237 | }
238 |
239 | .toolbarmenu-pen.active {
240 | pointer-events: all;
241 | opacity: 1;
242 | animation-fill-mode: forwards;
243 | animation: fadelogIn 0.3s;
244 | }
245 |
246 | .toolbarmenu-eraser {
247 | opacity: 0;
248 | transition: opacity 0.2s linear;
249 | pointer-events: none;
250 | animation-fill-mode: forwards;
251 | animation: fadelogOut 0.4s;
252 | margin-left: 50px;
253 | }
254 |
255 | .toolbarmenu-eraser.active {
256 | pointer-events: all;
257 | opacity: 1;
258 | animation-fill-mode: forwards;
259 | animation: fadelogIn 0.3s;
260 | }
261 |
262 | .toolbarmenu-eraser p {
263 | cursor: pointer;
264 | height: 45px;
265 | margin: 0;
266 | padding: 0;
267 | display: flex;
268 | align-items: center;
269 | background: #fff;
270 | transition: background-color 0.1s ease-out;
271 | }
272 |
273 | .toolbarmenu-eraser p:hover {
274 | background: #f1f1f2;
275 | }
276 |
277 | .toolbarmenu-eraser p img {
278 | float: left;
279 | margin-right: 7px;
280 | margin-left: 10px;
281 | transform: scale(0.8);
282 | }
283 |
284 | @keyframes fadelogIn {
285 | 0% {
286 | transform: translate3d(0, 80px, 0) scale(0.7);
287 | }
288 | 100% {
289 | transform: none;
290 | }
291 | }
292 |
293 | @keyframes fadelogOut {
294 | 0% {
295 | transform: none;
296 | }
297 | 100% {
298 | transform: translate3d(0, 80px, 0) scale(0.7);
299 | }
300 | }
301 |
302 | #toolbar-container {
303 | transition: opacity 0.2s ease-out;
304 | }
305 |
306 | .untouchable {
307 | pointer-events: none;
308 | opacity: 0.7;
309 | }
310 |
311 | .untouchable .active {
312 | pointer-events: none !important;
313 | }
314 |
--------------------------------------------------------------------------------
/core.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | const icons = {
3 | "pen": function (color) {
4 | return ` `;
5 | },
6 | "eraser": function () {
7 | return ` `;
8 | },
9 | "width-1": function (color) {
10 | return ` `;
11 | },
12 | "width-2": function (color) {
13 | return ` `;
14 | },
15 | "width-3": function (color) {
16 | return ` `;
17 | },
18 | "width-4": function (color) {
19 | return ` `;
20 | },
21 | }
22 | let canvas = document.querySelector("#mainCanvas");
23 | let ctx = canvas.getContext("2d");
24 |
25 | let offCanvas = document.createElement("canvas");
26 | let offCtx = offCanvas.getContext("2d");
27 |
28 | let toolbarPen = document.querySelector(".toolbar-pen");
29 | let toolbarEraser = document.querySelector(".toolbar-eraser");
30 | let toolbarPenMenu = document.querySelector(".toolbarmenu-pen");
31 | let toolbarEraserMenu = document.querySelector(".toolbarmenu-eraser");
32 |
33 | let eraser = document.querySelector(".eraser");
34 |
35 | for (i in document.images) document.images[i].ondragstart = function () { return false; };
36 |
37 | window.onresize = function () {
38 | offCanvas.width = canvas.width;
39 | offCanvas.height = canvas.height;
40 | offCtx.drawImage(canvas, 0, 0);
41 |
42 | canvas.width = document.documentElement.clientWidth;
43 | canvas.height = document.documentElement.clientHeight;
44 |
45 | let width = canvas.width, height = canvas.height;
46 | if (window.devicePixelRatio) {
47 | canvas.style.width = width + "px";
48 | canvas.style.height = height + "px";
49 | canvas.height = height * window.devicePixelRatio;
50 | canvas.width = width * window.devicePixelRatio;
51 | ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
52 | }
53 | ctx.drawImage(offCanvas, 0, 0);
54 | ctx.lineJoin = "round";
55 | ctx.lineCap = "round";
56 | }
57 |
58 | window.onresize();
59 |
60 | canDraw = false;
61 | let baseLineList = [6, 10, 15, 25];
62 | let baseLineMode = 0;
63 | let lineColorList = ["#000", "#5B2D90", "#0069BF", "#F6630C", "#AB228B", "#B7B7B7", "#E3E3E3", "#E71224", "#D20078", "#02A556", "#C09E66", "#FFC114"]; //线条颜色列表
64 | let lineColorMode = 0;
65 | let history = [];
66 | let priviousDraw = 0;
67 | let priviousPressure = 0;
68 |
69 | for (let i = 0; i < 4; i++) {
70 | document.querySelector(`.width-switcher-${i + 1}`).onpointerup = function () { setPenWidth(i) }
71 | }
72 |
73 | for (let i = 0; i < 12; i++) {
74 | let child = document.createElement("div");
75 | child.classList.add("color-switcher");
76 | child.style.backgroundColor = lineColorList[i];
77 | child.onpointerup = function () {
78 | setPenColor(i)
79 | }
80 | if (i == 0) {
81 | child.classList.add("active");
82 | }
83 | document.querySelector(`.switcher-container[type="color"]`).appendChild(child)
84 | }
85 |
86 | function setPenWidth(mode) {
87 | baseLineMode = mode;
88 | let viewerContainer = document.querySelector(".width-viewer");
89 | viewerContainer.innerHTML = icons[`width-${mode + 1}`](lineColorList[lineColorMode]);
90 | toolbarPen.innerHTML = icons.pen(lineColorList[lineColorMode]) + ` `;
91 | document.querySelector(`.switcher-container[type="width"] .active`).classList.remove("active");
92 | document.querySelector(`.width-switcher-${mode + 1}`).classList.add("active");
93 | }
94 |
95 | setPenWidth(0);
96 |
97 | function setPenColor(mode) {
98 | lineColorMode = mode;
99 | toolbarPen.innerHTML = icons.pen(lineColorList[mode]) + ` `;
100 | setPenWidth(baseLineMode);
101 | document.querySelector(`.color-switcher.active`).classList.remove("active");
102 | document.querySelectorAll(`.color-switcher`)[mode].classList.add("active");
103 | }
104 |
105 | setPenColor(0);
106 |
107 |
108 | let points = [];
109 | let beginPoint = null;
110 |
111 | const drawMode = {
112 | "down": function (e) {
113 | setToolbarStatus(false);
114 | writeHistory();
115 | canDraw = true;
116 | ctx.globalCompositeOperation = "source-over";
117 | ctx.strokeStyle = lineColorList[lineColorMode];
118 | const { x, y, pressure } = getPos(e);
119 | priviousPressure = pressure;
120 | points.push({ x, y });
121 | beginPoint = { x, y };
122 | },
123 | "up": function (e) {
124 | if (!canDraw) return;
125 | setToolbarStatus(true);
126 | const { x, y, pressure } = getPos(e);
127 |
128 | points.push({ x, y });
129 |
130 | if (points.length > 3) {
131 | const lastTwoPoints = points.slice(-2);
132 | const controlPoint = lastTwoPoints[0];
133 | const endPoint = lastTwoPoints[1];
134 | usePen(beginPoint, controlPoint, endPoint, (priviousPressure + pressure) / 2 * baseLineList[baseLineMode]);
135 | } else {
136 | priviousPressure = pressure;
137 | }
138 | beginPoint = null;
139 | canDraw = false;
140 | points = [];
141 | },
142 | "move": function (e) {
143 | if (!canDraw) return;
144 | const { x, y, pressure } = getPos(e);
145 | points.push({ x, y });
146 |
147 | if (points.length > 3) {
148 | const lastTwoPoints = points.slice(-2);
149 | const controlPoint = lastTwoPoints[0];
150 | const endPoint = {
151 | x: (lastTwoPoints[0].x + lastTwoPoints[1].x) / 2,
152 | y: (lastTwoPoints[0].y + lastTwoPoints[1].y) / 2,
153 | }
154 | usePen(beginPoint, controlPoint, endPoint, pressure * baseLineList[baseLineMode]);
155 | beginPoint = endPoint;
156 | }
157 | }
158 | }
159 |
160 | const eraserMode = {
161 | "down": function (e) {
162 | setToolbarStatus(false);
163 | writeHistory();
164 | canDraw = true;
165 | ctx.strokeStyle = "rgba(0,0,0,1)";
166 | ctx.globalCompositeOperation = "destination-out";
167 | const { x, y } = getPos(e);
168 | eraser.style.top = `${y - 30}px`;
169 | eraser.style.left = `${x - 30}px`;
170 | eraser.style.display = "block";
171 | points.push({ x, y });
172 | beginPoint = { x, y };
173 | },
174 | "up": function (e) {
175 | if (!canDraw) return;
176 | setToolbarStatus(true);
177 | const { x, y } = getPos(e);
178 |
179 | points.push({ x, y });
180 |
181 | if (points.length > 3) {
182 | const lastTwoPoints = points.slice(-2);
183 | const controlPoint = lastTwoPoints[0];
184 | const endPoint = lastTwoPoints[1];
185 | useEraser(beginPoint, controlPoint, endPoint, 60);
186 | }
187 | beginPoint = null;
188 | canDraw = false;
189 | eraser.style.display = "none";
190 | points = [];
191 | },
192 | "move": function (e) {
193 | if (!canDraw) return;
194 | const { x, y } = getPos(e);
195 | points.push({ x, y });
196 |
197 | if (points.length > 3) {
198 | const lastTwoPoints = points.slice(-2);
199 | const controlPoint = lastTwoPoints[0];
200 | const endPoint = {
201 | x: (lastTwoPoints[0].x + lastTwoPoints[1].x) / 2,
202 | y: (lastTwoPoints[0].y + lastTwoPoints[1].y) / 2,
203 | }
204 | eraser.style.top = `${y - 30}px`;
205 | eraser.style.left = `${x - 30}px`;
206 | useEraser(beginPoint, controlPoint, endPoint, 60);
207 | beginPoint = endPoint;
208 | }
209 | }
210 | }
211 | canvas.onpointerdown = drawMode["down"]
212 | canvas.onpointerup = drawMode["up"]
213 | canvas.onpointermove = drawMode["move"]
214 |
215 | function getPos(evt) {
216 | return {
217 | x: evt.clientX,
218 | y: evt.clientY,
219 | pressure: evt.pressure
220 | }
221 | }
222 | function usePen(beginPoint, controlPoint, endPoint, width) {
223 | ctx.beginPath();
224 | ctx.moveTo(beginPoint.x, beginPoint.y);
225 | ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, endPoint.x, endPoint.y);
226 | ctx.lineWidth = width;
227 | ctx.stroke();
228 | ctx.closePath();
229 | }
230 |
231 | function useEraser(beginPoint, controlPoint, endPoint, width) {
232 | ctx.beginPath();
233 | ctx.moveTo(beginPoint.x, beginPoint.y);
234 | ctx.quadraticCurveTo(controlPoint.x, controlPoint.y, endPoint.x, endPoint.y);
235 | ctx.lineWidth = width;
236 | ctx.stroke();
237 | }
238 |
239 | toolbarEraser.innerHTML = icons.eraser();
240 |
241 | document.querySelector(".clearAll").onpointerup = function () {
242 | ctx.clearRect(0, 0, canvas.width, canvas.height);
243 | toolbarEraserMenu.classList.remove("active");
244 | }
245 | toolbarPen.onpointerup = function () {
246 | toolbarEraserMenu.classList.remove("active");
247 | if (toolbarPen.classList.contains("active")) {
248 | toolbarPenMenu.classList.toggle("active");
249 | }
250 | toolbarPen.classList.add("active");
251 | toolbarEraser.classList.remove("active");
252 | canvas.onpointerdown = drawMode["down"];
253 | canvas.onpointerup = drawMode["up"];
254 | canvas.onpointermove = drawMode["move"];
255 | }
256 | toolbarEraser.onpointerup = function () {
257 | toolbarPenMenu.classList.remove("active");
258 | if (toolbarEraser.classList.contains("active")) {
259 | toolbarEraserMenu.classList.toggle("active");
260 | }
261 | toolbarEraser.classList.add("active");
262 | toolbarPen.classList.remove("active");
263 | canvas.onpointerdown = eraserMode["down"];
264 | canvas.onpointerup = eraserMode["up"];
265 | canvas.onpointermove = eraserMode["move"];
266 | }
267 |
268 |
269 | window.onkeyup = function (e) {
270 | if (e.ctrlKey == true && e.keyCode == 83) { //Ctrl+S 保存
271 | e.preventDefault();
272 | e.returnvalue = false;
273 | saveCanvas();
274 | }
275 | if (e.keyCode == 69) { //E 橡皮擦
276 | e.returnvalue = false;
277 | toolbarEraser.onpointerup();
278 | }
279 | if (e.keyCode == 66) { //B 笔
280 | e.returnvalue = false;
281 | toolbarPen.onpointerup();
282 | }
283 | if (e.ctrlKey == true && e.keyCode == 90) { //Ctrl+Z 撤销
284 | e.returnvalue = false;
285 | e.preventDefault();
286 | let content = popHistory();
287 | if (content) {
288 | ctx.putImageData(content, 0, 0);
289 | }
290 | }
291 | }
292 |
293 | function writeHistory() {
294 | if (history.length > 15) {
295 | history.shift()
296 | }
297 | if (priviousDraw == 0) {
298 | priviousDraw = new Date().getTime();
299 | history.push(ctx.getImageData(0, 0, canvas.width, canvas.height));
300 | } else {
301 | if (new Date().getTime() - priviousDraw > 1000) {
302 | history.push(ctx.getImageData(0, 0, canvas.width, canvas.height));
303 | }
304 | }
305 | }
306 |
307 | function popHistory() {
308 | if (history.length == 0) {
309 | return false;
310 | } else {
311 | return history.pop(history);
312 | }
313 | }
314 |
315 | function saveCanvas() {
316 | var link = document.createElement("a");
317 | var imgData = canvas.toDataURL();
318 | var blob = dataURLtoBlob(imgData);
319 | var objURL = URL.createObjectURL(blob);
320 | link.download = `DouBoard(${new Date().toLocaleString().replace(/\//g, "-")}).png`;
321 | link.href = objURL;
322 | link.click();
323 | link.remove();
324 |
325 | setTimeout(function () { URL.revokeObjectURL(objURL); }, 5000);
326 |
327 | function dataURLtoBlob(dataurl) {
328 | var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
329 | bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
330 | while (n--) {
331 | u8arr[n] = bstr.charCodeAt(n);
332 | }
333 | return new Blob([u8arr], { type: mime });
334 | }
335 | }
336 |
337 | function setToolbarStatus(status) {
338 | let toolbarContainer = document.querySelector("#toolbar-container");
339 | if (!status) {
340 | toolbarContainer.classList.add("untouchable");
341 | } else {
342 | toolbarContainer.classList.remove("untouchable");
343 | }
344 | }
345 | })()
--------------------------------------------------------------------------------