echoboi Claude Sonnet 4.6 commited on
Commit
b5633bb
Β·
1 Parent(s): 9e1495d

Add yuruyurau_5 (#3) equation and merge feature

Browse files

- Add 5th equation preset (#3): yuruyurau_5 with nested double-sin,
alternating m parameter, and 7 mutable trig sites
- Add merge zone UI below base equation selector: drag any of the 5
equation chips to layer a second equation onto all variant canvases
- Merged equations both rendered per frame; evolution mutates combined
parameter set (_2_ prefix for secondary equation params)
- Refactor rendering into shared drawEquationPoints() helper,
eliminating ~400 lines of duplicated if-else render chains
- Backend: merged_secondary_set field, set_merged_secondary API endpoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (2) hide show
  1. genetic_backend.py +78 -6
  2. templates/genetic_evolver.html +332 -453
genetic_backend.py CHANGED
@@ -32,7 +32,7 @@ class EquationGene:
32
  """
33
  keys = trig_keys if trig_keys is not None else [k for k in self.params.keys() if k.startswith('trig')]
34
  # Get all constant keys (numeric parameters, not trig function names)
35
- constant_keys = [k for k in self.params.keys() if not k.startswith('trig') and isinstance(self.params.get(k), (int, float))]
36
 
37
  self.mutation_log = []
38
 
@@ -119,6 +119,7 @@ class GeneticEquationEvolver:
119
  self.systematic_phase_generations = 3 # First 3 generations are systematic
120
  self.approved_trig_sites: set = set() # Track which trig sites user has approved (selected)
121
  self.current_base_set = 'always_finding_yourself' # Current base equation set name
 
122
  self.jump_size_multiplier = 1.0 # Multiplier for mutation delta (controls jump size)
123
  self.trig_mutation_prob_base = 1.0 # Base value for auto-decay (starts at 1.0, can be set by user)
124
  self.trig_mutation_prob_base_generation = 0 # Generation when base was last set (for decay calculation)
@@ -216,6 +217,37 @@ class GeneticEquationEvolver:
216
  'trig_q_sin2': 'sin', # sin(k*d/3) in q
217
  'trig_px_sin': 'sin', # sin(c) in px
218
  'trig_py_cos': 'cos' # cos(c-i%2+i%5*3+7) in py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  }
220
  }
221
 
@@ -223,13 +255,20 @@ class GeneticEquationEvolver:
223
 
224
  @property
225
  def base_params(self):
226
- """Get current base params based on selected equation set"""
227
- return self.base_equation_sets.get(self.current_base_set, self.base_equation_sets['always_finding_yourself'])
228
-
 
 
 
 
 
 
 
229
  @property
230
  def trig_keys(self):
231
- """Get trig keys for current base equation set"""
232
- return [k for k in self.base_params.keys() if k.startswith('trig')]
233
 
234
  def get_trig_mutation_probability(self) -> float:
235
  """Calculate probability of trig mutations vs constant mutations.
@@ -744,6 +783,7 @@ class GeneticEquationEvolver:
744
  'trig_mutation_prob_base_generation': self.trig_mutation_prob_base_generation,
745
  'jump_size_multiplier': self.jump_size_multiplier,
746
  }
 
747
  if not lite:
748
  data['base_params'] = self.base_params
749
  data['parameter_preferences'] = self.parameter_preferences
@@ -753,6 +793,7 @@ class GeneticEquationEvolver:
753
  """Change the base equation set and reset population"""
754
  if set_name in self.base_equation_sets:
755
  self.current_base_set = set_name
 
756
  # Reset all state when changing equation sets
757
  self.generation = 0
758
  self.trig_mutation_prob_base = 1.0
@@ -767,6 +808,23 @@ class GeneticEquationEvolver:
767
  return True
768
  return False
769
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
770
  # Global evolver instance
771
  evolver = GeneticEquationEvolver()
772
 
@@ -852,6 +910,8 @@ def reset_all():
852
  evolver.trig_mutation_prob_base_generation = 0 # Reset generation offset
853
  evolver.jump_size_multiplier = 1.0 # Reset jump size to default
854
 
 
 
855
  # Reinitialize population from scratch
856
  evolver.initialize_population()
857
 
@@ -880,5 +940,17 @@ def set_trig_mutation_prob():
880
  except Exception as e:
881
  return jsonify({'error': str(e)}), 500
882
 
 
 
 
 
 
 
 
 
 
 
 
 
883
  if __name__ == '__main__':
884
  app.run(debug=True, host='0.0.0.0', port=5000)
 
32
  """
33
  keys = trig_keys if trig_keys is not None else [k for k in self.params.keys() if k.startswith('trig')]
34
  # Get all constant keys (numeric parameters, not trig function names)
35
+ constant_keys = [k for k in self.params.keys() if not k.startswith('trig') and not k.startswith('_2_trig') and isinstance(self.params.get(k), (int, float))]
36
 
37
  self.mutation_log = []
38
 
 
119
  self.systematic_phase_generations = 3 # First 3 generations are systematic
120
  self.approved_trig_sites: set = set() # Track which trig sites user has approved (selected)
121
  self.current_base_set = 'always_finding_yourself' # Current base equation set name
122
+ self.merged_secondary_set = None # Optional secondary equation for merge mode
123
  self.jump_size_multiplier = 1.0 # Multiplier for mutation delta (controls jump size)
124
  self.trig_mutation_prob_base = 1.0 # Base value for auto-decay (starts at 1.0, can be set by user)
125
  self.trig_mutation_prob_base_generation = 0 # Generation when base was last set (for decay calculation)
 
217
  'trig_q_sin2': 'sin', # sin(k*d/3) in q
218
  'trig_px_sin': 'sin', # sin(c) in px
219
  'trig_py_cos': 'cos' # cos(c-i%2+i%5*3+7) in py
220
+ },
221
+ 'yuruyurau_5': {
222
+ # Equation: a=(m,d=mag(k=9*cos(i/61),e=i/692-13)**2/99+1)=>point(
223
+ # (q=79-e/2*sin(k/d*4)+k/d*(8+5*sin(sin(d*d+e/9-t+m))))*cos(c=d/2+cos(t-d*2+m)/9-t/16+m)+200,
224
+ # (q+40)*sin(c)+200)
225
+ # t=0,draw=$=>{t||createCanvas(w=400,w);background(9).stroke(w,96);for(t+=PI/45,i=2e4;i--;)a(i%2*9)}
226
+ # k = k_mult * cos(i / k_cos_div)
227
+ 'k_mult': 9, 'k_cos_div': 61,
228
+ # e = i / e_div - e_sub
229
+ 'e_div': 692, 'e_sub': 13,
230
+ # d = mag(k,e)**d_pow / d_div + d_add
231
+ 'd_pow': 2, 'd_div': 99, 'd_add': 1,
232
+ # m = (i%2) * m_mult (alternates 0 or m_mult per point)
233
+ 'm_mult': 9,
234
+ # q = q_base - e/q_e_div*sin1(k/d*q_sin1_mult) + k/d*(q_k_mult + q_sin2_mult*sin2(sin3(d*d+e/q_sin_e_div-t+m)))
235
+ 'q_base': 79, 'q_e_div': 2, 'q_sin1_mult': 4,
236
+ 'q_k_mult': 8, 'q_sin2_mult': 5, 'q_sin_e_div': 9,
237
+ # c = d/c_d_div + cosc(t - d*c_d2_mult + m)/c_cos_div - t/c_t_div + m
238
+ 'c_d_div': 2, 'c_d2_mult': 2, 'c_cos_div': 9, 'c_t_div': 16,
239
+ # px = q * cosp(c) + px_add
240
+ 'px_add': 200,
241
+ # py = (q + py_q_add) * sinp(c) + py_add
242
+ 'py_q_add': 40, 'py_add': 200,
243
+ # Trig sites:
244
+ 'trig_k_cos': 'cos', # cos(i/k_cos_div) in k
245
+ 'trig_q_sin1': 'sin', # outer sin in e term: -e/q_e_div*sin(k/d*q_sin1_mult)
246
+ 'trig_q_sin2': 'sin', # outer sin in nested double-sin: q_sin2_mult*sin(sin(...))
247
+ 'trig_q_sin3': 'sin', # inner sin in nested double-sin: sin(d*d+e/q_sin_e_div-t+m)
248
+ 'trig_c_cos': 'cos', # cos(t-d*c_d2_mult+m) in c
249
+ 'trig_px_cos': 'cos', # cos(c) in px
250
+ 'trig_py_sin': 'sin', # sin(c) in py
251
  }
252
  }
253
 
 
255
 
256
  @property
257
  def base_params(self):
258
+ """Get current base params based on selected equation set (and merged secondary if active)"""
259
+ primary = self.base_equation_sets.get(self.current_base_set, self.base_equation_sets['always_finding_yourself'])
260
+ if self.merged_secondary_set and self.merged_secondary_set in self.base_equation_sets:
261
+ secondary = self.base_equation_sets[self.merged_secondary_set]
262
+ combined = dict(primary)
263
+ for k, v in secondary.items():
264
+ combined['_2_' + k] = v
265
+ return combined
266
+ return primary
267
+
268
  @property
269
  def trig_keys(self):
270
+ """Get trig keys for current base equation set (including merged secondary)"""
271
+ return [k for k in self.base_params.keys() if k.startswith('trig') or k.startswith('_2_trig')]
272
 
273
  def get_trig_mutation_probability(self) -> float:
274
  """Calculate probability of trig mutations vs constant mutations.
 
783
  'trig_mutation_prob_base_generation': self.trig_mutation_prob_base_generation,
784
  'jump_size_multiplier': self.jump_size_multiplier,
785
  }
786
+ data['merged_secondary_set'] = self.merged_secondary_set
787
  if not lite:
788
  data['base_params'] = self.base_params
789
  data['parameter_preferences'] = self.parameter_preferences
 
793
  """Change the base equation set and reset population"""
794
  if set_name in self.base_equation_sets:
795
  self.current_base_set = set_name
796
+ self.merged_secondary_set = None # Clear merge when changing base set
797
  # Reset all state when changing equation sets
798
  self.generation = 0
799
  self.trig_mutation_prob_base = 1.0
 
808
  return True
809
  return False
810
 
811
+ def set_merged_secondary(self, set_name: Optional[str]):
812
+ """Set (or clear) the secondary equation for merge mode and reinitialize population"""
813
+ if set_name is None or set_name in self.base_equation_sets:
814
+ self.merged_secondary_set = set_name
815
+ # Reset evolution state for the new combined genome
816
+ self.generation = 0
817
+ self.trig_mutation_prob_base = 1.0
818
+ self.trig_mutation_prob_base_generation = 0
819
+ self.approved_trig_sites = set()
820
+ self.parameter_preferences = {}
821
+ self.mutation_penalties = {}
822
+ self.liked_variants = []
823
+ self.select_none_history = []
824
+ self.initialize_population()
825
+ return True
826
+ return False
827
+
828
  # Global evolver instance
829
  evolver = GeneticEquationEvolver()
830
 
 
910
  evolver.trig_mutation_prob_base_generation = 0 # Reset generation offset
911
  evolver.jump_size_multiplier = 1.0 # Reset jump size to default
912
 
913
+ evolver.merged_secondary_set = None # Clear merge on full reset
914
+
915
  # Reinitialize population from scratch
916
  evolver.initialize_population()
917
 
 
940
  except Exception as e:
941
  return jsonify({'error': str(e)}), 500
942
 
943
+ @app.route('/api/set_merged_secondary', methods=['POST'])
944
+ def set_merged_secondary():
945
+ """Set or clear the secondary equation for merge mode."""
946
+ try:
947
+ data = request.get_json()
948
+ set_name = data.get('set_name') # None/null to clear merge
949
+ if evolver.set_merged_secondary(set_name):
950
+ return jsonify({'success': True, 'merged_secondary_set': evolver.merged_secondary_set})
951
+ return jsonify({'success': False, 'error': 'Invalid equation set name'}), 400
952
+ except Exception as e:
953
+ return jsonify({'error': str(e)}), 500
954
+
955
  if __name__ == '__main__':
956
  app.run(debug=True, host='0.0.0.0', port=5000)
templates/genetic_evolver.html CHANGED
@@ -44,6 +44,14 @@
44
  .btn{background:#000;color:#fff;border:1px solid #666;padding:8px 16px;margin:0;cursor:pointer;font-family:monospace;font-size:11px;text-transform:uppercase;letter-spacing:0.5px}
45
  .btn:hover{background:#333;border-color:#999}
46
  .btn:disabled{background:#111;color:#666;border-color:#444;cursor:not-allowed}
 
 
 
 
 
 
 
 
47
  </style>
48
  </head>
49
  <body>
@@ -70,9 +78,24 @@
70
  <!-- <option value="yuruyurau_2">yuruyurau #2</option> -->
71
  <!-- <option value="yuruyurau_3">yuruyurau #3</option> -->
72
  <option value="yuruyurau_4">#2</option>
 
73
  </select>
74
  </div>
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  <div style="margin-bottom:15px;">
77
  <label style="font-size:9px;color:#666;text-transform:lowercase;letter-spacing:1px;display:block;margin-bottom:5px;">trigonometric mutation probability:</label>
78
  <div style="display:flex;gap:4px;align-items:center;height:20px;">
@@ -191,6 +214,7 @@
191
 
192
  <script>
193
  let variants=[]; let selectedIds=new Set();
 
194
  let currentParams = {
195
  k_div: 4, k_sub: 10.5, e_div: 9, e_add: 9, o_div: 9, x_offset: 60,
196
  y_trig_div: 8, q_mult: 0.7, px_add: 200, py_add: 200, py_q_div: 3,
@@ -200,6 +224,7 @@
200
  let currentBaseSet = 'always_finding_yourself'; // Current base equation set
201
  let variantSketches = []; // Track p5 instances for variants to clean them up
202
  let sketchBaseSet = null; // Which base set the current sketches were built for
 
203
  // Shared mutable param objects β€” p5 sketches hold references so we can update without recreation
204
  const variantParamSlots = Array.from({length: 16}, () => ({}));
205
  let currentMutations = []; // Track mutations for highlighting
@@ -223,7 +248,8 @@
223
  'yuruyurau': 6, // for(let i = 0; i < 12000; i += 6)
224
  'yuruyurau_2': 1, // for(let i = 30000; i > 0; i--)
225
  'yuruyurau_3': 1, // for(let i = 20000; i > 0; i--)
226
- 'yuruyurau_4': 1 // for(let i = 20000; i > 0; i--)
 
227
  };
228
  const LOOP_BOUNDS = {
229
  'always_finding_yourself': 6400,
@@ -231,7 +257,8 @@
231
  'yuruyurau': 12000,
232
  'yuruyurau_2': 10000,
233
  'yuruyurau_3': 10000,
234
- 'yuruyurau_4': 1000
 
235
  };
236
 
237
  // O(1) trig dispatch β€” avoids per-point if-else chain
@@ -265,6 +292,10 @@
265
  currentBaseSet = data.current_base_set;
266
  document.getElementById('base-equation-selector').value = currentBaseSet;
267
  }
 
 
 
 
268
  if(data.trig_mutation_prob !== undefined) {
269
  trigMutationProb = data.trig_mutation_prob;
270
  updateMutationProbChart();
@@ -379,6 +410,8 @@
379
  .then(data=>{
380
  if(data.success) {
381
  currentBaseSet = newSet;
 
 
382
  loadPopulation();
383
  } else {
384
  alert('Error changing base equation set: '+data.error);
@@ -451,6 +484,18 @@
451
  equation += `&nbsp;&nbsp;px = q*${getTrigSpan('trig_px_sin')}(c) + ${getParamSpan('px_add')}<br>`;
452
  equation += `&nbsp;&nbsp;py = q*${getTrigSpan('trig_py_cos')}(c-i%${getParamSpan('c_mod2')}+i%${getParamSpan('c_mod1')}*3+${getParamSpan('py_cos_add1')}) + ${getParamSpan('py_add')}<br>`;
453
  equation += `&nbsp;&nbsp;point(px,py)`;
 
 
 
 
 
 
 
 
 
 
 
 
454
  } else {
455
  // Original equations (always_finding_yourself)
456
  equation += 'for i in range(0, 6400, 3):<br>';
@@ -471,6 +516,167 @@
471
 
472
  let basePreviewSketch = null;
473
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
474
  function updateBaseEquationDisplay(){
475
  const container = document.getElementById('base-preview-canvas');
476
 
@@ -483,10 +689,8 @@
483
  const baseParams = {...currentParams};
484
 
485
  basePreviewSketch = new p5(p => {
486
- const trig = (name, val) => getTrig(name)(val);
487
-
488
  p.setup = () => {
489
- p.createCanvas(126, 126).parent(container); // Same size as grid variants
490
  p.pixelDensity(1);
491
  p.frameRate(FRAME_RATE);
492
  p.noFill();
@@ -494,227 +698,13 @@
494
  };
495
 
496
  p.draw = () => {
497
- // Animate over 300 frames, repeating (100x speed)
498
  const frame = p.frameCount % ANIMATION_FRAMES;
499
- const t = (frame / ANIMATION_FRAMES) * Math.PI * 2 * 100; // 0 to 200Ο€ over 300 frames (100x faster)
500
-
501
- p.clear(); // Remove background patching
502
- p.stroke(255, 40); // Subtle rendering for base pattern
503
-
504
- // Calculate center offset based on canvas size
505
- const centerX = p.width / 2;
506
- const centerY = p.height / 2;
507
-
508
- if(currentBaseSet === 'a_constant_state_of_bliss') {
509
- // Render "a constant state of bliss" equations
510
- const zoomFactor = 0.4; // 0.8x zoom out from 0.5
511
- const kDiv = baseParams.k_div || 8, kSub = baseParams.k_sub || 11.5;
512
- const eDiv = baseParams.e_div || 8, eSub = baseParams.e_sub || 12.5;
513
- const oDiv = baseParams.o_div || 139, dMult = baseParams.d_mult || 10;
514
- const pxDiv = baseParams.px_div || 2, pxAdd1 = baseParams.px_add1 || 150;
515
- const pyYDiv = baseParams.py_y_div || 9, pyDMult = baseParams.py_d_mult || 15;
516
- const pyCosMult = baseParams.py_cos_mult || 2, pyAdd = baseParams.py_add || 220;
517
- const strokeBase = baseParams.stroke_base || 99, strokeMult = baseParams.stroke_mult || 99, strokePow = baseParams.stroke_pow || 30;
518
- // Trig functions
519
- const trigDCos = baseParams.trig_d_cos || 'cos';
520
- const trigPxSin1 = baseParams.trig_px_sin1 || 'sin';
521
- const trigPxSin2 = baseParams.trig_px_sin2 || 'sin';
522
- const trigPyCos = baseParams.trig_py_cos || 'cos';
523
- const trigPySin = baseParams.trig_py_sin || 'sin';
524
- const trigStrokeSin1 = baseParams.trig_stroke_sin1 || 'sin';
525
- const trigStrokeSin2 = baseParams.trig_stroke_sin2 || 'sin';
526
-
527
- // Pattern center in world coordinates (approximate, based on typical px/py values)
528
- // For this equation, px typically ranges around pxAdd1 (150), py around pyAdd (220)
529
- const patternCenterX = pxAdd1 * zoomFactor;
530
- const patternCenterY = pyAdd * zoomFactor;
531
-
532
- const step = FRAME_SAMPLING_STEPS[currentBaseSet] || DEFAULT_FRAME_SAMPLING_STEP;
533
- const maxI = LOOP_BOUNDS[currentBaseSet] || 6400;
534
- for(let i = 0; i < maxI; i += step){
535
- let x = i % 200, y = i / 200;
536
- let k = x / kDiv - kSub;
537
- let e = y / eDiv - eSub;
538
- let o = Math.hypot(k, e) ** 2 / oDiv;
539
- let d = dMult * trig(trigDCos, o);
540
- let ksafe = Math.abs(k) > 1e-6 ? k : 1e-6;
541
- let stroke_r = strokeBase + strokeMult / trig(trigStrokeSin1, ksafe) * trig(trigStrokeSin2, t + e) ** strokePow;
542
- p.stroke(stroke_r, 66);
543
- let px = ((x + trig(trigPxSin1, d) * d * k) / pxDiv + pxAdd1 + o * k * trig(trigPxSin2, t + d * o)) * zoomFactor;
544
- let py = (y / pyYDiv - d * pyDMult - trig(trigPyCos, d * pyCosMult) * d + pyAdd + d * trig(trigPySin, d - t)) * zoomFactor;
545
- // Center the pattern in the canvas
546
- p.point(px - patternCenterX + centerX, py - patternCenterY + centerY);
547
- }
548
- } else if(currentBaseSet === 'yuruyurau') {
549
- // Render yuruyurau equation
550
- p.stroke(255, 96); // Match original stroke(w,96)
551
- const zoomFactor = 0.3;
552
- const kBase = baseParams.k_base || 4, kSinMult = baseParams.k_sin_mult || 3, kCosDiv = baseParams.k_cos_div || 29;
553
- const eDiv = baseParams.e_div || 8, eSub = baseParams.e_sub || 13;
554
- const qSin1Mult = baseParams.q_sin1_mult || 3, qSin1Mult2 = baseParams.q_sin1_mult2 || 2, qDiv = baseParams.q_div || 0.3;
555
- const qSin2Div = baseParams.q_sin2_div || 25, qBase = baseParams.q_base || 9;
556
- const qSin3Mult = baseParams.q_sin3_mult || 4, qSin3EMult = baseParams.q_sin3_e_mult || 9;
557
- const qSin3DMult = baseParams.q_sin3_d_mult || 3, qSin3TMult = baseParams.q_sin3_t_mult || 2;
558
- const pxCosMult = baseParams.px_cos_mult || 30, pxAdd = baseParams.px_add || 200;
559
- const pyDMult = baseParams.py_d_mult || 39, pySub = baseParams.py_sub || 220;
560
- // Trig functions
561
- const trigKSin = baseParams.trig_k_sin || 'sin';
562
- const trigKCos = baseParams.trig_k_cos || 'cos';
563
- const trigQSin1 = baseParams.trig_q_sin1 || 'sin';
564
- const trigQSin2 = baseParams.trig_q_sin2 || 'sin';
565
- const trigQSin3 = baseParams.trig_q_sin3 || 'sin';
566
- const trigPxCos = baseParams.trig_px_cos || 'cos';
567
- const trigPySin = baseParams.trig_py_sin || 'sin';
568
-
569
- // Pattern center estimate
570
- const patternCenterX = pxAdd * zoomFactor;
571
- const patternCenterY = (pySub / 2) * zoomFactor;
572
-
573
- const step = FRAME_SAMPLING_STEPS[currentBaseSet] || DEFAULT_FRAME_SAMPLING_STEP;
574
- const maxI = LOOP_BOUNDS[currentBaseSet] || 10000;
575
- for(let i = 0; i < maxI; i += step){
576
- let x = i, y = i / 235;
577
- let k = (kBase + trig(trigKSin, y * 2 - t) * kSinMult) * trig(trigKCos, x / kCosDiv);
578
- let e = y / eDiv - eSub;
579
- let d = Math.hypot(k, e);
580
- let ksafe = Math.abs(k) > 1e-6 ? k : 1e-6;
581
- let q = qSin1Mult * trig(trigQSin1, k * qSin1Mult2) + qDiv / ksafe + trig(trigQSin2, y / qSin2Div) * k * (qBase + qSin3Mult * trig(trigQSin3, e * qSin3EMult - d * qSin3DMult + t * qSin3TMult));
582
- let c = d - t;
583
- let px = (q + pxCosMult * trig(trigPxCos, c) + pxAdd) * zoomFactor;
584
- let py = (q * trig(trigPySin, c) + d * pyDMult - pySub) * zoomFactor;
585
- // Center the pattern in the canvas
586
- p.point(px - patternCenterX + centerX, py - patternCenterY + centerY);
587
- }
588
- /* } else if(currentBaseSet === 'yuruyurau_2') {
589
- // Render yuruyurau #2
590
- p.stroke(255, 46);
591
- const zoomFactor = 0.3;
592
- const kCondThreshold = baseParams.k_cond_threshold || 20000;
593
- const kSinDiv = baseParams.k_sin_div || 9, kSinMult = baseParams.k_sin_mult || 9;
594
- const kCosMult = baseParams.k_cos_mult || 4, kCosDiv1 = baseParams.k_cos_div1 || 49, kCosDiv2 = baseParams.k_cos_div2 || 3690;
595
- const eDiv = baseParams.e_div || 984, eSub = baseParams.e_sub || 12;
596
- const dPow = baseParams.d_pow || 2, dDiv = baseParams.d_div || 99, dAdd = baseParams.d_add || 1;
597
- const qKMult = baseParams.q_k_mult || 4, qSinDMult = baseParams.q_sin_d_mult || 16;
598
- const qAtan2Mult = baseParams.q_atan2_mult || 5, qAtan2Mult2 = baseParams.q_atan2_mult2 || 9;
599
- const pxSinMult = baseParams.px_sin_mult || 60, pxAdd = baseParams.px_add || 200;
600
- const pyQAdd = baseParams.py_q_add || 40, pyDMult = baseParams.py_d_mult || 79;
601
- const cDMult = baseParams.c_d_mult || 1.1, cTDiv = baseParams.c_t_div || 18;
602
- const trigKSin = baseParams.trig_k_sin || 'sin';
603
- const trigKCos1 = baseParams.trig_k_cos1 || 'cos';
604
- const trigKCos2 = baseParams.trig_k_cos2 || 'cos';
605
- const trigQSin = baseParams.trig_q_sin || 'sin';
606
- const trigQAtan2 = baseParams.trig_q_atan2 || 'atan';
607
- const trigPxSin = baseParams.trig_px_sin || 'sin';
608
- const trigPySin = baseParams.trig_py_sin || 'sin';
609
- const patternCenterX = pxAdd * zoomFactor;
610
- const patternCenterY = (pyDMult * 50) * zoomFactor;
611
- const step = FRAME_SAMPLING_STEPS[currentBaseSet] || DEFAULT_FRAME_SAMPLING_STEP;
612
- const maxI = LOOP_BOUNDS[currentBaseSet] || 30000;
613
- for(let i = maxI; i > 0; i -= step){
614
- let k = i < kCondThreshold ? trig(trigKSin, i / kSinDiv) * kSinMult : kCosMult * trig(trigKCos1, i / kCosDiv1) * trig(trigKCos2, i / kCosDiv2);
615
- let e = i / eDiv - eSub;
616
- let d = (Math.hypot(k, e) ** dPow / dDiv) + dAdd;
617
- let q = k * (qKMult + trig(trigQSin, d * qSinDMult - t + k)) - qAtan2Mult * Math.atan2(k, e) * qAtan2Mult2;
618
- let c = d * cDMult - t / cTDiv + (i % 2) * 3;
619
- let px = (q + pxSinMult * trig(trigPxSin, c) + pxAdd) * zoomFactor;
620
- let py = ((q + pyQAdd) * trig(trigPySin, c - d) + d * pyDMult) * zoomFactor;
621
- if(isFinite(px) && isFinite(py)) {
622
- p.point(px - patternCenterX + centerX, py - patternCenterY + centerY);
623
- }
624
- }
625
- } else if(currentBaseSet === 'yuruyurau_3') {
626
- // Render yuruyurau #3
627
- const zoomFactor = 0.3;
628
- const kMult = baseParams.k_mult || 5, kCosDiv1 = baseParams.k_cos_div1 || 49, kCosDiv2 = baseParams.k_cos_div2 || 3690;
629
- const eDiv = baseParams.e_div || 984, eSub = baseParams.e_sub || 12;
630
- const dPow = baseParams.d_pow || 2, dDiv = baseParams.d_div || 99, dAdd = baseParams.d_add || 1;
631
- const qKMult = baseParams.q_k_mult || 4, qSinDMult = baseParams.q_sin_d_mult || 18, qSinTMult = baseParams.q_sin_t_mult || 2;
632
- const qSinMod = baseParams.q_sin_mod || 3, qSinModMult = baseParams.q_sin_mod_mult || 2;
633
- const qAtan2Mult = baseParams.q_atan2_mult || 5, qAtan2Mult2 = baseParams.q_atan2_mult2 || 9;
634
- const pxSinMult = baseParams.px_sin_mult || 30, pxAdd = baseParams.px_add || 200;
635
- const pySinMult = baseParams.py_sin_mult || 80, pyDMult = baseParams.py_d_mult || 79;
636
- const cTDiv = baseParams.c_t_div || 18, cMod = baseParams.c_mod || 3, cModMult = baseParams.c_mod_mult || 4;
637
- const trigKCos1 = baseParams.trig_k_cos1 || 'cos';
638
- const trigKCos2 = baseParams.trig_k_cos2 || 'cos';
639
- const trigQSin = baseParams.trig_q_sin || 'sin';
640
- const trigQAtan2 = baseParams.trig_q_atan2 || 'atan';
641
- const trigPxSin = baseParams.trig_px_sin || 'sin';
642
- const trigPySin = baseParams.trig_py_sin || 'sin';
643
- const trigStrokeCos = baseParams.trig_stroke_cos || 'cos';
644
- const patternCenterX = pxAdd * zoomFactor;
645
- const patternCenterY = (pyDMult * 50) * zoomFactor;
646
- const step = FRAME_SAMPLING_STEPS[currentBaseSet] || DEFAULT_FRAME_SAMPLING_STEP;
647
- const maxI = LOOP_BOUNDS[currentBaseSet] || 20000;
648
- for(let i = maxI; i > 0; i -= step){
649
- let k = kMult * trig(trigKCos1, i / kCosDiv1) * trig(trigKCos2, i / kCosDiv2);
650
- let e = i / eDiv - eSub;
651
- let d = (Math.hypot(k, e) ** dPow / dDiv) + dAdd;
652
- let cosVal = trig(trigStrokeCos, t + e);
653
- let stroke_r = 36 + 3 / (Math.abs(cosVal) > 0.001 ? cosVal : 0.001);
654
- p.stroke(255, Math.min(255, Math.max(0, stroke_r)));
655
- let c = d - t / cTDiv + (i % cMod) * cModMult;
656
- let px = (k * (qKMult + trig(trigQSin, d * qSinDMult - t * qSinTMult + (i % qSinMod) * qSinModMult)) - qAtan2Mult * Math.atan2(k, e) * qAtan2Mult2 + pxSinMult * trig(trigPxSin, c) + pxAdd) * zoomFactor;
657
- let py = (pySinMult * trig(trigPySin, c - d) + d * pyDMult) * zoomFactor;
658
- p.point(px - patternCenterX + centerX, py - patternCenterY + centerY);
659
- }
660
- */ } else if(currentBaseSet === 'yuruyurau_4') {
661
- // Render yuruyurau #4
662
- p.stroke(255, 96);
663
- const zoomFactor = 0.3;
664
- const kMult = baseParams.k_mult || 5, kCosDiv = baseParams.k_cos_div || 8;
665
- const eMult = baseParams.e_mult || 5, eCosDiv = baseParams.e_cos_div || 9;
666
- const dDivBase = baseParams.d_div_base || 6, dPow = baseParams.d_pow || 4, dAdd = baseParams.d_add || 4;
667
- const qKMult = baseParams.q_k_mult || 3, qEDiv = baseParams.q_e_div || 2;
668
- const qSinMult = baseParams.q_sin_mult || 3, qSinKDDiv = baseParams.q_sin_kd_div || 3, qBitwiseMult = baseParams.q_bitwise_mult || 70;
669
- const pxSinMult = baseParams.px_sin_mult || 1, pxAdd = baseParams.px_add || 200;
670
- const pyCosAdd1 = baseParams.py_cos_add1 || 7, pyAdd = baseParams.py_add || 200;
671
- const cTDiv = baseParams.c_t_div || 9, cMod1 = baseParams.c_mod1 || 5, cMod1Mult = baseParams.c_mod1_mult || 5, cMod2 = baseParams.c_mod2 || 2;
672
- const trigKCos = baseParams.trig_k_cos || 'cos';
673
- const trigECos = baseParams.trig_e_cos || 'cos';
674
- const trigQSin = baseParams.trig_q_sin || 'sin';
675
- const trigQSin2 = baseParams.trig_q_sin2 || 'sin';
676
- const trigPxSin = baseParams.trig_px_sin || 'sin';
677
- const trigPyCos = baseParams.trig_py_cos || 'cos';
678
- const patternCenterX = pxAdd * zoomFactor;
679
- const patternCenterY = pyAdd * zoomFactor;
680
- const step = FRAME_SAMPLING_STEPS[currentBaseSet] || DEFAULT_FRAME_SAMPLING_STEP;
681
- const maxI = LOOP_BOUNDS[currentBaseSet] || 20000;
682
- for(let i = maxI; i > 0; i -= step){
683
- let y = i / 799;
684
- let k = kMult * trig(trigKCos, i / kCosDiv);
685
- let e = eMult * trig(trigECos, y / eCosDiv);
686
- let d = (Math.hypot(k, e) / (dDivBase + (i % 5))) ** dPow + dAdd;
687
- let q = k * (qKMult + e / qEDiv * trig(trigQSin, d * d - t)) - qSinMult * trig(trigQSin2, k * d / qSinKDDiv) - (~(i & 1)) * qBitwiseMult;
688
- let c = d - t / cTDiv + (i % cMod1) * cMod1Mult + (i % cMod2);
689
- let px = (q * trig(trigPxSin, c) + pxAdd) * zoomFactor;
690
- let py = (q * trig(trigPyCos, c - (i % cMod2) + (i % cMod1) * 3 + pyCosAdd1) + pyAdd) * zoomFactor;
691
- p.point(px - patternCenterX + centerX, py - patternCenterY + centerY);
692
- }
693
- } else {
694
- // Render "always_finding_yourself" equations
695
- const zoomFactor = 0.2;
696
- const kDiv = baseParams.k_div, kSub = baseParams.k_sub, eDiv = baseParams.e_div, eAdd = baseParams.e_add;
697
- const oDiv = baseParams.o_div, xOff = baseParams.x_offset, yTrigDiv = baseParams.y_trig_div;
698
- const qMult = baseParams.q_mult, pxAdd = baseParams.px_add, pyAdd = baseParams.py_add, pyQDiv = baseParams.py_q_div;
699
- const trigY = baseParams.trig_y, trigE = baseParams.trig_e, trigO = baseParams.trig_o;
700
- const trigCSin = baseParams.trig_c_sin, trigC4Cos = baseParams.trig_c4_cos, trigCCos = baseParams.trig_c_cos;
701
-
702
- // Pattern center in world coordinates (pxAdd and pyAdd are the base offsets)
703
- const patternCenterX = pxAdd * zoomFactor;
704
- const patternCenterY = pyAdd * zoomFactor;
705
-
706
- for(let i = 0; i < 6400; i += 3){
707
- let x = i % 200, y = i / 200;
708
- let k = x / kDiv - kSub; let ksafe = Math.abs(k) > 1e-6 ? k : 1e-6;
709
- let e = y / eDiv + eAdd;
710
- let o = Math.hypot(k, e) / oDiv;
711
- let q = x + xOff + y + 1/ksafe + o * k * (trig(trigY, y) / yTrigDiv + trig(trigE, e)) * trig(trigO, o * 4 - t);
712
- let c = o + e / 99 - t / 8 + (i % 2) * 3;
713
- let px = (qMult * q * trig(trigCSin, c) + pxAdd) * zoomFactor;
714
- let py = (pyAdd + y / 2 * trig(trigC4Cos, c * 4 - o) - q / pyQDiv * trig(trigCCos, c - 1)) * zoomFactor;
715
- // Center the pattern in the canvas
716
- p.point(px - patternCenterX + centerX, py - patternCenterY + centerY);
717
- }
718
  }
719
  };
720
  });
@@ -764,8 +754,8 @@
764
  currentMutations = [];
765
  variants.forEach(v => { if(v.mutations) currentMutations.push(...v.mutations); });
766
 
767
- // Fast path: same base set + same count β†’ just update params & labels, no sketch recreation
768
- if(sketchBaseSet === currentBaseSet && variantSketches.length === variants.length) {
769
  variants.forEach((v, i) => {
770
  // Update shared param slot β€” the p5 draw loop reads from this reference each frame
771
  Object.assign(variantParamSlots[i], v.params);
@@ -782,10 +772,11 @@
782
  return;
783
  }
784
 
785
- // Full rebuild: base set changed or first render
786
  variantSketches.forEach(s => { if(s && typeof s.remove === 'function') s.remove(); });
787
  variantSketches = [];
788
  sketchBaseSet = currentBaseSet;
 
789
  grid.innerHTML = '';
790
 
791
  variants.forEach((v, i) => {
@@ -818,9 +809,6 @@
818
 
819
  function renderEquation(container, params){
820
  const sketch = new p5(p=>{
821
- // Use global TRIG_FNS lookup (O(1)) β€” avoids per-point if-else chain
822
- const trig = (name, val) => getTrig(name)(val);
823
-
824
  p.setup=()=>{
825
  p.createCanvas(126,126).parent(container);
826
  p.pixelDensity(1);
@@ -831,230 +819,13 @@
831
  };
832
 
833
  p.draw=()=>{
834
- // Animate over 300 frames, repeating (100x speed)
835
  const frame = p.frameCount % ANIMATION_FRAMES;
836
- const t = (frame / ANIMATION_FRAMES) * Math.PI * 2 * 100; // 0 to 200Ο€ over 300 frames (100x faster)
837
-
838
  p.background(0);
839
-
840
- // Calculate center offset based on canvas size (variants are 126x126)
841
- const centerX = p.width / 2;
842
- const centerY = p.height / 2;
843
-
844
- if(currentBaseSet === 'a_constant_state_of_bliss') {
845
- // Render "a constant state of bliss" equations
846
- const zoomFactor = 0.4; // 0.8x zoom out from 0.5
847
- const kDiv = params.k_div || 8, kSub = params.k_sub || 11.5;
848
- const eDiv = params.e_div || 8, eSub = params.e_sub || 12.5;
849
- const oDiv = params.o_div || 139, dMult = params.d_mult || 10;
850
- const pxDiv = params.px_div || 2, pxAdd1 = params.px_add1 || 150;
851
- const pyYDiv = params.py_y_div || 9, pyDMult = params.py_d_mult || 15;
852
- const pyCosMult = params.py_cos_mult || 2, pyAdd = params.py_add || 220;
853
- const strokeBase = params.stroke_base || 99, strokeMult = params.stroke_mult || 99, strokePow = params.stroke_pow || 30;
854
- // Trig functions
855
- const trigDCos = params.trig_d_cos || 'cos';
856
- const trigPxSin1 = params.trig_px_sin1 || 'sin';
857
- const trigPxSin2 = params.trig_px_sin2 || 'sin';
858
- const trigPyCos = params.trig_py_cos || 'cos';
859
- const trigPySin = params.trig_py_sin || 'sin';
860
- const trigStrokeSin1 = params.trig_stroke_sin1 || 'sin';
861
- const trigStrokeSin2 = params.trig_stroke_sin2 || 'sin';
862
-
863
- // Pattern center in world coordinates (approximate, based on typical px/py values)
864
- // For this equation, px typically ranges around pxAdd1 (150), py around pyAdd (220)
865
- const patternCenterX = pxAdd1 * zoomFactor;
866
- const patternCenterY = pyAdd * zoomFactor;
867
-
868
- const step = FRAME_SAMPLING_STEPS[currentBaseSet] || DEFAULT_FRAME_SAMPLING_STEP;
869
- const maxI = LOOP_BOUNDS[currentBaseSet] || 6400;
870
- for(let i = 0; i < maxI; i += step){
871
- let x = i % 200, y = i / 200;
872
- let k = x / kDiv - kSub;
873
- let e = y / eDiv - eSub;
874
- let o = Math.hypot(k, e) ** 2 / oDiv;
875
- let d = dMult * trig(trigDCos, o);
876
- let ksafe = Math.abs(k) > 1e-6 ? k : 1e-6;
877
- let stroke_r = strokeBase + strokeMult / trig(trigStrokeSin1, ksafe) * trig(trigStrokeSin2, t + e) ** strokePow;
878
- p.stroke(stroke_r, 66);
879
- let px = ((x + trig(trigPxSin1, d) * d * k) / pxDiv + pxAdd1 + o * k * trig(trigPxSin2, t + d * o)) * zoomFactor;
880
- let py = (y / pyYDiv - d * pyDMult - trig(trigPyCos, d * pyCosMult) * d + pyAdd + d * trig(trigPySin, d - t)) * zoomFactor;
881
- // Center the pattern in the canvas
882
- p.point(px - patternCenterX + centerX, py - patternCenterY + centerY);
883
- }
884
- } else if(currentBaseSet === 'yuruyurau') {
885
- // Render yuruyurau equation
886
- p.stroke(255, 96); // Match original stroke(w,96)
887
- const zoomFactor = 0.3;
888
- const kBase = params.k_base || 4, kSinMult = params.k_sin_mult || 3, kCosDiv = params.k_cos_div || 29;
889
- const eDiv = params.e_div || 8, eSub = params.e_sub || 13;
890
- const qSin1Mult = params.q_sin1_mult || 3, qSin1Mult2 = params.q_sin1_mult2 || 2, qDiv = params.q_div || 0.3;
891
- const qSin2Div = params.q_sin2_div || 25, qBase = params.q_base || 9;
892
- const qSin3Mult = params.q_sin3_mult || 4, qSin3EMult = params.q_sin3_e_mult || 9;
893
- const qSin3DMult = params.q_sin3_d_mult || 3, qSin3TMult = params.q_sin3_t_mult || 2;
894
- const pxCosMult = params.px_cos_mult || 30, pxAdd = params.px_add || 200;
895
- const pyDMult = params.py_d_mult || 39, pySub = params.py_sub || 220;
896
- // Trig functions
897
- const trigKSin = params.trig_k_sin || 'sin';
898
- const trigKCos = params.trig_k_cos || 'cos';
899
- const trigQSin1 = params.trig_q_sin1 || 'sin';
900
- const trigQSin2 = params.trig_q_sin2 || 'sin';
901
- const trigQSin3 = params.trig_q_sin3 || 'sin';
902
- const trigPxCos = params.trig_px_cos || 'cos';
903
- const trigPySin = params.trig_py_sin || 'sin';
904
-
905
- // Pattern center estimate
906
- const patternCenterX = pxAdd * zoomFactor;
907
- const patternCenterY = (pySub / 2) * zoomFactor;
908
-
909
- const step = FRAME_SAMPLING_STEPS[currentBaseSet] || DEFAULT_FRAME_SAMPLING_STEP;
910
- const maxI = LOOP_BOUNDS[currentBaseSet] || 10000;
911
- for(let i = 0; i < maxI; i += step){
912
- let x = i, y = i / 235;
913
- let k = (kBase + trig(trigKSin, y * 2 - t) * kSinMult) * trig(trigKCos, x / kCosDiv);
914
- let e = y / eDiv - eSub;
915
- let d = Math.hypot(k, e);
916
- let ksafe = Math.abs(k) > 1e-6 ? k : 1e-6;
917
- let q = qSin1Mult * trig(trigQSin1, k * qSin1Mult2) + qDiv / ksafe + trig(trigQSin2, y / qSin2Div) * k * (qBase + qSin3Mult * trig(trigQSin3, e * qSin3EMult - d * qSin3DMult + t * qSin3TMult));
918
- let c = d - t;
919
- let px = (q + pxCosMult * trig(trigPxCos, c) + pxAdd) * zoomFactor;
920
- let py = (q * trig(trigPySin, c) + d * pyDMult - pySub) * zoomFactor;
921
- // Center the pattern in the canvas
922
- p.point(px - patternCenterX + centerX, py - patternCenterY + centerY);
923
- }
924
- /* } else if(currentBaseSet === 'yuruyurau_2') {
925
- // Render yuruyurau #2
926
- p.stroke(255, 46);
927
- const zoomFactor = 0.3;
928
- const kCondThreshold = params.k_cond_threshold || 20000;
929
- const kSinDiv = params.k_sin_div || 9, kSinMult = params.k_sin_mult || 9;
930
- const kCosMult = params.k_cos_mult || 4, kCosDiv1 = params.k_cos_div1 || 49, kCosDiv2 = params.k_cos_div2 || 3690;
931
- const eDiv = params.e_div || 984, eSub = params.e_sub || 12;
932
- const dPow = params.d_pow || 2, dDiv = params.d_div || 99, dAdd = params.d_add || 1;
933
- const qKMult = params.q_k_mult || 4, qSinDMult = params.q_sin_d_mult || 16;
934
- const qAtan2Mult = params.q_atan2_mult || 5, qAtan2Mult2 = params.q_atan2_mult2 || 9;
935
- const pxSinMult = params.px_sin_mult || 60, pxAdd = params.px_add || 200;
936
- const pyQAdd = params.py_q_add || 40, pyDMult = params.py_d_mult || 79;
937
- const cDMult = params.c_d_mult || 1.1, cTDiv = params.c_t_div || 18;
938
- const trigKSin = params.trig_k_sin || 'sin';
939
- const trigKCos1 = params.trig_k_cos1 || 'cos';
940
- const trigKCos2 = params.trig_k_cos2 || 'cos';
941
- const trigQSin = params.trig_q_sin || 'sin';
942
- const trigQAtan2 = params.trig_q_atan2 || 'atan';
943
- const trigPxSin = params.trig_px_sin || 'sin';
944
- const trigPySin = params.trig_py_sin || 'sin';
945
- const patternCenterX = pxAdd * zoomFactor;
946
- const patternCenterY = (pyDMult * 50) * zoomFactor;
947
- const step = FRAME_SAMPLING_STEPS[currentBaseSet] || DEFAULT_FRAME_SAMPLING_STEP;
948
- const maxI = LOOP_BOUNDS[currentBaseSet] || 30000;
949
- for(let i = maxI; i > 0; i -= step){
950
- let k = i < kCondThreshold ? trig(trigKSin, i / kSinDiv) * kSinMult : kCosMult * trig(trigKCos1, i / kCosDiv1) * trig(trigKCos2, i / kCosDiv2);
951
- let e = i / eDiv - eSub;
952
- let d = (Math.hypot(k, e) ** dPow / dDiv) + dAdd;
953
- let q = k * (qKMult + trig(trigQSin, d * qSinDMult - t + k)) - qAtan2Mult * Math.atan2(k, e) * qAtan2Mult2;
954
- let c = d * cDMult - t / cTDiv + (i % 2) * 3;
955
- let px = (q + pxSinMult * trig(trigPxSin, c) + pxAdd) * zoomFactor;
956
- let py = ((q + pyQAdd) * trig(trigPySin, c - d) + d * pyDMult) * zoomFactor;
957
- if(isFinite(px) && isFinite(py)) {
958
- p.point(px - patternCenterX + centerX, py - patternCenterY + centerY);
959
- }
960
- }
961
- } else if(currentBaseSet === 'yuruyurau_3') {
962
- // Render yuruyurau #3
963
- const zoomFactor = 0.3;
964
- const kMult = params.k_mult || 5, kCosDiv1 = params.k_cos_div1 || 49, kCosDiv2 = params.k_cos_div2 || 3690;
965
- const eDiv = params.e_div || 984, eSub = params.e_sub || 12;
966
- const dPow = params.d_pow || 2, dDiv = params.d_div || 99, dAdd = params.d_add || 1;
967
- const qKMult = params.q_k_mult || 4, qSinDMult = params.q_sin_d_mult || 18, qSinTMult = params.q_sin_t_mult || 2;
968
- const qSinMod = params.q_sin_mod || 3, qSinModMult = params.q_sin_mod_mult || 2;
969
- const qAtan2Mult = params.q_atan2_mult || 5, qAtan2Mult2 = params.q_atan2_mult2 || 9;
970
- const pxSinMult = params.px_sin_mult || 30, pxAdd = params.px_add || 200;
971
- const pySinMult = params.py_sin_mult || 80, pyDMult = params.py_d_mult || 79;
972
- const cTDiv = params.c_t_div || 18, cMod = params.c_mod || 3, cModMult = params.c_mod_mult || 4;
973
- const trigKCos1 = params.trig_k_cos1 || 'cos';
974
- const trigKCos2 = params.trig_k_cos2 || 'cos';
975
- const trigQSin = params.trig_q_sin || 'sin';
976
- const trigQAtan2 = params.trig_q_atan2 || 'atan';
977
- const trigPxSin = params.trig_px_sin || 'sin';
978
- const trigPySin = params.trig_py_sin || 'sin';
979
- const trigStrokeCos = params.trig_stroke_cos || 'cos';
980
- const patternCenterX = pxAdd * zoomFactor;
981
- const patternCenterY = (pyDMult * 50) * zoomFactor;
982
- const step = FRAME_SAMPLING_STEPS[currentBaseSet] || DEFAULT_FRAME_SAMPLING_STEP;
983
- const maxI = LOOP_BOUNDS[currentBaseSet] || 20000;
984
- for(let i = maxI; i > 0; i -= step){
985
- let k = kMult * trig(trigKCos1, i / kCosDiv1) * trig(trigKCos2, i / kCosDiv2);
986
- let e = i / eDiv - eSub;
987
- let d = (Math.hypot(k, e) ** dPow / dDiv) + dAdd;
988
- let cosVal = trig(trigStrokeCos, t + e);
989
- let stroke_r = 36 + 3 / (Math.abs(cosVal) > 0.001 ? cosVal : 0.001);
990
- p.stroke(255, Math.min(255, Math.max(0, stroke_r)));
991
- let c = d - t / cTDiv + (i % cMod) * cModMult;
992
- let px = (k * (qKMult + trig(trigQSin, d * qSinDMult - t * qSinTMult + (i % qSinMod) * qSinModMult)) - qAtan2Mult * Math.atan2(k, e) * qAtan2Mult2 + pxSinMult * trig(trigPxSin, c) + pxAdd) * zoomFactor;
993
- let py = (pySinMult * trig(trigPySin, c - d) + d * pyDMult) * zoomFactor;
994
- p.point(px - patternCenterX + centerX, py - patternCenterY + centerY);
995
- }
996
- */ } else if(currentBaseSet === 'yuruyurau_4') {
997
- // Render yuruyurau #4
998
- p.stroke(255, 96);
999
- const zoomFactor = 0.3;
1000
- const kMult = params.k_mult || 5, kCosDiv = params.k_cos_div || 8;
1001
- const eMult = params.e_mult || 5, eCosDiv = params.e_cos_div || 9;
1002
- const dDivBase = params.d_div_base || 6, dPow = params.d_pow || 4, dAdd = params.d_add || 4;
1003
- const qKMult = params.q_k_mult || 3, qEDiv = params.q_e_div || 2;
1004
- const qSinMult = params.q_sin_mult || 3, qSinKDDiv = params.q_sin_kd_div || 3, qBitwiseMult = params.q_bitwise_mult || 70;
1005
- const pxSinMult = params.px_sin_mult || 1, pxAdd = params.px_add || 200;
1006
- const pyCosAdd1 = params.py_cos_add1 || 7, pyAdd = params.py_add || 200;
1007
- const cTDiv = params.c_t_div || 9, cMod1 = params.c_mod1 || 5, cMod1Mult = params.c_mod1_mult || 5, cMod2 = params.c_mod2 || 2;
1008
- const trigKCos = params.trig_k_cos || 'cos';
1009
- const trigECos = params.trig_e_cos || 'cos';
1010
- const trigQSin = params.trig_q_sin || 'sin';
1011
- const trigQSin2 = params.trig_q_sin2 || 'sin';
1012
- const trigPxSin = params.trig_px_sin || 'sin';
1013
- const trigPyCos = params.trig_py_cos || 'cos';
1014
- const patternCenterX = pxAdd * zoomFactor;
1015
- const patternCenterY = pyAdd * zoomFactor;
1016
- const step = FRAME_SAMPLING_STEPS[currentBaseSet] || DEFAULT_FRAME_SAMPLING_STEP;
1017
- const maxI = LOOP_BOUNDS[currentBaseSet] || 20000;
1018
- for(let i = maxI; i > 0; i -= step){
1019
- let y = i / 799;
1020
- let k = kMult * trig(trigKCos, i / kCosDiv);
1021
- let e = eMult * trig(trigECos, y / eCosDiv);
1022
- let d = (Math.hypot(k, e) / (dDivBase + (i % 5))) ** dPow + dAdd;
1023
- let q = k * (qKMult + e / qEDiv * trig(trigQSin, d * d - t)) - qSinMult * trig(trigQSin2, k * d / qSinKDDiv) - (~(i & 1)) * qBitwiseMult;
1024
- let c = d - t / cTDiv + (i % cMod1) * cMod1Mult + (i % cMod2);
1025
- let px = (q * trig(trigPxSin, c) + pxAdd) * zoomFactor;
1026
- let py = (q * trig(trigPyCos, c - (i % cMod2) + (i % cMod1) * 3 + pyCosAdd1) + pyAdd) * zoomFactor;
1027
- p.point(px - patternCenterX + centerX, py - patternCenterY + centerY);
1028
- }
1029
- } else {
1030
- // Render "always_finding_yourself" equations
1031
- p.stroke(255, 40); // Same as base preview
1032
- const zoomFactor = 0.2; // Same as base
1033
- const kDiv = params.k_div, kSub = params.k_sub, eDiv = params.e_div, eAdd = params.e_add ?? 9;
1034
- const oDiv = params.o_div, xOff = params.x_offset, yTrigDiv = params.y_trig_div;
1035
- const qMult = params.q_mult, pxAdd = params.px_add, pyAdd = params.py_add, pyQDiv = params.py_q_div;
1036
- const trigY = params.trig_y, trigE = params.trig_e, trigO = params.trig_o;
1037
- const trigCSin = params.trig_c_sin, trigC4Cos = params.trig_c4_cos, trigCCos = params.trig_c_cos;
1038
-
1039
- // Pattern center in world coordinates (pxAdd and pyAdd are the base offsets)
1040
- const patternCenterX = pxAdd * zoomFactor;
1041
- const patternCenterY = pyAdd * zoomFactor;
1042
-
1043
- // Same point count and step as base preview (6400 points, i += 3)
1044
- const step = FRAME_SAMPLING_STEPS[currentBaseSet] || DEFAULT_FRAME_SAMPLING_STEP;
1045
- const maxI = LOOP_BOUNDS[currentBaseSet] || 6400;
1046
- for(let i=0;i<maxI;i+=step){
1047
- let x=i%200, y=i/200;
1048
- let k=x/kDiv-kSub; let ksafe=Math.abs(k)>1e-6?k:1e-6;
1049
- let e=y/eDiv+eAdd;
1050
- let o=Math.hypot(k,e)/oDiv;
1051
- let q=x+xOff+y+1/ksafe + o*k*(trig(trigY,y)/yTrigDiv + trig(trigE,e)) * trig(trigO,o*4 - t);
1052
- let c=o + e/99 - t/8 + (i%2)*3;
1053
- let px=(qMult*q*trig(trigCSin,c) + pxAdd) * zoomFactor;
1054
- let py=(pyAdd + y/2*trig(trigC4Cos,c*4 - o) - q/pyQDiv*trig(trigCCos,c - 1)) * zoomFactor;
1055
- // Center the pattern in the canvas
1056
- p.point(px - patternCenterX + centerX, py - patternCenterY + centerY);
1057
- }
1058
  }
1059
  };
1060
  });
@@ -1410,6 +1181,17 @@
1410
  equation += ` px = q*${getParam('trig_px_sin')}(c) + ${getParam('px_add')}\n`;
1411
  equation += ` py = q*${getParam('trig_py_cos')}(c-i%${getParam('c_mod2')}+i%${getParam('c_mod1')}*3+${getParam('py_cos_add1')}) + ${getParam('py_add')}\n`;
1412
  equation += ` point(px,py)`;
 
 
 
 
 
 
 
 
 
 
 
1413
  } else {
1414
  // Original equations (always_finding_yourself)
1415
  equation += 'for i in range(0, 6400, 3):\n';
@@ -1452,6 +1234,103 @@
1452
  // Make download function global
1453
  window.downloadEquation = downloadEquation;
1454
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1455
  // Initialize the application
1456
  updateMutationProbChart(); // Initialize chart
1457
  updateTrigProbIndicator(); // Initialize trig prob indicator
 
44
  .btn{background:#000;color:#fff;border:1px solid #666;padding:8px 16px;margin:0;cursor:pointer;font-family:monospace;font-size:11px;text-transform:uppercase;letter-spacing:0.5px}
45
  .btn:hover{background:#333;border-color:#999}
46
  .btn:disabled{background:#111;color:#666;border-color:#444;cursor:not-allowed}
47
+ .eq-chip{font-family:monospace;font-size:8px;color:#bbb;border:1px solid #444;padding:2px 6px;cursor:grab;background:#111;user-select:none;white-space:nowrap}
48
+ .eq-chip:hover{border-color:#aaa;color:#fff}
49
+ .eq-chip:active{cursor:grabbing}
50
+ .eq-chip-in-zone{display:inline-flex;align-items:center;gap:4px;border:1px solid #fff;padding:2px 6px;font-size:8px;font-family:monospace;color:#fff;background:#222}
51
+ .eq-chip-remove{cursor:pointer;color:#999;font-size:10px;line-height:1;padding:0 1px}
52
+ .eq-chip-remove:hover{color:#fff}
53
+ #merge-drop-zone{min-height:36px;border:1px dashed #444;padding:5px;background:#000;font-size:8px;color:#555;display:flex;align-items:center;gap:6px;flex-wrap:wrap;transition:border-color 0.2s,background 0.2s}
54
+ #merge-drop-zone.drag-over{border-color:#fff;background:#111}
55
  </style>
56
  </head>
57
  <body>
 
78
  <!-- <option value="yuruyurau_2">yuruyurau #2</option> -->
79
  <!-- <option value="yuruyurau_3">yuruyurau #3</option> -->
80
  <option value="yuruyurau_4">#2</option>
81
+ <option value="yuruyurau_5">#3</option>
82
  </select>
83
  </div>
84
 
85
+ <div style="margin-bottom:15px;">
86
+ <label style="font-size:9px;color:#666;text-transform:lowercase;letter-spacing:1px;display:block;margin-bottom:5px;">merge equations:</label>
87
+ <div id="equation-chips" style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:6px;">
88
+ <div class="eq-chip" draggable="true" data-eq="always_finding_yourself">always_finding</div>
89
+ <div class="eq-chip" draggable="true" data-eq="a_constant_state_of_bliss">a constant state</div>
90
+ <div class="eq-chip" draggable="true" data-eq="yuruyurau">#1</div>
91
+ <div class="eq-chip" draggable="true" data-eq="yuruyurau_4">#2</div>
92
+ <div class="eq-chip" draggable="true" data-eq="yuruyurau_5">#3</div>
93
+ </div>
94
+ <div id="merge-drop-zone">
95
+ <span id="merge-drop-hint" style="pointer-events:none;">drag to layer a second equation</span>
96
+ </div>
97
+ </div>
98
+
99
  <div style="margin-bottom:15px;">
100
  <label style="font-size:9px;color:#666;text-transform:lowercase;letter-spacing:1px;display:block;margin-bottom:5px;">trigonometric mutation probability:</label>
101
  <div style="display:flex;gap:4px;align-items:center;height:20px;">
 
214
 
215
  <script>
216
  let variants=[]; let selectedIds=new Set();
217
+ let mergedSecondarySet = null; // Secondary equation for merge mode
218
  let currentParams = {
219
  k_div: 4, k_sub: 10.5, e_div: 9, e_add: 9, o_div: 9, x_offset: 60,
220
  y_trig_div: 8, q_mult: 0.7, px_add: 200, py_add: 200, py_q_div: 3,
 
224
  let currentBaseSet = 'always_finding_yourself'; // Current base equation set
225
  let variantSketches = []; // Track p5 instances for variants to clean them up
226
  let sketchBaseSet = null; // Which base set the current sketches were built for
227
+ let sketchMergedSet = null; // Which merged secondary set the current sketches were built for
228
  // Shared mutable param objects β€” p5 sketches hold references so we can update without recreation
229
  const variantParamSlots = Array.from({length: 16}, () => ({}));
230
  let currentMutations = []; // Track mutations for highlighting
 
248
  'yuruyurau': 6, // for(let i = 0; i < 12000; i += 6)
249
  'yuruyurau_2': 1, // for(let i = 30000; i > 0; i--)
250
  'yuruyurau_3': 1, // for(let i = 20000; i > 0; i--)
251
+ 'yuruyurau_4': 1, // for(let i = 20000; i > 0; i--)
252
+ 'yuruyurau_5': 1 // for(let i = 20000; i > 0; i--)
253
  };
254
  const LOOP_BOUNDS = {
255
  'always_finding_yourself': 6400,
 
257
  'yuruyurau': 12000,
258
  'yuruyurau_2': 10000,
259
  'yuruyurau_3': 10000,
260
+ 'yuruyurau_4': 1000,
261
+ 'yuruyurau_5': 20000
262
  };
263
 
264
  // O(1) trig dispatch β€” avoids per-point if-else chain
 
292
  currentBaseSet = data.current_base_set;
293
  document.getElementById('base-equation-selector').value = currentBaseSet;
294
  }
295
+ if('merged_secondary_set' in data) {
296
+ mergedSecondarySet = data.merged_secondary_set;
297
+ updateMergeZoneUI();
298
+ }
299
  if(data.trig_mutation_prob !== undefined) {
300
  trigMutationProb = data.trig_mutation_prob;
301
  updateMutationProbChart();
 
410
  .then(data=>{
411
  if(data.success) {
412
  currentBaseSet = newSet;
413
+ mergedSecondarySet = null; // Backend clears merge on base set change
414
+ updateMergeZoneUI();
415
  loadPopulation();
416
  } else {
417
  alert('Error changing base equation set: '+data.error);
 
484
  equation += `&nbsp;&nbsp;px = q*${getTrigSpan('trig_px_sin')}(c) + ${getParamSpan('px_add')}<br>`;
485
  equation += `&nbsp;&nbsp;py = q*${getTrigSpan('trig_py_cos')}(c-i%${getParamSpan('c_mod2')}+i%${getParamSpan('c_mod1')}*3+${getParamSpan('py_cos_add1')}) + ${getParamSpan('py_add')}<br>`;
486
  equation += `&nbsp;&nbsp;point(px,py)`;
487
+ } else if(currentBaseSet === 'yuruyurau_5') {
488
+ // yuruyurau #3
489
+ equation += 'for i in range(20000, 0, -1):<br>';
490
+ equation += `&nbsp;&nbsp;m = (i%2)*${getParamSpan('m_mult')}<br>`;
491
+ equation += `&nbsp;&nbsp;k = ${getParamSpan('k_mult')}*${getTrigSpan('trig_k_cos')}(i/${getParamSpan('k_cos_div')})<br>`;
492
+ equation += `&nbsp;&nbsp;e = i/${getParamSpan('e_div')} - ${getParamSpan('e_sub')}<br>`;
493
+ equation += `&nbsp;&nbsp;d = mag(k,e)^${getParamSpan('d_pow')}/${getParamSpan('d_div')} + ${getParamSpan('d_add')}<br>`;
494
+ equation += `&nbsp;&nbsp;q = ${getParamSpan('q_base')} - e/${getParamSpan('q_e_div')}Β·${getTrigSpan('trig_q_sin1')}(k/dΒ·${getParamSpan('q_sin1_mult')}) + k/dΒ·(${getParamSpan('q_k_mult')}+${getParamSpan('q_sin2_mult')}Β·${getTrigSpan('trig_q_sin2')}(${getTrigSpan('trig_q_sin3')}(dΒ²+e/${getParamSpan('q_sin_e_div')}-t+m)))<br>`;
495
+ equation += `&nbsp;&nbsp;c = d/${getParamSpan('c_d_div')} + ${getTrigSpan('trig_c_cos')}(t-dΒ·${getParamSpan('c_d2_mult')}+m)/${getParamSpan('c_cos_div')} - t/${getParamSpan('c_t_div')} + m<br>`;
496
+ equation += `&nbsp;&nbsp;px = qΒ·${getTrigSpan('trig_px_cos')}(c) + ${getParamSpan('px_add')}<br>`;
497
+ equation += `&nbsp;&nbsp;py = (q+${getParamSpan('py_q_add')})Β·${getTrigSpan('trig_py_sin')}(c) + ${getParamSpan('py_add')}<br>`;
498
+ equation += `&nbsp;&nbsp;point(px,py)`;
499
  } else {
500
  // Original equations (always_finding_yourself)
501
  equation += 'for i in range(0, 6400, 3):<br>';
 
516
 
517
  let basePreviewSketch = null;
518
 
519
+ // Extract _2_ prefixed params (secondary merged equation) into a plain param object
520
+ function extractMergedParams(params) {
521
+ const result = {};
522
+ for(const [k, v] of Object.entries(params)) {
523
+ if(k.startsWith('_2_')) result[k.slice(3)] = v;
524
+ }
525
+ return result;
526
+ }
527
+
528
+ // Shared rendering function: draws one equation layer's points onto p
529
+ function drawEquationPoints(p, params, baseSet, t, centerX, centerY) {
530
+ const trig = (name, val) => getTrig(name)(val);
531
+
532
+ if(baseSet === 'a_constant_state_of_bliss') {
533
+ const zoomFactor = 0.4;
534
+ const kDiv = params.k_div || 8, kSub = params.k_sub || 11.5;
535
+ const eDiv = params.e_div || 8, eSub = params.e_sub || 12.5;
536
+ const oDiv = params.o_div || 139, dMult = params.d_mult || 10;
537
+ const pxDiv = params.px_div || 2, pxAdd1 = params.px_add1 || 150;
538
+ const pyYDiv = params.py_y_div || 9, pyDMult = params.py_d_mult || 15;
539
+ const pyCosMult = params.py_cos_mult || 2, pyAdd = params.py_add || 220;
540
+ const strokeBase = params.stroke_base || 99, strokeMult = params.stroke_mult || 99, strokePow = params.stroke_pow || 30;
541
+ const trigDCos = params.trig_d_cos || 'cos';
542
+ const trigPxSin1 = params.trig_px_sin1 || 'sin';
543
+ const trigPxSin2 = params.trig_px_sin2 || 'sin';
544
+ const trigPyCos = params.trig_py_cos || 'cos';
545
+ const trigPySin = params.trig_py_sin || 'sin';
546
+ const trigStrokeSin1 = params.trig_stroke_sin1 || 'sin';
547
+ const trigStrokeSin2 = params.trig_stroke_sin2 || 'sin';
548
+ const patternCenterX = pxAdd1 * zoomFactor, patternCenterY = pyAdd * zoomFactor;
549
+ const step = FRAME_SAMPLING_STEPS[baseSet] || DEFAULT_FRAME_SAMPLING_STEP;
550
+ const maxI = LOOP_BOUNDS[baseSet] || 6400;
551
+ for(let i = 0; i < maxI; i += step){
552
+ let x = i % 200, y = i / 200;
553
+ let k = x / kDiv - kSub, e = y / eDiv - eSub;
554
+ let o = Math.hypot(k, e) ** 2 / oDiv;
555
+ let d = dMult * trig(trigDCos, o);
556
+ let ksafe = Math.abs(k) > 1e-6 ? k : 1e-6;
557
+ let stroke_r = strokeBase + strokeMult / trig(trigStrokeSin1, ksafe) * trig(trigStrokeSin2, t + e) ** strokePow;
558
+ p.stroke(stroke_r, 66);
559
+ let px = ((x + trig(trigPxSin1, d) * d * k) / pxDiv + pxAdd1 + o * k * trig(trigPxSin2, t + d * o)) * zoomFactor;
560
+ let py = (y / pyYDiv - d * pyDMult - trig(trigPyCos, d * pyCosMult) * d + pyAdd + d * trig(trigPySin, d - t)) * zoomFactor;
561
+ p.point(px - patternCenterX + centerX, py - patternCenterY + centerY);
562
+ }
563
+ } else if(baseSet === 'yuruyurau') {
564
+ p.stroke(255, 96);
565
+ const zoomFactor = 0.3;
566
+ const kBase = params.k_base || 4, kSinMult = params.k_sin_mult || 3, kCosDiv = params.k_cos_div || 29;
567
+ const eDiv = params.e_div || 8, eSub = params.e_sub || 13;
568
+ const qSin1Mult = params.q_sin1_mult || 3, qSin1Mult2 = params.q_sin1_mult2 || 2, qDiv = params.q_div || 0.3;
569
+ const qSin2Div = params.q_sin2_div || 25, qBase = params.q_base || 9;
570
+ const qSin3Mult = params.q_sin3_mult || 4, qSin3EMult = params.q_sin3_e_mult || 9;
571
+ const qSin3DMult = params.q_sin3_d_mult || 3, qSin3TMult = params.q_sin3_t_mult || 2;
572
+ const pxCosMult = params.px_cos_mult || 30, pxAdd = params.px_add || 200;
573
+ const pyDMult = params.py_d_mult || 39, pySub = params.py_sub || 220;
574
+ const trigKSin = params.trig_k_sin || 'sin', trigKCos = params.trig_k_cos || 'cos';
575
+ const trigQSin1 = params.trig_q_sin1 || 'sin', trigQSin2 = params.trig_q_sin2 || 'sin';
576
+ const trigQSin3 = params.trig_q_sin3 || 'sin', trigPxCos = params.trig_px_cos || 'cos';
577
+ const trigPySin = params.trig_py_sin || 'sin';
578
+ const patternCenterX = pxAdd * zoomFactor, patternCenterY = (pySub / 2) * zoomFactor;
579
+ const step = FRAME_SAMPLING_STEPS[baseSet] || DEFAULT_FRAME_SAMPLING_STEP;
580
+ const maxI = LOOP_BOUNDS[baseSet] || 10000;
581
+ for(let i = 0; i < maxI; i += step){
582
+ let x = i, y = i / 235;
583
+ let k = (kBase + trig(trigKSin, y * 2 - t) * kSinMult) * trig(trigKCos, x / kCosDiv);
584
+ let e = y / eDiv - eSub, d = Math.hypot(k, e);
585
+ let ksafe = Math.abs(k) > 1e-6 ? k : 1e-6;
586
+ let q = qSin1Mult * trig(trigQSin1, k * qSin1Mult2) + qDiv / ksafe + trig(trigQSin2, y / qSin2Div) * k * (qBase + qSin3Mult * trig(trigQSin3, e * qSin3EMult - d * qSin3DMult + t * qSin3TMult));
587
+ let c = d - t;
588
+ let px = (q + pxCosMult * trig(trigPxCos, c) + pxAdd) * zoomFactor;
589
+ let py = (q * trig(trigPySin, c) + d * pyDMult - pySub) * zoomFactor;
590
+ p.point(px - patternCenterX + centerX, py - patternCenterY + centerY);
591
+ }
592
+ } else if(baseSet === 'yuruyurau_4') {
593
+ p.stroke(255, 96);
594
+ const zoomFactor = 0.3;
595
+ const kMult = params.k_mult || 5, kCosDiv = params.k_cos_div || 8;
596
+ const eMult = params.e_mult || 5, eCosDiv = params.e_cos_div || 9;
597
+ const dDivBase = params.d_div_base || 6, dPow = params.d_pow || 4, dAdd = params.d_add || 4;
598
+ const qKMult = params.q_k_mult || 3, qEDiv = params.q_e_div || 2;
599
+ const qSinMult = params.q_sin_mult || 3, qSinKDDiv = params.q_sin_kd_div || 3, qBitwiseMult = params.q_bitwise_mult || 70;
600
+ const pxSinMult = params.px_sin_mult || 1, pxAdd = params.px_add || 200;
601
+ const pyCosAdd1 = params.py_cos_add1 || 7, pyAdd = params.py_add || 200;
602
+ const cTDiv = params.c_t_div || 9, cMod1 = params.c_mod1 || 5, cMod1Mult = params.c_mod1_mult || 5, cMod2 = params.c_mod2 || 2;
603
+ const trigKCos = params.trig_k_cos || 'cos', trigECos = params.trig_e_cos || 'cos';
604
+ const trigQSin = params.trig_q_sin || 'sin', trigQSin2 = params.trig_q_sin2 || 'sin';
605
+ const trigPxSin = params.trig_px_sin || 'sin', trigPyCos = params.trig_py_cos || 'cos';
606
+ const patternCenterX = pxAdd * zoomFactor, patternCenterY = pyAdd * zoomFactor;
607
+ const step = FRAME_SAMPLING_STEPS[baseSet] || DEFAULT_FRAME_SAMPLING_STEP;
608
+ const maxI = LOOP_BOUNDS[baseSet] || 20000;
609
+ for(let i = maxI; i > 0; i -= step){
610
+ let y = i / 799;
611
+ let k = kMult * trig(trigKCos, i / kCosDiv);
612
+ let e = eMult * trig(trigECos, y / eCosDiv);
613
+ let d = (Math.hypot(k, e) / (dDivBase + (i % 5))) ** dPow + dAdd;
614
+ let q = k * (qKMult + e / qEDiv * trig(trigQSin, d * d - t)) - qSinMult * trig(trigQSin2, k * d / qSinKDDiv) - (~(i & 1)) * qBitwiseMult;
615
+ let c = d - t / cTDiv + (i % cMod1) * cMod1Mult + (i % cMod2);
616
+ let px = (q * trig(trigPxSin, c) + pxAdd) * zoomFactor;
617
+ let py = (q * trig(trigPyCos, c - (i % cMod2) + (i % cMod1) * 3 + pyCosAdd1) + pyAdd) * zoomFactor;
618
+ p.point(px - patternCenterX + centerX, py - patternCenterY + centerY);
619
+ }
620
+ } else if(baseSet === 'yuruyurau_5') {
621
+ // a=(m,d=mag(k=9*cos(i/61),e=i/692-13)**2/99+1)=>point(
622
+ // (q=79-e/2*sin(k/d*4)+k/d*(8+5*sin(sin(d*d+e/9-t+m))))*cos(c=d/2+cos(t-d*2+m)/9-t/16+m)+200,
623
+ // (q+40)*sin(c)+200)
624
+ p.stroke(255, 96);
625
+ const zoomFactor = 0.315;
626
+ const kMult = params.k_mult || 9, kCosDiv = params.k_cos_div || 61;
627
+ const eDiv = params.e_div || 692, eSub = params.e_sub || 13;
628
+ const dPow = params.d_pow || 2, dDiv = params.d_div || 99, dAdd = params.d_add || 1;
629
+ const mMult = params.m_mult || 9;
630
+ const qBase = params.q_base || 79, qEDiv = params.q_e_div || 2, qSin1Mult = params.q_sin1_mult || 4;
631
+ const qKMult = params.q_k_mult || 8, qSin2Mult = params.q_sin2_mult || 5, qSinEDiv = params.q_sin_e_div || 9;
632
+ const cDDiv = params.c_d_div || 2, cD2Mult = params.c_d2_mult || 2, cCosDiv = params.c_cos_div || 9, cTDiv = params.c_t_div || 16;
633
+ const pxAdd = params.px_add || 200, pyQAdd = params.py_q_add || 40, pyAdd = params.py_add || 200;
634
+ const trigKCos = params.trig_k_cos || 'cos';
635
+ const trigQSin1 = params.trig_q_sin1 || 'sin';
636
+ const trigQSin2 = params.trig_q_sin2 || 'sin';
637
+ const trigQSin3 = params.trig_q_sin3 || 'sin';
638
+ const trigCCos = params.trig_c_cos || 'cos';
639
+ const trigPxCos = params.trig_px_cos || 'cos';
640
+ const trigPySin = params.trig_py_sin || 'sin';
641
+ const patternCenterX = pxAdd * zoomFactor, patternCenterY = pyAdd * zoomFactor;
642
+ const step = FRAME_SAMPLING_STEPS[baseSet] || 1;
643
+ const maxI = LOOP_BOUNDS[baseSet] || 20000;
644
+ for(let i = maxI; i--;) {
645
+ let m = (i % 2) * mMult;
646
+ let k = kMult * trig(trigKCos, i / kCosDiv);
647
+ let e = i / eDiv - eSub;
648
+ let d = Math.hypot(k, e) ** dPow / dDiv + dAdd;
649
+ let q = qBase - e / qEDiv * trig(trigQSin1, k / d * qSin1Mult) + k / d * (qKMult + qSin2Mult * trig(trigQSin2, trig(trigQSin3, d * d + e / qSinEDiv - t + m)));
650
+ let c = d / cDDiv + trig(trigCCos, t - d * cD2Mult + m) / cCosDiv - t / cTDiv + m;
651
+ let px = (q * trig(trigPxCos, c) + pxAdd) * zoomFactor;
652
+ let py = ((q + pyQAdd) * trig(trigPySin, c) + pyAdd) * zoomFactor;
653
+ if(isFinite(px) && isFinite(py)) p.point(px - patternCenterX + centerX, py - patternCenterY + centerY);
654
+ }
655
+ } else {
656
+ // always_finding_yourself
657
+ p.stroke(255, 40);
658
+ const zoomFactor = 0.2;
659
+ const kDiv = params.k_div, kSub = params.k_sub, eDiv = params.e_div, eAdd = params.e_add ?? 9;
660
+ const oDiv = params.o_div, xOff = params.x_offset, yTrigDiv = params.y_trig_div;
661
+ const qMult = params.q_mult, pxAdd = params.px_add, pyAdd = params.py_add, pyQDiv = params.py_q_div;
662
+ const trigY = params.trig_y, trigE = params.trig_e, trigO = params.trig_o;
663
+ const trigCSin = params.trig_c_sin, trigC4Cos = params.trig_c4_cos, trigCCos = params.trig_c_cos;
664
+ const patternCenterX = pxAdd * zoomFactor, patternCenterY = pyAdd * zoomFactor;
665
+ const step = FRAME_SAMPLING_STEPS[baseSet] || DEFAULT_FRAME_SAMPLING_STEP;
666
+ const maxI = LOOP_BOUNDS[baseSet] || 6400;
667
+ for(let i = 0; i < maxI; i += step){
668
+ let x = i % 200, y = i / 200;
669
+ let k = x / kDiv - kSub; let ksafe = Math.abs(k) > 1e-6 ? k : 1e-6;
670
+ let e = y / eDiv + eAdd, o = Math.hypot(k, e) / oDiv;
671
+ let q = x + xOff + y + 1/ksafe + o * k * (trig(trigY, y) / yTrigDiv + trig(trigE, e)) * trig(trigO, o * 4 - t);
672
+ let c = o + e / 99 - t / 8 + (i % 2) * 3;
673
+ let px = (qMult * q * trig(trigCSin, c) + pxAdd) * zoomFactor;
674
+ let py = (pyAdd + y / 2 * trig(trigC4Cos, c * 4 - o) - q / pyQDiv * trig(trigCCos, c - 1)) * zoomFactor;
675
+ p.point(px - patternCenterX + centerX, py - patternCenterY + centerY);
676
+ }
677
+ }
678
+ }
679
+
680
  function updateBaseEquationDisplay(){
681
  const container = document.getElementById('base-preview-canvas');
682
 
 
689
  const baseParams = {...currentParams};
690
 
691
  basePreviewSketch = new p5(p => {
 
 
692
  p.setup = () => {
693
+ p.createCanvas(126, 126).parent(container);
694
  p.pixelDensity(1);
695
  p.frameRate(FRAME_RATE);
696
  p.noFill();
 
698
  };
699
 
700
  p.draw = () => {
 
701
  const frame = p.frameCount % ANIMATION_FRAMES;
702
+ const t = (frame / ANIMATION_FRAMES) * Math.PI * 2 * 100;
703
+ p.clear();
704
+ const centerX = p.width / 2, centerY = p.height / 2;
705
+ drawEquationPoints(p, baseParams, currentBaseSet, t, centerX, centerY);
706
+ if(mergedSecondarySet) {
707
+ drawEquationPoints(p, extractMergedParams(baseParams), mergedSecondarySet, t, centerX, centerY);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
708
  }
709
  };
710
  });
 
754
  currentMutations = [];
755
  variants.forEach(v => { if(v.mutations) currentMutations.push(...v.mutations); });
756
 
757
+ // Fast path: same base set + same merge + same count β†’ just update params & labels, no sketch recreation
758
+ if(sketchBaseSet === currentBaseSet && sketchMergedSet === mergedSecondarySet && variantSketches.length === variants.length) {
759
  variants.forEach((v, i) => {
760
  // Update shared param slot β€” the p5 draw loop reads from this reference each frame
761
  Object.assign(variantParamSlots[i], v.params);
 
772
  return;
773
  }
774
 
775
+ // Full rebuild: base set/merge changed or first render
776
  variantSketches.forEach(s => { if(s && typeof s.remove === 'function') s.remove(); });
777
  variantSketches = [];
778
  sketchBaseSet = currentBaseSet;
779
+ sketchMergedSet = mergedSecondarySet;
780
  grid.innerHTML = '';
781
 
782
  variants.forEach((v, i) => {
 
809
 
810
  function renderEquation(container, params){
811
  const sketch = new p5(p=>{
 
 
 
812
  p.setup=()=>{
813
  p.createCanvas(126,126).parent(container);
814
  p.pixelDensity(1);
 
819
  };
820
 
821
  p.draw=()=>{
 
822
  const frame = p.frameCount % ANIMATION_FRAMES;
823
+ const t = (frame / ANIMATION_FRAMES) * Math.PI * 2 * 100;
 
824
  p.background(0);
825
+ const centerX = p.width / 2, centerY = p.height / 2;
826
+ drawEquationPoints(p, params, currentBaseSet, t, centerX, centerY);
827
+ if(mergedSecondarySet) {
828
+ drawEquationPoints(p, extractMergedParams(params), mergedSecondarySet, t, centerX, centerY);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
829
  }
830
  };
831
  });
 
1181
  equation += ` px = q*${getParam('trig_px_sin')}(c) + ${getParam('px_add')}\n`;
1182
  equation += ` py = q*${getParam('trig_py_cos')}(c-i%${getParam('c_mod2')}+i%${getParam('c_mod1')}*3+${getParam('py_cos_add1')}) + ${getParam('py_add')}\n`;
1183
  equation += ` point(px,py)`;
1184
+ } else if(baseSet === 'yuruyurau_5') {
1185
+ equation += 'for i in range(20000, 0, -1):\n';
1186
+ equation += ` m = (i%2) * ${getParam('m_mult')}\n`;
1187
+ equation += ` k = ${getParam('k_mult')}*${getParam('trig_k_cos')}(i/${getParam('k_cos_div')})\n`;
1188
+ equation += ` e = i/${getParam('e_div')} - ${getParam('e_sub')}\n`;
1189
+ equation += ` d = mag(k,e)^${getParam('d_pow')}/${getParam('d_div')} + ${getParam('d_add')}\n`;
1190
+ equation += ` q = ${getParam('q_base')} - e/${getParam('q_e_div')}*${getParam('trig_q_sin1')}(k/d*${getParam('q_sin1_mult')}) + k/d*(${getParam('q_k_mult')}+${getParam('q_sin2_mult')}*${getParam('trig_q_sin2')}(${getParam('trig_q_sin3')}(d*d+e/${getParam('q_sin_e_div')}-t+m)))\n`;
1191
+ equation += ` c = d/${getParam('c_d_div')} + ${getParam('trig_c_cos')}(t-d*${getParam('c_d2_mult')}+m)/${getParam('c_cos_div')} - t/${getParam('c_t_div')} + m\n`;
1192
+ equation += ` px = q*${getParam('trig_px_cos')}(c) + ${getParam('px_add')}\n`;
1193
+ equation += ` py = (q+${getParam('py_q_add')})*${getParam('trig_py_sin')}(c) + ${getParam('py_add')}\n`;
1194
+ equation += ` point(px,py)`;
1195
  } else {
1196
  // Original equations (always_finding_yourself)
1197
  equation += 'for i in range(0, 6400, 3):\n';
 
1234
  // Make download function global
1235
  window.downloadEquation = downloadEquation;
1236
 
1237
+ // ── Merge zone logic ──────────────────────────────────────────────────────
1238
+
1239
+ const EQ_LABELS = {
1240
+ 'always_finding_yourself': 'always_finding',
1241
+ 'a_constant_state_of_bliss': 'a constant state',
1242
+ 'yuruyurau': '#1',
1243
+ 'yuruyurau_4': '#2',
1244
+ 'yuruyurau_5': '#3'
1245
+ };
1246
+
1247
+ function updateMergeZoneUI() {
1248
+ const zone = document.getElementById('merge-drop-zone');
1249
+ const hint = document.getElementById('merge-drop-hint');
1250
+ if(!zone) return;
1251
+ // Clear dynamic chips (keep the hint span)
1252
+ zone.querySelectorAll('.eq-chip-in-zone').forEach(el => el.remove());
1253
+ if(mergedSecondarySet) {
1254
+ if(hint) hint.style.display = 'none';
1255
+ const chip = document.createElement('div');
1256
+ chip.className = 'eq-chip-in-zone';
1257
+ chip.innerHTML = `${EQ_LABELS[mergedSecondarySet] || mergedSecondarySet}<span class="eq-chip-remove" onclick="clearMerge()" title="remove">βœ•</span>`;
1258
+ zone.appendChild(chip);
1259
+ } else {
1260
+ if(hint) hint.style.display = '';
1261
+ }
1262
+ }
1263
+
1264
+ function setMergedSecondary(setName) {
1265
+ if(setName === currentBaseSet) {
1266
+ // Can't merge an equation with itself as secondary β€” silently ignore
1267
+ return;
1268
+ }
1269
+ fetchJSON('/api/set_merged_secondary', {
1270
+ method: 'POST',
1271
+ headers: {'Content-Type': 'application/json'},
1272
+ body: JSON.stringify({set_name: setName})
1273
+ })
1274
+ .then(data => {
1275
+ if(data.success) {
1276
+ mergedSecondarySet = data.merged_secondary_set;
1277
+ updateMergeZoneUI();
1278
+ loadPopulation();
1279
+ }
1280
+ })
1281
+ .catch(err => console.error('Error setting merge:', err));
1282
+ }
1283
+
1284
+ function clearMerge() {
1285
+ fetchJSON('/api/set_merged_secondary', {
1286
+ method: 'POST',
1287
+ headers: {'Content-Type': 'application/json'},
1288
+ body: JSON.stringify({set_name: null})
1289
+ })
1290
+ .then(data => {
1291
+ if(data.success) {
1292
+ mergedSecondarySet = null;
1293
+ updateMergeZoneUI();
1294
+ loadPopulation();
1295
+ }
1296
+ })
1297
+ .catch(err => console.error('Error clearing merge:', err));
1298
+ }
1299
+ window.clearMerge = clearMerge;
1300
+
1301
+ // Set up draggable equation chips
1302
+ document.querySelectorAll('.eq-chip').forEach(chip => {
1303
+ chip.addEventListener('dragstart', e => {
1304
+ e.dataTransfer.setData('eq-set', chip.dataset.eq);
1305
+ e.dataTransfer.effectAllowed = 'copy';
1306
+ });
1307
+ });
1308
+
1309
+ // Set up merge drop zone
1310
+ const mergeDropZone = document.getElementById('merge-drop-zone');
1311
+ if(mergeDropZone) {
1312
+ mergeDropZone.addEventListener('dragover', e => {
1313
+ if(e.dataTransfer.types.includes('eq-set')) {
1314
+ e.preventDefault();
1315
+ e.dataTransfer.dropEffect = 'copy';
1316
+ mergeDropZone.classList.add('drag-over');
1317
+ }
1318
+ });
1319
+ mergeDropZone.addEventListener('dragleave', () => mergeDropZone.classList.remove('drag-over'));
1320
+ mergeDropZone.addEventListener('drop', e => {
1321
+ e.preventDefault();
1322
+ mergeDropZone.classList.remove('drag-over');
1323
+ const setName = e.dataTransfer.getData('eq-set');
1324
+ if(setName) setMergedSecondary(setName);
1325
+ });
1326
+ }
1327
+
1328
+ // Also reset sketchBaseSet when merge changes so renderVariants does a full rebuild
1329
+ const _origSetMergedSecondary = setMergedSecondary;
1330
+ // (sketchBaseSet reset happens via loadPopulation β†’ renderVariants which checks sketchBaseSet)
1331
+
1332
+ // ── End merge zone logic ──────────────────────────────────────────────────
1333
+
1334
  // Initialize the application
1335
  updateMutationProbChart(); // Initialize chart
1336
  updateTrigProbIndicator(); // Initialize trig prob indicator