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.