@ngenux/ngage-whiteboarding 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -94
- package/dist/index.esm.js +78 -9
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +78 -9
- package/dist/index.js.map +1 -1
- package/dist/src/components/Whiteboard/Board.d.ts +1 -0
- package/dist/src/components/Whiteboard/Board.d.ts.map +1 -1
- package/dist/src/components/Whiteboard/Toolbar.d.ts.map +1 -1
- package/dist/src/components/Whiteboard/index.d.ts.map +1 -1
- package/dist/src/utils/video-coordinates.d.ts +36 -0
- package/dist/src/utils/video-coordinates.d.ts.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -190,100 +190,6 @@ function CollaborativeApp() {
|
|
|
190
190
|
}
|
|
191
191
|
```
|
|
192
192
|
|
|
193
|
-
### Transparent Background Configuration
|
|
194
|
-
|
|
195
|
-
If you want to enforce a transparent background and prevent users from changing it:
|
|
196
|
-
|
|
197
|
-
```tsx
|
|
198
|
-
import { WhiteboardProvider, Whiteboard } from '@ngenux/ngage-whiteboarding';
|
|
199
|
-
|
|
200
|
-
function TransparentWhiteboard() {
|
|
201
|
-
return (
|
|
202
|
-
<WhiteboardProvider webSocketUrl="ws://localhost:3001">
|
|
203
|
-
<Whiteboard
|
|
204
|
-
roomId="transparent-room"
|
|
205
|
-
userId="user-123"
|
|
206
|
-
isAdmin={true}
|
|
207
|
-
allowedUsers={['user-123']}
|
|
208
|
-
transparentBackground={true} // Forces transparent & locks background
|
|
209
|
-
/>
|
|
210
|
-
</WhiteboardProvider>
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
**Behavior:**
|
|
216
|
-
- When `transparentBackground={true}`:
|
|
217
|
-
- ✅ Background is automatically set to transparent
|
|
218
|
-
- 🔒 Background color picker is disabled and shows "Transparent background locked"
|
|
219
|
-
- Perfect for overlaying the whiteboard on existing content
|
|
220
|
-
|
|
221
|
-
- When `transparentBackground={false}` (default):
|
|
222
|
-
- ✅ Background defaults to white
|
|
223
|
-
- 🎨 Users can change background color freely
|
|
224
|
-
|
|
225
|
-
### Video Background for Annotation
|
|
226
|
-
|
|
227
|
-
Annotate over live video streams (screen-share, webcam, etc.):
|
|
228
|
-
|
|
229
|
-
```tsx
|
|
230
|
-
import { WhiteboardProvider, Whiteboard } from '@ngenux/ngage-whiteboarding';
|
|
231
|
-
import { useEffect, useState } from 'react';
|
|
232
|
-
|
|
233
|
-
function VideoAnnotationWhiteboard() {
|
|
234
|
-
const [videoStream, setVideoStream] = useState<MediaStream | undefined>();
|
|
235
|
-
|
|
236
|
-
// Get screen share or webcam stream
|
|
237
|
-
useEffect(() => {
|
|
238
|
-
async function getStream() {
|
|
239
|
-
try {
|
|
240
|
-
// Screen share example
|
|
241
|
-
const stream = await navigator.mediaDevices.getDisplayMedia({
|
|
242
|
-
video: { width: 1920, height: 1080 }
|
|
243
|
-
});
|
|
244
|
-
setVideoStream(stream);
|
|
245
|
-
} catch (err) {
|
|
246
|
-
console.error('Failed to get video stream:', err);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
getStream();
|
|
250
|
-
|
|
251
|
-
return () => {
|
|
252
|
-
// Cleanup: stop all tracks when component unmounts
|
|
253
|
-
if (videoStream) {
|
|
254
|
-
videoStream.getTracks().forEach(track => track.stop());
|
|
255
|
-
}
|
|
256
|
-
};
|
|
257
|
-
}, []);
|
|
258
|
-
|
|
259
|
-
return (
|
|
260
|
-
<WhiteboardProvider webSocketUrl="ws://localhost:3001">
|
|
261
|
-
<Whiteboard
|
|
262
|
-
roomId="video-annotation-room"
|
|
263
|
-
userId="user-123"
|
|
264
|
-
isAdmin={true}
|
|
265
|
-
allowedUsers={['user-123']}
|
|
266
|
-
videoStream={videoStream} // Video renders as background layer
|
|
267
|
-
/>
|
|
268
|
-
</WhiteboardProvider>
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
**Features:**
|
|
274
|
-
- 🎥 **Video as background**: Stream renders behind whiteboard canvas
|
|
275
|
-
- 🖊️ **Draw over video**: All drawing tools work normally over the video
|
|
276
|
-
- 🔒 **Auto-transparent**: Background automatically set to transparent
|
|
277
|
-
- ⚡ **60fps performance**: HTML video layer, no canvas copying
|
|
278
|
-
- 📐 **Auto-scaling**: Video maintains aspect ratio with letterbox
|
|
279
|
-
- 🎯 **Pointer-events handled**: Video doesn't interfere with drawing
|
|
280
|
-
|
|
281
|
-
**Use cases:**
|
|
282
|
-
- Screen-share annotation for remote presentations
|
|
283
|
-
- Video call annotation
|
|
284
|
-
- Educational content markup
|
|
285
|
-
- Live streaming annotation
|
|
286
|
-
|
|
287
193
|
## 📝 API Reference
|
|
288
194
|
|
|
289
195
|
### WhiteboardProvider Props
|
package/dist/index.esm.js
CHANGED
|
@@ -36710,9 +36710,27 @@ const BoardComponent = forwardRef(({ roomId = 'default-room', queueAction, hasTo
|
|
|
36710
36710
|
const activeShapes = Object.values(state.activeDrawings);
|
|
36711
36711
|
return activeShapes.map(shape => (jsx(MemoizedShapeComponent, { shape: shape }, `active-${shape.id}`)));
|
|
36712
36712
|
}, [state.activeDrawings]);
|
|
36713
|
+
const getStageDataURL = useCallback((format = 'png') => {
|
|
36714
|
+
if (!stageRef.current) {
|
|
36715
|
+
return '';
|
|
36716
|
+
}
|
|
36717
|
+
try {
|
|
36718
|
+
const stage = stageRef.current;
|
|
36719
|
+
return stage.toDataURL({
|
|
36720
|
+
mimeType: format === 'png' ? 'image/png' : 'image/jpeg',
|
|
36721
|
+
quality: 1,
|
|
36722
|
+
pixelRatio: 2,
|
|
36723
|
+
});
|
|
36724
|
+
}
|
|
36725
|
+
catch (error) {
|
|
36726
|
+
console.error('Failed to get stage data URL:', error);
|
|
36727
|
+
return '';
|
|
36728
|
+
}
|
|
36729
|
+
}, []);
|
|
36713
36730
|
useImperativeHandle(ref, () => ({
|
|
36714
36731
|
exportAsImage,
|
|
36715
36732
|
exportAsPDF,
|
|
36733
|
+
getStageDataURL,
|
|
36716
36734
|
}));
|
|
36717
36735
|
// Memoized cursor style for performance
|
|
36718
36736
|
const cursorStyle = useMemo(() => ({
|
|
@@ -37244,16 +37262,20 @@ const LeftSidebar = ({ queueAction, hasToolAccess = false, shouldBeOpenByDefault
|
|
|
37244
37262
|
width: '250px',
|
|
37245
37263
|
maxHeight: '80vh',
|
|
37246
37264
|
cursor: isDragging ? 'grabbing' : 'grab'
|
|
37247
|
-
}, onMouseDown: handleMouseDown, children: [jsxs("div", { className: "flex items-center justify-between p-2 border-b border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 rounded-t-lg", children: [jsxs("div", { className: "flex items-center gap-2", children: [jsx("span", { className: "text-sm font-medium text-gray-700 dark:text-gray-200", children: "Style Panel" }), !hasToolAccess && (jsx("span", { className: "text-xs bg-orange-100 dark:bg-orange-900 text-orange-600 dark:text-orange-300 px-2 py-1 rounded", children: "Locked" }))] }), jsx("button", { className: "close-button p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded", onClick: handleManualClose, onMouseDown: (e) => e.stopPropagation(), title: "Close Panel", children: jsx(ChevronLeft, { size: 14, className: "text-gray-600 dark:text-gray-300" }) })] }), jsxs("div", { className: "sidebar-content p-3 overflow-y-auto", style: { maxHeight: 'calc(80vh - 40px)' }, children: [!hasToolAccess && (jsxs("div", { className: "mb-4 p-3 bg-orange-50 dark:bg-orange-900/30 border border-orange-200 dark:border-orange-800 rounded-lg text-center", children: [jsx("div", { className: "text-orange-700 dark:text-orange-300 text-sm font-medium mb-1", children: "\uD83D\uDD12 Tools Locked" }), jsx("div", { className: "text-orange-600 dark:text-orange-400 text-xs", children: "Contact admin for access" })] })), jsxs("div", { className: "mb-4", children: [jsx("div", { className: "text-xs font-medium text-gray-700 dark:text-gray-200 mb-2", children: "Stroke Color" }), jsx("div", { className: "grid grid-cols-6 gap-1", children: strokeColors.map((color) => (jsx("button", { className: `w-6 h-6 rounded
|
|
37248
|
-
? 'opacity-50 cursor-not-allowed border-gray-200 dark:border-gray-700'
|
|
37265
|
+
}, onMouseDown: handleMouseDown, children: [jsxs("div", { className: "flex items-center justify-between p-2 border-b border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 rounded-t-lg", children: [jsxs("div", { className: "flex items-center gap-2", children: [jsx("span", { className: "text-sm font-medium text-gray-700 dark:text-gray-200", children: "Style Panel" }), !hasToolAccess && (jsx("span", { className: "text-xs bg-orange-100 dark:bg-orange-900 text-orange-600 dark:text-orange-300 px-2 py-1 rounded", children: "Locked" }))] }), jsx("button", { className: "close-button p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded", onClick: handleManualClose, onMouseDown: (e) => e.stopPropagation(), title: "Close Panel", children: jsx(ChevronLeft, { size: 14, className: "text-gray-600 dark:text-gray-300" }) })] }), jsxs("div", { className: "sidebar-content p-3 overflow-y-auto", style: { maxHeight: 'calc(80vh - 40px)' }, children: [!hasToolAccess && (jsxs("div", { className: "mb-4 p-3 bg-orange-50 dark:bg-orange-900/30 border border-orange-200 dark:border-orange-800 rounded-lg text-center", children: [jsx("div", { className: "text-orange-700 dark:text-orange-300 text-sm font-medium mb-1", children: "\uD83D\uDD12 Tools Locked" }), jsx("div", { className: "text-orange-600 dark:text-orange-400 text-xs", children: "Contact admin for access" })] })), jsxs("div", { className: "mb-4", children: [jsx("div", { className: "text-xs font-medium text-gray-700 dark:text-gray-200 mb-2", children: "Stroke Color" }), jsx("div", { className: "grid grid-cols-6 gap-1", children: strokeColors.map((color) => (jsx("button", { className: `w-6 h-6 rounded transition-all relative ${!hasToolAccess
|
|
37266
|
+
? 'opacity-50 cursor-not-allowed border-2 border-gray-200 dark:border-gray-700'
|
|
37249
37267
|
: state.color === color
|
|
37250
|
-
? 'border-
|
|
37268
|
+
? 'border-[3px] border-yellow-400 dark:border-yellow-300 scale-125 shadow-lg'
|
|
37251
37269
|
: color === '#FFFFFF'
|
|
37252
|
-
? 'border-gray-
|
|
37253
|
-
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'}`, style: {
|
|
37270
|
+
? 'border-2 border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500'
|
|
37271
|
+
: 'border-2 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'}`, style: {
|
|
37254
37272
|
backgroundColor: color,
|
|
37255
|
-
boxShadow: color === '#FFFFFF'
|
|
37256
|
-
|
|
37273
|
+
boxShadow: color === '#FFFFFF'
|
|
37274
|
+
? 'inset 0 0 0 1px rgba(0,0,0,0.15)'
|
|
37275
|
+
: state.color === color
|
|
37276
|
+
? '0 4px 12px rgba(234, 179, 8, 0.5), 0 0 0 2px rgba(234, 179, 8, 0.3)'
|
|
37277
|
+
: 'none'
|
|
37278
|
+
}, onClick: () => handleColorChange(color), disabled: !hasToolAccess, title: hasToolAccess ? (color === '#FFFFFF' ? 'White' : color) : 'Access restricted', children: state.color === color && (jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: color === '#FFFFFF' || color === '#EA580C' || color === '#DC2626' ? '#000000' : '#FFFFFF', strokeWidth: "3", strokeLinecap: "round", strokeLinejoin: "round", children: jsx("polyline", { points: "20 6 9 17 4 12" }) }) })) }, color))) })] }), jsxs("div", { className: "mb-4", children: [jsx("div", { className: "text-xs font-medium text-gray-700 dark:text-gray-200 mb-2", children: "Background" }), disableBackgroundChange && (jsx("div", { className: "p-3 bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 rounded-lg text-center", children: jsx("div", { className: "text-blue-700 dark:text-blue-300 text-xs", title: "Disabled when video background is active", children: "\uD83D\uDD12 Background selection locked" }) })), !disableBackgroundChange && (jsx("div", { className: "grid grid-cols-6 gap-1", children: backgroundColors.map((color) => (jsx("button", { className: `w-6 h-6 rounded border-2 transition-all relative ${!hasToolAccess
|
|
37257
37279
|
? 'opacity-50 cursor-not-allowed border-gray-200 dark:border-gray-700'
|
|
37258
37280
|
: state.backgroundColor === color
|
|
37259
37281
|
? 'border-blue-400 dark:border-blue-500 scale-110'
|
|
@@ -42435,10 +42457,57 @@ const Whiteboard = ({ roomId, isAdmin = false, allowedUsers = [], userId, transp
|
|
|
42435
42457
|
}, [state.activeDrawings, state.lastClearTimestamp, dispatch]);
|
|
42436
42458
|
// Export functions to pass to toolbar
|
|
42437
42459
|
const handleExportImage = useCallback((format) => {
|
|
42438
|
-
if (boardRef.current) {
|
|
42460
|
+
if (!boardRef.current) {
|
|
42461
|
+
return;
|
|
42462
|
+
}
|
|
42463
|
+
// If video is active, capture both video and canvas
|
|
42464
|
+
if (videoStream && videoRef.current) {
|
|
42465
|
+
const video = videoRef.current;
|
|
42466
|
+
const canvas = document.createElement('canvas');
|
|
42467
|
+
const ctx = canvas.getContext('2d');
|
|
42468
|
+
if (!ctx)
|
|
42469
|
+
return;
|
|
42470
|
+
// Set canvas size to match the whiteboard
|
|
42471
|
+
canvas.width = video.videoWidth || video.offsetWidth;
|
|
42472
|
+
canvas.height = video.videoHeight || video.offsetHeight;
|
|
42473
|
+
try {
|
|
42474
|
+
// Draw the video frame
|
|
42475
|
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
42476
|
+
// Get the Konva stage data URL
|
|
42477
|
+
const stageDataURL = boardRef.current.getStageDataURL(format);
|
|
42478
|
+
// Create an image from the stage
|
|
42479
|
+
const stageImage = new Image();
|
|
42480
|
+
stageImage.onload = () => {
|
|
42481
|
+
// Draw the annotations on top of the video
|
|
42482
|
+
ctx.drawImage(stageImage, 0, 0, canvas.width, canvas.height);
|
|
42483
|
+
// Convert to blob and download
|
|
42484
|
+
canvas.toBlob((blob) => {
|
|
42485
|
+
if (blob) {
|
|
42486
|
+
const timestamp = new Date().toISOString().slice(0, 16).replace('T', '_').replace(/:/g, '-');
|
|
42487
|
+
const filename = `whiteboard_video_annotation_${timestamp}.${format}`;
|
|
42488
|
+
const link = document.createElement('a');
|
|
42489
|
+
link.download = filename;
|
|
42490
|
+
link.href = URL.createObjectURL(blob);
|
|
42491
|
+
document.body.appendChild(link);
|
|
42492
|
+
link.click();
|
|
42493
|
+
document.body.removeChild(link);
|
|
42494
|
+
URL.revokeObjectURL(link.href);
|
|
42495
|
+
}
|
|
42496
|
+
}, format === 'png' ? 'image/png' : 'image/jpeg', 1.0);
|
|
42497
|
+
};
|
|
42498
|
+
stageImage.src = stageDataURL;
|
|
42499
|
+
}
|
|
42500
|
+
catch (error) {
|
|
42501
|
+
console.error('Failed to export video with annotations:', error);
|
|
42502
|
+
// Fallback to regular export
|
|
42503
|
+
boardRef.current.exportAsImage(format);
|
|
42504
|
+
}
|
|
42505
|
+
}
|
|
42506
|
+
else {
|
|
42507
|
+
// No video - regular export
|
|
42439
42508
|
boardRef.current.exportAsImage(format);
|
|
42440
42509
|
}
|
|
42441
|
-
}, []);
|
|
42510
|
+
}, [videoStream]);
|
|
42442
42511
|
const handleExportPDF = useCallback(() => {
|
|
42443
42512
|
if (boardRef.current) {
|
|
42444
42513
|
boardRef.current.exportAsPDF();
|