PageSourceSearch

https://thomasahle.com/blog/uni.js

js thomasahle.com collected 2026-10-01 11:58:11 UTC 13,471 bytes, 373 lines download raw bytes

1// FIXME: These shouldn't be in the global space
2margin = {top: 10, right: 10, bottom: 0, left: 10};
3left_width = 300;
4right_width = 400;
5width = left_width+right_width;
6height = 400;
7sim_center = [left_width + right_width/2, height/2];
8
9class SimulationPlot {
10   mount(svg, step_button, restart_button, colors) {
11      this.svg = svg;
12      this.defs = svg.append('defs');
13      this.step_button = step_button;
14      this.restart_button = restart_button;
15      svg.attr('width', width).attr('height', height);
16      this.box = svg.append("g");
17      this.sim_g = svg.append('g')
18         .attr('x', left_width)
19         .attr('y', margin.top)
20         .attr('width', right_width)
21         .attr('height', height);
22      this.n = 200;
23      this.colors = colors;
24      if (colors == 2) {
25         this.wq = 50;
26         this.wu = 50;
27         this.w1 = 25;
28         this.t = .843;
29         this.D = 3;
30         this.initial_reps = 30;
31      } else {
32         this.w = 50;
33         this.t = .81071;
34         this.D = 2;
35         this.initial_reps = 15;
36      }
37      this.init_buttons();
38   }
39   restart() {
40      if (this.colors == 2) {
41         const set1_colors = d3.scaleSequential(d3.interpolateBlues).domain([-3, 2]);
42         const set2_colors = d3.scaleSequential(d3.interpolateOranges).domain([-3, 4]);
43         const uni_colors = d3.scaleSequential(d3.interpolateGreys).domain([-1, 5]);
44         this.data = Array.from({ length: this.n }, (_, i) => {
45            const set1_item = Math.random() < this.wq/this.n;
46            const set2_item = set1_item
47               ? Math.random() < this.w1/this.wq
48               : Math.random() < (this.wu-this.w1)/(this.n-this.wq);
49            const lum = Math.random();
50            const grey = uni_colors(lum);
51            return {
52              r: 2 * (4 + Math.random()**2)
53              ,color1: set1_item ? set1_colors(lum) : grey
54              ,color2: set2_item ? set2_colors(lum) : grey
55              ,is_set1_item: set1_item
56              ,is_set2_item: set2_item
57              ,id: Math.random()
58            }});
59      } else {
60         const set_colors = d3.scaleSequential(d3.interpolateBlues).domain([-3, 2]);
61         const uni_colors = d3.scaleSequential(d3.interpolateGreys).domain([-1, 5]);
62         this.data = Array.from({ length: this.n }, (_, i) => {
63            const set_item = Math.random() < this.w/this.n;
64            const color = set_item ? set_colors(Math.random()) : uni_colors(Math.random());
65            return {
66              r: 2 * (4 + Math.random()**2)
67              ,color1: color
68              ,color2: color
69              ,is_set1_item: set_item
70              ,is_set2_item: set_item
71              ,id: Math.random()
72            }});
73      }
74      this.update_uni();
75
76      this.clone_step = false;
77      this.step_button
78            .attr('style', 'visibility:show')
79            .html('<i class="material-icons">skip_next</i>'
80                  + (this.clone_step ? 'Multiply' : 'Mutate'));
81      this.reps = [];
82      for (let i = 0; i < this.initial_reps; i++) {
83         this.reps.push({
84            objs: [],
85            x: sim_center[0],
86            y: sim_center[1],
87            ox: sim_center[0],
88            oy: sim_center[1],
89            r: 15,
90            id: Math.random(),
91            val1: 0,
92            val2: 0,
93            alive: true
94         });
95      }
96      this.update_sim();
97   }
98   init_buttons() {
99      this.restart_button.on('click', () => {
100         // Remove alive class, so the reps don't shake
101         this.sim_g.selectAll('.alive').classed('alive', false);
102         this.restart();
103      });
104      let rep_sim = null;
105      this.step_button.on('click', () => {
106         for (const rep of this.reps)
107            if (rep.sim)
108               rep.sim.stop();
109         const new_reps = [];
110         if (this.clone_step) {
111            for (const rep of this.reps) {
112               if (!rep.alive)
113                  continue;
114               for (let i = 0; i < this.D; i++) {
115                  const new_rep = {
116                     objs: [],
117                     x: rep.x,
118                     y: rep.y,
119                     ox: rep.x,
120                     oy: rep.y,
121                     id: Math.random(),
122                     r: rep.r,
123                     val1: rep.val1,
124                     val2: rep.val2,
125                     alive: true
126                  };
127                  for (const old_obj of rep.objs) {
128                     const obj = {...old_obj};
129                     obj.ox = obj.x;
130                     obj.oy = obj.y;
131                     new_rep.objs.push(obj);
132                  }
133                  new_reps.push(new_rep);
134               }
135            }
136         }
137         else {
138            for (const rep of this.reps) {
139               if (!rep.alive)
140                  continue;
141               const par = this.data[Math.floor(Math.random()*this.n)];
142               rep.objs.push({
143                  r: par.r,
144                  x: par.x - rep.x,
145                  y: par.y - rep.y,
146                  ox: par.x - rep.x,
147                  oy: par.y - rep.y,
148                  id: Math.random(),
149                  color1: par.color1,
150                  color2: par.color2,
151                  pid: par.id, // Used for the gradient
152               });
153               rep.ox = rep.x;
154               rep.oy = rep.y;
155               rep.r = 10 + 10 * Math.sqrt(rep.objs.length);
156               rep.val1 = rep.val1 + (par.is_set1_item ? 1 : 0);
157               rep.val2 = rep.val2 + (par.is_set2_item ? 1 : 0);
158               rep.alive = Math.abs(rep.val1 - this.t*rep.objs.length)
159                              <= Math.pow(this.t*(1-this.t)*rep.objs.length, 1/3)
160                           && Math.abs(rep.val2 - this.t*rep.objs.length)
161                              <= Math.pow(this.t*(1-this.t)*rep.objs.length, 1/3);
162               new_reps.push(rep);
163            }
164         }
165         this.reps = new_reps;
166         this.clone_step = !this.clone_step;
167         if (!this.reps.some(d => d.alive))
168            this.step_button.attr('style', 'visibility:hidden');
169         else
170            this.step_button.html(
171                  '<i class="material-icons">skip_next</i>'
172                  + (this.clone_step ? 'Multiply' : 'Mutate'));
173         this.update_sim();
174      });
175   }
176   update_sim() {
177      const rep_nodes = this.sim_g
178         .selectAll('g')
179         .data(this.reps, d => d.id)
180         .join(
181            enter => enter
182               .append('g')
183               .attr('class', 'rep')
184               .attr('transform', d => `translate(${d.x}, ${d.y})`)
185               .call(gs => gs
186                  .append('circle')
187                  .attr('class', 'rep_circle')
188                  .attr('r', 0)
189                  .attr('opacity', 0))
190            ,
191            update => update,
192            exit => {
193               // Mark nodes for dying, so they are no longer force animated
194               exit.classed('dying', true);
195               const alives = exit.filter('.alive');
196               const not_alives = exit.filter(':not(.alive)');
197
198               // Remove dead individuals
199               not_alives
200                  .selectAll('circle')
201                  .attr('opacity', 1)
202                  .transition()
203                  .duration(1000)
204                  .attr('r', 0)
205                  .attr('opacity', 0)
206                  .attr('cx', 0)
207                  .attr('cy', 0);
208               not_alives
209                  .transition().delay(1000)
210                  .remove();
211
212               // Remove "alive" individuals that are branching
213               const shake = 15;
214               let shake_transition = alives;
215               for (let i = 0; i < 5; i++)
216                  shake_transition = shake_transition
217                     .transition().duration(100)
218                     .attr('transform', d => `translate(
219                        ${d.x + shake*(Math.random()-.5)},
220                        ${d.y + shake*(Math.random()-.5)})`);
221               shake_transition
222                  .transition().delay(500)
223                  .remove();
224
225               exit
226                  .filter('.alive')
227                  .selectAll('circle')
228                  .attr('opacity', 1)
229                  .transition().delay(400).duration(600)
230                  .attr('opacity', 0)
231                  .attr('r', 0)
232                  .attr('cx', 0)
233                  .attr('cy', 0);
234            }
235         )
236         .classed('alive', d => d.alive)
237      ;
238
239      // Update circle size
240      rep_nodes.selectAll('.rep_circle')
241         .transition()
242         .duration(1000)
243         .attr('r', d => d.r)
244         .attr('opacity', 1)
245      ;
246
247      const circles = rep_nodes
248         .selectAll('circle.obj')
249         .data(d => d.objs, d => d.id)
250         .join(
251            enter => {
252               const circles = enter
253                  .append('circle')
254                  .attr('class', 'obj')
255                  //.attr('fill', d => d.color)
256                  .attr('fill', d => d.color1 != d.color2 ? `url(#g${d.pid})` : d.color1)
257                  //.attr('r', d => d.r)
258               // If the objects arrived by cloning, we are now no longer
259               // in the clone step.
260               if (!this.clone_step)
261                  circles
262                     .attr('r', 0)
263                     .attr('opacity', 0)
264                     .transition()
265                     .duration(1000)
266                     .attr('r', d => d.r)
267                     .attr('opacity', 1)
268               else
269                  circles
270                     .attr('stroke', '#183752')
271                     .attr('stroke-width', '1px')
272                     .attr('r', d => 1.5*d.r)
273                     .transition()
274                     .duration(1000)
275                     .attr('stroke', 'transparent')
276                     .attr('r', d => d.r)
277               return circles;
278            }
279         )
280
281      rep_nodes
282         .each(outer_d => {
283               outer_d.sim = d3.forceSimulation(outer_d.objs)
284                .on("tick", function() {
285                   let a = this.alpha();
286                   a = Math.exp(-(Math.log(a)**4));
287                   circles
288                      .attr('cx', d => d.x * (1-a) + d.ox * a)
289                      .attr('cy', d => d.y * (1-a) + d.oy * a);
290                })
291                .force("collide", d3.forceCollide().radius(d => 1 + d.r))
292                .force("x", d3.forceX(0).strength(1e-1))
293                .force("y", d3.forceY(0).strength(1e-1))
294            }
295          )
296
297      if (this.rep_sim) this.rep_sim.stop();
298      let sim_g = this.sim_g;
299      this.rep_sim = d3.forceSimulation(this.reps)
300          .on("tick", function() {
301             let a = this.alpha();
302             if (!this.clone_step)
303                a = Math.exp(-(Math.log(a)**4)/4);
304             // We filter out dying reps, since they are busy shaking
305             sim_g.selectAll('g:not(.dying)')
306                .attr('transform', function(d) {
307                let x = d.x * (1-a) + d.ox * a;
308                let y = d.y * (1-a) + d.oy * a;
309                return `translate(${x}, ${y})`;
310             });
311          })
312          .force("collide", d3.forceCollide().radius(d => 5 + d.r))
313          .force("x", d3.forceX(sim_center[0]).strength(5e-2))
314          .force("y", d3.forceY(sim_center[1]).strength(5e-2));
315
316      if (this.reps.length == 0)
317         this.restart();
318   }
319   update_uni() {
320      const grads = this.data.filter(d => d.color1 != d.color2);
321      this.defs.selectAll('linearGradient')
322         .data(grads, d => d.id)
323         .join('linearGradient',
324            enter => enter.append('linearGradient'),
325            update => update,
326            // Wait a moment before removing the gradients, so we still have
327            // them as the old rep are exiting
328            exit => exit.delay(2000).remove()
329         )
330         .attr('id', d => `g${d.id}`)
331         .attr('x1', 0) .attr('y1', 0)
332         .attr('x2', 1) .attr('y2', 1)
333         .call(lg => lg.append('stop')
334            .attr('offset', '.5').attr('style', d=>`stop-color:${d.color1}`))
335         .call(lg => lg.append('stop')
336            .attr('offset', '.5').attr('style', d=>`stop-color:${d.color2}`))
337         ;
338      const nodes = this.box.selectAll('circle')
339         .data(this.data, d => d.id)
340         .join('circle')
341         .attr('r', d => d.r)
342         .attr('fill', d => d.color1 != d.color2 ? `url(#g${d.id})` : d.color1)
343         ;
344
345      function tick(nodes) {
346          nodes.attr("cx", d => d.x).attr("cy", d => d.y);
347        }
348      this.rescale = isNaN(this.data[0].x);
349      const box_center = [margin.left/2+left_width/2, (margin.top-margin.bottom+height)/2];
350      const simulation = d3
351          .forceSimulation(this.data)
352          .on("tick", () => tick(nodes))
353          .force("collide", d3.forceCollide().radius(d => 2 + d.r))
354          .force("x", d3.forceX(box_center[0]).strength(0.007))
355          .force("y", d3.forceY(box_center[1]).strength(0.001))
356          .stop();
357
358       // once the arrangement is initialized, scale and translate it
359       const scale = 1;
360       if (this.rescale) {
361         for (const node of this.data) {
362           node.x = node.x * scale + box_center[0];
363           node.y = node.y * scale + box_center[1];
364         }
365       }
366
367      // differ application of the forces
368     simulation.restart();
369   }
370   update() {
371      this.restart();
372   }
373}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.