diff --git a/packages/timeline/src/components/cells/Cell.tsx b/packages/timeline/src/components/cells/Cell.tsx index b5171aa4..43f8e09a 100644 --- a/packages/timeline/src/components/cells/Cell.tsx +++ b/packages/timeline/src/components/cells/Cell.tsx @@ -1,4 +1,4 @@ -import { Suspense } from "react" +import { Suspense, useMemo } from "react" import { a } from "@react-spring/three" import { ClapSegmentCategory } from "@aitube/clap" @@ -17,6 +17,8 @@ import { useThree } from "@react-three/fiber" import { SegmentArea } from "@/types/timeline" import { SegmentIcon } from "../icons/SegmentIcon" +import { getSegmentColorScheme } from "@/utils/getSegmentColorScheme" + export function Cell({ segment: s }: { @@ -31,8 +33,7 @@ export function Cell({ // this is only used to react to changes in the segment const segmentChanged = useSegmentChanges(s) - const getSegmentColorScheme = useTimeline(s => s.getSegmentColorScheme) - const colorScheme = getSegmentColorScheme(s) + const colorScheme = useMemo(() => getSegmentColorScheme(s), [segmentChanged]) const cellWidth = useTimeline((s) => s.cellWidth) const getCellHeight = useTimeline((s) => s.getCellHeight) diff --git a/packages/timeline/src/components/scroller/HorizontalScroller.tsx b/packages/timeline/src/components/scroller/HorizontalScroller.tsx index 919f4df7..ca35ed1a 100644 --- a/packages/timeline/src/components/scroller/HorizontalScroller.tsx +++ b/packages/timeline/src/components/scroller/HorizontalScroller.tsx @@ -1,12 +1,20 @@ +import { useMemo } from "react" + import { useTimeline } from "@/hooks/useTimeline" +import { getSegmentColorScheme } from "@/utils/getSegmentColorScheme" + import TimelineSlider from "../slider/TimelineSlider" export function HorizontalScroller() { const theme = useTimeline(s => s.theme) + const containerWidth = useTimeline(s => s.containerWidth) + const segments = useTimeline(s => s.segments) + const atLeastOneSegmentChanged = useTimeline(s => s.atLeastOneSegmentChanged) + const timelineCamera = useTimeline(s => s.timelineCamera) const timelineControls = useTimeline(s => s.timelineControls) @@ -26,33 +34,17 @@ export function HorizontalScroller() { const setScrollX = useTimeline(s => s.setScrollX) const contentWidth = useTimeline(s => s.contentWidth) - const getSegmentColorScheme = useTimeline(s => s.getSegmentColorScheme) - + const cachedSegments = useMemo(() => segments, [atLeastOneSegmentChanged, containerWidth, contentWidth]) + if (!timelineCamera || !timelineControls) { return null } + // TODO: we need to be able to change the zoom level from the horizontal scroller const handleZoomChange = (newZoom: number) => { setHorizontalZoomLevel(newZoom) } return (
- {/* - PREVIOUS COMPONENT, NOW OBSOLETE: - { - handleTimelinePositionChange(newRange[0]) - }} - onWheel={(e) => { - // handleZoomChange(cellWidth + e.deltaY) - }} - /> - */} - ({ - id: s.id, - track: s.track, - startTimeInMs: s.startTimeInMs, - endTimeInMs: s.endTimeInMs, - color: getSegmentColorScheme(s).backgroundColor, - }))} - eventOpacityWhenInsideSlidingWindowRangeThumb={1.0} - eventOpacityWhenOutsideSlidingWindowRangeThumb={0.7} + segments={cachedSegments} + segmentOpacityWhenInsideSlidingWindowRangeThumb={1.0} + segmentOpacityWhenOutsideSlidingWindowRangeThumb={0.7} onSlidingWindowRangeThumbUpdate={({ slidingWindowRangeThumbStartTimeInMs, slidingWindowRangeThumbEndTimeInMs diff --git a/packages/timeline/src/components/slider/TimelineSlider.tsx b/packages/timeline/src/components/slider/TimelineSlider.tsx index b49db488..dab627a3 100644 --- a/packages/timeline/src/components/slider/TimelineSlider.tsx +++ b/packages/timeline/src/components/slider/TimelineSlider.tsx @@ -1,14 +1,6 @@ -import { useTimeline } from '@/index'; +import { TimelineSegment, useTimeline } from '@/index'; import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react'; -export interface TimelineSliderEvent { - id: string; - track: number; - startTimeInMs: number; - endTimeInMs: number; - color: string; -} - export interface TimelineSliderProps { minTimeInMs: number; maxTimeInMs: number; @@ -25,9 +17,9 @@ export interface TimelineSliderProps { slidingWindowRangeThumbBorderRadiusInPx: number; slidingWindowRangeThumbBackgroundColor: string; className: string; - events?: TimelineSliderEvent[]; - eventOpacityWhenInsideSlidingWindowRangeThumb: number; - eventOpacityWhenOutsideSlidingWindowRangeThumb: number; + segments?: TimelineSegment[]; + segmentOpacityWhenInsideSlidingWindowRangeThumb: number; + segmentOpacityWhenOutsideSlidingWindowRangeThumb: number; onSlidingWindowRangeThumbUpdate: (update: { slidingWindowRangeThumbStartTimeInMs: number; slidingWindowRangeThumbEndTimeInMs: number; @@ -62,9 +54,9 @@ const TimelineSlider: React.FC = ({ slidingWindowRangeThumbBorderRadiusInPx, slidingWindowRangeThumbBackgroundColor, className, - events = [], - eventOpacityWhenInsideSlidingWindowRangeThumb, - eventOpacityWhenOutsideSlidingWindowRangeThumb, + segments = [], + segmentOpacityWhenInsideSlidingWindowRangeThumb, + segmentOpacityWhenOutsideSlidingWindowRangeThumb, onSlidingWindowRangeThumbUpdate, onPlaybackCursorUpdate, }) => { @@ -90,25 +82,25 @@ const TimelineSlider: React.FC = ({ ctx.clearRect(0, 0, width * dpr, height * dpr); - const totalTracks = Math.max(...events.map(e => e.track), 0) + 1; + const totalTracks = Math.max(...segments.map(e => e.track), 0) + 1; const trackHeight = height / totalTracks; - events.forEach(event => { - const startX = ((event.startTimeInMs - minTimeInMs) / (maxTimeInMs - minTimeInMs)) * width; - const endX = ((event.endTimeInMs - minTimeInMs) / (maxTimeInMs - minTimeInMs)) * width; - const y = event.track * trackHeight; + segments.forEach(segment => { + const startX = ((segment.startTimeInMs - minTimeInMs) / (maxTimeInMs - minTimeInMs)) * width; + const endX = ((segment.endTimeInMs - minTimeInMs) / (maxTimeInMs - minTimeInMs)) * width; + const y = segment.track * trackHeight; - ctx.fillStyle = event.color; + ctx.fillStyle = segment.colors.backgroundColor; ctx.globalAlpha = - (event.startTimeInMs >= windowStart && event.endTimeInMs <= windowEnd) - ? eventOpacityWhenInsideSlidingWindowRangeThumb - : eventOpacityWhenOutsideSlidingWindowRangeThumb; + (segment.startTimeInMs >= windowStart && segment.endTimeInMs <= windowEnd) + ? segmentOpacityWhenInsideSlidingWindowRangeThumb + : segmentOpacityWhenOutsideSlidingWindowRangeThumb; ctx.fillRect(startX, y, endX - startX, trackHeight); }); - }, [events, minTimeInMs, maxTimeInMs, windowStart, windowEnd, eventOpacityWhenInsideSlidingWindowRangeThumb, eventOpacityWhenOutsideSlidingWindowRangeThumb]); + }, [segments, minTimeInMs, maxTimeInMs, windowStart, windowEnd, segmentOpacityWhenInsideSlidingWindowRangeThumb, segmentOpacityWhenOutsideSlidingWindowRangeThumb]); - const memoizedEvents = useMemo(() => events, [events]); + const memoizedEvents = useMemo(() => segments, [segments]); const setCanvasSize = useCallback(() => { const canvas = canvasRef.current; diff --git a/packages/timeline/src/components/timeline/TopBarTimeScale.tsx b/packages/timeline/src/components/timeline/TopBarTimeScale.tsx index 2fec0c2a..f2fbccb2 100644 --- a/packages/timeline/src/components/timeline/TopBarTimeScale.tsx +++ b/packages/timeline/src/components/timeline/TopBarTimeScale.tsx @@ -1,21 +1,19 @@ import React, { useEffect, useMemo, useRef } from "react" - +import { useThree } from "@react-three/fiber" import { Plane, Text } from "@react-three/drei" -import { -useTimeline -} from "@/hooks" - +import { useTimeline } from "@/hooks" import { useTimeScaleGraduations } from "@/hooks/useTimeScaleGraduations" import { formatTimestamp } from "@/utils/formatTimestamp" import { leftBarTrackScaleWidth, topBarTimeScaleHeight } from "@/constants/themes" -import { useThree } from "@react-three/fiber" export function TopBarTimeScale() { const containerRef = useRef(null); - const { size } = useThree() + const containerWidth = useTimeline(s => s.containerWidth) + + const { size, camera } = useThree() const jumpAt = useTimeline(s => s.jumpAt) const togglePlayback = useTimeline(s => s.togglePlayback) @@ -153,7 +151,26 @@ export function TopBarTimeScale() { ))} - {timeScaleGraduations.filter((_, idx) => (idx * cellWidth) < maxWidth).map((lineGeometry, idx) => ( + {timeScaleGraduations + .filter((_, idx) => (idx * cellWidth) < maxWidth) + .map((lineGeometry, idx) => { + + if ( + // Hide text if it's too close to others or out of view + (cellWidth <= 4 && idx % 10 !== 0) || + (cellWidth <= 40 && idx % unit !== 0) || + idx === 0 // Always hide the 0 + + // TODO: those need more work + // || + // (idx * cellWidth) < camera.position.x - size.width / 2 - cellWidth || // Out of view on the left + // (idx * cellWidth) > camera.position.x + size.width / 2 + cellWidth // Out of view on the right + ) { + return null + } + + + return ( { formatTimestamp( @@ -204,7 +223,8 @@ export function TopBarTimeScale() { milliseconds: cellWidth > 20, })} - ))} + ) + }).filter(x => x)} ), [ @@ -215,6 +235,7 @@ export function TopBarTimeScale() { contentWidth, cellWidth, unit, + containerWidth, formatTimestamp, theme.topBarTimeScale.backgroundColor, theme.topBarTimeScale.lineColor, @@ -236,9 +257,10 @@ export function TopBarTimeScale() { const disableWheel = true if (disableWheel) { - console.log(`user tried to change the horizontal scale, but it is disabled due to rescaling bugs (@Julian fix this!)`) - e.stopPropagation() - return false + console.log( + `zoom in/out is currently disabled, we need to update the min and max values and check other redrawing routines (see https://github.com/jbilcke-hf/clapper/issues/47)`) + e.stopPropagation() + return false } const wheelFactor = 0.3 diff --git a/packages/timeline/src/demo.tsx b/packages/timeline/src/demo.tsx index 93ed27da..19b9b6f3 100644 --- a/packages/timeline/src/demo.tsx +++ b/packages/timeline/src/demo.tsx @@ -49,6 +49,21 @@ const segment: TimelineSegment = { isGrabbedOnLeftHandle: false, isGrabbedOnRightHandle: false, editionStatus: SegmentEditionStatus.EDITABLE, + colors: { + baseHue: 0, + baseSaturation: 0, + baseLightness: 0, + backgroundColor: '', + backgroundColorHover: '', + backgroundColorDisabled: '', + foregroundColor: '', + borderColor: '', + textColor: '', + textColorHover: '', + waveformLineSpacing: 0, + waveformGradientStart: 0, + waveformGradientEnd: 0 + }, } useTimeline.setState({ diff --git a/packages/timeline/src/hooks/useTimeScaleGraduations.ts b/packages/timeline/src/hooks/useTimeScaleGraduations.ts index 5344fe7f..c724c2fb 100644 --- a/packages/timeline/src/hooks/useTimeScaleGraduations.ts +++ b/packages/timeline/src/hooks/useTimeScaleGraduations.ts @@ -15,6 +15,7 @@ export const useTimeScaleGraduations = ({ }: { unit: number }) => { + const containerWidth = useTimeline(s => s.containerWidth) const cellWidth = useTimeline(s => s.cellWidth) const nbMaxShots = useTimeline(s => s.nbMaxShots) const contentWidth = useTimeline((s) => s.contentWidth) diff --git a/packages/timeline/src/hooks/useTimeline.ts b/packages/timeline/src/hooks/useTimeline.ts index d2ae784f..5a731b2b 100644 --- a/packages/timeline/src/hooks/useTimeline.ts +++ b/packages/timeline/src/hooks/useTimeline.ts @@ -336,68 +336,6 @@ export const useTimeline = create((set, get) => ({ } return height }, - - getSegmentColorScheme: (segment: TimelineSegment): ClapSegmentColorScheme => { - - const { theme } = get() - - let baseHue = 0 - - let baseSaturation = theme.cell.categoryColors.GENERIC.saturation - let baseLightness = theme.cell.categoryColors.GENERIC.lightness - - let backgroundColorSaturation = (segment.isSelected ? 2.2 : 1.4) * baseSaturation - let backgroundColorHoverSaturation = (segment.isSelected ? 2.2 : 1.8) * baseSaturation - - let colorScheme: ClapSegmentColorScheme = { - baseHue, - baseSaturation, - baseLightness, - - backgroundColor: hslToHex(baseHue, backgroundColorSaturation, baseLightness), - backgroundColorHover: hslToHex(baseHue, backgroundColorHoverSaturation, baseLightness + 1), - backgroundColorDisabled: hslToHex(baseHue, baseSaturation - 15, baseLightness - 2), - foregroundColor: hslToHex(baseHue, baseSaturation + 40, baseLightness), - borderColor: hslToHex(baseHue, baseSaturation + 40, baseLightness + 10), - textColor: hslToHex(baseHue, baseSaturation + 55, baseLightness - 60), - textColorHover: hslToHex(baseHue, baseSaturation + 55, baseLightness - 50), - - waveformLineSpacing: theme.cell.waveform.lineSpacing, - waveformGradientStart: theme.cell.waveform.gradientStart, - waveformGradientEnd: theme.cell.waveform.gradientEnd, - } - - if (!segment) { return colorScheme } - - const clapSegmentCategoryColors: ClapSegmentCategoryColors = theme.cell.categoryColors - - const candidateHSL = clapSegmentCategoryColors[segment.category] - if (!candidateHSL) { return colorScheme } - - baseHue = candidateHSL.hue - baseSaturation = candidateHSL.saturation - baseLightness = candidateHSL.lightness - - colorScheme = { - baseHue, - baseSaturation, - baseLightness, - - backgroundColor: hslToHex(baseHue, backgroundColorSaturation, baseLightness), - backgroundColorHover: hslToHex(baseHue, backgroundColorHoverSaturation, baseLightness + 1), - backgroundColorDisabled: hslToHex(baseHue, baseSaturation - 15, baseLightness - 2), - foregroundColor: hslToHex(baseHue, baseSaturation + 40, baseLightness), - borderColor: hslToHex(baseHue, baseSaturation + 40, baseLightness + 10), - textColor: hslToHex(baseHue, baseSaturation + 55, baseLightness - 60), - textColorHover: hslToHex(baseHue, baseSaturation + 55, baseLightness - 50), - - waveformLineSpacing: theme.cell.waveform.lineSpacing, - waveformGradientStart: theme.cell.waveform.gradientStart, - waveformGradientEnd: theme.cell.waveform.gradientEnd, - } - - return colorScheme - }, setHoveredSegment: ({ hoveredSegment, area, diff --git a/packages/timeline/src/types/timeline.ts b/packages/timeline/src/types/timeline.ts index cffff195..44f63590 100644 --- a/packages/timeline/src/types/timeline.ts +++ b/packages/timeline/src/types/timeline.ts @@ -69,6 +69,10 @@ export type BrowserOnlySegmentData = { // Cache for textures textures: Record + // pre-computed color scheme + // (I've added this cache after doing perf analysis) + colors: ClapSegmentColorScheme + /** * the following fields have been added to this type only very recently * and their implementation is not finished, @@ -264,7 +268,6 @@ export type TimelineStoreModifiers = { setVisibleSegments: (visibleSegments?: TimelineSegment[]) => void getCellHeight: (trackNumber?: number) => number getVerticalCellPosition: (start: number, end: number) => number - getSegmentColorScheme: (segment: TimelineSegment) => ClapSegmentColorScheme setHoveredSegment: (params?: { hoveredSegment?: TimelineSegment area?: SegmentArea diff --git a/packages/timeline/src/utils/clapSegmentToTimelineSegment.ts b/packages/timeline/src/utils/clapSegmentToTimelineSegment.ts index 20e9e29b..b90181fe 100644 --- a/packages/timeline/src/utils/clapSegmentToTimelineSegment.ts +++ b/packages/timeline/src/utils/clapSegmentToTimelineSegment.ts @@ -2,6 +2,7 @@ import { ClapOutputType, ClapSegment } from "@aitube/clap" import { SegmentEditionStatus, SegmentVisibility, TimelineSegment } from "@/types" import { getAudioBuffer } from "./getAudioBuffer" +import { getSegmentColorScheme } from "./getSegmentColorScheme" export async function clapSegmentToTimelineSegment(clapSegment: ClapSegment): Promise { @@ -33,6 +34,7 @@ export async function clapSegmentToTimelineSegment(clapSegment: ClapSegment): Pr if (!segment.editionStatus) { segment.editionStatus = SegmentEditionStatus.EDITABLE } + segment.colors = getSegmentColorScheme(segment) if (!segment.audioBuffer) { if (segment.outputType === ClapOutputType.AUDIO) { diff --git a/packages/timeline/src/utils/getSegmentColorScheme.ts b/packages/timeline/src/utils/getSegmentColorScheme.ts new file mode 100644 index 00000000..a3bfc663 --- /dev/null +++ b/packages/timeline/src/utils/getSegmentColorScheme.ts @@ -0,0 +1,67 @@ +import { ClapSegmentCategoryColors, ClapSegmentColorScheme } from "@/types/theme" +import { hslToHex } from "./hslToHex" +import { TimelineSegment, useTimeline } from ".." + +// TODO: optimize this to not re-compute thing that didn't change much + +export function getSegmentColorScheme(segment: TimelineSegment): ClapSegmentColorScheme { + + const { theme } = useTimeline.getState() + + let baseHue = 0 + + let baseSaturation = theme.cell.categoryColors.GENERIC.saturation + let baseLightness = theme.cell.categoryColors.GENERIC.lightness + + let backgroundColorSaturation = (segment.isSelected ? 2.2 : 1.4) * baseSaturation + let backgroundColorHoverSaturation = (segment.isSelected ? 2.2 : 1.8) * baseSaturation + + let colorScheme: ClapSegmentColorScheme = { + baseHue, + baseSaturation, + baseLightness, + + backgroundColor: hslToHex(baseHue, backgroundColorSaturation, baseLightness), + backgroundColorHover: hslToHex(baseHue, backgroundColorHoverSaturation, baseLightness + 1), + backgroundColorDisabled: hslToHex(baseHue, baseSaturation - 15, baseLightness - 2), + foregroundColor: hslToHex(baseHue, baseSaturation + 40, baseLightness), + borderColor: hslToHex(baseHue, baseSaturation + 40, baseLightness + 10), + textColor: hslToHex(baseHue, baseSaturation + 55, baseLightness - 60), + textColorHover: hslToHex(baseHue, baseSaturation + 55, baseLightness - 50), + + waveformLineSpacing: theme.cell.waveform.lineSpacing, + waveformGradientStart: theme.cell.waveform.gradientStart, + waveformGradientEnd: theme.cell.waveform.gradientEnd, + } + + if (!segment) { return colorScheme } + + const clapSegmentCategoryColors: ClapSegmentCategoryColors = theme.cell.categoryColors + + const candidateHSL = clapSegmentCategoryColors[segment.category] + if (!candidateHSL) { return colorScheme } + + baseHue = candidateHSL.hue + baseSaturation = candidateHSL.saturation + baseLightness = candidateHSL.lightness + + colorScheme = { + baseHue, + baseSaturation, + baseLightness, + + backgroundColor: hslToHex(baseHue, backgroundColorSaturation, baseLightness), + backgroundColorHover: hslToHex(baseHue, backgroundColorHoverSaturation, baseLightness + 1), + backgroundColorDisabled: hslToHex(baseHue, baseSaturation - 15, baseLightness - 2), + foregroundColor: hslToHex(baseHue, baseSaturation + 40, baseLightness), + borderColor: hslToHex(baseHue, baseSaturation + 40, baseLightness + 10), + textColor: hslToHex(baseHue, baseSaturation + 55, baseLightness - 60), + textColorHover: hslToHex(baseHue, baseSaturation + 55, baseLightness - 50), + + waveformLineSpacing: theme.cell.waveform.lineSpacing, + waveformGradientStart: theme.cell.waveform.gradientStart, + waveformGradientEnd: theme.cell.waveform.gradientEnd, + } + + return colorScheme +} \ No newline at end of file diff --git a/packages/timeline/src/utils/hslToHex.ts b/packages/timeline/src/utils/hslToHex.ts index facd8868..cb960c02 100644 --- a/packages/timeline/src/utils/hslToHex.ts +++ b/packages/timeline/src/utils/hslToHex.ts @@ -1,4 +1,41 @@ +const hue2rgb = (p: number, q: number, t: number) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; +}; + +// Cache for toHex function +const toHexCache = new Map(); +const MAX_CACHE_SIZE = 10000; // Adjust this value based on your memory constraints + +const toHex = (x: number) => { + const key = Math.round(x * 255); + if (toHexCache.has(key)) { + return toHexCache.get(key)!; + } + const hex = key.toString(16).padStart(2, '0'); + if (toHexCache.size >= MAX_CACHE_SIZE) { + // If cache is full, remove the oldest entry + const firstKey: any = toHexCache.keys().next().value; + toHexCache.delete(firstKey); + } + toHexCache.set(key, hex); + return hex; +}; + +// Cache for hslToHex function +const hslToHexCache = new Map(); +const MAX_HSL_CACHE_SIZE = 10000; // Adjust this value based on your memory constraints + export function hslToHex(h: number, s: number, l: number): string { + const key = `${h},${s},${l}`; + if (hslToHexCache.has(key)) { + return hslToHexCache.get(key)!; + } + h /= 360; s /= 100; l /= 100; @@ -6,23 +43,19 @@ export function hslToHex(h: number, s: number, l: number): string { if (s === 0) { r = g = b = l; // achromatic } else { - const hue2rgb = (p: number, q: number, t: number) => { - if (t < 0) t += 1; - if (t > 1) t -= 1; - if (t < 1 / 6) return p + (q - p) * 6 * t; - if (t < 1 / 2) return q; - if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; - return p; - }; const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; r = hue2rgb(p, q, h + 1 / 3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1 / 3); } - const toHex = (x: number) => { - const hex = Math.round(x * 255).toString(16); - return hex.length === 1 ? '0' + hex : hex; - }; - return `#${toHex(r)}${toHex(g)}${toHex(b)}`; + const result = `#${toHex(r)}${toHex(g)}${toHex(b)}`; + + if (hslToHexCache.size >= MAX_HSL_CACHE_SIZE) { + // If cache is full, remove the oldest entry + const firstKey: any = hslToHexCache.keys().next().value; + hslToHexCache.delete(firstKey); + } + hslToHexCache.set(key, result); + return result; } \ No newline at end of file diff --git a/packages/timeline/src/utils/timelineSegmentToClapSegment.ts b/packages/timeline/src/utils/timelineSegmentToClapSegment.ts index 695c4a79..a4470d85 100644 --- a/packages/timeline/src/utils/timelineSegmentToClapSegment.ts +++ b/packages/timeline/src/utils/timelineSegmentToClapSegment.ts @@ -26,5 +26,7 @@ export function timelineSegmentToClapSegment(timelineSegment: TimelineSegment): delete segment.isPlaying delete segment.editionStatus + delete segment.colors + return segment as ClapSegment } \ No newline at end of file