@dimina-kit/view-anchor 0.1.0-dev.20260610082053
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/LICENSE +21 -0
- package/README.md +87 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/measure-loop.d.ts +43 -0
- package/dist/measure-loop.d.ts.map +1 -0
- package/dist/measure-loop.js +49 -0
- package/dist/react.d.ts +38 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +145 -0
- package/dist/size-advertiser.d.ts +20 -0
- package/dist/size-advertiser.d.ts.map +1 -0
- package/dist/size-advertiser.js +82 -0
- package/dist/types.d.ts +111 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +18 -0
- package/dist/view-anchor.d.ts +111 -0
- package/dist/view-anchor.d.ts.map +1 -0
- package/dist/view-anchor.js +396 -0
- package/docs/anchor-3d.html +602 -0
- package/docs/bidirectional-design.md +151 -0
- package/docs/mechanism.mdx +96 -0
- package/package.json +76 -0
- package/src/follow-geometry-press-drag.fix.test.ts +255 -0
- package/src/index.ts +39 -0
- package/src/measure-loop.ts +87 -0
- package/src/placement.test.ts +338 -0
- package/src/react.test.tsx +566 -0
- package/src/react.ts +171 -0
- package/src/size-advertiser.extras.test.ts +294 -0
- package/src/size-advertiser.test.ts +444 -0
- package/src/size-advertiser.ts +94 -0
- package/src/types.ts +123 -0
- package/src/view-anchor.test.ts +1184 -0
- package/src/view-anchor.ts +486 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>view-anchor —— 渲染进程 DOM → 主进程 WebContentsView</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: hsl( 0 0% 7%);
|
|
10
|
+
--bg-soft: hsl( 0 0% 11%);
|
|
11
|
+
--text: hsl( 0 0% 95%);
|
|
12
|
+
--text-2: hsl( 0 0% 68%);
|
|
13
|
+
--text-3: hsl( 0 0% 48%);
|
|
14
|
+
|
|
15
|
+
--renderer: hsl(220 16% 16%);
|
|
16
|
+
--renderer-bd: hsl(220 22% 56%);
|
|
17
|
+
--placeholder: hsl(249 100% 67% / 0.18);
|
|
18
|
+
--placeholder-bd: hsl(249 100% 72%);
|
|
19
|
+
--native: hsl(160 40% 16%);
|
|
20
|
+
--native-bd: hsl(160 72% 58%);
|
|
21
|
+
--wire: hsl(160 72% 60%);
|
|
22
|
+
--accent: hsl(249 100% 70%);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
* { box-sizing: border-box; }
|
|
26
|
+
html, body {
|
|
27
|
+
margin: 0; padding: 0;
|
|
28
|
+
background: radial-gradient(120% 120% at 50% 0%, hsl(0 0% 10%) 0%, var(--bg) 60%);
|
|
29
|
+
color: var(--text);
|
|
30
|
+
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
|
31
|
+
font-size: 14px; line-height: 1.5;
|
|
32
|
+
-webkit-font-smoothing: antialiased;
|
|
33
|
+
min-height: 100vh;
|
|
34
|
+
}
|
|
35
|
+
code, .mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; }
|
|
36
|
+
|
|
37
|
+
.wrap { max-width: 1040px; margin: 0 auto; padding: 32px 28px 56px; }
|
|
38
|
+
header h1 { font-size: 19px; font-weight: 600; margin: 0 0 4px; letter-spacing: 0.01em; }
|
|
39
|
+
header p { margin: 0; color: var(--text-2); font-size: 13.5px; max-width: 72ch; }
|
|
40
|
+
|
|
41
|
+
.stage-shell {
|
|
42
|
+
margin-top: 26px;
|
|
43
|
+
background: var(--bg-soft);
|
|
44
|
+
border: 1px solid hsl(0 0% 18%);
|
|
45
|
+
border-radius: 16px;
|
|
46
|
+
padding: 30px 24px 20px;
|
|
47
|
+
box-shadow: 0 24px 60px hsl(0 0% 0% / 0.45);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* ---- 3D scene ---- */
|
|
51
|
+
.scene {
|
|
52
|
+
position: relative;
|
|
53
|
+
height: 380px;
|
|
54
|
+
perspective: 1500px;
|
|
55
|
+
perspective-origin: 50% 34%;
|
|
56
|
+
display: flex; align-items: center; justify-content: center;
|
|
57
|
+
}
|
|
58
|
+
/* the deck tilts as a whole on "explode" — both the DOM window and the
|
|
59
|
+
native view ride it, so they stay in registration in the 3D side view. */
|
|
60
|
+
.deck {
|
|
61
|
+
position: relative;
|
|
62
|
+
width: 560px; height: 300px;
|
|
63
|
+
transform-style: preserve-3d;
|
|
64
|
+
transition: transform 900ms cubic-bezier(0.6, 0, 0.2, 1);
|
|
65
|
+
}
|
|
66
|
+
.scene.exploded .deck { transform: rotateX(56deg) rotateZ(-30deg); }
|
|
67
|
+
|
|
68
|
+
/* bottom layer: renderer / DOM window (web content). Stays flat within the
|
|
69
|
+
deck (translateZ 0); view-anchor measures the placeholder inside it. */
|
|
70
|
+
.window {
|
|
71
|
+
position: absolute; inset: 0;
|
|
72
|
+
background: var(--renderer);
|
|
73
|
+
border: 1.5px solid var(--renderer-bd);
|
|
74
|
+
border-radius: 10px;
|
|
75
|
+
box-shadow: 0 22px 50px hsl(0 0% 0% / 0.5);
|
|
76
|
+
transform: translateZ(0);
|
|
77
|
+
}
|
|
78
|
+
.window .caption {
|
|
79
|
+
position: absolute; left: 14px; top: 34px;
|
|
80
|
+
font-size: 11.5px; letter-spacing: 0.03em; color: var(--text-2); pointer-events: none;
|
|
81
|
+
}
|
|
82
|
+
.window .caption b { color: var(--text); font-weight: 600; }
|
|
83
|
+
|
|
84
|
+
.chrome-bar {
|
|
85
|
+
position: absolute; left: 0; right: 0; top: 0; height: 26px;
|
|
86
|
+
background: hsl(0 0% 10%); border-bottom: 1px solid hsl(0 0% 22%);
|
|
87
|
+
border-radius: 10px 10px 0 0;
|
|
88
|
+
}
|
|
89
|
+
.traffic {
|
|
90
|
+
position: absolute; left: 12px; top: 9px; width: 42px; height: 8px;
|
|
91
|
+
background:
|
|
92
|
+
radial-gradient(circle, hsl(0 70% 60%) 0 3.4px, transparent 3.8px) 0 0/14px 8px no-repeat,
|
|
93
|
+
radial-gradient(circle, hsl(45 80% 55%) 0 3.4px, transparent 3.8px) 14px 0/14px 8px no-repeat,
|
|
94
|
+
radial-gradient(circle, hsl(135 50% 55%) 0 3.4px, transparent 3.8px) 28px 0/14px 8px no-repeat;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.dom-area { position: absolute; left: 10px; right: 10px; top: 56px; bottom: 12px; display: flex; gap: 8px; }
|
|
98
|
+
.dom-sidebar {
|
|
99
|
+
flex: 1 1 auto; min-width: 64px;
|
|
100
|
+
background: hsl(0 0% 14%); border: 1px solid hsl(0 0% 22%); border-radius: 6px;
|
|
101
|
+
display: flex; flex-direction: column; gap: 6px; padding: 8px;
|
|
102
|
+
}
|
|
103
|
+
.dom-sidebar i { display: block; height: 9px; border-radius: 3px; background: hsl(0 0% 24%); }
|
|
104
|
+
.dom-sidebar i:nth-child(2) { width: 80%; } .dom-sidebar i:nth-child(3){ width: 60%; }
|
|
105
|
+
.dom-sidebar i:nth-child(4) { width: 72%; } .dom-sidebar i:nth-child(5){ width: 45%; }
|
|
106
|
+
|
|
107
|
+
.placeholder {
|
|
108
|
+
flex: 0 0 58%;
|
|
109
|
+
position: relative;
|
|
110
|
+
border-radius: 6px;
|
|
111
|
+
background: var(--placeholder);
|
|
112
|
+
border: 1.5px dashed var(--placeholder-bd);
|
|
113
|
+
display: flex; align-items: center; justify-content: center;
|
|
114
|
+
/* no transition: track the slider 1:1 so the anchored view stays glued */
|
|
115
|
+
}
|
|
116
|
+
.placeholder .ph-tag { font-size: 11px; color: var(--placeholder-bd); text-align: center; padding: 0 8px; }
|
|
117
|
+
.placeholder .ph-tag small { color: var(--text-3); display:block; margin-top:2px; font-size:10px; }
|
|
118
|
+
|
|
119
|
+
/* top layer: native WebContentsView. A direct child of the deck so it lives
|
|
120
|
+
in the same 3D space; lifted in Z on explode to float above the DOM. */
|
|
121
|
+
.follower {
|
|
122
|
+
position: absolute;
|
|
123
|
+
border-radius: 6px;
|
|
124
|
+
background: linear-gradient(180deg, hsl(160 40% 20%), hsl(160 38% 13%));
|
|
125
|
+
border: 1.5px solid var(--native-bd);
|
|
126
|
+
box-shadow: 0 0 0 1px hsl(160 72% 60% / 0.25), inset 0 0 24px hsl(160 72% 50% / 0.12);
|
|
127
|
+
/* position/size snap each frame (like a real setBounds) — only the
|
|
128
|
+
explode lift + collapse fade animate; otherwise tracking rubber-bands. */
|
|
129
|
+
transition: transform 900ms cubic-bezier(0.6,0,0.2,1), box-shadow 900ms, opacity 300ms;
|
|
130
|
+
overflow: hidden;
|
|
131
|
+
display: flex; align-items: center; justify-content: center;
|
|
132
|
+
transform: translateZ(0.5px);
|
|
133
|
+
}
|
|
134
|
+
.follower .nv-label { font-size: 11px; color: var(--native-bd); letter-spacing: 0.02em; text-align:center; }
|
|
135
|
+
.follower .nv-label small { display:block; color: hsl(160 30% 62%); font-size:10px; margin-top:2px; }
|
|
136
|
+
.follower .devtools-row {
|
|
137
|
+
position:absolute; left:0; right:0; top:0; height:18px; background: hsl(160 30% 22% / 0.55);
|
|
138
|
+
border-bottom:1px solid hsl(160 50% 40% / 0.4);
|
|
139
|
+
}
|
|
140
|
+
.scene.exploded .follower {
|
|
141
|
+
transform: translateZ(120px);
|
|
142
|
+
box-shadow: 0 0 0 1px hsl(160 72% 60% / 0.4), 0 30px 60px hsl(0 0% 0% / 0.55),
|
|
143
|
+
0 0 44px hsl(160 72% 50% / 0.22);
|
|
144
|
+
}
|
|
145
|
+
.follower.collapsed { opacity: 0 !important; }
|
|
146
|
+
|
|
147
|
+
/* connector pillar standing along Z from the placeholder up to the view */
|
|
148
|
+
.pillar {
|
|
149
|
+
position: absolute; width: 0;
|
|
150
|
+
border-left: 1.5px dashed var(--native-bd);
|
|
151
|
+
transform-origin: top center;
|
|
152
|
+
transform: rotateX(-90deg);
|
|
153
|
+
opacity: 0; transition: opacity 400ms ease;
|
|
154
|
+
pointer-events: none;
|
|
155
|
+
}
|
|
156
|
+
.scene.exploded .pillar { opacity: 0.5; }
|
|
157
|
+
|
|
158
|
+
/* ---- controls ---- */
|
|
159
|
+
.controls {
|
|
160
|
+
display: flex; flex-wrap: wrap; align-items: center; gap: 18px;
|
|
161
|
+
margin-top: 22px; padding-top: 18px; border-top: 1px solid hsl(0 0% 17%);
|
|
162
|
+
}
|
|
163
|
+
.ctl { display: flex; align-items: center; gap: 9px; font-size: 12.5px; color: var(--text-2); }
|
|
164
|
+
.ctl label { user-select: none; }
|
|
165
|
+
|
|
166
|
+
.switch { position: relative; width: 42px; height: 24px; flex: none; }
|
|
167
|
+
.switch input { opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
|
|
168
|
+
.switch .track { position: absolute; inset: 0; border-radius: 999px; background: hsl(0 0% 24%); transition: background 200ms ease; pointer-events: none; }
|
|
169
|
+
.switch .thumb { position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%; background: hsl(0 0% 92%); transition: transform 200ms ease; pointer-events: none; }
|
|
170
|
+
.switch input:checked ~ .track { background: var(--accent); }
|
|
171
|
+
.switch input:checked ~ .thumb { transform: translateX(18px); }
|
|
172
|
+
|
|
173
|
+
input[type=range] { accent-color: var(--accent); width: 150px; }
|
|
174
|
+
|
|
175
|
+
.readout {
|
|
176
|
+
margin-left: auto;
|
|
177
|
+
font-family: ui-monospace, Menlo, monospace; font-size: 12px; color: var(--wire);
|
|
178
|
+
background: hsl(160 40% 12% / 0.5); border: 1px solid hsl(160 50% 40% / 0.35);
|
|
179
|
+
border-radius: 8px; padding: 8px 12px; min-width: 232px; text-align: center;
|
|
180
|
+
}
|
|
181
|
+
.readout .k { color: var(--text-3); }
|
|
182
|
+
|
|
183
|
+
.legend { display: flex; gap: 20px; flex-wrap: wrap; margin-top: 18px; font-size: 12px; color: var(--text-2); }
|
|
184
|
+
.legend span { display: inline-flex; align-items: center; gap: 7px; }
|
|
185
|
+
.sw { width: 13px; height: 13px; border-radius: 3px; flex: none; }
|
|
186
|
+
.sw.r { background: var(--renderer); border: 1.5px solid var(--renderer-bd); }
|
|
187
|
+
.sw.p { background: var(--placeholder); border: 1.5px dashed var(--placeholder-bd); }
|
|
188
|
+
.sw.n { background: var(--native); border: 1.5px solid var(--native-bd); }
|
|
189
|
+
|
|
190
|
+
footer.note { margin-top: 22px; color: var(--text-3); font-size: 12px; line-height: 1.6; }
|
|
191
|
+
footer.note code { color: var(--text-2); }
|
|
192
|
+
</style>
|
|
193
|
+
</head>
|
|
194
|
+
<body>
|
|
195
|
+
<div class="wrap">
|
|
196
|
+
<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>
|
|
199
|
+
</header>
|
|
200
|
+
|
|
201
|
+
<div class="stage-shell">
|
|
202
|
+
<div class="scene" id="scene">
|
|
203
|
+
<div class="deck" id="deck">
|
|
204
|
+
<!-- 底层:渲染进程 DOM 窗口 -->
|
|
205
|
+
<div class="window">
|
|
206
|
+
<div class="chrome-bar"><div class="traffic"></div></div>
|
|
207
|
+
<div class="caption"><b>渲染进程 DOM</b></div>
|
|
208
|
+
<div class="dom-area">
|
|
209
|
+
<div class="dom-sidebar"><i></i><i></i><i></i><i></i><i></i></div>
|
|
210
|
+
<div class="placeholder" id="placeholder">
|
|
211
|
+
<div class="ph-tag"><div ref={anchorRef} /></div>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<!-- 连接柱:沿 Z 轴从占位 div 指向上方的原生视图 -->
|
|
217
|
+
<div class="pillar" id="pillar"></div>
|
|
218
|
+
|
|
219
|
+
<!-- 顶层:原生 WebContentsView,由 view-anchor 驱动 -->
|
|
220
|
+
<div class="follower" id="follower">
|
|
221
|
+
<div class="devtools-row"></div>
|
|
222
|
+
<div class="nv-label">WebContentsView</div>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<div class="controls">
|
|
228
|
+
<div class="ctl">
|
|
229
|
+
<span class="switch">
|
|
230
|
+
<input type="checkbox" id="present" checked>
|
|
231
|
+
<span class="track"></span><span class="thumb"></span>
|
|
232
|
+
</span>
|
|
233
|
+
<label for="present"><code>present</code></label>
|
|
234
|
+
</div>
|
|
235
|
+
<div class="ctl">
|
|
236
|
+
<label for="split">分栏</label>
|
|
237
|
+
<input type="range" id="split" min="34" max="78" value="58">
|
|
238
|
+
</div>
|
|
239
|
+
<div class="ctl">
|
|
240
|
+
<span class="switch">
|
|
241
|
+
<input type="checkbox" id="explode">
|
|
242
|
+
<span class="track"></span><span class="thumb"></span>
|
|
243
|
+
</span>
|
|
244
|
+
<label for="explode">拆开图层</label>
|
|
245
|
+
</div>
|
|
246
|
+
<div class="readout" id="readout"></div>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<div class="legend">
|
|
250
|
+
<span><span class="sw r"></span> DOM 窗口</span>
|
|
251
|
+
<span><span class="sw p"></span> 占位 div</span>
|
|
252
|
+
<span><span class="sw n"></span> WebContentsView</span>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<!-- view-anchor 的真实构建产物,由 `pnpm -C packages/view-anchor build:docs`
|
|
258
|
+
从 src/view-anchor.ts 用 esbuild 打包后内联到此处(不另写一份实现)。
|
|
259
|
+
内联而非 import 是因为本页要支持 file:// 直接打开,浏览器会把跨源 ES 模块
|
|
260
|
+
import 拦掉。下面这段是生成的,请勿手改。 -->
|
|
261
|
+
<script>
|
|
262
|
+
/* __VIEW_ANCHOR_CORE_START__ — generated from src/view-anchor.ts by `pnpm build:docs`; do not edit */
|
|
263
|
+
"use strict";
|
|
264
|
+
var __viewAnchorCore = (() => {
|
|
265
|
+
var __defProp = Object.defineProperty;
|
|
266
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
267
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
268
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
269
|
+
var __export = (target, all) => {
|
|
270
|
+
for (var name in all)
|
|
271
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
272
|
+
};
|
|
273
|
+
var __copyProps = (to, from, except, desc) => {
|
|
274
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
275
|
+
for (let key of __getOwnPropNames(from))
|
|
276
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
277
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
278
|
+
}
|
|
279
|
+
return to;
|
|
280
|
+
};
|
|
281
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
282
|
+
|
|
283
|
+
// src/view-anchor.ts
|
|
284
|
+
var view_anchor_exports = {};
|
|
285
|
+
__export(view_anchor_exports, {
|
|
286
|
+
createPlacementAnchor: () => createPlacementAnchor,
|
|
287
|
+
createViewAnchor: () => createViewAnchor,
|
|
288
|
+
measurePlacement: () => measurePlacement
|
|
289
|
+
});
|
|
290
|
+
var ZERO = { x: 0, y: 0, width: 0, height: 0 };
|
|
291
|
+
var clampRect = (r) => ({
|
|
292
|
+
x: Math.round(r.x),
|
|
293
|
+
y: Math.round(r.y),
|
|
294
|
+
width: Math.max(0, Math.round(r.width)),
|
|
295
|
+
height: Math.max(0, Math.round(r.height))
|
|
296
|
+
});
|
|
297
|
+
function createViewAnchor(target, opts) {
|
|
298
|
+
let present = opts.present;
|
|
299
|
+
let publish = opts.publish;
|
|
300
|
+
let observer = null;
|
|
301
|
+
let lastPublished = null;
|
|
302
|
+
let disposed = false;
|
|
303
|
+
const measure = () => {
|
|
304
|
+
const r = target.getBoundingClientRect();
|
|
305
|
+
return clampRect({ x: r.left, y: r.top, width: r.width, height: r.height });
|
|
306
|
+
};
|
|
307
|
+
const sameRect = (a, b) => a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
|
|
308
|
+
const emit = () => {
|
|
309
|
+
if (disposed || !present) return;
|
|
310
|
+
const m = measure();
|
|
311
|
+
if (lastPublished && sameRect(lastPublished, m)) return;
|
|
312
|
+
lastPublished = m;
|
|
313
|
+
publish(m);
|
|
314
|
+
};
|
|
315
|
+
const startObserving = () => {
|
|
316
|
+
if (observer) return;
|
|
317
|
+
observer = new ResizeObserver(emit);
|
|
318
|
+
observer.observe(target);
|
|
319
|
+
window.addEventListener("resize", emit);
|
|
320
|
+
};
|
|
321
|
+
const stopObserving = () => {
|
|
322
|
+
if (observer) {
|
|
323
|
+
observer.disconnect();
|
|
324
|
+
observer = null;
|
|
325
|
+
}
|
|
326
|
+
window.removeEventListener("resize", emit);
|
|
327
|
+
};
|
|
328
|
+
const apply = () => {
|
|
329
|
+
lastPublished = null;
|
|
330
|
+
if (present) {
|
|
331
|
+
startObserving();
|
|
332
|
+
lastPublished = measure();
|
|
333
|
+
publish(lastPublished);
|
|
334
|
+
} else {
|
|
335
|
+
stopObserving();
|
|
336
|
+
publish(ZERO);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
apply();
|
|
340
|
+
return {
|
|
341
|
+
update(next) {
|
|
342
|
+
if (disposed) return;
|
|
343
|
+
publish = next.publish;
|
|
344
|
+
present = next.present;
|
|
345
|
+
apply();
|
|
346
|
+
},
|
|
347
|
+
dispose() {
|
|
348
|
+
if (disposed) return;
|
|
349
|
+
disposed = true;
|
|
350
|
+
stopObserving();
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function measurePlacement(target) {
|
|
355
|
+
const r = target.getBoundingClientRect();
|
|
356
|
+
return {
|
|
357
|
+
visible: true,
|
|
358
|
+
bounds: clampRect({ x: r.left, y: r.top, width: r.width, height: r.height })
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
var samePlacement = (a, b) => {
|
|
362
|
+
if (a.visible !== b.visible) return false;
|
|
363
|
+
if (a.visible && b.visible) {
|
|
364
|
+
return a.bounds.x === b.bounds.x && a.bounds.y === b.bounds.y && a.bounds.width === b.bounds.width && a.bounds.height === b.bounds.height;
|
|
365
|
+
}
|
|
366
|
+
return true;
|
|
367
|
+
};
|
|
368
|
+
function createPlacementAnchor(target, opts) {
|
|
369
|
+
let visible = opts.visible;
|
|
370
|
+
let publish = opts.publish;
|
|
371
|
+
const guardDisplayNone = opts.guardDisplayNone ?? false;
|
|
372
|
+
const followScroll = opts.followScroll ?? false;
|
|
373
|
+
const followGeometry = opts.followGeometry ?? false;
|
|
374
|
+
let observer = null;
|
|
375
|
+
let io = null;
|
|
376
|
+
let lastPublished = null;
|
|
377
|
+
let disposed = false;
|
|
378
|
+
let rafId = null;
|
|
379
|
+
let steadyFrames = 0;
|
|
380
|
+
const STEADY_CLOSE_FRAMES = 2;
|
|
381
|
+
let pointerHeld = false;
|
|
382
|
+
let sentinelDeadline = null;
|
|
383
|
+
const computePlacement = () => {
|
|
384
|
+
const p = measurePlacement(target);
|
|
385
|
+
if (guardDisplayNone && p.visible && (p.bounds.width === 0 || p.bounds.height === 0)) {
|
|
386
|
+
return { visible: false };
|
|
387
|
+
}
|
|
388
|
+
return p;
|
|
389
|
+
};
|
|
390
|
+
const emit = () => {
|
|
391
|
+
if (disposed || !visible) return;
|
|
392
|
+
const p = computePlacement();
|
|
393
|
+
if (lastPublished && samePlacement(lastPublished, p)) return;
|
|
394
|
+
lastPublished = p;
|
|
395
|
+
publish(p);
|
|
396
|
+
};
|
|
397
|
+
const sentinelFrame = () => {
|
|
398
|
+
rafId = null;
|
|
399
|
+
if (disposed || !visible) {
|
|
400
|
+
sentinelDeadline = null;
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (sentinelDeadline !== null && performance.now() >= sentinelDeadline) {
|
|
404
|
+
sentinelDeadline = null;
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const p = computePlacement();
|
|
408
|
+
if (lastPublished && samePlacement(lastPublished, p)) {
|
|
409
|
+
steadyFrames++;
|
|
410
|
+
if (steadyFrames >= STEADY_CLOSE_FRAMES && !pointerHeld) {
|
|
411
|
+
sentinelDeadline = null;
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
lastPublished = p;
|
|
416
|
+
publish(p);
|
|
417
|
+
steadyFrames = 0;
|
|
418
|
+
}
|
|
419
|
+
if (!disposed && visible) {
|
|
420
|
+
rafId = requestAnimationFrame(sentinelFrame);
|
|
421
|
+
} else {
|
|
422
|
+
sentinelDeadline = null;
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
const openSentinel = () => {
|
|
426
|
+
if (!followGeometry || disposed) return;
|
|
427
|
+
steadyFrames = 0;
|
|
428
|
+
if (rafId === null) rafId = requestAnimationFrame(sentinelFrame);
|
|
429
|
+
};
|
|
430
|
+
const closeSentinel = () => {
|
|
431
|
+
if (rafId !== null) {
|
|
432
|
+
cancelAnimationFrame(rafId);
|
|
433
|
+
rafId = null;
|
|
434
|
+
}
|
|
435
|
+
steadyFrames = 0;
|
|
436
|
+
sentinelDeadline = null;
|
|
437
|
+
pointerHeld = false;
|
|
438
|
+
};
|
|
439
|
+
const onScroll = () => {
|
|
440
|
+
if (followGeometry) openSentinel();
|
|
441
|
+
else emit();
|
|
442
|
+
};
|
|
443
|
+
const onPointerDown = (e) => {
|
|
444
|
+
const t = e.target;
|
|
445
|
+
if (t && t.closest && t.closest('[role="separator"]')) {
|
|
446
|
+
pointerHeld = true;
|
|
447
|
+
openSentinel();
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
const onPointerUp = () => {
|
|
451
|
+
if (!pointerHeld) return;
|
|
452
|
+
pointerHeld = false;
|
|
453
|
+
openSentinel();
|
|
454
|
+
};
|
|
455
|
+
const startObserving = () => {
|
|
456
|
+
if (observer) return;
|
|
457
|
+
observer = new ResizeObserver(emit);
|
|
458
|
+
observer.observe(target);
|
|
459
|
+
window.addEventListener("resize", emit);
|
|
460
|
+
if (guardDisplayNone && typeof IntersectionObserver !== "undefined") {
|
|
461
|
+
io = new IntersectionObserver(emit);
|
|
462
|
+
io.observe(target);
|
|
463
|
+
}
|
|
464
|
+
if (followScroll) {
|
|
465
|
+
window.addEventListener("scroll", onScroll, {
|
|
466
|
+
capture: true,
|
|
467
|
+
passive: true
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
if (followGeometry) {
|
|
471
|
+
window.addEventListener("pointerdown", onPointerDown, { capture: true });
|
|
472
|
+
window.addEventListener("pointerup", onPointerUp, { capture: true });
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
const stopObserving = () => {
|
|
476
|
+
if (observer) {
|
|
477
|
+
observer.disconnect();
|
|
478
|
+
observer = null;
|
|
479
|
+
}
|
|
480
|
+
if (io) {
|
|
481
|
+
io.disconnect();
|
|
482
|
+
io = null;
|
|
483
|
+
}
|
|
484
|
+
window.removeEventListener("resize", emit);
|
|
485
|
+
window.removeEventListener("scroll", onScroll, {
|
|
486
|
+
capture: true
|
|
487
|
+
});
|
|
488
|
+
window.removeEventListener("pointerdown", onPointerDown, {
|
|
489
|
+
capture: true
|
|
490
|
+
});
|
|
491
|
+
window.removeEventListener("pointerup", onPointerUp, {
|
|
492
|
+
capture: true
|
|
493
|
+
});
|
|
494
|
+
closeSentinel();
|
|
495
|
+
};
|
|
496
|
+
const apply = () => {
|
|
497
|
+
lastPublished = null;
|
|
498
|
+
if (visible) {
|
|
499
|
+
startObserving();
|
|
500
|
+
lastPublished = computePlacement();
|
|
501
|
+
publish(lastPublished);
|
|
502
|
+
} else {
|
|
503
|
+
stopObserving();
|
|
504
|
+
const hidden = { visible: false };
|
|
505
|
+
lastPublished = hidden;
|
|
506
|
+
publish(hidden);
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
apply();
|
|
510
|
+
return {
|
|
511
|
+
update(next) {
|
|
512
|
+
if (disposed) return;
|
|
513
|
+
publish = next.publish;
|
|
514
|
+
visible = next.visible;
|
|
515
|
+
apply();
|
|
516
|
+
},
|
|
517
|
+
dispose() {
|
|
518
|
+
if (disposed) return;
|
|
519
|
+
disposed = true;
|
|
520
|
+
stopObserving();
|
|
521
|
+
},
|
|
522
|
+
pulse(durationMs) {
|
|
523
|
+
if (disposed || !followGeometry) return;
|
|
524
|
+
if (durationMs !== void 0 && durationMs > 0) {
|
|
525
|
+
const next = performance.now() + durationMs;
|
|
526
|
+
sentinelDeadline = sentinelDeadline === null ? next : Math.max(sentinelDeadline, next);
|
|
527
|
+
}
|
|
528
|
+
openSentinel();
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
return __toCommonJS(view_anchor_exports);
|
|
533
|
+
})();
|
|
534
|
+
/* __VIEW_ANCHOR_CORE_END__ */
|
|
535
|
+
</script>
|
|
536
|
+
|
|
537
|
+
<script>
|
|
538
|
+
const { createViewAnchor } = __viewAnchorCore
|
|
539
|
+
|
|
540
|
+
const ZERO = { x: 0, y: 0, width: 0, height: 0 }
|
|
541
|
+
|
|
542
|
+
const deck = document.getElementById('deck')
|
|
543
|
+
const placeholder = document.getElementById('placeholder')
|
|
544
|
+
const follower = document.getElementById('follower')
|
|
545
|
+
const pillar = document.getElementById('pillar')
|
|
546
|
+
const presentEl = document.getElementById('present')
|
|
547
|
+
const splitEl = document.getElementById('split')
|
|
548
|
+
const explodeEl = document.getElementById('explode')
|
|
549
|
+
const scene = document.getElementById('scene')
|
|
550
|
+
const readout = document.getElementById('readout')
|
|
551
|
+
|
|
552
|
+
// 占位 div 在 deck 内的布局矩形(沿 offsetParent 链累加,与 3D 变换无关)——
|
|
553
|
+
// 这样无论平铺还是倾斜,绿色视图都摆在占位 div 正上方,对齐不漂。
|
|
554
|
+
function deckLocalRect(el) {
|
|
555
|
+
let x = 0, y = 0, n = el
|
|
556
|
+
while (n && n !== deck) { x += n.offsetLeft; y += n.offsetTop; n = n.offsetParent }
|
|
557
|
+
return { x, y, width: el.offsetWidth, height: el.offsetHeight }
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// 这是宿主侧的 publish —— view-anchor 把测量到的矩形交给它。真实应用里它会走
|
|
561
|
+
// IPC → WebContentsView.setBounds;这里把绿色视图摆到 deck 局部坐标的对应位置。
|
|
562
|
+
function publish(bounds) {
|
|
563
|
+
if (bounds.width === 0 || bounds.height === 0) {
|
|
564
|
+
follower.classList.add('collapsed')
|
|
565
|
+
renderReadout(ZERO, false)
|
|
566
|
+
return
|
|
567
|
+
}
|
|
568
|
+
follower.classList.remove('collapsed')
|
|
569
|
+
const r = deckLocalRect(placeholder)
|
|
570
|
+
follower.style.left = r.x + 'px'
|
|
571
|
+
follower.style.top = r.y + 'px'
|
|
572
|
+
follower.style.width = r.width + 'px'
|
|
573
|
+
follower.style.height = r.height + 'px'
|
|
574
|
+
// 连接柱立在占位 div 中线,沿 Z 伸到原生视图所在高度(120px)。
|
|
575
|
+
pillar.style.left = (r.x + r.width / 2) + 'px'
|
|
576
|
+
pillar.style.top = (r.y + r.height / 2) + 'px'
|
|
577
|
+
pillar.style.height = '120px'
|
|
578
|
+
renderReadout(r, true)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function renderReadout(b, present) {
|
|
582
|
+
readout.innerHTML =
|
|
583
|
+
'<span class="k">publish</span>({ x:' + b.x + ', y:' + b.y +
|
|
584
|
+
', w:' + b.width + ', h:' + b.height + ' })' +
|
|
585
|
+
(present ? '' : ' <span class="k">// 已摘除</span>')
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// 启动真实 anchor。之后分栏 resize 全由 view-anchor 自己的 ResizeObserver 驱动。
|
|
589
|
+
const handle = createViewAnchor(placeholder, { present: presentEl.checked, publish })
|
|
590
|
+
|
|
591
|
+
presentEl.addEventListener('change', () => {
|
|
592
|
+
handle.update({ present: presentEl.checked, publish })
|
|
593
|
+
})
|
|
594
|
+
splitEl.addEventListener('input', () => {
|
|
595
|
+
placeholder.style.flexBasis = splitEl.value + '%'
|
|
596
|
+
})
|
|
597
|
+
explodeEl.addEventListener('change', () => {
|
|
598
|
+
scene.classList.toggle('exploded', explodeEl.checked)
|
|
599
|
+
})
|
|
600
|
+
</script>
|
|
601
|
+
</body>
|
|
602
|
+
</html>
|