Sfoglia il codice sorgente

Reworked event streaming

master
Peter Millauer 3 giorni fa
parent
commit
d2de2452a8

+ 1
- 2
src/client/main.ts Vedi File

@@ -3,5 +3,4 @@ import { ClientDrawService } from './services/draw/draw.client-service'
3 3
 
4 4
 const stateService = new ClientStateService()
5 5
 const drawService = new ClientDrawService(stateService)
6
-drawService.draw()
7
-stateService.connect()
6
+stateService.connect(drawService)

+ 1
- 0
src/client/model/canvas-state.ts Vedi File

@@ -5,6 +5,7 @@ export type CanvasState = {
5 5
 export type Stroke = {
6 6
     points: Point[],
7 7
     color: string,
8
+    density: number,
8 9
     width: number
9 10
 }
10 11
 

+ 3
- 0
src/client/model/rpc-callbacks.ts Vedi File

@@ -0,0 +1,3 @@
1
+import { Stroke } from "./canvas-state";
2
+
3
+export type ListenCallbackParam = { strokeId: number, stroke: Stroke }

+ 124
- 0
src/client/services/draw/cat-mul-rom.util.ts Vedi File

@@ -0,0 +1,124 @@
1
+import { Point } from "../../model/canvas-state";
2
+
3
+
4
+/**
5
+ * Returns a point on a Catmull-Rom spline segment at parameter t
6
+ */
7
+export function catmullRomPoint(
8
+    p0: Point,
9
+    p1: Point,
10
+    p2: Point,
11
+    p3: Point,
12
+    t: number
13
+): Point {
14
+    const t2 = t * t;
15
+    const t3 = t2 * t;
16
+
17
+    const x =
18
+        0.5 *
19
+        (2 * p1.x +
20
+            (-p0.x + p2.x) * t +
21
+            (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 +
22
+            (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3);
23
+
24
+    const y =
25
+        0.5 *
26
+        (2 * p1.y +
27
+            (-p0.y + p2.y) * t +
28
+            (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 +
29
+            (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3);
30
+
31
+    return { x, y };
32
+}
33
+
34
+/**
35
+ * Converts a list of raw input points into a smooth Catmull-Rom spline
36
+ */
37
+export function getCatmullRomPath(
38
+    points: Point[],
39
+    density: number,
40
+): Point[] {
41
+    const segmentsPerInterval = density;
42
+
43
+    if (points.length === 0) return [];
44
+    if (points.length === 1) return [{ ...points[0] }];
45
+
46
+    const smoothed: Point[] = [];
47
+    const n = points.length;
48
+
49
+    for (let i = 0; i < n - 1; i++) {
50
+        const p0 = points[Math.max(0, i - 1)];
51
+        const p1 = points[i];
52
+        const p2 = points[i + 1];
53
+        const p3 = points[Math.min(n - 1, i + 2)];
54
+
55
+        const step = 1 / segmentsPerInterval;
56
+
57
+        for (let s = 0; s <= segmentsPerInterval; s++) {
58
+            const t = s * step;
59
+            const pt = catmullRomPoint(p0, p1, p2, p3, t);
60
+            smoothed.push(pt);
61
+        }
62
+    }
63
+
64
+    // Remove near-duplicates at segment boundaries
65
+    const result = smoothed.filter((pt, idx, arr) => {
66
+        if (idx === 0) return true;
67
+        const prev = arr[idx - 1];
68
+        return Math.hypot(pt.x - prev.x, pt.y - prev.y) > 0.001;
69
+    });
70
+
71
+    return result;
72
+}
73
+
74
+export function getBrushWidthAt(index: number, totalPoints: number, baseWidth: number = 12): number {
75
+    // Example: taper at start and end + speed-based variation
76
+    const t = index / (totalPoints - 1);
77
+    let width = baseWidth;
78
+
79
+    // Ease in / ease out
80
+    if (t < 0.1) width *= t * 10;
81
+    if (t > 0.9) width *= (1 - t) * 10;
82
+
83
+    return Math.max(1, width);
84
+}
85
+
86
+export function drawVariableWidthSegment(
87
+    ctx: CanvasRenderingContext2D,
88
+    p1: Point,
89
+    p2: Point,
90
+    width1: number,
91
+    width2: number
92
+): void {
93
+    const dx = p2.x - p1.x;
94
+    const dy = p2.y - p1.y;
95
+    const len = Math.hypot(dx, dy); // More efficient than sqrt(dx*dx + dy*dy)
96
+
97
+    if (len < 0.001) return; // Points are too close
98
+
99
+    // Normalized perpendicular vector (rotated 90 degrees)
100
+    const nx = -dy / len;
101
+    const ny = dx / len;
102
+
103
+    const halfW1 = width1 / 2;
104
+    const halfW2 = width2 / 2;
105
+
106
+    // Four corners of the quadrilateral
107
+    const x1 = p1.x + nx * halfW1;
108
+    const y1 = p1.y + ny * halfW1;
109
+    const x2 = p1.x - nx * halfW1;
110
+    const y2 = p1.y - ny * halfW1;
111
+    const x3 = p2.x - nx * halfW2;
112
+    const y3 = p2.y - ny * halfW2;
113
+    const x4 = p2.x + nx * halfW2;
114
+    const y4 = p2.y + ny * halfW2;
115
+
116
+    ctx.beginPath();
117
+    ctx.moveTo(x1, y1);
118
+    ctx.lineTo(x2, y2);
119
+    ctx.lineTo(x3, y3);
120
+    ctx.lineTo(x4, y4);
121
+    ctx.closePath();
122
+
123
+    ctx.fill();
124
+}

+ 39
- 146
src/client/services/draw/draw.client-service.ts Vedi File

@@ -1,23 +1,30 @@
1 1
 import { Point, Stroke } from "../../model/canvas-state";
2 2
 import { ClientStateService } from "../state/state.client-service";
3
-
3
+import { getBrushWidthAt, drawVariableWidthSegment, getCatmullRomPath } from "./cat-mul-rom.util";
4 4
 
5 5
 
6 6
 export class ClientDrawService {
7
-    private readonly canvasContext: CanvasRenderingContext2D
7
+    private readonly cctx: CanvasRenderingContext2D
8 8
 
9 9
     private currentColor
10 10
 
11
-    private currentWidth = 12;
11
+    private currentDensity
12
+
13
+    private currentWidth
12 14
 
13
-    private currentStroke?: number
15
+    private currentStrokeId?: number
14 16
 
15 17
     constructor(
16 18
         private readonly stateService: ClientStateService,
17 19
         private readonly canvas: HTMLCanvasElement = document.getElementById("canvas") as HTMLCanvasElement,
18 20
         private readonly colorPicker = document.getElementById('colorPicker')!,
21
+
22
+        private readonly densitySlider = document.getElementById('densitySlider')!,
23
+        private readonly densityValue = document.getElementById('densityValue')!,
24
+
19 25
         private readonly widthSlider = document.getElementById('widthSlider')!,
20
-        private readonly widthValue = document.getElementById('widthValue')!,
26
+        private readonly widthValue = document.getElementById('widthValue')!, 
27
+
21 28
         private readonly menuButton = document.getElementById('menuButton')!,
22 29
         private readonly drawer = document.getElementById('drawer')!,
23 30
         private readonly closeButton = document.getElementById('closeButton')!,
@@ -25,33 +32,33 @@ export class ClientDrawService {
25 32
         this.resizeCanvas()
26 33
         this.setupCanvasEvents()
27 34
         window.addEventListener("resize", () => this.resizeCanvas());
28
-        this.canvasContext = canvas.getContext("2d")!
29
-        this.currentColor = colorPicker?.getAttribute('value') ?? "#eaafff"
35
+        this.cctx = canvas.getContext("2d")!
36
+        this.currentColor = colorPicker!.getAttribute('value')!
37
+        this.currentDensity = Number(densitySlider!.getAttribute('value'))!
38
+        this.currentWidth = Number(widthSlider!.getAttribute('value'))!
30 39
     }
31 40
 
32 41
     draw() {
33
-        const strokes: Stroke[] = this.stateService.getStrokes()
34 42
 
35
-        this.canvasContext.globalAlpha = 0.05;
36
-        this.canvasContext.fillStyle = "white";
37
-        this.canvasContext.clearRect(0, 0, this.canvas.width, this.canvas.height);
38
-        this.canvasContext.globalAlpha = 1;
43
+            console.log("drawing")
44
+        const strokes: Stroke[] = this.stateService.getStrokes()
39 45
 
46
+        this.cctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
47
+        
48
+        this.cctx.globalAlpha = 1;
40 49
         strokes.forEach(stroke => {
41
-            this.canvasContext.fillStyle = stroke.color;
42
-            this.drawCurve(stroke.points, stroke.width)
50
+            this.cctx.fillStyle = stroke.color;
51
+            this.drawCurve(stroke.points, stroke.density, stroke.width)
43 52
         })
44
-
45
-        requestAnimationFrame(() => this.draw())
46 53
     }
47 54
 
48 55
 
49 56
     private addPointToStroke = (e: any) => {
50
-        if (this.currentStroke === undefined) {
57
+        if (this.currentStrokeId === undefined) {
51 58
             return
52 59
         }
53 60
         const rect = this.canvas.getBoundingClientRect();
54
-        this.stateService.addPoint(this.currentStroke, {
61
+        this.stateService.addPoint(this.currentStrokeId, {
55 62
             x: e.clientX - rect.left,
56 63
             y: e.clientY - rect.top,
57 64
         })
@@ -59,10 +66,10 @@ export class ClientDrawService {
59 66
 
60 67
     private beginStroke = (e: any) => {
61 68
         const rect = this.canvas.getBoundingClientRect();
62
-
63 69
         this.stateService.beginStroke({
64 70
             color: this.currentColor,
65 71
             width: this.currentWidth,
72
+            density: this.currentDensity,
66 73
             points: [{
67 74
                 x: e.clientX - rect.left,
68 75
                 y: e.clientY - rect.top
@@ -70,7 +77,7 @@ export class ClientDrawService {
70 77
             ,
71 78
         }).then((id: number) => {
72 79
             console.log("begin stroke", id)
73
-            this.currentStroke = id;
80
+            this.currentStrokeId = id;
74 81
             this.canvas.addEventListener("mousemove", this.addPointToStroke)
75 82
         })
76 83
     }
@@ -99,6 +106,12 @@ export class ClientDrawService {
99 106
             this.currentWidth = Number(target.value)
100 107
         });
101 108
 
109
+        this.densitySlider?.addEventListener('input', (e) => {
110
+            const target = e.target as HTMLTextAreaElement
111
+            this.densityValue!.innerHTML = target.value
112
+            this.currentDensity = Number(target.value)
113
+        });
114
+
102 115
         this.menuButton.addEventListener('click', () => {
103 116
             this.drawer.classList.toggle('open');
104 117
         });
@@ -125,138 +138,18 @@ export class ClientDrawService {
125 138
         this.canvas.height = size;
126 139
     }
127 140
 
128
-    private drawCurve(points: Point[], density: number) {
141
+    private drawCurve(points: Point[], density: number, width: number) {
129 142
         if (points.length < 2) return;
130 143
 
144
+        points = getCatmullRomPath(points, density)
145
+
131 146
         for (let i = 0; i < points.length - 1; i++) {
132
-            const width1 = getBrushWidthAt(i, points.length);
133
-            const width2 = getBrushWidthAt(i + 1, points.length);
147
+            const width1 = getBrushWidthAt(i, points.length, width);
148
+            const width2 = getBrushWidthAt(i + 1, points.length, width);
134 149
             const p1 = points[i];
135 150
             const p2 = points[i + 1];
136
-            drawVariableWidthSegment(this.canvasContext, p1, p2, width1, width2);
151
+            drawVariableWidthSegment(this.cctx, p1, p2, width1, width2);
137 152
 
138 153
         }
139 154
     }
140
-}
141
-
142
-/**
143
- * Returns a point on a Catmull-Rom spline segment at parameter t
144
- */
145
-export function catmullRomPoint(
146
-    p0: Point,
147
-    p1: Point,
148
-    p2: Point,
149
-    p3: Point,
150
-    t: number
151
-): Point {
152
-    const t2 = t * t;
153
-    const t3 = t2 * t;
154
-
155
-    const x =
156
-        0.5 *
157
-        (2 * p1.x +
158
-            (-p0.x + p2.x) * t +
159
-            (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 +
160
-            (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3);
161
-
162
-    const y =
163
-        0.5 *
164
-        (2 * p1.y +
165
-            (-p0.y + p2.y) * t +
166
-            (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 +
167
-            (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3);
168
-
169
-    return { x, y };
170
-}
171
-
172
-/**
173
- * Converts a list of raw input points into a smooth Catmull-Rom spline
174
- */
175
-export function getCatmullRomPath(
176
-    points: Point[],
177
-    density: number,
178
-): Point[] {
179
-    const segmentsPerInterval = density;
180
-
181
-    if (points.length === 0) return [];
182
-    if (points.length === 1) return [{ ...points[0] }];
183
-
184
-    const smoothed: Point[] = [];
185
-    const n = points.length;
186
-
187
-    for (let i = 0; i < n - 1; i++) {
188
-        const p0 = points[Math.max(0, i - 1)];
189
-        const p1 = points[i];
190
-        const p2 = points[i + 1];
191
-        const p3 = points[Math.min(n - 1, i + 2)];
192
-
193
-        const step = 1 / segmentsPerInterval;
194
-
195
-        for (let s = 0; s <= segmentsPerInterval; s++) {
196
-            const t = s * step;
197
-            const pt = catmullRomPoint(p0, p1, p2, p3, t);
198
-            smoothed.push(pt);
199
-        }
200
-    }
201
-
202
-    // Remove near-duplicates at segment boundaries
203
-    const result = smoothed.filter((pt, idx, arr) => {
204
-        if (idx === 0) return true;
205
-        const prev = arr[idx - 1];
206
-        return Math.hypot(pt.x - prev.x, pt.y - prev.y) > 0.001;
207
-    });
208
-
209
-    return result;
210
-}
211
-
212
-function getBrushWidthAt(index: number, totalPoints: number, baseWidth: number = 12): number {
213
-    // Example: taper at start and end + speed-based variation
214
-    const t = index / (totalPoints - 1);
215
-    let width = baseWidth;
216
-
217
-    // Ease in / ease out
218
-    if (t < 0.1) width *= t * 10;
219
-    if (t > 0.9) width *= (1 - t) * 10;
220
-
221
-    return Math.max(1, width);
222
-}
223
-
224
-export function drawVariableWidthSegment(
225
-    ctx: CanvasRenderingContext2D,
226
-    p1: Point,
227
-    p2: Point,
228
-    width1: number,
229
-    width2: number
230
-): void {
231
-    const dx = p2.x - p1.x;
232
-    const dy = p2.y - p1.y;
233
-    const len = Math.hypot(dx, dy); // More efficient than sqrt(dx*dx + dy*dy)
234
-
235
-    if (len < 0.001) return; // Points are too close
236
-
237
-    // Normalized perpendicular vector (rotated 90 degrees)
238
-    const nx = -dy / len;
239
-    const ny = dx / len;
240
-
241
-    const halfW1 = width1 / 2;
242
-    const halfW2 = width2 / 2;
243
-
244
-    // Four corners of the quadrilateral
245
-    const x1 = p1.x + nx * halfW1;
246
-    const y1 = p1.y + ny * halfW1;
247
-    const x2 = p1.x - nx * halfW1;
248
-    const y2 = p1.y - ny * halfW1;
249
-    const x3 = p2.x - nx * halfW2;
250
-    const y3 = p2.y - ny * halfW2;
251
-    const x4 = p2.x + nx * halfW2;
252
-    const y4 = p2.y + ny * halfW2;
253
-
254
-    ctx.beginPath();
255
-    ctx.moveTo(x1, y1);
256
-    ctx.lineTo(x2, y2);
257
-    ctx.lineTo(x3, y3);
258
-    ctx.lineTo(x4, y4);
259
-    ctx.closePath();
260
-
261
-    ctx.fill();
262 155
 }

+ 24
- 9
src/client/services/state/state.client-service.ts Vedi File

@@ -1,30 +1,45 @@
1 1
 import { RPCSocket } from "../../../../node_modules/rpclibrary/js/Index";
2 2
 import { CanvasState, Point, Stroke } from "../../model/canvas-state";
3
+import { ListenCallbackParam } from "../../model/rpc-callbacks";
4
+import { ClientDrawService } from "../draw/draw.client-service";
3 5
 
4 6
 
5 7
 export class ClientStateService {
6 8
 
7 9
     private remoteService: any
8
-    private canvasState: CanvasState = { strokes: [] }
9 10
 
10
-    getStrokes() {
11
-        return this.canvasState?.strokes ?? []
12
-    }
11
+    private canvasState: CanvasState = { strokes: [] }
13 12
 
14
-    async connect() {
13
+    async connect(drawService: ClientDrawService) {
15 14
         const sock = await new RPCSocket(8080, 'localhost').connect();
16 15
         this.remoteService = sock['StateService']
17
-        this.canvasState = await this.remoteService.listen((state: CanvasState) => {
18
-            this.canvasState = state      
16
+        this.canvasState = await this.getState()
17
+        await this.remoteService.listen((listenDto: ListenCallbackParam) => {
18
+            if(!this.canvasState.strokes[listenDto.strokeId]){
19
+                this.canvasState.strokes[listenDto.strokeId] = listenDto.stroke
20
+            }else{
21
+                this.canvasState.strokes[listenDto.strokeId].points = [...this.canvasState.strokes[listenDto.strokeId].points, ...listenDto.stroke.points]
22
+            }
23
+            drawService.draw()
19 24
         })
25
+        drawService.draw()
26
+    }
27
+
28
+    getState = async () => {
29
+        return await this.remoteService.getState()
20 30
     }
21 31
 
22
-    
23 32
     beginStroke = async (stroke: Stroke) => {
24 33
         return await this.remoteService.beginStroke(stroke)
25 34
     }
26 35
 
27
-    addPoint = async (strokeId: number, point: Point) =>{
36
+    addPoint = async (strokeId: number, point: Point) => {
28 37
         return this.remoteService.addPoint(strokeId, point)
29 38
     }
39
+
40
+    getStrokes() {
41
+        return this.canvasState?.strokes ?? []
42
+    }
43
+
44
+
30 45
 }

+ 19
- 8
src/public/index.html Vedi File

@@ -1,5 +1,6 @@
1 1
 <!DOCTYPE html>
2 2
 <html lang="en">
3
+
3 4
 <head>
4 5
   <meta charset="UTF-8" />
5 6
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -55,7 +56,8 @@
55 56
     .drawer {
56 57
       position: absolute;
57 58
       top: 0;
58
-      right: -500px;           /* Fully hidden when closed */
59
+      right: -500px;
60
+      /* Fully hidden when closed */
59 61
       width: 300px;
60 62
       height: 100vh;
61 63
       background: white;
@@ -137,10 +139,11 @@
137 139
     }
138 140
   </style>
139 141
 </head>
142
+
140 143
 <body>
141 144
   <!-- Canvas Area -->
142 145
   <div class="canvas-container">
143
-    <canvas id="canvas" width="1200" height="800"></canvas>
146
+    <canvas id="canvas" width="450" height="450"></canvas>
144 147
   </div>
145 148
 
146 149
   <!-- Burger Menu Button -->
@@ -152,21 +155,29 @@
152 155
       <h2>Brush Settings</h2>
153 156
       <button class="close-button" id="closeButton">×</button>
154 157
     </div>
155
-    
158
+
156 159
     <!-- Color Picker -->
157 160
     <div class="control">
158 161
       <label for="colorPicker">Color</label>
159
-      <input type="color" id="colorPicker" value="#000000">
162
+      <input type="color" id="colorPicker" value="#eaafff">
160 163
     </div>
161 164
 
162
-    <!-- Brush Width -->
165
+    <!-- density -->
163 166
     <div class="control">
164
-      <label for="widthSlider">Brush Size</label>
165
-      <input type="range" id="widthSlider" min="1" max="100" value="12">
166
-      <span id="widthValue" class="value">12 px</span>
167
+      <label for="densitySlider">Smoothness</label>
168
+      <input type="range" id="densitySlider" min="1" max="50" value="35">
169
+      <span id="densityValue" class="value">35</span>
170
+    </div>
171
+
172
+    <!-- width -->
173
+    <div class="control">
174
+      <label for="widthSlider">Width</label>
175
+      <input type="range" id="widthSlider" min="1" max="50" value="35">
176
+      <span id="widthValue" class="value">35</span>
167 177
     </div>
168 178
   </div>
169 179
 
170 180
   <script type="module" src="main.js"></script>
171 181
 </body>
182
+
172 183
 </html>

+ 12
- 9
src/server/services/state/state.service.ts Vedi File

@@ -1,13 +1,14 @@
1 1
 import { Singleton, Initializable } from "depents";
2 2
 import { RPCExporter } from "rpclibrary";
3 3
 import { CanvasState, Point, Stroke } from "../../../client/model/canvas-state";
4
+import { ListenCallbackParam } from "../../../client/model/rpc-callbacks";
4 5
 
5 6
 @Singleton()
6 7
 export class StateService implements Initializable, RPCExporter {
7 8
     name = 'StateService' as const
8 9
 
9 10
     private canvasState: CanvasState = { strokes: [] }
10
-    private clients: Array<(state: any) => void> = []
11
+    private clients: Array<(param: ListenCallbackParam) => void> = []
11 12
 
12 13
     initialize() {
13 14
         this.canvasState = { strokes: [] }
@@ -15,34 +16,36 @@ export class StateService implements Initializable, RPCExporter {
15 16
     };
16 17
 
17 18
     beginStroke = async (stroke: Stroke) => {
18
-        const strokeId = this.canvasState.strokes.length 
19
+        const strokeId = this.canvasState.strokes.length
19 20
         this.canvasState.strokes.push(stroke)
20
-        console.log("beginStroke", strokeId, stroke)
21
-        this.updateclients()
21
+        this.updateclients(strokeId, stroke)
22 22
         return strokeId
23 23
     }
24 24
 
25 25
     addPoint = async (strokeId: number, point: Point) => {
26
-        this.canvasState.strokes.length
27 26
         this.canvasState.strokes[strokeId].points.push(point)
28
-        this.updateclients()
27
+        this.updateclients(strokeId, { ...this.canvasState.strokes[strokeId], points: [point] })
29 28
     }
30 29
 
31 30
     listen = async (callback: (state: any) => Promise<void>) => {
32
-        await callback(this.canvasState)
33 31
         this.clients = [...this.clients, callback]
34 32
         return this.canvasState
35 33
     }
36 34
 
37
-    private updateclients = () => {
35
+    getState = async (): Promise<CanvasState> => {
36
+        return this.canvasState
37
+    }
38
+
39
+    private updateclients = (strokeId: number, stroke: Stroke) => {
38 40
         this.clients.forEach((client) => {
39
-            client(this.canvasState)
41
+            client({ strokeId, stroke })
40 42
         })
41 43
     }
42 44
 
43 45
     RPCs = [
44 46
         this.beginStroke,
45 47
         this.addPoint,
48
+        this.getState,
46 49
         {
47 50
             name: 'listen' as const,
48 51
             hook: (cb: any) => { this.listen(cb) }

Loading…
Annulla
Salva