component.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. import { defineComponent, onMounted , ref, effect} from "vue";
  2. import { string } from "vue-types";
  3. import { useCompData } from ".";
  4. import { useEditor } from "../../../..";
  5. import { View } from "../View";
  6. import { CompUI } from "../..";
  7. import { values } from "lodash";
  8. import { Angle, VectorLenth } from "@/modules/editor/controllers/SelectCtrl/objects/mathUtils";
  9. import { CompObject } from "@/modules/editor/controllers/SelectCtrl/compObj";
  10. import { ObjsContainer } from "@/modules/editor/controllers/SelectCtrl/ObjsContainer";
  11. function findNearestPoint(points: number[][], w:number, h:number, sx:number, sy:number) {
  12. const n = points.length;
  13. let minv = 100000;
  14. let minIndex = -1;
  15. const positions = []
  16. for(let i=0; i<n; i++) {
  17. const p = points[i];
  18. const x = w *p[0];
  19. const y = h *p[1];
  20. positions.push([x, y]);
  21. const ln = (sx-x)*(sx-x) + (sy-y)*(sy-y);
  22. if ( ln < minv ) {
  23. minIndex = i;
  24. minv = ln;
  25. if(!(sx < (x -10) || sx > (x + 10) || sy < (y -10) || sy > (y+10) ) ) {
  26. return {clicked: true, index: i};
  27. }
  28. }
  29. }
  30. let preIndex = minIndex -1;
  31. if (preIndex < 0 ) preIndex = n-1;
  32. let afterIndex = minIndex + 1;
  33. if ( afterIndex >= n) afterIndex = 0;
  34. const currV = {x:sx-positions[minIndex][0], y:sy-positions[minIndex][1]};
  35. const preV = {x:positions[preIndex][0]-positions[minIndex][0], y:positions[preIndex][1]-positions[minIndex][1]}
  36. const afterV = {x:positions[afterIndex][0]-positions[minIndex][0], y:positions[afterIndex][1]-positions[minIndex][1]}
  37. const a1 = Angle(currV, preV)
  38. const a2 = Angle(currV, afterV);
  39. if (a1 < a2) {
  40. return {index: preIndex};
  41. }
  42. return {index: minIndex};
  43. }
  44. function normalize(v1:{x:number,y:number}) {
  45. let x = v1.x;
  46. let y = v1.y;
  47. const s = Math.sqrt(x*x + y*y);
  48. v1.x = x / s;
  49. v1.y = y / s;
  50. return v1;
  51. }
  52. type PointItem = {
  53. x: number;
  54. y: number;
  55. cx: number;
  56. cy: number;
  57. preLineX: number; //界面显示控制点
  58. preLineY: number; //界面显示控制点
  59. aftLineX: number; //界面显示控制点
  60. aftLineY: number; //界面显示控制点
  61. preCirleX: number; //界面显示控制点
  62. preCirleY: number; //界面显示控制点
  63. aftCirleX: number; //界面显示控制点
  64. aftCirleY: number; //界面显示控制点
  65. isLine: boolean;
  66. }
  67. function parsePoints( points:number[][], width:number, height:number) {
  68. let out :PointItem[] = [];
  69. let n = points.length;
  70. for(let i=0; i<n; i++) {
  71. const p = points[i]
  72. const x = p[0]*width, y = p[1]*height;
  73. //绘制控制点的位置
  74. const cx = width * p[2];
  75. const cy = height *p[3];
  76. let v = {x: cx - x, y: cy-y};
  77. v = normalize(v);
  78. let slen = 90;
  79. const preCirleX = x -v.x * slen, preCirleY = y-v.y *slen;
  80. const aftCirleX = x +v.x *slen, aftCirleY = y+v.y*slen;
  81. slen = 80;
  82. const preLineX = x -v.x * slen, preLineY = y-v.y *slen;
  83. const aftLineX = x +v.x *slen, aftLineY = y+v.y*slen;
  84. out.push({x, y, cx, cy, isLine: p[4] != undefined,
  85. preCirleX,preCirleY, aftCirleX, aftCirleY,
  86. preLineX, preLineY, aftLineX, aftLineY});
  87. }
  88. return out;
  89. }
  90. function clickTest(points:PointItem[], e:MouseEvent, pxToDesignSize:any,) {
  91. const x = pxToDesignSize(e.offsetX )
  92. const y = pxToDesignSize(e.offsetY)
  93. let n = points.length;
  94. let pIndex = -1;
  95. let clickCurve = false;
  96. let clickLine = false;
  97. let moving = false;
  98. while(n--) {
  99. const p = points[n];
  100. let px = p.x;
  101. let py = p.y;
  102. const rect = 20
  103. if(!(x < (px -rect) || x > (px + rect) || y < (py -rect) || y > (py+rect) ) ) {
  104. pIndex = n;
  105. break;
  106. }
  107. if(!(x < (p.aftCirleX -rect) || x > (p.aftCirleX + rect) || y < (p.aftCirleY -rect) || y > (p.aftCirleY+rect) ) ) {
  108. pIndex = n;
  109. clickCurve = true;
  110. break;
  111. }
  112. if(!(x < (p.preLineX -rect) || x > (p.preLineX + rect) || y < (p.preLineY -rect) || y > (p.preLineY+rect) ) ) {
  113. pIndex = n;
  114. clickLine = true;
  115. break;
  116. }
  117. }
  118. if (pIndex == -1) {
  119. //ming
  120. const dta = 5
  121. if(!(x < (20 - dta) || x > (20 + dta) || y < (20 -dta) || y > (20 + dta) ) ) {
  122. moving = true;
  123. }
  124. }
  125. return {clickLine, clickCurve, pIndex, moving, x, y};
  126. }
  127. export const Component = defineComponent({
  128. props: {
  129. compId: string().isRequired,
  130. },
  131. setup(props) {
  132. const { helper, controls , store} = useEditor();
  133. const data = useCompData(props.compId);
  134. const canvasRef = ref<HTMLCanvasElement>();
  135. onMounted(()=>{
  136. canvasRef.value?.addEventListener("dblclick", function(e:MouseEvent){
  137. const x = helper.pxToDesignSize(e.offsetX )
  138. const y = helper.pxToDesignSize(e.offsetY)
  139. const points = data.value.points as number[][];
  140. const w = data.layout.size[0];
  141. const h = data.layout.size[1];
  142. //判断直线相交求解点到直线的距离
  143. const ret = findNearestPoint(data.value.points, w , h, x, y);
  144. //判断是否
  145. console.log("dbclick=>xxxxx", ret);
  146. if (ret.clicked) {//点击删除
  147. points.splice(ret.index, 1);
  148. if (points.length < 2) {
  149. return;
  150. }
  151. } else {
  152. let next = ret.index + 1;
  153. if (next >= points.length ) {
  154. next = 0;
  155. }
  156. //计算
  157. const x1 = x / w, y1= y / h;
  158. points.splice(ret.index+1, 0, [x1, y1 , (x1 + points[next][0])/ 2.0 , (y1 + points[next][1]) / 2.0 ]);
  159. }
  160. draw();
  161. })
  162. canvasRef.value?.addEventListener("mousedown", function(e:MouseEvent){
  163. if (store.currCompId != props.compId) return;
  164. const el = canvasRef.value as HTMLCanvasElement;
  165. const width = data.layout.size[0];
  166. const height = data.layout.size[1];
  167. const ps = parsePoints(data.value.points as number[][], width, height)
  168. const ret = clickTest(ps, e, helper.pxToDesignSize);
  169. if (ret.pIndex == -1 && !ret.moving) return;
  170. const dragingX = ret.x;
  171. const dragingY = ret.y;
  172. const points = data.value.points as number[][];
  173. const initValues = ret.pIndex != -1 ? points[ret.pIndex].slice(0) : {} as any;
  174. const initPoint = ret.pIndex != -1 ? {...ps[ret.pIndex]} : {} as any;
  175. const initPoints = points.map(item=>item.slice(0));
  176. e.preventDefault();
  177. e.stopPropagation();
  178. const isDragingIndex = ret.pIndex;
  179. const box = controls.selectCtrl.getCurrCardViewPortBox();
  180. const mvingPoint = !ret.clickCurve && !ret.clickLine
  181. let initControlpLen :number | undefined = undefined;
  182. const move = function (e:MouseEvent){
  183. e.preventDefault();
  184. e.stopPropagation();
  185. const obj = controls.selectCtrl.objContainer as ObjsContainer;
  186. const cardX = e.clientX - box.left, cardY = e.clientY - box.top;
  187. const p = {x: cardX, y:cardY} as any;
  188. obj.parent.worldTransform.applyInverse(p, p);
  189. obj.parent.updateTransform();
  190. const x = helper.pxToDesignSize(p.x), y = helper.pxToDesignSize(p.y);
  191. const offx = ( x - dragingX) / width;
  192. const offy = ( y - dragingY) / height;
  193. if (ret.moving) {//移动所有
  194. points.forEach((p,i)=>{
  195. p[0] = initPoints[i][0] + offx;
  196. p[1] = initPoints[i][1] + offy;
  197. p[2] = initPoints[i][2] + offx;
  198. p[3] = initPoints[i][3] + offy;
  199. })
  200. draw();
  201. return;
  202. }
  203. if (mvingPoint) {
  204. points[isDragingIndex][0] = initValues[0] + offx;
  205. points[isDragingIndex][1] = initValues[1] + offy;
  206. points[isDragingIndex][2] = initValues[2] + offx;
  207. points[isDragingIndex][3] = initValues[3] + offy;
  208. } else if (ret.clickCurve) {//移动曲线
  209. if (initControlpLen == undefined) { //初始控制点的长度
  210. initControlpLen = VectorLenth( initPoint.cx - initPoint.x, initPoint.cy - initPoint.y);
  211. }
  212. const half = initControlpLen / 2.0;
  213. //当前鼠标到远点的长度
  214. const len = half + VectorLenth( x - initPoint.x, y - initPoint.y) / 90.0 * half;
  215. let v = normalize({x: x - initPoint.x, y: y - initPoint.y});
  216. if (e.shiftKey) {//一条直线
  217. let nextIndex = ret.pIndex + 1;
  218. if (nextIndex >= ps.length) {
  219. nextIndex = 0;
  220. }
  221. v = normalize({x: ps[nextIndex].x - initPoint.x, y: ps[nextIndex].y - initPoint.y});
  222. }
  223. points[isDragingIndex][2] = (initPoint.x + v.x * len) / width;
  224. points[isDragingIndex][3] = (initPoint.y + v.y * len) / height;
  225. }
  226. draw();
  227. }
  228. if ( !ret.clickCurve && !ret.clickLine && !ret.moving) {
  229. el.addEventListener("mouseleave", function(){
  230. document.removeEventListener("mousemove", move);
  231. }, {once: true})
  232. }
  233. document.addEventListener("mousemove", move)
  234. document.addEventListener("mouseup", function(e:MouseEvent){
  235. document.removeEventListener("mousemove", move);
  236. }, {once: true});
  237. }, )
  238. effect(draw);
  239. })
  240. function draw() {
  241. const canvas = canvasRef.value as HTMLCanvasElement;
  242. if (!canvas) return;
  243. const ctx = canvasRef.value?.getContext("2d") as CanvasRenderingContext2D;
  244. const width = data.layout.size[0];
  245. const height = data.layout.size[1];
  246. canvas.width = Math.ceil(Math.max(1, width));
  247. canvas.height = Math.ceil(Math.max(1, height));
  248. ctx.clearRect(0, 0, canvas.width, canvas.height);
  249. ctx.lineWidth = data.value.lineWidth;
  250. if (data.value.reverseFill) {
  251. ctx.fillStyle = data.value.fillColor;
  252. ctx.fillRect(0, 0, canvas.width, canvas.height);
  253. }
  254. ctx.setLineDash([]);
  255. ctx.strokeStyle = data.value.lineColor;
  256. const padding = 0.2;
  257. ctx.lineJoin = "round";
  258. ctx.beginPath();
  259. let points = data.value.points as number[][];
  260. if (data.value.points.length == 0) {
  261. data.value.points = [[padding, 0.5, 0.4, 0.1], [1 - padding, 0.5, 0.5, 0.85]];
  262. points = data.value.points;
  263. }
  264. const ps = parsePoints(data.value.points, width, height);
  265. ctx.save();
  266. ps.forEach((p, index)=>{
  267. const x = p.x
  268. const y = p.y
  269. if (index == 0) {
  270. ctx.moveTo(x, y)
  271. } else {
  272. const preIndex = index -1;
  273. const prep = ps[preIndex];
  274. if (!prep.isLine) {
  275. ctx.quadraticCurveTo( prep.cx, prep.cy, x, y);
  276. } else {
  277. ctx.lineTo(x, y);
  278. }
  279. }
  280. })
  281. if (data.value.isClose) {
  282. const prep = ps[points.length-1];
  283. const lastPoint = ps[0];
  284. if (!prep.isLine) {
  285. ctx.quadraticCurveTo( prep.cx, prep.cy, lastPoint.x, lastPoint.y);
  286. }
  287. ctx.closePath();
  288. }
  289. if (data.value.lineWidth !== 0) {
  290. ctx.stroke();
  291. }
  292. if (data.value.isFill) {
  293. let bColor = data.value.fillColor;
  294. if (!bColor) bColor = data.value.lineColor;
  295. ctx.fillStyle = bColor;
  296. if (data.value.reverseFill) {
  297. ctx.fillStyle = "red";
  298. // 设置剪切区域
  299. ctx.clip();
  300. // 将当前路径指定的区域颜色扣除
  301. ctx.globalCompositeOperation = "destination-out";
  302. }
  303. ctx.fill();
  304. }
  305. ctx.restore();
  306. //绘制辅助gizmo
  307. if (store.isEditMode && store.currCompId == props.compId) {
  308. ctx.lineWidth = 2;
  309. ctx.strokeStyle = "orange";
  310. ctx.setLineDash([10, 5])
  311. ps.forEach((p, index)=>{
  312. if (index == 0) {
  313. ctx.beginPath();
  314. ctx.moveTo(p.x, p.y);
  315. } else {
  316. ctx.lineTo(p.x, p.y);
  317. }
  318. });
  319. if (data.value.isClose && ps.length > 2) {
  320. ctx.lineTo(ps[0].x, ps[0].y);
  321. }
  322. ctx.stroke();
  323. ctx.setLineDash([])
  324. ps.forEach((p)=>{
  325. const x = p.x
  326. const y = p.y
  327. ctx.fillStyle = "red"
  328. ctx.beginPath();
  329. ctx.arc(x, y, 5, 0, Math.PI*2);
  330. ctx.fill();
  331. ctx.beginPath()
  332. // ctx.setLineDash([5,5])
  333. ctx.moveTo(p.preLineX, p.preLineY);
  334. ctx.lineTo(p.aftLineX, p.aftLineY);
  335. ctx.stroke();
  336. // ctx.beginPath()
  337. // ctx.arc(p.preCirleX, p.preCirleY, r, 0, Math.PI*2)
  338. // ctx.stroke();
  339. const r = 10
  340. ctx.beginPath()
  341. ctx.arc(p.aftCirleX, p.aftCirleY, r, 0, Math.PI*2)
  342. ctx.fill();
  343. })
  344. ctx.beginPath()
  345. ctx.arc(20, 20 , 5, 0, Math.PI*2)
  346. ctx.fill();
  347. }
  348. }
  349. return () => (
  350. <View compId={props.compId}>
  351. <canvas ref={canvasRef} style={{width:"100%", height: "100%"}}> </canvas>
  352. </View>
  353. );
  354. },
  355. });