viewer fixes

This commit is contained in:
Robin Appelman 2023-04-29 16:21:05 +02:00
commit e94f0474ef
9 changed files with 125 additions and 100 deletions

25
Cargo.lock generated
View file

@ -1879,6 +1879,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
dependencies = [
"mime",
"unicase",
]
[[package]] [[package]]
name = "minimal-lexical" name = "minimal-lexical"
version = "0.2.1" version = "0.2.1"
@ -4666,7 +4676,13 @@ dependencies = [
"http", "http",
"http-body", "http-body",
"http-range-header", "http-range-header",
"httpdate",
"mime",
"mime_guess",
"percent-encoding",
"pin-project-lite", "pin-project-lite",
"tokio",
"tokio-util",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing", "tracing",
@ -4781,6 +4797,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
[[package]]
name = "unicase"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
dependencies = [
"version_check",
]
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.13" version = "0.3.13"

View file

@ -19,7 +19,7 @@ maud = { version = "0.25.0", features = ["axum"] }
axum = { version = "0.6.12", features = ["headers", "macros"] } axum = { version = "0.6.12", features = ["headers", "macros"] }
hyper = "0.14.25" hyper = "0.14.25"
hyperlocal = "0.8.0" hyperlocal = "0.8.0"
tower-http = { version = "0.4.0", features = ["trace"] } tower-http = { version = "0.4.0", features = ["trace", "fs"] }
steamid-ng = "1.0.0" steamid-ng = "1.0.0"
itertools = "0.10.5" itertools = "0.10.5"
const-fnv1a-hash = "1.1.0" const-fnv1a-hash = "1.1.0"

View file

@ -8,7 +8,7 @@ import {Header, WorldBoundaries} from "./Data/Parser";
import {AsyncParser} from "./Data/AsyncParser"; import {AsyncParser} from "./Data/AsyncParser";
import {getMapBoundaries} from "./MapBoundries"; import {getMapBoundaries} from "./MapBoundries";
import {createSignal} from "solid-js"; import {createEffect, createSignal} from "solid-js";
import {Session, StateUpdate} from "./Session"; import {Session, StateUpdate} from "./Session";
export interface AnalyseProps { export interface AnalyseProps {

View file

@ -21,8 +21,10 @@ export const MapContainer = (props: ParentProps<MapContainerProps>) => {
} }
} }
createEffect(() => { createEffect(() => {
if (isFinite(scale())) { const s = scale();
props.onScale && props.onScale(scale()); console.log(s)
if (isFinite(s)) {
props.onScale && props.onScale(s);
} }
}); });

View file

@ -2,7 +2,7 @@ import {Player as PlayerDot} from './Render/Player';
import {Building as BuildingDot} from './Render/Building'; import {Building as BuildingDot} from './Render/Building';
import {findMapAlias} from './MapBoundries'; import {findMapAlias} from './MapBoundries';
import {PlayerState, Header, WorldBoundaries, BuildingState} from "./Data/Parser"; import {PlayerState, Header, WorldBoundaries, BuildingState} from "./Data/Parser";
import {splitProps} from "solid-js"; import {createEffect, Show} from "solid-js";
export interface MapRenderProps { export interface MapRenderProps {
header: Header; header: Header;
@ -18,29 +18,22 @@ export interface MapRenderProps {
export function MapRender(props: MapRenderProps) { export function MapRender(props: MapRenderProps) {
const mapAlias = findMapAlias(props.header.map); const mapAlias = findMapAlias(props.header.map);
const image = `images/leveloverview/dist/${mapAlias}.webp`; const image = `/images/leveloverview/dist/${mapAlias}.webp`;
const background = `url(${image})`; const background = `url(${image})`;
const playerDots = () => props.players
.filter((player: PlayerState) => player.health)
.map((player: PlayerState) => {
return <PlayerDot player={player} mapBoundary={props.world}
targetSize={props.size} scale={props.scale} />
});
const buildingDots = () => props.buildings
.filter((building: PlayerState) => building.position.x)
.map((building: PlayerState) => {
return <BuildingDot building={building}
mapBoundary={props.world}
targetSize={props.size} scale={props.scale}/>
});
return ( return (
<svg class="map-background" width={props.size.width} height={props.size.height} <svg class="map-background" width={props.size.width} height={props.size.height}
style={{"background-image": background}}> style={{"background-image": background}}>
{playerDots()} <For each={props.players}>{(player) =>
{buildingDots()} <Show when={player.health}>
<PlayerDot player={player} mapBoundary={props.world} targetSize={props.size} scale={props.scale} />
</Show>
}</For>
<For each={props.buildings}>{(building) =>
<Show when={building.position.x}>
<BuildingDot building={building} mapBoundary={props.world} targetSize={props.size} scale={props.scale}/>
</Show>
}</For>
</svg> </svg>
); );
} }

View file

@ -40,29 +40,32 @@ function getIcon(building: BuildingState) {
return `/images/building_icons/${icon}_${team}.png`; return `/images/building_icons/${icon}_${team}.png`;
} }
export function Building({building, mapBoundary, targetSize, scale}: BuildingProp) { export function Building(props: BuildingProp) {
const worldWidth = mapBoundary.boundary_max.x - mapBoundary.boundary_min.x; const worldWidth = props.mapBoundary.boundary_max.x - props.mapBoundary.boundary_min.x;
const worldHeight = mapBoundary.boundary_max.y - mapBoundary.boundary_min.y; const worldHeight = props.mapBoundary.boundary_max.y - props.mapBoundary.boundary_min.y;
const x = building.position.x - mapBoundary.boundary_min.x; const x = () => props.building.position.x - props.mapBoundary.boundary_min.x;
const y = building.position.y - mapBoundary.boundary_min.y; const y = () => props.building.position.y - props.mapBoundary.boundary_min.y;
const scaledX = x / worldWidth * targetSize.width; const scaledX = () => x() / worldWidth * props.targetSize.width;
const scaledY = (worldHeight - y) / worldHeight * targetSize.height; const scaledY = () => (worldHeight - y()) / worldHeight * props.targetSize.height;
const maxHealth = healthMap[building.level]; const maxHealth = () => healthMap[props.building.level];
if (!maxHealth) { if (!maxHealth) {
return null; return null;
} }
const alpha = building.health / maxHealth; const transform = () => `translate(${scaledX()} ${scaledY()}) scale(${1 / props.scale})`;
const rotate = () => `rotate(${270 - props.building.angle})`;
const alpha = () => props.building.health / maxHealth;
try { try {
const image = getIcon(building); const image = getIcon(props.building);
return <g transform={`translate(${scaledX} ${scaledY}) scale(${1 / scale})`} return <g transform={transform()}
opacity={alpha}> opacity={alpha()}>
<image href={image} className={"player-icon"} height={32} <image href={image} className={"player-icon"} height={32}
width={32} width={32}
transform={`translate(-16 -16)`}/> transform={`translate(-16 -16)`}/>
<Show when={building.angle}> <Show when={props.building.angle}>
<polygon points="-6,14 0, 16 6,14 0,24" fill="white" <polygon points="-6,14 0, 16 6,14 0,24" fill="white"
transform={`rotate(${270 - building.angle})`}/> transform={rotate()}/>
</Show> </Show>
</g> </g>
} catch (e) { } catch (e) {

View file

@ -1,73 +1,76 @@
import {PlayerState, WorldBoundaries, Team} from "../Data/Parser"; import {PlayerState, WorldBoundaries, Team} from "../Data/Parser";
import {createEffect} from "solid-js";
export interface PlayerProp { export interface PlayerProp {
player: PlayerState; player: PlayerState;
mapBoundary: WorldBoundaries; mapBoundary: WorldBoundaries;
targetSize: { targetSize: {
width: number; width: number;
height: number; height: number;
}; };
scale: number; scale: number;
} }
const healthMap = { const healthMap = {
0: 100, //fallback 0: 100, //fallback
1: 125,//scout 1: 125,//scout
2: 150,//sniper 2: 150,//sniper
3: 200,//soldier, 3: 200,//soldier,
4: 175,//demoman, 4: 175,//demoman,
5: 150,//medic, 5: 150,//medic,
6: 300,//heavy, 6: 300,//heavy,
7: 175,//pyro 7: 175,//pyro
8: 125,//spy 8: 125,//spy
9: 125,//engineer 9: 125,//engineer
}; };
const classMap = { const classMap = {
0: "empty", 0: "empty",
1: "scout", 1: "scout",
2: "sniper", 2: "sniper",
3: "soldier", 3: "soldier",
4: "demoman", 4: "demoman",
5: "medic", 5: "medic",
6: "heavy", 6: "heavy",
7: "pyro", 7: "pyro",
8: "spy", 8: "spy",
9: "engineer" 9: "engineer"
}; };
export function Player({player, mapBoundary, targetSize, scale}: PlayerProp) { export function Player(props: PlayerProp) {
const worldWidth = mapBoundary.boundary_max.x - mapBoundary.boundary_min.x; const worldWidth = props.mapBoundary.boundary_max.x - props.mapBoundary.boundary_min.x;
const worldHeight = mapBoundary.boundary_max.y - mapBoundary.boundary_min.y; const worldHeight = props.mapBoundary.boundary_max.y - props.mapBoundary.boundary_min.y;
const x = player.position.x - mapBoundary.boundary_min.x; const x = () => props.player.position.x - props.mapBoundary.boundary_min.x;
const y = player.position.y - mapBoundary.boundary_min.y; const y = () => props.player.position.y - props.mapBoundary.boundary_min.y;
const scaledX = x / worldWidth * targetSize.width; const scaledX = () => x() / worldWidth * props.targetSize.width;
const scaledY = (worldHeight - y) / worldHeight * targetSize.height; const scaledY = () => (worldHeight - y()) / worldHeight * props.targetSize.height;
const maxHealth = healthMap[player.playerClass]; const maxHealth = () => healthMap[props.player.playerClass];
const alpha = player.health / maxHealth; const alpha = () => props.player.health / maxHealth();
const teamColor = (player.team === Team.Red) ? '#a75d50' : '#5b818f'; const teamColor = () => (props.player.team === Team.Red) ? '#a75d50' : '#5b818f';
const imageOpacity = player.health === 0 ? 0 : (1 + alpha) / 2; const imageOpacity = () => props.player.health === 0 ? 0 : (1 + alpha()) / 2;
const transform = () => `translate(${scaledX()} ${scaledY()}) scale(${1 / props.scale})`;
const rotate = () => `rotate(${270 - props.player.angle})`;
return <g return <g
transform={`translate(${scaledX} ${scaledY}) scale(${1 / scale})`}> transform={transform()}>
<polygon points="-6,14 0, 16 6,14 0,24" fill="white" <polygon points="-6,14 0, 16 6,14 0,24" fill="white"
opacity={imageOpacity} opacity={imageOpacity()}
transform={`rotate(${270 - player.angle})`}/> transform={rotate()}/>
<circle r={16} stroke-width={1} stroke="white" fill={teamColor} <circle r={16} stroke-width={1} stroke="white" fill={teamColor()}
opacity={alpha}/> opacity={alpha()}/>
{getClassImage(player, imageOpacity)} {getClassImage(props.player, imageOpacity())}
</g> </g>
} }
function getClassImage(player: PlayerState, imageOpacity: number) { function getClassImage(player: PlayerState, imageOpacity: number) {
if (!classMap[player.playerClass]) { if (!classMap[player.playerClass]) {
return []; return [];
} }
const image = `/images/class_icons/${classMap[player.playerClass]}.svg`; const image = `/images/class_icons/${classMap[player.playerClass]}.svg`;
return <image href={image} return <image href={image}
class={"player-icon " + player.team} class={"player-icon " + player.team}
opacity={imageOpacity} opacity={imageOpacity}
height={32} height={32}
width={32} width={32}
transform={`translate(-16 -16)`}/> transform={`translate(-16 -16)`}/>
} }

View file

@ -42,8 +42,8 @@ export const Panner = (props: ParentProps<PannerProps>) => {
startX = event.pageX; startX = event.pageX;
startY = event.pageY; startY = event.pageY;
setTranslateX(panner.viewport.x); setTranslateX(Math.floor(panner.viewport.x));
setTranslateY(panner.viewport.y); setTranslateY(Math.floor(panner.viewport.y));
setScale(panner.scale); setScale(panner.scale);
} }
@ -66,9 +66,7 @@ export const Panner = (props: ParentProps<PannerProps>) => {
setTranslateY(panner.viewport.y); setTranslateY(panner.viewport.y);
setScale(panner.scale); setScale(panner.scale);
if (props.onScale) { props.onScale(panner.scale);
props.onScale(panner.scale);
}
} }
const mouseWheel = (event) => { const mouseWheel = (event) => {

View file

@ -42,6 +42,7 @@ use std::net::SocketAddr;
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
use std::sync::Arc; use std::sync::Arc;
use steam_openid::SteamOpenId; use steam_openid::SteamOpenId;
use tower_http::services::ServeDir;
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
use tracing::{error, info, info_span}; use tracing::{error, info, info_span};
use tracing_subscriber::{ use tracing_subscriber::{
@ -117,6 +118,7 @@ async fn main() -> Result<()> {
.route("/viewer", get(viewer)) .route("/viewer", get(viewer))
.route("/viewer/:id", get(viewer)) .route("/viewer/:id", get(viewer))
.route("/:id", get(demo)) .route("/:id", get(demo))
.nest_service("/images", ServeDir::new("images"))
.layer( .layer(
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| { TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
let matched_path = request let matched_path = request
@ -373,7 +375,6 @@ async fn viewer(
}; };
Ok(render(ViewerPage { demo }, session)) Ok(render(ViewerPage { demo }, session))
} }
async fn handler_404() -> impl IntoResponse { async fn handler_404() -> impl IntoResponse {
Error::NotFound Error::NotFound
} }