<div class="timeline">
<div class="timeline__grid">
<div class="timeline__item">
<div class="timeline__content">-1-</div>
<div class="timeline__number">1</div>
</div>
<div class="timeline__item">
<div class="timeline__content">-2-</div>
<div class="timeline__number">2</div>
</div>
<div class="timeline__item">
<div class="timeline__content">-4-</div>
<div class="timeline__number">4</div>
</div>
<div class="timeline__item">
<div class="timeline__content">-3-</div>
<div class="timeline__number">3</div>
</div>
<div class="timeline__item">
<div class="timeline__content">-5-</div>
<div class="timeline__number">5</div>
</div>
<div class="timeline__item">
<div class="timeline__content">-6-</div>
<div class="timeline__number">6</div>
</div>
<div class="timeline__item">
<div class="timeline__content">-7-</div>
<div class="timeline__number">7</div>
</div>
<div class="timeline__item">
<div class="timeline__content">-8-</div>
<div class="timeline__number">8</div>
</div>
<div class="timeline__item">
<div class="timeline__content">-9-</div>
<div class="timeline__number">9</div>
</div>
<div class="timeline__item">
<div class="timeline__content">-10-</div>
<div class="timeline__number">10</div>
</div>
<div class="timeline__item">
<div class="timeline__content">-11-</div>
<div class="timeline__number">11</div>
</div>
<div class="timeline__item">
<div class="timeline__content">-12-</div>
<div class="timeline__number">12</div>
</div>
</div>
<svg class="svg-timeline" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad" x1="0" y1="0" x2="100%" y2="100%" >
<stop offset="0%" stop-color="#d73370"/>
<stop offset="100%" stop-color="#716ca3"/>
</linearGradient>
</defs>
<path class="svg-timeline-static" fill="none" stroke="url(#grad)" stroke-width="2" stroke-dasharray="5" d="" />
<path class="svg-timeline-animatable" fill="none" stroke="url(#grad)" stroke-width="3" d="" />
</svg>
</div>
* {
box-sizing: border-box;
}
body {
height: 5000px;
background-color: #0a0816;
}
.timeline {
position: fixed;
margin: 50px;
width: 75%;
display: flex;
flex-direction: column;
text-align: center;
}
.timeline__grid {
display: flex;
flex-wrap: wrap;
margin-top: -100px;
margin-left: -10px;
margin-right: -10px;
}
.timeline__item {
position: relative;
width: calc(33.33% - 20px);
margin-top: 100px;
margin-left: 10px;
margin-right: 10px;
}
/*
.timeline__item:nth-child(2n) {
margin-right: calc(50% - 200px);
margin-left: calc(50% - 150px);
}
.timeline__item:nth-child(4n-1) {
margin-left: 100px;
} */
.timeline__content {
background-color: #673ab7;
color: #fff;
height: 100px;
margin-bottom: 20px;
transition: background-color 250ms;
}
.timeline__number {
position: absolute;
left: calc(50% - 15px);
bottom: -15px;
width: 30px;
height: 30px;
line-height: 30px;
background-color: #2196f3;
border-radius: 50%;
color: #fff;
transition: background-color 250ms;
}
.timeline__item--active .timeline__number {
background-color: #e91e63;
}
.timeline__item--active .timeline__content {
background-color: #4caf50;
}
.svg-timeline {
position: absolute;
width: 100%;
height: 100%;
overflow: visible;
pointer-events: none;
z-index: -1;
}
class Timeline {
constructor(options) {
this.svg = options.svg;
this.cols = options.cols || 2;
this.padding = options.padding || { x: 10, y: 10 };
this.bevel = options.bevel || { x: 10, y: 10 };
this.pivot = options.pivot || { x: 0.5, y: 0.5 };
this.intesectOffset = options.intesectOffset || { x: 2, y: 2 };
this.els = options.els || [];
this.items = this.els.map(el => ({ el }));
this.pathStatic = this.svg.querySelector('.svg-timeline-static');
this.pathAnimatable = this.svg.querySelector('.svg-timeline-animatable');
this.totalLength = 0;
this.animProgress = 0;
this.init();
}
init() {
this.reorderItems();
this.updateItems();
this.draw();
this.setProgress(0);
}
reorderItems() {
for (let i = 0, ri = 0, row; i < this.items.length; i += this.cols, ri++) {
if (ri % 2 === 0) {
row = this.items.slice(i, i + this.cols);
row[row.length - 1].last = true;
row.forEach(item => item.reversed = false);
}
else {
row = this.items.slice(i, i + this.cols).reverse();
row[row.length - 1].last = true;
row.forEach(item => item.reversed = true);
}
this.items.splice(i, this.cols, ...row);
}
}
updateItems() {
const svgRect = this.svg.getBoundingClientRect();
this.svgRect = svgRect;
this.items.forEach(item => {
const rect = item.el.getBoundingClientRect();
const point = {
x: rect.left - svgRect.left + rect.width * this.pivot.x,
y: rect.top - svgRect.top + rect.height * this.pivot.y,
};
item.point = point;
});
}
draw() {
let d = `M ${this.items[0].point.x} ${this.items[0].point.y}`;
for (let i = 1, item; i < this.items.length - 1; i++) {
item = this.items[i];
d += ` L ${item.point.x} ${item.point.y}`;
if (item.last) {
let toX = item.reversed ?
this.bevel.x - this.padding.x :
this.svgRect.width - this.bevel.x + this.padding.x;
let toY = item.point.y;
d += ` L ${toX} ${toY}`;
toX = item.reversed ?
-this.padding.x :
this.svgRect.width + this.padding.x;
toY = item.point.y + this.bevel.y;
const sweepFlag = item.reversed ? 0 : 1;
d += ` A ${this.bevel.x} ${this.bevel.y} 0 0 ${sweepFlag} ${toX} ${toY}`;
toY = this.items[i + 1].point.y - this.bevel.y;
d += ` L ${toX} ${toY}`;
toX = item.reversed ?
-this.padding.x + this.bevel.x:
this.svgRect.width + this.padding.x - this.bevel.x;
toY = this.items[i + 1].point.y;
d += ` A ${this.bevel.x} ${this.bevel.y} 0 0 ${sweepFlag} ${toX} ${toY}`;
}
}
const last = this.items[this.items.length - 1];
d += ` L ${last.point.x} ${last.point.y}`;
this.pathStatic.setAttribute('d', d);
this.pathAnimatable.setAttribute('d', d);
}
setProgress(p) {
this.animProgress = p;
this.totalLength = this.pathStatic.getTotalLength();
this.pathAnimatable.setAttribute('stroke-dashoffset', this.totalLength - this.totalLength * this.animProgress);
this.pathAnimatable.setAttribute('stroke-dasharray', this.totalLength);
this.checkIntersect();
}
checkIntersect() {
const len = this.totalLength * this.animProgress;
const step = Math.max(1, Math.min(this.intesectOffset.x, this.intesectOffset.y));
let intersectIndex = -1;
for (let l = len, point; l >= 0; l -= step) {
point = this.pathAnimatable.getPointAtLength(l);
intersectIndex = this.items.findIndex(item => {
return (
(point.x > item.point.x - this.intesectOffset.x) &&
(point.x < item.point.x + this.intesectOffset.x) &&
(point.y > item.point.y - this.intesectOffset.y) &&
(point.y < item.point.y + this.intesectOffset.y)
);
});
if (intersectIndex !== -1) break;
}
if (intersectIndex === -1) return;
for (let i = 0; i <= intersectIndex; i++) {
this.items[i].el.classList.add('timeline__item--active');
}
for (let i = intersectIndex + 1; i < this.items.length; i++) {
this.items[i].el.classList.remove('timeline__item--active');
}
}
resize() {
this.updateItems();
this.draw();
this.setProgress(this.animProgress);
}
}
const tl = new Timeline({
cols: 3,
els: Array.from(document.querySelectorAll('.timeline__item')),
svg: document.querySelector('.svg-timeline'),
pivot: { x: 0.5, y: 1 },
bevel: { x: 35, y: 35 },
padding: { x: 20, y: 20 },
intesectOffset: { x: 15, y: 15 },
});
window.addEventListener('resize', () => tl.resize() );
window.addEventListener('scroll', () => {
const dt = document.documentElement.scrollTop || document.body.scrollTop;
const sh = document.documentElement.scrollHeight || document.body.scrollHeight;
const scrolled = dt / (sh - document.documentElement.clientHeight);
tl.setProgress(scrolled);
})
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.