akari-video 0.1.90 → 0.1.91
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/vendor/packages/akari-launcher/package.json +1 -1
- package/vendor/packages/edit-lint/src/edit-lint.mjs +12 -4
- package/vendor/packages/edit-store/lib/caption-display.js +58 -13
- package/vendor/packages/edit-store/lib/caption-store.d.ts +3 -0
- package/vendor/packages/edit-store/lib/caption-store.js +10 -1
- package/vendor/packages/edit-store/lib/webview-kernel.js +48 -9
- package/vendor/packages/overlay-runtime/src/handle-geometry.js +67 -22
- package/vendor/packages/overlay-runtime/src/interaction.js +43 -5
- package/vendor/packages/schemas/bin/validate-captions.mjs +11 -1
- package/vendor/packages/schemas/captions.schema.json +3 -0
- package/vendor/packages/schemas/test/caption-context-style-v2.test.mjs +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.91",
|
|
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": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akari-video",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.91",
|
|
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": [
|
|
@@ -2596,7 +2596,8 @@ async function validateOverlays(overlays, timeline, findings, paths) {
|
|
|
2596
2596
|
});
|
|
2597
2597
|
}
|
|
2598
2598
|
if (!isNonEmptyString(overlay.html)) continue;
|
|
2599
|
-
const
|
|
2599
|
+
const htmlBinding = resolveReferenceBinding(paths.editPath, overlay.html, paths);
|
|
2600
|
+
const htmlPath = htmlBinding.path;
|
|
2600
2601
|
const isHtmlFile = await isRegularFile(htmlPath);
|
|
2601
2602
|
// overlay.html は file 参照(相対パス)とインライン HTML の両方をとりうる。参照でなければ
|
|
2602
2603
|
// フィールドの値そのものを断片本文として扱う(inspectHtmlFragment 以降のルート要素検証は
|
|
@@ -2626,9 +2627,16 @@ async function validateOverlays(overlays, timeline, findings, paths) {
|
|
|
2626
2627
|
});
|
|
2627
2628
|
continue;
|
|
2628
2629
|
}
|
|
2630
|
+
// 共有ライブラリ参照(.akari/asset-references.json 経由で原本を読む)の断片は、ライブラリが
|
|
2631
|
+
// data-start="0" data-duration="<素材の長さ>" で配っていて、プロジェクト側からは書き換えられない。
|
|
2632
|
+
// 置いた時刻・長さの写しを作る取り込み(placedFragmentCopy)も参照では走らないため、ここで
|
|
2633
|
+
// 検査すると置いた時点で必ず不一致になり書き出しが止まる。ランタイムは edit.json から作る
|
|
2634
|
+
// .akari-overlay-container の値だけを使うので、原本ルートの値は検査しない。
|
|
2635
|
+
const libraryFragment = htmlBinding.scope === "library";
|
|
2629
2636
|
if (
|
|
2630
|
-
|
|
2631
|
-
|
|
2637
|
+
!libraryFragment
|
|
2638
|
+
&& (Object.hasOwn(fragment.rootAttributes, "data-start")
|
|
2639
|
+
|| Object.hasOwn(fragment.rootAttributes, "data-duration"))
|
|
2632
2640
|
) {
|
|
2633
2641
|
addFinding(findings, {
|
|
2634
2642
|
severity: "warning",
|
|
@@ -2659,7 +2667,7 @@ async function validateOverlays(overlays, timeline, findings, paths) {
|
|
|
2659
2667
|
}
|
|
2660
2668
|
}
|
|
2661
2669
|
|
|
2662
|
-
for (const [attribute, expected] of [
|
|
2670
|
+
for (const [attribute, expected] of libraryFragment ? [] : [
|
|
2663
2671
|
["data-start", overlay.start],
|
|
2664
2672
|
["data-duration", overlay.duration],
|
|
2665
2673
|
]) {
|
|
@@ -45,7 +45,7 @@ exports.CAPTION_UNIT_METRIC = 'ascii-half-other-one-v1';
|
|
|
45
45
|
// (legacy 専用キーを display_policy 側が使うと単に無視される — 不正ではない)。
|
|
46
46
|
const CAPTION_STYLE_KEYS = new Set([
|
|
47
47
|
'color', 'size_px', 'font_weight', 'line_height', 'stroke', 'background', 'zone', 'layout',
|
|
48
|
-
'font_family', 'weight', 'italic', 'underline', 'letter_spacing_em', 'align',
|
|
48
|
+
'font_family', 'weight', 'italic', 'underline', 'strikethrough', 'list', 'opacity', 'letter_spacing_em', 'align',
|
|
49
49
|
'vertical_align', 'vertical', 'text_transform', 'max_width_pct', 'wrap_width_pct', 'max_characters', 'text_anchor',
|
|
50
50
|
'position', 'scale', 'rotate', 'shadow', 'glow', 'animation', 'reference_height_px'
|
|
51
51
|
]);
|
|
@@ -712,6 +712,9 @@ function validateTextStyleV0(value, label) {
|
|
|
712
712
|
&& (!Number.isInteger(value.weight) || value.weight < 100 || value.weight > 900), 'weight must be an integer within [100, 900]');
|
|
713
713
|
failIf(has('italic') && typeof value.italic !== 'boolean', 'italic must be a boolean');
|
|
714
714
|
failIf(has('underline') && typeof value.underline !== 'boolean', 'underline must be a boolean');
|
|
715
|
+
failIf(has('strikethrough') && typeof value.strikethrough !== 'boolean', 'strikethrough must be a boolean');
|
|
716
|
+
failIf(has('list') && value.list !== 'bullet' && value.list !== null, 'list must be bullet or null');
|
|
717
|
+
failIf(has('opacity') && (!finiteNumber(value.opacity) || value.opacity < 0 || value.opacity > 1), 'opacity must be within [0, 1]');
|
|
715
718
|
failIf(has('letter_spacing_em') && !finiteNumber(value.letter_spacing_em), 'letter_spacing_em must be a finite number');
|
|
716
719
|
failIf(has('align') && !CAPTION_ALIGN_VALUES.has(value.align), 'align must be one of left, center, right');
|
|
717
720
|
failIf(has('vertical_align') && !CAPTION_VERTICAL_ALIGN_VALUES.has(value.vertical_align), 'vertical_align must be one of top, middle, bottom');
|
|
@@ -1421,13 +1424,16 @@ function normalizeCaptionLineTextStyle(value) {
|
|
|
1421
1424
|
? { weight: value.weight }
|
|
1422
1425
|
: Number.isInteger(value.font_weight) && value.font_weight >= 1 && value.font_weight <= 1000
|
|
1423
1426
|
? { weight: value.font_weight } : {}),
|
|
1424
|
-
...(value.italic ===
|
|
1425
|
-
...(value.underline ===
|
|
1427
|
+
...(typeof value.italic === 'boolean' ? { italic: value.italic } : {}),
|
|
1428
|
+
...(typeof value.underline === 'boolean' ? { underline: value.underline } : {}),
|
|
1429
|
+
...(typeof value.strikethrough === 'boolean' ? { strikethrough: value.strikethrough } : {}),
|
|
1430
|
+
...(value.list === 'bullet' || value.list === null ? { list: value.list } : {}),
|
|
1431
|
+
...(finiteNumber(value.opacity) && value.opacity >= 0 && value.opacity <= 1 ? { opacity: value.opacity } : {}),
|
|
1426
1432
|
...(finiteNumber(value.letter_spacing_em) ? { letter_spacing_em: value.letter_spacing_em } : {}),
|
|
1427
1433
|
...(finitePositive(value.line_height) ? { line_height: value.line_height } : {}),
|
|
1428
1434
|
...(CAPTION_ALIGN_VALUES.has(value.align) ? { align: value.align } : {}),
|
|
1429
1435
|
...(CAPTION_VERTICAL_ALIGN_VALUES.has(value.vertical_align) ? { vertical_align: value.vertical_align } : {}),
|
|
1430
|
-
...(value.vertical ===
|
|
1436
|
+
...(typeof value.vertical === 'boolean' ? { vertical: value.vertical } : {}),
|
|
1431
1437
|
...(CAPTION_TEXT_TRANSFORM_MAP[value.text_transform]
|
|
1432
1438
|
? { text_transform: CAPTION_TEXT_TRANSFORM_MAP[value.text_transform] } : {}),
|
|
1433
1439
|
...(finiteNumber(value.max_width_pct) && value.max_width_pct > 0 && value.max_width_pct < 100
|
|
@@ -1623,6 +1629,11 @@ function captionAnchorPositionVars(anchorValue, positionValue, verticalAlignValu
|
|
|
1623
1629
|
}
|
|
1624
1630
|
return vars;
|
|
1625
1631
|
}
|
|
1632
|
+
function cssCaptionFontFamily(value) {
|
|
1633
|
+
if (value.includes(',') || /^(['"]).*\1$/s.test(value.trim()))
|
|
1634
|
+
return value;
|
|
1635
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
1636
|
+
}
|
|
1626
1637
|
function resolveCaptionLineStyleVarsAtScale(style, scale) {
|
|
1627
1638
|
const vars = {};
|
|
1628
1639
|
const px = (value) => scaleCaptionPx(value, scale);
|
|
@@ -1651,15 +1662,20 @@ function resolveCaptionLineStyleVarsAtScale(style, scale) {
|
|
|
1651
1662
|
vars[name] = `${px(style.background.radius_px)}px`;
|
|
1652
1663
|
}
|
|
1653
1664
|
if (typeof style.font_family === 'string')
|
|
1654
|
-
vars['--caption-font-family'] = style.font_family;
|
|
1665
|
+
vars['--caption-font-family'] = cssCaptionFontFamily(style.font_family);
|
|
1655
1666
|
if (finiteNumber(style.weight))
|
|
1656
1667
|
vars['--caption-font-weight'] = String(style.weight);
|
|
1657
1668
|
else if (Number.isInteger(style.font_weight))
|
|
1658
1669
|
vars['--caption-font-weight'] = String(style.font_weight);
|
|
1659
1670
|
if (style.italic)
|
|
1660
1671
|
vars['--caption-font-style'] = 'italic';
|
|
1661
|
-
if (style.underline)
|
|
1662
|
-
vars['--caption-text-decoration'] =
|
|
1672
|
+
if (style.underline || style.strikethrough)
|
|
1673
|
+
vars['--caption-text-decoration'] =
|
|
1674
|
+
[style.underline ? 'underline' : '', style.strikethrough ? 'line-through' : ''].filter(Boolean).join(' ');
|
|
1675
|
+
if (style.list === 'bullet')
|
|
1676
|
+
vars['--caption-list-display'] = 'list-item';
|
|
1677
|
+
if (finiteNumber(style.opacity))
|
|
1678
|
+
vars['--caption-opacity'] = String(style.opacity);
|
|
1663
1679
|
if (finiteNumber(style.letter_spacing_em))
|
|
1664
1680
|
vars['--caption-letter-spacing'] = `${style.letter_spacing_em}em`;
|
|
1665
1681
|
if (finiteNumber(style.line_height))
|
|
@@ -1671,8 +1687,13 @@ function resolveCaptionLineStyleVarsAtScale(style, scale) {
|
|
|
1671
1687
|
vars['--caption-line-max-width'] = `${style.max_width_pct}%`;
|
|
1672
1688
|
if (finiteNumber(style.wrap_width_pct))
|
|
1673
1689
|
vars['--caption-wrap-width'] = `${style.wrap_width_pct}%`;
|
|
1674
|
-
if (style.vertical)
|
|
1690
|
+
if (style.vertical) {
|
|
1675
1691
|
vars['--caption-writing-mode'] = 'vertical-rl';
|
|
1692
|
+
vars['--caption-text-orientation'] = 'upright';
|
|
1693
|
+
vars['--caption-width'] = 'max-content';
|
|
1694
|
+
delete vars['--caption-line-max-width'];
|
|
1695
|
+
delete vars['--caption-wrap-width'];
|
|
1696
|
+
}
|
|
1676
1697
|
if (extendedBackground && isRecord(style.background)) {
|
|
1677
1698
|
if (style.background.fit !== 'frame') {
|
|
1678
1699
|
vars['--plate-ext-width'] = percentageBackground
|
|
@@ -1693,11 +1714,25 @@ function resolveCaptionLineStyleVarsAtScale(style, scale) {
|
|
|
1693
1714
|
if (textShadow !== null)
|
|
1694
1715
|
vars['--caption-text-shadow'] = textShadow;
|
|
1695
1716
|
Object.assign(vars, captionZoneVars(style.zone));
|
|
1696
|
-
Object.assign(vars, captionAnchorPositionVars(style.text_anchor, style.position, style.vertical_align));
|
|
1717
|
+
Object.assign(vars, captionAnchorPositionVars(style.text_anchor, style.position, style.vertical ? undefined : style.vertical_align));
|
|
1718
|
+
if (style.vertical && style.vertical_align && !(isRecord(style.position) && finiteNumber(style.position.x))) {
|
|
1719
|
+
vars['--caption-left'] = style.vertical_align === 'top' ? 'auto' : style.vertical_align === 'middle' ? '50%' : '4%';
|
|
1720
|
+
vars['--caption-right'] = style.vertical_align === 'top' ? '4%' : 'auto';
|
|
1721
|
+
vars['--caption-align-items'] = style.vertical_align === 'top' ? 'flex-end'
|
|
1722
|
+
: style.vertical_align === 'middle' ? 'center' : 'flex-start';
|
|
1723
|
+
if (style.vertical_align === 'middle')
|
|
1724
|
+
vars['--caption-translate'] = '-50% 0';
|
|
1725
|
+
}
|
|
1726
|
+
else if (style.vertical && !style.vertical_align && !style.text_anchor
|
|
1727
|
+
&& (!style.zone || style.zone === 'bottom')
|
|
1728
|
+
&& !(isRecord(style.position) && finiteNumber(style.position.x))) {
|
|
1729
|
+
vars['--caption-left'] = '50%';
|
|
1730
|
+
vars['--caption-right'] = 'auto';
|
|
1731
|
+
vars['--caption-align-items'] = 'center';
|
|
1732
|
+
vars['--caption-translate'] = '-50% 0';
|
|
1733
|
+
}
|
|
1697
1734
|
if (style.align) {
|
|
1698
1735
|
vars['--caption-text-align'] = style.align;
|
|
1699
|
-
vars['--caption-align-items'] = style.align === 'left'
|
|
1700
|
-
? 'flex-start' : style.align === 'right' ? 'flex-end' : 'center';
|
|
1701
1736
|
}
|
|
1702
1737
|
return vars;
|
|
1703
1738
|
}
|
|
@@ -1705,7 +1740,16 @@ function resolveCaptionLineStyleVarsAtScale(style, scale) {
|
|
|
1705
1740
|
function resolveCaptionLineStyleVars(style, output) {
|
|
1706
1741
|
if (!isRecord(style))
|
|
1707
1742
|
return {};
|
|
1708
|
-
|
|
1743
|
+
const vars = resolveCaptionLineStyleVarsAtScale(style, resolveCaptionReferenceScale(style, output));
|
|
1744
|
+
Object.assign(vars, captionVerticalHeightVars(style, output));
|
|
1745
|
+
return vars;
|
|
1746
|
+
}
|
|
1747
|
+
function captionVerticalHeightVars(style, output) {
|
|
1748
|
+
if (style.vertical !== true || !output || !finitePositive(output.height))
|
|
1749
|
+
return {};
|
|
1750
|
+
const pct = finitePositive(style.wrap_width_pct) ? style.wrap_width_pct
|
|
1751
|
+
: finitePositive(style.max_width_pct) ? style.max_width_pct : 90;
|
|
1752
|
+
return { '--caption-vertical-max-height': `${formatCssNumber(output.height * pct / 100)}px` };
|
|
1709
1753
|
}
|
|
1710
1754
|
function resolveCaptionStyleForOutput(style, output) {
|
|
1711
1755
|
let layout;
|
|
@@ -1724,6 +1768,7 @@ function resolveCaptionStyleForOutput(style, output) {
|
|
|
1724
1768
|
scale = resolveCaptionReferenceScale(style, output);
|
|
1725
1769
|
}
|
|
1726
1770
|
const vars = resolveCaptionLineStyleVarsAtScale(style, scale);
|
|
1771
|
+
Object.assign(vars, captionVerticalHeightVars(style, output));
|
|
1727
1772
|
vars['--caption-paint-order'] = 'stroke fill';
|
|
1728
1773
|
if (!isRecord(style.shadow) && !isRecord(style.glow))
|
|
1729
1774
|
vars['--caption-text-shadow'] = 'none';
|
|
@@ -1771,7 +1816,7 @@ function resolveCaptionWordStyleVars(style, output) {
|
|
|
1771
1816
|
if (finitePositive(style.size_px))
|
|
1772
1817
|
vars['--caption-tok-font-size'] = `${formatCssNumber(style.size_px * scale)}px`;
|
|
1773
1818
|
if (typeof style.font_family === 'string' && style.font_family.length > 0) {
|
|
1774
|
-
vars['--caption-tok-font-family'] = style.font_family;
|
|
1819
|
+
vars['--caption-tok-font-family'] = cssCaptionFontFamily(style.font_family);
|
|
1775
1820
|
}
|
|
1776
1821
|
if (Number.isInteger(style.weight) && style.weight >= 100 && style.weight <= 900) {
|
|
1777
1822
|
vars['--caption-tok-font-weight'] = String(style.weight);
|
|
@@ -973,7 +973,7 @@ function isFiniteInRange(value, min, max) {
|
|
|
973
973
|
// captions.schema.json の $defs/textStyle が受理する全プロパティ名(2026-08-10 拡張)。
|
|
974
974
|
// これ以外のキーは「未知」として個別に無視する(行やスタイル全体は破棄しない)。
|
|
975
975
|
const TEXT_STYLE_KEYS = new Set([
|
|
976
|
-
'color', 'size_px', 'reference_height_px', 'font_family', 'font_weight', 'weight', 'italic', 'underline',
|
|
976
|
+
'color', 'size_px', 'reference_height_px', 'font_family', 'font_weight', 'weight', 'italic', 'underline', 'strikethrough', 'list', 'opacity',
|
|
977
977
|
'letter_spacing_em', 'line_height', 'align', 'vertical_align', 'vertical',
|
|
978
978
|
'text_transform', 'max_width_pct', 'wrap_width_pct', 'max_characters', 'text_anchor', 'position', 'scale', 'rotate', 'shadow', 'glow',
|
|
979
979
|
'animation', 'stroke', 'background', 'zone', 'layout'
|
|
@@ -1019,6 +1019,12 @@ function normalizeTextStyle(value, onUnknownKeys) {
|
|
|
1019
1019
|
if (typeof value.underline === 'boolean') {
|
|
1020
1020
|
style.underline = value.underline;
|
|
1021
1021
|
}
|
|
1022
|
+
if (typeof value.strikethrough === 'boolean')
|
|
1023
|
+
style.strikethrough = value.strikethrough;
|
|
1024
|
+
if (value.list === 'bullet' || value.list === null)
|
|
1025
|
+
style.list = value.list;
|
|
1026
|
+
if (isFiniteNumber(value.opacity) && value.opacity >= 0 && value.opacity <= 1)
|
|
1027
|
+
style.opacity = value.opacity;
|
|
1022
1028
|
if (isFiniteNumber(value.letter_spacing_em)) {
|
|
1023
1029
|
style.letterSpacingEm = value.letter_spacing_em;
|
|
1024
1030
|
}
|
|
@@ -1259,6 +1265,9 @@ function textStyleToJson(style) {
|
|
|
1259
1265
|
...(style.weight !== undefined ? { weight: style.weight } : {}),
|
|
1260
1266
|
...(style.italic !== undefined ? { italic: style.italic } : {}),
|
|
1261
1267
|
...(style.underline !== undefined ? { underline: style.underline } : {}),
|
|
1268
|
+
...(style.strikethrough !== undefined ? { strikethrough: style.strikethrough } : {}),
|
|
1269
|
+
...(style.list !== undefined ? { list: style.list } : {}),
|
|
1270
|
+
...(style.opacity !== undefined ? { opacity: style.opacity } : {}),
|
|
1262
1271
|
...(style.letterSpacingEm !== undefined ? { letter_spacing_em: style.letterSpacingEm } : {}),
|
|
1263
1272
|
...(style.lineHeight !== undefined ? { line_height: style.lineHeight } : {}),
|
|
1264
1273
|
...(style.align !== undefined ? { align: style.align } : {}),
|
|
@@ -5166,13 +5166,16 @@ var AkariEditKernel = (() => {
|
|
|
5166
5166
|
...positiveInteger(value.reference_height_px) ? { reference_height_px: value.reference_height_px } : {},
|
|
5167
5167
|
...typeof value.font_family === "string" && value.font_family !== "" ? { font_family: value.font_family } : {},
|
|
5168
5168
|
...finiteNumber(value.weight) && value.weight >= 100 && value.weight <= 900 ? { weight: value.weight } : Number.isInteger(value.font_weight) && value.font_weight >= 1 && value.font_weight <= 1e3 ? { weight: value.font_weight } : {},
|
|
5169
|
-
...value.italic ===
|
|
5170
|
-
...value.underline ===
|
|
5169
|
+
...typeof value.italic === "boolean" ? { italic: value.italic } : {},
|
|
5170
|
+
...typeof value.underline === "boolean" ? { underline: value.underline } : {},
|
|
5171
|
+
...typeof value.strikethrough === "boolean" ? { strikethrough: value.strikethrough } : {},
|
|
5172
|
+
...value.list === "bullet" || value.list === null ? { list: value.list } : {},
|
|
5173
|
+
...finiteNumber(value.opacity) && value.opacity >= 0 && value.opacity <= 1 ? { opacity: value.opacity } : {},
|
|
5171
5174
|
...finiteNumber(value.letter_spacing_em) ? { letter_spacing_em: value.letter_spacing_em } : {},
|
|
5172
5175
|
...finitePositive2(value.line_height) ? { line_height: value.line_height } : {},
|
|
5173
5176
|
...CAPTION_ALIGN_VALUES.has(value.align) ? { align: value.align } : {},
|
|
5174
5177
|
...CAPTION_VERTICAL_ALIGN_VALUES.has(value.vertical_align) ? { vertical_align: value.vertical_align } : {},
|
|
5175
|
-
...value.vertical ===
|
|
5178
|
+
...typeof value.vertical === "boolean" ? { vertical: value.vertical } : {},
|
|
5176
5179
|
...CAPTION_TEXT_TRANSFORM_MAP[value.text_transform] ? { text_transform: CAPTION_TEXT_TRANSFORM_MAP[value.text_transform] } : {},
|
|
5177
5180
|
...finiteNumber(value.max_width_pct) && value.max_width_pct > 0 && value.max_width_pct < 100 ? { max_width_pct: value.max_width_pct } : {},
|
|
5178
5181
|
...finiteNumber(value.wrap_width_pct) && value.wrap_width_pct > 0 && value.wrap_width_pct <= 100 ? { wrap_width_pct: value.wrap_width_pct } : {},
|
|
@@ -5313,6 +5316,10 @@ var AkariEditKernel = (() => {
|
|
|
5313
5316
|
}
|
|
5314
5317
|
return vars;
|
|
5315
5318
|
}
|
|
5319
|
+
function cssCaptionFontFamily(value) {
|
|
5320
|
+
if (value.includes(",") || /^(['"]).*\1$/s.test(value.trim())) return value;
|
|
5321
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
5322
|
+
}
|
|
5316
5323
|
function resolveCaptionLineStyleVarsAtScale(style, scale) {
|
|
5317
5324
|
const vars = {};
|
|
5318
5325
|
const px = (value) => scaleCaptionPx(value, scale);
|
|
@@ -5339,11 +5346,13 @@ var AkariEditKernel = (() => {
|
|
|
5339
5346
|
const name = style.background.mode === "block" ? "--plate-block-radius" : extendedBackground ? "--plate-ext-radius" : "--plate-radius";
|
|
5340
5347
|
vars[name] = `${px(style.background.radius_px)}px`;
|
|
5341
5348
|
}
|
|
5342
|
-
if (typeof style.font_family === "string") vars["--caption-font-family"] = style.font_family;
|
|
5349
|
+
if (typeof style.font_family === "string") vars["--caption-font-family"] = cssCaptionFontFamily(style.font_family);
|
|
5343
5350
|
if (finiteNumber(style.weight)) vars["--caption-font-weight"] = String(style.weight);
|
|
5344
5351
|
else if (Number.isInteger(style.font_weight)) vars["--caption-font-weight"] = String(style.font_weight);
|
|
5345
5352
|
if (style.italic) vars["--caption-font-style"] = "italic";
|
|
5346
|
-
if (style.underline) vars["--caption-text-decoration"] = "underline";
|
|
5353
|
+
if (style.underline || style.strikethrough) vars["--caption-text-decoration"] = [style.underline ? "underline" : "", style.strikethrough ? "line-through" : ""].filter(Boolean).join(" ");
|
|
5354
|
+
if (style.list === "bullet") vars["--caption-list-display"] = "list-item";
|
|
5355
|
+
if (finiteNumber(style.opacity)) vars["--caption-opacity"] = String(style.opacity);
|
|
5347
5356
|
if (finiteNumber(style.letter_spacing_em)) vars["--caption-letter-spacing"] = `${style.letter_spacing_em}em`;
|
|
5348
5357
|
if (finiteNumber(style.line_height)) vars["--caption-line-height"] = String(style.line_height);
|
|
5349
5358
|
if (typeof style.text_transform === "string" && CAPTION_TEXT_TRANSFORM_MAP[style.text_transform]) {
|
|
@@ -5351,7 +5360,13 @@ var AkariEditKernel = (() => {
|
|
|
5351
5360
|
}
|
|
5352
5361
|
if (finiteNumber(style.max_width_pct)) vars["--caption-line-max-width"] = `${style.max_width_pct}%`;
|
|
5353
5362
|
if (finiteNumber(style.wrap_width_pct)) vars["--caption-wrap-width"] = `${style.wrap_width_pct}%`;
|
|
5354
|
-
if (style.vertical)
|
|
5363
|
+
if (style.vertical) {
|
|
5364
|
+
vars["--caption-writing-mode"] = "vertical-rl";
|
|
5365
|
+
vars["--caption-text-orientation"] = "upright";
|
|
5366
|
+
vars["--caption-width"] = "max-content";
|
|
5367
|
+
delete vars["--caption-line-max-width"];
|
|
5368
|
+
delete vars["--caption-wrap-width"];
|
|
5369
|
+
}
|
|
5355
5370
|
if (extendedBackground && isRecord4(style.background)) {
|
|
5356
5371
|
if (style.background.fit !== "frame") {
|
|
5357
5372
|
vars["--plate-ext-width"] = percentageBackground ? `${style.background.width_pct ?? 0}%` : `${px(style.background.padding_px ?? 0)}px`;
|
|
@@ -5366,16 +5381,37 @@ var AkariEditKernel = (() => {
|
|
|
5366
5381
|
const textShadow = captionTextShadowValue(style.shadow, style.glow, scale);
|
|
5367
5382
|
if (textShadow !== null) vars["--caption-text-shadow"] = textShadow;
|
|
5368
5383
|
Object.assign(vars, captionZoneVars(style.zone));
|
|
5369
|
-
Object.assign(vars, captionAnchorPositionVars(
|
|
5384
|
+
Object.assign(vars, captionAnchorPositionVars(
|
|
5385
|
+
style.text_anchor,
|
|
5386
|
+
style.position,
|
|
5387
|
+
style.vertical ? void 0 : style.vertical_align
|
|
5388
|
+
));
|
|
5389
|
+
if (style.vertical && style.vertical_align && !(isRecord4(style.position) && finiteNumber(style.position.x))) {
|
|
5390
|
+
vars["--caption-left"] = style.vertical_align === "top" ? "auto" : style.vertical_align === "middle" ? "50%" : "4%";
|
|
5391
|
+
vars["--caption-right"] = style.vertical_align === "top" ? "4%" : "auto";
|
|
5392
|
+
vars["--caption-align-items"] = style.vertical_align === "top" ? "flex-end" : style.vertical_align === "middle" ? "center" : "flex-start";
|
|
5393
|
+
if (style.vertical_align === "middle") vars["--caption-translate"] = "-50% 0";
|
|
5394
|
+
} else if (style.vertical && !style.vertical_align && !style.text_anchor && (!style.zone || style.zone === "bottom") && !(isRecord4(style.position) && finiteNumber(style.position.x))) {
|
|
5395
|
+
vars["--caption-left"] = "50%";
|
|
5396
|
+
vars["--caption-right"] = "auto";
|
|
5397
|
+
vars["--caption-align-items"] = "center";
|
|
5398
|
+
vars["--caption-translate"] = "-50% 0";
|
|
5399
|
+
}
|
|
5370
5400
|
if (style.align) {
|
|
5371
5401
|
vars["--caption-text-align"] = style.align;
|
|
5372
|
-
vars["--caption-align-items"] = style.align === "left" ? "flex-start" : style.align === "right" ? "flex-end" : "center";
|
|
5373
5402
|
}
|
|
5374
5403
|
return vars;
|
|
5375
5404
|
}
|
|
5376
5405
|
function resolveCaptionLineStyleVars(style, output) {
|
|
5377
5406
|
if (!isRecord4(style)) return {};
|
|
5378
|
-
|
|
5407
|
+
const vars = resolveCaptionLineStyleVarsAtScale(style, resolveCaptionReferenceScale(style, output));
|
|
5408
|
+
Object.assign(vars, captionVerticalHeightVars(style, output));
|
|
5409
|
+
return vars;
|
|
5410
|
+
}
|
|
5411
|
+
function captionVerticalHeightVars(style, output) {
|
|
5412
|
+
if (style.vertical !== true || !output || !finitePositive2(output.height)) return {};
|
|
5413
|
+
const pct = finitePositive2(style.wrap_width_pct) ? style.wrap_width_pct : finitePositive2(style.max_width_pct) ? style.max_width_pct : 90;
|
|
5414
|
+
return { "--caption-vertical-max-height": `${formatCssNumber(output.height * pct / 100)}px` };
|
|
5379
5415
|
}
|
|
5380
5416
|
var CAPTION_TEXT_TRANSFORM_MAP = {
|
|
5381
5417
|
upper: "uppercase",
|
|
@@ -5415,6 +5451,9 @@ var AkariEditKernel = (() => {
|
|
|
5415
5451
|
const alpha = explicitOpacity ?? alphaFromColor;
|
|
5416
5452
|
return `rgba(${parseInt(rgb.slice(0, 2), 16)},${parseInt(rgb.slice(2, 4), 16)},${parseInt(rgb.slice(4, 6), 16)},${Number(alpha.toFixed(4))})`;
|
|
5417
5453
|
}
|
|
5454
|
+
function formatCssNumber(value) {
|
|
5455
|
+
return Number(value.toFixed(6)).toString();
|
|
5456
|
+
}
|
|
5418
5457
|
function finiteNumber(value) {
|
|
5419
5458
|
return typeof value === "number" && Number.isFinite(value);
|
|
5420
5459
|
}
|
|
@@ -39,43 +39,88 @@ globalThis.akariHandleGeometry = (() => {
|
|
|
39
39
|
return { x: anchor.x - c * ratioX * lx + s * ratioY * ly,
|
|
40
40
|
y: anchor.y - s * ratioX * lx - c * ratioY * ly };
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
// 他の素材への吸着は、揃えとして意味のある組み合わせ(中央どうし・同じ辺どうし・隣接する辺)
|
|
43
|
+
// だけに絞る。辺と中央の交差まで候補にすると、小さい素材ほど近くに候補がひしめいて飛び移る。
|
|
44
|
+
// 添字は coordinates() の並び(0: 左/上, 1: 中央, 2: 右/下)。
|
|
45
|
+
const ITEM_PAIRS = new Set(['0:0', '1:1', '2:2', '0:2', '2:0']);
|
|
46
|
+
// 吸着距離(表示px)。合わせたい画面中央を一番強く、画面の端、他の素材の順に弱くする。
|
|
47
|
+
// tolerance は画面中央の距離(従来の 6px)。端と素材は options で上書きできる。
|
|
48
|
+
const CANVAS_EDGE_TOLERANCE = 4;
|
|
49
|
+
const ITEM_TOLERANCE = 3;
|
|
50
|
+
// 保持中の吸着先から乗り換えるのは、この差以上に近い候補が現れたとき(または、より近い画面の端・中央)。
|
|
51
|
+
const SWITCH_MARGIN = 2;
|
|
52
|
+
// options.previous: 直前に吸着していた先({x, y})。
|
|
53
|
+
// options.fast: 素早く動かしている最中(true か軸別の {x, y})。その軸では新しく吸着しない。
|
|
54
|
+
function snapBounds(moving, others, canvas, displayScale = 1, tolerance = 6, options = {}) {
|
|
55
|
+
const edgeTolerance = Number.isFinite(options.edgeTolerance) ? options.edgeTolerance
|
|
56
|
+
: Math.min(tolerance, CANVAS_EDGE_TOLERANCE);
|
|
57
|
+
const itemTolerance = Number.isFinite(options.itemTolerance) ? options.itemTolerance
|
|
58
|
+
: Math.min(tolerance, ITEM_TOLERANCE);
|
|
43
59
|
const coordinates = (b, axis) => axis === 'x'
|
|
44
60
|
? [b.left, (b.left + b.right) / 2, b.right]
|
|
45
61
|
: [b.top, (b.top + b.bottom) / 2, b.bottom];
|
|
46
62
|
const pick = axis => {
|
|
47
63
|
const own = coordinates(moving, axis);
|
|
64
|
+
const size = axis === 'x' ? canvas.width : canvas.height;
|
|
65
|
+
const previous = options.previous?.[axis] ?? null;
|
|
66
|
+
const fast = typeof options.fast === 'object' && options.fast !== null
|
|
67
|
+
? Boolean(options.fast[axis]) : Boolean(options.fast);
|
|
48
68
|
const targets = [
|
|
49
|
-
...[0,
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
...[0, size / 2, size].map((value, targetIndex) => ({ value, kind: 'canvas', bounds: null, targetIndex })),
|
|
70
|
+
...others.flatMap(bounds => coordinates(bounds, axis)
|
|
71
|
+
.map((value, targetIndex) => ({ value, kind: 'item', bounds, targetIndex })))
|
|
52
72
|
];
|
|
53
|
-
|
|
73
|
+
const limitFor = target => target.kind === 'item' ? itemTolerance
|
|
74
|
+
: target.targetIndex === 1 ? tolerance : edgeTolerance;
|
|
75
|
+
// いったん吸着した先は、その吸着距離を超えて離れるまで保つ。近くの別候補へ毎回
|
|
76
|
+
// 選び直すと、素早いドラッグで候補から候補へ飛び移って見える。
|
|
77
|
+
let held = null;
|
|
78
|
+
if (previous && Number.isInteger(previous.sourceIndex) && Number.isFinite(previous.target)) {
|
|
79
|
+
const kept = targets.find(target => target.kind === previous.kind
|
|
80
|
+
&& Math.abs(target.value - previous.target) <= 1e-6);
|
|
81
|
+
const correction = previous.target - own[previous.sourceIndex];
|
|
82
|
+
if (kept && Number.isFinite(correction) && Math.abs(correction) * displayScale <= limitFor(kept)) {
|
|
83
|
+
held = { ...previous, correction, bounds: kept.bounds, distance: Math.abs(correction) * displayScale };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const finish = snap => snap && { ...snap, guide: guideFor(axis, moving, snap.bounds, canvas) };
|
|
87
|
+
if (fast) return finish(held);
|
|
88
|
+
const candidates = [];
|
|
54
89
|
own.forEach((source, sourceIndex) => targets.forEach(target => {
|
|
90
|
+
if (target.kind === 'item' && !ITEM_PAIRS.has(sourceIndex + ':' + target.targetIndex)) return;
|
|
55
91
|
const correction = target.value - source;
|
|
56
92
|
const distance = Math.abs(correction) * displayScale;
|
|
57
|
-
if (distance >
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
(visiblyEqual && target.kind === 'canvas' && best.kind !== 'canvas')) {
|
|
61
|
-
best = { correction, target: target.value, sourceIndex, kind: target.kind,
|
|
62
|
-
bounds: target.bounds, distance };
|
|
63
|
-
}
|
|
93
|
+
if (distance > limitFor(target)) return;
|
|
94
|
+
candidates.push({ correction, target: target.value, sourceIndex, kind: target.kind,
|
|
95
|
+
bounds: target.bounds, distance });
|
|
64
96
|
}));
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
97
|
+
// 保持中でも、はっきり近い候補や、より近い画面の端・中央へは乗り換える
|
|
98
|
+
// (素材の辺に掴まったまま画面中央へ合わせられない、を防ぐ)。
|
|
99
|
+
const pool = held ? candidates.filter(candidate => candidate.distance + SWITCH_MARGIN <= held.distance
|
|
100
|
+
|| (candidate.kind === 'canvas' && held.kind !== 'canvas' && candidate.distance < held.distance)) : candidates;
|
|
101
|
+
if (held && pool.length === 0) return finish(held);
|
|
102
|
+
let best = null;
|
|
103
|
+
for (const candidate of pool) {
|
|
104
|
+
const visiblyEqual = best && Math.abs(candidate.distance - best.distance) <= .5;
|
|
105
|
+
if (!best || candidate.distance < best.distance - .5 ||
|
|
106
|
+
(visiblyEqual && candidate.kind === 'canvas' && best.kind !== 'canvas')) best = candidate;
|
|
107
|
+
}
|
|
108
|
+
return finish(best);
|
|
73
109
|
};
|
|
74
110
|
return { x: pick('x'), y: pick('y') };
|
|
75
111
|
}
|
|
76
|
-
function
|
|
112
|
+
function guideFor(axis, moving, other, canvas) {
|
|
113
|
+
return axis === 'x'
|
|
114
|
+
? { start: other ? Math.min(moving.top, other.top) : 0,
|
|
115
|
+
end: other ? Math.max(moving.bottom, other.bottom) : canvas.height }
|
|
116
|
+
: { start: other ? Math.min(moving.left, other.left) : 0,
|
|
117
|
+
end: other ? Math.max(moving.right, other.right) : canvas.width };
|
|
118
|
+
}
|
|
119
|
+
function snapEndpoint(point, others, canvas, displayScale = 1, tolerance = 6, options = {}) {
|
|
120
|
+
// 線の端点は 1 点(左・中央・右が同じ値)なので、組み合わせを絞っても相手のどの辺・中央へも届く。
|
|
121
|
+
// 端点を相手の角へ正確に置く用途のため、吸着距離は従来どおり画面の端・中央と同じにする。
|
|
77
122
|
return snapBounds({ left: point.x, right: point.x, top: point.y, bottom: point.y },
|
|
78
|
-
others, canvas, displayScale, tolerance);
|
|
123
|
+
others, canvas, displayScale, tolerance, { itemTolerance: tolerance, edgeTolerance: tolerance, ...options });
|
|
79
124
|
}
|
|
80
125
|
function solveLineEndpoint(fixed, pointer, others, canvas, displayScale = 1, disabled = false,
|
|
81
126
|
anglePointer = pointer) {
|
|
@@ -1338,7 +1338,7 @@ function marqueeHits(candidates, rect) {
|
|
|
1338
1338
|
startClientX: event.clientX, startClientY: event.clientY,
|
|
1339
1339
|
startStagePoint: stageLocalPoint(event.clientX, event.clientY),
|
|
1340
1340
|
startX: world.x ?? 0, startY: world.y ?? 0, dx: 0, dy: 0,
|
|
1341
|
-
snapX: null, snapY: null, moved: false, duplicate: event.altKey,
|
|
1341
|
+
snapX: null, snapY: null, snapMotion: beginSnapMotion(fragmentVideoBounds(container)), moved: false, duplicate: event.altKey,
|
|
1342
1342
|
writeContext: captureWriteContext() };
|
|
1343
1343
|
try { container.setPointerCapture?.(event.pointerId); } catch { /* synthetic pointer */ }
|
|
1344
1344
|
}
|
|
@@ -1445,7 +1445,8 @@ function marqueeHits(candidates, rect) {
|
|
|
1445
1445
|
if (disabled || !tl || !br) { drag.snapX = null; drag.snapY = null; hideSnapGuides(); return; }
|
|
1446
1446
|
const bounds = { left: tl.x, top: tl.y, right: br.x, bottom: br.y,
|
|
1447
1447
|
centerX: (tl.x + br.x) / 2, centerY: (tl.y + br.y) / 2 };
|
|
1448
|
-
const snap = computeSnapCorrection(bounds, { x: drag.snapX, y: drag.snapY });
|
|
1448
|
+
const snap = computeSnapCorrection(bounds, { x: drag.snapX, y: drag.snapY, motion: drag.snapMotion });
|
|
1449
|
+
drag.snapMotion = snap.motion;
|
|
1449
1450
|
if (lockedAxis === 'x') snap.y = null;
|
|
1450
1451
|
if (lockedAxis === 'y') snap.x = null;
|
|
1451
1452
|
drag.snapX = snap.x; drag.snapY = snap.y;
|
|
@@ -1856,6 +1857,36 @@ function marqueeHits(candidates, rect) {
|
|
|
1856
1857
|
// {left,top,right,bottom,centerX,centerY} から計算する。overlays のドラッグに限らず、
|
|
1857
1858
|
// resize・layers[]・cut/caption のドラッグからも共通で呼べるよう window.akari.interaction
|
|
1858
1859
|
// 経由でも公開する(㉒ スナップ統一の単一正本)。
|
|
1860
|
+
// 素早く動かしている最中は、その軸で新しく吸着しない(通り過ぎる候補ごとにカクッと引っかかって、
|
|
1861
|
+
// 掴んだ素材がジャンプしたように見えるため)。速さは動かしている素材の中心の表示px/ms を軸ごとに、
|
|
1862
|
+
// 時間基準の移動平均でならして測る。止める閾値と戻す閾値を分けて、境目でばたつかないようにする。
|
|
1863
|
+
// 状態は吸着結果(motion)に載せて返し、呼び出し元が次回の previousSnap として渡す = ドラッグごとに閉じる。
|
|
1864
|
+
const FAST_SNAP_START_SPEED = 0.6;
|
|
1865
|
+
const FAST_SNAP_END_SPEED = 0.35;
|
|
1866
|
+
const SNAP_SPEED_SMOOTHING_MS = 50;
|
|
1867
|
+
function nextSnapMotion(bounds, previous) {
|
|
1868
|
+
const now = performance.now();
|
|
1869
|
+
const x = (bounds.left + bounds.right) / 2;
|
|
1870
|
+
const y = (bounds.top + bounds.bottom) / 2;
|
|
1871
|
+
if (!previous || !Number.isFinite(previous.at)) {
|
|
1872
|
+
return { at: now, x, y, speedX: 0, speedY: 0, fastX: false, fastY: false };
|
|
1873
|
+
}
|
|
1874
|
+
const elapsed = now - previous.at;
|
|
1875
|
+
if (!(elapsed > 0)) return previous;
|
|
1876
|
+
const scale = currentDisplayScale();
|
|
1877
|
+
const alpha = 1 - Math.exp(-elapsed / SNAP_SPEED_SMOOTHING_MS);
|
|
1878
|
+
const speedX = previous.speedX + alpha * (Math.abs(x - previous.x) * scale / elapsed - previous.speedX);
|
|
1879
|
+
const speedY = previous.speedY + alpha * (Math.abs(y - previous.y) * scale / elapsed - previous.speedY);
|
|
1880
|
+
return { at: now, x, y, speedX, speedY,
|
|
1881
|
+
fastX: previous.fastX ? speedX > FAST_SNAP_END_SPEED : speedX > FAST_SNAP_START_SPEED,
|
|
1882
|
+
fastY: previous.fastY ? speedY > FAST_SNAP_END_SPEED : speedY > FAST_SNAP_START_SPEED };
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// ドラッグ開始時(pointerdown)の位置で速度の起点を作る。最初の move から実速度で判定できる。
|
|
1886
|
+
function beginSnapMotion(bounds) {
|
|
1887
|
+
return bounds ? nextSnapMotion(bounds, null) : null;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1859
1890
|
function computeSnapCorrection(bounds, previousSnap) {
|
|
1860
1891
|
if (!bounds) return { x: null, y: null };
|
|
1861
1892
|
if (!globalThis.akariHandleGeometry) {
|
|
@@ -1865,10 +1896,15 @@ function marqueeHits(candidates, rect) {
|
|
|
1865
1896
|
y: closestAxisSnap([bounds.top, bounds.centerY, bounds.bottom], targets.y,
|
|
1866
1897
|
previousSnap?.y ?? null, currentDisplayScale()) };
|
|
1867
1898
|
}
|
|
1899
|
+
// 動かしている素材自身(単体選択・グループ/複数選択のメンバー)は吸着先にしない。
|
|
1900
|
+
const moving = new Set([selectedOverlay, ...selectionMembers()].filter(Boolean));
|
|
1868
1901
|
const others = stage ? Array.from(stage.children)
|
|
1869
|
-
.filter(element => isSelectable(element) && element
|
|
1902
|
+
.filter(element => isSelectable(element) && !moving.has(element))
|
|
1870
1903
|
.map(fragmentVideoBounds).filter(Boolean) : [];
|
|
1871
|
-
|
|
1904
|
+
const motion = nextSnapMotion(bounds, previousSnap?.motion);
|
|
1905
|
+
const snap = globalThis.akariHandleGeometry.snapBounds(bounds, others, outputSize(), currentDisplayScale(), 6,
|
|
1906
|
+
{ previous: previousSnap, fast: { x: motion.fastX, y: motion.fastY } });
|
|
1907
|
+
return { ...snap, motion };
|
|
1872
1908
|
}
|
|
1873
1909
|
|
|
1874
1910
|
function applyDragSnapping(drag, rawX, rawY, disabled, lockedAxis = null) {
|
|
@@ -1890,7 +1926,8 @@ function marqueeHits(candidates, rect) {
|
|
|
1890
1926
|
return;
|
|
1891
1927
|
}
|
|
1892
1928
|
|
|
1893
|
-
const snap = computeSnapCorrection(bounds, { x: drag.snapX, y: drag.snapY });
|
|
1929
|
+
const snap = computeSnapCorrection(bounds, { x: drag.snapX, y: drag.snapY, motion: drag.snapMotion });
|
|
1930
|
+
drag.snapMotion = snap.motion;
|
|
1894
1931
|
if (lockedAxis === 'x') snap.y = null;
|
|
1895
1932
|
if (lockedAxis === 'y') snap.x = null;
|
|
1896
1933
|
drag.snapX = snap.x;
|
|
@@ -2859,6 +2896,7 @@ function marqueeHits(candidates, rect) {
|
|
|
2859
2896
|
startY: transform.y,
|
|
2860
2897
|
snapX: null,
|
|
2861
2898
|
snapY: null,
|
|
2899
|
+
snapMotion: beginSnapMotion(fragmentVideoBounds(container)),
|
|
2862
2900
|
moved: false,
|
|
2863
2901
|
duplicate: event.altKey,
|
|
2864
2902
|
motionDriven,
|
|
@@ -360,7 +360,7 @@ function validateTextStyle(value, label) {
|
|
|
360
360
|
// 正本は captions.schema.json の $defs/textStyle。
|
|
361
361
|
const allowedKeys = new Set([
|
|
362
362
|
"color", "size_px", "font_weight", "line_height", "stroke", "background", "zone", "layout",
|
|
363
|
-
"font_family", "weight", "italic", "underline", "letter_spacing_em", "align",
|
|
363
|
+
"font_family", "weight", "italic", "underline", "strikethrough", "list", "opacity", "letter_spacing_em", "align",
|
|
364
364
|
"vertical_align", "vertical", "text_transform", "max_width_pct", "wrap_width_pct", "max_characters", "text_anchor",
|
|
365
365
|
"position", "scale", "rotate", "shadow", "glow", "animation", "reference_height_px",
|
|
366
366
|
]);
|
|
@@ -411,6 +411,16 @@ function validateTextStyleV0(value, label) {
|
|
|
411
411
|
for (const key of ["italic", "underline", "vertical"]) {
|
|
412
412
|
if (hasOwn(value, key) && typeof value[key] !== "boolean") fail(`${label}.${key} は boolean である必要があります`);
|
|
413
413
|
}
|
|
414
|
+
if (hasOwn(value, "strikethrough") && typeof value.strikethrough !== "boolean") {
|
|
415
|
+
fail(`${label}.strikethrough は boolean である必要があります`);
|
|
416
|
+
}
|
|
417
|
+
if (hasOwn(value, "list") && value.list !== "bullet" && value.list !== null) {
|
|
418
|
+
fail(`${label}.list は bullet または null である必要があります`);
|
|
419
|
+
}
|
|
420
|
+
if (hasOwn(value, "opacity")
|
|
421
|
+
&& (!isFiniteNumber(value.opacity) || value.opacity < 0 || value.opacity > 1)) {
|
|
422
|
+
fail(`${label}.opacity は 0 から 1 の範囲の有限数である必要があります`);
|
|
423
|
+
}
|
|
414
424
|
if (hasOwn(value, "letter_spacing_em") && !isFiniteNumber(value.letter_spacing_em)) {
|
|
415
425
|
fail(`${label}.letter_spacing_em は有限数である必要があります`);
|
|
416
426
|
}
|
|
@@ -219,6 +219,9 @@
|
|
|
219
219
|
"weight": { "type": "integer", "minimum": 100, "maximum": 900 },
|
|
220
220
|
"italic": { "type": "boolean" },
|
|
221
221
|
"underline": { "type": "boolean" },
|
|
222
|
+
"strikethrough": { "type": "boolean" },
|
|
223
|
+
"list": { "enum": ["bullet", null] },
|
|
224
|
+
"opacity": { "type": "number", "minimum": 0, "maximum": 1 },
|
|
222
225
|
"letter_spacing_em": { "type": "number" },
|
|
223
226
|
"line_height": { "type": "number", "exclusiveMinimum": 0 },
|
|
224
227
|
"align": { "type": "string", "enum": ["left", "center", "right"] },
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
const schema = JSON.parse(readFileSync(new URL('../captions.schema.json', import.meta.url), 'utf8'));
|
|
6
|
+
|
|
7
|
+
test('字幕 style v2 の取り消し線・箇条書き・透明度はスキーマにある', () => {
|
|
8
|
+
const style = schema.$defs.textStyle.properties;
|
|
9
|
+
assert.deepEqual(style.strikethrough, { type: 'boolean' });
|
|
10
|
+
assert.deepEqual(style.list, { enum: ['bullet', null] });
|
|
11
|
+
assert.deepEqual(style.opacity, { type: 'number', minimum: 0, maximum: 1 });
|
|
12
|
+
});
|