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 +0 -11
- package/demo/index.html +1 -1
- package/demo/native-main.js +27 -4
- package/demo/shared/demo-native.css +39 -0
- package/demo/shared/geometry-presets.js +2 -2
- package/demo/shared/params-panel.js +20 -0
- package/demo/shared/track-helpers.js +92 -6
- package/dist/bezier-slider.mjs +12 -15
- package/package.json +1 -1
- package/src/bezier-slider.native.js +15 -16
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>
|
package/demo/native-main.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
@@ -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
|
-
|
|
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
|
|
151
|
+
setSvgContent(svg, layout, BezierSliderClass, activeControl);
|
|
66
152
|
}
|
|
67
153
|
|
|
68
154
|
export function readImageFile(file) {
|
package/dist/bezier-slider.mjs
CHANGED
|
@@ -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:
|
|
36
|
-
//
|
|
37
|
-
p2: { x: 0.985, y:
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
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
|
@@ -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:
|
|
47
|
-
p2: { x: 0.985, y:
|
|
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
|
-
*
|
|
376
|
+
* 右端高度微调:仅在 fitted 基础上叠加 P2 的 Y 偏移
|
|
377
|
+
* 不修改 P1,以便控制点 P1 的 X/Y 滑块直接生效
|
|
378
378
|
*/
|
|
379
379
|
function easeRightTilt(bezier, tilt, rightEndOffset) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
|
393
|
+
y: bezier.p2.y - rightEndOffset * tilt
|
|
395
394
|
}
|
|
396
395
|
};
|
|
397
396
|
}
|