I am working on a billiards scorekeeping app implemented in HTML/CSS/javascript at this repo:
https://github.com/Zenilogix-Carl/Site
I have been developing using Windows and Chrome, getting expected outputs or at least able to diagnose issues in that environment. I’ve seen it run on some friends’ iPhones and I get very different (and flawed) results. I am hoping to find someone who can help me identify and correct the issues affecting iPhone.
The three files relevant to my issue are:
- NineBall.html
- Billiards.js
- styles.css
I’ve attached screenshots below. Notice that in Chrome, all balls are rendered with a drop-shadow; this is defined in the BilliardBall class and extended into BilliardBallWithState (both in Billiards.js) and, depending on current state, may be turned on or off by function updateBallState in NineBall.html. Initial state should show the drop-shadow for all balls, which it does in Chrome, but fails to on iPhone.
class BilliardBall {
constructor(number, size, allowForShadow) {
var colors = ["yellow", "blue", "red", "purple", "orange", "green", "brown", "var(--ballBlack)"];
var color = Number.isInteger(number) ? colors[(number - 1) % 8] : colors[0];
var isStripe = Number.isInteger(number) ? (((number - 1) / 8) >= 1) : false;
this.number = number;
var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("viewBox", allowForShadow ? "0 0 120 120" : "0 0 105 100");
svg.setAttribute("width", size);
svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
var g = document.createElementNS("http://www.w3.org/2000/svg", "g");
this.ballGraphic = g;
var circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
if (isStripe) {
circle.setAttribute("cx", 50);
circle.setAttribute("cy", 50);
circle.setAttribute("r", 48);
circle.setAttribute("style", "fill: white;");
g.appendChild(circle);
var path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", "M 16 16 L 84 16 A 50 50 0 0 1 84 84 L 16 84 A 50 50 0 0 1 16 16 ");
path.setAttribute("style", "fill: " + color + "; stroke-width: 1; stroke: grey;");
g.appendChild(path);
circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
circle.setAttribute("cx", 50);
circle.setAttribute("cy", 50);
circle.setAttribute("r", 48);
circle.setAttribute("style", "fill: transparent; stroke-width: 1; stroke: var(--ballOutline);");
g.appendChild(circle);
} else {
circle.setAttribute("cx", 50);
circle.setAttribute("cy", 50);
circle.setAttribute("r", 48);
circle.setAttribute("style", `fill: ${color}; stroke-width: 1; stroke: var(--ballOutline);`);
g.appendChild(circle);
}
circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
circle.setAttribute("cx", 50);
circle.setAttribute("cy", 50);
circle.setAttribute("r", 27);
circle.setAttribute("style", "fill: white; stroke-width: 1; stroke: grey;");
g.appendChild(circle);
var text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.setAttribute("x", 50);
text.setAttribute("y", 53);
text.setAttribute("text-anchor", "middle");
text.setAttribute("dominant-baseline", "middle");
text.setAttribute("font-weight", "bold");
text.setAttribute("font-size", number > 9 ? "32px" : "40px");
text.innerHTML = number;
g.appendChild(text);
svg.appendChild(g);
this.element = svg;
}
}
class BilliardBallWithState extends BilliardBall {
constructor(number, size, clickFn, foulText) {
super(number, size, true);
const svg = this.element;
svg.onclick = function () { clickFn(number)};
let text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.setAttribute("style", "visibility: hidden;");
text.classList.add("dropShadow");
text.setAttribute("x", 50);
text.setAttribute("y", 50);
text.setAttribute("text-anchor", "middle");
text.setAttribute("dominant-baseline", "middle");
text.setAttribute("font-size", "30px");
text.setAttribute("fill", "red");
text.setAttribute("stroke-width", "1");
text.setAttribute("stroke", "white");
text.innerHTML = foulText;
svg.appendChild(text);
this.foulText = text;
text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.setAttribute("style", "visibility: hidden;");
text.classList.add("dropShadow");
text.setAttribute("x", 40);
text.setAttribute("y", 90);
text.setAttribute("font-size", "80px");
text.setAttribute("fill", "green");
text.setAttribute("stroke-width", "1");
text.setAttribute("stroke", "white");
text.innerHTML = "✔";
svg.appendChild(text);
this.checkMark = text;
this.showNormal();
}
dimElement(elem, dim) {
if (dim) {
elem.classList.remove("dropShadow");
elem.classList.add("dimmed");
} else {
elem.classList.add("dropShadow");
elem.classList.remove("dimmed");
}
}
showElement(elem, show) {
elem.style.visibility = show ? "visible" : "hidden";
}
showNormal() {
this.dimElement(this.ballGraphic, false);
this.showElement(this.foulText, false);
this.showElement(this.checkMark, false);
}
showPocketed(checked) {
this.dimElement(this.ballGraphic, true);
this.showElement(this.foulText, false);
this.showElement(this.checkMark, checked);
}
showFoul() {
this.dimElement(this.ballGraphic, true);
this.showElement(this.foulText, true);
this.showElement(this.checkMark, false);
}
}
function updateBallState(number) {
const currentBallState = match.ballStates[number - 1];
const ball = balls[number];
switch (currentBallState) {
case "normal":
ball.showNormal();
break;
case "dead":
ball.showFoul();
break;
default:
ball.showPocketed(currentBallState === "won");
break;
}
}
Also, although not reflecting in the screen caps, it seems that cookies are not working correctly on iPhone, so I am wondering if I am handling them correctly – see onLoad function in NineBall.html and Preferences class in Billiards.js.
I need to understand what I am doing that is not functional/uniformly supported across browsers and what I need to do for my app to run correctly in both Android (Chrome) and iPhone (presumably Safari) browsers.
Chrome screen-cap (expected appearance):
iPhone screen-cap (shows anomaly as described above):
2
Answers
Update:
After a fair amount of digging, I am satisfied with this answer in another Stack Overflow post.
It seems that the
g
element is intended only to be a container, and is not designed to have style applied to it. While most browsers work as expected, applying styling to theg
element directly does not appear to be officially supported functionality. As my initial answer suggests, it seems like the best course of action is to move your styling classes to the parent SVG element.I don’t yet have the why part of your issue, but it appears that when run on iOS, all of the elements that still have a drop shadow have the
dropShadow
class set on thesvg
element itself, whereas all the elements with missing drop shadows, the class is being added to the direct childg
element.Initially, I thought that iOS WebKit had nonstandard behavior for clipping SVG elements, but then I realized that your
dimmed
class also does not function on the iPhone.Tweaking your JavaScript to apply the classes to the
svg
element itself, rather than theg
element, should be a negligible change to your code that should get you on your way.I’m going to keep looking into this… I would like to know why iOS behaves this way.
Actually you can apply css styles to SVG
<g>
elements that will can inherited – but apparently webkit does not accept css filters applied to SVG child elements.I could reproduce this rendering issue in Epiphany/Web and Midori (running in a virtual Linux Mint environment).
Workaround: CSS drop shadow for parent
<svg>
SVG filter for child elementsAs suggested by Clint Warner you can apply the CSS drop shadow to the outer
<svg>
elements.Inner child elements can use a native SVG filter.
You could add an invisible svg containing a native svg filter like this:
and apply it via css rule/inline style