Chunte HF Staff commited on
Commit
93319a7
Β·
verified Β·
1 Parent(s): 7844b58

Upload index.html

Browse files
Files changed (1) hide show
  1. index.html +275 -40
index.html CHANGED
@@ -476,6 +476,89 @@ function computePositions() {
476
  return positions;
477
  }
478
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
479
  let positions = computePositions();
480
 
481
  // ── Model Data Array ───────────────────────────────────────
@@ -496,6 +579,124 @@ function syncModelData() {
496
  }
497
  }
498
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  // ── Elastic Pull State ──────────────────────────────────────
500
  const elasticDx = new Float64Array(BASE_MODEL_COUNT + 1);
501
  const elasticDy = new Float64Array(BASE_MODEL_COUNT + 1);
@@ -1240,6 +1441,12 @@ function frame() {
1240
  if (labelBounceAlpha <= 0) labelBounceRect = null;
1241
  }
1242
 
 
 
 
 
 
 
1243
  // Update elastic pull (before particles read md[j].x/y)
1244
  updateElasticPull();
1245
 
@@ -1333,7 +1540,18 @@ function frame() {
1333
  // Draw model cores last β€” task icons
1334
  for (let j = 0; j < activeModelCount; j++) {
1335
  const iconCol = (customModelActive && j === CUSTOM_MODEL_IDX) ? ORANGE : COLOR;
1336
- drawTaskIcon(ctx, md[j].x, md[j].y, models[j].task, iconCol);
 
 
 
 
 
 
 
 
 
 
 
1337
  }
1338
 
1339
  // Update pinned label position (follows elastic pull)
@@ -1682,28 +1900,37 @@ function insertCustomModel(customModel) {
1682
  const idx = CUSTOM_MODEL_IDX;
1683
  models[idx] = customModel;
1684
 
 
 
 
 
 
 
1685
  // Compute mass normalization relative to existing base range
1686
  const dl = Math.log(customModel.downloads);
1687
  const rawMass = Math.pow(dl, exponent);
1688
  const normVal = Math.max(0, Math.min(1, (rawMass - massMin) / massRange));
1689
- // Extend massNorm array
1690
  if (massNorm.length <= idx) massNorm.push(normVal);
1691
  else massNorm[idx] = normVal;
1692
 
1693
- // Set orbit parameters
1694
- modelOrbitBase[idx] = 30 + 180 * normVal;
1695
- modelCaptureRadius[idx] = modelOrbitBase[idx] * 1.2;
 
 
 
1696
 
1697
- // Create md entry before activating (so syncModelData can write to it)
1698
  md[idx] = {
1699
  x: 0, y: 0,
1700
  massNorm: normVal,
1701
- captureRadius: modelCaptureRadius[idx]
1702
  };
1703
 
1704
- // Activate
1705
- activeModelCount = BASE_MODEL_COUNT + 1;
1706
- customModelActive = true;
 
1707
 
1708
  // Recompute total mass norm and spawn weights
1709
  totalMassNorm = 0;
@@ -1715,20 +1942,46 @@ function insertCustomModel(customModel) {
1715
  }
1716
  totalSpawnWeight = totalMassNorm;
1717
 
1718
- // Recompute positions and sync all model data
1719
- positions = computePositions();
1720
- syncModelData();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1721
  computeNeighbors();
1722
 
1723
- // Reset elastic state
1724
- elasticDx.fill(0); elasticDy.fill(0);
1725
- elasticVx.fill(0); elasticVy.fill(0);
 
 
 
 
 
1726
 
1727
  // Set rotation direction (random)
1728
  modelRotDir[idx] = Math.random() < 0.5 ? 1 : -1;
1729
  }
1730
 
1731
- // ── removeCustomModel: eject particles, deactivate ──────────
1732
  function removeCustomModel() {
1733
  if (!customModelActive) return;
1734
  const idx = CUSTOM_MODEL_IDX;
@@ -1737,7 +1990,6 @@ function removeCustomModel() {
1737
  for (let i = 0; i < PARTICLE_COUNT; i++) {
1738
  const p = particles[i];
1739
  if (p.attractorIdx === idx) {
1740
- // Convert orbital state to cartesian velocity
1741
  const m = md[idx];
1742
  const r = p.orbitRadius || 1;
1743
  const rawOmega = p.angularMomentum / (r * r);
@@ -1746,32 +1998,15 @@ function removeCustomModel() {
1746
  p.vy = Math.cos(p.angle) * omega * r;
1747
  p.phase = 0;
1748
  p.attractorIdx = -1;
1749
- // orangeBlend will decay naturally in updateParticle
1750
  }
1751
  }
1752
 
1753
- // Deactivate
1754
- activeModelCount = BASE_MODEL_COUNT;
1755
- customModelActive = false;
1756
 
1757
- // Recompute spawn weights for base models only
1758
- totalMassNorm = 0;
1759
- for (let j = 0; j < activeModelCount; j++) totalMassNorm += massNorm[j];
1760
- const totalTgt = PARTICLE_COUNT * TARGET_ORBIT_FRACTION;
1761
- for (let j = 0; j < activeModelCount; j++) {
1762
- modelTargetPop[j] = totalTgt * (massNorm[j] / totalMassNorm);
1763
- spawnWeight[j] = massNorm[j];
1764
- }
1765
- totalSpawnWeight = totalMassNorm;
1766
-
1767
- // Recompute layout
1768
- positions = computePositions();
1769
- syncModelData();
1770
- computeNeighbors();
1771
-
1772
- // Reset elastic state
1773
- elasticDx.fill(0); elasticDy.fill(0);
1774
- elasticVx.fill(0); elasticVy.fill(0);
1775
  }
1776
 
1777
  })();
 
476
  return positions;
477
  }
478
 
479
+ // ── Find best open position for custom model (center-biased) ─
480
+ function findOpenPosition(idx) {
481
+ const w = canvas.width / DPR;
482
+ const h = canvas.height / DPR;
483
+ const cx = w / 2, cy = h / 2;
484
+ const myR = modelCaptureRadius[idx];
485
+ const margin = myR + EDGE_MARGIN;
486
+ const cols = 20, rows = 20;
487
+ let bestX = cx, bestY = cy, bestScore = -Infinity;
488
+ const maxDist = Math.sqrt(cx * cx + cy * cy);
489
+
490
+ for (let r = 0; r < rows; r++) {
491
+ for (let c = 0; c < cols; c++) {
492
+ const px = margin + (w - 2 * margin) * (c / (cols - 1));
493
+ const py = margin + (h - 2 * margin) * (r / (rows - 1));
494
+ // Minimum clearance from any existing model
495
+ let minClear = Infinity;
496
+ for (let j = 0; j < activeModelCount; j++) {
497
+ if (j === idx) continue;
498
+ const dx = px - positions[j].x, dy = py - positions[j].y;
499
+ const dist = Math.sqrt(dx * dx + dy * dy) - modelCaptureRadius[j] - myR;
500
+ if (dist < minClear) minClear = dist;
501
+ }
502
+ // Score: 70% center proximity, 30% clearance
503
+ const centerScore = 1 - Math.sqrt((px - cx) ** 2 + (py - cy) ** 2) / maxDist;
504
+ const clearScore = Math.max(0, Math.min(minClear / 200, 1));
505
+ const score = centerScore * 0.7 + clearScore * 0.3;
506
+ if (score > bestScore) {
507
+ bestScore = score;
508
+ bestX = px; bestY = py;
509
+ }
510
+ }
511
+ }
512
+ return { x: bestX, y: bestY };
513
+ }
514
+
515
+ // ── Separate models in-place (no spiral reassignment) ────────
516
+ // Starts from current positions, only resolves overlaps locally.
517
+ // Models move the minimum distance needed β€” no cross-screen flights.
518
+ function separateInPlace() {
519
+ const cx = (canvas.width / DPR) / 2;
520
+ const cy = (canvas.height / DPR) / 2;
521
+ // Clone current positions as starting points
522
+ const result = [];
523
+ for (let i = 0; i < activeModelCount; i++) {
524
+ result.push({ x: positions[i].x, y: positions[i].y });
525
+ }
526
+
527
+ // Run separation solver (same logic as computePositions but no spiral init)
528
+ for (let iter = 0; iter < 200; iter++) {
529
+ let moved = false;
530
+ for (let a = 0; a < activeModelCount; a++) {
531
+ for (let b = a + 1; b < activeModelCount; b++) {
532
+ const dx = result[b].x - result[a].x;
533
+ const dy = result[b].y - result[a].y;
534
+ const dist = Math.sqrt(dx * dx + dy * dy) || 1;
535
+ const minDist = modelCaptureRadius[a] + modelCaptureRadius[b] + SEPARATION_PAD;
536
+ if (dist < minDist) {
537
+ const overlap = (minDist - dist) / 2;
538
+ const nx = dx / dist;
539
+ const ny = dy / dist;
540
+ const wA = 1 - massNorm[a] * 0.7;
541
+ const wB = 1 - massNorm[b] * 0.7;
542
+ const total = wA + wB;
543
+ result[a].x -= nx * overlap * (wA / total);
544
+ result[a].y -= ny * overlap * (wA / total);
545
+ result[b].x += nx * overlap * (wB / total);
546
+ result[b].y += ny * overlap * (wB / total);
547
+ moved = true;
548
+ }
549
+ }
550
+ }
551
+ // Edge clamping
552
+ for (let i = 0; i < activeModelCount; i++) {
553
+ const r = modelCaptureRadius[i];
554
+ result[i].x = Math.max(r + EDGE_MARGIN, Math.min(canvas.width / DPR - r - EDGE_MARGIN, result[i].x));
555
+ result[i].y = Math.max(r + EDGE_MARGIN, Math.min(canvas.height / DPR - r - EDGE_MARGIN, result[i].y));
556
+ }
557
+ if (!moved) break;
558
+ }
559
+ return result;
560
+ }
561
+
562
  let positions = computePositions();
563
 
564
  // ── Model Data Array ───────────────────────────────────────
 
579
  }
580
  }
581
 
582
+ // ── Smooth Layout Animation State ────────────────────────────
583
+ const layoutTargetX = new Float64Array(BASE_MODEL_COUNT + 1);
584
+ const layoutTargetY = new Float64Array(BASE_MODEL_COUNT + 1);
585
+ const layoutActive = new Uint8Array(BASE_MODEL_COUNT + 1);
586
+ const LAYOUT_LERP = 0.07;
587
+
588
+ // Saved positions before custom model insertion (for precise return on removal)
589
+ const preInsertX = new Float64Array(BASE_MODEL_COUNT);
590
+ const preInsertY = new Float64Array(BASE_MODEL_COUNT);
591
+
592
+ // Orbit scale animation (shrinks all models when custom model is added)
593
+ let orbitScale = 1.0;
594
+ let orbitScaleTarget = 1.0;
595
+ const ORBIT_SCALE_LERP = 0.05;
596
+
597
+ // Store original orbit bases (at scale 1.0)
598
+ const orbitBaseOriginal = new Float64Array(BASE_MODEL_COUNT + 1);
599
+ for (let i = 0; i < BASE_MODEL_COUNT; i++) {
600
+ orbitBaseOriginal[i] = modelOrbitBase[i];
601
+ }
602
+
603
+ function applyOrbitScale(scale) {
604
+ for (let j = 0; j < activeModelCount; j++) {
605
+ modelOrbitBase[j] = orbitBaseOriginal[j] * scale;
606
+ modelCaptureRadius[j] = modelOrbitBase[j] * 1.2;
607
+ md[j].captureRadius = modelCaptureRadius[j];
608
+ }
609
+ }
610
+
611
+ function updateLayoutAnimation() {
612
+ // Animate orbit scale
613
+ if (Math.abs(orbitScale - orbitScaleTarget) > 0.001) {
614
+ orbitScale += (orbitScaleTarget - orbitScale) * ORBIT_SCALE_LERP;
615
+ applyOrbitScale(orbitScale);
616
+ }
617
+ // Animate positions toward targets
618
+ for (let j = 0; j < activeModelCount; j++) {
619
+ if (!layoutActive[j]) continue;
620
+ const dx = layoutTargetX[j] - positions[j].x;
621
+ const dy = layoutTargetY[j] - positions[j].y;
622
+ if (dx * dx + dy * dy < 1) {
623
+ positions[j].x = layoutTargetX[j];
624
+ positions[j].y = layoutTargetY[j];
625
+ layoutActive[j] = 0;
626
+ } else {
627
+ positions[j].x += dx * LAYOUT_LERP;
628
+ positions[j].y += dy * LAYOUT_LERP;
629
+ }
630
+ }
631
+ }
632
+
633
+ // ── Pop Scale Animation (custom model) ──────────────────────
634
+ let customPopScale = 0;
635
+ let customPopTarget = 0;
636
+ let customPopVelocity = 0;
637
+ const POP_STIFFNESS = 0.08;
638
+ const POP_DAMPING = 0.72;
639
+
640
+ function updatePopAnimation() {
641
+ if (!customModelActive && customPopScale <= 0.001) return;
642
+ const force = (customPopTarget - customPopScale) * POP_STIFFNESS;
643
+ customPopVelocity = customPopVelocity * POP_DAMPING + force;
644
+ customPopScale += customPopVelocity;
645
+ if (customPopScale < 0) { customPopScale = 0; customPopVelocity = 0; }
646
+ // Scale custom model's capture radius with pop (on top of orbit scale)
647
+ if (customModelActive && customPopTarget === 1) {
648
+ const idx = CUSTOM_MODEL_IDX;
649
+ const baseR = orbitBaseOriginal[idx] * orbitScale;
650
+ modelOrbitBase[idx] = baseR;
651
+ modelCaptureRadius[idx] = baseR * 1.2 * customPopScale;
652
+ md[idx].captureRadius = modelCaptureRadius[idx];
653
+ }
654
+ // When shrinking and settled, finalize removal
655
+ if (customPopTarget === 0 && customPopScale < 0.001 && Math.abs(customPopVelocity) < 0.001) {
656
+ customPopScale = 0;
657
+ customPopVelocity = 0;
658
+ finalizeRemoval();
659
+ }
660
+ }
661
+
662
+ function finalizeRemoval() {
663
+ // Safety: eject any particles still referencing the custom model
664
+ const cidx = CUSTOM_MODEL_IDX;
665
+ for (let i = 0; i < PARTICLE_COUNT; i++) {
666
+ const p = particles[i];
667
+ if (p.attractorIdx === cidx) {
668
+ p.phase = 0;
669
+ p.attractorIdx = -1;
670
+ }
671
+ }
672
+ activeModelCount = BASE_MODEL_COUNT;
673
+ customModelActive = false;
674
+ totalMassNorm = 0;
675
+ for (let j = 0; j < activeModelCount; j++) totalMassNorm += massNorm[j];
676
+ const totalTgt = PARTICLE_COUNT * TARGET_ORBIT_FRACTION;
677
+ for (let j = 0; j < activeModelCount; j++) {
678
+ modelTargetPop[j] = totalTgt * (massNorm[j] / totalMassNorm);
679
+ spawnWeight[j] = massNorm[j];
680
+ }
681
+ totalSpawnWeight = totalMassNorm;
682
+ // Scale orbits back to full size
683
+ orbitScaleTarget = 1.0;
684
+ // Return base models to their exact pre-insertion positions
685
+ for (let j = 0; j < activeModelCount; j++) {
686
+ const dx = preInsertX[j] - positions[j].x;
687
+ const dy = preInsertY[j] - positions[j].y;
688
+ if (dx * dx + dy * dy > 1) {
689
+ layoutTargetX[j] = preInsertX[j];
690
+ layoutTargetY[j] = preInsertY[j];
691
+ layoutActive[j] = 1;
692
+ } else {
693
+ positions[j].x = preInsertX[j];
694
+ positions[j].y = preInsertY[j];
695
+ }
696
+ }
697
+ computeNeighbors();
698
+ }
699
+
700
  // ── Elastic Pull State ──────────────────────────────────────
701
  const elasticDx = new Float64Array(BASE_MODEL_COUNT + 1);
702
  const elasticDy = new Float64Array(BASE_MODEL_COUNT + 1);
 
1441
  if (labelBounceAlpha <= 0) labelBounceRect = null;
1442
  }
1443
 
1444
+ // Animate pop scale for custom model
1445
+ updatePopAnimation();
1446
+
1447
+ // Animate layout transitions (position + orbit scale)
1448
+ updateLayoutAnimation();
1449
+
1450
  // Update elastic pull (before particles read md[j].x/y)
1451
  updateElasticPull();
1452
 
 
1540
  // Draw model cores last β€” task icons
1541
  for (let j = 0; j < activeModelCount; j++) {
1542
  const iconCol = (customModelActive && j === CUSTOM_MODEL_IDX) ? ORANGE : COLOR;
1543
+ if (customModelActive && j === CUSTOM_MODEL_IDX && customPopScale < 1) {
1544
+ const sc = customPopScale;
1545
+ if (sc > 0.01) {
1546
+ ctx.save();
1547
+ ctx.translate(md[j].x, md[j].y);
1548
+ ctx.scale(sc, sc);
1549
+ drawTaskIcon(ctx, 0, 0, models[j].task, iconCol);
1550
+ ctx.restore();
1551
+ }
1552
+ } else {
1553
+ drawTaskIcon(ctx, md[j].x, md[j].y, models[j].task, iconCol);
1554
+ }
1555
  }
1556
 
1557
  // Update pinned label position (follows elastic pull)
 
1900
  const idx = CUSTOM_MODEL_IDX;
1901
  models[idx] = customModel;
1902
 
1903
+ // Save base model positions BEFORE any changes (for precise return on removal)
1904
+ for (let j = 0; j < BASE_MODEL_COUNT; j++) {
1905
+ preInsertX[j] = positions[j].x;
1906
+ preInsertY[j] = positions[j].y;
1907
+ }
1908
+
1909
  // Compute mass normalization relative to existing base range
1910
  const dl = Math.log(customModel.downloads);
1911
  const rawMass = Math.pow(dl, exponent);
1912
  const normVal = Math.max(0, Math.min(1, (rawMass - massMin) / massRange));
 
1913
  if (massNorm.length <= idx) massNorm.push(normVal);
1914
  else massNorm[idx] = normVal;
1915
 
1916
+ // Store original orbit base for the custom model
1917
+ orbitBaseOriginal[idx] = 30 + 180 * normVal;
1918
+
1919
+ // Activate
1920
+ activeModelCount = BASE_MODEL_COUNT + 1;
1921
+ customModelActive = true;
1922
 
1923
+ // Create md entry BEFORE applyOrbitScale (which reads md[j].captureRadius)
1924
  md[idx] = {
1925
  x: 0, y: 0,
1926
  massNorm: normVal,
1927
+ captureRadius: 0
1928
  };
1929
 
1930
+ // Shrink all orbits to fit the extra model (scale factor)
1931
+ const newScale = BASE_MODEL_COUNT / activeModelCount;
1932
+ orbitScaleTarget = newScale;
1933
+ applyOrbitScale(newScale);
1934
 
1935
  // Recompute total mass norm and spawn weights
1936
  totalMassNorm = 0;
 
1942
  }
1943
  totalSpawnWeight = totalMassNorm;
1944
 
1945
+ // Find best open position for custom model (center-biased, clearance-aware)
1946
+ const openPos = findOpenPosition(idx);
1947
+ md[idx].x = openPos.x;
1948
+ md[idx].y = openPos.y;
1949
+ if (positions.length <= idx) positions.push({ x: openPos.x, y: openPos.y });
1950
+ else { positions[idx].x = openPos.x; positions[idx].y = openPos.y; }
1951
+
1952
+ // Resolve any overlaps locally β€” models only move minimum distance needed
1953
+ const separated = separateInPlace();
1954
+ for (let j = 0; j < BASE_MODEL_COUNT; j++) {
1955
+ const dx = separated[j].x - positions[j].x;
1956
+ const dy = separated[j].y - positions[j].y;
1957
+ if (dx * dx + dy * dy > 4) {
1958
+ layoutTargetX[j] = separated[j].x;
1959
+ layoutTargetY[j] = separated[j].y;
1960
+ layoutActive[j] = 1;
1961
+ }
1962
+ }
1963
+ // Update custom model position from separation too
1964
+ positions[idx].x = separated[idx].x;
1965
+ positions[idx].y = separated[idx].y;
1966
+ md[idx].x = separated[idx].x;
1967
+ md[idx].y = separated[idx].y;
1968
+
1969
  computeNeighbors();
1970
 
1971
+ // Reset elastic state for custom model only
1972
+ elasticDx[idx] = 0; elasticDy[idx] = 0;
1973
+ elasticVx[idx] = 0; elasticVy[idx] = 0;
1974
+
1975
+ // Start pop-in animation (0% β†’ 100% with elastic overshoot)
1976
+ customPopScale = 0;
1977
+ customPopVelocity = 0;
1978
+ customPopTarget = 1;
1979
 
1980
  // Set rotation direction (random)
1981
  modelRotDir[idx] = Math.random() < 0.5 ? 1 : -1;
1982
  }
1983
 
1984
+ // ── removeCustomModel: eject particles, start pop-out ────────
1985
  function removeCustomModel() {
1986
  if (!customModelActive) return;
1987
  const idx = CUSTOM_MODEL_IDX;
 
1990
  for (let i = 0; i < PARTICLE_COUNT; i++) {
1991
  const p = particles[i];
1992
  if (p.attractorIdx === idx) {
 
1993
  const m = md[idx];
1994
  const r = p.orbitRadius || 1;
1995
  const rawOmega = p.angularMomentum / (r * r);
 
1998
  p.vy = Math.cos(p.angle) * omega * r;
1999
  p.phase = 0;
2000
  p.attractorIdx = -1;
 
2001
  }
2002
  }
2003
 
2004
+ // Prevent new captures during pop-out
2005
+ modelCaptureRadius[idx] = 0;
2006
+ md[idx].captureRadius = 0;
2007
 
2008
+ // Start pop-out animation (finalizeRemoval called when scale reaches 0)
2009
+ customPopTarget = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2010
  }
2011
 
2012
  })();