Skip to content

Instantly share code, notes, and snippets.

@aaaa-zhen
Created February 2, 2026 06:27
Show Gist options
  • Select an option

  • Save aaaa-zhen/dc83036a9319d050b69ab376bd11752f to your computer and use it in GitHub Desktop.

Select an option

Save aaaa-zhen/dc83036a9319d050b69ab376bd11752f to your computer and use it in GitHub Desktop.
picker number
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>Picker 教程 - 分步学习</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif;
background: #f5f5f7;
min-height: 100vh;
padding: 20px;
user-select: none;
-webkit-user-select: none;
}
.container {
max-width: 600px;
margin: 0 auto;
}
h1 {
font-size: 28px;
font-weight: 600;
color: #1d1d1f;
margin-bottom: 20px;
text-align: center;
}
/* Tab Navigation */
.tabs {
display: flex;
gap: 8px;
margin-bottom: 20px;
background: #fff;
padding: 8px;
border-radius: 12px;
}
.tab {
flex: 1;
padding: 12px 16px;
border: none;
background: transparent;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
color: #86868b;
cursor: pointer;
transition: all 0.2s;
}
.tab.active {
background: #007aff;
color: #fff;
}
.tab:hover:not(.active) {
background: #f0f0f0;
}
/* Demo Area */
.demo-area {
background: #fff;
border-radius: 16px;
padding: 24px;
margin-bottom: 16px;
}
.demo-title {
font-size: 18px;
font-weight: 600;
color: #1d1d1f;
margin-bottom: 8px;
}
.demo-desc {
font-size: 14px;
color: #86868b;
margin-bottom: 20px;
line-height: 1.5;
}
.picker-container {
position: relative;
width: 120px;
height: 200px;
margin: 0 auto 20px;
background: #f5f5f7;
border-radius: 12px;
overflow: hidden;
}
.picker-container::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 40px;
transform: translateY(-50%);
background: rgba(0,122,255,0.1);
border-top: 1px solid rgba(0,122,255,0.3);
border-bottom: 1px solid rgba(0,122,255,0.3);
pointer-events: none;
z-index: 10;
}
.picker-wheel {
position: absolute;
width: 100%;
height: 100%;
}
.picker-item {
position: absolute;
width: 100%;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
font-weight: 500;
color: #1d1d1f;
}
/* Data Display */
.data-panel {
background: #1d1d1f;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.data-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #333;
font-size: 14px;
}
.data-row:last-child {
border-bottom: none;
}
.data-label {
color: #86868b;
}
.data-value {
color: #30d158;
font-family: 'SF Mono', monospace;
font-weight: 600;
}
/* Code Display */
.code-area {
background: #1d1d1f;
border-radius: 16px;
padding: 20px;
overflow-x: auto;
}
.code-title {
font-size: 14px;
color: #86868b;
margin-bottom: 12px;
}
pre {
margin: 0;
font-family: 'SF Mono', Monaco, monospace;
font-size: 13px;
line-height: 1.6;
color: #fff;
white-space: pre-wrap;
}
.comment { color: #6a9955; }
.keyword { color: #569cd6; }
.function { color: #dcdcaa; }
.number { color: #b5cea8; }
.string { color: #ce9178; }
.property { color: #9cdcfe; }
/* Step specific styles */
.step-content {
display: none;
}
.step-content.active {
display: block;
}
</style>
</head>
<body>
<div class="container">
<h1>Picker 教程</h1>
<div class="tabs">
<button class="tab active" data-step="1">第一步<br><small>拖拽</small></button>
<button class="tab" data-step="2">第二步<br><small>速度</small></button>
<button class="tab" data-step="3">第三步<br><small>弹簧</small></button>
<button class="tab" data-step="4">第四步<br><small>对齐</small></button>
</div>
<!-- Step 1: Drag -->
<div class="step-content active" id="step1">
<div class="demo-area">
<div class="demo-title">第一步:基础拖拽</div>
<div class="demo-desc">
学习如何追踪拖拽手势。<br>
试着上下拖动数字 — 松手后会停在你释放的位置。
</div>
<div class="picker-container" id="picker1">
<div class="picker-wheel" id="wheel1"></div>
</div>
</div>
<div class="data-panel">
<div class="data-row">
<span class="data-label">是否拖拽中</span>
<span class="data-value" id="data1-dragging">false</span>
</div>
<div class="data-row">
<span class="data-label">拖拽起点 Y</span>
<span class="data-value" id="data1-startY">0</span>
</div>
<div class="data-row">
<span class="data-label">当前位置</span>
<span class="data-value" id="data1-pos">0</span>
</div>
</div>
<div class="code-area">
<div class="code-title">// 第一步:基础拖拽</div>
<pre><span class="keyword">let</span> pos = <span class="number">0</span>
<span class="keyword">let</span> isDragging = <span class="keyword">false</span>
<span class="keyword">let</span> dragStartY = <span class="number">0</span>
<span class="keyword">let</span> dragStartPos = <span class="number">0</span>
<span class="comment">// 按下:记录起点</span>
picker.<span class="function">addEventListener</span>(<span class="string">'mousedown'</span>, e => {
isDragging = <span class="keyword">true</span>
dragStartY = e.<span class="property">pageY</span>
dragStartPos = pos
})
<span class="comment">// 移动:计算偏移量</span>
window.<span class="function">addEventListener</span>(<span class="string">'mousemove'</span>, e => {
<span class="keyword">if</span> (!isDragging) <span class="keyword">return</span>
pos = dragStartPos + (dragStartY - e.<span class="property">pageY</span>)
<span class="function">render</span>()
})
<span class="comment">// 松手:停止拖拽</span>
window.<span class="function">addEventListener</span>(<span class="string">'mouseup'</span>, () => {
isDragging = <span class="keyword">false</span>
})</pre>
</div>
</div>
<!-- Step 2: Velocity -->
<div class="step-content" id="step2">
<div class="demo-area">
<div class="demo-title">第二步:获取速度</div>
<div class="demo-desc">
记录指针历史来计算释放时的速度。<br>
试着快速滑动 — 观察松手时的速度值!
</div>
<div class="picker-container" id="picker2">
<div class="picker-wheel" id="wheel2"></div>
</div>
</div>
<div class="data-panel">
<div class="data-row">
<span class="data-label">当前位置</span>
<span class="data-value" id="data2-pos">0</span>
</div>
<div class="data-row">
<span class="data-label">速度</span>
<span class="data-value" id="data2-velocity">0</span>
</div>
<div class="data-row">
<span class="data-label">历史记录数</span>
<span class="data-value" id="data2-history">0</span>
</div>
</div>
<div class="code-area">
<div class="code-title">// 第二步:计算速度</div>
<pre><span class="keyword">let</span> pointerHistory = []
<span class="comment">// 移动时:记录时间和位置</span>
window.<span class="function">addEventListener</span>(<span class="string">'mousemove'</span>, e => {
<span class="keyword">if</span> (!isDragging) <span class="keyword">return</span>
pointerHistory.<span class="function">push</span>({
y: e.<span class="property">pageY</span>,
time: performance.<span class="function">now</span>()
})
<span class="comment">// 只保留最近 10 个点</span>
<span class="keyword">if</span> (pointerHistory.<span class="property">length</span> > <span class="number">10</span>) {
pointerHistory.<span class="function">shift</span>()
}
})
<span class="comment">// 松手时:计算速度</span>
window.<span class="function">addEventListener</span>(<span class="string">'mouseup'</span>, () => {
<span class="keyword">const</span> now = performance.<span class="function">now</span>()
<span class="keyword">const</span> recent = pointerHistory[<span class="number">0</span>]
<span class="keyword">const</span> last = pointerHistory.<span class="function">at</span>(-<span class="number">1</span>)
<span class="keyword">const</span> dt = now - recent.<span class="property">time</span>
<span class="keyword">const</span> dy = recent.<span class="property">y</span> - last.<span class="property">y</span>
<span class="comment">// 速度 = 距离 / 时间 (px/s)</span>
velocity = dy / dt * <span class="number">1000</span>
})</pre>
</div>
</div>
<!-- Step 3: Spring -->
<div class="step-content" id="step3">
<div class="demo-area">
<div class="demo-title">第三步:弹簧物理</div>
<div class="demo-desc">
使用弹簧物理驱动到目标位置。<br>
松手后会弹簧式地滑动到最近的格子。调节 k(刚度)和 b(阻尼)看效果变化。
</div>
<div class="picker-container" id="picker3">
<div class="picker-wheel" id="wheel3"></div>
</div>
<div style="display: flex; gap: 16px; margin-top: 16px;">
<div style="flex: 1;">
<label style="font-size: 13px; color: #86868b;">刚度 (k): <span id="k3-value">200</span></label>
<input type="range" id="k3-slider" min="50" max="500" value="200" style="width: 100%;">
</div>
<div style="flex: 1;">
<label style="font-size: 13px; color: #86868b;">阻尼 (b): <span id="b3-value">28</span></label>
<input type="range" id="b3-slider" min="5" max="60" value="28" style="width: 100%;">
</div>
</div>
</div>
<div class="data-panel">
<div class="data-row">
<span class="data-label">当前位置</span>
<span class="data-value" id="data3-pos">0</span>
</div>
<div class="data-row">
<span class="data-label">速度</span>
<span class="data-value" id="data3-velocity">0</span>
</div>
<div class="data-row">
<span class="data-label">目标位置</span>
<span class="data-value" id="data3-dest">0</span>
</div>
</div>
<div class="code-area">
<div class="code-title">// 第三步:弹簧物理</div>
<pre><span class="keyword">function</span> <span class="function">springStep</span>(spring) {
<span class="keyword">const</span> dt = <span class="number">0.004</span> <span class="comment">// 每步 4ms</span>
<span class="comment">// 弹力:把位置拉向目标</span>
<span class="keyword">const</span> Fspring = -spring.<span class="property">k</span> * (spring.<span class="property">pos</span> - spring.<span class="property">dest</span>)
<span class="comment">// 阻尼力:消耗速度</span>
<span class="keyword">const</span> Fdamper = -spring.<span class="property">b</span> * spring.<span class="property">v</span>
<span class="comment">// 更新速度和位置</span>
spring.<span class="property">v</span> += (Fspring + Fdamper) * dt
spring.<span class="property">pos</span> += spring.<span class="property">v</span> * dt
}
<span class="comment">// 松手时:设置目标为最近的格子</span>
spring.<span class="property">dest</span> = Math.<span class="function">round</span>(pos / ITEM_HEIGHT) * ITEM_HEIGHT
spring.<span class="property">v</span> = velocity <span class="comment">// 保持 fling 的速度</span></pre>
</div>
</div>
<!-- Step 4: Snap -->
<div class="step-content" id="step4">
<div class="demo-area">
<div class="demo-title">第四步:智能对齐</div>
<div class="demo-desc">
预测惯性终点,然后对齐到最近的格子。<br>
快速滑动可以跨越多格,慢滑只移动一格!
</div>
<div class="picker-container" id="picker4">
<div class="picker-wheel" id="wheel4"></div>
</div>
</div>
<div class="data-panel">
<div class="data-row">
<span class="data-label">当前位置</span>
<span class="data-value" id="data4-pos">0</span>
</div>
<div class="data-row">
<span class="data-label">速度</span>
<span class="data-value" id="data4-velocity">0</span>
</div>
<div class="data-row">
<span class="data-label">预测终点</span>
<span class="data-value" id="data4-predict">0</span>
</div>
<div class="data-row">
<span class="data-label">对齐后目标</span>
<span class="data-value" id="data4-snap">0</span>
</div>
<div class="data-row">
<span class="data-label">选中值</span>
<span class="data-value" id="data4-value">0</span>
</div>
</div>
<div class="code-area">
<div class="code-title">// 第四步:预测 + 对齐</div>
<pre><span class="keyword">const</span> ITEM_HEIGHT = <span class="number">40</span>
<span class="keyword">const</span> FRICTION = <span class="number">2</span>
<span class="keyword">function</span> <span class="function">calculateSnapTarget</span>(pos, velocity) {
<span class="comment">// 1. 预测自然衰减会停在哪里</span>
<span class="keyword">const</span> decayTarget = pos + velocity / FRICTION
<span class="comment">// 2. 对齐到最近的格子</span>
<span class="keyword">const</span> snapped = Math.<span class="function">round</span>(decayTarget / ITEM_HEIGHT) * ITEM_HEIGHT
<span class="keyword">return</span> snapped
}
<span class="comment">// 松手时:</span>
<span class="keyword">const</span> target = <span class="function">calculateSnapTarget</span>(spring.<span class="property">pos</span>, velocity)
spring.<span class="property">dest</span> = target <span class="comment">// 弹簧会动画到这里</span>
spring.<span class="property">v</span> = velocity <span class="comment">// 保持惯性</span>
<span class="comment">// 弹簧物理会处理平滑过渡!</span></pre>
</div>
</div>
</div>
<script>
'use strict'
// ============================================
// 常量
// ============================================
const ITEM_HEIGHT = 40
const CENTER_Y = 80
const MIN_VALUE = 0
const MAX_VALUE = 60
// ============================================
// Tab 切换
// ============================================
const tabs = document.querySelectorAll('.tab')
const steps = document.querySelectorAll('.step-content')
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const step = tab.dataset.step
tabs.forEach(t => t.classList.remove('active'))
steps.forEach(s => s.classList.remove('active'))
tab.classList.add('active')
document.getElementById('step' + step).classList.add('active')
})
})
// ============================================
// 辅助函数:创建数字元素
// ============================================
function createItems(wheel, count = 11) {
const items = []
for (let i = 0; i < count; i++) {
const item = document.createElement('div')
item.className = 'picker-item'
wheel.appendChild(item)
items.push(item)
}
return items
}
// ============================================
// 辅助函数:限制范围
// ============================================
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value))
}
// ============================================
// 第一步:基础拖拽
// ============================================
;(function step1() {
const picker = document.getElementById('picker1')
const wheel = document.getElementById('wheel1')
const items = createItems(wheel)
let pos = 0
let isDragging = false
let dragStartY = 0
let dragStartPos = 0
function render() {
const centerIndex = pos / ITEM_HEIGHT
const startIndex = Math.floor(centerIndex) - 5
for (let i = 0; i < items.length; i++) {
const itemIndex = startIndex + i
const offset = itemIndex - centerIndex
const y = CENTER_Y + offset * ITEM_HEIGHT
// 只显示 0-60
if (itemIndex < MIN_VALUE || itemIndex > MAX_VALUE) {
items[i].style.visibility = 'hidden'
} else {
items[i].style.visibility = 'visible'
items[i].textContent = itemIndex
}
items[i].style.transform = `translateY(${y}px)`
items[i].style.opacity = Math.abs(offset) < 0.5 ? 1 : 0.4
}
document.getElementById('data1-pos').textContent = Math.round(pos)
}
picker.addEventListener('mousedown', e => {
isDragging = true
dragStartY = e.pageY
dragStartPos = pos
document.getElementById('data1-dragging').textContent = 'true'
document.getElementById('data1-startY').textContent = Math.round(dragStartY)
})
picker.addEventListener('touchstart', e => {
isDragging = true
dragStartY = e.touches[0].pageY
dragStartPos = pos
document.getElementById('data1-dragging').textContent = 'true'
document.getElementById('data1-startY').textContent = Math.round(dragStartY)
}, { passive: true })
window.addEventListener('mousemove', e => {
if (!isDragging) return
pos = clamp(dragStartPos + (dragStartY - e.pageY), MIN_VALUE * ITEM_HEIGHT, MAX_VALUE * ITEM_HEIGHT)
render()
})
window.addEventListener('touchmove', e => {
if (!isDragging) return
pos = clamp(dragStartPos + (dragStartY - e.touches[0].pageY), MIN_VALUE * ITEM_HEIGHT, MAX_VALUE * ITEM_HEIGHT)
render()
}, { passive: true })
window.addEventListener('mouseup', () => {
if (!isDragging) return
isDragging = false
document.getElementById('data1-dragging').textContent = 'false'
})
window.addEventListener('touchend', () => {
if (!isDragging) return
isDragging = false
document.getElementById('data1-dragging').textContent = 'false'
})
render()
})()
// ============================================
// 第二步:获取速度
// ============================================
;(function step2() {
const picker = document.getElementById('picker2')
const wheel = document.getElementById('wheel2')
const items = createItems(wheel)
let pos = 0
let velocity = 0
let isDragging = false
let dragStartY = 0
let dragStartPos = 0
let pointerHistory = []
function render() {
const centerIndex = pos / ITEM_HEIGHT
const startIndex = Math.floor(centerIndex) - 5
for (let i = 0; i < items.length; i++) {
const itemIndex = startIndex + i
const offset = itemIndex - centerIndex
const y = CENTER_Y + offset * ITEM_HEIGHT
if (itemIndex < MIN_VALUE || itemIndex > MAX_VALUE) {
items[i].style.visibility = 'hidden'
} else {
items[i].style.visibility = 'visible'
items[i].textContent = itemIndex
}
items[i].style.transform = `translateY(${y}px)`
items[i].style.opacity = Math.abs(offset) < 0.5 ? 1 : 0.4
}
document.getElementById('data2-pos').textContent = Math.round(pos)
document.getElementById('data2-history').textContent = pointerHistory.length
}
function handleDown(y) {
isDragging = true
dragStartY = y
dragStartPos = pos
pointerHistory = [{ y, time: performance.now() }]
}
function handleMove(y) {
if (!isDragging) return
pos = clamp(dragStartPos + (dragStartY - y), MIN_VALUE * ITEM_HEIGHT, MAX_VALUE * ITEM_HEIGHT)
pointerHistory.push({ y, time: performance.now() })
if (pointerHistory.length > 10) pointerHistory.shift()
render()
}
function handleUp() {
if (!isDragging) return
isDragging = false
const now = performance.now()
if (pointerHistory.length >= 2) {
let i = pointerHistory.length - 1
while (i > 0 && now - pointerHistory[i].time < 100) i--
const dt = now - pointerHistory[i].time
if (dt > 0) {
velocity = (pointerHistory[i].y - pointerHistory.at(-1).y) / dt * 1000
}
}
document.getElementById('data2-velocity').textContent = Math.round(velocity)
// 简单的惯性衰减动画
const decay = () => {
velocity *= 0.95
pos = clamp(pos + velocity * 0.016, MIN_VALUE * ITEM_HEIGHT, MAX_VALUE * ITEM_HEIGHT)
render()
if (Math.abs(velocity) > 1) {
requestAnimationFrame(decay)
}
}
decay()
}
picker.addEventListener('mousedown', e => handleDown(e.pageY))
picker.addEventListener('touchstart', e => handleDown(e.touches[0].pageY), { passive: true })
window.addEventListener('mousemove', e => handleMove(e.pageY))
window.addEventListener('touchmove', e => handleMove(e.touches[0].pageY), { passive: true })
window.addEventListener('mouseup', handleUp)
window.addEventListener('touchend', handleUp)
render()
})()
// ============================================
// 第三步:弹簧物理
// ============================================
;(function step3() {
const picker = document.getElementById('picker3')
const wheel = document.getElementById('wheel3')
const items = createItems(wheel)
const MS_PER_STEP = 4
let spring = { pos: 0, dest: 0, v: 0, k: 200, b: 28 }
let isDragging = false
let dragStartY = 0
let dragStartPos = 0
let pointerHistory = []
let animating = false
let lastTime = null
function springStep() {
const dt = MS_PER_STEP / 1000
const Fspring = -spring.k * (spring.pos - spring.dest)
const Fdamper = -spring.b * spring.v
spring.v += (Fspring + Fdamper) * dt
spring.pos += spring.v * dt
}
function render(now) {
if (lastTime !== null && !isDragging) {
const steps = Math.floor((now - lastTime) / MS_PER_STEP)
for (let i = 0; i < steps; i++) springStep()
lastTime += steps * MS_PER_STEP
}
const centerIndex = spring.pos / ITEM_HEIGHT
const startIndex = Math.floor(centerIndex) - 5
for (let i = 0; i < items.length; i++) {
const itemIndex = startIndex + i
const offset = itemIndex - centerIndex
const y = CENTER_Y + offset * ITEM_HEIGHT
if (itemIndex < MIN_VALUE || itemIndex > MAX_VALUE) {
items[i].style.visibility = 'hidden'
} else {
items[i].style.visibility = 'visible'
items[i].textContent = itemIndex
}
items[i].style.transform = `translateY(${y}px)`
items[i].style.opacity = Math.abs(offset) < 0.5 ? 1 : 0.4
}
document.getElementById('data3-pos').textContent = Math.round(spring.pos)
document.getElementById('data3-velocity').textContent = Math.round(spring.v)
document.getElementById('data3-dest').textContent = Math.round(spring.dest)
if (animating && (Math.abs(spring.v) > 0.5 || Math.abs(spring.pos - spring.dest) > 0.5)) {
requestAnimationFrame(render)
} else if (animating) {
animating = false
spring.pos = spring.dest
spring.v = 0
render(now)
}
}
function handleDown(y) {
isDragging = true
animating = false
dragStartY = y
dragStartPos = spring.pos
spring.v = 0
pointerHistory = [{ y, time: performance.now() }]
}
function handleMove(y) {
if (!isDragging) return
spring.pos = spring.dest = clamp(dragStartPos + (dragStartY - y), MIN_VALUE * ITEM_HEIGHT, MAX_VALUE * ITEM_HEIGHT)
pointerHistory.push({ y, time: performance.now() })
if (pointerHistory.length > 10) pointerHistory.shift()
render(performance.now())
}
function handleUp() {
if (!isDragging) return
isDragging = false
const now = performance.now()
let velocity = 0
if (pointerHistory.length >= 2) {
let i = pointerHistory.length - 1
while (i > 0 && now - pointerHistory[i].time < 100) i--
const dt = now - pointerHistory[i].time
if (dt > 0) {
velocity = (pointerHistory[i].y - pointerHistory.at(-1).y) / dt * 1000
}
}
// 目标 = 当前位置对齐到最近的格子(限制在 0-60 范围内)
const nearestIndex = clamp(Math.round(spring.pos / ITEM_HEIGHT), MIN_VALUE, MAX_VALUE)
spring.dest = nearestIndex * ITEM_HEIGHT
spring.v = velocity
lastTime = now
animating = true
requestAnimationFrame(render)
}
picker.addEventListener('mousedown', e => handleDown(e.pageY))
picker.addEventListener('touchstart', e => handleDown(e.touches[0].pageY), { passive: true })
window.addEventListener('mousemove', e => handleMove(e.pageY))
window.addEventListener('touchmove', e => handleMove(e.touches[0].pageY), { passive: true })
window.addEventListener('mouseup', handleUp)
window.addEventListener('touchend', handleUp)
// 滑块
document.getElementById('k3-slider').addEventListener('input', e => {
spring.k = +e.target.value
document.getElementById('k3-value').textContent = e.target.value
})
document.getElementById('b3-slider').addEventListener('input', e => {
spring.b = +e.target.value
document.getElementById('b3-value').textContent = e.target.value
})
render(performance.now())
})()
// ============================================
// 第四步:智能对齐
// ============================================
;(function step4() {
const picker = document.getElementById('picker4')
const wheel = document.getElementById('wheel4')
const items = createItems(wheel)
const MS_PER_STEP = 4
const FRICTION = 2
let spring = { pos: 0, dest: 0, v: 0, k: 200, b: 28 }
let isDragging = false
let dragStartY = 0
let dragStartPos = 0
let pointerHistory = []
let animating = false
let lastTime = null
function springStep() {
const dt = MS_PER_STEP / 1000
const Fspring = -spring.k * (spring.pos - spring.dest)
const Fdamper = -spring.b * spring.v
spring.v += (Fspring + Fdamper) * dt
spring.pos += spring.v * dt
}
function calculateSnapTarget(pos, velocity) {
// 预测自然衰减终点
const decayTarget = pos + velocity / FRICTION
document.getElementById('data4-predict').textContent = Math.round(decayTarget)
// 对齐到最近格子,并限制范围
const snappedIndex = clamp(Math.round(decayTarget / ITEM_HEIGHT), MIN_VALUE, MAX_VALUE)
const snapped = snappedIndex * ITEM_HEIGHT
document.getElementById('data4-snap').textContent = snappedIndex
return snapped
}
function render(now) {
if (lastTime !== null && !isDragging) {
const steps = Math.floor((now - lastTime) / MS_PER_STEP)
for (let i = 0; i < steps; i++) springStep()
lastTime += steps * MS_PER_STEP
}
const centerIndex = spring.pos / ITEM_HEIGHT
const startIndex = Math.floor(centerIndex) - 5
for (let i = 0; i < items.length; i++) {
const itemIndex = startIndex + i
const offset = itemIndex - centerIndex
const y = CENTER_Y + offset * ITEM_HEIGHT
if (itemIndex < MIN_VALUE || itemIndex > MAX_VALUE) {
items[i].style.visibility = 'hidden'
} else {
items[i].style.visibility = 'visible'
items[i].textContent = itemIndex
}
items[i].style.transform = `translateY(${y}px)`
items[i].style.opacity = Math.abs(offset) < 0.5 ? 1 : 0.4
}
document.getElementById('data4-pos').textContent = Math.round(spring.pos)
document.getElementById('data4-velocity').textContent = Math.round(spring.v)
document.getElementById('data4-value').textContent = clamp(Math.round(spring.pos / ITEM_HEIGHT), MIN_VALUE, MAX_VALUE)
if (animating && (Math.abs(spring.v) > 0.5 || Math.abs(spring.pos - spring.dest) > 0.5)) {
requestAnimationFrame(render)
} else if (animating) {
animating = false
spring.pos = spring.dest
spring.v = 0
render(now)
}
}
function handleDown(y) {
isDragging = true
animating = false
dragStartY = y
dragStartPos = spring.pos
spring.v = 0
pointerHistory = [{ y, time: performance.now() }]
}
function handleMove(y) {
if (!isDragging) return
spring.pos = spring.dest = clamp(dragStartPos + (dragStartY - y), MIN_VALUE * ITEM_HEIGHT, MAX_VALUE * ITEM_HEIGHT)
pointerHistory.push({ y, time: performance.now() })
if (pointerHistory.length > 10) pointerHistory.shift()
render(performance.now())
}
function handleUp() {
if (!isDragging) return
isDragging = false
const now = performance.now()
let velocity = 0
if (pointerHistory.length >= 2) {
let i = pointerHistory.length - 1
while (i > 0 && now - pointerHistory[i].time < 100) i--
const dt = now - pointerHistory[i].time
if (dt > 0) {
velocity = (pointerHistory[i].y - pointerHistory.at(-1).y) / dt * 1000
}
}
document.getElementById('data4-velocity').textContent = Math.round(velocity)
spring.dest = calculateSnapTarget(spring.pos, velocity)
spring.v = velocity
lastTime = now
animating = true
requestAnimationFrame(render)
}
picker.addEventListener('mousedown', e => handleDown(e.pageY))
picker.addEventListener('touchstart', e => handleDown(e.touches[0].pageY), { passive: true })
window.addEventListener('mousemove', e => handleMove(e.pageY))
window.addEventListener('touchmove', e => handleMove(e.touches[0].pageY), { passive: true })
window.addEventListener('mouseup', handleUp)
window.addEventListener('touchend', handleUp)
render(performance.now())
})()
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment