Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions benchmark.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const count = 10000;
const media = Array.from({ length: count }, (_, i) => ({
id: `id-${i}`,
kind: 'image',
url: `url-${i}`,
fileName: `file-${i}`,
mimeType: 'image/png'
}));

const searchId = `id-${count - 1}`;

console.log(`Benchmarking with ${count} items...`);

console.time('Array.find');
for (let i = 0; i < 10000; i++) {
media.find(m => m.id === searchId);
}
console.timeEnd('Array.find');

const mediaMap = new Map(media.map(m => [m.id, m]));
console.time('Map.get');
for (let i = 0; i < 10000; i++) {
mediaMap.get(searchId);
}
console.timeEnd('Map.get');
147 changes: 74 additions & 73 deletions components/MediaPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,73 +1,74 @@
import React, { useState } from 'react';
import type { MediaItem } from '../types';

interface MediaPanelProps {
media: MediaItem[];
activeMediaId?: string;
onSelect: (id?: string) => void;
onRemove: (id: string) => void;
}

export const MediaPanel: React.FC<MediaPanelProps> = ({ media, activeMediaId, onSelect, onRemove }) => {
const [focusedId, setFocusedId] = useState<string | null>(activeMediaId || null);

const handleOpen = (id: string) => {
setFocusedId(id);
onSelect(id);
};

const handleClose = () => {
setFocusedId(null);
onSelect(undefined);
};

return (
<section className="bg-gray-800 p-4 rounded-lg shadow-lg mb-6">
<h3 className="text-white font-semibold mb-3">Media</h3>
{media.length === 0 ? (
<div className="text-gray-400">No media added yet.</div>
) : (
<div className="grid grid-cols-4 gap-3">
{media.map(item => (
<div key={item.id} className="relative bg-gray-900 rounded overflow-hidden">
{item.kind === 'video' ? (
<video src={item.url} className="w-full h-24 object-cover" muted loop={!!item.loop} playsInline />
) : (
<img src={item.url} alt={item.fileName || 'media'} className="w-full h-24 object-cover" />
)}

<div className="absolute right-1 top-1 flex gap-1">
<button onClick={() => handleOpen(item.id)} className="bg-black/50 text-white px-2 py-1 rounded text-xs">View</button>
<button onClick={() => onRemove(item.id)} className="bg-red-600 text-white px-2 py-1 rounded text-xs">Remove</button>
</div>
</div>
))}
</div>
)}

{focusedId && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70" onClick={handleClose}>
<div className="bg-black rounded-lg p-4 max-w-5xl max-h-[80vh] w-full" onClick={(e) => e.stopPropagation()}>
<div className="flex justify-between items-start mb-2">
<button onClick={handleClose} className="text-gray-300">Close</button>
</div>
<div className="flex items-center justify-center">
{(() => {
const item = media.find(m => m.id === focusedId);
if (!item) return null;
if (item.kind === 'video') {
return (
<video src={item.url} controls autoPlay muted={!!item.muted} loop={!!item.loop} className="max-h-[70vh] w-auto max-w-full" />
);
}
// images & gifs
return <img src={item.url} alt={item.fileName || 'media'} className="max-h-[70vh] w-auto max-w-full" />;
})()}
</div>
</div>
</div>
)}
</section>
);
};

import React, { useState, useMemo } from 'react';
import type { MediaItem } from '../types';

interface MediaPanelProps {
media: MediaItem[];
activeMediaId?: string;
onSelect: (id?: string) => void;
onRemove: (id: string) => void;
}

export const MediaPanel: React.FC<MediaPanelProps> = ({ media, activeMediaId, onSelect, onRemove }) => {
const [focusedId, setFocusedId] = useState<string | null>(activeMediaId || null);

const mediaMap = useMemo(() => new Map(media.map(item => [item.id, item])), [media]);

const handleOpen = (id: string) => {
setFocusedId(id);
onSelect(id);
};

const handleClose = () => {
setFocusedId(null);
onSelect(undefined);
};

return (
<section className="bg-gray-800 p-4 rounded-lg shadow-lg mb-6">
<h3 className="text-white font-semibold mb-3">Media</h3>
{media.length === 0 ? (
<div className="text-gray-400">No media added yet.</div>
) : (
<div className="grid grid-cols-4 gap-3">
{media.map(item => (
<div key={item.id} className="relative bg-gray-900 rounded overflow-hidden">
{item.kind === 'video' ? (
<video src={item.url} className="w-full h-24 object-cover" muted loop={!!item.loop} playsInline />
) : (
<img src={item.url} alt={item.fileName || 'media'} className="w-full h-24 object-cover" />
)}

<div className="absolute right-1 top-1 flex gap-1">
<button onClick={() => handleOpen(item.id)} className="bg-black/50 text-white px-2 py-1 rounded text-xs">View</button>
<button onClick={() => onRemove(item.id)} className="bg-red-600 text-white px-2 py-1 rounded text-xs">Remove</button>
</div>
</div>
))}
</div>
)}

{focusedId && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70" onClick={handleClose}>
<div className="bg-black rounded-lg p-4 max-w-5xl max-h-[80vh] w-full" onClick={(e) => e.stopPropagation()}>
<div className="flex justify-between items-start mb-2">
<button onClick={handleClose} className="text-gray-300">Close</button>
</div>
<div className="flex items-center justify-center">
{(() => {
const item = mediaMap.get(focusedId);
if (!item) return null;
if (item.kind === 'video') {
return (
<video src={item.url} controls autoPlay muted={!!item.muted} loop={!!item.loop} className="max-h-[70vh] w-auto max-w-full" />
);
}
// images & gifs
return <img src={item.url} alt={item.fileName || 'media'} className="max-h-[70vh] w-auto max-w-full" />;
})()}
</div>
</div>
</div>
)}
</section>
);
};
6 changes: 1 addition & 5 deletions hooks/useWebGPURender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,16 +370,12 @@ export function useWebGPURender(
useEffect(() => {
const device = deviceRef.current;
if (!device || !gpuReady) return;
const p = renderParamsRef.current;
console.log(`[PatternDisplay] Updating cells buffer: matrix=${matrix ? 'yes' : 'null'}, rows=${matrix?.numRows}, channels=${matrix?.numChannels}`);
if (cellsBufferRef.current) cellsBufferRef.current.destroy();
const isHighPrec = shaderFile.includes('v0.36') || shaderFile.includes('v0.37') || shaderFile.includes('v0.38') || shaderFile.includes('v0.39') || shaderFile.includes('v0.40') || shaderFile.includes('v0.42') || shaderFile.includes('v0.43') || shaderFile.includes('v0.44') || shaderFile.includes('v0.45') || shaderFile.includes('v0.46') || shaderFile.includes('v0.47') || shaderFile.includes('v0.48') || shaderFile.includes('v0.49') || shaderFile.includes('v0.50');
const packFunc = isHighPrec ? packPatternMatrixHighPrecision : packPatternMatrix;
const { packedData, noteCount } = packFunc(p.matrix, p.padTopChannel);
let noteCount = 0;
for (let i = 0; i < packedData.length; i += 2) {
const note = ((packedData[i] ?? 0) >> 24) & 0xFF;
if (note > 0) noteCount++;
}
console.log(`[PatternDisplay] Packed data contains ${noteCount} notes in ${packedData.length / 2} cells`);
cellsBufferRef.current = createBufferWithData(device, packedData, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST);
if (layoutTypeRef.current === 'extended') {
Expand Down
Loading