1
0

feat: Implement Animations for landing page

This commit is contained in:
2026-03-04 15:49:53 +01:00
parent f8065f06cd
commit 7f44ed1112
10 changed files with 768 additions and 347 deletions
+2 -2
View File
@@ -13,9 +13,9 @@
"@astrojs/sitemap": "^3.7.0",
"@astrojs/vue": "^5.1.4",
"@astrolib/seo": "1.0.0-beta.8",
"astro": "^5.17.3",
"astro": "^5.18.0",
"astro-robots-txt": "^1.0.0",
"gsap": "^3.14.2",
"vue": "^3.5.28"
"vue": "^3.5.29"
}
}
+293 -292
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -18,7 +18,7 @@ function getCurrentYear() {
<span class="list-title">Explore</span>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/blog">Blog</a></li>
{/* <li><a href="/blog">Blog</a></li> */}
</ul>
</div>
+24
View File
@@ -96,3 +96,27 @@
border: 2px solid var(--clr-ts-red-400);
}
</style>
<script>
import { gsap } from "gsap";
import { SplitText } from "gsap/SplitText";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(SplitText);
gsap.registerPlugin(ScrollTrigger);
const footer = document.querySelector("footer");
if (footer) {
gsap.from(footer, {
scrollTrigger: {
trigger: footer,
start: "top 90%",
},
duration: 2,
opacity: 0,
yPercent: 10,
ease: "expo.out",
});
}
</script>
+4 -4
View File
@@ -11,7 +11,7 @@ import SkipNavLink from "./SkipNavLink.astro";
<Logo class="logo" />
{/* Contact Link */}
<a class="contact" href="mailto:office@tideshiftdigital.com">
<a href="mailto:office@tideshiftdigital.com">
<span class="heading-gradient">Contact us</span>
<ArrowUpRight class="icon char" />
</a>
@@ -42,7 +42,7 @@ import SkipNavLink from "./SkipNavLink.astro";
grid-column: 1 / 3;
}
.contact {
a {
display: flex;
align-items: center;
gap: 0.5rem;
@@ -78,9 +78,9 @@ import SkipNavLink from "./SkipNavLink.astro";
const header = document.querySelector("header");
if (header) {
const timeline = gsap.timeline();
const tl = gsap.timeline();
timeline.from(header.children, {
tl.from(header.children, {
duration: 1,
yPercent: -120,
opacity: 0,
+120 -19
View File
@@ -3,20 +3,21 @@ import AboutImage from "../../assets/img/about.jpg";
import { Picture } from "astro:assets";
---
<Fragment>
<div class="about" aria-label="About section">
<div class="about">
<div class="heading" aria-label="About section">
<h2>
<span class="heading-gradient">Creative.</span>
<span class="heading-gradient">Innovative.</span>
<span class="heading-gradient">Authentic.</span>
</h2>
<div class="text">
<div>
<p>
Based in the beautiful city of Vienna, Austria, Tideshift aims
to create long lasting partnerships built on sustainable tech
and processes.
</p>
<p>
Whether you're just starting out as a business or you're an
enterprise, we will cater to your specific needs and provide
@@ -39,38 +40,41 @@ import { Picture } from "astro:assets";
source software.
</p>
</div>
</Fragment>
</div>
<style>
.about {
}
.heading {
display: grid;
grid-template-columns: var(--base-grid);
max-width: var(--max-content-width);
margin-inline: auto;
padding-block: 8rem 5.5rem;
padding-inline: 5rem;
}
h2 {
grid-column: 1 / 6;
& h2 {
grid-column: 1 / 6;
font-size: 4.75rem;
line-height: var(--leading-title);
letter-spacing: var(--tracking-narrow);
font-size: 4.75rem;
line-height: var(--leading-title);
letter-spacing: var(--tracking-narrow);
span {
display: block;
span {
display: block;
}
}
}
.text {
grid-column: -1 / -6;
& div {
grid-column: -1 / -6;
font-size: 1.125rem;
line-height: var(--leading-paragraph);
font-size: 1.125rem;
line-height: var(--leading-paragraph);
* + * {
margin-top: 1.25rem;
> p {
margin-top: 1.25rem;
}
}
}
@@ -102,6 +106,103 @@ import { Picture } from "astro:assets";
max-width: 43ch;
font-size: 2rem;
line-height: var(--leading-paragraph);
z-index: 1;
}
}
</style>
<script>
import { gsap } from "gsap";
import { SplitText } from "gsap/SplitText";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(SplitText);
gsap.registerPlugin(ScrollTrigger);
const aboutSection = document.querySelector(".about");
if (aboutSection) {
const tl = gsap.timeline({
scrollTrigger: {
trigger: aboutSection,
start: "top 70%",
},
});
tl.addLabel("heading", 0);
tl.addLabel("heading-text", 0.6);
tl.addLabel("image", 1);
tl.addLabel("image-text", 1.4);
new SplitText(aboutSection.querySelector("h2"), {
type: "words, chars",
autoSplit: true,
mask: "chars",
charsClass: "char",
onSplit: (self) => {
tl.from(
self.chars,
{
duration: 0.9,
yPercent: -120,
scale: 1.2,
stagger: 0.015,
ease: "expo.out",
onComplete: () => self.revert(),
},
"heading",
);
},
});
new SplitText(aboutSection.querySelectorAll(".heading p"), {
type: "lines, words",
autoSplit: true,
mask: "lines",
linesClass: "line",
onSplit: (self) => {
tl.from(
self.lines,
{
duration: 0.9,
yPercent: 105,
stagger: 0.06,
ease: "expo.out",
onComplete: () => self.revert(),
},
"heading-text",
);
},
});
tl.from(
aboutSection.querySelector(".background"),
{
duration: 2,
opacity: 0,
ease: "expo.out",
},
"image",
);
new SplitText(aboutSection.querySelectorAll(".image > p"), {
type: "lines, words",
autoSplit: true,
mask: "lines",
linesClass: "line",
onSplit: (self) => {
tl.from(
self.lines,
{
duration: 0.9,
yPercent: 105,
stagger: 0.06,
ease: "expo.out",
onComplete: () => self.revert(),
},
"image-text",
);
},
});
}
</script>
@@ -92,3 +92,100 @@ import ArrowUpRight from "../icons/ArrowUpRight.astro";
}
}
</style>
<script>
import { gsap } from "gsap";
import { SplitText } from "gsap/SplitText";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(SplitText);
gsap.registerPlugin(ScrollTrigger);
const contactSection = document.querySelector(".contact");
if (contactSection) {
const tl = gsap.timeline({
scrollTrigger: {
trigger: contactSection,
start: "top 60%",
},
});
tl.addLabel("image", 0.2);
tl.addLabel("heading", 0.5);
tl.addLabel("text", 0.6);
new SplitText(contactSection.querySelector("h2"), {
type: "words, chars",
autoSplit: true,
mask: "chars",
charsClass: "char",
onSplit: (self) => {
tl.from(
self.chars,
{
duration: 0.9,
yPercent: -120,
scale: 1.2,
stagger: 0.015,
ease: "expo.out",
onComplete: () => self.revert(),
},
"heading",
);
},
});
new SplitText(contactSection.querySelector("p"), {
type: "lines, words",
autoSplit: true,
mask: "lines",
linesClass: "line",
onSplit: (self) => {
tl.from(
self.lines,
{
duration: 0.9,
yPercent: 105,
stagger: 0.06,
ease: "expo.out",
onComplete: () => self.revert(),
},
"text",
);
},
});
tl.from(
contactSection.querySelector(".image"),
{
duration: 2,
opacity: 0,
xPercent: -10,
ease: "expo.out",
},
"image",
);
new SplitText(contactSection.querySelector("a"), {
type: "words, chars",
autoSplit: true,
mask: "chars",
charsClass: "char",
onSplit: (self) => {
gsap.from(self.chars, {
scrollTrigger: {
trigger: contactSection,
start: "top 50%",
},
duration: 0.9,
yPercent: -120,
scale: 1.2,
stagger: 0.015,
ease: "expo.out",
onComplete: () => self.revert(),
});
},
});
}
</script>
+35 -25
View File
@@ -125,23 +125,22 @@ import ArrowDown from "../icons/ArrowDown.astro";
const heroSection = document.querySelector(".hero");
if (heroSection) {
const timeline = gsap.timeline().pause();
const tl = gsap.timeline().pause();
timeline.from(heroSection.querySelector(".image"), {
duration: 2,
opacity: 0,
// yPercent: 20,
ease: "expo.out",
});
tl.addLabel("heading", 0);
tl.addLabel("image", 0.2);
tl.addLabel("text", 0.8);
tl.addLabel("arrow", 1.5);
timeline.from(
heroSection.querySelector(".down"),
tl.from(
heroSection.querySelector(".image"),
{
duration: 1,
duration: 2,
opacity: 0,
xPercent: 10,
ease: "expo.out",
},
// ">-1",
"image",
);
new SplitText(heroSection.querySelector("h1"), {
@@ -150,7 +149,7 @@ import ArrowDown from "../icons/ArrowDown.astro";
mask: "chars",
charsClass: "char",
onSplit: (self) => {
timeline.from(
tl.from(
self.chars,
{
duration: 1,
@@ -158,8 +157,9 @@ import ArrowDown from "../icons/ArrowDown.astro";
scale: 1.2,
stagger: 0.015,
ease: "expo.out",
onComplete: () => self.revert(),
},
">-2.8",
"heading",
);
},
});
@@ -170,19 +170,29 @@ import ArrowDown from "../icons/ArrowDown.astro";
mask: "lines",
linesClass: "line",
onSplit: (self) => {
timeline
.from(
self.lines,
{
duration: 0.9,
yPercent: 105,
stagger: 0.06,
ease: "expo.out",
},
">-1",
)
.play();
tl.from(
self.lines,
{
duration: 0.9,
yPercent: 105,
stagger: 0.06,
ease: "expo.out",
onComplete: () => self.revert(),
},
"text",
).play();
},
});
tl.from(
heroSection.querySelector(".down"),
{
duration: 1,
opacity: 0,
translateY: "-100%",
ease: "bounce.out",
},
"arrow",
);
}
</script>
+79 -2
View File
@@ -47,7 +47,7 @@ const services: Array<ComponentProps<typeof ServicesCard>> = [
</p>
</div>
<ul>
<ul class="cards">
{services.map((service) => <ServicesCard {...service} />)}
</ul>
</div>
@@ -86,7 +86,7 @@ const services: Array<ComponentProps<typeof ServicesCard>> = [
}
}
ul {
.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
@@ -98,3 +98,80 @@ const services: Array<ComponentProps<typeof ServicesCard>> = [
margin-top: 7rem;
}
</style>
<script>
import { gsap } from "gsap";
import { SplitText } from "gsap/SplitText";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(SplitText);
gsap.registerPlugin(ScrollTrigger);
const servicesSection = document.querySelector(".services");
if (servicesSection) {
const tl = gsap.timeline({
scrollTrigger: {
trigger: servicesSection,
start: "top 60%",
},
});
tl.addLabel("heading", 0);
tl.addLabel("text", 0.3);
tl.addLabel("cards", 0.8);
new SplitText(servicesSection.querySelector("h2"), {
type: "words, chars",
autoSplit: true,
mask: "chars",
charsClass: "char",
onSplit: (self) => {
tl.from(
self.chars,
{
duration: 1,
yPercent: -120,
scale: 1.2,
stagger: 0.015,
ease: "expo.out",
onComplete: () => self.revert(),
},
"heading",
);
},
});
new SplitText(servicesSection.querySelector("p"), {
type: "lines, words",
autoSplit: true,
mask: "lines",
linesClass: "line",
onSplit: (self) => {
tl.from(
self.lines,
{
duration: 1.5,
yPercent: 105,
stagger: 0.06,
ease: "expo.out",
onComplete: () => self.revert(),
},
"text",
);
},
});
tl.from(
servicesSection.querySelectorAll(".cards > *"),
{
duration: 1,
opacity: 0,
yPercent: 5,
stagger: 0.15,
ease: "expo.out",
},
"cards",
);
}
</script>
+113 -2
View File
@@ -2,7 +2,7 @@
const values = [
{
title: "Sustainability",
text: "Choosing Tideshift as your digital partner is a sustainable way to build your next project and keep it running. We will build your vision on open tech and support you in gaining digital sovereignty.",
text: "Choosing Tideshift as your digital partner is a future-proof way to build your next project and keep it running. We will build your vision on open tech and sustainable infrastructure to maximize control and minimize resource usage.",
},
{
title: "Collaboration",
@@ -10,7 +10,7 @@ const values = [
},
{
title: "Privacy",
text: "We strongly believe that individuals and businesses should be in control of their data and stay aware of what is happening with their creative work. That's why we focus on using technology and services that protect your data instead of selling it for profit.",
text: "We strongly believe that individuals and businesses should be in control of their data and stay aware of what is happening with their creative work. That's why we focus on using technology and infrastructure that protects your data instead of selling it for profit.",
},
];
@@ -101,3 +101,114 @@ function formatIndex(index: number) {
line-height: var(--leading-tightest);
}
</style>
<script>
import { gsap } from "gsap";
import { SplitText } from "gsap/SplitText";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(SplitText);
gsap.registerPlugin(ScrollTrigger);
const valuesSection = document.querySelector(".values");
if (valuesSection) {
const heading = valuesSection.querySelector("h2");
new SplitText(heading, {
type: "words, chars",
autoSplit: true,
mask: "chars",
charsClass: "char",
onSplit: (self) => {
gsap.from(self.chars, {
scrollTrigger: {
trigger: heading,
start: "top 70%",
},
duration: 0.9,
yPercent: -120,
scale: 1.2,
stagger: 0.015,
ease: "expo.out",
onComplete: () => self.revert(),
});
},
});
const items = valuesSection.querySelectorAll("li");
items.forEach((item) => {
const itemTl = gsap.timeline({
scrollTrigger: {
trigger: item,
start: "top 70%",
},
});
itemTl.addLabel("number", 0);
itemTl.addLabel("heading", 0.2);
itemTl.addLabel("text", 0.5);
new SplitText(item.querySelector("span"), {
type: "words, chars",
autoSplit: true,
mask: "chars",
charsClass: "char",
onSplit: (self) => {
itemTl.from(
self.chars,
{
duration: 0.9,
yPercent: -120,
stagger: 0.25,
ease: "expo.out",
onComplete: () => self.revert(),
},
"number",
);
},
});
new SplitText(item.querySelector("h3"), {
type: "words, chars",
autoSplit: true,
mask: "chars",
charsClass: "char",
onSplit: (self) => {
itemTl.from(
self.chars,
{
duration: 0.9,
yPercent: -120,
stagger: 0.015,
ease: "expo.out",
onComplete: () => self.revert(),
},
"heading",
);
},
});
new SplitText(item.querySelector("p"), {
type: "lines, words",
autoSplit: true,
mask: "lines",
linesClass: "line",
onSplit: (self) => {
itemTl.from(
self.lines,
{
duration: 0.9,
yPercent: 105,
stagger: 0.06,
ease: "expo.out",
onComplete: () => self.revert(),
},
"text",
);
},
});
});
}
</script>