import { format } from "date-fns"; import { css, html, nothing } from "lit"; export enum TICK { MINOR = "minor", MAJOR = "major", BOUND = "bound" } export type Tick = { ms: number, percent: number, type: TICK, label: string } /** Available minute divisions */ export type MinuteDivision = 2 | 4 | 6 | 12 | 30; /** * Format data into a tick value */ export const tick = ( ms: number, duration: number, type: TICK ): Tick => ({ ms, percent: ms / duration * 100, type, label: format(ms, "m:ss") }) /** * Take a minute interval, divide it into a given number of segments and return array of `Tick` objects: * - any number of TICK.MINOR for seconds * - one last TICK.MAJOR for minute end * All ticks are returned only when they are smaller than the overall duration. */ export const processTickMinute = ( /** Milliseconds from which the minute starts */ from: number, /** Milliseconds to which the minute ends */ to: number, /** Number of divisions to which a minute shall be splitted */ count: MinuteDivision, /** The overall duration of the entire timeline */ duration: number ): Tick[] => { const ticks: Tick[] = []; let i = 1; const partial = (to - from) / count; while (i < count) { const value = from + (i * partial); if (value < duration) { ticks.push(tick( value, duration, TICK.MINOR )); } i += 1; } if (to < duration) { ticks.push(tick(to, duration, TICK.MAJOR)); } return ticks; } const minute = 60 * 1000; export const tickWidth = 50; export const tickPointerHeight = 3; export const calculateTicks = ( /** Width of the element */ width: number, /** Overall duration of the timeline in miliseconds */ duration: number ): Tick[] => { const numTicks = Math.floor(width / tickWidth); const numMinutes = Math.floor(duration / (60 * 1000)); const ticksPerMinuteRaw = numTicks / numMinutes; let ticksPerMinute: MinuteDivision = 2; if (ticksPerMinuteRaw >= 2) ticksPerMinute = 4; if (ticksPerMinuteRaw >= 6) ticksPerMinute = 6; if (ticksPerMinuteRaw >= 12) ticksPerMinute = 12; if (ticksPerMinuteRaw >= 30) ticksPerMinute = 30; const ticks: Tick[] = []; let from = 0; let to = minute; while ( from < duration ) { // Process the minute processTickMinute( from, to, ticksPerMinute, duration ).forEach( tick => ticks.push(tick) ); // Add a minute from += minute; to += minute; } ticks.push( tick(0, duration, TICK.BOUND) ); ticks.push( tick(duration, duration, TICK.BOUND) ); return ticks; } export const renderTick = (tick: Tick) => { return html`
${tick.label}
`; } export const renderPointer = ( percent: number, label: string, type: "pointer"|"primary" ) => { return html`
${label}
`; } export const renderTicks = ( duration: number, ticks: Tick[], currentMs: number, pointerMs?: number ) => { const currentPercent = currentMs / duration * 100; const pointerPercent = pointerMs !== undefined ? pointerMs / duration * 100 : undefined; return html`
${ticks.map( renderTick )} ${renderPointer( currentPercent, format(currentMs, "m:ss:SSS" ), "primary" )} ${pointerMs !== undefined && pointerPercent !== undefined ? renderPointer( pointerPercent, format( pointerMs, "m:ss:SSS" ), "pointer" ) : nothing }
`; } export const ticksCss = css` :host { --tick-color: var( --thermal-slate ); --tick-opacity: 1; --cursor-color: var( --thermal-primary ); --cursor-bg: var( --thermal-background ); --fs-sm: calc( var(--thermal-fs) * .7 ); } .indicator-cursor { position: absolute; width: 0px; right: 0; font-size: var( --fs-sm ); z-index: 11; } .indicator-cursor__primary { --cursor-bg: var( --thermal-primary ); --cursor-color: white; } .indicator-cursor__pointer { --cursor-bg: var( --thermal-foreground ); --cursor-color: white; .indicator-cursor-arrow { position: absolute; top: calc( var( --thermal-fs ) * -1 - 6px); } .indicator-cursor-label { position: absolute; top: calc( var( --thermal-fs ) * -2 - 3px ); } } .indicator-cursor-arrow { position: relative; width: 6px; height: 6px; content: ""; background: var( --cursor-bg ); left: -4px; rotate: 45deg; } .indicator-cursor-label { position: relative; top: -3px; width: ${tickWidth}px; left: -${tickWidth / 2}px; background: var( --cursor-bg ); color: var(--cursor-color); text-align: center; } .ticks { width: 100%; height: calc( var(--thermal-fs) + ${tickPointerHeight}px); position: relative; } .ticks-horizontal-indent { padding-left: ${tickWidth / 2}px; padding-right: ${tickWidth / 2}px; box-sizing: border-box; width: 100%; } .tick { position: absolute; width: 0; color: var( --tick-color ); opacity: var( --tick-opacity ); font-size: var( --fs-sm ); } .tick-bound { --tick-color: var( --thermal-foreground ); .tick-label { background: var(--thermal-slate-dark); color: var(--thermal-background); position: relative; top: -${tickPointerHeight}px; } .tick-pointer { width: ${tickPointerHeight*2}px; height: ${tickPointerHeight*2}px; background: var( --thermal-slate-dark ); position: relative; left: -${tickPointerHeight}px; rotate: 45deg; } } .tick-major { --tick-color: var( --thermal-slate-dark ); } .tick-minor { --tick-color: var( --thermal-slate ); } .tick-pointer { height: ${tickPointerHeight}px; width: 1px; content: ""; background-color: currentcolor; } .tick-label { width: ${tickWidth}px; position: relative; left: -${tickWidth / 2}px; text-align: center; color: currentcolor; } `;