bezier-slider 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -618,14 +618,3 @@ console.log(BezierSlider.DEFAULTS);
618
618
  }
619
619
  ```
620
620
 
621
- ---
622
-
623
- ## 版本记录
624
-
625
- | 版本 | 日期 | 说明 |
626
- |------|------|------|
627
- | v3.0 | 2026-06-10 | 支持 Vue 3;添加 `fadeEnabled` 参数;项目结构重构 |
628
- | v2.2 | 2026-06-09 | 支持图片/SVG/font-icon 多种图标;边界拉扯回弹 |
629
- | v2.1 | 2026-06-09 | 抽象公共组件;中文注释;动态布局 |
630
- | v2.0 | 2026-06-09 | 二次贝塞尔滑轨;弧度/间距/回调可配置 |
631
- | v1.0 | 2026-06-09 | 初始圆弧版本(已废弃) |
package/demo/index.html CHANGED
@@ -41,7 +41,7 @@
41
41
  </div>
42
42
  <label class="demo-options" id="debugOption">
43
43
  <input type="checkbox" id="showDebugTrack" checked />
44
- 显示轨迹调试线
44
+ 显示轨迹调试线(含 P0–P2 控制点)
45
45
  </label>
46
46
  <div class="slider-compose" id="sliderCompose">
47
47
  <div class="carousel-bg" id="carouselBg"></div>
@@ -57,6 +57,29 @@ document.addEventListener('DOMContentLoaded', () => {
57
57
  let customBgUrl = null;
58
58
  let bgNaturalSize = { ...DEFAULT_BG_NATURAL };
59
59
  let displaySize = fitImageDisplaySize(bgNaturalSize.width, bgNaturalSize.height);
60
+ let activeControl = null;
61
+
62
+ function refreshDebugOverlay() {
63
+ if (!slider) return;
64
+ updateDebugTrack(
65
+ sliderMount,
66
+ slider.getLayoutState(),
67
+ showDebugTrack.checked,
68
+ BezierSlider,
69
+ activeControl
70
+ );
71
+ }
72
+
73
+ function setActiveControl(hint) {
74
+ activeControl = hint;
75
+ refreshDebugOverlay();
76
+ paramsForm.querySelectorAll('.param-row-cp').forEach((row) => {
77
+ const match = activeControl
78
+ && row.dataset.controlPoint === activeControl.point
79
+ && row.dataset.controlAxis === activeControl.axis;
80
+ row.classList.toggle('is-adjusting', Boolean(match));
81
+ });
82
+ }
60
83
 
61
84
  function getActiveBgUrl() {
62
85
  return customBgUrl;
@@ -144,7 +167,7 @@ document.addEventListener('DOMContentLoaded', () => {
144
167
  },
145
168
  onSlideEnd: (index) => console.log('停留下标:', index),
146
169
  onLayout: (layout) => {
147
- updateDebugTrack(sliderMount, layout, showDebugTrack.checked, BezierSlider);
170
+ updateDebugTrack(sliderMount, layout, showDebugTrack.checked, BezierSlider, activeControl);
148
171
  }
149
172
  });
150
173
 
@@ -193,6 +216,8 @@ document.addEventListener('DOMContentLoaded', () => {
193
216
  const unbindPanel = bindParamsPanel(paramsForm, {
194
217
  getParams: () => params,
195
218
  onParamChange: handleParamChange,
219
+ onControlPointAdjustStart: setActiveControl,
220
+ onControlPointAdjustEnd: () => setActiveControl(null),
196
221
  schema: PARAM_SCHEMA
197
222
  });
198
223
  const unbindGeometryBar = bindGeometryPresetBar(geometryPresetBar, {
@@ -235,9 +260,7 @@ document.addEventListener('DOMContentLoaded', () => {
235
260
  });
236
261
 
237
262
  showDebugTrack.addEventListener('change', () => {
238
- if (slider) {
239
- updateDebugTrack(sliderMount, slider.getLayoutState(), showDebugTrack.checked, BezierSlider);
240
- }
263
+ refreshDebugOverlay();
241
264
  });
242
265
 
243
266
  window.addEventListener('resize', () => {
@@ -80,6 +80,45 @@
80
80
  height: 100%;
81
81
  pointer-events: none;
82
82
  z-index: 2;
83
+ overflow: visible;
84
+ }
85
+
86
+ .debug-track .debug-point-label,
87
+ .debug-track .debug-axis-label,
88
+ .debug-track .debug-point-hint {
89
+ pointer-events: none;
90
+ user-select: none;
91
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
92
+ }
93
+
94
+ .debug-track .debug-point-hint {
95
+ font-size: 11px;
96
+ font-weight: 600;
97
+ }
98
+
99
+ .debug-track .debug-axis-label {
100
+ font-size: 11px;
101
+ font-weight: 600;
102
+ }
103
+
104
+ .debug-track .debug-point-pulse {
105
+ animation: debug-point-pulse 1.2s ease-in-out infinite;
106
+ }
107
+
108
+ @keyframes debug-point-pulse {
109
+ 0%, 100% { opacity: 0.3; transform: scale(1); transform-origin: center; transform-box: fill-box; }
110
+ 50% { opacity: 0.75; transform: scale(1.25); transform-origin: center; transform-box: fill-box; }
111
+ }
112
+
113
+ .param-row-cp.is-adjusting {
114
+ background: rgba(252, 211, 77, 0.08);
115
+ border-radius: 8px;
116
+ margin: 0 -6px;
117
+ padding: 4px 6px;
118
+ }
119
+
120
+ .param-row-cp.is-adjusting label {
121
+ color: #fcd34d;
83
122
  }
84
123
 
85
124
  .legend {
@@ -86,8 +86,8 @@ export const GEOMETRY_PRESETS = [
86
86
  bezier: {
87
87
  fitted: {
88
88
  p0: { x: 0.015, y: 0.48 },
89
- p1: { x: 0.48, y: 1.08 },
90
- p2: { x: 0.985, y: -0.02 }
89
+ p1: { x: 0.48, y: 0.48 },
90
+ p2: { x: 0.985, y: 0.48 }
91
91
  },
92
92
  curveSmooth: 0.1,
93
93
  rightTilt: 1,
@@ -1,9 +1,12 @@
1
1
  import { PARAM_SCHEMA } from './constants.js';
2
2
  import { getByPath, paramFieldId } from './param-utils.js';
3
+ import { parseControlPointPath } from './track-helpers.js';
3
4
 
4
5
  export function bindParamsPanel(paramsForm, {
5
6
  getParams,
6
7
  onParamChange,
8
+ onControlPointAdjustStart,
9
+ onControlPointAdjustEnd,
7
10
  schema = PARAM_SCHEMA,
8
11
  fadeSectionTitle = '动画',
9
12
  animationCheckboxes = [
@@ -48,6 +51,23 @@ export function bindParamsPanel(paramsForm, {
48
51
  output.textContent = String(num);
49
52
  });
50
53
 
54
+ const controlHint = parseControlPointPath(field.path);
55
+ if (controlHint) {
56
+ row.classList.add('param-row-cp');
57
+ row.dataset.controlPoint = controlHint.point;
58
+ row.dataset.controlAxis = controlHint.axis;
59
+
60
+ input.addEventListener('pointerdown', () => {
61
+ onControlPointAdjustStart?.(controlHint);
62
+ window.addEventListener('pointerup', () => {
63
+ onControlPointAdjustEnd?.();
64
+ }, { once: true });
65
+ });
66
+ input.addEventListener('blur', () => {
67
+ onControlPointAdjustEnd?.();
68
+ });
69
+ }
70
+
51
71
  sections.get(field.section).appendChild(row);
52
72
  });
53
73
 
@@ -48,21 +48,107 @@ export function applyBgLayer(bgLayer, bgUrl = null) {
48
48
  bgLayer.style.backgroundSize = '100% 100%';
49
49
  }
50
50
 
51
- export function updateDebugTrack(trackLayer, layout, visible, BezierSliderClass) {
51
+ const CONTROL_POINT_META = {
52
+ p0: { label: 'P0', color: '#38bdf8', influence: [0, 0.45], hint: '左端起点' },
53
+ p1: { label: 'P1', color: '#fb923c', influence: [0.15, 0.85], hint: '中间弧度' },
54
+ p2: { label: 'P2', color: '#c084fc', influence: [0.55, 1], hint: '右端终点' }
55
+ };
56
+
57
+ export function parseControlPointPath(path) {
58
+ const match = String(path).match(/^bezier\.fitted\.(p[012])\.([xy])$/);
59
+ if (!match) return null;
60
+ return { point: match[1], axis: match[2] };
61
+ }
62
+
63
+ function sampleCurveSegment(curve, tStart, tEnd, pointOnCurve, steps = 20) {
64
+ const parts = [];
65
+ for (let i = 0; i <= steps; i++) {
66
+ const t = tStart + (tEnd - tStart) * (i / steps);
67
+ const { x, y } = pointOnCurve(t, curve);
68
+ parts.push(`${i === 0 ? 'M' : 'L'} ${x.toFixed(2)} ${y.toFixed(2)}`);
69
+ }
70
+ return parts.join(' ');
71
+ }
72
+
73
+ function setSvgContent(svg, layout, BezierSliderClass, activeControl) {
74
+ const { bezier } = layout;
75
+ if (!bezier) return;
76
+
77
+ const pointOnCurve = BezierSliderClass.pointOnCurve;
78
+ const curvePath = BezierSliderClass.bezierPath(bezier);
79
+ const { p0, p1, p2 } = bezier;
80
+ const activeKey = activeControl?.point ?? null;
81
+ const activeMeta = activeKey ? CONTROL_POINT_META[activeKey] : null;
82
+
83
+ let influencePath = '';
84
+ if (activeMeta) {
85
+ const [t0, t1] = activeMeta.influence;
86
+ influencePath = `<path class="debug-influence" fill="none" stroke="${activeMeta.color}" stroke-width="5" stroke-linecap="round" opacity="0.55" d="${sampleCurveSegment(bezier, t0, t1, pointOnCurve)}"/>`;
87
+ }
88
+
89
+ let axisHint = '';
90
+ if (activeControl && activeMeta) {
91
+ const pt = bezier[activeControl.point];
92
+ const len = Math.min(56, Math.max(36, layout.width * 0.1));
93
+ const half = len / 2;
94
+ const color = activeMeta.color;
95
+ if (activeControl.axis === 'x') {
96
+ axisHint = `
97
+ <line class="debug-axis" x1="${pt.x - half}" y1="${pt.y}" x2="${pt.x + half}" y2="${pt.y}" stroke="${color}" stroke-width="2" marker-start="url(#debugArrow)" marker-end="url(#debugArrow)" opacity="0.95"/>
98
+ <text class="debug-axis-label" x="${pt.x + half + 6}" y="${pt.y + 4}" fill="${color}">X →</text>`;
99
+ } else {
100
+ axisHint = `
101
+ <line class="debug-axis" x1="${pt.x}" y1="${pt.y - half}" x2="${pt.x}" y2="${pt.y + half}" stroke="${color}" stroke-width="2" marker-start="url(#debugArrow)" marker-end="url(#debugArrow)" opacity="0.95"/>
102
+ <text class="debug-axis-label" x="${pt.x + 6}" y="${pt.y + half + 14}" fill="${color}">Y ↓</text>`;
103
+ }
104
+ }
105
+
106
+ const controlPoints = ['p0', 'p1', 'p2'].map((key) => {
107
+ const pt = bezier[key];
108
+ const meta = CONTROL_POINT_META[key];
109
+ const isActive = key === activeKey;
110
+ const r = isActive ? 7 : 5;
111
+ const opacity = isActive ? 1 : 0.55;
112
+ const stroke = isActive ? '#fff' : meta.color;
113
+ const fill = isActive ? meta.color : 'rgba(0,0,0,0.35)';
114
+ const pulse = isActive
115
+ ? `<circle cx="${pt.x}" cy="${pt.y}" r="14" fill="none" stroke="${meta.color}" stroke-width="2" opacity="0.45" class="debug-point-pulse"/>`
116
+ : '';
117
+ const hint = isActive
118
+ ? `<text class="debug-point-hint" x="${pt.x}" y="${pt.y - 16}" fill="${meta.color}" text-anchor="middle">${meta.hint} · 调${activeControl.axis.toUpperCase()}</text>`
119
+ : '';
120
+ return `${pulse}
121
+ <circle class="debug-point" data-point="${key}" cx="${pt.x}" cy="${pt.y}" r="${r}" fill="${fill}" stroke="${stroke}" stroke-width="2" opacity="${opacity}"/>
122
+ <text class="debug-point-label" x="${pt.x}" y="${pt.y + 4}" fill="${isActive ? '#fff' : meta.color}" text-anchor="middle" font-size="10" font-weight="600">${meta.label}</text>
123
+ ${hint}`;
124
+ }).join('');
125
+
126
+ svg.innerHTML = `
127
+ <defs>
128
+ <marker id="debugArrow" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
129
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor"/>
130
+ </marker>
131
+ </defs>
132
+ ${influencePath}
133
+ <path class="debug-curve" fill="none" stroke="lime" stroke-width="2" stroke-dasharray="6 4" opacity="0.85" d="${curvePath}"/>
134
+ <line class="debug-polygon" x1="${p0.x}" y1="${p0.y}" x2="${p1.x}" y2="${p1.y}" stroke="rgba(255,255,255,0.22)" stroke-width="1" stroke-dasharray="4 3"/>
135
+ <line class="debug-polygon" x1="${p1.x}" y1="${p1.y}" x2="${p2.x}" y2="${p2.y}" stroke="rgba(255,255,255,0.22)" stroke-width="1" stroke-dasharray="4 3"/>
136
+ <g class="debug-axis-hints" color="${activeMeta?.color ?? '#fff'}">${axisHint}</g>
137
+ <g class="debug-control-points">${controlPoints}</g>`;
138
+ }
139
+
140
+ export function updateDebugTrack(trackLayer, layout, visible, BezierSliderClass, activeControl = null) {
52
141
  let svg = trackLayer.querySelector('.debug-track');
53
142
  if (!visible) {
54
143
  svg?.remove();
55
144
  return;
56
145
  }
57
146
  if (!svg) {
58
- trackLayer.insertAdjacentHTML(
59
- 'afterbegin',
60
- '<svg class="debug-track" preserveAspectRatio="none"><path fill="none" stroke="lime" stroke-width="2" stroke-dasharray="6 4" opacity="0.85"/></svg>'
61
- );
147
+ trackLayer.insertAdjacentHTML('afterbegin', '<svg class="debug-track" preserveAspectRatio="none"></svg>');
62
148
  svg = trackLayer.querySelector('.debug-track');
63
149
  }
64
150
  svg.setAttribute('viewBox', getTrackViewBox(layout));
65
- svg.querySelector('path').setAttribute('d', BezierSliderClass.bezierPath(layout.bezier));
151
+ setSvgContent(svg, layout, BezierSliderClass, activeControl);
66
152
  }
67
153
 
68
154
  export function readImageFile(file) {
@@ -32,10 +32,10 @@ var k = (c, a, o) => (q(c, typeof a != "symbol" ? a + "" : a, o), o);
32
32
  fitted: {
33
33
  p0: { x: 0.015, y: 0.48 },
34
34
  // 左端略低(接近原默认高度)
35
- p1: { x: 0.48, y: 1.08 },
36
- // 控制点下压,保留滑轨弧度
37
- p2: { x: 0.985, y: -0.02 }
38
- // 右端偏高
35
+ p1: { x: 0.48, y: 0.48 },
36
+ // 控制点 Y 直接参与弧度(不再被 rightTilt 覆盖)
37
+ p2: { x: 0.985, y: 0.48 }
38
+ // 右端 Y 为基准,rightEndOffset 在其上叠加
39
39
  },
40
40
  curveSmooth: 0.1,
41
41
  // 略平滑,弧线仍清晰
@@ -240,20 +240,17 @@ var k = (c, a, o) => (q(c, typeof a != "symbol" ? a + "" : a, o), o);
240
240
  };
241
241
  }
242
242
  function O(e, t, s) {
243
- const i = e.p0.y, n = i - s, r = {
244
- x: 0.45,
245
- y: (i + n) / 2 + 0.05
246
- };
247
- return {
248
- p0: e.p0,
249
- p1: {
250
- x: e.p1.x + (r.x - e.p1.x) * t,
251
- y: e.p1.y + (r.y - e.p1.y) * t
252
- },
243
+ return t ? {
244
+ p0: { ...e.p0 },
245
+ p1: { ...e.p1 },
253
246
  p2: {
254
247
  x: e.p2.x,
255
- y: e.p2.y + (n - e.p2.y) * t
248
+ y: e.p2.y - s * t
256
249
  }
250
+ } : {
251
+ p0: { ...e.p0 },
252
+ p1: { ...e.p1 },
253
+ p2: { ...e.p2 }
257
254
  };
258
255
  }
259
256
  function D(e, { t, degrees: s }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bezier-slider",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "二次贝塞尔弧形图标滑块组件,支持原生 JavaScript、Vue 3 和 React",
5
5
  "main": "dist/bezier-slider.mjs",
6
6
  "module": "dist/bezier-slider.mjs",
@@ -43,8 +43,8 @@
43
43
  // 基础二次贝塞尔三控制点(归一化坐标 0~1,y 越大越低)
44
44
  fitted: {
45
45
  p0: { x: 0.015, y: 0.48 }, // 左端略低(接近原默认高度)
46
- p1: { x: 0.48, y: 1.08 }, // 控制点下压,保留滑轨弧度
47
- p2: { x: 0.985, y: -0.02 } // 右端偏高
46
+ p1: { x: 0.48, y: 0.48 }, // 控制点 Y 直接参与弧度(不再被 rightTilt 覆盖)
47
+ p2: { x: 0.985, y: 0.48 } // 右端 Y 为基准,rightEndOffset 在其上叠加
48
48
  },
49
49
  curveSmooth: 0.1, // 略平滑,弧线仍清晰
50
50
  rightTilt: 1,
@@ -373,25 +373,24 @@
373
373
  }
374
374
 
375
375
  /**
376
- * 右端高度微调:控制右端相对左端的上扬幅度
377
- * 通过调整 P1、P2y 实现,保持二次贝塞尔类型不变
376
+ * 右端高度微调:仅在 fitted 基础上叠加 P2 的 Y 偏移
377
+ * 不修改 P1,以便控制点 P1 X/Y 滑块直接生效
378
378
  */
379
379
  function easeRightTilt(bezier, tilt, rightEndOffset) {
380
- const leftY = bezier.p0.y;
381
- const targetP2Y = leftY - rightEndOffset;
382
- const targetP1 = {
383
- x: 0.45,
384
- y: (leftY + targetP2Y) / 2 + 0.05
385
- };
380
+ if (!tilt) {
381
+ return {
382
+ p0: { ...bezier.p0 },
383
+ p1: { ...bezier.p1 },
384
+ p2: { ...bezier.p2 }
385
+ };
386
+ }
387
+
386
388
  return {
387
- p0: bezier.p0,
388
- p1: {
389
- x: bezier.p1.x + (targetP1.x - bezier.p1.x) * tilt,
390
- y: bezier.p1.y + (targetP1.y - bezier.p1.y) * tilt
391
- },
389
+ p0: { ...bezier.p0 },
390
+ p1: { ...bezier.p1 },
392
391
  p2: {
393
392
  x: bezier.p2.x,
394
- y: bezier.p2.y + (targetP2Y - bezier.p2.y) * tilt
393
+ y: bezier.p2.y - rightEndOffset * tilt
395
394
  }
396
395
  };
397
396
  }