PageSourceSearch

https://golanlevin.github.io/dumpster/paragraph_balloon_manager.js

js golanlevin.github.io collected 2026-10-03 09:15:20 UTC 10,696 bytes, 259 lines download raw bytes

1// Ported from ParagraphBalloonManager.pde
2// Key change from original: SadnessFetcher (async HTTP) replaced by
3// synchronous getBreakupText() lookup from the pre-loaded Files dict.
4
5class ParagraphBalloonManager {
6
7  constructor() {
8    this.currentBreakupID   = DUMPSTER_INVALID;
9    this.mouseoverBalloonID = DUMPSTER_INVALID;
10    this.nExtantBalloons    = 0;
11    this.currentBalloonIndex = DUMPSTER_INVALID;
12
13    this.mouseX = 0;
14    this.mouseY = 0;
15    this.bMousePressed  = false;
16    this.bNewRequestMade = false;
17
18    // Use Georgia Italic for the balloon paragraph text.
19    // (Original used "Georgia-Italic-9.vlw"; we use a CSS string font.)
20    this.paragraphFont = 'Georgia';
21
22    this.balloons = new Array(MAX_N_BALLOONS);
23    for (let i = 0; i < MAX_N_BALLOONS; i++) {
24      this.balloons[i] = new ParagraphBalloon();
25      this.balloons[i].setFont(this.paragraphFont);
26      this.balloons[i].setStringAndComputeLayout('', '', DUMPSTER_INVALID, true);
27    }
28  }
29
30  //====================================================================
31  informOfMouse(mx, my, bm) {
32    this.bMousePressed = bm;
33    this.mouseX = mx;
34    this.mouseY = my;
35  }
36
37  //====================================================================
38  execute(breakupId, heartId) {
39    if (breakupId < 0 || breakupId >= N_BREAKUP_DATABASE_RECORDS) return;
40
41    const alreadyIdx = this._findIndexOfBalloonAlreadyContaining(breakupId);
42    if (alreadyIdx !== DUMPSTER_INVALID) {
43      if (alreadyIdx === this.currentBalloonIndex) {
44        // Already on top — do nothing.
45      } else if (alreadyIdx < this.currentBalloonIndex) {
46        // Case I: shift down, bring to top
47        const tempPy      = this.balloons[alreadyIdx].py;
48        const tempBalloon = this.balloons[alreadyIdx];
49        for (let i = alreadyIdx; i < this.currentBalloonIndex; i++) {
50          this.balloons[i] = this.balloons[(i + 1) % MAX_N_BALLOONS];
51        }
52        this.balloons[this.currentBalloonIndex] = tempBalloon;
53        this.balloons[this.currentBalloonIndex].setPositionY(tempPy);
54        this.balloons[this.currentBalloonIndex].setTargetY(BALLOON_START_Y);
55        this._retargetFrom(this.currentBalloonIndex);
56      } else {
57        // Case II: shift up, bring to current+1
58        const tempPy      = this.balloons[alreadyIdx].py;
59        const tempBalloon = this.balloons[alreadyIdx];
60        for (let i = alreadyIdx - 1; i > this.currentBalloonIndex; i--) {
61          this.balloons[(i + 1) % MAX_N_BALLOONS] = this.balloons[i];
62        }
63        this.currentBalloonIndex = (this.currentBalloonIndex + 1) % MAX_N_BALLOONS;
64        this.nExtantBalloons = Math.min(this.nExtantBalloons + 1, MAX_N_BALLOONS);
65        this.balloons[this.currentBalloonIndex] = tempBalloon;
66        this.balloons[this.currentBalloonIndex].setPositionY(tempPy);
67        this.balloons[this.currentBalloonIndex].setTargetY(BALLOON_START_Y);
68        this._retargetFrom(this.currentBalloonIndex);
69      }
70    } else {
71      // New balloon: advance ring buffer, load text synchronously.
72      this.currentBalloonIndex = (this.currentBalloonIndex + 1) % MAX_N_BALLOONS;
73      this.nExtantBalloons = Math.min(this.nExtantBalloons + 1, MAX_N_BALLOONS);
74
75      const text   = getBreakupText(breakupId);
76      const author = getBreakupAuthorDisplay(breakupId);
77      this.balloons[this.currentBalloonIndex].setStringAndComputeLayout(text, author, breakupId, false, '');
78      const nLines = this.balloons[this.currentBalloonIndex].myParagraph.nLines;
79      this.balloons[this.currentBalloonIndex].dateString = this._dateStringForBupId(breakupId, nLines >= 3);
80      this.balloons[this.currentBalloonIndex].setPositionY(BALLOON_START_Y);
81      this.balloons[this.currentBalloonIndex].heartId = heartId;
82      this.bNewRequestMade = true;
83    }
84  }
85
86  //====================================================================
87  _findIndexOfBalloonAlreadyContaining(breakupId) {
88    for (let i = 0; i < this.nExtantBalloons; i++) {
89      if (this.balloons[i].breakupId === breakupId) return i;
90    }
91    return DUMPSTER_INVALID;
92  }
93
94  //====================================================================
95  _retargetFrom(fromIndex) {
96    let starty = this.balloons[fromIndex].targety;
97    for (let i = 0; i < this.nExtantBalloons; i++) {
98      const bid = (fromIndex - i + MAX_N_BALLOONS) % MAX_N_BALLOONS;
99      this.balloons[bid].setTargetY(starty);
100      starty += this.balloons[bid].ph + BALLOON_SPACING_Y;
101    }
102  }
103
104  //====================================================================
105  render() {
106    this._update();
107    noTint();
108    noStroke();
109    textFont(this.paragraphFont);
110    textStyle(ITALIC);
111    textSize(BALLOON_TEXT_SIZE);
112
113    for (let i = 0; i < this.nExtantBalloons; i++) {
114      this.balloons[i].informOfCurrency(i === this.currentBalloonIndex);
115      this.balloons[i].render(i === this.mouseoverBalloonID);
116    }
117    textStyle(NORMAL);
118  }
119
120  //====================================================================
121  getMouseContainingBalloon() {
122    let resultIndex = DUMPSTER_INVALID;
123    if (this.mouseX > HEART_WALL_L && this.mouseY < HEART_WALL_B &&
124        this.mouseX < HEART_WALL_R && this.mouseY > HEART_WALL_T) {
125      for (let i = 0; i < this.nExtantBalloons; i++) {
126        const b = this.balloons[i];
127        if (this.mouseX > b.px && this.mouseX < b.px + b.pw &&
128            this.mouseY > b.py && this.mouseY < b.py + b.ph) {
129          resultIndex = i;
130          break;
131        }
132      }
133    }
134    this.mouseoverBalloonID = resultIndex;
135    return resultIndex;
136  }
137
138  //====================================================================
139  getTopBalloonCenterY() {
140    if (this.nExtantBalloons > 0 && this.currentBalloonIndex > DUMPSTER_INVALID) {
141      const b = this.balloons[this.currentBalloonIndex];
142      return b.py + b.ph / 2.0;
143    }
144    return DUMPSTER_INVALID;
145  }
146
147  //====================================================================
148  _update() {
149    this._retargetBalloons();
150    for (let i = 0; i < this.nExtantBalloons; i++) {
151      this.balloons[i].updatePositionY();
152    }
153  }
154
155  //====================================================================
156  //====================================================================
157  // Update the topmost balloon's text in-place (no new stack entry).
158  // Ensures a minimum height of 4 lines during pixel-view drag.
159  updateTopmostBalloonInPlace(breakupId, heartId) {
160    if (this.currentBalloonIndex === DUMPSTER_INVALID || this.nExtantBalloons === 0) return;
161    const b = this.balloons[this.currentBalloonIndex];
162    const text   = getBreakupText(breakupId);
163    const author = getBreakupAuthorDisplay(breakupId);
164    b.setStringAndComputeLayout(text, author, breakupId, false, '');
165    const minPh = Math.ceil(5 * b.myParagraph.myLeading + b.margT + b.margB);
166    b.ph = Math.max(b.ph, minPh);
167    b.dateString = this._dateStringForBupId(breakupId, b.myParagraph.nLines >= 3);
168    if (heartId !== undefined && heartId !== DUMPSTER_INVALID) b.heartId = heartId;
169  }
170
171  //====================================================================
172  // Re-layout the topmost balloon at its natural height after a drag ends.
173  restoreTopmostBalloonHeight() {
174    if (this.currentBalloonIndex === DUMPSTER_INVALID || this.nExtantBalloons === 0) return;
175    const b = this.balloons[this.currentBalloonIndex];
176    if (b.breakupId === DUMPSTER_INVALID) return;
177    const text   = getBreakupText(b.breakupId);
178    const author = getBreakupAuthorDisplay(b.breakupId);
179    b.setStringAndComputeLayout(text, author, b.breakupId, false, '');
180    b.dateString = this._dateStringForBupId(b.breakupId, b.myParagraph.nLines >= 3);
181    // bNewTextAppeared is true, so lower balloons will smoothly retarget upward.
182  }
183
184  //====================================================================
185  _dateStringForBupId(bupId, bShowYear) {
186    if (bupId < 0 || bupId >= N_BREAKUP_DATABASE_RECORDS) return '';
187    const d = BM.bups[bupId].date;
188    if (d <= 0) return '';
189    const lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
190    let remaining = d - 1;
191    for (let m = 0; m < 12; m++) {
192      if (remaining < lengths[m]) {
193        const str = (m + 1) + '-' + (remaining + 1);
194        return bShowYear ? str + '-05' : str;
195      }
196      remaining -= lengths[m];
197    }
198    return '';
199  }
200
201  //====================================================================
202  _retargetBalloons() {
203    if (this.bNewRequestMade) {
204      this.bNewRequestMade = false;
205      let starty = this.balloons[this.currentBalloonIndex].py;
206      starty += this.balloons[this.currentBalloonIndex].ph + BALLOON_SPACING_Y;
207
208      if (this.nExtantBalloons < MAX_N_BALLOONS) {
209        for (let i = this.currentBalloonIndex - 1; i >= 0; i--) {
210          this.balloons[i].setTargetY(starty);
211          starty += this.balloons[i].ph + BALLOON_SPACING_Y;
212        }
213      } else {
214        for (let i = 1; i < MAX_N_BALLOONS; i++) {
215          const bid = (this.currentBalloonIndex - i + MAX_N_BALLOONS) % MAX_N_BALLOONS;
216          this.balloons[bid].setTargetY(starty);
217          starty += this.balloons[bid].ph + BALLOON_SPACING_Y;
218        }
219      }
220    }
221
222    for (let b = 0; b < this.nExtantBalloons; b++) {
223      if (this.balloons[b].bNewTextAppeared) {
224        this.balloons[b].bNewTextAppeared = false;
225        const BI = b;
226        let starty = this.balloons[BI].py + this.balloons[BI].ph + BALLOON_SPACING_Y;
227
228        if (this.nExtantBalloons < MAX_N_BALLOONS) {
229          for (let i = BI - 1; i >= 0; i--) {
230            this.balloons[i].setTargetY(starty);
231            starty += this.balloons[i].ph + BALLOON_SPACING_Y;
232          }
233        } else {
234          if (this.currentBalloonIndex === BI) {
235            for (let i = 1; i < MAX_N_BALLOONS; i++) {
236              const bid = (this.currentBalloonIndex - i + MAX_N_BALLOONS) % MAX_N_BALLOONS;
237              this.balloons[bid].setTargetY(starty);
238              starty += this.balloons[bid].ph + BALLOON_SPACING_Y;
239            }
240          } else if (this.currentBalloonIndex > BI) {
241            for (let i = BI - 1; i >= 0; i--) {
242              this.balloons[i].setTargetY(starty);
243              starty += this.balloons[i].ph + BALLOON_SPACING_Y;
244            }
245            for (let i = MAX_N_BALLOONS - 1; i > this.currentBalloonIndex; i--) {
246              this.balloons[i].setTargetY(starty);
247              starty += this.balloons[i].ph + BALLOON_SPACING_Y;
248            }
249          } else {
250            for (let i = BI - 1; i > this.currentBalloonIndex; i--) {
251              this.balloons[i].setTargetY(starty);
252              starty += this.balloons[i].ph + BALLOON_SPACING_Y;
253            }
254          }
255        }
256      }
257    }
258  }
259}

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.