Dial.tsx 16 KB

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