@ash-qw/dsh-theme-prts 0.1.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +24 -0
- package/README.en.md +103 -0
- package/README.md +107 -0
- package/THIRD_PARTY_NOTICES.md +48 -0
- package/cordis.patch.yml +4 -0
- package/lib/client.js +7466 -0
- package/lib/index.js +3 -0
- package/package.json +64 -0
- package/src/assets/faction-emblems/lungmen.png +0 -0
- package/src/assets/faction-emblems/penguin-logistics.png +0 -0
- package/src/assets/faction-emblems/reunion.png +0 -0
- package/src/assets/faction-emblems/rhine-lab.png +0 -0
- package/src/assets/faction-emblems/rhodes-island.png +0 -0
- package/src/assets/prts-original-avatar.png +0 -0
- package/src/assets/rhodes-dsh.svg +6 -0
- package/src/assets/topographic-map.svg +72 -0
- package/src/assets/topographic-tech.svg +19 -0
- package/src/client/assistant-glass-adapter.js +231 -0
- package/src/client/composer-glass-adapter.js +210 -0
- package/src/client/conversation-control-adapter.js +109 -0
- package/src/client/conversation-scale-adapter.js +2243 -0
- package/src/client/facility-geometry.js +60 -0
- package/src/client/facility-vector.js +230 -0
- package/src/client/floating-glass-adapter.js +177 -0
- package/src/client/host-geometry-adapter.js +123 -0
- package/src/client/index.js +359 -0
- package/src/client/layout-stability-diagnostics.js +162 -0
- package/src/client/operations-shell-v2.js +248 -0
- package/src/client/particle-field-adapter.js +1840 -0
- package/src/client/preferences.js +239 -0
- package/src/client/rc7-adapter.js +83 -0
- package/src/client/resize-shield-adapter.js +64 -0
- package/src/client/settings-page.js +62 -0
- package/src/client/sidebar-control-adapter.js +317 -0
- package/src/client/startup-sequence.js +366 -0
- package/src/client/status-projection.js +79 -0
- package/src/client/terrain-motion-adapter.js +188 -0
- package/src/client/theme-controller-v2.js +134 -0
- package/src/client/theme-controller.js +119 -0
- package/src/client/theme-settings-workbench.js +583 -0
- package/src/client/to-bottom-adapter.js +85 -0
- package/src/client/ui-store.js +23 -0
- package/src/index.js +4 -0
- package/src/styles/conversation-scale.css +471 -0
- package/src/styles/facility-vector.css +292 -0
- package/src/styles/prts.css +1223 -0
- package/src/styles/refinement.css +429 -0
- package/src/styles/stabilization.css +161 -0
- package/src/styles/startup-sequence.css +496 -0
- package/src/styles/theme-settings-workbench.css +803 -0
|
@@ -0,0 +1,1840 @@
|
|
|
1
|
+
const PHONE_BREAKPOINT = 640
|
|
2
|
+
const TABLET_BREAKPOINT = 1024
|
|
3
|
+
const HERO_SELECTOR = '[data-slot="conversation.hero.brand.mark"], [data-phase="hero"] [data-slot*="hero.brand.mark"]'
|
|
4
|
+
const TRANSITION_MS = 900
|
|
5
|
+
const DENSITY_DEBOUNCE_MS = 180
|
|
6
|
+
const DENSITY_REASSEMBLY_MS = 780
|
|
7
|
+
const DENSITY_CROSSFADE_MS = 140
|
|
8
|
+
const RESIZE_SETTLE_MS = 160
|
|
9
|
+
const RESIZE_RESTORE_MS = 120
|
|
10
|
+
const HYDRATION_SETTLE_MS = 120
|
|
11
|
+
const IDLE_SETTLE_FRAMES = 8
|
|
12
|
+
const IDLE_SETTLE_SPEED = 0.02
|
|
13
|
+
const IDLE_SETTLE_GLOW_DISTANCE = 0.15
|
|
14
|
+
const HERO_MINIMUM_DENSITY = 'standard'
|
|
15
|
+
const DENSITY_ORDER = Object.freeze(['sparse', 'light', 'standard', 'dense', 'ultra'])
|
|
16
|
+
export const PARTICLE_DENSITY_PROFILES = Object.freeze({
|
|
17
|
+
sparse: Object.freeze({ pitch: 5.4, radius: 1.04 }),
|
|
18
|
+
light: Object.freeze({ pitch: 4.8, radius: 0.92 }),
|
|
19
|
+
standard: Object.freeze({ pitch: 4.2, radius: 0.82 }),
|
|
20
|
+
dense: Object.freeze({ pitch: 3.7, radius: 0.73 }),
|
|
21
|
+
ultra: Object.freeze({ pitch: 3.25, radius: 0.65 }),
|
|
22
|
+
})
|
|
23
|
+
const SIGNIFICANT_COMPONENT_AREA = 20
|
|
24
|
+
const LATTICE_PHASE_DIVISIONS = 20
|
|
25
|
+
const MASK_COMPONENT_CACHE = new WeakMap()
|
|
26
|
+
|
|
27
|
+
function countMatchingDescendants(node, selector, limit = 16) {
|
|
28
|
+
const document = node?.ownerDocument
|
|
29
|
+
if (!document?.createTreeWalker) {
|
|
30
|
+
return Math.min(limit, node?.querySelectorAll?.(selector)?.length ?? 0)
|
|
31
|
+
}
|
|
32
|
+
const showElement = document.defaultView?.NodeFilter?.SHOW_ELEMENT ?? 1
|
|
33
|
+
const walker = document.createTreeWalker(node, showElement)
|
|
34
|
+
let count = 0
|
|
35
|
+
let current = walker.firstChild()
|
|
36
|
+
while (current && count < limit) {
|
|
37
|
+
if (current.matches?.(selector)) count += 1
|
|
38
|
+
current = walker.nextNode()
|
|
39
|
+
}
|
|
40
|
+
return count
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function clamp(value, min, max) {
|
|
44
|
+
return Math.min(max, Math.max(min, value))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function pointerRadiusForFieldSize(fieldSize) {
|
|
48
|
+
const size = Number(fieldSize)
|
|
49
|
+
return clamp((Number.isFinite(size) ? size : 0) * 0.2, 56, 96)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function easeInOut(value) {
|
|
53
|
+
const progress = clamp(value, 0, 1)
|
|
54
|
+
return progress < 0.5
|
|
55
|
+
? 2 * progress * progress
|
|
56
|
+
: 1 - Math.pow(-2 * progress + 2, 2) / 2
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function easeOut(value) {
|
|
60
|
+
return 1 - Math.pow(1 - clamp(value, 0, 1), 3)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function interpolate(from, to, progress) {
|
|
64
|
+
return from + (to - from) * clamp(progress, 0, 1)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function seededRandom(seed) {
|
|
68
|
+
let value = seed >>> 0
|
|
69
|
+
return () => {
|
|
70
|
+
value += 0x6D2B79F5
|
|
71
|
+
let result = value
|
|
72
|
+
result = Math.imul(result ^ result >>> 15, result | 1)
|
|
73
|
+
result ^= result + Math.imul(result ^ result >>> 7, result | 61)
|
|
74
|
+
return ((result ^ result >>> 14) >>> 0) / 4294967296
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function stableHash(value) {
|
|
79
|
+
let hash = 2166136261
|
|
80
|
+
for (const character of String(value)) {
|
|
81
|
+
hash ^= character.codePointAt(0)
|
|
82
|
+
hash = Math.imul(hash, 16777619)
|
|
83
|
+
}
|
|
84
|
+
return hash >>> 0
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function findAlphaBounds(pixels, width, height, threshold = 16) {
|
|
88
|
+
const channels = pixels?.length >= width * height * 4 ? 4 : 1
|
|
89
|
+
let left = width
|
|
90
|
+
let top = height
|
|
91
|
+
let right = -1
|
|
92
|
+
let bottom = -1
|
|
93
|
+
for (let y = 0; y < height; y += 1) {
|
|
94
|
+
for (let x = 0; x < width; x += 1) {
|
|
95
|
+
const alpha = pixels[(y * width + x) * channels + (channels === 4 ? 3 : 0)]
|
|
96
|
+
if (alpha <= threshold) continue
|
|
97
|
+
left = Math.min(left, x)
|
|
98
|
+
top = Math.min(top, y)
|
|
99
|
+
right = Math.max(right, x)
|
|
100
|
+
bottom = Math.max(bottom, y)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (right < left || bottom < top) return null
|
|
104
|
+
return { left, top, right, bottom, width: right - left + 1, height: bottom - top + 1 }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function alphaAt(mask, x, y) {
|
|
108
|
+
const column = Math.round(x)
|
|
109
|
+
const row = Math.round(y)
|
|
110
|
+
if (column < 0 || row < 0 || column >= mask.width || row >= mask.height) return 0
|
|
111
|
+
return mask.alpha[row * mask.width + column] ?? 0
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function maskComponents(mask) {
|
|
115
|
+
const cached = MASK_COMPONENT_CACHE.get(mask)
|
|
116
|
+
if (cached) return cached
|
|
117
|
+
const labels = new Int32Array(mask.width * mask.height)
|
|
118
|
+
labels.fill(-1)
|
|
119
|
+
const sizes = []
|
|
120
|
+
for (let index = 0; index < labels.length; index += 1) {
|
|
121
|
+
if (labels[index] >= 0 || (mask.alpha[index] ?? 0) <= 40) continue
|
|
122
|
+
const id = sizes.length
|
|
123
|
+
const queue = [index]
|
|
124
|
+
labels[index] = id
|
|
125
|
+
let size = 0
|
|
126
|
+
for (let cursor = 0; cursor < queue.length; cursor += 1) {
|
|
127
|
+
const current = queue[cursor]
|
|
128
|
+
const x = current % mask.width
|
|
129
|
+
const y = Math.floor(current / mask.width)
|
|
130
|
+
size += 1
|
|
131
|
+
for (let offsetY = -1; offsetY <= 1; offsetY += 1) {
|
|
132
|
+
for (let offsetX = -1; offsetX <= 1; offsetX += 1) {
|
|
133
|
+
if (!offsetX && !offsetY) continue
|
|
134
|
+
const column = x + offsetX
|
|
135
|
+
const row = y + offsetY
|
|
136
|
+
if (column < 0 || row < 0 || column >= mask.width || row >= mask.height) continue
|
|
137
|
+
const neighbor = row * mask.width + column
|
|
138
|
+
if (labels[neighbor] >= 0 || (mask.alpha[neighbor] ?? 0) <= 40) continue
|
|
139
|
+
labels[neighbor] = id
|
|
140
|
+
queue.push(neighbor)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
sizes.push(size)
|
|
145
|
+
}
|
|
146
|
+
const required = new Set(sizes.flatMap((size, index) => size >= SIGNIFICANT_COMPONENT_AREA ? [index] : []))
|
|
147
|
+
const result = { labels, sizes, required }
|
|
148
|
+
MASK_COMPONENT_CACHE.set(mask, result)
|
|
149
|
+
return result
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function latticeAtPhase(mask, pitch, offsetX, offsetY, components) {
|
|
153
|
+
const points = []
|
|
154
|
+
const componentHits = new Set()
|
|
155
|
+
const probe = Math.max(1.5, pitch * 0.46)
|
|
156
|
+
let latticeRow = 0
|
|
157
|
+
for (let y = offsetY; y < mask.height; y += pitch, latticeRow += 1) {
|
|
158
|
+
let latticeColumn = 0
|
|
159
|
+
for (let x = offsetX; x < mask.width; x += pitch, latticeColumn += 1) {
|
|
160
|
+
if (alphaAt(mask, x, y) <= 40) continue
|
|
161
|
+
const pixelX = Math.round(x)
|
|
162
|
+
const pixelY = Math.round(y)
|
|
163
|
+
const component = components.labels[pixelY * mask.width + pixelX]
|
|
164
|
+
if (component >= 0) componentHits.add(component)
|
|
165
|
+
const boundary = alphaAt(mask, x - probe, y) <= 40
|
|
166
|
+
|| alphaAt(mask, x + probe, y) <= 40
|
|
167
|
+
|| alphaAt(mask, x, y - probe) <= 40
|
|
168
|
+
|| alphaAt(mask, x, y + probe) <= 40
|
|
169
|
+
points.push({
|
|
170
|
+
x: (x - mask.width / 2) / Math.max(mask.width, mask.height),
|
|
171
|
+
y: (y - mask.height / 2) / Math.max(mask.width, mask.height),
|
|
172
|
+
kind: boundary ? 'skeleton' : 'fill',
|
|
173
|
+
latticeColumn,
|
|
174
|
+
latticeRow,
|
|
175
|
+
component,
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
let missing = 0
|
|
180
|
+
for (const component of components.required) if (!componentHits.has(component)) missing += 1
|
|
181
|
+
return { points, componentHits, missing }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function findOrthogonalLayout(mask, density = 'standard') {
|
|
185
|
+
if (!mask?.alpha?.length || !Number.isInteger(mask.width) || !Number.isInteger(mask.height)) return null
|
|
186
|
+
const profile = PARTICLE_DENSITY_PROFILES[density] ?? PARTICLE_DENSITY_PROFILES.standard
|
|
187
|
+
const components = maskComponents(mask)
|
|
188
|
+
if (!components.sizes.length) return null
|
|
189
|
+
const hint = mask.latticeHints?.[density]
|
|
190
|
+
if (Number.isFinite(hint?.pitch) && Number.isFinite(hint?.offsetX) && Number.isFinite(hint?.offsetY)) {
|
|
191
|
+
const candidate = latticeAtPhase(mask, hint.pitch, hint.offsetX, hint.offsetY, components)
|
|
192
|
+
if (candidate.missing === 0) {
|
|
193
|
+
return { ...candidate, pitch: hint.pitch, offsetX: hint.offsetX, offsetY: hint.offsetY }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
let pitch = profile.pitch
|
|
197
|
+
let best
|
|
198
|
+
for (let refinement = 0; refinement < 12; refinement += 1) {
|
|
199
|
+
for (let phaseY = 0; phaseY < LATTICE_PHASE_DIVISIONS; phaseY += 1) {
|
|
200
|
+
for (let phaseX = 0; phaseX < LATTICE_PHASE_DIVISIONS; phaseX += 1) {
|
|
201
|
+
const offsetX = (phaseX + 0.5) * pitch / LATTICE_PHASE_DIVISIONS
|
|
202
|
+
const offsetY = (phaseY + 0.5) * pitch / LATTICE_PHASE_DIVISIONS
|
|
203
|
+
const candidate = latticeAtPhase(mask, pitch, offsetX, offsetY, components)
|
|
204
|
+
if (!best
|
|
205
|
+
|| candidate.missing < best.missing
|
|
206
|
+
|| candidate.missing === best.missing && candidate.points.length > best.points.length) {
|
|
207
|
+
best = { ...candidate, pitch, offsetX, offsetY }
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (best?.missing === 0) break
|
|
212
|
+
pitch *= 0.94
|
|
213
|
+
}
|
|
214
|
+
return best ?? null
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function createOrthogonalTargets(mask, density = 'standard') {
|
|
218
|
+
return findOrthogonalLayout(mask, density)?.points ?? []
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function inspectOrthogonalCoverage(mask, density = 'standard') {
|
|
222
|
+
const targets = createOrthogonalTargets(mask, density)
|
|
223
|
+
const components = maskComponents(mask)
|
|
224
|
+
const hits = new Set(targets.map(target => target.component).filter(component => component >= 0))
|
|
225
|
+
const missingComponents = [...components.required].filter(component => !hits.has(component))
|
|
226
|
+
return {
|
|
227
|
+
targets,
|
|
228
|
+
requiredComponents: components.required.size,
|
|
229
|
+
coveredComponents: components.required.size - missingComponents.length,
|
|
230
|
+
missingComponents,
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function sideForSegment(segment) {
|
|
234
|
+
return Math.abs(segment) % 2 === 0 ? -1 : 1
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function normalizeEmblems(emblems, legacyEmblem) {
|
|
238
|
+
const normalized = Array.isArray(emblems)
|
|
239
|
+
? emblems.filter(item => item && (item.key || typeof item.source === 'string')).map((item, index) => ({
|
|
240
|
+
key: String(item.key || `emblem-${index + 1}`),
|
|
241
|
+
label: String(item.label || item.key || `徽记 ${index + 1}`),
|
|
242
|
+
source: typeof item.source === 'string' ? item.source : '',
|
|
243
|
+
}))
|
|
244
|
+
: []
|
|
245
|
+
if (normalized.length) return normalized
|
|
246
|
+
return legacyEmblem ? [{ key: 'rhodes-island', label: '罗德岛', source: legacyEmblem }] : []
|
|
247
|
+
}
|
|
248
|
+
function decodeAlphaMask(encoded, window, encoding, expectedLength) {
|
|
249
|
+
if (typeof encoded !== 'string' || !encoded) return null
|
|
250
|
+
try {
|
|
251
|
+
const binary = window.atob(encoded)
|
|
252
|
+
if (encoding === 'binary-rle-v1') {
|
|
253
|
+
if (!Number.isInteger(expectedLength) || expectedLength < 1 || !binary.length) return null
|
|
254
|
+
const alpha = new Uint8Array(expectedLength)
|
|
255
|
+
let byteIndex = 1
|
|
256
|
+
let outputIndex = 0
|
|
257
|
+
let value = binary.charCodeAt(0) ? 255 : 0
|
|
258
|
+
while (byteIndex < binary.length && outputIndex < expectedLength) {
|
|
259
|
+
let runLength = 0
|
|
260
|
+
let shift = 0
|
|
261
|
+
let byte
|
|
262
|
+
do {
|
|
263
|
+
if (byteIndex >= binary.length || shift > 28) return null
|
|
264
|
+
byte = binary.charCodeAt(byteIndex++)
|
|
265
|
+
runLength |= (byte & 0x7f) << shift
|
|
266
|
+
shift += 7
|
|
267
|
+
} while (byte & 0x80)
|
|
268
|
+
if (runLength < 1 || outputIndex + runLength > expectedLength) return null
|
|
269
|
+
alpha.fill(value, outputIndex, outputIndex + runLength)
|
|
270
|
+
outputIndex += runLength
|
|
271
|
+
value = value ? 0 : 255
|
|
272
|
+
}
|
|
273
|
+
return outputIndex === expectedLength ? alpha : null
|
|
274
|
+
}
|
|
275
|
+
const alpha = new Uint8Array(binary.length)
|
|
276
|
+
for (let index = 0; index < binary.length; index += 1) alpha[index] = binary.charCodeAt(index)
|
|
277
|
+
return alpha
|
|
278
|
+
} catch {
|
|
279
|
+
return null
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function normalizeEmblemMasks(masks, window) {
|
|
283
|
+
const records = new Map()
|
|
284
|
+
for (const mask of Array.isArray(masks) ? masks : []) {
|
|
285
|
+
const width = Number(mask?.width)
|
|
286
|
+
const height = Number(mask?.height)
|
|
287
|
+
const alpha = decodeAlphaMask(mask?.alpha, window, mask?.encoding, width * height)
|
|
288
|
+
if (!mask?.key || !Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1) continue
|
|
289
|
+
if (!alpha || alpha.length !== width * height) continue
|
|
290
|
+
records.set(String(mask.key), {
|
|
291
|
+
width,
|
|
292
|
+
height,
|
|
293
|
+
alpha,
|
|
294
|
+
opticalScale: Number(mask.opticalScale) || 0.86,
|
|
295
|
+
latticeHints: mask.latticeHints && typeof mask.latticeHints === 'object' ? mask.latticeHints : {},
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
return records
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
function createAmbient(document) {
|
|
303
|
+
const layer = document.createElement('div')
|
|
304
|
+
layer.setAttribute('data-prts-ambient-layer', '')
|
|
305
|
+
layer.setAttribute('aria-hidden', 'true')
|
|
306
|
+
|
|
307
|
+
const glow = document.createElement('span')
|
|
308
|
+
glow.setAttribute('data-prts-ambient-glow', '')
|
|
309
|
+
layer.appendChild(glow)
|
|
310
|
+
|
|
311
|
+
for (const side of ['left', 'right']) {
|
|
312
|
+
const edge = document.createElement('span')
|
|
313
|
+
edge.setAttribute('data-prts-ambient-edge', side)
|
|
314
|
+
layer.appendChild(edge)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return { layer, glow }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
export function createParticleFieldAdapter({ document, window, emblem = '', emblems = [], emblemMasks = [], heroEmblemMask, onStateChange = () => {} }) {
|
|
322
|
+
const suppliedMasks = document && window ? normalizeEmblemMasks(emblemMasks, window) : new Map()
|
|
323
|
+
const requestedEmblems = normalizeEmblems(emblems, emblem)
|
|
324
|
+
const emblemSequence = requestedEmblems.filter(entry => suppliedMasks.has(entry.key))
|
|
325
|
+
const missingEmblems = requestedEmblems.filter(entry => !suppliedMasks.has(entry.key))
|
|
326
|
+
const rhodesIndex = Math.max(0, emblemSequence.findIndex(entry => entry.key === 'rhodes-island'))
|
|
327
|
+
const suppliedHeroMask = normalizeEmblemMasks(heroEmblemMask ? [heroEmblemMask] : [], window).get('rhodes-island-hero')
|
|
328
|
+
const empty = { mounted: false, hero: false, side: -1, transition: null, anchor: { horizontalProgress: 0, verticalProgress: 0 }, phase: 'error', error: '徽记资源异常' }
|
|
329
|
+
if (!document || !window) return { update() {}, inspect: () => empty, dispose() {} }
|
|
330
|
+
|
|
331
|
+
let preferences = {}
|
|
332
|
+
let operation
|
|
333
|
+
let scroller
|
|
334
|
+
let ambient
|
|
335
|
+
let glow
|
|
336
|
+
let canvas
|
|
337
|
+
let context
|
|
338
|
+
let mutationObserver
|
|
339
|
+
let mountFrame
|
|
340
|
+
let resizeObserver
|
|
341
|
+
let intersectionObserver
|
|
342
|
+
let mediaQuery
|
|
343
|
+
let mediaListener
|
|
344
|
+
let frame
|
|
345
|
+
let resizeFrame
|
|
346
|
+
let resizeCommitFrame
|
|
347
|
+
let resizeFinishFrame
|
|
348
|
+
let resizeTimer
|
|
349
|
+
let resizeRestoreTimer
|
|
350
|
+
let pendingResizeSize
|
|
351
|
+
const maskRecords = emblemSequence.map(entry => {
|
|
352
|
+
const mask = suppliedMasks.get(entry.key)
|
|
353
|
+
return {
|
|
354
|
+
mask: { key: entry.key, width: mask.width, height: mask.height, alpha: mask.alpha, latticeHints: mask.latticeHints },
|
|
355
|
+
opticalScale: mask.opticalScale,
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
const heroMaskRecord = suppliedHeroMask ? {
|
|
359
|
+
mask: {
|
|
360
|
+
key: 'rhodes-island-hero',
|
|
361
|
+
width: suppliedHeroMask.width,
|
|
362
|
+
height: suppliedHeroMask.height,
|
|
363
|
+
alpha: suppliedHeroMask.alpha,
|
|
364
|
+
latticeHints: suppliedHeroMask.latticeHints,
|
|
365
|
+
},
|
|
366
|
+
opticalScale: suppliedHeroMask.opticalScale,
|
|
367
|
+
} : maskRecords[rhodesIndex]
|
|
368
|
+
const targetCache = new Map()
|
|
369
|
+
let particles = []
|
|
370
|
+
let pointerActive = false
|
|
371
|
+
let pointerX = 0
|
|
372
|
+
let pointerY = 0
|
|
373
|
+
let pointerClientX = 0
|
|
374
|
+
let pointerClientY = 0
|
|
375
|
+
let width = 1
|
|
376
|
+
let height = 1
|
|
377
|
+
let dpr = 1
|
|
378
|
+
let side = -1
|
|
379
|
+
let segment = 0
|
|
380
|
+
let transition
|
|
381
|
+
let pendingSegment
|
|
382
|
+
let transitionStartedAt
|
|
383
|
+
let densityTransition
|
|
384
|
+
let densityStartedAt
|
|
385
|
+
let densityDebounce
|
|
386
|
+
let queuedDensityCount
|
|
387
|
+
let queuedTargetRefresh = false
|
|
388
|
+
let geometryDirty = true
|
|
389
|
+
let resizing = false
|
|
390
|
+
let resizeObservations = 0
|
|
391
|
+
let resizeCommits = 0
|
|
392
|
+
let backingStoreCommits = 0
|
|
393
|
+
let targetPrewarmHandle
|
|
394
|
+
let targetPrewarmQueue = []
|
|
395
|
+
let pauseStartedAt
|
|
396
|
+
let hydrationPaused = false
|
|
397
|
+
let hydrationTimer
|
|
398
|
+
let hydrationFrame
|
|
399
|
+
let hydrationStableFrames = 0
|
|
400
|
+
let mutationBurstScore = 0
|
|
401
|
+
let mutationBurstTimer
|
|
402
|
+
let glowX = 0
|
|
403
|
+
let glowY = 0
|
|
404
|
+
let idleStableFrames = 0
|
|
405
|
+
let heroMarker
|
|
406
|
+
let heroActive = false
|
|
407
|
+
let disposed = false
|
|
408
|
+
let documentVisible = document.visibilityState !== 'hidden'
|
|
409
|
+
let canvasVisible = true
|
|
410
|
+
let stateReported = false
|
|
411
|
+
let particleState = {
|
|
412
|
+
phase: missingEmblems.length || !emblemSequence.length ? 'error' : 'preparing',
|
|
413
|
+
requestedPattern: 'orthogonal',
|
|
414
|
+
appliedPattern: null,
|
|
415
|
+
error: missingEmblems.length || !emblemSequence.length ? '徽记资源异常' : null,
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const motionReduced = () => preferences.motion === 'reduced' || Boolean(mediaQuery?.matches)
|
|
419
|
+
const phoneStatic = () => window.innerWidth <= PHONE_BREAKPOINT
|
|
420
|
+
const effectiveHeroDensity = density => {
|
|
421
|
+
const requested = DENSITY_ORDER.indexOf(density)
|
|
422
|
+
const minimum = DENSITY_ORDER.indexOf(HERO_MINIMUM_DENSITY)
|
|
423
|
+
return requested < minimum ? HERO_MINIMUM_DENSITY : density
|
|
424
|
+
}
|
|
425
|
+
const activeDensity = () => heroActive
|
|
426
|
+
? effectiveHeroDensity(preferences.heroParticleDensity)
|
|
427
|
+
: preferences.conversationParticleDensity
|
|
428
|
+
const densityProfile = density => PARTICLE_DENSITY_PROFILES[density] ?? PARTICLE_DENSITY_PROFILES.standard
|
|
429
|
+
const particleRadius = density => densityProfile(density ?? activeDensity()).radius
|
|
430
|
+
const canRenderFrame = () => documentVisible && canvasVisible && !hydrationPaused && !resizing
|
|
431
|
+
const layoutWidth = () => width
|
|
432
|
+
const layoutHeight = () => height
|
|
433
|
+
const layoutShiftX = () => 0
|
|
434
|
+
|
|
435
|
+
function reportState(next) {
|
|
436
|
+
const state = { ...particleState, ...next }
|
|
437
|
+
if (missingEmblems.length || !emblemSequence.length) {
|
|
438
|
+
state.phase = 'error'
|
|
439
|
+
state.error = '徽记资源异常'
|
|
440
|
+
}
|
|
441
|
+
const changed = !stateReported || Object.keys(state).some(key => state[key] !== particleState[key])
|
|
442
|
+
particleState = state
|
|
443
|
+
stateReported = true
|
|
444
|
+
if (changed) onStateChange({ ...particleState })
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function sequenceIndexForSegment(segmentValue) {
|
|
448
|
+
const length = Math.max(1, emblemSequence.length)
|
|
449
|
+
return ((segmentValue % length) + length) % length
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function emblemForSegment(segmentValue) {
|
|
453
|
+
return emblemSequence[sequenceIndexForSegment(segmentValue)] ?? emblemSequence[0]
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function targetsForIndex(index, density = 'standard') {
|
|
457
|
+
const key = `${index}:${density}`
|
|
458
|
+
const cached = targetCache.get(key)
|
|
459
|
+
if (cached) return cached
|
|
460
|
+
const record = maskRecords[index]
|
|
461
|
+
const targets = record ? createOrthogonalTargets(record.mask, density) : []
|
|
462
|
+
if (!targets.length) return []
|
|
463
|
+
targetCache.set(key, targets)
|
|
464
|
+
return targets
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function targetsForSegment(segmentValue, density = preferences.conversationParticleDensity) {
|
|
468
|
+
return targetsForIndex(sequenceIndexForSegment(segmentValue), density)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function heroEmblem() {
|
|
472
|
+
return emblemSequence[rhodesIndex] ?? emblemSequence[0]
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function targetsForHero(density = preferences.heroParticleDensity) {
|
|
476
|
+
const effectiveDensity = effectiveHeroDensity(density)
|
|
477
|
+
const key = `hero:${effectiveDensity}`
|
|
478
|
+
const cached = targetCache.get(key)
|
|
479
|
+
if (cached) return cached
|
|
480
|
+
const targets = heroMaskRecord ? createOrthogonalTargets(heroMaskRecord.mask, effectiveDensity) : []
|
|
481
|
+
if (targets.length) targetCache.set(key, targets)
|
|
482
|
+
return targets
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function populationSource(density = activeDensity()) {
|
|
486
|
+
return heroActive ? targetsForHero(density) : targetsForSegment(segment, density)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function targetParticleCount(density = activeDensity()) {
|
|
490
|
+
return populationSource(density).length
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function heroLogoSizeFor(fieldWidth, fieldHeight) {
|
|
494
|
+
const viewportWidth = Number(window.innerWidth) || fieldWidth
|
|
495
|
+
const viewportHeight = Number(window.innerHeight) || fieldHeight
|
|
496
|
+
const phoneSize = Math.min(viewportWidth * 0.58, 240)
|
|
497
|
+
const tabletSize = Math.min(fieldWidth * 0.4, 420)
|
|
498
|
+
const desktopSize = clamp(fieldWidth * 0.42, 320, 580)
|
|
499
|
+
const phoneToTablet = clamp((viewportWidth - 560) / 160, 0, 1)
|
|
500
|
+
const tabletToDesktop = clamp((viewportWidth - 960) / 128, 0, 1)
|
|
501
|
+
let size = interpolate(phoneSize, tabletSize, phoneToTablet)
|
|
502
|
+
size = interpolate(size, desktopSize, tabletToDesktop)
|
|
503
|
+
size = Math.min(size, viewportHeight * 0.42)
|
|
504
|
+
return Math.max(1, Math.min(size, fieldWidth * 0.9))
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function heroLogoSize() {
|
|
508
|
+
return heroLogoSizeFor(layoutWidth(), layoutHeight())
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function heroCenterFor(fieldWidth, fieldHeight, shiftX = 0) {
|
|
512
|
+
const size = heroLogoSizeFor(fieldWidth, fieldHeight)
|
|
513
|
+
const operationRect = operation?.getBoundingClientRect?.() ?? { top: 0 }
|
|
514
|
+
const scrollerRect = scroller?.getBoundingClientRect?.() ?? { top: operationRect.top }
|
|
515
|
+
const composer = scroller?.querySelector?.('[data-composer-seat]')
|
|
516
|
+
const composerRect = composer?.getBoundingClientRect?.()
|
|
517
|
+
let paddingTop = 0
|
|
518
|
+
let reserved = size + 44
|
|
519
|
+
try {
|
|
520
|
+
paddingTop = Number.parseFloat(window.getComputedStyle(scroller).paddingTop) || 0
|
|
521
|
+
if (!/jsdom/i.test(window.navigator?.userAgent || '')) {
|
|
522
|
+
const pseudo = window.getComputedStyle(scroller, '::before')
|
|
523
|
+
reserved = Number.parseFloat(pseudo?.marginTop) || reserved
|
|
524
|
+
}
|
|
525
|
+
} catch {}
|
|
526
|
+
const minY = Math.max(12, scrollerRect.top - operationRect.top + 12)
|
|
527
|
+
const recordTop = scrollerRect.top - operationRect.top + paddingTop + reserved
|
|
528
|
+
const composerLimit = composerRect ? composerRect.top - operationRect.top - 40 : fieldHeight - 40
|
|
529
|
+
const maxCenter = Math.max(minY + size / 2, composerLimit - size / 2)
|
|
530
|
+
return {
|
|
531
|
+
x: fieldWidth * 0.5 + shiftX,
|
|
532
|
+
y: clamp(recordTop - 22 - size / 2, minY + size / 2, maxCenter),
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function heroCenter() {
|
|
537
|
+
return heroCenterFor(layoutWidth(), layoutHeight(), layoutShiftX())
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function currentLayout() {
|
|
541
|
+
if (transition?.fromHero) {
|
|
542
|
+
const progress = easeInOut(transition.progress)
|
|
543
|
+
const to = centerFromAnchor(anchorFor(transition.toSide, 0))
|
|
544
|
+
return {
|
|
545
|
+
center: {
|
|
546
|
+
x: transition.fromCenter.x + (to.x - transition.fromCenter.x) * progress,
|
|
547
|
+
y: transition.fromCenter.y + (to.y - transition.fromCenter.y) * progress,
|
|
548
|
+
},
|
|
549
|
+
size: transition.fromSize + (logoSize() - transition.fromSize) * progress,
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (heroActive) return { center: heroCenter(), size: heroLogoSize() }
|
|
553
|
+
return { center: centerFromAnchor(inspectAnchor()), size: logoSize() }
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
function scrollProgress() {
|
|
558
|
+
if (!scroller) return 0
|
|
559
|
+
const viewport = Math.max(1, Number(scroller.clientHeight) || height)
|
|
560
|
+
const raw = (Number(scroller.scrollTop) || 0) / viewport
|
|
561
|
+
return raw - Math.floor(raw)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function anchorFor(sideValue, verticalProgress) {
|
|
565
|
+
if (phoneStatic()) return { horizontalProgress: 0.5, verticalProgress: 0.52 }
|
|
566
|
+
return {
|
|
567
|
+
horizontalProgress: sideValue < 0 ? 0.28 : 0.72,
|
|
568
|
+
verticalProgress: clamp(verticalProgress, 0, 1),
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function inspectAnchor() {
|
|
573
|
+
if (heroActive) {
|
|
574
|
+
const center = heroCenter()
|
|
575
|
+
return { horizontalProgress: 0.5, verticalProgress: clamp(center.y / Math.max(1, layoutHeight()), 0, 1) }
|
|
576
|
+
}
|
|
577
|
+
if (transition?.fromHero) {
|
|
578
|
+
const progress = easeInOut(transition.progress)
|
|
579
|
+
const to = centerFromAnchor(anchorFor(transition.toSide, 0))
|
|
580
|
+
return {
|
|
581
|
+
horizontalProgress: (transition.fromCenter.x + (to.x - transition.fromCenter.x) * progress) / Math.max(1, layoutWidth()),
|
|
582
|
+
verticalProgress: (transition.fromCenter.y + (to.y - transition.fromCenter.y) * progress) / Math.max(1, layoutHeight()),
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (!transition || motionReduced() || phoneStatic()) {
|
|
586
|
+
return anchorFor(side, motionReduced() ? 0 : scrollProgress())
|
|
587
|
+
}
|
|
588
|
+
const down = transition.direction > 0
|
|
589
|
+
const from = anchorFor(transition.fromSide, down ? 1 : 0)
|
|
590
|
+
const to = anchorFor(transition.toSide, down ? 0 : 1)
|
|
591
|
+
const progress = easeInOut(transition.progress)
|
|
592
|
+
return {
|
|
593
|
+
horizontalProgress: from.horizontalProgress + (to.horizontalProgress - from.horizontalProgress) * progress,
|
|
594
|
+
verticalProgress: from.verticalProgress + (to.verticalProgress - from.verticalProgress) * progress,
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function centerFromAnchorFor(anchor, fieldWidth, fieldHeight, shiftX = 0) {
|
|
599
|
+
if (phoneStatic()) return { x: fieldWidth * 0.5 + shiftX, y: fieldHeight * 0.52 }
|
|
600
|
+
return {
|
|
601
|
+
x: fieldWidth * anchor.horizontalProgress + shiftX,
|
|
602
|
+
y: fieldHeight * (0.54 - anchor.verticalProgress * 0.34),
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function centerFromAnchor(anchor) {
|
|
607
|
+
return centerFromAnchorFor(anchor, layoutWidth(), layoutHeight(), layoutShiftX())
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function logoSizeFor(fieldWidth) {
|
|
611
|
+
if (phoneStatic()) return Math.min(fieldWidth * 0.72, 250)
|
|
612
|
+
if (fieldWidth <= TABLET_BREAKPOINT) return Math.min(fieldWidth * 0.31, 230)
|
|
613
|
+
return clamp(fieldWidth * 0.23, 230, 320)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function logoSize() {
|
|
617
|
+
return logoSizeFor(layoutWidth())
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function createParticle(target, center, size, random, density = activeDensity()) {
|
|
621
|
+
return {
|
|
622
|
+
target,
|
|
623
|
+
kind: target.kind || 'fill',
|
|
624
|
+
fromKind: target.kind || 'fill',
|
|
625
|
+
toKind: target.kind || 'fill',
|
|
626
|
+
fromTarget: target,
|
|
627
|
+
toTarget: target,
|
|
628
|
+
x: center.x + target.x * size,
|
|
629
|
+
y: center.y + target.y * size,
|
|
630
|
+
vx: 0,
|
|
631
|
+
vy: 0,
|
|
632
|
+
size: particleRadius(density),
|
|
633
|
+
scatterX: (random() - 0.5) * 2,
|
|
634
|
+
scatterY: (random() - 0.5) * 2,
|
|
635
|
+
gatherX: (random() - 0.5) * 2,
|
|
636
|
+
gatherY: (random() - 0.5) * 2,
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function cancelTargetPrewarm() {
|
|
640
|
+
if (targetPrewarmHandle !== undefined) window.cancelIdleCallback?.(targetPrewarmHandle)
|
|
641
|
+
targetPrewarmHandle = undefined
|
|
642
|
+
targetPrewarmQueue = []
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function scheduleTargetPrewarm() {
|
|
646
|
+
cancelTargetPrewarm()
|
|
647
|
+
if (typeof window.requestIdleCallback !== 'function' || !particles.length) return
|
|
648
|
+
targetPrewarmQueue = emblemSequence.map((_, index) => index)
|
|
649
|
+
.filter(index => index !== sequenceIndexForSegment(segment))
|
|
650
|
+
const warmNext = () => {
|
|
651
|
+
targetPrewarmHandle = undefined
|
|
652
|
+
if (!canvas?.isConnected || disposed) {
|
|
653
|
+
targetPrewarmQueue = []
|
|
654
|
+
return
|
|
655
|
+
}
|
|
656
|
+
const index = targetPrewarmQueue.shift()
|
|
657
|
+
if (index !== undefined) targetsForIndex(index, preferences.conversationParticleDensity)
|
|
658
|
+
if (targetPrewarmQueue.length) {
|
|
659
|
+
targetPrewarmHandle = window.requestIdleCallback(warmNext, { timeout: 800 })
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (targetPrewarmQueue.length) {
|
|
663
|
+
targetPrewarmHandle = window.requestIdleCallback(warmNext, { timeout: 800 })
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function buildParticles() {
|
|
668
|
+
const selected = populationSource()
|
|
669
|
+
if (!selected.length) {
|
|
670
|
+
reportState({ phase: 'error', error: '徽记资源异常' })
|
|
671
|
+
return false
|
|
672
|
+
}
|
|
673
|
+
const random = seededRandom(0x50525453)
|
|
674
|
+
const { center, size } = currentLayout()
|
|
675
|
+
particles = selected.map(target => createParticle(target, center, size, random))
|
|
676
|
+
reportState({ phase: 'applied', appliedPattern: 'orthogonal', error: null })
|
|
677
|
+
scheduleTargetPrewarm()
|
|
678
|
+
return true
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function reconcileParticlePopulation(nextCount) {
|
|
682
|
+
const count = Math.max(1, Math.round(nextCount))
|
|
683
|
+
if (count === particles.length) return
|
|
684
|
+
if (count < particles.length) {
|
|
685
|
+
particles.length = count
|
|
686
|
+
return
|
|
687
|
+
}
|
|
688
|
+
const availableTargets = populationSource()
|
|
689
|
+
const currentTargets = availableTargets.slice(0, count)
|
|
690
|
+
const transitionTargets = transition
|
|
691
|
+
? targetsForSegment(transition.targetSegment).slice(0, count)
|
|
692
|
+
: currentTargets
|
|
693
|
+
const nextTargets = transitionTargets.length === count ? transitionTargets : currentTargets
|
|
694
|
+
const random = seededRandom(0x50525453 + particles.length)
|
|
695
|
+
const { center, size } = currentLayout()
|
|
696
|
+
for (let index = particles.length; index < count; index += 1) {
|
|
697
|
+
const particle = createParticle(currentTargets[index], center, size, random)
|
|
698
|
+
particle.fromTarget = currentTargets[index]
|
|
699
|
+
particle.fromKind = currentTargets[index]?.kind || 'fill'
|
|
700
|
+
particle.toKind = nextTargets[index]?.kind || 'fill'
|
|
701
|
+
particle.toTarget = nextTargets[index]
|
|
702
|
+
particles.push(particle)
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function completeDensityReassembly() {
|
|
707
|
+
if (!densityTransition) return
|
|
708
|
+
const targetCount = densityTransition.to
|
|
709
|
+
particles.length = targetCount
|
|
710
|
+
particles.forEach(particle => {
|
|
711
|
+
particle.size = densityTransition.toRadius
|
|
712
|
+
particle.target = particle.densityToTarget
|
|
713
|
+
particle.kind = particle.densityToKind || particle.densityToTarget?.kind || 'fill'
|
|
714
|
+
particle.fromTarget = particle.target
|
|
715
|
+
particle.toTarget = particle.target
|
|
716
|
+
particle.fromKind = particle.kind
|
|
717
|
+
particle.toKind = particle.kind
|
|
718
|
+
delete particle.densityFromTarget
|
|
719
|
+
delete particle.densityToTarget
|
|
720
|
+
delete particle.densityFromKind
|
|
721
|
+
delete particle.densityToKind
|
|
722
|
+
delete particle.densityScatterX
|
|
723
|
+
delete particle.densityScatterY
|
|
724
|
+
delete particle.existedBeforeDensity
|
|
725
|
+
delete particle.existsAfterDensity
|
|
726
|
+
delete particle.densityDelay
|
|
727
|
+
})
|
|
728
|
+
densityTransition = undefined
|
|
729
|
+
densityStartedAt = undefined
|
|
730
|
+
reportState({ phase: 'applied', appliedPattern: 'orthogonal', error: null })
|
|
731
|
+
const queuedSegment = pendingSegment
|
|
732
|
+
pendingSegment = undefined
|
|
733
|
+
if (queuedSegment !== undefined && queuedSegment !== segment) {
|
|
734
|
+
startTransition(queuedSegment)
|
|
735
|
+
return
|
|
736
|
+
}
|
|
737
|
+
const queued = queuedDensityCount
|
|
738
|
+
const force = queuedTargetRefresh
|
|
739
|
+
queuedDensityCount = undefined
|
|
740
|
+
queuedTargetRefresh = false
|
|
741
|
+
if (queued !== undefined && (queued !== particles.length || force)) startDensityReassembly(queued, force)
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function startDensityReassembly(nextCount, force = false) {
|
|
745
|
+
const baseNextTargets = populationSource()
|
|
746
|
+
const targetCount = baseNextTargets.length
|
|
747
|
+
if (!targetCount) {
|
|
748
|
+
reportState({ phase: 'error', error: '徽记资源异常' })
|
|
749
|
+
return
|
|
750
|
+
}
|
|
751
|
+
if (!particles.length) {
|
|
752
|
+
buildParticles()
|
|
753
|
+
return
|
|
754
|
+
}
|
|
755
|
+
if (transition || densityTransition) {
|
|
756
|
+
queuedDensityCount = targetCount
|
|
757
|
+
queuedTargetRefresh ||= force
|
|
758
|
+
return
|
|
759
|
+
}
|
|
760
|
+
if (targetCount === particles.length && !force) return
|
|
761
|
+
|
|
762
|
+
const fromCount = particles.length
|
|
763
|
+
const workingCount = Math.max(fromCount, targetCount)
|
|
764
|
+
const random = seededRandom(stableHash((heroActive ? heroEmblem() : emblemForSegment(segment))?.key) ^ targetCount)
|
|
765
|
+
const { center, size } = currentLayout()
|
|
766
|
+
const nextTargets = baseNextTargets
|
|
767
|
+
for (let index = 0; index < workingCount; index += 1) {
|
|
768
|
+
let particle = particles[index]
|
|
769
|
+
const nextTarget = nextTargets[index]
|
|
770
|
+
if (!particle) {
|
|
771
|
+
particle = createParticle(nextTarget, center, size, random)
|
|
772
|
+
particles.push(particle)
|
|
773
|
+
}
|
|
774
|
+
particle.densityFromTarget = particle.target
|
|
775
|
+
particle.densityToTarget = nextTarget ?? particle.target
|
|
776
|
+
particle.densityFromKind = particle.kind || particle.target?.kind || 'fill'
|
|
777
|
+
particle.densityToKind = nextTarget?.kind || particle.densityFromKind
|
|
778
|
+
particle.densityScatterX = (random() - 0.5) * 2
|
|
779
|
+
particle.densityScatterY = (random() - 0.5) * 2
|
|
780
|
+
particle.existedBeforeDensity = index < fromCount
|
|
781
|
+
particle.existsAfterDensity = index < targetCount
|
|
782
|
+
particle.densityDelay = 0
|
|
783
|
+
}
|
|
784
|
+
densityTransition = {
|
|
785
|
+
from: fromCount,
|
|
786
|
+
to: targetCount,
|
|
787
|
+
duration: motionReduced() || phoneStatic() ? DENSITY_CROSSFADE_MS : DENSITY_REASSEMBLY_MS,
|
|
788
|
+
mode: motionReduced() || phoneStatic() ? 'crossfade' : 'reassemble',
|
|
789
|
+
layout: false,
|
|
790
|
+
fromRadius: particles[0]?.size ?? particleRadius(),
|
|
791
|
+
toRadius: particleRadius(),
|
|
792
|
+
progress: 0,
|
|
793
|
+
}
|
|
794
|
+
densityStartedAt = undefined
|
|
795
|
+
pointerActive = false
|
|
796
|
+
reportState({ phase: 'reassembling', requestedPattern: 'orthogonal', error: null })
|
|
797
|
+
scheduleFrame()
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function scheduleDensityReassembly(nextCount, force = false) {
|
|
801
|
+
if (densityDebounce !== undefined) window.clearTimeout?.(densityDebounce)
|
|
802
|
+
reportState({ phase: 'preparing', requestedPattern: 'orthogonal', error: null })
|
|
803
|
+
densityDebounce = window.setTimeout?.(() => {
|
|
804
|
+
densityDebounce = undefined
|
|
805
|
+
if (transition || densityTransition) {
|
|
806
|
+
queuedDensityCount = nextCount
|
|
807
|
+
queuedTargetRefresh ||= force
|
|
808
|
+
} else startDensityReassembly(nextCount, force)
|
|
809
|
+
}, DENSITY_DEBOUNCE_MS)
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function advanceDensity(timestamp) {
|
|
813
|
+
if (!densityTransition) return
|
|
814
|
+
densityStartedAt ??= timestamp
|
|
815
|
+
densityTransition.progress = clamp((timestamp - densityStartedAt) / densityTransition.duration, 0, 1)
|
|
816
|
+
if (densityTransition.progress >= 1) completeDensityReassembly()
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function densityAlpha(particle) {
|
|
820
|
+
if (!densityTransition) return 1
|
|
821
|
+
const progress = densityTransition.progress
|
|
822
|
+
if (densityTransition.mode === 'crossfade') {
|
|
823
|
+
let alpha
|
|
824
|
+
if (!particle.existedBeforeDensity) alpha = progress < 0.5 ? 0 : easeOut((progress - 0.5) * 2)
|
|
825
|
+
else if (!particle.existsAfterDensity) alpha = 1 - easeOut(Math.min(1, progress * 2))
|
|
826
|
+
else alpha = progress < 0.5 ? 1 - easeOut(progress * 2) : easeOut((progress - 0.5) * 2)
|
|
827
|
+
return alpha
|
|
828
|
+
}
|
|
829
|
+
if (!particle.existedBeforeDensity) return progress < 0.28 ? 0 : easeOut((progress - 0.28) / 0.72)
|
|
830
|
+
if (!particle.existsAfterDensity) return 1 - easeOut(progress / 0.46)
|
|
831
|
+
return progress < 0.28
|
|
832
|
+
? 1 - easeOut(progress / 0.28) * 0.7
|
|
833
|
+
: 0.3 + easeOut((progress - 0.28) / 0.72) * 0.7
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function prepareTransitionTargets(targetSegment) {
|
|
837
|
+
const targetDensity = preferences.conversationParticleDensity
|
|
838
|
+
const selected = targetsForSegment(targetSegment, targetDensity)
|
|
839
|
+
if (!selected.length) return { fromCount: particles.length, toCount: 0 }
|
|
840
|
+
const fromCount = particles.length
|
|
841
|
+
const toCount = selected.length
|
|
842
|
+
const workingCount = Math.max(fromCount, toCount)
|
|
843
|
+
const random = seededRandom(stableHash('transition:' + targetSegment + ':' + activeDensity()))
|
|
844
|
+
const { center, size } = currentLayout()
|
|
845
|
+
for (let index = 0; index < workingCount; index += 1) {
|
|
846
|
+
let particle = particles[index]
|
|
847
|
+
const nextTarget = selected[index] ?? selected[index % toCount]
|
|
848
|
+
if (!particle) {
|
|
849
|
+
const source = particles[index % Math.max(1, fromCount)]?.target ?? nextTarget
|
|
850
|
+
particle = createParticle(source, center, size, random, preferences.conversationParticleDensity)
|
|
851
|
+
particles.push(particle)
|
|
852
|
+
}
|
|
853
|
+
particle.fromTarget = particle.target
|
|
854
|
+
particle.fromKind = particle.kind || particle.target?.kind || 'fill'
|
|
855
|
+
particle.toKind = nextTarget?.kind || 'fill'
|
|
856
|
+
particle.toTarget = nextTarget
|
|
857
|
+
particle.existedBeforeTransition = index < fromCount
|
|
858
|
+
particle.existsAfterTransition = index < toCount
|
|
859
|
+
}
|
|
860
|
+
return {
|
|
861
|
+
fromCount,
|
|
862
|
+
toCount,
|
|
863
|
+
fromRadius: particles[0]?.size ?? particleRadius(),
|
|
864
|
+
toRadius: particleRadius(targetDensity),
|
|
865
|
+
targetDensity,
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function commitTransitionTargets(targetSegment, snap = false, density = preferences.conversationParticleDensity) {
|
|
870
|
+
const selected = targetsForSegment(targetSegment, density)
|
|
871
|
+
if (!selected.length) return
|
|
872
|
+
segment = targetSegment
|
|
873
|
+
const { center, size } = currentLayout()
|
|
874
|
+
const random = seededRandom(stableHash('commit:' + targetSegment + ':' + activeDensity()))
|
|
875
|
+
if (particles.length > selected.length) particles.length = selected.length
|
|
876
|
+
while (particles.length < selected.length) {
|
|
877
|
+
particles.push(createParticle(selected[particles.length], center, size, random, density))
|
|
878
|
+
}
|
|
879
|
+
particles.forEach((particle, index) => {
|
|
880
|
+
const target = selected[index]
|
|
881
|
+
particle.size = particleRadius(density)
|
|
882
|
+
particle.target = target
|
|
883
|
+
particle.kind = target.kind || 'fill'
|
|
884
|
+
particle.fromTarget = target
|
|
885
|
+
particle.fromKind = particle.kind
|
|
886
|
+
particle.toTarget = target
|
|
887
|
+
particle.toKind = particle.kind
|
|
888
|
+
delete particle.existedBeforeTransition
|
|
889
|
+
delete particle.existsAfterTransition
|
|
890
|
+
if (snap) {
|
|
891
|
+
particle.x = center.x + target.x * size
|
|
892
|
+
particle.y = center.y + target.y * size
|
|
893
|
+
particle.vx = 0
|
|
894
|
+
particle.vy = 0
|
|
895
|
+
}
|
|
896
|
+
})
|
|
897
|
+
scheduleTargetPrewarm()
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function clearDensityMetadata() {
|
|
901
|
+
particles.forEach(particle => {
|
|
902
|
+
delete particle.densityFromTarget
|
|
903
|
+
delete particle.densityToTarget
|
|
904
|
+
delete particle.densityFromKind
|
|
905
|
+
delete particle.densityToKind
|
|
906
|
+
delete particle.densityScatterX
|
|
907
|
+
delete particle.densityScatterY
|
|
908
|
+
delete particle.densityDelay
|
|
909
|
+
delete particle.existedBeforeDensity
|
|
910
|
+
delete particle.existsAfterDensity
|
|
911
|
+
})
|
|
912
|
+
densityTransition = undefined
|
|
913
|
+
densityStartedAt = undefined
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
function syncCanvasBackingStore() {
|
|
918
|
+
if (!canvas) return
|
|
919
|
+
const nextDpr = Math.min(Number(window.devicePixelRatio) || 1, 1.5)
|
|
920
|
+
const pixelWidth = Math.round(width * nextDpr)
|
|
921
|
+
const pixelHeight = Math.round(height * nextDpr)
|
|
922
|
+
const changed = canvas.width !== pixelWidth || canvas.height !== pixelHeight || dpr !== nextDpr
|
|
923
|
+
dpr = nextDpr
|
|
924
|
+
if (!changed) return
|
|
925
|
+
if (canvas.width !== pixelWidth) canvas.width = pixelWidth
|
|
926
|
+
if (canvas.height !== pixelHeight) canvas.height = pixelHeight
|
|
927
|
+
context?.setTransform(nextDpr, 0, 0, nextDpr, 0, 0)
|
|
928
|
+
backingStoreCommits += 1
|
|
929
|
+
return true
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function projectPointerFromClient() {
|
|
933
|
+
if (!pointerActive || !canvas) return
|
|
934
|
+
const rect = canvas.getBoundingClientRect?.() ?? {}
|
|
935
|
+
const rectWidth = Math.max(1, Number(rect.width) || width)
|
|
936
|
+
const rectHeight = Math.max(1, Number(rect.height) || height)
|
|
937
|
+
pointerX = (pointerClientX - (Number(rect.left) || 0)) * width / rectWidth
|
|
938
|
+
pointerY = (pointerClientY - (Number(rect.top) || 0)) * height / rectHeight
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function sizeFromRect(rect = {}) {
|
|
942
|
+
return {
|
|
943
|
+
width: Math.max(1, Number(rect.width) || Number(operation?.clientWidth) || Number(window.innerWidth) || 1),
|
|
944
|
+
height: Math.max(1, Number(rect.height) || Number(operation?.clientHeight) || Number(window.innerHeight) || 1),
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function readOperationSize() {
|
|
949
|
+
const rect = operation.getBoundingClientRect?.() ?? {}
|
|
950
|
+
return sizeFromRect(rect)
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function applyFieldSize(nextSize) {
|
|
954
|
+
const nextWidth = nextSize?.width ?? width
|
|
955
|
+
const nextHeight = nextSize?.height ?? height
|
|
956
|
+
const changed = nextWidth !== width || nextHeight !== height
|
|
957
|
+
width = nextWidth
|
|
958
|
+
height = nextHeight
|
|
959
|
+
projectPointerFromClient()
|
|
960
|
+
geometryDirty ||= changed
|
|
961
|
+
return changed
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function clearResizeTimers() {
|
|
965
|
+
if (resizeFrame !== undefined) {
|
|
966
|
+
window.cancelAnimationFrame?.(resizeFrame)
|
|
967
|
+
window.clearTimeout?.(resizeFrame)
|
|
968
|
+
}
|
|
969
|
+
for (const pendingFrame of [resizeCommitFrame, resizeFinishFrame]) {
|
|
970
|
+
if (pendingFrame === undefined) continue
|
|
971
|
+
window.cancelAnimationFrame?.(pendingFrame)
|
|
972
|
+
window.clearTimeout?.(pendingFrame)
|
|
973
|
+
}
|
|
974
|
+
if (resizeTimer !== undefined) window.clearTimeout?.(resizeTimer)
|
|
975
|
+
if (resizeRestoreTimer !== undefined) window.clearTimeout?.(resizeRestoreTimer)
|
|
976
|
+
resizeFrame = undefined
|
|
977
|
+
resizeCommitFrame = undefined
|
|
978
|
+
resizeFinishFrame = undefined
|
|
979
|
+
resizeTimer = undefined
|
|
980
|
+
resizeRestoreTimer = undefined
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function clearResizeState() {
|
|
984
|
+
clearResizeTimers()
|
|
985
|
+
pendingResizeSize = undefined
|
|
986
|
+
resizing = false
|
|
987
|
+
setParticleResizeState('data-prts-resizing', false)
|
|
988
|
+
setParticleResizeState('data-prts-resize-restoring', false)
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function setParticleResizeState(attribute, active) {
|
|
992
|
+
for (const node of [canvas, ambient]) {
|
|
993
|
+
if (!node || node.hasAttribute(attribute) === active) continue
|
|
994
|
+
node.toggleAttribute(attribute, active)
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function resumePausedTimelines(timestamp) {
|
|
999
|
+
if (pauseStartedAt === undefined) return
|
|
1000
|
+
const pausedFor = Math.max(0, timestamp - pauseStartedAt)
|
|
1001
|
+
if (transitionStartedAt !== undefined) transitionStartedAt += pausedFor
|
|
1002
|
+
if (densityStartedAt !== undefined) densityStartedAt += pausedFor
|
|
1003
|
+
pauseStartedAt = undefined
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function requestResizeFrame(callback) {
|
|
1007
|
+
return window.requestAnimationFrame?.(callback) ?? window.setTimeout?.(() => callback(Number(window.performance?.now?.()) || 0), 16)
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function restoreResizeTransaction() {
|
|
1011
|
+
resizeFinishFrame = undefined
|
|
1012
|
+
resizing = false
|
|
1013
|
+
setParticleResizeState('data-prts-resizing', false)
|
|
1014
|
+
setParticleResizeState('data-prts-resize-restoring', true)
|
|
1015
|
+
resizeRestoreTimer = window.setTimeout?.(() => {
|
|
1016
|
+
resizeRestoreTimer = undefined
|
|
1017
|
+
setParticleResizeState('data-prts-resize-restoring', false)
|
|
1018
|
+
}, RESIZE_RESTORE_MS)
|
|
1019
|
+
updateFrameGate()
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function paintResizeCommit(timestamp) {
|
|
1023
|
+
resizeCommitFrame = undefined
|
|
1024
|
+
if (!operation || !canvas) {
|
|
1025
|
+
clearResizeState()
|
|
1026
|
+
return
|
|
1027
|
+
}
|
|
1028
|
+
resumePausedTimelines(Number(timestamp) || Number(window.performance?.now?.()) || 0)
|
|
1029
|
+
syncCanvasBackingStore()
|
|
1030
|
+
geometryDirty = false
|
|
1031
|
+
resizing = false
|
|
1032
|
+
cancelFrame()
|
|
1033
|
+
draw(Number(timestamp) || Number(window.performance?.now?.()) || 0)
|
|
1034
|
+
cancelFrame()
|
|
1035
|
+
resizing = true
|
|
1036
|
+
resizeFinishFrame = requestResizeFrame(restoreResizeTransaction)
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function finishResizeTransaction() {
|
|
1040
|
+
resizeTimer = undefined
|
|
1041
|
+
if (!operation || !canvas) {
|
|
1042
|
+
clearResizeState()
|
|
1043
|
+
return
|
|
1044
|
+
}
|
|
1045
|
+
const finalSize = pendingResizeSize ?? readOperationSize()
|
|
1046
|
+
pendingResizeSize = undefined
|
|
1047
|
+
const changed = applyFieldSize(finalSize)
|
|
1048
|
+
if (changed && particles.length) snapParticlesToCurrentLayout()
|
|
1049
|
+
resizeCommits += changed ? 1 : 0
|
|
1050
|
+
const { center } = currentLayout()
|
|
1051
|
+
if (!glowX && !glowY) {
|
|
1052
|
+
glowX = center.x
|
|
1053
|
+
glowY = center.y
|
|
1054
|
+
}
|
|
1055
|
+
resizeCommitFrame = requestResizeFrame(paintResizeCommit)
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function enterResizeFreeze() {
|
|
1059
|
+
if (resizing) return
|
|
1060
|
+
resizing = true
|
|
1061
|
+
pointerActive = false
|
|
1062
|
+
setParticleResizeState('data-prts-resize-restoring', false)
|
|
1063
|
+
setParticleResizeState('data-prts-resizing', true)
|
|
1064
|
+
updateFrameGate()
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function beginResizeTransaction(nextSize) {
|
|
1068
|
+
if (!operation || !canvas) return
|
|
1069
|
+
pendingResizeSize = nextSize ?? pendingResizeSize ?? readOperationSize()
|
|
1070
|
+
if (!resizing && pendingResizeSize.width === width && pendingResizeSize.height === height) {
|
|
1071
|
+
pendingResizeSize = undefined
|
|
1072
|
+
return
|
|
1073
|
+
}
|
|
1074
|
+
enterResizeFreeze()
|
|
1075
|
+
if (resizeTimer !== undefined) window.clearTimeout?.(resizeTimer)
|
|
1076
|
+
resizeTimer = window.setTimeout?.(finishResizeTransaction, RESIZE_SETTLE_MS)
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function onOperationResize(entries = []) {
|
|
1080
|
+
if (!operation || !canvas) return
|
|
1081
|
+
resizeObservations += 1
|
|
1082
|
+
const entry = entries.find(candidate => candidate.target === operation) ?? entries[0]
|
|
1083
|
+
const contentRect = entry?.contentRect
|
|
1084
|
+
pendingResizeSize = contentRect ? sizeFromRect(contentRect) : readOperationSize()
|
|
1085
|
+
if (resizeFrame !== undefined) return
|
|
1086
|
+
resizeFrame = window.requestAnimationFrame?.(() => {
|
|
1087
|
+
resizeFrame = undefined
|
|
1088
|
+
beginResizeTransaction(pendingResizeSize)
|
|
1089
|
+
}) ?? window.setTimeout?.(() => {
|
|
1090
|
+
resizeFrame = undefined
|
|
1091
|
+
beginResizeTransaction(pendingResizeSize)
|
|
1092
|
+
}, 0)
|
|
1093
|
+
if (!resizing && (pendingResizeSize.width !== width || pendingResizeSize.height !== height)) {
|
|
1094
|
+
enterResizeFreeze()
|
|
1095
|
+
}
|
|
1096
|
+
if (!resizing) return
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function initializeFieldSize() {
|
|
1100
|
+
const changed = applyFieldSize(readOperationSize())
|
|
1101
|
+
const { center } = currentLayout()
|
|
1102
|
+
if (!glowX && !glowY) {
|
|
1103
|
+
glowX = center.x
|
|
1104
|
+
glowY = center.y
|
|
1105
|
+
}
|
|
1106
|
+
return changed
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function targetPosition(particle, frameGeometry) {
|
|
1110
|
+
if (densityTransition) {
|
|
1111
|
+
const { center, size } = frameGeometry.activeLayout
|
|
1112
|
+
const fromTarget = particle.densityFromTarget ?? particle.target
|
|
1113
|
+
const toTarget = particle.densityToTarget ?? particle.target
|
|
1114
|
+
const fromX = center.x + fromTarget.x * size
|
|
1115
|
+
const fromY = center.y + fromTarget.y * size
|
|
1116
|
+
const toX = center.x + toTarget.x * size
|
|
1117
|
+
const toY = center.y + toTarget.y * size
|
|
1118
|
+
const progress = densityTransition.progress
|
|
1119
|
+
if (densityTransition.mode === 'crossfade') {
|
|
1120
|
+
return progress < 0.5
|
|
1121
|
+
? { x: fromX, y: fromY, accent: 0 }
|
|
1122
|
+
: { x: toX, y: toY, accent: 0 }
|
|
1123
|
+
}
|
|
1124
|
+
const breakPoint = 0.28
|
|
1125
|
+
const scatterX = fromX + particle.densityScatterX * size * 0.34
|
|
1126
|
+
const scatterY = fromY + particle.densityScatterY * size * 0.28
|
|
1127
|
+
if (progress < breakPoint) {
|
|
1128
|
+
const local = easeOut(progress / breakPoint)
|
|
1129
|
+
return {
|
|
1130
|
+
x: fromX + (scatterX - fromX) * local,
|
|
1131
|
+
y: fromY + (scatterY - fromY) * local,
|
|
1132
|
+
accent: local * 0.42,
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
const local = easeInOut((progress - breakPoint) / (1 - breakPoint))
|
|
1136
|
+
return {
|
|
1137
|
+
x: scatterX + (toX - scatterX) * local,
|
|
1138
|
+
y: scatterY + (toY - scatterY) * local,
|
|
1139
|
+
accent: (1 - local) * 0.42,
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
if (!transition || frameGeometry.staticField) {
|
|
1143
|
+
const { center, size } = frameGeometry.activeLayout
|
|
1144
|
+
return { x: center.x + particle.target.x * size, y: center.y + particle.target.y * size, accent: 0 }
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
const { from, to, fromSize, toSize } = frameGeometry.transitionLayout
|
|
1148
|
+
const fromX = from.x + particle.fromTarget.x * fromSize
|
|
1149
|
+
const fromY = from.y + particle.fromTarget.y * fromSize
|
|
1150
|
+
const toX = to.x + particle.toTarget.x * toSize
|
|
1151
|
+
const toY = to.y + particle.toTarget.y * toSize
|
|
1152
|
+
const progress = transition.progress
|
|
1153
|
+
|
|
1154
|
+
if (progress < 0.36) {
|
|
1155
|
+
const local = easeOut(progress / 0.36)
|
|
1156
|
+
return {
|
|
1157
|
+
x: fromX + particle.scatterX * width * 0.46 * local,
|
|
1158
|
+
y: fromY + particle.scatterY * height * 0.38 * local,
|
|
1159
|
+
accent: local * 0.45,
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
if (progress < 0.7) {
|
|
1163
|
+
const local = easeInOut((progress - 0.36) / 0.34)
|
|
1164
|
+
const startX = fromX + particle.scatterX * width * 0.46
|
|
1165
|
+
const startY = fromY + particle.scatterY * height * 0.38
|
|
1166
|
+
const endX = toX + particle.gatherX * width * 0.32
|
|
1167
|
+
const endY = toY + particle.gatherY * height * 0.28
|
|
1168
|
+
return {
|
|
1169
|
+
x: startX + (endX - startX) * local,
|
|
1170
|
+
y: startY + (endY - startY) * local + Math.sin(local * Math.PI) * particle.scatterY * 44,
|
|
1171
|
+
accent: 0.45 + Math.sin(local * Math.PI) * 0.55,
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
const local = easeInOut((progress - 0.7) / 0.3)
|
|
1175
|
+
return {
|
|
1176
|
+
x: toX + particle.gatherX * width * 0.32 * (1 - local),
|
|
1177
|
+
y: toY + particle.gatherY * height * 0.28 * (1 - local),
|
|
1178
|
+
accent: 1 - local,
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function transitionAlpha(particle) {
|
|
1183
|
+
if (!transition) return 1
|
|
1184
|
+
if (!particle.existedBeforeTransition) {
|
|
1185
|
+
return easeOut(clamp((transition.progress - 0.42) / 0.58, 0, 1))
|
|
1186
|
+
}
|
|
1187
|
+
if (!particle.existsAfterTransition) {
|
|
1188
|
+
return 1 - easeOut(clamp(transition.progress / 0.58, 0, 1))
|
|
1189
|
+
}
|
|
1190
|
+
return 1
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function particleColor(accent, lightScheme) {
|
|
1194
|
+
const base = lightScheme ? [41, 49, 46] : [238, 246, 242]
|
|
1195
|
+
const cyan = lightScheme ? [0, 142, 156] : [32, 213, 227]
|
|
1196
|
+
return base.map((value, index) => Math.round(value + (cyan[index] - value) * accent))
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function completeTransition() {
|
|
1200
|
+
const completed = transition
|
|
1201
|
+
side = completed.toSide
|
|
1202
|
+
transition = undefined
|
|
1203
|
+
transitionStartedAt = undefined
|
|
1204
|
+
commitTransitionTargets(completed.targetSegment, false, completed.targetDensity)
|
|
1205
|
+
const queued = pendingSegment
|
|
1206
|
+
pendingSegment = undefined
|
|
1207
|
+
if (queued !== undefined && queued !== segment) {
|
|
1208
|
+
if (sideForSegment(queued) !== side) {
|
|
1209
|
+
startTransition(queued)
|
|
1210
|
+
return
|
|
1211
|
+
}
|
|
1212
|
+
commitTransitionTargets(queued)
|
|
1213
|
+
}
|
|
1214
|
+
const queuedDensity = queuedDensityCount
|
|
1215
|
+
const force = queuedTargetRefresh
|
|
1216
|
+
queuedDensityCount = undefined
|
|
1217
|
+
queuedTargetRefresh = false
|
|
1218
|
+
if (queuedDensity !== undefined && (queuedDensity !== particles.length || force)) startDensityReassembly(queuedDensity, force)
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function draw(timestamp) {
|
|
1222
|
+
frame = undefined
|
|
1223
|
+
if (!ambient || !canvas) return
|
|
1224
|
+
if (!canRenderFrame()) return
|
|
1225
|
+
if (geometryDirty) {
|
|
1226
|
+
syncCanvasBackingStore()
|
|
1227
|
+
geometryDirty = false
|
|
1228
|
+
}
|
|
1229
|
+
if (transition) {
|
|
1230
|
+
transitionStartedAt ??= timestamp
|
|
1231
|
+
transition.progress = clamp((timestamp - transitionStartedAt) / TRANSITION_MS, 0, 1)
|
|
1232
|
+
if (transition.progress >= 1) completeTransition()
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
advanceDensity(timestamp)
|
|
1236
|
+
const reducedMotion = motionReduced()
|
|
1237
|
+
const staticPhone = phoneStatic()
|
|
1238
|
+
const staticField = reducedMotion || staticPhone
|
|
1239
|
+
const activeLayout = currentLayout()
|
|
1240
|
+
let transitionLayout
|
|
1241
|
+
if (transition && !staticField) {
|
|
1242
|
+
const down = transition.direction > 0
|
|
1243
|
+
transitionLayout = {
|
|
1244
|
+
from: transition.fromHero
|
|
1245
|
+
? transition.fromCenter
|
|
1246
|
+
: centerFromAnchor(anchorFor(transition.fromSide, down ? 1 : 0)),
|
|
1247
|
+
to: centerFromAnchor(anchorFor(transition.toSide, transition.fromHero ? 0 : (down ? 0 : 1))),
|
|
1248
|
+
fromSize: transition.fromHero ? transition.fromSize : logoSize(),
|
|
1249
|
+
toSize: logoSize(),
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
const frameGeometry = { activeLayout, staticField, transitionLayout }
|
|
1253
|
+
const { center, size: fieldSize } = activeLayout
|
|
1254
|
+
const follow = reducedMotion ? 1 : 0.035
|
|
1255
|
+
glowX += (center.x - glowX) * follow
|
|
1256
|
+
glowY += (center.y - glowY) * follow
|
|
1257
|
+
if (glow) glow.style.transform = 'translate3d(calc(' + glowX.toFixed(2) + 'px - 50%), calc(' + glowY.toFixed(2) + 'px - 50%), 0)'
|
|
1258
|
+
const glowDistance = Math.hypot(center.x - glowX, center.y - glowY)
|
|
1259
|
+
let maximumVelocity = 0
|
|
1260
|
+
|
|
1261
|
+
if (context) {
|
|
1262
|
+
context.clearRect(0, 0, width, height)
|
|
1263
|
+
const heroPhysics = heroActive
|
|
1264
|
+
const pointerRadius = pointerRadiusForFieldSize(fieldSize)
|
|
1265
|
+
const spring = heroPhysics ? 0.029 : 0.034
|
|
1266
|
+
const damping = heroPhysics ? 0.86 : 0.84
|
|
1267
|
+
const pointerForce = heroPhysics ? 2.2 : 2.7
|
|
1268
|
+
const lightScheme = document.documentElement.dataset.prtsScheme === 'light'
|
|
1269
|
+
const baseAlpha = lightScheme ? 0.66 : 0.72
|
|
1270
|
+
const radiusScale = lightScheme ? 1.12 : 1
|
|
1271
|
+
const particleBatches = typeof context.moveTo === 'function' ? new Map() : null
|
|
1272
|
+
|
|
1273
|
+
for (const particle of particles) {
|
|
1274
|
+
const target = targetPosition(particle, frameGeometry)
|
|
1275
|
+
if (staticField) {
|
|
1276
|
+
particle.x = target.x
|
|
1277
|
+
particle.y = target.y
|
|
1278
|
+
particle.vx = 0
|
|
1279
|
+
particle.vy = 0
|
|
1280
|
+
}
|
|
1281
|
+
if (!staticField) {
|
|
1282
|
+
particle.vx += (target.x - particle.x) * spring
|
|
1283
|
+
particle.vy += (target.y - particle.y) * spring
|
|
1284
|
+
|
|
1285
|
+
if (pointerActive && !transition) {
|
|
1286
|
+
const dx = particle.x - pointerX
|
|
1287
|
+
const dy = particle.y - pointerY
|
|
1288
|
+
const distanceSquared = dx * dx + dy * dy
|
|
1289
|
+
if (distanceSquared < pointerRadius * pointerRadius && distanceSquared > 1) {
|
|
1290
|
+
const distance = Math.sqrt(distanceSquared)
|
|
1291
|
+
const force = Math.pow(1 - distance / pointerRadius, 2) * pointerForce
|
|
1292
|
+
particle.vx += dx / distance * force
|
|
1293
|
+
particle.vy += dy / distance * force
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
particle.vx *= damping
|
|
1298
|
+
particle.vy *= damping
|
|
1299
|
+
particle.x += particle.vx
|
|
1300
|
+
particle.y += particle.vy
|
|
1301
|
+
}
|
|
1302
|
+
maximumVelocity = Math.max(maximumVelocity, Math.abs(particle.vx), Math.abs(particle.vy))
|
|
1303
|
+
const alpha = clamp(
|
|
1304
|
+
baseAlpha * densityAlpha(particle) * transitionAlpha(particle),
|
|
1305
|
+
0,
|
|
1306
|
+
1,
|
|
1307
|
+
)
|
|
1308
|
+
const color = particleColor(target.accent, lightScheme)
|
|
1309
|
+
const fillStyle = 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',' + alpha + ')'
|
|
1310
|
+
const densityRadius = densityTransition
|
|
1311
|
+
? interpolate(densityTransition.fromRadius, densityTransition.toRadius, easeInOut(densityTransition.progress))
|
|
1312
|
+
: particle.size
|
|
1313
|
+
const transitionRadius = transition
|
|
1314
|
+
? interpolate(transition.fromRadius ?? densityRadius, transition.toRadius ?? densityRadius, easeInOut(transition.progress))
|
|
1315
|
+
: densityRadius
|
|
1316
|
+
const radius = transitionRadius * radiusScale
|
|
1317
|
+
if (particleBatches) {
|
|
1318
|
+
const points = particleBatches.get(fillStyle) ?? []
|
|
1319
|
+
points.push(particle.x, particle.y, radius)
|
|
1320
|
+
particleBatches.set(fillStyle, points)
|
|
1321
|
+
} else {
|
|
1322
|
+
context.fillStyle = fillStyle
|
|
1323
|
+
context.beginPath()
|
|
1324
|
+
context.arc(particle.x, particle.y, radius, 0, Math.PI * 2)
|
|
1325
|
+
context.fill()
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
if (particleBatches) {
|
|
1329
|
+
for (const [fillStyle, points] of particleBatches) {
|
|
1330
|
+
context.fillStyle = fillStyle
|
|
1331
|
+
context.beginPath()
|
|
1332
|
+
for (let index = 0; index < points.length; index += 3) {
|
|
1333
|
+
context.moveTo(points[index] + points[index + 2], points[index + 1])
|
|
1334
|
+
context.arc(points[index], points[index + 1], points[index + 2], 0, Math.PI * 2)
|
|
1335
|
+
}
|
|
1336
|
+
context.fill()
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
if (!canvas.hasAttribute('data-prts-particle-ready')) {
|
|
1341
|
+
canvas.setAttribute('data-prts-particle-ready', '')
|
|
1342
|
+
canvas.style.visibility = ''
|
|
1343
|
+
}
|
|
1344
|
+
const explicitMotion = Boolean(densityTransition || transition)
|
|
1345
|
+
const animatedField = !staticField && context && particles.length
|
|
1346
|
+
if (explicitMotion) {
|
|
1347
|
+
idleStableFrames = 0
|
|
1348
|
+
} else if (animatedField) {
|
|
1349
|
+
if (maximumVelocity <= IDLE_SETTLE_SPEED && glowDistance <= IDLE_SETTLE_GLOW_DISTANCE) {
|
|
1350
|
+
idleStableFrames += 1
|
|
1351
|
+
} else {
|
|
1352
|
+
idleStableFrames = 0
|
|
1353
|
+
}
|
|
1354
|
+
} else {
|
|
1355
|
+
idleStableFrames = 0
|
|
1356
|
+
}
|
|
1357
|
+
if (!disposed && (explicitMotion || (animatedField && idleStableFrames < IDLE_SETTLE_FRAMES))) scheduleFrame()
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function scheduleFrame() {
|
|
1361
|
+
if (frame !== undefined || disposed || !canRenderFrame()) return
|
|
1362
|
+
frame = window.requestAnimationFrame?.(draw)
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function cancelFrame() {
|
|
1366
|
+
if (frame !== undefined) window.cancelAnimationFrame?.(frame)
|
|
1367
|
+
frame = undefined
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function startTransition(targetSegment) {
|
|
1371
|
+
if (densityTransition) {
|
|
1372
|
+
pendingSegment = targetSegment
|
|
1373
|
+
return
|
|
1374
|
+
}
|
|
1375
|
+
const targetSide = sideForSegment(targetSegment)
|
|
1376
|
+
if (targetSide === side || motionReduced() || phoneStatic()) {
|
|
1377
|
+
side = targetSide
|
|
1378
|
+
transition = undefined
|
|
1379
|
+
transitionStartedAt = undefined
|
|
1380
|
+
commitTransitionTargets(targetSegment, motionReduced() || phoneStatic())
|
|
1381
|
+
return
|
|
1382
|
+
}
|
|
1383
|
+
const population = prepareTransitionTargets(targetSegment)
|
|
1384
|
+
if (!population.toCount) {
|
|
1385
|
+
reportState({ phase: 'error', error: '徽记资源异常' })
|
|
1386
|
+
return
|
|
1387
|
+
}
|
|
1388
|
+
transition = {
|
|
1389
|
+
...population,
|
|
1390
|
+
fromSide: side,
|
|
1391
|
+
toSide: targetSide,
|
|
1392
|
+
direction: targetSegment > segment ? 1 : -1,
|
|
1393
|
+
targetSegment,
|
|
1394
|
+
progress: 0,
|
|
1395
|
+
}
|
|
1396
|
+
transitionStartedAt = undefined
|
|
1397
|
+
pointerActive = false
|
|
1398
|
+
scheduleFrame()
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
function resetTraversal() {
|
|
1402
|
+
side = -1
|
|
1403
|
+
segment = 0
|
|
1404
|
+
pendingSegment = undefined
|
|
1405
|
+
queuedDensityCount = undefined
|
|
1406
|
+
queuedTargetRefresh = false
|
|
1407
|
+
densityTransition = undefined
|
|
1408
|
+
densityStartedAt = undefined
|
|
1409
|
+
if (densityDebounce !== undefined) window.clearTimeout?.(densityDebounce)
|
|
1410
|
+
densityDebounce = undefined
|
|
1411
|
+
transition = undefined
|
|
1412
|
+
transitionStartedAt = undefined
|
|
1413
|
+
pointerActive = false
|
|
1414
|
+
glowX = 0
|
|
1415
|
+
glowY = 0
|
|
1416
|
+
if (maskRecords.length) buildParticles()
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function enterHero(marker) {
|
|
1420
|
+
heroMarker = marker
|
|
1421
|
+
heroActive = true
|
|
1422
|
+
scroller?.setAttribute('data-prts-hero-active', '')
|
|
1423
|
+
resetTraversal()
|
|
1424
|
+
scheduleFrame()
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
function leaveHero() {
|
|
1428
|
+
const fromCenter = heroCenter()
|
|
1429
|
+
const fromSize = heroLogoSize()
|
|
1430
|
+
heroMarker = undefined
|
|
1431
|
+
heroActive = false
|
|
1432
|
+
scroller?.removeAttribute('data-prts-hero-active')
|
|
1433
|
+
pendingSegment = undefined
|
|
1434
|
+
queuedDensityCount = undefined
|
|
1435
|
+
queuedTargetRefresh = false
|
|
1436
|
+
if (densityDebounce !== undefined) window.clearTimeout?.(densityDebounce)
|
|
1437
|
+
densityDebounce = undefined
|
|
1438
|
+
densityTransition = undefined
|
|
1439
|
+
densityStartedAt = undefined
|
|
1440
|
+
if (motionReduced() || phoneStatic() || !particles.length) {
|
|
1441
|
+
resetTraversal()
|
|
1442
|
+
commitTransitionTargets(0, true)
|
|
1443
|
+
return
|
|
1444
|
+
}
|
|
1445
|
+
const population = prepareTransitionTargets(0)
|
|
1446
|
+
transition = {
|
|
1447
|
+
...population,
|
|
1448
|
+
fromHero: true,
|
|
1449
|
+
fromCenter,
|
|
1450
|
+
fromSize,
|
|
1451
|
+
fromSide: -1,
|
|
1452
|
+
toSide: -1,
|
|
1453
|
+
direction: 1,
|
|
1454
|
+
targetSegment: 0,
|
|
1455
|
+
progress: 0,
|
|
1456
|
+
}
|
|
1457
|
+
transitionStartedAt = undefined
|
|
1458
|
+
pointerActive = false
|
|
1459
|
+
scheduleFrame()
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function syncHeroState() {
|
|
1463
|
+
if (!scroller) return
|
|
1464
|
+
const nextMarker = scroller.querySelector(HERO_SELECTOR)
|
|
1465
|
+
if (nextMarker) {
|
|
1466
|
+
if (!heroActive || nextMarker !== heroMarker) enterHero(nextMarker)
|
|
1467
|
+
return
|
|
1468
|
+
}
|
|
1469
|
+
if (heroActive) leaveHero()
|
|
1470
|
+
}
|
|
1471
|
+
function onScroll() {
|
|
1472
|
+
if (!scroller) return
|
|
1473
|
+
if (heroActive) {
|
|
1474
|
+
scheduleFrame()
|
|
1475
|
+
return
|
|
1476
|
+
}
|
|
1477
|
+
const viewport = Math.max(1, Number(scroller.clientHeight) || height)
|
|
1478
|
+
const desiredSegment = Math.floor(((Number(scroller.scrollTop) || 0) + 1) / viewport)
|
|
1479
|
+
if (transition) pendingSegment = desiredSegment
|
|
1480
|
+
else if (desiredSegment !== segment) startTransition(desiredSegment)
|
|
1481
|
+
scheduleFrame()
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function onPointerMove(event) {
|
|
1485
|
+
if (!operation || transition || motionReduced() || phoneStatic()) return
|
|
1486
|
+
pointerClientX = event.clientX
|
|
1487
|
+
pointerClientY = event.clientY
|
|
1488
|
+
pointerActive = true
|
|
1489
|
+
projectPointerFromClient()
|
|
1490
|
+
scheduleFrame()
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function onPointerLeave() {
|
|
1494
|
+
pointerActive = false
|
|
1495
|
+
scheduleFrame()
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
function cancelHydrationResume() {
|
|
1499
|
+
if (hydrationTimer !== undefined) window.clearTimeout?.(hydrationTimer)
|
|
1500
|
+
if (hydrationFrame !== undefined) {
|
|
1501
|
+
if (window.cancelAnimationFrame) window.cancelAnimationFrame(hydrationFrame)
|
|
1502
|
+
else window.clearTimeout?.(hydrationFrame)
|
|
1503
|
+
}
|
|
1504
|
+
hydrationTimer = undefined
|
|
1505
|
+
hydrationFrame = undefined
|
|
1506
|
+
hydrationStableFrames = 0
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function resumeAfterStableFrames() {
|
|
1510
|
+
hydrationFrame = undefined
|
|
1511
|
+
hydrationStableFrames += 1
|
|
1512
|
+
if (hydrationStableFrames < 2) {
|
|
1513
|
+
hydrationFrame = window.requestAnimationFrame?.(resumeAfterStableFrames)
|
|
1514
|
+
?? window.setTimeout?.(resumeAfterStableFrames, 16)
|
|
1515
|
+
return
|
|
1516
|
+
}
|
|
1517
|
+
hydrationPaused = false
|
|
1518
|
+
updateFrameGate()
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function registerContentMutations(mutations) {
|
|
1522
|
+
let weight = mutations.length
|
|
1523
|
+
for (const mutation of mutations) {
|
|
1524
|
+
for (const node of [...mutation.addedNodes, ...mutation.removedNodes]) {
|
|
1525
|
+
if (node?.nodeType === 1) {
|
|
1526
|
+
if (node.matches?.('[data-chat-flow-kind], [data-message-role]')) weight += 1
|
|
1527
|
+
weight += countMatchingDescendants(
|
|
1528
|
+
node,
|
|
1529
|
+
'[data-chat-flow-kind], [data-message-role]',
|
|
1530
|
+
)
|
|
1531
|
+
} else if (node?.nodeType === 3) weight += 1
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
mutationBurstScore += weight
|
|
1535
|
+
if (mutationBurstTimer !== undefined) window.clearTimeout?.(mutationBurstTimer)
|
|
1536
|
+
mutationBurstTimer = window.setTimeout?.(() => {
|
|
1537
|
+
mutationBurstTimer = undefined
|
|
1538
|
+
mutationBurstScore = 0
|
|
1539
|
+
}, 48)
|
|
1540
|
+
if (mutationBurstScore >= 12) pauseForHydration()
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
function pauseForHydration() {
|
|
1544
|
+
hydrationPaused = true
|
|
1545
|
+
updateFrameGate()
|
|
1546
|
+
cancelHydrationResume()
|
|
1547
|
+
hydrationTimer = window.setTimeout?.(() => {
|
|
1548
|
+
hydrationTimer = undefined
|
|
1549
|
+
hydrationFrame = window.requestAnimationFrame?.(resumeAfterStableFrames)
|
|
1550
|
+
?? window.setTimeout?.(resumeAfterStableFrames, 16)
|
|
1551
|
+
}, HYDRATION_SETTLE_MS)
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
function snapParticlesToCurrentLayout() {
|
|
1555
|
+
const { center, size } = currentLayout()
|
|
1556
|
+
particles.forEach(particle => {
|
|
1557
|
+
if (!particle.target) return
|
|
1558
|
+
particle.x = center.x + particle.target.x * size
|
|
1559
|
+
particle.y = center.y + particle.target.y * size
|
|
1560
|
+
particle.vx = 0
|
|
1561
|
+
particle.vy = 0
|
|
1562
|
+
})
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
function updateFrameGate() {
|
|
1566
|
+
const now = Number(window.performance?.now?.()) || 0
|
|
1567
|
+
if (!canRenderFrame()) {
|
|
1568
|
+
pauseStartedAt ??= now
|
|
1569
|
+
cancelFrame()
|
|
1570
|
+
return
|
|
1571
|
+
}
|
|
1572
|
+
if (pauseStartedAt !== undefined) {
|
|
1573
|
+
resumePausedTimelines(now)
|
|
1574
|
+
if (canvas && particles.length) {
|
|
1575
|
+
syncCanvasBackingStore()
|
|
1576
|
+
cancelFrame()
|
|
1577
|
+
draw(now)
|
|
1578
|
+
return
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
scheduleFrame()
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
function onVisibilityChange() {
|
|
1585
|
+
documentVisible = document.visibilityState !== 'hidden'
|
|
1586
|
+
updateFrameGate()
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function onIntersection(entries) {
|
|
1590
|
+
canvasVisible = entries.some(entry => entry.isIntersecting || entry.intersectionRatio > 0)
|
|
1591
|
+
updateFrameGate()
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function unmount() {
|
|
1595
|
+
clearResizeState()
|
|
1596
|
+
cancelTargetPrewarm()
|
|
1597
|
+
scroller?.removeAttribute('data-prts-hero-active')
|
|
1598
|
+
heroMarker = undefined
|
|
1599
|
+
heroActive = false
|
|
1600
|
+
scroller?.removeEventListener('scroll', onScroll)
|
|
1601
|
+
operation?.removeEventListener('pointermove', onPointerMove)
|
|
1602
|
+
operation?.removeEventListener('pointerleave', onPointerLeave)
|
|
1603
|
+
resizeObserver?.disconnect()
|
|
1604
|
+
intersectionObserver?.disconnect()
|
|
1605
|
+
intersectionObserver = undefined
|
|
1606
|
+
canvasVisible = true
|
|
1607
|
+
resizeObserver = undefined
|
|
1608
|
+
ambient?.remove()
|
|
1609
|
+
canvas?.remove()
|
|
1610
|
+
ambient = undefined
|
|
1611
|
+
glow = undefined
|
|
1612
|
+
canvas = undefined
|
|
1613
|
+
context = undefined
|
|
1614
|
+
scroller = undefined
|
|
1615
|
+
geometryDirty = true
|
|
1616
|
+
particles = []
|
|
1617
|
+
idleStableFrames = 0
|
|
1618
|
+
transition = undefined
|
|
1619
|
+
densityTransition = undefined
|
|
1620
|
+
densityStartedAt = undefined
|
|
1621
|
+
queuedDensityCount = undefined
|
|
1622
|
+
queuedTargetRefresh = false
|
|
1623
|
+
if (densityDebounce !== undefined) window.clearTimeout?.(densityDebounce)
|
|
1624
|
+
densityDebounce = undefined
|
|
1625
|
+
pauseStartedAt = undefined
|
|
1626
|
+
hydrationPaused = false
|
|
1627
|
+
cancelHydrationResume()
|
|
1628
|
+
if (mutationBurstTimer !== undefined) window.clearTimeout?.(mutationBurstTimer)
|
|
1629
|
+
mutationBurstTimer = undefined
|
|
1630
|
+
mutationBurstScore = 0
|
|
1631
|
+
cancelFrame()
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
function mount(nextOperation, nextScroller) {
|
|
1635
|
+
if (operation === nextOperation && scroller === nextScroller && ambient?.isConnected && canvas?.isConnected) return
|
|
1636
|
+
unmount()
|
|
1637
|
+
operation = nextOperation
|
|
1638
|
+
scroller = nextScroller
|
|
1639
|
+
if (!operation || !scroller) return
|
|
1640
|
+
|
|
1641
|
+
const created = createAmbient(document)
|
|
1642
|
+
ambient = created.layer
|
|
1643
|
+
glow = created.glow
|
|
1644
|
+
canvas = document.createElement('canvas')
|
|
1645
|
+
canvas.setAttribute('data-prts-particle-layer', '')
|
|
1646
|
+
canvas.setAttribute('aria-label', '随对话滚动轮换阵营标识的粒子画布')
|
|
1647
|
+
canvas.style.visibility = 'hidden'
|
|
1648
|
+
if (typeof window.CanvasRenderingContext2D === 'function') context = canvas.getContext('2d')
|
|
1649
|
+
targetCache.clear()
|
|
1650
|
+
initializeFieldSize()
|
|
1651
|
+
syncHeroState()
|
|
1652
|
+
if (!particles.length && maskRecords.length) buildParticles()
|
|
1653
|
+
if (context && particles.length) {
|
|
1654
|
+
syncCanvasBackingStore()
|
|
1655
|
+
geometryDirty = false
|
|
1656
|
+
draw(Number(window.performance?.now?.()) || 0)
|
|
1657
|
+
} else {
|
|
1658
|
+
canvas.setAttribute('data-prts-particle-ready', '')
|
|
1659
|
+
canvas.style.visibility = ''
|
|
1660
|
+
}
|
|
1661
|
+
operation.insertBefore(ambient, operation.firstChild)
|
|
1662
|
+
operation.insertBefore(canvas, ambient.nextSibling)
|
|
1663
|
+
scroller.addEventListener('scroll', onScroll, { passive: true })
|
|
1664
|
+
operation.addEventListener('pointermove', onPointerMove, { passive: true })
|
|
1665
|
+
operation.addEventListener('pointerleave', onPointerLeave)
|
|
1666
|
+
if (typeof window.ResizeObserver === 'function') {
|
|
1667
|
+
resizeObserver = new window.ResizeObserver(onOperationResize)
|
|
1668
|
+
resizeObserver.observe(operation)
|
|
1669
|
+
}
|
|
1670
|
+
if (typeof window.IntersectionObserver === 'function') {
|
|
1671
|
+
intersectionObserver = new window.IntersectionObserver(onIntersection)
|
|
1672
|
+
intersectionObserver.observe(canvas)
|
|
1673
|
+
}
|
|
1674
|
+
if (!maskRecords.length) reportState({ phase: 'error', error: '徽记资源异常' })
|
|
1675
|
+
scheduleFrame()
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
function scheduleFindMount() {
|
|
1679
|
+
if (mountFrame !== undefined) return
|
|
1680
|
+
mountFrame = window.requestAnimationFrame?.(() => {
|
|
1681
|
+
mountFrame = undefined
|
|
1682
|
+
findMount()
|
|
1683
|
+
}) ?? window.setTimeout?.(() => {
|
|
1684
|
+
mountFrame = undefined
|
|
1685
|
+
findMount()
|
|
1686
|
+
}, 0)
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function cancelFindMount() {
|
|
1690
|
+
if (mountFrame === undefined) return
|
|
1691
|
+
if (window.cancelAnimationFrame) window.cancelAnimationFrame(mountFrame)
|
|
1692
|
+
else window.clearTimeout?.(mountFrame)
|
|
1693
|
+
mountFrame = undefined
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function findMount() {
|
|
1697
|
+
const nextOperation = document.querySelector('[data-prts-region="operation"]')
|
|
1698
|
+
const nextScroller = nextOperation?.querySelector('[data-conversation-scroll]')
|
|
1699
|
+
mount(nextOperation, nextScroller)
|
|
1700
|
+
syncHeroState()
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
function onViewportChange() {
|
|
1704
|
+
transition = undefined
|
|
1705
|
+
transitionStartedAt = undefined
|
|
1706
|
+
side = sideForSegment(segment)
|
|
1707
|
+
if (particles.length) snapParticlesToCurrentLayout()
|
|
1708
|
+
scheduleFrame()
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
mediaQuery = window.matchMedia?.('(prefers-reduced-motion: reduce)')
|
|
1712
|
+
mediaListener = onViewportChange
|
|
1713
|
+
mediaQuery?.addEventListener?.('change', mediaListener)
|
|
1714
|
+
if (!mediaQuery?.addEventListener) mediaQuery?.addListener?.(mediaListener)
|
|
1715
|
+
document.addEventListener?.('visibilitychange', onVisibilityChange)
|
|
1716
|
+
return {
|
|
1717
|
+
update(nextPreferences = {}) {
|
|
1718
|
+
const previousPreferences = preferences
|
|
1719
|
+
preferences = nextPreferences
|
|
1720
|
+
disposed = false
|
|
1721
|
+
if (!preferences.enabled) {
|
|
1722
|
+
mutationObserver?.disconnect()
|
|
1723
|
+
mutationObserver = undefined
|
|
1724
|
+
cancelFindMount()
|
|
1725
|
+
unmount()
|
|
1726
|
+
operation = undefined
|
|
1727
|
+
return
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
findMount()
|
|
1731
|
+
const previousActiveDensity = heroActive
|
|
1732
|
+
? previousPreferences.heroParticleDensity
|
|
1733
|
+
: previousPreferences.conversationParticleDensity
|
|
1734
|
+
const activeDensityChanged = previousActiveDensity !== undefined && previousActiveDensity !== activeDensity()
|
|
1735
|
+
reportState({ requestedPattern: 'orthogonal' })
|
|
1736
|
+
if (particles.length && activeDensityChanged) scheduleDensityReassembly(targetParticleCount(), true)
|
|
1737
|
+
if (!mutationObserver && window.MutationObserver && document.body) {
|
|
1738
|
+
const observationRoot = document.querySelector('[data-slot="root"]')
|
|
1739
|
+
?? document.querySelector('[data-prts-region="frame"]')
|
|
1740
|
+
?? operation
|
|
1741
|
+
if (observationRoot) {
|
|
1742
|
+
mutationObserver = new window.MutationObserver(mutations => {
|
|
1743
|
+
let mountChanged = !operation?.isConnected || !scroller?.isConnected
|
|
1744
|
+
let heroChanged = false
|
|
1745
|
+
let contentChanged = false
|
|
1746
|
+
for (const mutation of mutations) {
|
|
1747
|
+
if (mutation.target?.closest?.('[data-prts-particle-layer], [data-prts-ambient-layer]')) continue
|
|
1748
|
+
if (operation?.contains?.(mutation.target)) contentChanged = true
|
|
1749
|
+
for (const node of [...mutation.addedNodes, ...mutation.removedNodes]) {
|
|
1750
|
+
if (node?.nodeType !== 1) continue
|
|
1751
|
+
if (node.matches?.('[data-prts-region="operation"], [data-conversation-scroll]')
|
|
1752
|
+
|| node.querySelector?.('[data-prts-region="operation"], [data-conversation-scroll]')) mountChanged = true
|
|
1753
|
+
if (node === heroMarker || node.matches?.(HERO_SELECTOR) || node.querySelector?.(HERO_SELECTOR)) heroChanged = true
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
if (contentChanged) registerContentMutations(mutations)
|
|
1757
|
+
if (mountChanged) scheduleFindMount()
|
|
1758
|
+
else if (heroChanged) syncHeroState()
|
|
1759
|
+
})
|
|
1760
|
+
mutationObserver.observe(observationRoot, { childList: true, subtree: true })
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
if (ambient) ambient.hidden = preferences.texture === 'off'
|
|
1764
|
+
scheduleFrame()
|
|
1765
|
+
},
|
|
1766
|
+
inspect() {
|
|
1767
|
+
return {
|
|
1768
|
+
mounted: Boolean(ambient?.isConnected && canvas?.isConnected),
|
|
1769
|
+
hero: heroActive,
|
|
1770
|
+
side,
|
|
1771
|
+
emblemKey: (heroActive || transition?.fromHero ? heroEmblem() : emblemForSegment(segment))?.key,
|
|
1772
|
+
nextEmblemKey: transition ? emblemForSegment(transition.targetSegment)?.key : null,
|
|
1773
|
+
segment,
|
|
1774
|
+
transition: transition ? {
|
|
1775
|
+
fromSide: transition.fromSide,
|
|
1776
|
+
toSide: transition.toSide,
|
|
1777
|
+
direction: transition.direction,
|
|
1778
|
+
} : null,
|
|
1779
|
+
anchor: inspectAnchor(),
|
|
1780
|
+
particles: densityTransition
|
|
1781
|
+
? Math.round(densityTransition.from + (densityTransition.to - densityTransition.from) * easeInOut(densityTransition.progress))
|
|
1782
|
+
: transition?.toCount !== undefined
|
|
1783
|
+
? Math.round(transition.fromCount + (transition.toCount - transition.fromCount) * easeInOut(transition.progress))
|
|
1784
|
+
: particles.length,
|
|
1785
|
+
targetParticles: transition?.toCount ?? targetParticleCount(),
|
|
1786
|
+
density: activeDensity() || 'standard',
|
|
1787
|
+
firstFrameReady: canvas?.hasAttribute('data-prts-particle-ready') === true,
|
|
1788
|
+
hydrationPaused,
|
|
1789
|
+
sleeping: Boolean(context && particles.length && frame === undefined && canRenderFrame() && !motionReduced() && !phoneStatic()),
|
|
1790
|
+
pointer: {
|
|
1791
|
+
active: pointerActive,
|
|
1792
|
+
clientX: pointerClientX,
|
|
1793
|
+
clientY: pointerClientY,
|
|
1794
|
+
x: pointerX,
|
|
1795
|
+
y: pointerY,
|
|
1796
|
+
},
|
|
1797
|
+
densityCapped: false,
|
|
1798
|
+
pattern: 'orthogonal',
|
|
1799
|
+
appliedPattern: particleState.appliedPattern,
|
|
1800
|
+
phase: particleState.phase,
|
|
1801
|
+
error: particleState.error,
|
|
1802
|
+
densityTransition: densityTransition ? { mode: densityTransition.mode, from: densityTransition.from, to: densityTransition.to } : null,
|
|
1803
|
+
densityPending: densityDebounce !== undefined || queuedDensityCount !== undefined || queuedTargetRefresh,
|
|
1804
|
+
layoutPending: false,
|
|
1805
|
+
layoutPhase: null,
|
|
1806
|
+
layoutFractureProgress: null,
|
|
1807
|
+
layoutStable: null,
|
|
1808
|
+
layoutReassemblyProgress: null,
|
|
1809
|
+
canvasWidth: canvas?.width ?? 0,
|
|
1810
|
+
canvasHeight: canvas?.height ?? 0,
|
|
1811
|
+
resizing,
|
|
1812
|
+
resizePending: resizing || resizeFrame !== undefined || resizeCommitFrame !== undefined || resizeFinishFrame !== undefined || resizeTimer !== undefined,
|
|
1813
|
+
resizeObservations,
|
|
1814
|
+
resizeCommits,
|
|
1815
|
+
backingStoreCommits,
|
|
1816
|
+
fieldWidth: width,
|
|
1817
|
+
fieldHeight: height,
|
|
1818
|
+
resizeRestorePending: resizeRestoreTimer !== undefined,
|
|
1819
|
+
layoutReassemblies: 0,
|
|
1820
|
+
heroSize: heroActive || transition?.fromHero ? heroLogoSize() : null,
|
|
1821
|
+
suspended: !canRenderFrame(),
|
|
1822
|
+
}
|
|
1823
|
+
},
|
|
1824
|
+
dispose() {
|
|
1825
|
+
disposed = true
|
|
1826
|
+
mutationObserver?.disconnect()
|
|
1827
|
+
mutationObserver = undefined
|
|
1828
|
+
cancelFindMount()
|
|
1829
|
+
unmount()
|
|
1830
|
+
operation = undefined
|
|
1831
|
+
if (mediaQuery && mediaListener) {
|
|
1832
|
+
mediaQuery.removeEventListener?.('change', mediaListener)
|
|
1833
|
+
mediaQuery.removeListener?.(mediaListener)
|
|
1834
|
+
}
|
|
1835
|
+
mediaQuery = undefined
|
|
1836
|
+
mediaListener = undefined
|
|
1837
|
+
document.removeEventListener?.('visibilitychange', onVisibilityChange)
|
|
1838
|
+
},
|
|
1839
|
+
}
|
|
1840
|
+
}
|