Bubbles


More HTML/JavaScript graphics experimentation, this time with SVG animation. Click/tap and drag in the frame below.


Source:

<svg id="mySvg" viewBox="0 0 400 300" style="border: solid 1px black;
    width:100%; aspect-ratio: 4/3"></svg>

<script>
    var svg = document.getElementById("mySvg"),
        svgViewRect, clientViewRect, // svg and client coordinates
        currMouseDown = null,
        mouseDownIdleID = 0;

    const initialMouseTimeout = 250, fasterMouseTimeout = 25;

    function animate(element, attrName, to, dur, repeatCount, fill = null) {
        // animates SVG element attribute from its current value towards "to" value over timespan "dur"
        var animation = document.createElementNS("http://www.w3.org/2000/svg", "animate");

        animation.setAttribute("attributeName", attrName);
        animation.setAttribute("to", to);
        animation.setAttribute("dur", dur);
        animation.setAttribute("repeatCount", repeatCount);
        if (fill) animation.setAttribute("fill", fill);
        animation.setAttribute("begin", svg.getCurrentTime());
        element.appendChild(animation);
    }

    function blurListener(event) {
        if (currMouseDown) {
            clearTimeout(mouseDownIdleID);
            currMouseDown = null;
            mouseDownIdleID = 0;
        }
    }

    function clickListener(event) {
    }

    function clientToSVGcoords({ x, y }) {
        return {
            x: (svgViewRect.width * (arguments[0].x - clientViewRect.left)) / clientViewRect.width,
            y: (svgViewRect.height * (arguments[0].y - clientViewRect.top)) / clientViewRect.height
        };
    }

    function containerResized() {
        svgCoordinatesChanged();
    }

    function focusListener(event) {
    }

    function loadListener(event) {
    }

    function mouseDownIdle() {
        // perform this action while mouse is held down in same spot for longer than initialMouseTimeout
        randomCircle(clientToSVGcoords({ x: currMouseDown.clientX, y: currMouseDown.clientY }));

        mouseDownIdleID = setTimeout(mouseDownIdle, fasterMouseTimeout); // repeat faster after initial timeout
    }

    function mouseDownListener(event) {
        if (event.buttons & 1) { // if left click...

            svg.focus(); // note: svg.focus to generate the event, not focusListener()
            currMouseDown = event;
            mouseDownIdleID = setTimeout(mouseDownIdle, initialMouseTimeout);

            randomCircle(clientToSVGcoords({ x: event.clientX, y: event.clientY }))

            event.preventDefault();
            event.stopPropagation();
        } else {
            // do something for other buttons?
        }
    }

    function mouseMoveListener(event) {
        // listen for this outside element; it may be part of a mousedown started inside
        if (currMouseDown) {
            randomCircle(clientToSVGcoords({ x: event.clientX, y: event.clientY }));

            clearTimeout(mouseDownIdleID);
            currMouseDown = event; // update current mousedown position and reset idle timout
            mouseDownIdleID = setTimeout(mouseDownIdle, initialMouseTimeout);

            event.preventDefault();
            event.stopPropagation();
            // prevent default, otherwise mousing down out of element may select neighboring content
        }
    }

    function mouseUpListener(event) {
        // listen for this outside element; it may be part of a mousedown started inside
        if (currMouseDown) {
            clearTimeout(mouseDownIdleID);
            currMouseDown = null;
            mouseDownIdleID = 0;
        }
    }

    function randomCircle({ x, y }, rMin = 15, rMax = 40, dur = 5) {
        // creates a randomly sized and colored SVG circle at x,y moving in a random direction
        // opacity decreasing with duration "dur" in seconds
        // circle is removed by a timeout callback at the end of that time, when completely transparent
        var circle, r;

        r = (rMax - rMin) * Math.random() + rMin;
        circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
        circle.setAttribute("cx", arguments[0].x);
        circle.setAttribute("cy", arguments[0].y);
        circle.setAttribute("r", r);
        circle.setAttribute("fill", "#" + (((1 << 24) * Math.random()) | 0).toString(16));
        svg.appendChild(circle);
        animate(circle, "cx", Math.random() * svgViewRect.width + svgViewRect.x, `${dur}s`, "indefinite");
        animate(circle, "cy", Math.random() * svgViewRect.height + svgViewRect.y, `${dur}s`, "indefinite");
        animate(circle, "opacity", "0", `${dur}s`, "1", "freeze");

        setTimeout(function () { svg.removeChild(circle) }, dur * 1000);

        return circle;
    }

    function scrollResizeListener(event) {
        svgCoordinatesChanged();
    }

    function svgCoordinatesChanged() {
        let viewBoxArray = svg.getAttribute("viewBox").split(" ");
        svgViewRect = new DOMRect(viewBoxArray[0], viewBoxArray[1], viewBoxArray[2], viewBoxArray[3]);

        clientViewRect = svg.getBoundingClientRect(); // includes padding and border, excludes margin

        // convert to rectangle of content area, excluding padding and border:
        let style = getComputedStyle(svg);
        let leftOffset = parseInt(style.paddingLeft) + parseInt(style.borderLeft);
        let topOffset = parseInt(style.paddingTop) + parseInt(style.borderTop);
        clientViewRect.x += leftOffset;
        clientViewRect.y += topOffset;
        clientViewRect.width -= leftOffset + parseInt(style.paddingRight) + parseInt(style.borderRight);
        clientViewRect.height -= topOffset + parseInt(style.paddingBottom) + parseInt(style.borderBottom);
    }

    function touchCancelEndListener(event) {
        // listen for this outside element; it may be part of a mousedown started inside
        if (currMouseDown) {
            clearTimeout(mouseDownIdleID);
            currMouseDown = null;
            mouseDownIdleID = 0;
            event.preventDefault();
            event.stopPropagation();
        }
    }

    function touchMoveListener(event) {
        if (currMouseDown) {
            currMouseDown = event;

            for (var i = 0, t = event.touches; i < t.length; ++i) {
                randomCircle(clientToSVGcoords({ x: t.item(i).clientX, y: t.item(i).clientY }));
            }
            event.preventDefault();
            event.stopPropagation();
        }
    }

    function touchStartListener(event) {
        svg.focus();
        currMouseDown = event;

        for (var i = 0, t = event.touches; i < t.length; ++i) {
            randomCircle(clientToSVGcoords({ x: t.item(i).clientX, y: t.item(i).clientY }));
        }
        event.preventDefault();
        event.stopPropagation();
    }

    function initialize() {
        window.addEventListener("mousemove", mouseMoveListener);
        window.addEventListener("mouseup", mouseUpListener);
        window.addEventListener("scroll", scrollResizeListener);
        window.addEventListener("resize", scrollResizeListener);
        window.addEventListener("touchmove", touchMoveListener);
        window.addEventListener("touchend", touchCancelEndListener);
        window.addEventListener("touchcancel", touchCancelEndListener);
        svg.addEventListener("focus", focusListener);
        svg.addEventListener("blur", blurListener);
        svg.addEventListener("load", loadListener);
        svg.addEventListener("mousedown", mouseDownListener);
        svg.addEventListener("touchstart", touchStartListener);
        svg.addEventListener("click", clickListener);
        // click happens after mouseup over same element where mousedown occured

        // neighboring elements may affect svg size/position, so monitor common container
        new ResizeObserver(containerResized).observe(svg.parentNode);
    }

    initialize();
</script>

Comments