add contact form
This commit is contained in:
parent
dd0d6c25a2
commit
1d735144ed
10 changed files with 610 additions and 76 deletions
226
src/lib/components/ContactCanvas.svelte
Normal file
226
src/lib/components/ContactCanvas.svelte
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
<script lang="ts">
|
||||
import * as PIXI from 'pixi.js';
|
||||
import { BulgePinchFilter } from 'pixi-filters';
|
||||
import gsap from 'gsap';
|
||||
import ScrollTrigger from 'gsap/dist/ScrollTrigger';
|
||||
import SplitText from 'gsap/dist/SplitText';
|
||||
import { PixiPlugin } from "gsap/dist/PixiPlugin";
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import createCanvasText from '$lib/utils/createCanvasText';
|
||||
import createCanvasImg from '$lib/utils/createCanvasImg';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
export let textsToCanvas: Array<HTMLElement> = [];
|
||||
export let imgsToCanvas: Array<HTMLElement> = [];
|
||||
let app: PIXI.Application;
|
||||
let canvas: HTMLCanvasElement;
|
||||
|
||||
onMount(()=>{
|
||||
|
||||
function xFrac(x: number){
|
||||
return window.innerWidth * x;
|
||||
}
|
||||
function yFrac(y: number){
|
||||
return window.innerHeight * y;
|
||||
}
|
||||
|
||||
let is_fine = window.matchMedia('(pointer:fine)').matches
|
||||
let is_coarse = window.matchMedia('(pointer:coarse)').matches
|
||||
let is_landscape = window.matchMedia('(orientation:landscape)').matches
|
||||
let is_portrait = window.matchMedia('(orientation:portrait)').matches
|
||||
|
||||
gsap.registerPlugin(PixiPlugin, ScrollTrigger, SplitText);
|
||||
|
||||
app = new PIXI.Application({
|
||||
resizeTo: window,
|
||||
antialias: true,
|
||||
autoDensity: true,
|
||||
resolution: 2,
|
||||
backgroundAlpha: 0,
|
||||
view: canvas,
|
||||
});
|
||||
|
||||
//for debugging but Typescript has an issue with this:
|
||||
(globalThis as any).__PIXI_APP__ = app;
|
||||
|
||||
PixiPlugin.registerPIXI(PIXI);
|
||||
|
||||
let bulgegroup = new PIXI.Container();
|
||||
bulgegroup.pivot.set(xFrac(0.5), yFrac(0.5));
|
||||
bulgegroup.x = xFrac(0.5);
|
||||
bulgegroup.y = yFrac(0.5);
|
||||
app.stage.addChild(bulgegroup);
|
||||
|
||||
let bulgebg = new PIXI.Graphics();
|
||||
bulgebg.beginFill('rgb(0, 0, 0)');
|
||||
bulgebg.drawRect(0, 0, xFrac(1), yFrac(1));
|
||||
bulgebg.endFill();
|
||||
bulgebg.alpha = 0;
|
||||
bulgebg.pivot.set(xFrac(.5), yFrac(.5));
|
||||
bulgebg.x = xFrac(0.5);
|
||||
bulgebg.y = yFrac(0.5);
|
||||
bulgegroup.addChild(bulgebg);
|
||||
|
||||
let center = [0.5, 0.5];
|
||||
let bulgefilter = new BulgePinchFilter();
|
||||
bulgefilter.radius = is_landscape ? xFrac(0.5) : xFrac(0.55);
|
||||
bulgefilter.strength = 0.5;
|
||||
bulgefilter.center = is_landscape ? center : [0.5, 0];
|
||||
bulgefilter.resolution = 2;
|
||||
|
||||
bulgegroup.filters = [bulgefilter];
|
||||
|
||||
/*----------------------------------
|
||||
* Convert text to canvas using
|
||||
* createCanvasText function
|
||||
----------------------------------*/
|
||||
let canvasTexts: Array<PIXI.Text> = [];
|
||||
let elems: Array<HTMLElement> = [];
|
||||
|
||||
async function convertText(){
|
||||
await tick();
|
||||
textsToCanvas.forEach((element) => {
|
||||
elems.push(element);
|
||||
let canvasText = createCanvasText(element, bulgegroup);
|
||||
canvasTexts.push(canvasText);
|
||||
})
|
||||
}
|
||||
|
||||
/*----------------------------------
|
||||
* Function to update text on canvas
|
||||
* runs in the Ticker
|
||||
----------------------------------*/
|
||||
function updateText(){
|
||||
canvasTexts.forEach((text, index) => {
|
||||
let headlinePosition = elems[index].getBoundingClientRect();
|
||||
headlinePosition.x = headlinePosition.x ;
|
||||
headlinePosition.y = headlinePosition.y ;
|
||||
text.position.set(headlinePosition.x, headlinePosition.y);
|
||||
text.alpha = elems[index].style.opacity as unknown as number;
|
||||
text.style.fill = window.getComputedStyle(elems[index]).color;
|
||||
})
|
||||
}
|
||||
|
||||
/*----------------------------------
|
||||
* Convert images to canvas
|
||||
* createCanvacImgs function
|
||||
----------------------------------*/
|
||||
let canvasImgs: Array<PIXI.Sprite> = [];
|
||||
let imgElems: Array<HTMLElement> = [];
|
||||
|
||||
function convertImgs(){
|
||||
imgsToCanvas.forEach((element) => {
|
||||
imgElems.push(element);
|
||||
let canvasImg = createCanvasImg(element as HTMLImageElement, app.stage);
|
||||
canvasImgs.push(canvasImg);
|
||||
})
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
convertImgs();
|
||||
convertText();
|
||||
}, 100);
|
||||
|
||||
/*----------------------------------
|
||||
* Function to update text on canvas
|
||||
* runs in the Ticker
|
||||
----------------------------------*/
|
||||
function updateImgs(){
|
||||
canvasImgs.forEach((image, index) => {
|
||||
let imagePosition = imgElems[index].getBoundingClientRect();
|
||||
imagePosition.x = imagePosition.x + xFrac(0.1);
|
||||
imagePosition.y = imagePosition.y + yFrac(0.1);
|
||||
image.position.set(imagePosition.x, imagePosition.y);
|
||||
image.width = imagePosition.width;
|
||||
image.height = imagePosition.height;
|
||||
})
|
||||
}
|
||||
|
||||
/*----------------------------------
|
||||
* Mousemove events
|
||||
*----------------------------------*/
|
||||
let tween = {
|
||||
x: 0.5,
|
||||
y: is_fine ? 0.5 : 250/window.innerHeight,
|
||||
};
|
||||
const mouse = {
|
||||
x: xFrac(0.5),
|
||||
y: yFrac(0.5),
|
||||
}
|
||||
|
||||
if (is_fine) {
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
const pointerX = e.clientX / window.innerWidth;
|
||||
const pointerY = e.clientY / window.innerHeight;
|
||||
const pointerXfrac = pointerX - 0.5;
|
||||
const pointerYfrac = pointerY - 0.5;
|
||||
|
||||
gsap.to(mouse, {
|
||||
duration: 0.5,
|
||||
ease: 'power3.out',
|
||||
overwrite: true,
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
})
|
||||
|
||||
gsap.to(tween, {
|
||||
duration: .5,
|
||||
ease: 'power3.out',
|
||||
overwrite: true,
|
||||
x: 0.5 + pointerXfrac/2,
|
||||
y: 0.5 + pointerYfrac/2,
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/*----------------------------------
|
||||
* The Ticker
|
||||
* ----------------------------------*/
|
||||
let elapsed = 0.0;
|
||||
|
||||
app.ticker.add((delta) => {
|
||||
elapsed += delta;
|
||||
if (!is_landscape) {
|
||||
// bulgefilter.center = [(tween.x + Math.sin(elapsed/200)/20 ),(tween.y + Math.cos(elapsed/200)/20 )];
|
||||
bulgefilter.center = [(0.5 + Math.sin(elapsed/200)/20 ),(0.25 + Math.cos(elapsed/200)/20 )];
|
||||
// bulgefilter.center = [0.5, 0.25];
|
||||
} else {
|
||||
bulgefilter.center = [tween.x, 0.5];
|
||||
}
|
||||
updateImgs();
|
||||
updateText();
|
||||
})
|
||||
|
||||
window.addEventListener('resize', (e) => {
|
||||
tween.y = is_fine ? 0.5 : 250/window.innerHeight;
|
||||
})
|
||||
return () => {
|
||||
gsap.killTweensOf(imgElems);
|
||||
gsap.killTweensOf(elems);
|
||||
ScrollTrigger.getAll().forEach( instance => instance.kill() );
|
||||
}
|
||||
}) // <- end onMount
|
||||
|
||||
onDestroy(() => {
|
||||
if (browser){
|
||||
app.destroy(true, true);
|
||||
}
|
||||
}) // <- end onDestroy
|
||||
|
||||
</script>
|
||||
|
||||
<canvas bind:this={canvas}></canvas>
|
||||
|
||||
<style>
|
||||
canvas {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: -1;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -39,10 +39,12 @@
|
|||
'--height': summaryHeight, duration: .4, ease: 'power4.out', overwrite: true,
|
||||
onComplete: () => {
|
||||
details.toggleAttribute('open')
|
||||
details.classList.remove('is-open');
|
||||
}
|
||||
})
|
||||
} else {
|
||||
details.toggleAttribute('open');
|
||||
details.classList.add('is-open');
|
||||
contentHeight = (details?.querySelector('.faq-content') as HTMLElement).offsetHeight;
|
||||
let fullHeight = contentHeight + summaryHeight;
|
||||
gsap.to( details, {
|
||||
|
|
@ -61,7 +63,7 @@
|
|||
|
||||
<details bind:this={details}>
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<summary on:click={summaryClickHandler} on:keyup={summaryClickHandler}>{summary}</summary>
|
||||
<summary on:click={summaryClickHandler} on:keyup={summaryClickHandler}>{@html summary}</summary>
|
||||
<div class="faq-content">
|
||||
<slot />
|
||||
</div>
|
||||
|
|
@ -82,11 +84,15 @@
|
|||
display: block;
|
||||
cursor: url('/pointer.svg'), auto;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 1;
|
||||
line-height: 1.2;
|
||||
font-size: 1em;
|
||||
padding: 0.5em 0 0.5em 1.5em;
|
||||
|
||||
&::-webkit-details-marker {
|
||||
display:none;
|
||||
}
|
||||
|
||||
&:before, &:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
|
@ -95,16 +101,23 @@
|
|||
width: 17px;
|
||||
height: 2px;
|
||||
background-color: var(--color-text);
|
||||
transform-origin: center;
|
||||
transition: transform .2s ease-in-out;
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
&:before {
|
||||
transform: rotate(90deg);
|
||||
transition: all .2s ease-in-out;
|
||||
}
|
||||
}
|
||||
details[open] summary {
|
||||
|
||||
&::before {
|
||||
}
|
||||
:global(summary em) {
|
||||
font-weight: 800;
|
||||
color: var(--color-highlight);
|
||||
}
|
||||
// details[open] summary,
|
||||
:global(details.is-open summary) {
|
||||
|
||||
&:before {
|
||||
transition: all .2s ease-in-out;
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@ onMount(()=>{
|
|||
|
||||
let center = [0.5, 0.5];
|
||||
let bulgefilter = new BulgePinchFilter();
|
||||
bulgefilter.radius = xFrac(0.5);
|
||||
bulgefilter.strength = is_fine ? 0.5 : 0.4;
|
||||
bulgefilter.radius = is_landscape ? xFrac(0.5) : xFrac(0.55);
|
||||
bulgefilter.strength = 0.5;
|
||||
bulgefilter.center = center;
|
||||
bulgefilter.resolution = 2;
|
||||
|
||||
|
|
@ -75,14 +75,19 @@ onMount(()=>{
|
|||
|
||||
let introTl = gsap.timeline();
|
||||
introTl.to(bulgefilter, {
|
||||
strength: 0.75,
|
||||
duration: .75,
|
||||
ease: 'power4.out',
|
||||
strength: -0.25,
|
||||
duration: .5,
|
||||
ease: 'power4.inOut',
|
||||
}, 1.5)
|
||||
// introTl.to(bulgefilter, {
|
||||
// strength: 0.75,
|
||||
// duration: .75,
|
||||
// ease: 'power4.inOut',
|
||||
// })
|
||||
introTl.to(bulgefilter, {
|
||||
strength: is_fine ? 0.5 : 0.4,
|
||||
strength: 0.5,
|
||||
duration: 1.25,
|
||||
ease: 'elastic.out(1.5, .2)',
|
||||
ease: 'elastic.out(1.25, .2)',
|
||||
})
|
||||
|
||||
/*----------------------------------
|
||||
|
|
@ -198,10 +203,11 @@ onMount(()=>{
|
|||
|
||||
app.ticker.add((delta) => {
|
||||
elapsed += delta;
|
||||
if (is_coarse) {
|
||||
bulgefilter.center = [(tween.x + Math.sin(elapsed/200)/20 ),(tween.y + Math.cos(elapsed/200)/20 )];
|
||||
if (is_portrait) {
|
||||
bulgefilter.center = [(0.5 + Math.sin(elapsed/200)/20 ),(0.45 + Math.cos(elapsed/200)/20 )];
|
||||
// bulgefilter.center = [0.5, 0.45];
|
||||
} else {
|
||||
bulgefilter.center = [tween.x, tween.y];
|
||||
bulgefilter.center = [tween.x, 0.5];
|
||||
}
|
||||
updateImgs();
|
||||
updateText();
|
||||
|
|
|
|||
|
|
@ -36,16 +36,14 @@
|
|||
const randomShape = shapes[Math.floor(Math.random() * shapes.length)];
|
||||
|
||||
let rowindex = (index: number) => {
|
||||
// based on the index of an element within an array of 5x3 grid, return the columns index, so for example 0, 5 and 10 would return 0, index 1, 6 and 11 would return 1, etc
|
||||
return index % 5;
|
||||
};
|
||||
|
||||
gsap.to(shape, {
|
||||
duration: 1,
|
||||
rotationZ: "+=" + generateAngle(),
|
||||
morphSVG: randomShape,
|
||||
ease: 'power4.out',
|
||||
delay: rowindex(index) * 0.1,
|
||||
delay: rowindex(index) * 0.05,
|
||||
onComplete: () => { setTimeout( () => randomMorph(shape), 2000 ) }
|
||||
});
|
||||
}
|
||||
|
|
@ -79,10 +77,10 @@
|
|||
.home-illu-shapes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
gap: clamp(1px, 0.3vw, 5px);
|
||||
}
|
||||
:global(.home-illu-shapes > *) {
|
||||
flex-basis: calc(20% - 5px);
|
||||
flex-basis: calc(20% - clamp(1px, 0.3vw, 5px));
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -80,8 +80,9 @@ a {
|
|||
.button {
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
font-family: stratos, sans-serif;
|
||||
display: inline-block;
|
||||
margin: 1em 0.125em;
|
||||
margin: 1em 0;
|
||||
padding: .75em;
|
||||
text-decoration: none;
|
||||
box-sizing: border-box;
|
||||
|
|
@ -91,9 +92,6 @@ a {
|
|||
z-index: 2;
|
||||
transition: all .3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
|
||||
&:hover {
|
||||
font-size: clamp(21px, 1.7vw, 1.7vw);
|
||||
}
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
|
|
@ -109,9 +107,9 @@ a {
|
|||
transition: all .3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
&:hover:before {
|
||||
width: 100%;
|
||||
width: calc(100% + 0.25em);
|
||||
height: calc(100% + 10px);
|
||||
left: 0;
|
||||
left: -0.125em;
|
||||
bottom: -5px;
|
||||
transition: all .3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@
|
|||
})
|
||||
canvasElems = Array.from(document.querySelectorAll('.lineChildren, .wordChildren'));
|
||||
imgElems = Array.from(document.querySelectorAll('img'));
|
||||
|
||||
return () => {
|
||||
gsap.killTweensOf(canvasElems);
|
||||
ScrollTrigger.getAll().forEach( instance => instance.kill() );
|
||||
|
|
@ -75,14 +76,14 @@
|
|||
{/if}
|
||||
<article class="scroller">
|
||||
<section class="splash">
|
||||
<h1>Hallo! I'm Simon.<br> I forge websites<br/><em>that stand out</em> with great beauty.</h1>
|
||||
<h1 class="align-middle">Hallo! I'm Simon. I forge websites that stand out with exemplary beauty.</h1>
|
||||
</section>
|
||||
<section class="intro">
|
||||
<figure class="intro-image">
|
||||
<HomeIlluDev />
|
||||
</figure>
|
||||
<h2><em>Creative</em> Development</h2>
|
||||
<p class="toCanvas">I specialise in fashioning exquisitly tailored websites and webapps for the discerning enterprise executive.</p>
|
||||
<h2>Creative Development</h2>
|
||||
<p class="toCanvas">I fashion exquisitly tailored web experiences for discerning enterprises and their audiences.</p>
|
||||
<div class="cta">
|
||||
<a href="/service" class="button">My Services</a>
|
||||
<a href="/work" class="button button--primary">Hire Simon</a>
|
||||
|
|
@ -93,15 +94,15 @@
|
|||
<HomeIlluShapes />
|
||||
</figure>
|
||||
<h2>Visual Design</h2>
|
||||
<p class="toCanvas">With years in the craft, I've mastered the art of designing websites, user interfaces, illustrations, house styles, and everything betwixt.</p>
|
||||
<p class="toCanvas">I'm also a seasoned designer, sculpting communication that's wickedly nice, full of jaw-dropping surprises, and utterly delightful.</p>
|
||||
<div class="cta">
|
||||
<a href="/service" class="button">My Services</a> <a href="/work" class="button button--primary">Hire Simon</a>
|
||||
</div>
|
||||
</section>
|
||||
<section class="more">
|
||||
<h2>I create products that help great companies reach their audiences.</h2>
|
||||
<h2 class="align-middle">I work as a free agent. Both companies and noble causes can enlist my services for a reasonable wage.</h2>
|
||||
<div class="cta">
|
||||
<a href="/work" class="button button--xl">Reach out!</a>
|
||||
<a href="/work" class="button button--xl">Enlist my services</a>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
|
|
@ -165,12 +166,29 @@
|
|||
.design-illu {
|
||||
position:relative;
|
||||
left: 0;
|
||||
width: 40%;
|
||||
width: 60%;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
display: block;
|
||||
margin-bottom: -1em;
|
||||
z-index: -2;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
.more {
|
||||
@media screen and (min-width: 768px) {
|
||||
max-width: 95vw;
|
||||
}
|
||||
}
|
||||
.more h2 {
|
||||
margin-top: -2em;
|
||||
margin-bottom: 1em;
|
||||
@media screen and (min-width: 768px) {
|
||||
margin-bottom: 0.5em;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
h1, h2 {
|
||||
line-height: 1.1;
|
||||
|
|
@ -183,19 +201,14 @@
|
|||
user-select: none;
|
||||
}
|
||||
h1 {
|
||||
line-height: 1;
|
||||
font-size: 11vw;
|
||||
line-height: .9;
|
||||
font-size: 13.25vw;
|
||||
margin-top: -.5em;
|
||||
letter-spacing: -0.05em;
|
||||
letter-spacing: -0.02em;
|
||||
opacity: 0;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
font-size: 9vw;
|
||||
line-height: .9;
|
||||
}
|
||||
& em {
|
||||
color: var(--color-highlight);
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
|
|
@ -203,11 +216,6 @@
|
|||
letter-spacing: -0.033em;
|
||||
margin-bottom: 0;
|
||||
line-height: .9;
|
||||
|
||||
& em {
|
||||
color: var(--color-highlight);
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
.toCanvas {
|
||||
visibility: hidden;
|
||||
|
|
|
|||
214
src/routes/contact/+page.svelte
Normal file
214
src/routes/contact/+page.svelte
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import ContactCanvas from '$lib/components/ContactCanvas.svelte';
|
||||
import gsap from 'gsap';
|
||||
import SplitText from 'gsap/dist/SplitText';
|
||||
import ScrollTrigger from 'gsap/dist/ScrollTrigger';
|
||||
|
||||
let canvasTexts: Array<HTMLElement> = [];
|
||||
|
||||
onMount( () => {
|
||||
|
||||
gsap.registerPlugin( SplitText );
|
||||
|
||||
|
||||
let split = new SplitText('.toCanvas', { type: 'lines', linesClass: 'lineChildren' });
|
||||
|
||||
let alignmiddles: Array<HTMLElement> = Array.from(document.querySelectorAll('.toCanvas'));
|
||||
alignmiddles.forEach( (toCanvas) => {
|
||||
let spanlengths: Array<number> = [];
|
||||
toCanvas.querySelectorAll('.lineChildren').forEach( (line) => {
|
||||
line.innerHTML = '<span>' + line.innerHTML + '</span>'
|
||||
})
|
||||
toCanvas.querySelectorAll('.lineChildren span').forEach( (span) => {
|
||||
spanlengths.push(span.getBoundingClientRect().width);
|
||||
})
|
||||
// get the longest spanlength
|
||||
let longestSpan = Math.max(...spanlengths);
|
||||
toCanvas.style.position = 'relative';
|
||||
toCanvas.style.left = (toCanvas.offsetWidth - longestSpan) / 2 + 'px';
|
||||
})
|
||||
|
||||
canvasTexts = Array.from(document.querySelectorAll('.lineChildren'));
|
||||
|
||||
const introIn = document.querySelector('.contact') as HTMLElement;
|
||||
gsap.fromTo(introIn.querySelectorAll('.lineChildren'), {
|
||||
opacity: 0, yPercent: -100
|
||||
}, {
|
||||
opacity: 1, yPercent: 0, duration: 1.5, stagger: 0.05, ease: 'power4.inOut',
|
||||
})
|
||||
gsap.fromTo(introIn?.querySelectorAll('p, input, .button'), {
|
||||
opacity: 0, yPercent: 100
|
||||
}, {
|
||||
opacity: 1, yPercent: 0, duration: 1, stagger: 0.05, ease: 'power4.inOut',
|
||||
})
|
||||
|
||||
const scrollIns = document.querySelectorAll('.scrollIn');
|
||||
scrollIns.forEach( scrollIn => {
|
||||
gsap.set(scrollIn.querySelectorAll('p, input, textarea, .button'), { opacity: 0, y: 100 });
|
||||
gsap.to(scrollIn.querySelectorAll('p, input, textarea, .button'), { opacity: 1, y: 0, duration: 1, stagger: 0.05, ease: 'power4.inOut',
|
||||
scrollTrigger: { trigger: scrollIn, start: 'top 66%', end: 'bottom 50%', scrub: false }
|
||||
})
|
||||
gsap.fromTo(scrollIn.querySelectorAll('.lineChildren'), {
|
||||
opacity: 0, yPercent: 100
|
||||
},{
|
||||
opacity: 1, yPercent: 0, duration: 1, stagger: 0.05, ease: 'power4.inOut',
|
||||
scrollTrigger: { trigger: scrollIn, start: 'top 66%', end: 'bottom 50%', scrub: false }
|
||||
})
|
||||
})
|
||||
|
||||
return () => {
|
||||
gsap.killTweensOf('.toCanvas, .lineChildren');
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<div class="formwrapper">
|
||||
<form
|
||||
name="contact"
|
||||
action="/success"
|
||||
method="POST"
|
||||
data-netlify="true"
|
||||
>
|
||||
<section class="contact">
|
||||
<h1 class="toCanvas">Don't be a stranger. Let's get acquainted.</h1>
|
||||
<div class="alternatives">
|
||||
<p>You can fill in the form below or:</p>
|
||||
<ul>
|
||||
<li><a class="button" href="mailto:simon@floter.design">Send an Email</a></li>
|
||||
<li><a class="button" href="https://www.linkedin.com/in/floter/">Connect on LinkedIn</a></li>
|
||||
</div>
|
||||
<label for="name">
|
||||
<p>How would you like to be addressed?</p>
|
||||
<input type="text" name="name" id="name" placeholder="Your name">
|
||||
</label>
|
||||
<label for="email">
|
||||
<p>For receiving a reply, add your Email address:</p>
|
||||
<input type="email" name="email" id="email" placeholder="Your Email?*" required>
|
||||
</label>
|
||||
</section>
|
||||
<section class="message scrollIn">
|
||||
<h3 class="toCanvas contactheadline">How can I assist in your noble cause?</h3>
|
||||
<label for="contact">
|
||||
<p>Please describe your plight in a few words</p>
|
||||
<textarea rows="6" name="contact" id="contact" />
|
||||
</label>
|
||||
<button class="button" type="submit">Send it!</button>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
<ContactCanvas textsToCanvas={canvasTexts} />
|
||||
|
||||
<style lang="scss">
|
||||
h1, h3 {
|
||||
visibility: hidden;
|
||||
font-size: 2.5em;
|
||||
line-height: .9;
|
||||
letter-spacing: -0.04em;
|
||||
margin: 1em var(--spacing-nav) 1em var(--spacing-nav);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
margin: 1em var(--spacing-outer) 0.33em var(--spacing-outer);
|
||||
font-size: 4.5em;
|
||||
}
|
||||
}
|
||||
h3 {
|
||||
font-size: 2em;
|
||||
@media screen and (min-width: 768px) {
|
||||
font-size: 3em;
|
||||
}
|
||||
}
|
||||
.contact p, .contact input {
|
||||
opacity: 0;
|
||||
}
|
||||
.formwrapper {
|
||||
margin: 0 auto;
|
||||
padding: 1em 1em calc(4 * var(--spacing-outer)) 1em;
|
||||
}
|
||||
form {
|
||||
box-sizing: border-box;
|
||||
max-width: inherit;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
.alternatives {
|
||||
margin: 4em auto 1em auto;
|
||||
width: 100%;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
margin: 0 auto 2em auto;
|
||||
padding-bottom: 1em;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
& p {
|
||||
margin: 0;
|
||||
}
|
||||
& ul {
|
||||
list-style-type: none;
|
||||
display: flex;
|
||||
gap: .5em;
|
||||
margin: 0;
|
||||
}
|
||||
& .button {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
section {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-height: 70vh;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// justify-content: center;
|
||||
max-width: 75vw;
|
||||
margin: 0 auto;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
label {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
input[type='text'], input[type='email'], textarea {
|
||||
width: 100%;
|
||||
border: 0px solid var(--color-text);
|
||||
background-color: rgba(255, 205, 205, 0.2);
|
||||
color: var(--color-text);
|
||||
border-radius: 4px;
|
||||
font-family: 'Stratos', sans-serif;
|
||||
font-size: 1em;
|
||||
padding: .75em;
|
||||
transition: all 0.3s ease-out;
|
||||
margin: 0 auto 1em auto;
|
||||
display: block;
|
||||
max-width: 640px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
color: var(--color-bg);
|
||||
background-color: var(--color-text);
|
||||
}
|
||||
&::placeholder {
|
||||
color: var(--color-text);
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
label p {
|
||||
max-width: 640px;
|
||||
margin: 0 auto 1em auto;
|
||||
display: block;
|
||||
font-size: .75em;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
font-size: 1.25em;
|
||||
max-width: 640px;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -2,24 +2,37 @@
|
|||
import { onMount } from 'svelte';
|
||||
import gsap from 'gsap';
|
||||
import ScrollTrigger from 'gsap/dist/ScrollTrigger';
|
||||
import SplitText from 'gsap/dist/SplitText';
|
||||
import Faq from '$lib/components/Faq.svelte';
|
||||
import ServiceCanvas from './ServiceCanvas.svelte';
|
||||
|
||||
onMount( () => {
|
||||
gsap.registerPlugin( ScrollTrigger );
|
||||
gsap.registerPlugin( ScrollTrigger, SplitText );
|
||||
|
||||
let split = new SplitText('h1', { type: 'words', wordsClass: 'wordChildren' });
|
||||
|
||||
let introTl = gsap.timeline({
|
||||
paused: true,
|
||||
});
|
||||
introTl.fromTo('h1', {
|
||||
opacity: 0, yPercent: 200
|
||||
introTl.fromTo(split.words, {
|
||||
opacity: 0, scaleY: 0, yPercent: 100
|
||||
},{
|
||||
opacity: 1, yPercent: 0,
|
||||
duration: .75, ease: 'back.out(.75)'
|
||||
}, 0.1)
|
||||
opacity: 1, scaleY: 1, yPercent: 0,
|
||||
transformOrigin: '50% 100%',
|
||||
duration: 1, ease: 'elastic.out(.75, .33)',
|
||||
stagger: 0.01,
|
||||
}, 1)
|
||||
|
||||
introTl.play();
|
||||
|
||||
// let trigger = ScrollTrigger.create({
|
||||
// trigger: 'article',
|
||||
// start: 'top top',
|
||||
// end: '200px top',
|
||||
// scrub: true,
|
||||
// // markers: true,
|
||||
// })
|
||||
|
||||
gsap.to('.services', {
|
||||
autoAlpha: 1,
|
||||
scrollTrigger: {
|
||||
|
|
@ -27,7 +40,16 @@
|
|||
start: 'top top',
|
||||
end: '200px top',
|
||||
scrub: true,
|
||||
// markers: true,
|
||||
}
|
||||
})
|
||||
gsap.to('h1 em', {
|
||||
autoAlpha: 0,
|
||||
yPercent: -100,
|
||||
scrollTrigger: {
|
||||
trigger: 'article',
|
||||
start: 'top top',
|
||||
end: '200px top',
|
||||
scrub: true,
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -45,7 +67,7 @@
|
|||
|
||||
<ServiceCanvas />
|
||||
<article>
|
||||
<h1>What I will (and won't) do to earn a living</h1>
|
||||
<h1>What I will do to earn a living<br><em>↓ Scroll down</em></h1>
|
||||
<div class="services">
|
||||
<section>
|
||||
<h2>Web Development</h2>
|
||||
|
|
@ -54,11 +76,13 @@
|
|||
<li><em>Consultation</em> on the best technology solutions for your situation (eg CMS, frontend frameworks, etc)</li>
|
||||
<li><em>Advice</em> on how to enhance the user experience through savvy use of technology</li>
|
||||
<li><em>Development</em> in HTML/JS/CSS and/or whichever framework is best suited to your project</li>
|
||||
<a class="button" href="/contact">Get a quote</a>
|
||||
</ul>
|
||||
<div class="tech">
|
||||
<div class="faqs">
|
||||
<h3>FAQs</h3>
|
||||
<Faq summary="What kind of websites do you create?">
|
||||
<p>In frontend work, I blend professionalism and creativity. My aim is simple: bring your ideas to life. Projects include:</p>
|
||||
<p>I blend professionalism and creativity. My aim is simple: bring your ideas to life. My websites are often fun and bold and full of interesting interaction.</p>
|
||||
<p>Projects include:</p>
|
||||
<ul>
|
||||
<li>Marketing websites</li>
|
||||
<li>E-commerce websites</li>
|
||||
|
|
@ -70,13 +94,13 @@
|
|||
<p>I center on crafting interfaces that dazzle visually and deliver an instinctive user journey. The essence is to make your users feel at home in the interaction with your content.</p>
|
||||
<p>Should your venture demand a backend touch, like managing databases or Content Management, I'm well-versed in leveraging existing open source solutions. I advocate for savvy fixes, seamlessly tailored to harmonize with your project.</p>
|
||||
</Faq>
|
||||
<Faq summary="What kind of websites do you NOT create?">
|
||||
<Faq summary="What kind of websites do you <em>not</em> create?">
|
||||
<p>As a frontend developer, I'm not your guy for the intricate machinery that powers colossal enterprise web applications. Put simply, I can't whip up an Amazon or a TikTok from the ground up.</p>
|
||||
<p>Moreover, my moral code is steadfast. Requests involving, but not confined to, promoting weapons manufacturers or propagating conspiracy theories won't find a home with me. I must politely decline such propositions.</p>
|
||||
</Faq>
|
||||
<Faq summary="Which technologies and frameworks do you use/support?">
|
||||
<p>A web browser grasps just HTML, CSS, and Javascript. Mastery of these is the bedrock for crafting top-notch web applications.</p>
|
||||
<p>Yet, I've treaded diverse coding landscapes, navigating the realms of PHP, Perl, and Python. Among the myriad frameworks danced with, names like React, Svelte, Eleventy, Astro, and the esteemed Ruby on Rails echo.</p>
|
||||
<p>A web browser understands only HTML, CSS, and Javascript. Mastery of these is the bedrock for crafting top-notch web applications.</p>
|
||||
<p>Further, I've treaded diverse coding landscapes, navigating the realms of PHP, Perl, and Python. Among the myriad frameworks danced with, names like React, Svelte, Eleventy, Astro, and the esteemed Ruby on Rails echo.</p>
|
||||
<p>I serve as your counsel, steering you toward the optimal solution tailored to your project's idiosyncrasies.</p>
|
||||
<p>Should you possess a standing website, yearning for expansion, toss me the code, and we'll unravel the possibilities together. Not encountered a site I couldn't tame, so far.</p>
|
||||
<p>And, sometimes, the most fitting scaffold is none at all. I crafted some elegant, straightforward websites in the raw embrace of HTML, CSS, and JS.</p>
|
||||
|
|
@ -86,9 +110,9 @@
|
|||
<p>I've tinkered with various Content Management Systems — the likes of WordPress, Strapi, Builder, Magento, and Shopify. Each carries its own set of virtues and vices.</p>
|
||||
<p>Let's talk about the folks responsible for constructing and upkeeping your content. Only then can we jointly discern the optimal solution.</p>
|
||||
</Faq>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>UX/Visual Design</h2>
|
||||
|
|
@ -99,7 +123,7 @@
|
|||
<li><em>Illustration & animation</em> to bring your ideas to life</li>
|
||||
<li><em>Data visualisation</em> to aid crucial information cross the seas of abstraction</li>
|
||||
</ul>
|
||||
<div class="tech">
|
||||
<div class="faqs">
|
||||
<h3>FAQs</h3>
|
||||
<Faq summary="What do you design?">
|
||||
<p>Primarily, I craft beautiful web experiences for my clients, aiming to seize their audience's imagination.</p>
|
||||
|
|
@ -118,6 +142,14 @@
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- <section>
|
||||
<h2>Full service</h2>
|
||||
<div class="service-content">
|
||||
<ul class="bullets">
|
||||
<li><em>All of the above</em></li>
|
||||
</ul>
|
||||
</div>
|
||||
</section> -->
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
|
@ -125,22 +157,36 @@
|
|||
article {
|
||||
margin: auto;
|
||||
max-width: 1200px;
|
||||
padding: calc(50vh - var(--spacing-outer) - 35vw) var(--spacing-outer) var(--spacing-outer) var(--spacing-outer);
|
||||
padding: calc(50vh - var(--spacing-outer) - 10vw) var(--spacing-outer) var(--spacing-outer) var(--spacing-outer);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
padding: calc(50vh - var(--spacing-outer) - 12vw) var(--spacing-outer) var(--spacing-outer) var(--spacing-outer);
|
||||
padding: calc(50vh - var(--spacing-outer) - 6vw) var(--spacing-outer) var(--spacing-outer) var(--spacing-outer);
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
font-size: 10vw;
|
||||
line-height: 1;
|
||||
|
||||
& em {
|
||||
position: absolute;
|
||||
top: 120%;
|
||||
font-size: .4em;
|
||||
letter-spacing: -0.02em;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
font-size: 14vw;
|
||||
@media screen and (min-width: 768px) {
|
||||
font-size: 8vw;
|
||||
font-size: 6vw;
|
||||
letter-spacing: -0.045em;
|
||||
}
|
||||
}
|
||||
.services {
|
||||
opacity: 0; //changed in ServiceCanvas
|
||||
padding-bottom: calc( 2 * var(--spacing-outer));
|
||||
}
|
||||
.service-content {
|
||||
|
||||
|
|
@ -154,12 +200,34 @@
|
|||
}
|
||||
.service-content > *,
|
||||
.service-content > * > :first-child {
|
||||
@media screen and (min-width: 768px) {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
|
||||
}
|
||||
}
|
||||
.faqs {
|
||||
margin-top: 2em;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
border-bottom: 1px solid;
|
||||
padding-bottom: .5em;
|
||||
font-size: 1.5em;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.25em;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
}
|
||||
em {
|
||||
font-weight: 800;
|
||||
|
|
@ -170,7 +238,7 @@
|
|||
line-height: 1.25;
|
||||
}
|
||||
li, p {
|
||||
font-size: 1.15em;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -110,8 +110,8 @@
|
|||
let textgroupIn = gsap.to(textgroup, {
|
||||
y: is_landscape ? textRows[0][0].height * 0.4 : textRows[0][0].height * 0.15,
|
||||
alpha: 1,
|
||||
duration: 1.5,
|
||||
ease: 'power4.out',
|
||||
duration: 1,
|
||||
ease: 'power2.inOut',
|
||||
overwrite: true,
|
||||
onComplete: () => {
|
||||
introDone = true;
|
||||
|
|
@ -157,7 +157,7 @@
|
|||
scrollTrigger: {
|
||||
trigger: 'article',
|
||||
start: 'top top',
|
||||
end: '200px top',
|
||||
end: 'top -50%',
|
||||
scrub: true,
|
||||
// markers: true,
|
||||
}
|
||||
|
|
@ -183,7 +183,7 @@
|
|||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
/* z-index: 2; */
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
3
src/routes/success/+page.svelte
Normal file
3
src/routes/success/+page.svelte
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<h1>
|
||||
Yee-ha!
|
||||
</h1>
|
||||
Loading…
Add table
Reference in a new issue