akari-video 1.2.0 → 1.3.0-beta.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akari-video",
3
- "version": "1.2.0",
3
+ "version": "1.3.0-beta.1",
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": "1.2.0",
3
+ "version": "1.3.0-beta.1",
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": [
@@ -340,6 +340,96 @@ function elementBoxCompanions(style, parentStyle, width, height) {
340
340
  // テキスト編集で断片 HTML を保存するときに作者の元指定へ戻せるよう、初回値だけ保持する。
341
341
  const hitPolicyOriginalPointerEvents = new WeakMap();
342
342
  const hitPolicyAppliedContainers = new WeakSet();
343
+ const hitPolicyTimes = new WeakMap();
344
+ const staleHitPolicyContainers = new Set();
345
+ const pausedHitPolicyContainers = new Set();
346
+ let pausedHitPolicyTimer = null;
347
+ let lastPointerHitPolicyRefresh = -Infinity;
348
+
349
+ function hitPolicyOperationBusy() {
350
+ return Boolean(activeEdit || activeDrag || activeResize || activeRotate || activeLine
351
+ || marqueeFrame || pointerOwner != null);
352
+ }
353
+
354
+ function schedulePausedHitPolicyRefresh(delay = 120) {
355
+ if (typeof setTimeout !== 'function') return;
356
+ if (pausedHitPolicyTimer !== null) clearTimeout(pausedHitPolicyTimer);
357
+ pausedHitPolicyTimer = setTimeout(flushPausedHitPolicies, delay);
358
+ }
359
+
360
+ // runtime は可視コンテナの時刻だけを記録する。CSS の読み取りはここでは行わない。
361
+ function markOverlayHitPolicyStale(container, localTimeMs, paused = false, reentered = false,
362
+ animationReady = null) {
363
+ if (!container) return;
364
+ let state = hitPolicyTimes.get(container);
365
+ if (!state) {
366
+ state = { current: localTimeMs, measured: null, paused: null };
367
+ hitPolicyTimes.set(container, state);
368
+ }
369
+ const timeChanged = state.current !== localTimeMs;
370
+ const enteredPause = paused && state.paused !== true;
371
+ state.current = localTimeMs;
372
+ state.paused = paused;
373
+ if (reentered) {
374
+ state.measured = null;
375
+ hitPolicyAppliedContainers.delete(container);
376
+ }
377
+ if (state.measured !== localTimeMs) staleHitPolicyContainers.add(container);
378
+ // pause()/currentTime の pending 補正はこの tick の測定より後に解決し得る。
379
+ // 暫定 apply が stale を消しても、停止後の一回は別の印で必ず残す。
380
+ if (paused && (reentered || timeChanged || enteredPause)) pausedHitPolicyContainers.add(container);
381
+ if (!paused) pausedHitPolicyContainers.delete(container);
382
+ if (paused && pausedHitPolicyContainers.size) {
383
+ // スクラブ中は連続 tick をまとめ、停止後にポインタ無しで確定する。
384
+ schedulePausedHitPolicyRefresh();
385
+ }
386
+ if (paused && (reentered || timeChanged || enteredPause) && animationReady?.then) {
387
+ const expectedState = state;
388
+ animationReady.then(() => {
389
+ if (hitPolicyTimes.get(container) !== expectedState || !state.paused
390
+ || state.current !== localTimeMs) return;
391
+ // ready 後の currentTime 補正が 120ms の測定より遅れた場合も追い付く。
392
+ const alreadyRefreshed = !pausedHitPolicyContainers.has(container);
393
+ pausedHitPolicyContainers.add(container);
394
+ schedulePausedHitPolicyRefresh(alreadyRefreshed ? 0 : 120);
395
+ });
396
+ }
397
+ }
398
+
399
+ function flushPausedHitPolicies() {
400
+ pausedHitPolicyTimer = null;
401
+ if (pausedHitPolicyContainers.size === 0) return;
402
+ if (hitPolicyOperationBusy()) {
403
+ // 編集・ジェスチャーが終わるまで印を保持する。
404
+ pausedHitPolicyTimer = setTimeout(flushPausedHitPolicies, 120);
405
+ return;
406
+ }
407
+ refreshStaleHitPolicies(true);
408
+ }
409
+
410
+ function refreshStaleHitPolicies(includePaused = false) {
411
+ if (hitPolicyOperationBusy()) return false;
412
+ let refreshed = false;
413
+ const candidates = includePaused ? pausedHitPolicyContainers : staleHitPolicyContainers;
414
+ for (const container of candidates) {
415
+ if (includePaused) pausedHitPolicyContainers.delete(container);
416
+ if (!container.isConnected || container.style.visibility === 'hidden') {
417
+ staleHitPolicyContainers.delete(container);
418
+ continue;
419
+ }
420
+ invalidateOverlayHitPolicy(container);
421
+ applyOverlayHitPolicy(container);
422
+ refreshed = true;
423
+ }
424
+ return refreshed;
425
+ }
426
+
427
+ function forgetOverlayHitPolicyTracking(container) {
428
+ staleHitPolicyContainers.delete(container);
429
+ pausedHitPolicyContainers.delete(container);
430
+ hitPolicyTimes.delete(container);
431
+ hitPolicyAppliedContainers.delete(container);
432
+ }
343
433
 
344
434
  // overlay_write は操作順を保存する。selftest は今回の書き込みをこの記録から待つ。
345
435
  let writeTail = Promise.resolve();
@@ -982,7 +1072,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
982
1072
 
983
1073
  // 全画面の外側コンテナと断片ルートは素通しにし、実際に背景・枠・影・文字・置換要素を
984
1074
  // 描く可視の子孫だけを拾う。data-akari-hit は最寄りの指定を配下へ継承し、機械判定より
985
- // 優先する。字幕が 1,000 件級でも全件を走査しないよう、runtime の可視化時に一度だけ呼ぶ。
1075
+ // 優先する。字幕が 1,000 件級でも全件を走査しないよう、runtime は可視コンテナの
1076
+ // 初回・停止後・ポインタ入力時だけ適用し、再生 tick では時刻の印だけを付ける。
986
1077
  function fragmentRootCoversContainer(element, container) {
987
1078
  const rootRect = element.getBoundingClientRect();
988
1079
  const containerRect = container.getBoundingClientRect();
@@ -1056,6 +1147,9 @@ function elementBoxCompanions(style, parentStyle, width, height) {
1056
1147
 
1057
1148
  for (const root of container.children) visit(root, null, true, true);
1058
1149
  hitPolicyAppliedContainers.add(container);
1150
+ const state = hitPolicyTimes.get(container);
1151
+ if (state) state.measured = state.current;
1152
+ staleHitPolicyContainers.delete(container);
1059
1153
  updateShapeLineHitProxyWidths();
1060
1154
  }
1061
1155
 
@@ -1169,6 +1263,34 @@ function elementBoxCompanions(style, parentStyle, width, height) {
1169
1263
  }
1170
1264
  }
1171
1265
 
1266
+ // 08-07 裁定: 背景は選択・削除できる。旧矩形フォールバックと違い、stage 面に
1267
+ // 落ちたクリックで、全面を覆って自ら塗るルートだけを拾う。実際のヒット先が
1268
+ // あるときは使わず 4680d90d8 の巻き込みを防ぎ、素通し規約は変えない。
1269
+ function paintedStageOverlayAt(clientX, clientY) {
1270
+ const candidates = [...stage.children]
1271
+ .flatMap((container, index) => {
1272
+ if (!isSelectable(container)) return [];
1273
+ const value = Number.parseFloat(getComputedStyle(container).zIndex);
1274
+ return [{ container, index, zIndex: Number.isFinite(value) ? value : 0 }];
1275
+ })
1276
+ .sort((a, b) => b.zIndex - a.zIndex || b.index - a.index);
1277
+
1278
+ for (const { container } of candidates) {
1279
+ for (const root of container.children) {
1280
+ if (root.hasAttribute("data-akari-interaction") || root.getAttribute("data-akari-hit") === "pass") continue;
1281
+ const style = getComputedStyle(root);
1282
+ if (style.display === "none" || Number(style.opacity) <= 0
1283
+ || ["hidden", "collapse"].includes(style.visibility)
1284
+ || !fragmentRootCoversContainer(root, container)
1285
+ || !drawsOwnContent(root, style)) continue;
1286
+ const rect = root.getBoundingClientRect();
1287
+ if (clientX >= rect.left && clientX <= rect.right
1288
+ && clientY >= rect.top && clientY <= rect.bottom) return container;
1289
+ }
1290
+ }
1291
+ return null;
1292
+ }
1293
+
1172
1294
  function overlayForEvent(event) {
1173
1295
  const eventTargetOverlay = findOverlayContainer(event.target);
1174
1296
  if (eventTargetOverlay && !isSelectable(eventTargetOverlay)) {
@@ -1180,6 +1302,11 @@ function elementBoxCompanions(style, parentStyle, width, height) {
1180
1302
  ) {
1181
1303
  return selftestOverlayOverride;
1182
1304
  }
1305
+ if (event.target === stage && !eventTargetOverlay
1306
+ && Number.isFinite(event.clientX) && Number.isFinite(event.clientY)) {
1307
+ const paintedOverlay = paintedStageOverlayAt(event.clientX, event.clientY);
1308
+ if (paintedOverlay) return paintedOverlay;
1309
+ }
1183
1310
  return isSelectable(eventTargetOverlay) ? eventTargetOverlay : null;
1184
1311
  }
1185
1312
 
@@ -3983,6 +4110,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
3983
4110
 
3984
4111
  function onPointerDown(event) {
3985
4112
  if (!interactionEnabled || !canBeginPointerInteraction(pointerOwner)) return;
4113
+ event = correctedHitPolicyEvent(event);
3986
4114
  if (event.button !== 0 || activeDrag || activeResize || activeRotate || activeLine) return;
3987
4115
  if (selectedId && stage && event.target instanceof Element) {
3988
4116
  const bounds = stage.getBoundingClientRect();
@@ -4833,6 +4961,7 @@ function elementBoxCompanions(style, parentStyle, width, height) {
4833
4961
 
4834
4962
  function onClick(event) {
4835
4963
  if (!interactionEnabled || activeEdit) return;
4964
+ event = correctedHitPolicyEvent(event);
4836
4965
  const hit = overlayForEvent(event);
4837
4966
  // pointerdown already toggled. Do not toggle twice or update the cycle clock.
4838
4967
  if (event.shiftKey && clickOrigin?.scopedHit) {
@@ -5329,11 +5458,73 @@ function elementBoxCompanions(style, parentStyle, width, height) {
5329
5458
  for (const type of ["pointerdown", "pointerup", "click", "dblclick"]) {
5330
5459
  window.addEventListener(type, passTransparentCanvasEvent, true);
5331
5460
  }
5461
+ const correctedHitPolicyEvents = new WeakMap();
5462
+ let redirectedHitPolicyClick = null;
5463
+ function pointerInsideStage(event) {
5464
+ if (!stage || !Number.isFinite(event.clientX) || !Number.isFinite(event.clientY)) return false;
5465
+ const rect = stage.getBoundingClientRect();
5466
+ return event.clientX >= rect.left && event.clientX <= rect.right
5467
+ && event.clientY >= rect.top && event.clientY <= rect.bottom;
5468
+ }
5469
+ function trustedEventAtTarget(event, target) {
5470
+ // 合成 Event は isTrusted=false になり E2 の焦点を失う。元の信頼済み
5471
+ // イベントを保ったまま、選択処理に見せる target だけ現在の hit-test にする。
5472
+ return new Proxy(event, { get(original, key) {
5473
+ if (key === 'target') return target;
5474
+ const value = Reflect.get(original, key, original);
5475
+ return typeof value === 'function' ? value.bind(original) : value;
5476
+ } });
5477
+ }
5478
+ function correctHitPolicyEvent(event, target) {
5479
+ correctedHitPolicyEvents.set(event, target);
5480
+ // Window capture の後続ホストにも素の HTML hit と同じ target を見せる。
5481
+ // Event 自体は元の信頼済みイベントなので、再生停止や混在選択は一度だけ走る。
5482
+ try { Object.defineProperty(event, 'target', { configurable: true, value: target }); }
5483
+ catch { /* document 側は WeakMap の target へフォールバックする。 */ }
5484
+ }
5485
+ function correctedHitPolicyEvent(event) {
5486
+ const target = correctedHitPolicyEvents.get(event);
5487
+ return target && event.target !== target ? trustedEventAtTarget(event, target) : event;
5488
+ }
5489
+ function stopCorrectedHitPolicyPropagation(event) {
5490
+ // window のホスト捕捉は通し、古い preview-stage への下流だけ止める。
5491
+ if (findOverlayContainer(correctedHitPolicyEvents.get(event))) event.stopPropagation();
5492
+ }
5493
+ window.addEventListener('pointermove', event => {
5494
+ if (!pointerInsideStage(event) || !staleHitPolicyContainers.size
5495
+ || hitPolicyOperationBusy()) return;
5496
+ const now = performance.now();
5497
+ if (now - lastPointerHitPolicyRefresh < 125) return;
5498
+ if (!refreshStaleHitPolicies()) return;
5499
+ lastPointerHitPolicyRefresh = now;
5500
+ const target = document.elementFromPoint(event.clientX, event.clientY);
5501
+ if (target && target !== event.target) correctHitPolicyEvent(event, target);
5502
+ }, true);
5503
+ window.addEventListener('pointerdown', event => {
5504
+ if (event.button !== 0 || !pointerInsideStage(event) || hitPolicyOperationBusy()) return;
5505
+ redirectedHitPolicyClick = null;
5506
+ if (!refreshStaleHitPolicies()) return;
5507
+ const target = document.elementFromPoint(event.clientX, event.clientY);
5508
+ if (!target || target === event.target) return;
5509
+ redirectedHitPolicyClick = { x: event.clientX, y: event.clientY };
5510
+ correctHitPolicyEvent(event, target);
5511
+ }, true);
5512
+ window.addEventListener('click', event => {
5513
+ if (!redirectedHitPolicyClick) return;
5514
+ const redirect = redirectedHitPolicyClick;
5515
+ redirectedHitPolicyClick = null;
5516
+ if (Math.hypot(event.clientX - redirect.x, event.clientY - redirect.y) > 6) return;
5517
+ const target = document.elementFromPoint(event.clientX, event.clientY);
5518
+ if (!target || target === event.target) return;
5519
+ correctHitPolicyEvent(event, target);
5520
+ }, true);
5332
5521
  listenerRoot.addEventListener('pointermove', updateShapeLineHitProxyWidths, true);
5333
5522
  listenerRoot.addEventListener('pointerdown', updateShapeLineHitProxyWidths, true);
5334
5523
  window.addEventListener('resize', updateShapeLineHitProxyWidths);
5335
5524
  listenerRoot.addEventListener("click", onClick, true);
5336
5525
  listenerRoot.addEventListener("pointerdown", onPointerDown, true);
5526
+ listenerRoot.addEventListener("click", stopCorrectedHitPolicyPropagation, true);
5527
+ listenerRoot.addEventListener("pointerdown", stopCorrectedHitPolicyPropagation, true);
5337
5528
  listenerRoot.addEventListener("dblclick", onDoubleClick, true);
5338
5529
  listenerRoot.addEventListener("blur", onBlur, true);
5339
5530
  listenerRoot.addEventListener("input", onEditableInput, true);
@@ -5444,6 +5635,8 @@ function elementBoxCompanions(style, parentStyle, width, height) {
5444
5635
  // ㉑ 素通し: overlay-runtime.js の tick() が可視化タイミングで呼ぶ。
5445
5636
  applyOverlayHitPolicy,
5446
5637
  invalidateOverlayHitPolicy,
5638
+ markOverlayHitPolicyStale,
5639
+ forgetOverlayHitPolicyTracking,
5447
5640
  syncOverlayHitRegion,
5448
5641
  // Web UI(preview-server)が編集モードを抜けるときに選択枠を畳むための公開口
5449
5642
  // (Phase 2-4 一本化。shell では未使用の追加 export で挙動不変)。
@@ -341,6 +341,7 @@ function createOverlayRuntime(options = {}) {
341
341
 
342
342
  function unmount() {
343
343
  for (const overlay of mountedOverlays) {
344
+ window.akari.interaction?.forgetOverlayHitPolicyTracking?.(overlay.container);
344
345
  for (const animation of overlay.animations ?? []) releaseAnimation(animation);
345
346
  for (const runtime of overlayRuntimes(overlay)) runtime.dispose(overlay.container);
346
347
  }
@@ -490,6 +491,7 @@ function createOverlayRuntime(options = {}) {
490
491
  runtimesVersion: runtimeRegistryVersion,
491
492
  keepLayoutWhenHidden,
492
493
  hitPolicyPending: false,
494
+ hitPolicyEntry: false,
493
495
  // getAnimations({ subtree: true }) の 250ms キャッシュ(tick() 参照)
494
496
  animations: undefined,
495
497
  animationsAt: 0,
@@ -574,6 +576,7 @@ function createOverlayRuntime(options = {}) {
574
576
  }
575
577
  mounted.container.style.setProperty("opacity", String(statics.opacity));
576
578
  mounted.hitPolicyPending = true;
579
+ mounted.hitPolicyEntry = true;
577
580
  mounted.statics = statics;
578
581
  mounted.keyframes = Array.isArray(value.keyframes) ? value.keyframes : undefined;
579
582
  mounted.fps = finiteNumber(summary?.output?.fps, 30);
@@ -595,6 +598,7 @@ function createOverlayRuntime(options = {}) {
595
598
  mounted.animationsAt = 0;
596
599
  mounted.animationEndTimes = [];
597
600
  mounted.hitPolicyPending = true;
601
+ mounted.hitPolicyEntry = true;
598
602
  return true;
599
603
  }
600
604
 
@@ -636,7 +640,14 @@ function createOverlayRuntime(options = {}) {
636
640
  // することで、実際に存在する CSS Animation を「今可視のオーバーレイ分だけ」に
637
641
  // 抑え、この地雷を踏まない。
638
642
  overlay.container.toggleAttribute("data-akari-active", visible);
643
+ if (!visible && !isPlaying && overlay.animations?.length) {
644
+ // 停止中の連続シークが同一描画フレームで外→内へ戻っても、CSS が
645
+ // inactive を一度観測するようにする。これが無いとゲートの off/on が
646
+ // 相殺され、新しい Animation が生成されず初期 opacity 0 に残る。
647
+ void overlay.container.offsetWidth;
648
+ }
639
649
  overlay.hitPolicyPending = visible;
650
+ overlay.hitPolicyEntry = visible;
640
651
  // getAnimations() のキャッシュ(下記)は可視化フリップで必ず捨てる。ゲート属性の付け外しで
641
652
  // CSS animation の顔ぶれが変わるため、可視化直後の tick は引き直す。非表示化でも捨て、
642
653
  // 非可視の間 Animation 参照を持ち越さない。
@@ -650,6 +661,7 @@ function createOverlayRuntime(options = {}) {
650
661
  overlay.animationsAt = 0;
651
662
  overlay.animationEndTimes = [];
652
663
  if (!visible) {
664
+ window.akari.interaction?.forgetOverlayHitPolicyTracking?.(overlay.container);
653
665
  if (!overlay.keepLayoutWhenHidden) {
654
666
  overlay.container.setAttribute(RUNTIME_HIDDEN_ATTRIBUTE, "");
655
667
  }
@@ -762,15 +774,18 @@ function createOverlayRuntime(options = {}) {
762
774
  runtime.render(overlay.container, localTimeMs / 1000,
763
775
  { syncVideos: true, maxRenderSize, playing: Boolean(playing) });
764
776
  }
765
- // opacity と clip-path は現在時刻へ合わせ、可視な間は入場アニメの終了まで毎 tick
766
- // 測り直す。フリップ時だけでは通常再生の localTimeMs がほぼ 0 となり、0% 姿勢の
767
- // bbox が焼き付くため。キャッシュした有限アニメの終端とローカル時刻で確定を判断する。
768
- // 終わった tick で確定する。以後は呼ばず、無限ループも終端無しとして数えないので、
769
- // 対象を可視オーバーレイだけにする性能原則「見えている分だけ」は維持される。
777
+ const ready = !isPlaying
778
+ ? overlay.animations.map(animation => pendingReady.get(animation)).filter(Boolean) : [];
779
+ window.akari.interaction?.markOverlayHitPolicyStale?.(
780
+ overlay.container, localTimeMs, !isPlaying, overlay.hitPolicyEntry,
781
+ ready.length ? Promise.allSettled(ready) : null);
782
+ overlay.hitPolicyEntry = false;
783
+ // 初回の仮判定と有限アニメ終端の確定判定だけをここで適用する。
784
+ // 途中の姿は上の時刻印を使い、停止後またはポインタ入力時に interaction が
785
+ // 必要な可視コンテナだけ測り直す。終端 tick が存在しない断片も停止後に更新される。
770
786
  if (overlay.hitPolicyPending) {
771
787
  window.akari.interaction?.syncOverlayHitRegion?.(overlay.container);
772
- // 当たり判定ポリシーは初回適用が WeakSet でガードされるため、ここでの再呼び出しは
773
- // 実質 no-op(暫定適用)。確定姿勢に達した tick で invalidate してから測り直す。
788
+ // 初回以外は WeakSet のガードで no-op。再入場時は時刻印側が解除する。
774
789
  window.akari.interaction?.applyOverlayHitPolicy?.(overlay.container);
775
790
  if (entryAnimationsSettled(overlay.animationEndTimes, localTimeMs)) {
776
791
  window.akari.interaction?.invalidateOverlayHitPolicy?.(overlay.container);