<div class="landing-hero ">
<div class="landing-hero__inner">
<h1 class="landing-hero__title-container">
<span class="landing-hero__title h1" data-text="Letters"><span class="landing-hero__title-text">Letters</span>Letters</span>
<span class="landing-hero__title h1 landing-hero__title--second" data-text="from"><span class="landing-hero__title-text">from</span>from</span>
<span class="landing-hero__title h1 landing-hero__title--third" data-text="Tartu"><span class="landing-hero__title-text">Tartu</span>Tartu</span>
</h1>
<div class="landing-hero__info-container">
<div class="landing-hero__info text-large">
Creative initiative showcasing typefaces designed in Tartu and historical letters found around the town.
</div>
<a href="#" class="button button--icon-text landing-hero__button ">
<span class="button__inner" data-text="Read more">
<span class="button__text">
Read more
<svg class="icon button__icon">
<use xlink:href="../../inc/svg/global.49d3f64de1a3f5ccf16c763316702027.svg#arrow-right"></use>
</svg>
</span>
</span>
</a>
</div>
</div>
</div>
<div class="landing-hero {{ modifier }} {{ class }}">
<div class="landing-hero__inner">
<h1 class="landing-hero__title-container">
<span class="landing-hero__title h1" data-text="Letters"><span class="landing-hero__title-text">Letters</span>Letters</span>
<span class="landing-hero__title h1 landing-hero__title--second" data-text="from"><span class="landing-hero__title-text">from</span>from</span>
<span class="landing-hero__title h1 landing-hero__title--third" data-text="Tartu"><span class="landing-hero__title-text">Tartu</span>Tartu</span>
</h1>
<div class="landing-hero__info-container">
{% if data.info %}
<div class="landing-hero__info text-large">
{{ data.info }}
</div>
{% endif %}
{% if data.button %}
{% include '@button' with { data: data.button|merge({icon: 'arrow-right'}), modifier: 'button--icon-text', class: 'landing-hero__button'}%}
{% endif %}
</div>
</div>
</div>
{
"language": "en-US",
"data": {
"info": "Creative initiative showcasing typefaces designed in Tartu and historical letters found around the town.",
"button": {
"text": "Read more",
"icon": "arrow-right",
"link": "#"
}
}
}
@keyframes appear-horizontal {
from {
width: 0;
}
to {
width: 100%;
}
}
@keyframes appear-vertical {
from {
height: 0;
}
to {
height: 100%;
}
}
@keyframes appear-title {
from {
font-size: 425px;
line-height: 450px;
transform: translate(100vw, 100vh);
}
to {
position: absolute;
font-size: $font-size-h1-lg;
line-height: $font-line-height-h1-lg;
transform: translate(0, 0);
}
}
.landing-hero {
display: flex;
flex-direction: column;
justify-content: flex-start;
height: 101vh;
min-height: 700px;
padding-top: 80px;
@include bp(sm-min) {
padding-top: 120px;
min-height: 800px;
}
@include bp(md-min) {
padding-top: 0;
justify-content: center;
}
}
.landing-hero__inner {
display: flex;
position: relative;
flex-direction: column;
@include bp(md-min) {
flex-direction: row;
}
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 0;
bottom: 0;
border-top: 1px solid var(--color-brand);
transition: $transition-duration $transition-easing;
transition-property: border-top;
}
.landing-hero.is-animating &:before {
animation: .8s $transition-easing 0s appear-horizontal forwards;
}
&:after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 0;
bottom: 0;
border-bottom: 1px solid var(--color-brand);
pointer-events: none;
transition: $transition-duration $transition-easing;
transition-property: border-bottom;
}
.landing-hero.is-animating &:after {
animation: .8s $transition-easing 1s appear-horizontal forwards;
@include bp(md-min) {
animation: .8s $transition-easing .75s appear-horizontal forwards;
}
}
}
.landing-hero__title-container {
display: flex;
flex: 1;
flex-direction: column;
position: relative;
border-right: none;
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 0;
@include bp(md-min) {
border-right: 1px solid var(--color-brand);
transition: $transition-duration $transition-easing;
transition-property: border-right;
}
}
.landing-hero.is-animating &:before {
animation: .8s $transition-easing 0s appear-vertical forwards;
}
}
.landing-hero__title {
color: transparent;
display: block;
border-right: none;
position: relative;
padding-left: 16px;
flex-grow: 1;
@include bp(md-min) {
padding-left: 92px;
}
& + &:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 0;
bottom: 0;
border-top: 1px solid var(--color-brand);
transition: $transition-duration $transition-easing;
transition-property: border-top;
}
}
.landing-hero__title--second {
.landing-hero.is-animating &:before {
animation: .8s $transition-easing .25s appear-horizontal forwards;
}
}
.landing-hero__title--third {
&:after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 0;
bottom: 0;
border-bottom: 1px solid var(--color-brand);
transition: $transition-duration $transition-easing;
transition-property: border-bottom;
}
.landing-hero.is-animating &:before {
animation: .8s $transition-easing .5s appear-horizontal forwards;
}
.landing-hero.is-animating &:after {
animation: .8s $transition-easing .75s appear-horizontal forwards;
@include bp(md-min) {
visibility: hidden;
}
}
}
.landing-hero__title-text {
position: absolute;
top: 0;
color: var(--color-text);
transition: $transition-duration $transition-easing;
transition-property: color;
}
.landing-hero__info-container {
flex: 1;
}
.landing-hero__info {
padding: 80px 32px 50px 71px;
transition: $transition-duration $transition-easing $transition-duration * 3;
transition-property: opacity, transform;
opacity: 0;
transform: translateY(-50px);
.landing-hero.is-animating & {
opacity: 1;
transform: translateY(0);
}
}
.landing-hero__button {
margin: 0 32px 50px 71px;
transition: $transition-duration $transition-easing $transition-duration * 4;
transition-property: opacity, transform;
opacity: 0;
transform: translateY(-50px);
.landing-hero.is-animating & {
opacity: 1;
transform: translateY(0);
}
}
import './landing-hero.scss';
import Component from '@component';
import helpers from '@helpers';
interface ICoords {
x: number;
y: number;
}
export default class LandingHero extends Component {
static initSelector: string = '.landing-hero';
widthFactor: number = 1.1;
heightFactor: number = 0.8;
h1Size: number = 80;
h1SizeLg: number = 120;
titleSize: number = 105;
titleSizeLg: number = 425;
h1Height: number = 85;
h1HeightLg: number = 140;
titleHeight: number = 125;
titleHeightLg: number = 450;
timingLength: number = .8;
animationDelay: number = 1000;
animationPercent: number = 1;
isAnimating: boolean = false;
titles: JQuery;
titleCoords: ICoords[] = [];
animationRequest: number;
constructor(target: HTMLElement) {
super(target);
this.titles = this.element.find('.landing-hero__title-text');
this.titles.each((index: number, domElement: HTMLElement) => {
this.titleCoords.push({x: domElement.getBoundingClientRect().x, y: domElement.getBoundingClientRect().y});
});
this.init();
}
init(): void {
if ($(window).scrollTop() === 0) {
helpers.disableScroll();
if (helpers.isMobileDevice) {
window.addEventListener('touchstart', this.startAnimation.bind(this));
} else {
window.addEventListener('wheel', this.startAnimation.bind(this));
}
window.addEventListener('resize', () => this.animate(1));
this.animationRequest = requestAnimationFrame(() => this.animate(2));
requestAnimationFrame(() => $('body').removeClass('header-is-visible'));
} else {
this.element.addClass('is-animating');
this.cleanUp();
}
}
smoothstep(x : number): number {
const clampedValue: number = Math.min(1, Math.max(x, 0));
return clampedValue * clampedValue * (3 - 2 * clampedValue);
}
viewportWidthToPixels(vw: number): number {
return vw * (document.documentElement.clientWidth / 100);
}
setTitlePosition(index: number, percent: number): void {
const title: HTMLElement = this.titles.get()[index];
const titleRect: DOMRect = title.getBoundingClientRect();
const maxX: number = window.innerWidth - title.parentElement.getBoundingClientRect().x - titleRect.width * this.widthFactor;
const maxY: number = window.innerHeight - title.parentElement.getBoundingClientRect().y - titleRect.height * this.heightFactor + titleRect.height * index * 2;
const translateX: number = maxX * percent;
const translateY: number = maxY * percent;
let h1Size: number = this.h1Size;
const titleSize: number = this.viewportWidthToPixels(28);
let h1Height: number = this.h1Height;
const titleHeight: number = this.viewportWidthToPixels(28);
if (window.matchMedia('only screen and (min-width: 768px)').matches) {
h1Size = this.h1SizeLg;
h1Height = this.h1HeightLg;
}
const fontSize: number = h1Size + (titleSize - h1Size) * percent;
const lineHeight: number = h1Height + (titleHeight - h1Height) * percent;
title.style.transform = 'translate(' + translateX + 'px, ' + translateY + 'px' + ')';
title.style.fontSize = fontSize + 'px';
title.style.lineHeight = lineHeight + 'px';
}
startAnimation(): void {
if (!this.isAnimating) {
this.element.addClass('is-animating');
this.isAnimating = true;
window.removeEventListener('resize', this.animate.bind(this));
this.animationRequest = requestAnimationFrame(this.animate.bind(this));
}
}
adjustAnimationTiming(percent: number, length: number, delay: number): number {
const lower: number = Math.max((1 - percent) - delay, 0);
return 1 - Math.min(lower / length, 1);
}
animate(times?: number): void {
if (this.isAnimating) {
this.animationPercent = Math.max(0, this.animationPercent - 0.03);
}
if (this.animationPercent > 0) {
this.titles.each((index: number) => {
this.setTitlePosition(index, this.smoothstep(this.adjustAnimationTiming(this.animationPercent, this.timingLength, (1- this.timingLength) / 3 * index)));
});
if (times !== undefined) {
if (times > 0) {
this.animationRequest = requestAnimationFrame(() => this.animate(times - 1));
}
} else {
this.animationRequest = requestAnimationFrame(this.animate.bind(this));
}
} else {
this.cleanUp();
}
}
cleanUp(): void {
helpers.enableScroll();
requestAnimationFrame(() => $('body').addClass('header-is-visible'));
this.titles.get().forEach((title: HTMLElement) => {
title.style.transform = '';
title.style.fontSize = '';
title.style.lineHeight = '';
});
}
}