akari-video 0.1.41 → 0.1.42
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/package.json +1 -1
- package/src/self-update.mjs +16 -2
- package/vendor/.akari-capability-sources.json +1 -0
- package/vendor/docs/contract-2026-07-22-render-basics.md +2 -0
- package/vendor/docs/contract-2026-09-03-clip-adjust-v0.md +1 -0
- package/vendor/docs/contract-2026-09-05-clip-adjust-v1.md +102 -0
- package/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/edit-lint/src/edit-lint.mjs +56 -3
- package/vendor/packages/edit-store/lib/adjust-css-visual.d.ts +38 -0
- package/vendor/packages/edit-store/lib/adjust-css-visual.js +67 -0
- package/vendor/packages/edit-store/lib/edit-v2.d.ts +40 -2
- package/vendor/packages/edit-store/lib/edit-v2.js +42 -2
- package/vendor/packages/edit-store/lib/index.d.ts +1 -0
- package/vendor/packages/edit-store/lib/index.js +2 -0
- package/vendor/packages/edit-store/lib/webview-kernel.d.ts +1 -0
- package/vendor/packages/edit-store/lib/webview-kernel.js +89 -2
- package/vendor/packages/schemas/bin/validate-edit.mjs +54 -2
- package/vendor/packages/schemas/edit.schema.json +285 -13
- package/vendor/packages/schemas/examples/edit-v2-adjust-v1-hue-empty-invalid/edit.json +95 -0
- package/vendor/packages/schemas/examples/edit-v2-adjust-v1-order-invalid/edit.json +100 -0
- package/vendor/packages/schemas/examples/edit-v2-adjust-v1-unknown-invalid/edit.json +101 -0
- package/vendor/packages/schemas/examples/edit-v2-adjust-v1-valid/edit.json +100 -0
- package/vendor/packages/schemas/examples/edit-v2-adjust-v1-wheels-invalid/edit.json +100 -0
- package/vendor/packages/schemas/test/adjust-v1.test.mjs +47 -0
- package/vendor/packages/schemas/test/edit-v2-schema.test.mjs +1 -1
- package/vendor/packages/schemas/test/fixtures/adjust-v1-cases.mjs +37 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.42",
|
|
4
4
|
"description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/self-update.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { resolveAkariHome } from './update-check.mjs';
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* `akari update` の実適用(update-and-versioning 契約(内部リポ)§11):
|
|
11
|
-
* フィードの `components.app`(install.sh
|
|
11
|
+
* フィードの `components.app`(install.sh と共通のソース tarball、既定は CLI + プレビュー)を
|
|
12
12
|
* DL → sha256 検証 → `~/.akari/staging/<version>/` へ展開 →(検証完了までスワップ開始しない)
|
|
13
13
|
* → `~/.akari/app` と rename ベースで入れ替え → 旧版は `~/.akari/app-previous/` に 1 世代保持。
|
|
14
14
|
*
|
|
@@ -106,8 +106,22 @@ function extractTarGz({ tarPath, destDir }) {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/**
|
|
109
|
+
/** 展開先だけを CLI 用に絞る。明示的なシェル opt-in 時は配布元の設定を保持する。 */
|
|
110
|
+
export function applyCliOnlyWorkspaces({ cwd }) {
|
|
111
|
+
if (process.env.AKARI_INSTALL_SHELL === '1') return;
|
|
112
|
+
const packagePath = join(cwd, 'package.json');
|
|
113
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
114
|
+
pkg.workspaces = ['packages/*'];
|
|
115
|
+
writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** install.sh と同じ意味論: スワップ後に CLI 用 workspaces を適用して npm install する。 */
|
|
110
119
|
function defaultRunNpmInstall({ cwd }) {
|
|
120
|
+
try {
|
|
121
|
+
applyCliOnlyWorkspaces({ cwd });
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return { ok: false, message: error.message };
|
|
124
|
+
}
|
|
111
125
|
const result = spawnSync('npm', ['install', '--no-audit', '--no-fund', '--loglevel=error'], { cwd, stdio: 'pipe' });
|
|
112
126
|
if (result.error) {
|
|
113
127
|
return { ok: false, message: result.error.message };
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"docs/contract-2026-09-02-word-book-v0.md",
|
|
64
64
|
"docs/contract-2026-09-03-clip-adjust-v0.md",
|
|
65
65
|
"docs/contract-2026-09-03-preview-playback-rate-v1.md",
|
|
66
|
+
"docs/contract-2026-09-05-clip-adjust-v1.md",
|
|
66
67
|
"packages/akari-launcher/package.json",
|
|
67
68
|
"packages/akari-launcher/README.md",
|
|
68
69
|
"packages/akari-tools/package.json",
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
|
|
28
28
|
- 除外(次段送り): ブレンドモード・PinP・プリレンダ合成レール(レイヤー機構が前提のため)
|
|
29
29
|
|
|
30
|
+
現況(2026-09-05): ffmpeg `lut3d` 経路は撤去済み。`output.look` は frame-engine の最終パス(WebGL2 LUT)で gpu / osr の 2 出口が適用し、per-clip の色補正は `docs/contract-2026-09-03-clip-adjust-v0.md` §4 の順(item adjust → 合成 → look)で同じエンジンが適用する。
|
|
31
|
+
|
|
30
32
|
## 2. 横断要件
|
|
31
33
|
|
|
32
34
|
1. schema は**追記のみ**(既存 edit.json が全て無変更で valid のまま)。validate-edit /
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
lifecycle: accepted
|
|
3
|
+
date: 2026-09-05
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# edit.json v2 clip adjust v1 契約
|
|
7
|
+
|
|
8
|
+
## 0. 位置づけ
|
|
9
|
+
|
|
10
|
+
v0 の基本補正と LUT に curves / wheels / hue を追加する。旧 src/lib/color-grade.ts の数式を正本値として移植する。
|
|
11
|
+
|
|
12
|
+
## 1. 席
|
|
13
|
+
|
|
14
|
+
version 2 visual item の tracks[].items[].adjust。media / html / telop / filter / group / captions / caption の 7 定義共通。audio と shape、version 0 / 1 の cuts[] / layers[] には追加しない。
|
|
15
|
+
|
|
16
|
+
## 2. 語彙仕様 v1
|
|
17
|
+
|
|
18
|
+
`$defs.adjustV1`(v0 の `basic` / `lut` / `sections` は不変。以下を追加。すべて任意・additionalProperties false):
|
|
19
|
+
|
|
20
|
+
- `curves`: object `{ master?, r?, g?, b? }`。各値は `[{ in: number 0..1, out: number 0..1 }]`(minItems 2・maxItems 16・
|
|
21
|
+
各点 additionalProperties false・in / out とも required)。**`in` は狭義単調増加**(schema では表現できないので
|
|
22
|
+
validate-edit / edit-store / edit-lint で検査)。チャンネル identity = ちょうど 2 点 `[{0,0},{1,1}]`(許容 1e-5)。
|
|
23
|
+
- `wheels`: object `{ lift?, gamma?, gain?, offset? }`。各値は object `{ r?, g?, b? }`(number)。範囲:
|
|
24
|
+
lift ±0.25 / gamma ±0.5 / gain ±0.5 / offset ±0.1。省略 = 0(中立)。
|
|
25
|
+
- `hue`: object `{ hue?, sat?, luma? }`。各値は `[{ hue: number 0..1, value: number 0..1 }]`(minItems 1・maxItems 16・
|
|
26
|
+
additionalProperties false・両方 required)。**`hue` は狭義単調増加**。中立 value = 0.5。チャンネル identity =
|
|
27
|
+
省略または全点 `|value-0.5| ≤ 1e-4`。
|
|
28
|
+
- `sections`: `basic` / `lut` に加えて `curves` / `wheels` / `hue`(boolean・false のときだけバイパス)。
|
|
29
|
+
- **演算順(固定)**: ① basic(v0 §3 の 1〜6)→ ② lut(trilinear・intensity 混合)→ ③ wheels → ④ curves → ⑤ hue。
|
|
30
|
+
- ③ wheels: チャンネルごとに `c = v*(1-lift)+lift` → `c = pow(max(0,c), 1/(1+gamma))` → `c *= 1+gain` → `c = clamp01(c+offset)`
|
|
31
|
+
(lift / gamma / gain / offset は当該チャンネルの値・省略 0)。
|
|
32
|
+
- ④ curves: 区分線形。点列を in 昇順に評価し、`x ≤ 最初の in` は最初の out、`x ≥ 最後の in` は最後の out、
|
|
33
|
+
それ以外は挟む 2 点で線形補間、各評価を clamp01。**master を 3 ch に適用してから r / g / b を各 ch に適用**。
|
|
34
|
+
- ⑤ hue: RGB→HSV(`d = max-min`、`d > 1e-4` のときだけ h を計算(`cmax===r: ((g-b)/d+6)%6`・`cmax===g: (b-r)/d+2`・
|
|
35
|
+
else `(r-g)/d+4`・÷6)、`s = cmax > 1e-4 ? d/cmax : 0`、`v = cmax`)→ `shift = (sample(hue, h)-0.5)*2`・
|
|
36
|
+
`h' = (h+shift+1) % 1`・`s' = clamp01(s * sample(sat, h)*2)`・`v' = clamp01(v * sample(luma, h)*2)` → HSV→RGB
|
|
37
|
+
(sector = floor(h'*6) の 6 分割・`c = s'v'`・`x = c(1-|((h'*6) mod 2)-1|)`・`m = v'-c`・各 clamp01)。
|
|
38
|
+
`sample(ch, h)` = 点列を hue 昇順で評価、0 点なら 0.5、1 点ならその value、範囲外は端点、間は線形。
|
|
39
|
+
**h の参照は 3 ch とも変換前の h**(旧実装どおり)。
|
|
40
|
+
- identity 判定(書き込み側の規範): basic 全 0・lut 無し・wheels 全 0・curves 全 ch identity・hue 全 ch identity なら
|
|
41
|
+
`adjust` field 自体を除去(v0 §7 と同じ)。
|
|
42
|
+
- v1(cuts[] / layers[] 文書)には席を作らない(v0 と同じ)。
|
|
43
|
+
- ルックプリセット(D13)は語彙ではない: `presets/looks/index.jsonl`(`{id, kind:"look", name, description, when_to_use}`)+
|
|
44
|
+
`presets/looks/<id>.json` = `{ "id": "<id>", "adjust": { "basic": {…}, "wheels": {…} } }`。`adjust` の中身は `$defs.adjustV1` に
|
|
45
|
+
適合すること(schema テストで担保)。
|
|
46
|
+
|
|
47
|
+
基本補正は exposure ±3 EV、contrast / highlights / shadows / blacks / whites / temperature / tint / vibrance / saturation は ±1。全て任意、既定 0。lut は null または {lut: 空でない文字列, intensity?: 0..1}、intensity 既定 1。各 object は追加キー不可。
|
|
48
|
+
|
|
49
|
+
## 3. 基本補正の数値契約
|
|
50
|
+
|
|
51
|
+
演算は video-space、すなわち gamma 符号化 sRGB 値のまま行う。scene-linear へ変換しない。
|
|
52
|
+
入力チャンネルを `c = (r, g, b)`、`clamp01(x) = min(1, max(0, x))`、Rec.709 luma を
|
|
53
|
+
`Y(c) = 0.2126r + 0.7152g + 0.0722b` とする。`smoothstep(a,b,x)` は
|
|
54
|
+
`t = clamp01((x-a)/(b-a))`、`t²(3-2t)` である。
|
|
55
|
+
|
|
56
|
+
演算順は固定で、次の順に進める。
|
|
57
|
+
|
|
58
|
+
1. exposure: `c *= 2^exposure`。
|
|
59
|
+
2. white balance: `r *= 1 + temperature*0.18`、`b *= 1 - temperature*0.18`、
|
|
60
|
+
`g *= 1 - tint*0.12` とし、各チャンネルを `clamp01` する。
|
|
61
|
+
3. tone zones: highlights → shadows → whites → blacks の順に処理する。各有効ステップの直前に
|
|
62
|
+
現在の `c` から luma を再計算し、処理後は各チャンネルを `clamp01` する。
|
|
63
|
+
- highlights: `c *= 1 + highlights*smoothstep(0.5,0.9,Y(c))`
|
|
64
|
+
- shadows: `c *= 1 + shadows*(1-smoothstep(0.1,0.5,Y(c)))`
|
|
65
|
+
- whites: `c += whites*smoothstep(0.7,1.0,Y(c))*0.3`
|
|
66
|
+
- blacks: `c += blacks*(1-smoothstep(0.0,0.3,Y(c)))*0.3`
|
|
67
|
+
4. contrast: 各チャンネルを `(c-0.5)*(1+contrast)+0.5` とし、`clamp01` する。pivot は `0.5`。
|
|
68
|
+
5. saturation: 現在の Rec.709 luma を使い、各チャンネルを
|
|
69
|
+
`Y(c) + (c-Y(c))*(1+saturation)` として `clamp01` する。
|
|
70
|
+
6. vibrance: 現在値の `max` と `min` から
|
|
71
|
+
`S = max > 1e-6 ? (max-min)/max : 0`、`amount = vibrance*(1-S)` を求め、各チャンネルを
|
|
72
|
+
`Y(c) + (c-Y(c))*(1+amount)` として `clamp01` する。
|
|
73
|
+
7. 外部 LUT: `lut` が有効なら trilinear sampler で `c` を置換し、`intensity` で identity 入力と
|
|
74
|
+
LUT 出力を線形混合する。
|
|
75
|
+
|
|
76
|
+
定数の正本値は `REC709 = (0.2126, 0.7152, 0.0722)`、`TEMP_COEF = 0.18`、
|
|
77
|
+
`TINT_COEF = 0.12`、`CONTRAST_PIVOT = 0.5` である。基本補正を LUT に bake する消費者は
|
|
78
|
+
`LUT_3D_SIZE 33`、R fastest → G → B、各成分小数 6 桁とし、適用時は trilinear 補間する。
|
|
79
|
+
|
|
80
|
+
## 4. 解決順とバイパス
|
|
81
|
+
|
|
82
|
+
basic → lut → wheels → curves → hue → item 合成 → output.look。LUT はスラッシュなしなら presets/luts/<id>/<id>.cube、ありならプロジェクト相対(Windows / POSIX の区切り対応)。sections は false のときだけ該当段をバイパスする。OFF はプレビューと書き出しの両方でバイパスし、保存値は保持してよい。
|
|
83
|
+
|
|
84
|
+
33³、R fastest → G → B、各成分 toFixed(6) の後 Float32 に格納、適用時 trilinear。正規化は範囲 clamp、点列昇順ソート、省略既定の補完。検証器は不正な点列を修復せず拒否する。補間 span < 1e-9 では左点を使用。curves identity は旧実装どおり各差の絶対値 < 1e-5、hue identity は ≤ 1e-4。basic の演算上 identity 許容は 1e-6。
|
|
85
|
+
|
|
86
|
+
注: hue value=1.0 は式 (value-0.5)*2 により +360°(一周)、+180° は value=0.75。旧実装の式を優先する。
|
|
87
|
+
|
|
88
|
+
## 5. 消費側の約束
|
|
89
|
+
|
|
90
|
+
M2-1 時点で新 3 セクションはエンジン未消費(bake 関数は用意・plan 未配線)。2/4 便で配線。GPU / OSR と frame-engine preview は同じ全段 bake とバイパスを使う。DOM fallback は新 3 セクションを CSS 近似できないため適用せず、色調整は近似表示の指標を出す。frame-engine active 時は二重適用しない。台帳は adjust の path 単位であり本便では行を増やさない。
|
|
91
|
+
|
|
92
|
+
## 6. lint 分担
|
|
93
|
+
|
|
94
|
+
Schema は閉じた構造・型・範囲・点数・必須キーを検査。validate-edit / edit-store / edit-lint はさらに in / hue の狭義単調増加を検査する。依存ゼロの検証器は意図的重複。edit-lint は adjust.curves.* / adjust.wheels.* / adjust.hue.* の check id と正確な path を返す。
|
|
95
|
+
|
|
96
|
+
## 7. 書き込み側・パネル側の約束
|
|
97
|
+
|
|
98
|
+
全段 identity なら adjust 自体を除去する。読み取り側は identity object を許す。セクション OFF は保存値を失わず戻せる。isItemAdjustIdentity は OFF を考慮する実効判定であり、書き込み側が OFF の値を削除する指示ではない。ルック適用は basic / wheels を丸ごと置換し、lut / curves / hue は保持する。
|
|
99
|
+
|
|
100
|
+
## 8. 非スコープ
|
|
101
|
+
|
|
102
|
+
effects / vignette / スプリット比較、plan / compositor / page-builder / preview / inspector の配線、LUT ライブラリ。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.42",
|
|
4
4
|
"description": "AKARI Video launcher CLI — start an AI-edited video project from any directory: scaffold, connection check, then hand over to Claude Code (or opencode). AKARI Video を opencode や Claude Code で、どのディレクトリからでも始めるための `akari` ランチャー CLI。接続確認(doctor)→ 未セットアップならプロジェクト雛形を作成 → AI エージェントを起動する。外部 npm 依存ゼロ(Node.js 組み込みモジュールのみ)。 [akari-video npm vendor: bin/akari.mjs is reference-only. These CLI entrypoints are not included in the akari-video npm package. Use `akari doctor --json` and run the path reported in `render_cut.path`. Full installations provide it in a monorepo checkout, ~/.akari/app, /Applications/AKARI Video.app/Contents/Resources/packages, or %LOCALAPPDATA%\\Programs\\@akari-videoshell\\resources\\packages.]",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -1565,6 +1565,7 @@ function validateLook(value, findings, path) {
|
|
|
1565
1565
|
// docs/contract-2026-09-03-clip-adjust-v0.md。edit-lint は依存ゼロを保つため、schema と
|
|
1566
1566
|
// validate-edit.mjs の閉じた adjust 語彙をここでも独立に検証する。
|
|
1567
1567
|
function validateAdjust(value, findings, path) {
|
|
1568
|
+
validateAdjustV1Sections(value, findings, path);
|
|
1568
1569
|
if (!isRecord(value)) {
|
|
1569
1570
|
addFinding(findings, {
|
|
1570
1571
|
severity: "error", check: "adjust.structure", message: "adjust must be an object", path,
|
|
@@ -1577,12 +1578,12 @@ function validateAdjust(value, findings, path) {
|
|
|
1577
1578
|
addFinding(findings, {
|
|
1578
1579
|
severity: "error",
|
|
1579
1580
|
check: "adjust.unknown-key",
|
|
1580
|
-
message: `${key} is not defined by clip adjust
|
|
1581
|
+
message: `${key} is not defined by clip adjust v1`,
|
|
1581
1582
|
path: `${ownerPath}.${key}`,
|
|
1582
1583
|
});
|
|
1583
1584
|
}
|
|
1584
1585
|
};
|
|
1585
|
-
reportUnknownKeys(value, new Set(["basic", "lut", "sections"]), path);
|
|
1586
|
+
reportUnknownKeys(value, new Set(["basic", "lut", "sections", "curves", "wheels", "hue"]), path);
|
|
1586
1587
|
|
|
1587
1588
|
if (Object.hasOwn(value, "basic")) {
|
|
1588
1589
|
const basicPath = `${path}.basic`;
|
|
@@ -1644,7 +1645,7 @@ function validateAdjust(value, findings, path) {
|
|
|
1644
1645
|
severity: "error", check: "adjust.sections.structure", message: "sections must be an object", path: sectionsPath,
|
|
1645
1646
|
});
|
|
1646
1647
|
} else {
|
|
1647
|
-
const sectionKeys = new Set(["basic", "lut"]);
|
|
1648
|
+
const sectionKeys = new Set(["basic", "lut", "curves", "wheels", "hue"]);
|
|
1648
1649
|
reportUnknownKeys(value.sections, sectionKeys, sectionsPath);
|
|
1649
1650
|
for (const key of sectionKeys) {
|
|
1650
1651
|
if (Object.hasOwn(value.sections, key) && typeof value.sections[key] !== "boolean") {
|
|
@@ -6376,3 +6377,55 @@ function formatDb(value) {
|
|
|
6376
6377
|
function messageOf(error) {
|
|
6377
6378
|
return error instanceof Error ? error.message : String(error);
|
|
6378
6379
|
}
|
|
6380
|
+
|
|
6381
|
+
// Intentional dependency-free duplicate of the closed adjustV1 structure.
|
|
6382
|
+
function validateAdjustV1Sections(value, findings, path) {
|
|
6383
|
+
if (!isRecord(value)) return;
|
|
6384
|
+
const report = (section, check, at, message) => addFinding(findings, { severity: "error", check: "adjust." + section + "." + check, path: at, message });
|
|
6385
|
+
const object = (v, keys, section, at) => {
|
|
6386
|
+
if (!isRecord(v)) { report(section, 'structure', at, 'は object である必要があります'); return false; }
|
|
6387
|
+
for (const key of Object.keys(v)) if (!keys.includes(key)) report(section, 'unknown-key', at + '.' + key, 'は未知のキーです');
|
|
6388
|
+
return true;
|
|
6389
|
+
};
|
|
6390
|
+
const number = (v, min, max, section, at) => {
|
|
6391
|
+
if (!isFiniteNumber(v) || v < min || v > max) report(section, 'range', at, 'は ' + min + ' から ' + max + ' の範囲の有限数である必要があります');
|
|
6392
|
+
};
|
|
6393
|
+
for (const section of ['curves', 'hue']) {
|
|
6394
|
+
if (!Object.hasOwn(value, section)) continue;
|
|
6395
|
+
const channels = value[section], at = path + '.' + section;
|
|
6396
|
+
const axis = section === 'curves' ? 'in' : 'hue';
|
|
6397
|
+
const output = section === 'curves' ? 'out' : 'value';
|
|
6398
|
+
const minimum = section === 'curves' ? 2 : 1;
|
|
6399
|
+
const keys = section === 'curves' ? ['master', 'r', 'g', 'b'] : ['hue', 'sat', 'luma'];
|
|
6400
|
+
if (!object(channels, keys, section, at)) continue;
|
|
6401
|
+
for (const channel of keys) {
|
|
6402
|
+
if (!Object.hasOwn(channels, channel)) continue;
|
|
6403
|
+
const points = channels[channel], channelPath = at + '.' + channel;
|
|
6404
|
+
if (!Array.isArray(points) || points.length < minimum || points.length > 16) {
|
|
6405
|
+
report(section, 'points', channelPath, 'は ' + minimum + ' から 16 点の配列である必要があります'); continue;
|
|
6406
|
+
}
|
|
6407
|
+
let previous = -Infinity;
|
|
6408
|
+
for (const [index, point] of points.entries()) {
|
|
6409
|
+
const pointPath = channelPath + '[' + index + ']';
|
|
6410
|
+
if (!object(point, [axis, output], section, pointPath)) continue;
|
|
6411
|
+
number(point[axis], 0, 1, section, pointPath + '.' + axis);
|
|
6412
|
+
number(point[output], 0, 1, section, pointPath + '.' + output);
|
|
6413
|
+
if (isFiniteNumber(point[axis])) {
|
|
6414
|
+
if (point[axis] <= previous) report(section, 'order', pointPath + '.' + axis, 'は狭義単調増加である必要があります');
|
|
6415
|
+
previous = point[axis];
|
|
6416
|
+
}
|
|
6417
|
+
}
|
|
6418
|
+
}
|
|
6419
|
+
}
|
|
6420
|
+
if (Object.hasOwn(value, 'wheels')) {
|
|
6421
|
+
const ranges = { lift: 0.25, gamma: 0.5, gain: 0.5, offset: 0.1 };
|
|
6422
|
+
if (!object(value.wheels, Object.keys(ranges), 'wheels', path + '.wheels')) return;
|
|
6423
|
+
for (const [wheel, range] of Object.entries(ranges)) {
|
|
6424
|
+
if (!Object.hasOwn(value.wheels, wheel)) continue;
|
|
6425
|
+
const channels = value.wheels[wheel], at = path + '.wheels.' + wheel;
|
|
6426
|
+
if (!object(channels, ['r', 'g', 'b'], 'wheels', at)) continue;
|
|
6427
|
+
for (const channel of ['r', 'g', 'b']) if (Object.hasOwn(channels, channel)) number(channels[channel], -range, range, 'wheels', at + '.' + channel);
|
|
6428
|
+
}
|
|
6429
|
+
}
|
|
6430
|
+
}
|
|
6431
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AdjustCurvesV1, AdjustWheelsV1, AdjustHueCurvesV1 } from './edit-v2';
|
|
2
|
+
export interface AdjustCssBasic {
|
|
3
|
+
exposure?: number;
|
|
4
|
+
contrast?: number;
|
|
5
|
+
highlights?: number;
|
|
6
|
+
shadows?: number;
|
|
7
|
+
blacks?: number;
|
|
8
|
+
whites?: number;
|
|
9
|
+
temperature?: number;
|
|
10
|
+
tint?: number;
|
|
11
|
+
vibrance?: number;
|
|
12
|
+
saturation?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface AdjustCssInput {
|
|
15
|
+
basic?: AdjustCssBasic | null;
|
|
16
|
+
curves?: AdjustCurvesV1;
|
|
17
|
+
wheels?: AdjustWheelsV1;
|
|
18
|
+
hue?: AdjustHueCurvesV1;
|
|
19
|
+
sections?: {
|
|
20
|
+
basic?: boolean;
|
|
21
|
+
lut?: boolean;
|
|
22
|
+
curves?: boolean;
|
|
23
|
+
wheels?: boolean;
|
|
24
|
+
hue?: boolean;
|
|
25
|
+
} | null;
|
|
26
|
+
}
|
|
27
|
+
export interface AdjustCssVisual {
|
|
28
|
+
filter: string;
|
|
29
|
+
/** True when active controls contain values that CSS filters cannot reproduce. */
|
|
30
|
+
hasApproximation: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolves an item's basic colour adjustment and an optional transition filter into the single
|
|
34
|
+
* CSS `filter` seat used by DOM preview media. Returns null only when neither input owns that
|
|
35
|
+
* seat. A basic section with all-zero values deliberately returns `{ filter: '' }`, allowing an
|
|
36
|
+
* edit refresh to clear a previously active adjustment without inventing a CSS no-op.
|
|
37
|
+
*/
|
|
38
|
+
export declare function computeAdjustCssVisual(adjust: AdjustCssInput | null | undefined, transitionFilter?: string | null): AdjustCssVisual | null;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.computeAdjustCssVisual = computeAdjustCssVisual;
|
|
4
|
+
/**
|
|
5
|
+
* Resolves an item's basic colour adjustment and an optional transition filter into the single
|
|
6
|
+
* CSS `filter` seat used by DOM preview media. Returns null only when neither input owns that
|
|
7
|
+
* seat. A basic section with all-zero values deliberately returns `{ filter: '' }`, allowing an
|
|
8
|
+
* edit refresh to clear a previously active adjustment without inventing a CSS no-op.
|
|
9
|
+
*/
|
|
10
|
+
function computeAdjustCssVisual(adjust, transitionFilter) {
|
|
11
|
+
const source = adjust && typeof adjust === 'object' && !Array.isArray(adjust) ? adjust : null;
|
|
12
|
+
const rawBasic = source && source.sections?.basic !== false ? source.basic : null;
|
|
13
|
+
const basic = rawBasic && typeof rawBasic === 'object' && !Array.isArray(rawBasic) ? rawBasic : null;
|
|
14
|
+
const rawTransition = typeof transitionFilter === 'string' ? transitionFilter.trim() : '';
|
|
15
|
+
const transition = rawTransition === 'none' ? '' : rawTransition;
|
|
16
|
+
// Match kernel normalization and identity tolerances without a runtime dependency.
|
|
17
|
+
const clamp01 = (value) => Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0;
|
|
18
|
+
const hasWheels = source?.sections?.wheels !== false
|
|
19
|
+
&& ['lift', 'gamma', 'gain', 'offset'].some(wheel => ['r', 'g', 'b'].some(channel => {
|
|
20
|
+
const value = source?.wheels?.[wheel]?.[channel];
|
|
21
|
+
return Number.isFinite(value) && value !== 0;
|
|
22
|
+
}));
|
|
23
|
+
const hasCurves = source?.sections?.curves !== false
|
|
24
|
+
&& ['master', 'r', 'g', 'b'].some(channel => {
|
|
25
|
+
const raw = source?.curves?.[channel];
|
|
26
|
+
if (raw == null)
|
|
27
|
+
return false;
|
|
28
|
+
const points = raw.map(point => ({ in: clamp01(point.in), out: clamp01(point.out) }))
|
|
29
|
+
.sort((a, b) => a.in - b.in);
|
|
30
|
+
return !(points.length === 2
|
|
31
|
+
&& Math.abs(points[0].in) < 1e-5 && Math.abs(points[0].out) < 1e-5
|
|
32
|
+
&& Math.abs(points[1].in - 1) < 1e-5 && Math.abs(points[1].out - 1) < 1e-5);
|
|
33
|
+
});
|
|
34
|
+
const hasHue = source?.sections?.hue !== false
|
|
35
|
+
&& ['hue', 'sat', 'luma'].some(channel => (source?.hue?.[channel] ?? []).some(point => Math.abs((Number.isFinite(point.value) ? clamp01(point.value) : 0.5) - 0.5) > 1e-4));
|
|
36
|
+
const hasUnsupportedSection = hasWheels || hasCurves || hasHue;
|
|
37
|
+
if (!basic && !transition && !hasUnsupportedSection)
|
|
38
|
+
return null;
|
|
39
|
+
const exposure = basic && Number.isFinite(basic.exposure) ? basic.exposure : 0;
|
|
40
|
+
const contrast = basic && Number.isFinite(basic.contrast) ? basic.contrast : 0;
|
|
41
|
+
const saturation = basic && Number.isFinite(basic.saturation) ? basic.saturation : 0;
|
|
42
|
+
const temperature = basic && Number.isFinite(basic.temperature) ? basic.temperature : 0;
|
|
43
|
+
const parts = [];
|
|
44
|
+
if (Math.abs(exposure) > 0.005) {
|
|
45
|
+
parts.push('brightness(' + Math.pow(2, exposure).toFixed(2) + ')');
|
|
46
|
+
}
|
|
47
|
+
if (Math.abs(contrast) > 0.005) {
|
|
48
|
+
parts.push('contrast(' + (1 + contrast).toFixed(2) + ')');
|
|
49
|
+
}
|
|
50
|
+
if (Math.abs(saturation) > 0.005) {
|
|
51
|
+
parts.push('saturate(' + (1 + saturation).toFixed(2) + ')');
|
|
52
|
+
}
|
|
53
|
+
if (temperature > 0.005) {
|
|
54
|
+
parts.push('sepia(' + (temperature * 0.3).toFixed(2) + ')');
|
|
55
|
+
}
|
|
56
|
+
else if (temperature < -0.005) {
|
|
57
|
+
parts.push('hue-rotate(' + (-temperature * 20).toFixed(0) + 'deg)');
|
|
58
|
+
}
|
|
59
|
+
if (transition)
|
|
60
|
+
parts.push(transition);
|
|
61
|
+
const unsupportedKeys = ['tint', 'highlights', 'shadows', 'blacks', 'whites', 'vibrance'];
|
|
62
|
+
const hasApproximation = hasUnsupportedSection || Boolean(basic) && unsupportedKeys.some(key => {
|
|
63
|
+
const value = basic?.[key];
|
|
64
|
+
return Number.isFinite(value) && value !== 0;
|
|
65
|
+
});
|
|
66
|
+
return { filter: parts.join(' '), hasApproximation };
|
|
67
|
+
}
|
|
@@ -180,14 +180,52 @@ export interface AdjustLutV0 {
|
|
|
180
180
|
lut: string;
|
|
181
181
|
intensity?: number;
|
|
182
182
|
}
|
|
183
|
-
export interface
|
|
183
|
+
export interface AdjustCurvePointV1 {
|
|
184
|
+
in: number;
|
|
185
|
+
out: number;
|
|
186
|
+
}
|
|
187
|
+
export interface AdjustCurvesV1 {
|
|
188
|
+
master?: AdjustCurvePointV1[];
|
|
189
|
+
r?: AdjustCurvePointV1[];
|
|
190
|
+
g?: AdjustCurvePointV1[];
|
|
191
|
+
b?: AdjustCurvePointV1[];
|
|
192
|
+
}
|
|
193
|
+
export interface AdjustWheelV1 {
|
|
194
|
+
r?: number;
|
|
195
|
+
g?: number;
|
|
196
|
+
b?: number;
|
|
197
|
+
}
|
|
198
|
+
export interface AdjustWheelsV1 {
|
|
199
|
+
lift?: AdjustWheelV1;
|
|
200
|
+
gamma?: AdjustWheelV1;
|
|
201
|
+
gain?: AdjustWheelV1;
|
|
202
|
+
offset?: AdjustWheelV1;
|
|
203
|
+
}
|
|
204
|
+
export interface AdjustHuePointV1 {
|
|
205
|
+
hue: number;
|
|
206
|
+
value: number;
|
|
207
|
+
}
|
|
208
|
+
export interface AdjustHueCurvesV1 {
|
|
209
|
+
hue?: AdjustHuePointV1[];
|
|
210
|
+
sat?: AdjustHuePointV1[];
|
|
211
|
+
luma?: AdjustHuePointV1[];
|
|
212
|
+
}
|
|
213
|
+
export interface AdjustV1 {
|
|
184
214
|
basic?: AdjustBasicV0;
|
|
185
215
|
lut?: AdjustLutV0 | null;
|
|
216
|
+
curves?: AdjustCurvesV1;
|
|
217
|
+
wheels?: AdjustWheelsV1;
|
|
218
|
+
hue?: AdjustHueCurvesV1;
|
|
186
219
|
sections?: {
|
|
187
220
|
basic?: boolean;
|
|
188
221
|
lut?: boolean;
|
|
222
|
+
curves?: boolean;
|
|
223
|
+
wheels?: boolean;
|
|
224
|
+
hue?: boolean;
|
|
189
225
|
};
|
|
190
226
|
}
|
|
227
|
+
/** @deprecated Use AdjustV1. */
|
|
228
|
+
export type AdjustV0 = AdjustV1;
|
|
191
229
|
export interface ItemV2Base {
|
|
192
230
|
id: string;
|
|
193
231
|
name?: string;
|
|
@@ -210,7 +248,7 @@ export interface ItemV2Base {
|
|
|
210
248
|
opacity?: number;
|
|
211
249
|
blend?: BlendModeV2;
|
|
212
250
|
crop?: CropV2;
|
|
213
|
-
adjust?:
|
|
251
|
+
adjust?: AdjustV1;
|
|
214
252
|
perspective?: Record<string, unknown>;
|
|
215
253
|
motion?: MotionV0;
|
|
216
254
|
animator?: AnimatorV0[];
|
|
@@ -461,7 +461,47 @@ function validateCrop(value, path) {
|
|
|
461
461
|
}
|
|
462
462
|
function validateAdjust(value, path) {
|
|
463
463
|
requireRecord(value, path);
|
|
464
|
-
requireExactKeys(value, new Set(['basic', 'lut', 'sections']), path);
|
|
464
|
+
requireExactKeys(value, new Set(['basic', 'lut', 'sections', 'curves', 'wheels', 'hue']), path);
|
|
465
|
+
for (const section of ['curves', 'hue']) {
|
|
466
|
+
if (!hasOwn(value, section))
|
|
467
|
+
continue;
|
|
468
|
+
const channels = value[section];
|
|
469
|
+
const sectionPath = `${path}.${section}`;
|
|
470
|
+
requireRecord(channels, sectionPath);
|
|
471
|
+
const axis = section === 'curves' ? 'in' : 'hue';
|
|
472
|
+
const output = section === 'curves' ? 'out' : 'value';
|
|
473
|
+
const minimum = section === 'curves' ? 2 : 1;
|
|
474
|
+
requireExactKeys(channels, new Set(section === 'curves' ? ['master', 'r', 'g', 'b'] : ['hue', 'sat', 'luma']), sectionPath);
|
|
475
|
+
for (const [channel, points] of Object.entries(channels)) {
|
|
476
|
+
const channelPath = `${sectionPath}.${channel}`;
|
|
477
|
+
if (!Array.isArray(points) || points.length < minimum || points.length > 16) {
|
|
478
|
+
throw invalid(channelPath, `${minimum} から 16 点の配列である必要があります`);
|
|
479
|
+
}
|
|
480
|
+
let previous = -Infinity;
|
|
481
|
+
for (const [index, point] of points.entries()) {
|
|
482
|
+
const pointPath = `${channelPath}[${index}]`;
|
|
483
|
+
requireRecord(point, pointPath);
|
|
484
|
+
requireExactKeys(point, new Set([axis, output]), pointPath);
|
|
485
|
+
requireRange(point[axis], 0, 1, `${pointPath}.${axis}`);
|
|
486
|
+
requireRange(point[output], 0, 1, `${pointPath}.${output}`);
|
|
487
|
+
if (point[axis] <= previous)
|
|
488
|
+
throw invalid(`${pointPath}.${axis}`, '狭義単調増加である必要があります');
|
|
489
|
+
previous = point[axis];
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (hasOwn(value, 'wheels')) {
|
|
494
|
+
requireRecord(value.wheels, `${path}.wheels`);
|
|
495
|
+
const ranges = { lift: 0.25, gamma: 0.5, gain: 0.5, offset: 0.1 };
|
|
496
|
+
requireExactKeys(value.wheels, new Set(Object.keys(ranges)), `${path}.wheels`);
|
|
497
|
+
for (const [wheel, channels] of Object.entries(value.wheels)) {
|
|
498
|
+
const wheelPath = `${path}.wheels.${wheel}`;
|
|
499
|
+
requireRecord(channels, wheelPath);
|
|
500
|
+
requireExactKeys(channels, new Set(['r', 'g', 'b']), wheelPath);
|
|
501
|
+
for (const [channel, amount] of Object.entries(channels))
|
|
502
|
+
requireRange(amount, -ranges[wheel], ranges[wheel], `${wheelPath}.${channel}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
465
505
|
if (hasOwn(value, 'basic')) {
|
|
466
506
|
requireRecord(value.basic, `${path}.basic`);
|
|
467
507
|
const basicKeys = new Set([
|
|
@@ -485,7 +525,7 @@ function validateAdjust(value, path) {
|
|
|
485
525
|
}
|
|
486
526
|
if (hasOwn(value, 'sections')) {
|
|
487
527
|
requireRecord(value.sections, `${path}.sections`);
|
|
488
|
-
const sectionKeys = new Set(['basic', 'lut']);
|
|
528
|
+
const sectionKeys = new Set(['basic', 'lut', 'curves', 'wheels', 'hue']);
|
|
489
529
|
requireExactKeys(value.sections, sectionKeys, `${path}.sections`);
|
|
490
530
|
for (const key of sectionKeys) {
|
|
491
531
|
if (hasOwn(value.sections, key) && typeof value.sections[key] !== 'boolean') {
|
|
@@ -29,6 +29,7 @@ __exportStar(require("./caption-window"), exports);
|
|
|
29
29
|
__exportStar(require("./caption-clock"), exports);
|
|
30
30
|
__exportStar(require("./timeline-map"), exports);
|
|
31
31
|
__exportStar(require("./caption-display"), exports);
|
|
32
|
+
// Includes AdjustV1 and its curve/wheel/hue types; AdjustV0 remains an alias.
|
|
32
33
|
__exportStar(require("./edit-v2"), exports);
|
|
33
34
|
__exportStar(require("./edit-v2-item-write"), exports);
|
|
34
35
|
__exportStar(require("./internal-model"), exports);
|
|
@@ -56,3 +57,4 @@ var legacy_parse_1 = require("./migrate/legacy-parse");
|
|
|
56
57
|
Object.defineProperty(exports, "parseEdit", { enumerable: true, get: function () { return legacy_parse_1.parseEdit; } });
|
|
57
58
|
var error_1 = require("./migrate/error");
|
|
58
59
|
Object.defineProperty(exports, "LegacyEditVersionError", { enumerable: true, get: function () { return error_1.LegacyEditVersionError; } });
|
|
60
|
+
__exportStar(require("./adjust-css-visual"), exports);
|
|
@@ -37,6 +37,7 @@ var AkariEditKernel = (() => {
|
|
|
37
37
|
captionClockDomainOf: () => captionClockDomainOf,
|
|
38
38
|
captionWindowSeconds: () => captionWindowSeconds,
|
|
39
39
|
composeEnvelopesDb: () => composeEnvelopesDb,
|
|
40
|
+
computeAdjustCssVisual: () => computeAdjustCssVisual,
|
|
40
41
|
computeDuckEnvelope: () => computeDuckEnvelope,
|
|
41
42
|
computeDuckIntervals: () => computeDuckIntervals,
|
|
42
43
|
computeTransitionVisual: () => computeTransitionVisual,
|
|
@@ -2153,7 +2154,44 @@ var AkariEditKernel = (() => {
|
|
|
2153
2154
|
}
|
|
2154
2155
|
function validateAdjust(value, path) {
|
|
2155
2156
|
requireRecord(value, path);
|
|
2156
|
-
requireExactKeys(value, /* @__PURE__ */ new Set(["basic", "lut", "sections"]), path);
|
|
2157
|
+
requireExactKeys(value, /* @__PURE__ */ new Set(["basic", "lut", "sections", "curves", "wheels", "hue"]), path);
|
|
2158
|
+
for (const section of ["curves", "hue"]) {
|
|
2159
|
+
if (!hasOwn(value, section)) continue;
|
|
2160
|
+
const channels = value[section];
|
|
2161
|
+
const sectionPath = `${path}.${section}`;
|
|
2162
|
+
requireRecord(channels, sectionPath);
|
|
2163
|
+
const axis = section === "curves" ? "in" : "hue";
|
|
2164
|
+
const output = section === "curves" ? "out" : "value";
|
|
2165
|
+
const minimum = section === "curves" ? 2 : 1;
|
|
2166
|
+
requireExactKeys(channels, new Set(section === "curves" ? ["master", "r", "g", "b"] : ["hue", "sat", "luma"]), sectionPath);
|
|
2167
|
+
for (const [channel, points] of Object.entries(channels)) {
|
|
2168
|
+
const channelPath = `${sectionPath}.${channel}`;
|
|
2169
|
+
if (!Array.isArray(points) || points.length < minimum || points.length > 16) {
|
|
2170
|
+
throw invalid(channelPath, `${minimum} \u304B\u3089 16 \u70B9\u306E\u914D\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059`);
|
|
2171
|
+
}
|
|
2172
|
+
let previous = -Infinity;
|
|
2173
|
+
for (const [index, point] of points.entries()) {
|
|
2174
|
+
const pointPath = `${channelPath}[${index}]`;
|
|
2175
|
+
requireRecord(point, pointPath);
|
|
2176
|
+
requireExactKeys(point, /* @__PURE__ */ new Set([axis, output]), pointPath);
|
|
2177
|
+
requireRange(point[axis], 0, 1, `${pointPath}.${axis}`);
|
|
2178
|
+
requireRange(point[output], 0, 1, `${pointPath}.${output}`);
|
|
2179
|
+
if (point[axis] <= previous) throw invalid(`${pointPath}.${axis}`, "\u72ED\u7FA9\u5358\u8ABF\u5897\u52A0\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059");
|
|
2180
|
+
previous = point[axis];
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
if (hasOwn(value, "wheels")) {
|
|
2185
|
+
requireRecord(value.wheels, `${path}.wheels`);
|
|
2186
|
+
const ranges = { lift: 0.25, gamma: 0.5, gain: 0.5, offset: 0.1 };
|
|
2187
|
+
requireExactKeys(value.wheels, new Set(Object.keys(ranges)), `${path}.wheels`);
|
|
2188
|
+
for (const [wheel, channels] of Object.entries(value.wheels)) {
|
|
2189
|
+
const wheelPath = `${path}.wheels.${wheel}`;
|
|
2190
|
+
requireRecord(channels, wheelPath);
|
|
2191
|
+
requireExactKeys(channels, /* @__PURE__ */ new Set(["r", "g", "b"]), wheelPath);
|
|
2192
|
+
for (const [channel, amount] of Object.entries(channels)) requireRange(amount, -ranges[wheel], ranges[wheel], `${wheelPath}.${channel}`);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2157
2195
|
if (hasOwn(value, "basic")) {
|
|
2158
2196
|
requireRecord(value.basic, `${path}.basic`);
|
|
2159
2197
|
const basicKeys = /* @__PURE__ */ new Set([
|
|
@@ -2183,7 +2221,7 @@ var AkariEditKernel = (() => {
|
|
|
2183
2221
|
}
|
|
2184
2222
|
if (hasOwn(value, "sections")) {
|
|
2185
2223
|
requireRecord(value.sections, `${path}.sections`);
|
|
2186
|
-
const sectionKeys = /* @__PURE__ */ new Set(["basic", "lut"]);
|
|
2224
|
+
const sectionKeys = /* @__PURE__ */ new Set(["basic", "lut", "curves", "wheels", "hue"]);
|
|
2187
2225
|
requireExactKeys(value.sections, sectionKeys, `${path}.sections`);
|
|
2188
2226
|
for (const key of sectionKeys) {
|
|
2189
2227
|
if (hasOwn(value.sections, key) && typeof value.sections[key] !== "boolean") {
|
|
@@ -3630,6 +3668,55 @@ var AkariEditKernel = (() => {
|
|
|
3630
3668
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3631
3669
|
}
|
|
3632
3670
|
|
|
3671
|
+
// src/adjust-css-visual.ts
|
|
3672
|
+
function computeAdjustCssVisual(adjust, transitionFilter) {
|
|
3673
|
+
const source = adjust && typeof adjust === "object" && !Array.isArray(adjust) ? adjust : null;
|
|
3674
|
+
const rawBasic = source && source.sections?.basic !== false ? source.basic : null;
|
|
3675
|
+
const basic = rawBasic && typeof rawBasic === "object" && !Array.isArray(rawBasic) ? rawBasic : null;
|
|
3676
|
+
const rawTransition = typeof transitionFilter === "string" ? transitionFilter.trim() : "";
|
|
3677
|
+
const transition = rawTransition === "none" ? "" : rawTransition;
|
|
3678
|
+
const clamp01 = (value) => Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0;
|
|
3679
|
+
const hasWheels = source?.sections?.wheels !== false && ["lift", "gamma", "gain", "offset"].some((wheel) => ["r", "g", "b"].some((channel) => {
|
|
3680
|
+
const value = source?.wheels?.[wheel]?.[channel];
|
|
3681
|
+
return Number.isFinite(value) && value !== 0;
|
|
3682
|
+
}));
|
|
3683
|
+
const hasCurves = source?.sections?.curves !== false && ["master", "r", "g", "b"].some((channel) => {
|
|
3684
|
+
const raw = source?.curves?.[channel];
|
|
3685
|
+
if (raw == null) return false;
|
|
3686
|
+
const points = raw.map((point) => ({ in: clamp01(point.in), out: clamp01(point.out) })).sort((a, b) => a.in - b.in);
|
|
3687
|
+
return !(points.length === 2 && Math.abs(points[0].in) < 1e-5 && Math.abs(points[0].out) < 1e-5 && Math.abs(points[1].in - 1) < 1e-5 && Math.abs(points[1].out - 1) < 1e-5);
|
|
3688
|
+
});
|
|
3689
|
+
const hasHue = source?.sections?.hue !== false && ["hue", "sat", "luma"].some((channel) => (source?.hue?.[channel] ?? []).some((point) => Math.abs((Number.isFinite(point.value) ? clamp01(point.value) : 0.5) - 0.5) > 1e-4));
|
|
3690
|
+
const hasUnsupportedSection = hasWheels || hasCurves || hasHue;
|
|
3691
|
+
if (!basic && !transition && !hasUnsupportedSection) return null;
|
|
3692
|
+
const exposure = basic && Number.isFinite(basic.exposure) ? basic.exposure : 0;
|
|
3693
|
+
const contrast = basic && Number.isFinite(basic.contrast) ? basic.contrast : 0;
|
|
3694
|
+
const saturation = basic && Number.isFinite(basic.saturation) ? basic.saturation : 0;
|
|
3695
|
+
const temperature = basic && Number.isFinite(basic.temperature) ? basic.temperature : 0;
|
|
3696
|
+
const parts = [];
|
|
3697
|
+
if (Math.abs(exposure) > 5e-3) {
|
|
3698
|
+
parts.push("brightness(" + Math.pow(2, exposure).toFixed(2) + ")");
|
|
3699
|
+
}
|
|
3700
|
+
if (Math.abs(contrast) > 5e-3) {
|
|
3701
|
+
parts.push("contrast(" + (1 + contrast).toFixed(2) + ")");
|
|
3702
|
+
}
|
|
3703
|
+
if (Math.abs(saturation) > 5e-3) {
|
|
3704
|
+
parts.push("saturate(" + (1 + saturation).toFixed(2) + ")");
|
|
3705
|
+
}
|
|
3706
|
+
if (temperature > 5e-3) {
|
|
3707
|
+
parts.push("sepia(" + (temperature * 0.3).toFixed(2) + ")");
|
|
3708
|
+
} else if (temperature < -5e-3) {
|
|
3709
|
+
parts.push("hue-rotate(" + (-temperature * 20).toFixed(0) + "deg)");
|
|
3710
|
+
}
|
|
3711
|
+
if (transition) parts.push(transition);
|
|
3712
|
+
const unsupportedKeys = ["tint", "highlights", "shadows", "blacks", "whites", "vibrance"];
|
|
3713
|
+
const hasApproximation = hasUnsupportedSection || Boolean(basic) && unsupportedKeys.some((key) => {
|
|
3714
|
+
const value = basic?.[key];
|
|
3715
|
+
return Number.isFinite(value) && value !== 0;
|
|
3716
|
+
});
|
|
3717
|
+
return { filter: parts.join(" "), hasApproximation };
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3633
3720
|
// src/webview-kernel.ts
|
|
3634
3721
|
function findActiveResolvedCaption(cues, outputTime) {
|
|
3635
3722
|
return cues.find((cue) => cue.start <= outputTime && outputTime < cue.end);
|