bezier-slider 1.0.3 → 1.0.4
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/demo/index.html +16 -8
- package/demo/native-main.js +65 -5
- package/demo/shared/code-highlight.js +41 -0
- package/demo/shared/constants.js +5 -0
- package/demo/shared/demo-native.css +85 -5
- package/demo/shared/demo.css +124 -6
- package/demo/shared/geometry-preset-bar.js +67 -16
- package/demo/shared/params-panel.js +97 -12
- package/demo/shared/saved-presets.js +66 -0
- package/package.json +5 -4
package/demo/index.html
CHANGED
|
@@ -18,13 +18,21 @@
|
|
|
18
18
|
<aside class="params-panel" id="paramsPanel">
|
|
19
19
|
<div class="params-panel-header">
|
|
20
20
|
<h2>调节参数</h2>
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
<div class="params-panel-actions">
|
|
22
|
+
<button
|
|
23
|
+
type="button"
|
|
24
|
+
class="params-save"
|
|
25
|
+
id="paramsSave"
|
|
26
|
+
data-tooltip="保存到快捷预设"
|
|
27
|
+
>保存</button>
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
class="params-reset"
|
|
31
|
+
id="paramsReset"
|
|
32
|
+
aria-label="恢复默认"
|
|
33
|
+
data-tooltip="恢复默认"
|
|
34
|
+
></button>
|
|
35
|
+
</div>
|
|
28
36
|
</div>
|
|
29
37
|
<div id="paramsForm"></div>
|
|
30
38
|
</aside>
|
|
@@ -73,7 +81,7 @@
|
|
|
73
81
|
<button type="button" data-code="react">React</button>
|
|
74
82
|
<button type="button" data-code="vue">Vue</button>
|
|
75
83
|
</div>
|
|
76
|
-
<pre class="code-content" id="codeContent"></pre>
|
|
84
|
+
<pre class="code-content" id="codeContent"><code></code></pre>
|
|
77
85
|
</aside>
|
|
78
86
|
</div>
|
|
79
87
|
|
package/demo/native-main.js
CHANGED
|
@@ -9,8 +9,14 @@ import {
|
|
|
9
9
|
} from './shared/param-utils.js';
|
|
10
10
|
import { bindParamsPanel } from './shared/params-panel.js';
|
|
11
11
|
import { bindGeometryPresetBar } from './shared/geometry-preset-bar.js';
|
|
12
|
+
import {
|
|
13
|
+
buildPresetSnapshot,
|
|
14
|
+
removeSavedPreset,
|
|
15
|
+
savePresetSnapshot
|
|
16
|
+
} from './shared/saved-presets.js';
|
|
12
17
|
import { bindCopyButton, bindResetButton } from './shared/clipboard.js';
|
|
13
18
|
import { CODE_FORMATTERS } from './shared/format-code.js';
|
|
19
|
+
import { getCodeTabLanguage, getPlainCodeText, renderHighlightedCode } from './shared/code-highlight.js';
|
|
14
20
|
import {
|
|
15
21
|
applyComposeLayout,
|
|
16
22
|
fitImageDisplaySize,
|
|
@@ -48,6 +54,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
48
54
|
const codeContent = document.getElementById('codeContent');
|
|
49
55
|
const codeCopyBtn = document.getElementById('codeCopyBtn');
|
|
50
56
|
const paramsReset = document.getElementById('paramsReset');
|
|
57
|
+
const paramsSave = document.getElementById('paramsSave');
|
|
51
58
|
const geometryPresetBar = document.getElementById('geometryPresetBar');
|
|
52
59
|
|
|
53
60
|
let slider = null;
|
|
@@ -108,7 +115,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
108
115
|
|
|
109
116
|
function updateParamsPreview() {
|
|
110
117
|
const formatter = CODE_FORMATTERS[currentCodeTab] ?? CODE_FORMATTERS.native;
|
|
111
|
-
|
|
118
|
+
const source = formatter(params, getCodePreviewOptions());
|
|
119
|
+
renderHighlightedCode(codeContent, source, getCodeTabLanguage(currentCodeTab));
|
|
112
120
|
legendCenterT.textContent = `centerT = ${params.centerT}`;
|
|
113
121
|
}
|
|
114
122
|
|
|
@@ -213,6 +221,55 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
213
221
|
scheduleRebuild();
|
|
214
222
|
}
|
|
215
223
|
|
|
224
|
+
async function applySavedPreset(preset) {
|
|
225
|
+
params = deepClone(preset.params);
|
|
226
|
+
syncParamsForm(paramsForm, params, PARAM_SCHEMA);
|
|
227
|
+
|
|
228
|
+
if (preset.bgUrl) {
|
|
229
|
+
bgNaturalSize = { ...preset.bgNaturalSize };
|
|
230
|
+
await applyBackground(preset.bgUrl, preset.bgFileName || '已存背景');
|
|
231
|
+
} else {
|
|
232
|
+
bgNaturalSize = preset.bgNaturalSize
|
|
233
|
+
? { ...preset.bgNaturalSize }
|
|
234
|
+
: { ...DEFAULT_BG_NATURAL };
|
|
235
|
+
await applyBackground(null);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
updateParamsPreview();
|
|
239
|
+
createSlider(true);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function handleDeleteSavedPreset(preset) {
|
|
243
|
+
removeSavedPreset(preset.id);
|
|
244
|
+
geometryBarApi.refresh();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function handleSavePreset() {
|
|
248
|
+
const defaultLabel = customBgUrl
|
|
249
|
+
? (bgFileName.textContent || '我的配置')
|
|
250
|
+
: `配置 ${new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
|
|
251
|
+
const label = window.prompt('为当前配置命名', defaultLabel);
|
|
252
|
+
if (label == null || !label.trim()) return;
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const snapshot = buildPresetSnapshot({
|
|
256
|
+
label: label.trim(),
|
|
257
|
+
params,
|
|
258
|
+
bgUrl: customBgUrl,
|
|
259
|
+
bgFileName: bgFileName.textContent,
|
|
260
|
+
bgNaturalSize
|
|
261
|
+
});
|
|
262
|
+
savePresetSnapshot(snapshot);
|
|
263
|
+
geometryBarApi.refresh();
|
|
264
|
+
|
|
265
|
+
if (snapshot.bgOmitted && customBgUrl) {
|
|
266
|
+
window.alert('配置已保存。背景图过大未写入本地,加载该预设后请重新上传背景图。');
|
|
267
|
+
}
|
|
268
|
+
} catch (err) {
|
|
269
|
+
window.alert(err.message || '保存失败');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
216
273
|
const unbindPanel = bindParamsPanel(paramsForm, {
|
|
217
274
|
getParams: () => params,
|
|
218
275
|
onParamChange: handleParamChange,
|
|
@@ -220,11 +277,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
220
277
|
onControlPointAdjustEnd: () => setActiveControl(null),
|
|
221
278
|
schema: PARAM_SCHEMA
|
|
222
279
|
});
|
|
223
|
-
const
|
|
224
|
-
onPreset: handleGeometryPreset
|
|
280
|
+
const geometryBarApi = bindGeometryPresetBar(geometryPresetBar, {
|
|
281
|
+
onPreset: handleGeometryPreset,
|
|
282
|
+
onSavedPreset: applySavedPreset,
|
|
283
|
+
onDeleteSaved: handleDeleteSavedPreset
|
|
225
284
|
});
|
|
226
285
|
const unbindReset = bindResetButton(paramsReset, handleReset);
|
|
227
|
-
|
|
286
|
+
paramsSave?.addEventListener('click', handleSavePreset);
|
|
287
|
+
const unbindCopy = bindCopyButton(codeCopyBtn, () => getPlainCodeText(codeContent));
|
|
228
288
|
|
|
229
289
|
applyDisplayLayout();
|
|
230
290
|
applyBgLayer(carouselBg, null);
|
|
@@ -272,7 +332,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
272
332
|
|
|
273
333
|
window.addEventListener('beforeunload', () => {
|
|
274
334
|
unbindPanel();
|
|
275
|
-
|
|
335
|
+
geometryBarApi.destroy();
|
|
276
336
|
unbindReset();
|
|
277
337
|
unbindCopy();
|
|
278
338
|
slider?.destroy();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import hljs from 'highlight.js/lib/common';
|
|
2
|
+
import 'highlight.js/styles/atom-one-dark.min.css';
|
|
3
|
+
|
|
4
|
+
const CODE_TAB_LANGUAGES = {
|
|
5
|
+
native: 'html',
|
|
6
|
+
react: 'javascript',
|
|
7
|
+
vue: 'xml'
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function ensureCodeElement(container) {
|
|
11
|
+
let codeEl = container.querySelector('code');
|
|
12
|
+
if (!codeEl) {
|
|
13
|
+
container.textContent = '';
|
|
14
|
+
codeEl = document.createElement('code');
|
|
15
|
+
container.appendChild(codeEl);
|
|
16
|
+
}
|
|
17
|
+
return codeEl;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getCodeTabLanguage(tab) {
|
|
21
|
+
return CODE_TAB_LANGUAGES[tab] ?? 'javascript';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function renderHighlightedCode(container, source, language) {
|
|
25
|
+
const codeEl = ensureCodeElement(container);
|
|
26
|
+
const lang = hljs.getLanguage(language) ? language : 'javascript';
|
|
27
|
+
|
|
28
|
+
codeEl.removeAttribute('data-highlighted');
|
|
29
|
+
codeEl.className = `hljs language-${lang}`;
|
|
30
|
+
codeEl.textContent = source;
|
|
31
|
+
|
|
32
|
+
if (source.trim()) {
|
|
33
|
+
hljs.highlightElement(codeEl);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return source;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getPlainCodeText(container) {
|
|
40
|
+
return container.querySelector('code')?.textContent ?? container.textContent ?? '';
|
|
41
|
+
}
|
package/demo/shared/constants.js
CHANGED
|
@@ -42,3 +42,8 @@ export const COPY_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="curre
|
|
|
42
42
|
export const COPIED_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
43
43
|
<polyline points="20 6 9 17 4 12"/>
|
|
44
44
|
</svg>`;
|
|
45
|
+
|
|
46
|
+
export const DELETE_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
|
|
47
|
+
<path d="M6 6l12 12"/>
|
|
48
|
+
<path d="M18 6L6 18"/>
|
|
49
|
+
</svg>`;
|
|
@@ -110,11 +110,24 @@
|
|
|
110
110
|
50% { opacity: 0.75; transform: scale(1.25); transform-origin: center; transform-box: fill-box; }
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
.param-row-cp
|
|
114
|
-
|
|
113
|
+
.param-row-cp {
|
|
114
|
+
position: relative;
|
|
115
|
+
z-index: 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.param-row-cp::before {
|
|
119
|
+
content: '';
|
|
120
|
+
position: absolute;
|
|
121
|
+
inset: -2px -6px;
|
|
115
122
|
border-radius: 8px;
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
background: transparent;
|
|
124
|
+
pointer-events: none;
|
|
125
|
+
z-index: -1;
|
|
126
|
+
transition: background 0.15s ease;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.param-row-cp.is-adjusting::before {
|
|
130
|
+
background: rgba(252, 211, 77, 0.08);
|
|
118
131
|
}
|
|
119
132
|
|
|
120
133
|
.param-row-cp.is-adjusting label {
|
|
@@ -150,12 +163,79 @@
|
|
|
150
163
|
display: flex;
|
|
151
164
|
flex-wrap: wrap;
|
|
152
165
|
justify-content: center;
|
|
166
|
+
align-items: center;
|
|
153
167
|
gap: 8px;
|
|
154
|
-
max-width: min(calc(100vw - 32px),
|
|
168
|
+
max-width: min(calc(100vw - 32px), 900px);
|
|
155
169
|
transform: translateX(-50%);
|
|
156
170
|
pointer-events: none;
|
|
157
171
|
}
|
|
158
172
|
|
|
173
|
+
.geometry-preset-divider {
|
|
174
|
+
width: 1px;
|
|
175
|
+
height: 22px;
|
|
176
|
+
background: rgba(255, 255, 255, 0.2);
|
|
177
|
+
pointer-events: none;
|
|
178
|
+
flex-shrink: 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.geometry-preset-bar .geometry-preset-btn-saved {
|
|
182
|
+
background: rgba(59, 130, 246, 0.35);
|
|
183
|
+
border-color: rgba(147, 197, 253, 0.45);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.geometry-preset-bar .geometry-preset-btn-saved:hover {
|
|
187
|
+
background: rgba(59, 130, 246, 0.5);
|
|
188
|
+
border-color: rgba(191, 219, 254, 0.65);
|
|
189
|
+
color: #fff;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.geometry-preset-saved-item {
|
|
193
|
+
position: relative;
|
|
194
|
+
pointer-events: auto;
|
|
195
|
+
flex-shrink: 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.geometry-preset-delete {
|
|
199
|
+
position: absolute;
|
|
200
|
+
top: -5px;
|
|
201
|
+
right: -5px;
|
|
202
|
+
z-index: 2;
|
|
203
|
+
display: flex;
|
|
204
|
+
align-items: center;
|
|
205
|
+
justify-content: center;
|
|
206
|
+
width: 16px;
|
|
207
|
+
height: 16px;
|
|
208
|
+
padding: 0;
|
|
209
|
+
border: 1px solid rgba(255, 255, 255, 0.35);
|
|
210
|
+
border-radius: 50%;
|
|
211
|
+
background: rgba(15, 23, 42, 0.92);
|
|
212
|
+
color: rgba(255, 255, 255, 0.9);
|
|
213
|
+
cursor: pointer;
|
|
214
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
|
|
215
|
+
opacity: 0;
|
|
216
|
+
pointer-events: none;
|
|
217
|
+
transition: opacity 0.15s, background 0.15s, color 0.15s, border-color 0.15s, transform 0.15s;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.geometry-preset-saved-item:hover .geometry-preset-delete,
|
|
221
|
+
.geometry-preset-saved-item:focus-within .geometry-preset-delete {
|
|
222
|
+
opacity: 1;
|
|
223
|
+
pointer-events: auto;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.geometry-preset-delete svg {
|
|
227
|
+
width: 9px;
|
|
228
|
+
height: 9px;
|
|
229
|
+
display: block;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.geometry-preset-delete:hover {
|
|
233
|
+
background: rgba(239, 68, 68, 0.95);
|
|
234
|
+
border-color: rgba(254, 202, 202, 0.85);
|
|
235
|
+
color: #fff;
|
|
236
|
+
transform: scale(1.08);
|
|
237
|
+
}
|
|
238
|
+
|
|
159
239
|
.geometry-preset-bar .geometry-preset-btn {
|
|
160
240
|
pointer-events: auto;
|
|
161
241
|
padding: 7px 14px;
|
package/demo/shared/demo.css
CHANGED
|
@@ -99,9 +99,14 @@ body {
|
|
|
99
99
|
z-index: 20;
|
|
100
100
|
backdrop-filter: blur(8px);
|
|
101
101
|
flex-shrink: 0;
|
|
102
|
+
box-sizing: border-box;
|
|
103
|
+
min-width: 200px;
|
|
104
|
+
width: 20%;
|
|
105
|
+
max-width: 20%;
|
|
106
|
+
overflow-y: auto;
|
|
102
107
|
}
|
|
103
108
|
|
|
104
|
-
.params-panel {
|
|
109
|
+
/* .params-panel {
|
|
105
110
|
width: 260px;
|
|
106
111
|
overflow-y: auto;
|
|
107
112
|
}
|
|
@@ -109,13 +114,14 @@ body {
|
|
|
109
114
|
.code-panel {
|
|
110
115
|
width: 320px;
|
|
111
116
|
overflow-y: auto;
|
|
112
|
-
}
|
|
117
|
+
} */
|
|
113
118
|
|
|
114
119
|
body.demo-native .params-panel {
|
|
115
120
|
position: absolute;
|
|
116
121
|
top: 20px;
|
|
117
122
|
left: 20px;
|
|
118
123
|
height: calc(100vh - 80px);
|
|
124
|
+
scrollbar-gutter: stable;
|
|
119
125
|
}
|
|
120
126
|
|
|
121
127
|
body.demo-native .code-panel {
|
|
@@ -134,6 +140,61 @@ body.demo-native .code-panel {
|
|
|
134
140
|
margin-bottom: 12px;
|
|
135
141
|
}
|
|
136
142
|
|
|
143
|
+
.params-panel-actions {
|
|
144
|
+
display: flex;
|
|
145
|
+
align-items: center;
|
|
146
|
+
gap: 6px;
|
|
147
|
+
flex-shrink: 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.params-save {
|
|
151
|
+
position: relative;
|
|
152
|
+
flex-shrink: 0;
|
|
153
|
+
padding: 4px 10px;
|
|
154
|
+
width: 46px;
|
|
155
|
+
height: 28px;
|
|
156
|
+
display: flex;
|
|
157
|
+
align-items: center;
|
|
158
|
+
justify-content: center;
|
|
159
|
+
border: 1px solid rgba(252, 211, 77, 0.35);
|
|
160
|
+
border-radius: 6px;
|
|
161
|
+
background: rgba(252, 211, 77, 0.12);
|
|
162
|
+
color: #fcd34d;
|
|
163
|
+
font-size: 11px;
|
|
164
|
+
/* line-height: 1.2; */
|
|
165
|
+
cursor: pointer;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.params-save:hover {
|
|
169
|
+
background: rgba(252, 211, 77, 0.22);
|
|
170
|
+
border-color: rgba(252, 211, 77, 0.5);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.params-save::after {
|
|
174
|
+
content: attr(data-tooltip);
|
|
175
|
+
position: absolute;
|
|
176
|
+
top: calc(100% + 6px);
|
|
177
|
+
right: 0;
|
|
178
|
+
padding: 4px 8px;
|
|
179
|
+
border-radius: 4px;
|
|
180
|
+
background: rgba(0, 0, 0, 0.85);
|
|
181
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
182
|
+
color: rgba(255, 255, 255, 0.9);
|
|
183
|
+
font-size: 11px;
|
|
184
|
+
white-space: nowrap;
|
|
185
|
+
pointer-events: none;
|
|
186
|
+
opacity: 0;
|
|
187
|
+
transform: translateY(-2px);
|
|
188
|
+
transition: opacity 0.15s, transform 0.15s;
|
|
189
|
+
z-index: 1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.params-save:hover::after,
|
|
193
|
+
.params-save:focus-visible::after {
|
|
194
|
+
opacity: 1;
|
|
195
|
+
transform: translateY(0);
|
|
196
|
+
}
|
|
197
|
+
|
|
137
198
|
.params-panel-header h2,
|
|
138
199
|
.code-panel-header h2 {
|
|
139
200
|
margin: 0;
|
|
@@ -236,10 +297,12 @@ body.demo-native .code-panel {
|
|
|
236
297
|
|
|
237
298
|
.param-row {
|
|
238
299
|
display: grid;
|
|
239
|
-
grid-template-columns: 100px 1fr
|
|
300
|
+
grid-template-columns: 100px 1fr 44px;
|
|
240
301
|
gap: 6px;
|
|
241
302
|
align-items: center;
|
|
242
303
|
margin-bottom: 6px;
|
|
304
|
+
min-height: 24px;
|
|
305
|
+
width: 100%;
|
|
243
306
|
}
|
|
244
307
|
|
|
245
308
|
.param-row label {
|
|
@@ -247,8 +310,44 @@ body.demo-native .code-panel {
|
|
|
247
310
|
color: rgba(255, 255, 255, 0.72);
|
|
248
311
|
}
|
|
249
312
|
|
|
313
|
+
.param-stepper {
|
|
314
|
+
display: grid;
|
|
315
|
+
grid-template-columns: 24px 1fr 24px;
|
|
316
|
+
gap: 4px;
|
|
317
|
+
align-items: center;
|
|
318
|
+
min-width: 0;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.param-step-btn {
|
|
322
|
+
width: 24px;
|
|
323
|
+
height: 24px;
|
|
324
|
+
padding: 0;
|
|
325
|
+
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
326
|
+
border-radius: 6px;
|
|
327
|
+
background: rgba(255, 255, 255, 0.06);
|
|
328
|
+
color: rgba(255, 255, 255, 0.85);
|
|
329
|
+
font-size: 15px;
|
|
330
|
+
line-height: 1;
|
|
331
|
+
cursor: pointer;
|
|
332
|
+
flex-shrink: 0;
|
|
333
|
+
touch-action: manipulation;
|
|
334
|
+
user-select: none;
|
|
335
|
+
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.param-step-btn:hover {
|
|
339
|
+
background: rgba(252, 211, 77, 0.15);
|
|
340
|
+
border-color: rgba(252, 211, 77, 0.35);
|
|
341
|
+
color: #fcd34d;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.param-step-btn:active {
|
|
345
|
+
background: rgba(252, 211, 77, 0.25);
|
|
346
|
+
}
|
|
347
|
+
|
|
250
348
|
.param-row input[type="range"] {
|
|
251
349
|
width: 100%;
|
|
350
|
+
min-width: 0;
|
|
252
351
|
accent-color: #fcd34d;
|
|
253
352
|
}
|
|
254
353
|
|
|
@@ -257,6 +356,12 @@ body.demo-native .code-panel {
|
|
|
257
356
|
color: #fcd34d;
|
|
258
357
|
text-align: right;
|
|
259
358
|
font-variant-numeric: tabular-nums;
|
|
359
|
+
width: 44px;
|
|
360
|
+
min-height: 1.5em;
|
|
361
|
+
line-height: 1.5;
|
|
362
|
+
white-space: nowrap;
|
|
363
|
+
overflow: hidden;
|
|
364
|
+
text-overflow: ellipsis;
|
|
260
365
|
}
|
|
261
366
|
|
|
262
367
|
.param-check {
|
|
@@ -328,16 +433,28 @@ body.demo-native .code-copy-btn:focus-visible::after {
|
|
|
328
433
|
}
|
|
329
434
|
|
|
330
435
|
.code-content {
|
|
331
|
-
padding:
|
|
436
|
+
padding: 0;
|
|
332
437
|
background: rgba(0, 0, 0, 0.35);
|
|
333
438
|
border-radius: 8px;
|
|
334
439
|
font-family: ui-monospace, Consolas, monospace;
|
|
335
440
|
font-size: 11px;
|
|
336
441
|
line-height: 1.5;
|
|
337
|
-
color: rgba(255, 255, 255, 0.8);
|
|
338
442
|
white-space: pre-wrap;
|
|
339
443
|
word-break: break-all;
|
|
340
444
|
overflow-y: auto;
|
|
445
|
+
margin: 0;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.code-content code.hljs {
|
|
449
|
+
display: block;
|
|
450
|
+
padding: 12px;
|
|
451
|
+
background: transparent;
|
|
452
|
+
overflow: visible;
|
|
453
|
+
white-space: pre-wrap;
|
|
454
|
+
word-break: break-all;
|
|
455
|
+
font-family: inherit;
|
|
456
|
+
font-size: inherit;
|
|
457
|
+
line-height: inherit;
|
|
341
458
|
}
|
|
342
459
|
|
|
343
460
|
body.demo-native .code-content {
|
|
@@ -376,7 +493,8 @@ body.demo-native .code-content {
|
|
|
376
493
|
.params-panel,
|
|
377
494
|
.code-panel {
|
|
378
495
|
width: 100%;
|
|
379
|
-
|
|
496
|
+
min-width: 200px;
|
|
497
|
+
max-width: 100%;
|
|
380
498
|
position: static;
|
|
381
499
|
max-height: none;
|
|
382
500
|
}
|
|
@@ -1,25 +1,76 @@
|
|
|
1
1
|
import { GEOMETRY_PRESETS } from './geometry-presets.js';
|
|
2
|
-
|
|
2
|
+
import { DELETE_ICON_SVG } from './constants.js';
|
|
3
|
+
import { loadSavedPresets } from './saved-presets.js';
|
|
3
4
|
export function bindGeometryPresetBar(container, {
|
|
4
5
|
presets = GEOMETRY_PRESETS,
|
|
5
|
-
|
|
6
|
+
getSavedPresets = loadSavedPresets,
|
|
7
|
+
onPreset,
|
|
8
|
+
onSavedPreset,
|
|
9
|
+
onDeleteSaved
|
|
6
10
|
} = {}) {
|
|
7
|
-
if (!container
|
|
8
|
-
return ()
|
|
11
|
+
if (!container) {
|
|
12
|
+
return { refresh() {}, destroy() {} };
|
|
9
13
|
}
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
presets.forEach((preset) => {
|
|
13
|
-
const btn = document.createElement('button');
|
|
14
|
-
btn.type = 'button';
|
|
15
|
-
btn.className = 'geometry-preset-btn';
|
|
16
|
-
btn.dataset.preset = preset.id;
|
|
17
|
-
btn.textContent = preset.label;
|
|
18
|
-
btn.addEventListener('click', () => onPreset(preset));
|
|
19
|
-
container.appendChild(btn);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
return () => {
|
|
15
|
+
const render = () => {
|
|
23
16
|
container.innerHTML = '';
|
|
17
|
+
|
|
18
|
+
presets.forEach((preset) => {
|
|
19
|
+
const btn = document.createElement('button');
|
|
20
|
+
btn.type = 'button';
|
|
21
|
+
btn.className = 'geometry-preset-btn';
|
|
22
|
+
btn.dataset.preset = preset.id;
|
|
23
|
+
btn.textContent = preset.label;
|
|
24
|
+
btn.addEventListener('click', () => onPreset?.(preset));
|
|
25
|
+
container.appendChild(btn);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const savedList = typeof getSavedPresets === 'function' ? getSavedPresets() : [];
|
|
29
|
+
if (savedList.length === 0) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const divider = document.createElement('span');
|
|
34
|
+
divider.className = 'geometry-preset-divider';
|
|
35
|
+
divider.setAttribute('aria-hidden', 'true');
|
|
36
|
+
container.appendChild(divider);
|
|
37
|
+
|
|
38
|
+
savedList.forEach((preset) => {
|
|
39
|
+
const wrap = document.createElement('div');
|
|
40
|
+
wrap.className = 'geometry-preset-saved-item';
|
|
41
|
+
|
|
42
|
+
const btn = document.createElement('button');
|
|
43
|
+
btn.type = 'button';
|
|
44
|
+
btn.className = 'geometry-preset-btn geometry-preset-btn-saved';
|
|
45
|
+
btn.dataset.preset = preset.id;
|
|
46
|
+
btn.textContent = preset.label;
|
|
47
|
+
btn.title = preset.bgOmitted
|
|
48
|
+
? '点击加载(背景图未存入,需重新上传)'
|
|
49
|
+
: '点击加载';
|
|
50
|
+
btn.addEventListener('click', () => onSavedPreset?.(preset));
|
|
51
|
+
|
|
52
|
+
const deleteBtn = document.createElement('button');
|
|
53
|
+
deleteBtn.type = 'button';
|
|
54
|
+
deleteBtn.className = 'geometry-preset-delete';
|
|
55
|
+
deleteBtn.innerHTML = DELETE_ICON_SVG;
|
|
56
|
+
deleteBtn.setAttribute('aria-label', `删除 ${preset.label}`);
|
|
57
|
+
deleteBtn.addEventListener('click', (event) => {
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
event.stopPropagation();
|
|
60
|
+
onDeleteSaved?.(preset);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
wrap.appendChild(btn);
|
|
64
|
+
wrap.appendChild(deleteBtn);
|
|
65
|
+
container.appendChild(wrap);
|
|
66
|
+
}); };
|
|
67
|
+
|
|
68
|
+
render();
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
refresh: render,
|
|
72
|
+
destroy() {
|
|
73
|
+
container.innerHTML = '';
|
|
74
|
+
}
|
|
24
75
|
};
|
|
25
76
|
}
|
|
@@ -2,6 +2,74 @@ import { PARAM_SCHEMA } from './constants.js';
|
|
|
2
2
|
import { getByPath, paramFieldId } from './param-utils.js';
|
|
3
3
|
import { parseControlPointPath } from './track-helpers.js';
|
|
4
4
|
|
|
5
|
+
function parseParamValue(raw, field) {
|
|
6
|
+
const num = Number(raw);
|
|
7
|
+
return field.step >= 1 ? Math.round(num) : num;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function clampParamValue(value, field) {
|
|
11
|
+
const num = parseParamValue(value, field);
|
|
12
|
+
return Math.min(field.max, Math.max(field.min, num));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function formatParamOutput(value, field) {
|
|
16
|
+
const num = parseParamValue(value, field);
|
|
17
|
+
if (field.step >= 1) return String(num);
|
|
18
|
+
const stepText = String(field.step);
|
|
19
|
+
const decimals = stepText.includes('.') ? stepText.split('.')[1].length : 0;
|
|
20
|
+
return String(Number(num.toFixed(decimals)));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function applyParamInput(input, output, field, value, onParamChange) {
|
|
24
|
+
const num = clampParamValue(value, field);
|
|
25
|
+
input.value = String(num);
|
|
26
|
+
output.textContent = formatParamOutput(num, field);
|
|
27
|
+
onParamChange(field.path, num);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const STEP_HOLD_DELAY_MS = 400;
|
|
31
|
+
const STEP_REPEAT_INTERVAL_MS = 80;
|
|
32
|
+
|
|
33
|
+
function bindStepButtonHold(btn, stepOnce, { onHoldStart, onHoldEnd } = {}) {
|
|
34
|
+
let delayTimer = null;
|
|
35
|
+
let repeatTimer = null;
|
|
36
|
+
let holding = false;
|
|
37
|
+
|
|
38
|
+
const clearTimers = () => {
|
|
39
|
+
if (delayTimer != null) {
|
|
40
|
+
clearTimeout(delayTimer);
|
|
41
|
+
delayTimer = null;
|
|
42
|
+
}
|
|
43
|
+
if (repeatTimer != null) {
|
|
44
|
+
clearInterval(repeatTimer);
|
|
45
|
+
repeatTimer = null;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const stopHold = () => {
|
|
50
|
+
if (!holding) return;
|
|
51
|
+
holding = false;
|
|
52
|
+
clearTimers();
|
|
53
|
+
onHoldEnd?.();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
btn.addEventListener('pointerdown', (event) => {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
btn.setPointerCapture(event.pointerId);
|
|
59
|
+
holding = true;
|
|
60
|
+
onHoldStart?.();
|
|
61
|
+
stepOnce();
|
|
62
|
+
clearTimers();
|
|
63
|
+
delayTimer = setTimeout(() => {
|
|
64
|
+
repeatTimer = setInterval(stepOnce, STEP_REPEAT_INTERVAL_MS);
|
|
65
|
+
}, STEP_HOLD_DELAY_MS);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
btn.addEventListener('pointerup', stopHold);
|
|
69
|
+
btn.addEventListener('pointercancel', stopHold);
|
|
70
|
+
btn.addEventListener('lostpointercapture', stopHold);
|
|
71
|
+
}
|
|
72
|
+
|
|
5
73
|
export function bindParamsPanel(paramsForm, {
|
|
6
74
|
getParams,
|
|
7
75
|
onParamChange,
|
|
@@ -36,36 +104,53 @@ export function bindParamsPanel(paramsForm, {
|
|
|
36
104
|
|
|
37
105
|
row.innerHTML = `
|
|
38
106
|
<label for="${id}">${field.label}</label>
|
|
39
|
-
<
|
|
40
|
-
|
|
107
|
+
<div class="param-stepper">
|
|
108
|
+
<button type="button" class="param-step-btn" data-step="-1" aria-label="${field.label} 减小">−</button>
|
|
109
|
+
<input type="range" id="${id}" min="${field.min}" max="${field.max}" step="${field.step}" value="${val}" />
|
|
110
|
+
<button type="button" class="param-step-btn" data-step="1" aria-label="${field.label} 增大">+</button>
|
|
111
|
+
</div>
|
|
112
|
+
<output for="${id}">${formatParamOutput(val, field)}</output>
|
|
41
113
|
`;
|
|
42
114
|
|
|
43
115
|
const input = row.querySelector('input');
|
|
44
116
|
const output = row.querySelector('output');
|
|
117
|
+
const stepButtons = row.querySelectorAll('.param-step-btn');
|
|
45
118
|
|
|
46
119
|
input.addEventListener('input', () => {
|
|
47
|
-
|
|
48
|
-
? Math.round(Number(input.value))
|
|
49
|
-
: Number(input.value);
|
|
50
|
-
onParamChange(field.path, num);
|
|
51
|
-
output.textContent = String(num);
|
|
120
|
+
applyParamInput(input, output, field, input.value, onParamChange);
|
|
52
121
|
});
|
|
53
122
|
|
|
54
123
|
const controlHint = parseControlPointPath(field.path);
|
|
124
|
+
|
|
125
|
+
stepButtons.forEach((btn) => {
|
|
126
|
+
const delta = Number(btn.dataset.step) * field.step;
|
|
127
|
+
const stepOnce = () => {
|
|
128
|
+
applyParamInput(input, output, field, Number(input.value) + delta, onParamChange);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const holdCallbacks = controlHint
|
|
132
|
+
? {
|
|
133
|
+
onHoldStart: () => onControlPointAdjustStart?.(controlHint),
|
|
134
|
+
onHoldEnd: () => onControlPointAdjustEnd?.()
|
|
135
|
+
}
|
|
136
|
+
: {};
|
|
137
|
+
|
|
138
|
+
bindStepButtonHold(btn, stepOnce, holdCallbacks);
|
|
139
|
+
});
|
|
140
|
+
|
|
55
141
|
if (controlHint) {
|
|
56
142
|
row.classList.add('param-row-cp');
|
|
57
143
|
row.dataset.controlPoint = controlHint.point;
|
|
58
144
|
row.dataset.controlAxis = controlHint.axis;
|
|
59
145
|
|
|
60
|
-
|
|
146
|
+
const startControlAdjust = () => {
|
|
61
147
|
onControlPointAdjustStart?.(controlHint);
|
|
62
148
|
window.addEventListener('pointerup', () => {
|
|
63
149
|
onControlPointAdjustEnd?.();
|
|
64
150
|
}, { once: true });
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
});
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
input.addEventListener('pointerdown', startControlAdjust);
|
|
69
154
|
}
|
|
70
155
|
|
|
71
156
|
sections.get(field.section).appendChild(row);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { deepClone } from './param-utils.js';
|
|
2
|
+
|
|
3
|
+
export const SAVED_PRESETS_STORAGE_KEY = 'bezier-slider-demo-saved-presets';
|
|
4
|
+
const MAX_SAVED_COUNT = 16;
|
|
5
|
+
const MAX_BG_DATA_URL_LENGTH = 900_000;
|
|
6
|
+
|
|
7
|
+
export function loadSavedPresets() {
|
|
8
|
+
try {
|
|
9
|
+
const raw = localStorage.getItem(SAVED_PRESETS_STORAGE_KEY);
|
|
10
|
+
if (!raw) return [];
|
|
11
|
+
const list = JSON.parse(raw);
|
|
12
|
+
return Array.isArray(list) ? list : [];
|
|
13
|
+
} catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function writeSavedPresets(list) {
|
|
19
|
+
localStorage.setItem(SAVED_PRESETS_STORAGE_KEY, JSON.stringify(list));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function removeSavedPreset(id) {
|
|
23
|
+
const next = loadSavedPresets().filter((item) => item.id !== id);
|
|
24
|
+
writeSavedPresets(next);
|
|
25
|
+
return next;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildPresetSnapshot({
|
|
29
|
+
label,
|
|
30
|
+
params,
|
|
31
|
+
bgUrl,
|
|
32
|
+
bgFileName,
|
|
33
|
+
bgNaturalSize
|
|
34
|
+
}) {
|
|
35
|
+
const includeBg = typeof bgUrl === 'string'
|
|
36
|
+
&& bgUrl.length > 0
|
|
37
|
+
&& bgUrl.length <= MAX_BG_DATA_URL_LENGTH;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
id: `saved-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
41
|
+
label: label.trim(),
|
|
42
|
+
savedAt: Date.now(),
|
|
43
|
+
params: deepClone(params),
|
|
44
|
+
bgUrl: includeBg ? bgUrl : null,
|
|
45
|
+
bgFileName: includeBg ? (bgFileName || '') : '',
|
|
46
|
+
bgNaturalSize: deepClone(bgNaturalSize),
|
|
47
|
+
bgOmitted: Boolean(bgUrl && !includeBg)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function savePresetSnapshot(snapshot) {
|
|
52
|
+
const list = loadSavedPresets();
|
|
53
|
+
if (list.length >= MAX_SAVED_COUNT) {
|
|
54
|
+
throw new Error(`最多保存 ${MAX_SAVED_COUNT} 个配置,请先点击快捷按钮右上角删除旧配置`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
list.push(snapshot);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
writeSavedPresets(list);
|
|
61
|
+
} catch {
|
|
62
|
+
throw new Error('保存失败:浏览器存储空间不足,请删除部分已存配置或减少背景图大小');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return list;
|
|
66
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bezier-slider",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "二次贝塞尔弧形图标滑块组件,支持原生 JavaScript、Vue 3 和 React",
|
|
5
5
|
"main": "dist/bezier-slider.mjs",
|
|
6
6
|
"module": "dist/bezier-slider.mjs",
|
|
@@ -23,13 +23,14 @@
|
|
|
23
23
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"
|
|
27
|
-
"react": "
|
|
28
|
-
"
|
|
26
|
+
"react": ">=18",
|
|
27
|
+
"react-dom": ">=18",
|
|
28
|
+
"vue": ">=3.4"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@vitejs/plugin-react": "^4.2.1",
|
|
32
32
|
"@vitejs/plugin-vue": "^4.6.2",
|
|
33
|
+
"highlight.js": "^11.11.1",
|
|
33
34
|
"vite": "^4.5.2"
|
|
34
35
|
},
|
|
35
36
|
"peerDependencies": {
|