How can I simulate firefox animation behaviour on chrome when using calcMode=”linear”?

I have two charts, the first one plots two functions, predators and prey. Each containing a circle with an animateMotion that goes along the function path.

The other chart is a phase curve generated by the functions. There is also a circle with an animateMotion that goes along.

Markers on the first graph should have constant speed with respect to X axis. The marker on the second graph should follow along the Y values of the first ones.

On firefox, using calcMode="linear" makes it work:
firefox linear animation.

On chrome, calcMode="linear" behaves the same way as calcMode="paced", so speed is constant along the curve:
chrome “linear” animation.

Including a somewhat ‘minimal’ piece of code, I kept the axis on so it’s easier to understand what’s going on.

<script src="https://d3js.org/d3.v4.js"></script>

<div style="min-width: 100px; max-width: 450px; width:100%">
    <div id="prey_predator_chart" style="width:100%;">
    </div>
    <div id="prey_predator_phase_chart" style="width:100%;">
    </div>
</div>
<script>
let prey_predator = {
    prey_color: "blue",
    predator_color: "green",
    phase_curve_color: "red",
    draw_graph: function() {
        // set the dimensions and margins of the graph
        var margin = {top: 0, right: 40, bottom: 40, left: 40},
            width = 450 - margin.left - margin.right,
            height = 400 - margin.top - margin.bottom;

        var total_width = width + margin.left + margin.right;
        var total_height = height + margin.top + margin.bottom;

        // line graph
        var svg_pop = d3.select("#prey_predator_chart")
            .append("svg")
                .attr("viewBox", "0 0 " + total_width + " " + total_height)
            .append("g")
                .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

        // phase graph
        svg_pop_phase = d3.select("#prey_predator_phase_chart")
            .append("svg")
                .attr("viewBox", "0 0 " + total_width + " " + total_height)
            .append("g")
                .attr("transform", "translate(" + margin.left + "," + margin.top + ")");


        var xDomain = [0,40];
        var yDomain = [0,30];

        var prey_c = {i_density: 10, growth: 1.1, death: 0.4}
        var predator_c = {i_density: 10, growth: 0.1, death: 0.4}
        
        var yScale = d3.scaleLinear().range([height,0]).domain(yDomain);
        var xScale = d3.scaleLinear().range([0,width]).domain(xDomain);

        var yScale_phase = d3.scaleLinear().range([height,0]).domain(yDomain);
        var xScale_phase = d3.scaleLinear().range([0,width]).domain(yDomain);

        var eps = 0.0005
        var x_space = d3.range(xDomain[0], xDomain[1], eps)

        var prey_growth = function(current_prey, current_predator, eps) {
            return prey_c.growth*eps*current_prey - prey_c.death*current_prey*eps*current_predator
        }
        var predator_growth = function(current_prey, current_predator, eps) {
            return predator_c.growth*current_prey*eps*current_predator - predator_c.death*eps*current_predator
        }

        var preys = []
        var predators = []
        preys = [prey_c.i_density]
        predators = [predator_c.i_density]

        x_space.forEach((_, i) => {
            preys.push(preys[i] + prey_growth(preys[i], predators[i], eps))
            predators.push(predators[i] + predator_growth(preys[i], predators[i], eps))
        });

        var c_preys = d3.line()
                .x(function(i) { return xScale(x_space[i]) })
                .y(function(i) { return yScale(preys[i]) })

        var c_predators = d3.line()
            .x(function(i) { return xScale(x_space[i]) })
            .y(function(i) { return yScale(predators[i]) })

        var c_phase = d3.line()
            .x(function(i) {return xScale_phase(preys[i])})
            .y(function(i) {return yScale_phase(predators[i])})

        predators_curve = svg_pop.append('path')
            .attr('stroke', this.predator_color)
            .attr('fill', 'none')
            .attr('stroke-width', 2).attr('d', c_predators(d3.range(0, x_space.length, 1)));

        predators_marker = svg_pop.append('circle')
            .attr('r', 3)
            .attr('stroke', this.predator_color)
        predators_marker.append('animateMotion')
            .attr('repeatCount', 'indefinite')
            .attr('fill', 'freeze')
            .attr('calcMode','linear')
            .attr('dur', '10s')
            .attr('path', c_predators(d3.range(0, x_space.length, 1)));

        preys_curve = svg_pop.append('path')
            .attr('stroke', this.prey_color)
            .attr('fill', 'none')
            .attr('stroke-width', 1).attr('d', c_preys(d3.range(0, x_space.length, 1)));
            
        preys_marker = svg_pop.append('circle')
            .attr('r', 3)
            .attr('stroke', this.prey_color)
        preys_marker.append('animateMotion')
            .attr('repeatCount', 'indefinite')
            .attr('fill', 'freeze')
            .attr('calcMode','linear')
            .attr('dur', '10s')
            .attr('path', c_preys(d3.range(0, x_space.length, 1)));

        phase_curve = svg_pop_phase.append('path')
            .attr('stroke', this.phase_curve_color)
            .attr('stroke-width', 1)
            .attr('fill', 'none').attr('d', c_phase(d3.range(0, x_space.length, 1)));
        phase_marker = svg_pop_phase.append('circle')
            .attr('r', 3)
            .attr('stroke', this.phase_curve_color)
        phase_marker.append('animateMotion')
            .attr('repeatCount', 'indefinite')
            .attr('fill', 'freeze')
            .attr('calcMode','linear')
            .attr('dur', '10s')
            .attr('path', c_phase(d3.range(0, x_space.length, 1)));

        bottomAxis = svg_pop.append("g").attr("transform", "translate(0," + height + ")")
                .call(d3.axisBottom(xScale));
        bottomAxis.append("text")
                .attr("class", "axis-title")
                .attr("y", 25)
                .attr("dy", ".71em")
                .attr("x", (width+margin.left)/2)
                .style("text-anchor", "end")
                .attr("fill", "black")
                .text("Tiempo");

        leftAxis = svg_pop.append("g")
                .call(d3.axisLeft(yScale));
        leftAxis.append("text")
                .attr("class", "axis-title")
                .attr("transform", "rotate(-90)")
                .attr("y", -30)
                .attr("dy", ".71em")
                .attr("x", -(height-margin.bottom)/2)
                .style("text-anchor", "end")
                .attr("fill", "black")
                .text("Densidad");

        bottomAxis_phase = svg_pop_phase.append("g").attr("transform", "translate(0," + height + ")")
                .call(d3.axisBottom(xScale_phase));
        bottomAxis_phase.append("text")
                .attr("class", "axis-title")
                .attr("y", 25)
                .attr("dy", ".71em")
                .attr("x", (width+margin.left)/2)
                .style("text-anchor", "end")
                .attr("fill", "black")
                .text("Densidad presa");

        leftAxis_phase = svg_pop_phase.append("g")
                .call(d3.axisLeft(yScale_phase));
        leftAxis_phase.append("text")
                .attr("class", "axis-title")
                .attr("transform", "rotate(-90)")
                .attr("y", -35)
                .attr("dy", ".71em")
                .attr("x", -(height-margin.bottom)/2)
                .style("text-anchor", "end")
                .attr("fill", "black")
                .text("Densidad predador");
        diag_phase = svg_pop_phase.append('line')
            .attr('stroke', 'black')
            .attr('stroke-width', 1)
            .attr('stroke-dasharray', '5,5')
            .attr('x1', xScale_phase(yDomain[0]))
            .attr('y1', yScale_phase(yDomain[0]))
            .attr('x2', xScale_phase(yDomain[1]))
            .attr('y2', yScale_phase(yDomain[1]))
    }
}
prey_predator.draw_graph();
</script>

Did a manual synchronization using keyTimes and keyPoints, and this does work for the first chart, but I don’t know how to calculate the values for the second one.
This also is a bit cumbersome given how firefox just works.

        get_curve_points = function(curve, n) {
            var points = []
            for (var i = 0; i < n; i++) {
                points.push(curve.node().getPointAtLength(i * curve.node().getTotalLength() / n))
            }
            if (points.length > n) {
                points.pop()
            }
            return points;
        },
        n = 50
        points = get_curve_points(preys_curve, n)
        xs = points.map(p => p.x)
        var min_x = d3.min(xs);
        var max_x = d3.max(xs);
        xs = xs.map(x => (x - min_x) / (max_x - min_x));
        keyTimes = xs.reduce((acc, x) => acc + x + ';', '') + "1"
        keyPoints = d3.range(0,1,1/n).reduce((acc, x) => acc + x + ';', '') + "1"
        preys_marker.append('animateMotion')
            .attr('repeatCount', 'indefinite')
            .attr('fill', 'freeze')
            .attr('calcMode','linear')
            .attr('dur', '10s')
            .attr('keyTimes', keyTimes)
            .attr('keyPoints', keyPoints)
            .attr('path', c_preys(d3.range(0, x_space.length, 1)));


        predators_curve = svg_pop.append('path')
            .attr('stroke', this.predator_color)
            .attr('fill', 'none')
            .attr('stroke-width', 2).attr('d', c_predators(d3.range(0, x_space.length, 1)));

        predators_marker = svg_pop.append('circle')
            .attr('r', 3)
            .attr('stroke', this.predator_color)

        points = get_curve_points(predators_curve, n)
        xs = points.map(p => p.x)
        var min_x = d3.min(xs);
        var max_x = d3.max(xs);
        xs = xs.map(x => (x - min_x) / (max_x - min_x));
        keyTimes = xs.reduce((acc, x) => acc + x + ';', '') + "1"
        keyPoints = d3.range(0,1,1/n).reduce((acc, x) => acc + x + ';', '') + "1"
        predators_marker.append('animateMotion')
            .attr('repeatCount', 'indefinite')
            .attr('fill', 'freeze')
            .attr('calcMode','linear')
            .attr('dur', '10s')
            .attr('keyTimes', keyTimes)
            .attr('keyPoints', keyPoints)
            .attr('path', c_predators(d3.range(0, x_space.length, 1)));


        phase_curve = svg_pop_phase.append('path')
            .attr('stroke', this.phase_curve_color)
            .attr('stroke-width', 1)
            .attr('fill', 'none').attr('d', c_phase(d3.range(0, x_space.length, 1)));
        phase_marker = svg_pop_phase.append('circle')
            .attr('r', 3)
            .attr('stroke', this.phase_curve_color)
        

        phase_marker.append('animateMotion')
            .attr('repeatCount', 'indefinite')
            .attr('fill', 'freeze')
            .attr('calcMode','linear')
            .attr('dur', '10s')