הטמעת לוח שיתופי לאתר שלך
סנכרון שינויים במהלך עריכה שיתופית של לוח הוא אחד האתגרים הקשים ביותר בפיתוח אתרים. ללא מחסנית רינדור ומנגנון סנכרון מתאימים, הפרויקט שלך עלול לסבול מבאגים וביצועים ירודים. אנו בונים לוחות מקוונים לשיתוף פעולה בצוות: ציור חופשי, פתקיות דביקות, צורות, קווים וסנכרון בזמן אמת. המהנדסים שלנו מיישמים לוחות מאפס או משלבים SDK קיימים (tldraw, Excalidraw) עם סנכרון CRDT מותאם אישית. נמסרו מעל 30 פרויקטים—מלוחות הערות פשוטים ועד לעורכים שיתופיים מלאים עם WebRTC ושיחות שמע. צוותים המשתמשים בגישה שלנו חוסכים עד 40% מזמן הפיתוח בהשוואה לבנייה פנימית.
בחירת מחסנית הרינדור הנכונה
| אפשרות | זמן ל-MVP | רמת התאמה אישית | ביצועים (1000 צורות) | עלות תחזוקה |
|---|---|---|---|---|
| tldraw | 1–2 ימים | בינונית (SDK) | גבוהה (Canvas) | נמוכה (קוד פתוח) |
| Konva.js | 3–4 שבועות | מלאה | גבוהה (Canvas) | בינונית |
| Excalidraw | 1–2 ימים (פיצול) | נמוכה (הפיצול מורכב) | בינונית (SVG) | גבוהה (פיצול) |
| Fabric.js | 2–3 שבועות | בינונית | בינונית (Canvas) | גבוהה (מיושן) |
| SVG + vanilla | 1–2 שבועות | מלאה | נמוכה מעל 500 צורות | בינונית |
למוצר ייצור עם דרישות מותאמות אישית, אנו מעדיפים את tldraw כבסיס או את Konva.js מאפס. פיצולים של Excalidraw אפשריים אך עלות התחזוקה גבוהה. אנו בוחרים את המחסנית בהתאם למקרה השימוש ולתקציב שלך.
מדוע CRDT הוא אופטימלי לסנכרון
CRDT (סוג נתונים משוכפל ללא התנגשויות) פותר אוטומטית התנגשויות עריכה ללא שרת מרכזי. עבור לוחות אנו משתמשים ב-Yjs, ספריית CRDT המותאמת לעריכה שיתופית. Yjs מהיר עד פי 3 מ-OT (טרנספורמציה אופרטיבית) במבחני ביצועים עם 10 משתמשים בו-זמנית—הבדל אמיתי בעת עריכה של לוח עמוס.
ארכיטקטורה: מצב הלוח
לוח הוא אוסף של אובייקטי צורה. לכל צורה יש סוג, גיאומטריה וסגנון.
סוגי צורות נפוצים
| סוג | מאפיינים |
|---|---|
| מלבן | x, y, width, height, fill, stroke, strokeWidth, cornerRadius |
| אליפסה | x, y, radiusX, radiusY, fill, stroke |
| קו | points, stroke, strokeWidth |
| חץ | points, stroke, headStyle |
| טקסט | content, fontSize, fontFamily, color, width |
| ציור חופשי | points, stroke, strokeWidth, pressure |
| תמונה | src, x, y, width, height |
CRDT לסנכרון צורות
Y.Map מושלם לאחסון כל צורה לפי המזהה שלה:
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
const ydoc = new Y.Doc();
const yshapes = ydoc.getMap<Y.Map<unknown>>('shapes');
const provider = new WebsocketProvider(
process.env.WS_URL!,
`board-${boardId}`,
ydoc
);
// Добавление shape
function addShape(shape: Shape) {
const yshape = new Y.Map<unknown>(Object.entries(shape));
yshapes.set(shape.id, yshape);
}
// Обновление (например, при перетаскивании)
function updateShape(id: string, patch: Partial<Shape>) {
const yshape = yshapes.get(id);
if (!yshape) return;
ydoc.transact(() => {
Object.entries(patch).forEach(([key, value]) => {
yshape.set(key, value);
});
});
}
// Удаление
function deleteShape(id: string) {
yshapes.delete(id);
}
// Реактивность
yshapes.observeDeep((events) => {
events.forEach((event) => {
rerenderCanvas(yshapes);
});
});
ציור חופשי: אופטימיזציה של נקודות
ציור חופשי מייצר מאות נקודות. שליחת כולן היא יקרה. אנו משתמשים באלגוריתם Ramer-Douglas-Peucker כדי לפשט את העקומה:
function rdp(points: [number, number][], epsilon: number): [number, number][] {
if (points.length < 3) return points;
let maxDist = 0;
let maxIdx = 0;
const end = points.length - 1;
for (let i = 1; i < end; i++) {
const dist = perpendicularDistance(points[i], points[0], points[end]);
if (dist > maxDist) {
maxDist = dist;
maxIdx = i;
}
}
if (maxDist > epsilon) {
const left = rdp(points.slice(0, maxIdx + 1), epsilon);
const right = rdp(points.slice(maxIdx), epsilon);
return [...left.slice(0, -1), ...right];
}
return [points[0], points[end]];
}לאחר שהמשתמש מסיים לצייר, אנו מריצים פישוט ומסנכרנים את קבוצת הנקודות המצומצמת. לעקומות חלקות, אנו מסתמכים על ספריית import * as Y from 'yjs'; import { WebsocketProvider } from 'y-websocket'; const ydoc = new Y.Doc(); const yshapes = ydoc.getMap<Y.Map<unknown>>('shapes'); const provider = new WebsocketProvider( process.env.WS_URL!, `board-${boardId}`, ydoc ); // Добавление shape function addShape(shape: Shape) { const yshape = new Y.Map<unknown>(Object.entries(shape)); yshapes.set(shape.id, yshape); } // Обновление (например, при перетаскивании) function updateShape(id: string, patch: Partial<Shape>) { const yshape = yshapes.get(id); if (!yshape) return; ydoc.transact(() => { Object.entries(patch).forEach(([key, value]) => { yshape.set(key, value); }); }); } // Удаление function deleteShape(id: string) { yshapes.delete(id); } // Реактивность yshapes.observeDeep((events) => { events.forEach((event) => { rerenderCanvas(yshapes); }); }); .
תצוגה: פאן וזום
הלוח שלנו משתמש בקנבס אינסופי. כל הקואורדינטות הן במרחב עולמי; התצוגה מגדירה מה נראה:
interface Viewport {
x: number; // смещение
y: number;
zoom: number; // 0.1 – 4.0
}
// Мировые координаты → экранные
function worldToScreen(wx: number, wy: number, vp: Viewport) {
return {
x: wx * vp.zoom + vp.x,
y: wy * vp.zoom + vp.y,
};
}
// Экранные → мировые (для pointer events)
function screenToWorld(sx: number, sy: number, vp: Viewport) {
return {
x: (sx - vp.x) / vp.zoom,
y: (sy - vp.y) / vp.zoom,
};
}
// Zoom к точке (pinch или колёсико)
function zoomAt(vp: Viewport, screenX: number, screenY: number, delta: number): Viewport {
const factor = delta > 0 ? 1.1 : 0.9;
const newZoom = Math.max(0.1, Math.min(4.0, vp.zoom * factor));
const zoomRatio = newZoom / vp.zoom;
return {
x: screenX - (screenX - vp.x) * zoomRatio,
y: screenY - (screenY - vp.y) * zoomRatio,
zoom: newZoom,
};
} Canvas לעומת SVG: בחירה תחת עומס
מתחת ל-500 צורות, SVG עובד מצוין ומפשט את בדיקת הפגיעות. מעל 1000 צורות עם ציור פעיל, אנו עוברים ל-Canvas (2D) או ל-WebGL באמצעות Pixi.js. גישה היברידית מציגה צורות על Canvas ואלמנטים של ממשק משתמש ב-HTML מעל.
מקרה קונקרטי: פלטפורמת Ed-Tech
עבור לקוח ed-tech, בנינו לוח שיתופי המשמש מורים ותלמידים. בחרנו ב-tldraw לאב-טיפוס מהיר, ואז החלפנו את שכבת הסנכרון שלו ב-Yjs כדי להתמודד עם עד 30 משתמשים בו-זמנית. התוצאה: זמן השהיית הסנכרון ירד מ-~200 אלפיות השנייה לפחות מ-50 אלפיות השנייה, והלוח נשאר מגיב גם עם 500+ צורות. הפרויקט עבר מעיצוב לייצור תוך 10 ימים.
יישום Undo / Redo
Yjs מספק UndoManager שמקבץ פעולות בחלון של 500 אלפיות השנייה:
const undoManager = new Y.UndoManager(yshapes, {
trackedOrigins: new Set([ydoc.clientID]),
captureTimeout: 500,
});
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'z') undoManager.undo();
if (e.ctrlKey && e.key === 'y') undoManager.redo();
}); ייצוא הלוח ל-PNG
async function exportToPNG(boardId: string): Promise<Blob> {
const canvas = document.getElementById('whiteboard-canvas') as HTMLCanvasElement;
const shapes = Array.from(yshapes.values()).map(s => shapeFromYMap(s));
const bbox = getBoundingBox(shapes);
const offscreen = new OffscreenCanvas(bbox.width + 80, bbox.height + 80);
const ctx = offscreen.getContext('2d')!;
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, offscreen.width, offscreen.height);
renderShapes(ctx, shapes, { x: -bbox.x + 40, y: -bbox.y + 40, zoom: 1 });
return await offscreen.convertToBlob({ type: 'image/png' });
} תהליך העבודה שלנו
- ניתוח: הבנת תרחישי משתמש, בחירת המחסנית הנכונה.
- עיצוב: ארכיטקטורת נתונים, פרוטוקול סנכרון, פריסת ממשק משתמש.
- יישום: בניית מנוע קנבס, שילוב Yjs, יצירת כלים מותאמים אישית.
- בדיקות: בדיקות יחידה, בדיקות עומס, אימות סנכרון בין משתמשים.
- פריסה: תמונת Docker, הגדרת WebSocket, CDN לנכסים סטטיים.
הערכות לוחות זמנים
- שילוב tldraw עם סנכרון Yjs מותאם אישית: 5–7 ימים.
- לוח מלא מאפס (Konva.js + ציור חופשי, צורות, תצוגה, ביטול, נוכחות, ייצוא): 3–4 שבועות.
- הוספת WebRTC לוידאו/שמע לצד הלוח: +שבוע.
- כל הקוד מגיע עם אחריות חינם לתיקון באגים למשך חודש.
מה כלול
- תיעוד ארכיטקטוני (בחירת מחסנית, סכמת נתונים, פרוטוקול סנכרון).
- קוד מקור מלא של הלוח עם הערות ובדיקות למודולים קריטיים.
- שילוב עם מערכת האימות וה-API שלך.
- פריסה (תמונת Docker, הגדרת WebSocket, CDN).
- סדנת צוות על התאמה אישית ותחזוקה.
- אחריות חינם לתיקון באגים למשך חודש.
- מהנדסים מוסמכים עם ניסיון עמוק בשיתוף פעולה בזמן אמת.
קבל ייעוץ מהמהנדסים שלנו—נעריך את הפרויקט שלך ונציע את הפתרון האופטימלי. צור קשר כדי לדון בפרטים.







