@linear_non/stellar-libs 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +34 -0
- package/src/ScrollBar/index.js +125 -0
- package/src/Smooth/index.js +149 -0
- package/src/SplitOnScroll/index.js +55 -0
- package/src/Sticky/index.js +123 -0
- package/src/index.js +4 -0
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@linear_non/stellar-libs",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Reusable JavaScript libraries for Non-Linear Studio projects.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "vite",
|
|
15
|
+
"build": "vite build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"non-linear",
|
|
19
|
+
"frontend",
|
|
20
|
+
"core",
|
|
21
|
+
"scroll",
|
|
22
|
+
"raf",
|
|
23
|
+
"gsap"
|
|
24
|
+
],
|
|
25
|
+
"author": "Non-Linear Studio",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@linear_non/stellar-kit": "^1.0.7"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@linear_non/prettier-config": "^1.0.6",
|
|
32
|
+
"vite": "^6.3.5"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Scrollbar.js
|
|
2
|
+
import { kitStore } from "@linear_non/stellar-kit";
|
|
3
|
+
import { emitter, EVENTS, Raf } from "@linear_non/stellar-kit/events";
|
|
4
|
+
|
|
5
|
+
export default class Scrollbar {
|
|
6
|
+
constructor(obj) {
|
|
7
|
+
this.container = obj.container || document.body;
|
|
8
|
+
this.el = null;
|
|
9
|
+
this.handle = null;
|
|
10
|
+
this.touching = false;
|
|
11
|
+
|
|
12
|
+
this.state = {
|
|
13
|
+
clicked: false,
|
|
14
|
+
scale: 0,
|
|
15
|
+
current: 0,
|
|
16
|
+
};
|
|
17
|
+
this.height = 0;
|
|
18
|
+
this.init();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
init() {
|
|
22
|
+
this.create();
|
|
23
|
+
this.setBounds();
|
|
24
|
+
this.on();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
on() {
|
|
28
|
+
emitter.on(EVENTS.APP_TICK, this.tick);
|
|
29
|
+
emitter.on(EVENTS.APP_RESIZE, this.resize);
|
|
30
|
+
this.handle.addEventListener("mousedown", this.mouseDown);
|
|
31
|
+
window.addEventListener("mouseup", this.mouseUp);
|
|
32
|
+
window.addEventListener("mousemove", this.mouseMove);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
off() {
|
|
36
|
+
emitter.off(EVENTS.APP_TICK, this.tick);
|
|
37
|
+
emitter.off(EVENTS.APP_RESIZE, this.resize);
|
|
38
|
+
this.handle.removeEventListener("mousedown", this.mouseDown);
|
|
39
|
+
window.removeEventListener("mouseup", this.mouseUp);
|
|
40
|
+
window.removeEventListener("mousemove", this.mouseMove);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
mouseDown = () => {
|
|
44
|
+
this.touching = true;
|
|
45
|
+
document.body.style.userSelect = "none";
|
|
46
|
+
document.body.style.webkitUserSelect = "none";
|
|
47
|
+
document.body.style.pointerEvents = "none";
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
mouseUp = () => {
|
|
51
|
+
this.touching = false;
|
|
52
|
+
document.body.style.userSelect = "inherit";
|
|
53
|
+
document.body.style.webkitUserSelect = "inherit";
|
|
54
|
+
document.body.style.pointerEvents = "inherit";
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
mouseMove = (e) => {
|
|
58
|
+
if (this.touching === true) {
|
|
59
|
+
const { isLocked } = kitStore.flags;
|
|
60
|
+
|
|
61
|
+
if (isLocked) return;
|
|
62
|
+
|
|
63
|
+
kitStore.raf.scroll.target = e.clientY * this.state.scale;
|
|
64
|
+
kitStore.raf.clamp();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
setBounds() {
|
|
69
|
+
const { vh, fh } = kitStore.sizes;
|
|
70
|
+
const scrollLimit = fh;
|
|
71
|
+
this.state.scale = (scrollLimit + vh) / vh;
|
|
72
|
+
this.handle.style.height = `${vh / this.state.scale}px`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
tick = ({ current }) => {
|
|
76
|
+
const scroll = current / this.state.scale;
|
|
77
|
+
this.state.current = scroll;
|
|
78
|
+
this.handle.style.transform = `translate3d(0, ${this.state.current}px, 0)`;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
resize = () => {
|
|
82
|
+
this.setBounds();
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
calcScroll(e) {
|
|
86
|
+
const delta = e.clientY * this.state.scale;
|
|
87
|
+
Raf.target = delta;
|
|
88
|
+
Raf.clampTarget();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
create() {
|
|
92
|
+
this.el = document.createElement("div");
|
|
93
|
+
this.handle = document.createElement("div");
|
|
94
|
+
this.el.classList.add("scrollbar");
|
|
95
|
+
this.handle.classList.add("handle");
|
|
96
|
+
|
|
97
|
+
Object.assign(this.el.style, {
|
|
98
|
+
position: "fixed",
|
|
99
|
+
top: 0,
|
|
100
|
+
right: 0,
|
|
101
|
+
height: "100%",
|
|
102
|
+
pointerEvents: "all",
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
Object.assign(this.handle.style, {
|
|
106
|
+
position: "absolute",
|
|
107
|
+
top: 0,
|
|
108
|
+
left: 0,
|
|
109
|
+
width: "100%",
|
|
110
|
+
cursor: "pointer",
|
|
111
|
+
zIndex: 101,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
this.container.appendChild(this.el);
|
|
115
|
+
this.el.appendChild(this.handle);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
update() {
|
|
119
|
+
this.setBounds();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
destroy() {
|
|
123
|
+
this.off();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Smooth.js
|
|
2
|
+
import Scrollbar from "../ScrollBar";
|
|
3
|
+
import { kitStore } from "@linear_non/stellar-kit";
|
|
4
|
+
import { emitter, EVENTS } from "@linear_non/stellar-kit/events";
|
|
5
|
+
import { qs, qsa, bounds } from "@linear_non/stellar-kit/utils";
|
|
6
|
+
|
|
7
|
+
export default class Smooth {
|
|
8
|
+
constructor(obj) {
|
|
9
|
+
this.scroll = obj.scroll;
|
|
10
|
+
this.el = qs("[data-view]");
|
|
11
|
+
this.elems = null;
|
|
12
|
+
this.current = 0;
|
|
13
|
+
this.threshold = 100;
|
|
14
|
+
this.sections = null;
|
|
15
|
+
this.scrollbar = null;
|
|
16
|
+
this.init();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getSections() {
|
|
20
|
+
const { isSmooth } = kitStore.flags;
|
|
21
|
+
if (!this.elems || !isSmooth) return;
|
|
22
|
+
|
|
23
|
+
this.sections = [];
|
|
24
|
+
this.elems.forEach((el) => {
|
|
25
|
+
el.style.transform = "translate3d(0, 0, 0)";
|
|
26
|
+
const speed = el.dataset.speed || 1;
|
|
27
|
+
const { top, bottom, offset } = this.getVars(el, speed);
|
|
28
|
+
let parent = el.parentNode.closest("[data-smooth]");
|
|
29
|
+
|
|
30
|
+
if (parent) {
|
|
31
|
+
this.sections.some((obj) => {
|
|
32
|
+
if (obj.el === parent) parent = obj;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.sections.push({
|
|
37
|
+
el,
|
|
38
|
+
parent,
|
|
39
|
+
top,
|
|
40
|
+
bottom,
|
|
41
|
+
offset,
|
|
42
|
+
speed,
|
|
43
|
+
out: true,
|
|
44
|
+
transform: 0,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
tick = ({ current }) => {
|
|
50
|
+
this.current = current;
|
|
51
|
+
this.transformSections();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
transformSections() {
|
|
55
|
+
const { isSmooth, isResizing } = kitStore.flags;
|
|
56
|
+
if (!this.sections || !isSmooth) return;
|
|
57
|
+
|
|
58
|
+
this.sections.forEach((section) => {
|
|
59
|
+
const { isVisible, transform } = this.isVisible(section);
|
|
60
|
+
|
|
61
|
+
if (isVisible || isResizing || !section.out) {
|
|
62
|
+
section.out = !isVisible;
|
|
63
|
+
section.transform = transform;
|
|
64
|
+
section.el.style.transform = this.getTransform(transform);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
isVisible(section) {
|
|
70
|
+
const { vh } = kitStore.sizes;
|
|
71
|
+
const { top, bottom, offset, speed, parent } = section;
|
|
72
|
+
|
|
73
|
+
const extra = (parent && parent.transform) || 0;
|
|
74
|
+
const translate = this.current * speed;
|
|
75
|
+
const transform = translate - offset - extra;
|
|
76
|
+
const start = top - translate;
|
|
77
|
+
const end = bottom - translate;
|
|
78
|
+
const isVisible = start < this.threshold + vh && end > -this.threshold;
|
|
79
|
+
|
|
80
|
+
return { isVisible, transform };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getTransform(transform) {
|
|
84
|
+
return `translate3d(0, ${-transform}px, 0)`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getVars(el, speed) {
|
|
88
|
+
const { vh } = kitStore.sizes;
|
|
89
|
+
const rect = bounds(el);
|
|
90
|
+
const centering = vh / 2 - rect.height / 2;
|
|
91
|
+
const offset =
|
|
92
|
+
rect.top < vh
|
|
93
|
+
? 0
|
|
94
|
+
: (rect.top - centering) * speed - (rect.top - centering);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
top: rect.top + offset,
|
|
98
|
+
bottom: rect.bottom + offset,
|
|
99
|
+
offset,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
resize = () => {
|
|
104
|
+
if (!this.sections) return;
|
|
105
|
+
|
|
106
|
+
this.sections.forEach((section) => {
|
|
107
|
+
section.el.style.transform = "translate3d(0, 0, 0)";
|
|
108
|
+
const { top, bottom, offset } = this.getVars(section.el, section.speed);
|
|
109
|
+
Object.assign(section, { top, bottom, offset });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
emitter.emit(EVENTS.APP_SMOOTH_RESIZE);
|
|
113
|
+
this.transformSections();
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
update(elems) {
|
|
117
|
+
kitStore.flags.isResizing = true;
|
|
118
|
+
|
|
119
|
+
this.scroll.setScrollBounds();
|
|
120
|
+
this.elems = elems || qsa("[data-smooth]");
|
|
121
|
+
this.scrollbar.update();
|
|
122
|
+
this.getSections();
|
|
123
|
+
this.transformSections();
|
|
124
|
+
|
|
125
|
+
kitStore.flags.isResizing = false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
clean() {
|
|
129
|
+
this.elems = this.sections = null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
on() {
|
|
133
|
+
emitter.on(EVENTS.APP_TICK, this.tick);
|
|
134
|
+
emitter.on(EVENTS.APP_RESIZE, this.resize);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
off() {
|
|
138
|
+
emitter.off(EVENTS.APP_TICK, this.tick);
|
|
139
|
+
emitter.off(EVENTS.APP_RESIZE, this.resize);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
init(elems) {
|
|
143
|
+
this.elems = elems || qsa("[data-smooth]");
|
|
144
|
+
this.getSections();
|
|
145
|
+
this.scrollbar = new Scrollbar(this.current);
|
|
146
|
+
this.on();
|
|
147
|
+
this.update();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { splitText, reverseSplit } from "@linear_non/stellar-kit/utils";
|
|
2
|
+
import { ScrollTrigger } from "@linear_non/stellar-kit/libraries/gsap";
|
|
3
|
+
|
|
4
|
+
export default class SplitonScroll {
|
|
5
|
+
constructor({
|
|
6
|
+
el,
|
|
7
|
+
splitText,
|
|
8
|
+
animate,
|
|
9
|
+
reverse,
|
|
10
|
+
start = "top bottom", // "top top+=100%",
|
|
11
|
+
end = "bottom top", // "bottom bottom-=100%",
|
|
12
|
+
scrub = false,
|
|
13
|
+
once = true,
|
|
14
|
+
}) {
|
|
15
|
+
this.element = el;
|
|
16
|
+
this.splitText = splitText;
|
|
17
|
+
this.splits = [];
|
|
18
|
+
this.animateCallback = animate || (() => {});
|
|
19
|
+
this.reverseCallback = reverse || (() => {});
|
|
20
|
+
this.start = start;
|
|
21
|
+
this.end = end;
|
|
22
|
+
this.scrub = scrub;
|
|
23
|
+
this.once = once;
|
|
24
|
+
|
|
25
|
+
if (!this.splitText[0]) return;
|
|
26
|
+
|
|
27
|
+
this.initScrollTrigger();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
initScrollTrigger() {
|
|
31
|
+
this.myScrollTrigger = ScrollTrigger.create({
|
|
32
|
+
trigger: this.element,
|
|
33
|
+
start: this.start,
|
|
34
|
+
end: this.end,
|
|
35
|
+
scrub: this.scrub,
|
|
36
|
+
once: this.once,
|
|
37
|
+
onEnter: () => this.handleEnter(),
|
|
38
|
+
onLeave: () => this.handleLeave(),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
handleEnter() {
|
|
43
|
+
const split = splitText(this.splitText);
|
|
44
|
+
split.on("ready", (splits) => {
|
|
45
|
+
this.splits = [...splits];
|
|
46
|
+
this.animateCallback();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
handleLeave() {
|
|
51
|
+
reverseSplit(this.splits);
|
|
52
|
+
this.reverseCallback();
|
|
53
|
+
this.myScrollTrigger.kill();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Sticky.js
|
|
2
|
+
import { bounds } from "@linear_non/stellar-kit/utils";
|
|
3
|
+
import { gsap, ScrollTrigger } from "@linear_non/stellar-kit/libraries/gsap";
|
|
4
|
+
|
|
5
|
+
export default class Sticky {
|
|
6
|
+
constructor(obj) {
|
|
7
|
+
const { el, sticky, isSmooth, onUpdateCallback } = obj;
|
|
8
|
+
|
|
9
|
+
this.el = el;
|
|
10
|
+
this.sticky = sticky;
|
|
11
|
+
this.isSticky = false;
|
|
12
|
+
this.isSmooth = isSmooth;
|
|
13
|
+
this.progress = 0;
|
|
14
|
+
this.total = 0;
|
|
15
|
+
this.positionX = 0;
|
|
16
|
+
this.positionY = 0;
|
|
17
|
+
this.scrollTrigger = null;
|
|
18
|
+
this.onUpdateCallback =
|
|
19
|
+
typeof onUpdateCallback === "function" ? onUpdateCallback : null;
|
|
20
|
+
|
|
21
|
+
this.config = {
|
|
22
|
+
width: 0,
|
|
23
|
+
height: 0,
|
|
24
|
+
totalWidth: 0,
|
|
25
|
+
totalHeight: 0,
|
|
26
|
+
totalItems: 0,
|
|
27
|
+
};
|
|
28
|
+
if (!isSmooth) sticky.style.pointerEvents = "none";
|
|
29
|
+
this.init();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
observer() {
|
|
33
|
+
if (this.scrollTrigger) this.scrollTrigger.kill(); // Prevent duplicate listeners
|
|
34
|
+
|
|
35
|
+
this.scrollTrigger = ScrollTrigger.create({
|
|
36
|
+
trigger: this.el,
|
|
37
|
+
start: "top top",
|
|
38
|
+
end: "bottom bottom",
|
|
39
|
+
scrub: true,
|
|
40
|
+
onEnter: () => this.toggleSticky(true),
|
|
41
|
+
onEnterBack: () => this.toggleSticky(true),
|
|
42
|
+
onLeave: () => this.toggleSticky(false, true), // ✅ Ensure totalHeight is passed
|
|
43
|
+
onLeaveBack: () => this.toggleSticky(false),
|
|
44
|
+
onUpdate: (self) => this.onUpdate(self),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
toggleSticky(isSticky, hasTotalHeight = false) {
|
|
49
|
+
this.isSticky = isSticky;
|
|
50
|
+
isSticky
|
|
51
|
+
? this.addSticky()
|
|
52
|
+
: this.removeSticky(hasTotalHeight ? this.config.totalHeight : 0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
addSticky() {
|
|
56
|
+
if (!this.sticky || this.isSmooth) return;
|
|
57
|
+
this.sticky.classList.add("-fixed");
|
|
58
|
+
gsap.set(this.sticky, { top: 0, position: "" });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
removeSticky(totalHeight = 0) {
|
|
62
|
+
if (!this.sticky || this.isSmooth) return;
|
|
63
|
+
this.sticky.classList.remove("-fixed");
|
|
64
|
+
gsap.set(this.sticky, { top: `${totalHeight}px`, position: "relative" });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
onUpdate(self) {
|
|
68
|
+
const { totalWidth, totalHeight } = this.config;
|
|
69
|
+
const newProgress = self.progress;
|
|
70
|
+
const newY = newProgress * totalHeight;
|
|
71
|
+
const newX = -newProgress * totalWidth;
|
|
72
|
+
|
|
73
|
+
// Only update if values change to prevent unnecessary calls
|
|
74
|
+
if (
|
|
75
|
+
this.progress !== newProgress ||
|
|
76
|
+
this.positionY !== newY ||
|
|
77
|
+
this.positionX !== newX
|
|
78
|
+
) {
|
|
79
|
+
this.progress = newProgress;
|
|
80
|
+
this.positionY = newY;
|
|
81
|
+
this.positionX = newX;
|
|
82
|
+
|
|
83
|
+
if (this.isSmooth) {
|
|
84
|
+
gsap.set(this.sticky, { y: this.positionY });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Call the callback function if provided
|
|
88
|
+
if (this.onUpdateCallback) {
|
|
89
|
+
this.onUpdateCallback({
|
|
90
|
+
progress: this.progress,
|
|
91
|
+
positionX: this.positionX,
|
|
92
|
+
positionY: this.positionY,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
resize() {
|
|
99
|
+
if (!this.sticky) return;
|
|
100
|
+
|
|
101
|
+
// Get the viewport height dynamically instead of using store
|
|
102
|
+
const vh = window.innerHeight;
|
|
103
|
+
let stickyRect = bounds(this.el);
|
|
104
|
+
|
|
105
|
+
Object.assign(this.config, {
|
|
106
|
+
totalHeight: stickyRect.height - vh,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
destroy() {
|
|
111
|
+
if (this.scrollTrigger) {
|
|
112
|
+
this.scrollTrigger.kill();
|
|
113
|
+
this.scrollTrigger = null;
|
|
114
|
+
}
|
|
115
|
+
this.sticky = null;
|
|
116
|
+
this.el = null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
init() {
|
|
120
|
+
this.resize();
|
|
121
|
+
this.observer();
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/index.js
ADDED