| | <div class="d3-rope-demo"></div> |
| |
|
| | <style> |
| | .d3-rope-demo { |
| | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
| | line-height: 1.5; |
| | color: var(--text-color); |
| | padding: 20px 0; |
| | display: flex; |
| | flex-direction: column; |
| | align-items: center; |
| | } |
| | |
| | .d3-rope-demo .subtitle { |
| | color: var(--text-color); |
| | font-size: 18px; |
| | font-weight: 600; |
| | margin-bottom: 20px; |
| | text-align: center; |
| | max-width: 600px; |
| | line-height: 1.5; |
| | } |
| | |
| | .d3-rope-demo .sentence { |
| | display: flex; |
| | gap: 0; |
| | margin: 25px 0; |
| | flex-wrap: wrap; |
| | justify-content: center; |
| | font-size: 18px; |
| | } |
| | |
| | .d3-rope-demo .slider-container { |
| | display: flex; |
| | align-items: center; |
| | justify-content: center; |
| | margin: 15px 0; |
| | } |
| | |
| | .d3-rope-demo .slider-label { |
| | font-size: 14px; |
| | color: var(--muted-color); |
| | font-weight: 500; |
| | min-width: 80px; |
| | } |
| | |
| | .d3-rope-demo .slider { |
| | width: 200px; |
| | height: 6px; |
| | border-radius: 3px; |
| | background: var(--border-color); |
| | outline: none; |
| | cursor: pointer; |
| | } |
| | |
| | .d3-rope-demo .slider::-webkit-slider-thumb { |
| | appearance: none; |
| | width: 18px; |
| | height: 18px; |
| | border-radius: 50%; |
| | background: var(--primary-color); |
| | cursor: pointer; |
| | border: 2px solid var(--page-bg); |
| | box-shadow: 0 2px 4px var(--border-color); |
| | } |
| | |
| | .d3-rope-demo .slider::-moz-range-thumb { |
| | width: 18px; |
| | height: 18px; |
| | border-radius: 50%; |
| | background: var(--primary-color); |
| | cursor: pointer; |
| | border: 2px solid var(--page-bg); |
| | box-shadow: 0 2px 4px var(--border-color); |
| | } |
| | |
| | .d3-rope-demo .slider-value { |
| | font-size: 14px; |
| | color: var(--text-color); |
| | font-weight: 600; |
| | min-width: 40px; |
| | text-align: center; |
| | } |
| | |
| | .d3-rope-demo .rotation-info { |
| | text-align: center; |
| | margin: 20px auto; |
| | font-size: 16px; |
| | font-weight: 500; |
| | color: var(--text-color); |
| | padding: 20px; |
| | background: var(--page-bg); |
| | border-radius: 8px; |
| | border: 1px solid var(--border-color) !important; |
| | box-shadow: 0 2px 4px rgba(0,0,0,0.05); |
| | max-width: 500px; |
| | } |
| | |
| | .d3-rope-demo .equation-gap { |
| | height: 15px; |
| | } |
| | |
| | .d3-rope-demo .word-highlight { |
| | color: var(--primary-color); |
| | font-weight: 700; |
| | background: var(--page-bg); |
| | padding: 2px 6px; |
| | border-radius: 4px; |
| | border: 1px solid var(--border-color); |
| | display: inline-block; |
| | min-width: 60px; |
| | text-align: center; |
| | } |
| | |
| | .d3-rope-demo .position-highlight { |
| | color: var(--primary-color); |
| | font-weight: 700; |
| | background: var(--page-bg); |
| | padding: 2px 6px; |
| | border-radius: 4px; |
| | border: 1px solid var(--border-color); |
| | } |
| | |
| | .d3-rope-demo .angle-highlight { |
| | color: var(--primary-color); |
| | font-weight: 600; |
| | font-family: 'Courier New', monospace; |
| | font-size: 20px; |
| | padding: 12px 16px; |
| | border-radius: 6px; |
| | background: var(--page-bg); |
| | border: 1px solid var(--border-color); |
| | display: inline-block; |
| | width: 100%; |
| | text-align: center; |
| | } |
| | |
| | .d3-rope-demo .word { |
| | cursor: pointer; |
| | font-weight: 700; |
| | font-size: 18px; |
| | user-select: none; |
| | padding: 8px 12px; |
| | border-radius: 0; |
| | transition: all 0.2s ease; |
| | border: 1px solid var(--border-color); |
| | border-right: none; |
| | } |
| | |
| | .d3-rope-demo .word:first-child { |
| | border-radius: 6px 0 0 6px; |
| | } |
| | |
| | .d3-rope-demo .word:last-child { |
| | border-radius: 0 6px 6px 0; |
| | border-right: 1px solid var(--border-color); |
| | } |
| | |
| | .d3-rope-demo .word:only-child { |
| | border-radius: 6px; |
| | border-right: 1px solid var(--border-color); |
| | } |
| | |
| | |
| | .button { |
| | background: var(--primary-color)!important; |
| | color: var(--page-bg)!important; |
| | border: 1px solid var(--primary-color)!important; |
| | } |
| | |
| | .button--ghost { |
| | background: var(--page-bg)!important; |
| | color: var(--primary-color)!important; |
| | border: 1px solid var(--primary-color)!important; |
| | } |
| | |
| | .d3-rope-demo .svg-container { |
| | margin: 0; |
| | display: inline-block; |
| | } |
| | |
| | .d3-rope-demo svg { |
| | display: block; |
| | } |
| | |
| | .d3-rope-demo .explanation { |
| | max-width: 700px; |
| | text-align: center; |
| | margin-top: 20px; |
| | color: var(--text-color); |
| | font-size: 15px; |
| | line-height: 1.6; |
| | } |
| | |
| | |
| | @media (max-width: 768px) { |
| | .d3-rope-demo { |
| | padding: 16px 0; |
| | } |
| | |
| | .d3-rope-demo .sentence { |
| | gap: 10px; |
| | } |
| | |
| | .d3-rope-demo .word { |
| | font-size: 16px; |
| | padding: 6px 10px; |
| | } |
| | |
| | .d3-rope-demo .svg-container { |
| | width: 100%; |
| | max-width: 400px; |
| | } |
| | |
| | .d3-rope-demo svg { |
| | width: 100%; |
| | height: auto; |
| | } |
| | |
| | .d3-rope-demo .explanation { |
| | font-size: 14px; |
| | } |
| | } |
| | </style> |
| |
|
| | <script> |
| | (() => { |
| | const bootstrap = () => { |
| | const scriptEl = document.currentScript; |
| | let container = scriptEl ? scriptEl.previousElementSibling : null; |
| | if (!(container && container.classList && container.classList.contains('d3-rope-demo'))) { |
| | const candidates = Array.from(document.querySelectorAll('.d3-rope-demo')) |
| | .filter((el) => !(el.dataset && el.dataset.mounted === 'true')); |
| | container = candidates[candidates.length - 1] || null; |
| | } |
| | if (!container) return; |
| | if (container.dataset) { |
| | if (container.dataset.mounted === 'true') return; |
| | container.dataset.mounted = 'true'; |
| | } |
| | |
| | const sentence = ["The", "quick", "brown", "fox", "jumps", "..."]; |
| | |
| | |
| | container.innerHTML = ` |
| | <div class="subtitle">RoPE rotation of the first (x₁, x₂) pair in Q/K vectors<br/> based on token position</div> |
| | <div class="sentence" id="sentence"></div> |
| | <div class="slider-container"> |
| | <input type="range" class="slider" id="positionSlider" min="0" max="5" step="1" value="0"> |
| | </div> |
| | <div class="svg-container"> |
| | <svg id="ropeSvg" width="500" height="400" viewBox="0 0 500 400"></svg> |
| | </div> |
| | |
| | <div class="rotation-info" id="rotationInfo"> |
| | <span class="word-highlight">The</span> at position <span class="position-highlight">0</span> gets rotated by |
| | <div class="equation-gap"></div> |
| | <span class="angle-highlight">θ = 0 rad (0°)</span> |
| | </div> |
| | <div class="explanation"> |
| | <strong>RoPE Formula:</strong> θ (theta) = position × 1 / base<sup>2 × pair_index/h_dim</sup> (pair_index=0 here) |
| | <br><br> |
| | <strong>Key insight:</strong> The first dimension pair gets the largest rotations, and the relative angle between words depends only on their distance apart. |
| | </div> |
| | `; |
| | |
| | const svg = container.querySelector('#ropeSvg'); |
| | const sentenceEl = container.querySelector('#sentence'); |
| | const slider = container.querySelector('#positionSlider'); |
| | const rotationInfo = container.querySelector('#rotationInfo'); |
| | |
| | const R = 140; |
| | const R_LABELS = 180; |
| | const cx = 250; |
| | const cy = 200; |
| | const ANGLE_OFFSET = 5; |
| | |
| | |
| | const base = 10000; |
| | const d = 2048; |
| | const m = 0; |
| | |
| | function getRopeAngle(pos) { |
| | return pos * (1 / Math.pow(base, (2 * m) / d)); |
| | } |
| | |
| | let activeIndex = 0; |
| | let animating = true; |
| | let animationTimeout = null; |
| | |
| | function renderSentence() { |
| | sentenceEl.innerHTML = ""; |
| | sentence.forEach((word, i) => { |
| | const span = document.createElement("span"); |
| | span.textContent = word; |
| | span.className = "word button" + (i === activeIndex ? "" : " button--ghost"); |
| | span.addEventListener("click", () => { |
| | stopAnimation(); |
| | activeIndex = i; |
| | slider.value = i; |
| | updateRotationInfo(); |
| | draw(); |
| | renderSentence(); |
| | }); |
| | sentenceEl.appendChild(span); |
| | }); |
| | } |
| | |
| | function draw() { |
| | |
| | svg.innerHTML = ''; |
| | |
| | |
| | const backgroundElements = []; |
| | const foregroundElements = []; |
| | const textElements = []; |
| | |
| | |
| | const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); |
| | circle.setAttribute('cx', cx); |
| | circle.setAttribute('cy', cy); |
| | circle.setAttribute('r', R); |
| | circle.setAttribute('fill', 'none'); |
| | circle.setAttribute('stroke', 'var(--border-color)'); |
| | circle.setAttribute('stroke-width', '1.5'); |
| | circle.setAttribute('opacity', '0.6'); |
| | backgroundElements.push(circle); |
| | |
| | |
| | sentence.forEach((word, i) => { |
| | const theta = getRopeAngle(i) + (ANGLE_OFFSET * Math.PI / 180); |
| | const x = cx + R * Math.cos(theta); |
| | const y = cy + R * Math.sin(theta); |
| | |
| | const isActive = (i === activeIndex); |
| | const isGhost = i > activeIndex; |
| | |
| | |
| | const point = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); |
| | point.setAttribute('cx', x); |
| | point.setAttribute('cy', y); |
| | point.setAttribute('r', isActive ? 10 : 5); |
| | point.setAttribute('fill', isActive ? 'var(--primary-color)' : (isGhost ? 'var(--muted-color)' : 'var(--primary-color)')); |
| | point.setAttribute('stroke', isActive ? 'var(--page-bg)' : (isGhost ? 'var(--surface-bg)' : 'var(--page-bg)')); |
| | point.setAttribute('stroke-width', isActive ? '3' : '2'); |
| | point.setAttribute('opacity', isActive ? '1' : (isGhost ? '0.3' : '0.7')); |
| | backgroundElements.push(point); |
| | |
| | |
| | if (isActive) { |
| | const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'line'); |
| | arrow.setAttribute('x1', cx); |
| | arrow.setAttribute('y1', cy); |
| | arrow.setAttribute('x2', x); |
| | arrow.setAttribute('y2', y); |
| | arrow.setAttribute('stroke', 'var(--primary-color)'); |
| | arrow.setAttribute('stroke-width', '3'); |
| | arrow.setAttribute('stroke-linecap', 'round'); |
| | arrow.setAttribute('opacity', '0.8'); |
| | foregroundElements.push(arrow); |
| | } |
| | }); |
| | |
| | |
| | const centerPoint = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); |
| | centerPoint.setAttribute('cx', cx); |
| | centerPoint.setAttribute('cy', cy); |
| | centerPoint.setAttribute('r', 5); |
| | centerPoint.setAttribute('fill', 'var(--text-color)'); |
| | centerPoint.setAttribute('stroke', 'var(--page-bg)'); |
| | centerPoint.setAttribute('stroke-width', '2'); |
| | centerPoint.setAttribute('opacity', '0.8'); |
| | foregroundElements.push(centerPoint); |
| | |
| | |
| | if (activeIndex !== null && activeIndex > 0) { |
| | const theta = getRopeAngle(activeIndex) + (ANGLE_OFFSET * Math.PI / 180); |
| | const startAngle = ANGLE_OFFSET * Math.PI / 180; |
| | const endAngle = theta; |
| | |
| | |
| | const radius = R * 0.7; |
| | const startX = cx + radius * Math.cos(startAngle); |
| | const startY = cy + radius * Math.sin(startAngle); |
| | const endX = cx + radius * Math.cos(endAngle); |
| | const endY = cy + radius * Math.sin(endAngle); |
| | |
| | const largeArcFlag = theta > Math.PI ? 1 : 0; |
| | const pathData = `M ${startX} ${startY} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endX} ${endY}`; |
| | |
| | const arc = document.createElementNS('http://www.w3.org/2000/svg', 'path'); |
| | arc.setAttribute('d', pathData); |
| | arc.setAttribute('fill', 'none'); |
| | arc.setAttribute('stroke', 'var(--primary-color)'); |
| | arc.setAttribute('stroke-width', '2.5'); |
| | arc.setAttribute('stroke-dasharray', '6,4'); |
| | arc.setAttribute('opacity', '0.8'); |
| | foregroundElements.push(arc); |
| | } |
| | |
| | |
| | sentence.forEach((word, i) => { |
| | const theta = getRopeAngle(i); |
| | const x = cx + R * Math.cos(theta); |
| | const y = cy + R * Math.sin(theta); |
| | |
| | const isActive = (i === activeIndex); |
| | const isGhost = i > activeIndex; |
| | |
| | |
| | const labelX = cx + R_LABELS * Math.cos(theta); |
| | const labelY = cy + R_LABELS * Math.sin(theta); |
| | const wordLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); |
| | wordLabel.setAttribute('x', labelX); |
| | wordLabel.setAttribute('y', labelY); |
| | wordLabel.setAttribute('text-anchor', 'middle'); |
| | wordLabel.setAttribute('dominant-baseline', 'middle'); |
| | wordLabel.setAttribute('fill', isActive ? 'var(--text-color)' : (isGhost ? 'var(--muted-color)' : 'var(--text-color)')); |
| | wordLabel.setAttribute('font-family', '-apple-system, BlinkMacSystemFont, sans-serif'); |
| | wordLabel.setAttribute('font-size', isActive ? '18' : '15'); |
| | wordLabel.setAttribute('font-weight', isActive ? '700' : '500'); |
| | wordLabel.setAttribute('opacity', isActive ? '1' : (isGhost ? '0.3' : '0.8')); |
| | wordLabel.textContent = word; |
| | textElements.push(wordLabel); |
| | }); |
| | |
| | |
| | if (activeIndex !== null && activeIndex > 0) { |
| | const theta = getRopeAngle(activeIndex) + (ANGLE_OFFSET * Math.PI / 180); |
| | const radius = R * 0.7; |
| | const angleLabelX = cx + radius * 0.5 * Math.cos(theta / 2); |
| | const angleLabelY = cy + radius * 0.5 * Math.sin(theta / 2); |
| | |
| | |
| | const angleLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); |
| | angleLabel.setAttribute('x', angleLabelX); |
| | angleLabel.setAttribute('y', angleLabelY); |
| | angleLabel.setAttribute('text-anchor', 'middle'); |
| | angleLabel.setAttribute('font-family', '-apple-system, BlinkMacSystemFont, sans-serif'); |
| | angleLabel.setAttribute('font-size', '13'); |
| | angleLabel.setAttribute('font-weight', '600'); |
| | |
| | |
| | const thetaSpan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan'); |
| | thetaSpan.setAttribute('fill', 'var(--primary-color)'); |
| | thetaSpan.textContent = 'θ'; |
| | angleLabel.appendChild(thetaSpan); |
| | |
| | |
| | const equalsSpan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan'); |
| | equalsSpan.setAttribute('fill', 'var(--primary-color)'); |
| | equalsSpan.setAttribute('opacity', '0.5'); |
| | equalsSpan.textContent = ' = '; |
| | angleLabel.appendChild(equalsSpan); |
| | |
| | |
| | const numberSpan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan'); |
| | numberSpan.setAttribute('fill', 'var(--primary-color)'); |
| | numberSpan.textContent = activeIndex.toString(); |
| | angleLabel.appendChild(numberSpan); |
| | |
| | textElements.push(angleLabel); |
| | } |
| | |
| | |
| | backgroundElements.forEach(el => svg.appendChild(el)); |
| | foregroundElements.forEach(el => svg.appendChild(el)); |
| | textElements.forEach(el => svg.appendChild(el)); |
| | } |
| | |
| | function updateRotationInfo() { |
| | const theta = getRopeAngle(activeIndex); |
| | const degrees = Math.round(theta * 180 / Math.PI); |
| | rotationInfo.innerHTML = ` |
| | <span class="word-highlight">${sentence[activeIndex]}</span> at position <span class="position-highlight">${activeIndex}</span> gets rotated by |
| | <div class="equation-gap"></div> |
| | <div class="angle-highlight"> |
| | <span style="color: var(--muted-color); opacity: 0.6;">θ</span> |
| | <span style="color: var(--muted-color); opacity: 0.4; margin: 0 8px;">=</span> |
| | <span style="opacity: 1;">${activeIndex}</span> |
| | <span style="color: var(--muted-color); opacity: 0.6;">rad</span> |
| | <span style="color: var(--muted-color); opacity: 0.4;">(</span> |
| | <span style="opacity: 1;">${degrees}°</span> |
| | <span style="color: var(--muted-color); opacity: 0.4;">)</span> |
| | </div> |
| | `; |
| | } |
| | |
| | function stopAnimation() { |
| | animating = false; |
| | if (animationTimeout) { |
| | clearTimeout(animationTimeout); |
| | animationTimeout = null; |
| | } |
| | } |
| | |
| | function animate() { |
| | if (!animating) return; |
| | |
| | animationTimeout = setTimeout(() => { |
| | activeIndex = (activeIndex + 1) % sentence.length; |
| | slider.value = activeIndex; |
| | updateRotationInfo(); |
| | renderSentence(); |
| | draw(); |
| | animate(); |
| | }, 1500); |
| | } |
| | |
| | |
| | slider.addEventListener('input', (e) => { |
| | stopAnimation(); |
| | activeIndex = parseInt(e.target.value); |
| | updateRotationInfo(); |
| | renderSentence(); |
| | draw(); |
| | }); |
| | |
| | |
| | renderSentence(); |
| | updateRotationInfo(); |
| | draw(); |
| | animate(); |
| | }; |
| | |
| | if (document.readyState === 'loading') { |
| | document.addEventListener('DOMContentLoaded', bootstrap, { once: true }); |
| | } else { |
| | bootstrap(); |
| | } |
| | })(); |
| | </script> |
| |
|