-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathlayout.ts
226 lines (216 loc) · 8.23 KB
/
layout.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
import TWEEN from '@tweenjs/tween.js'
import { CREATE_OUTCOME_WITH_CONNECTION } from '../../persistent/projects/outcomes/actions'
import { updateLayout } from '../layout/actions'
import layoutFormula from '../../../drawing/layoutFormula'
import { RootState } from '../../reducer'
import { LAYOUT_ANIMATION_DURATION_MS } from '../../../constants'
import { ComputedOutcome, LayeringAlgorithm } from '../../../types'
import { getGraphForState } from './getGraphForState'
import { coordsCanvasToPage } from '../../../drawing/coordinateSystems'
import { ActionHashB64 } from '../../../types/shared'
import { CoordinatesState, DimensionsState } from '../layout/state-type'
import { getOutcomeHeight, getOutcomeWidth } from '../../../drawing/dimensions'
import { getPlaceholderOutcome } from '../../../drawing/drawOutcome/placeholderOutcome'
function calcDestTranslate(
outcomeActionHash: ActionHashB64,
outcomeCoordinates: {
[x: string]: {
x: number
y: number
}
},
zoomLevel: number,
additionalOffset = { x: 0, y: 0 }
) {
let translateForFixedPositionOutcome: { x: number; y: number }
if (outcomeActionHash && outcomeCoordinates[outcomeActionHash]) {
let pageCoords = coordsCanvasToPage(
outcomeCoordinates[outcomeActionHash],
{ x: 0, y: 0 },
zoomLevel
)
translateForFixedPositionOutcome = {
// top left of the screen, offset by the original offset
x: -1 * pageCoords.x + additionalOffset.x,
y: -1 * pageCoords.y + additionalOffset.y,
}
}
return translateForFixedPositionOutcome
}
// By default, this function performs an animated transition asynchronously
// between the old state and the new state, in terms of layout.
// However, if you pass `instant: true` it will perform it synchronously and
// instantly without transition
export default function performLayoutAnimation(
store: any,
action: {
type: string
payload?: { outcome?: ComputedOutcome; instant?: boolean }
},
currentState: RootState
) {
// called nextState because by now the
// initial action has been integrated
const nextState: RootState = store.getState()
const graph = getGraphForState(nextState)
const zoomLevel = nextState.ui.viewport.scale
const translate = nextState.ui.viewport.translate
const depthPerception = nextState.ui.depthPerception.value
const projectId = nextState.ui.activeProject
const closestOutcome = nextState.ui.mouse.closestOutcome
const hiddenSmallOutcomes = nextState.ui.mapViewSettings.hiddenSmallOutcomes
const hiddenAchievedOutcomes =
nextState.ui.mapViewSettings.hiddenAchievedOutcomes
const projectTags = Object.values(nextState.projects.tags[projectId] || {})
const collapsedOutcomes =
nextState.ui.collapsedOutcomes.collapsedOutcomes[projectId] || {}
const hiddenSmalls = hiddenSmallOutcomes.includes(projectId)
const hiddenAchieved = hiddenAchievedOutcomes.includes(projectId)
const projectMeta = nextState.projects.projectMeta
const layeringAlgorithm =
projectMeta[projectId]?.layeringAlgorithm || LayeringAlgorithm.LongestPath
// this is our final destination layout
// that we'll be animating to
const newLayout = layoutFormula(
graph,
layeringAlgorithm,
zoomLevel,
projectTags,
collapsedOutcomes,
hiddenSmalls,
hiddenAchieved,
depthPerception
)
// in terms of 'fixing' on a given outcome
// get the 'starting position' for that Outcome onscreen
let originalOutcomeFixedPosition: { x: number; y: number }
if (closestOutcome && currentState.ui.layout.coordinates[closestOutcome]) {
originalOutcomeFixedPosition = coordsCanvasToPage(
currentState.ui.layout.coordinates[closestOutcome],
translate,
zoomLevel
)
}
// just instantly update to the new layout without
// animating / transitioning between the current one and the new one
// if instructed to
if (typeof action.payload === 'object' && action.payload.instant) {
// figure out what the new Translate value should be
// to keep that closest Outcome in a fixed position on the screen
let translateForFixedPositionOutcome = originalOutcomeFixedPosition
? calcDestTranslate(
closestOutcome,
newLayout.coordinates,
zoomLevel,
originalOutcomeFixedPosition
)
: undefined
store.dispatch(updateLayout(newLayout, translateForFixedPositionOutcome))
return
}
// not instant, so continue and run an animated transition
// if creating an Outcome, we also want to animate
// from the position wherever the user was creating it
// to its new resting place in the new layout
let outcomeCreatedCoord: CoordinatesState = {}
let outcomeCreatedDimensions: DimensionsState = {}
if (action.type === CREATE_OUTCOME_WITH_CONNECTION) {
// at this point we have the actionHash of the new Outcome
// and we also have the coordinates where the "Outcome Form"
// was open and being used
outcomeCreatedCoord[action.payload.outcome.actionHash] = {
x: currentState.ui.outcomeForm.leftConnectionXPosition,
y: currentState.ui.outcomeForm.topConnectionYPosition,
}
const placeholderOutcomeWithText = getPlaceholderOutcome(
action.payload.outcome.content
)
const newOutcomeWidth = getOutcomeWidth({
outcome: placeholderOutcomeWithText,
zoomLevel,
})
const newOutcomeHeight = getOutcomeHeight({
outcome: placeholderOutcomeWithText,
projectTags,
width: newOutcomeWidth,
zoomLevel,
// we set this because in the case of creating a new outcome
// it should use the full text at the proper text scaling
noStatementPlaceholder: true,
useLineLimit: false,
})
outcomeCreatedDimensions[action.payload.outcome.actionHash] = {
width: newOutcomeWidth,
height: newOutcomeHeight,
}
}
// this is expanding coordinates for Outcomes
// where the key is their actionHash
// and the value is an object with `x` and `y` values
// (tween is going to directly modify this object)
const currentLayoutTween = {
dimensions: {
...newLayout.dimensions,
...outcomeCreatedDimensions,
},
coordinates: {
// do this to add any new ones
// that will just start out in their final position
...newLayout.coordinates,
// do this to override the coordinates of the newly
// created Outcome when handling a create action
// and make its original position equal to the position
// of the Outcome Form when it was open
...outcomeCreatedCoord,
},
}
// we do NOT want to keep original layouts for Outcomes
// that are no longer in the project, so those are automatically
// ignored during this loop
for (const outcomeActionHash in newLayout) {
// do this to override any new ones with existing ones
// to begin with
if (currentState.ui.layout.coordinates[outcomeActionHash]) {
currentLayoutTween.coordinates[outcomeActionHash] = {
...currentState.ui.layout.coordinates[outcomeActionHash],
}
}
if (currentState.ui.layout.dimensions[outcomeActionHash]) {
currentLayoutTween.dimensions[outcomeActionHash] = {
...currentState.ui.layout.dimensions[outcomeActionHash],
}
}
}
// transition currentLayoutTween object
// into newLayout object
new TWEEN.Tween(currentLayoutTween)
.to(newLayout)
// use this easing, adjust me to tune, see TWEEN.Easing for options
.easing(TWEEN.Easing.Quadratic.InOut)
.duration(LAYOUT_ANIMATION_DURATION_MS)
.start()
// updatedLayout is the transitionary state between currentLayoutTween and newLayout
.onUpdate((updatedLayout) => {
// figure out what the new Translate value should be
// to keep that closest Outcome in a fixed position on the screen
let translateForFixedPositionOutcome = originalOutcomeFixedPosition
? calcDestTranslate(
closestOutcome,
updatedLayout.coordinates,
zoomLevel,
originalOutcomeFixedPosition
)
: undefined
// dispatch an update to the layout
// which will trigger a repaint on the canvas
// every time the animation loop fires an update
store.dispatch(
updateLayout(
{
...updatedLayout,
},
translateForFixedPositionOutcome
)
)
})
}