fix improve timeline element visual display by tiling repeating media thumbnails

This commit is contained in:
Anwarul Islam 2025-07-14 23:53:11 +06:00
parent 1e64bc4abd
commit c628b5a8ee
3 changed files with 114 additions and 20 deletions

View File

@ -24,6 +24,8 @@ import { useTimelineElementResize } from "@/hooks/use-timeline-element-resize";
import {
getTrackElementClasses,
TIMELINE_CONSTANTS,
getTrackHeight,
calculateMediaTiling,
} from "@/constants/timeline-constants";
import {
DropdownMenu,
@ -268,34 +270,75 @@ export function TimelineElement({
}
if (mediaItem.type === "image") {
// Use utility function to calculate optimal tiling
const tiling = calculateMediaTiling(elementWidth, track.type, "image");
return (
<div className="w-full h-full flex items-center justify-center">
<div className="bg-[#004D52] py-3 w-full h-full">
<img
src={mediaItem.url}
alt={mediaItem.name}
className="w-full h-full object-cover"
draggable={false}
/>
<div className="w-full h-full flex items-center justify-start overflow-hidden">
<div className="bg-[#004D52]/20 h-full flex">
{Array.from({ length: tiling.totalTiles }, (_, index) => {
const isPartialTile =
index === tiling.numCompleteTiles && tiling.showPartialTile;
const tileWidthToUse = isPartialTile
? tiling.remainingWidth
: tiling.tileWidth;
return (
<div
key={index}
className="flex-shrink-0 h-full overflow-hidden"
style={{ width: `${tileWidthToUse}px` }}
>
<img
src={mediaItem.url}
alt={mediaItem.name}
className="w-full h-full object-cover"
draggable={false}
/>
</div>
);
})}
</div>
</div>
);
}
if (mediaItem.type === "video" && mediaItem.thumbnailUrl) {
// Use utility function to calculate optimal tiling
const tiling = calculateMediaTiling(elementWidth, track.type, "video");
return (
<div className="w-full h-full flex items-center gap-2">
<div className="w-8 h-8 flex-shrink-0">
<img
src={mediaItem.thumbnailUrl}
alt={mediaItem.name}
className="w-full h-full object-cover rounded-sm"
draggable={false}
/>
<div className="w-full h-full flex items-center overflow-hidden relative">
<div className="flex h-full">
{Array.from({ length: tiling.totalTiles }, (_, index) => {
const isPartialTile =
index === tiling.numCompleteTiles && tiling.showPartialTile;
const tileWidthToUse = isPartialTile
? tiling.remainingWidth
: tiling.tileWidth;
return (
<div
key={index}
className="flex-shrink-0 h-full flex items-center justify-center p-1 overflow-hidden"
style={{ width: `${tileWidthToUse}px` }}
>
<img
src={mediaItem.thumbnailUrl}
alt={mediaItem.name}
className="w-full h-full object-cover rounded-sm"
draggable={false}
/>
</div>
);
})}
</div>
<span className="text-xs text-foreground/80 truncate flex-1">
{element.name}
</span>
{/* Show name overlay on the right if there's sufficient space */}
{tiling.canShowOverlay && (
<div className="absolute right-2 top-1/2 -translate-y-1/2 bg-black/70 text-white text-xs px-2 py-1 rounded pointer-events-none max-w-[40%] truncate">
{element.name}
</div>
)}
</div>
);
}

View File

@ -192,7 +192,7 @@ export function Timeline() {
let scrollLeft = 0;
if (isRulerClick) {
// Calculate based on ruler position
// Calculate based on ruler position
const rulerContent = rulerScrollRef.current?.querySelector(
"[data-radix-scroll-area-viewport]"
) as HTMLElement;

View File

@ -104,3 +104,54 @@ export function snapTimeToFrame(time: number, fps: number): number {
export function getFrameDuration(fps: number): number {
return 1 / fps;
}
// Media thumbnail tiling constants
export const MEDIA_THUMBNAIL_CONSTANTS = {
DEFAULT_ASPECT_RATIO: 16 / 9,
IMAGE_PADDING: 8,
VIDEO_PADDING: 16,
PARTIAL_TILE_THRESHOLD_IMAGE: 0.2, // Show partial tile if >20% visible for images
PARTIAL_TILE_THRESHOLD_VIDEO: 0.3, // Show partial tile if >30% visible for videos
MIN_OVERLAY_WIDTH_RATIO: 1.5, // Minimum width ratio to show name overlay
} as const;
// Utility function to calculate media thumbnail tiling
export function calculateMediaTiling(
elementWidth: number,
trackType: TrackType,
mediaType: "image" | "video"
) {
const trackHeight = getTrackHeight(trackType);
const aspectRatio = MEDIA_THUMBNAIL_CONSTANTS.DEFAULT_ASPECT_RATIO;
const padding =
mediaType === "image"
? MEDIA_THUMBNAIL_CONSTANTS.IMAGE_PADDING
: MEDIA_THUMBNAIL_CONSTANTS.VIDEO_PADDING;
const tileHeight = trackHeight - padding;
const tileWidth = tileHeight * aspectRatio;
// Calculate how many complete tiles we can fit
const numCompleteTiles = Math.floor(elementWidth / tileWidth);
const remainingWidth = elementWidth - numCompleteTiles * tileWidth;
const threshold =
mediaType === "image"
? MEDIA_THUMBNAIL_CONSTANTS.PARTIAL_TILE_THRESHOLD_IMAGE
: MEDIA_THUMBNAIL_CONSTANTS.PARTIAL_TILE_THRESHOLD_VIDEO;
const showPartialTile = remainingWidth > tileWidth * threshold;
const totalTiles = numCompleteTiles + (showPartialTile ? 1 : 0);
return {
tileWidth,
tileHeight,
numCompleteTiles,
remainingWidth,
showPartialTile,
totalTiles: Math.max(1, totalTiles), // Always show at least one tile
canShowOverlay:
elementWidth >
tileWidth * MEDIA_THUMBNAIL_CONSTANTS.MIN_OVERLAY_WIDTH_RATIO,
};
}