|
122 | 122 | const lineHeight = 22; |
123 | 123 | let wordIdx = 0; |
124 | 124 |
|
125 | | - // Build list of obstacle circles from dragon segments |
126 | | - const obstacles = []; |
| 125 | + // Build list of obstacles (dragon circles + DOM element rectangles) |
| 126 | + const circularObstacles = []; |
127 | 127 | if (dragon && dragon.segments) { |
128 | 128 | // Head obstacle |
129 | 129 | const head = dragon.segments[0]; |
130 | | - obstacles.push({ x: head.x, y: head.y, r: 48 }); |
| 130 | + circularObstacles.push({ x: head.x, y: head.y, r: 48 }); |
131 | 131 |
|
132 | 132 | // Body segment obstacles (every 3rd segment) |
133 | 133 | for (let i = 2; i < dragon.segments.length; i += 3) { |
134 | 134 | const seg = dragon.segments[i]; |
135 | | - obstacles.push({ x: seg.x, y: seg.y, r: Math.max(16, seg.width * 0.6) }); |
| 135 | + circularObstacles.push({ x: seg.x, y: seg.y, r: Math.max(16, seg.width * 0.6) }); |
136 | 136 | } |
137 | 137 | } |
138 | 138 |
|
| 139 | + // Query DOM obstacles inside container |
| 140 | + const domObstacles = []; |
| 141 | + const containerRect = container.getBoundingClientRect(); |
| 142 | + const obstacleElements = container.querySelectorAll('.pretext-obstacle, .hero-title, .hero-subtitle'); |
| 143 | + obstacleElements.forEach(el => { |
| 144 | + const rect = el.getBoundingClientRect(); |
| 145 | + const pad = 12; // padding around UI elements |
| 146 | + const top = rect.top - containerRect.top - pad; |
| 147 | + const bottom = rect.bottom - containerRect.top + pad; |
| 148 | + const left = rect.left - containerRect.left - pad; |
| 149 | + const right = rect.right - containerRect.left + pad; |
| 150 | + if (right > 0 && left < width && bottom > 0 && top < height) { |
| 151 | + domObstacles.push({ left, right, top, bottom }); |
| 152 | + } |
| 153 | + }); |
| 154 | + |
139 | 155 | for (let y = 18; y < height; y += lineHeight) { |
140 | 156 | let x = 12; |
141 | 157 |
|
142 | 158 | while (x < width - 12) { |
143 | 159 | let availableSpan = width - 12 - x; |
144 | 160 |
|
145 | | - // Subtract dragon segment obstacles intersecting line at y |
146 | | - for (let obs of obstacles) { |
| 161 | + // 1. Subtract dragon circular segment obstacles intersecting line at y |
| 162 | + for (let obs of circularObstacles) { |
147 | 163 | const dy = Math.abs(y - obs.y); |
148 | 164 | if (dy < obs.r) { |
149 | 165 | const dx = Math.sqrt(obs.r * obs.r - dy * dy); |
|
161 | 177 | } |
162 | 178 | } |
163 | 179 |
|
| 180 | + // 2. Subtract DOM rectangular obstacles intersecting line at y |
| 181 | + for (let obs of domObstacles) { |
| 182 | + if (y >= obs.top && y <= obs.bottom) { |
| 183 | + if (x < obs.right && x + availableSpan > obs.left) { |
| 184 | + if (x < obs.left) { |
| 185 | + availableSpan = obs.left - x; |
| 186 | + } else { |
| 187 | + x = obs.right; |
| 188 | + availableSpan = width - 12 - x; |
| 189 | + } |
| 190 | + } |
| 191 | + } |
| 192 | + } |
| 193 | + |
164 | 194 | let count = 0; |
165 | 195 | let currentSpan = 0; |
166 | 196 | let checkIdx = wordIdx; |
|
173 | 203 | } |
174 | 204 |
|
175 | 205 | if (count === 0) { |
176 | | - x += 16; |
| 206 | + // If phrase doesn't fit in gap, try CJK/character sub-chunk fitting |
| 207 | + const phrase = phraseStream[wordIdx]; |
| 208 | + let subFit = ""; |
| 209 | + let subWidth = 0; |
| 210 | + for (let char of phrase) { |
| 211 | + const charWidth = ctx.measureText(char).width; |
| 212 | + if (subWidth + charWidth <= availableSpan) { |
| 213 | + subFit += char; |
| 214 | + subWidth += charWidth; |
| 215 | + } else { |
| 216 | + break; |
| 217 | + } |
| 218 | + } |
| 219 | + |
| 220 | + if (subFit.length > 0 && availableSpan > 20) { |
| 221 | + ctx.fillText(subFit, x, y); |
| 222 | + x += subWidth + 8; |
| 223 | + } else { |
| 224 | + x += 16; |
| 225 | + } |
177 | 226 | continue; |
178 | 227 | } |
179 | 228 |
|
|
0 commit comments