@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.
Files changed (50) hide show
  1. package/LICENSE +24 -0
  2. package/README.en.md +103 -0
  3. package/README.md +107 -0
  4. package/THIRD_PARTY_NOTICES.md +48 -0
  5. package/cordis.patch.yml +4 -0
  6. package/lib/client.js +7466 -0
  7. package/lib/index.js +3 -0
  8. package/package.json +64 -0
  9. package/src/assets/faction-emblems/lungmen.png +0 -0
  10. package/src/assets/faction-emblems/penguin-logistics.png +0 -0
  11. package/src/assets/faction-emblems/reunion.png +0 -0
  12. package/src/assets/faction-emblems/rhine-lab.png +0 -0
  13. package/src/assets/faction-emblems/rhodes-island.png +0 -0
  14. package/src/assets/prts-original-avatar.png +0 -0
  15. package/src/assets/rhodes-dsh.svg +6 -0
  16. package/src/assets/topographic-map.svg +72 -0
  17. package/src/assets/topographic-tech.svg +19 -0
  18. package/src/client/assistant-glass-adapter.js +231 -0
  19. package/src/client/composer-glass-adapter.js +210 -0
  20. package/src/client/conversation-control-adapter.js +109 -0
  21. package/src/client/conversation-scale-adapter.js +2243 -0
  22. package/src/client/facility-geometry.js +60 -0
  23. package/src/client/facility-vector.js +230 -0
  24. package/src/client/floating-glass-adapter.js +177 -0
  25. package/src/client/host-geometry-adapter.js +123 -0
  26. package/src/client/index.js +359 -0
  27. package/src/client/layout-stability-diagnostics.js +162 -0
  28. package/src/client/operations-shell-v2.js +248 -0
  29. package/src/client/particle-field-adapter.js +1840 -0
  30. package/src/client/preferences.js +239 -0
  31. package/src/client/rc7-adapter.js +83 -0
  32. package/src/client/resize-shield-adapter.js +64 -0
  33. package/src/client/settings-page.js +62 -0
  34. package/src/client/sidebar-control-adapter.js +317 -0
  35. package/src/client/startup-sequence.js +366 -0
  36. package/src/client/status-projection.js +79 -0
  37. package/src/client/terrain-motion-adapter.js +188 -0
  38. package/src/client/theme-controller-v2.js +134 -0
  39. package/src/client/theme-controller.js +119 -0
  40. package/src/client/theme-settings-workbench.js +583 -0
  41. package/src/client/to-bottom-adapter.js +85 -0
  42. package/src/client/ui-store.js +23 -0
  43. package/src/index.js +4 -0
  44. package/src/styles/conversation-scale.css +471 -0
  45. package/src/styles/facility-vector.css +292 -0
  46. package/src/styles/prts.css +1223 -0
  47. package/src/styles/refinement.css +429 -0
  48. package/src/styles/stabilization.css +161 -0
  49. package/src/styles/startup-sequence.css +496 -0
  50. 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
+ }