-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathpan-and-zoom.ts
154 lines (139 loc) · 5.19 KB
/
pan-and-zoom.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
import TWEEN from '@tweenjs/tween.js'
import { getOutcomeHeight, getOutcomeWidth } from '../../../drawing/dimensions'
import layoutFormula from '../../../drawing/layoutFormula'
import { ActionHashB64 } from '../../../types/shared'
import { RootState } from '../../reducer'
import { updateLayout } from '../layout/actions'
import { selectOutcome, unselectAll } from '../selection/actions'
import { changeAllDirect } from '../viewport/actions'
import { getGraphForState } from './getGraphForState'
/*
In this function as we animate the "pan and zoom", or "translate and scale"
**we take responsibility** not only for those values, but for the Outcome layout
which would typically change according to a changing zoomLevel as well
*/
export default function panZoomToFrame(
store: any,
action: {
payload: {
outcomeActionHash: ActionHashB64
adjustScale: boolean
instant?: boolean
}
}
) {
let { outcomeActionHash, adjustScale } = action.payload
const state: RootState = store.getState()
// Destination viewport
// is to center the outcome
// we can also adjust the scale back to a default value, or not,
// depending on the 'action.adjustScale' value
// 0.7 is a good choice here because the text should be visible
const defaultScaleForPanAndZoom = 0.7
const zoomLevel = adjustScale
? defaultScaleForPanAndZoom
: state.ui.viewport.scale
const { activeProject } = state.ui
const graph = getGraphForState(state)
const projectTags = Object.values(state.projects.tags[activeProject] || {})
const hiddenSmallOutcomes = state.ui.mapViewSettings.hiddenSmallOutcomes
const hiddenAchievedOutcomes = state.ui.mapViewSettings.hiddenAchievedOutcomes
const depthPerception = state.ui.depthPerception.value
const hiddenSmalls = hiddenSmallOutcomes.includes(activeProject)
const hiddenAchieved = hiddenAchievedOutcomes.includes(activeProject)
const layeringAlgorithm =
state.projects.projectMeta[activeProject].layeringAlgorithm
const collapsedOutcomes =
state.ui.collapsedOutcomes.collapsedOutcomes[activeProject] || {}
// this is our final destination layout
// that we'll be animating to
// use the target zoomLevel
const newLayout = layoutFormula(
graph,
layeringAlgorithm,
zoomLevel,
projectTags,
collapsedOutcomes,
hiddenSmalls,
hiddenAchieved,
depthPerception,
)
// this accounts for a special case where the caller doesn't
// provide the intended Outcome ActionHash, but instead expects this
// function to pick the best one
if (!outcomeActionHash) {
// pick the first parent to center on
outcomeActionHash = (graph.outcomes.computedOutcomesAsTree[0] || {})
.actionHash
}
const outcome = graph.outcomes.computedOutcomesKeyed[outcomeActionHash]
// important, for the outcomeCoordinates we should
// definitely choose them from the new intended layout,
// not the existing one
const outcomeCoordinates = newLayout.coordinates[outcomeActionHash]
if (!outcomeCoordinates) {
console.log('could not find coordinates for outcome to animate to')
return
}
// since we will be transitioning the viewport, which changes the zoom
// we should start out by updating the layout to the layout it would be
// at the destination zoomLevel
store.dispatch(updateLayout(newLayout))
// we should also deselect all other Outcomes, and select the one
// we are panning and zooming to
store.dispatch(unselectAll())
store.dispatch(selectOutcome(outcomeActionHash))
const { width, height } = state.ui.screensize
const dpr = window.devicePixelRatio || 1
const halfScreenWidth = width / (2 * dpr)
const halfScreenHeight = height / (2 * dpr)
const outcomeWidth = getOutcomeWidth({
outcome,
zoomLevel, // use the target scale
depthPerception,
})
const outcomeHeight = getOutcomeHeight({
outcome,
projectTags,
zoomLevel, // use the target scale
width: outcomeWidth,
useLineLimit: true,
})
const newViewport = {
scale: zoomLevel,
translate: {
x:
-1 * (outcomeCoordinates.x * zoomLevel) +
halfScreenWidth -
(outcomeWidth / 2) * zoomLevel,
y:
-1 * (outcomeCoordinates.y * zoomLevel) +
halfScreenHeight -
(outcomeHeight / 2) * 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) {
store.dispatch(changeAllDirect(newViewport))
return
}
// not instant, so continue and run an animated transition
const currentViewportTween = {
...state.ui.viewport,
}
new TWEEN.Tween(currentViewportTween)
.to(newViewport)
// use this easing, adjust me to tune, see TWEEN.Easing for options
.easing(TWEEN.Easing.Quadratic.InOut)
.duration(1000) // last 1000 milliseconds, adjust me to tune
.start()
// updatedViewport is the transitionary state between currentViewportTween and newViewport
.onUpdate((updated) => {
// dispatch an update to the viewport
// which will trigger a repaint on the canvas
// every time the animation loop fires an update
store.dispatch(changeAllDirect(updated))
})
}