Skip to content

Commit

Permalink
✨ Allow dynamic height/width of IFAB soccer fields
Browse files Browse the repository at this point in the history
  • Loading branch information
nguyenank committed Jun 24, 2024
1 parent ea0d9e7 commit 8adee25
Show file tree
Hide file tree
Showing 12 changed files with 1,317 additions and 1,109 deletions.
1,951 changes: 1,032 additions & 919 deletions html/index.html

Large diffs are not rendered by default.

32 changes: 9 additions & 23 deletions html/soccer-ifab-m.html
Original file line number Diff line number Diff line change
Expand Up @@ -117,22 +117,13 @@ <h1>Shot-Plotter</h1>
<svg id="soccer-ifab-m-svg" xmlns="http://www.w3.org/2000/svg" width="100%" viewBox=" -1 -1 107 70">
<g id="transformations">
<clipPath id="clipBorder">
<path d=" M 0 0
L 105 0
L 105 68
L 0 68
Z" />
<rect width="105" height="68" />
</clipPath>
<path id="background" d="
M 0 0
L 105 0
L 105 68
L 0 68
Z" stroke="transparent" fill="#036602"></path>
<rect id="background" width="105" height="68" stroke="transparent" fill="#036602" />
<path id="halfway-line" d="M 52.5 0 L 52.5 68" stroke-width="0.12" stroke="white"></path>
<circle id="halfway-circle" cx="52.5" cy="34" r="9.15" fill="transparent" stroke-width="0.12" stroke="white">
</circle>
<g id="left-goal-area">
<g id="left-goal">
<path id="left-eighteen-yd-box" d="
M 0 13.84
L 16.5 13.84
Expand All @@ -154,16 +145,16 @@ <h1>Shot-Plotter</h1>
<path id="goal-line" d="
M 0 30.34
L 0 37.66" stroke-width="0.75" stroke="white" />
<path id="top-left-corner" d="
<path id="left-top-corner" d="
M 1 0
A 1 1 1 0 1 0 1
" stroke-width="0.12" stroke="white" fill="transparent"></path>
<path id="top-right-corner" d="
<path id="left-bottom-corner" d="
M 0 67
A 1 1 0 0 1 1 68
" stroke-width="0.12" stroke="white" fill="transparent"></path>
</g>
<g id="right-goal-area" transform="translate(105 68) rotate(180)">
<g id="right-goal" transform="translate(105 68) rotate(180)">
<path id="right-eighteen-yd-box" d="
M 0 13.84
L 16.5 13.84
Expand All @@ -185,21 +176,16 @@ <h1>Shot-Plotter</h1>
<path id="right-goal-line" d="
M 0 30.34
L 0 37.66" stroke-width="0.75" stroke="white" />
<path id="top-right-corner" d="
<path id="right-top-corner" d="
M 1 0
A 1 1 1 0 1 0 1
" stroke-width="0.12" stroke="white" fill="transparent"></path>
<path id="top-right-corner" d="
<path id="right-bottom-corner" d="
M 0 67
A 1 1 0 0 1 1 68
" stroke-width="0.12" stroke="white" fill="transparent"></path>
</g>
<path id="outside-perimeter" d="
M 0 0
L 105 0
L 105 68
L 0 68
Z" stroke-width="0.12" stroke="white" fill="transparent"></path>
<rect id="outside-perimeter" width="105" height="68" stroke-width="0.12" stroke="white" fill="transparent" />
</g>
</svg>
<!-- endinject -->
Expand Down
12 changes: 12 additions & 0 deletions index.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 51 additions & 0 deletions js/custom-setups/card-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
export function customCardSetup(s) {
if (_.startsWith(s.id, "soccer-ifab")) {
customSoccerCardSetup(s.id, s.appearance.width, s.appearance.height);
}
}

function customSoccerCardSetup(id, width, height) {
const card = d3.select(`#${id}`).attr("href", undefined);
const dim = card.select(".card-text").select(".dimensions");
const inYards = _.endsWith(id, "-yd");
dim.selectAll("*").remove();
dim.append("div").attr("class", "bold").text("Dimensions: ");
const widthField = dim.append("div");
widthField.append("label").attr("for", `${id}-width`).text("Width:");
const minWidth = inYards ? 100 : 90;
const maxWidth = inYards ? 130 : 120;
widthField
.append("input")
.attr("id", `${id}-width`)
.attr("type", "number")
.attr("name", `${id}-width`)
.attr("min", minWidth)
.attr("max", maxWidth)
.attr("value", width);
widthField.append("span").text(`(min: ${minWidth}, max: ${maxWidth})`);

const heightField = dim.append("div");
heightField.append("label").attr("for", `${id}-height`).text("Height:");
const minHeight = inYards ? 50 : 45;
const maxHeight = inYards ? 100 : 90;
heightField
.append("input")
.attr("id", `${id}-height`)
.attr("type", "number")
.attr("name", `${id}-height`)
.attr("min", minHeight)
.attr("max", maxHeight)
.attr("value", height);
heightField.append("span").text(`(min: ${minHeight}, max: ${maxHeight})`);

card.select("button").on("click", function () {
const width = d3.select(`#${id}-width`).property("value");
const height = d3.select(`#${id}-height`).property("value");
console.log(width, height);
let params = new URLSearchParams({
width: width,
height: height,
});
window.location.href = `./${id}?${params.toString()}`;
});
}
39 changes: 39 additions & 0 deletions js/custom-setups/config-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
export function customConfigSetup(config) {
if (_.startsWith(config.id, "soccer-ifab")) {
return customSoccerConfigSetup(config);
}
}

function customSoccerConfigSetup(config) {
const urlParams = new URLSearchParams(window.location.search);
const w = parseInt(urlParams.get("width")) || config.appearance.width;
const h = parseInt(urlParams.get("height")) || config.appearance.height;
config.appearance.width = w;
config.appearance.height = h;
config.goalCoords = [
[0, h / 2],
[w, h / 2],
];

const ice_hockey = {
width: "200",
circleR: "2",
polyR: "2.75",
fontSize: "0.15", //rem
strokeWidth: "0.5", //px
heatMapScale: 1.25,
};

const scaleFactor = parseFloat(ice_hockey.width) / Math.max(w, h);
for (const key in ice_hockey) {
if (key === "heatMapScale") {
config.appearance[key] = parseFloat(ice_hockey[key]) * scaleFactor;
} else {
const val = parseFloat(ice_hockey[key]) / scaleFactor;
const suffix =
key === "fontSize" ? "rem" : key === "strokeWidth" ? "px" : "";
config.appearance[key] = `${val.toFixed(3)}${suffix}`;
}
}
return config;
}
90 changes: 90 additions & 0 deletions js/custom-setups/playing-area-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { sport, cfgSportA } from "../../setup.js";

export function customPlayingAreaSetup() {
if (_.startsWith(sport, "soccer-ifab")) {
customSoccerPlayingAreaSetup();
}
}

function customSoccerPlayingAreaSetup() {
const inYards = _.endsWith(sport, "-yd");

const minWidth = inYards ? 100 : 90;
const maxWidth = inYards ? 130 : 120;

const minHeight = inYards ? 50 : 45;
const maxHeight = inYards ? 100 : 90;

let w = cfgSportA.width;
w = w < minWidth || w > maxWidth ? 0 : w;
let h = cfgSportA.height;
h = h < minHeight || h > maxHeight || h >= w ? 0 : h;

const halfw = w / 2;
const halfh = h / 2;

const goal = inYards ? 8 : 7.32;
const halfgoal = goal / 2;
const eighteenyd = inYards ? 18 : 16.5;
const goalarea = inYards ? 6 : 5.5;
const arc = inYards ? 8 : 7.312;

const svg = d3.select(`#${sport}-svg`);
svg.attr("viewBox", `-1 -1 ${w + 2} ${h + 2}`);

const trans = svg.select("#transformations");

trans.select("clipPath").select("rect").attr("width", w).attr("height", h);
trans.select("#background").attr("width", w).attr("height", h);

trans.select("#halfway-line").attr("d", `M ${halfw} 0 L ${halfw} ${h}`);
trans.select("#halfway-circle").attr("cx", halfw).attr("cy", halfh);

trans.select("#outside-perimeter").attr("width", w).attr("height", h);

for (const dir of ["left", "right"]) {
const ga = trans.select(`#${dir}-goal`);
if (dir === "right") {
ga.attr("transform", `translate(${w} ${h}) rotate(180)`);
}

ga.select(`#${dir}-eighteen-yd-box`).attr(
"d",
`M 0 ${halfh - halfgoal - eighteenyd}
L 18 ${halfh - halfgoal - eighteenyd}
L 18 ${halfh + halfgoal + eighteenyd}
L 0 ${halfh + halfgoal + eighteenyd}`
);

ga.select(`#${dir}-goal-area`).attr(
"d",
`M 0 ${halfh - halfgoal - goalarea}
L 6 ${halfh - halfgoal - goalarea}
L 6 ${halfh + halfgoal + goalarea}
L 0 ${halfh + halfgoal + goalarea}`
);

ga.select(`#${dir}-penalty-kick-mark`).attr("cy", halfh);

ga.select(`#${dir}-goal-arc`).attr(
"d",
`M 18 ${halfh - arc}
A 10 10 1 0 1 18 ${halfh + arc}
`
);

ga.select(`#${dir}-goal-line`).attr(
"d",
`M 0 ${halfh - halfgoal}
L 0 ${halfh + halfgoal}
`
);

ga.select(`#${dir}-bottom-corner`).attr(
"d",
`M 0 ${h - 1}
A 1 1 0 0 1 1 ${h}
`
);
}
}
80 changes: 2 additions & 78 deletions js/playing-area.js
Original file line number Diff line number Diff line change
@@ -1,84 +1,9 @@
import { sport, cfgSportA, cfgSportCustomSetup } from "../setup.js";

function generateCustomSoccerSVG() {
console.log("hit");
const inYards = _.endsWith(sport, "-yd");
const w = cfgSportA.width;
const h = cfgSportA.height;
const halfw = w / 2;
const halfh = h / 2;

const goal = inYards ? 8 : 7.32;
const halfgoal = goal / 2;
const eighteenyd = inYards ? 18 : 16.5;
const goalarea = inYards ? 6 : 5.5;
const arc = inYards ? 8 : 7.312;

const svg = d3.select(`#${sport}-svg`);
svg.attr("viewBox", `-1 -1 ${w + 2} ${h + 2}`);

const trans = svg.select("#transformations");

trans.select("clipPath").select("rect").attr("width", w).attr("height", h);
trans.select("#background").attr("width", w).attr("height", h);

trans.select("#halfway-line").attr("d", `M ${halfw} 0 L ${halfw} ${h}`);
trans.select("#halfway-circle").attr("cx", halfw).attr("cy", halfh);

trans.select("#outside-perimeter").attr("width", w).attr("height", h);

for (const dir of ["left", "right"]) {
const ga = trans.select(`#${dir}-goal`);
if (dir === "right") {
ga.attr("transform", `translate(${w} ${h}) rotate(180)`);
}

ga.select(`#${dir}-eighteen-yd-box`).attr(
"d",
`M 0 ${halfh - halfgoal - eighteenyd}
L 18 ${halfh - halfgoal - eighteenyd}
L 18 ${halfh + halfgoal + eighteenyd}
L 0 ${halfh + halfgoal + eighteenyd}`
);

ga.select(`#${dir}-goal-area`).attr(
"d",
`M 0 ${halfh - halfgoal - goalarea}
L 6 ${halfh - halfgoal - goalarea}
L 6 ${halfh + halfgoal + goalarea}
L 0 ${halfh + halfgoal + goalarea}`
);

ga.select(`#${dir}-penalty-kick-mark`).attr("cy", halfh);

ga.select(`#${dir}-goal-arc`).attr(
"d",
`M 18 ${halfh - arc}
A 10 10 1 0 1 18 ${halfh + arc}
`
);

ga.select(`#${dir}-goal-line`).attr(
"d",
`M 0 ${halfh - halfgoal}
L 0 ${halfh + halfgoal}
`
);

ga.select(`#${dir}-bottom-corner`).attr(
"d",
`M 0 ${h - 1}
A 1 1 0 0 1 1 ${h}
`
);
}
}
import { customPlayingAreaSetup } from "./custom-setups/playing-area-setup.js";

function setUpPlayingArea() {
if (cfgSportCustomSetup) {
if (_.startsWith(sport, "soccer-ifab")) {
generateCustomSoccerSVG();
}
customPlayingAreaSetup();
}
// dimensions of padding, window and playing area
const padding = 20;
Expand All @@ -97,7 +22,6 @@ function setUpPlayingArea() {

if (paWidth / paHeight < 1.3) {
const adjFactor = 0.6 + (Math.max(paWidth / paHeight, 1) - 1);
console.log(adjFactor);
resize = (Number(resize) * adjFactor).toFixed(1);
}

Expand Down
Loading

0 comments on commit 8adee25

Please sign in to comment.