Skip to content

Instantly share code, notes, and snippets.

@esaramago
Created March 25, 2021 15:34
Show Gist options
  • Save esaramago/92417453da582b9a71de05ffdffcf644 to your computer and use it in GitHub Desktop.
Save esaramago/92417453da582b9a71de05ffdffcf644 to your computer and use it in GitHub Desktop.
Game Timeline 2
<game-timeline
parts='[
{
"time": 45,
"extraTime": 4
},
{
"time": 45,
"extraTime": 9
},
{
"time": 15,
"extraTime": 2
},
{
"time": 15,
"extraTime": 2
}
]'
events-home='[
{
"time": 2,
"extraTime": 0,
"icon": "A",
"description": "Golo marcado"
},
{
"time": 45,
"extraTime": 4,
"icon": "B",
"description": "Golo marcado"
},
{
"time": 66,
"extraTime": 0,
"icon": "C",
"description": "Golo marcado"
},
{
"time": 120,
"extraTime": 2,
"icon": "D",
"description": "Golo marcado"
}
]'
events-away='[
{
"time": 99,
"extraTime": 0,
"icon": "A",
"description": "Golo marcado"
}
]'
time="90"
extra-time="2">
</game-timeline>
<!--
-->
class GameTimeline extends HTMLElement {
constructor() {
super();
// element created
this.classList.add("c-game-timeline");
this.sliderEl = undefined;
this.timeEl = undefined;
this.fullTime = null;
}
render() {
const sliderValue = this.calculateSliderValue(this.parts, this.time, this.extraTime);
this.fullTime = this.calculateFullTime(this.parts);
this.innerHTML = `
<ul class="c-game-timeline__events">
${this.renderEvents(this.eventsHome)}
</ul>
<div class="c-game-timeline__line">
<input class="c-game-timeline__input js-game-timeline-input" type="range" min="0" max="${this.fullTime}" value="${sliderValue}">
<div class="c-game-timeline__track">
${this.renderTicks(this.parts)}
</div>
<span class="c-game-timeline__label js-game-timeline-time" style="left: ${this.calculateLabelPosition(sliderValue, this.fullTime)}">
<span class="is-visually-hidden">Tempo de jogo:</span>
<span>${this.setLabelText(sliderValue)}</span>
</span>
</div>
<ul class="c-game-timeline__events">
${this.renderEvents(this.eventsAway)}
</ul>
`;
// set elements
this.sliderEl = this.querySelector(".js-game-timeline-input");
this.timeEl = this.querySelector(".js-game-timeline-time");
// set events
const that = this;
this.sliderEl.addEventListener("input", this.onChangeCurrentTime.bind(that, this));
this.sliderEl.addEventListener("change", this.onChangeCurrentTime.bind(that, this));
}
// Events
onChangeCurrentTime(that, event) {
const time = this.calculateCurrentTime(this.parts, event.target.value);
this.time = time.time;
this.extraTime = time.extraTime;
this.updateLabel();
}
// Methods
calculateSliderValue(parts, time, extraTime) {
var gameTime = 0;
var totalExtraTime = 0;
for (let part in parts) {
const partTime = parts[part].time;
const partExtraTime = parts[part].extraTime;
gameTime += partTime;
if (time <= (gameTime + totalExtraTime)) {
// return (event time) + (total extra times passed) + (event extra time)
return time + totalExtraTime + extraTime;
}
else if (time <= (gameTime + totalExtraTime + partExtraTime)) {
// return (event time) + (total extra times passed) + (current part extra time) + (event extra time)
return time + totalExtraTime + partExtraTime + extraTime;
}
totalExtraTime += partExtraTime;
}
}
calculateCurrentTime(parts, sliderValue) {
var gameTime = 0;
var totalExtraTime = 0;
for (let part in parts) {
const time = parts[part].time;
const extraTime = parts[part].extraTime;
gameTime += time;
if (sliderValue <= (gameTime + totalExtraTime)) {
return {
time: sliderValue - totalExtraTime,
extraTime: 0
};
}
else if (sliderValue <= (gameTime + totalExtraTime + extraTime)) {
const partExtraTime = sliderValue - totalExtraTime - gameTime;
return {
time: gameTime,
extraTime: partExtraTime
};
}
totalExtraTime += extraTime;
}
}
calculateFullTime(parts) {
var fullTime = 0;
parts.forEach(part => {
fullTime = fullTime + part.time + part.extraTime;
});
return fullTime;
}
updateTime(newValue) {
if(this.sliderEl) {
this.sliderEl.value = this.calculateCurrentTime(this.parts, newValue);
}
this.updateLabel();
}
updateLabel() {
if(this.timeEl) {
this.timeEl.textContent = this.setLabelText(this.sliderEl.value);
this.timeEl.style.left = this.calculateLabelPosition(this.sliderEl.value, this.fullTime);
}
}
calculateLabelPosition(currentTime, fullTime) {
return (currentTime * 100) / fullTime + "%";
}
setLabelText(sliderValue) {
const time = this.calculateCurrentTime(this.parts, sliderValue);
if(time.extraTime) {
return `${time.time}+${time.extraTime}'`;
}
else {
return `${time.time}'`;
}
}
renderTicks(parts) {
var timePassed = 0;
var html = "";
parts.forEach((part, index) => {
const time = part.time;
const extraTime = part.extraTime;
const isLastPart = index === this.parts.length-1;
const width = this.calculateExtraTimeWidth(extraTime);
if(isLastPart) {
html += `<span class="c-game-timeline__extra-time" style="right: 0%; width: ${width}"></span>`;
}
else {
const left = this.calculateExtraTimePosition(time, timePassed);
html += `<span class="c-game-timeline__extra-time" style="left: ${left}; width: ${width}"></span>`;
}
timePassed = timePassed + time + extraTime;
});
return html;
}
calculateExtraTimeWidth(extraTime) {
if(!extraTime) return 0;
return (extraTime * 100) / this.fullTime + "%";
}
calculateExtraTimePosition(time, timePassed) {
if(!time) return 0;
return ((time + timePassed) * 100) / this.fullTime + "%";
}
renderEvents(events) {
if(!events) return '';
var html = '';
events.forEach(event => {
const minute = `${event.time} ${(event.extraTime ? '+' + event.extraTime : '')}'`;
html += `
<li data-role="tooltip" title="<div class=u-font-semibold>${minute}</div> ${event.description}" style="left: ${this.calculateEventPosition(event)}">
<span>${event.icon}</span>
</li>`
});
/*
<svg class="c-icon--xs u-align-middle">
<use xlink:href="Content/images/sprite.svg#${event.icon}"></use>
</svg>
*/
return html;
}
calculateEventPosition(event) {
const eventTime = this.calculateSliderValue(this.parts, event.time, event.extraTime);
return (eventTime * 100) / this.fullTime + "%";
}
parseArray(attribute) {
const string = this.getAttribute(attribute);
if(string) {
let array = JSON.parse(string);
// fix time and extra time
array.time = Number(array.time) || 0;
array.extraTime = Number(array.extraTime) || 0;
return array;
}
}
connectedCallback() {
this.render();
}
static get observedAttributes() {
return ["current-time"];
}
attributeChangedCallback(name, oldValue, newValue) {
if(name === "time") {
this.updateTime(newValue, this.extraTime);
} else if (name === "extraTime") {
this.updateTime(this.time, newValue);
}
}
get parts() {
return this.parseArray("parts");
}
get time() {
return Number(this.getAttribute("time")) || 0;
}
get extraTime() {
return Number(this.getAttribute("extra-time")) || 0;
}
get eventsHome() {
return this.parseArray("events-home");
}
get eventsAway() {
return this.parseArray("events-away");
}
set time(newValue) {
this.setAttribute("time", newValue);
}
set extraTime(newValue) {
this.setAttribute("extraTime", newValue);
}
}
customElements.define("game-timeline", GameTimeline);
:root {
--color-main-500: #c40002;
--color-grey-400: #CCC;
--color-grey-500: #DDD;
--color-grey-800: #AAA;
}
html {
font-size: 62.5%;
}
body {
font-size: 1.4rem;
font-family: "Segoe UI", serif;
}
.is-visually-hidden {
opacity: 0;
position: absolute;
}
@mixin gameTimelineHandler { // handler
-webkit-appearance: none;
position: relative;
height: calc(var(--handler-size) * 2);
width: var(--handler-size);
border-radius: 4px;
background: var(--color-main-500);
box-shadow:
0 0 4px 1px rgb(0 0 0 / 30%),
inset 0 0 2px 2px rgb(255 255 255 / 40%);
cursor: e-resize;
}
@mixin gameTimelineMinuteMark {
content: "";
position: absolute;
right: 0;
display: block;
width: .2rem;
height: calc(var(--track-height) + .4rem);
margin: -.2rem auto 0;
background-color: var(--color-main-500);
}
.c-game-timeline {
--track-height: .8rem;
--handler-size: 1.2rem;
position: relative;
display: block;
max-width: 100rem;
padding: 2.4rem 0;
margin: auto;
}
.c-game-timeline__line {
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: 3.2rem;
margin: 0 calc(var(--handler-size) * 0.5);
}
.c-game-timeline__input {
-webkit-appearance: none;
position: absolute;
z-index: 1;
width: calc(100% + var(--handler-size));
height: 2rem;
background: transparent;
outline: 0;
&::-webkit-slider-thumb {
@include gameTimelineHandler;
}
&::-moz-range-thumb {
@include gameTimelineHandler;
}
&::-ms-thumb {
@include gameTimelineHandler;
position: relative;
margin-top: 0;
z-index: 2;
}
}
.c-game-timeline__track {
position: relative;
width: 100%;
height: var(--track-height);
background: var(--color-grey-500);
// first minute tick
&::before {
@include gameTimelineMinuteMark;
left: 0;
right: auto;
}
}
.c-game-timeline__extra-time {
position: absolute;
height: var(--track-height);
background-color: var(--color-grey-800);
&::after {
@include gameTimelineMinuteMark;
}
}
.c-game-timeline__label {
position: absolute;
bottom: calc(100% + 1rem);
display: flex;
justify-content: center;
width: var(--handler-size);
margin-left: calc(var(--handler-size) * -0.5 + .2rem);
font-weight: bold;
text-align: center;
white-space: nowrap;
}
.c-game-timeline__events {
position: relative;
margin: 0 calc(var(--handler-size) * 0.5);
> li {
position: absolute;
display: flex;
justify-content: center;
width: 0;
line-height: 0;
cursor: default;
&::before {
content: "";
position: absolute;
bottom: -1.2rem;
left: 50%;
width: 1px;
height: .8rem;
background: var(--color-grey-400);
}
}
~ .c-game-timeline__events {
> li {
bottom: 0;
&::before {
top: -1.2rem;
bottom: auto;
}
}
}
}
@esaramago
Copy link
Author

Game Timeline

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment