skip to Main Content

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):

Expected appearance (Chrome)


iPhone screen-cap (shows anomaly as described above):

Actual appearance (iPhone)

2

Answers


  1. 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 the g 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 the svg element itself, whereas all the elements with missing drop shadows, the class is being added to the direct child g 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 the g 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.

    Login or Signup to reply.
  2. 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 elements

    As 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:

    <svg height="0" width="0">
      <filter y="-50%" height="250%" id='dropshadowSvg' color-interpolation-filters="sRGB">
        <feDropShadow dx="5" dy="5" stdDeviation="5" flood-opacity="1" />
      </filter>
    </svg>
    

    and apply it via css rule/inline style

    .dropShadowSVG {
        filter: url(#dropshadowSvg);
    }
    
    :root{
          --background: lightblue;
        --popupBackground: white;
        --foreground: black;
        --dropShadowFilter: drop-shadow(10px 10px 5px black);
        --ballOutline: grey;
        --ballBlack: black;
    }
    
    svg{
      overflow:visible;
      width:25%;
    }
    
    
    .dropShadowCSS {
        filter: var(--dropShadowFilter);
    }
    
    .dropShadowSVG {
        filter: url(#dropshadowSvg);
    }
    <svg height="0" width="0">
      <filter y="-50%" height="250%" id='dropshadowSvg' color-interpolation-filters="sRGB">
        <feDropShadow dx="5" dy="5" stdDeviation="5" flood-opacity="1" />
      </filter>
    </svg>
    
    <div id="balls" style="padding: 10px;">
      <svg viewBox="0 0 120 120"  class="dropShadowCSS">
        <g>
          <circle cx="50" cy="50" r="48" style="fill: yellow; stroke-width: 1; stroke: var(--ballOutline);"></circle>
          <circle cx="50" cy="50" r="27" style="fill: white; stroke-width: 1; stroke: grey;"></circle><text x="50" y="53" text-anchor="middle" dominant-baseline="middle" font-weight="bold" font-size="40px">⏳</text>
        </g><text style="visibility: visible;" class="dropShadow" x="50" y="75" text-anchor="middle" dominant-baseline="middle" font-size="30px" fill="white" stroke-width="1" stroke="white"></text><text style="visibility: hidden;" class="dropShadow" x="40" y="90" font-size="80px" fill="green" stroke-width="1" stroke="white">✔</text>
      </svg>
    
      <svg viewBox="0 0 120 120"  class="dropShadowCSS">
        <g style="color:purple">
          <circle cx="50" cy="50" r="48" style="fill: #ccc; stroke-width: 1; stroke: grey ;">
          </circle>
          <text class="dropShadowSVG" x="50" y="50" text-anchor="middle" dominant-baseline="middle" font-size="20px" font-weight="bold" fill="currentColor" stroke-width="1">Time-out</text>
    
        </g>
      </svg>
      
      <svg viewBox="0 0 120 120" class="dropShadowCSS">
        <g style="color:purple">
          <circle cx="50" cy="50" r="48" style="fill: #ccc; stroke-width: 1; stroke: grey ;">
          </circle>
          <text class="dropShadowCSS" x="50" y="50" text-anchor="middle" dominant-baseline="middle" font-size="20px" font-weight="bold" fill="currentColor" stroke-width="1">Time-out</text>
    
        </g>
      </svg>
      
    
    </div>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search