@1337exp/pkt-threejs-blueprint 0.0.0-stage → 0.1.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/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +69 -2
- package/README.ru.md +70 -0
- package/dist/components/vue/tuning-panel.d.ts +74 -0
- package/dist/components/vue/tuning-panel.js +155 -0
- package/dist/components/vue/tuning-panel.js.map +1 -0
- package/dist/core/ease.d.ts +20 -0
- package/dist/core/ease.js +19 -0
- package/dist/core/ease.js.map +1 -0
- package/dist/core/loop.d.ts +37 -0
- package/dist/core/loop.js +47 -0
- package/dist/core/loop.js.map +1 -0
- package/dist/core/rng.d.ts +16 -0
- package/dist/core/rng.js +23 -0
- package/dist/core/rng.js.map +1 -0
- package/dist/core/spring.d.ts +38 -0
- package/dist/core/spring.js +91 -0
- package/dist/core/spring.js.map +1 -0
- package/dist/core/ticker.d.ts +44 -0
- package/dist/core/ticker.js +71 -0
- package/dist/core/ticker.js.map +1 -0
- package/dist/core/tuning.d.ts +74 -0
- package/dist/core/tuning.js +117 -0
- package/dist/core/tuning.js.map +1 -0
- package/dist/three/dispose.d.ts +19 -0
- package/dist/three/dispose.js +47 -0
- package/dist/three/dispose.js.map +1 -0
- package/dist/three/pointer.d.ts +34 -0
- package/dist/three/pointer.js +28 -0
- package/dist/three/pointer.js.map +1 -0
- package/dist/three/renderer.d.ts +36 -0
- package/dist/three/renderer.js +41 -0
- package/dist/three/renderer.js.map +1 -0
- package/package.json +104 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First release. Each module is its own import (`@1337exp/pkt-threejs-blueprint/<module>`), there is no root entry.
|
|
6
|
+
|
|
7
|
+
- **`ease`**: `Ease`, our curves (`easeOutBack` overshoots), `smoothstep(edge0, edge1, x)` in GLSL argument order.
|
|
8
|
+
- **`ticker`**: `Ticker`, game time with a `speed` (0 is a hit-stop), `tween` and `wait` as promises.
|
|
9
|
+
- **`spring`**: `spring(stiffness, zeta)`, `stepSpring`, `stepSpring3`. The exact solution of the spring equation: stable at any step, the same path at any frame rate.
|
|
10
|
+
- **`rng`**: `mulberry32`, `randomInt`. The sequence is frozen by a test, so saved seeds stay valid.
|
|
11
|
+
- **`tuning`**: `createTuning`, the core of a "tweak it live" panel (live values, saving only the differences from the defaults, reset, copy as code).
|
|
12
|
+
- **`loop`**: `startLoop(frame, { maxDt })`, the frame loop with a capped delta; `maxDt` is required.
|
|
13
|
+
- **`renderer`** (needs `three`): `createRenderer`, `bindResize`.
|
|
14
|
+
- **`pointer`** (needs `three`): `clientToNdc`, `rayOnPlane`.
|
|
15
|
+
- **`dispose`** (needs `three`): `disposeObject`.
|
|
16
|
+
- **`vue/tuning-panel`** (needs `vue`): `TuningPanel`, styled with `--tbp-*` CSS variables.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 1337exp
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,70 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @1337exp/pkt-threejs-blueprint
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**English** · [Русский](README.ru.md)
|
|
4
|
+
|
|
5
|
+
The small shared layer our Three.js prototypes keep rewriting: a game clock with tweens, springs that do not blow up, seeded random, and the core of a "tweak it live" panel. Import only what you need; nothing is pulled in that you did not ask for.
|
|
6
|
+
|
|
7
|
+
## Stack
|
|
8
|
+
|
|
9
|
+
TypeScript 6, Vitest, plain `tsc` (no bundler: one file per module is the tree-shaking). The core has no dependencies and no DOM. `three` is an optional peer, used only by the modules that say so in the table; `vue` is an optional peer of the panel. It does not depend on TaoUI or TaoUX.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @1337exp/pkt-threejs-blueprint # plus three and vue if you use the modules that need them
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Work on the library itself:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install
|
|
19
|
+
npm run typecheck
|
|
20
|
+
npm test
|
|
21
|
+
npm run build
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## What it can do
|
|
25
|
+
|
|
26
|
+
Each module is its own import (`@1337exp/pkt-threejs-blueprint/<module>`), there is no root entry.
|
|
27
|
+
|
|
28
|
+
| Module | What |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `ease` | `Ease = (t) => number`, our curves (`easeOutBack` overshoots), `smoothstep(edge0, edge1, x)` in GLSL argument order |
|
|
31
|
+
| `ticker` | `Ticker`: game time with a `speed` (0 is a hit-stop), `tween` and `wait` as promises, `add`, `busy`, `clear` |
|
|
32
|
+
| `spring` | `spring(stiffness, zeta)`, `stepSpring` (a number) and `stepSpring3` (a vector): the exact solution, stable at any step, the same path at any frame rate |
|
|
33
|
+
| `rng` | `mulberry32(seed)`, `randomInt`; the sequence is frozen by a test, saved seeds stay valid |
|
|
34
|
+
| `tuning` | `createTuning(values, { groups, storageKey })`: live values, saving only the differences from the defaults, reset, "copy as code" (`toSource`), safe `localStorage` |
|
|
35
|
+
| `loop` | `startLoop(frame, { maxDt })`: the frame loop; `frame(dt, now)` gets a delta capped at `maxDt` (required: you choose it); `stop()` |
|
|
36
|
+
| `renderer` | needs `three`. `createRenderer(container)`, `bindResize(renderer, container, onResize)`: size and a pixel ratio capped at 2, read again on every resize |
|
|
37
|
+
| `pointer` | needs `three`. `clientToNdc(event, element)`, `rayOnPlane(ndc, camera, plane)`: a pointer to the point on a plane, with the camera matrix refreshed |
|
|
38
|
+
| `vue/tuning-panel` | needs `vue`. `TuningPanel`: draws a `tuning` (groups of sliders, what differs from the defaults, Copy as code, Reset). No `.vue` file, no UI library; looks are `--tbp-*` CSS variables with our dark-theme values as fallbacks |
|
|
39
|
+
| `dispose` | needs `three`. `disposeObject(root, { textures })`: geometries and materials (arrays too) freed once however many meshes share them; textures only when asked |
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { Ticker } from '@1337exp/pkt-threejs-blueprint/ticker';
|
|
43
|
+
import { easeOutBack } from '@1337exp/pkt-threejs-blueprint/ease';
|
|
44
|
+
|
|
45
|
+
const ticker = new Ticker();
|
|
46
|
+
// every frame: ticker.update(Math.min(realDt, maxDt));
|
|
47
|
+
await ticker.tween(0.4, (k) => (mesh.scale.x = 1 + k), easeOutBack);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### The frame delta
|
|
51
|
+
|
|
52
|
+
`dt` is the time between two frames in seconds, not a simulation rate: the frame rate stays the screen's (60, 120, 144 Hz). Cap it from above (`maxDt`) before feeding the ticker, so a hidden tab or a stall does not deliver a `dt` of seconds and send every tween to its end in one frame. Our projects use `1/30`, `0.05` or `0.1`; pick one deliberately.
|
|
53
|
+
|
|
54
|
+
Springs do not need a cap to stay alive: `stepSpring` and `stepSpring3` use the exact solution of the spring equation, so they are stable for any `dt` and give the same path at 30, 60 or 144 frames per second. The usual one-line Euler step (`v += (k·(target − x) − c·v)·dt; x += v·dt`) does neither: stiffness 2000 with `zeta` 1 blows up below ~54 frames per second, and even where it holds the path depends on the frame rate. Do not write that line by hand.
|
|
55
|
+
|
|
56
|
+
## How it is built
|
|
57
|
+
|
|
58
|
+
Three layers by dependency: `core/` (nothing), `three/` (needs `three`), `components/` (ready-made interface blocks over the core, styled with our dark-theme values copied as CSS variables, no TaoUI). Design, decisions and the analysis behind them: [docs/design.md](docs/design.md), [docs/analysis-grok.md](docs/analysis-grok.md).
|
|
59
|
+
|
|
60
|
+
## Not yet
|
|
61
|
+
|
|
62
|
+
A plain-DOM panel over `tuning` (the Vue one is done). Particles, ribbons, cameras, bloom and shaders stay in the games on purpose: they differ because the pictures differ.
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT, see [LICENSE](LICENSE).
|
|
67
|
+
|
|
68
|
+
## Us
|
|
69
|
+
|
|
70
|
+
Homepage: [666.io](https://666.io). If you want to help, suggest an idea or just talk, come by.
|
package/README.ru.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# @1337exp/pkt-threejs-blueprint
|
|
2
|
+
|
|
3
|
+
[English](README.md) · **Русский**
|
|
4
|
+
|
|
5
|
+
Небольшой общий слой, который наши прототипы на Three.js постоянно пишут заново: игровое время с твинами, пружины, которые не разлетаются, случайные числа с зерном и ядро панели «покрутить вживую». Подключай только то, что нужно; лишнего не тянется.
|
|
6
|
+
|
|
7
|
+
## Стек
|
|
8
|
+
|
|
9
|
+
TypeScript 6, Vitest, обычный `tsc` (без бандлера: по файлу на модуль и есть tree-shaking). Ядро без зависимостей и без DOM. `three` необязательный peer, он нужен только модулям, у которых это сказано в таблице; `vue` необязательный peer панели. От TaoUI и TaoUX не зависит.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @1337exp/pkt-threejs-blueprint # плюс three и vue, если берёшь модули, которым они нужны
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Работа над самой библиотекой:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install
|
|
19
|
+
npm run typecheck
|
|
20
|
+
npm test
|
|
21
|
+
npm run build
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Что умеет
|
|
25
|
+
|
|
26
|
+
Каждый модуль подключается отдельно (`@1337exp/pkt-threejs-blueprint/<модуль>`), общего входа нет.
|
|
27
|
+
|
|
28
|
+
| Модуль | Что |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `ease` | `Ease = (t) => number`, наши кривые (`easeOutBack` с перелётом), `smoothstep(edge0, edge1, x)` в порядке аргументов GLSL |
|
|
31
|
+
| `ticker` | `Ticker`: игровое время со `speed` (0 — hit-stop), `tween` и `wait` как обещания, `add`, `busy`, `clear` |
|
|
32
|
+
| `spring` | `spring(stiffness, zeta)`, `stepSpring` (число) и `stepSpring3` (вектор): точное решение, устойчиво при любом шаге, один и тот же путь на любой частоте кадров |
|
|
33
|
+
| `rng` | `mulberry32(seed)`, `randomInt`; последовательность заморожена тестом, сохранённые зёрна остаются верными |
|
|
34
|
+
| `tuning` | `createTuning(values, { groups, storageKey })`: живые значения, сохранение только отличий от умолчаний, сброс, «скопировать как код» (`toSource`), безопасный `localStorage` |
|
|
35
|
+
| `loop` | `startLoop(frame, { maxDt })`: цикл кадров; `frame(dt, now)` получает дельту не больше `maxDt` (параметр обязателен: выбираешь ты); `stop()` |
|
|
36
|
+
| `renderer` | нужен `three`. `createRenderer(container)`, `bindResize(renderer, container, onResize)`: размер и плотность пикселей с потолком 2, читаются заново при каждом изменении размера |
|
|
37
|
+
| `pointer` | нужен `three`. `clientToNdc(event, element)`, `rayOnPlane(ndc, camera, plane)`: от указателя до точки на плоскости, с обновлением матрицы камеры |
|
|
38
|
+
| `vue/tuning-panel` | нужен `vue`. `TuningPanel`: рисует `tuning` (группы ползунков, что отличается от умолчаний, «скопировать как код», сброс). Без `.vue`-файла и без UI-библиотеки; вид задают CSS-переменные `--tbp-*`, запасные значения это наша тёмная тема |
|
|
39
|
+
| `dispose` | нужен `three`. `disposeObject(root, { textures })`: геометрии и материалы (и массивы) освобождаются один раз, сколько бы мешей их ни делило; текстуры только по просьбе |
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { Ticker } from '@1337exp/pkt-threejs-blueprint/ticker';
|
|
43
|
+
import { easeOutBack } from '@1337exp/pkt-threejs-blueprint/ease';
|
|
44
|
+
|
|
45
|
+
const ticker = new Ticker();
|
|
46
|
+
// каждый кадр: ticker.update(Math.min(realDt, maxDt));
|
|
47
|
+
await ticker.tween(0.4, (k) => (mesh.scale.x = 1 + k), easeOutBack);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Дельта кадра
|
|
51
|
+
|
|
52
|
+
`dt` — время между двумя кадрами в секундах, а не частота симуляции: частота кадров остаётся экранной (60, 120, 144 Гц). Ограничь её сверху (`maxDt`) перед подачей в тикер, чтобы после фона вкладки или подвисания не прилетел `dt` в секунды и все твины не ушли в конец за один кадр. В наших проектах это `1/30`, `0.05` или `0.1`; выбирай осознанно.
|
|
53
|
+
|
|
54
|
+
Пружине потолок, чтобы выжить, не нужен: `stepSpring` и `stepSpring3` берут точное решение уравнения пружины, поэтому устойчивы при любом `dt` и дают один и тот же путь на 30, 60 и 144 кадрах в секунду. Привычный однострочный шаг Эйлера (`v += (k·(цель − x) − c·v)·dt; x += v·dt`) не даёт ни того ни другого: жёсткость 2000 при `zeta` 1 разлетается ниже ~54 кадров в секунду, а и там, где держится, путь зависит от частоты кадров. Не пиши эту строку руками.
|
|
55
|
+
|
|
56
|
+
## Как устроено
|
|
57
|
+
|
|
58
|
+
Три слоя по зависимостям: `core/` (ничего), `three/` (нужен `three`), `components/` (готовые блоки интерфейса поверх ядра; стили это значения нашей тёмной темы, скопированные как CSS-переменные, без TaoUI). Дизайн, решения и анализ, на котором они стоят: [docs/design.md](docs/design.md), [docs/analysis-grok.md](docs/analysis-grok.md).
|
|
59
|
+
|
|
60
|
+
## Пока нет
|
|
61
|
+
|
|
62
|
+
Панель на чистом DOM поверх `tuning` (на Vue готова). Частицы, ленты, камеры, bloom и шейдеры остаются в играх намеренно: они разные, потому что разные картинки.
|
|
63
|
+
|
|
64
|
+
## Лицензия
|
|
65
|
+
|
|
66
|
+
MIT, см. [LICENSE](LICENSE).
|
|
67
|
+
|
|
68
|
+
## Мы
|
|
69
|
+
|
|
70
|
+
Домашняя страница: [666.io](https://666.io). Хочешь помочь, предложить идею или просто поговорить — заходи.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type PropType, type VNode } from 'vue';
|
|
2
|
+
import type { Tuning } from '../../core/tuning.js';
|
|
3
|
+
/**
|
|
4
|
+
* The "tweak it live" panel for Vue: draws the schema of a `Tuning` (groups of sliders), shows what differs from the defaults,
|
|
5
|
+
* and offers "copy as code" and "reset". It owns no state: every change goes through `tuning.set`, so saving and the live object
|
|
6
|
+
* stay the core's business. Written with `defineComponent` and render functions, not a `.vue` file, so plain `tsc` builds it.
|
|
7
|
+
* Панель «покрутить вживую» для Vue: рисует схему `Tuning` (группы ползунков), показывает, что отличается от умолчаний,
|
|
8
|
+
* и даёт «скопировать как код» и «сбросить». Своего состояния не держит: любое изменение идёт через `tuning.set`, поэтому
|
|
9
|
+
* сохранение и живой объект остаются делом ядра. Написана на `defineComponent` и функциях отрисовки, а не в `.vue`, чтобы её собирал обычный `tsc`.
|
|
10
|
+
*/
|
|
11
|
+
/** Texts of the panel. Defaults are English; pass your own for another language. / Тексты панели. По умолчанию английские; передай свои для другого языка. */
|
|
12
|
+
export interface TuningPanelLabels {
|
|
13
|
+
copy: string;
|
|
14
|
+
reset: string;
|
|
15
|
+
copied: string;
|
|
16
|
+
copyFailed: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A value for the label next to a slider: as many decimals as the step needs, plus the unit.
|
|
20
|
+
* Значение для подписи рядом с ползунком: столько знаков, сколько нужно шагу, и единица.
|
|
21
|
+
*/
|
|
22
|
+
export declare function formatKnobValue(value: number, step: number, unit?: string): string;
|
|
23
|
+
export declare const TuningPanel: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
24
|
+
/** The store to draw. May be swapped at runtime (the panel rebinds). / Хранилище, которое рисуем. Можно менять на лету (панель переподключится). */
|
|
25
|
+
tuning: {
|
|
26
|
+
type: PropType<Tuning<any>>;
|
|
27
|
+
required: true;
|
|
28
|
+
};
|
|
29
|
+
/** Pin to the right edge of a positioned parent. Off: an ordinary block you place yourself. / Прижать к правому краю позиционированного родителя. Выключено: обычный блок, ставь сам. */
|
|
30
|
+
floating: {
|
|
31
|
+
type: BooleanConstructor;
|
|
32
|
+
default: boolean;
|
|
33
|
+
};
|
|
34
|
+
labels: {
|
|
35
|
+
type: PropType<Partial<TuningPanelLabels>>;
|
|
36
|
+
default: () => {};
|
|
37
|
+
};
|
|
38
|
+
/** The name in "copy as code": `export const <name> = { … }`. / Имя в «скопировать как код»: `export const <name> = { … }`. */
|
|
39
|
+
sourceName: {
|
|
40
|
+
type: StringConstructor;
|
|
41
|
+
default: string;
|
|
42
|
+
};
|
|
43
|
+
}>, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
44
|
+
[key: string]: any;
|
|
45
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
46
|
+
/** After every change and reset made through the panel or anywhere else through the same store. / После каждого изменения и сброса, из панели или откуда угодно через то же хранилище. */
|
|
47
|
+
change: () => true;
|
|
48
|
+
}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
49
|
+
/** The store to draw. May be swapped at runtime (the panel rebinds). / Хранилище, которое рисуем. Можно менять на лету (панель переподключится). */
|
|
50
|
+
tuning: {
|
|
51
|
+
type: PropType<Tuning<any>>;
|
|
52
|
+
required: true;
|
|
53
|
+
};
|
|
54
|
+
/** Pin to the right edge of a positioned parent. Off: an ordinary block you place yourself. / Прижать к правому краю позиционированного родителя. Выключено: обычный блок, ставь сам. */
|
|
55
|
+
floating: {
|
|
56
|
+
type: BooleanConstructor;
|
|
57
|
+
default: boolean;
|
|
58
|
+
};
|
|
59
|
+
labels: {
|
|
60
|
+
type: PropType<Partial<TuningPanelLabels>>;
|
|
61
|
+
default: () => {};
|
|
62
|
+
};
|
|
63
|
+
/** The name in "copy as code": `export const <name> = { … }`. / Имя в «скопировать как код»: `export const <name> = { … }`. */
|
|
64
|
+
sourceName: {
|
|
65
|
+
type: StringConstructor;
|
|
66
|
+
default: string;
|
|
67
|
+
};
|
|
68
|
+
}>> & Readonly<{
|
|
69
|
+
onChange?: (() => any) | undefined;
|
|
70
|
+
}>, {
|
|
71
|
+
floating: boolean;
|
|
72
|
+
labels: Partial<TuningPanelLabels>;
|
|
73
|
+
sourceName: string;
|
|
74
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { defineComponent, h, onBeforeUnmount, shallowRef, watch } from 'vue';
|
|
2
|
+
const DEFAULT_LABELS = { copy: 'Copy', reset: 'Reset', copied: 'Copied', copyFailed: 'Clipboard unavailable' };
|
|
3
|
+
/**
|
|
4
|
+
* A value for the label next to a slider: as many decimals as the step needs, plus the unit.
|
|
5
|
+
* Значение для подписи рядом с ползунком: столько знаков, сколько нужно шагу, и единица.
|
|
6
|
+
*/
|
|
7
|
+
export function formatKnobValue(value, step, unit = '') {
|
|
8
|
+
const text = value.toFixed(step < 0.01 ? 3 : step < 1 ? 2 : 0);
|
|
9
|
+
return unit ? `${text} ${unit}` : text;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The look. Every colour, radius and gap is a CSS variable with a fallback, so an app restyles the panel by setting `--tbp-*`
|
|
13
|
+
* anywhere above it. The fallbacks are the values of our dark theme (TaoUI), copied, not imported.
|
|
14
|
+
* Вид. Каждый цвет, радиус и отступ это CSS-переменная с запасным значением, поэтому приложение перекрашивает панель, задав `--tbp-*`
|
|
15
|
+
* где угодно выше неё. Запасные значения это значения нашей тёмной темы (TaoUI), скопированные, а не подключённые.
|
|
16
|
+
*/
|
|
17
|
+
const CSS = `
|
|
18
|
+
.tbp-panel {
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
gap: var(--tbp-space-3, 12px);
|
|
23
|
+
padding: var(--tbp-space-3, 12px);
|
|
24
|
+
background: var(--tbp-bg, #151515);
|
|
25
|
+
border: 1px solid var(--tbp-border, rgb(51 51 51 / 0.4));
|
|
26
|
+
border-radius: var(--tbp-radius, 8px);
|
|
27
|
+
color: var(--tbp-text, #bfbfbf);
|
|
28
|
+
font: var(--tbp-font-size, 12px) / 1.4 var(--tbp-font, system-ui, sans-serif);
|
|
29
|
+
}
|
|
30
|
+
.tbp-panel--floating {
|
|
31
|
+
position: absolute;
|
|
32
|
+
top: var(--tbp-space-4, 16px);
|
|
33
|
+
right: var(--tbp-space-4, 16px);
|
|
34
|
+
bottom: var(--tbp-space-4, 16px);
|
|
35
|
+
width: 280px;
|
|
36
|
+
max-height: calc(100% - 2 * var(--tbp-space-4, 16px));
|
|
37
|
+
}
|
|
38
|
+
.tbp-scroll { flex: 1; min-height: 0; overflow-y: auto; padding-right: var(--tbp-space-1, 4px); }
|
|
39
|
+
.tbp-group + .tbp-group { margin-top: var(--tbp-space-3, 12px); }
|
|
40
|
+
.tbp-title { margin-bottom: var(--tbp-space-2, 8px); color: var(--tbp-text-bright, #fff); font-weight: 600; font-size: var(--tbp-font-size-title, 13px); }
|
|
41
|
+
.tbp-knob { display: block; margin-bottom: var(--tbp-space-2, 8px); }
|
|
42
|
+
.tbp-head { display: flex; justify-content: space-between; margin-bottom: var(--tbp-space-1, 4px); color: var(--tbp-text-muted, #8a8a8a); }
|
|
43
|
+
.tbp-value { color: var(--tbp-text-bright, #fff); font-weight: 500; font-variant-numeric: tabular-nums; }
|
|
44
|
+
.tbp-value--changed { color: var(--tbp-accent, #ffc04a); }
|
|
45
|
+
.tbp-range { display: block; width: 100%; margin: 0; accent-color: var(--tbp-accent, #ffc04a); }
|
|
46
|
+
.tbp-actions { display: flex; gap: var(--tbp-space-2, 8px); }
|
|
47
|
+
.tbp-btn {
|
|
48
|
+
flex: 1;
|
|
49
|
+
padding: var(--tbp-space-2, 8px) var(--tbp-space-3, 12px);
|
|
50
|
+
background: var(--tbp-field, rgb(51 51 51 / 0.2));
|
|
51
|
+
border: 1px solid var(--tbp-border, rgb(51 51 51 / 0.4));
|
|
52
|
+
border-radius: var(--tbp-radius-sm, 4px);
|
|
53
|
+
color: var(--tbp-text, #bfbfbf);
|
|
54
|
+
font: inherit;
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
}
|
|
57
|
+
.tbp-btn:hover { background: var(--tbp-hover, #262626); color: var(--tbp-text-bright, #fff); }
|
|
58
|
+
.tbp-btn--primary { background: var(--tbp-accent, #ffc04a); border-color: transparent; color: var(--tbp-on-accent, #0d0d0d); font-weight: 600; }
|
|
59
|
+
.tbp-btn--primary:hover { background: var(--tbp-accent-hover, #ffb40b); color: var(--tbp-on-accent, #0d0d0d); }
|
|
60
|
+
`;
|
|
61
|
+
const STYLE_ID = 'tbp-styles';
|
|
62
|
+
/** Put the stylesheet into the page once. Does nothing without a `document` (server rendering, tests in node). / Положить стили на страницу один раз. Ничего не делает без `document` (серверная отрисовка, тесты в node). */
|
|
63
|
+
function ensureStyles() {
|
|
64
|
+
if (typeof document === 'undefined' || document.getElementById(STYLE_ID))
|
|
65
|
+
return;
|
|
66
|
+
const style = document.createElement('style');
|
|
67
|
+
style.id = STYLE_ID;
|
|
68
|
+
style.textContent = CSS;
|
|
69
|
+
document.head.appendChild(style);
|
|
70
|
+
}
|
|
71
|
+
export const TuningPanel = defineComponent({
|
|
72
|
+
name: 'TbpTuningPanel',
|
|
73
|
+
props: {
|
|
74
|
+
/** The store to draw. May be swapped at runtime (the panel rebinds). / Хранилище, которое рисуем. Можно менять на лету (панель переподключится). */
|
|
75
|
+
tuning: { type: Object, required: true },
|
|
76
|
+
/** Pin to the right edge of a positioned parent. Off: an ordinary block you place yourself. / Прижать к правому краю позиционированного родителя. Выключено: обычный блок, ставь сам. */
|
|
77
|
+
floating: { type: Boolean, default: false },
|
|
78
|
+
labels: { type: Object, default: () => ({}) },
|
|
79
|
+
/** The name in "copy as code": `export const <name> = { … }`. / Имя в «скопировать как код»: `export const <name> = { … }`. */
|
|
80
|
+
sourceName: { type: String, default: 'tuning' },
|
|
81
|
+
},
|
|
82
|
+
emits: {
|
|
83
|
+
/** After every change and reset made through the panel or anywhere else through the same store. / После каждого изменения и сброса, из панели или откуда угодно через то же хранилище. */
|
|
84
|
+
change: () => true,
|
|
85
|
+
},
|
|
86
|
+
setup(props, { emit, slots }) {
|
|
87
|
+
ensureStyles();
|
|
88
|
+
// The store's values are a plain object, not reactive: bump a counter to redraw after the store says it changed.
|
|
89
|
+
// Значения хранилища это обычный объект, не реактивный: после оповещения хранилища увеличиваем счётчик, чтобы перерисоваться.
|
|
90
|
+
const version = shallowRef(0);
|
|
91
|
+
const copyState = shallowRef('idle');
|
|
92
|
+
let unsubscribe = () => { };
|
|
93
|
+
let timer;
|
|
94
|
+
watch(() => props.tuning, (tuning) => {
|
|
95
|
+
unsubscribe();
|
|
96
|
+
unsubscribe = tuning.subscribe(() => {
|
|
97
|
+
version.value++;
|
|
98
|
+
emit('change');
|
|
99
|
+
});
|
|
100
|
+
version.value++;
|
|
101
|
+
}, { immediate: true });
|
|
102
|
+
onBeforeUnmount(() => {
|
|
103
|
+
unsubscribe();
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
});
|
|
106
|
+
async function copy() {
|
|
107
|
+
try {
|
|
108
|
+
await navigator.clipboard.writeText(props.tuning.toSource(props.sourceName));
|
|
109
|
+
copyState.value = 'copied';
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
copyState.value = 'failed';
|
|
113
|
+
}
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
timer = setTimeout(() => (copyState.value = 'idle'), 1600);
|
|
116
|
+
}
|
|
117
|
+
return () => {
|
|
118
|
+
void version.value;
|
|
119
|
+
const { tuning } = props;
|
|
120
|
+
const labels = { ...DEFAULT_LABELS, ...props.labels };
|
|
121
|
+
const copyLabel = copyState.value === 'copied' ? labels.copied : copyState.value === 'failed' ? labels.copyFailed : labels.copy;
|
|
122
|
+
const groups = tuning.groups.map((group) => h('section', { class: 'tbp-group', key: group.title }, [
|
|
123
|
+
h('div', { class: 'tbp-title' }, group.title),
|
|
124
|
+
...group.knobs.map((knob) => {
|
|
125
|
+
const value = tuning.values[knob.key];
|
|
126
|
+
const changed = value !== tuning.defaults[knob.key];
|
|
127
|
+
return h('label', { class: 'tbp-knob', key: knob.key }, [
|
|
128
|
+
h('span', { class: 'tbp-head' }, [
|
|
129
|
+
h('span', knob.label),
|
|
130
|
+
h('b', { class: ['tbp-value', { 'tbp-value--changed': changed }] }, formatKnobValue(value, knob.step, knob.unit)),
|
|
131
|
+
]),
|
|
132
|
+
h('input', {
|
|
133
|
+
class: 'tbp-range',
|
|
134
|
+
type: 'range',
|
|
135
|
+
min: knob.min,
|
|
136
|
+
max: knob.max,
|
|
137
|
+
step: knob.step,
|
|
138
|
+
value,
|
|
139
|
+
'aria-label': knob.label,
|
|
140
|
+
onInput: (event) => tuning.set(knob.key, Number(event.target.value)),
|
|
141
|
+
}),
|
|
142
|
+
]);
|
|
143
|
+
}),
|
|
144
|
+
]));
|
|
145
|
+
return h('div', { class: ['tbp-panel', { 'tbp-panel--floating': props.floating }] }, [
|
|
146
|
+
h('div', { class: 'tbp-scroll' }, [slots.before?.(), ...groups]),
|
|
147
|
+
h('div', { class: 'tbp-actions' }, [
|
|
148
|
+
h('button', { class: 'tbp-btn tbp-btn--primary', type: 'button', onClick: copy }, copyLabel),
|
|
149
|
+
h('button', { class: 'tbp-btn', type: 'button', onClick: () => tuning.reset() }, labels.reset),
|
|
150
|
+
]),
|
|
151
|
+
]);
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
//# sourceMappingURL=tuning-panel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tuning-panel.js","sourceRoot":"","sources":["../../../src/components/vue/tuning-panel.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Easing curve: `t` from 0 to 1 → progress from 0 to 1 (Back overshoots past 1).
|
|
3
|
+
* Кривая времени: `t` от 0 до 1 → прогресс от 0 до 1 (у Back — с перелётом).
|
|
4
|
+
*/
|
|
5
|
+
export type Ease = (t: number) => number;
|
|
6
|
+
export declare const linear: Ease;
|
|
7
|
+
export declare const easeInQuad: Ease;
|
|
8
|
+
export declare const easeOutQuad: Ease;
|
|
9
|
+
export declare const easeInCubic: Ease;
|
|
10
|
+
export declare const easeOutCubic: Ease;
|
|
11
|
+
export declare const easeInOutCubic: Ease;
|
|
12
|
+
/** Overshoots by ~10 % and settles (overshoot constant 1.70158). / Перелёт около 10 % и возврат (константа 1.70158). */
|
|
13
|
+
export declare const easeOutBack: Ease;
|
|
14
|
+
/**
|
|
15
|
+
* Smooth 0 → 1 transition between `edge0` and `edge1`, the same argument order as `smoothstep` in GLSL.
|
|
16
|
+
* Note: `THREE.MathUtils.smoothstep` takes `(x, min, max)`.
|
|
17
|
+
* Плавный переход от 0 к 1 между `edge0` и `edge1`, порядок аргументов как у `smoothstep` в GLSL.
|
|
18
|
+
* Внимание: `THREE.MathUtils.smoothstep` принимает `(x, min, max)`.
|
|
19
|
+
*/
|
|
20
|
+
export declare const smoothstep: (edge0: number, edge1: number, x: number) => number;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const linear = (t) => t;
|
|
2
|
+
export const easeInQuad = (t) => t * t;
|
|
3
|
+
export const easeOutQuad = (t) => 1 - (1 - t) * (1 - t);
|
|
4
|
+
export const easeInCubic = (t) => t * t * t;
|
|
5
|
+
export const easeOutCubic = (t) => 1 - (1 - t) ** 3;
|
|
6
|
+
export const easeInOutCubic = (t) => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2);
|
|
7
|
+
/** Overshoots by ~10 % and settles (overshoot constant 1.70158). / Перелёт около 10 % и возврат (константа 1.70158). */
|
|
8
|
+
export const easeOutBack = (t) => 1 + 2.70158 * (t - 1) ** 3 + 1.70158 * (t - 1) ** 2;
|
|
9
|
+
/**
|
|
10
|
+
* Smooth 0 → 1 transition between `edge0` and `edge1`, the same argument order as `smoothstep` in GLSL.
|
|
11
|
+
* Note: `THREE.MathUtils.smoothstep` takes `(x, min, max)`.
|
|
12
|
+
* Плавный переход от 0 к 1 между `edge0` и `edge1`, порядок аргументов как у `smoothstep` в GLSL.
|
|
13
|
+
* Внимание: `THREE.MathUtils.smoothstep` принимает `(x, min, max)`.
|
|
14
|
+
*/
|
|
15
|
+
export const smoothstep = (edge0, edge1, x) => {
|
|
16
|
+
const t = Math.min(1, Math.max(0, (x - edge0) / (edge1 - edge0)));
|
|
17
|
+
return t * t * (3 - 2 * t);
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=ease.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ease.js","sourceRoot":"","sources":["../../src/core/ease.ts"],"names":[],"mappings":"AAMA,MAAM,CAAC,MAAM,MAAM,GAAS,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AACrC,MAAM,CAAC,MAAM,UAAU,GAAS,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;AAC7C,MAAM,CAAC,MAAM,WAAW,GAAS,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AAC9D,MAAM,CAAC,MAAM,WAAW,GAAS,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,CAAC,MAAM,YAAY,GAAS,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;AAC1D,MAAM,CAAC,MAAM,cAAc,GAAS,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACjG,wHAAwH;AACxH,MAAM,CAAC,MAAM,WAAW,GAAS,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,OAAO,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;AAE5F;;;;;GAKG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,KAAa,EAAE,KAAa,EAAE,CAAS,EAAU,EAAE;IAC1E,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAClE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;AAC/B,CAAC,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frame loop: calls `frame(dt, now)` once per screen frame with the delta capped at `maxDt`.
|
|
3
|
+
* No `three` and no DOM types here: `requestAnimationFrame` is taken from `globalThis` (or injected), so the cap logic runs in tests.
|
|
4
|
+
* Цикл кадров: зовёт `frame(dt, now)` раз за кадр экрана с дельтой, ограниченной `maxDt`.
|
|
5
|
+
* Здесь нет ни `three`, ни типов DOM: `requestAnimationFrame` берётся из `globalThis` (или подставляется), так что логика потолка проверяется тестами.
|
|
6
|
+
*/
|
|
7
|
+
export interface LoopOptions {
|
|
8
|
+
/**
|
|
9
|
+
* The upper bound of the frame delta, in seconds. It is NOT a simulation rate: the frame rate stays the screen's.
|
|
10
|
+
* It exists so that a hidden tab or a stall does not deliver a `dt` of seconds, which would send every tween to its end
|
|
11
|
+
* and every spring across the screen in one frame. Our projects use 1/30, 0.05 or 0.1: pick it deliberately.
|
|
12
|
+
* Springs from `spring` do not depend on it for stability.
|
|
13
|
+
* Верхний предел дельты кадра в секундах. Это НЕ частота симуляции: частота кадров остаётся экранной.
|
|
14
|
+
* Он нужен, чтобы после фона вкладки или подвисания не прилетел `dt` в секунды: все твины ушли бы в конец,
|
|
15
|
+
* а пружины прыгнули бы через весь экран за один кадр. У нас 1/30, 0.05 или 0.1: выбирай осознанно.
|
|
16
|
+
* Пружинам из `spring` он для устойчивости не нужен.
|
|
17
|
+
*/
|
|
18
|
+
maxDt: number;
|
|
19
|
+
/** Default: the global `requestAnimationFrame`. / По умолчанию глобальный `requestAnimationFrame`. */
|
|
20
|
+
schedule?: (callback: (now: number) => void) => number;
|
|
21
|
+
/** Default: the global `cancelAnimationFrame`. / По умолчанию глобальный `cancelAnimationFrame`. */
|
|
22
|
+
cancel?: (handle: number) => void;
|
|
23
|
+
/** Time in milliseconds, on the same clock as the timestamps `schedule` passes. Default: `performance.now`. / Время в миллисекундах на тех же часах, что и метки от `schedule`. По умолчанию `performance.now`. */
|
|
24
|
+
now?: () => number;
|
|
25
|
+
}
|
|
26
|
+
export interface Loop {
|
|
27
|
+
/** Stop the loop. Safe to call twice and from inside `frame`. / Остановить цикл. Можно звать дважды и изнутри `frame`. */
|
|
28
|
+
stop(): void;
|
|
29
|
+
readonly running: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Start the loop. `frame(dt, now)` gets `dt` in seconds (never negative, never above `maxDt`) and `now`, the raw timestamp in milliseconds:
|
|
33
|
+
* keep real time for shaders from `now`, and move things by `dt`. The first frame measures from the moment of the call.
|
|
34
|
+
* Запустить цикл. `frame(dt, now)` получает `dt` в секундах (не меньше нуля и не больше `maxDt`) и `now`, сырую метку в миллисекундах:
|
|
35
|
+
* реальное время для шейдеров бери из `now`, а двигай всё по `dt`. Первый кадр отсчитывается от момента вызова.
|
|
36
|
+
*/
|
|
37
|
+
export declare function startLoop(frame: (dt: number, now: number) => void, options: LoopOptions): Loop;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frame loop: calls `frame(dt, now)` once per screen frame with the delta capped at `maxDt`.
|
|
3
|
+
* No `three` and no DOM types here: `requestAnimationFrame` is taken from `globalThis` (or injected), so the cap logic runs in tests.
|
|
4
|
+
* Цикл кадров: зовёт `frame(dt, now)` раз за кадр экрана с дельтой, ограниченной `maxDt`.
|
|
5
|
+
* Здесь нет ни `three`, ни типов DOM: `requestAnimationFrame` берётся из `globalThis` (или подставляется), так что логика потолка проверяется тестами.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Start the loop. `frame(dt, now)` gets `dt` in seconds (never negative, never above `maxDt`) and `now`, the raw timestamp in milliseconds:
|
|
9
|
+
* keep real time for shaders from `now`, and move things by `dt`. The first frame measures from the moment of the call.
|
|
10
|
+
* Запустить цикл. `frame(dt, now)` получает `dt` в секундах (не меньше нуля и не больше `maxDt`) и `now`, сырую метку в миллисекундах:
|
|
11
|
+
* реальное время для шейдеров бери из `now`, а двигай всё по `dt`. Первый кадр отсчитывается от момента вызова.
|
|
12
|
+
*/
|
|
13
|
+
export function startLoop(frame, options) {
|
|
14
|
+
const { maxDt } = options;
|
|
15
|
+
if (!(maxDt > 0) || !Number.isFinite(maxDt))
|
|
16
|
+
throw new RangeError(`startLoop: maxDt must be a positive number of seconds, got ${maxDt}`);
|
|
17
|
+
const host = globalThis;
|
|
18
|
+
const schedule = options.schedule ?? host.requestAnimationFrame?.bind(host);
|
|
19
|
+
const cancel = options.cancel ?? host.cancelAnimationFrame?.bind(host);
|
|
20
|
+
const now = options.now ?? (() => host.performance?.now() ?? Date.now());
|
|
21
|
+
if (!schedule || !cancel)
|
|
22
|
+
throw new Error('startLoop: requestAnimationFrame is not available here, pass `schedule` and `cancel`');
|
|
23
|
+
let last = now();
|
|
24
|
+
let handle = 0;
|
|
25
|
+
let running = true;
|
|
26
|
+
const tick = (time) => {
|
|
27
|
+
// Schedule first: a throw inside `frame` must not silently end the loop, and `stop()` inside it must still win.
|
|
28
|
+
// Сначала планируем следующий: исключение в `frame` не должно молча остановить цикл, а `stop()` внутри него всё равно побеждает.
|
|
29
|
+
handle = schedule(tick);
|
|
30
|
+
const dt = Math.min(Math.max((time - last) / 1000, 0), maxDt);
|
|
31
|
+
last = time;
|
|
32
|
+
frame(dt, time);
|
|
33
|
+
};
|
|
34
|
+
handle = schedule(tick);
|
|
35
|
+
return {
|
|
36
|
+
get running() {
|
|
37
|
+
return running;
|
|
38
|
+
},
|
|
39
|
+
stop() {
|
|
40
|
+
if (!running)
|
|
41
|
+
return;
|
|
42
|
+
running = false;
|
|
43
|
+
cancel(handle);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=loop.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loop.js","sourceRoot":"","sources":["../../src/core/loop.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAkCH;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,KAAwC,EAAE,OAAoB;IACpF,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IAC1B,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,8DAA8D,KAAK,EAAE,CAAC,CAAC;IAEzI,MAAM,IAAI,GAAG,UAAkB,CAAC;IAChC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,IAAI,CAAC,qBAAqB,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5E,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,oBAAoB,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACvE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,EAAE,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IACzE,IAAI,CAAC,QAAQ,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,sFAAsF,CAAC,CAAC;IAElI,IAAI,IAAI,GAAG,GAAG,EAAE,CAAC;IACjB,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,OAAO,GAAG,IAAI,CAAC;IAEnB,MAAM,IAAI,GAAG,CAAC,IAAY,EAAQ,EAAE;QAChC,gHAAgH;QAChH,iIAAiI;QACjI,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QACxB,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QAC9D,IAAI,GAAG,IAAI,CAAC;QACZ,KAAK,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IACpB,CAAC,CAAC;IACF,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;IAExB,OAAO;QACH,IAAI,OAAO;YACP,OAAO,OAAO,CAAC;QACnB,CAAC;QACD,IAAI;YACA,IAAI,CAAC,OAAO;gBAAE,OAAO;YACrB,OAAO,GAAG,KAAK,CAAC;YAChB,MAAM,CAAC,MAAM,CAAC,CAAC;QACnB,CAAC;KACJ,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A random number generator returning [0, 1). Any one fits, a seeded one included.
|
|
3
|
+
* Генератор случайных чисел [0, 1). Подойдёт любой, в том числе с зерном.
|
|
4
|
+
*/
|
|
5
|
+
export type Rng = () => number;
|
|
6
|
+
/**
|
|
7
|
+
* A tiny deterministic generator (mulberry32): the same seed gives the same sequence on every machine, so a game,
|
|
8
|
+
* a map or a scatter of grass can be replayed from its seed. The sequence is frozen by a test against the original
|
|
9
|
+
* from pkt-3match: do not change the arithmetic, saved seeds depend on it.
|
|
10
|
+
* Маленький детерминированный генератор (mulberry32): одно зерно даёт одну последовательность на любой машине, поэтому партию,
|
|
11
|
+
* карту или раскладку травы можно повторить по зерну. Последовательность заморожена тестом по оригиналу из pkt-3match:
|
|
12
|
+
* арифметику не менять, от неё зависят сохранённые зёрна.
|
|
13
|
+
*/
|
|
14
|
+
export declare function mulberry32(seed: number): Rng;
|
|
15
|
+
/** An integer in [0, count). / Целое число в [0, count). */
|
|
16
|
+
export declare function randomInt(rng: Rng, count: number): number;
|
package/dist/core/rng.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny deterministic generator (mulberry32): the same seed gives the same sequence on every machine, so a game,
|
|
3
|
+
* a map or a scatter of grass can be replayed from its seed. The sequence is frozen by a test against the original
|
|
4
|
+
* from pkt-3match: do not change the arithmetic, saved seeds depend on it.
|
|
5
|
+
* Маленький детерминированный генератор (mulberry32): одно зерно даёт одну последовательность на любой машине, поэтому партию,
|
|
6
|
+
* карту или раскладку травы можно повторить по зерну. Последовательность заморожена тестом по оригиналу из pkt-3match:
|
|
7
|
+
* арифметику не менять, от неё зависят сохранённые зёрна.
|
|
8
|
+
*/
|
|
9
|
+
export function mulberry32(seed) {
|
|
10
|
+
let a = seed >>> 0;
|
|
11
|
+
return () => {
|
|
12
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
13
|
+
let t = a;
|
|
14
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
15
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
16
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** An integer in [0, count). / Целое число в [0, count). */
|
|
20
|
+
export function randomInt(rng, count) {
|
|
21
|
+
return Math.floor(rng() * count);
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=rng.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rng.js","sourceRoot":"","sources":["../../src/core/rng.ts"],"names":[],"mappings":"AAMA;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY;IACnC,IAAI,CAAC,GAAG,IAAI,KAAK,CAAC,CAAC;IACnB,OAAO,GAAG,EAAE;QACR,CAAC,GAAG,CAAC,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;QAC3B,IAAI,CAAC,GAAG,CAAC,CAAC;QACV,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACrC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;QAC1C,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,UAAU,CAAC;IACjD,CAAC,CAAC;AACN,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,SAAS,CAAC,GAAQ,EAAE,KAAa;IAC7C,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC;AACrC,CAAC"}
|