@ash-qw/dsh-theme-prts 0.1.97 → 0.1.99
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/CHANGELOG.md +13 -0
- package/lib/client.js +441 -32
- package/package.json +1 -1
- package/src/client/composer-glass-adapter.js +26 -9
- package/src/client/facility-vector.js +29 -0
- package/src/client/index.js +18 -4
- package/src/client/operations-shell-v2.js +4 -1
- package/src/client/particle-field-adapter.js +23 -11
- package/src/client/preferences.js +3 -1
- package/src/client/session-lifeline.js +255 -0
- package/src/client/sidebar-control-adapter.js +12 -0
- package/src/client/startup-sequence.js +102 -9
- package/src/client/theme-controller-v2.js +2 -0
- package/src/client/theme-controller.js +2 -0
- package/src/client/theme-settings-workbench.js +8 -1
- package/src/styles/facility-vector.css +65 -0
- package/src/styles/prts.css +11 -5
- package/src/styles/refinement.css +5 -2
- package/src/styles/stabilization.css +35 -1
- package/src/styles/startup-sequence.css +13 -6
package/package.json
CHANGED
|
@@ -2,8 +2,13 @@ const CONTROL_ATTRIBUTE = 'data-prts-glass-control'
|
|
|
2
2
|
const MENU_ATTRIBUTE = 'data-prts-glass-menu'
|
|
3
3
|
const FALLBACK_COMPOSER_ATTRIBUTE = 'data-prts-composer-fallback'
|
|
4
4
|
const COMPOSER_SIGNAL_ATTRIBUTE = 'data-prts-composer-signal'
|
|
5
|
+
const FALLBACK_COMPOSER_ROOT_SELECTOR = [
|
|
6
|
+
'[data-slot="conversation.composer"]',
|
|
7
|
+
'[data-composer-seat]',
|
|
8
|
+
].join(', ')
|
|
5
9
|
const COMPOSER_RELEVANCE_SELECTOR = [
|
|
6
10
|
'[data-composer-card]',
|
|
11
|
+
FALLBACK_COMPOSER_ROOT_SELECTOR,
|
|
7
12
|
'form textarea',
|
|
8
13
|
'button[aria-haspopup="menu"]',
|
|
9
14
|
'button[aria-haspopup="listbox"]',
|
|
@@ -56,6 +61,18 @@ function markPopup(popup, kind) {
|
|
|
56
61
|
}
|
|
57
62
|
}
|
|
58
63
|
|
|
64
|
+
function fallbackComposerForms(document) {
|
|
65
|
+
const forms = new Set()
|
|
66
|
+
for (const root of document?.querySelectorAll?.(FALLBACK_COMPOSER_ROOT_SELECTOR) ?? []) {
|
|
67
|
+
if (root.querySelector('[data-composer-card]')) continue
|
|
68
|
+
for (const textarea of root.querySelectorAll('form textarea')) {
|
|
69
|
+
const form = textarea.closest('form')
|
|
70
|
+
if (form && root.contains(form)) forms.add(form)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return forms
|
|
74
|
+
}
|
|
75
|
+
|
|
59
76
|
export function createComposerGlassAdapter({ document, window }) {
|
|
60
77
|
let observer
|
|
61
78
|
let scanFrame
|
|
@@ -104,17 +121,17 @@ export function createComposerGlassAdapter({ document, window }) {
|
|
|
104
121
|
signal.remove?.()
|
|
105
122
|
ownedSignals.delete(signal)
|
|
106
123
|
}
|
|
124
|
+
const fallbackForms = fallbackComposerForms(document)
|
|
107
125
|
for (const form of [...ownedFallbacks]) {
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
126
|
+
if (form.isConnected && fallbackForms.has(form) && !form.hasAttribute('data-composer-card')) continue
|
|
127
|
+
form.removeAttribute?.(FALLBACK_COMPOSER_ATTRIBUTE)
|
|
128
|
+
ownedFallbacks.delete(form)
|
|
129
|
+
if (form.hasAttribute('data-composer-card')) continue
|
|
130
|
+
const signal = form.querySelector?.(`:scope > [${COMPOSER_SIGNAL_ATTRIBUTE}]`)
|
|
131
|
+
if (!ownedSignals.has(signal)) continue
|
|
132
|
+
signal.remove?.()
|
|
133
|
+
ownedSignals.delete(signal)
|
|
112
134
|
}
|
|
113
|
-
const fallbackForms = new Set(
|
|
114
|
-
[...(document?.querySelectorAll?.('form textarea') ?? [])]
|
|
115
|
-
.map(textarea => textarea.closest('form'))
|
|
116
|
-
.filter(form => form && !form.querySelector('[data-composer-card]')),
|
|
117
|
-
)
|
|
118
135
|
for (const form of fallbackForms) {
|
|
119
136
|
form.setAttribute(FALLBACK_COMPOSER_ATTRIBUTE, '')
|
|
120
137
|
ownedFallbacks.add(form)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createFacilityPath } from './facility-geometry.js'
|
|
2
|
+
import { configureSessionLifeline } from './session-lifeline.js'
|
|
2
3
|
|
|
3
4
|
const SVG_NS = 'http://www.w3.org/2000/svg'
|
|
4
5
|
const SILHOUETTE_TRACK_WIDTH = 512
|
|
@@ -117,6 +118,16 @@ function appendSessionPickup(document, graphic) {
|
|
|
117
118
|
clip.append(svgNode(document, 'path', { 'data-prts-facility-clip': '' }))
|
|
118
119
|
defs.append(clip)
|
|
119
120
|
|
|
121
|
+
const lifeline = svgNode(document, 'g', {
|
|
122
|
+
'data-prts-session-lifeline': '',
|
|
123
|
+
'clip-path': `url(#${clipId})`,
|
|
124
|
+
})
|
|
125
|
+
for (const strand of ['back', 'core', 'front']) {
|
|
126
|
+
lifeline.append(svgNode(document, 'path', {
|
|
127
|
+
'data-prts-session-lifeline-filament': strand,
|
|
128
|
+
}))
|
|
129
|
+
}
|
|
130
|
+
|
|
120
131
|
const pickup = svgNode(document, 'g', {
|
|
121
132
|
'data-prts-session-pickup': '',
|
|
122
133
|
'clip-path': `url(#${clipId})`,
|
|
@@ -146,9 +157,22 @@ function appendSessionPickup(document, graphic) {
|
|
|
146
157
|
})
|
|
147
158
|
|
|
148
159
|
graphic.insertBefore(defs, graphic.firstChild)
|
|
160
|
+
graphic.insertBefore(lifeline, graphic.querySelector('[data-prts-facility-layer="outline"]'))
|
|
149
161
|
graphic.insertBefore(pickup, graphic.querySelector('[data-prts-facility-layer="outline"]'))
|
|
150
162
|
}
|
|
151
163
|
|
|
164
|
+
function clamp(value, min, max) {
|
|
165
|
+
return Math.min(max, Math.max(min, value))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function sessionLifelineGeometry(width, height) {
|
|
169
|
+
const start = clamp(width * .04, 8, 12)
|
|
170
|
+
const end = Math.max(start + 1, width - 8)
|
|
171
|
+
const baseline = height * .56
|
|
172
|
+
const lift = clamp(height * .35, 10, 14)
|
|
173
|
+
return { start, end, baseline, lift }
|
|
174
|
+
}
|
|
175
|
+
|
|
152
176
|
export function ensureFacilityGraphic(document, owner, { kind, topNotch = false, texture = 'none' } = {}) {
|
|
153
177
|
if (!owner) return null
|
|
154
178
|
let graphic = owner.querySelector?.(':scope > svg[data-prts-facility-svg]')
|
|
@@ -200,6 +224,8 @@ export function renderFacilityGraphic(owner, { width, height } = {}) {
|
|
|
200
224
|
}
|
|
201
225
|
|
|
202
226
|
const size = `${Number(width.toFixed(3))}x${Number(height.toFixed(3))}`
|
|
227
|
+
const lifeline = graphic.querySelector('[data-prts-session-lifeline]')
|
|
228
|
+
const lifelineGeometry = lifeline ? sessionLifelineGeometry(width, height) : undefined
|
|
203
229
|
if (owner.getAttribute('data-prts-facility-size') !== size) {
|
|
204
230
|
graphic.setAttribute('viewBox', `0 0 ${Number(width.toFixed(3))} ${Number(height.toFixed(3))}`)
|
|
205
231
|
for (const layer of graphic.querySelectorAll('[data-prts-facility-layer], [data-prts-facility-clip]')) layer.setAttribute('d', path)
|
|
@@ -223,6 +249,9 @@ export function renderFacilityGraphic(owner, { width, height } = {}) {
|
|
|
223
249
|
bar.setAttribute('y', String(Number((center - barHeight / 2).toFixed(3))))
|
|
224
250
|
}
|
|
225
251
|
}
|
|
252
|
+
if (lifeline) {
|
|
253
|
+
configureSessionLifeline(lifeline, lifelineGeometry)
|
|
254
|
+
}
|
|
226
255
|
owner.setAttribute('data-prts-facility-size', size)
|
|
227
256
|
}
|
|
228
257
|
setReady(owner, true)
|
package/src/client/index.js
CHANGED
|
@@ -44,7 +44,6 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
44
44
|
const safeMode = isSafeMode(window?.location?.search ?? '')
|
|
45
45
|
const storage = window?.localStorage
|
|
46
46
|
const themeService = contextService(ctx, 'theme')
|
|
47
|
-
const startup = createPrtsStartupSequence({ document, window, prtsEmblem: assets.prtsEmblem, rhodesEmblem: assets.emblem, timings: startupTimings })
|
|
48
47
|
const composerGlass = createComposerGlassAdapter({ document, window })
|
|
49
48
|
const assistantGlass = createAssistantGlassAdapter({
|
|
50
49
|
document,
|
|
@@ -62,6 +61,19 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
62
61
|
sync()
|
|
63
62
|
},
|
|
64
63
|
})
|
|
64
|
+
const startup = createPrtsStartupSequence({
|
|
65
|
+
document,
|
|
66
|
+
window,
|
|
67
|
+
prtsEmblem: assets.prtsEmblem,
|
|
68
|
+
rhodesEmblem: assets.emblem,
|
|
69
|
+
timings: startupTimings,
|
|
70
|
+
onActiveChange(active) {
|
|
71
|
+
particleField.setStartupActive(active)
|
|
72
|
+
},
|
|
73
|
+
onReady() {
|
|
74
|
+
particleField.setStartupActive(false)
|
|
75
|
+
},
|
|
76
|
+
})
|
|
65
77
|
const theme = createThemeController({
|
|
66
78
|
document,
|
|
67
79
|
window,
|
|
@@ -144,6 +156,7 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
144
156
|
}
|
|
145
157
|
|
|
146
158
|
const disposeVisualState = () => {
|
|
159
|
+
particleField.update({ ...preferences, enabled: false })
|
|
147
160
|
startup.stop({ restoreFocus: false })
|
|
148
161
|
stopStartupReadyWatch()
|
|
149
162
|
stopMountWatch()
|
|
@@ -151,7 +164,6 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
151
164
|
disposeOperations()
|
|
152
165
|
composerGlass.dispose()
|
|
153
166
|
assistantGlass.dispose()
|
|
154
|
-
particleField.update({ ...preferences, enabled: false })
|
|
155
167
|
toBottom.dispose()
|
|
156
168
|
conversationControls.dispose()
|
|
157
169
|
layoutDiagnostics.dispose()
|
|
@@ -227,9 +239,11 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
227
239
|
phase: result.persisted ? 'saved' : 'error',
|
|
228
240
|
revision: settingsPersistence.revision + 1,
|
|
229
241
|
}
|
|
242
|
+
const shouldPlayStartup = !safeMode && !wasEnabled && preferences.enabled && preferences.bootAnimation
|
|
243
|
+
if (shouldPlayStartup) particleField.setStartupActive(true)
|
|
230
244
|
applyVisualState()
|
|
231
245
|
sync()
|
|
232
|
-
if (
|
|
246
|
+
if (shouldPlayStartup && !startup.play()) particleField.setStartupActive(false)
|
|
233
247
|
}
|
|
234
248
|
|
|
235
249
|
|
|
@@ -302,13 +316,13 @@ export function applyPrtsPlugin(ctx, environment) {
|
|
|
302
316
|
}
|
|
303
317
|
ctx.effect(() => () => {
|
|
304
318
|
statusSource.dispose()
|
|
319
|
+
particleField.dispose()
|
|
305
320
|
startup.dispose({ restoreFocus: false })
|
|
306
321
|
stopStartupReadyWatch()
|
|
307
322
|
stopMountWatch()
|
|
308
323
|
disposeOperations()
|
|
309
324
|
composerGlass.dispose()
|
|
310
325
|
assistantGlass.dispose()
|
|
311
|
-
particleField.dispose()
|
|
312
326
|
toBottom.dispose()
|
|
313
327
|
conversationControls.dispose()
|
|
314
328
|
layoutDiagnostics.dispose()
|
|
@@ -27,7 +27,10 @@ function markup(emblem) {
|
|
|
27
27
|
<svg data-prts-scheme-icon="dark" viewBox="0 0 24 24" aria-hidden="true"><path d="M20.2 15.4A8.4 8.4 0 0 1 8.6 3.8 8.5 8.5 0 1 0 20.2 15.4Z"></path></svg>
|
|
28
28
|
<small>明暗</small>
|
|
29
29
|
</button>
|
|
30
|
-
<button type="button" data-prts-theme-disable aria-label="
|
|
30
|
+
<button type="button" data-prts-theme-disable aria-label="退出 P.R.T.S.,返回 Harness 原生界面">
|
|
31
|
+
<svg data-prts-theme-disable-icon viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v8"></path><path d="M7.1 6.8a7 7 0 1 0 9.8 0"></path></svg>
|
|
32
|
+
<small>退出 P.R.T.S.</small>
|
|
33
|
+
</button>
|
|
31
34
|
</div>
|
|
32
35
|
</nav>
|
|
33
36
|
${themeSettingsMarkup()}
|
|
@@ -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() {}, setSchemeTransitionActive() {}, inspect: () => empty, dispose() {} }
|
|
329
|
+
if (!document || !window) return { update() {}, setStartupActive() {}, setSchemeTransitionActive() {}, inspect: () => empty, dispose() {} }
|
|
330
330
|
|
|
331
331
|
let preferences = {}
|
|
332
332
|
let operation
|
|
@@ -396,6 +396,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
396
396
|
let pauseStartedAt
|
|
397
397
|
let hydrationPaused = false
|
|
398
398
|
let schemeTransitionPaused = false
|
|
399
|
+
let startupPaused = false
|
|
399
400
|
let hydrationTimer
|
|
400
401
|
let hydrationFrame
|
|
401
402
|
let hydrationStableFrames = 0
|
|
@@ -429,7 +430,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
429
430
|
: preferences.conversationParticleDensity
|
|
430
431
|
const densityProfile = density => PARTICLE_DENSITY_PROFILES[density] ?? PARTICLE_DENSITY_PROFILES.standard
|
|
431
432
|
const particleRadius = density => densityProfile(density ?? activeDensity()).radius
|
|
432
|
-
const canRenderFrame = () => !disposed && documentVisible && canvasVisible && !hydrationPaused && !schemeTransitionPaused && !resizing
|
|
433
|
+
const canRenderFrame = () => !disposed && documentVisible && canvasVisible && !hydrationPaused && !startupPaused && !schemeTransitionPaused && !resizing
|
|
433
434
|
const layoutWidth = () => width
|
|
434
435
|
const layoutHeight = () => height
|
|
435
436
|
const layoutShiftX = () => 0
|
|
@@ -707,6 +708,7 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
707
708
|
}
|
|
708
709
|
|
|
709
710
|
function buildParticles() {
|
|
711
|
+
if (startupPaused) return false
|
|
710
712
|
const selected = populationSource()
|
|
711
713
|
if (!selected.length) {
|
|
712
714
|
reportState({ phase: 'error', error: '徽记资源异常' })
|
|
@@ -1646,14 +1648,17 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1646
1648
|
cancelFrame()
|
|
1647
1649
|
return
|
|
1648
1650
|
}
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1651
|
+
const resumedFromPause = pauseStartedAt !== undefined
|
|
1652
|
+
if (resumedFromPause) resumePausedTimelines(now)
|
|
1653
|
+
let particlesBuilt = false
|
|
1654
|
+
if (canvas && !particles.length && maskRecords.length) {
|
|
1655
|
+
particlesBuilt = buildParticles()
|
|
1656
|
+
}
|
|
1657
|
+
if (canvas && particles.length && (resumedFromPause || particlesBuilt)) {
|
|
1658
|
+
syncCanvasBackingStore()
|
|
1659
|
+
cancelFrame()
|
|
1660
|
+
draw(now)
|
|
1661
|
+
return
|
|
1657
1662
|
}
|
|
1658
1663
|
scheduleFrame()
|
|
1659
1664
|
}
|
|
@@ -1819,6 +1824,12 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1819
1824
|
if (!mediaQuery?.addEventListener) mediaQuery?.addListener?.(mediaListener)
|
|
1820
1825
|
document.addEventListener?.('visibilitychange', onVisibilityChange)
|
|
1821
1826
|
return {
|
|
1827
|
+
setStartupActive(active) {
|
|
1828
|
+
const next = Boolean(active)
|
|
1829
|
+
if (startupPaused === next) return
|
|
1830
|
+
startupPaused = next
|
|
1831
|
+
updateFrameGate()
|
|
1832
|
+
},
|
|
1822
1833
|
setSchemeTransitionActive(active) {
|
|
1823
1834
|
const next = Boolean(active)
|
|
1824
1835
|
if (schemeTransitionPaused === next) return
|
|
@@ -1903,12 +1914,13 @@ export function createParticleFieldAdapter({ document, window, emblem = '', embl
|
|
|
1903
1914
|
: transition?.toCount !== undefined
|
|
1904
1915
|
? Math.round(transition.fromCount + (transition.toCount - transition.fromCount) * easeInOut(transition.progress))
|
|
1905
1916
|
: particles.length,
|
|
1906
|
-
targetParticles: transition?.toCount ?? targetParticleCount(),
|
|
1917
|
+
targetParticles: startupPaused && !particles.length ? 0 : transition?.toCount ?? targetParticleCount(),
|
|
1907
1918
|
density: activeDensity() || 'standard',
|
|
1908
1919
|
firstFrameReady: canvas?.hasAttribute('data-prts-particle-ready') === true,
|
|
1909
1920
|
hydrationPaused,
|
|
1910
1921
|
sleeping: Boolean(context && particles.length && frame === undefined && canRenderFrame() && !motionReduced() && !phoneStatic()),
|
|
1911
1922
|
schemeTransitionPaused,
|
|
1923
|
+
startupPaused,
|
|
1912
1924
|
pointer: {
|
|
1913
1925
|
active: pointerActive,
|
|
1914
1926
|
clientX: pointerClientX,
|
|
@@ -18,7 +18,8 @@ export const DEFAULT_PREFERENCES = Object.freeze({
|
|
|
18
18
|
particleTraversalSpeed: PARTICLE_TRAVERSAL_SPEED_DEFAULT,
|
|
19
19
|
particlePattern: 'orthogonal',
|
|
20
20
|
railDefaultHidden: false,
|
|
21
|
-
|
|
21
|
+
sessionFlow: true,
|
|
22
|
+
conversationStyle: 'native',
|
|
22
23
|
})
|
|
23
24
|
|
|
24
25
|
export const VISUAL_PRESETS = Object.freeze({
|
|
@@ -147,6 +148,7 @@ export function normalizePreferences(value) {
|
|
|
147
148
|
),
|
|
148
149
|
particlePattern: 'orthogonal',
|
|
149
150
|
railDefaultHidden: booleanOr(input.railDefaultHidden, DEFAULT_PREFERENCES.railDefaultHidden),
|
|
151
|
+
sessionFlow: booleanOr(input.sessionFlow, DEFAULT_PREFERENCES.sessionFlow),
|
|
150
152
|
conversationStyle: enumOr(input.conversationStyle, CONVERSATION_STYLES, DEFAULT_PREFERENCES.conversationStyle),
|
|
151
153
|
}
|
|
152
154
|
normalized.preset = matchedVisualPreset(normalized)
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
const HOVER_SETTLE = 180
|
|
2
|
+
const STATIC_TIME = 2.4
|
|
3
|
+
const POINT_COUNT = 9
|
|
4
|
+
|
|
5
|
+
function clamp(value, min, max) {
|
|
6
|
+
return Math.min(max, Math.max(min, value))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function number(value) {
|
|
10
|
+
return Number(value.toFixed(3))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function readGeometry(lifeline) {
|
|
14
|
+
const start = Number(lifeline?.getAttribute?.('data-prts-lifeline-start'))
|
|
15
|
+
const end = Number(lifeline?.getAttribute?.('data-prts-lifeline-end'))
|
|
16
|
+
const baseline = Number(lifeline?.getAttribute?.('data-prts-lifeline-baseline'))
|
|
17
|
+
const lift = Number(lifeline?.getAttribute?.('data-prts-lifeline-lift'))
|
|
18
|
+
if (![start, end, baseline, lift].every(Number.isFinite) || end <= start || lift <= 0) return null
|
|
19
|
+
return { start, end, baseline, lift }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function filamentNodes(lifeline) {
|
|
23
|
+
if (!lifeline) return []
|
|
24
|
+
if (lifeline.matches?.('[data-prts-session-lifeline-filament]')) return [lifeline]
|
|
25
|
+
return Array.from(lifeline.querySelectorAll?.('[data-prts-session-lifeline-filament]') ?? [])
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createSmoothPath(points) {
|
|
29
|
+
if (points.length < 2) return ''
|
|
30
|
+
const commands = [`M ${number(points[0].x)} ${number(points[0].y)}`]
|
|
31
|
+
for (let index = 0; index < points.length - 1; index += 1) {
|
|
32
|
+
const previous = points[Math.max(0, index - 1)]
|
|
33
|
+
const current = points[index]
|
|
34
|
+
const next = points[index + 1]
|
|
35
|
+
const following = points[Math.min(points.length - 1, index + 2)]
|
|
36
|
+
const firstControl = {
|
|
37
|
+
x: current.x + (next.x - previous.x) / 6,
|
|
38
|
+
y: current.y + (next.y - previous.y) / 6,
|
|
39
|
+
}
|
|
40
|
+
const secondControl = {
|
|
41
|
+
x: next.x - (following.x - current.x) / 6,
|
|
42
|
+
y: next.y - (following.y - current.y) / 6,
|
|
43
|
+
}
|
|
44
|
+
commands.push([
|
|
45
|
+
'C',
|
|
46
|
+
number(firstControl.x),
|
|
47
|
+
number(firstControl.y),
|
|
48
|
+
number(secondControl.x),
|
|
49
|
+
number(secondControl.y),
|
|
50
|
+
number(next.x),
|
|
51
|
+
number(next.y),
|
|
52
|
+
].join(' '))
|
|
53
|
+
}
|
|
54
|
+
return commands.join(' ')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sampleFlow(u, time, strand) {
|
|
58
|
+
const edgeEnvelope = Math.sin(Math.PI * u) ** .88
|
|
59
|
+
const slowTurn = Math.sin(time * .43 + u * Math.PI * 1.16 + Math.sin(time * .13) * .48)
|
|
60
|
+
const crossCurrent = Math.sin(time * -.29 + u * Math.PI * 2.42 + .7)
|
|
61
|
+
const fineCurrent = Math.sin(time * .19 + u * Math.PI * 3.76 - .9)
|
|
62
|
+
const sharedFlow = slowTurn * .52 + crossCurrent * .24 + fineCurrent * .08
|
|
63
|
+
|
|
64
|
+
const fan = Math.sin(Math.PI * u) ** 1.35
|
|
65
|
+
const separation = strand * (.15 + Math.sin(time * .31 + u * Math.PI * 1.48 + strand * .8) * .1)
|
|
66
|
+
const independentDrift = Math.sin(time * (.36 + strand * .021) + u * Math.PI * 2.18 + strand * 1.35) * .055
|
|
67
|
+
return edgeEnvelope * clamp(sharedFlow + fan * separation + independentDrift, -.9, .9)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createSessionLifelinePath(geometry, time = STATIC_TIME, strength = 1, strand = 0) {
|
|
71
|
+
const { start, end, baseline, lift } = geometry
|
|
72
|
+
const span = end - start
|
|
73
|
+
const tension = clamp(strength, 0, 1)
|
|
74
|
+
const points = []
|
|
75
|
+
for (let index = 0; index < POINT_COUNT; index += 1) {
|
|
76
|
+
const u = index / (POINT_COUNT - 1)
|
|
77
|
+
points.push({
|
|
78
|
+
x: start + span * u,
|
|
79
|
+
y: baseline - lift * sampleFlow(u, time, strand) * tension,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
return createSmoothPath(points)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function configureSessionLifeline(lifeline, { start, end, baseline, lift }) {
|
|
86
|
+
if (!lifeline) return false
|
|
87
|
+
const geometry = { start, end, baseline, lift }
|
|
88
|
+
lifeline.setAttribute('data-prts-lifeline-start', String(number(start)))
|
|
89
|
+
lifeline.setAttribute('data-prts-lifeline-end', String(number(end)))
|
|
90
|
+
lifeline.setAttribute('data-prts-lifeline-baseline', String(number(baseline)))
|
|
91
|
+
lifeline.setAttribute('data-prts-lifeline-lift', String(number(lift)))
|
|
92
|
+
filamentNodes(lifeline).forEach((filament, index, filaments) => {
|
|
93
|
+
const strand = filaments.length === 1 ? 0 : index - (filaments.length - 1) / 2
|
|
94
|
+
filament.setAttribute('d', createSessionLifelinePath(geometry, STATIC_TIME, 1, strand))
|
|
95
|
+
})
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function createSessionLifelineAnimator({ document, window }) {
|
|
100
|
+
const requestFrame = window?.requestAnimationFrame?.bind(window)
|
|
101
|
+
?? (callback => window?.setTimeout?.(() => callback(Date.now()), 1000 / 60))
|
|
102
|
+
const cancelFrame = window?.cancelAnimationFrame?.bind(window)
|
|
103
|
+
?? window?.clearTimeout?.bind(window)
|
|
104
|
+
const reducedMotion = window?.matchMedia?.('(prefers-reduced-motion: reduce)')
|
|
105
|
+
const MotionObserver = window?.MutationObserver
|
|
106
|
+
|
|
107
|
+
let frame
|
|
108
|
+
let lastTime
|
|
109
|
+
let flowTime = STATIC_TIME
|
|
110
|
+
let strength = 1
|
|
111
|
+
let row
|
|
112
|
+
let lifeline
|
|
113
|
+
let filaments = []
|
|
114
|
+
let hovered = false
|
|
115
|
+
let motionObserver
|
|
116
|
+
|
|
117
|
+
function render() {
|
|
118
|
+
const geometry = readGeometry(lifeline)
|
|
119
|
+
if (!geometry || filaments.length === 0) return false
|
|
120
|
+
filaments.forEach((filament, index) => {
|
|
121
|
+
const strand = filaments.length === 1 ? 0 : index - (filaments.length - 1) / 2
|
|
122
|
+
filament.setAttribute('d', createSessionLifelinePath(geometry, flowTime, strength, strand))
|
|
123
|
+
})
|
|
124
|
+
return true
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function canAnimate() {
|
|
128
|
+
return Boolean(
|
|
129
|
+
row?.isConnected
|
|
130
|
+
&& lifeline?.isConnected
|
|
131
|
+
&& row.getAttribute('aria-selected') === 'true'
|
|
132
|
+
&& document?.visibilityState !== 'hidden'
|
|
133
|
+
&& document?.documentElement?.dataset?.prtsSessionFlow !== 'off'
|
|
134
|
+
&& document?.documentElement?.dataset?.prtsMotion !== 'reduced'
|
|
135
|
+
&& !reducedMotion?.matches
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function cancel() {
|
|
140
|
+
if (frame !== undefined) cancelFrame?.(frame)
|
|
141
|
+
frame = undefined
|
|
142
|
+
lastTime = undefined
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function needsFrame() {
|
|
146
|
+
return canAnimate() && (!hovered || strength > 0)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function schedule() {
|
|
150
|
+
if (frame !== undefined || !needsFrame()) return
|
|
151
|
+
frame = requestFrame?.(tick)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function tick(time) {
|
|
155
|
+
frame = undefined
|
|
156
|
+
if (!canAnimate()) {
|
|
157
|
+
lastTime = undefined
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
const now = Number.isFinite(time) ? time : Date.now()
|
|
161
|
+
if (lastTime === undefined) lastTime = now
|
|
162
|
+
const elapsed = Math.min(100, Math.max(0, now - lastTime))
|
|
163
|
+
lastTime = now
|
|
164
|
+
flowTime += elapsed / 1000
|
|
165
|
+
const target = hovered ? 0 : 1
|
|
166
|
+
if (strength !== target) {
|
|
167
|
+
const step = elapsed / HOVER_SETTLE
|
|
168
|
+
strength = target > strength
|
|
169
|
+
? Math.min(target, strength + step)
|
|
170
|
+
: Math.max(target, strength - step)
|
|
171
|
+
}
|
|
172
|
+
render()
|
|
173
|
+
schedule()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function motionReduced() {
|
|
177
|
+
return document?.documentElement?.dataset?.prtsMotion === 'reduced' || reducedMotion?.matches
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function syncMotion() {
|
|
181
|
+
if (canAnimate()) {
|
|
182
|
+
schedule()
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
cancel()
|
|
186
|
+
if (lifeline && motionReduced()) {
|
|
187
|
+
flowTime = STATIC_TIME
|
|
188
|
+
strength = 1
|
|
189
|
+
render()
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function onPointerEnter() {
|
|
194
|
+
hovered = true
|
|
195
|
+
syncMotion()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function onPointerLeave() {
|
|
199
|
+
hovered = false
|
|
200
|
+
syncMotion()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function detachRow() {
|
|
204
|
+
row?.removeEventListener?.('pointerenter', onPointerEnter)
|
|
205
|
+
row?.removeEventListener?.('pointerleave', onPointerLeave)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function attachObservers() {
|
|
209
|
+
document?.addEventListener?.('visibilitychange', syncMotion)
|
|
210
|
+
reducedMotion?.addEventListener?.('change', syncMotion)
|
|
211
|
+
if (!motionObserver && typeof MotionObserver === 'function' && document?.documentElement) {
|
|
212
|
+
motionObserver = new MotionObserver(syncMotion)
|
|
213
|
+
motionObserver.observe(document.documentElement, {
|
|
214
|
+
attributes: true,
|
|
215
|
+
attributeFilter: ['data-prts-motion', 'data-prts-session-flow'],
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
start() {
|
|
222
|
+
attachObservers()
|
|
223
|
+
syncMotion()
|
|
224
|
+
},
|
|
225
|
+
setTarget(nextRow, nextLifeline) {
|
|
226
|
+
if (row === nextRow && lifeline === nextLifeline) {
|
|
227
|
+
syncMotion()
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
cancel()
|
|
231
|
+
detachRow()
|
|
232
|
+
row = nextRow
|
|
233
|
+
lifeline = nextLifeline
|
|
234
|
+
filaments = filamentNodes(lifeline)
|
|
235
|
+
hovered = Boolean(row?.matches?.(':hover'))
|
|
236
|
+
flowTime = STATIC_TIME
|
|
237
|
+
strength = hovered ? 0 : 1
|
|
238
|
+
row?.addEventListener?.('pointerenter', onPointerEnter)
|
|
239
|
+
row?.addEventListener?.('pointerleave', onPointerLeave)
|
|
240
|
+
render()
|
|
241
|
+
syncMotion()
|
|
242
|
+
},
|
|
243
|
+
dispose() {
|
|
244
|
+
cancel()
|
|
245
|
+
detachRow()
|
|
246
|
+
row = undefined
|
|
247
|
+
lifeline = undefined
|
|
248
|
+
filaments = []
|
|
249
|
+
motionObserver?.disconnect()
|
|
250
|
+
motionObserver = undefined
|
|
251
|
+
document?.removeEventListener?.('visibilitychange', syncMotion)
|
|
252
|
+
reducedMotion?.removeEventListener?.('change', syncMotion)
|
|
253
|
+
},
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ensureFacilityGraphic, ensureFacilityTextureDefs, renderFacilityGraphic } from './facility-vector.js'
|
|
2
|
+
import { createSessionLifelineAnimator } from './session-lifeline.js'
|
|
2
3
|
|
|
3
4
|
const OWNED_ATTRIBUTES = [
|
|
4
5
|
'data-prts-row-title',
|
|
@@ -128,6 +129,7 @@ export function createSidebarControlAdapter({ document, window }) {
|
|
|
128
129
|
let vectorOwners = new Set()
|
|
129
130
|
const dirtyVectors = new Map()
|
|
130
131
|
const vectorSizes = new WeakMap()
|
|
132
|
+
const lifelineAnimator = createSessionLifelineAnimator({ document, window })
|
|
131
133
|
|
|
132
134
|
function scheduleVectors() {
|
|
133
135
|
if (resizeFrame !== undefined) return
|
|
@@ -191,6 +193,8 @@ export function createSidebarControlAdapter({ document, window }) {
|
|
|
191
193
|
const root = document?.querySelector?.('[data-prts-region="sessions"] [data-slot="sidebar.workspaces"]')
|
|
192
194
|
const next = new Set()
|
|
193
195
|
const nextVectorOwners = new Set()
|
|
196
|
+
let activeLifelineRow
|
|
197
|
+
let activeLifeline
|
|
194
198
|
if (root) {
|
|
195
199
|
for (const projection of root.querySelectorAll('[data-prts-owned-projection]')) projection.remove()
|
|
196
200
|
for (const row of root.querySelectorAll('div[role="treeitem"][aria-expanded]')) {
|
|
@@ -222,6 +226,10 @@ export function createSidebarControlAdapter({ document, window }) {
|
|
|
222
226
|
const spine = ensureFacilitySpine(document, row, 'session', next)
|
|
223
227
|
registerVector(face, { kind: 'face', topNotch: true, texture: 'pickup' }, next, nextVectorOwners)
|
|
224
228
|
registerVector(spine, { kind: 'spine' }, next, nextVectorOwners)
|
|
229
|
+
if (!activeLifelineRow && row.getAttribute('aria-selected') === 'true') {
|
|
230
|
+
activeLifelineRow = row
|
|
231
|
+
activeLifeline = face.querySelector('[data-prts-session-lifeline]')
|
|
232
|
+
}
|
|
225
233
|
markSidebarNode(row, 'data-prts-session-row', next)
|
|
226
234
|
row.dataset.prtsSessionIndex = sessionIndex
|
|
227
235
|
const title = resolveRowTitle(row, 'session', controls.actions)
|
|
@@ -236,6 +244,7 @@ export function createSidebarControlAdapter({ document, window }) {
|
|
|
236
244
|
registerVector(controls.menu, { kind: 'button' }, next, nextVectorOwners)
|
|
237
245
|
}
|
|
238
246
|
}
|
|
247
|
+
lifelineAnimator.setTarget(activeLifelineRow, activeLifeline)
|
|
239
248
|
for (const node of owned) {
|
|
240
249
|
if (next.has(node)) continue
|
|
241
250
|
if (node.hasAttribute?.('data-prts-owned-projection')
|
|
@@ -252,6 +261,7 @@ export function createSidebarControlAdapter({ document, window }) {
|
|
|
252
261
|
}
|
|
253
262
|
|
|
254
263
|
function clear() {
|
|
264
|
+
lifelineAnimator.setTarget(undefined, undefined)
|
|
255
265
|
for (const node of document?.querySelectorAll?.('[data-prts-owned-projection], [data-prts-owned-facility], [data-prts-owned-facility-spine], [data-prts-owned-facility-vector], [data-prts-owned-facility-defs]') ?? []) node.remove()
|
|
256
266
|
for (const node of owned) {
|
|
257
267
|
for (const attribute of OWNED_ATTRIBUTES) node.removeAttribute?.(attribute)
|
|
@@ -267,6 +277,7 @@ export function createSidebarControlAdapter({ document, window }) {
|
|
|
267
277
|
return {
|
|
268
278
|
start() {
|
|
269
279
|
if (!document) return
|
|
280
|
+
lifelineAnimator.start()
|
|
270
281
|
ensureFacilityTextureDefs(document)
|
|
271
282
|
const ResizeObserver = window?.ResizeObserver
|
|
272
283
|
if (!resizeObserver && typeof ResizeObserver === 'function') {
|
|
@@ -311,6 +322,7 @@ export function createSidebarControlAdapter({ document, window }) {
|
|
|
311
322
|
resizeFrame = undefined
|
|
312
323
|
if (resizeTimer !== undefined) window?.clearTimeout?.(resizeTimer)
|
|
313
324
|
resizeTimer = undefined
|
|
325
|
+
lifelineAnimator.dispose()
|
|
314
326
|
clear()
|
|
315
327
|
},
|
|
316
328
|
}
|