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 */
|
/* Theme variables */
|
||||||
import './theme/variables.css';
|
import './theme/variables.css';
|
||||||
|
|
||||||
|
import Map from './components/map/Map';
|
||||||
|
|
||||||
setupIonicReact();
|
setupIonicReact();
|
||||||
|
|
||||||
const App: React.FC = () => (
|
const App: React.FC = () => (
|
||||||
<IonApp>
|
<IonApp>
|
||||||
<h2>This works!</h2>
|
<Map height={window.innerHeight} width={window.innerWidth} />
|
||||||
</IonApp>
|
</IonApp>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default App;
|
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