100 lines
3.6 KiB
JavaScript
100 lines
3.6 KiB
JavaScript
console.clear();
|
|
|
|
// Config
|
|
const DURATION = 10000;
|
|
const TURNS = 1.5;
|
|
const SPLITS_INITIAL = 16;
|
|
const SPLITS_FINAL = 32;
|
|
|
|
// Elements
|
|
const ringSelectors = ['.ring1', '.ring2', '.ring3', '.ring4', '.ring5', '.ring6'];
|
|
const ringElements = ringSelectors.map(selector => document.querySelector(selector));
|
|
|
|
// Helpers
|
|
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
|
const map = (num, min, max, minTarget, maxTarget) => {
|
|
const currentRatio = (num - min) / (max - min);
|
|
return (maxTarget - minTarget) * currentRatio + minTarget;
|
|
};
|
|
const lerp = (a, b, p) => (b - a) * p + a;
|
|
const easeOut1_5 = p => 1 - ((1-p) ** 1.5);
|
|
const easeOut5 = p => 1 - ((1-p) ** 5);
|
|
|
|
// Gradient sampler factory.
|
|
// Creates an instance with a `sample(position)` method used to query a color from a specific position on the gradient.
|
|
// `position` is a number, where `0` is the start of the gradient and `1` is the end. Out-of-range values are wrapped.
|
|
// I experimented with this kind of gradient here: https://codepen.io/MillerTime/pen/NXxxma?editors=0010
|
|
const gradientSampler = (function GradientSamplerFactory() {
|
|
// The instance to be returned.
|
|
const sampler = {};
|
|
|
|
// Gradient color stops in RGB format.
|
|
// Note: does not currently wrap smoothly - this is by design.
|
|
// Perhaps a `wrap` flag could be added to `sample()` method.
|
|
const colors = [
|
|
{ r: 255, g: 232, b: 0 },
|
|
{ r: 255, g: 103, b: 0 },
|
|
{ r: 191, g: 26, b: 156 },
|
|
{ r: 0, g: 79, b: 229 },
|
|
{ r: 0, g: 196, b: 9 }
|
|
// The real gradient continues from green back into orange and pink, but IMO it looks better without it.
|
|
// { r: 247, g: 154, b: 0 },
|
|
// { r: 243, g: 63, b: 149 }
|
|
];
|
|
|
|
const colorCount = colors.length;
|
|
const colorSpans = colorCount - 1;
|
|
const spanSize = 1 / colorSpans;
|
|
|
|
sampler.sample = function sample(position) {
|
|
// Normalize position to 0..1 scale (inclusive of 0, exlusive of 1).
|
|
position -= position | 0;
|
|
if (position < 0) position = 1 - position * -1;
|
|
|
|
const startIndex = position * colorSpans | 0;
|
|
const startColor = colors[startIndex];
|
|
const endColor = colors[startIndex + 1];
|
|
// Compute relative position between two chosen color stops.
|
|
const innerPosition = (position - (startIndex / colorSpans)) / spanSize;
|
|
|
|
const r = lerp(startColor.r, endColor.r, innerPosition) | 0;
|
|
const g = lerp(startColor.g, endColor.g, innerPosition) | 0;
|
|
const b = lerp(startColor.b, endColor.b, innerPosition) | 0;
|
|
|
|
return `rgb(${r},${g},${b})`;
|
|
};
|
|
|
|
return sampler;
|
|
})();
|
|
|
|
// 200 is the diameter of the <circle> element
|
|
const ringCircumference = 200 * Math.PI;
|
|
|
|
// Style a ring, given the current animation time in milliseconds. Setting `flip` to `true` reverses rotation.
|
|
function styleRing(el, time, flip) {
|
|
const progress = Math.max(0, time) % DURATION / DURATION;
|
|
const delayedProgress = clamp(map(progress, 0, 1, -0.1, 1), 0, 1);
|
|
el.style.stroke = gradientSampler.sample(easeOut1_5(progress));
|
|
el.style.transform = `rotate(${flip ? '-' : ''}${progress ** 1.25 * TURNS}turn) scale(${progress ** 1.35 * 6})`;
|
|
el.style.strokeWidth = lerp(200, 0, easeOut5(delayedProgress));
|
|
const dash = lerp(ringCircumference / SPLITS_INITIAL, ringCircumference / (2*SPLITS_FINAL), easeOut5(delayedProgress));
|
|
const gap = lerp(0, ringCircumference / (2*SPLITS_FINAL), easeOut5(delayedProgress));
|
|
el.style.strokeDasharray = `${dash} ${gap}`;
|
|
}
|
|
|
|
let startTime = -1;
|
|
|
|
// Animation Loop
|
|
function tick(time) {
|
|
if (startTime === -1) startTime = time;
|
|
const timeFromZero = time - startTime;
|
|
|
|
ringElements.forEach((ringEl, i) => {
|
|
styleRing(ringEl, timeFromZero - DURATION / ringElements.length * i, i % 2 !== 0);
|
|
});
|
|
|
|
requestAnimationFrame(tick);
|
|
}
|
|
|
|
requestAnimationFrame(tick);
|