mirror of
https://github.com/weaveworks/scope.git
synced 2026-09-06 10:17:19 +00:00
Merge pull request #2812 from weaveworks/unified-zooming-sensitivity
Unified zooming sensitivity
This commit is contained in:
@@ -3,12 +3,15 @@ import { connect } from 'react-redux';
|
||||
|
||||
import NodesChartElements from './nodes-chart-elements';
|
||||
import ZoomableCanvas from '../components/zoomable-canvas';
|
||||
import { transformToString } from '../utils/transform-utils';
|
||||
import { clickBackground } from '../actions/app-actions';
|
||||
import {
|
||||
graphZoomLimitsSelector,
|
||||
graphLimitsSelector,
|
||||
graphZoomStateSelector,
|
||||
} from '../selectors/graph-view/zoom';
|
||||
|
||||
import { CONTENT_INCLUDED } from '../constants/naming';
|
||||
|
||||
|
||||
const EdgeMarkerDefinition = ({ selectedNodeId }) => {
|
||||
const markerOffset = selectedNodeId ? '35' : '40';
|
||||
@@ -43,17 +46,25 @@ class NodesChart extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
renderContent(transform) {
|
||||
return (
|
||||
<g transform={transformToString(transform)}>
|
||||
<EdgeMarkerDefinition selectedNodeId={this.props.selectedNodeId} />
|
||||
<NodesChartElements />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { selectedNodeId } = this.props;
|
||||
return (
|
||||
<div className="nodes-chart">
|
||||
<ZoomableCanvas
|
||||
onClick={this.handleMouseClick}
|
||||
zoomLimitsSelector={graphZoomLimitsSelector}
|
||||
boundContent={CONTENT_INCLUDED}
|
||||
limitsSelector={graphLimitsSelector}
|
||||
zoomStateSelector={graphZoomStateSelector}
|
||||
disabled={selectedNodeId}>
|
||||
<EdgeMarkerDefinition selectedNodeId={selectedNodeId} />
|
||||
<NodesChartElements />
|
||||
disabled={this.props.selectedNodeId}>
|
||||
{transform => this.renderContent(transform)}
|
||||
</ZoomableCanvas>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,11 +5,13 @@ import ZoomableCanvas from './zoomable-canvas';
|
||||
import NodesResourcesLayer from './nodes-resources/node-resources-layer';
|
||||
import { layersTopologyIdsSelector } from '../selectors/resource-view/layout';
|
||||
import {
|
||||
resourcesZoomLimitsSelector,
|
||||
resourcesLimitsSelector,
|
||||
resourcesZoomStateSelector,
|
||||
} from '../selectors/resource-view/zoom';
|
||||
import { clickBackground } from '../actions/app-actions';
|
||||
|
||||
import { CONTENT_COVERING } from '../constants/naming';
|
||||
|
||||
|
||||
class NodesResources extends React.Component {
|
||||
constructor(props, context) {
|
||||
@@ -40,8 +42,8 @@ class NodesResources extends React.Component {
|
||||
<div className="nodes-resources">
|
||||
<ZoomableCanvas
|
||||
onClick={this.handleMouseClick}
|
||||
bounded forwardTransform fixVertical
|
||||
zoomLimitsSelector={resourcesZoomLimitsSelector}
|
||||
fixVertical boundContent={CONTENT_COVERING}
|
||||
limitsSelector={resourcesLimitsSelector}
|
||||
zoomStateSelector={resourcesZoomStateSelector}>
|
||||
{transform => this.renderLayers(transform)}
|
||||
</ZoomableCanvas>
|
||||
|
||||
@@ -168,7 +168,7 @@ class TimeTravelTimeline extends React.Component {
|
||||
}
|
||||
|
||||
handleZoom(ev) {
|
||||
let durationPerPixel = scaleDuration(this.state.durationPerPixel, zoomFactor(ev));
|
||||
let durationPerPixel = scaleDuration(this.state.durationPerPixel, 1 / zoomFactor(ev));
|
||||
if (durationPerPixel > MAX_DURATION_PER_PX) durationPerPixel = MAX_DURATION_PER_PX;
|
||||
if (durationPerPixel < MIN_DURATION_PER_PX) durationPerPixel = MIN_DURATION_PER_PX;
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import { debounce, pick } from 'lodash';
|
||||
import { clamp, debounce, pick } from 'lodash';
|
||||
import { fromJS } from 'immutable';
|
||||
|
||||
import { drag } from 'd3-drag';
|
||||
import { event as d3Event, select } from 'd3-selection';
|
||||
import { zoom, zoomIdentity } from 'd3-zoom';
|
||||
|
||||
import Logo from '../components/logo';
|
||||
import ZoomControl from '../components/zoom-control';
|
||||
import { cacheZoomState } from '../actions/app-actions';
|
||||
import { transformToString } from '../utils/transform-utils';
|
||||
import { zoomFactor } from '../utils/zoom-utils';
|
||||
import { applyTransform, inverseTransform } from '../utils/transform-utils';
|
||||
import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/zooming';
|
||||
import {
|
||||
canvasMarginsSelector,
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
} from '../selectors/canvas';
|
||||
|
||||
import { ZOOM_CACHE_DEBOUNCE_INTERVAL } from '../constants/timer';
|
||||
import { CONTENT_INCLUDED, CONTENT_COVERING } from '../constants/naming';
|
||||
|
||||
|
||||
class ZoomableCanvas extends React.Component {
|
||||
@@ -25,10 +28,11 @@ class ZoomableCanvas extends React.Component {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
minTranslateX: 0,
|
||||
maxTranslateX: 0,
|
||||
minTranslateY: 0,
|
||||
maxTranslateY: 0,
|
||||
isPanning: false,
|
||||
contentMinX: 0,
|
||||
contentMaxX: 0,
|
||||
contentMinY: 0,
|
||||
contentMaxY: 0,
|
||||
translateX: 0,
|
||||
translateY: 0,
|
||||
minScale: 1,
|
||||
@@ -40,27 +44,33 @@ class ZoomableCanvas extends React.Component {
|
||||
this.debouncedCacheZoom = debounce(this.cacheZoom.bind(this), ZOOM_CACHE_DEBOUNCE_INTERVAL);
|
||||
this.handleZoomControlAction = this.handleZoomControlAction.bind(this);
|
||||
this.canChangeZoom = this.canChangeZoom.bind(this);
|
||||
this.zoomed = this.zoomed.bind(this);
|
||||
|
||||
this.handleZoom = this.handleZoom.bind(this);
|
||||
this.handlePanStart = this.handlePanStart.bind(this);
|
||||
this.handlePanEnd = this.handlePanEnd.bind(this);
|
||||
this.handlePan = this.handlePan.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.svg = select('.zoomable-canvas svg');
|
||||
this.drag = drag()
|
||||
.on('start', this.handlePanStart)
|
||||
.on('end', this.handlePanEnd)
|
||||
.on('drag', this.handlePan);
|
||||
this.svg.call(this.drag);
|
||||
|
||||
this.zoomRestored = false;
|
||||
this.zoom = zoom().on('zoom', this.zoomed);
|
||||
this.svg = select('svg#canvas');
|
||||
|
||||
this.setZoomTriggers(!this.props.disabled);
|
||||
this.updateZoomLimits(this.props);
|
||||
this.restoreZoomState(this.props);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.setZoomTriggers(false);
|
||||
this.debouncedCacheZoom.cancel();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const layoutChanged = nextProps.layoutId !== this.props.layoutId;
|
||||
const disabledChanged = nextProps.disabled !== this.props.disabled;
|
||||
|
||||
// If the layout has changed (either active topology or its options) or
|
||||
// relayouting has been requested, stop pending zoom caching event and
|
||||
@@ -70,11 +80,6 @@ class ZoomableCanvas extends React.Component {
|
||||
this.zoomRestored = false;
|
||||
}
|
||||
|
||||
// If the zooming has been enabled/disabled, update its triggers.
|
||||
if (disabledChanged) {
|
||||
this.setZoomTriggers(!nextProps.disabled);
|
||||
}
|
||||
|
||||
this.updateZoomLimits(nextProps);
|
||||
if (!this.zoomRestored) {
|
||||
this.restoreZoomState(nextProps);
|
||||
@@ -82,31 +87,25 @@ class ZoomableCanvas extends React.Component {
|
||||
}
|
||||
|
||||
handleZoomControlAction(scale) {
|
||||
// Update the canvas scale (not touching the translation).
|
||||
this.svg.call(this.zoom.scaleTo, scale);
|
||||
|
||||
// Update the scale state and propagate to the global cache.
|
||||
this.setState(this.cachableState({
|
||||
scaleX: scale,
|
||||
scaleY: scale,
|
||||
}));
|
||||
this.debouncedCacheZoom();
|
||||
// Get the center of the SVG and zoom around it.
|
||||
const { top, bottom, left, right } = this.svg.node().getBoundingClientRect();
|
||||
const centerOfCanvas = {
|
||||
x: (left + right) / 2,
|
||||
y: (top + bottom) / 2,
|
||||
};
|
||||
// Zoom factor diff is obtained by dividing the new zoom scale with the old one.
|
||||
this.zoomAtPositionByFactor(centerOfCanvas, scale / this.state.scaleX);
|
||||
}
|
||||
|
||||
render() {
|
||||
// `forwardTransform` says whether the zoom transform is forwarded to the child
|
||||
// component. The advantage of that is more control rendering control in the
|
||||
// children, while the disadvantage is that it's slower, as all the children
|
||||
// get updated on every zoom/pan action.
|
||||
const { children, forwardTransform } = this.props;
|
||||
const transform = forwardTransform ? '' : transformToString(this.state);
|
||||
const className = classNames({ panning: this.state.isPanning });
|
||||
|
||||
return (
|
||||
<div className="zoomable-canvas">
|
||||
<svg id="canvas" width="100%" height="100%" onClick={this.props.onClick}>
|
||||
<svg className={className} onClick={this.props.onClick} onWheel={this.handleZoom}>
|
||||
<Logo transform="translate(24,24) scale(0.25)" />
|
||||
<g className="zoom-content" transform={transform}>
|
||||
{forwardTransform ? children(this.state) : children}
|
||||
<g className="zoom-content">
|
||||
{this.props.children(this.state)}
|
||||
</g>
|
||||
</svg>
|
||||
{this.canChangeZoom() && <ZoomControl
|
||||
@@ -119,16 +118,6 @@ class ZoomableCanvas extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
setZoomTriggers(zoomingEnabled) {
|
||||
if (zoomingEnabled) {
|
||||
// use d3-zoom defaults but exclude double clicks
|
||||
this.svg.call(this.zoom)
|
||||
.on('dblclick.zoom', null);
|
||||
} else {
|
||||
this.svg.on('.zoom', null);
|
||||
}
|
||||
}
|
||||
|
||||
// Decides which part of the zoom state is cachable depending
|
||||
// on the horizontal/vertical degrees of freedom.
|
||||
cachableState(state = this.state) {
|
||||
@@ -144,28 +133,7 @@ class ZoomableCanvas extends React.Component {
|
||||
}
|
||||
|
||||
updateZoomLimits(props) {
|
||||
const zoomLimits = props.layoutZoomLimits.toJS();
|
||||
|
||||
this.zoom = this.zoom.scaleExtent([zoomLimits.minScale, zoomLimits.maxScale]);
|
||||
|
||||
if (props.bounded) {
|
||||
this.zoom = this.zoom
|
||||
// Translation limits are only set if explicitly demanded (currently we are using them
|
||||
// in the resource view, but not in the graph view, although I think the idea would be
|
||||
// to use them everywhere).
|
||||
.translateExtent([
|
||||
[zoomLimits.minTranslateX, zoomLimits.minTranslateY],
|
||||
[zoomLimits.maxTranslateX, zoomLimits.maxTranslateY],
|
||||
])
|
||||
// This is to ensure that the translation limits are properly
|
||||
// centered, so that the canvas margins are respected.
|
||||
.extent([
|
||||
[props.canvasMargins.left, props.canvasMargins.top],
|
||||
[props.canvasMargins.left + props.width, props.canvasMargins.top + props.height]
|
||||
]);
|
||||
}
|
||||
|
||||
this.setState(zoomLimits);
|
||||
this.setState(props.layoutLimits.toJS());
|
||||
}
|
||||
|
||||
// Restore the zooming settings
|
||||
@@ -173,11 +141,6 @@ class ZoomableCanvas extends React.Component {
|
||||
if (!props.layoutZoomState.isEmpty()) {
|
||||
const zoomState = props.layoutZoomState.toJS();
|
||||
|
||||
// After the limits have been set, update the zoom.
|
||||
this.svg.call(this.zoom.transform, zoomIdentity
|
||||
.translate(zoomState.translateX, zoomState.translateY)
|
||||
.scale(zoomState.scaleX, zoomState.scaleY));
|
||||
|
||||
// Update the state variables.
|
||||
this.setState(zoomState);
|
||||
this.zoomRestored = true;
|
||||
@@ -185,23 +148,108 @@ class ZoomableCanvas extends React.Component {
|
||||
}
|
||||
|
||||
canChangeZoom() {
|
||||
const { disabled, layoutZoomLimits } = this.props;
|
||||
const canvasHasContent = !layoutZoomLimits.isEmpty();
|
||||
const { disabled, layoutLimits } = this.props;
|
||||
const canvasHasContent = !layoutLimits.isEmpty();
|
||||
return !disabled && canvasHasContent;
|
||||
}
|
||||
|
||||
zoomed() {
|
||||
if (this.canChangeZoom()) {
|
||||
const updatedState = this.cachableState({
|
||||
scaleX: d3Event.transform.k,
|
||||
scaleY: d3Event.transform.k,
|
||||
translateX: d3Event.transform.x,
|
||||
translateY: d3Event.transform.y,
|
||||
});
|
||||
handlePanStart() {
|
||||
this.setState({ isPanning: true });
|
||||
}
|
||||
|
||||
this.setState(updatedState);
|
||||
this.debouncedCacheZoom();
|
||||
handlePanEnd() {
|
||||
this.setState({ isPanning: false });
|
||||
}
|
||||
|
||||
handlePan() {
|
||||
let state = this.state;
|
||||
// Apply the translation respecting the boundaries.
|
||||
state = this.clampedTranslation({ ...state,
|
||||
translateX: this.state.translateX + d3Event.dx,
|
||||
translateY: this.state.translateY + d3Event.dy,
|
||||
});
|
||||
this.updateState(state);
|
||||
}
|
||||
|
||||
handleZoom(ev) {
|
||||
if (this.canChangeZoom()) {
|
||||
// Get the exact mouse cursor position in the SVG and zoom around it.
|
||||
const { top, left } = this.svg.node().getBoundingClientRect();
|
||||
const mousePosition = {
|
||||
x: ev.clientX - left,
|
||||
y: ev.clientY - top,
|
||||
};
|
||||
this.zoomAtPositionByFactor(mousePosition, zoomFactor(ev));
|
||||
}
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
clampedTranslation(state) {
|
||||
const { width, height, canvasMargins, boundContent, layoutLimits } = this.props;
|
||||
const { contentMinX, contentMaxX, contentMinY, contentMaxY } = layoutLimits.toJS();
|
||||
|
||||
if (boundContent) {
|
||||
// If the content is required to be bounded in any way, the translation will
|
||||
// be adjusted so that certain constraints between the viewport and displayed
|
||||
// content bounding box are met.
|
||||
const viewportMin = { x: canvasMargins.left, y: canvasMargins.top };
|
||||
const viewportMax = { x: canvasMargins.left + width, y: canvasMargins.top + height };
|
||||
const contentMin = applyTransform(state, { x: contentMinX, y: contentMinY });
|
||||
const contentMax = applyTransform(state, { x: contentMaxX, y: contentMaxY });
|
||||
|
||||
switch (boundContent) {
|
||||
case CONTENT_COVERING:
|
||||
// These lines will adjust the translation by 'minimal effort' in
|
||||
// such a way that the content always FULLY covers the viewport,
|
||||
// i.e. that the viewport rectangle is always fully contained in
|
||||
// the content bounding box rectangle - the assumption made here
|
||||
// is that that can always be done.
|
||||
state.translateX += Math.max(0, viewportMax.x - contentMax.x);
|
||||
state.translateX -= Math.max(0, contentMin.x - viewportMin.x);
|
||||
state.translateY += Math.max(0, viewportMax.y - contentMax.y);
|
||||
state.translateY -= Math.max(0, contentMin.y - viewportMin.y);
|
||||
break;
|
||||
case CONTENT_INCLUDED:
|
||||
// These lines will adjust the translation by 'minimal effort' in
|
||||
// such a way that the content is always at least PARTLY contained
|
||||
// within the viewport, i.e. that the intersection between the
|
||||
// viewport and the content bounding box always exists.
|
||||
state.translateX -= Math.max(0, contentMin.x - viewportMax.x);
|
||||
state.translateX += Math.max(0, viewportMin.x - contentMax.x);
|
||||
state.translateY -= Math.max(0, contentMin.y - viewportMax.y);
|
||||
state.translateY += Math.max(0, viewportMin.y - contentMax.y);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
zoomAtPositionByFactor(position, factor) {
|
||||
// Update the scales by the given factor, respecting the zoom limits.
|
||||
const { minScale, maxScale } = this.state;
|
||||
const scaleX = clamp(this.state.scaleX * factor, minScale, maxScale);
|
||||
const scaleY = clamp(this.state.scaleY * factor, minScale, maxScale);
|
||||
let state = { ...this.state, scaleX, scaleY };
|
||||
|
||||
// Get the position in the coordinates before the transition and use it
|
||||
// to adjust the translation part of the new transition (respecting the
|
||||
// translation limits). Adapted from:
|
||||
// https://github.com/d3/d3-zoom/blob/807f02c7a5fe496fbd08cc3417b62905a8ce95fa/src/zoom.js#L251
|
||||
const inversePosition = inverseTransform(this.state, position);
|
||||
state = this.clampedTranslation({ ...state,
|
||||
translateX: position.x - (inversePosition.x * scaleX),
|
||||
translateY: position.y - (inversePosition.y * scaleY),
|
||||
});
|
||||
|
||||
this.updateState(state);
|
||||
}
|
||||
|
||||
updateState(state) {
|
||||
this.setState(this.cachableState(state));
|
||||
this.debouncedCacheZoom();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +260,7 @@ function mapStateToProps(state, props) {
|
||||
height: canvasHeightSelector(state),
|
||||
canvasMargins: canvasMarginsSelector(state),
|
||||
layoutZoomState: props.zoomStateSelector(state),
|
||||
layoutZoomLimits: props.zoomLimitsSelector(state),
|
||||
layoutLimits: props.limitsSelector(state),
|
||||
layoutId: JSON.stringify(activeTopologyZoomCacheKeyPathSelector(state)),
|
||||
forceRelayout: state.get('forceRelayout'),
|
||||
};
|
||||
|
||||
@@ -16,3 +16,6 @@ export const HIGHLIGHTED_EDGES_LAYER = 'highlighted-edges';
|
||||
export const HIGHLIGHTED_NODES_LAYER = 'highlighted-nodes';
|
||||
export const HOVERED_EDGES_LAYER = 'hovered-edges';
|
||||
export const HOVERED_NODES_LAYER = 'hovered-nodes';
|
||||
|
||||
export const CONTENT_INCLUDED = 'content-included';
|
||||
export const CONTENT_COVERING = 'content-covering';
|
||||
|
||||
@@ -38,7 +38,7 @@ export const NODE_BASE_SIZE = 100;
|
||||
export const EDGE_WAYPOINTS_CAP = 10;
|
||||
|
||||
export const CANVAS_MARGINS = {
|
||||
[GRAPH_VIEW_MODE]: { top: 160, left: 40, right: 40, bottom: 150 },
|
||||
[GRAPH_VIEW_MODE]: { top: 160, left: 80, right: 80, bottom: 150 },
|
||||
[TABLE_VIEW_MODE]: { top: 220, left: 40, right: 40, bottom: 30 },
|
||||
[RESOURCE_VIEW_MODE]: { top: 140, left: 210, right: 40, bottom: 150 },
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { canvasMarginsSelector, canvasWidthSelector, canvasHeightSelector } from
|
||||
import { activeLayoutCachedZoomSelector } from '../zooming';
|
||||
import { graphNodesSelector } from './graph';
|
||||
|
||||
// Nodes in the layout are always kept between 1px and 200px big.
|
||||
// Nodes in the layout are always kept between 3px and 200px big.
|
||||
const MAX_SCALE = 200 / NODE_BASE_SIZE;
|
||||
const MIN_SCALE = 3 / NODE_BASE_SIZE;
|
||||
|
||||
@@ -58,10 +58,24 @@ export const graphDefaultZoomSelector = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
// NOTE: This constant is made into a selector to fit
|
||||
// props requirements for <ZoomableCanvas /> component.
|
||||
export const graphZoomLimitsSelector = createSelector(
|
||||
[], () => makeMap({ minScale: MIN_SCALE, maxScale: MAX_SCALE })
|
||||
export const graphLimitsSelector = createSelector(
|
||||
[
|
||||
graphBoundingRectangleSelector,
|
||||
],
|
||||
(boundingRectangle) => {
|
||||
if (!boundingRectangle) return makeMap();
|
||||
|
||||
const { xMin, xMax, yMin, yMax } = boundingRectangle.toJS();
|
||||
|
||||
return makeMap({
|
||||
minScale: MIN_SCALE,
|
||||
maxScale: MAX_SCALE,
|
||||
contentMinX: xMin,
|
||||
contentMaxX: xMax,
|
||||
contentMinY: yMin,
|
||||
contentMaxY: yMax,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export const graphZoomStateSelector = createSelector(
|
||||
|
||||
@@ -66,7 +66,7 @@ export const resourcesDefaultZoomSelector = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
export const resourcesZoomLimitsSelector = createSelector(
|
||||
export const resourcesLimitsSelector = createSelector(
|
||||
[
|
||||
resourcesDefaultZoomSelector,
|
||||
resourceNodesBoundingRectangleSelector,
|
||||
@@ -83,10 +83,10 @@ export const resourcesZoomLimitsSelector = createSelector(
|
||||
maxScale: width / minNodeWidth,
|
||||
// Minimal zoom is equivalent to the initial one, where the whole layout matches the canvas.
|
||||
minScale: defaultZoom.get('scaleX'),
|
||||
minTranslateX: xMin,
|
||||
maxTranslateX: xMax,
|
||||
minTranslateY: yMin,
|
||||
maxTranslateY: yMax,
|
||||
contentMinX: xMin,
|
||||
contentMaxX: xMax,
|
||||
contentMinY: yMin,
|
||||
contentMaxY: yMax,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -4,13 +4,27 @@ const applyTranslateY = ({ scaleY = 1, translateY = 0 }, y) => (y * scaleY) + tr
|
||||
const applyScaleX = ({ scaleX = 1 }, width) => width * scaleX;
|
||||
const applyScaleY = ({ scaleY = 1 }, height) => height * scaleY;
|
||||
|
||||
export const applyTransform = (transform, { width, height, x, y }) => ({
|
||||
export const applyTransform = (transform, { width = 0, height = 0, x, y }) => ({
|
||||
x: applyTranslateX(transform, x),
|
||||
y: applyTranslateY(transform, y),
|
||||
width: applyScaleX(transform, width),
|
||||
height: applyScaleY(transform, height),
|
||||
});
|
||||
|
||||
|
||||
const inverseTranslateX = ({ scaleX = 1, translateX = 0 }, x) => (x - translateX) / scaleX;
|
||||
const inverseTranslateY = ({ scaleY = 1, translateY = 0 }, y) => (y - translateY) / scaleY;
|
||||
const inverseScaleX = ({ scaleX = 1 }, width) => width / scaleX;
|
||||
const inverseScaleY = ({ scaleY = 1 }, height) => height / scaleY;
|
||||
|
||||
export const inverseTransform = (transform, { width = 0, height = 0, x, y }) => ({
|
||||
x: inverseTranslateX(transform, x),
|
||||
y: inverseTranslateY(transform, y),
|
||||
width: inverseScaleX(transform, width),
|
||||
height: inverseScaleY(transform, height),
|
||||
});
|
||||
|
||||
|
||||
export const transformToString = ({ translateX = 0, translateY = 0, scaleX = 1, scaleY = 1 }) => (
|
||||
`translate(${translateX},${translateY}) scale(${scaleX},${scaleY})`
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ function wheelDelta(ev) {
|
||||
// Only Firefox seems to use the line unit (which we assume to
|
||||
// be 25px), otherwise the delta is already measured in pixels.
|
||||
const unitInPixels = (ev.deltaMode === DOM_DELTA_LINE ? 25 : 1);
|
||||
return ev.deltaY * unitInPixels * ZOOM_SENSITIVITY;
|
||||
return -ev.deltaY * unitInPixels * ZOOM_SENSITIVITY;
|
||||
}
|
||||
|
||||
export function zoomFactor(ev) {
|
||||
|
||||
@@ -63,6 +63,14 @@ a {
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
.fully-pannable {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@extend .grabbable;
|
||||
|
||||
&.panning { @extend .grabbing; }
|
||||
}
|
||||
|
||||
.shadow-2 {
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.16), 0 3px 10px rgba(0, 0, 0, 0.23);
|
||||
}
|
||||
@@ -284,15 +292,11 @@ a {
|
||||
height: $timeline-height;
|
||||
|
||||
svg {
|
||||
@extend .grabbable;
|
||||
@extend .fully-pannable;
|
||||
background-color: rgba(255, 255, 255, 0.85);
|
||||
box-shadow: inset 0 0 7px #aaa;
|
||||
pointer-events: all;
|
||||
margin: 0 7px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&.panning { @extend .grabbing; }
|
||||
|
||||
.available-range {
|
||||
fill: #888;
|
||||
@@ -358,6 +362,9 @@ a {
|
||||
}
|
||||
}
|
||||
|
||||
.zoomable-canvas svg {
|
||||
@extend .fully-pannable;
|
||||
}
|
||||
|
||||
.topologies {
|
||||
margin: 0 4px;
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
"d3-selection": "1.0.5",
|
||||
"d3-shape": "1.0.6",
|
||||
"d3-time-format": "2.0.5",
|
||||
"d3-transition": "1.0.4",
|
||||
"d3-zoom": "1.1.4",
|
||||
"dagre": "0.7.4",
|
||||
"debug": "2.6.6",
|
||||
"filesize": "3.5.9",
|
||||
|
||||
Reference in New Issue
Block a user