PeacebinfLow commited on
Commit
d4c9e1a
·
verified ·
1 Parent(s): a895a86

Update assets/js/ui/flow_view.js

Browse files
Files changed (1) hide show
  1. assets/js/ui/flow_view.js +55 -232
assets/js/ui/flow_view.js CHANGED
@@ -3,264 +3,91 @@ export class FlowView {
3
  this.canvas = canvas;
4
  this.g = canvas.getContext("2d");
5
  this.ctx = ctx;
 
6
 
7
- // camera / viewport (world -> screen)
8
- this.view = {
9
- x: 0,
10
- y: 0,
11
- scale: 1
12
- };
13
-
14
- // input state
15
- this.drag = { active:false, sx:0, sy:0, vx:0, vy:0 };
16
  this.pulses = [];
17
  this.lastSeenLedgerId = 0;
18
 
19
- // animation loop
20
- this.running = true;
21
- this.lastFrame = performance.now();
22
- this.heartbeatAcc = 0;
23
-
24
- // interactions
25
- this.bindInteractions();
26
- this.resetView();
27
-
28
- requestAnimationFrame((t)=>this.loop(t));
29
  }
30
 
31
- resetView(){
32
- // put router near center of the screen in world coords
33
- this.view.scale = 1;
34
- this.view.x = this.canvas.width * 0.5;
35
- this.view.y = this.canvas.height * 0.5;
36
- }
37
-
38
- bindInteractions(){
39
- // Mouse drag to pan
40
- this.canvas.addEventListener("mousedown", (e)=>{
41
- this.drag.active = true;
42
- this.drag.sx = e.clientX;
43
- this.drag.sy = e.clientY;
44
- this.drag.vx = this.view.x;
45
- this.drag.vy = this.view.y;
46
- });
47
-
48
- window.addEventListener("mouseup", ()=>{
49
- this.drag.active = false;
50
- });
51
-
52
- window.addEventListener("mousemove", (e)=>{
53
- if (!this.drag.active) return;
54
- const dx = e.clientX - this.drag.sx;
55
- const dy = e.clientY - this.drag.sy;
56
- this.view.x = this.drag.vx + dx;
57
- this.view.y = this.drag.vy + dy;
58
- });
59
-
60
- // Wheel zoom (zoom around cursor)
61
- this.canvas.addEventListener("wheel", (e)=>{
62
- e.preventDefault();
63
- const { offsetX, offsetY } = e;
64
-
65
- const zoom = Math.exp(-e.deltaY * 0.0015);
66
- const oldScale = this.view.scale;
67
- const newScale = clamp(oldScale * zoom, 0.6, 2.4);
68
-
69
- // keep cursor position stable in world coords
70
- const wx = (offsetX - this.view.x) / oldScale;
71
- const wy = (offsetY - this.view.y) / oldScale;
72
-
73
- this.view.scale = newScale;
74
- this.view.x = offsetX - wx * newScale;
75
- this.view.y = offsetY - wy * newScale;
76
- }, { passive:false });
77
-
78
- // Touch pan (single finger)
79
- this.canvas.addEventListener("touchstart", (e)=>{
80
- if (e.touches.length !== 1) return;
81
- const t = e.touches[0];
82
- this.drag.active = true;
83
- this.drag.sx = t.clientX;
84
- this.drag.sy = t.clientY;
85
- this.drag.vx = this.view.x;
86
- this.drag.vy = this.view.y;
87
- }, { passive:true });
88
 
89
- this.canvas.addEventListener("touchmove", (e)=>{
90
- if (!this.drag.active || e.touches.length !== 1) return;
91
- const t = e.touches[0];
92
- const dx = t.clientX - this.drag.sx;
93
- const dy = t.clientY - this.drag.sy;
94
- this.view.x = this.drag.vx + dx;
95
- this.view.y = this.drag.vy + dy;
96
- }, { passive:true });
97
-
98
- this.canvas.addEventListener("touchend", ()=>{
99
- this.drag.active = false;
100
- });
101
-
102
- // click info (optional)
103
- this.canvas.addEventListener("click", (e)=>{
104
- const p = this.screenToWorld(e.offsetX, e.offsetY);
105
- const info = `world @ ${p.x.toFixed(0)},${p.y.toFixed(0)} • zoom ${(this.view.scale*100).toFixed(0)}%`;
106
- const out = document.getElementById("flowInfo");
107
- if (out) out.textContent = info;
108
- });
109
  }
110
 
111
- loop(t){
112
- const dt = Math.min(0.05, (t - this.lastFrame) / 1000);
113
- this.lastFrame = t;
 
114
 
115
- this.step(dt);
116
- this.draw();
 
 
 
 
117
 
118
- if (this.running) requestAnimationFrame((nt)=>this.loop(nt));
119
- }
120
-
121
- step(dt){
122
- const { ledger } = this.ctx;
123
 
124
- // spawn pulses on new ledger entries (continuous response)
125
- const last = ledger.entries[ledger.entries.length - 1];
126
  if (last && last.id > this.lastSeenLedgerId) {
127
- // if multiple entries arrived since last frame, play them all (tight + correct)
128
- for (let i = this.lastSeenLedgerId; i < last.id; i++){
129
- const entry = ledger.entries[i]; // id starts at 1, array is 0-index
130
- if (!entry) continue;
131
- this.spawnPulse(entry.type, entry.payload);
132
- }
133
  this.lastSeenLedgerId = last.id;
 
134
  }
135
 
136
- // heartbeat: if inbox has stuff, emit a subtle pulse every ~1.2s
137
- this.heartbeatAcc += dt;
138
- if (this.heartbeatAcc > 1.2) {
139
- this.heartbeatAcc = 0;
140
- const inboxCount = this.ctx.state.inbox?.length || 0;
141
- if (inboxCount > 0) {
142
- this.pulses.push({ path:["Inbox","Router"], t:0, ttl:110, r:4 });
143
- }
144
- }
145
-
146
- // advance pulses
147
- this.pulses.forEach(p => p.t += dt * 60);
148
- this.pulses = this.pulses.filter(p => p.t < p.ttl);
149
- }
150
-
151
- spawnPulse(type, payload){
152
- if (type === "email.received") {
153
- this.pulses.push({ path:["External","Inbox"], t:0, ttl:90, r:6 });
154
- return;
155
- }
156
- if (type === "email.tagged") {
157
- this.pulses.push({ path:["Inbox","Router"], t:0, ttl:90, r:6 });
158
- return;
159
- }
160
- if (type === "email.routed") {
161
- const dest = payload?.routeTo || "Support";
162
- const safe = ["Support","Sales","Finance","Engineering"].includes(dest) ? dest : "Support";
163
- this.pulses.push({ path:["Router", safe], t:0, ttl:90, r:6 });
164
- return;
165
- }
166
- if (type === "external.signal") {
167
- this.pulses.push({ path:["External","Router"], t:0, ttl:90, r:6 });
168
- return;
169
- }
170
- if (type === "hunt.started") {
171
- this.pulses.push({ path:["Hunt","Router"], t:0, ttl:90, r:6 });
172
- return;
173
- }
174
- if (type === "hunt.match") {
175
- this.pulses.push({ path:["Router","Hunt"], t:0, ttl:90, r:6 });
176
- return;
177
- }
178
- if (type === "hunt.done") {
179
- this.pulses.push({ path:["Hunt","Router"], t:0, ttl:90, r:6 });
180
- return;
181
- }
182
- }
183
-
184
- draw(){
185
- const g = this.g;
186
- const w = this.canvas.width, h = this.canvas.height;
187
- g.clearRect(0,0,w,h);
188
-
189
- // apply camera transform
190
- g.save();
191
- g.translate(this.view.x, this.view.y);
192
- g.scale(this.view.scale, this.view.scale);
193
-
194
- // world layout (router centered at 0,0)
195
- const nodes = defaultNodes();
196
-
197
- drawGrid(g, w, h, this.view);
198
- drawEdges(g, nodes);
199
- drawNodes(g, nodes);
200
  drawPulses(g, nodes, this.pulses);
201
 
202
- g.restore();
 
203
  }
204
 
205
- screenToWorld(sx, sy){
206
- return {
207
- x: (sx - this.view.x) / this.view.scale,
208
- y: (sy - this.view.y) / this.view.scale
 
 
 
 
 
 
209
  };
 
 
210
  }
211
  }
212
 
213
- function defaultNodes(){
 
214
  return {
215
- External: { x: -320, y: -170 },
216
- Inbox: { x: -210, y: 0 },
217
- Router: { x: 0, y: 0 },
218
- Support: { x: 320, y: -120 },
219
- Sales: { x: 320, y: -20 },
220
- Finance: { x: 320, y: 80 },
221
- Engineering:{ x: 320, y: 180 },
222
- Hunt: { x: -320, y: 180 }
223
  };
224
  }
225
 
226
- function drawGrid(g, w, h, view){
227
- // draw grid in world space
228
- g.save();
229
- g.globalAlpha = 0.18;
230
- g.strokeStyle = "#4a6a8f";
231
- g.lineWidth = 1 / view.scale;
232
-
233
- const step = 30;
234
- const minX = (-view.x) / view.scale - w;
235
- const maxX = (-view.x) / view.scale + w;
236
- const minY = (-view.y) / view.scale - h;
237
- const maxY = (-view.y) / view.scale + h;
238
-
239
- const startX = Math.floor(minX/step)*step;
240
- const endX = Math.ceil(maxX/step)*step;
241
- const startY = Math.floor(minY/step)*step;
242
- const endY = Math.ceil(maxY/step)*step;
243
-
244
- for (let x = startX; x <= endX; x += step){
245
- g.beginPath();
246
- g.moveTo(x, startY);
247
- g.lineTo(x, endY);
248
- g.stroke();
249
- }
250
- for (let y = startY; y <= endY; y += step){
251
- g.beginPath();
252
- g.moveTo(startX, y);
253
- g.lineTo(endX, y);
254
- g.stroke();
255
- }
256
- g.restore();
257
- }
258
-
259
  function drawNodes(g, nodes){
260
  for (const [name,n] of Object.entries(nodes)){
261
  g.fillStyle = "#0b0f14";
262
  g.strokeStyle = "#ffd24a";
263
- g.lineWidth = 1.4;
264
  g.beginPath();
265
  g.rect(n.x-55, n.y-18, 110, 36);
266
  g.fill();
@@ -282,7 +109,7 @@ function drawEdges(g, nodes){
282
  ["Router","Finance"],
283
  ["Router","Engineering"],
284
  ["Router","Hunt"],
285
- ["Hunt","Router"]
286
  ];
287
  g.strokeStyle = "rgba(143,176,214,.35)";
288
  g.lineWidth = 2;
@@ -299,17 +126,13 @@ function drawPulses(g, nodes, pulses){
299
  pulses.forEach(p=>{
300
  const [a,b] = p.path;
301
  const A = nodes[a], B = nodes[b];
302
- if (!A || !B) return;
303
-
304
  const t = p.t / p.ttl;
305
  const x = A.x + (B.x - A.x) * t;
306
  const y = A.y + (B.y - A.y) * t;
307
 
308
  g.fillStyle = "rgba(77,255,136,.85)";
309
  g.beginPath();
310
- g.arc(x,y,p.r || 6,0,Math.PI*2);
311
  g.fill();
312
  });
313
  }
314
-
315
- function clamp(v, a, b){ return Math.max(a, Math.min(b, v)); }
 
3
  this.canvas = canvas;
4
  this.g = canvas.getContext("2d");
5
  this.ctx = ctx;
6
+ this.scene = null;
7
 
 
 
 
 
 
 
 
 
 
8
  this.pulses = [];
9
  this.lastSeenLedgerId = 0;
10
 
11
+ canvas.addEventListener("click", (e)=> this.onClick(e));
 
 
 
 
 
 
 
 
 
12
  }
13
 
14
+ setScenario(scene){ this.scene = scene; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ onClick(e){
17
+ const r = this.canvas.getBoundingClientRect();
18
+ const x = (e.clientX - r.left) * (this.canvas.width / r.width);
19
+ const y = (e.clientY - r.top) * (this.canvas.height / r.height);
20
+ // v1: no selection logic yet; we’ll add node hit-test in next iteration
21
+ document.getElementById("flowInfo").textContent = `click @ ${x.toFixed(0)},${y.toFixed(0)}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  }
23
 
24
+ render(){
25
+ const { g } = this;
26
+ const { ledger } = this.ctx;
27
+ g.clearRect(0,0,this.canvas.width,this.canvas.height);
28
 
29
+ // retro grid
30
+ g.globalAlpha = 0.15;
31
+ g.strokeStyle = "#4a6a8f";
32
+ for (let i=0;i<this.canvas.width;i+=30){ g.beginPath(); g.moveTo(i,0); g.lineTo(i,this.canvas.height); g.stroke(); }
33
+ for (let j=0;j<this.canvas.height;j+=30){ g.beginPath(); g.moveTo(0,j); g.lineTo(this.canvas.width,j); g.stroke(); }
34
+ g.globalAlpha = 1;
35
 
36
+ const nodes = defaultNodes(this.canvas.width, this.canvas.height);
37
+ drawEdges(g, nodes);
38
+ drawNodes(g, nodes);
 
 
39
 
40
+ // detect new ledger events -> spawn pulses
41
+ const last = ledger.entries[ledger.entries.length-1];
42
  if (last && last.id > this.lastSeenLedgerId) {
 
 
 
 
 
 
43
  this.lastSeenLedgerId = last.id;
44
+ this.spawnPulse(nodes, last.type);
45
  }
46
 
47
+ // animate pulses
48
+ this.pulses.forEach(p=> p.t += 1);
49
+ this.pulses = this.pulses.filter(p=> p.t < p.ttl);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  drawPulses(g, nodes, this.pulses);
51
 
52
+ // request next frame if pulses exist
53
+ if (this.pulses.length > 0) requestAnimationFrame(()=> this.render());
54
  }
55
 
56
+ spawnPulse(nodes, type){
57
+ // simple routing map (v1). We’ll evolve this with actual route events later.
58
+ const map = {
59
+ "email.received": ["External", "Inbox"],
60
+ "email.tagged": ["Inbox", "Router"],
61
+ "email.routed": ["Router", "Support"],
62
+ "hunt.started": ["Hunt", "Router"],
63
+ "hunt.match": ["Router", "Hunt"],
64
+ "hunt.done": ["Hunt", "Router"],
65
+ "external.signal": ["External", "Router"],
66
  };
67
+ const path = map[type] || ["Inbox","Router"];
68
+ this.pulses.push({ path, t:0, ttl:90 });
69
  }
70
  }
71
 
72
+ function defaultNodes(w,h){
73
+ const cx = w/2, cy = h/2;
74
  return {
75
+ External: { x: 110, y: 90 },
76
+ Inbox: { x: 220, y: 260 },
77
+ Router: { x: cx, y: cy },
78
+ Support: { x: w-160, y: 140 },
79
+ Sales: { x: w-160, y: 240 },
80
+ Finance: { x: w-160, y: 340 },
81
+ Engineering:{ x: w-160, y: 440 },
82
+ Hunt: { x: 110, y: 440 },
83
  };
84
  }
85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  function drawNodes(g, nodes){
87
  for (const [name,n] of Object.entries(nodes)){
88
  g.fillStyle = "#0b0f14";
89
  g.strokeStyle = "#ffd24a";
90
+ g.lineWidth = 1;
91
  g.beginPath();
92
  g.rect(n.x-55, n.y-18, 110, 36);
93
  g.fill();
 
109
  ["Router","Finance"],
110
  ["Router","Engineering"],
111
  ["Router","Hunt"],
112
+ ["Hunt","Router"],
113
  ];
114
  g.strokeStyle = "rgba(143,176,214,.35)";
115
  g.lineWidth = 2;
 
126
  pulses.forEach(p=>{
127
  const [a,b] = p.path;
128
  const A = nodes[a], B = nodes[b];
 
 
129
  const t = p.t / p.ttl;
130
  const x = A.x + (B.x - A.x) * t;
131
  const y = A.y + (B.y - A.y) * t;
132
 
133
  g.fillStyle = "rgba(77,255,136,.85)";
134
  g.beginPath();
135
+ g.arc(x,y,6,0,Math.PI*2);
136
  g.fill();
137
  });
138
  }