Draw.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. <template>
  2. <div class="paint">
  3. <canvas ref="canvas" v-show="show"></canvas>
  4. <!-- <div class="toolbar" v-show="showPaint">
  5. <ul>
  6. <li @click="onDraw('drawStart')" v-show="show == false">
  7. <i class="iconfont icontagging"></i>
  8. <div>画笔</div>
  9. </li>
  10. <li @click="onDraw('drawUndo')" v-if="show" :class="{ disable: !canUndo }">
  11. <i class="iconfont icon_cancel"></i>
  12. <div>撤回</div>
  13. </li>
  14. <li @click="onDraw('drawStop')" v-if="show">
  15. <i class="iconfont iconclose"></i>
  16. <div>关闭</div>
  17. </li>
  18. </ul>
  19. </div> -->
  20. </div>
  21. </template>
  22. <script lang="ts">
  23. import { getRole, sendToH5 } from "../../../utils/rtc_socket";
  24. import { objects } from "/@/core/base";
  25. import math from "/@/core/util/math";
  26. import convertTool from "/@/core/util/convertTool";
  27. export default {
  28. props: {
  29. showPaint: Boolean,
  30. },
  31. data() {
  32. return {
  33. role: getRole(),
  34. show: false,
  35. canUndo: false,
  36. colorA: "#02c8ae",
  37. colorB: "#2e98fe",
  38. };
  39. },
  40. watch: {
  41. showPaint() {
  42. if (this.showPaint) {
  43. this.role = getRole();
  44. }
  45. },
  46. show() {
  47. this.$bus.emit("shop/header/disable", this.show);
  48. },
  49. },
  50. created() {
  51. this.$bus.on("shop/sync/action", (data) => {
  52. if (data.type == "drawStart") {
  53. this.show = true;
  54. this.draw = [];
  55. this.drawHistory = [];
  56. this.$nextTick(() => {
  57. this.onDrawStart();
  58. });
  59. } else if (data.type == "drawStop") {
  60. this.show = false;
  61. this.draw = null;
  62. this.drawHistory = null;
  63. } else if (data.type == "drawing") {
  64. const draw = this.transformTo2d(data.data.drawing);
  65. if (data.data.role != this.role) {
  66. this.drawHistory.push(JSON.parse(JSON.stringify(draw)));
  67. this.drawing(draw);
  68. }
  69. } else if (data.type == "drawUndo") {
  70. this.drawUndo(data.data.role);
  71. }
  72. });
  73. },
  74. mounted() {
  75. this.canvas = this.$refs.canvas;
  76. this.context = this.canvas.getContext("2d");
  77. this.canvas.onmousedown = (e) => {
  78. if (!this.show || this.role != "leader") {
  79. return;
  80. }
  81. e.preventDefault();
  82. this.beginStroke({
  83. x: e.clientX,
  84. y: e.clientY,
  85. });
  86. };
  87. this.canvas.onmouseup = (e) => {
  88. if (!this.show || this.role != "leader") {
  89. return;
  90. }
  91. e.preventDefault();
  92. this.endStroke();
  93. };
  94. this.canvas.onmouseout = (e) => {
  95. if (!this.show || this.role != "leader") {
  96. return;
  97. }
  98. e.preventDefault();
  99. this.endStroke();
  100. };
  101. this.canvas.onmousemove = (e) => {
  102. if (!this.show || this.role != "leader") {
  103. return;
  104. }
  105. e.preventDefault();
  106. if (this._mouseDown) {
  107. this.moveStroke({
  108. x: e.clientX,
  109. y: e.clientY,
  110. });
  111. }
  112. };
  113. let touch;
  114. // 移动端触控
  115. this.canvas.addEventListener("touchstart", (e) => {
  116. if (!this.show || this.role != "leader") {
  117. return;
  118. }
  119. e.preventDefault();
  120. touch = e.touches[0];
  121. this.beginStroke({
  122. x: touch.pageX,
  123. y: touch.pageY,
  124. });
  125. });
  126. this.canvas.addEventListener("touchmove", (e) => {
  127. if (!this.show || this.role != "leader") {
  128. return;
  129. }
  130. e.preventDefault();
  131. if (this._mouseDown) {
  132. touch = e.touches[0];
  133. this.moveStroke({
  134. x: touch.pageX,
  135. y: touch.pageY,
  136. });
  137. }
  138. });
  139. this.canvas.addEventListener("touchend", (e) => {
  140. if (!this.show || this.role != "leader") {
  141. return;
  142. }
  143. e.preventDefault();
  144. this.endStroke();
  145. });
  146. this.mouse = new THREE.Vector2();
  147. },
  148. methods: {
  149. /**
  150. * 2d数据转3d数据
  151. */
  152. transformTo3d(draw) {
  153. const data = [];
  154. if (draw.length == 0) {
  155. return [];
  156. }
  157. draw.forEach((item, index) => {
  158. math.convertScreenPositionToNDC(item.x, item.y, this.mouse);
  159. var intersect = .getMouseIntersect(
  160. objects.player.camera,
  161. [this.intersectPlane],
  162. this.mouse
  163. );
  164. if (!intersect) {
  165. console.error("no intersect ??");
  166. } else {
  167. item.pos3d = intersect.point;
  168. data.push(item);
  169. }
  170. });
  171. return data;
  172. },
  173. /**
  174. * 3d数据转2d数据
  175. */
  176. transformTo2d(draw) {
  177. const data = [];
  178. draw.forEach((item) => {
  179. var pos3d = new THREE.Vector3(item.pos3d.x, item.pos3d.y, item.pos3d.z);
  180. var pos2d = convertTool.getPos2d(pos3d, objects.player.camera);
  181. //delete item.pos3d;
  182. item.x = pos2d.pos.x;
  183. item.y = pos2d.pos.y;
  184. data.push(item);
  185. });
  186. return data;
  187. },
  188. onDraw(type) {
  189. if (type == "drawStart") {
  190. this.show = true;
  191. this.draw = [];
  192. this.drawHistory = [];
  193. this.$nextTick(() => {
  194. this.onDrawStart();
  195. });
  196. } else if (type == "drawStop") {
  197. this.show = false;
  198. this.draw = null;
  199. this.drawHistory = null;
  200. } else if (type == "drawing") {
  201. const draw = this.transformTo2d(data.content.drawing);
  202. if (data.role != role) {
  203. this.drawHistory.push(JSON.parse(JSON.stringify(draw)));
  204. //this.drawing(draw);
  205. }
  206. } else if (type == "drawUndo") {
  207. this.drawUndo(this.role);
  208. }
  209. sendToH5({
  210. type,
  211. data: {
  212. role: this.role,
  213. },
  214. });
  215. },
  216. onPainting() {
  217. const draw = this.transformTo3d(this.draw);
  218. this.drawHistory.push(JSON.parse(JSON.stringify(draw)));
  219. sendToH5({
  220. type: "drawing",
  221. data: {
  222. drawing: draw,
  223. },
  224. });
  225. this.draw = [];
  226. this.canUndo = true;
  227. this._endTime = 0;
  228. this._mouseDown = false;
  229. this._lastTimestamp = 0;
  230. this.$emit("sendCanUndo", this.canUndo);
  231. },
  232. beginStroke(point) {
  233. this._mouseDown = true;
  234. this._lastTimestamp = Date.now();
  235. this._lastPosition = this.windowToCanvas(point.x, point.y);
  236. this.draw.push({
  237. role: this.role,
  238. width: 0,
  239. x: this._lastPosition.x,
  240. y: this._lastPosition.y,
  241. t: 5, //this._lastTimestamp - this._endTime
  242. });
  243. },
  244. onDrawStart() {
  245. let dpr = window.devicePixelRatio || 1;
  246. let rect = this.canvas.getBoundingClientRect();
  247. this.ratio = 1; // window.innerWidth / 375;
  248. this.canvas.width = rect.width * dpr;
  249. this.canvas.height = rect.height * dpr;
  250. this.context.scale(dpr, dpr);
  251. this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
  252. this._endTime = 0;
  253. this._mouseDown = false;
  254. this._lastTimestamp = 0;
  255. this._lastLineWidth = -1;
  256. this._lastPosition = {
  257. x: 0,
  258. y: 0,
  259. };
  260. math.convertScreenPositionToNDC(0, 0, this.mouse); //取屏幕中点
  261. var intersect = convertTool.getMouseIntersect(
  262. objects.player.camera,
  263. [objects.model.skybox, objects.sceneRenderer.scene.skyboxBG],
  264. this.mouse
  265. );
  266. this.placeIntersectPlane(intersect && intersect.point);
  267. },
  268. moveStroke(point) {
  269. let timestamp = Date.now();
  270. let position = this.windowToCanvas(point.x, point.y);
  271. let s = this.calcDistance(position, this._lastPosition);
  272. let t = timestamp - this._lastTimestamp;
  273. let lineWidth = this.calcLineWidth(t, s);
  274. //draw
  275. this.context.beginPath();
  276. this.context.moveTo(this._lastPosition.x, this._lastPosition.y);
  277. this.context.lineTo(position.x, position.y);
  278. this.draw.push({
  279. role: this.role,
  280. width: lineWidth,
  281. x: position.x,
  282. y: position.y,
  283. t: 5, //t
  284. });
  285. this.context.strokeStyle = this.colorA;
  286. this.context.lineWidth = lineWidth;
  287. this.context.lineCap = "round";
  288. this.context.linJoin = "round";
  289. this.context.stroke();
  290. //每次过程结束时,将结束值赋给初始值,一直延续
  291. this._lastPosition = position;
  292. this._lastTimestamp = timestamp;
  293. this._lastLineWidth = lineWidth;
  294. },
  295. endStroke() {
  296. this.draw.push({
  297. role: this.role,
  298. width: 0,
  299. x: this._lastPosition.x,
  300. y: this._lastPosition.y,
  301. t: 0,
  302. });
  303. this.onPainting();
  304. this._mouseDown = false;
  305. this._endTime = Date.now();
  306. },
  307. calcLineWidth(t, s) {
  308. let v = s / t;
  309. let resultLineWidth;
  310. if (v <= 0.1) {
  311. resultLineWidth = 6;
  312. } else if (v >= 3) {
  313. resultLineWidth = 2;
  314. } else {
  315. resultLineWidth = 6 - ((v - 0.1) / (3 - 0.1)) * (6 - 4);
  316. }
  317. if (this._lastLineWidth == -1) {
  318. return resultLineWidth;
  319. }
  320. return (this._lastLineWidth * 2) / 3 + (resultLineWidth * 1) / 3;
  321. },
  322. calcDistance(pos1, pos2) {
  323. return Math.sqrt(
  324. (pos1.x - pos2.x) * (pos1.x - pos2.x) +
  325. (pos1.y - pos2.y) * (pos1.y - pos2.y)
  326. ); //通过起始结束坐标x,y值计算路程长度
  327. },
  328. windowToCanvas(x, y) {
  329. var bbox = this.canvas.getBoundingClientRect(); //获取canvas的位置信息
  330. return {
  331. x: Math.round(x - bbox.left),
  332. y: Math.round(y - bbox.top),
  333. }; //返回当前鼠标相对于canvas的位置
  334. },
  335. drawing(draw) {
  336. for (let i = 0; i < draw.length - 1; i++) {
  337. draw[i].t &&
  338. setTimeout(() => {
  339. this.context.beginPath();
  340. this.context.strokeStyle =
  341. draw[i].role == this.role ? this.colorA : this.colorB;
  342. this.context.moveTo(draw[i].x * this.ratio, draw[i].y * this.ratio);
  343. this.context.lineTo(
  344. draw[i + 1].x * this.ratio,
  345. draw[i + 1].y * this.ratio
  346. );
  347. this.context.lineWidth = draw[i].width * this.ratio;
  348. this.context.lineCap = "round";
  349. this.context.linJoin = "round";
  350. this.context.stroke();
  351. }, 5);
  352. }
  353. },
  354. drawUndo(sender) {
  355. this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
  356. for (let i = this.drawHistory.length - 1; i >= 0; i--) {
  357. if (this.drawHistory[i][0].role == sender) {
  358. this.drawHistory.splice(i, 1);
  359. break;
  360. }
  361. }
  362. this.drawHistory.forEach((draw) => {
  363. for (let i = 0; i < draw.length - 1; i++) {
  364. if (draw[i].t) {
  365. this.context.beginPath();
  366. this.context.strokeStyle =
  367. draw[i].role == this.role ? this.colorA : this.colorB;
  368. this.context.moveTo(draw[i].x * this.ratio, draw[i].y * this.ratio);
  369. this.context.lineTo(
  370. draw[i + 1].x * this.ratio,
  371. draw[i + 1].y * this.ratio
  372. );
  373. this.context.lineWidth = draw[i].width * this.ratio;
  374. this.context.lineCap = "round";
  375. this.context.linJoin = "round";
  376. this.context.stroke();
  377. }
  378. }
  379. });
  380. this.canUndo = this.drawHistory.some((item) => item[0].role == this.role);
  381. },
  382. placeIntersectPlane(pos) {
  383. //用于判断mesh拖拽移动距离的平面 需要和视线垂直,以保证遮住视野范围
  384. if (!this.intersectPlane) {
  385. var geo = new THREE.PlaneGeometry(8000, 80000, 1, 1);
  386. //var geo = new THREE.PlaneGeometry(3,3,1,1);
  387. this.intersectPlane = new THREE.Mesh(
  388. geo,
  389. new THREE.MeshBasicMaterial({
  390. transparent: true,
  391. wireframe: false,
  392. opacity: 0,
  393. side: THREE.DoubleSide,
  394. depthTest: false,
  395. })
  396. );
  397. this.intersectPlane.lookAt(new THREE.Vector3(0, 1, 0));
  398. this.intersectPlane.name = "intersectPlane";
  399. objects.model.add(this.intersectPlane);
  400. }
  401. if (pos) {
  402. this.intersectPlane.position.copy(pos);
  403. var cameraDir = objects.player.getDirection(
  404. null,
  405. objects.player.camera
  406. ); //向里
  407. this.intersectPlane.lookAt(pos.clone().add(cameraDir)); //看向相机
  408. }
  409. },
  410. },
  411. };
  412. </script>
  413. <style lang="scss" scoped>
  414. .paint {
  415. position: absolute;
  416. left: 0;
  417. top: 0;
  418. width: 100%;
  419. height: 100%;
  420. z-index: 2000;
  421. pointer-events: none !important;
  422. canvas {
  423. width: 100vw;
  424. height: 100vh;
  425. image-rendering: pixelated;
  426. image-rendering: crisp-edges;
  427. pointer-events: auto;
  428. position: fixed;
  429. top: 0;
  430. left: 0;
  431. }
  432. .toolbar {
  433. pointer-events: auto;
  434. position: absolute;
  435. right: 0.35rem;
  436. bottom: 4.5rem;
  437. padding: 0.4rem 0.2rem;
  438. border-radius: 30px;
  439. z-index: 100;
  440. background-color: rgba(0, 0, 0, 0.3);
  441. ul,
  442. li {
  443. margin: 0;
  444. padding: 0;
  445. list-style: none;
  446. }
  447. li {
  448. padding: 0.3px;
  449. text-align: center;
  450. font-size: 14px;
  451. margin-bottom: 0.5rem;
  452. &:last-child {
  453. margin-bottom: 0;
  454. }
  455. i {
  456. font-size: 20px;
  457. &.icon_cancel {
  458. font-size: 22px;
  459. }
  460. &.iconclose {
  461. font-size: 14px;
  462. }
  463. }
  464. }
  465. }
  466. }
  467. </style>