"use strict";
import { arc as d3Arc } from "d3-shape";
import { select as d3Select } from "d3-selection";
import { interpolate as d3Interpolate } from "d3-interpolate";
import { easeLinear as d3EaseLinear } from "d3-ease";
import defined from "terriajs-cesium/Source/Core/defined";
/**
* Returns a function that returns a string representing the svg path of an arc.
*
* @param {number} radius
*/
function arcFactory(radius) {
return d3Arc()
.innerRadius(0)
.outerRadius(radius)
.startAngle(0);
}
// Interpolate from 0 to 2*pi radians
function angleInterpolator(t, startAngle = 0) {
return d3Interpolate(startAngle, Math.PI * 2)(t);
}
/**
* Runs the timer animation, making an arc fill from 0 to 100% of the circle.
* @param {number} radius Radius of timer.
* @param {number} interval Timer duration in seconds.
* @param {DOMElement} elapsedTimeElement SVG path containing the elapsed time "pie".
* @param {DOMElement} backgroundElement SVG path containing the background circle.
* @param {number} [options.deltaOpacity=0.5] Change in opacity when fading in and out timer.
* @param {number} [options.opacityAnimationInterval=3] How long fade in/out lasts in seconds.
* @param {number} [options.minOpacity=0.1] When fading out, doesn't let opacity go below `minOpacity`.
* @param {number} [options.elapsed=0] How much time has already passed.
*/
function animateTimer(
radius,
interval,
elapsedTimeElement,
backgroundElement,
options = {}
) {
options = {
deltaOpacity: defined(options.deltaOpacity) ? options.deltaOpacity : 0.7,
opacityAnimationInterval: defined(options.opacityAnimationInterval)
? options.opacityAnimationInterval
: 3,
minOpacity: defined(options.minOpacity) ? options.minOpacity : 0.1,
elapsed: defined(options.elapsed) ? options.elapsed : 0
};
// The arc representing the elapsed time should be filled up to the current time.
// We find the elapsed time as a percentage of the total duration, and then get the angle interpolator to calculate
// the corresponding angle.
const startAngle = angleInterpolator(options.elapsed / interval);
elapsedTimeElement
.datum({ endAngle: angleInterpolator(startAngle) })
.transition("arc" + new Date().getTime().toString())
.duration((interval - options.elapsed) * 1000) // d3 uses milliseconds
.ease(d3EaseLinear)
// attrTween requires a function A that returns an interpolator function B
// when B is passed the time, t, it should return the new value of the attribute, in this case `d`
.attrTween("d", () => t =>
arcFactory(radius)({ endAngle: angleInterpolator(t, startAngle) })
);
const opacityTransition = (element, max, repeatsLeft) => {
if (repeatsLeft <= 0) {
element.interrupt();
return;
}
// Clamp the minimum opacity to options.minOpacity.
const min =
max - options.deltaOpacity > options.minOpacity
? max - options.deltaOpacity
: options.minOpacity;
element
.transition("in")
.duration((options.opacityAnimationInterval * 1000) / 2)
.styleTween("opacity", () => t => d3Interpolate(max, min)(t));
element
.transition("out")
.delay((options.opacityAnimationInterval * 1000) / 2)
.duration((options.opacityAnimationInterval * 1000) / 2)
.styleTween("opacity", () => t => d3Interpolate(min, max)(t))
.on("end", () => opacityTransition(element, max, repeatsLeft - 1)); // start cycle again
};
// Start our opacity animation.
const repeats = Math.ceil(interval / options.opacityAnimationInterval) - 1;
// Use the element's existing opacity as the maximum opacity.
// We calculate it here once and then pass it into opacityTransition to stop the animation's max opacity from drifting
const backgroundMax = parseFloat(backgroundElement.style("opacity"));
opacityTransition(backgroundElement, backgroundMax, repeats);
const elaspedMax = parseFloat(elapsedTimeElement.style("opacity"));
opacityTransition(elapsedTimeElement, elaspedMax, repeats);
}
/**
* Adds a new timer to the DOM. Call {@link updateTimer()} to make it start animating.
* @param {number} radius Radius of timer.
* @param {string} containerId The id of the element to insert the timer into.
* @param {string} elapsedTimeClass A class for styling the animation that fills the timer as it runs.
* @param {string} backgroundClass A class for styling the timer's background circle.
*/
export function createTimer(
radius,
containerId,
elapsedTimeClass,
backgroundClass
) {
const container = d3Select("#" + containerId);
if (!defined(container)) {
// If we couldn't select the container from the DOM, abort!
// A missing timer is not a big problem, so we fail silently.
return null;
}
const diameter = 2 * radius;
const g = container
.append("svg")
.attr("width", diameter)
.attr("height", diameter)
.append("g")
// We want to translate everything down and left so that the entire circle is draw in view, not just the bottom
// right quadrant
.attr("transform", `translate(${radius},${radius})`);
// Add background circle
g.append("circle")
.attr("class", backgroundClass)
.attr("cx", 0)
.attr("cy", 0)
.attr("r", radius);
// Add arc representing the elapsed time
g.append("path")
.attr("class", elapsedTimeClass)
.datum({ endAngle: 0 })
.attr("d", arcFactory(radius));
}
/**
* Start an existing timer. This will restart the animation if it is already running.
* @param {number} radius Radius of timer.
* @param {number} interval Timer duration in seconds.
* @param {string} containerId The id of the element to insert the timer into.
* @param {string} elapsedTimeClass A class for styling the animation that fills the timer as it runs.
* @param {string} backgroundClass A class for styling the timer's background circle.
* @param {string} [elapsed=0] How much time (in seconds) has already passed.
*/
export function startTimer(
radius,
interval,
containerId,
elapsedTimeClass,
backgroundClass,
elapsed = 0
) {
const elapsedTimeElement = d3Select("#" + containerId).select(
"." + elapsedTimeClass
);
if (!defined(elapsedTimeElement) || elapsedTimeElement.empty()) {
// If we couldn't select the element from the DOM, abort!
// A missing timer is not a big problem, so we fail silently.
return null;
}
const backgroundElement = d3Select("#" + containerId).select(
"." + backgroundClass
);
if (!defined(backgroundElement) || backgroundElement.empty()) {
return null;
}
animateTimer(radius, interval, elapsedTimeElement, backgroundElement, {
elapsed: elapsed
});
}