| 1 | <template id="decision-tree-template"> |
| 2 | <div data-composition-id="decision-tree" data-width="1920" data-height="1080" data-duration="12"> |
| 3 | <div class="canvas"> |
| 4 | <svg class="connectors" width="1920" height="1080" viewBox="0 0 1920 1080"> |
| 5 | <!-- Level 1 to 2 --> |
| 6 | <path |
| 7 | id="path-1-L" |
| 8 | class="connector" |
| 9 | d="M 960 130 L 960 200 L 600 200 L 600 270" |
| 10 | fill="none" |
| 11 | stroke="#333" |
| 12 | stroke-width="4" |
| 13 | /> |
| 14 | <path |
| 15 | id="path-1-R" |
| 16 | class="connector" |
| 17 | d="M 960 130 L 960 200 L 1320 200 L 1320 270" |
| 18 | fill="none" |
| 19 | stroke="#333" |
| 20 | stroke-width="4" |
| 21 | /> |
| 22 | |
| 23 | <!-- Level 2 to 3 (Left side) --> |
| 24 | <path |
| 25 | id="path-2-LL" |
| 26 | class="connector" |
| 27 | d="M 600 330 L 600 400 L 400 400 L 400 470" |
| 28 | fill="none" |
| 29 | stroke="#333" |
| 30 | stroke-width="4" |
| 31 | /> |
| 32 | <path |
| 33 | id="path-2-LR" |
| 34 | class="connector" |
| 35 | d="M 600 330 L 600 400 L 800 400 L 800 470" |
| 36 | fill="none" |
| 37 | stroke="#333" |
| 38 | stroke-width="4" |
| 39 | /> |
| 40 | |
| 41 | <!-- Level 2 to 3 (Right side) --> |
| 42 | <path |
| 43 | id="path-2-RL" |
| 44 | class="connector" |
| 45 | d="M 1320 330 L 1320 400 L 1120 400 L 1120 470" |
| 46 | fill="none" |
| 47 | stroke="#333" |
| 48 | stroke-width="4" |
| 49 | /> |
| 50 | <path |
| 51 | id="path-2-RR" |
| 52 | class="connector" |
| 53 | d="M 1320 330 L 1320 400 L 1520 400 L 1520 470" |
| 54 | fill="none" |
| 55 | stroke="#333" |
| 56 | stroke-width="4" |
| 57 | /> |
| 58 | </svg> |
| 59 | |
| 60 | <div id="label-yes" class="connector-label">Yes</div> |
| 61 | <div id="label-not-sure" class="connector-label">Not sure</div> |
| 62 | |
| 63 | <!-- Level 1 --> |
| 64 | <div id="node-root" class="node sticky yellow" style="left: 960px; top: 100px"> |
| 65 | Should I learn to code? |
| 66 | </div> |
| 67 | |
| 68 | <!-- Level 2 --> |
| 69 | <div id="node-yes" class="node sticky green" style="left: 600px; top: 300px">Yes</div> |
| 70 | <div id="node-not-sure" class="node sticky peach" style="left: 1320px; top: 300px"> |
| 71 | Not sure |
| 72 | </div> |
| 73 | |
| 74 | <!-- Level 3 --> |
| 75 | <div id="node-python" class="node sticky lavender" style="left: 400px; top: 500px"> |
| 76 | <div style="position: relative; display: inline-block"> |
| 77 | <span id="python-text">Start with Pythom</span> |
| 78 | <svg class="squiggle-container" width="64" height="6" viewBox="0 0 64 6"> |
| 79 | <path |
| 80 | id="squiggle" |
| 81 | d="M 0 3 Q 2 0 4 3 T 8 3 T 12 3 T 16 3 T 20 3 T 24 3 T 28 3 T 32 3 T 36 3 T 40 3 T 44 3 T 48 3 T 52 3 T 56 3 T 60 3 T 64 3" |
| 82 | fill="none" |
| 83 | stroke="#E53935" |
| 84 | stroke-width="2" |
| 85 | /> |
| 86 | </svg> |
| 87 | </div> |
| 88 | </div> |
| 89 | <div id="node-nocode" class="node sticky blue" style="left: 800px; top: 500px"> |
| 90 | Try no-code first |
| 91 | </div> |
| 92 | <div id="node-website" class="node sticky pink" style="left: 1120px; top: 500px"> |
| 93 | Build a personal website |
| 94 | </div> |
| 95 | <div id="node-course" class="node sticky yellow" style="left: 1520px; top: 500px"> |
| 96 | Take a free intro course |
| 97 | </div> |
| 98 | |
| 99 | <!-- Cursor --> |
| 100 | <div id="cursor" class="cursor-container"> |
| 101 | <svg width="24" height="24" viewBox="0 0 24 24" fill="none"> |
| 102 | <path |
| 103 | d="M5.65376 12.3673H5.46026L5.31717 12.4976L0.500002 16.8829L0.500002 1.19841L11.7841 12.3673H5.65376Z" |
| 104 | fill="white" |
| 105 | stroke="black" |
| 106 | /> |
| 107 | </svg> |
| 108 | <div class="cursor-tag">You</div> |
| 109 | </div> |
| 110 | |
| 111 | <!-- Emoji --> |
| 112 | <div id="emoji-thumb" class="emoji">👍</div> |
| 113 | |
| 114 | <!-- Overlay --> |
| 115 | <div id="fade-overlay"></div> |
| 116 | </div> |
| 117 | |
| 118 | <style> |
| 119 | [data-composition-id="decision-tree"] { |
| 120 | width: 1920px; |
| 121 | height: 1080px; |
| 122 | background-color: #ffffff; |
| 123 | background-image: radial-gradient(#e5e5e5 1px, transparent 1px); |
| 124 | background-size: 20px 20px; |
| 125 | position: absolute; |
| 126 | overflow: hidden; |
| 127 | font-family: "Inter", sans-serif; |
| 128 | } |
| 129 | |
| 130 | [data-composition-id="decision-tree"] .canvas { |
| 131 | width: 100%; |
| 132 | height: 100%; |
| 133 | position: absolute; |
| 134 | transform: scale(1.2); |
| 135 | transform-origin: 960px 300px; |
| 136 | } |
| 137 | |
| 138 | [data-composition-id="decision-tree"] .node { |
| 139 | position: absolute; |
| 140 | transform: translate(-50%, -50%) scale(0); |
| 141 | padding: 16px 24px; |
| 142 | border-radius: 12px; |
| 143 | font-size: 18px; |
| 144 | font-weight: 500; |
| 145 | color: #333; |
| 146 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
| 147 | white-space: nowrap; |
| 148 | display: flex; |
| 149 | flex-direction: column; |
| 150 | align-items: center; |
| 151 | justify-content: center; |
| 152 | z-index: 10; |
| 153 | } |
| 154 | |
| 155 | [data-composition-id="decision-tree"] .node.yellow { |
| 156 | background-color: #e8d44d; |
| 157 | } |
| 158 | [data-composition-id="decision-tree"] .node.green { |
| 159 | background-color: #c2e8a0; |
| 160 | } |
| 161 | [data-composition-id="decision-tree"] .node.peach { |
| 162 | background-color: #f5c5a3; |
| 163 | } |
| 164 | [data-composition-id="decision-tree"] .node.lavender { |
| 165 | background-color: #d4c5f9; |
| 166 | } |
| 167 | [data-composition-id="decision-tree"] .node.blue { |
| 168 | background-color: #a8d8f0; |
| 169 | } |
| 170 | [data-composition-id="decision-tree"] .node.pink { |
| 171 | background-color: #f8b4c8; |
| 172 | } |
| 173 | |
| 174 | [data-composition-id="decision-tree"] .connectors { |
| 175 | position: absolute; |
| 176 | top: 0; |
| 177 | left: 0; |
| 178 | z-index: 5; |
| 179 | pointer-events: none; |
| 180 | } |
| 181 | |
| 182 | [data-composition-id="decision-tree"] .connector { |
| 183 | stroke-dasharray: 1000; |
| 184 | stroke-dashoffset: 1000; |
| 185 | } |
| 186 | |
| 187 | [data-composition-id="decision-tree"] .connector-label { |
| 188 | position: absolute; |
| 189 | font-size: 14px; |
| 190 | font-weight: 600; |
| 191 | color: #333; |
| 192 | background: white; |
| 193 | padding: 2px 6px; |
| 194 | border-radius: 4px; |
| 195 | opacity: 0; |
| 196 | z-index: 6; |
| 197 | transform: translate(-50%, -50%); |
| 198 | } |
| 199 | |
| 200 | #label-yes { |
| 201 | left: 780px; |
| 202 | top: 230px; |
| 203 | } |
| 204 | #label-not-sure { |
| 205 | left: 1140px; |
| 206 | top: 230px; |
| 207 | } |
| 208 | |
| 209 | [data-composition-id="decision-tree"] .cursor-container { |
| 210 | position: absolute; |
| 211 | left: 1920px; |
| 212 | top: 1080px; |
| 213 | z-index: 100; |
| 214 | pointer-events: none; |
| 215 | display: flex; |
| 216 | align-items: flex-start; |
| 217 | } |
| 218 | |
| 219 | [data-composition-id="decision-tree"] .cursor-tag { |
| 220 | background-color: #9747ff; |
| 221 | color: white; |
| 222 | padding: 2px 8px; |
| 223 | border-radius: 4px; |
| 224 | font-size: 12px; |
| 225 | font-weight: 600; |
| 226 | margin-left: 4px; |
| 227 | margin-top: 12px; |
| 228 | } |
| 229 | |
| 230 | [data-composition-id="decision-tree"] .squiggle-container { |
| 231 | position: absolute; |
| 232 | right: 0; |
| 233 | bottom: -4px; |
| 234 | opacity: 0; |
| 235 | } |
| 236 | |
| 237 | [data-composition-id="decision-tree"] .emoji { |
| 238 | position: absolute; |
| 239 | font-size: 32px; |
| 240 | left: 510px; |
| 241 | top: 500px; |
| 242 | transform: translate(-50%, -50%) scale(0); |
| 243 | z-index: 20; |
| 244 | } |
| 245 | |
| 246 | [data-composition-id="decision-tree"] #fade-overlay { |
| 247 | position: absolute; |
| 248 | top: 0; |
| 249 | left: 0; |
| 250 | width: 100%; |
| 251 | height: 100%; |
| 252 | background-color: white; |
| 253 | opacity: 0; |
| 254 | z-index: 1000; |
| 255 | pointer-events: none; |
| 256 | } |
| 257 | |
| 258 | .selection-border { |
| 259 | position: absolute; |
| 260 | top: -4px; |
| 261 | left: -4px; |
| 262 | right: -4px; |
| 263 | bottom: -4px; |
| 264 | border: 2px solid #0b84f3; |
| 265 | border-radius: 16px; |
| 266 | pointer-events: none; |
| 267 | opacity: 0; |
| 268 | } |
| 269 | |
| 270 | .text-highlight { |
| 271 | background-color: #d0e4ff; |
| 272 | } |
| 273 | </style> |
| 274 | |
| 275 | <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script> |
| 276 | <script> |
| 277 | (function () { |
| 278 | const tl = gsap.timeline({ paused: true }); |
| 279 | |
| 280 | // 1. Root node scales from 0→100% over 0.4s |
| 281 | tl.to("#node-root", { scale: 1, duration: 0.4, ease: "power2.out" }); |
| 282 | |
| 283 | // 2. Hold 0.6s |
| 284 | tl.addLabel("hold1", "+=0.6"); |
| 285 | |
| 286 | // 3. Two elbow connectors draw downward-left and downward-right simultaneously (0.5s) |
| 287 | tl.to( |
| 288 | ".connector#path-1-L, .connector#path-1-R", |
| 289 | { |
| 290 | strokeDashoffset: 0, |
| 291 | duration: 0.5, |
| 292 | ease: "none", |
| 293 | }, |
| 294 | "hold1", |
| 295 | ); |
| 296 | |
| 297 | // Labels fade in at midpoint |
| 298 | tl.to( |
| 299 | "#label-yes, #label-not-sure", |
| 300 | { |
| 301 | opacity: 1, |
| 302 | duration: 0.2, |
| 303 | }, |
| 304 | "hold1+=0.25", |
| 305 | ); |
| 306 | |
| 307 | // 4. Level 2 nodes scale in at endpoints with 0.2s stagger |
| 308 | tl.to("#node-yes", { scale: 1, duration: 0.4, ease: "back.out(1.7)" }, "hold1+=0.4"); |
| 309 | tl.to("#node-not-sure", { scale: 1, duration: 0.4, ease: "back.out(1.7)" }, "hold1+=0.6"); |
| 310 | |
| 311 | // 5. Hold 0.5s |
| 312 | tl.addLabel("hold2", "+=0.5"); |
| 313 | |
| 314 | // 6. Four connectors draw down from level 2 (0.4s) |
| 315 | tl.to( |
| 316 | ".connector#path-2-LL, .connector#path-2-LR, .connector#path-2-RL, .connector#path-2-RR", |
| 317 | { |
| 318 | strokeDashoffset: 0, |
| 319 | duration: 0.4, |
| 320 | ease: "none", |
| 321 | }, |
| 322 | "hold2", |
| 323 | ); |
| 324 | |
| 325 | // 7. Four leaf nodes scale in with 0.15s stagger |
| 326 | const leafNodes = ["#node-python", "#node-nocode", "#node-website", "#node-course"]; |
| 327 | leafNodes.forEach((id, i) => { |
| 328 | tl.to(id, { scale: 1, duration: 0.4, ease: "back.out(1.7)" }, `hold2+=${0.3 + i * 0.15}`); |
| 329 | }); |
| 330 | |
| 331 | // Red wavy underline appears with text |
| 332 | tl.to(".squiggle-container", { opacity: 1, duration: 0.1 }, "hold2+=0.3"); |
| 333 | |
| 334 | // 8. Hold 0.8s |
| 335 | tl.addLabel("hold3", "+=0.8"); |
| 336 | |
| 337 | // 9. Cursor drifts in from bottom-right (1s) to lavender sticky |
| 338 | tl.to( |
| 339 | "#cursor", |
| 340 | { |
| 341 | left: 450, |
| 342 | top: 540, |
| 343 | duration: 1, |
| 344 | ease: "power1.inOut", |
| 345 | }, |
| 346 | "hold3", |
| 347 | ); |
| 348 | |
| 349 | // 10. Cursor clicks. Selection border appears. |
| 350 | tl.add(() => { |
| 351 | const node = document.querySelector("#node-python"); |
| 352 | if (!node) return; |
| 353 | if (!node.querySelector(".selection-border")) { |
| 354 | const border = document.createElement("div"); |
| 355 | border.className = "selection-border"; |
| 356 | node.appendChild(border); |
| 357 | } |
| 358 | gsap.set(".selection-border", { opacity: 1 }); |
| 359 | }, "hold3+=1"); |
| 360 | |
| 361 | // Visual click feedback |
| 362 | tl.to( |
| 363 | "#cursor", |
| 364 | { scale: 0.8, duration: 0.05, yoyo: true, repeat: 1, overwrite: "auto" }, |
| 365 | "hold3+=1", |
| 366 | ); |
| 367 | |
| 368 | // 11. Hold 0.3s |
| 369 | tl.addLabel("hold4", "+=0.3"); |
| 370 | |
| 371 | // 12. Cursor double-clicks "Pythom" — highlight in blue |
| 372 | tl.to( |
| 373 | "#cursor", |
| 374 | { scale: 0.8, duration: 0.05, yoyo: true, repeat: 3, overwrite: "auto" }, |
| 375 | "hold4", |
| 376 | ); |
| 377 | tl.add(() => { |
| 378 | const textEl = document.querySelector("#python-text"); |
| 379 | if (!textEl) return; |
| 380 | textEl.innerHTML = 'Start with <span class="text-highlight">Pythom</span>'; |
| 381 | }, "hold4+=0.1"); |
| 382 | |
| 383 | // 13. Hold 0.2s |
| 384 | tl.addLabel("hold5", "+=0.2"); |
| 385 | |
| 386 | // 14. Highlighted text deletes and "Python" types in (fast) |
| 387 | // Squiggle disappears when last char lands |
| 388 | const typingStart = tl.labels["hold5"]; |
| 389 | const words = ["P", "Py", "Pyt", "Pyth", "Pytho", "Python"]; |
| 390 | words.forEach((word, i) => { |
| 391 | tl.add( |
| 392 | () => { |
| 393 | const el = document.querySelector("#python-text"); |
| 394 | if (el) el.innerHTML = `Start with <span class="text-highlight">${word}</span>`; |
| 395 | }, |
| 396 | typingStart + i * 0.05, |
| 397 | ); |
| 398 | }); |
| 399 | |
| 400 | // Remove highlight and squiggle at the end of typing |
| 401 | const typingEnd = typingStart + words.length * 0.05; |
| 402 | tl.add(() => { |
| 403 | const pyEl = document.querySelector("#python-text"); |
| 404 | if (pyEl) pyEl.innerHTML = "Start with Python"; |
| 405 | gsap.set(".squiggle-container", { opacity: 0 }); |
| 406 | }, typingEnd); |
| 407 | |
| 408 | // 15. Hold 0.5s |
| 409 | tl.addLabel("hold6", typingEnd + 0.5); |
| 410 | |
| 411 | // 16. Cursor moves slightly and clicks away to deselect |
| 412 | tl.to("#cursor", { left: 500, top: 580, duration: 0.3, overwrite: "auto" }, "hold6"); |
| 413 | tl.to( |
| 414 | "#cursor", |
| 415 | { scale: 0.8, duration: 0.05, yoyo: true, repeat: 1, overwrite: "auto" }, |
| 416 | "hold6+=0.3", |
| 417 | ); |
| 418 | tl.to(".selection-border", { opacity: 0, duration: 0.1 }, "hold6+=0.3"); |
| 419 | |
| 420 | // 17. 👍 emoji pops in |
| 421 | tl.to("#emoji-thumb", { scale: 1, duration: 0.15, ease: "back.out(2)" }, "hold6+=0.4"); |
| 422 | |
| 423 | // 18. Hold 2s |
| 424 | tl.addLabel("hold7", "+=2"); |
| 425 | |
| 426 | // 19. Fade entire screen to white over 0.5s |
| 427 | tl.to("#fade-overlay", { opacity: 1, duration: 0.5 }, "hold7"); |
| 428 | |
| 429 | window.__timelines = window.__timelines || {}; |
| 430 | window.__timelines["decision-tree"] = tl; |
| 431 | })(); |
| 432 | </script> |
| 433 | </div> |
| 434 | </template> |
| 435 |