diff --git a/web/src/components/Footer/Footer.tsx b/web/src/components/Footer/Footer.tsx index 52f93d955..fb1836f68 100644 --- a/web/src/components/Footer/Footer.tsx +++ b/web/src/components/Footer/Footer.tsx @@ -1,7 +1,7 @@ import React, { useState, useRef, useEffect } from 'react' import useOnClickOutside from 'use-onclickoutside' import { useRouteMatch } from 'react-router-dom' -import Zoom from '../Zoom/Zoom.connector' +import Zoom from '../Zoom/Zoom.connected' import './Footer.scss' import Icon from '../Icon/Icon' import Button from '../Button/Button' @@ -10,6 +10,7 @@ import MapViewingOptions from '../MapViewingOptions/MapViewingOptions' import { AgentPubKeyB64, CellIdString } from '../../types/shared' import { getAdminWs, getAppWs } from '../../hcWebsockets' import SyncingIndicator from '../SyncingIndicator/SyncingIndicator' +import ZoomDepth from '../Zoom/ZoomDepth.connected' export type FooterProps = { agentAddress: AgentPubKeyB64 @@ -164,6 +165,7 @@ const Footer: React.FC = ({ )} {/* Map Zooming */} {mapPage && } + {mapPage && } )} diff --git a/web/src/components/Zoom/Zoom.component.tsx b/web/src/components/Zoom/Zoom.component.tsx index ba96c0e87..a82d39b8c 100644 --- a/web/src/components/Zoom/Zoom.component.tsx +++ b/web/src/components/Zoom/Zoom.component.tsx @@ -2,61 +2,45 @@ import React from 'react' import './Zoom.scss' import Icon from '../Icon/Icon' -export type StateZoomProps = { - screensize: { width: number; height: number } - scale: number -} +// export type StateZoomProps = { +// screensize: { width: number; height: number } +// scale: number +// } -export type DispatchZoomProps = { - zoom: ( - zoom: number, - pageCoord: { x: number; y: number }, - instant?: boolean - ) => void -} +// export type DispatchZoomProps = { +// zoom: ( +// zoom: number, +// pageCoord: { x: number; y: number }, +// instant?: boolean +// ) => void +// } -export type ZoomProps = StateZoomProps & DispatchZoomProps +export type ZoomProps = { + onClickPlus: () => void + onClickMinus: () => void + value: string +} -class Zoom extends React.Component { - constructor(props: ZoomProps) { - super(props) - this.zoomIn = this.zoomIn.bind(this) - this.zoomOut = this.zoomOut.bind(this) - } - zoomIn() { - const zoomIntensity = 0.05 - const zoom = Math.exp(1 * zoomIntensity) - let { width, height } = this.props.screensize - const instant = true - this.props.zoom(zoom, { x: width / 2, y: height / 2 }, instant) - } - zoomOut() { - const zoomIntensity = 0.05 - const zoom = Math.exp(-1 * zoomIntensity) - let { width, height } = this.props.screensize - const instant = true - this.props.zoom(zoom, { x: width / 2, y: height / 2 }, instant) - } - render() { - return ( -
- - - {Math.round(this.props.scale * 100)}% -
- ) - } +const Zoom: React.FC = ({ onClickPlus, onClickMinus, value }) => { + return ( +
+ + + {value} +
+ ) } + export default Zoom diff --git a/web/src/components/Zoom/Zoom.connected.tsx b/web/src/components/Zoom/Zoom.connected.tsx new file mode 100644 index 000000000..181764d57 --- /dev/null +++ b/web/src/components/Zoom/Zoom.connected.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { changeScale } from '../../redux/ephemeral/viewport/actions' +import { RootState } from '../../redux/reducer' +import Zoom from './Zoom.component' + +const ConnectedZoom = () => { + const dispatch = useDispatch() + const scale = useSelector((state: RootState) => state.ui.viewport.scale) + const screensize = useSelector((state: RootState) => state.ui.screensize) + const value = `${Math.round(scale * 100)}%` + const zoom = ( + zoom: number, + pageCoord: { x: number; y: number }, + instant?: boolean + ) => { + return dispatch(changeScale(zoom, pageCoord, instant)) + } + const onClickPlus = () => { + const zoomIntensity = 0.05 + const newZoom = Math.exp(1 * zoomIntensity) + let { width, height } = screensize + const instant = true + zoom(newZoom, { x: width / 2, y: height / 2 }, instant) + } + const onClickMinus = () => { + const zoomIntensity = 0.05 + const newZoom = Math.exp(-1 * zoomIntensity) + let { width, height } = screensize + const instant = true + zoom(newZoom, { x: width / 2, y: height / 2 }, instant) + } + + return ( + + ) +} + +export default ConnectedZoom \ No newline at end of file diff --git a/web/src/components/Zoom/Zoom.connector.ts b/web/src/components/Zoom/Zoom.connector.ts deleted file mode 100644 index b5326c34b..000000000 --- a/web/src/components/Zoom/Zoom.connector.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { connect } from 'react-redux' -import { changeScale } from '../../redux/ephemeral/viewport/actions' -import { RootState } from '../../redux/reducer' -import Zoom, { DispatchZoomProps, StateZoomProps } from './Zoom.component' - -function mapStateToProps(state: RootState): StateZoomProps { - return { - screensize: state.ui.screensize, - scale: state.ui.viewport.scale, - } -} - -function mapDispatchToProps(dispatch: any): DispatchZoomProps { - return { - zoom: (zoom, pageCoord, instant) => { - return dispatch(changeScale(zoom, pageCoord, instant)) - }, - } -} - -export default connect(mapStateToProps, mapDispatchToProps)(Zoom) diff --git a/web/src/components/Zoom/ZoomDepth.connected.tsx b/web/src/components/Zoom/ZoomDepth.connected.tsx new file mode 100644 index 000000000..c49553a9c --- /dev/null +++ b/web/src/components/Zoom/ZoomDepth.connected.tsx @@ -0,0 +1,31 @@ +import React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { RootState } from '../../redux/reducer' +import Zoom from './Zoom.component' +import { changeDepthPerception } from '../../redux/ephemeral/depth-perception/actions' + +const ConnectedZoomDepth = () => { + const dispatch = useDispatch() + const depthPerception = useSelector( + (state: RootState) => state.ui.depthPerception.value + ) + const changeP = (depthPerception: number) => { + return dispatch(changeDepthPerception(depthPerception)) + } + const onClickPlus = () => { + changeP(depthPerception + 1) + } + const onClickMinus = () => { + changeP(depthPerception - 1) + } + + return ( + + ) +} + +export default ConnectedZoomDepth diff --git a/web/src/drawing/dimensions.ts b/web/src/drawing/dimensions.ts index 4f54a87e9..ef0f99f10 100644 --- a/web/src/drawing/dimensions.ts +++ b/web/src/drawing/dimensions.ts @@ -283,23 +283,48 @@ export function getOutcomeDimensions({ export function getOutcomeWidth({ outcome, + depthPerception = 0, zoomLevel, }: { outcome: ComputedOutcome - zoomLevel: number + depthPerception?: number + zoomLevel?: number }) { // outcome width = outcome statement width + ( 2 * width padding) - const defaultWidth = 520 // 520 = 392 + ( 2 * 64 ) - - if (outcome.computedScope === ComputedScope.Small) { - // 0.02 < zoomLevel < 2.5 - if (zoomLevel < 1) { - // 0.02 < zoomLevel < 1 - return defaultWidth * Math.min(zoomLevel * 1.4, 1) - } else return defaultWidth - } else { - return defaultWidth + // 520 = 392 + ( 2 * 64 ) + const defaultWidth = 520 + const depth = outcome.depth || 0 + // linear + const minWidth = 50 + let depthSubtraction = 0 + if (depthPerception === 1) { + if (depth === 1) { + depthSubtraction = 50 + } else if (depth === 2) { + depthSubtraction = 100 + } else if (depth === 3) { + depthSubtraction = 150 + } else if (depth > 3) { + depthSubtraction = 200 + } + } else if (depthPerception === 2) { + + } else if (depthPerception === 3) { + } + return Math.max(minWidth, defaultWidth - depthSubtraction) + // logarithmic + // return defaultWidth * (1 / depth + 1) + + // if (outcome.computedScope === ComputedScope.Small) { + // // 0.02 < zoomLevel < 2.5 + // if (zoomLevel < 1) { + // // 0.02 < zoomLevel < 1 + // return defaultWidth * Math.min(zoomLevel * 1.4, 1) + // } else return defaultWidth + // } else { + // return defaultWidth + // } } // height is a function of width diff --git a/web/src/drawing/layoutFormula.ts b/web/src/drawing/layoutFormula.ts index 4b06dca49..83fdd34e5 100644 --- a/web/src/drawing/layoutFormula.ts +++ b/web/src/drawing/layoutFormula.ts @@ -192,7 +192,8 @@ export default function layoutFormula( [outcomeActionHash: string]: boolean }, hiddenSmalls: boolean, - hiddenAchieved: boolean + hiddenAchieved: boolean, + depthPerception: number ): LayoutState { let coordinates = {} // just do this for efficiency, it's not going to @@ -207,7 +208,7 @@ export default function layoutFormula( Object.keys(graph.outcomes.computedOutcomesKeyed).forEach( (outcomeActionHash) => { const outcome = graph.outcomes.computedOutcomesKeyed[outcomeActionHash] - const width = getOutcomeWidth({ outcome, zoomLevel }) + const width = getOutcomeWidth({ outcome, zoomLevel, depthPerception }) const height = getOutcomeHeight({ ctx, outcome, diff --git a/web/src/event-listeners/index.ts b/web/src/event-listeners/index.ts index c7702480a..388cc7a88 100644 --- a/web/src/event-listeners/index.ts +++ b/web/src/event-listeners/index.ts @@ -78,6 +78,7 @@ import { setNavModalOpenChildren, setNavModalOpenParents, } from '../redux/ephemeral/navigation-modal/actions' +import { changeDepthPerception } from '../redux/ephemeral/depth-perception/actions' // The "modifier" key is different on Mac and non-Mac // Pattern borrowed from TinyKeys library. @@ -433,10 +434,11 @@ export default function setupEventListeners( } function canvasWheel(event: WheelEvent) { - const state = store.getState() + const state: RootState = store.getState() const { ui: { localPreferences: { navigation }, + depthPerception: { value: depthPerception } }, } = state if (!state.ui.outcomeForm.isOpen) { @@ -444,7 +446,8 @@ export default function setupEventListeners( store.dispatch(unsetContextMenu()) // from https://medium.com/@auchenberg/detecting-multi-touch-trackpad-gestures-in-javascript-a2505babb10e // and https://stackoverflow.com/questions/2916081/zoom-in-on-a-point-using-scale-and-translate - if (navigation === MOUSE || (navigation === TRACKPAD && event.ctrlKey)) { + if (!event.metaKey && (navigation === MOUSE || (navigation === TRACKPAD && event.ctrlKey))) { + // NORMAL VIEWPORT ZOOMING // Normalize wheel to +1 or -1. const wheel = event.deltaY < 0 ? 1 : -1 const zoomIntensity = 0.07 // 0.05 @@ -453,7 +456,13 @@ export default function setupEventListeners( const pageCoord = { x: event.clientX, y: event.clientY } const instant = true store.dispatch(changeScale(zoom, pageCoord, instant)) + } else if (event.metaKey) { + // Normalize wheel to +1 or -1. + const wheel = event.deltaY < 0 ? -1 : 1 + store.dispatch(changeDepthPerception(depthPerception + wheel)) + console.log('depthPerception', depthPerception + wheel) } else { + // NORMAL PANNING // invert the pattern so that it uses new mac style // of panning store.dispatch(changeTranslate(-1 * event.deltaX, -1 * event.deltaY)) diff --git a/web/src/redux/ephemeral/animations/layout.ts b/web/src/redux/ephemeral/animations/layout.ts index 491074cbc..b40695753 100644 --- a/web/src/redux/ephemeral/animations/layout.ts +++ b/web/src/redux/ephemeral/animations/layout.ts @@ -58,6 +58,7 @@ export default function performLayoutAnimation( 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 @@ -81,7 +82,8 @@ export default function performLayoutAnimation( projectTags, collapsedOutcomes, hiddenSmalls, - hiddenAchieved + hiddenAchieved, + depthPerception ) // in terms of 'fixing' on a given outcome diff --git a/web/src/redux/ephemeral/animations/pan-and-zoom.ts b/web/src/redux/ephemeral/animations/pan-and-zoom.ts index 4a0a06766..c8e55b980 100644 --- a/web/src/redux/ephemeral/animations/pan-and-zoom.ts +++ b/web/src/redux/ephemeral/animations/pan-and-zoom.ts @@ -43,6 +43,7 @@ export default function panZoomToFrame( 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 = @@ -60,7 +61,8 @@ export default function panZoomToFrame( projectTags, collapsedOutcomes, hiddenSmalls, - hiddenAchieved + hiddenAchieved, + depthPerception, ) // this accounts for a special case where the caller doesn't @@ -100,6 +102,7 @@ export default function panZoomToFrame( const outcomeWidth = getOutcomeWidth({ outcome, zoomLevel, // use the target scale + depthPerception, }) const outcomeHeight = getOutcomeHeight({ outcome, diff --git a/web/src/redux/ephemeral/depth-perception/actions.ts b/web/src/redux/ephemeral/depth-perception/actions.ts new file mode 100644 index 000000000..18f66e177 --- /dev/null +++ b/web/src/redux/ephemeral/depth-perception/actions.ts @@ -0,0 +1,23 @@ +/* + There should be an actions.js file in every + feature folder, and it should start with a list + of constants defining all the types of actions + that can be taken within that feature. +*/ + +/* constants */ +const CHANGE_DEPTH_PERCEPTION = 'CHANGE_DEPTH_PERCEPTION' + +/* action creator functions */ + +function changeDepthPerception(value: number) { + return { + type: CHANGE_DEPTH_PERCEPTION, + payload: value + } +} + +export { + CHANGE_DEPTH_PERCEPTION, + changeDepthPerception +} diff --git a/web/src/redux/ephemeral/depth-perception/reducer.ts b/web/src/redux/ephemeral/depth-perception/reducer.ts new file mode 100644 index 000000000..d032c8328 --- /dev/null +++ b/web/src/redux/ephemeral/depth-perception/reducer.ts @@ -0,0 +1,22 @@ +import { CHANGE_DEPTH_PERCEPTION } from './actions' +import { DepthPerceptionState } from './state-type' + +const defaultState: DepthPerceptionState = { + value: 1, +} + +export default function ( + state = defaultState, + action: any +): DepthPerceptionState { + const { payload, type } = action + switch (type) { + case CHANGE_DEPTH_PERCEPTION: + return { + ...state, + value: payload >= 0 ? payload : 0, // can't be less than 0 + } + default: + return state + } +} diff --git a/web/src/redux/ephemeral/depth-perception/state-type.ts b/web/src/redux/ephemeral/depth-perception/state-type.ts new file mode 100644 index 000000000..8cd9f3d94 --- /dev/null +++ b/web/src/redux/ephemeral/depth-perception/state-type.ts @@ -0,0 +1,5 @@ +interface DepthPerceptionState { + value: number +} + +export { DepthPerceptionState } diff --git a/web/src/redux/ephemeral/layout/middleware.ts b/web/src/redux/ephemeral/layout/middleware.ts index 6f406df9c..01fca26d7 100644 --- a/web/src/redux/ephemeral/layout/middleware.ts +++ b/web/src/redux/ephemeral/layout/middleware.ts @@ -34,6 +34,7 @@ import { FETCH_PROJECT_METAS, UPDATE_PROJECT_META, } from '../../persistent/projects/project-meta/actions' +import { CHANGE_DEPTH_PERCEPTION } from '../depth-perception/actions' const isOneOfLayoutAffectingActions = (action: { type: string @@ -71,7 +72,8 @@ const isOneOfLayoutAffectingActions = (action: { type === HIDE_SMALL_OUTCOMES || type === SHOW_SMALL_OUTCOMES || type === FETCH_PROJECT_METAS || - type === UPDATE_PROJECT_META + type === UPDATE_PROJECT_META || + type === CHANGE_DEPTH_PERCEPTION ) } diff --git a/web/src/redux/persistent/projects/outcomes/outcomesAsGraph.ts b/web/src/redux/persistent/projects/outcomes/outcomesAsGraph.ts index e972623e7..67f3e24fa 100644 --- a/web/src/redux/persistent/projects/outcomes/outcomesAsGraph.ts +++ b/web/src/redux/persistent/projects/outcomes/outcomesAsGraph.ts @@ -80,7 +80,7 @@ export default function outcomesAsGraph( const computedOutcomesKeyed: ProjectComputedOutcomes['computedOutcomesKeyed'] = {} // recursively calls itself // so that it constructs the full sub-tree for each root Outcome - function getOutcome(outcomeActionHash: ActionHashB64): ComputedOutcome { + const getOutcome = (depth: number) => (outcomeActionHash: ActionHashB64) => { const self = allOutcomes[outcomeActionHash] if (!self) { // defensive coding, during loading @@ -90,7 +90,7 @@ export default function outcomesAsGraph( // find the connections indicating the children of this outcome .filter((connection) => connection.parentActionHash === outcomeActionHash) // actually nest the children Outcomes, recurse - .map((connection) => getOutcome(connection.childActionHash)) + .map((connection) => getOutcome(depth + 1)(connection.childActionHash)) .filter((maybeOutcome) => !!maybeOutcome) const computedOutcome = { @@ -98,6 +98,7 @@ export default function outcomesAsGraph( computedAchievementStatus: computeAchievementStatus(self, children), computedScope: computeScope(self, children), children, + depth, } // add it to our 'tracking' object as well // which will be used for quicker access to specific @@ -106,7 +107,7 @@ export default function outcomesAsGraph( return computedOutcome } // start with the root Outcomes, and recurse down to their children - const computedOutcomesAsTree = noParentsAddresses.map(getOutcome) + const computedOutcomesAsTree = noParentsAddresses.map(getOutcome(0)) return { outcomes: { diff --git a/web/src/redux/reducer.ts b/web/src/redux/reducer.ts index 69ba608b6..93c4d3b51 100644 --- a/web/src/redux/reducer.ts +++ b/web/src/redux/reducer.ts @@ -24,6 +24,7 @@ import layout from './ephemeral/layout/reducer' import mouse from './ephemeral/mouse/reducer' import screensize from './ephemeral/screensize/reducer' import viewport from './ephemeral/viewport/reducer' +import depthPerception from './ephemeral/depth-perception/reducer' import expandedView from './ephemeral/expanded-view/reducer' import outcomeClone from './ephemeral/outcome-clone/reducer' import activeProject from './ephemeral/active-project/reducer' @@ -56,6 +57,7 @@ const rootReducer = combineReducers({ keyboard, screensize, viewport, + depthPerception, mouse, expandedView, outcomeClone, diff --git a/web/src/routes/App.component.tsx b/web/src/routes/App.component.tsx index 4dff9f5ec..181c602db 100644 --- a/web/src/routes/App.component.tsx +++ b/web/src/routes/App.component.tsx @@ -135,9 +135,9 @@ const App: React.FC = ({ }, [fileDownloaded, setFileDownloaded]) useEffect(() => { - if (updateVersionInfo) { - setShowUpdateModal(true) - } + // if (updateVersionInfo) { + // setShowUpdateModal(true) + // } }, [JSON.stringify(updateVersionInfo)]) const onProfileSubmit = async (profile: Profile) => { diff --git a/web/src/types/outcome.ts b/web/src/types/outcome.ts index d67192207..cd96b0fd0 100644 --- a/web/src/types/outcome.ts +++ b/web/src/types/outcome.ts @@ -78,6 +78,7 @@ export type OptionalOutcomeData = { votes?: OutcomeVote[] // for representing this data in a nested tree structure children?: ComputedOutcome[] + depth?: number } // These are the things which are computed and stored within ProjectView