Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

Timeline.tsx 7.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import React, { MouseEvent, useCallback, useEffect, useMemo, useRef } from 'react';
  2. import { useDispatch, useSelector } from 'react-redux';
  3. import { IReduxState } from '../../../app/types';
  4. import { getConferenceTimestamp } from '../../../base/conference/functions';
  5. import { FaceLandmarks } from '../../../face-landmarks/types';
  6. import { addToOffset, setTimelinePanning } from '../../actions.any';
  7. import { SCROLL_RATE, TIMELINE_COLORS } from '../../constants';
  8. import { getFaceLandmarksEnd, getFaceLandmarksStart, getTimelineBoundaries } from '../../functions';
  9. interface IProps {
  10. faceLandmarks?: FaceLandmarks[];
  11. }
  12. const Timeline = ({ faceLandmarks }: IProps) => {
  13. const startTimestamp = useSelector((state: IReduxState) => getConferenceTimestamp(state)) ?? 0;
  14. const { left, right } = useSelector((state: IReduxState) => getTimelineBoundaries(state));
  15. const { timelinePanning } = useSelector((state: IReduxState) => state['features/speaker-stats']);
  16. const dispatch = useDispatch();
  17. const containerRef = useRef<HTMLDivElement>(null);
  18. const intervalDuration = useMemo(() => right - left, [ left, right ]);
  19. const getSegments = useCallback(() => {
  20. const segments = faceLandmarks?.filter(landmarks => {
  21. const timeStart = getFaceLandmarksStart(landmarks, startTimestamp);
  22. const timeEnd = getFaceLandmarksEnd(landmarks, startTimestamp);
  23. if (timeEnd > left && timeStart < right) {
  24. return true;
  25. }
  26. return false;
  27. }) ?? [];
  28. let leftCut;
  29. let rightCut;
  30. if (segments.length) {
  31. const start = getFaceLandmarksStart(segments[0], startTimestamp);
  32. const end = getFaceLandmarksEnd(segments[segments.length - 1], startTimestamp);
  33. if (start <= left) {
  34. leftCut = segments[0];
  35. }
  36. if (end >= right) {
  37. rightCut = segments[segments.length - 1];
  38. }
  39. }
  40. if (leftCut) {
  41. segments.shift();
  42. }
  43. if (rightCut) {
  44. segments.pop();
  45. }
  46. return {
  47. segments,
  48. leftCut,
  49. rightCut
  50. };
  51. }, [ faceLandmarks, left, right, startTimestamp ]);
  52. const { segments, leftCut, rightCut } = getSegments();
  53. const getStyle = useCallback((duration: number, faceExpression: string) => {
  54. return {
  55. width: `${100 / (intervalDuration / duration)}%`,
  56. backgroundColor: TIMELINE_COLORS[faceExpression] ?? TIMELINE_COLORS['no-detection']
  57. };
  58. }, [ intervalDuration ]);
  59. const getStartStyle = useCallback(() => {
  60. let startDuration = 0;
  61. let color = TIMELINE_COLORS['no-detection'];
  62. if (leftCut) {
  63. const { faceExpression } = leftCut;
  64. startDuration = getFaceLandmarksEnd(leftCut, startTimestamp) - left;
  65. color = TIMELINE_COLORS[faceExpression];
  66. } else if (segments.length) {
  67. startDuration = getFaceLandmarksStart(segments[0], startTimestamp) - left;
  68. } else if (rightCut) {
  69. startDuration = getFaceLandmarksStart(rightCut, startTimestamp) - left;
  70. }
  71. return {
  72. width: `${100 / (intervalDuration / startDuration)}%`,
  73. backgroundColor: color
  74. };
  75. }, [ leftCut, rightCut, startTimestamp, left, intervalDuration, segments ]);
  76. const getEndStyle = useCallback(() => {
  77. let endDuration = 0;
  78. let color = TIMELINE_COLORS['no-detection'];
  79. if (rightCut) {
  80. const { faceExpression } = rightCut;
  81. endDuration = right - getFaceLandmarksStart(rightCut, startTimestamp);
  82. color = TIMELINE_COLORS[faceExpression];
  83. } else if (segments.length) {
  84. endDuration = right - getFaceLandmarksEnd(segments[segments.length - 1], startTimestamp);
  85. } else if (leftCut) {
  86. endDuration = right - getFaceLandmarksEnd(leftCut, startTimestamp);
  87. }
  88. return {
  89. width: `${100 / (intervalDuration / endDuration)}%`,
  90. backgroundColor: color
  91. };
  92. }, [ leftCut, rightCut, startTimestamp, right, intervalDuration, segments ]);
  93. const getOneSegmentStyle = useCallback((faceExpression?: string) => {
  94. return {
  95. width: '100%',
  96. backgroundColor: faceExpression ? TIMELINE_COLORS[faceExpression] : TIMELINE_COLORS['no-detection'],
  97. borderRadius: 0
  98. };
  99. }, []);
  100. const handleOnWheel = useCallback((event: WheelEvent) => {
  101. // check if horizontal scroll
  102. if (Math.abs(event.deltaX) >= Math.abs(event.deltaY)) {
  103. const value = event.deltaX * SCROLL_RATE;
  104. dispatch(addToOffset(value));
  105. event.preventDefault();
  106. }
  107. }, [ dispatch, addToOffset ]);
  108. const hideStartAndEndSegments = useCallback(() => leftCut && rightCut
  109. && leftCut.faceExpression === rightCut.faceExpression
  110. && !segments.length,
  111. [ leftCut, rightCut, segments ]);
  112. useEffect(() => {
  113. containerRef.current?.addEventListener('wheel', handleOnWheel, { passive: false });
  114. return () => containerRef.current?.removeEventListener('wheel', handleOnWheel);
  115. }, []);
  116. const getPointOnTimeline = useCallback((event: MouseEvent) => {
  117. const axisRect = event.currentTarget.getBoundingClientRect();
  118. const eventOffsetX = event.pageX - axisRect.left;
  119. return (eventOffsetX * right) / axisRect.width;
  120. }, [ right ]);
  121. const handleOnMouseMove = useCallback((event: MouseEvent) => {
  122. const { active, x } = timelinePanning;
  123. if (active) {
  124. const point = getPointOnTimeline(event);
  125. dispatch(addToOffset(x - point));
  126. dispatch(setTimelinePanning({ ...timelinePanning,
  127. x: point }));
  128. }
  129. }, [ timelinePanning, dispatch, addToOffset, setTimelinePanning, getPointOnTimeline ]);
  130. const handleOnMouseDown = useCallback((event: MouseEvent) => {
  131. const point = getPointOnTimeline(event);
  132. dispatch(setTimelinePanning(
  133. {
  134. active: true,
  135. x: point
  136. }
  137. ));
  138. event.preventDefault();
  139. event.stopPropagation();
  140. }, [ getPointOnTimeline, dispatch, setTimelinePanning ]);
  141. return (
  142. <div
  143. className = 'timeline-container'
  144. onMouseDown = { handleOnMouseDown }
  145. onMouseMove = { handleOnMouseMove }
  146. ref = { containerRef }>
  147. <div
  148. className = 'timeline'>
  149. {!hideStartAndEndSegments() && <div
  150. aria-label = 'start'
  151. style = { getStartStyle() } />}
  152. {hideStartAndEndSegments() && <div
  153. style = { getOneSegmentStyle(leftCut?.faceExpression) } />}
  154. {segments?.map(({ duration, timestamp, faceExpression }) =>
  155. (<div
  156. aria-label = { faceExpression }
  157. key = { timestamp }
  158. style = { getStyle(duration, faceExpression) } />)) }
  159. {!hideStartAndEndSegments() && <div
  160. aria-label = 'end'
  161. style = { getEndStyle() } />}
  162. </div>
  163. </div>
  164. );
  165. };
  166. export default Timeline;