@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 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 border-2 transition-all ${!hasToolAccess
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-purple-400 dark:border-purple-500 scale-110'
37268
+ ? 'border-[3px] border-yellow-400 dark:border-yellow-300 scale-125 shadow-lg'
37251
37269
  : color === '#FFFFFF'
37252
- ? 'border-gray-400 dark:border-gray-500 hover:border-gray-500 dark:hover:border-gray-400'
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' ? 'inset 0 0 0 1px rgba(0,0,0,0.1)' : 'none'
37256
- }, onClick: () => handleColorChange(color), disabled: !hasToolAccess, title: hasToolAccess ? (color === '#FFFFFF' ? 'White' : color) : 'Access restricted' }, 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
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();