Basic SVG viewport with mouse support.
This commit is contained in:
parent
6538154445
commit
95db69da23
|
@ -19,12 +19,14 @@ import '@ionic/react/css/display.css';
|
|||
/* Theme variables */
|
||||
import './theme/variables.css';
|
||||
|
||||
import Map from './components/map/Map';
|
||||
|
||||
setupIonicReact();
|
||||
|
||||
const App: React.FC = () => (
|
||||
<IonApp>
|
||||
<h2>This works!</h2>
|
||||
</IonApp>
|
||||
<Map height={window.innerHeight} width={window.innerWidth} />
|
||||
</IonApp>
|
||||
);
|
||||
|
||||
export default App;
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.map {
|
||||
overflow: hidden;
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { IonContent, IonApp } from '@ionic/react';
|
||||
import react, { useState } from 'react';
|
||||
|
||||
import './Map.css';
|
||||
import MouseHandler from './MouseHandler';
|
||||
import Viewport from './Viewport';
|
||||
|
||||
interface MapProperties {
|
||||
height: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
const Map: react.FC<MapProperties> = (props: MapProperties) => {
|
||||
const boardSize = Math.max(props.width, props.height) * 2;
|
||||
|
||||
return (
|
||||
<IonContent fullscreen={true}>
|
||||
<div
|
||||
className='map'
|
||||
style={{ width: props.width + 'px', height: props.height + 'px' }}
|
||||
>
|
||||
<MouseHandler shift={{ x: 0, y: 0 }} zoom={1} boardSize={boardSize} />
|
||||
</div>
|
||||
</IonContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default Map;
|
|
@ -0,0 +1,110 @@
|
|||
import react, { useState } from 'react';
|
||||
import Viewport from './Viewport';
|
||||
|
||||
interface MouseHandlerProperties {
|
||||
boardSize: number;
|
||||
shift: { x: number; y: number };
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
const MouseHandler: react.FC<MouseHandlerProperties> = (
|
||||
props: MouseHandlerProperties
|
||||
) => {
|
||||
const initialMouseState = {
|
||||
down: false,
|
||||
starting: { x: -1, y: -1 },
|
||||
timestamp: 0,
|
||||
};
|
||||
|
||||
const [mouseState, setMouseState] = useState(initialMouseState);
|
||||
|
||||
const [shift, setShift] = useState(props.shift);
|
||||
const [zoom, setZoom] = useState(props.zoom);
|
||||
|
||||
console.log('MouseHandler, mouseState: ' + JSON.stringify(mouseState));
|
||||
console.log(
|
||||
'MouseHandler, shift: ' + JSON.stringify(shift) + ', zoom:' + zoom
|
||||
);
|
||||
|
||||
const genericHandler = (event: any) => {
|
||||
console.log(`Log - Event: ${event.type}`);
|
||||
if (event.pageX !== undefined) {
|
||||
console.log(
|
||||
`Mouse : ${event.pageX}, ${event.pageY}, target: ${event.target}`
|
||||
);
|
||||
console.log(
|
||||
`mouseState: ' ${JSON.stringify(mouseState)} (+${
|
||||
Date.now() - mouseState.timestamp
|
||||
}ms) `
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const mouseLeaveHandler = (event: any) => {
|
||||
genericHandler(event);
|
||||
setMouseState(initialMouseState);
|
||||
};
|
||||
|
||||
const mouseDownHandler = (event: any) => {
|
||||
genericHandler(event);
|
||||
setMouseState({
|
||||
down: true,
|
||||
starting: { x: event.pageX, y: event.pageY },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
};
|
||||
|
||||
const mouseUpHandler = (event: any) => {
|
||||
genericHandler(event);
|
||||
setMouseState(initialMouseState);
|
||||
};
|
||||
|
||||
const mouseMoveHandler = (event: any) => {
|
||||
if (mouseState.down && Date.now() - mouseState.timestamp > 5) {
|
||||
genericHandler(event);
|
||||
console.log(
|
||||
`dispatch ${JSON.stringify({
|
||||
x: event.pageX - mouseState.starting.x,
|
||||
y: event.pageY - mouseState.starting.y,
|
||||
})}`
|
||||
);
|
||||
setShift({
|
||||
x: shift.x + (event.pageX - mouseState.starting.x),
|
||||
y: shift.y + (event.pageY - mouseState.starting.y),
|
||||
});
|
||||
setMouseState({
|
||||
down: true,
|
||||
starting: {
|
||||
x: event.pageX,
|
||||
y: event.pageY,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const doubleClickHandler = (event: any) => {
|
||||
genericHandler(event);
|
||||
const newZoom = zoom * Math.SQRT2;
|
||||
setShift({
|
||||
x: shift.x + (shift.x - event.pageX) * (newZoom / zoom - 1),
|
||||
y: shift.y + (shift.y - event.pageY) * (newZoom / zoom - 1),
|
||||
});
|
||||
setZoom(newZoom);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseDown={mouseDownHandler}
|
||||
onMouseMove={mouseMoveHandler}
|
||||
onMouseUp={mouseUpHandler}
|
||||
onMouseLeave={mouseLeaveHandler}
|
||||
onDoubleClick={doubleClickHandler}
|
||||
>
|
||||
<Viewport boardSize={props.boardSize} shift={shift} zoom={zoom} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MouseHandler;
|
|
@ -0,0 +1,63 @@
|
|||
import react from 'react';
|
||||
|
||||
interface ViewportProperties {
|
||||
boardSize: number;
|
||||
shift: { x: number; y: number };
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Let's call:
|
||||
* - x and y the SVG coordinates
|
||||
* - X and Y the screen coordinates (pixels on screen)
|
||||
*
|
||||
* X0 = (x + shift.x * zoom) * zoom
|
||||
* or
|
||||
* x = X0 * zoom - shift.x
|
||||
* id for Y
|
||||
*
|
||||
* To add a new shift of S screen pixels, we need to apply a zoom of S/zoom
|
||||
*
|
||||
* How can we zoom so that X and x stay constant ?
|
||||
*
|
||||
* Knowing X0, x0, zoom0, zoom1 and shift.x0,
|
||||
*
|
||||
*
|
||||
* X0 = (x0 + shift.x0 *zoom0) * zoom0
|
||||
* X1 = (x1 + shift.x1 *zoom1) * zoom1
|
||||
* X0 = X1 (=X)
|
||||
* x0 = x1 (=x)
|
||||
* =>
|
||||
* (x + shift.x1*zoom1) * zoom1 = (x + shift.x0 * zoom0) * zoom0
|
||||
* =>
|
||||
* (x + shift.x1*zoom1) = (x + shift.x0*zoom0) * zoom0 / zoom1
|
||||
* shift.x1 = ((x + shift.x0) * zoom0 / zoom1 - x) / zoom1
|
||||
*
|
||||
* x = 333
|
||||
* 282 => -25,5
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* viewBox={`${-props.shift.x} ${-props.shift.y} ${
|
||||
props.boardSize / props.zoom
|
||||
} ${props.boardSize / props.zoom}`}
|
||||
|
||||
*
|
||||
*/
|
||||
|
||||
const Viewport: react.FC<ViewportProperties> = (props: ViewportProperties) => {
|
||||
return (
|
||||
<svg height={props.boardSize} width={props.boardSize}>
|
||||
<g
|
||||
transform={`translate(${props.shift.x}, ${props.shift.y}) scale(${props.zoom})`}
|
||||
>
|
||||
<circle cx='50' cy='50' r='50' />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Viewport;
|
|
@ -0,0 +1,9 @@
|
|||
import react from 'react';
|
||||
|
||||
interface MapProperties {}
|
||||
|
||||
const Map: react.FC<MapProperties> = (props: MapProperties) => {
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default Map;
|
Loading…
Reference in New Issue