mirror of
https://github.com/icewind1991/mapboundaries.git
synced 2026-06-03 10:54: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
|
||||
build
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"@types/node": "^7.0.12",
|
||||
"@types/react": "^15.0.21",
|
||||
"@types/react-dom": "^0.14.23",
|
||||
"clean-webpack-plugin": "^0.1.16",
|
||||
"css-loader": "^0.28.0",
|
||||
"extract-text-webpack-plugin": "^2.1.0",
|
||||
"html-webpack-plugin": "^2.28.0",
|
||||
"postcss-cssnext": "^2.10.0",
|
||||
"postcss-loader": "^1.3.3",
|
||||
"postcss-nested": "^1.0.1",
|
||||
"react-hot-loader": "^3.0.0-beta.6",
|
||||
"style-loader": "^0.16.1",
|
||||
"ts-loader": "^2.0.3",
|
||||
"typescript": "^2.2.2",
|
||||
"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 {Demo} from 'tf2-demo/build/Demo';
|
||||
import {World} from 'tf2-demo/build/Data/World';
|
||||
import {DemoViewer} from "./DemoViewer";
|
||||
|
||||
|
||||
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> {
|
||||
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() {
|
||||
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