mirror of
https://github.com/icewind1991/mapboundaries.git
synced 2026-06-03 19:04:07 +02:00
basic render
This commit is contained in:
parent
abfdb9d2d9
commit
9e7d0c4eaa
12 changed files with 654 additions and 3 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,3 +1,3 @@
|
||||||
.idea
|
.ideaabfdb9d2d9abee3fa2055f10f3eb860954e4a8c4
|
||||||
node_modules
|
node_modules
|
||||||
build
|
build
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
{
|
{
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^7.0.12",
|
||||||
"@types/react": "^15.0.21",
|
"@types/react": "^15.0.21",
|
||||||
"@types/react-dom": "^0.14.23",
|
"@types/react-dom": "^0.14.23",
|
||||||
"clean-webpack-plugin": "^0.1.16",
|
"clean-webpack-plugin": "^0.1.16",
|
||||||
|
"css-loader": "^0.28.0",
|
||||||
"extract-text-webpack-plugin": "^2.1.0",
|
"extract-text-webpack-plugin": "^2.1.0",
|
||||||
"html-webpack-plugin": "^2.28.0",
|
"html-webpack-plugin": "^2.28.0",
|
||||||
"postcss-cssnext": "^2.10.0",
|
"postcss-cssnext": "^2.10.0",
|
||||||
"postcss-loader": "^1.3.3",
|
"postcss-loader": "^1.3.3",
|
||||||
"postcss-nested": "^1.0.1",
|
"postcss-nested": "^1.0.1",
|
||||||
"react-hot-loader": "^3.0.0-beta.6",
|
"react-hot-loader": "^3.0.0-beta.6",
|
||||||
|
"style-loader": "^0.16.1",
|
||||||
"ts-loader": "^2.0.3",
|
"ts-loader": "^2.0.3",
|
||||||
"typescript": "^2.2.2",
|
"typescript": "^2.2.2",
|
||||||
"webpack": "^2.4.1",
|
"webpack": "^2.4.1",
|
||||||
|
|
|
||||||
11
src/App.css
Normal file
11
src/App.css
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
.background {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
95
src/App.tsx
95
src/App.tsx
|
|
@ -1,11 +1,102 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import {Demo} from 'tf2-demo/build/Demo';
|
||||||
|
import {World} from 'tf2-demo/build/Data/World';
|
||||||
|
import {DemoViewer} from "./DemoViewer";
|
||||||
|
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
|
scale: number;
|
||||||
|
originalHeight: number;
|
||||||
|
originalWidth: number;
|
||||||
|
demo: Demo | null;
|
||||||
|
background: string | null;
|
||||||
|
world: World;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import './App.css';
|
||||||
|
import {MapContainer} from "./MapContainer";
|
||||||
|
|
||||||
export class App extends React.Component<{}, AppState> {
|
export class App extends React.Component<{}, AppState> {
|
||||||
|
state: AppState = {
|
||||||
|
scale: 8,
|
||||||
|
originalWidth: 2560,
|
||||||
|
originalHeight: 1440,
|
||||||
|
demo: null,
|
||||||
|
background: null,
|
||||||
|
world: {
|
||||||
|
boundaryMin: {x: 0, y: 0, z: 0},
|
||||||
|
boundaryMax: {x: 1000, y: 1000, z: 1000},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onNumberInputChange(key, event) {
|
||||||
|
const value = parseInt(event.target.value, 10);
|
||||||
|
const update = {};
|
||||||
|
update[key] = value;
|
||||||
|
this.setState(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectDemo = (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const fileData = reader.result as ArrayBuffer;
|
||||||
|
const demo = new Demo(fileData);
|
||||||
|
const parser = demo.getParser();
|
||||||
|
parser.readHeader();
|
||||||
|
const match = parser.match;
|
||||||
|
|
||||||
|
while (match.world.boundaryMin.x === 0) {
|
||||||
|
parser.tick();
|
||||||
|
}
|
||||||
|
this.setState({demo, world: match.world});
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectImage = (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const background = reader.result as string;
|
||||||
|
this.setState({background});
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
getDimensions(): { width: number, height: number } {
|
||||||
|
const originalHeight = 1024 * this.state.scale; // 1024 is a magic number straight from cl_leveloverview code
|
||||||
|
const originalWidth = originalHeight * (this.state.originalWidth / this.state.originalHeight);
|
||||||
|
|
||||||
|
return {width: originalWidth, height: originalHeight};
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div>app</div>;
|
const {width, height} = this.getDimensions();
|
||||||
|
const demoWidth = (this.state.world.boundaryMax.x - this.state.world.boundaryMin.x) / 10;
|
||||||
|
const demoHeight = (this.state.world.boundaryMax.y - this.state.world.boundaryMin.y) / 10;
|
||||||
|
const size = Math.max(demoWidth, demoHeight);
|
||||||
|
console.log(demoWidth, demoHeight);
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<input onChange={this.onNumberInputChange.bind(this, 'scale')}
|
||||||
|
placeholder="scale"
|
||||||
|
value={this.state.scale}/>
|
||||||
|
<input
|
||||||
|
onChange={this.onNumberInputChange.bind(this, 'originalHeight')}
|
||||||
|
placeholder="screen height"
|
||||||
|
value={this.state.originalHeight}/>
|
||||||
|
<input
|
||||||
|
onChange={this.onNumberInputChange.bind(this, 'originalWidth')}
|
||||||
|
placeholder="screen width"
|
||||||
|
value={this.state.originalWidth}/>
|
||||||
|
<input onChange={this.onSelectDemo} type="file"/>
|
||||||
|
<MapContainer contentSize={{width: demoWidth, height: demoHeight}}>
|
||||||
|
<DemoViewer className="viewer" height={demoHeight}
|
||||||
|
width={demoWidth}
|
||||||
|
demo={this.state.demo}/>
|
||||||
|
</MapContainer>
|
||||||
|
<input onChange={this.onSelectImage} type="file"/>
|
||||||
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
69
src/DemoRender.ts
Normal file
69
src/DemoRender.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import {Demo} from 'tf2-demo/build/Demo';
|
||||||
|
|
||||||
|
export class DemoRender {
|
||||||
|
static render(canvas: HTMLCanvasElement, demo: Demo, width: number, height: number) {
|
||||||
|
const ctx = <CanvasRenderingContext2D>canvas.getContext('2d');
|
||||||
|
|
||||||
|
const parser = demo.getParser();
|
||||||
|
const match = parser.match;
|
||||||
|
|
||||||
|
const scaleX = width / (match.world.boundaryMax.x - match.world.boundaryMin.x);
|
||||||
|
const scaleY = height / (match.world.boundaryMax.y - match.world.boundaryMin.y);
|
||||||
|
const scale = Math.max(scaleX, scaleY);
|
||||||
|
|
||||||
|
const boundaryMin = match.world.boundaryMin;
|
||||||
|
|
||||||
|
const translatePosition = (position) => {
|
||||||
|
return {
|
||||||
|
x: Math.floor(position.x - boundaryMin.x) * scale,
|
||||||
|
y: Math.floor(position.y - boundaryMin.y) * scale
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawScale = Math.max(width, height) / 1500;
|
||||||
|
|
||||||
|
const drawTick = () => {
|
||||||
|
for (const player of match.players) {
|
||||||
|
if (!player.user.team) {
|
||||||
|
continue;// spec
|
||||||
|
}
|
||||||
|
if (player.team === 2) {
|
||||||
|
ctx.fillStyle = 'red';
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = 'blue';
|
||||||
|
}
|
||||||
|
const pos = translatePosition(player.position);
|
||||||
|
ctx.fillRect(
|
||||||
|
pos.x,
|
||||||
|
pos.y,
|
||||||
|
drawScale,
|
||||||
|
drawScale
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawNext = () => {
|
||||||
|
let tick;
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
tick = parser.tick();
|
||||||
|
if (tick) {
|
||||||
|
drawTick();
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tick) {
|
||||||
|
// queue up the next batch of ticks
|
||||||
|
requestAnimationFrame(drawNext);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
requestAnimationFrame(drawNext);
|
||||||
|
}
|
||||||
|
|
||||||
|
static clear(canvas: HTMLCanvasElement) {
|
||||||
|
const ctx = <CanvasRenderingContext2D>canvas.getContext('2d');
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
45
src/DemoViewer.tsx
Normal file
45
src/DemoViewer.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import {Demo} from 'tf2-demo/build/Demo';
|
||||||
|
import {DemoRender} from "./DemoRender";
|
||||||
|
|
||||||
|
export interface DemoViewerProps {
|
||||||
|
demo: Demo | null;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DemoViewer extends React.Component<DemoViewerProps, {}> {
|
||||||
|
canvas: HTMLCanvasElement;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.updateCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCanvas() {
|
||||||
|
if (this.props.demo) {
|
||||||
|
DemoRender.render(this.canvas, this.props.demo, this.props.width, this.props.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps: DemoViewerProps) {
|
||||||
|
if (nextProps.demo !== this.props.demo && nextProps.width === nextProps.width && nextProps.height === nextProps.height) {
|
||||||
|
if (this.canvas) {
|
||||||
|
DemoRender.clear(this.canvas);
|
||||||
|
}
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.updateCanvas();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <canvas
|
||||||
|
width={this.props.width}
|
||||||
|
height={this.props.height}
|
||||||
|
className={this.props.className}
|
||||||
|
ref={(canvas) => {
|
||||||
|
this.canvas = canvas
|
||||||
|
}}/>
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/MapContainer.css
Normal file
5
src/MapContainer.css
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
.map-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
66
src/MapContainer.tsx
Normal file
66
src/MapContainer.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import {Panner} from "./Panner/Panner";
|
||||||
|
|
||||||
|
import './MapContainer.css';
|
||||||
|
|
||||||
|
export class MapContainerProps {
|
||||||
|
children?: any;
|
||||||
|
contentSize: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
onScale?: (scale: number) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MapContainerState {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MapContainer extends React.Component<MapContainerProps, MapContainerState> {
|
||||||
|
container: Element;
|
||||||
|
state: MapContainerState = {
|
||||||
|
width: 500,
|
||||||
|
height: 800
|
||||||
|
};
|
||||||
|
sendScale = false;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (this.container.clientWidth == this.state.width && this.container.clientHeight == this.state.height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
height: this.container.clientHeight,
|
||||||
|
width: this.container.clientWidth
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps({contentSize}: MapContainerProps) {
|
||||||
|
this.setState({
|
||||||
|
height: this.container.clientHeight,
|
||||||
|
width: this.container.clientWidth
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const scale = Math.min(this.state.width / this.props.contentSize.width, this.state.height / this.props.contentSize.height);
|
||||||
|
if (!this.sendScale && this.props.onScale) {
|
||||||
|
if (isFinite(scale)) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.props.onScale && this.props.onScale(scale);
|
||||||
|
this.sendScale = true;
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="map-container" ref={(div) => this.container = div}>
|
||||||
|
<Panner width={this.state.width} height={this.state.height}
|
||||||
|
scale={scale} contentSize={this.props.contentSize}
|
||||||
|
onScale={this.props.onScale}>
|
||||||
|
{this.props.children}
|
||||||
|
</Panner>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
102
src/Panner/CenteredPanZoom.ts
Normal file
102
src/Panner/CenteredPanZoom.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
import {Viewport, Point} from './Viewport';
|
||||||
|
|
||||||
|
export interface CenteredPanZoomOptions {
|
||||||
|
screenWidth: number;
|
||||||
|
screenHeight: number;
|
||||||
|
scale?: number;
|
||||||
|
contentSize: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CenteredPanZoom {
|
||||||
|
screen: Viewport;
|
||||||
|
viewport: Viewport;
|
||||||
|
scale: number;
|
||||||
|
contentSize: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(options: CenteredPanZoomOptions) {
|
||||||
|
this.screen = new Viewport({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: options.screenWidth,
|
||||||
|
height: options.screenHeight
|
||||||
|
});
|
||||||
|
this.viewport = new Viewport({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: options.screenWidth,
|
||||||
|
height: options.screenHeight
|
||||||
|
});
|
||||||
|
this.scale = options.scale || 1;
|
||||||
|
this.contentSize = options.contentSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSize(width, height) {
|
||||||
|
this.screen.width = width;
|
||||||
|
this.screen.height = height;
|
||||||
|
this.viewport.width = width * this.scale;
|
||||||
|
this.viewport.height = height * this.scale;
|
||||||
|
this.constrainPan(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
setContentSize(width, height) {
|
||||||
|
this.contentSize = {width, height};
|
||||||
|
this.constrainPan(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
pan(screenX, screenY) {
|
||||||
|
this.viewport.x = this.viewport.x + screenX;
|
||||||
|
this.viewport.y = this.viewport.y + screenY;
|
||||||
|
this.constrainPan();
|
||||||
|
}
|
||||||
|
|
||||||
|
constrainPan(strict = false) {
|
||||||
|
const minX = (strict) ? 0 : this.screen.width * 0.5;
|
||||||
|
const minY = (strict) ? 0 : this.screen.height * 0.5;
|
||||||
|
this.viewport.x = Math.min(minX, this.viewport.x);
|
||||||
|
this.viewport.y = Math.min(minY * 0.5, this.viewport.y);
|
||||||
|
const maxY = (this.screen.height - (this.contentSize.height * this.scale)) - (strict ? 0 : this.screen.height * 0.5);
|
||||||
|
const maxX = (this.screen.width - (this.contentSize.width * this.scale)) - (strict ? 0 : this.screen.width * 0.5);
|
||||||
|
this.viewport.y = Math.max(maxY, this.viewport.y);
|
||||||
|
this.viewport.x = Math.max(maxX, this.viewport.x);
|
||||||
|
if (maxY > ((strict) ? 0 : this.screen.height * 0.5)) {
|
||||||
|
this.viewport.y = Math.floor(maxY / 2);
|
||||||
|
}
|
||||||
|
if (maxX > ((strict) ? 0 : this.screen.width * 0.5)) {
|
||||||
|
this.viewport.x = Math.floor(maxX / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
panFrom(screenStart: Point, screenEnd: Point) {
|
||||||
|
this.pan(screenEnd.x - screenStart.x, screenEnd.y - screenStart.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
//find zoom point in pre-zoom viewport
|
||||||
|
//make that point the same in the post-zoom viewport
|
||||||
|
zoom(scale: number, screenCenter: Point) {
|
||||||
|
const v1 = Viewport.convert(screenCenter, {
|
||||||
|
from: this.screen,
|
||||||
|
to: this.viewport
|
||||||
|
});
|
||||||
|
this.viewport.x = this.viewport.x * (scale / this.scale);
|
||||||
|
this.viewport.y = this.viewport.y * (scale / this.scale);
|
||||||
|
this.viewport.width = this.screen.width * scale;
|
||||||
|
this.viewport.height = this.screen.height * scale;
|
||||||
|
const minScale = Math.min(this.screen.width / this.contentSize.width, this.screen.height / this.contentSize.height);
|
||||||
|
scale = Math.max(minScale, scale);
|
||||||
|
this.scale = scale;
|
||||||
|
|
||||||
|
const v2 = Viewport.convert(screenCenter, {
|
||||||
|
from: this.screen,
|
||||||
|
to: this.viewport
|
||||||
|
});
|
||||||
|
const deltaX = v2.x - v1.x;
|
||||||
|
const deltaY = v2.y - v1.y;
|
||||||
|
this.pan(deltaX * scale, deltaY * scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/Panner/Panner.css
Normal file
54
src/Panner/Panner.css
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
.pan-zoom-element {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.noselect {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoommenu {
|
||||||
|
position: fixed;
|
||||||
|
color: #888;
|
||||||
|
|
||||||
|
bottom: 100px;
|
||||||
|
right: 0;
|
||||||
|
margin: 10px;
|
||||||
|
font-size: 200%;
|
||||||
|
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: all 0.5s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
div {
|
||||||
|
transition: all 0.5s;
|
||||||
|
width: 32px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div:first-child {
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
169
src/Panner/Panner.tsx
Normal file
169
src/Panner/Panner.tsx
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import {CenteredPanZoom} from './CenteredPanZoom';
|
||||||
|
|
||||||
|
import './Panner.css';
|
||||||
|
|
||||||
|
export interface PannerState {
|
||||||
|
scale: number;
|
||||||
|
translate: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PannerProps {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
scale: number;
|
||||||
|
contentSize: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
onScale?: (scale: number) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Panner extends React.Component<PannerProps, PannerState> {
|
||||||
|
private startX = 0;
|
||||||
|
private startY = 0;
|
||||||
|
private panner: CenteredPanZoom;
|
||||||
|
|
||||||
|
state: PannerState = {
|
||||||
|
scale: 0,
|
||||||
|
translate: {
|
||||||
|
x: 0,
|
||||||
|
y: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.panner = new CenteredPanZoom({
|
||||||
|
screenHeight: this.props.height,
|
||||||
|
screenWidth: this.props.width,
|
||||||
|
scale: this.props.scale,
|
||||||
|
contentSize: props.contentSize
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.setState({
|
||||||
|
translate: {
|
||||||
|
x: this.panner.viewport.x,
|
||||||
|
y: this.panner.viewport.y
|
||||||
|
},
|
||||||
|
scale: this.panner.scale
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps({width, height, scale, contentSize}:PannerProps) {
|
||||||
|
if (
|
||||||
|
width == this.panner.screen.width && height == this.panner.screen.height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.panner.scale = scale;
|
||||||
|
this.panner.setSize(width, height);
|
||||||
|
this.panner.setContentSize(contentSize.width, contentSize.height);
|
||||||
|
this.setState({scale});
|
||||||
|
this.setState({
|
||||||
|
translate: {
|
||||||
|
x: this.panner.viewport.x,
|
||||||
|
y: this.panner.viewport.y
|
||||||
|
},
|
||||||
|
scale: this.panner.scale
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const style = {
|
||||||
|
transform: `translate(${this.state.translate.x}px, ${this.state.translate.y}px) scale(${this.state.scale})`,
|
||||||
|
transformOrigin: 'top left'
|
||||||
|
};
|
||||||
|
const center = {
|
||||||
|
x: Math.floor(this.panner.screen.width / 2),
|
||||||
|
y: Math.floor(this.panner.screen.height / 2)
|
||||||
|
};
|
||||||
|
const setZoomFactor = this.zoomFactor;
|
||||||
|
return (
|
||||||
|
<div className="pan-zoom-element"
|
||||||
|
ref="element"
|
||||||
|
style={{width: this.props.width, height: this.props.height}}
|
||||||
|
onMouseDown={this._onMouseDown}
|
||||||
|
onWheel={this._onWheel}>
|
||||||
|
<div ref="content" className="content-container noselect"
|
||||||
|
style={style}>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
<div className="zoommenu">
|
||||||
|
<div className="plus"
|
||||||
|
onClick={()=>{setZoomFactor(1.10, center)}}>+
|
||||||
|
</div>
|
||||||
|
<div className="minus"
|
||||||
|
onClick={()=>{setZoomFactor(0.90, center)}}>
|
||||||
|
-
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onMouseDown = (event) => {
|
||||||
|
this.startX = event.pageX;
|
||||||
|
this.startY = event.pageY;
|
||||||
|
document.addEventListener('mouseup', this._onMouseUp, true);
|
||||||
|
document.addEventListener('mousemove', this._onMouseMove, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
_onMouseUp = () => {
|
||||||
|
document.removeEventListener('mouseup', this._onMouseUp, true);
|
||||||
|
document.removeEventListener('mousemove', this._onMouseMove, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
_onMouseMove = (event) => {
|
||||||
|
const {pageX, pageY} = event;
|
||||||
|
this.panner.panFrom(
|
||||||
|
{
|
||||||
|
x: this.startX,
|
||||||
|
y: this.startY
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: pageX,
|
||||||
|
y: pageY
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.startX = event.pageX;
|
||||||
|
this.startY = event.pageY;
|
||||||
|
this.setState({
|
||||||
|
translate: {
|
||||||
|
x: this.panner.viewport.x,
|
||||||
|
y: this.panner.viewport.y
|
||||||
|
},
|
||||||
|
scale: this.panner.scale
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
zoomFactor = (zoomFactor, point) => {
|
||||||
|
const newScale = this.state.scale * zoomFactor;
|
||||||
|
|
||||||
|
this.panner.zoom(newScale, point);
|
||||||
|
this.setState({
|
||||||
|
translate: {
|
||||||
|
x: this.panner.viewport.x,
|
||||||
|
y: this.panner.viewport.y
|
||||||
|
},
|
||||||
|
scale: this.panner.scale
|
||||||
|
});
|
||||||
|
if (this.props.onScale) {
|
||||||
|
this.props.onScale(this.panner.scale);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_onWheel = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const center = {x: event.pageX, y: event.pageY};
|
||||||
|
if (event.deltaY < 0) {
|
||||||
|
this.zoomFactor(1.05, center);
|
||||||
|
} else {
|
||||||
|
this.zoomFactor(0.95, center);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
36
src/Panner/Viewport.ts
Normal file
36
src/Panner/Viewport.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
export interface ViewPortOptions {
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Point {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Viewport {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
|
||||||
|
constructor(options: ViewPortOptions) {
|
||||||
|
this.x = options.x || 0;
|
||||||
|
this.y = options.y || 0;
|
||||||
|
this.width = options.width;
|
||||||
|
this.height = options.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
static convert(point: Point, {from, to}: {from: Viewport, to: Viewport}): Point {
|
||||||
|
const widthScale = from.width / to.width;
|
||||||
|
const heightScale = from.height / to.height;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: point.x * widthScale - to.x * widthScale,
|
||||||
|
y: point.y * heightScale - to.y * heightScale
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue