Compare commits

...

9 Commits

Author SHA1 Message Date
noah metz 54122c0c32 Event Changes 2024-02-03 14:50:59 -07:00
noah metz 8d9e9fcbc3 Merge remote-tracking branch 'github/master' into nm_live 2024-02-03 10:32:37 -07:00
noah metz 29ae52286e Live Fixes 2024-02-03 10:30:28 -07:00
Liam Conway cff83084b6
Update CSS for team/event/sponsor names 2024-02-03 10:30:13 -07:00
Liam Conway 978a3b1570
Very basic field name added + event name moved 2024-02-03 00:35:32 -07:00
Avinav Bhandari 2ff4ae934a added stream pre-game page 2024-02-02 19:29:49 -07:00
noah metz 6b850db777 Removed static assets and updated 2024-02-02 23:41:07 +00:00
noah metz ced9ba1384 Changed MQTT server to local and fixed offset 2024-02-02 18:27:47 +00:00
Liam Conway 8313d5de4e
Quick! Working admin panel 2024-02-02 02:07:43 -07:00
21 changed files with 592 additions and 37 deletions

1
.gitignore vendored

@ -8,3 +8,4 @@ node_modules
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
static/

@ -1,4 +1,4 @@
build/index.html: src/routes/+page.svelte src/routes/control/+page.svelte
build/index.html: src/routes/+page.svelte src/routes/control/+page.svelte src/routes/admin/+page.svelte static/gifs/* static/robots/*
npm run build
.PHONY: run dev iframe

@ -6,7 +6,7 @@ html{
background-color: pink;
}
body{
div{
margin: 0;
padding: 0;
display: flex;
@ -27,10 +27,14 @@ iframe{
}
</style>
</head>
<body>
<iframe src="index.html?field=1/1" width="1176" height="168"></iframe>
<iframe src="index.html?field=1/2" width="1176" height="168"></iframe>
<iframe src="index.html?field=1/3" width="896" height="128"></iframe>
<iframe src="index.html?field=1/4" width="896" height="128"></iframe>
</body>
<div>
<body>
<iframe src="index.html?field=2/4" width="896" height="128"></iframe>
<iframe src="index.html?field=2/5" width="1176" height="168"></iframe>
<iframe src="index.html?field=2/6" width="896" height="128"></iframe>
<iframe src="index.html?field=1/1" width="896" height="128"></iframe>
<iframe src="index.html?field=1/2" width="1176" height="168"></iframe>
<iframe src="index.html?field=1/3" width="896" height="128"></iframe>
</body>
</div>
</html>

@ -12,12 +12,20 @@ let field_score_topic;
let display_state_topic;
/** @type {string} */
let display_class_topic;
/** @type {string} */
let display_name_topic;
/** @type {string} */
let display_sponsor_topic;
/** @type {number} */
let score_midpoint_current = 50;
/** @type {number} */
let score_midpoint_setpoint = 50;
/** @type {string} */
let sponsor_state = "westmech";
let coeff_p = 0.1;
let red_anim_x = 0;
@ -40,14 +48,18 @@ let animation_timer_interval = setInterval(animation_timer, 10);
let event_name = "Mecha Mayhem 2024";
let match_name = "No Match";
let match_name_show = "No Match";
let round = "";
let number = "";
let score_red = 0;
let score_red_str = "";
let score_blue = 0;
let score_blue_str = "";
/** @type {string[]} */
let teams_red = [];
/** @type {string[]} */
let teams_blue = [];
/** @type {undefined | Date} */
/** @type {undefined | Number} */
let timer_end = undefined;
/** @type {undefined | ReturnType<typeof setInterval>} */
let timer_next_tick = undefined;
@ -56,10 +68,13 @@ let timer = "Next Match";
/** @type {string} */
let display_state = "init";
/** @type {string} */
let display_name = "default";
/** @type {string} */
let display_class = "side-display";
const client = mqtt.connect("ws://metznet.ca:8883");
const client = mqtt.connect("ws://10.42.0.36:8883");
client.on("connect", () => {
@ -88,6 +103,22 @@ client.on("connect", () => {
}
});
client.subscribe(display_name_topic, (err) => {
if (err) {
console.log(err);
} else {
console.log(`subscribed to ${display_name_topic}`);
}
});
client.subscribe(display_sponsor_topic, (err) => {
if (err) {
console.log(err);
} else {
console.log(`subscribed to ${display_sponsor_topic}`);
}
});
client.subscribe(field_metadata_topic, (err) => {
if (err) {
console.log(err);
@ -129,7 +160,8 @@ function tick_timer() {
}
const now = new Date()
const time_diff = timer_end.getTime() - now.getTime() + offset;
const time_diff = (timer_end*1000) - now.getTime() - offset;
console.log(`TIME_DIFF: ${time_diff} = ${timer_end} - ${now.getTime()} - ${offset}`);
if (time_diff <= 0) {
timer = "0:00";
stop_timer();
@ -145,56 +177,85 @@ client.on("message", (topic, message) => {
let topic_str = topic.toString();
let message_str = message.toString();
console.log(`${topic_str} - ${message_str}`);
switch(topic_str){
case "time":
let local = Date.now();
let server = JSON.parse(message_str) * 1000;
offset = local - server;
console.log(`NEW offset: ${local} - ${server} = ${offset}`);
offset = server - local;
if (timer != "Scoring" && timer != "Scored" && match_name_show != "Next Match") {
stop_timer();
tick_timer();
timer_next_tick = setInterval(tick_timer, 50);
}
console.log(`NEW_OFFSET: ${server} - ${local} = ${offset}`);
break;
case field_metadata_topic:
const field_obj = JSON.parse(message_str);
teams_red = [field_obj.red_teams[0], field_obj.red_teams[1]];
teams_blue = [field_obj.blue_teams[0], field_obj.blue_teams[1]];
let num = field_obj.tuple.match_num;
let round = field_obj.tuple.round;
round = field_obj.tuple.round;
if (round == "Qualification") {
round = "Qual";
}
match_name = `${round} ${num}`;
number = `${num}`;
break;
case field_state_topic:
stop_timer();
const state_obj = JSON.parse(message_str);
const start_ms = state_obj.start * 1000;
console.log(`Start_ms: ${start_ms}`);
const start_ms = state_obj.start;
switch(state_obj.state) {
case "Scheduled":
timer = match_name;
score_red_str = "";
score_blue_str = "";
score_blue = 0;
score_red = 0;
match_name_show = "Next Match";
display_state = "pre-game";
break;
case "Timeout":
timer = "Timeout";
match_name_show = match_name;
break;
case "Driver":
timer_end = new Date(start_ms + 105000);
timer_end = start_ms + 105;
tick_timer();
timer_next_tick = setInterval(tick_timer, 50);
match_name_show = match_name;
break;
case "DriverDone":
timer = "0:00";
timer = "Scoring";
score_red_str = "";
score_blue_str = "";
display_state = sponsor_state;
setTimeout(() => {
display_state = "timer";
}, 6000);
break;
case "Autonomous":
timer_end = new Date(start_ms + 15000);
if(display_state == "pre-game") {
display_state = "timer";
}
score_red = 0;
score_blue = 0;
score_red_str = "0";
score_blue_str = "0";
timer_end = start_ms + 15;
tick_timer();
timer_next_tick = setInterval(tick_timer, 50);
match_name_show = match_name;
break;
case "AutonomousDone":
timer = "0:00";
match_name_show = match_name;
timer = "Scoring";
score_red_str = "";
score_blue_str = "";
break;
case "Abandoned":
timer = "Abandoned";
@ -205,7 +266,10 @@ client.on("message", (topic, message) => {
match_name_show = match_name;
break;
case "Scored":
timer = "Scoring";
timer = "Scored";
score_red_str = `${score_red}`;
score_blue_str = `${score_blue}`;
display_state = "timer";
match_name_show = match_name;
break;
}
@ -214,11 +278,21 @@ client.on("message", (topic, message) => {
const score_obj = JSON.parse(message_str);
score_red = score_obj.red_total;
score_blue = score_obj.blue_total;
if (timer != "Scoring") {
score_red_str = `${score_red}`;
score_blue_str = `${score_blue}`;
}
score_midpoint_setpoint = ((score_red + 1) / (score_red + score_blue + 2)) * 100;
break;
case display_state_topic:
display_state = message_str;
break;
case display_sponsor_topic:
sponsor_state = message_str;
break;
case display_name_topic:
display_name = message_str;
break;
case display_class_topic:
switch(message_str){
case "side":
@ -248,6 +322,8 @@ onMount(() => {
field_score_topic = `field/${field_id}/score`;
display_state_topic = `display/${field_id}/state`;
display_class_topic = `display/${field_id}/class`;
display_name_topic = `display/${field_id}/name`;
display_sponsor_topic = `display/${field_id}/sponsor`;
});
</script>
@ -257,14 +333,14 @@ onMount(() => {
<div class="banner"></div>
{:else if (display_state == "timer")}
<div class="score-grid">
<p id="event-name">{event_name}</p>
<p id="red-name">Red Alliance</p>
<p id="blue-name">Blue Alliance</p>
<!-- <p id="event-name">{event_name}</p> -->
<p id="red-name">{event_name}</p>
<p id="blue-name">{display_name}</p>
<p id="timer">{timer}</p>
<p id="match">{match_name_show}</p>
<p id="score-red">{score_red}</p>
<p id="score-blue">{score_blue}</p>
<p id="score-red">{score_red_str}</p>
<p id="score-blue">{score_blue_str}</p>
<div class="teams" id="teams-red">
{#each teams_red as team}
@ -290,9 +366,41 @@ onMount(() => {
</div>
</div>
{:else if (display_state == "pre-game")}
<div id="pre-game"></div>
<img class="gif-background" alt="wavy" src="/gifs/wavy_background.gif">
<div class="pre-game">
<div class="pre-bot" id="pre-r1">
<p>{teams_red[0]}</p>
<object type="image/png" data="/robots/{teams_red[0]}.gif">
<img src="/robots/fallback.jpg">
</object>
</div>
<div class="pre-bot" id="pre-r2">
<p>{teams_red[1]}</p>
<object type="image/png" data="/robots/{teams_red[1]}.gif">
<img src="/robots/fallback.jpg">
</object>
</div>
<div id="pre-mid">
<p id="pre-mid-top">{round}</p>
<div id="pre-mid-vs"><p>VS</p></div>
<p id="pre-mid-bot">{number}</p>
</div>
<div class="pre-bot" id="pre-b1">
<p>{teams_blue[0]}</p>
<object type="image/png" data="/robots/{teams_blue[0]}.gif">
<img src="/robots/fallback.jpg">
</object>
</div>
<div class="pre-bot" id="pre-b2">
<p>{teams_blue[1]}</p>
<object type="image/png" data="/robots/{teams_blue[1]}.gif">
<img src="/robots/fallback.jgp">
</object>
</div>
</div>
{:else if (display_state == "event-name")}
<div class="banner"><p>{event_name}</p></div>
<img class="gif-background" alt="wavy" src="/gifs/wavy_background.gif">
<img class="gif-foreground" alt="Mecha Mayhem 2024" src="/gifs/mm2024.gif">
{:else if (display_state == "alliance-selection")}
<div class="banner"><p>210Y Selecting</p></div>
{:else if (display_state == "break")}
@ -333,7 +441,7 @@ onMount(() => {
<style>
@font-face {
font-family: "apple2";
src: url('/static/fonts/apple2.woff2') format('woff2');
src: url('/fonts/apple2.woff2') format('woff2');
}
:global(html){
@ -364,8 +472,9 @@ p {
.gif-foreground {
z-index: 1;
position: relative;
top: -103.3%;
position: absolute;
top: 0;
height: 100%;
width: 100%;
}
@ -374,8 +483,100 @@ p {
height: 100%;
}
#pre-mid-vs {
height: 60%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 3cqw;
}
#pre-mid-top {
height: 20%;
font-size: 2cqw;
}
#pre-mid-bot {
height: 20%;
font-size: 2cqw;
}
#pre-mid {
text-wrap: nowrap;
font-size: 7cqw;
grid-area: mid;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
flex-grow: 0;
flex-basis: min-content;
height: 100%;
width: 100%;
}
.pre-bot object {
height: 80cqh;
width: 80cqh;
}
.pre-bot object img {
height: 100%;
width: 100%;
}
.pre-bot {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
flex-grow: 0;
flex-basis: min-content;
height: 100%;
width: 100%;
}
#pre-b1 {
font-size: 2cqw;
grid-area: b1;
}
#pre-b2 {
font-size: 2cqw;
grid-area: b2;
}
#pre-r1 {
font-size: 2cqw;
grid-area: r1;
}
#pre-r2 {
font-size: 2cqw;
grid-area: r2;
}
.pre-game {
z-index: 1;
position: absolute;
top: 0;
height: 100%;
width: 100%;
display: grid;
justify-items: center;
align-items: center;
grid-template-columns: 22.5% 22.5% 10% 22.5% 22.5%;
grid-template-rows: 100%;
grid-template-areas: "r1 r2 mid b1 b2";
}
.gif-background {
z-index: 0;
position: absolute;
top: 0;
height: 100%;
width: 100%;
}
@ -409,7 +610,18 @@ p {
background-color: #ee0000;
}
.banner-gif {
z-index: 0;
position: absolute;
top: 0;
height: 100%;
width: 100%;
}
.banner {
z-index: 1;
position: absolute;
top: 0;
display: flex;
font-size: 5cqw;
align-items: center;
@ -420,7 +632,7 @@ p {
}
.teams {
width: 100%;
width: 125%;
height: 100%;
font-size: 2cqw;
display: flex;
@ -431,11 +643,13 @@ p {
#teams-red {
flex-direction: row;
grid-area: red-bot;
margin-left: 50px;
}
#teams-blue {
flex-direction: row-reverse;
grid-area: blue-bot;
margin-right: 50px;
}
#score-red {
@ -467,11 +681,16 @@ p {
#blue-name {
grid-area: blue-top;
font-size: 1.5cqw;
margin-right: 15px;
margin-top: 10px;
text-align: right;
}
#red-name {
grid-area: red-top;
font-size: 1.5cqw;
margin-left: 15px;
margin-top: 10px;
}
#timer {

@ -0,0 +1,104 @@
<script>
import mqtt from "mqtt";
// Consts
let topics = ['display/#', 'division/#'];
let states = ["init", "timer", "pre-game", "event-name", "alliance-selection", "break", "red-wins", "blue-wins", "prairies", "rockies", "ab-education", "ab-innovate", "tcenergy", "tourismcalgary", "westmech"];
let led_sizes = {
"center": { width: 1176, height: 168 },
"side": { width: 896, height: 128 }
}
const client = mqtt.connect("ws://10.42.0.36:8883");
let displays = {};
let division_names = {};
let divisions = {};
let active_division = "1";
client.on("connect", () => {
console.log("connected to mqtt");
topics.forEach((topic) => {
client.subscribe(topic, (err) => {
if (err) {
console.log(err);
} else {
console.log(`subscribed to topic '${topic}'`);
}
});
});
});
client.on("message", (topic, message) => {
let display_field_match = /^display\/(\d+)\/(\d+)\/(\w+)$/.exec(topic);
let division_metadata_match = /^division\/(\d+)\/(\w+)$/.exec(topic);
let division_stat_match = /^division\/(\d+)\/(\w+)\/(\d+)(?:\/(\w+))?$/.exec(topic);
if(division_metadata_match) {
let [_, division, type] = division_metadata_match;
switch(type) {
case "name":
division_names[division] = message.toString();
console.log(`Set division ${type} for ${division} to ${message.toString()}`)
console.log(divisions);
break;
default:
console.warn(`Unknown division metadata type: ${type}`);
}
} else if(division_stat_match) {
let [_, division, matchType, matchNumber, type] = division_stat_match;
type ||= "info";
divisions[division] ||= {};
divisions[division][matchType] ||= {};
divisions[division][matchType][matchNumber] ||= {};
divisions[division][matchType][matchNumber][type] ||= {};
} else if(display_field_match) {
let [_, division, field, type] = display_field_match;
displays[division] ||= {};
displays[division][field] ||= {};
displays[division][field][type] = message.toString();
console.log(displays[active_division]);
} else {
console.warn(`no match for topic ${topic}`);
}
});
</script>
<div class="displays">
<select bind:value={active_division} on:change={() => console.log(`Changed: ${active_division}`)}>
{#each Object.entries(division_names) as [division, name]}
<option value={division} selected={active_division === division}>{name}</option>
{/each}
</select>
{#if division_names[active_division] === undefined}
<p>Division {active_division} undefined</p>
{:else}
<div class="division">
<h2>{`${division_names[active_division]} Division`}</h2>
{#each Object.entries(displays[active_division]) as [field, field_data] }
<div class="field">
<h3>{field} - {field_data.name}</h3>
<iframe src="/?field={active_division}/{field}" width={led_sizes[field_data.class].width} height={led_sizes[field_data.class].height} title="Field {active_division}/{field}"></iframe>
<br />
<select bind:value={field_data.class} on:change={() => client.publish(`display/${active_division}/${field}/class`, field_data.class, { retain: true })}>
{#each Object.entries(led_sizes) as [size, _]}
<option value={size}>{size}</option>
{/each}
</select>
<select bind:value={field_data.state} on:change={() => client.publish(`display/${active_division}/${field}/state`, field_data.state, { retain: true })}>
{#each states as s}
<option value={s}>
{s}
</option>
{/each}
</select>
</div>
{/each}
</div>
{/if}
</div>

@ -1,7 +1,7 @@
<script>
import mqtt from "mqtt";
const client = mqtt.connect("ws://metznet.ca:8883");
const client = mqtt.connect("ws://10.42.0.36:8883");
/**
* @type any
@ -82,14 +82,14 @@ client.on("message", (topic, message) => {
{#each Object.entries(displays) as [name, info]}
<div>
<p>{name}</p>
<select bind:value={info.class} on:change={() => client.publish(`display/${name}/class`, info.class, {retain: true})}>
<select bind:value={info.class} on:change={() => client.publish(`display/${name}/class`, info.class, { retain: true })}>
{#each classes as c}
<option value={c}>
{c}
</option>
{/each}
</select>
<select bind:value={info.state} on:change={() => client.publish(`display/${name}/state`, info.state, {retain: true})}>
<select bind:value={info.state} on:change={() => client.publish(`display/${name}/state`, info.state, { retain: true })}>
{#each states as s}
<option value={s}>
{s}

@ -0,0 +1,227 @@
<script>
import mqtt from "mqtt";
let div_id;
/** @type {string[]} */
let red_teams = [];
let blue_teams = [];
let field_matches = [];
let latest_start_time = 0;
let active_field = 0;
let offset = 0;
let event_name = "Mecha Mayhem 2024";
let match_name = "No Match";
let match_name_show = "No Match";
/** @type {string[]} */
let teams_red = [];
/** @type {string[]} */
let teams_blue = [];
/** @type {undefined | Date} */
let timer_end = undefined;
/** @type {undefined | ReturnType<typeof setInterval>} */
let timer_next_tick = undefined;
let timer = "Next Match";
/** @type {string} */
let display_state = "init";
/** @type {string} */
let display_class = "side-display";
const client = mqtt.connect("ws://metznet.ca:8883");
function stop_timer() {
if(timer_next_tick === undefined) {
} else {
clearInterval(timer_next_tick);
timer_next_tick = undefined;
}
}
function tick_timer() {
if(timer_end === undefined) {
stop_timer();
return
}
const now = new Date()
const time_diff = timer_end.getTime() - now.getTime() + offset;
if (time_diff <= 0) {
timer = "0:00";
stop_timer();
} else {
const seconds_total = Math.ceil(time_diff/ 1000);
const minutes = Math.floor(seconds_total / 60);
const seconds = seconds_total - (minutes * 60);
timer = `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
}
client.on("message", (topic, message) => {
/** @type {RegExp} */
let field_metadata_topic = /^field\/\d+\/(\d+)$/;
/** @type {RegExp} */
let field_state_topic = /^field\/\d+\/(\d+)\/state$/;
let topic_str = topic.toString();
let message_str = message.toString();
let field_state_match = field_state_topic.exec(topic_str);
let field_metadata_match = field_metadata_topic.exec(topic_str);
console.log(`${topic_str} - ${message_str}`);
if (field_state_match) {
console.log("here")
let [_, field] = field_state_match;
const state_obj = JSON.parse(message_str);
if (state_obj.state != "Scheduled") { return; }
if (state_obj.start <= latest_start_time) { return; }
latest_start_time = parseFloat(state_obj.start);
active_field = parseInt(field);
console.log(`${latest_start_time}`);
console.log(`active field: ${active_field}`);
let field_obj = field_matches[active_field];
red_teams = [field_obj.red_teams[0], field_obj.red_teams[1]];
blue_teams = [field_obj.blue_teams[0], field_obj.blue_teams[1]];
}
else if (field_metadata_match) {
let [_, field] = field_metadata_match;
const field_obj = JSON.parse(message_str);
field_matches[parseInt(field)] = field_obj;
if(field == active_field) {
red_teams = [field_obj.red_teams[0], field_obj.red_teams[1]];
blue_teams = [field_obj.blue_teams[0], field_obj.blue_teams[1]];
}
}
else {
console.log(`${topic_str} not defined`);
}
// switch(topic_str){
// case "time":
// let local = Date.now();
// let server = JSON.parse(message_str) * 1000;
// offset = local - server;
// console.log(`NEW offset: ${local} - ${server} = ${offset}`);
// break;
// case field_metadata_topic:
// const field_obj = JSON.parse(message_str);
// teams_red = [field_obj.red_teams[0], field_obj.red_teams[1]];
// teams_blue = [field_obj.blue_teams[0], field_obj.blue_teams[1]];
// let num = field_obj.tuple.match_num;
// let round = field_obj.tuple.round;
// match_name = `${round} ${num}`;
// break;
// case field_state_topic:
// stop_timer();
// const state_obj = JSON.parse(message_str);
// const start_ms = state_obj.start * 1000;
// console.log(`Start_ms: ${start_ms}`);
// switch(state_obj.state) {
// case "Scheduled":
// timer = match_name;
// match_name_show = "Next Match";
// if (state_obj.start > latest_start_time) {
// latest_start_time = state_obj.start;
// active_field = parseFloat(topic_str.split("/")[2]);
// console.log(`${latest_start_time}`);
// console.log(`${active_field}`);
// }
// break;
// }
// break;
// default:
// console.log(`Unhandled topic ${topic_str}`)
// }
});
import { onMount } from 'svelte';
onMount(() => {
const search_params = new URLSearchParams(window.location.search);
div_id = search_params.get("division");
if (div_id === null) {
div_id = "default";
}
// field_metadata_topic = `field/${div_id}/+`;
// field_state_topic = `field/${div_id}/+/state`;
client.on("connect", () => {
console.log("connected to mqtt");
client.subscribe(`field/${div_id}/#`, (err) => {
if (err) {
console.log("failed to subscribe to field metadata")
console.log(err);
} else {
console.log(`subscribed`);
}
});
});
});
</script>
<div class="img-grid">
{#each Object.entries(red_teams) as [index, team]}
<div class="item">
<video autoplay loop muted playsinline src="./{team}.mp4" alt="video {team}"></video>
<div class="text">{team}</div>
</div>
{/each}
{#each Object.entries(blue_teams) as [index, team]}
<div class="item">
<video autoplay loop muted playsinline src="./{team}.mp4" alt="video {team}"></video>
<div class="text">{team}</div>
</div>
{/each}
</div>
<style>
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap');
.img-grid {
display: grid;
grid-template-columns: repeat(2, lfr);
grid-template-rows: repeat(2, lfr);
gap: 20px;
width: 1920px;
height: 1080px;
justify-content: center;
align-content: center;
margin: auto;
background-color: #2ae400;
font-family: 'Bebas Neue';
font-weight: 400;
}
.item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
img {
width: 100%;
height: auto;
}
.text {
margin-top: 10px;
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 820 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 739 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB