view-anchor 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +118 -34
  2. package/README.zh-CN.md +128 -44
  3. package/dist/index.d.ts +4 -16
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +2 -14
  6. package/dist/measure-loop.d.ts +9 -28
  7. package/dist/measure-loop.d.ts.map +1 -1
  8. package/dist/measure-loop.js +37 -16
  9. package/dist/protocol-publisher.d.ts +41 -0
  10. package/dist/protocol-publisher.d.ts.map +1 -0
  11. package/dist/protocol-publisher.js +191 -0
  12. package/dist/protocol-types.d.ts +36 -0
  13. package/dist/protocol-types.d.ts.map +1 -0
  14. package/dist/protocol-types.js +10 -0
  15. package/dist/protocol.d.ts +35 -0
  16. package/dist/protocol.d.ts.map +1 -0
  17. package/dist/protocol.js +131 -0
  18. package/dist/react.d.ts +18 -30
  19. package/dist/react.d.ts.map +1 -1
  20. package/dist/react.js +125 -122
  21. package/dist/size-advertiser.d.ts +9 -14
  22. package/dist/size-advertiser.d.ts.map +1 -1
  23. package/dist/size-advertiser.js +20 -28
  24. package/dist/types.d.ts +29 -73
  25. package/dist/types.d.ts.map +1 -1
  26. package/dist/types.js +1 -15
  27. package/dist/view-anchor.d.ts +36 -77
  28. package/dist/view-anchor.d.ts.map +1 -1
  29. package/dist/view-anchor.js +206 -174
  30. package/docs/bidirectional-design.md +64 -96
  31. package/docs/{anchor-3d.html → index.html} +215 -73
  32. package/docs/mechanism.mdx +55 -49
  33. package/docs/performance-report.md +63 -0
  34. package/docs/protocol.md +79 -0
  35. package/package.json +30 -4
  36. package/src/index.ts +6 -15
  37. package/src/measure-loop.ts +36 -41
  38. package/src/protocol-publisher.ts +236 -0
  39. package/src/protocol-types.ts +43 -0
  40. package/src/protocol.ts +193 -0
  41. package/src/react.ts +186 -141
  42. package/src/size-advertiser.ts +24 -31
  43. package/src/types.ts +34 -79
  44. package/src/view-anchor.ts +228 -212
@@ -1,9 +1,10 @@
1
1
  <!DOCTYPE html>
2
- <html lang="zh-CN">
2
+ <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>view-anchor —— 渲染进程 DOM 主进程 WebContentsView</title>
6
+ <title>view-anchor 3D demo: align an Electron WebContentsView, webview or iframe to a DOM element</title>
7
+ <meta name="description" content="view-anchor is a high-performance geometry bridge that keeps an Electron WebContentsView, native webview or cross-origin iframe aligned to a DOM element. This interactive 3D demo runs the real core in your browser.">
7
8
  <style>
8
9
  :root {
9
10
  --bg: hsl( 0 0% 7%);
@@ -189,22 +190,44 @@
189
190
 
190
191
  footer.note { margin-top: 22px; color: var(--text-3); font-size: 12px; line-height: 1.6; }
191
192
  footer.note code { color: var(--text-2); }
193
+
194
+ /* ---- landing intro ---- */
195
+ .badges { display: flex; gap: 8px; flex-wrap: wrap; margin: 0 0 16px; }
196
+ .badges img { display: block; }
197
+ .tag-en { color: var(--text-2); font-size: 14.5px; margin: 10px 0 0; max-width: 72ch; }
198
+ .actions { display: flex; gap: 18px; flex-wrap: wrap; margin-top: 12px; font-size: 13px; }
199
+ .actions a { color: var(--placeholder-bd); text-decoration: none; border-bottom: 1px solid hsl(249 100% 72% / 0.3); }
200
+ .actions a:hover { color: var(--text); border-bottom-color: var(--text-2); }
201
+ footer.note { display: flex; flex-direction: column; gap: 10px; }
202
+ footer.note .hint { color: var(--text-3); font-size: 12.5px; }
203
+
192
204
  </style>
193
205
  </head>
194
206
  <body>
195
207
  <div class="wrap">
196
208
  <header>
197
- <h1>view-anchor —— 原生视图贴住 DOM 矩形</h1>
198
- <p>绿色 <b style="color:var(--native-bd)">WebContentsView</b> 跟随蓝色<b style="color:var(--placeholder-bd)">占位 div</b>,由 view-anchor 实时驱动。</p>
209
+ <div class="badges">
210
+ <img alt="npm version" src="https://img.shields.io/npm/v/view-anchor">
211
+ <img alt="npm downloads" src="https://img.shields.io/npm/dm/view-anchor">
212
+ <img alt="License" src="https://img.shields.io/npm/l/view-anchor">
213
+ <img alt="Node" src="https://img.shields.io/badge/node-%3E%3D24-339933">
214
+ </div>
215
+ <h1>view-anchor: interactive 3D demo</h1>
216
+ <p class="tag-en">Align a native <b style="color:var(--native-bd)">WebContentsView</b> to a DOM element's geometry. Resize the splitter, toggle <code>present</code>, or explode the 3D scene below: the green view is positioned by the real view-anchor core running in this page.</p>
217
+ <div class="actions">
218
+ <a href="https://github.com/lbb00/view-anchor">GitHub</a>
219
+ <a href="https://www.npmjs.com/package/view-anchor">npm</a>
220
+ <a href="https://github.com/lbb00/view-anchor/blob/main/README.md">README</a>
221
+ </div>
199
222
  </header>
200
223
 
201
- <div class="stage-shell">
224
+ <div class="stage-shell">
202
225
  <div class="scene" id="scene">
203
226
  <div class="deck" id="deck">
204
- <!-- 底层:渲染进程 DOM 窗口 -->
227
+ <!-- bottom layer: the renderer / DOM window -->
205
228
  <div class="window">
206
229
  <div class="chrome-bar"><div class="traffic"></div></div>
207
- <div class="caption"><b>渲染进程 DOM</b></div>
230
+ <div class="caption"><b>Renderer DOM</b></div>
208
231
  <div class="dom-area">
209
232
  <div class="dom-sidebar"><i></i><i></i><i></i><i></i><i></i></div>
210
233
  <div class="placeholder" id="placeholder">
@@ -213,10 +236,10 @@
213
236
  </div>
214
237
  </div>
215
238
 
216
- <!-- 连接柱:沿 Z 轴从占位 div 指向上方的原生视图 -->
239
+ <!-- connector pillar: from the placeholder up to the native view along Z -->
217
240
  <div class="pillar" id="pillar"></div>
218
241
 
219
- <!-- 顶层:原生 WebContentsView,由 view-anchor 驱动 -->
242
+ <!-- top layer: the native WebContentsView, driven by view-anchor -->
220
243
  <div class="follower" id="follower">
221
244
  <div class="row-marker"></div>
222
245
  <div class="nv-label">WebContentsView</div>
@@ -233,31 +256,41 @@
233
256
  <label for="present"><code>present</code></label>
234
257
  </div>
235
258
  <div class="ctl">
236
- <label for="split">分栏</label>
259
+ <label for="split">Split</label>
237
260
  <input type="range" id="split" min="34" max="78" value="58">
238
261
  </div>
239
262
  <div class="ctl">
240
263
  <span class="switch">
241
- <input type="checkbox" id="explode">
264
+ <input type="checkbox" id="explode" checked>
242
265
  <span class="track"></span><span class="thumb"></span>
243
266
  </span>
244
- <label for="explode">拆开图层</label>
267
+ <label for="explode">Explode</label>
245
268
  </div>
246
269
  <div class="readout" id="readout"></div>
247
270
  </div>
248
271
 
249
272
  <div class="legend">
250
- <span><span class="sw r"></span> DOM 窗口</span>
251
- <span><span class="sw p"></span> 占位 div</span>
273
+ <span><span class="sw r"></span> DOM window</span>
274
+ <span><span class="sw p"></span> Placeholder div</span>
252
275
  <span><span class="sw n"></span> WebContentsView</span>
253
276
  </div>
254
277
  </div>
278
+
279
+ <footer class="note">
280
+ <div class="actions">
281
+ <a href="https://github.com/lbb00/view-anchor">GitHub repository</a>
282
+ <a href="https://www.npmjs.com/package/view-anchor">npm · view-anchor</a>
283
+ <span style="color:var(--text-3)">MIT licensed</span>
284
+ </div>
285
+ <div class="hint">Drag <code>Split</code> to resize the placeholder, toggle <code>present</code> to collapse or restore, and toggle <code>Explode</code> for the 3D perspective. The green view's bounds come directly from view-anchor's <code>publish</code> callback.</div>
286
+ </footer>
287
+
255
288
  </div>
256
289
 
257
- <!-- view-anchor 的真实构建产物,由 `pnpm -C packages/view-anchor build:docs`
258
- src/view-anchor.ts 用 esbuild 打包后内联到此处(不另写一份实现)。
259
- 内联而非 import 是因为本页要支持 file:// 直接打开,浏览器会把跨源 ES 模块
260
- import 拦掉。下面这段是生成的,请勿手改。 -->
290
+ <!-- Bootstrapped from src/view-anchor.ts by `pnpm build:docs` and inlined here
291
+ as an esbuild IIFE — this block is GENERATED, do not hand-edit. Inlined
292
+ rather than imported so the page can open from file://, where browsers
293
+ block cross-origin ES module imports. -->
261
294
  <script>
262
295
  /* __VIEW_ANCHOR_CORE_START__ — generated from src/view-anchor.ts by `pnpm build:docs`; do not edit */
263
296
  "use strict";
@@ -299,18 +332,33 @@ var __viewAnchorCore = (() => {
299
332
  let publish = opts.publish;
300
333
  let observer = null;
301
334
  let lastPublished = null;
335
+ let publicationRevision = 0;
302
336
  let disposed = false;
303
337
  const measure = () => {
304
338
  const r = target.getBoundingClientRect();
339
+ if (!Number.isFinite(r.left) || !Number.isFinite(r.top) || !Number.isFinite(r.width) || !Number.isFinite(r.height)) return null;
305
340
  return clampRect({ x: r.left, y: r.top, width: r.width, height: r.height });
306
341
  };
307
342
  const sameRect = (a, b) => a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
343
+ const publishCandidate = (candidate) => {
344
+ const previous = lastPublished;
345
+ const attempt = ++publicationRevision;
346
+ lastPublished = candidate;
347
+ try {
348
+ const accepted = publish(candidate) !== false;
349
+ if (!accepted && publicationRevision === attempt) lastPublished = previous;
350
+ return accepted;
351
+ } catch (error) {
352
+ if (publicationRevision === attempt) lastPublished = previous;
353
+ throw error;
354
+ }
355
+ };
308
356
  const emit = () => {
309
357
  if (disposed || !present) return;
310
358
  const m = measure();
359
+ if (!m) return;
311
360
  if (lastPublished && sameRect(lastPublished, m)) return;
312
- lastPublished = m;
313
- publish(m);
361
+ publishCandidate(m);
314
362
  };
315
363
  const startObserving = () => {
316
364
  if (observer) return;
@@ -329,11 +377,11 @@ var __viewAnchorCore = (() => {
329
377
  lastPublished = null;
330
378
  if (present) {
331
379
  startObserving();
332
- lastPublished = measure();
333
- publish(lastPublished);
380
+ const measured = measure();
381
+ if (measured) publishCandidate(measured);
334
382
  } else {
335
383
  stopObserving();
336
- publish(ZERO);
384
+ publishCandidate(ZERO);
337
385
  }
338
386
  };
339
387
  apply();
@@ -368,32 +416,54 @@ var __viewAnchorCore = (() => {
368
416
  function createPlacementAnchor(target, opts) {
369
417
  let visible = opts.visible;
370
418
  let publish = opts.publish;
371
- const guardDisplayNone = opts.guardDisplayNone ?? false;
372
- const followScroll = opts.followScroll ?? false;
373
- const followGeometry = opts.followGeometry ?? false;
419
+ let guardDisplayNone = opts.guardDisplayNone ?? false;
420
+ let followScroll = opts.followScroll ?? false;
421
+ let followGeometry = opts.followGeometry ?? false;
374
422
  let observer = null;
375
423
  let io = null;
424
+ let scrollListening = false;
425
+ let geometryListening = false;
426
+ const capture = { capture: true };
427
+ const passiveCapture = { capture: true, passive: true };
376
428
  let lastPublished = null;
429
+ let publicationRevision = 0;
377
430
  let disposed = false;
378
431
  let rafId = null;
379
432
  let steadyFrames = 0;
380
433
  const STEADY_CLOSE_FRAMES = 2;
381
434
  const MAX_HIDDEN_FOLLOW_FRAMES = 30;
435
+ const MAX_INVALID_FOLLOW_FRAMES = 30;
436
+ let invalidFrames = 0;
382
437
  let pointerHeld = false;
438
+ let activePointerId = null;
383
439
  let sentinelDeadline = null;
384
440
  const computePlacement = () => {
385
441
  const p = measurePlacement(target);
442
+ if (p.visible && (!Number.isFinite(p.bounds.x) || !Number.isFinite(p.bounds.y) || !Number.isFinite(p.bounds.width) || !Number.isFinite(p.bounds.height))) return null;
386
443
  if (guardDisplayNone && p.visible && (p.bounds.width === 0 || p.bounds.height === 0)) {
387
444
  return { visible: false };
388
445
  }
389
446
  return p;
390
447
  };
448
+ const publishCandidate = (candidate) => {
449
+ const previous = lastPublished;
450
+ const attempt = ++publicationRevision;
451
+ lastPublished = candidate;
452
+ try {
453
+ const accepted = publish(candidate) !== false;
454
+ if (!accepted && publicationRevision === attempt) lastPublished = previous;
455
+ return accepted;
456
+ } catch (error) {
457
+ if (publicationRevision === attempt) lastPublished = previous;
458
+ throw error;
459
+ }
460
+ };
391
461
  const emit = () => {
392
462
  if (disposed || !visible) return;
393
463
  const p = computePlacement();
464
+ if (!p) return;
394
465
  if (lastPublished && samePlacement(lastPublished, p)) return;
395
- lastPublished = p;
396
- publish(p);
466
+ publishCandidate(p);
397
467
  };
398
468
  const shouldCloseOnHiddenPoll = () => {
399
469
  if (lastPublished?.visible === false) return true;
@@ -410,12 +480,29 @@ var __viewAnchorCore = (() => {
410
480
  return;
411
481
  }
412
482
  const p = computePlacement();
483
+ if (!p) {
484
+ if (invalidFrames++ >= MAX_INVALID_FOLLOW_FRAMES) {
485
+ sentinelDeadline = null;
486
+ return;
487
+ }
488
+ if (!disposed && visible && followGeometry) {
489
+ rafId = requestAnimationFrame(sentinelFrame);
490
+ } else {
491
+ sentinelDeadline = null;
492
+ }
493
+ return;
494
+ }
495
+ invalidFrames = 0;
413
496
  if (!p.visible) {
414
497
  if (shouldCloseOnHiddenPoll()) {
415
498
  sentinelDeadline = null;
416
499
  return;
417
500
  }
418
- rafId = requestAnimationFrame(sentinelFrame);
501
+ if (!disposed && visible && followGeometry) {
502
+ rafId = requestAnimationFrame(sentinelFrame);
503
+ } else {
504
+ sentinelDeadline = null;
505
+ }
419
506
  return;
420
507
  }
421
508
  if (lastPublished && samePlacement(lastPublished, p)) {
@@ -425,11 +512,10 @@ var __viewAnchorCore = (() => {
425
512
  return;
426
513
  }
427
514
  } else {
428
- lastPublished = p;
429
- publish(p);
515
+ publishCandidate(p);
430
516
  steadyFrames = 0;
431
517
  }
432
- if (!disposed && visible) {
518
+ if (!disposed && visible && followGeometry) {
433
519
  rafId = requestAnimationFrame(sentinelFrame);
434
520
  } else {
435
521
  sentinelDeadline = null;
@@ -438,7 +524,10 @@ var __viewAnchorCore = (() => {
438
524
  const openSentinel = () => {
439
525
  if (!followGeometry || disposed) return;
440
526
  steadyFrames = 0;
441
- if (rafId === null) rafId = requestAnimationFrame(sentinelFrame);
527
+ if (rafId === null) {
528
+ invalidFrames = 0;
529
+ rafId = requestAnimationFrame(sentinelFrame);
530
+ }
442
531
  };
443
532
  const closeSentinel = () => {
444
533
  if (rafId !== null) {
@@ -446,8 +535,10 @@ var __viewAnchorCore = (() => {
446
535
  rafId = null;
447
536
  }
448
537
  steadyFrames = 0;
538
+ invalidFrames = 0;
449
539
  sentinelDeadline = null;
450
540
  pointerHeld = false;
541
+ activePointerId = null;
451
542
  };
452
543
  const onScroll = () => {
453
544
  if (followGeometry) openSentinel();
@@ -456,67 +547,105 @@ var __viewAnchorCore = (() => {
456
547
  const onPointerDown = (e) => {
457
548
  const t = e.target;
458
549
  if (t && t.closest && t.closest('[role="separator"]')) {
550
+ if (!pointerHeld) activePointerId = e.pointerId;
459
551
  pointerHeld = true;
460
552
  openSentinel();
461
553
  }
462
554
  };
463
- const onPointerUp = () => {
555
+ const releasePointer = (e) => {
464
556
  if (!pointerHeld) return;
557
+ if (e && activePointerId !== e.pointerId) return;
465
558
  pointerHeld = false;
559
+ activePointerId = null;
466
560
  openSentinel();
467
561
  };
468
- const startObserving = () => {
469
- if (observer) return;
470
- observer = new ResizeObserver(emit);
471
- observer.observe(target);
472
- window.addEventListener("resize", emit);
473
- if (guardDisplayNone && typeof IntersectionObserver !== "undefined") {
562
+ const onPointerUp = (e) => {
563
+ releasePointer(e);
564
+ };
565
+ const onPointerCancel = (e) => {
566
+ releasePointer(e);
567
+ };
568
+ const onWindowBlur = () => {
569
+ releasePointer();
570
+ };
571
+ const startOptionalObserving = () => {
572
+ if (guardDisplayNone && !io && typeof IntersectionObserver !== "undefined") {
474
573
  io = new IntersectionObserver(emit);
475
574
  io.observe(target);
476
575
  }
477
- if (followScroll) {
478
- window.addEventListener("scroll", onScroll, {
479
- capture: true,
480
- passive: true
481
- });
576
+ if (followScroll && !scrollListening) {
577
+ window.addEventListener("scroll", onScroll, passiveCapture);
578
+ scrollListening = true;
482
579
  }
483
- if (followGeometry) {
484
- window.addEventListener("pointerdown", onPointerDown, { capture: true });
485
- window.addEventListener("pointerup", onPointerUp, { capture: true });
580
+ if (followGeometry && !geometryListening) {
581
+ window.addEventListener("pointerdown", onPointerDown, capture);
582
+ window.addEventListener("pointerup", onPointerUp, capture);
583
+ window.addEventListener("pointercancel", onPointerCancel, capture);
584
+ window.addEventListener("blur", onWindowBlur);
585
+ geometryListening = true;
486
586
  }
487
587
  };
488
- const stopObserving = () => {
489
- if (observer) {
490
- observer.disconnect();
491
- observer = null;
588
+ const stopOptionalObserving = () => {
589
+ if (io && !guardDisplayNone) {
590
+ io.disconnect();
591
+ io = null;
492
592
  }
593
+ if (scrollListening && !followScroll) {
594
+ window.removeEventListener("scroll", onScroll, passiveCapture);
595
+ scrollListening = false;
596
+ }
597
+ if (geometryListening && !followGeometry) {
598
+ window.removeEventListener("pointerdown", onPointerDown, capture);
599
+ window.removeEventListener("pointerup", onPointerUp, capture);
600
+ window.removeEventListener("pointercancel", onPointerCancel, capture);
601
+ window.removeEventListener("blur", onWindowBlur);
602
+ geometryListening = false;
603
+ closeSentinel();
604
+ }
605
+ };
606
+ const stopAllOptionalObserving = () => {
493
607
  if (io) {
494
608
  io.disconnect();
495
609
  io = null;
496
610
  }
497
- window.removeEventListener("resize", emit);
498
- window.removeEventListener("scroll", onScroll, {
499
- capture: true
500
- });
501
- window.removeEventListener("pointerdown", onPointerDown, {
502
- capture: true
503
- });
504
- window.removeEventListener("pointerup", onPointerUp, {
505
- capture: true
506
- });
611
+ if (scrollListening) {
612
+ window.removeEventListener("scroll", onScroll, passiveCapture);
613
+ scrollListening = false;
614
+ }
615
+ if (geometryListening) {
616
+ window.removeEventListener("pointerdown", onPointerDown, capture);
617
+ window.removeEventListener("pointerup", onPointerUp, capture);
618
+ window.removeEventListener("pointercancel", onPointerCancel, capture);
619
+ window.removeEventListener("blur", onWindowBlur);
620
+ geometryListening = false;
621
+ }
507
622
  closeSentinel();
508
623
  };
624
+ const startObserving = () => {
625
+ if (observer) return;
626
+ observer = new ResizeObserver(emit);
627
+ observer.observe(target);
628
+ window.addEventListener("resize", emit);
629
+ startOptionalObserving();
630
+ };
631
+ const stopObserving = () => {
632
+ if (observer) {
633
+ observer.disconnect();
634
+ observer = null;
635
+ }
636
+ window.removeEventListener("resize", emit);
637
+ stopAllOptionalObserving();
638
+ };
509
639
  const apply = () => {
510
640
  lastPublished = null;
511
641
  if (visible) {
512
642
  startObserving();
513
- lastPublished = computePlacement();
514
- publish(lastPublished);
643
+ const placement = computePlacement();
644
+ if (placement) publishCandidate(placement);
515
645
  } else {
516
646
  stopObserving();
517
647
  const hidden = { visible: false };
518
- lastPublished = hidden;
519
- publish(hidden);
648
+ publishCandidate(hidden);
520
649
  }
521
650
  };
522
651
  apply();
@@ -525,6 +654,13 @@ var __viewAnchorCore = (() => {
525
654
  if (disposed) return;
526
655
  publish = next.publish;
527
656
  visible = next.visible;
657
+ guardDisplayNone = next.guardDisplayNone ?? guardDisplayNone;
658
+ followScroll = next.followScroll ?? followScroll;
659
+ followGeometry = next.followGeometry ?? followGeometry;
660
+ if (visible && observer) {
661
+ stopOptionalObserving();
662
+ startOptionalObserving();
663
+ }
528
664
  apply();
529
665
  },
530
666
  dispose() {
@@ -562,16 +698,18 @@ var __viewAnchorCore = (() => {
562
698
  const scene = document.getElementById('scene')
563
699
  const readout = document.getElementById('readout')
564
700
 
565
- // 占位 div deck 内的布局矩形(沿 offsetParent 链累加,与 3D 变换无关)——
566
- // 这样无论平铺还是倾斜,绿色视图都摆在占位 div 正上方,对齐不漂。
701
+ // The placeholder's rect inside the deck (accumulated along the offsetParent
702
+ // chain, independent of the 3D transform) — flat or tilted, the native view
703
+ // stays registered directly above the placeholder.
567
704
  function deckLocalRect(el) {
568
705
  let x = 0, y = 0, n = el
569
706
  while (n && n !== deck) { x += n.offsetLeft; y += n.offsetTop; n = n.offsetParent }
570
707
  return { x, y, width: el.offsetWidth, height: el.offsetHeight }
571
708
  }
572
709
 
573
- // 这是宿主侧的 publish —— view-anchor 把测量到的矩形交给它。真实应用里它会走
574
- // IPC → WebContentsView.setBounds;这里把绿色视图摆到 deck 局部坐标的对应位置。
710
+ // The host-side publish view-anchor hands it the measured rect. A real app
711
+ // would go IPC → WebContentsView.setBounds; here we place the view in the
712
+ // deck's local coordinates.
575
713
  function publish(bounds) {
576
714
  if (bounds.width === 0 || bounds.height === 0) {
577
715
  follower.classList.add('collapsed')
@@ -584,7 +722,8 @@ var __viewAnchorCore = (() => {
584
722
  follower.style.top = r.y + 'px'
585
723
  follower.style.width = r.width + 'px'
586
724
  follower.style.height = r.height + 'px'
587
- // 连接柱立在占位 div 中线,沿 Z 伸到原生视图所在高度(120px)。
725
+ // Pillar stands on the placeholder's centerline, rising along Z to the
726
+ // view's height (120px).
588
727
  pillar.style.left = (r.x + r.width / 2) + 'px'
589
728
  pillar.style.top = (r.y + r.height / 2) + 'px'
590
729
  pillar.style.height = '120px'
@@ -595,10 +734,10 @@ var __viewAnchorCore = (() => {
595
734
  readout.innerHTML =
596
735
  '<span class="k">publish</span>({ x:' + b.x + ', y:' + b.y +
597
736
  ', w:' + b.width + ', h:' + b.height + ' })' +
598
- (present ? '' : ' <span class="k">// 已摘除</span>')
737
+ (present ? '' : ' <span class="k">// detached</span>')
599
738
  }
600
739
 
601
- // 启动真实 anchor。之后分栏 resize 全由 view-anchor 自己的 ResizeObserver 驱动。
740
+ // Start the real anchor; split resizes are driven by view-anchor's own ResizeObserver.
602
741
  const handle = createViewAnchor(placeholder, { present: presentEl.checked, publish })
603
742
 
604
743
  presentEl.addEventListener('change', () => {
@@ -607,6 +746,9 @@ var __viewAnchorCore = (() => {
607
746
  splitEl.addEventListener('input', () => {
608
747
  placeholder.style.flexBasis = splitEl.value + '%'
609
748
  })
749
+ // Explode defaults ON: apply the exploded pose at boot — uncheck to see the flat view.
750
+ if (explodeEl.checked) scene.classList.add('exploded')
751
+
610
752
  explodeEl.addEventListener('change', () => {
611
753
  scene.classList.toggle('exploded', explodeEl.checked)
612
754
  })