bezier-slider 1.0.0 → 1.0.2

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  基于**二次贝塞尔曲线**的弧形图标滑块组件。图标沿平滑弧线排列,支持拖动、吸附、边界回弹,以及近大远小的视觉反馈。
4
4
 
5
- > **Arc** 指弧形滑轨(一段曲线,非整圆);**Bezier** 指轨迹由二次贝塞尔(`Q` 命令)描述。
5
+ [demo](https://jimole775.github.io/bezier-slider/demo)
6
6
 
7
7
  ---
8
8
 
@@ -24,38 +24,6 @@
24
24
 
25
25
  ---
26
26
 
27
- ## 文件结构
28
-
29
- ```
30
- slider-master/
31
- ├── demo/
32
- │ ├── index.html # 演示页(滑轨预览 + 参数调节 + 代码输出)
33
- │ ├── native-main.js # 演示入口
34
- │ └── shared/ # 演示公共样式与工具
35
- ├── src/
36
- │ ├── bezier-slider.native.js # 核心:拖动交互、图标布局、贝塞尔轨迹(不含滑轨绘制)
37
- │ ├── track-renderer.js # 可选:默认 SVG 滑轨渲染(ensure / update / renderDefaultTrack)
38
- │ ├── index.js # 包入口 → export BezierSlider + 滑轨工具
39
- │ ├── native-bridge.js # 框架封装共享:pickNativeOptions / mountNativeSlider
40
- │ ├── vue-component.js # Vue 包入口 → export BezierSlider 组件 + 滑轨工具
41
- │ ├── react-component.js # React 包入口 → export BezierSlider 组件 + 滑轨工具
42
- │ └── components/
43
- │ ├── BezierSlider.vue # Vue 3 薄封装(内部使用 native 核心)
44
- │ └── BezierSlider.jsx # React 薄封装(内部使用 native 核心)
45
- ├── dist/ # npm run build 生成(.gitignore,使用前需先 build)
46
- │ ├── bezier-slider.mjs # import 'bezier-slider'
47
- │ ├── bezier-slider.vue.mjs # import 'bezier-slider/vue'
48
- │ ├── bezier-slider.react.mjs # import 'bezier-slider/react'
49
- │ └── style.css # Vue 组件样式
50
- ├── package.json
51
- ├── package-lock.json
52
- ├── vite.config.js
53
- ├── .gitignore
54
- └── README.md
55
- ```
56
-
57
- ---
58
-
59
27
  ## 快速开始
60
28
 
61
29
  ### 方式一:原生 JavaScript
@@ -543,7 +511,7 @@ console.log(BezierSlider.DEFAULTS);
543
511
  | `visibleIconCount` | number | `2` | 可见图标上限 |
544
512
  | `centerT` | number | `0.72` | 滑轨中心点 |
545
513
  | `initialIndex` | number | `0` | 初始化时停在中心的图标下标 |
546
- | `sensitivity` | number | `0.008` | 拖动灵敏度 |
514
+ | `sensitivity` | number | `0.004` | 拖动灵敏度 |
547
515
  | `snapDuration` | number | `300` | 吸附动画时长(ms) |
548
516
  | `rubberBandLimit` | number | `0.42` | 边界拉扯最大越界量 |
549
517
  | `rubberBandDuration` | number | `420` | 边界回弹动画时长(ms) |
@@ -652,16 +620,6 @@ console.log(BezierSlider.DEFAULTS);
652
620
 
653
621
  ---
654
622
 
655
- ## 技术说明
656
-
657
- - 滑轨类型:**二次贝塞尔**(`M … Q …`),非三次贝塞尔,非折线近似
658
- - 图标位置:沿曲线参数 `t` 均匀偏移,中心固定在 `centerT`
659
- - 拖动区域:初始化时通过 `container.clientWidth / clientHeight` 动态测量
660
- - 边界回弹:越界拖动时阻尼递增,松手后以 `easeOutBack` 回弹至合法边界
661
- - 无第三方依赖,纯原生 JavaScript + Vue 3
662
-
663
- ---
664
-
665
623
  ## 版本记录
666
624
 
667
625
  | 版本 | 日期 | 说明 |
package/demo/index.html CHANGED
@@ -10,13 +10,8 @@
10
10
  <body class="demo-native">
11
11
  <h1 class="title">弧形图标滑块 Demo</h1>
12
12
 
13
- <div class="mode-tabs">
14
- <button type="button" class="active" data-mode="svg">默认 SVG 滑轨</button>
15
- <button type="button" data-mode="bg">背景图滑轨</button>
16
- </div>
17
-
18
13
  <p class="demo-desc" id="modeDesc">
19
- 使用内置 renderDefaultTrack onLayout 中绘制 SVG 滑轨。
14
+ 上传背景图按原图比例 1:1 展示;滑轨在独立图层,trackScale&gt;1 可伸出背景外。
20
15
  </p>
21
16
 
22
17
  <div class="demo-layout">
@@ -35,7 +30,7 @@
35
30
  </aside>
36
31
 
37
32
  <div class="demo-stage">
38
- <div class="bg-options hidden" id="bgOptions">
33
+ <div class="bg-options" id="bgOptions">
39
34
  <label class="bg-upload-btn">
40
35
  <input type="file" id="bgUpload" accept="image/*" />
41
36
  上传背景图
@@ -44,7 +39,7 @@
44
39
  <span class="bg-size-hint" id="bgSizeHint"></span>
45
40
  <button type="button" class="bg-reset-btn hidden" id="bgReset">恢复示例</button>
46
41
  </div>
47
- <label class="demo-options hidden" id="debugOption">
42
+ <label class="demo-options" id="debugOption">
48
43
  <input type="checkbox" id="showDebugTrack" checked />
49
44
  显示轨迹调试线
50
45
  </label>
@@ -1,5 +1,5 @@
1
- import { BezierSlider, renderDefaultTrack } from '../src/index.js';
2
- import { ICONS, MODE_DESC, PARAM_SCHEMA, RESET_ICON_SVG, COPY_ICON_SVG } from './shared/constants.js';
1
+ import { BezierSlider } from '../src/index.js';
2
+ import { ICONS, PARAM_SCHEMA, RESET_ICON_SVG, COPY_ICON_SVG } from './shared/constants.js';
3
3
  import {
4
4
  createDefaultParams,
5
5
  buildSliderConfig,
@@ -13,13 +13,11 @@ import { bindCopyButton, bindResetButton } from './shared/clipboard.js';
13
13
  import { CODE_FORMATTERS } from './shared/format-code.js';
14
14
  import {
15
15
  applyComposeLayout,
16
- fitDefaultSvgSize,
17
16
  fitImageDisplaySize,
18
17
  getDisplaySizeHint
19
18
  } from './shared/container-size.js';
20
19
  import {
21
20
  applyBgLayer,
22
- clearBgLayer,
23
21
  clearTrackArtifacts,
24
22
  DEFAULT_BG_NATURAL,
25
23
  loadImageNaturalSize,
@@ -38,17 +36,13 @@ document.addEventListener('DOMContentLoaded', () => {
38
36
  const sliderMount = document.getElementById('sliderMount');
39
37
  const selectedIcon = document.getElementById('selectedIcon');
40
38
  const selectedName = document.getElementById('selectedName');
41
- const modeDesc = document.getElementById('modeDesc');
42
- const bgOptions = document.getElementById('bgOptions');
43
39
  const bgUpload = document.getElementById('bgUpload');
44
40
  const bgFileName = document.getElementById('bgFileName');
45
41
  const bgSizeHint = document.getElementById('bgSizeHint');
46
42
  const bgReset = document.getElementById('bgReset');
47
- const debugOption = document.getElementById('debugOption');
48
43
  const showDebugTrack = document.getElementById('showDebugTrack');
49
44
  const legendExtra = document.getElementById('legendExtra');
50
45
  const legendCenterT = document.getElementById('legendCenterT');
51
- const tabButtons = document.querySelectorAll('.mode-tabs button');
52
46
  const codeTabButtons = document.querySelectorAll('#codeTabs button');
53
47
  const paramsForm = document.getElementById('paramsForm');
54
48
  const codeContent = document.getElementById('codeContent');
@@ -57,27 +51,25 @@ document.addEventListener('DOMContentLoaded', () => {
57
51
  const geometryPresetBar = document.getElementById('geometryPresetBar');
58
52
 
59
53
  let slider = null;
60
- let currentMode = 'svg';
61
54
  let currentCodeTab = 'native';
62
55
  let params = createDefaultParams(BezierSlider.DEFAULTS);
63
56
  let rebuildTimer = null;
64
57
  let customBgUrl = null;
65
- let customBgName = '';
66
58
  let bgNaturalSize = { ...DEFAULT_BG_NATURAL };
67
- let displaySize = fitDefaultSvgSize();
59
+ let displaySize = fitImageDisplaySize(bgNaturalSize.width, bgNaturalSize.height);
68
60
 
69
61
  function getActiveBgUrl() {
70
62
  return customBgUrl;
71
63
  }
72
64
 
65
+ function updateLegendExtra() {
66
+ legendExtra.textContent = customBgUrl ? '自定义背景 + bezier 对齐' : '背景图 + bezier 对齐';
67
+ }
68
+
73
69
  function updateBgSizeHint() {
74
70
  if (!bgSizeHint) return;
75
71
  const hint = getDisplaySizeHint(displaySize, params.trackScale);
76
- if (currentMode === 'bg') {
77
- bgSizeHint.textContent = `背景 ${hint.width}×${hint.height}px · 滑轨容器 ${hint.trackWidth}×${hint.trackHeight}px(trackScale=${hint.trackScale})`;
78
- } else {
79
- bgSizeHint.textContent = '';
80
- }
72
+ bgSizeHint.textContent = `背景 ${hint.width}×${hint.height}px · 滑轨容器 ${hint.trackWidth}×${hint.trackHeight}px(trackScale=${hint.trackScale})`;
81
73
  }
82
74
 
83
75
  function applyDisplayLayout() {
@@ -86,10 +78,8 @@ document.addEventListener('DOMContentLoaded', () => {
86
78
  }
87
79
 
88
80
  function getCodePreviewOptions() {
89
- const sizeHint = getDisplaySizeHint(displaySize, params.trackScale);
90
81
  return {
91
- trackMode: currentMode === 'bg' ? 'bg' : 'svg',
92
- displaySize: sizeHint
82
+ displaySize: getDisplaySizeHint(displaySize, params.trackScale)
93
83
  };
94
84
  }
95
85
 
@@ -107,12 +97,6 @@ document.addEventListener('DOMContentLoaded', () => {
107
97
  updateParamsPreview();
108
98
  }
109
99
 
110
- function refreshBgOverlays() {
111
- if (currentMode !== 'bg' || !slider) return;
112
- const layout = slider.getLayoutState();
113
- updateDebugTrack(sliderMount, layout, showDebugTrack.checked, BezierSlider);
114
- }
115
-
116
100
  async function resolveBgNaturalSize(url) {
117
101
  if (!url) return { ...DEFAULT_BG_NATURAL };
118
102
  try {
@@ -124,40 +108,30 @@ document.addEventListener('DOMContentLoaded', () => {
124
108
 
125
109
  async function applyBackground(url, fileName = '') {
126
110
  customBgUrl = url;
127
- customBgName = fileName;
128
111
  bgFileName.textContent = fileName || '内置示例背景';
129
112
  bgReset.classList.toggle('hidden', !url);
113
+ updateLegendExtra();
130
114
 
131
- if (currentMode === 'bg') {
132
- bgNaturalSize = await resolveBgNaturalSize(url);
133
- displaySize = fitImageDisplaySize(bgNaturalSize.width, bgNaturalSize.height);
134
- applyDisplayLayout();
135
- applyBgLayer(carouselBg, url);
136
- if (slider) {
137
- refreshBgOverlays();
138
- } else {
139
- createSlider('bg', false);
140
- }
115
+ bgNaturalSize = await resolveBgNaturalSize(url);
116
+ displaySize = fitImageDisplaySize(bgNaturalSize.width, bgNaturalSize.height);
117
+ applyDisplayLayout();
118
+ applyBgLayer(carouselBg, url);
119
+ if (slider) {
120
+ slider.initLayout();
121
+ } else {
122
+ createSlider(false);
141
123
  }
142
124
  }
143
125
 
144
- function createSlider(mode, keepIndex) {
126
+ function createSlider(keepIndex) {
145
127
  const prevIndex = keepIndex && slider ? slider.getCurrentIndex() : params.initialIndex;
146
128
 
147
129
  slider?.destroy();
148
130
  clearTrackArtifacts(sliderMount);
149
131
 
150
- const isBgMode = mode === 'bg';
151
-
152
- if (isBgMode) {
153
- displaySize = fitImageDisplaySize(bgNaturalSize.width, bgNaturalSize.height);
154
- applyDisplayLayout();
155
- applyBgLayer(carouselBg, getActiveBgUrl());
156
- } else {
157
- clearBgLayer(carouselBg);
158
- displaySize = fitDefaultSvgSize();
159
- applyDisplayLayout();
160
- }
132
+ displaySize = fitImageDisplaySize(bgNaturalSize.width, bgNaturalSize.height);
133
+ applyDisplayLayout();
134
+ applyBgLayer(carouselBg, getActiveBgUrl());
161
135
 
162
136
  slider = new BezierSlider({
163
137
  container: sliderMount,
@@ -170,11 +144,7 @@ document.addEventListener('DOMContentLoaded', () => {
170
144
  },
171
145
  onSlideEnd: (index) => console.log('停留下标:', index),
172
146
  onLayout: (layout) => {
173
- if (isBgMode) {
174
- updateDebugTrack(sliderMount, layout, showDebugTrack.checked, BezierSlider);
175
- } else {
176
- renderDefaultTrack(sliderMount, layout);
177
- }
147
+ updateDebugTrack(sliderMount, layout, showDebugTrack.checked, BezierSlider);
178
148
  }
179
149
  });
180
150
 
@@ -187,7 +157,7 @@ document.addEventListener('DOMContentLoaded', () => {
187
157
  clearTimeout(rebuildTimer);
188
158
  rebuildTimer = setTimeout(() => {
189
159
  updateParamsPreview();
190
- createSlider(currentMode, true);
160
+ createSlider(true);
191
161
  }, 80);
192
162
  }
193
163
 
@@ -220,21 +190,6 @@ document.addEventListener('DOMContentLoaded', () => {
220
190
  scheduleRebuild();
221
191
  }
222
192
 
223
- function setMode(mode) {
224
- currentMode = mode;
225
- tabButtons.forEach((btn) => {
226
- btn.classList.toggle('active', btn.dataset.mode === mode);
227
- });
228
- modeDesc.textContent = MODE_DESC[mode];
229
- bgOptions.classList.toggle('hidden', mode !== 'bg');
230
- debugOption.classList.toggle('hidden', mode !== 'bg');
231
- legendExtra.textContent = mode === 'bg'
232
- ? (customBgUrl ? '自定义背景 + bezier 对齐' : '背景图 + bezier 对齐')
233
- : '';
234
- updateParamsPreview();
235
- createSlider(mode, false);
236
- }
237
-
238
193
  const unbindPanel = bindParamsPanel(paramsForm, {
239
194
  getParams: () => params,
240
195
  onParamChange: handleParamChange,
@@ -247,11 +202,10 @@ document.addEventListener('DOMContentLoaded', () => {
247
202
  const unbindCopy = bindCopyButton(codeCopyBtn, () => codeContent.textContent);
248
203
 
249
204
  applyDisplayLayout();
205
+ applyBgLayer(carouselBg, null);
206
+ updateLegendExtra();
250
207
  updateParamsPreview();
251
-
252
- tabButtons.forEach((btn) => {
253
- btn.addEventListener('click', () => setMode(btn.dataset.mode));
254
- });
208
+ createSlider(false);
255
209
 
256
210
  codeTabButtons.forEach((btn) => {
257
211
  btn.addEventListener('click', () => setCodeTab(btn.dataset.code));
@@ -281,24 +235,18 @@ document.addEventListener('DOMContentLoaded', () => {
281
235
  });
282
236
 
283
237
  showDebugTrack.addEventListener('change', () => {
284
- if (currentMode === 'bg' && slider) {
238
+ if (slider) {
285
239
  updateDebugTrack(sliderMount, slider.getLayoutState(), showDebugTrack.checked, BezierSlider);
286
240
  }
287
241
  });
288
242
 
289
243
  window.addEventListener('resize', () => {
290
- if (currentMode === 'bg') {
291
- displaySize = fitImageDisplaySize(bgNaturalSize.width, bgNaturalSize.height);
292
- } else {
293
- displaySize = fitDefaultSvgSize();
294
- }
244
+ displaySize = fitImageDisplaySize(bgNaturalSize.width, bgNaturalSize.height);
295
245
  applyDisplayLayout();
296
246
  slider?.initLayout();
297
247
  updateParamsPreview();
298
248
  });
299
249
 
300
- setMode('svg');
301
-
302
250
  window.addEventListener('beforeunload', () => {
303
251
  unbindPanel();
304
252
  unbindGeometryBar();
@@ -29,11 +29,6 @@ export const PARAM_SCHEMA = [
29
29
  { section: '控制点 P2', path: 'bezier.fitted.p2.y', label: '终点 Y', min: -1, max: 2, step: 0.01 }
30
30
  ];
31
31
 
32
- export const MODE_DESC = {
33
- svg: 'SVG 滑轨由 renderDefaultTrack 绘制;左侧调 bezier 可实时改弧度。',
34
- bg: '上传背景图按原图比例 1:1 展示;滑轨在独立图层,trackScale>1 可伸出背景外。'
35
- };
36
-
37
32
  export const RESET_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
38
33
  <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
39
34
  <path d="M3 3v5h5"/>
@@ -1,28 +1,3 @@
1
- .mode-tabs {
2
- display: flex;
3
- gap: 8px;
4
- margin-bottom: 12px;
5
- flex-wrap: wrap;
6
- justify-content: center;
7
- padding: 0 16px;
8
- }
9
-
10
- .mode-tabs button {
11
- border: 1px solid rgba(255, 255, 255, 0.25);
12
- background: rgba(255, 255, 255, 0.08);
13
- color: rgba(255, 255, 255, 0.75);
14
- padding: 8px 16px;
15
- border-radius: 999px;
16
- cursor: pointer;
17
- font-size: 13px;
18
- }
19
-
20
- .mode-tabs button.active {
21
- background: rgba(252, 211, 77, 0.2);
22
- border-color: #fcd34d;
23
- color: #fcd34d;
24
- }
25
-
26
1
  .demo-options {
27
2
  display: flex;
28
3
  align-items: center;
@@ -32,8 +7,6 @@
32
7
  font-size: 13px;
33
8
  }
34
9
 
35
- .demo-options.hidden { visibility: hidden; }
36
-
37
10
  .bg-options {
38
11
  display: flex;
39
12
  align-items: center;
@@ -54,8 +27,6 @@
54
27
  color: rgba(255, 255, 255, 0.45);
55
28
  }
56
29
 
57
- .bg-options.hidden { display: none; }
58
-
59
30
  .bg-upload-btn {
60
31
  display: inline-flex;
61
32
  align-items: center;
@@ -70,11 +70,10 @@ function buildIconsBlock(indent = '') {
70
70
 
71
71
  export function formatNativeCode(params, options = {}) {
72
72
  const cfg = buildSliderConfig(params);
73
- const { trackMode = 'svg', displaySize } = options;
73
+ const { displaySize } = options;
74
74
  const sliderOptions = buildSliderOptions(cfg);
75
75
 
76
- if (trackMode === 'bg') {
77
- return `<!-- 复制 HTML + 下方 script 即可运行 -->
76
+ return `<!-- 复制 HTML + 下方 script 即可运行 -->
78
77
  ${buildContainerHtml(displaySize)}
79
78
 
80
79
  <script type="module">
@@ -92,42 +91,17 @@ ${sliderOptions},
92
91
  // 滑轨由页面背景承载;layout.bezier 可用于对齐调试
93
92
  }
94
93
  });
95
- </script>`;
96
- }
97
-
98
- return `<!-- 复制 HTML + 下方 script 即可运行 -->
99
- ${buildContainerHtml(displaySize)}
100
-
101
- <script type="module">
102
- import BezierSlider, { renderDefaultTrack } from 'bezier-slider';
103
-
104
- ${buildIconsBlock()}
105
-
106
- const container = document.getElementById('slider');
107
-
108
- new BezierSlider({
109
- container,
110
- icons,
111
- ${sliderOptions},
112
- onLayout: (layout) => renderDefaultTrack(container, layout)
113
- });
114
94
  </script>`;
115
95
  }
116
96
 
117
97
  export function formatReactCode(params, options = {}) {
118
98
  const cfg = buildSliderConfig(params);
119
- const { trackMode = 'svg', displaySize } = options;
99
+ const { displaySize } = options;
120
100
  const { width, height } = buildContainerSize(displaySize);
121
101
  const sliderOptions = buildReactProps(cfg);
122
102
  const icons = formatObjectBlock(ICONS, 6);
123
- const importLine = trackMode === 'bg'
124
- ? "import BezierSlider from 'bezier-slider/react';"
125
- : "import BezierSlider, { renderDefaultTrack } from 'bezier-slider/react';";
126
- const renderTrackProp = trackMode === 'bg'
127
- ? ' renderTrack={null}'
128
- : ' renderTrack={renderDefaultTrack}';
129
103
 
130
- return `${importLine}
104
+ return `import BezierSlider from 'bezier-slider/react';
131
105
 
132
106
  const icons = ${icons};
133
107
 
@@ -137,7 +111,7 @@ export function SliderDemo() {
137
111
  style={{ position: 'relative', overflow: 'visible', width: ${width}, height: ${height} }}
138
112
  icons={icons}
139
113
  ${sliderOptions}
140
- ${renderTrackProp}
114
+ renderTrack={null}
141
115
  onSelect={handleSelect}
142
116
  onSlideEnd={handleSlideEnd}
143
117
  />
@@ -147,11 +121,10 @@ ${renderTrackProp}
147
121
 
148
122
  export function formatVueCode(params, options = {}) {
149
123
  const cfg = buildSliderConfig(params);
150
- const { trackMode = 'svg', displaySize } = options;
124
+ const { displaySize } = options;
151
125
  const { width, height } = buildContainerSize(displaySize);
152
126
  const sliderOptions = buildVueProps(cfg);
153
127
  const icons = formatObjectBlock(ICONS, 2);
154
- const renderTrackAttr = trackMode === 'bg' ? '\n :render-track="null"' : '';
155
128
 
156
129
  return `<script setup>
157
130
  import { BezierSlider } from 'bezier-slider/vue';
@@ -163,7 +136,8 @@ const icons = ${icons};
163
136
  <BezierSlider
164
137
  :root-style="{ position: 'relative', overflow: 'visible', width: '${width}px', height: '${height}px' }"
165
138
  :icons="icons"
166
- ${sliderOptions}${renderTrackAttr}
139
+ ${sliderOptions}
140
+ :render-track="null"
167
141
  @select="onSelect"
168
142
  @slide-end="onSlideEnd"
169
143
  />