littlejsengine 1.19.3 → 1.20.0
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/FAQ.md +841 -820
- package/README.md +29 -25
- package/dist/littlejs.d.ts +2455 -935
- package/dist/littlejs.esm.js +30344 -21110
- package/dist/littlejs.esm.min.js +4 -3
- package/dist/littlejs.js +30259 -21052
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +26567 -20455
- package/package.json +10 -5
- package/plugins/audioEffects.js +28 -24
- package/plugins/box2d.js +2516 -2073
- package/plugins/drawUtilities.js +206 -31
- package/plugins/gltf.js +740 -0
- package/plugins/jsconfig.json +19 -0
- package/plugins/lightSystem.js +719 -332
- package/plugins/math3d.js +58 -17
- package/plugins/medalSystem.js +326 -280
- package/plugins/newgrounds.js +400 -199
- package/plugins/pathFinder.js +208 -175
- package/plugins/pluginExport.js +15 -0
- package/plugins/postProcess.js +56 -13
- package/plugins/render3d.js +1643 -1577
- package/plugins/render3dDebug.js +109 -0
- package/plugins/render3dExtras.js +1754 -0
- package/plugins/render3dVoxels.js +502 -0
- package/plugins/textureSheet.js +496 -458
- package/plugins/threejs.js +183 -163
- package/plugins/tweakables.js +411 -0
- package/plugins/tweenSystem.js +263 -145
- package/plugins/uiSystem.js +2024 -1687
- package/plugins/zzfxm.js +159 -159
- package/src/engine.js +191 -75
- package/src/engineAudio.js +229 -125
- package/src/engineBuild.mjs +289 -256
- package/src/engineDebug.js +912 -742
- package/src/engineDraw.js +295 -193
- package/src/engineEditor.js +2431 -0
- package/src/engineExport.js +12 -0
- package/src/engineInput.js +1462 -1293
- package/src/engineMath.js +1274 -1252
- package/src/engineObject.js +194 -80
- package/src/engineParticles.js +583 -532
- package/src/engineRelease.js +67 -45
- package/src/engineSettings.js +77 -29
- package/src/engineTileLayer.js +995 -741
- package/src/engineUtilities.js +19 -13
- package/src/engineWebGL.js +239 -82
- package/src/jsconfig.json +17 -13
- package/plugins/box2d.wasm.js +0 -630
- package/plugins/box2d.wasm.wasm +0 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LittleJS Tweakables Plugin
|
|
3
|
+
* - Change values live from a panel while the game runs, for tuning without a reload
|
|
4
|
+
* - tweak('name') adds a global, or a field of an object with the object option, a number gets a slider when it
|
|
5
|
+
* has a range, a boolean a checkbox, a Color a color picker with an alpha slider, a Vector2 or Vector3 a number
|
|
6
|
+
* for each axis
|
|
7
|
+
* - tweakButton adds a button that calls a function, like one to restart the level
|
|
8
|
+
* - Press 9 while the debug overlay is open to show the panel, or set debugTweakables to show it from the start
|
|
9
|
+
* - Changes are saved and come back after a refresh, until the value in the code itself changes
|
|
10
|
+
* - Copy puts the changed values on the clipboard as lines of code, to paste over the values in the code
|
|
11
|
+
* - Debug builds only, the release build has none of this code, its functions are stubs that do nothing, so the
|
|
12
|
+
* code's values are used as they are
|
|
13
|
+
* @namespace Tweakables
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
19
|
+
|
|
20
|
+
// the tweaks and dividers in the order they were added
|
|
21
|
+
const tweakList = [];
|
|
22
|
+
|
|
23
|
+
// the saved values by name, each with the code value it was saved over, read when the first tweak is added
|
|
24
|
+
let tweakSaved;
|
|
25
|
+
|
|
26
|
+
// the panel, made the first time it is shown, and if its rows need to be made again
|
|
27
|
+
let tweakPanel, tweakRows, tweakRowsDirty = true;
|
|
28
|
+
|
|
29
|
+
// a global's name, or a dotted path to a field on one
|
|
30
|
+
const tweakPathRegex = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/;
|
|
31
|
+
|
|
32
|
+
/** Options for a tweak
|
|
33
|
+
* @typedef {Object} TweakOptions
|
|
34
|
+
* @property {number} [min] - Lowest value on the slider, the slider shows when min and max are both set
|
|
35
|
+
* @property {number} [max] - Highest value on the slider
|
|
36
|
+
* @property {number} [step] - Step for the slider and number box, a thousandth of the range by default
|
|
37
|
+
* @property {string} [label] - Name to show in place of the path, and the name it is saved by with an object
|
|
38
|
+
* @property {Object} [object] - Object the path is a field of, in place of a global, for an ES module game
|
|
39
|
+
* @memberof Tweakables */
|
|
40
|
+
|
|
41
|
+
/** Add a value to the tweakables panel, so it can be changed while the game runs
|
|
42
|
+
* - The path is the name of a global, or a dotted path to a field on one like 'player.speed'
|
|
43
|
+
* - Only globals of a script can be found by name, an ES module game passes the object its values are on
|
|
44
|
+
* - Its type comes from the value it has now: a number, boolean, Color, Vector2 or Vector3
|
|
45
|
+
* - Call it after the value is set, at the end of gameInit
|
|
46
|
+
* - Adding the same path again keeps its row and brings its tweaked value back, for an object made again;
|
|
47
|
+
* with the object option it is known by its label, so two objects with the same field need their own labels
|
|
48
|
+
* - Does nothing in release builds
|
|
49
|
+
* @param {string} path - Name of a global, or a dotted path to a field on one or on the object option
|
|
50
|
+
* @param {TweakOptions} [options]
|
|
51
|
+
* @memberof Tweakables
|
|
52
|
+
* @example
|
|
53
|
+
* tweak('jumpSpeed', {min: 0, max: 1});
|
|
54
|
+
* tweak('skyColor');
|
|
55
|
+
* tweak('player.size', {label: 'Player Size'});
|
|
56
|
+
* tweak('speed', {object: settings}); // settings.speed, in an ES module */
|
|
57
|
+
function tweak(path, options={})
|
|
58
|
+
{
|
|
59
|
+
if (!debug) return;
|
|
60
|
+
ASSERT(isStringLike(path) && tweakPathRegex.test(path), 'tweak path must be a name like speed or player.speed');
|
|
61
|
+
|
|
62
|
+
const {object} = options;
|
|
63
|
+
let name = path, get, set, toCode;
|
|
64
|
+
if (object)
|
|
65
|
+
{
|
|
66
|
+
// a field of the object, the object at each step is looked up again so one made again is found
|
|
67
|
+
ASSERT(typeof object === 'object', 'tweak object must be an object');
|
|
68
|
+
const keys = path.split('.'), last = keys.pop();
|
|
69
|
+
const parent = ()=> keys.reduce((o, key)=> o?.[key], object);
|
|
70
|
+
name = options.label ?? path;
|
|
71
|
+
get = ()=> parent()?.[last];
|
|
72
|
+
set = (v)=> { parent()[last] = v; };
|
|
73
|
+
toCode = (v)=> `${path}: ${v},`; // as it is written in the object
|
|
74
|
+
}
|
|
75
|
+
else
|
|
76
|
+
{
|
|
77
|
+
// a global, compiled once, a missing object on the path reads as undefined
|
|
78
|
+
get = new Function(`try { return ${path} } catch {}`);
|
|
79
|
+
set = new Function('v', `'use strict'; ${path} = v`);
|
|
80
|
+
toCode = (v)=> `${path} = ${v};`;
|
|
81
|
+
}
|
|
82
|
+
const value = get();
|
|
83
|
+
ASSERT(value !== undefined, object ? `tweak could not find ${path} on the object` :
|
|
84
|
+
`tweak could not find ${path}, it must be a global of a script, an ES module game passes the object option`);
|
|
85
|
+
try { set(value); }
|
|
86
|
+
catch { ASSERT(false, `tweak can not change ${path}, it must not be a const or frozen`); }
|
|
87
|
+
tweakAdd(name, get, set, toCode, options);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Add a button to the tweakables panel that calls a function, like one to restart the level
|
|
91
|
+
* - Adding the same label again replaces its function
|
|
92
|
+
* - Does nothing in release builds
|
|
93
|
+
* @param {string} label
|
|
94
|
+
* @param {function():void} callback
|
|
95
|
+
* @memberof Tweakables
|
|
96
|
+
* @example
|
|
97
|
+
* tweakButton('Restart', restartLevel); */
|
|
98
|
+
function tweakButton(label, callback)
|
|
99
|
+
{
|
|
100
|
+
if (!debug) return;
|
|
101
|
+
ASSERT(isStringLike(label), 'tweakButton label must be a string');
|
|
102
|
+
ASSERT(typeof callback === 'function', 'tweakButton callback must be a function');
|
|
103
|
+
const t = tweakList.find(t=> t.type === 'button' && t.label === label);
|
|
104
|
+
if (t)
|
|
105
|
+
t.callback = callback;
|
|
106
|
+
else
|
|
107
|
+
{
|
|
108
|
+
tweakList.push({type: 'button', label, callback});
|
|
109
|
+
tweakRowsDirty = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Add a divider to the tweakables panel, with a label for the tweaks after it
|
|
114
|
+
* - Does nothing in release builds
|
|
115
|
+
* @param {string} [label]
|
|
116
|
+
* @memberof Tweakables */
|
|
117
|
+
function tweakDivider(label='')
|
|
118
|
+
{
|
|
119
|
+
if (!debug) return;
|
|
120
|
+
if (label && tweakList.find(t=> t.type === 'divider' && t.label === label))
|
|
121
|
+
return; // added again, as a game that calls its setup again does
|
|
122
|
+
tweakList.push({type: 'divider', label});
|
|
123
|
+
tweakRowsDirty = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Add the engine settings a game most often tunes to the tweakables panel:
|
|
127
|
+
* gravity, timeScale, cameraScale and soundVolume, under an Engine divider
|
|
128
|
+
* - They are changed through their setters, so this works in the ES module build too
|
|
129
|
+
* - Does nothing in release builds
|
|
130
|
+
* @memberof Tweakables */
|
|
131
|
+
function tweakEngineDefaults()
|
|
132
|
+
{
|
|
133
|
+
if (!debug) return;
|
|
134
|
+
tweakDivider('Engine');
|
|
135
|
+
tweakAdd('gravity', ()=> gravity, setGravity, (v)=> `setGravity(${v});`, {min: -.05, max: .05});
|
|
136
|
+
tweakAdd('timeScale', ()=> timeScale, setTimeScale, (v)=> `setTimeScale(${v});`, {min: 0, max: 2});
|
|
137
|
+
tweakAdd('cameraScale', ()=> cameraScale, setCameraScale, (v)=> `setCameraScale(${v});`,
|
|
138
|
+
{min: 1, max: 128, step: 1});
|
|
139
|
+
tweakAdd('soundVolume', ()=> soundVolume, setSoundVolume, (v)=> `setSoundVolume(${v});`, {min: 0, max: 1});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
143
|
+
// tweaks
|
|
144
|
+
|
|
145
|
+
function tweakAdd(name, get, set, toCode, options)
|
|
146
|
+
{
|
|
147
|
+
const value = get();
|
|
148
|
+
const type = isNumber(value) ? 'number' : typeof value === 'boolean' ? 'boolean' :
|
|
149
|
+
isColor(value) ? 'color' : isVector2(value) ? 'vector2' : tweakIsVector3(value) ? 'vector3' : undefined;
|
|
150
|
+
ASSERT(type, `tweak ${name} must be a number, boolean, Color, Vector2 or Vector3`);
|
|
151
|
+
if (!type) return;
|
|
152
|
+
|
|
153
|
+
// added again, the code value stays the one it had first, the value now may be a tweaked one
|
|
154
|
+
let t = tweakList.find(t=> t.name === name);
|
|
155
|
+
if (!t)
|
|
156
|
+
{
|
|
157
|
+
tweakList.push(t = {name, codeValue: tweakCopy(value)});
|
|
158
|
+
tweakRowsDirty = true;
|
|
159
|
+
}
|
|
160
|
+
Object.assign(t, {type, get, set, toCode, options});
|
|
161
|
+
|
|
162
|
+
// a saved value comes back while the code value is the one it was saved over, after an edit it is dropped
|
|
163
|
+
tweakSaved ||= readSaveData(tweakSaveName(), {});
|
|
164
|
+
const saved = tweakSaved[name];
|
|
165
|
+
if (!saved) return;
|
|
166
|
+
if (tweakSaveText(saved.code) === tweakSaveText(tweakToSave(t.codeValue)))
|
|
167
|
+
set(tweakFromSave(type, saved.value));
|
|
168
|
+
else
|
|
169
|
+
{
|
|
170
|
+
delete tweakSaved[name];
|
|
171
|
+
writeSaveData(tweakSaveName(), tweakSaved);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// every page keeps its own tweaks
|
|
176
|
+
function tweakSaveName() { return 'LittleJS tweaks ' + (globalThis.location?.pathname ?? ''); }
|
|
177
|
+
|
|
178
|
+
function tweakCopy(v) { return v?.copy ? v.copy() : v; }
|
|
179
|
+
|
|
180
|
+
// a Vector3, a build may leave out the 3D math
|
|
181
|
+
function tweakIsVector3(v) { return typeof Vector3 !== 'undefined' && isVector3(v); }
|
|
182
|
+
|
|
183
|
+
// the value as json can hold it, and back
|
|
184
|
+
function tweakToSave(v)
|
|
185
|
+
{
|
|
186
|
+
return isColor(v) ? [v.r, v.g, v.b, v.a] : isVector2(v) ? [v.x, v.y] :
|
|
187
|
+
tweakIsVector3(v) ? [v.x, v.y, v.z] : v;
|
|
188
|
+
}
|
|
189
|
+
function tweakFromSave(type, v)
|
|
190
|
+
{
|
|
191
|
+
return type === 'color' ? rgb(...v) : type === 'vector2' ? vec2(...v) : type === 'vector3' ? vec3(...v) : v;
|
|
192
|
+
}
|
|
193
|
+
function tweakSaveText(v) { return JSON.stringify(v); }
|
|
194
|
+
|
|
195
|
+
// remember a tweak's value, or forget it when it is back to the code value
|
|
196
|
+
function tweakSave(t)
|
|
197
|
+
{
|
|
198
|
+
const value = tweakToSave(t.get()), code = tweakToSave(t.codeValue);
|
|
199
|
+
if (tweakSaveText(value) === tweakSaveText(code))
|
|
200
|
+
delete tweakSaved[t.name];
|
|
201
|
+
else
|
|
202
|
+
tweakSaved[t.name] = {value, code};
|
|
203
|
+
writeSaveData(tweakSaveName(), tweakSaved);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// put every tweak back to its code value and forget the saved ones
|
|
207
|
+
function tweakReset()
|
|
208
|
+
{
|
|
209
|
+
for (const t of tweakList)
|
|
210
|
+
t.set?.(tweakCopy(t.codeValue));
|
|
211
|
+
tweakSaved = {};
|
|
212
|
+
writeSaveData(tweakSaveName(), tweakSaved);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// a line of code for each tweak changed from its code value
|
|
216
|
+
function tweakChangedCode()
|
|
217
|
+
{
|
|
218
|
+
// numbers as the code writes them, without float noise or a leading zero,
|
|
219
|
+
// colors as hsl rounded more, which changes nothing that can be seen
|
|
220
|
+
const number = (n, digits=6)=> String(+n.toFixed(digits)).replace(/^(-?)0\./, '$1.');
|
|
221
|
+
const code = (v)=>
|
|
222
|
+
{
|
|
223
|
+
if (isColor(v))
|
|
224
|
+
{
|
|
225
|
+
const [h, s, l, a] = v.HSLA();
|
|
226
|
+
return `hsl(${[h, s, l, ...(a < 1 ? [a] : [])].map((n)=> number(n, 3)).join(', ')})`;
|
|
227
|
+
}
|
|
228
|
+
if (isVector2(v))
|
|
229
|
+
return `vec2(${number(v.x)}, ${number(v.y)})`;
|
|
230
|
+
if (tweakIsVector3(v))
|
|
231
|
+
return `vec3(${number(v.x)}, ${number(v.y)}, ${number(v.z)})`;
|
|
232
|
+
return isNumber(v) ? number(v) : String(v);
|
|
233
|
+
};
|
|
234
|
+
return tweakList.filter(t=> t.get && tweakSaveText(tweakToSave(t.get())) !==
|
|
235
|
+
tweakSaveText(tweakToSave(t.codeValue))).map(t=> t.toCode(code(t.get()))).join('\n');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
239
|
+
// panel
|
|
240
|
+
|
|
241
|
+
// shows or hides the panel each frame, and shows values the game changed itself
|
|
242
|
+
function tweakRender()
|
|
243
|
+
{
|
|
244
|
+
if (headlessMode) return;
|
|
245
|
+
if (!debugTweakables)
|
|
246
|
+
{
|
|
247
|
+
if (tweakPanel && tweakPanel.style.display !== 'none')
|
|
248
|
+
{
|
|
249
|
+
// hidden, a box it had focus in lets go of the keys
|
|
250
|
+
const active = /** @type {HTMLElement} */ (document.activeElement);
|
|
251
|
+
tweakPanel.contains(active) && active.blur();
|
|
252
|
+
tweakPanel.style.display = 'none';
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
tweakPanel || tweakPanelInit();
|
|
258
|
+
tweakPanel.style.display = '';
|
|
259
|
+
if (tweakRowsDirty)
|
|
260
|
+
{
|
|
261
|
+
tweakRowsDirty = false;
|
|
262
|
+
tweakRows.replaceChildren();
|
|
263
|
+
for (const t of tweakList)
|
|
264
|
+
t.refresh = tweakRow(t);
|
|
265
|
+
tweakList.length || tweakElement('div', tweakRows, 'color:#888', 'Nothing to tweak, add values with tweak()');
|
|
266
|
+
}
|
|
267
|
+
for (const t of tweakList)
|
|
268
|
+
t.refresh?.();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function tweakPanelInit()
|
|
272
|
+
{
|
|
273
|
+
tweakPanel = tweakElement('div', document.body,
|
|
274
|
+
'position:fixed;top:8px;right:8px;width:260px;max-height:calc(100% - 16px);overflow-y:auto;' +
|
|
275
|
+
'box-sizing:border-box;padding:8px;background:#111d;color:#eee;font:12px monospace;' +
|
|
276
|
+
'border-radius:4px;z-index:9999');
|
|
277
|
+
|
|
278
|
+
// a click or touch on the panel is not the game's, a mouse up still goes on so a button can let go
|
|
279
|
+
for (const type of ['mousedown','wheel','touchstart','touchmove','touchend','touchcancel'])
|
|
280
|
+
tweakPanel.addEventListener(type, (e)=> e.stopPropagation());
|
|
281
|
+
|
|
282
|
+
tweakElement('div', tweakPanel, 'font-weight:bold;margin-bottom:4px', 'Tweakables');
|
|
283
|
+
tweakRows = tweakElement('div', tweakPanel);
|
|
284
|
+
const buttons = tweakElement('div', tweakPanel, 'display:flex;gap:6px;margin-top:8px');
|
|
285
|
+
const button = (text, onclick)=>
|
|
286
|
+
tweakElement('button', buttons, 'flex:1;padding:4px;cursor:pointer', text).onclick = onclick;
|
|
287
|
+
button('Copy', (e)=>
|
|
288
|
+
{
|
|
289
|
+
// lines for the changed values, to paste over the values in the code
|
|
290
|
+
const code = tweakChangedCode(), target = /** @type {HTMLElement} */ (e.target);
|
|
291
|
+
code && navigator.clipboard?.writeText(code).catch(()=> console.log(code));
|
|
292
|
+
target.textContent = code ? 'Copied' : 'No changes';
|
|
293
|
+
setTimeout(()=> target.textContent = 'Copy', 1e3);
|
|
294
|
+
});
|
|
295
|
+
button('Reset', tweakReset);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// the row for a tweak, returns what shows its value
|
|
299
|
+
function tweakRow(t)
|
|
300
|
+
{
|
|
301
|
+
const row = tweakElement('div', tweakRows, 'margin:6px 0');
|
|
302
|
+
if (t.type === 'divider')
|
|
303
|
+
{
|
|
304
|
+
row.style.cssText = 'margin:10px 0 4px;padding-top:4px;border-top:1px solid #555;color:#aaa';
|
|
305
|
+
row.textContent = t.label;
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (t.type === 'button')
|
|
309
|
+
{
|
|
310
|
+
// the function is looked up on click, so one added again replaces it
|
|
311
|
+
const button = tweakElement('button', row, 'width:100%;padding:4px;cursor:pointer', t.label);
|
|
312
|
+
button.onclick = ()=> t.callback();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const label = t.options.label ?? t.name;
|
|
317
|
+
const save = ()=> tweakSave(t);
|
|
318
|
+
if (t.type === 'boolean')
|
|
319
|
+
{
|
|
320
|
+
const labelElement = tweakElement('label', row, 'display:flex;gap:6px;align-items:center;cursor:pointer');
|
|
321
|
+
const box = tweakElement('input', labelElement);
|
|
322
|
+
box.type = 'checkbox';
|
|
323
|
+
tweakElement('span', labelElement, '', label);
|
|
324
|
+
box.onchange = ()=> { t.set(box.checked); save(); };
|
|
325
|
+
return ()=> { box.checked = !!t.get(); };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
tweakElement('div', row, '', label);
|
|
329
|
+
if (t.type === 'number')
|
|
330
|
+
return tweakNumber(row, '', t.get, t.set, t.options, save);
|
|
331
|
+
|
|
332
|
+
// a part of a vector or color is a number of its own, set as a new copy with that part changed
|
|
333
|
+
const part = (key, options)=> tweakNumber(row, key, ()=> t.get()?.[key],
|
|
334
|
+
(n)=> { const v = t.get().copy(); v[key] = n; t.set(v); }, options, save);
|
|
335
|
+
if (t.type === 'vector2' || t.type === 'vector3')
|
|
336
|
+
{
|
|
337
|
+
const axes = (t.type === 'vector2' ? ['x', 'y'] : ['x', 'y', 'z']).map((key)=> part(key, t.options));
|
|
338
|
+
return ()=> axes.forEach((refresh)=> refresh());
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// a color picker has no alpha, a slider below it has
|
|
342
|
+
const picker = tweakElement('input', row, 'width:100%;height:24px;padding:0;border:0;background:none');
|
|
343
|
+
picker.type = 'color';
|
|
344
|
+
picker.oninput = ()=>
|
|
345
|
+
{
|
|
346
|
+
const color = rgb().setHex(picker.value);
|
|
347
|
+
color.a = t.get().a;
|
|
348
|
+
t.set(color);
|
|
349
|
+
save();
|
|
350
|
+
};
|
|
351
|
+
const alpha = part('a', {min: 0, max: 1});
|
|
352
|
+
return ()=>
|
|
353
|
+
{
|
|
354
|
+
const color = t.get();
|
|
355
|
+
const hex = isColor(color) && color.toString(false);
|
|
356
|
+
hex && picker !== document.activeElement && picker.value !== hex && (picker.value = hex);
|
|
357
|
+
alpha();
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// a slider when there is a range and a box to type in, returns what shows the value
|
|
362
|
+
function tweakNumber(parent, axis, get, set, options, save)
|
|
363
|
+
{
|
|
364
|
+
const {min, max} = options;
|
|
365
|
+
const hasRange = isNumber(min) && isNumber(max);
|
|
366
|
+
const value = get(), size = abs(value);
|
|
367
|
+
const step = options.step ?? (hasRange ? (max - min) / 1e3 : size >= 10 ? 1 : size >= 1 ? .1 : .001);
|
|
368
|
+
|
|
369
|
+
const row = tweakElement('div', parent, 'display:flex;gap:4px;align-items:center');
|
|
370
|
+
axis && tweakElement('span', row, 'color:#888', axis);
|
|
371
|
+
const slider = hasRange ? tweakElement('input', row, 'flex:1;min-width:0') : undefined;
|
|
372
|
+
const box = tweakElement('input', row,
|
|
373
|
+
'width:80px;box-sizing:border-box;background:#222;color:#eee;border:1px solid #555' +
|
|
374
|
+
(hasRange ? '' : ';flex:1'));
|
|
375
|
+
if (slider)
|
|
376
|
+
Object.assign(slider, {type: 'range', min, max, step});
|
|
377
|
+
Object.assign(box, {type: 'number', step});
|
|
378
|
+
|
|
379
|
+
const change = (input)=>
|
|
380
|
+
{
|
|
381
|
+
const v = parseFloat(input.value);
|
|
382
|
+
if (!isNumber(v)) return; // a box part way through typing
|
|
383
|
+
set(v);
|
|
384
|
+
save();
|
|
385
|
+
};
|
|
386
|
+
slider && (slider.oninput = ()=> change(slider));
|
|
387
|
+
box.oninput = ()=> change(box);
|
|
388
|
+
return ()=>
|
|
389
|
+
{
|
|
390
|
+
// the one being used keeps what it has, so typing is not undone
|
|
391
|
+
const v = get();
|
|
392
|
+
if (!isNumber(v)) return;
|
|
393
|
+
for (const input of [slider, box])
|
|
394
|
+
input && input !== document.activeElement && (input.value === '' || +input.value !== v) &&
|
|
395
|
+
(input.value = String(v));
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function tweakElement(tag, parent, style='', text='')
|
|
400
|
+
{
|
|
401
|
+
const element = document.createElement(tag);
|
|
402
|
+
element.style.cssText = style;
|
|
403
|
+
element.textContent = text;
|
|
404
|
+
parent?.appendChild(element);
|
|
405
|
+
return element;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
409
|
+
// plugin
|
|
410
|
+
|
|
411
|
+
debug && engineAddPlugin(undefined, tweakRender);
|