Dial.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. import Taro from '@tarojs/taro';
  2. import { Canvas, View, Image } from '@tarojs/components';
  3. import { useEffect, useRef, useState } from 'react';
  4. import React from 'react';
  5. import './Dial.scss'
  6. import { useSelector } from 'react-redux';
  7. const Component = (props) => {
  8. const [scenario] = useState(useSelector((state: any) => state.scenario))
  9. const [currentContext, setCurrentContext] = useState(null)
  10. const canvasWidth: number = 334;
  11. const canvasHeight: number = 334;
  12. const dpr = Taro.getSystemInfoSync().pixelRatio; // 获取设备的像素比
  13. const canvasRef = useRef<any>(null);
  14. const watchLineWidth = 300
  15. const canvasId = 'canvasDialId';
  16. var canStartDrag = false;
  17. var canEndDrag = false;
  18. var canRingDrag = false;
  19. var current = {
  20. start: global.startTime,
  21. end: global.endTime
  22. }
  23. var startCount = parseInt((current.start.split(':') as any)[0]) * 60 + parseInt((current.start.split(':') as any)[1]);
  24. var endCount = parseInt((current.end.split(':') as any)[0]) * 60 + parseInt((current.end.split(':') as any)[1]);
  25. var startAngle = (startCount / 1440) * 2 * Math.PI - Math.PI / 2;
  26. var endAngle = (endCount / 1440) * 2 * Math.PI - Math.PI / 2;
  27. endAngle = endAngle > startAngle ? endAngle : endAngle + 2 * Math.PI;
  28. var lastAngle;
  29. var lastDuration = 0;
  30. var strStart = current.start;
  31. var strEnd = current.end;
  32. var needDrawFastRing = false;
  33. if (scenario.name == 'FAST_SLEEP' && scenario.step == 'sleep') {
  34. needDrawFastRing = true;
  35. }
  36. useEffect(() => {
  37. const query = Taro.createSelectorQuery();
  38. query.select(`.${canvasId}`).fields({ node: true, size: true });
  39. query.exec((res) => {
  40. if (res[0] == null) return;
  41. const _canvas = res[0].node;
  42. _canvas.width = res[0].width * dpr;
  43. _canvas.height = res[0].height * dpr;
  44. global.canvas = _canvas
  45. const ctx = _canvas.getContext('2d');
  46. setCurrentContext(ctx)
  47. // 设置画布尺寸
  48. ctx.scale(dpr, dpr);
  49. drawCanvas(ctx);
  50. });
  51. }, []);
  52. const drawCanvas = (ctx: any) => {
  53. const centerX = canvasWidth / 2;
  54. const centerY = canvasHeight / 2;
  55. const radius = 140;//Math.min(centerX, centerY) - 38;
  56. ctx.clearRect(0, 0, canvasWidth, canvasHeight);
  57. //画断食环
  58. if (needDrawFastRing) {
  59. ctx.beginPath();
  60. ctx.arc(centerX, centerY, radius + 16 + 8, global.fast_start_angle, global.fast_end_angle);
  61. ctx.strokeStyle = 'rgba(170, 255, 0, 0.3)';
  62. ctx.lineWidth = 2;
  63. ctx.stroke();
  64. }
  65. // 绘制圆环
  66. ctx.beginPath();
  67. ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
  68. ctx.strokeStyle = "rgba(0,0,0,0.5)";
  69. ctx.lineWidth = 38;
  70. ctx.stroke();
  71. //绘制起终点圆弧
  72. ctx.beginPath();
  73. ctx.arc(centerX, centerY, radius, startAngle, endAngle);
  74. ctx.strokeStyle = canRingDrag ? "#3e3e3e" : "#1c1c1c";
  75. ctx.lineWidth = 38;
  76. ctx.lineCap = "round";
  77. ctx.stroke();
  78. //画刻度线
  79. ctx.save()
  80. ctx.beginPath()
  81. ctx.moveTo(centerX, centerY)
  82. ctx.arc(
  83. centerX, centerY, watchLineWidth / 2.0, startAngle, endAngle
  84. )
  85. ctx.clip()
  86. if (global.canvas_watch_line) {
  87. ctx.drawImage(global.canvas_watch_line, centerX - watchLineWidth / 2.0, centerY - watchLineWidth / 2.0, watchLineWidth, watchLineWidth)
  88. }
  89. else {
  90. let tempImage = global.canvas.createImage()
  91. tempImage.src = require('@/assets/images/watch_line.png')//'./watch_line.png'//'..//assets/images/watch_line.png'
  92. tempImage.onload = () => {
  93. global.canvas_watch_line = tempImage
  94. drawCanvas(ctx)
  95. }
  96. }
  97. ctx.restore()
  98. ctx.closePath()
  99. if (scenario.step == 'fast') {
  100. global.fast_start_angle = startAngle;
  101. global.fast_end_angle = endAngle;
  102. }
  103. const pointX = centerX + Math.cos(startAngle) * radius;
  104. const pointY = centerY + Math.sin(startAngle) * radius;
  105. //绘制起点圆点
  106. ctx.beginPath();
  107. ctx.arc(pointX, pointY, 17, 0, 2 * Math.PI);
  108. ctx.fillStyle = (canRingDrag || canStartDrag) ? "#3e3e3e" : "#1c1c1c";
  109. ctx.fill();
  110. //绘制起点图标
  111. // ctx.save()
  112. // ctx.beginPath()
  113. // ctx.translate(pointX, pointY)
  114. // ctx.rotate(startAngle)
  115. // ctx.translate(-pointX, -pointY)
  116. if (global.canvas_start_icon) {
  117. ctx.drawImage(global.canvas_start_icon, pointX - 20, pointY - 20, 40, 40)
  118. }
  119. else {
  120. let tempImage = global.canvas.createImage()
  121. tempImage.src = require('@/assets/images/wakeup-white.png')//'./watch_line.png'//'..//assets/images/watch_line.png'
  122. tempImage.onload = () => {
  123. global.canvas_start_icon = tempImage
  124. drawCanvas(ctx)
  125. }
  126. }
  127. // ctx.restore()
  128. // ctx.closePath()
  129. // 绘制终点圆点
  130. const pointX2 = centerX + Math.cos(endAngle) * radius;
  131. const pointY2 = centerY + Math.sin(endAngle) * radius;
  132. ctx.beginPath();
  133. ctx.arc(pointX2, pointY2, 17, 0, 2 * Math.PI);
  134. ctx.fillStyle = (canRingDrag || canEndDrag) ? "#3e3e3e" : "#1c1c1c";
  135. ctx.fill();
  136. //绘制终点图标
  137. if (global.canvas_end_icon) {
  138. ctx.drawImage(global.canvas_end_icon, pointX2 - 20, pointY2 - 20, 40, 40)
  139. }
  140. else {
  141. let tempImage = global.canvas.createImage()
  142. tempImage.src = require('@/assets/images/watch_end_white.png')//'./watch_line.png'//'..//assets/images/watch_line.png'
  143. tempImage.onload = () => {
  144. global.canvas_end_icon = tempImage
  145. drawCanvas(ctx)
  146. }
  147. }
  148. };
  149. global.updateDial = (start, end) => {
  150. var startCount = parseInt((start.split(':') as any)[0]) * 60 + parseInt((start.split(':') as any)[1]);
  151. var endCount = parseInt((end.split(':') as any)[0]) * 60 + parseInt((end.split(':') as any)[1]);
  152. startAngle = (startCount / 1440) * 2 * Math.PI - Math.PI / 2;
  153. endAngle = (endCount / 1440) * 2 * Math.PI - Math.PI / 2;
  154. endAngle = endAngle > startAngle ? endAngle : endAngle + 2 * Math.PI;
  155. drawCanvas(currentContext);
  156. }
  157. function twoPointDistance(p1, p2) {
  158. let dep = Math.sqrt(Math.pow((p1.x - p2.x), 2) + Math.pow((p1.y - p2.y), 2));
  159. return dep;
  160. }
  161. function limitAngle(start, end) {
  162. var angle1 = start < 0 ? start + 2 * Math.PI : start;
  163. var angle2 = end < 0 ? end + 2 * Math.PI : end;
  164. if (angle2 < angle1) {
  165. angle2 = angle2 + 2 * Math.PI;
  166. }
  167. var leftAngle = angle2 - angle1;
  168. if (leftAngle > Math.PI * 23 / 12) {
  169. return 1;
  170. }
  171. else if (leftAngle < Math.PI / 12) {
  172. return 2;
  173. }
  174. return 0;
  175. }
  176. function durationAngle(end, start) {
  177. var angle1 = start < 0 ? start + 2 * Math.PI : start;
  178. var angle2 = end < 0 ? end + 2 * Math.PI : end;
  179. if (angle2 < angle1) {
  180. angle2 = angle2 + 2 * Math.PI;
  181. }
  182. return angle2 - angle1;
  183. }
  184. const handleTouchMove = (e: any) => {
  185. const ctx = currentContext;//canvasRef.current.getContext('2d');
  186. if (!canStartDrag && !canEndDrag && !canRingDrag) {
  187. drawCanvas(ctx);
  188. return;
  189. }
  190. const { x, y } = e.touches[0];
  191. let angle = Math.atan2(y - canvasWidth / 2.0, x - canvasWidth / 2.0)
  192. if (canStartDrag) {
  193. if (Math.abs(durationAngle(endAngle, angle) - lastDuration) > Math.PI) {
  194. //禁止跨越
  195. return;
  196. }
  197. var result = limitAngle(angle, endAngle);
  198. switch (result) {
  199. case 0:
  200. startAngle = angle;
  201. break;
  202. case 1:
  203. startAngle = endAngle - 23 * Math.PI / 12;
  204. break;
  205. case 2:
  206. startAngle = endAngle - Math.PI / 12;
  207. break;
  208. }
  209. }
  210. if (canEndDrag) {
  211. if (Math.abs(durationAngle(angle, startAngle) - lastDuration) > Math.PI) {
  212. //禁止跨越
  213. return;
  214. }
  215. var result = limitAngle(startAngle, angle);
  216. switch (result) {
  217. case 0:
  218. endAngle = angle;
  219. break;
  220. case 1:
  221. endAngle = startAngle + 23 * Math.PI / 12;
  222. break;
  223. case 2:
  224. endAngle = startAngle + Math.PI / 12;
  225. break;
  226. }
  227. }
  228. if (canRingDrag) {
  229. let delta = angle - lastAngle;
  230. startAngle += delta;
  231. endAngle += delta;
  232. lastAngle = angle;
  233. }
  234. startAngle = normalizeAngle(startAngle);
  235. endAngle = normalizeAngle(endAngle);
  236. angleToTime();
  237. drawCanvas(ctx);
  238. lastDuration = durationAngle(endAngle, startAngle);
  239. };
  240. function angleToTime() {
  241. var oldStart = parseInt(strStart.split(':')[0] as any) * 60 + parseInt(strStart.split(':')[1] as any);
  242. var oldEnd = parseInt(strEnd.split(':')[0] as any) * 60 + parseInt(strEnd.split(':')[1] as any);
  243. if (oldEnd < oldStart) oldEnd += 1440;
  244. var oldDuration = oldEnd - oldStart;
  245. var startCount = (startAngle + Math.PI / 2) / (2 * Math.PI) * 1440;
  246. var endCount = (endAngle + Math.PI / 2) / (2 * Math.PI) * 1440;
  247. startCount = startCount > 0 ? startCount : startCount + 1440;
  248. endCount = endCount > 0 ? endCount : endCount + 1440;
  249. startCount = roundToNearestFive(startCount);
  250. endCount = roundToNearestFive(endCount);
  251. var startHour = Math.floor(startCount / 60);
  252. var startMinute = Math.floor(startCount % 60);
  253. var endHour = Math.floor(endCount / 60);
  254. var endMinute = Math.floor(endCount % 60);
  255. startHour = startHour < 24 ? startHour : startHour - 24;
  256. endHour = endHour < 24 ? endHour : endHour - 24;
  257. var start = (startHour < 10 ? '0' + startHour : startHour) + ':' + (startMinute < 10 ? '0' + startMinute : startMinute);
  258. var end = (endHour < 10 ? '0' + endHour : endHour) + ':' + (endMinute < 10 ? '0' + endMinute : endMinute);
  259. if (canStartDrag) {
  260. strStart = start;
  261. global.updateDuration(start, strEnd);
  262. }
  263. else if (canEndDrag) {
  264. strEnd = end;
  265. global.updateDuration(strStart, end);
  266. }
  267. else if (canRingDrag) {
  268. var newCount = startHour * 60 + startMinute
  269. var newEnd = oldDuration + newCount
  270. const resultHour = Math.floor(newEnd / 60) % 24; // 对 24 取模以确保结果在 0-23 之间
  271. const resultMinute = newEnd % 60;
  272. strStart = start
  273. strEnd = `${String(resultHour).padStart(2, '0')}:${String(resultMinute).padStart(2, '0')}`;
  274. global.updateDuration(start, strEnd);
  275. }
  276. }
  277. function roundToNearestFive(minutes: number): number {
  278. const remainder = minutes % 5;
  279. if (remainder === 0) {
  280. return minutes;
  281. } else if (remainder < 3) {
  282. return minutes - remainder;
  283. } else {
  284. return minutes + (5 - remainder);
  285. }
  286. }
  287. const handleClick = (e) => {
  288. const scaleX = 1//dpr//canvasRect.width / canvasRect.width;
  289. const scaleY = 1//dpr//canvasRect.height / canvasRect.height;
  290. const canvasX = (e.touches[0].x - 0) * scaleX;
  291. const canvasY = (e.touches[0].y - 0) * scaleY;
  292. // 判断点击位置是否在圆点范围内
  293. const centerX = canvasWidth / 2;
  294. const centerY = canvasWidth / 2;
  295. const radius = Math.min(centerX, centerY) - 10;
  296. const pointX = centerX + Math.cos(startAngle) * radius;
  297. const pointY = centerY + Math.sin(startAngle) * radius;
  298. const distance = Math.sqrt(Math.pow(canvasX - pointX, 2) + Math.pow(canvasY - pointY, 2));
  299. if (distance <= 40) {
  300. canStartDrag = true;
  301. } else {
  302. canStartDrag = false;
  303. }
  304. const pointX2 = centerX + Math.cos(endAngle) * radius;
  305. const pointY2 = centerY + Math.sin(endAngle) * radius;
  306. const distance2 = Math.sqrt(Math.pow(canvasX - pointX2, 2) + Math.pow(canvasY - pointY2, 2));
  307. if (distance2 <= 40) {
  308. canEndDrag = true;
  309. } else {
  310. canEndDrag = false;
  311. }
  312. if (canStartDrag && canEndDrag) {
  313. if (distance < distance2) {
  314. canEndDrag = false
  315. }
  316. else {
  317. canStartDrag = false
  318. }
  319. }
  320. lastDuration = durationAngle(endAngle, startAngle);
  321. if (canStartDrag || canEndDrag) {
  322. drawCanvas(currentContext);
  323. canRingDrag = false;
  324. if (canStartDrag || canEndDrag){
  325. var type = canStartDrag?1:2
  326. global.startDuration(type)
  327. }
  328. return;
  329. }
  330. const distance3 = twoPointDistance({ x: canvasX, y: canvasY }, { x: centerX, y: centerY });
  331. let angle = Math.atan2(canvasY - canvasWidth / 2.0, canvasX - canvasWidth / 2.0)
  332. if (distance3 > 120 && distance3 < 190 && isCenterAngle(startAngle, angle, endAngle)) {
  333. canRingDrag = true;
  334. }
  335. lastAngle = angle;
  336. startAngle = normalizeAngle(startAngle);
  337. endAngle = normalizeAngle(endAngle);
  338. drawCanvas(currentContext);
  339. if (canStartDrag || canEndDrag || canRingDrag){
  340. var type = canStartDrag?1:canEndDrag?2:3
  341. global.startDuration(type)
  342. }
  343. else {
  344. global.endDuration()
  345. }
  346. };
  347. const normalizeAngle = (angle) => {
  348. if (angle < 0) {
  349. angle = angle + 2 * Math.PI;
  350. normalizeAngle(angle);
  351. }
  352. if (angle > 2 * Math.PI) {
  353. angle = angle - 2 * Math.PI;
  354. normalizeAngle(angle);
  355. }
  356. return angle;
  357. }
  358. const isCenterAngle = (start, center, end) => {
  359. start = normalizeAngle(start);
  360. center = normalizeAngle(center);
  361. end = normalizeAngle(end);
  362. if (start > end) {
  363. end += 2 * Math.PI;
  364. }
  365. if (end > 2 * Math.PI && center < start) {
  366. center += 2 * Math.PI;
  367. }
  368. return start < center && center < end;
  369. }
  370. const handleTouchEnd = (e) => {
  371. canStartDrag = false;
  372. canEndDrag = false;
  373. canRingDrag = false;
  374. global.endDuration()
  375. handleTouchMove(e)
  376. }
  377. return <View style={{ width: 334, height: 334, borderRadius: 167, backgroundColor: '#000', position: 'relative' }} ref={props.ref}>
  378. <Canvas canvasId={canvasId} id={canvasId} className={canvasId} type="2d"
  379. style={{ width: 334, height: 334, zIndex: 0 }}
  380. onTouchMove={handleTouchMove}
  381. onTouchEnd={handleTouchEnd}
  382. onTouchStart={handleClick}
  383. ref={canvasRef}
  384. />
  385. <View className='inner'>
  386. <Image src={require('@/assets/images/watch-inner.png')} style={{ width: 226, height: 226 }} />
  387. </View>
  388. </View>
  389. }
  390. export default React.memo(Component);