@ash-qw/dsh-theme-prts 0.1.91 → 0.1.93

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.
@@ -326,7 +326,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
326
326
  const rhodesIndex = Math.max(0, emblemSequence.findIndex(entry => entry.key === 'rhodes-island'))
327
327
  const suppliedHeroMask = normalizeEmblemMasks(heroEmblemMask ? [heroEmblemMask] : [], window).get('rhodes-island-hero')
328
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() {} }
329
+ if (!document || !window) return { update() {}, setSchemeTransitionActive() {}, inspect: () => empty, dispose() {} }
330
330
 
331
331
  let preferences = {}
332
332
  let operation
@@ -378,6 +378,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
378
378
  let side = -1
379
379
  let segment = 0
380
380
  let transition
381
+ let traversalOffset = 0
381
382
  let pendingSegment
382
383
  let transitionStartedAt
383
384
  let densityTransition
@@ -394,6 +395,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
394
395
  let targetPrewarmQueue = []
395
396
  let pauseStartedAt
396
397
  let hydrationPaused = false
398
+ let schemeTransitionPaused = false
397
399
  let hydrationTimer
398
400
  let hydrationFrame
399
401
  let hydrationStableFrames = 0
@@ -427,7 +429,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
427
429
  : preferences.conversationParticleDensity
428
430
  const densityProfile = density => PARTICLE_DENSITY_PROFILES[density] ?? PARTICLE_DENSITY_PROFILES.standard
429
431
  const particleRadius = density => densityProfile(density ?? activeDensity()).radius
430
- const canRenderFrame = () => documentVisible && canvasVisible && !hydrationPaused && !resizing
432
+ const canRenderFrame = () => !disposed && documentVisible && canvasVisible && !hydrationPaused && !schemeTransitionPaused && !resizing
431
433
  const layoutWidth = () => width
432
434
  const layoutHeight = () => height
433
435
  const layoutShiftX = () => 0
@@ -554,11 +556,50 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
554
556
  }
555
557
 
556
558
 
559
+ function particleTraversalSpeed(value = preferences) {
560
+ const numeric = Number(value?.particleTraversalSpeed)
561
+ return Number.isFinite(numeric) ? clamp(numeric, 0, 2) : 1
562
+ }
563
+
564
+ function currentScrollTop() {
565
+ return Number(scroller?.scrollTop) || 0
566
+ }
567
+
568
+ function traversalViewport() {
569
+ return Math.max(1, Number(scroller?.clientHeight) || height)
570
+ }
571
+
572
+ function traversalCoordinate(value = preferences) {
573
+ return traversalOffset + currentScrollTop() * particleTraversalSpeed(value) / traversalViewport()
574
+ }
575
+
557
576
  function scrollProgress() {
558
577
  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)
578
+ const coordinate = traversalCoordinate()
579
+ return coordinate - Math.floor(coordinate)
580
+ }
581
+
582
+ function restoreTraversalFromScroll() {
583
+ traversalOffset = 0
584
+ const coordinate = traversalCoordinate()
585
+ segment = Math.floor(coordinate)
586
+ side = sideForSegment(segment)
587
+ pendingSegment = undefined
588
+ transition = undefined
589
+ transitionStartedAt = undefined
590
+ }
591
+
592
+ function reanchorTraversal(coordinate = traversalCoordinate()) {
593
+ if (transition) {
594
+ const targetSegment = transition.targetSegment
595
+ side = sideForSegment(targetSegment)
596
+ transition = undefined
597
+ transitionStartedAt = undefined
598
+ commitTransitionTargets(targetSegment, true)
599
+ }
600
+ pendingSegment = undefined
601
+ traversalOffset = coordinate - currentScrollTop() * particleTraversalSpeed() / traversalViewport()
602
+ if (particles.length) snapParticlesToCurrentLayout()
562
603
  }
563
604
 
564
605
  function anchorFor(sideValue, verticalProgress) {
@@ -1373,6 +1414,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1373
1414
  return
1374
1415
  }
1375
1416
  const targetSide = sideForSegment(targetSegment)
1417
+ const direction = targetSegment > segment ? 1 : -1
1376
1418
  if (targetSide === side || motionReduced() || phoneStatic()) {
1377
1419
  side = targetSide
1378
1420
  transition = undefined
@@ -1389,7 +1431,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1389
1431
  ...population,
1390
1432
  fromSide: side,
1391
1433
  toSide: targetSide,
1392
- direction: targetSegment > segment ? 1 : -1,
1434
+ direction,
1393
1435
  targetSegment,
1394
1436
  progress: 0,
1395
1437
  }
@@ -1398,9 +1440,27 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1398
1440
  scheduleFrame()
1399
1441
  }
1400
1442
 
1443
+ function retargetTransition(targetSegment) {
1444
+ if (!transition || targetSegment === transition.targetSegment) return
1445
+ const population = prepareTransitionTargets(targetSegment)
1446
+ if (!population.toCount) {
1447
+ reportState({ phase: 'error', error: '徽记资源异常' })
1448
+ return
1449
+ }
1450
+ transition = {
1451
+ ...transition,
1452
+ ...population,
1453
+ toSide: sideForSegment(targetSegment),
1454
+ direction: targetSegment >= segment ? 1 : -1,
1455
+ targetSegment,
1456
+ }
1457
+ pendingSegment = undefined
1458
+ }
1459
+
1401
1460
  function resetTraversal() {
1402
1461
  side = -1
1403
1462
  segment = 0
1463
+ traversalOffset = -currentScrollTop() * particleTraversalSpeed() / traversalViewport()
1404
1464
  pendingSegment = undefined
1405
1465
  queuedDensityCount = undefined
1406
1466
  queuedTargetRefresh = false
@@ -1430,6 +1490,9 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1430
1490
  heroMarker = undefined
1431
1491
  heroActive = false
1432
1492
  scroller?.removeAttribute('data-prts-hero-active')
1493
+ side = -1
1494
+ segment = 0
1495
+ traversalOffset = -currentScrollTop() * particleTraversalSpeed() / traversalViewport()
1433
1496
  pendingSegment = undefined
1434
1497
  queuedDensityCount = undefined
1435
1498
  queuedTargetRefresh = false
@@ -1474,9 +1537,8 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1474
1537
  scheduleFrame()
1475
1538
  return
1476
1539
  }
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
1540
+ const desiredSegment = Math.floor(traversalCoordinate())
1541
+ if (transition) retargetTransition(desiredSegment)
1480
1542
  else if (desiredSegment !== segment) startTransition(desiredSegment)
1481
1543
  scheduleFrame()
1482
1544
  }
@@ -1649,6 +1711,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1649
1711
  targetCache.clear()
1650
1712
  initializeFieldSize()
1651
1713
  syncHeroState()
1714
+ if (!heroActive) restoreTraversalFromScroll()
1652
1715
  if (!particles.length && maskRecords.length) buildParticles()
1653
1716
  if (context && particles.length) {
1654
1717
  syncCanvasBackingStore()
@@ -1714,8 +1777,19 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1714
1777
  if (!mediaQuery?.addEventListener) mediaQuery?.addListener?.(mediaListener)
1715
1778
  document.addEventListener?.('visibilitychange', onVisibilityChange)
1716
1779
  return {
1780
+ setSchemeTransitionActive(active) {
1781
+ const next = Boolean(active)
1782
+ if (schemeTransitionPaused === next) return
1783
+ schemeTransitionPaused = next
1784
+ updateFrameGate()
1785
+ },
1717
1786
  update(nextPreferences = {}) {
1718
1787
  const previousPreferences = preferences
1788
+ const traversalSpeedChanged = previousPreferences.particleTraversalSpeed !== undefined
1789
+ && particleTraversalSpeed(previousPreferences) !== particleTraversalSpeed(nextPreferences)
1790
+ const preservedTraversalCoordinate = traversalSpeedChanged && scroller && !heroActive
1791
+ ? traversalCoordinate(previousPreferences)
1792
+ : undefined
1719
1793
  preferences = nextPreferences
1720
1794
  disposed = false
1721
1795
  if (!preferences.enabled) {
@@ -1734,6 +1808,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1734
1808
  const activeDensityChanged = previousActiveDensity !== undefined && previousActiveDensity !== activeDensity()
1735
1809
  reportState({ requestedPattern: 'orthogonal' })
1736
1810
  if (particles.length && activeDensityChanged) scheduleDensityReassembly(targetParticleCount(), true)
1811
+ if (preservedTraversalCoordinate !== undefined && scroller && !heroActive) reanchorTraversal(preservedTraversalCoordinate)
1737
1812
  if (!mutationObserver && window.MutationObserver && document.body) {
1738
1813
  const observationRoot = document.querySelector('[data-slot="root"]')
1739
1814
  ?? document.querySelector('[data-prts-region="frame"]')
@@ -1771,6 +1846,10 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1771
1846
  emblemKey: (heroActive || transition?.fromHero ? heroEmblem() : emblemForSegment(segment))?.key,
1772
1847
  nextEmblemKey: transition ? emblemForSegment(transition.targetSegment)?.key : null,
1773
1848
  segment,
1849
+ traversalSpeed: particleTraversalSpeed(),
1850
+ traversalOffset,
1851
+ traversalCoordinate: traversalCoordinate(),
1852
+ traversalProgress: scrollProgress(),
1774
1853
  transition: transition ? {
1775
1854
  fromSide: transition.fromSide,
1776
1855
  toSide: transition.toSide,
@@ -1787,6 +1866,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
1787
1866
  firstFrameReady: canvas?.hasAttribute('data-prts-particle-ready') === true,
1788
1867
  hydrationPaused,
1789
1868
  sleeping: Boolean(context && particles.length && frame === undefined && canRenderFrame() && !motionReduced() && !phoneStatic()),
1869
+ schemeTransitionPaused,
1790
1870
  pointer: {
1791
1871
  active: pointerActive,
1792
1872
  clientX: pointerClientX,
@@ -10,8 +10,13 @@ export const CONVERSATION_SCALE_FOCUS_CONTRAST_MAX = 100
10
10
  export const CONVERSATION_SCALE_FOCUS_CONTRAST_STEP = 10
11
11
  export const CONVERSATION_SCALE_FOCUS_CONTRAST_DEFAULT = 70
12
12
 
13
+ export const PARTICLE_TRAVERSAL_SPEED_MIN = 0
14
+ export const PARTICLE_TRAVERSAL_SPEED_MAX = 2
15
+ export const PARTICLE_TRAVERSAL_SPEED_STEP = 0.25
16
+ export const PARTICLE_TRAVERSAL_SPEED_DEFAULT = 1
17
+
13
18
  export const DEFAULT_PREFERENCES = Object.freeze({
14
- version: 6,
19
+ version: 8,
15
20
  enabled: false,
16
21
  preset: 'standard-tactical',
17
22
  texture: 'full',
@@ -20,7 +25,9 @@ export const DEFAULT_PREFERENCES = Object.freeze({
20
25
  bootAnimation: true,
21
26
  conversationParticleDensity: 'sparse',
22
27
  heroParticleDensity: 'light',
28
+ particleTraversalSpeed: PARTICLE_TRAVERSAL_SPEED_DEFAULT,
23
29
  particlePattern: 'orthogonal',
30
+ railDefaultHidden: false,
24
31
  conversationScaleMaxDistance: CONVERSATION_SCALE_DISTANCE_DEFAULT,
25
32
  conversationScaleFocusContrast: CONVERSATION_SCALE_FOCUS_CONTRAST_DEFAULT,
26
33
  })
@@ -60,10 +67,10 @@ export const PARTICLE_DETAIL_LEVELS = Object.freeze({
60
67
  })
61
68
  export const PREFERENCE_GROUPS = Object.freeze({
62
69
  background: Object.freeze(['texture']),
63
- particles: Object.freeze(['conversationParticleDensity', 'heroParticleDensity']),
70
+ particles: Object.freeze(['conversationParticleDensity', 'heroParticleDensity', 'particleTraversalSpeed']),
64
71
  material: Object.freeze(['glass']),
65
72
  accessibility: Object.freeze(['motion', 'bootAnimation']),
66
- navigation: Object.freeze(['conversationScaleMaxDistance', 'conversationScaleFocusContrast']),
73
+ navigation: Object.freeze(['railDefaultHidden', 'conversationScaleMaxDistance', 'conversationScaleFocusContrast']),
67
74
  })
68
75
 
69
76
  const TEXTURES = new Set(['off', 'restrained', 'full'])
@@ -114,6 +121,13 @@ function scaleFocusContrastOr(value, fallback) {
114
121
  return Math.round(clamped / CONVERSATION_SCALE_FOCUS_CONTRAST_STEP) * CONVERSATION_SCALE_FOCUS_CONTRAST_STEP
115
122
  }
116
123
 
124
+ function particleTraversalSpeedOr(value, fallback) {
125
+ const numeric = Number(value)
126
+ if (!Number.isFinite(numeric)) return fallback
127
+ const clamped = Math.min(PARTICLE_TRAVERSAL_SPEED_MAX, Math.max(PARTICLE_TRAVERSAL_SPEED_MIN, numeric))
128
+ return Math.round(clamped / PARTICLE_TRAVERSAL_SPEED_STEP) * PARTICLE_TRAVERSAL_SPEED_STEP
129
+ }
130
+
117
131
  function normalizeGlass(input) {
118
132
  if (input.glassEnabled === false || input.glass === 'off') return 'off'
119
133
  if (input.glass === 'liquid') return 'clear'
@@ -141,7 +155,7 @@ export function normalizePreferences(value) {
141
155
  const particlePreset = VISUAL_PRESETS[input.preset] ?? DEFAULT_PREFERENCES
142
156
  const legacyDensities = legacyParticleDensities(input)
143
157
  const normalized = {
144
- version: 6,
158
+ version: 8,
145
159
  enabled: booleanOr(input.enabled, DEFAULT_PREFERENCES.enabled),
146
160
  preset: enumOr(migratedPreset, PRESETS, DEFAULT_PREFERENCES.preset),
147
161
  texture: enumOr(input.texture, TEXTURES, DEFAULT_PREFERENCES.texture),
@@ -150,7 +164,12 @@ export function normalizePreferences(value) {
150
164
  bootAnimation: booleanOr(input.bootAnimation, DEFAULT_PREFERENCES.bootAnimation),
151
165
  conversationParticleDensity: enumOr(legacyDensities.conversation, PARTICLE_DENSITIES, particlePreset.conversationParticleDensity),
152
166
  heroParticleDensity: enumOr(legacyDensities.hero, PARTICLE_DENSITIES, particlePreset.heroParticleDensity),
167
+ particleTraversalSpeed: particleTraversalSpeedOr(
168
+ input.particleTraversalSpeed,
169
+ DEFAULT_PREFERENCES.particleTraversalSpeed,
170
+ ),
153
171
  particlePattern: 'orthogonal',
172
+ railDefaultHidden: booleanOr(input.railDefaultHidden, DEFAULT_PREFERENCES.railDefaultHidden),
154
173
  conversationScaleMaxDistance: scaleDistanceOr(
155
174
  input.conversationScaleMaxDistance,
156
175
  DEFAULT_PREFERENCES.conversationScaleMaxDistance,
@@ -205,7 +224,7 @@ export function loadPreferences(storage) {
205
224
  const raw = storage?.getItem(PRTS_STORAGE_KEY)
206
225
  if (raw === null || raw === undefined) return { ...DEFAULT_PREFERENCES }
207
226
  const parsed = JSON.parse(raw)
208
- if (![1, 2, 3, 4, 5, 6].includes(parsed?.version)) return { ...DEFAULT_PREFERENCES }
227
+ if (![1, 2, 3, 4, 5, 6, 7, 8].includes(parsed?.version)) return { ...DEFAULT_PREFERENCES }
209
228
  return normalizePreferences(parsed)
210
229
  } catch {
211
230
  return { ...DEFAULT_PREFERENCES }