dsh-design-studio 0.11.0 → 0.12.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.
- package/README.md +2 -2
- package/lib/client.js +1893 -539
- package/lib/core.js +210 -6
- package/lib/draft-page.js +763 -0
- package/lib/draft.js +151 -0
- package/lib/index.js +17 -3
- package/lib/picker.js +227 -1
- package/lib/serve.js +177 -1
- package/lib/tweaks.js +58 -24
- package/package.json +1 -1
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 边写边看的草稿页里的小脚本(预览服务器给 /某屏.html?__ddp_draft=… 时注入在页面最前面)。
|
|
3
|
+
*
|
|
4
|
+
* - 跟着写到的地方往下滚(自己往上翻了就不跟,翻回底部再接着跟)
|
|
5
|
+
* - 三只带名字的鼠标(排版 / 配色 / 文案)只去碰真正新长出来的东西,而且真的在「干活」:
|
|
6
|
+
* 排版把新的一大块拖出一个框(标上是什么、多大),配色点一下给新上色的块刷上颜色,
|
|
7
|
+
* 文案把新出来的一段字一个个打出来(这段字先藏着,打到哪露到哪);
|
|
8
|
+
* 模型停着不写,它们也停着——不循环、不演
|
|
9
|
+
* - 页面还没有正文(模型在写样式)时,把样式里已经定下来的颜色、字体一个个摆出来
|
|
10
|
+
* - 写完(文档读完)鼠标退场;右侧栏随后换上真文件
|
|
11
|
+
*/
|
|
12
|
+
(() => {
|
|
13
|
+
if (window.__ddpDraft) return
|
|
14
|
+
window.__ddpDraft = 1
|
|
15
|
+
const zh = new URLSearchParams(location.search).get('lang') !== 'en'
|
|
16
|
+
const NAMES = zh ? ['排版', '配色', '文案'] : ['Layout', 'Color', 'Copy']
|
|
17
|
+
const PAL = zh ? '配色和字体' : 'Colors & type'
|
|
18
|
+
const KIND = zh
|
|
19
|
+
? { top: '顶栏', bottom: '底栏', list: '列表', form: '表单', img: '图片', title: '标题', block: '内容' }
|
|
20
|
+
: { top: 'Header', bottom: 'Footer', list: 'List', form: 'Form', img: 'Image', title: 'Title', block: 'Block' }
|
|
21
|
+
const C = ['#4d6bfe', '#e8590c', '#0ca678']
|
|
22
|
+
let still = false
|
|
23
|
+
try { still = matchMedia('(prefers-reduced-motion: reduce)').matches } catch { /* 老浏览器 */ }
|
|
24
|
+
const de = document.documentElement
|
|
25
|
+
let zk = 1 // 画布缩小时鼠标、字放大的倍数(和 --k 一样)
|
|
26
|
+
|
|
27
|
+
// 草稿里不要滚动条(跟着往下滚时一闪一闪的)
|
|
28
|
+
const css = document.createElement('style')
|
|
29
|
+
// 样式常写在正文后面:写到一半时没定尺寸的图标会撑满一屏,草稿里先给个上限(写完换真文件就没有了)
|
|
30
|
+
css.textContent = 'html{scrollbar-width:none}html::-webkit-scrollbar{display:none}svg:not([width]){max-width:40vw;max-height:40vh}' +
|
|
31
|
+
// 新长出来的东西淡进来(只动透明度:不碰页面自己的 transform;:where 零权重,页面自己写了动画就听页面的)
|
|
32
|
+
(still ? '' : '@keyframes ddp-in{from{opacity:0}}:where(body *){animation:ddp-in .3s ease-out backwards}')
|
|
33
|
+
;(document.head || de).appendChild(css)
|
|
34
|
+
|
|
35
|
+
const host = document.createElement('ddp-draft-fx')
|
|
36
|
+
host.setAttribute('aria-hidden', 'true')
|
|
37
|
+
host.style.cssText = 'all:initial;position:fixed;inset:0;z-index:2147483647;pointer-events:none;display:block'
|
|
38
|
+
const sr = host.attachShadow({ mode: 'closed' })
|
|
39
|
+
const EASE = 'cubic-bezier(.3,.7,.2,1)'
|
|
40
|
+
sr.innerHTML = `<style>
|
|
41
|
+
:host{--k:1}
|
|
42
|
+
*{box-sizing:border-box}
|
|
43
|
+
.cur{position:absolute;left:0;top:0;opacity:0;transition:transform .6s ${EASE},opacity .35s;will-change:transform}
|
|
44
|
+
.cur.on{opacity:1}
|
|
45
|
+
.cur.live{transition:opacity .35s}
|
|
46
|
+
.cur svg{transition:transform .14s}
|
|
47
|
+
.cur.tap svg{transform:scale(.8)}
|
|
48
|
+
.cur svg{display:block;width:calc(16px * var(--k));height:calc(20px * var(--k));overflow:visible;filter:drop-shadow(0 1px 2px rgba(0,0,0,.3))}
|
|
49
|
+
.cur path{fill:var(--c);stroke:#fff;stroke-width:1.5;stroke-linejoin:round}
|
|
50
|
+
.cur em{position:absolute;left:calc(12px * var(--k));top:calc(16px * var(--k));display:flex;align-items:center;gap:calc(4px * var(--k));padding:calc(2px * var(--k)) calc(7px * var(--k));border-radius:calc(6px * var(--k));
|
|
51
|
+
background:var(--c);color:#fff;font:600 calc(11px * var(--k))/1.4 -apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;font-style:normal;white-space:nowrap;box-shadow:0 2px 6px rgba(0,0,0,.18)}
|
|
52
|
+
.cur.flip em{left:auto;right:calc(-4px * var(--k))}
|
|
53
|
+
.cur em i{display:none;width:calc(9px * var(--k));height:calc(9px * var(--k));border-radius:50%;box-shadow:0 0 0 calc(1.5px * var(--k)) rgba(255,255,255,.95)}
|
|
54
|
+
.cur em i.on{display:block}
|
|
55
|
+
.cur em s{text-decoration:none;opacity:.85;font-weight:500}
|
|
56
|
+
.box{position:absolute;left:0;top:0;border:calc(1.5px * var(--k)) solid var(--c);background:color-mix(in srgb,var(--c) 7%,transparent);transition:opacity .6s}
|
|
57
|
+
.box.out{opacity:0}
|
|
58
|
+
.box s{position:absolute;width:calc(7px * var(--k));height:calc(7px * var(--k));background:#fff;border:calc(1.5px * var(--k)) solid var(--c);border-radius:calc(1.5px * var(--k))}
|
|
59
|
+
.box s:nth-of-type(1){left:calc(-4.25px * var(--k));top:calc(-4.25px * var(--k))}
|
|
60
|
+
.box s:nth-of-type(2){right:calc(-4.25px * var(--k));top:calc(-4.25px * var(--k))}
|
|
61
|
+
.box s:nth-of-type(3){left:calc(-4.25px * var(--k));bottom:calc(-4.25px * var(--k))}
|
|
62
|
+
.box s:nth-of-type(4){right:calc(-4.25px * var(--k));bottom:calc(-4.25px * var(--k))}
|
|
63
|
+
.box.in b{top:calc(4px * var(--k));left:calc(4px * var(--k))}
|
|
64
|
+
.box span{position:absolute;left:50%;top:100%;margin-top:calc(7px * var(--k));transform:translateX(-50%);padding:calc(1px * var(--k)) calc(6px * var(--k));border-radius:calc(4px * var(--k));
|
|
65
|
+
background:var(--c);color:#fff;white-space:nowrap;font:600 calc(10px * var(--k))/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;font-variant-numeric:tabular-nums}
|
|
66
|
+
.box.up span{top:auto;bottom:calc(7px * var(--k));margin-top:0}
|
|
67
|
+
.paint{position:absolute;left:0;top:0;overflow:hidden}
|
|
68
|
+
.paint i{position:absolute;width:2px;height:2px;margin:-1px 0 0 -1px;border-radius:50%;transform:scale(0);
|
|
69
|
+
background:radial-gradient(circle closest-side,transparent 58%,color-mix(in srgb,var(--c) 50%,#fff) 84%,transparent 100%)}
|
|
70
|
+
.caret{position:absolute;left:0;top:0;width:calc(2px * var(--k));border-radius:1px;background:${C[2]};opacity:0;transition:opacity .2s}
|
|
71
|
+
.caret.on{opacity:1;animation:blink 1s steps(1) infinite}
|
|
72
|
+
@keyframes blink{50%{opacity:.25}}
|
|
73
|
+
.box b{position:absolute;left:calc(-1.5px * var(--k));top:calc(-19px * var(--k));padding:calc(1px * var(--k)) calc(6px * var(--k));border-radius:calc(4px * var(--k));background:var(--c);color:#fff;
|
|
74
|
+
font:600 calc(10px * var(--k))/1.5 -apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif}
|
|
75
|
+
.pal{position:absolute;left:50%;top:42%;transform:translate(-50%,-50%);transition:opacity .4s,transform .3s;width:min(86%, calc(340px * var(--k)));padding:calc(16px * var(--k)) calc(18px * var(--k));border-radius:calc(14px * var(--k));
|
|
76
|
+
background:rgba(255,255,255,.94);box-shadow:0 calc(10px * var(--k)) calc(30px * var(--k)) rgba(20,20,40,.14),0 0 0 calc(.5px * var(--k)) rgba(0,0,0,.08);color:#1d1d24;
|
|
77
|
+
font:500 calc(11px * var(--k))/1.4 -apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;opacity:0}
|
|
78
|
+
.pal.on{opacity:1}
|
|
79
|
+
.pal.out{opacity:0;transform:translate(-50%,-50%) scale(.96);transition-duration:.22s}
|
|
80
|
+
.pal h6{margin:0 0 calc(10px * var(--k));font-size:calc(11px * var(--k));font-weight:600;color:#8a8a96;letter-spacing:.04em}
|
|
81
|
+
.sw{display:flex;flex-wrap:wrap;gap:calc(10px * var(--k))}
|
|
82
|
+
.sw span{display:flex;flex-direction:column;align-items:center;gap:calc(4px * var(--k));width:calc(48px * var(--k));animation:pop .42s ${EASE} both}
|
|
83
|
+
.sw i{width:calc(30px * var(--k));height:calc(30px * var(--k));border-radius:50%;box-shadow:inset 0 0 0 calc(1px * var(--k)) rgba(0,0,0,.1)}
|
|
84
|
+
.sw em{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-style:normal;font-size:calc(9.5px * var(--k));color:#6b6b78}
|
|
85
|
+
.ft{display:flex;flex-direction:column;gap:calc(6px * var(--k));margin-top:calc(12px * var(--k))}
|
|
86
|
+
.ft span{display:flex;align-items:baseline;gap:calc(8px * var(--k));animation:pop .42s ${EASE} both}
|
|
87
|
+
.ft b{font-size:calc(20px * var(--k));font-weight:600;line-height:1}
|
|
88
|
+
.ft em{font-style:normal;color:#6b6b78;font-size:calc(10.5px * var(--k))}
|
|
89
|
+
@keyframes pop{from{opacity:0;transform:translateY(calc(6px * var(--k))) scale(.8)}}
|
|
90
|
+
@media (prefers-reduced-motion:reduce){.cur,.box,.pal{transition:none}.sw span,.ft span,.caret.on{animation:none}}
|
|
91
|
+
</style><div class="pal"><h6>${PAL}</h6><div class="sw"></div><div class="ft"></div></div><div class="caret"></div>`
|
|
92
|
+
const ARROW = '<svg viewBox="0 0 17 21"><path d="M1.2 1.2v15.6l4.3-4 3 6.6 2.9-1.3-3-6.5h6z"/></svg>'
|
|
93
|
+
const curs = NAMES.map((n, i) => {
|
|
94
|
+
const el = document.createElement('div')
|
|
95
|
+
el.className = 'cur'
|
|
96
|
+
el.style.setProperty('--c', C[i])
|
|
97
|
+
el.innerHTML = `${ARROW}<em><i></i><span></span><s></s></em>`
|
|
98
|
+
el.querySelector('span').textContent = n
|
|
99
|
+
sr.appendChild(el)
|
|
100
|
+
// x/y:视口坐标;at:贴着的元素(滚动时跟着它走)和它身上的点(相对元素左上)
|
|
101
|
+
// anim:正在演一个动作(拖框、打字),这时位置由动作自己一帧帧给,滚动跟随先别管它
|
|
102
|
+
return { el, tip: el.querySelector('s'), dot: el.querySelector('i'), x: 0, y: 0, at: null, dx: 0, dy: 0, busyTill: 0, anim: false }
|
|
103
|
+
})
|
|
104
|
+
const caret = sr.querySelector('.caret')
|
|
105
|
+
const pal = sr.querySelector('.pal')
|
|
106
|
+
const swBox = sr.querySelector('.sw')
|
|
107
|
+
const ftBox = sr.querySelector('.ft')
|
|
108
|
+
de.appendChild(host)
|
|
109
|
+
|
|
110
|
+
const vw = () => innerWidth || de.clientWidth || 390
|
|
111
|
+
const vh = () => innerHeight || de.clientHeight || 844
|
|
112
|
+
const place = (c, x, y, loose) => {
|
|
113
|
+
c.x = Math.max(4, Math.min(vw() - 6 - 16 * zk, x))
|
|
114
|
+
c.y = loose ? y : Math.max(4, Math.min(vh() - 10, y))
|
|
115
|
+
c.el.style.transform = `translate(${c.x}px, ${c.y}px)`
|
|
116
|
+
c.el.classList.toggle('flip', c.x > vw() * 0.68)
|
|
117
|
+
}
|
|
118
|
+
// 一开始三只停在顶上,等第一笔
|
|
119
|
+
curs.forEach((c, i) => place(c, vw() * (0.3 + i * 0.18), 56 + i * 10))
|
|
120
|
+
|
|
121
|
+
// ── 跟着写到的地方往下滚 ──
|
|
122
|
+
let follow = true
|
|
123
|
+
let done = false
|
|
124
|
+
const bottomGap = () => Math.max(0, de.scrollHeight - vh() - scrollY)
|
|
125
|
+
const stop = () => { follow = false }
|
|
126
|
+
addEventListener('wheel', (e) => { if (e.deltaY < 0) stop() }, { passive: true })
|
|
127
|
+
addEventListener('touchstart', stop, { passive: true })
|
|
128
|
+
addEventListener('keydown', stop)
|
|
129
|
+
addEventListener('scroll', () => {
|
|
130
|
+
if (!follow && !done && bottomGap() < 24) follow = true // 翻回底部:接着跟
|
|
131
|
+
track()
|
|
132
|
+
}, { passive: true })
|
|
133
|
+
const chase = () => {
|
|
134
|
+
if (!follow || done) return
|
|
135
|
+
if (bottomGap() > 2) scrollTo({ top: de.scrollHeight, behavior: still ? 'auto' : 'smooth' })
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── 新长出来的东西 ──
|
|
139
|
+
const fresh = []
|
|
140
|
+
const SKIP = /^(SCRIPT|STYLE|LINK|META|TITLE|TEMPLATE|NOSCRIPT|BR|HEAD|HTML|BODY|BASE|DDP-DRAFT-FX|SOURCE|TRACK|PARAM|WBR)$/
|
|
141
|
+
// 文案会去打的:成段的字(标题、段落、按钮、标签);列表项、链接常常是整张卡片,不算
|
|
142
|
+
const TYPE = /^(H1|H2|H3|H4|H5|H6|P|BUTTON|LABEL|FIGCAPTION|BLOCKQUOTE|DT|DD)$/
|
|
143
|
+
let typing = null // { el, since }:藏着等打的那段
|
|
144
|
+
const hold = (el) => {
|
|
145
|
+
el.__ddpClip = [el.style.getPropertyValue('clip-path'), el.style.getPropertyPriority('clip-path')]
|
|
146
|
+
el.style.setProperty('clip-path', 'inset(0 100% 100% 0)', 'important')
|
|
147
|
+
}
|
|
148
|
+
const release = (el) => {
|
|
149
|
+
const was = el && el.__ddpClip
|
|
150
|
+
if (!was) return
|
|
151
|
+
el.style.removeProperty('clip-path')
|
|
152
|
+
if (was[0]) el.style.setProperty('clip-path', was[0], was[1])
|
|
153
|
+
delete el.__ddpClip
|
|
154
|
+
}
|
|
155
|
+
const mo = new MutationObserver((recs) => {
|
|
156
|
+
if (!palGone && palOn && hasBody()) dropPal()
|
|
157
|
+
for (const r of recs) {
|
|
158
|
+
for (const n of r.addedNodes) {
|
|
159
|
+
if (n.nodeType !== 1 || SKIP.test(n.tagName)) continue
|
|
160
|
+
fresh.push({ el: n, age: 0 })
|
|
161
|
+
// 配色空着手:新上色的块先收着底色,等它来刷
|
|
162
|
+
if (!painting && !still && !done && PAINT.test(n.tagName) && !n.closest('head')) {
|
|
163
|
+
const col = solidBg(n)
|
|
164
|
+
if (col && apart(col, pageBg())) {
|
|
165
|
+
painting = { el: n, color: col, since: Date.now(), on: false }
|
|
166
|
+
painted.add(n)
|
|
167
|
+
holdBg(n, col)
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// 文案空着手:新出来的一段字先藏起来(这时还没画到屏上),等它来一个个打出来
|
|
172
|
+
if (!typing && !still && !done && TYPE.test(n.tagName) && !n.closest('head')) {
|
|
173
|
+
typing = { el: n, since: Date.now() }
|
|
174
|
+
hold(n)
|
|
175
|
+
requestAnimationFrame(tryType)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
mo.observe(de, { childList: true, subtree: true })
|
|
181
|
+
|
|
182
|
+
const visible = (r) => r.width > 6 && r.height > 6 && r.bottom > 0 && r.top < vh() && r.right > 0 && r.left < vw()
|
|
183
|
+
const bgOf = (el) => {
|
|
184
|
+
try {
|
|
185
|
+
const v = getComputedStyle(el).backgroundColor
|
|
186
|
+
const m = /rgba?\(([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:[,\s/]+([\d.]+))?/.exec(v)
|
|
187
|
+
if (!m || (m[4] !== undefined && Number(m[4]) < 0.15)) return ''
|
|
188
|
+
return '#' + [m[1], m[2], m[3]].map((x) => Math.round(Number(x)).toString(16).padStart(2, '0')).join('')
|
|
189
|
+
} catch { return '' }
|
|
190
|
+
}
|
|
191
|
+
const pageBg = () => (document.body && bgOf(document.body)) || bgOf(de) || '#ffffff'
|
|
192
|
+
const rgb = (hx) => [1, 3, 5].map((i) => parseInt(hx.slice(i, i + 2), 16))
|
|
193
|
+
const dist = (a, b) => {
|
|
194
|
+
const x = rgb(a)
|
|
195
|
+
const y = rgb(b)
|
|
196
|
+
return Math.hypot(x[0] - y[0], x[1] - y[1], x[2] - y[2])
|
|
197
|
+
}
|
|
198
|
+
// 暗色页面的卡片常常只比底色亮一点点,也算
|
|
199
|
+
const apart = (a, b) => !!a && (!b || dist(a, b) >= 24)
|
|
200
|
+
const kindOf = (el, r) => {
|
|
201
|
+
const tag = el.tagName
|
|
202
|
+
let pos = ''
|
|
203
|
+
try { pos = getComputedStyle(el).position } catch { /* 拿不到就算普通的 */ }
|
|
204
|
+
// 钉在屏幕上的宽条:看在上面还是下面
|
|
205
|
+
if ((pos === 'fixed' || pos === 'sticky') && r.width > vw() * 0.8) return r.top < vh() * 0.5 ? 'top' : 'bottom'
|
|
206
|
+
// 不钉住的 header / nav:在页面最上面的是顶栏;中间的 header 是一节的标题,nav 是底下的导航
|
|
207
|
+
if (tag === 'HEADER' || tag === 'NAV') return r.top + scrollY < vh() * 0.25 ? 'top' : tag === 'NAV' ? 'bottom' : 'title'
|
|
208
|
+
if (tag === 'FOOTER') return 'bottom'
|
|
209
|
+
if (tag === 'UL' || tag === 'OL' || tag === 'TABLE') return 'list'
|
|
210
|
+
if (tag === 'FORM') return 'form'
|
|
211
|
+
if (tag === 'IMG' || tag === 'PICTURE' || tag === 'VIDEO' || tag === 'CANVAS') return 'img'
|
|
212
|
+
if (/^H[1-3]$/.test(tag)) return 'title'
|
|
213
|
+
return 'block'
|
|
214
|
+
}
|
|
215
|
+
const BLOCK = /^(HEADER|NAV|SECTION|MAIN|FOOTER|ASIDE|ARTICLE|FORM|UL|OL|DIV|TABLE|FIGURE|IMG|PICTURE)$/
|
|
216
|
+
|
|
217
|
+
// ── 鼠标去碰一个元素 ──
|
|
218
|
+
const boxes = [] // 排版拖出来的框
|
|
219
|
+
const paints = [] // 配色刷的那一下
|
|
220
|
+
const firstCur = () => sr.querySelector('.cur')
|
|
221
|
+
const sizeBox = (item, r) => {
|
|
222
|
+
const s = item.b.style
|
|
223
|
+
const p = item.p == null ? 1 : item.p
|
|
224
|
+
s.width = r.width * p + 'px'
|
|
225
|
+
s.height = r.height * p + 'px'
|
|
226
|
+
s.transform = `translate(${r.left}px, ${r.top}px)`
|
|
227
|
+
}
|
|
228
|
+
const ease = (t) => 1 - Math.pow(1 - t, 3)
|
|
229
|
+
// 一个动作的每一帧;写完了(done)就直接跳到最后
|
|
230
|
+
const anim = (ms, step, end) => {
|
|
231
|
+
const t0 = performance.now()
|
|
232
|
+
const f = (now) => {
|
|
233
|
+
const t = done ? 1 : Math.min(1, (now - t0) / ms)
|
|
234
|
+
step(t)
|
|
235
|
+
if (t < 1) requestAnimationFrame(f)
|
|
236
|
+
else if (end) end()
|
|
237
|
+
}
|
|
238
|
+
requestAnimationFrame(f)
|
|
239
|
+
}
|
|
240
|
+
const go = (c, el, dx, dy, tip, dot) => {
|
|
241
|
+
c.at = el
|
|
242
|
+
c.dx = dx
|
|
243
|
+
c.dy = dy
|
|
244
|
+
c.el.classList.add('on')
|
|
245
|
+
c.tip.textContent = tip ? ' ' + tip : ''
|
|
246
|
+
c.dot.classList.toggle('on', !!dot)
|
|
247
|
+
if (dot) c.dot.style.background = dot
|
|
248
|
+
const r = el.getBoundingClientRect()
|
|
249
|
+
place(c, r.left + dx, r.top + dy)
|
|
250
|
+
}
|
|
251
|
+
const tap = (c) => {
|
|
252
|
+
c.el.classList.add('tap')
|
|
253
|
+
setTimeout(() => c.el.classList.remove('tap'), 150)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 排版:走到这块的左上角,按住往右下拖出一个选框(四角把手,下面实时标着多大),松手标上是什么
|
|
257
|
+
const drag = (c, el, label) => {
|
|
258
|
+
go(c, el, 0, 0, '')
|
|
259
|
+
const b = document.createElement('div')
|
|
260
|
+
b.className = 'box'
|
|
261
|
+
b.style.setProperty('--c', C[0])
|
|
262
|
+
b.innerHTML = '<s></s><s></s><s></s><s></s><b></b><span></span>'
|
|
263
|
+
b.querySelector('b').textContent = label
|
|
264
|
+
const sz = b.querySelector('span')
|
|
265
|
+
b.style.opacity = '0'
|
|
266
|
+
sr.insertBefore(b, firstCur())
|
|
267
|
+
const item = { b, el, p: 0 }
|
|
268
|
+
boxes.push(item)
|
|
269
|
+
sizeBox(item, el.getBoundingClientRect())
|
|
270
|
+
setTimeout(() => {
|
|
271
|
+
if (done || !el.isConnected) {
|
|
272
|
+
b.remove()
|
|
273
|
+
boxes.splice(boxes.indexOf(item), 1)
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
b.style.opacity = ''
|
|
277
|
+
c.anim = true
|
|
278
|
+
c.el.classList.add('live')
|
|
279
|
+
tap(c)
|
|
280
|
+
anim(still ? 0 : 620, (t) => {
|
|
281
|
+
item.p = ease(t)
|
|
282
|
+
const r = el.getBoundingClientRect()
|
|
283
|
+
sizeBox(item, r)
|
|
284
|
+
// 名字贴不下(块顶着屏幕上沿)就放进框里;尺寸贴不下(块顶着下沿)就放框里底下
|
|
285
|
+
b.classList.toggle('in', r.top < 22 * zk)
|
|
286
|
+
b.classList.toggle('up', r.top + r.height * item.p > vh() - 26 * zk)
|
|
287
|
+
sz.textContent = `${Math.round(r.width * item.p)} × ${Math.round(r.height * item.p)}`
|
|
288
|
+
place(c, r.left + r.width * item.p, r.top + r.height * item.p)
|
|
289
|
+
}, () => {
|
|
290
|
+
const r = el.getBoundingClientRect()
|
|
291
|
+
c.anim = false
|
|
292
|
+
c.el.classList.remove('live')
|
|
293
|
+
// 松手,往里收一点(别停在屏幕边上)
|
|
294
|
+
c.dx = Math.max(0, r.width - 26 * zk)
|
|
295
|
+
c.dy = Math.max(0, r.height - 8 * zk)
|
|
296
|
+
place(c, r.left + c.dx, r.top + c.dy)
|
|
297
|
+
setTimeout(() => b.classList.add('out'), 1200)
|
|
298
|
+
setTimeout(() => { b.remove(); boxes.splice(boxes.indexOf(item), 1) }, 1900)
|
|
299
|
+
})
|
|
300
|
+
}, still ? 0 : 560)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 配色:新上色的块先只留一圈虚线(底色先收着),配色走过去点一下,颜色从点的地方一圈刷满——刷的是块自己的底色,字一直在上面
|
|
304
|
+
let painting = null // { el, color, since, on }:收着底色等刷的那块
|
|
305
|
+
const BG = ['background-color', 'background-image', 'background-size', 'background-position', 'background-repeat', 'outline', 'outline-offset', 'transition']
|
|
306
|
+
const PAINT = /^(DIV|SECTION|ARTICLE|ASIDE|HEADER|FOOTER|NAV|BUTTON|A|LI|FIGURE|FORM|LABEL)$/
|
|
307
|
+
const solidBg = (el) => {
|
|
308
|
+
try {
|
|
309
|
+
const cs = getComputedStyle(el)
|
|
310
|
+
if (cs.backgroundImage !== 'none') return ''
|
|
311
|
+
const m = /rgba?\(([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:[,\s/]+([\d.]+))?/.exec(cs.backgroundColor)
|
|
312
|
+
if (!m || (m[4] !== undefined && Number(m[4]) < 0.6)) return ''
|
|
313
|
+
return '#' + [m[1], m[2], m[3]].map((x) => Math.round(Number(x)).toString(16).padStart(2, '0')).join('')
|
|
314
|
+
} catch { return '' }
|
|
315
|
+
}
|
|
316
|
+
const holdBg = (el, color) => {
|
|
317
|
+
const st = el.style
|
|
318
|
+
let line = color
|
|
319
|
+
try {
|
|
320
|
+
const m = /rgba?\(([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/.exec(getComputedStyle(el).color)
|
|
321
|
+
const fg = m ? '#' + [m[1], m[2], m[3]].map((x) => Math.round(Number(x)).toString(16).padStart(2, '0')).join('') : ''
|
|
322
|
+
const bg = pageBg()
|
|
323
|
+
if (fg && dist(fg, bg) > dist(color, bg)) line = fg
|
|
324
|
+
} catch { /* 拿不到字色就用块色 */ }
|
|
325
|
+
el.__ddpBg = BG.map((k) => [k, st.getPropertyValue(k), st.getPropertyPriority(k)])
|
|
326
|
+
st.setProperty('transition', 'none', 'important')
|
|
327
|
+
st.setProperty('background-color', 'transparent', 'important')
|
|
328
|
+
st.setProperty('background-image', 'none', 'important')
|
|
329
|
+
st.setProperty('background-size', 'auto', 'important')
|
|
330
|
+
st.setProperty('background-position', '0 0', 'important')
|
|
331
|
+
st.setProperty('background-repeat', 'no-repeat', 'important')
|
|
332
|
+
st.setProperty('outline', `${1.5 * zk}px dashed color-mix(in srgb, ${line} 55%, transparent)`, 'important')
|
|
333
|
+
st.setProperty('outline-offset', `${-1.5 * zk}px`, 'important')
|
|
334
|
+
}
|
|
335
|
+
const releaseBg = (el) => {
|
|
336
|
+
const was = el && el.__ddpBg
|
|
337
|
+
if (!was) return
|
|
338
|
+
delete el.__ddpBg
|
|
339
|
+
const put = (k) => {
|
|
340
|
+
const w = was.find((x) => x[0] === k)
|
|
341
|
+
el.style.removeProperty(k)
|
|
342
|
+
if (w && w[1]) el.style.setProperty(k, w[1], w[2])
|
|
343
|
+
}
|
|
344
|
+
for (const k of BG) if (k !== 'transition') put(k)
|
|
345
|
+
// 过渡最后再还(同一帧还的话,页面自己的 transition 会把底色从透明慢慢变过来,闪一下)
|
|
346
|
+
requestAnimationFrame(() => requestAnimationFrame(() => put('transition')))
|
|
347
|
+
}
|
|
348
|
+
const painted = new WeakSet()
|
|
349
|
+
const fill = (c, el, color) => {
|
|
350
|
+
const r0 = el.getBoundingClientRect()
|
|
351
|
+
const dx = Math.max(4, Math.min(r0.width * 0.7, r0.width - 6))
|
|
352
|
+
const dy = Math.max(4, Math.min(r0.height * 0.6, r0.height - 4, vh() - r0.top - 24))
|
|
353
|
+
go(c, el, dx, dy, color, color)
|
|
354
|
+
setTimeout(() => {
|
|
355
|
+
if (!el.__ddpBg) return
|
|
356
|
+
tap(c)
|
|
357
|
+
el.style.setProperty('outline-color', 'transparent', 'important')
|
|
358
|
+
const rim = `color-mix(in srgb, ${color} 55%, #fff)`
|
|
359
|
+
anim(still ? 0 : 950, (t) => {
|
|
360
|
+
if (!el.__ddpBg) return
|
|
361
|
+
const r = el.getBoundingClientRect()
|
|
362
|
+
const R = (Math.hypot(Math.max(dx, r.width - dx), Math.max(dy, r.height - dy)) + 16) * (1 - (1 - t) * (1 - t))
|
|
363
|
+
const e = Math.min(R, 10 * zk)
|
|
364
|
+
el.style.setProperty('background-image', `radial-gradient(circle at ${dx}px ${dy}px, ${color} ${R - e}px, ${rim} ${R - e * 0.3}px, transparent ${R}px)`, 'important')
|
|
365
|
+
}, () => {
|
|
366
|
+
releaseBg(el)
|
|
367
|
+
if (painting && painting.el === el) painting = null
|
|
368
|
+
})
|
|
369
|
+
}, still ? 0 : 620)
|
|
370
|
+
}
|
|
371
|
+
// 没收住底色的(样式后到 / 渐变底):点一下,泛起一圈光
|
|
372
|
+
const paint = (c, el, color) => {
|
|
373
|
+
const r0 = el.getBoundingClientRect()
|
|
374
|
+
const dx = Math.min(r0.width * 0.7, r0.width - 6)
|
|
375
|
+
const dy = Math.min(r0.height * 0.6, r0.height - 4)
|
|
376
|
+
go(c, el, dx, dy, color, color)
|
|
377
|
+
if (still) return
|
|
378
|
+
setTimeout(() => {
|
|
379
|
+
if (done || !el.isConnected || c.at !== el) return
|
|
380
|
+
tap(c)
|
|
381
|
+
const r = el.getBoundingClientRect()
|
|
382
|
+
const w = document.createElement('div')
|
|
383
|
+
w.className = 'paint'
|
|
384
|
+
let rad = ''
|
|
385
|
+
try { rad = getComputedStyle(el).borderRadius } catch { /* 拿不到就方的 */ }
|
|
386
|
+
w.style.width = r.width + 'px'
|
|
387
|
+
w.style.height = r.height + 'px'
|
|
388
|
+
w.style.borderRadius = rad
|
|
389
|
+
w.style.transform = `translate(${r.left}px, ${r.top}px)`
|
|
390
|
+
const dot = document.createElement('i')
|
|
391
|
+
dot.style.setProperty('--c', color)
|
|
392
|
+
dot.style.left = dx + 'px'
|
|
393
|
+
dot.style.top = dy + 'px'
|
|
394
|
+
w.appendChild(dot)
|
|
395
|
+
sr.insertBefore(w, firstCur())
|
|
396
|
+
const item = { w, el }
|
|
397
|
+
paints.push(item)
|
|
398
|
+
const R = Math.hypot(Math.max(dx, r.width - dx), Math.max(dy, r.height - dy)) + 2
|
|
399
|
+
try {
|
|
400
|
+
dot.animate([{ transform: 'scale(0)', opacity: 0.9 }, { transform: `scale(${R * 1.15})`, opacity: 0 }], { duration: 800, easing: EASE, fill: 'forwards' })
|
|
401
|
+
} catch { /* 没有 WAAPI 就不泛 */ }
|
|
402
|
+
setTimeout(() => { w.remove(); paints.splice(paints.indexOf(item), 1) }, 850)
|
|
403
|
+
}, 600)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 文案:藏着的那段字,有字了、量得出大小了,就从头一个个打出来
|
|
407
|
+
const linesOf = (el, er) => {
|
|
408
|
+
const out = []
|
|
409
|
+
try {
|
|
410
|
+
const rg = document.createRange()
|
|
411
|
+
rg.selectNodeContents(el)
|
|
412
|
+
const rects = rg.getClientRects()
|
|
413
|
+
if (rects.length > 400) return [{ x0: 0, x1: er.width, y0: 0, y1: er.height }] // 太长的一段:整块当一行,不逐行量
|
|
414
|
+
for (const q of rects) {
|
|
415
|
+
if (q.width < 1 || q.height < 1) continue
|
|
416
|
+
const y0 = q.top - er.top
|
|
417
|
+
const y1 = q.bottom - er.top
|
|
418
|
+
const hit = (o) => y0 < o.y1 - 2 && y1 > o.y0 + 2
|
|
419
|
+
const z = out[out.length - 1]
|
|
420
|
+
const l = z && hit(z) ? z : out.find(hit) // 大多接在最后一行上:先看它
|
|
421
|
+
if (l) {
|
|
422
|
+
l.x0 = Math.min(l.x0, q.left - er.left)
|
|
423
|
+
l.x1 = Math.max(l.x1, q.right - er.left)
|
|
424
|
+
l.y0 = Math.min(l.y0, y0)
|
|
425
|
+
l.y1 = Math.max(l.y1, y1)
|
|
426
|
+
} else out.push({ x0: q.left - er.left, x1: q.right - er.left, y0, y1 })
|
|
427
|
+
}
|
|
428
|
+
} catch { /* 量不了就整块一行 */ }
|
|
429
|
+
if (!out.length) out.push({ x0: 0, x1: er.width, y0: 0, y1: er.height })
|
|
430
|
+
out.sort((a, b) => a.y0 - b.y0)
|
|
431
|
+
return out
|
|
432
|
+
}
|
|
433
|
+
function tryType() {
|
|
434
|
+
const t = typing
|
|
435
|
+
if (!t) return
|
|
436
|
+
const el = t.el
|
|
437
|
+
const c = curs[2]
|
|
438
|
+
if (done || !el.isConnected) {
|
|
439
|
+
release(el)
|
|
440
|
+
typing = null
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
const r = el.getBoundingClientRect()
|
|
444
|
+
const n = (el.textContent || '').trim().length
|
|
445
|
+
const ready = n >= 2 && r.width > 6 && r.height > 6 && r.top < vh() && r.bottom > 0 && Date.now() >= c.busyTill
|
|
446
|
+
if (ready && el.children.length <= 4 && n <= 600) return type(c, el, n)
|
|
447
|
+
// 等了一阵还没字 / 一直在屏外 / 其实是一大块(里面一堆子元素、一大段字):不打了,直接露出来
|
|
448
|
+
if (Date.now() - t.since > 1600 || el.children.length > 4 || n > 600) {
|
|
449
|
+
release(el)
|
|
450
|
+
typing = null
|
|
451
|
+
return
|
|
452
|
+
}
|
|
453
|
+
requestAnimationFrame(tryType)
|
|
454
|
+
}
|
|
455
|
+
const type = (c, el, n) => {
|
|
456
|
+
const ms = Math.max(280, Math.min(1100, n * 38))
|
|
457
|
+
c.busyTill = Date.now() + ms + 400
|
|
458
|
+
c.at = el
|
|
459
|
+
c.anim = true
|
|
460
|
+
c.el.classList.add('on', 'live')
|
|
461
|
+
c.tip.textContent = ''
|
|
462
|
+
c.dot.classList.remove('on')
|
|
463
|
+
caret.classList.add('on')
|
|
464
|
+
anim(ms, (t) => {
|
|
465
|
+
const er = el.getBoundingClientRect()
|
|
466
|
+
const lines = linesOf(el, er)
|
|
467
|
+
const total = lines.reduce((s, l) => s + (l.x1 - l.x0), 0) || 1
|
|
468
|
+
let left = total * t
|
|
469
|
+
let i = 0
|
|
470
|
+
while (i < lines.length - 1 && left > lines[i].x1 - lines[i].x0) {
|
|
471
|
+
left -= lines[i].x1 - lines[i].x0
|
|
472
|
+
i++
|
|
473
|
+
}
|
|
474
|
+
const L = lines[i]
|
|
475
|
+
const x = Math.min(L.x1, L.x0 + left)
|
|
476
|
+
const W = er.width + 8
|
|
477
|
+
if (t < 1) el.style.setProperty('clip-path', `polygon(-8px -8px, ${W}px -8px, ${W}px ${L.y0}px, ${x}px ${L.y0}px, ${x}px ${L.y1}px, -8px ${L.y1}px)`, 'important')
|
|
478
|
+
caret.style.height = L.y1 - L.y0 + 'px'
|
|
479
|
+
caret.style.transform = `translate(${er.left + x}px, ${er.top + L.y0}px)`
|
|
480
|
+
place(c, er.left + x + 4, er.top + L.y0 + (L.y1 - L.y0) * 0.7)
|
|
481
|
+
}, () => {
|
|
482
|
+
release(el)
|
|
483
|
+
typing = null
|
|
484
|
+
caret.classList.remove('on')
|
|
485
|
+
c.anim = false
|
|
486
|
+
c.el.classList.remove('live')
|
|
487
|
+
const er = el.getBoundingClientRect()
|
|
488
|
+
const ls = linesOf(el, er)
|
|
489
|
+
const L = ls[ls.length - 1]
|
|
490
|
+
c.dx = Math.min(er.width, L.x1 + 6 * zk)
|
|
491
|
+
c.dy = L.y1 + 2 * zk
|
|
492
|
+
place(c, er.left + c.dx, er.top + c.dy)
|
|
493
|
+
})
|
|
494
|
+
}
|
|
495
|
+
// 滚动 / 版面变了:贴着元素的鼠标、框跟着走
|
|
496
|
+
let raf = 0
|
|
497
|
+
function track() {
|
|
498
|
+
if (raf) return
|
|
499
|
+
raf = requestAnimationFrame(() => {
|
|
500
|
+
raf = 0
|
|
501
|
+
for (const c of curs) {
|
|
502
|
+
if (c.anim || !c.at || !c.at.isConnected) continue
|
|
503
|
+
const r = c.at.getBoundingClientRect()
|
|
504
|
+
place(c, r.left + Math.min(c.dx, r.width), r.top + Math.min(c.dy, r.height), r.bottom < 0 || r.top > vh())
|
|
505
|
+
}
|
|
506
|
+
for (const it of boxes) if (it.el.isConnected) sizeBox(it, it.el.getBoundingClientRect())
|
|
507
|
+
for (const it of paints) {
|
|
508
|
+
if (!it.el.isConnected) continue
|
|
509
|
+
const r = it.el.getBoundingClientRect()
|
|
510
|
+
it.w.style.transform = `translate(${r.left}px, ${r.top}px)`
|
|
511
|
+
}
|
|
512
|
+
})
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ── 还没正文:样式里定下来的颜色、字体 ──
|
|
516
|
+
let palOn = false
|
|
517
|
+
let palGone = false
|
|
518
|
+
const swatches = new Map()
|
|
519
|
+
const fonts = new Set()
|
|
520
|
+
const GENERIC = /^(inherit|initial|unset|system-ui|-apple-system|BlinkMacSystemFont|sans-serif|serif|monospace|cursive|ui-\w+)$/i
|
|
521
|
+
const firstFamily = (v) => {
|
|
522
|
+
const f = String(v).split(',')[0].trim().replace(/^["']|["']$/g, '')
|
|
523
|
+
return f && !/^var\(/i.test(f) && !GENERIC.test(f) ? f : ''
|
|
524
|
+
}
|
|
525
|
+
// 写到一半的 <style> 浏览器还没收进去(要等结束标签):自己再拿一份原文流,边到边从里面挑颜色、字体
|
|
526
|
+
let raw = ''
|
|
527
|
+
;(async () => {
|
|
528
|
+
try {
|
|
529
|
+
const r = await fetch(location.href, { cache: 'no-store' })
|
|
530
|
+
if (!r.body) return
|
|
531
|
+
const rd = r.body.getReader()
|
|
532
|
+
const td = new TextDecoder()
|
|
533
|
+
for (;;) {
|
|
534
|
+
if (palGone || raw.length > 600000) break
|
|
535
|
+
const { value, done: end } = await rd.read()
|
|
536
|
+
if (end) break
|
|
537
|
+
raw += td.decode(value, { stream: true })
|
|
538
|
+
if (/<body[\s>]/i.test(raw)) break
|
|
539
|
+
}
|
|
540
|
+
rd.cancel().catch(() => {})
|
|
541
|
+
} catch { /* 拿不到就只看已经生效的样式 */ }
|
|
542
|
+
})()
|
|
543
|
+
const scanCss = () => {
|
|
544
|
+
const b = raw.search(/<body[\s>]/i)
|
|
545
|
+
let txt = b >= 0 ? raw.slice(0, b) : raw
|
|
546
|
+
for (const s of document.querySelectorAll('style')) if (s !== css && !raw) txt += s.textContent + '\n'
|
|
547
|
+
for (const sh of document.styleSheets) {
|
|
548
|
+
if (!sh.ownerNode || sh.ownerNode.tagName === 'STYLE') continue
|
|
549
|
+
try { for (const r of sh.cssRules) txt += r.cssText + '\n' } catch { /* 跨源的样式读不了 */ }
|
|
550
|
+
}
|
|
551
|
+
const re = /--([\w-]+)\s*:\s*([^;{}]+)/g
|
|
552
|
+
let m
|
|
553
|
+
const added = { color: null, font: null }
|
|
554
|
+
while ((m = re.exec(txt))) {
|
|
555
|
+
const name = m[1]
|
|
556
|
+
const v = m[2].trim()
|
|
557
|
+
if (/font|family|serif|sans/i.test(name)) {
|
|
558
|
+
const f = firstFamily(v)
|
|
559
|
+
if (f && !fonts.has(f) && fonts.size < 3) { fonts.add(f); added.font = addFont(f, v) }
|
|
560
|
+
continue
|
|
561
|
+
}
|
|
562
|
+
if (swatches.size >= 12 || swatches.has(v)) continue
|
|
563
|
+
if (!/^(#[0-9a-f]{3,8}\b|rgba?\(|hsla?\(|oklch\(|oklab\(|lab\(|lch\(|color\()/i.test(v)) continue
|
|
564
|
+
let ok = false
|
|
565
|
+
try { ok = CSS.supports('color', v) } catch { ok = false }
|
|
566
|
+
if (ok) { swatches.set(v, name); added.color = addSwatch(v, name) }
|
|
567
|
+
}
|
|
568
|
+
if (fonts.size < 3) {
|
|
569
|
+
const fr = /font-family\s*:\s*([^;{}]+)/gi
|
|
570
|
+
while ((m = fr.exec(txt)) && fonts.size < 3) {
|
|
571
|
+
const f = firstFamily(m[1])
|
|
572
|
+
if (f && !fonts.has(f)) { fonts.add(f); added.font = addFont(f, m[1]) }
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return added
|
|
576
|
+
}
|
|
577
|
+
const addSwatch = (v, name) => {
|
|
578
|
+
const s = document.createElement('span')
|
|
579
|
+
s.innerHTML = '<i></i><em></em>'
|
|
580
|
+
s.firstChild.style.background = v
|
|
581
|
+
s.lastChild.textContent = name
|
|
582
|
+
swBox.appendChild(s)
|
|
583
|
+
return s
|
|
584
|
+
}
|
|
585
|
+
const addFont = (f, stack) => {
|
|
586
|
+
const s = document.createElement('span')
|
|
587
|
+
s.innerHTML = '<b></b><em></em>'
|
|
588
|
+
s.firstChild.textContent = zh ? 'Aa 字' : 'Aa'
|
|
589
|
+
s.firstChild.style.fontFamily = stack
|
|
590
|
+
s.lastChild.textContent = f
|
|
591
|
+
ftBox.appendChild(s)
|
|
592
|
+
return s
|
|
593
|
+
}
|
|
594
|
+
// 正文一冒头,配色卡片马上让开(别和页面叠着)
|
|
595
|
+
const dropPal = () => {
|
|
596
|
+
if (palGone) return
|
|
597
|
+
palGone = true
|
|
598
|
+
if (palOn) {
|
|
599
|
+
pal.classList.add('out')
|
|
600
|
+
setTimeout(() => pal.remove(), 300)
|
|
601
|
+
} else pal.remove()
|
|
602
|
+
}
|
|
603
|
+
// 正文有东西了没有(body 里有看得见的元素)
|
|
604
|
+
const hasBody = () => {
|
|
605
|
+
const b = document.body
|
|
606
|
+
if (!b) return false
|
|
607
|
+
for (const k of b.children) {
|
|
608
|
+
if (SKIP.test(k.tagName)) continue
|
|
609
|
+
const r = k.getBoundingClientRect()
|
|
610
|
+
if (r.width > 0 && r.height > 8) return true
|
|
611
|
+
}
|
|
612
|
+
return false
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// ── 主循环:每一拍看看长出了什么,有新的才动 ──
|
|
616
|
+
let turn = 0
|
|
617
|
+
// 有正文了:页面没给底色就垫白(不然底下的占位透上来,和字叠在一起)
|
|
618
|
+
// 每拍重判(样式可能后到):先撤掉自己垫的,页面自己给了底色就不垫
|
|
619
|
+
let padded = false
|
|
620
|
+
const backdrop = () => {
|
|
621
|
+
if (padded) de.style.removeProperty('background-color')
|
|
622
|
+
padded = false
|
|
623
|
+
if (bgOf(de)) return
|
|
624
|
+
de.style.backgroundColor = (document.body && bgOf(document.body)) || '#ffffff'
|
|
625
|
+
padded = true
|
|
626
|
+
}
|
|
627
|
+
const tick = () => {
|
|
628
|
+
if (done) return
|
|
629
|
+
const now = Date.now()
|
|
630
|
+
if (!palGone) {
|
|
631
|
+
if (hasBody()) dropPal()
|
|
632
|
+
else {
|
|
633
|
+
const add = scanCss()
|
|
634
|
+
if (swatches.size || fonts.size) {
|
|
635
|
+
if (!palOn) { palOn = true; pal.classList.add('on') }
|
|
636
|
+
const pr = () => pal.getBoundingClientRect()
|
|
637
|
+
// 配色那只去点刚摆上的颜色,排版那只去指刚出的字体
|
|
638
|
+
if (add.color) setTimeout(() => { const r = add.color.getBoundingClientRect(); if (r.width) { curs[1].at = null; curs[1].el.classList.add('on'); curs[1].tip.textContent = ''; curs[1].dot.classList.remove('on'); place(curs[1], r.left + r.width * 0.6, r.top + r.height * 0.45); setTimeout(() => tap(curs[1]), 520) } }, 120)
|
|
639
|
+
if (add.font) setTimeout(() => { const r = add.font.getBoundingClientRect(); if (r.width) { curs[0].at = null; curs[0].el.classList.add('on'); curs[0].tip.textContent = ''; curs[0].dot.classList.remove('on'); place(curs[0], r.left + 30, r.top + r.height * 0.7) } }, 260)
|
|
640
|
+
if (!curs[2].el.classList.contains('on')) { const r = pr(); curs[2].el.classList.add('on'); place(curs[2], r.right - 24, r.top + 8) }
|
|
641
|
+
}
|
|
642
|
+
return
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
backdrop()
|
|
646
|
+
// 刚长出来的:过一两拍再看(字、样式可能还没到,量出来是 0)
|
|
647
|
+
const ready = []
|
|
648
|
+
for (let i = fresh.length - 1; i >= 0; i--) {
|
|
649
|
+
const f = fresh[i]
|
|
650
|
+
if (!f.el.isConnected || f.el.closest('head')) { fresh.splice(i, 1); continue }
|
|
651
|
+
f.age++
|
|
652
|
+
if (f.age >= 1) { ready.push(f.el); fresh.splice(i, 1) }
|
|
653
|
+
}
|
|
654
|
+
if (fresh.length > 400) fresh.splice(0, fresh.length - 400)
|
|
655
|
+
chase()
|
|
656
|
+
if (painting && !painting.on) {
|
|
657
|
+
const p = painting
|
|
658
|
+
const r = p.el.getBoundingClientRect()
|
|
659
|
+
const c = curs[1]
|
|
660
|
+
if (!p.el.isConnected || now - p.since > 2600) {
|
|
661
|
+
releaseBg(p.el)
|
|
662
|
+
painting = null
|
|
663
|
+
} else if (r.width >= 36 && r.height >= 22 && visible(r) && (now >= c.busyTill || c.soft) && !c.anim) {
|
|
664
|
+
p.on = true
|
|
665
|
+
c.busyTill = now + 2000
|
|
666
|
+
c.soft = false
|
|
667
|
+
fill(c, p.el, p.color)
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (!ready.length) return
|
|
671
|
+
const bg = pageBg()
|
|
672
|
+
const picks = [null, null, null]
|
|
673
|
+
// ready 里新的在前:倒着挑,先挑早出来的(外层的块先出来,量起来也更完整)
|
|
674
|
+
for (let i = ready.length - 1; i >= 0; i--) {
|
|
675
|
+
const el = ready[i]
|
|
676
|
+
const r = el.getBoundingClientRect()
|
|
677
|
+
if (!visible(r)) continue
|
|
678
|
+
if (!picks[0] && BLOCK.test(el.tagName) && r.width >= vw() * 0.42 && r.height >= 30 && r.height < vh() * 1.2) picks[0] = { el, r }
|
|
679
|
+
if (!picks[1] && !painted.has(el)) {
|
|
680
|
+
const c = bgOf(el)
|
|
681
|
+
if (c && apart(c, bg) && r.width >= 14 && r.height >= 12 && r.width * r.height < vw() * vh() * 0.7) picks[1] = { el, r, c }
|
|
682
|
+
}
|
|
683
|
+
if (picks[0] && picks[1]) break
|
|
684
|
+
}
|
|
685
|
+
// 轮流来:这一拍谁先动换着排,两只不同时起步(文案那只看见新的字就自己去打,见 tryType)
|
|
686
|
+
const order = [0, 1].map((k) => (k + turn) % 2)
|
|
687
|
+
turn++
|
|
688
|
+
order.forEach((k, j) => {
|
|
689
|
+
const p = picks[k]
|
|
690
|
+
const c = curs[k]
|
|
691
|
+
if (!p || p.el === c.at || now < c.busyTill || c.anim) return
|
|
692
|
+
if (k === 1 && painting) return // 有块收着底色等刷:先刷它
|
|
693
|
+
c.busyTill = now + (k === 0 ? 1700 : 1500)
|
|
694
|
+
if (k === 1) c.soft = true
|
|
695
|
+
setTimeout(() => {
|
|
696
|
+
if (done || !p.el.isConnected) return
|
|
697
|
+
const r = p.el.getBoundingClientRect()
|
|
698
|
+
if (k === 0) drag(c, p.el, KIND[kindOf(p.el, r)])
|
|
699
|
+
else paint(c, p.el, p.c)
|
|
700
|
+
}, j * 260)
|
|
701
|
+
})
|
|
702
|
+
// 闲着的:挪到正在写的地方(最新冒出来的东西下面)候着——只在真有新东西时挪,不自己晃
|
|
703
|
+
let front = null
|
|
704
|
+
for (const el of ready) {
|
|
705
|
+
const r = el.getBoundingClientRect()
|
|
706
|
+
if (visible(r)) { front = r; break }
|
|
707
|
+
}
|
|
708
|
+
if (!front) return
|
|
709
|
+
curs.forEach((c, k) => {
|
|
710
|
+
if (c.anim || now < c.busyTill + 500 || now < (c.restTill || 0)) return
|
|
711
|
+
if ((k === 2 && typing) || (k === 1 && painting)) return
|
|
712
|
+
c.restTill = now + 1500 + k * 250
|
|
713
|
+
c.at = null
|
|
714
|
+
c.tip.textContent = ''
|
|
715
|
+
c.dot.classList.remove('on')
|
|
716
|
+
c.el.classList.add('on')
|
|
717
|
+
place(c, vw() * (0.2 + k * 0.27) + (turn % 3) * 6 * zk, Math.min(vh() - 34 * zk, front.bottom + (10 + k * 6) * zk))
|
|
718
|
+
})
|
|
719
|
+
}
|
|
720
|
+
const timer = setInterval(tick, still ? 900 : 650)
|
|
721
|
+
addEventListener('resize', track)
|
|
722
|
+
|
|
723
|
+
// ── 写完:停下、鼠标退场、回到顶上(右侧栏马上换上真文件) ──
|
|
724
|
+
// 文档读完就算(不等 load:页面引的外部字体、图片卡着的话 load 要等很久)
|
|
725
|
+
let fin = false
|
|
726
|
+
const finish = () => {
|
|
727
|
+
if (fin) return
|
|
728
|
+
fin = true
|
|
729
|
+
tick()
|
|
730
|
+
done = true
|
|
731
|
+
if (typing) {
|
|
732
|
+
release(typing.el)
|
|
733
|
+
typing = null
|
|
734
|
+
}
|
|
735
|
+
if (painting) {
|
|
736
|
+
releaseBg(painting.el)
|
|
737
|
+
painting = null
|
|
738
|
+
}
|
|
739
|
+
caret.classList.remove('on')
|
|
740
|
+
clearInterval(timer)
|
|
741
|
+
mo.disconnect()
|
|
742
|
+
if (!palGone) pal.remove()
|
|
743
|
+
setTimeout(() => {
|
|
744
|
+
curs.forEach((c, i) => setTimeout(() => { c.el.classList.remove('on'); place(c, c.x, vh() + 40) }, i * 140))
|
|
745
|
+
if (scrollY > 0) scrollTo({ top: 0, behavior: still ? 'auto' : 'smooth' })
|
|
746
|
+
}, 700)
|
|
747
|
+
// 放完了(鼠标也退场了):告诉右侧栏,它这时才换上真文件
|
|
748
|
+
setTimeout(() => { try { parent.postMessage({ dshDraft: 'end' }, '*') } catch { /* 没有父页面 */ } }, still ? 200 : 1400)
|
|
749
|
+
}
|
|
750
|
+
if (document.readyState === 'loading') addEventListener('DOMContentLoaded', finish)
|
|
751
|
+
else finish()
|
|
752
|
+
addEventListener('load', finish)
|
|
753
|
+
|
|
754
|
+
// ── 和右侧栏:报到,收缩放(画布缩小时鼠标、字跟着放大,看起来一样大) ──
|
|
755
|
+
addEventListener('message', (e) => {
|
|
756
|
+
if (e.source !== parent || !e.data || e.data.dshDraft !== 'zoom') return
|
|
757
|
+
const z = Number(e.data.z) || 1
|
|
758
|
+
zk = Math.max(1, Math.min(6, 1 / z))
|
|
759
|
+
host.style.setProperty('--k', String(zk))
|
|
760
|
+
track()
|
|
761
|
+
})
|
|
762
|
+
try { parent.postMessage({ dshDraft: 'hello' }, '*') } catch { /* 没有父页面 */ }
|
|
763
|
+
})()
|