@squeditor/squeditor-framework 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/LICENSE +21 -0
- package/README.md +106 -0
- package/package.json +36 -0
- package/php/functions.php +92 -0
- package/project-template/package.json +29 -0
- package/project-template/postcss.config.js +6 -0
- package/project-template/squeditor.config.js +81 -0
- package/project-template/src/404.php +21 -0
- package/project-template/src/assets/css/squeditor-icons.css +4719 -0
- package/project-template/src/assets/css/tailwind.css +3 -0
- package/project-template/src/assets/css/uikit-components.css +14586 -0
- package/project-template/src/assets/js/gsap-advanced.js +26 -0
- package/project-template/src/assets/js/gsap-init.js +672 -0
- package/project-template/src/assets/js/gsap-modules/cursor-preview.js +132 -0
- package/project-template/src/assets/js/gsap-modules/cursor.js +456 -0
- package/project-template/src/assets/js/gsap-modules/loop-panels.js +78 -0
- package/project-template/src/assets/js/gsap-modules/marquee.js +106 -0
- package/project-template/src/assets/js/gsap-modules/pinned-panels.js +105 -0
- package/project-template/src/assets/js/gsap-modules/scroll-to.js +54 -0
- package/project-template/src/assets/js/gsap-modules/swipe-slider.js +121 -0
- package/project-template/src/assets/js/gsap-modules/text-mask.js +93 -0
- package/project-template/src/assets/js/gsap-modules/tilt.js +70 -0
- package/project-template/src/assets/js/main.js +302 -0
- package/project-template/src/assets/js/uikit-components.js +18171 -0
- package/project-template/src/assets/scss/_base.scss +140 -0
- package/project-template/src/assets/scss/_components.scss +165 -0
- package/project-template/src/assets/scss/_config.scss +13 -0
- package/project-template/src/assets/scss/_functions.scss +81 -0
- package/project-template/src/assets/scss/_tokens.scss +229 -0
- package/project-template/src/assets/scss/_transitions.scss +36 -0
- package/project-template/src/assets/scss/_uikit-overrides.scss +187 -0
- package/project-template/src/assets/scss/_uikit_dynamic.scss +43 -0
- package/project-template/src/assets/scss/_utilities.scss +31 -0
- package/project-template/src/assets/scss/custom.scss +10 -0
- package/project-template/src/assets/scss/main.scss +11 -0
- package/project-template/src/assets/static/fonts/squeditor-icons/squeditor-icons.eot +0 -0
- package/project-template/src/assets/static/fonts/squeditor-icons/squeditor-icons.svg +1183 -0
- package/project-template/src/assets/static/fonts/squeditor-icons/squeditor-icons.ttf +0 -0
- package/project-template/src/assets/static/fonts/squeditor-icons/squeditor-icons.woff +0 -0
- package/project-template/src/config/site-config.php +34 -0
- package/project-template/src/data/blog.php +21 -0
- package/project-template/src/data/portfolio.php +23 -0
- package/project-template/src/data/team.php +23 -0
- package/project-template/src/index.php +57 -0
- package/project-template/src/init.php +19 -0
- package/project-template/src/page-templates/base.php +39 -0
- package/project-template/src/page-templates/body-scripts.php +26 -0
- package/project-template/src/page-templates/head.php +47 -0
- package/project-template/src/page-templates/transition.php +45 -0
- package/project-template/src/sections/cards/cards-grid.php +34 -0
- package/project-template/src/sections/cards/cards-horizontal.php +28 -0
- package/project-template/src/sections/cta/cta-banner.php +34 -0
- package/project-template/src/sections/cta/cta-newsletter.php +19 -0
- package/project-template/src/sections/footer/layout-01.php +35 -0
- package/project-template/src/sections/header/layout-01.php +36 -0
- package/project-template/src/sections/hero/hero-centered.php +44 -0
- package/project-template/src/sections/hero/hero-split.php +132 -0
- package/project-template/src/sections/hero/hero-video.php +22 -0
- package/project-template/src/sections/sidebar/sidebar-right.php +11 -0
- package/project-template/src/template-parts/breadcrumbs.php +17 -0
- package/project-template/src/template-parts/footer.php +74 -0
- package/project-template/src/template-parts/header.php +120 -0
- package/project-template/src/template-parts/mega-menu.php +7 -0
- package/project-template/src/template-parts/nav.php +16 -0
- package/project-template/src/template-parts/page-title-bar.php +14 -0
- package/project-template/tailwind.config.js +26 -0
- package/project-template/vite.config.js +67 -0
- package/scripts/build-components.js +109 -0
- package/scripts/copy-static.js +150 -0
- package/scripts/dev-router.php +23 -0
- package/scripts/dev.js +55 -0
- package/scripts/get-port.js +27 -0
- package/scripts/package-customer.js +278 -0
- package/scripts/package-dist.js +54 -0
- package/scripts/scaffold.js +72 -0
- package/scripts/snapshot.js +74 -0
- package/uikit-manifest.json +248 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// src/assets/js/gsap-modules/marquee.js
|
|
2
|
+
import { gsap } from 'gsap';
|
|
3
|
+
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
|
4
|
+
|
|
5
|
+
gsap.registerPlugin(ScrollTrigger);
|
|
6
|
+
|
|
7
|
+
export function init() {
|
|
8
|
+
const marquees = document.querySelectorAll('[data-sq-marquee]');
|
|
9
|
+
if (!marquees.length) return;
|
|
10
|
+
|
|
11
|
+
marquees.forEach(wrapper => {
|
|
12
|
+
const attr = wrapper.getAttribute('data-sq-marquee') || '';
|
|
13
|
+
const config = parseModuleAttr(attr);
|
|
14
|
+
const speed = config.speed ?? 50; // px per second
|
|
15
|
+
const gap = config.gap ?? 40;
|
|
16
|
+
const direction = config.direction === 'right' ? 1 : -1;
|
|
17
|
+
|
|
18
|
+
// Clone items to fill width for seamless loop
|
|
19
|
+
const items = Array.from(wrapper.children);
|
|
20
|
+
const cloneGroup = document.createElement('div');
|
|
21
|
+
cloneGroup.className = 'sq-marquee__inner';
|
|
22
|
+
const origGroup = document.createElement('div');
|
|
23
|
+
origGroup.className = 'sq-marquee__inner';
|
|
24
|
+
|
|
25
|
+
items.forEach(item => {
|
|
26
|
+
origGroup.appendChild(item.cloneNode(true));
|
|
27
|
+
cloneGroup.appendChild(item.cloneNode(true));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
wrapper.innerHTML = '';
|
|
31
|
+
wrapper.style.overflow = 'hidden';
|
|
32
|
+
wrapper.style.display = 'flex';
|
|
33
|
+
|
|
34
|
+
injectStyles(`
|
|
35
|
+
.sq-marquee__inner {
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: ${gap}px;
|
|
39
|
+
white-space: nowrap;
|
|
40
|
+
flex-shrink: 0;
|
|
41
|
+
padding-right: ${gap}px;
|
|
42
|
+
}
|
|
43
|
+
`);
|
|
44
|
+
|
|
45
|
+
wrapper.appendChild(origGroup);
|
|
46
|
+
wrapper.appendChild(cloneGroup);
|
|
47
|
+
|
|
48
|
+
// Calculate total width for one group
|
|
49
|
+
const totalWidth = origGroup.scrollWidth;
|
|
50
|
+
|
|
51
|
+
let currentSpeed = speed * direction * -1;
|
|
52
|
+
|
|
53
|
+
// Infinite scroll animation
|
|
54
|
+
const tween = gsap.to([origGroup, cloneGroup], {
|
|
55
|
+
x: `+=${totalWidth * (direction < 0 ? -1 : 1)}`,
|
|
56
|
+
modifiers: {
|
|
57
|
+
x: gsap.utils.unitize(x => parseFloat(x) % totalWidth),
|
|
58
|
+
},
|
|
59
|
+
duration: totalWidth / speed,
|
|
60
|
+
ease: 'none',
|
|
61
|
+
repeat: -1,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Reverse / slow on scroll direction change
|
|
65
|
+
ScrollTrigger.create({
|
|
66
|
+
onUpdate: (self) => {
|
|
67
|
+
const velocity = self.getVelocity();
|
|
68
|
+
if (Math.abs(velocity) > 10) {
|
|
69
|
+
const factor = velocity < 0 ? -1 : 1;
|
|
70
|
+
gsap.to(tween, {
|
|
71
|
+
timeScale: factor * Math.min(Math.abs(velocity) / 500, 3),
|
|
72
|
+
duration: 0.5,
|
|
73
|
+
ease: 'power2.out',
|
|
74
|
+
overwrite: true,
|
|
75
|
+
});
|
|
76
|
+
} else {
|
|
77
|
+
gsap.to(tween, {
|
|
78
|
+
timeScale: direction < 0 ? -1 : 1,
|
|
79
|
+
duration: 0.8,
|
|
80
|
+
ease: 'power2.out',
|
|
81
|
+
overwrite: true,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseModuleAttr(str) {
|
|
90
|
+
const config = {};
|
|
91
|
+
if (!str) return config;
|
|
92
|
+
str.split(';').forEach(part => {
|
|
93
|
+
const idx = part.indexOf(':');
|
|
94
|
+
if (idx === -1) return;
|
|
95
|
+
const key = part.slice(0, idx).trim();
|
|
96
|
+
const val = part.slice(idx + 1).trim().replace(/['"]/g, '');
|
|
97
|
+
config[key] = isNaN(val) ? val : parseFloat(val);
|
|
98
|
+
});
|
|
99
|
+
return config;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function injectStyles(css) {
|
|
103
|
+
const style = document.createElement('style');
|
|
104
|
+
style.textContent = css;
|
|
105
|
+
document.head.appendChild(style);
|
|
106
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// src/assets/js/gsap-modules/pinned-panels.js
|
|
2
|
+
import { gsap } from 'gsap';
|
|
3
|
+
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
|
4
|
+
|
|
5
|
+
gsap.registerPlugin(ScrollTrigger);
|
|
6
|
+
|
|
7
|
+
export function init() {
|
|
8
|
+
const wrappers = document.querySelectorAll('[data-sq-panels]');
|
|
9
|
+
if (!wrappers.length) return;
|
|
10
|
+
|
|
11
|
+
wrappers.forEach(wrapper => {
|
|
12
|
+
const attr = wrapper.getAttribute('data-sq-panels') || '';
|
|
13
|
+
const config = parseModuleAttr(attr);
|
|
14
|
+
const rounded = config.rounded ?? true;
|
|
15
|
+
const panels = Array.from(wrapper.children);
|
|
16
|
+
|
|
17
|
+
injectStyles(`
|
|
18
|
+
[data-sq-panels] {
|
|
19
|
+
position: relative;
|
|
20
|
+
z-index: 1;
|
|
21
|
+
}
|
|
22
|
+
.sq-panel {
|
|
23
|
+
width: 100%;
|
|
24
|
+
height: 100vh;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
position: relative;
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
${rounded ? 'border-radius: 0 0 24px 24px;' : ''}
|
|
29
|
+
will-change: transform;
|
|
30
|
+
}
|
|
31
|
+
.sq-panel-inner {
|
|
32
|
+
width: 100%;
|
|
33
|
+
height: auto;
|
|
34
|
+
min-height: 100vh;
|
|
35
|
+
}
|
|
36
|
+
`);
|
|
37
|
+
|
|
38
|
+
// All panels except the last one pin and scale away
|
|
39
|
+
const pinningPanels = panels.slice(0, -1);
|
|
40
|
+
|
|
41
|
+
pinningPanels.forEach((panel) => {
|
|
42
|
+
let innerpanel = panel.querySelector(".sq-panel-inner") || panel;
|
|
43
|
+
|
|
44
|
+
// Calculate how much inner content exceeds the viewport height
|
|
45
|
+
let panelHeight = innerpanel.offsetHeight;
|
|
46
|
+
let windowHeight = window.innerHeight;
|
|
47
|
+
let difference = panelHeight - windowHeight;
|
|
48
|
+
|
|
49
|
+
// Calculate the ratio of the scroll distance that applies to fake-scrolling
|
|
50
|
+
let fakeScrollRatio = difference > 0 ? (difference / (difference + windowHeight)) : 0;
|
|
51
|
+
|
|
52
|
+
// Add margin to push the start of the next panel down, giving us scroll distance
|
|
53
|
+
if (fakeScrollRatio) {
|
|
54
|
+
panel.style.marginBottom = panelHeight * fakeScrollRatio + "px";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let tl = gsap.timeline({
|
|
58
|
+
scrollTrigger: {
|
|
59
|
+
trigger: panel,
|
|
60
|
+
start: "top top",
|
|
61
|
+
end: () => fakeScrollRatio ? `+=${innerpanel.offsetHeight}` : "bottom top",
|
|
62
|
+
pinSpacing: false,
|
|
63
|
+
pin: true,
|
|
64
|
+
scrub: true
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// If there's overflow, translate the inner content up to simulate scrolling
|
|
69
|
+
if (fakeScrollRatio) {
|
|
70
|
+
tl.to(innerpanel, {
|
|
71
|
+
yPercent: -100,
|
|
72
|
+
y: windowHeight,
|
|
73
|
+
duration: 1 / (1 - fakeScrollRatio) - 1,
|
|
74
|
+
ease: "none"
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Standard scale and fade away transition for the panel itself
|
|
79
|
+
tl.fromTo(panel,
|
|
80
|
+
{ scale: 1, opacity: 1 },
|
|
81
|
+
{ scale: 0.85, opacity: 0.5, duration: 0.9, ease: "none" }
|
|
82
|
+
)
|
|
83
|
+
.to(panel, { opacity: 0, duration: 0.1, ease: "none" });
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseModuleAttr(str) {
|
|
89
|
+
const config = {};
|
|
90
|
+
if (!str) return config;
|
|
91
|
+
str.split(';').forEach(part => {
|
|
92
|
+
const idx = part.indexOf(':');
|
|
93
|
+
if (idx === -1) return;
|
|
94
|
+
const key = part.slice(0, idx).trim();
|
|
95
|
+
const val = part.slice(idx + 1).trim().replace(/['"]/g, '');
|
|
96
|
+
config[key] = isNaN(val) ? (val === 'true' ? true : (val === 'false' ? false : val)) : parseFloat(val);
|
|
97
|
+
});
|
|
98
|
+
return config;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function injectStyles(css) {
|
|
102
|
+
const style = document.createElement('style');
|
|
103
|
+
style.textContent = css;
|
|
104
|
+
document.head.appendChild(style);
|
|
105
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// src/assets/js/gsap-modules/scroll-to.js
|
|
2
|
+
import { gsap } from 'gsap';
|
|
3
|
+
import { ScrollToPlugin } from 'gsap/ScrollToPlugin';
|
|
4
|
+
|
|
5
|
+
gsap.registerPlugin(ScrollToPlugin);
|
|
6
|
+
|
|
7
|
+
export function init() {
|
|
8
|
+
// Select all elements that have the [data-sq-scrollto] attribute
|
|
9
|
+
const triggers = document.querySelectorAll('[data-sq-scrollto]');
|
|
10
|
+
|
|
11
|
+
triggers.forEach(trigger => {
|
|
12
|
+
// Read configuration from the attribute, fallback to empty string
|
|
13
|
+
const configStr = trigger.getAttribute('data-sq-scrollto') || '';
|
|
14
|
+
|
|
15
|
+
// Parse simple config (e.g., target: "#section2", duration: 1, offsetY: 120)
|
|
16
|
+
// If the attribute contains just a string, we assume it's the target selector.
|
|
17
|
+
let config = {};
|
|
18
|
+
|
|
19
|
+
if (configStr.startsWith('{')) {
|
|
20
|
+
try {
|
|
21
|
+
// To allow unquoted keys like {target: "#id", offsetY: 50} we use a small regex replacer or Function constructor
|
|
22
|
+
// A safe enough approach for typical declarative attributes:
|
|
23
|
+
const sanitizedStr = configStr
|
|
24
|
+
.replace(/([{,]\s*)([a-zA-Z0-9_]+)\s*:/g, '$1"$2":') // Quote keys
|
|
25
|
+
.replace(/:\s*'([^']*)'/g, ':"$1"'); // Convert single quotes to double quotes for values
|
|
26
|
+
|
|
27
|
+
config = JSON.parse(sanitizedStr);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.error('GSAP ScrollTo: Invalid JSON config in attribute', configStr, e);
|
|
30
|
+
}
|
|
31
|
+
} else if (configStr) {
|
|
32
|
+
config.target = configStr;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// If target exists in config OR the trigger is an anchor tag, find the destination
|
|
36
|
+
const destination = config.target || trigger.getAttribute('href');
|
|
37
|
+
|
|
38
|
+
if (!destination || destination === '#') return;
|
|
39
|
+
|
|
40
|
+
trigger.addEventListener('click', (e) => {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
|
|
43
|
+
gsap.to(window, {
|
|
44
|
+
duration: config.duration || 1,
|
|
45
|
+
scrollTo: {
|
|
46
|
+
y: destination,
|
|
47
|
+
offsetY: config.offsetY !== undefined ? config.offsetY : 0,
|
|
48
|
+
autoKill: true
|
|
49
|
+
},
|
|
50
|
+
ease: config.ease || "power2.out"
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// src/assets/js/gsap-modules/swipe-slider.js
|
|
2
|
+
import { gsap } from 'gsap';
|
|
3
|
+
import { Observer } from 'gsap/observer';
|
|
4
|
+
import { SplitText } from 'gsap/SplitText';
|
|
5
|
+
|
|
6
|
+
gsap.registerPlugin(Observer, SplitText);
|
|
7
|
+
|
|
8
|
+
export function init() {
|
|
9
|
+
const sliders = document.querySelectorAll('[data-sq-swipe]');
|
|
10
|
+
if (!sliders.length) return;
|
|
11
|
+
|
|
12
|
+
sliders.forEach(wrapper => {
|
|
13
|
+
const attr = wrapper.getAttribute('data-sq-swipe') || '';
|
|
14
|
+
const config = parseModuleAttr(attr);
|
|
15
|
+
const duration = config.duration ?? 1.25;
|
|
16
|
+
const ease = config.ease ?? 'power1.inOut';
|
|
17
|
+
|
|
18
|
+
injectStyles(`
|
|
19
|
+
.sq-swipe-section { visibility: hidden; }
|
|
20
|
+
.sq-swipe-bg .clip-text { overflow: hidden; }
|
|
21
|
+
`);
|
|
22
|
+
|
|
23
|
+
const sections = Array.from(wrapper.querySelectorAll('.sq-swipe-section'));
|
|
24
|
+
const outerWrappers = Array.from(wrapper.querySelectorAll('.sq-swipe-outer'));
|
|
25
|
+
const innerWrappers = Array.from(wrapper.querySelectorAll('.sq-swipe-inner'));
|
|
26
|
+
const images = Array.from(wrapper.querySelectorAll('.sq-swipe-bg'));
|
|
27
|
+
const headings = Array.from(wrapper.querySelectorAll('.section-heading'));
|
|
28
|
+
|
|
29
|
+
let currentIndex = -1;
|
|
30
|
+
let animating = false;
|
|
31
|
+
const wrap = gsap.utils.wrap(0, sections.length);
|
|
32
|
+
|
|
33
|
+
// Prepare text splits if heading class is present
|
|
34
|
+
let splitHeadings = headings.map(heading => {
|
|
35
|
+
return new SplitText(heading, { type: "chars,words,lines", linesClass: "clip-text" });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Reset layout for outer/inner
|
|
39
|
+
gsap.set(outerWrappers, { yPercent: 100 });
|
|
40
|
+
gsap.set(innerWrappers, { yPercent: -100 });
|
|
41
|
+
|
|
42
|
+
function goTo(index, direction) {
|
|
43
|
+
if (animating) return;
|
|
44
|
+
index = wrap(index);
|
|
45
|
+
animating = true;
|
|
46
|
+
let fromTop = direction === -1,
|
|
47
|
+
dFactor = fromTop ? -1 : 1,
|
|
48
|
+
tl = gsap.timeline({
|
|
49
|
+
defaults: { duration: duration, ease: ease },
|
|
50
|
+
onComplete: () => animating = false
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (currentIndex >= 0) {
|
|
54
|
+
gsap.set(sections[currentIndex], { zIndex: 0 });
|
|
55
|
+
tl.to(images[currentIndex], { yPercent: -15 * dFactor })
|
|
56
|
+
.set(sections[currentIndex], { autoAlpha: 0 });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
gsap.set(sections[index], { autoAlpha: 1, zIndex: 1 });
|
|
60
|
+
|
|
61
|
+
tl.fromTo([outerWrappers[index], innerWrappers[index]], {
|
|
62
|
+
yPercent: i => i ? -100 * dFactor : 100 * dFactor
|
|
63
|
+
}, {
|
|
64
|
+
yPercent: 0
|
|
65
|
+
}, 0)
|
|
66
|
+
.fromTo(images[index], { yPercent: 15 * dFactor }, { yPercent: 0 }, 0);
|
|
67
|
+
|
|
68
|
+
if (splitHeadings[index]) {
|
|
69
|
+
tl.fromTo(splitHeadings[index].chars, {
|
|
70
|
+
autoAlpha: 0,
|
|
71
|
+
yPercent: 150 * dFactor
|
|
72
|
+
}, {
|
|
73
|
+
autoAlpha: 1,
|
|
74
|
+
yPercent: 0,
|
|
75
|
+
duration: 1,
|
|
76
|
+
ease: "power2",
|
|
77
|
+
stagger: {
|
|
78
|
+
each: 0.02,
|
|
79
|
+
from: "random"
|
|
80
|
+
}
|
|
81
|
+
}, 0.2);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
currentIndex = index;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
Observer.create({
|
|
88
|
+
target: wrapper,
|
|
89
|
+
type: "wheel,touch,pointer",
|
|
90
|
+
wheelSpeed: -1,
|
|
91
|
+
tolerance: 10,
|
|
92
|
+
preventDefault: true,
|
|
93
|
+
onDown: () => !animating && goTo(currentIndex - 1, -1),
|
|
94
|
+
onUp: () => !animating && goTo(currentIndex + 1, 1)
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
goTo(0, 1);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseModuleAttr(str) {
|
|
102
|
+
const config = {};
|
|
103
|
+
if (!str) return config;
|
|
104
|
+
str.split(';').forEach(part => {
|
|
105
|
+
const idx = part.indexOf(':');
|
|
106
|
+
if (idx === -1) return;
|
|
107
|
+
const key = part.slice(0, idx).trim();
|
|
108
|
+
const val = part.slice(idx + 1).trim().replace(/['"]/g, '');
|
|
109
|
+
if (val === 'true') config[key] = true;
|
|
110
|
+
else if (val === 'false') config[key] = false;
|
|
111
|
+
else if (!isNaN(val)) config[key] = parseFloat(val);
|
|
112
|
+
else config[key] = val;
|
|
113
|
+
});
|
|
114
|
+
return config;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function injectStyles(css) {
|
|
118
|
+
const style = document.createElement('style');
|
|
119
|
+
style.textContent = css;
|
|
120
|
+
document.head.appendChild(style);
|
|
121
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// src/assets/js/gsap-modules/text-mask.js
|
|
2
|
+
import { gsap } from 'gsap';
|
|
3
|
+
import { SplitText } from 'gsap/SplitText';
|
|
4
|
+
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
|
5
|
+
|
|
6
|
+
gsap.registerPlugin(SplitText, ScrollTrigger);
|
|
7
|
+
|
|
8
|
+
export function init() {
|
|
9
|
+
const elements = document.querySelectorAll('[data-sq-mask]');
|
|
10
|
+
if (!elements.length) return;
|
|
11
|
+
|
|
12
|
+
injectStyles(`
|
|
13
|
+
.sq-mask-wrapper {
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
display: inline-block; /* Support nested or inline layouts */
|
|
16
|
+
vertical-align: top;
|
|
17
|
+
width: 100%;
|
|
18
|
+
}
|
|
19
|
+
`);
|
|
20
|
+
|
|
21
|
+
elements.forEach(el => {
|
|
22
|
+
const attr = el.getAttribute('data-sq-mask') || '';
|
|
23
|
+
const config = parseModuleAttr(attr);
|
|
24
|
+
const type = config.type ?? 'lines';
|
|
25
|
+
const duration = config.duration ?? 1.2;
|
|
26
|
+
const stagger = config.stagger ?? 0.15;
|
|
27
|
+
const ease = config.ease ?? 'expo.out';
|
|
28
|
+
const useScroll = config.scroll ?? true;
|
|
29
|
+
|
|
30
|
+
// Ensure opacity is visible before we split (if it was hidden initially via CSS)
|
|
31
|
+
gsap.set(el, { opacity: 1 });
|
|
32
|
+
|
|
33
|
+
// In order for masking to work, we need an outer container with overflow:hidden.
|
|
34
|
+
// If we want to mask lines, we split by lines, then wrap each line in a div.
|
|
35
|
+
const splitFormat = type === 'words' ? 'words' : 'lines';
|
|
36
|
+
|
|
37
|
+
const split = new SplitText(el, {
|
|
38
|
+
type: `${splitFormat},words,chars`, // Force deep split to prevent reflow bugs
|
|
39
|
+
linesClass: splitFormat === 'lines' ? 'sq-split-target' : '',
|
|
40
|
+
wordsClass: splitFormat === 'words' ? 'sq-split-target' : '',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const targets = el.querySelectorAll('.sq-split-target');
|
|
44
|
+
|
|
45
|
+
// Wrap each target in an overflow:hidden mask
|
|
46
|
+
targets.forEach(target => {
|
|
47
|
+
const wrapper = document.createElement('div');
|
|
48
|
+
wrapper.className = 'sq-mask-wrapper';
|
|
49
|
+
target.parentNode.insertBefore(wrapper, target);
|
|
50
|
+
wrapper.appendChild(target);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const vars = {
|
|
54
|
+
yPercent: 100,
|
|
55
|
+
opacity: 0,
|
|
56
|
+
duration,
|
|
57
|
+
stagger,
|
|
58
|
+
ease,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (useScroll) {
|
|
62
|
+
vars.scrollTrigger = {
|
|
63
|
+
trigger: el,
|
|
64
|
+
start: config.start ?? 'top 85%',
|
|
65
|
+
toggleActions: 'play none none none',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
gsap.from(targets, vars);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseModuleAttr(str) {
|
|
74
|
+
const config = {};
|
|
75
|
+
if (!str) return config;
|
|
76
|
+
str.split(';').forEach(part => {
|
|
77
|
+
const idx = part.indexOf(':');
|
|
78
|
+
if (idx === -1) return;
|
|
79
|
+
const key = part.slice(0, idx).trim();
|
|
80
|
+
const val = part.slice(idx + 1).trim().replace(/['"]/g, '');
|
|
81
|
+
if (val === 'true') config[key] = true;
|
|
82
|
+
else if (val === 'false') config[key] = false;
|
|
83
|
+
else if (!isNaN(val)) config[key] = parseFloat(val);
|
|
84
|
+
else config[key] = val;
|
|
85
|
+
});
|
|
86
|
+
return config;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function injectStyles(css) {
|
|
90
|
+
const style = document.createElement('style');
|
|
91
|
+
style.textContent = css;
|
|
92
|
+
document.head.appendChild(style);
|
|
93
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// src/assets/js/gsap-modules/tilt.js
|
|
2
|
+
import { gsap } from 'gsap';
|
|
3
|
+
|
|
4
|
+
export function init() {
|
|
5
|
+
const elements = document.querySelectorAll('[data-sq-tilt]');
|
|
6
|
+
if (!elements.length) return;
|
|
7
|
+
|
|
8
|
+
elements.forEach(el => {
|
|
9
|
+
const attr = el.getAttribute('data-sq-tilt') || '';
|
|
10
|
+
const config = parseModuleAttr(attr);
|
|
11
|
+
|
|
12
|
+
const max = config.max ?? 15;
|
|
13
|
+
const scale = config.scale ?? 1.05;
|
|
14
|
+
const speed = (config.speed ?? 400) / 1000; // convert to seconds for quickTo
|
|
15
|
+
|
|
16
|
+
// Wrap content for inner tilt layer
|
|
17
|
+
el.style.transformStyle = 'preserve-3d';
|
|
18
|
+
el.style.perspective = '1000px';
|
|
19
|
+
|
|
20
|
+
// Setup quickTo mutators for high performance
|
|
21
|
+
const rotateX = gsap.quickTo(el, "rotationX", { ease: "power3", duration: speed });
|
|
22
|
+
const rotateY = gsap.quickTo(el, "rotationY", { ease: "power3", duration: speed });
|
|
23
|
+
const scaleTo = gsap.quickTo(el, "scale", { ease: "power3", duration: speed });
|
|
24
|
+
|
|
25
|
+
// Inner parallax tracking
|
|
26
|
+
const innerLayers = el.querySelectorAll('[data-sq-tilt-inner]');
|
|
27
|
+
const innerX = innerLayers.length ? gsap.quickTo(innerLayers, "x", { ease: "power3", duration: speed }) : null;
|
|
28
|
+
const innerY = innerLayers.length ? gsap.quickTo(innerLayers, "y", { ease: "power3", duration: speed }) : null;
|
|
29
|
+
|
|
30
|
+
el.addEventListener('pointermove', (e) => {
|
|
31
|
+
const rect = el.getBoundingClientRect();
|
|
32
|
+
const centerX = rect.left + rect.width / 2;
|
|
33
|
+
const centerY = rect.top + rect.height / 2;
|
|
34
|
+
|
|
35
|
+
// Calculate interpolation relative to center
|
|
36
|
+
const pctX = (e.clientX - centerX) / (rect.width / 2);
|
|
37
|
+
const pctY = (e.clientY - centerY) / (rect.height / 2);
|
|
38
|
+
|
|
39
|
+
// Map limits
|
|
40
|
+
rotateX(pctY * -max);
|
|
41
|
+
rotateY(pctX * max);
|
|
42
|
+
scaleTo(scale);
|
|
43
|
+
|
|
44
|
+
// Inner elements move double the max in opposite parallax relative to rotation
|
|
45
|
+
if (innerX) innerX(pctX * -(max * 2));
|
|
46
|
+
if (innerY) innerY(pctY * -(max * 2));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
el.addEventListener('pointerleave', () => {
|
|
50
|
+
rotateX(0);
|
|
51
|
+
rotateY(0);
|
|
52
|
+
scaleTo(1);
|
|
53
|
+
if (innerX) innerX(0);
|
|
54
|
+
if (innerY) innerY(0);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseModuleAttr(str) {
|
|
60
|
+
const config = {};
|
|
61
|
+
if (!str) return config;
|
|
62
|
+
str.split(';').forEach(part => {
|
|
63
|
+
const idx = part.indexOf(':');
|
|
64
|
+
if (idx === -1) return;
|
|
65
|
+
const key = part.slice(0, idx).trim();
|
|
66
|
+
const val = part.slice(idx + 1).trim().replace(/['"]/g, '');
|
|
67
|
+
config[key] = isNaN(val) ? val : parseFloat(val);
|
|
68
|
+
});
|
|
69
|
+
return config;
|
|
70
|
+
}
|