summaryrefslogtreecommitdiff
path: root/apps/web/src
diff options
context:
space:
mode:
authorAnwarul Islam <anwaarulislaam@gmail.com>2025-07-14 23:53:11 +0600
committerAnwarul Islam <anwaarulislaam@gmail.com>2025-07-14 23:53:11 +0600
commitc628b5a8ee6650b223cdc7aa5b6b000bc1e2466d (patch)
tree26ce2dea7f0f6e1adf22aad6eaa607e6ef8d87d4 /apps/web/src
parent1e64bc4abd8464ae733f884de46ffa13760035cc (diff)
downloadopencut-c628b5a8ee6650b223cdc7aa5b6b000bc1e2466d.tar.gz
fix improve timeline element visual display by tiling repeating media thumbnails
Diffstat (limited to 'apps/web/src')
-rw-r--r--apps/web/src/components/editor/timeline-element.tsx81
-rw-r--r--apps/web/src/components/editor/timeline.tsx2
-rw-r--r--apps/web/src/constants/timeline-constants.ts51
3 files changed, 114 insertions, 20 deletions
diff --git a/apps/web/src/components/editor/timeline-element.tsx b/apps/web/src/components/editor/timeline-element.tsx
index ee9445f3..2a704227 100644
--- a/apps/web/src/components/editor/timeline-element.tsx
+++ b/apps/web/src/components/editor/timeline-element.tsx
@@ -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>
);
}
diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx
index 6c3d731d..71e2997b 100644
--- a/apps/web/src/components/editor/timeline.tsx
+++ b/apps/web/src/components/editor/timeline.tsx
@@ -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;
diff --git a/apps/web/src/constants/timeline-constants.ts b/apps/web/src/constants/timeline-constants.ts
index 92045c5e..44773b84 100644
--- a/apps/web/src/constants/timeline-constants.ts
+++ b/apps/web/src/constants/timeline-constants.ts
@@ -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,
+ };
+}