@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 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
@@ -0,0 +1,4 @@
1
+ import Sticky from "./Sticky";
2
+ import Smooth from "./Smooth";
3
+
4
+ export { Sticky, Smooth };