-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsite.js
More file actions
140 lines (119 loc) · 4.96 KB
/
Copy pathsite.js
File metadata and controls
140 lines (119 loc) · 4.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
/**
* DCS Site JavaScript
* Marketing enhancements on top of base.js app shell
* Particles, scroll reveal, footer year
* Copyright © 2026 Mark Constable <mc@dcs.spa> (MIT License)
*/
const Site = {
config: {
particleCount: 20
},
// ============================================
// FLOATING PARTICLES
// ============================================
initParticles() {
const container = document.getElementById("particles");
if (!container) return;
for (let i = 0; i < this.config.particleCount; i++) {
const particle = document.createElement("div");
particle.className = "particle";
particle.style.left = Math.random() * 100 + "%";
particle.style.animationDelay = Math.random() * 15 + "s";
particle.style.animationDuration = (10 + Math.random() * 10) + "s";
container.appendChild(particle);
}
},
// ============================================
// SCROLL REVEAL ANIMATION
// ============================================
// Reveal-on-scroll: snap whatever is already on screen at load visible (no
// slide-up), then animate a section one-way only as it scrolls into view.
// (The old version animated everything already visible at load and re-fired on
// every scroll / layout reflow.)
initScrollReveal() {
const els = document.querySelectorAll(".reveal");
if (els.length === 0) return;
const vh = window.innerHeight || document.documentElement.clientHeight;
// Already in view at load → snap visible, no animation.
els.forEach(el => {
if (el.getBoundingClientRect().top < vh) el.classList.add("active", "reveal-instant");
});
if (!("IntersectionObserver" in window)) {
els.forEach(el => el.classList.add("active"));
return;
}
// The rest animate one-way, only as they scroll into view.
const io = new IntersectionObserver(entries => {
entries.forEach(en => {
if (en.isIntersecting) { en.target.classList.add("active"); io.unobserve(en.target); }
});
}, { rootMargin: "0px 0px -8% 0px" });
els.forEach(el => { if (!el.classList.contains("active")) io.observe(el); });
},
// ============================================
// DYNAMIC YEAR IN FOOTER
// ============================================
initFooterYear() {
const yearSpan = document.getElementById("year");
if (yearSpan) {
yearSpan.textContent = new Date().getFullYear();
}
},
// ============================================
// TOUR DECK (sidebar slide-deck)
// Independent of the outer panel carousel.
// State is kept in closure — it does NOT touch base-state.
// ============================================
initDeck() {
const deck = document.querySelector('.deck');
if (!deck) return;
const track = deck.querySelector('.deck-track');
const slides = deck.querySelectorAll('.deck-slide');
const dots = deck.querySelectorAll('.deck-dot');
const counter = deck.querySelector('.deck-current');
const count = slides.length;
if (!track || !count) return;
let idx = 0;
deck.style.setProperty('--deck-count', count);
const render = () => {
deck.style.setProperty('--deck-idx', idx);
slides.forEach((s, i) => s.classList.toggle('active', i === idx));
dots.forEach((d, i) => d.classList.toggle('active', i === idx));
if (counter) counter.textContent = String(idx + 1).padStart(2, '0');
};
const goto = (n) => {
idx = ((n % count) + count) % count; // wrap both directions
render();
};
deck.addEventListener('click', (e) => {
const dir = e.target.closest('[data-deck-dir]');
if (dir) { goto(idx + (dir.dataset.deckDir === 'next' ? 1 : -1)); return; }
const dot = e.target.closest('[data-deck-goto]');
if (dot) { goto(parseInt(dot.dataset.deckGoto, 10)); }
});
deck.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') { goto(idx - 1); e.preventDefault(); }
if (e.key === 'ArrowRight') { goto(idx + 1); e.preventDefault(); }
if (e.key === 'Home') { goto(0); e.preventDefault(); }
if (e.key === 'End') { goto(count - 1); e.preventDefault(); }
});
render();
},
// ============================================
// INITIALIZE ALL
// ============================================
init() {
this.initParticles();
this.initScrollReveal();
this.initFooterYear();
this.initDeck();
}
};
// Auto-init when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => Site.init());
} else {
Site.init();
}
// Global export
window.Site = Site;