Merge branch 'v2'

This commit is contained in:
2022-09-26 21:15:59 +02:00
313 changed files with 17561 additions and 19171 deletions

View File

@@ -1,3 +0,0 @@
.git
*Dockerfile*
.env

View File

@@ -1,149 +0,0 @@
# DEPLOY
kind: pipeline
name: deploy
trigger:
status:
- success
event:
- push
steps:
#
# Restore cache (node_modules, build, etc)
#
- name: cache-restore
image: meltwater/drone-cache:dev
pull: true
volumes:
- name: cache
path: /tmp/cache
settings:
backend: "filesystem"
restore: true
cache_key: "volume"
archive_format: "gzip"
mount:
- "node_modules"
#
# Build Node.js app
#
- name: build-node
image: node:latest
commands:
- yarn
- yarn build
#
# Build the Docker image
#
# DEV
- name: build-docker-dev
image: docker:latest
volumes:
- name: docker_sock
path: /var/run/docker.sock
commands:
- docker build --pull --no-cache -t cetrucflotte/housesof:dev .
when:
branch:
- dev
# PROD
- name: build-docker-prod
image: docker:latest
volumes:
- name: docker_sock
path: /var/run/docker.sock
commands:
- docker build --pull --no-cache -t cetrucflotte/housesof:latest .
when:
branch:
- master
#
# Restart container
#
# DEV
- name: restart-container-dev
image: appleboy/drone-ssh
settings:
host: flayks.com
username:
from_secret: ssh_user
key:
from_secret: ssh_key
script:
- cd /data/sites/housesof.world/test
- docker-compose down
- docker-compose up -d --remove-orphans
when:
branch:
- dev
# PROD
- name: restart-container-prod
image: appleboy/drone-ssh
settings:
host: flayks.com
username:
from_secret: ssh_user
key:
from_secret: ssh_key
script:
- cd /data/sites/housesof.world/www
- docker-compose down
- docker-compose up -d --remove-orphans
when:
branch:
- master
#
# Rebuild cache
#
- name: cache-rebuild
image: meltwater/drone-cache:dev
pull: true
volumes:
- name: cache
path: /tmp/cache
settings:
backend: "filesystem"
rebuild: true
cache_key: "volume"
archive_format: "gzip"
mount:
- "node_modules"
#
# Notify (by Email)
#
- name: notify-email
image: drillster/drone-email
settings:
host:
from_secret: notify_email_server
from:
from_secret: notify_email_address
username:
from_secret: notify_email_address
password:
from_secret: notify_email_password
recipients:
- hello@flayks.com
recipients_only: true
# Mount cache volume
volumes:
- name: cache
host:
path: /var/lib/cache
- name: docker_sock
host:
path: /var/run/docker.sock

11
.env
View File

@@ -1,11 +0,0 @@
# Website
PROD_URL="https://housesof.world"
HOME_PHOTOS_LIMIT=6
# API
API_TOKEN="NJk0urljsdSvApUDzWxGgoO6"
API_URL_DEV="http://api.housesof.localhost/how"
API_URL_PROD="https://api.housesof.world/how"
# Tracking
GA_TRACKER_ID="UA-4060922-27"

20
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,20 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
plugins: ['svelte3', '@typescript-eslint'],
ignorePatterns: ['*.cjs'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: {
'svelte3/typescript': () => require('typescript')
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 2019
},
env: {
browser: true,
es2017: true,
node: true
}
}

View File

@@ -1,22 +0,0 @@
module.exports = {
extends: [
// 'eslint:recommended'
],
env: {
'browser': true,
'es6': true
},
plugins: ['svelte3'],
parserOptions: {
'ecmaVersion': 2019,
'sourceType': 'module'
},
overrides: [{
files: ['**/*.svelte'],
processor: 'svelte3/svelte3'
}],
rules: {
indent: [ 'error', 4 ],
'no-multiple-empty-lines': [ 1, { 'max': 5 } ]
}
}

7
.gitignore vendored
View File

@@ -1,4 +1,7 @@
.DS_Store .DS_Store
node_modules node_modules
__sapper__ /build
/cypress/screenshots /.svelte-kit
.env
.env.*
!.env.example

View File

@@ -1,14 +0,0 @@
FROM node:current-alpine
WORKDIR /var/www
# Copy build and modules
COPY . .
# Open port
EXPOSE 3000
# Serve
CMD node dist
VOLUME ["/var/www"]

View File

@@ -1,11 +0,0 @@
module.exports = {
presets: [
'@babel/preset-env'
],
plugins: [
'@babel/plugin-syntax-dynamic-import',
['@babel/plugin-transform-runtime', {
'useESModules': true
}]
]
}

View File

@@ -1,61 +1,58 @@
{ {
"name": "housesof", "name": "housesof",
"version": "1.2.0", "version": "2.0.0",
"description": "Houses Of website",
"author": {
"name": "Flayks",
"email": "hello@flayks.com",
"url": "http://flayks.com"
},
"private": true, "private": true,
"author": "Félix Péault <hello@flayks.com> (https://flayks.com)",
"contributors": [
"Shelby Kay <bonjour@shelbykay.dev> (https://shelbykay.dev)"
],
"scripts": { "scripts": {
"dev": "sapper dev --port 3000", "dev": "vite dev",
"build": "sapper build dist --legacy", "build": "vite build",
"export": "sapper export export --legacy", "preview": "vite preview",
"start": "node dist" "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "eslint --ignore-path .gitignore ."
}, },
"dependencies": { "dependencies": {
"animejs": "^3.2.1", "@studio-freight/lenis": "^0.2.6",
"compression": "^1.7.4", "dayjs": "^1.11.5",
"imagesloaded": "^4.1.4", "embla-carousel": "^7.0.3",
"lazysizes": "^5.3.0", "focus-visible": "^5.2.0",
"node-fetch": "^2.6.1", "motion": "^10.14.2",
"normalize.css": "^8.0.1", "ogl": "^0.0.99",
"polka": "^1.0.0-next.11", "sanitize.css": "^13.0.0",
"rellax": "^1.12.1", "swell-js": "^3.17.6",
"scroll-out": "^2.2.12", "tweakpane": "^3.1.0"
"sirv": "^1.0.10",
"svelte-lazy": "^1.0.8",
"swipe-listener": "^1.3.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.12.10", "@sveltejs/adapter-auto": "^1.0.0-next.80",
"@babel/plugin-syntax-dynamic-import": "^7.8.3", "@sveltejs/adapter-node": "^1.0.0-next.96",
"@babel/plugin-transform-runtime": "^7.12.10", "@sveltejs/adapter-vercel": "^1.0.0-next.77",
"@babel/preset-env": "^7.12.11", "@sveltejs/kit": "^1.0.0-next.504",
"@babel/runtime": "^7.12.5", "@typescript-eslint/eslint-plugin": "^5.38.1",
"@rollup/plugin-alias": "^3.1.1", "@typescript-eslint/parser": "^5.38.1",
"@rollup/plugin-babel": "^5.2.2", "base-64": "^1.0.0",
"@rollup/plugin-commonjs": "17.0.0", "browserslist": "^4.21.4",
"@rollup/plugin-node-resolve": "^11.1.0", "cssnano": "^5.1.13",
"@rollup/plugin-replace": "^2.3.4", "eslint": "^8.24.0",
"autoprefixer": "^10.2.1", "eslint-plugin-svelte3": "^4.0.0",
"dotenv": "^8.2.0", "postcss": "^8.4.16",
"node-sass": "^5.0.0", "postcss-focus-visible": "^7.1.0",
"postcss": "^8.2.4", "postcss-normalize": "^10.0.1",
"postcss-load-config": "^3.0.0", "postcss-preset-env": "^7.8.2",
"postcss-preset-env": "^6.7.0", "postcss-sort-media-queries": "^4.3.0",
"rollup": "^2.36.2", "sass": "^1.55.0",
"rollup-plugin-glslify": "^1.2.0", "svelte": "^3.50.1",
"rollup-plugin-svelte": "^7.0.0", "svelte-check": "^2.9.1",
"rollup-plugin-terser": "^7.0.2", "svelte-preprocess": "^4.10.7",
"sapper": "^0.28.10", "tslib": "^2.4.0",
"svelte": "^3.31.2", "typescript": "^4.8.3",
"svelte-preprocess": "^4.6.1" "vite": "^3.1.3"
}, },
"type": "module",
"browserslist": [ "browserslist": [
"> 0.5%", "last 3 versions",
"last 2 versions", "> 0.3%"
"not dead"
] ]
} }

6634
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

37
postcss.config.cjs Normal file
View File

@@ -0,0 +1,37 @@
const cssnano = require('cssnano')
const presetEnv = require('postcss-preset-env')
const focusVisible = require('postcss-focus-visible')
// const sortMediaQueries = require('postcss-sort-media-queries')
const normalize = require('postcss-normalize')
const dev = process.env.NODE_ENV !== 'development'
module.exports = {
plugins: [
// Preset Env
presetEnv({
stage: 2,
}),
// Focus visible
focusVisible({}),
// Sort media queries
// sortMediaQueries({
// sort: 'mobile-first'
// }),
// Normalize
normalize({}),
// CSS Nano
!dev && cssnano({
preset: ['default', {
autoprefixer: true,
discardComments: { removeAll: true },
calc: { precision: 2 },
safe: true
}]
}),
]
}

View File

@@ -1,30 +0,0 @@
const pkg = require('./package.json')
const dev = process.env.NODE_ENV === 'development'
module.exports = () => ({
plugins: [
// Preset Env
require('postcss-preset-env')({}),
// Autoprefixer
require('autoprefixer')({}),
// Extract media queries
// require('postcss-combine-media-query')({}),
// CSS Nano
!dev && require('cssnano')({
preset: ['default', {
autoprefixer: true,
discardComments: { removeAll: true },
calc: { precision: 2 },
safe: true
}]
}),
// Banner
!dev && require('postcss-banner')({
banner: `${pkg.name} v${pkg.version} \nBy ${pkg.author.name} \n${pkg.author.url}`
})
]
})

View File

@@ -1,152 +0,0 @@
import path from 'path'
import resolve from '@rollup/plugin-node-resolve'
import alias from '@rollup/plugin-alias'
import replace from '@rollup/plugin-replace'
import commonjs from '@rollup/plugin-commonjs'
import babel from '@rollup/plugin-babel'
import svelte from 'rollup-plugin-svelte'
import autoPreprocess from 'svelte-preprocess'
import { terser } from 'rollup-plugin-terser'
import glslify from 'rollup-plugin-glslify'
import config from 'sapper/config/rollup'
import { config as dotenv } from 'dotenv'
import pkg from './package.json'
// Define environment and things
const mode = process.env.NODE_ENV
const dev = mode === 'development'
const legacy = !!process.env.SAPPER_LEGACY_BUILD
const replaceOptions = {
'process.env.NODE_ENV': JSON.stringify(mode),
'process.env.CONFIG': JSON.stringify(dotenv().parsed)
}
// Svelte
const onwarn = (warning, onwarn) => (warning.code === 'MISSING_EXPORT' && /'preload'/.test(warning.message)) || (warning.code === 'CIRCULAR_DEPENDENCY' && /[/\\]@sapper[/\\]/.test(warning.message)) || onwarn(warning)
// Preprocessors
const preprocess = autoPreprocess({
scss: {
includePaths: ['src', 'node_modules'],
renderSync: true
},
postcss: true
})
// Resolve and Alias
const resolveExtensions = ['.mjs', '.js', '.svelte', '.scss', '.json', '.html']
const aliases = alias({
resolve: resolveExtensions,
entries: [
{ find: 'utils', replacement: path.resolve(__dirname, 'src/utils') },
{ find: 'animations', replacement: path.resolve(__dirname, 'src/animations') },
{ find: 'atoms', replacement: path.resolve(__dirname, 'src/atoms') },
{ find: 'molecules', replacement: path.resolve(__dirname, 'src/molecules') },
{ find: 'organisms', replacement: path.resolve(__dirname, 'src/organisms') },
{ find: 'globe', replacement: path.resolve(__dirname, 'src/globe') },
]
})
export default {
/*
** Client
*/
client: {
input: config.client.input(),
output: config.client.output(),
plugins: [
// Javascript
replace({
'process.browser': true,
...replaceOptions
}),
svelte({
preprocess,
emitCss: true,
compilerOptions: {
dev,
hydratable: true,
}
}),
aliases,
glslify(),
resolve({
browser: true,
extensions: resolveExtensions,
dedupe: ['svelte']
}),
commonjs(),
legacy && babel({
extensions: resolveExtensions,
babelHelpers: 'runtime',
exclude: ['node_modules/@babel/**']
}),
// Compress Javascript
!dev && terser({
module: true
}),
],
preserveEntrySignatures: false,
onwarn,
},
/*
** Server
*/
server: {
input: config.server.input(),
output: config.server.output(),
plugins: [
replace({
'process.browser': false,
...replaceOptions
}),
svelte({
preprocess,
emitCss: false,
compilerOptions: {
generate: 'ssr',
hydratable: true,
dev,
}
}),
aliases,
glslify(),
resolve({
extensions: resolveExtensions,
dedupe: ['svelte']
}),
commonjs()
],
external: Object.keys(pkg.dependencies).concat(require('module').builtinModules),
preserveEntrySignatures: 'strict',
onwarn,
},
/*
** Service worker
*/
// ...(!dev && {
// serviceworker: {
// input: config.serviceworker.input(),
// output: config.serviceworker.output(),
// plugins: [
// resolve(),
// replace({
// 'process.browser': false,
// ...replaceOptions
// }),
// aliases,
// glslify(),
// commonjs(),
// !dev && terser()
// ],
// preserveEntrySignatures: false,
// onwarn,
// }
// })
}

View File

@@ -1,76 +0,0 @@
import anime from 'animejs'
import ScrollOut from 'scroll-out'
import { animDuration, animDelay } from 'utils/store'
/*
** Transition In
*/
export const animateIn = scope => {
const tl = anime.timeline({
easing: 'easeOutQuart',
duration: animDuration,
autoplay: false
})
// Carousel
tl.add({
targets: scope,
translateY: [window.innerWidth <= 768 ? 16 : 32, 0],
translateZ: [0, 0],
opacity: [0, 1],
complete: event => event.animatables[0].target.removeAttribute('style')
})
// Photo: Active
tl.add({
targets: scope.querySelector('.is-active picture'),
translateY: [8, 0],
translateZ: [0, 0],
complete: event => event.animatables[0].target.removeAttribute('style')
}, 100)
// Photo: Prev
tl.add({
targets: scope.querySelector('.is-prev picture'),
translateY: [8, 0],
translateX: [64, 0],
translateZ: [0, 0],
rotate: window.innerWidth >= 768 ? [-2, 0] : [0, 0],
complete: event => event.animatables[0].target.removeAttribute('style')
}, 100)
// Photo: Next
tl.add({
targets: scope.querySelector('.is-next picture'),
translateY: [8, 0],
translateX: [-48, 0],
translateZ: [0, 0],
rotate: window.innerWidth >= 768 ? [2, 0] : [0, 0],
complete: event => event.animatables[0].target.removeAttribute('style')
}, 100)
// Reveal on scroll
let visible = false
setTimeout(() => {
const carouselReveal = ScrollOut({
once: true,
targets: scope,
onChange: (el, ctx) => {
if (ctx.visible === 0) {
visible = true
}
},
onShown: (el, ctx) => {
// If revealed on scroll, no delay
if (visible) {
setTimeout(() => tl.restart(), 10)
}
// If revealed on load, add a delay
else {
setTimeout(() => tl.restart(), animDelay * 2)
}
}
})
}, 10)
}

View File

@@ -1,58 +0,0 @@
import anime from 'animejs'
import ScrollOut from 'scroll-out'
import { animDurationLong } from 'utils/store'
/*
** Transition In
*/
export const animateIn = scope => {
let delay = 0
// Each location (reveal on scroll)
scope.querySelectorAll('.location').forEach(location => {
const tl = anime.timeline({
easing: 'easeOutQuart',
duration: 600,
autoplay: false,
delay,
complete: () => {
// Reset delay
delay = 0
}
})
// Image
tl.add({
targets: location.querySelector('img'),
scale: [1.3, 1],
opacity: [0, 1],
translateZ: [0, 0],
duration: 1800
}, 100)
// Name
tl.add({
targets: location.querySelector('h3'),
translateY: ['100%', 0],
translateZ: [0, 0],
}, 150)
// Country
tl.add({
targets: location.querySelector('p'),
translateY: ['100%', 0],
translateZ: [0, 0]
}, 200)
// Increase delay between locations
delay += 65
// Scroll reveal
ScrollOut({
once: true,
targets: location,
onShown: () => tl.restart()
})
})
}

View File

@@ -1,95 +0,0 @@
import anime from 'animejs'
import ScrollOut from 'scroll-out'
import imagesLoaded from 'imagesloaded'
import { throttle, parallaxAnime } from 'utils/functions'
import { animDuration } from 'utils/store'
/*
** Transition In
*/
export const animateIn = scope => {
const tlLocation = anime.timeline({
easing: 'easeOutQuart',
duration: 1000,
autoplay: false
})
// Title
tlLocation.add({
targets: scope.querySelectorAll('.photo__location .line span'),
translateY: ['120%', 0],
translateZ: [0, 0],
delay: anime.stagger(120)
}, 200)
// Description
tlLocation.add({
targets: scope.querySelectorAll('.photo__location p'),
opacity: [0, 1],
duration: animDuration
}, 400)
// Reveal on scroll
const locationScroll = ScrollOut({
once: true,
targets: scope,
onShown: () => tlLocation.restart()
})
// Image (reveal on scroll)
const photoImage = scope.querySelector('.photo__image')
const photoReveal = anime.timeline({
easing: 'easeOutQuart',
duration: 2000,
autoplay: false
})
photoReveal.add({
targets: scope.querySelector('.photo__picture'),
opacity: [0, 1]
}, 50)
photoReveal.add({
targets: scope.querySelector('.photo__picture img'),
scale: [1.12, 1],
translateZ: [0, 0]
}, 50)
// Show photo when image is loaded
imagesLoaded(photoImage, instance => {
const photoScroll = ScrollOut({
once: true,
targets: photoImage,
onShown: () => photoReveal.restart()
})
})
// Number parallax on scroll
const media768 = window.matchMedia('(min-width: 768px)')
const number = scope.querySelector('.photo__number')
const numberPallax = anime({
targets: number.querySelector('span'),
translateY: (window.innerWidth <= 768) ? ['0%', '20%'] : ['-20%', '20%'],
easing: 'linear',
duration: 2000,
autoplay: false
})
const numberPallaxAnime = () => parallaxAnime(number, numberPallax)
const numberPallaxScroll = matchMedia => {
if (matchMedia.matches) {
const scroll = ScrollOut({
targets: scope,
onShown: () => {
window.addEventListener('scroll', throttle(numberPallaxAnime, 50))
requestAnimationFrame(numberPallaxAnime)
},
onHidden: () => {
if (parallaxAnime) window.removeEventListener('scroll', parallaxAnime)
}
})
}
}
// Listen on screen size to run the function
media768.addListener(numberPallaxAnime)
numberPallaxScroll(media768)
}

View File

@@ -1,31 +0,0 @@
import anime from 'animejs'
import ScrollOut from 'scroll-out'
import { firstLoad, animDurationLong, animDelay } from 'utils/store'
let firstLoadValue
firstLoad.subscribe(store => firstLoadValue = store)
/*
** Transition In
*/
export const animateIn = (scope, init) => {
// Stagger each letters and words
const letters = anime({
targets: scope.querySelectorAll('span, em span'),
translateY: ['100%', 0],
translateZ: [0, 0],
easing: 'easeOutQuart',
duration: 1000,
delay: anime.stagger(40, { start: init ? 0 : (firstLoadValue) ? animDurationLong : animDelay }),
autoplay: false
})
// On scroll animation
requestAnimationFrame(() => {
const title = ScrollOut({
once: true,
targets: scope,
onShown: () => requestAnimationFrame(() => letters.restart())
})
})
}

View File

@@ -1,16 +0,0 @@
import { quartInOut } from 'svelte/easing'
/*
** Animation Out: Background
*/
export const panelBackgroundOut = (node, params) => {
return {
delay: params.delay || 0,
duration: params.duration || 400,
easing: params.easing || quartInOut,
css: (t, u) => `
transform: scaleY(${t})
`
}
}

View File

@@ -3,8 +3,9 @@ import { quartOut } from 'svelte/easing'
// Crossfade transition // Crossfade transition
export const [send, receive] = crossfade({ export const [send, receive] = crossfade({
// duration: 1200,
duration: d => Math.sqrt(d * 200), duration: d => Math.sqrt(d * 200),
fallback: (node, params) => { fallback (node, params) {
const { const {
duration = 600, duration = 600,
easing = quartOut, easing = quartOut,
@@ -13,6 +14,7 @@ export const [send, receive] = crossfade({
const style = getComputedStyle(node) const style = getComputedStyle(node)
const transform = style.transform === 'none' ? '' : style.transform const transform = style.transform === 'none' ? '' : style.transform
const sd = 1 - start const sd = 1 - start
return { return {
duration, duration,
easing, easing,
@@ -22,4 +24,4 @@ export const [send, receive] = crossfade({
` `
} }
} }
}) })

View File

@@ -0,0 +1,7 @@
import type { Easing } from 'motion'
/**
* Ease: Quart Out Array
*/
export const quartOut: Easing = [.165, .84, .44, 1]

View File

@@ -1,93 +0,0 @@
import anime from 'animejs'
import ScrollOut from 'scroll-out'
import { animDuration, animDelay } from 'utils/store'
import { throttle, parallaxAnime } from 'utils/functions'
/*
** Transition In
*/
export const animateIn = () => {
// Title: Houses
const titleHouses = anime({
targets: document.querySelectorAll('#title-houses span'),
translateY: ['-70%', 0],
translateZ: [0, 0],
easing: 'easeOutQuart',
delay: anime.stagger(80, { start: animDelay }),
duration: animDuration
})
// Title: Parallax on scroll
const translate = anime({
targets: '#title-houses',
translateX: window.innerWidth <= 1920 ? ['25%', '-15%'] : ['7%', '-7%'],
translateZ: [0, 0],
easing: 'linear',
duration: animDuration,
autoplay: false
})
window.addEventListener('scroll', throttle(() => parallaxAnime(document.getElementById('title-houses'), translate), 5))
requestAnimationFrame(() => parallaxAnime(document.getElementById('title-houses'), translate))
// Intro: Description
const introDescription = anime({
targets: document.getElementById('intro-description').querySelectorAll('p, a'),
opacity: [0, 1],
translateY: [8, 0],
translateZ: [0, 0],
easing: 'easeOutQuart',
duration: animDuration,
delay: anime.stagger(200, { start: animDelay + 200 })
})
// Title: Of (reveal on scroll)
const titleOf = document.getElementById('title-of')
const titleOfReveal = anime({
targets: titleOf.querySelectorAll('span'),
translateY: ['100%', 0],
translateZ: [0, 0],
easing: 'easeOutQuart',
delay: anime.stagger(70),
duration: animDuration,
autoplay: false
})
const titleOfScroll = ScrollOut({
once: true,
targets: titleOf,
onShown: () => titleOfReveal.restart()
})
// Title: World (reveal on scroll)
const titleWorld = document.getElementById('title-world')
const titleWorldReveal = anime({
targets: titleWorld.querySelectorAll('span'),
translateY: ['100%', 0],
translateZ: [0, 0],
easing: 'easeOutQuart',
delay: anime.stagger(70),
duration: animDuration,
autoplay: false
})
const titleWorldParallax = anime({
targets: titleWorld,
translateX: ['5%', '-3%'],
translateZ: [0, 0],
easing: 'linear',
duration: animDuration,
autoplay: false
})
const titleWorldAnime = () => parallaxAnime(titleWorld, titleWorldParallax)
const titleWorldScroll = ScrollOut({
once: true,
targets: titleWorld,
onShown: () => {
titleWorldReveal.restart()
window.addEventListener('scroll', throttle(titleWorldAnime, 10))
requestAnimationFrame(titleWorldAnime)
},
onHidden: () => {
if (parallaxAnime) window.removeEventListener('scroll', parallaxAnime)
}
})
}

View File

@@ -1,22 +0,0 @@
import anime from 'animejs'
import { animDuration, animDelay } from 'utils/store'
/*
** Transition In
*/
export const animateIn = () => {
const tl = anime.timeline({
easing: 'easeOutQuart',
duration: animDuration
})
// Simple slide and fade on each part of the page
tl.add({
targets: document.querySelectorAll('.page__part, .globe'),
opacity: [0, 1],
translateY: [8, 0],
translateZ: [0, 0],
delay: anime.stagger(200, { start: animDelay })
})
}

View File

@@ -1,62 +0,0 @@
import anime from 'animejs'
import Rellax from 'rellax'
import { animDuration, animDelay } from 'utils/store'
/*
** Transition In
*/
export const animateIn = () => {
const tl = anime.timeline({
duration: animDuration,
delay: animDelay, // Delay in AnimeJS waits to run but sets the starting style as opposed to a setTimeout
easing: 'easeOutQuart'
})
// Title: Houses
tl.add({
targets: '.place__title_houses',
translateY: ['150%', 0],
translateZ: [0, 0]
})
// Title: Of
tl.add({
targets: '.place__title_of',
opacity: [0, 1]
}, 600)
// Title: Place name
tl.add({
targets: '.place__title_name',
translateY: ['150%', 0],
translateZ: [0, 0]
}, 150)
// Switcher link
tl.add({
targets: '.place__title .button-control',
scale: [0.95, 1],
opacity: [0, 1]
}, 500)
// Illustration
tl.add({
targets: '.place__illustration',
scale: [1.05, 1],
translateZ: [0, 0],
opacity: [0, 1]
}, 0)
// Description
tl.add({
targets: '.place__description',
opacity: [0, 1],
translateY: [24, 0],
translateZ: [0, 0]
}, 450)
/*
** Parallax
*/
const rellax = new Rellax('[data-rellax-speed]')
}

45
src/animations/reveal.ts Normal file
View File

@@ -0,0 +1,45 @@
import { animate, inView, stagger } from 'motion'
import { quartOut } from '$animations/easings'
const defaultOptions = {
stagger: null,
delay: 0,
duration: 1.6,
easing: quartOut,
}
export default (node: Element | any, {
enable = true,
children = undefined,
animation = [],
options = defaultOptions,
}: RevealOptions) => {
if (!enable) return
// Define targets from children, if empty get node
const targets = children ? node.querySelectorAll(children) : [node]
// If animation has opacity starting with 0, hide it first
if (animation.opacity && animation.opacity[0] === 0) {
targets.forEach((el: HTMLElement) => el.style.opacity = '0')
}
// Create inView instance
inView(node, ({ isIntersecting }) => {
const anim = animate(
targets,
animation,
{
delay: options.stagger ? stagger(options.stagger, { start: options.delay }) : options.delay,
duration: options.duration,
easing: options.easing,
}
)
anim.stop()
// Run animation if in view and tab is active
isIntersecting && requestAnimationFrame(anim.play)
}, {
amount: options.threshold,
})
}

View File

@@ -0,0 +1,25 @@
import { animate } from 'motion'
import { quartOut } from './easings'
export const scaleFade = (node: HTMLElement, {
delay = 0,
duration = 1,
scale = [0.7, 1],
opacity = [1, 0],
x = null,
}) => {
return {
css: () => {
animate(node, {
scale,
opacity,
x,
z: 0,
}, {
easing: quartOut,
duration,
delay,
})
}
}
}

View File

@@ -1,40 +0,0 @@
import anime from 'animejs'
import { animDuration, animDelay } from 'utils/store'
/*
** Transition In
*/
export const animateIn = () => {
const viewer = document.querySelector('.viewer')
const tl = anime.timeline({
easing: 'easeOutQuart',
duration: animDuration,
delay: 1400
})
// Carousel: Number
tl.add({
targets: viewer.querySelector('.counter'),
opacity: [0, 1],
translateY: [window.innerWidth >= 768 ? -24 : 24, 0]
}, 0)
// Dots
tl.add({
targets: viewer.querySelectorAll('.carousel__dots'),
translateY: [16, 0],
translateZ: [0, 0],
opacity: [0, 1]
}, 150)
// Buttons
tl.add({
targets: viewer.querySelectorAll('.tip, .viewer__buttons a'),
translateY: [-32, 0],
translateZ: [0, 0],
opacity: [0, 1],
delay: anime.stagger(120),
}, 400)
}

88
src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,88 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
// interface Locals {}
// interface PageData {}
// interface Error {}
// interface Platform {}
}
/**
* Custom Events
*/
// Swipe
declare namespace svelte.JSX {
interface HTMLAttributes<T> {
onswipe?: (event: CustomEvent<string> & { target: EventTarget & T }) => any,
ontap?: (event: CustomEvent<boolean> & { target: EventTarget & T }) => any,
oncopied?: (event: CustomEvent<any> & { target: EventTarget & T }) => any,
}
}
/**
* Custom Types
*/
declare interface PhotoGridAbout {
id: string
title: string
slug: string
image: {
id: string
title: string
}
}
/**
* Notifcation
*/
declare interface ShopNotification {
title: string
name: string
image: string
timeout?: number
id?: number
}
/**
* Smooth Scroll Options
*/
declare interface smoothScrollOptions {
hash: string
changeHash?: boolean
event?: MouseEvent
callback?: Function
}
/**
* Swipe options
*/
declare interface SwipeOptions {
travelX?: number
travelY?: number
timeframe?: number
}
/**
* Reveal Animation
*/
declare type RevealOptions = {
enable?: boolean
options?: TransitionOptions
children?: string | HTMLElement
animation: any
}
// Options interface
declare type TransitionOptions = {
threshold?: number
duration?: number
stagger?: number
delay?: number
easing?: string | Easing
}

23
src/app.html Normal file
View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="theme-color" content="#3C0576">
<meta name="mobile-web-app-capable" content="yes">
<link rel="manifest" href="%sveltekit.assets%/manifest.json">
<link type="text/plain" rel="author" href="%sveltekit.assets%/humans.txt">
<link rel="icon" type="image/png" sizes="64x64" href="/images/favicon.png">
<link rel="apple-touch-icon" sizes="180x180" href="/images/siteicon.png">
%sveltekit.head%
</head>
<body>
%sveltekit.body%
<script>
document.body.style.opacity = '0'
</script>
</body>
</html>

View File

@@ -1,9 +0,0 @@
<script>
// Props
export let text
export let size = 'small'
</script>
<div class="badge badge--{size}">
<span>{text}</span>
</div>

View File

@@ -1,23 +0,0 @@
<script>
export let href = '#'
export let type = 'a'
export let text = ''
export let noScroll = undefined
</script>
{#if type === 'button'}
<button class={$$props.class ? $$props.class : 'button'} on:click>
<slot></slot>
<div class="text" data-text={text}>
<span>{text}</span>
</div>
</button>
{:else}
<a {href} class={$$props.class ? $$props.class : 'button'} on:click sapper-noscroll={noScroll}>
<slot></slot>
<div class="text" data-text={text}>
<span>{text}</span>
</div>
</a>
{/if}

View File

@@ -1,47 +0,0 @@
<script>
import { onMount } from 'svelte'
// Props
export let currentIndex = 0
export let className = null
// Variables
let counter
const numbers = [...Array(10).keys()]
// Reactive variables depending on currentIndex
$: actualIndex = currentIndex + 1
$: amount = String(actualIndex).length
$: index = (actualIndex < 10) ? String(actualIndex).padStart(2, '0') : String(actualIndex)
$: digits = index.split('')
// Set columns height
const setColumnHeight = () => {
// Set each digit column's height = its spans combined (in order to translate in tens of %)
counter.querySelectorAll('div').forEach(column => {
const spans = column.querySelectorAll('span')
column.style.height = spans[0].offsetHeight * spans.length + 'px'
})
}
/*
** Run code when mounted
*/
onMount(() => {
setColumnHeight()
})
</script>
<svelte:window on:resize={setColumnHeight} />
<div class="counter {className}" bind:this={counter}>
{#each digits as digit}
<div class="counter__column" style="transform: translateY(-{digit}0%) translateZ(0);">
{#each numbers as number}
<span>{number}</span>
{/each}
</div>
{/each}
</div>

View File

@@ -1,20 +0,0 @@
<script>
export let direction = 'left'
export let color = '#fff'
export let width = 20
export let hidden = undefined
</script>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewbox="0 0 20 20"
width={width} height={width}
fill={color}
class={$$props.class}
aria-hidden={hidden}
data-width={width}
>
{#if direction === 'left'}
<path fill-rule="nonzero" d="M.26 10.85l-.06-.11-.08-.15-.05-.16-.04-.13a1.5 1.5 0 010-.6c0-.05.03-.09.04-.13l.05-.16.08-.15.06-.1c.06-.1.13-.17.2-.25L9.2.45c.61-.6 1.61-.6 2.23 0 .62.6.62 1.57 0 2.17L5.4 8.47h13.02c.87 0 1.58.68 1.58 1.53s-.7 1.53-1.58 1.53H5.4l6.03 5.85c.62.6.62 1.57 0 2.17-.3.3-.71.45-1.12.45-.4 0-.8-.15-1.11-.45L.46 11.08a1.5 1.5 0 01-.2-.23"/>
{:else if direction === 'right'}
<path fill-rule="nonzero" d="M19.74 10.85l.06-.11.08-.15.05-.16.04-.13c.04-.2.04-.4 0-.6 0-.05-.03-.09-.04-.13l-.05-.16-.08-.15-.06-.1a1.53 1.53 0 00-.2-.25L10.8.45c-.61-.6-1.61-.6-2.23 0a1.5 1.5 0 000 2.17l6.03 5.85H1.58C.71 8.47 0 9.15 0 10s.7 1.53 1.58 1.53H14.6l-6.03 5.85a1.5 1.5 0 000 2.17c.3.3.71.45 1.12.45.4 0 .8-.15 1.11-.45l8.74-8.47.2-.23"/>
{/if}
</svg>

View File

@@ -1,9 +0,0 @@
<script>
export let width = 18
export let color = '#fff'
export let hidden = undefined
</script>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 17 17" {width} fill={color} class={$$props.class} aria-hidden={hidden}>
<path fill-rule="nonzero" d="M16.63.37c.47.47.5 1.2.09 1.7l-.09.1-6.32 6.33 6.32 6.32a1.28 1.28 0 01-1.7 1.9l-.1-.09-6.33-6.32-6.32 6.32a1.28 1.28 0 01-1.9-1.7l.09-.1L6.69 8.5.37 2.18a1.28 1.28 0 011.7-1.9l.1.09L8.5 6.69 14.82.37c.5-.5 1.31-.5 1.81 0z"/>
</svg>

View File

@@ -1,40 +0,0 @@
<script>
import { stores } from '@sapper/app'
import { randomString } from 'utils/functions'
// Props
export let width = 56
export let color = '#000'
export let animated = false
const { page } = stores()
// Generate a random ID for the mask
const randomId = randomString(6, 'A')
</script>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1"
width={width} viewBox="0 0 56 75"
fill={color}
class="icon-svg icon-globe" class:is-animated={animated}
>
<defs>
<clipPath id={randomId}>
<ellipse cx="31.5" cy="60.6" rx="24.5" ry="24.4"/>
</clipPath>
</defs>
<g transform="translate(0,-32)">
<g clip-path="url({$page.path}#{randomId})">
<g class="anim anim-spinGlobe">
<path d="M43 65.3c-2.2-.1-1.8.9-3.6-.7-1 .1-1.6-.3-2.6-.2-.4-.1-.5-.5-1.1-.3-.8 0-1.2 1-2.1.6-.2.3-.4.7-1 .8-.3 0-.8.3-1.2 0-.7-2.4-1.8.7-1.8-2h-.1c-2.4 4.5-4.8-4.2 1.2-1.7h.1v-.1c-1-1-.2-1.7 0-2.9-.7-.6.7-2.7 1.7-2.9.1.3.8-.3 1.1-.8-.2-.3-.2-.2-.9.2-1-.4-.3-1.3-1.6.2-.3-1.6 3.1-1.6 2.2-3-1.1-.1-1.5-.6-2.3-1.4-.8 1.7-4.6-2-2.7 1.9.5 4.3-.2.5-3.5 1.2-2.3-1.2 2.1-4.1.8-4.8-1-.7-.4 2.9-3.4-1-1 .9 2.2 2.7-.3 2.4-.4.9-1 .6-1.8.9-.2.4-.2.5-.8.6-1.5.1-3.6-.5-4.9 1v-.4c-1.8 1.9-9.1.6-6.1 5 0 .3 0 .2-.3.7.1.2.1.3.2.5.5.1 1.4-.2 1.9.3-.4.8.4.8.5 0 2.3-3.6 5.7-1.9 8.4.1.6 1.8 1.6 3.2 3.5 4.2.3.9 2.3 1.6.5-.1.3-1.4 2.9 2.9 5.1 1.7h.3c1.3-.1 3 1.1 4.3.7 1.2.7-.3 3 1.4 3.7 2.7 1.2 4 3.6 3.6 6.6.8 1.3.4 4 2.4 3.9 1.9-.3-2.2-3.1 1.2-5.5.5-2.3 1.7-4.1 2.4-6.4-.6-1.1.8-2.6-.7-3zM76.1 36.6c-.5-.9 6.3-5.7 5.5-3.8-.3.7-1.5 2.5-.1 2.8.8-.5.4-2.2 1.1-3 3.6-1.5 3.6-6-.8-3.7-7.1 1.4-4.2-1-8.6 2.6-2.1-1.5-3.4-.1-5.8.8-.9-.2 1.4-1 .2-1.6-3.2-.5-5.5 3-6.8 5.4-1.9-.8-1.5-1.1-2.4 1.1-.9.2-1.4.6-2 1.5-1.5-.1-3.5 4.7-4.3 2.1.4-.3.9-.1 1.3-.7-3.2-4.4-10.3 8.2-3.9 5.6-.8-1.3.5-5.3.7-1.8 2.3 2.8-3.9 3.2-3.9 5.8.6 1.6-.3 1.2-.9 2.1-.4 1.5 1.3 1.9 2.2.9.8-1.8 1.2-3.2 3.5-2.1-.4 1.2 1.2 0 0-.5-.8-.2-1.3-1.4.2-.6.5.2.8 1.1 1.3.8.7-1.1.8.1 1.8.1 3.3-1.1.7 2.2-1.2 1.7-.6.5-1.4 1-2.2.6-4.4-2.8-8.1 8-2.1 8.2 4-.8 2.9-2.7 5.7 1 1.1 4 5.1 9.1 6 2.2 0-1.2.9-2.4.3-3.6-1.7-2.3.5-3.7.2-5.5-1.4.9-4.9-1.7-5-2.9 1.4 0 3.4 2.9 4.3 2.3.5-.6 2.8-2.7 1.5-3.1-2.2 1.1-4.3-1.7-1-.6 6.1-1.4 4.9 2.4 7 2.1.1-1.5 1.1-5.8 2.7-2.5.6-1.3.9-.3 1.4.8 0 .5 1.6 1.3 1 .4-.1 0-.2 0-.4-.1-1.1-1.7 3.7.6.2-2.9 1.4-1.1 3.6-2.6 1.8-5 .4-.2.3-.9-.3-.4-1.5-1.2 1.3-.9 2.1-.4-1.1-2.2 2.9-3.4 1.2-6-.5-.5-.9.1-1.5-.1zM56.3 48.3c-.9.1-1.6.7-2.7.7-.4-.4.1-2.4 1-1.3.3-.3.1-.8.9-1 0 .1 0 .4.1.8.4.1.6 0 1.1.4-.2.1-.3.3-.4.4zm2.1-.8c-.9-.5-.9-1.2 0-1.9.8-.1.1.8.4 1.1 1.8.2.7 3.3-.4.8zM27.4 45c1.1.2 4.1-1.3 3.9 1.2.6-.7 2.1 0 .5.4.8.3.7.6 1 1.4 1.7 2.1 3.2 1.7 3.1-1 4-3.6 2.6-2.5 2.2-7.3 0-.6 1.2-1.1.5-1.6-.6-.1-1.1 1.1-1.8.3-3.7-.9-8.6 3.1-10.6 6.2-.1.9.6.2 1.2.4zM84.7 53.3c-.3-.2-.5-.7-1.2-.8 0 .5.2 2.1-.9 1.5-.3-1.4-1.6-1.1-1.7.2-2.6 2.4-3.6 2.8-1.3 5.8 1.1-.2 2.2-2.1 3.5-2 .8.5 1.6-.1 2.2.8 1 .3 2.6-.2 2.2-1.7.4-2.8-1.3-2.5-2.8-3.8z"/>
<path d="M21.5 45.8c.7.2 1.2-.3 2-.1 0 .1-.1.1-.2.4-.4.1-.1.6.4.3 2.1-.5 1.5-.5 2-2.3.8-.9 3.8-3.7.5-2.4-1.2.7-4.8 1.8-4.3 2.9-.3.4-1.6.9-.4 1.2zM29.9 49.1c1.1.2.7-1-.7-.8-.9-.9-2.4-1-3.7-.5-.4-.3-.6-.4-1 .1-.4-.8-1.4.4-.6 1 2.1.6 2.7-1.1 5 .8v.1c-1.2-.2-2.6 1.8-.6.6.5 0 1.3.7 1.9.2 0 0-.2-.3-.5-.4.1-.1.3-.2.4-.4-.1-.1-1.1-.9-.2-.7zM17.5 51.1c.3.1 1-.1 1 .6.7.1 2.2-.4 2.9-.9-.6-.1-1.5-1.9-1.9-1.3-.4 1.1-1.8.3-2 1.6zM81.5 50.9c-1.2-.5-1.8.4.2.5.5 0 .7.9 1.3.6.4-.8 3.8.1 1.6-.9-.7-.7-2.3-.9-3.1-.2zM76.2 50.4c-.2.6-1 1.4-1 2.1 1.5 1.4 2.4-.4 1.4-1.9-.1-.3-.3-.3-.4-.2zM62.4 60.2c-.5.2-.2.9-.2 1.5 1.8 3.5 2-1 .2-1.5zM56.7 37.4c-.7-1.2.1-1.8.9-2.7 1.5-.6 1.2-1.8-.5 0-1.1.3-2.5 3-.4 2.7zM16.4 51.5c.9-.8 1.7-2.2-.6-1.2-.5.4-.4 1.5.6 1.2zM45.4 46.8c-.9-.9-2.7 1.3-.6 1 .2-.2.8-.5.6-1zM41.5 45c-.4-.1-1.4.4-2 .7.5.7 2.4.5 2-.7zM45.1 48.4c0 .3.4.3.9.5 1.2-1-.3-1.6-.9-.5zM33.6 53.3c-.8.4 1.1 1.6 1.3.6-.1-.4-.9-.8-1.3-.6zM93.4 57.2c-.1 0-.1-.1-.2-.1-1.9 1.5-.2 3.2.2.1zM47.8 36.5c.7-.2.6-1.2-.4-.8-.5.3-.4.9.4.8zM45.8 37.7c.7-.2.5-1.2-.5-.7-.3.3-.2.9.5.7zM93.6 55.8c-.1-.5-1-.4-.4.2 0 .2.1.4.2.6-.1.3.7.8.4-.1l.3-.6c-.2 0-.3-.1-.5-.1zM44 48.3c-.2.3-.6 1 .3.8.5-.4.4-.6-.3-.8zM22.6 47.5c-.3 0-.3.6.1.4 1.1.3.8-1.3-.1-.4zM87.1 59.1c-.1.3-.2.4-.1.6.4.4 1-.2.1-.6zM50.1 51c-.2-.2-1 .1-.2.5.1-.2.1-.4.2-.5zM47.6 37.2c-.3 0-.5-.2-.6.3.2 0 .4.2.6-.3zM21.1 46.7v.1c.1 0 .5.1.6-.2-.2-.1-.3-.2-.6.1zM17.9 49.4c-.2-.1-.4-.2-.6.2.2.1.3.1.6-.2zM19.5 48.8v-.1c-.1.1-.4-.1-.5.2.2.1.4.1.5-.1zM21.3 49.8c-.2 0-.4-.2-.5.1.2.2.3.2.5-.1zM68.3 52.7c0 .2-.1.2-.1.3.2.3.7-.1.1-.3z"/>
<path d="M27.5 49.2c0-.1-.2-.1-.3-.1 0 .1-.2.3.2.4 0-.2.1-.3.1-.3zM26.1 47.1c-.2 0-.4.1-.5.1v.1c.3.1.3.1.5-.2zM53.9 50.7c-.1.1-.2.1-.3.2 0 0 0 .1.1.1s.2-.1.3-.1c0-.1-.1-.1-.1-.2zM48.1 46.8c0-.1.2-.1-.1-.3.1 0-.1.1.1.3zM26.6 51.7s0-.1-.2 0c.1 0 .1.2.2 0zM22 48.1s.1 0 0 0c.1-.1.1-.1 0 0zM14.6 52.9zM76.1 53.7c-.5-.1-1.1 0-1.6-.4-5.7-3.3-1.2 2.2 1.6.9.9 0 1-.8 0-.5zM26.9 51c-.2-.2-.1-.2-.8-.1-.2 0-.8-.3-.7.3-.3.1-.2.8.3.3.1 0 .2-.1.3-.1.1-.1.9.1.9-.4zM123.3 37c-2.2-.1-1.8.9-3.6-.7-1 .1-1.6-.3-2.6-.2-.4-.1-.5-.5-1.1-.3-.8 0-1.2 1-2.1.6-.2.3-.4.7-1 .8-.3 0-.8.3-1.2 0-.7-2.4-1.8.7-1.8-2h-.1c-2.4 4.5-4.8-4.2 1.2-1.7h.1v-.1c-1-1-.2-1.7 0-2.9-.7-.6.7-2.7 1.7-2.9.1.3.8-.3 1.1-.8-.2-.3-.2-.2-.9.2-1-.4-.3-1.3-1.6.2-.3-1.6 3.1-1.6 2.2-3-1.1-.1-1.5-.6-2.3-1.4-.8 1.7-4.6-2-2.7 1.9.5 4.3-.2.5-3.5 1.2-2.3-1.2 2.1-4.1.8-4.8-1-.7-.4 2.9-3.4-1-1 .9 2.2 2.7-.3 2.4-.4.9-1 .6-1.8.9-.2.4-.2.5-.8.6-1.5.1-3.6-.5-4.9 1v-.4c-1.8 1.9-9.1.6-6.1 5 0 .3 0 .2-.3.7.1.2.1.3.2.5.5.1 1.4-.2 1.9.3-.4.8.4.8.5 0 2.3-3.6 5.7-1.9 8.4.1.6 1.8 1.6 3.2 3.5 4.2.3.9 2.3 1.6.5-.1.3-1.4 2.9 2.9 5.1 1.7h.3c1.3-.1 3 1.1 4.3.7 1.2.7-.3 3 1.4 3.7 2.7 1.2 4 3.6 3.6 6.6.8 1.3.4 4 2.4 3.9 1.9-.3-2.2-3.1 1.2-5.5.5-2.3 1.7-4.1 2.4-6.4-.6-1 .8-2.6-.7-3zM156.4 8.3c-.5-.9 6.3-5.7 5.5-3.8-.3.7-1.5 2.5-.1 2.8.8-.5.4-2.2 1.1-3 3.6-1.5 3.6-6-.8-3.7-7.1 1.4-4.2-1-8.6 2.6-2.1-1.5-3.4-.1-5.8.8-.9-.2 1.4-1 .2-1.6-3.2-.5-5.5 3-6.8 5.4-1.9-.8-1.5-1.1-2.4 1.1-.9.2-1.4.6-2 1.5-1.5-.1-3.5 4.7-4.3 2.1.4-.3.9-.1 1.3-.7-3.2-4.4-10.3 8.2-3.9 5.6-.8-1.3.5-5.3.7-1.8 2.3 2.8-3.9 3.2-3.9 5.8.6 1.6-.3 1.2-.9 2.1-.4 1.5 1.3 1.9 2.2.9.8-1.8 1.2-3.2 3.5-2.1-.4 1.2 1.2 0 0-.5-.8-.2-1.3-1.4.2-.6.5.2.8 1.1 1.3.8.7-1.1.8.1 1.8.1 3.3-1.1.7 2.2-1.2 1.7-.6.5-1.4 1-2.2.6-4.4-2.8-8.1 8-2.1 8.2 4-.8 2.9-2.7 5.7 1 1.1 4 5.1 9.1 6 2.2 0-1.2.9-2.4.3-3.6-1.7-2.3.5-3.7.2-5.5-1.4.9-4.9-1.7-5-2.9 1.4 0 3.4 2.9 4.3 2.3.5-.6 2.8-2.7 1.5-3.1-2.2 1.1-4.3-1.7-1-.6 6.1-1.4 4.9 2.4 7 2.1.1-1.5 1.1-5.8 2.7-2.5.6-1.3.9-.3 1.4.8 0 .5 1.6 1.3 1 .4-.1 0-.2 0-.4-.1-1.1-1.7 3.7.6.2-2.9 1.4-1.1 3.6-2.6 1.8-5 .4-.2.3-.9-.3-.4-1.5-1.2 1.3-.9 2.1-.4-1.1-2.2 2.9-3.4 1.2-6-.5-.4-.8.1-1.5-.1zM136.6 20c-.9.1-1.6.7-2.7.7-.4-.4.1-2.4 1-1.3.3-.3.1-.8.9-1 0 .1 0 .4.1.8.4.1.6 0 1.1.4-.2.1-.3.3-.4.4zm2.1-.8c-.9-.5-.9-1.2 0-1.9.8-.1.1.8.4 1.1 1.8.3.7 3.4-.4.8zM107.7 16.7c1.1.2 4.1-1.3 3.9 1.2.6-.7 2.1 0 .5.4.8.3.7.6 1 1.4 1.7 2.1 3.2 1.7 3.1-1 4-3.6 2.6-2.5 2.2-7.3 0-.6 1.2-1.1.5-1.6-.6-.1-1.1 1.1-1.8.3-3.7-.9-8.6 3.1-10.6 6.2 0 .9.7.2 1.2.4zM165 25c-.3-.2-.5-.7-1.2-.8 0 .5.2 2.1-.9 1.5-.3-1.4-1.6-1.1-1.7.2-2.6 2.4-3.6 2.8-1.3 5.8 1.1-.2 2.2-2.1 3.5-2 .8.5 1.6-.1 2.2.8 1 .3 2.6-.2 2.2-1.7.4-2.8-1.3-2.5-2.8-3.8z"/>
<path d="M101.8 17.5c.7.2 1.2-.3 2-.1 0 .1-.1.1-.2.4-.4.1-.1.6.4.3 2.1-.5 1.5-.5 2-2.3.8-.9 3.8-3.7.5-2.4-1.2.7-4.8 1.8-4.3 2.9-.2.5-1.6.9-.4 1.2zM110.2 20.8c1.1.2.7-1-.7-.8-.9-.9-2.4-1-3.7-.5-.4-.3-.6-.4-1 .1-.4-.8-1.4.4-.6 1 2.1.6 2.7-1.1 5 .8v.1c-1.2-.2-2.6 1.8-.6.6.5 0 1.3.7 1.9.2 0 0-.2-.3-.5-.4.1-.1.3-.2.4-.4-.1-.1-1.1-.9-.2-.7zM97.8 22.8c.3.1 1-.1 1 .6.7.1 2.2-.4 2.9-.9-.6-.1-1.5-1.9-1.9-1.3-.4 1.1-1.8.4-2 1.6zM161.8 22.6c-1.2-.5-1.8.4.2.5.5 0 .7.9 1.3.6.4-.8 3.8.1 1.6-.9-.7-.6-2.3-.9-3.1-.2zM156.5 22.2c-.2.6-1 1.4-1 2.1 1.5 1.4 2.4-.4 1.4-1.9-.1-.3-.2-.4-.4-.2zM142.7 32c-.5.2-.2.9-.2 1.5 1.8 3.5 2-1 .2-1.5zM137 9.1c-.7-1.2.1-1.8.9-2.7 1.5-.6 1.2-1.8-.5 0-1.1.4-2.5 3-.4 2.7zM96.7 23.2c.9-.8 1.7-2.2-.6-1.2-.4.4-.3 1.6.6 1.2zM125.7 18.5c-.9-.9-2.7 1.3-.6 1 .2-.1.8-.5.6-1zM121.8 16.8c-.4-.1-1.4.4-2 .7.5.6 2.4.5 2-.7zM125.4 20.2c0 .3.4.3.9.5 1.2-1-.3-1.7-.9-.5zM114 25.1c-.8.4 1.1 1.6 1.3.6-.2-.5-1-.8-1.3-.6zM173.7 29c-.1 0-.1-.1-.2-.1-1.9 1.4-.2 3.2.2.1zM128.1 8.2c.7-.2.6-1.2-.4-.8-.4.4-.3.9.4.8zM126.2 9.4c.7-.2.5-1.2-.5-.7-.4.3-.3.9.5.7zM174.5 27.7c-.2 0-.4-.1-.6-.1-.1-.5-1-.4-.4.2 0 .2.1.4.2.6-.1.3.7.8.4-.1.2-.2.3-.4.4-.6zM124.3 20.1c-.2.3-.6 1 .3.8.5-.4.4-.7-.3-.8zM102.9 19.3c-.3 0-.3.6.1.4 1.1.3.8-1.3-.1-.4zM167.4 30.9c-.1.3-.2.4-.1.6.4.3 1-.3.1-.6zM130.4 22.7c-.2-.2-1 .1-.2.5.1-.2.1-.3.2-.5zM128 8.9c-.3 0-.5-.2-.6.3.1.1.4.3.6-.3zM101.4 18.5v.1c.1 0 .5.1.6-.2-.2-.2-.3-.2-.6.1zM98.2 21.1c-.2-.1-.4-.2-.6.2.3.2.3.1.6-.2zM99.8 20.5v-.1c-.1.1-.4-.1-.5.2.2.2.4.1.5-.1zM101.6 21.5c-.2 0-.4-.2-.5.1.2.3.3.2.5-.1zM148.7 24.4c0 .2-.1.2-.1.3.1.3.6 0 .1-.3z"/>
<path d="M107.8 20.9c0-.1-.2-.1-.3-.1 0 .1-.2.3.2.4 0-.1.1-.2.1-.3zM106.5 18.8c-.2 0-.4.1-.5.1v.1c.3.1.3.1.5-.2zM134.2 22.5c-.1.1-.2.1-.3.2 0 0 0 .1.1.1s.2-.1.3-.1c0-.1 0-.2-.1-.2zM128.5 18.5c0-.1.2-.1-.1-.3.1.1-.2.1.1.3zM106.9 23.4s0-.1-.2 0c.1 0 .1.2.2 0zM102.3 19.9c.1 0 .1-.1 0 0 .1-.1.1-.1 0 0 0-.1 0 0 0 0zM94.9 24.6v.1-.1zM156.4 25.4c-.5-.1-1.1 0-1.6-.4-5.7-3.3-1.2 2.2 1.6.9.9.1 1-.7 0-.5zM107.2 22.8c-.2-.2-.1-.2-.8-.1-.2 0-.8-.3-.7.3-.3.1-.2.8.3.3.1 0 .2-.1.3-.1.2-.1.9.1.9-.4z"/>
</g>
</g>
<g>
<path d="M7 60.6C7 74 18 85 31.5 85S56 74 56 60.6 45 36.3 31.5 36.3 7 47.2 7 60.6zm24.5-21.9c12.1 0 22 9.8 22 21.9s-9.9 21.9-22 21.9-22-9.8-22-21.9 9.9-21.9 22-21.9z"/>
<path d="M48.7 84c-5 3.6-10.9 5.5-17.1 5.5-16.1 0-29.1-13-29.1-29C2.5 49 9.3 38.6 19.9 34c.6-.3.9-1 .6-1.6-.3-.6-1-.9-1.6-.6C7.4 36.8 0 48.2 0 60.6c0 16 12.1 29.3 27.7 31.2v6.1h-5.8c-2.4 0-4.4 2-4.4 4.4 0 2.4 2 4.4 4.4 4.4H36c2.4 0 4.4-2 4.4-4.4 0-2.4-2-4.4-4.4-4.4h-5.8V92h1.4c6.7 0 13.2-2.1 18.6-6 .6-.4.7-1.2.3-1.7-.4-.5-1.2-.7-1.8-.3zM38 102.3c0 1.1-.9 1.9-1.9 1.9H21.9a1.9 1.9 0 110-3.8H36c1.1 0 2 .9 2 1.9z"/>
</g>
</g>
</svg>

View File

@@ -1,38 +0,0 @@
<script>
import { stores } from '@sapper/app'
import { randomString } from 'utils/functions'
// Props
export let width = 56
export let color = '#000'
export let animated = false
const { page } = stores()
// Generate a random ID for the mask
const randomId = randomString(6, 'A')
</script>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1"
width={width} viewBox="0 0 56 56"
fill={color}
class="icon-svg icon-globe icon-globe--small" class:is-animated={animated}
>
<defs>
<clipPath id={randomId}>
<circle cx="28.1" cy="28" r="26.6"/>
</clipPath>
</defs>
<g clip-path="url({$page.path}#{randomId})">
<g class="anim anim-spinGlobeSmall">
<path d="M36.5 36.9c-2.3-1-2.2.3-3.5-2.2-1.1-.3-1.6-1-2.7-1.2-.4-.3-.3-.8-1.1-.8-.8-.3-1.7.6-2.4-.2-.4.2-.8.6-1.4.5-.3-.2-.9 0-1.3-.5.2-2.9-2.2.1-1.1-2.9l-.1-.1c-2.7 3.2-4.2-2.7-1.2-2.6.9 0 2.5-.6 2.7.9 0 .1.1.4.4.3h.1V28c.1 0-.2-1-.2-1.1-.1-.8 1.3-1.1 1.3-2-.4-.7-.2-.7.6-1.2.8-.6 1.1-1.5 2.3-1.3 0 .4 1 0 1.5-.4-.1-.4-.2-.3-1-.1-1-.8.2-1.5-1.8-.5.3-1.8 4-.6 3.5-2.4-1.2-.5-1.4-1.2-1.9-2.4-.4.1-1.1 1-1.4.2-1.2-2.1-2.9-1.6-2.2.8.1.6-.6.8-.4 1.6.3.8-.6 1-.9 0-.1-1.5-1.9-1-2.8-1.7-2-2.3 3.8-3.6 2.8-4.9-1.1-1.4-.4 2.5-2.7-.9-.5-.5 0-1-.5-1.5-1.4.6 1.3 3.7-1.3 2.5-.8.8-1.3.2-2.3.3-.4.3-.4.4-1.1.4-.8.1-.8-.4-1.5-.7-1.2-.3-2.9-.9-4.2-.1.1-.2.1-.3.2-.5-2.6 1.4-10.1-2.9-8.5 3-.1.4 0 .3-.6.6v.6c.5.3 1.6.3 1.9 1.1-.8.8.1 1 .5.1 3.9-2.9 6.8.2 8.9 3.4 0 2.2.5 4.1 2.1 5.9 0 1 1.8 2.7.5.1.9-1.4 2 4.3 4.8 3.8.1 0 .1 0 .3.1.8.4 1.8.9 2.6 1.7.4.5.9.8 1.5.7l.2.1c1 1.3-1.5 3.1.1 4.5.6 1.5 2.4 1.9 2.3 3.6-.3 1.6 0 3.4-1 4.9.4 1.7-1.2 4.5 1 5.2 1.8.3.4-1.1.6-2.2.1-.6.7-.7.5-1.5-.1-.9 1.6-1.3 2.3-1.8.8-1.3 1.8-2 2.6-3.5.8-1.1 2-.9 2.5-2.5-.1-1 2-2.1.5-3.1zM92.5 12.7c-8.2-1.2-4.2-2.7-10.2-.6-.4-.1-.6-.3-.9-.7-.8-2.2-3.8-.2-5.7-.7-.9-.5 1.9-.6.9-1.7-2.4-1.5-5.4.4-7.7 1.5-.8.4-.6 1.5-1.7 1.6-1.7-1.6-1.2-1.7-3 .2-1.1-.1-1.7.1-2.7.8-1.8.1-2.6.5-4.2 1.7-.6.5-1.9-.5-1.2-1.1.5-.2 1 .3 1.6-.3.5-2.4-4.6-1.7-5.7-.8-5.3 2.8-2.6 6.4-.8 5.3.4-.9-.1-2.1.9-2.8 1.7-1.6.6.3.5 1.1.6.9.3 1.4-.5 2.1-1.8 1.5-4.7.2-5.9 2.7.1 2-.8 1.2-1.8 1.9-1 1.5.6 2.6 2 1.8 1.5-1.2 1.9-3 4-1.3.1.1.1 0 .4.2l.2.2c-.9 1.2 1.3.4.2-.5-.7-.6-.8-2 .5-.5.4.4.4 1.4 1.1 1.3 1.2-.9.8.4 1.9.8 2-.2 2.3 1 .6 1.7-.8.3-1.6-.2-2.6-.3-.9.3-1.9.6-2.6-.3-.8-2.2-3.4-.7-5.2-.5-1 .4-.7 1.6-1.6 2.4-1.9 1.6-1.3 5.3 1.2 6.1 4.6.7 4.2-1.8 5.8 3.4-.4 4.7 1.9 11.8 5.5 4.8.5-.5.4-.9.8-1.6.6-.6 1.2-1.2.9-2.2-.9-3.2 2-3.8 2.3-5.9-1.9.4-4.6-3.7-4.2-5.1 1.5.5 2.5 4.5 3.8 4.1.8-.5 4.1-1.9 2.8-2.8-2.8.3-3.9-3.5-.9-1 7.1.8 4.3 4.5 6.7 5 .7-1.6 3.4-5.9 3.9-1.6 1.2-1.1 1 0 1.2 1.4-.3.5 1.2 2.1.9.8-.1 0-.2-.1-.3-.3-.4-1.9.7 0 1.6-.4 1.3-.7-.3-1.7-.3-2.7.2-.8.9-.2 1.6-.4 2-.6 2.8-2.3 2.3-4.2.5 0 .7-.9-.1-.5-.8-.3 0-1.6 1.1-.7.5.2.6.9 1.3 1.1-.2-1.9 1-2 2.5-3 .9-.7 1.2-1.9 1.1-3.1-.4-.5-1-.1-1.6-.5.5-.7 1.1-1.3 2.2-1.6 1.1-.4 2 0 3.2-.5.4-.2 2.9-1.5 2 .1-.6.7-2.6 2.1-1.2 3 1-.2 1.3-2.2 2.4-2.8 1.7-.2 5.5-1.8 4.1-3.6-.9-.6-2-.6-3.4-.5zm-35 11c-1-.3-2 .1-3.1-.3-.3-.6 1.1-2.6 1.5-1 .4-.2.4-.8 1.3-.8 0 .2-.1.4-.2.8.4.2.7.2 1.1.9-.3.2-.4.3-.6.4zm3.9.8c.1 1.4-1.5.5-1.3-.8-.8-.8-.5-1.7.7-2 .9.2-.2.9 0 1.3.9.5.6.4.6 1.5zM40.5 11.2c-.4-.6.2-1.1.3-1.9.2-.7 0-1.3.5-1.9.2-.6 1.7-.7 1.2-1.5-.7-.4-1.6.7-2.1-.3-2.4-2.5-6.6.1-9.6.1-1.7.3-2.8 1.5-4.2 2.4-.4.8.6.3 1.2.7.4.3.6.6 1.3.4 1.1-.4 3.4 1.1 2.4 2.4.9-.5 2.3.8.3.6.7.6.5.9.5 1.9.3 1 1.6 3.2 2.7 2.2.6-.6.3-1.4 1-2 1.2-.2 1.7-1.1 2.9-1.5.8 0 2.2-.4 1.6-1.6zM85.1 38.9c-.2.6-.6 2.4-1.6 1.3-.2-.7.2-1.5-.8-1.3-.7-.1-.6.4-1.1.9-.5.1-.9 0-1.3.4-3.6 2.8-3.1 1.1-2.3 5.4 1.3.2 3.2-1.4 4.6-.8.6.9 1.7.6 2 1.7.9.7 2.9.8 3.1-1 1.6-3-.4-3.3-1.5-5.3-.4-.4-.4-1-1.1-1.3zM21.1 7.4c.7.5 1.4.1 2.1.7-.1.1-.1.1-.4.3-.5 0-.3.7.3.5 2.5.3 1.8.1 3-1.7 1.2-.7 5.5-2.5 1.5-2.4-1.6.3-5.8.2-5.7 1.5-.3.3-2 .3-.8 1.1z"/>
<path d="M28.8 14.2c1.1.6 1.2-.8-.4-1.2-.5-.9-.7-1-1.8-1.6-.6-.5-1.2-.2-2-.4-.3-.5-.4-.6-1.1-.3-.1-1-1.7-.1-1.1.8 2 1.4 3.3-.1 5.1 2.8v.2c-.3-.2-.6-.2-.9-.1-1.7.1-1.5.9 0 .5.6.2 1.1 1.3 2 1 0 0-.1-.4-.3-.6l.6-.3c-.2-.1-.9-1.3-.1-.8zM15.5 12.3v.3c.3.5 1.1.1 1.9.2.6 0 .9.5 1.6 0-.6-.3-.8-2.7-1.5-2.1-.3.2-.7.6-1.3.3-.5-.2-1-.1-1.4.5 0 .3.9.3.7.8zM84.7 38.1c.5.1 1.1-.4 1.7.1.9 1 1.5.4.4-.3-.5-1-2.1-1.9-3.3-1.5-.2-.1-.2-.2-.8-.5h-.1c-1.2-.1 0 .6.9 1.1.7.1.5 1.2 1.2 1.1zM78 33.8c-.4.6-1.6 1.1-1.9 1.8 1.1 2.1 2.8.6 2.3-1.5 0-.3-.1-.4-.4-.3zM59.4 39c-.6.1-.6.9-.8 1.5.5 4.5 2.5-.3.8-1.5zM62.2 12c-.2-1.3.4-1.8 1.5-2.3.1 0 .1 0 .3-.3 0 0 .1 0 .1-.1 1.8 0 1.9-1.5-.5-.2-1.2.2-3.7 2.5-1.4 2.9zM13.4 11.6c1.3-.5 2.7-1.7-.2-1.6-.6.3-1 1.5.2 1.6zM44.8 18.4c.2.1.4.1.6.2.3-.1 1-.2 1-.8-.2-.9-3-.4-1.6.6zM42.9 14.4c-.2-.4-.4-.1-1.1 0-.5 0-.8-.3-1.4 0 .4.9 2.5 1.4 2.5 0zM45.4 19.5c-.1.4.3.5.8.9 1.6-.6.4-1.9-.8-.9zM32.3 21.5c0-.6-.7-1.3-1.1-1.2-1 .1.5 2.2 1.1 1.2zM93.9 47.9c0-.1-.1-.2-.1-.2-2.8.8-1.6 3.3.1.2zM53.1 7.6c.9 0 1.1-1.1-.2-1-.6.2-.7.9.2 1zM50.4 8.2c.9.1 1-1.1-.2-1-.5.2-.6.9.2 1zM94.4 46c-.2-.2-.7-.1-.3.5-.1.2-.1.5 0 .7-.2.3.4 1.1.5.1.2-.2.4-.3.6-.5l-.6-.3c0-.2-.1-.4-.2-.5zM44.3 19.9c.7-.1.7-.5 0-.9-.4.2-1 .7 0 .9zM21.6 9.7c-.3-.1-.6.5 0 .5.1.1.2.1.4.1.3 0 .5-.2.5-.4 0-.3-.5-.4-.9-.2zM85.9 48c.3.6 1.2.1.4-.6-.2.3-.3.5-.4.6zM49.3 24.6c.2-.2.3-.3.4-.5-.1-.2-1-.2-.4.5zM52.6 8.3c-.3-.1-.5-.4-.8.1.2.1.4.4.8-.1zM21 8.4c-.2-.2-.3-.3-.7-.1v.2c.1-.1.4.2.7-.1zM15.4 10c.1 0 .2-.1.4-.2-.2-.2-.3-.4-.7-.1.1.2.2.3.3.3zM17.8 9.9v-.2c-.1.1-.4-.2-.6.1.2.2.4.2.6.1zM18.9 11.8c.1 0 .2-.1.4-.2-.2-.1-.3-.4-.6 0l.2.2zM68.5 33.5c0 .3.7.2.2-.3-.1.1-.2.2-.2.3z"/>
<path d="M26.2 13.4c0-.1-.2-.1-.2-.2-.1.1-.4.2 0 .5.1-.1.2-.2.2-.3zM25 10.5v.1c.3.3.3.3.6 0-.2 0-.4-.1-.6-.1zM54 25.4h-.4v.2h.4v-.2zM49.4 18.6c0 .1-.3 0-.1.3 0-.1.3-.1.1-.3zM24.1 15.7s0 .2.2 0c-.1.1 0-.1-.2 0zM20.7 10.1c0 .1.1.1.1 0 0 0 .1-.1-.1 0zM10.9 12.4zM76.7 37.3c-.5-.3-1.2-.5-1.5-1.1-4.8-5.7-2.1 1.9 1.4 1.6.3 0 .6.4.9 0 0-.3-.6-.5-.8-.5zM24.9 15.2c-.1-.3-.1-.3-.8-.4-.2-.1-.7-.7-.8 0-.3 0-.6.8.2.5h.4c0-.1.7.4 1-.1zM133.5 36.9c-2.3-1-2.2.3-3.5-2.2-1.1-.3-1.6-1-2.7-1.2-.4-.3-.3-.8-1.1-.8-.8-.3-1.7.6-2.4-.2-.4.2-.8.6-1.4.5-.3-.2-.9 0-1.3-.5.2-2.9-2.2.1-1.1-2.9l-.1-.1c-2.7 3.2-4.2-2.7-1.2-2.6.9 0 2.5-.6 2.7.9 0 .1.1.4.4.3h.1V28c.1 0-.2-1-.2-1.1-.1-.8 1.3-1.1 1.3-2-.4-.7-.2-.7.6-1.2.8-.6 1.1-1.5 2.3-1.3 0 .4 1 0 1.5-.4-.1-.4-.2-.3-1-.1-1-.8.2-1.5-1.8-.5.3-1.8 4-.6 3.5-2.4-1.2-.5-1.4-1.2-1.9-2.4-.4.1-1.1 1-1.4.2-1.2-2.1-2.9-1.6-2.2.8.1.6-.6.8-.4 1.6.3.8-.6 1-.9 0-.1-1.5-1.9-1-2.8-1.7-2-2.3 3.8-3.6 2.8-4.9-1.1-1.4-.4 2.5-2.7-.9-.5-.5 0-1-.5-1.5-1.4.6 1.3 3.7-1.3 2.5-.8.8-1.3.2-2.3.3-.4.3-.4.4-1.1.4-.8.1-.8-.4-1.5-.7-1.2-.3-2.9-.9-4.2-.1.1-.2.1-.3.2-.5-2.6 1.4-10.1-2.9-8.5 3-.1.4 0 .3-.6.6v.6c.5.3 1.6.3 1.9 1.1-.8.8.1 1 .5.1 3.9-2.9 6.8.2 8.9 3.4 0 2.2.5 4.1 2.1 5.9 0 1 1.8 2.7.5.1.9-1.4 2 4.3 4.8 3.8.1 0 .1 0 .3.1.8.4 1.8.9 2.6 1.7.4.5.9.8 1.5.7l.2.1c1 1.3-1.5 3.1.1 4.5.6 1.5 2.4 1.9 2.3 3.6-.3 1.6 0 3.4-1 4.9.4 1.7-1.2 4.5 1 5.2 1.8.3.4-1.1.6-2.2.1-.6.7-.7.5-1.5-.1-.9 1.6-1.3 2.3-1.8.8-1.3 1.8-2 2.6-3.5.8-1.1 2-.9 2.5-2.5-.2-1 2-2.1.5-3.1zM193 13.4c-1-.8-2.1-.8-3.5-.7-8.2-1.2-4.2-2.7-10.2-.6-.4-.1-.6-.3-.9-.7-.8-2.2-3.8-.2-5.7-.7-.9-.5 1.9-.6.9-1.7-2.4-1.5-5.4.4-7.7 1.5-.8.4-.6 1.5-1.7 1.6-1.7-1.6-1.2-1.7-3 .2-1.1-.1-1.7.1-2.7.8-1.8.1-2.6.5-4.2 1.7-.6.5-1.9-.5-1.2-1.1.5-.2 1 .3 1.6-.3.5-2.4-4.6-1.7-5.7-.8-5.3 2.8-2.6 6.4-.8 5.3.4-.9-.1-2.1.9-2.8 1.7-1.6.6.3.5 1.1.6.9.3 1.4-.5 2.1-1.8 1.5-4.7.2-5.9 2.7.1 2-.8 1.2-1.8 1.9-1 1.5.6 2.6 2 1.8 1.5-1.2 1.9-3 4-1.3.1.1.1 0 .4.2l.2.2c-.9 1.2 1.3.4.2-.5-.7-.6-.8-2 .5-.5.4.4.4 1.4 1.1 1.3 1.2-.9.8.4 1.9.8 2-.2 2.3 1 .6 1.7-.8.3-1.6-.2-2.6-.3-.9.3-1.9.6-2.6-.3-.8-2.2-3.4-.7-5.2-.5-1 .4-.7 1.6-1.6 2.4-1.9 1.6-1.3 5.3 1.2 6.1 4.6.7 4.2-1.8 5.8 3.4-.4 4.7 1.9 11.8 5.5 4.8.5-.5.4-.9.8-1.6.6-.6 1.2-1.2.9-2.2-.9-3.2 2-3.8 2.3-5.9-1.9.4-4.6-3.7-4.2-5.1 1.5.5 2.5 4.5 3.8 4.1.8-.5 4.1-1.9 2.8-2.8-2.8.3-3.9-3.5-.9-1 7.1.8 4.3 4.5 6.7 5 .7-1.6 3.4-5.9 3.9-1.6 1.2-1.1 1 0 1.2 1.4-.3.5 1.2 2.1.9.8-.1 0-.2-.1-.3-.3-.4-1.9.7 0 1.6-.4 1.3-.7-.3-1.7-.3-2.7.2-.8.9-.2 1.6-.4 2-.6 2.8-2.3 2.3-4.2.5 0 .7-.9-.1-.5-.8-.3 0-1.6 1.1-.7.5.2.6.9 1.3 1.1-.2-1.9 1-2 2.5-3 .9-.7 1.2-1.9 1.1-3.1-.4-.5-1-.1-1.6-.5.5-.7 1.1-1.3 2.2-1.6 1.1-.4 2 0 3.2-.5.4-.2 2.9-1.5 2 .1-.6.7-2.6 2.1-1.2 3 1-.2 1.3-2.2 2.4-2.8 1.7.1 5.5-1.5 4.2-3.4zm-38.6 10.3c-1-.3-2 .1-3.1-.3-.3-.6 1.1-2.6 1.5-1 .4-.2.4-.8 1.3-.8 0 .2-.1.4-.2.8.4.2.7.2 1.1.9-.2.2-.4.3-.6.4zm3.9.8c.1 1.4-1.5.5-1.3-.8-.8-.8-.5-1.7.7-2 .9.2-.2.9 0 1.3 1 .5.6.4.6 1.5zM137.5 11.2c-.4-.6.2-1.1.3-1.9.1-.7-.1-1.3.4-1.9.2-.6 1.7-.7 1.2-1.5-.7-.4-1.6.7-2.1-.3-2.4-2.5-6.6.1-9.6.1-1.7.3-2.7 1.5-4.1 2.4-.4.8.6.3 1.2.7.4.3.6.6 1.3.4 1.1-.4 3.4 1.1 2.4 2.4.9-.5 2.3.8.3.6.7.6.5.9.5 1.9.3 1 1.6 3.2 2.7 2.2.6-.6.3-1.4 1-2 1.2-.2 1.7-1.1 2.9-1.5.7 0 2.2-.4 1.6-1.6zM182.1 38.9c-.2.6-.6 2.4-1.6 1.3-.2-.7.2-1.5-.8-1.3-.7-.1-.6.4-1.1.9-.5.1-.9 0-1.3.4-3.6 2.8-3.1 1.1-2.3 5.4 1.3.2 3.2-1.4 4.6-.8.6.9 1.7.6 2 1.7.9.7 2.9.8 3.1-1 1.6-3-.4-3.3-1.5-5.3-.5-.4-.5-1-1.1-1.3zM118.1 7.4c.7.5 1.4.1 2.1.7-.1.1-.1.1-.4.3-.5 0-.3.7.3.5 2.5.3 1.8.1 3-1.7 1.2-.7 5.5-2.5 1.5-2.4-1.6.3-5.9.1-5.8 1.5-.3.3-1.9.3-.7 1.1z"/>
<path d="M125.8 14.2c1.1.6 1.2-.8-.4-1.2-.5-.9-.7-1-1.8-1.6-.6-.5-1.2-.2-2-.4-.3-.5-.4-.6-1.1-.3-.1-1-1.7-.1-1.1.8 2 1.4 3.3-.1 5.1 2.8v.2c-.3-.2-.6-.2-.9-.1-1.7.1-1.5.9 0 .5.6.2 1.1 1.3 2 1 0 0-.1-.4-.3-.6l.6-.3c-.2-.1-1-1.3-.1-.8zM112.5 12.3v.3c.3.5 1.1.1 1.9.2.6 0 .9.5 1.6 0-.6-.3-.8-2.7-1.5-2.1-.3.2-.7.6-1.3.3-.5-.2-1-.1-1.4.5 0 .3.9.3.7.8zM181.7 38.1c.5.1 1.1-.4 1.7.1.9 1 1.5.4.4-.3-.5-1-2.1-1.9-3.3-1.5-.2-.1-.2-.2-.8-.5h-.1c-1.2-.1 0 .6.9 1.1.6.1.4 1.2 1.2 1.1zM175 33.8c-.4.6-1.6 1.1-1.9 1.8 1.1 2.1 2.8.6 2.3-1.5 0-.3-.2-.4-.4-.3zM156.4 39c-.6.1-.6.9-.8 1.5.5 4.5 2.5-.3.8-1.5zM159.2 12c-.2-1.3.4-1.8 1.5-2.3.1 0 .1 0 .3-.3 0 0 .1 0 .1-.1 1.8 0 1.9-1.5-.5-.2-1.2.2-3.8 2.5-1.4 2.9zM110.4 11.6c1.3-.5 2.7-1.7-.2-1.6-.7.3-1 1.5.2 1.6zM141.7 18.4c.2.1.4.1.6.2.3-.1 1-.2 1-.8-.2-.9-3-.4-1.6.6zM139.9 14.4c-.2-.4-.4-.1-1.1 0-.5 0-.8-.3-1.4 0 .3.9 2.4 1.4 2.5 0zM142.4 19.5c-.1.4.3.5.8.9 1.6-.6.3-1.9-.8-.9zM129.3 21.5c0-.6-.7-1.3-1.1-1.2-1 .1.4 2.2 1.1 1.2zM190.8 47.9c0-.1-.1-.2-.1-.2-2.7.8-1.6 3.3.1.2zM150 7.6c.9 0 1.1-1.1-.2-1-.6.2-.7.9.2 1zM147.4 8.2c.9.1 1-1.1-.2-1-.6.2-.6.9.2 1zM191.4 46c-.2-.2-.7-.1-.3.5-.1.2-.1.5 0 .7-.2.3.4 1.1.5.1.2-.2.4-.3.6-.5l-.6-.3c-.1-.2-.1-.4-.2-.5zM141.3 19.9c.7-.1.7-.5 0-.9-.4.2-1.1.7 0 .9zM118.6 9.7c-.3-.1-.6.5 0 .5.1.1.2.1.4.1.3 0 .5-.2.5-.4-.1-.3-.5-.4-.9-.2zM182.9 48c.3.6 1.2.1.4-.6-.2.3-.4.5-.4.6zM146.3 24.6c.2-.2.3-.3.4-.5-.2-.2-1.1-.2-.4.5zM149.5 8.3c-.3-.1-.5-.4-.8.1.2.1.4.4.8-.1zM118 8.4c-.2-.2-.3-.3-.7-.1v.2c.1-.1.4.2.7-.1zM112.3 10c.1 0 .2-.1.4-.2-.2-.2-.3-.4-.7-.1.1.2.2.3.3.3zM114.7 9.9v-.2c-.1.1-.4-.2-.6.1.2.2.4.2.6.1zM115.9 11.8c.1 0 .2-.1.4-.2-.2-.1-.3-.4-.6 0 0 .1.1.2.2.2zM165.5 33.5c0 .3.7.2.2-.3-.1.1-.2.2-.2.3z"/>
<path d="M123.1 13.4c0-.1-.2-.1-.2-.2-.1.1-.4.2 0 .5.1-.1.2-.2.2-.3zM121.9 10.5v.1c.3.3.3.3.6 0-.2 0-.4-.1-.6-.1zM150.9 25.4h-.4v.2h.4c.1 0 .1-.1 0-.2zM146.4 18.6c0 .1-.3 0-.1.3 0-.1.3-.1.1-.3zM121 15.7s0 .2.2 0c0 .1 0-.1-.2 0zM117.7 10.1c0 .1.1.1.1 0 0 0 0-.1-.1 0zM107.9 12.4h-.1.1zM173.6 37.3c-.5-.3-1.2-.5-1.5-1.1-4.8-5.7-2.1 1.9 1.4 1.6.3 0 .6.4.9 0 .1-.3-.5-.5-.8-.5zM121.8 15.2c-.1-.3-.1-.3-.8-.4-.2-.1-.7-.7-.8 0-.3 0-.6.8.2.5h.4c.1-.1.8.4 1-.1z"/>
</g>
</g>
<g>
<path fill="none" d="M28 2.8C14.1 2.8 2.8 14.1 2.8 28S14.1 53.2 28 53.2 53.2 41.9 53.2 28 41.9 2.8 28 2.8z"/>
<path d="M28 0C12.6 0 0 12.6 0 28s12.6 28 28 28 28-12.6 28-28S43.4 0 28 0zm0 53.2C14.1 53.2 2.8 41.9 2.8 28S14.1 2.8 28 2.8 53.2 14.1 53.2 28 41.9 53.2 28 53.2z"/>
</g>
</svg>

View File

@@ -1,9 +0,0 @@
<script>
export let width = 18
export let color = '#fff'
export let hidden = undefined
</script>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 19 18" {width} fill={color} class={$$props.class} aria-hidden={hidden}>
<path fill-rule="evenodd" d="M8.36 0a7.76 7.76 0 016.02 12.63l3.99 3.99a.81.81 0 11-1.15 1.14l-3.99-3.99A7.76 7.76 0 118.35 0zm0 1.62a6.14 6.14 0 10.01 12.28 6.14 6.14 0 00-.01-12.28zm2.38 5.32a.81.81 0 110 1.62H5.98a.81.81 0 110-1.62z"/>
</svg>

View File

@@ -1,12 +0,0 @@
<script>
export let href = '#'
export let text = ''
export let noScroll = undefined
</script>
<a href={href} class="link-change" sapper-noscroll={noScroll}>
{text}
<span class="icon">
<slot></slot>
</span>
</a>

View File

@@ -1,15 +0,0 @@
<script>
export let href = '/'
export let text = ''
export let target = null
export let rel = null
export let active = false
export let noScroll = undefined
</script>
<a {href} {target} {rel} class="link-translate" class:is-active={!!active} sapper-noscroll={noScroll}>
<slot />
<div class="text" data-text={text}>
<span>{text}</span>
</div>
</a>

View File

@@ -1,42 +0,0 @@
<script>
import { onMount } from 'svelte'
import { charsToSpan } from 'utils/functions'
// Animations
import { animateIn } from 'animations/TitleSite'
// Props and variables
export let init = false
let mounted = false
let scope
/*
** Run code when mounted
*/
onMount(() => {
animateIn(scope, init)
mounted = true
})
</script>
<div class="title-location title-location--inline" role="heading" aria-level="1"
bind:this={scope}
style="opacity: {mounted ? 1 : 0}"
>
<div aria-label="Houses">
<div class="anim-mask">
{@html charsToSpan('Houses')}
</div>
</div>
<em class="anim-mask">
<span>of</span>
<span>the</span>
</em>
<div aria-label="World">
<div class="anim-mask">
{@html charsToSpan('World')}
</div>
</div>
</div>

View File

@@ -1,89 +0,0 @@
<script>
import { onMount } from 'svelte'
// Variables
let toggleEl
let grid
let pill
let layoutSetting
// Elements
const layoutGridClass = 'photos--grid'
const layoutListClass = 'photos--list'
// Change active pill
const toggleAnimate = (elem, pill, toggle) => {
pill.style.width = Math.round(elem.getBoundingClientRect().width) + 'px'
pill.style.left = Math.round(elem.getBoundingClientRect().left - toggleEl.getBoundingClientRect().left) + 'px'
}
// Toggle layout
const toggleLayout = event => {
const clicked = event.currentTarget
const type = clicked.dataset.layout
// Change the layout
switch (type) {
case 'grid':
grid.classList.add(layoutGridClass)
grid.classList.remove(layoutListClass)
break
case 'list':
grid.classList.add(layoutListClass)
grid.classList.remove(layoutGridClass)
break
default: break
}
// Animate the active pill
toggleAnimate(clicked, pill, toggleEl)
// Add/Remove active classes
toggleEl.querySelectorAll('button').forEach(button => button.classList.remove('active'))
clicked.classList.add('active')
// Remember this setting
localStorage.setItem('photosLayout', type)
}
/*
** Run code when mounted
*/
onMount(() => {
grid = document.querySelector('.photos')
// Get layout setting from storage
layoutSetting = (localStorage.getItem('photosLayout')) ? localStorage.getItem('photosLayout') : 'list'
// Init of first option
toggleAnimate(toggleEl.querySelector(`[data-layout="${layoutSetting}"]`), pill, toggleEl)
})
</script>
<div class="toggle" role="switch" aria-checked="true" bind:this={toggleEl}>
<button data-layout="list" class:active={layoutSetting === 'list'} on:click={toggleLayout}>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="19" height="17" viewBox="0 0 19 17">
<g class="anim">
<rect width="15" height="3"/>
<rect width="15" height="3" x="4" y="7"/>
<rect width="15" height="3" y="14"/>
</g>
</svg>
<span>List</span>
</button>
<button data-layout="grid" class:active={layoutSetting === 'grid'} on:click={toggleLayout}>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="18" height="20" viewBox="0 0 18 20">
<g class="anim">
<rect width="7" height="7"/>
<rect width="7" height="7" x="11" y="2"/>
<rect width="7" height="7" y="11"/>
<rect width="7" height="7" x="11" y="13"/>
</g>
</svg>
<span>Grid</span>
</button>
<div class="pill" aria-hidden="true" role="presentation" bind:this={pill}></div>
</div>

View File

@@ -1,6 +0,0 @@
import * as sapper from '@sapper/app'
// Start Sapper
sapper.start({
target: document.body
})

View File

@@ -0,0 +1,37 @@
<script lang="ts">
// @ts-nocheck
import { page } from '$app/stores'
import { sendPage } from '$utils/analytics'
export let appKey: any
export let url: any
export let enabled: boolean = process.env.NODE_ENV !== 'development'
let loaded = false
const handleLoad = () => {
// Init Countly
if (Countly) {
Countly.init({
app_key: appKey,
url,
})
Countly.track_sessions()
Countly.track_pageview()
}
loaded = true
}
// Send page to Analytics when changing path
$: enabled && $page.url.pathname && loaded && sendPage()
</script>
<svelte:head>
{#if enabled}
<script defer src="https://cdn.jsdelivr.net/npm/countly-sdk-web@latest/lib/countly.min.js" on:load={handleLoad} />
{/if}
</svelte:head>
<noscript>
<img src="{url}/pixel.png?app_key={appKey}&begin_session=1" alt="countly" width="0" height="0" />
</noscript>

View File

@@ -0,0 +1,32 @@
<script lang="ts">
import { getContext } from 'svelte'
import { getAssetUrlKey } from '$utils/api'
const { settings }: any = getContext('global')
export let title: string
export let description: string = undefined
export let image: string = getAssetUrlKey(settings.seo_image.id, 'share-image')
export let url: string = undefined
export let type: string = 'website'
export let card: string = 'summary_large_image'
export let creator: string = undefined
</script>
<svelte:head>
<title>{title}</title>
<meta name="description" content={description}>
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content={type} />
{#if image}
<meta property="og:image" content={image} />
{/if}
{#if url}
<meta property="og:url" content={url} />
{/if}
<meta property="twitter:card" content={card} />
<meta property="twitter:creator" content={creator} />
</svelte:head>

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import { page } from '$app/stores'
import { fade } from 'svelte/transition'
import { scrollToTop } from '$utils/functions'
import { DURATION } from '$utils/contants'
export let name: string
$: doNotScroll = !$page.url.searchParams.get('country') && !$page.url.pathname.includes('/shop/')
</script>
<main class={name}
in:fade={{ duration: DURATION.PAGE_IN, delay: DURATION.PAGE_DELAY }}
out:fade={{ duration: DURATION.PAGE_OUT }}
on:outroend={() => doNotScroll && scrollToTop()}
>
<slot />
</main>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,37 @@
<script lang="ts">
import { browser } from '$app/environment'
import { onMount } from 'svelte'
import Lenis from '@studio-freight/lenis'
import { smoothScroll } from '$utils/stores'
let smoothScrollRAF = 0
// Setup smooth scroll
if (browser) {
$smoothScroll = new Lenis({
duration: 1.2,
easing: (t: number) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t)), // https://easings.net/
smooth: true,
direction: 'vertical',
})
}
// Lenis RAF
const update = (time: number) => {
$smoothScroll.raf(time)
smoothScrollRAF = requestAnimationFrame(update)
}
onMount(() => {
// Enable smooth scroll
requestAnimationFrame(update)
// Destroy
return () => {
cancelAnimationFrame(smoothScrollRAF)
$smoothScroll.destroy()
}
})
</script>

View File

@@ -0,0 +1,42 @@
<script lang="ts">
import { splitText } from '$utils/functions'
export let text: string
export let mode: string = undefined
export let clone: boolean = false
$: split = splitText(text, mode)
const classes = ['text-split', $$props.class].join(' ').trim()
</script>
{#if clone}
{#if mode && mode === 'words'}
<span class={classes}>
{#each Array(2) as _, index}
<span class="text-split__line" aria-hidden={index === 1}>
{#each split as word, i}
<span class="word" style:--i-w={i}>{word}</span>{#if word.includes('\n')}<br>{/if}
<!-- svelte-ignore empty-block -->
{#if i < split.length - 1}{/if}
{/each}
</span>
{/each}
</span>
{:else}
<span class={classes}>
{#each Array(2) as _, index}
<span class="text-split__line" aria-hidden={index === 1}>
{text}
</span>
{/each}
</span>
{/if}
{:else}
<span class={classes}>
{#each split as char, i}
<span class="char" style:--i-c={i}>{char}</span>
{/each}
</span>
{/if}

View File

@@ -0,0 +1,41 @@
<script lang="ts">
import Image from './Image.svelte'
export let id: string
export let alt: string
export let disabled: boolean = false
let hovering: boolean = false
let timer: ReturnType<typeof setTimeout> | number = null
$: classes = [
hovering ? 'is-hovered' : undefined,
disabled ? 'is-disabled' : undefined,
$$props.class
].join(' ').trim()
// Hovering functions
const handleMouseEnter = () => {
clearTimeout(timer)
hovering = true
}
const handleMouseLeave = () => {
// Reset hovering to false after a delay
timer = setTimeout(() => hovering = false, 800)
}
</script>
<figure class={classes}
on:mouseenter={handleMouseEnter}
on:mouseleave={handleMouseLeave}
>
<Image
{id}
sizeKey="photo-list"
sizes={{
small: { width: 250 },
}}
ratio={1.5}
{alt}
/>
</figure>

View File

@@ -0,0 +1,12 @@
<style lang="scss">
@import "../../style/atoms/badge";
</style>
<script lang="ts">
export let text: string
export let size: string = 'small'
</script>
<div class="badge badge--{size}">
<span>{text}</span>
</div>

View File

@@ -0,0 +1,23 @@
<style lang="scss">
@import "../../style/atoms/box-cta";
</style>
<script lang="ts">
import Icon from '$components/atoms/Icon.svelte'
export let icon: string
export let alt: string
export let label: string
export let url: string
</script>
<a href={url} class="box-cta"
data-sveltekit-prefetch={url.includes('http') ? true : undefined}
>
<div class="icon">
<Icon icon={icon} label={alt} />
</div>
<span class="text-label">
{label}
</span>
</a>

View File

@@ -0,0 +1,61 @@
<style lang="scss">
@import "../../style/atoms/button";
</style>
<script lang="ts">
import SplitText from '$components/SplitText.svelte'
export let tag: string = 'a'
export let text: string
export let url: string = undefined
export let color: string = undefined
export let size: string = undefined
export let effect: string = 'link-3d'
export let disabled: boolean = undefined
export let slotPosition: string = 'before'
const className = 'button'
const classes = [
className,
effect ? effect : undefined,
...[color, size].map(variant => variant && `${className}--${variant}`),
Object.keys($$slots).length !== 0 ? `has-icon-${slotPosition}` : undefined,
$$props.class
].join(' ').trim()
// Define external links
$: isExternal = /^(http|https):\/\//i.test(url)
$: isProtocol = /^(mailto|tel):/i.test(url)
$: rel = isExternal ? 'external noopener' : null
$: target = isExternal ? '_blank' : null
</script>
{#if tag === 'button'}
<button class={classes} tabindex="0" {disabled} on:click>
{#if slotPosition === 'before'}
<slot />
{/if}
<SplitText {text} clone={true} />
{#if slotPosition === 'after'}
<slot />
{/if}
</button>
{:else if tag === 'a'}
<a
href={url} class={classes}
{target} {rel}
data-sveltekit-prefetch={url && (isExternal || isProtocol) ? 'off' : ''}
data-sveltekit-noscroll={isExternal || isProtocol ? 'off' : ''}
{disabled}
tabindex="0"
on:click
>
{#if slotPosition === 'before'}
<slot />
{/if}
<SplitText {text} clone={true} />
{#if slotPosition === 'after'}
<slot />
{/if}
</a>
{/if}

View File

@@ -0,0 +1,25 @@
<style lang="scss">
@import "../../style/atoms/button-cart";
</style>
<script lang="ts">
import { scale } from 'svelte/transition'
import { quartOut } from 'svelte/easing'
import { cartOpen, cartAmount } from '$utils/stores/shop'
// Components
import Icon from '$components/atoms/Icon.svelte'
import ButtonCircle from '$components/atoms/ButtonCircle.svelte'
const openCart = () => {
$cartOpen = true
}
</script>
<div class="button-cart">
<ButtonCircle color="purple" on:click={openCart}>
<Icon icon="bag" label="Cart icon" />
{#if $cartAmount > 0}
<span class="quantity" transition:scale={{ start: 0.6, duration: 400, easing: quartOut }}>{$cartAmount}</span>
{/if}
</ButtonCircle>
</div>

View File

@@ -0,0 +1,44 @@
<style lang="scss">
@import "../../style/atoms/button-circle";
</style>
<script lang="ts">
export let tag: string = 'button'
export let url: string = undefined
export let color: string = undefined
export let size: string = undefined
export let type: string = undefined
export let clone: boolean = false
export let disabled: boolean = undefined
export let label: string = undefined
const className = 'button-circle'
const classes = [
className,
...[color, size].map(variant => variant && `${className}--${variant}`),
clone ? 'has-clone' : null,
$$props.class
].join(' ').trim()
</script>
{#if tag === 'a'}
<a href={url} class={classes} tabindex="0" aria-label={label} on:click>
{#if clone}
{#each Array(2) as _}
<slot />
{/each}
{:else}
<slot />
{/if}
</a>
{:else}
<button {type} class={classes} disabled={disabled} tabindex="0" aria-label={label} on:click>
{#if clone}
{#each Array(2) as _}
<slot />
{/each}
{:else}
<slot />
{/if}
</button>
{/if}

View File

@@ -0,0 +1,15 @@
<style lang="scss">
@import "../../style/atoms/discover";
</style>
<script lang="ts">
import { getContext } from 'svelte'
const { count }: any = getContext('global')
</script>
<p class="discover">
Discover <strong>{count.photos} homes</strong><br>
from <strong>{count.locations} places</strong>
in <strong>{count.countries} countries</strong>
</p>

View File

@@ -0,0 +1,10 @@
<script lang="ts">
export let icon: string
export let label: string = undefined
const classes = [$$props.class].join(' ').trim()
</script>
<svg class={classes} aria-label={label} width="32" height="32">
<use xlink:href="#icon-{icon}" />
</svg>

View File

@@ -0,0 +1,15 @@
<style lang="scss">
@import "../../style/atoms/arrow";
</style>
<script lang="ts">
export let color: string = undefined
export let flip: boolean = false
</script>
<svg width="12" height="14"
class="arrow arrow--{color}"
class:arrow--flip={flip}
>
<use xlink:href="#arrow" />
</svg>

View File

@@ -0,0 +1,44 @@
<style lang="scss">
svg {
width: 24px;
height: 24px;
color: $color-gray;
}
</style>
<script lang="ts">
export let animate: boolean = false
const classes = ['icon-earth', $$props.class].join(' ').trim()
</script>
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"
class={classes}
>
{#if animate}
<defs>
<mask id="circle" style="mask-type: alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="48" height="48">
<circle cx="24" cy="24" r="23" fill="currentColor" stroke="currentColor" stroke-width="2"/>
</mask>
</defs>
<g mask="url(#circle)" fill="currentColor">
<g class:anim-earth={animate}>
<path d="M23.56 26.87a.63.63 0 0 1-.61-.64c-.06-1.26-1.06.38-2.42-1.48a.65.65 0 0 0-.56-.28c-7.11.3-2.58-.64-5.48-1.45a.65.65 0 0 1-.46-.6c0-1.73-2.2-.06-.78-2.55a.6.6 0 0 1 .33-.28c1.91-.7 2.12-.44 2.3-.07a.63.63 0 0 0 1.2-.11c.37-1.16 2.28-1.98 3.3-2.6a.64.64 0 0 0 .3-.68l-.1-.4a.62.62 0 0 1 0-.21.63.63 0 0 1 .71-.54l2.9.3h.02a.63.63 0 0 0 .69-.59c.05-.93.28-2.27-.56-2.23a.67.67 0 0 1-.6-.37l-.39-.78a.63.63 0 0 0-.67-.36c-.76.14-2.04.3-2.56.75a.65.65 0 0 1-.69.11c-2.41-1.04.7-.32 1.49-3.05l.02-.14a.63.63 0 0 0-.58-.68c-1.7-.15-4.22-.95-5.15.6a.65.65 0 0 1-.63.3L5.21 7.67a.62.62 0 0 0-.47.12.63.63 0 0 0-.23.67l.18.78a.66.66 0 0 1-.03.39.63.63 0 0 1-.58.39c-1.15-.04-1.11.2-1.06 1.64a.63.63 0 0 0 .32.52c.52.28 1.1.42 1.69.4v.04a.63.63 0 0 0 .63-.55c.2-1.27 1.77-.53 2.85-.53a.63.63 0 0 1 .5.25 3.28 3.28 0 0 1 .6 2.47.65.65 0 0 1-.57.56h-.16a.63.63 0 0 0-.56.58l-.33 3.66a.63.63 0 0 0 .18.52c.82.8 1.9 1.27 1.49 2.37a.61.61 0 0 0 .44.85c1.06.2 2.76.48 2.98.95a.63.63 0 0 0 .44.35c2.05.45.32 2.35 1.8 1.45a.64.64 0 0 1 .96.73c-.13.45-.32.88-.75 1.04a.61.61 0 0 0-.4.58v.93a.62.62 0 0 0 .23.48c4.02 3.05-1.3 2.33 2.98 4.47a.64.64 0 0 1 .37.56c.04 1.49-.48 5.8.6 6.64a.65.65 0 0 1 .24.67c-.37 1.53.55 1.67 1.73 2.27l.1.04a.63.63 0 0 0 .79-.41c.96-2.72-1.45.76-.82-4.97a.62.62 0 0 1 .24-.43c7.92-6.03 1.38-3.9 5.24-7.52a.65.65 0 0 0 .12-.8c-.95-1.61-1.34-2.86-3.39-2.97ZM84.07 9.8c-.76-1.37-3.07-.3-4.46-.16a.46.46 0 0 1-.47-.24c-.85-1.6-4.89-1.83-6.36-2.13a.52.52 0 0 0-.18 0c-3.1.5-4.22 1.8-6.53-.98a.47.47 0 0 0-.37-.19h-7.25a.48.48 0 0 0-.24.06c-3.01 1.7-.04 1.78-3.3 2.55a.48.48 0 0 1-.5-.19c-.55-.7-3.78-.18-4.21-.33l-3.48-1.2a.48.48 0 0 0-.31 0c-2.05.82-2.75 1.35-3.35 3.52a.5.5 0 0 0 .06.34l.5.96a.46.46 0 0 0 .4.25c.84.03 1.4.13 1.57-.84a.49.49 0 0 1 .24-.34c1.49-.76.97.9 1.08 1.96a.47.47 0 0 1-.3.5l-2 .7a.46.46 0 0 1-.62-.35c-.13-.68-.31-.96-1.02-1.08a.46.46 0 0 0-.52.5c.06 1.25-.35 1.3-1.65 1.6-.11.04-.2.1-.26.2-.4.51-.62.9-.47 1.4a.47.47 0 0 1-.45.58c-1.22.02-.74.65-.5 1.86a.46.46 0 0 0 .45.39c.8.04 1.34.1 1.52-.74a.46.46 0 0 1 .5-.36c.64.07 1.29-.04 1.86-.33a.46.46 0 0 1 .58.07c.37.43.65.75.65 1.21a.47.47 0 0 0 .65.43h.02a.46.46 0 0 0 .24-.61c-.1-.22-.22-.48-.3-.73a.47.47 0 0 1 .82-.42c.7.98.95 1.63 2.34.93a.47.47 0 0 1 .54.07c.73.7 1.29 1.28.9 2.34a.47.47 0 0 1-.32.28c-1.54.45-3.55 1-5.02.06a.46.46 0 0 1-.2-.28c-.38-1.64-2.03-1.2-3.33-1.28a.47.47 0 0 0-.32.1l-3.87 3.1a.46.46 0 0 0-.19.33c-.03.67-.14 2.18-.7 2.53a.48.48 0 0 0-.2.41v1.77c0 .12.04.23.12.32 3.22 3.5 4.4.82 7.25 2.64a.48.48 0 0 1 .23.39c0 1.34-.08 1.94 1.28 2.44a.47.47 0 0 1 .3.52c-1.12 6.14.72 3.44.89 8.13a.47.47 0 0 0 .33.45c2.88.83 3.2-.28 5.15-2.16a.47.47 0 0 0 .13-.37c-.1-2.31-.44-3.1 1.68-4.43a.47.47 0 0 0 .22-.41c0-2.44-.15-3.85 1.9-5.53a.49.49 0 0 0 .18-.39c-.1-1.64-1.28-1.6-2.55-1.93a.46.46 0 0 1-.35-.47c.04-2.16-1.37-1.62-1.11-3.7a.46.46 0 0 1 .72-.34c.63.45 1.32 1.68 1.7 2.16a.47.47 0 0 1 .09.37c-.5 2.66 2.06.75 3.38-.09a.46.46 0 0 0 .15-.64v-.01l-1.48-2.23a.46.46 0 0 1 .09-.65c.08-.06.18-.1.28-.1l.67-.02a.47.47 0 0 1 .48.32c.41 1.41 2.34 2.4 3.7 2.94a.46.46 0 0 1 .28.54c-.47 2.05 1.08 2.94 2.42 4.13a.47.47 0 0 0 .75-.18l1.48-4.06a.47.47 0 0 1 .52-.3l1.03.19a.47.47 0 0 1 .37.3l1 2.79.11.15.19.2a.46.46 0 0 0 .82-.33c-.04-.79.09-1.38.9-.41a.46.46 0 0 0 .55.14c2.04-.89 1.33-2.53 1.3-4.37a.46.46 0 0 1 .39-.46c1.3-.23 1.4-.86 1.67-2.1a.47.47 0 0 0-.11-.41l-1.2-1.36a.46.46 0 0 1-.08-.53c.28-.55.73-1.45 1.34-1.45a.44.44 0 0 0 .43-.24c.44-.97 1.86-3.96 2.36-5.1a.46.46 0 0 1 .69-.18c.6.39.61.98.57 1.76a.47.47 0 0 0 .24.45c1.92 1.04-.01-1.79 4.65-3.94a.46.46 0 0 0 .24-.61l-.01-.04Z"/>
<path d="m30.64 5.77-2.4-1.5a1.77 1.77 0 0 0-1.39-.21c-1.74.46-5.93 1.1-5.05 2.58 2.1 2.98-.99 3.03 1.99 3.54.38.07.73.28.98.6h.02c1.93 2.41 2.4 2.13 3.72-.47.18-.37.49-.65.87-.8 1.92-.73 2.57-.49 1.97-2.77-.1-.4-.36-.75-.7-.97Zm8.63 8.02.54.1c.06.02.12.02.18 0h.02c1.12-.18 1.1-.81 1.04-1.61a.56.56 0 0 0-1.04-.13l-.02.04a.52.52 0 0 1-.48.39c-.52.03-.69.18-.69.69a.56.56 0 0 0 .45.52ZM77.7 37a.26.26 0 0 0-.05-.1c-.3-.5-.92-2.46-1.63-3.04a.26.26 0 0 0-.41.23c.02.65 0 .89-.82.61a.25.25 0 0 1-.15-.15v.02c-.31-.74-1.48-.37-2.15-.24a.26.26 0 0 0-.17.13c-.37.74-1.41 1.45-2.23 1.82a.61.61 0 0 0-.37.6l.13 1.99a.61.61 0 0 0 .61.58h2.42c.07 0 .14.03.18.09.56.65 2.29.98 3.15 1.13a.26.26 0 0 0 .22-.07 3.9 3.9 0 0 0 1.26-3.6Zm1.54 2.15a10.45 10.45 0 0 1-2.75 2.95.44.44 0 0 0-.2.49c.29.8.9.74 1.68.73a.44.44 0 0 0 .41-.38c.69-3.22 2.66-2.36 1.49-3.76a.45.45 0 0 0-.63-.03ZM55.63 35.6c-.32-.17-.62.12-.86.48a3.5 3.5 0 0 0-.6 2.42c.14.93.5 1.73 1.46.95-.08-.62 1.43-3.87 0-3.86Zm20.72-14.18c.95 2.14 1.92-.15 2.57-1.34a.9.9 0 0 0 .1-.45c-.14-1.06-.25-3.16-1.23-3.15a.69.69 0 0 0-.66.94v.01c.34 1-.37 2.46-.78 3.39a.72.72 0 0 0 0 .6Zm34.09 5.45a.63.63 0 0 1-.61-.64c-.06-1.26-1.06.38-2.42-1.48a.65.65 0 0 0-.56-.28c-7.12.3-2.58-.64-5.48-1.45a.65.65 0 0 1-.46-.6c0-1.73-2.2-.06-.78-2.55a.6.6 0 0 1 .33-.28c1.91-.7 2.12-.44 2.3-.07a.63.63 0 0 0 1 .22c.1-.1.16-.2.2-.33.37-1.16 2.28-1.98 3.3-2.6.11-.08.2-.18.26-.3a.63.63 0 0 0 .04-.38l-.1-.4a.63.63 0 0 1 0-.21.63.63 0 0 1 .7-.54l2.9.3h.03a.63.63 0 0 0 .68-.59c.06-.93.28-2.27-.55-2.23a.67.67 0 0 1-.6-.37l-.39-.78a.64.64 0 0 0-.67-.36c-.76.14-2.04.3-2.56.75a.65.65 0 0 1-.7.11c-2.4-1.04.72-.32 1.5-3.05l.02-.14a.63.63 0 0 0-.58-.68c-1.71-.15-4.22-.95-5.15.6a.65.65 0 0 1-.63.3l-9.37-1.16a.63.63 0 0 0-.7.78l.18.78a.66.66 0 0 1-.03.4.63.63 0 0 1-.58.38c-1.16-.03-1.12.2-1.06 1.64a.63.63 0 0 0 .31.52c.52.29 1.1.43 1.7.41v.04a.63.63 0 0 0 .63-.56c.2-1.26 1.76-.52 2.84-.52a.63.63 0 0 1 .5.24 3.28 3.28 0 0 1 .6 2.48.65.65 0 0 1-.56.56h-.16a.63.63 0 0 0-.56.57l-.34 3.67a.63.63 0 0 0 .18.52c.82.8 1.9 1.27 1.5 2.37a.61.61 0 0 0 .44.85c1.06.2 2.75.48 2.97.95a.63.63 0 0 0 .45.35c2.04.45.31 2.35 1.8 1.45a.64.64 0 0 1 .95.73c-.13.45-.32.88-.74 1.04a.61.61 0 0 0-.41.58v.93a.62.62 0 0 0 .24.48c4.01 3.05-1.3 2.33 2.97 4.47a.64.64 0 0 1 .37.56c.04 1.49-.48 5.8.6 6.64a.65.65 0 0 1 .24.67c-.37 1.53.56 1.67 1.73 2.27a.62.62 0 0 0 .77-.16c.06-.06.1-.13.12-.21.97-2.72-1.45.76-.82-4.97a.61.61 0 0 1 .25-.43c7.91-6.03 1.37-3.9 5.24-7.52a.65.65 0 0 0 .1-.8c-.94-1.61-1.32-2.86-3.37-2.97Zm60.52-17.02a.3.3 0 0 0-.02-.04c-.76-1.38-3.06-.3-4.46-.17a.46.46 0 0 1-.46-.24c-.85-1.6-4.89-1.83-6.36-2.13a.52.52 0 0 0-.18 0c-3.1.5-4.22 1.8-6.53-.98a.48.48 0 0 0-.37-.19h-7.25a.47.47 0 0 0-.24.06c-3.01 1.7-.04 1.78-3.3 2.55a.48.48 0 0 1-.51-.19c-.54-.7-3.77-.18-4.2-.33l-3.48-1.2a.48.48 0 0 0-.31 0c-2.05.82-2.75 1.35-3.35 3.52a.5.5 0 0 0 .06.34l.5.96a.46.46 0 0 0 .4.25c.84.03 1.4.13 1.57-.84a.48.48 0 0 1 .24-.34c1.49-.76.97.9 1.08 1.96a.47.47 0 0 1-.3.5l-2.01.7a.46.46 0 0 1-.5-.12.47.47 0 0 1-.11-.23c-.13-.68-.32-.96-1.03-1.08a.46.46 0 0 0-.52.5c.06 1.25-.35 1.3-1.65 1.6-.1.04-.2.1-.26.2-.4.51-.61.9-.47 1.4a.47.47 0 0 1-.44.58c-1.23.02-.75.65-.5 1.86.01.1.07.2.15.28.08.07.18.1.3.11.8.04 1.33.1 1.52-.74a.46.46 0 0 1 .5-.36c.64.07 1.28-.04 1.86-.33a.46.46 0 0 1 .57.07c.38.43.66.75.66 1.21 0 .08.01.15.05.22a.46.46 0 0 0 .37.25c.08 0 .15-.01.23-.04h.01a.47.47 0 0 0 .28-.43.45.45 0 0 0-.03-.18c-.1-.22-.23-.48-.3-.73a.47.47 0 0 1 .22-.57.47.47 0 0 1 .6.15c.7.98.94 1.63 2.34.93a.47.47 0 0 1 .54.07c.72.7 1.28 1.28.89 2.34a.46.46 0 0 1-.32.28c-1.54.45-3.55 1-5.02.06a.46.46 0 0 1-.2-.28c-.37-1.64-2.03-1.2-3.33-1.28a.46.46 0 0 0-.31.1l-3.87 3.1a.46.46 0 0 0-.19.33c-.03.67-.15 2.18-.7 2.53a.49.49 0 0 0-.2.41v1.77c0 .12.04.23.12.32 3.22 3.5 4.39.82 7.25 2.64a.49.49 0 0 1 .22.39c0 1.34-.07 1.94 1.29 2.44a.46.46 0 0 1 .3.52c-1.12 6.14.72 3.44.89 8.13a.47.47 0 0 0 .33.45c2.88.83 3.2-.28 5.15-2.16a.42.42 0 0 0 .1-.17.46.46 0 0 0 .03-.2c-.11-2.31-.45-3.1 1.67-4.43a.46.46 0 0 0 .23-.41c0-2.44-.15-3.85 1.9-5.53a.49.49 0 0 0 .18-.39c-.1-1.64-1.28-1.6-2.55-1.93a.46.46 0 0 1-.35-.47c.04-2.16-1.38-1.62-1.12-3.7a.46.46 0 0 1 .51-.41c.08 0 .15.03.22.07.63.45 1.32 1.68 1.69 2.16a.48.48 0 0 1 .1.37c-.5 2.66 2.06.75 3.38-.09a.47.47 0 0 0 .15-.64v-.01l-1.49-2.23a.46.46 0 0 1-.08-.35.46.46 0 0 1 .18-.3c.08-.06.18-.1.28-.1l.67-.02a.47.47 0 0 1 .48.32c.4 1.41 2.34 2.4 3.7 2.94a.47.47 0 0 1 .28.54c-.47 2.05 1.07 2.94 2.41 4.13a.47.47 0 0 0 .66-.04.5.5 0 0 0 .09-.15l1.48-4.05a.47.47 0 0 1 .53-.3l1.02.19a.47.47 0 0 1 .37.3l1 2.78c.03.06.07.11.11.15l.19.2a.46.46 0 0 0 .82-.33c-.04-.78.1-1.37.9-.4a.46.46 0 0 0 .55.14c2.04-.89 1.34-2.53 1.3-4.37a.47.47 0 0 1 .39-.46c1.3-.23 1.4-.86 1.67-2.1a.47.47 0 0 0-.11-.4l-1.19-1.37a.46.46 0 0 1-.07-.53c.28-.55.72-1.45 1.34-1.45a.44.44 0 0 0 .42-.24c.45-.97 1.86-3.96 2.36-5.1a.47.47 0 0 1 .44-.27.47.47 0 0 1 .25.09c.6.39.62.98.58 1.76a.47.47 0 0 0 .24.45c1.91 1.04-.02-1.79 4.65-3.94a.46.46 0 0 0 .23-.61Z"/>
<path d="m117.52 5.77-2.4-1.5a1.77 1.77 0 0 0-1.4-.21c-1.74.46-5.92 1.1-5.05 2.58 2.1 2.98-.98 3.03 1.99 3.54.39.07.74.28.98.6h.02c1.94 2.41 2.4 2.13 3.72-.47.18-.37.5-.65.87-.8 1.92-.73 2.57-.49 1.97-2.77a1.6 1.6 0 0 0-.7-.97Zm8.62 8.02.54.1a.3.3 0 0 0 .19 0h.02c1.11-.18 1.1-.81 1.04-1.61a.56.56 0 0 0-.48-.43.56.56 0 0 0-.56.3l-.02.04a.52.52 0 0 1-.48.39c-.53.03-.7.18-.7.69a.56.56 0 0 0 .45.52ZM164.57 37a.28.28 0 0 0-.04-.1c-.3-.5-.93-2.46-1.64-3.04a.26.26 0 0 0-.2-.04.26.26 0 0 0-.16.1.26.26 0 0 0-.05.17c.02.65 0 .89-.81.61a.25.25 0 0 1-.15-.15v.02c-.32-.74-1.49-.37-2.16-.24a.26.26 0 0 0-.17.13c-.37.74-1.4 1.45-2.23 1.82a.61.61 0 0 0-.37.6l.13 1.99a.61.61 0 0 0 .62.58h2.41c.07 0 .14.03.19.09.56.65 2.28.98 3.14 1.13a.25.25 0 0 0 .22-.07 3.9 3.9 0 0 0 1.27-3.6Zm1.54 2.15a10.44 10.44 0 0 1-2.75 2.95.44.44 0 0 0-.19.49c.28.8.9.74 1.68.73a.45.45 0 0 0 .4-.38c.7-3.22 2.66-2.36 1.5-3.76a.44.44 0 0 0-.49-.12.44.44 0 0 0-.15.1ZM142.5 35.6c-.31-.17-.61.12-.85.48a3.5 3.5 0 0 0-.6 2.42c.13.93.48 1.73 1.45.95-.07-.62 1.43-3.87 0-3.86Zm20.73-14.18c.95 2.14 1.92-.15 2.57-1.34.07-.14.1-.3.1-.45-.14-1.06-.25-3.16-1.24-3.15a.69.69 0 0 0-.65.94v.01c.34 1-.37 2.46-.78 3.39a.73.73 0 0 0 0 .6Z"/>
</g>
</g>
<rect x="1.25" y="1.25" width="45.5" height="45.5" rx="22.75" stroke="currentColor" stroke-width="2.5"/>
{:else}
<g clip-path="url(#a)" fill="currentColor">
<path d="M58.07 8.8c-.76-1.37-3.07-.3-4.46-.16a.46.46 0 0 1-.47-.24c-.85-1.6-4.89-1.83-6.36-2.13a.52.52 0 0 0-.18 0c-3.1.5-4.22 1.8-6.53-.98a.47.47 0 0 0-.37-.19h-7.25a.48.48 0 0 0-.24.06c-3.01 1.7-.04 1.78-3.3 2.55a.48.48 0 0 1-.5-.19c-.55-.7-3.78-.18-4.21-.33l-3.48-1.2a.48.48 0 0 0-.31 0c-2.05.82-2.75 1.35-3.35 3.52a.5.5 0 0 0 .06.34l.5.96a.46.46 0 0 0 .4.25c.84.03 1.4.13 1.57-.84a.49.49 0 0 1 .24-.34c1.49-.76.97.9 1.08 1.96a.47.47 0 0 1-.3.5l-2 .7a.46.46 0 0 1-.62-.35c-.13-.68-.31-.96-1.02-1.08a.46.46 0 0 0-.52.5c.06 1.25-.35 1.3-1.65 1.6-.11.04-.2.1-.26.2-.4.51-.62.9-.47 1.4a.47.47 0 0 1-.45.58c-1.22.02-.74.65-.5 1.86a.46.46 0 0 0 .45.39c.8.04 1.34.1 1.52-.74a.46.46 0 0 1 .5-.36c.64.07 1.29-.04 1.86-.33a.46.46 0 0 1 .58.07c.37.43.65.75.65 1.21a.47.47 0 0 0 .65.43h.02a.46.46 0 0 0 .24-.61c-.1-.22-.22-.48-.3-.73a.47.47 0 0 1 .82-.42c.7.98.95 1.63 2.34.93a.47.47 0 0 1 .54.07c.73.7 1.29 1.28.9 2.34a.47.47 0 0 1-.32.28c-1.54.45-3.55 1-5.02.06a.46.46 0 0 1-.2-.28c-.38-1.64-2.03-1.2-3.33-1.28a.47.47 0 0 0-.32.1l-3.87 3.1a.47.47 0 0 0-.19.33c-.03.67-.14 2.18-.7 2.53a.48.48 0 0 0-.2.41v1.77c0 .12.04.23.12.32 3.22 3.5 4.4.82 7.25 2.64a.49.49 0 0 1 .23.39c0 1.34-.08 1.94 1.28 2.44a.47.47 0 0 1 .3.52c-1.12 6.14.72 3.44.89 8.13a.47.47 0 0 0 .33.45c2.88.83 3.2-.28 5.15-2.16a.47.47 0 0 0 .13-.37c-.1-2.31-.44-3.1 1.68-4.43a.47.47 0 0 0 .22-.41c0-2.44-.15-3.85 1.9-5.53a.49.49 0 0 0 .18-.39c-.1-1.64-1.28-1.6-2.55-1.93a.46.46 0 0 1-.35-.47c.04-2.16-1.37-1.62-1.11-3.7a.47.47 0 0 1 .72-.34c.63.45 1.32 1.68 1.7 2.16a.47.47 0 0 1 .09.37c-.5 2.66 2.06.75 3.38-.09a.47.47 0 0 0 .15-.64v-.01l-1.48-2.23a.46.46 0 0 1 .37-.75l.67-.02a.47.47 0 0 1 .48.32c.41 1.41 2.34 2.4 3.7 2.94a.46.46 0 0 1 .28.54c-.47 2.05 1.08 2.94 2.42 4.13a.47.47 0 0 0 .75-.18l1.48-4.06a.47.47 0 0 1 .52-.3l1.03.19a.47.47 0 0 1 .37.3l1 2.79.11.15.19.2a.46.46 0 0 0 .82-.33c-.04-.79.09-1.38.9-.41a.46.46 0 0 0 .55.14c2.04-.89 1.33-2.53 1.3-4.37a.46.46 0 0 1 .39-.46c1.3-.23 1.4-.86 1.67-2.1a.47.47 0 0 0-.11-.41l-1.2-1.36a.46.46 0 0 1-.08-.53c.28-.55.73-1.45 1.34-1.45a.44.44 0 0 0 .43-.24c.44-.97 1.86-3.96 2.36-5.1a.47.47 0 0 1 .69-.18c.6.39.61.98.57 1.76a.47.47 0 0 0 .24.45c1.92 1.04-.01-1.79 4.65-3.94a.46.46 0 0 0 .24-.61l-.01-.04Z"/>
<path d="m13.27 12.79.54.1c.06.02.12.02.18 0h.02c1.12-.18 1.1-.81 1.04-1.61a.56.56 0 0 0-1.04-.13l-.02.04a.52.52 0 0 1-.48.39c-.52.03-.69.18-.69.69a.56.56 0 0 0 .45.52ZM29.63 34.6c-.32-.17-.62.12-.86.48a3.5 3.5 0 0 0-.6 2.42c.14.93.5 1.73 1.46.95-.08-.62 1.43-3.87 0-3.86Zm20.72-14.18c.95 2.14 1.92-.15 2.57-1.34a.9.9 0 0 0 .1-.45c-.14-1.06-.25-3.16-1.23-3.15a.69.69 0 0 0-.66.94v.01c.34 1-.37 2.46-.78 3.39a.72.72 0 0 0 0 .6Z"/>
</g>
<rect x="1.25" y="1.25" width="45.5" height="45.5" rx="22.75" stroke="currentColor" stroke-width="2.5"/>
<defs>
<clipPath id="a">
<rect width="48" height="48" rx="24" fill="#fff"/>
</clipPath>
</defs>
{/if}
</svg>

View File

@@ -0,0 +1,86 @@
<script lang="ts">
import { getAssetUrlKey } from '$utils/api'
export let src: string = undefined
export let id: string = undefined
export let sizeKey: string = undefined
export let sizes: Sizes = undefined
export let width: number = sizes && sizes.medium && sizes.medium.width
export let height: number = sizes && sizes.medium && sizes.medium.height
export let ratio: number = undefined
export let alt: string
export let lazy: boolean = true
export let decoding: "auto" | "sync" | "async" = "auto"
interface Sizes {
small?: { width?: number, height?: number }
medium?: { width?: number, height?: number }
large?: { width?: number, height?: number }
}
let srcSet = { webp: [], jpg: [] }
/**
* Define height from origin ratio if not defined
*/
const setHeightFromRatio = (w: number, r: number = ratio) => {
return Math.round(w / r)
}
if (ratio && !height) {
// Set height from width using ratio
height = setHeightFromRatio(width)
// Add height to all sizes
if (sizes) {
Object.entries(sizes).forEach(size => {
const [key, value]: [string, { width?: number, height?: number }] = size
sizes[key].height = setHeightFromRatio(value.width)
})
}
}
/**
* Image attributes
*/
$: imgWidth = sizes && sizes.small ? sizes.small.width : width
$: imgHeight = sizes && sizes.small ? sizes.small.height : height
$: imgSrc = id ? getAssetUrlKey(id, `${sizeKey}-small-jpg`) : src ? src : undefined
$: srcSet = {
// WebP
webp:
sizes ? [
`${getAssetUrlKey(id, `${sizeKey}-small-webp`)} 345w`,
sizes.medium ? `${getAssetUrlKey(id, `${sizeKey}-medium-webp`)} 768w` : null,
sizes.large ? `${getAssetUrlKey(id, `${sizeKey}-large-webp`)} 1280w` : null,
]
: [getAssetUrlKey(id, `${sizeKey}-webp`)],
// JPG
jpg:
sizes ? [
`${getAssetUrlKey(id, `${sizeKey}-small-jpg`)} 345w`,
sizes.medium ? `${getAssetUrlKey(id, `${sizeKey}-medium-jpg`)} 768w` : null,
sizes.large ? `${getAssetUrlKey(id, `${sizeKey}-large-jpg`)} 1280w` : null,
]
: [getAssetUrlKey(id, `${sizeKey}-jpg`)]
}
</script>
<picture class={$$props.class}>
<source
type="image/webp"
srcset={srcSet.webp.join(', ').trim()}
>
<img
src={imgSrc}
sizes={sizes ? '(min-width: 1200px) 864px, (min-width: 992px) 708px, (min-width: 768px) 540px, 100%' : null}
srcset={srcSet.jpg.join(', ').trim()}
width={imgWidth}
height={imgHeight}
{alt}
loading={lazy ? 'lazy' : undefined}
{decoding}
/>
</picture>

View File

@@ -0,0 +1,72 @@
<style lang="scss">
:global(.scrolling-title) {
transform: translate3d(var(--parallax-x), 0, 0);
transition: transform 1.2s var(--ease-quart);
will-change: transform;
}
</style>
<script lang="ts">
import { map } from '$utils/functions'
import reveal from '$animations/reveal'
export let tag: string
export let label: string = undefined
export let parallax: number = undefined
export let offsetStart: number = undefined
export let offsetEnd: number = undefined
export let animate: boolean = true
let scrollY: number
let innerWidth: number
let innerHeight: number
let titleEl: HTMLElement
let isLarger: boolean
// Define default values
$: if (titleEl && !offsetStart && !offsetEnd) {
offsetStart = titleEl.offsetTop - innerHeight * 0.75
offsetEnd = titleEl.offsetTop + innerHeight * 0.25
}
// Check if title is larger than viewport to translate it
$: isLarger = titleEl && titleEl.offsetWidth >= innerWidth
// Calculate the parallax value
$: if (titleEl) {
const toTranslate = 100 - innerWidth / titleEl.offsetWidth * 100
parallax = isLarger ? map(scrollY, offsetStart, offsetEnd, 0, -toTranslate, true) : 0
}
const classes = [
'scrolling-title',
'title-huge',
$$props.class
].join(' ').trim()
const revealOptions = animate ? {
children: '.char',
animation: { y: ['-105%', 0] },
options: {
stagger: 0.06,
duration: 1.6,
delay: 0.2,
threshold: 0.2,
},
} : null
</script>
<svelte:window
bind:scrollY
bind:innerWidth bind:innerHeight
/>
<svelte:element this={tag}
bind:this={titleEl}
class={classes} aria-label={label}
style:--parallax-x="{parallax}%"
use:reveal={revealOptions}
>
<slot />
</svelte:element>

View File

@@ -0,0 +1,37 @@
<style lang="scss">
@import "../../style/atoms/site-title";
</style>
<script lang="ts">
import SplitText from '$components/SplitText.svelte'
import reveal from '$animations/reveal'
import { DURATION } from '$utils/contants'
export let variant: string = 'lines'
export let tag: string = 'h1'
</script>
{#if tag === 'h1'}
<h1 class="site-title site-title--{variant}"
use:reveal={{
children: '.char',
animation: { y: ['105%', 0] },
options: {
stagger: 0.04,
duration: 1,
delay: DURATION.PAGE_IN / 1000,
threshold: 0,
},
}}
>
<SplitText text="Houses" mode="chars" class="pink mask" />
<SplitText text="Of The" mode="chars" class="middle mask" />
<SplitText text="World" mode="chars" class="pink mask" />
</h1>
{:else}
<div class="site-title site-title--{variant}">
<span class="word-1">Houses</span>
<span class="middle word-2">Of The</span>
<span class="word-3">World</span>
</div>
{/if}

View File

@@ -0,0 +1,134 @@
<style lang="scss">
@import "../../style/layouts/poster";
</style>
<script lang="ts">
import { addToCart } from '$utils/functions/shop'
import { capitalizeFirstLetter } from '$utils/functions'
// Components
import SplitText from '$components/SplitText.svelte'
import Button from '$components/atoms/Button.svelte'
import Image from '$components/atoms/Image.svelte'
import ScrollingTitle from '$components/atoms/ScrollingTitle.svelte'
import Carousel from '$components/organisms/Carousel.svelte'
export let product: any
export let shopProduct: any
$: hasStock = shopProduct.stock_level > 0
/**
* Preview photos specs
*/
let lastPreviewPhoto: any = undefined
$: if (product && product.photos_preview.length) {
lastPreviewPhoto = product.photos_preview[product.photos_preview.length - 1].directus_files_id
}
// Images sizes
const photosPreview = [
{
sizes: {
small: { width: 275 },
medium: { width: 500 },
large: { width: 800 },
},
ratio: 0.75,
},
{
sizes: {
small: { width: 200 },
medium: { width: 300 },
large: { width: 400 },
},
ratio: 0.8,
},
{
sizes: {
small: { width: 200 },
medium: { width: 300 },
large: { width: 400 },
},
ratio: 1.28,
},
{
sizes: {
small: { width: 450 },
medium: { width: 700 },
large: { width: 1000 },
},
ratio: 0.68,
},
]
</script>
<section class="poster-layout grid" id="poster">
<div class="poster-layout__title">
<ScrollingTitle tag="h2" label={product.location.name}>
<SplitText mode="chars" text={product.location.name} />
</ScrollingTitle>
</div>
<aside class="poster-layout__buy">
<div class="poster-layout__info">
<dl>
<dt class="title-small">{capitalizeFirstLetter(product.type)}</dt>
<dd class="text-info">{shopProduct.name} {shopProduct.price}</dd>
</dl>
<Button
tag="button"
text={hasStock ? 'Add to cart' : 'Sold out'}
color="pinklight"
disabled={!hasStock}
on:click={() => addToCart(shopProduct)}
/>
</div>
<Carousel
class="shadow-box-dark"
slides={product.photos_product.map(({ directus_files_id }) => ({
id: directus_files_id.id,
alt: directus_files_id.title,
}))}
/>
</aside>
{#if product.photos_preview.length}
<div class="poster-layout__images grid container">
{#each product.photos_preview.slice(0, 3) as { directus_files_id}, index}
<Image
class="image image--{index + 1} photo shadow-box-light"
id={directus_files_id.id}
sizeKey="photo-list"
sizes={photosPreview[index].sizes}
ratio={photosPreview[index].ratio}
alt={directus_files_id.title}
/>
{/each}
</div>
{/if}
<div class="poster-layout__about grid">
<div class="text container">
{#if product.description}
<p class="headline text-large">{product.description}</p>
{/if}
{#if product.details}
<p class="details text-xsmall">{product.details}</p>
{/if}
</div>
</div>
{#if lastPreviewPhoto}
<Image
class="image image--4 photo shadow-box-light"
id={lastPreviewPhoto.id}
sizeKey="photo-grid"
sizes={photosPreview[photosPreview.length - 1].sizes}
ratio={photosPreview[photosPreview.length - 1].ratio}
alt={lastPreviewPhoto.title}
/>
{/if}
</section>

View File

@@ -0,0 +1,69 @@
<style lang="scss">
@import "../../style/molecules/cart-item";
</style>
<script lang="ts">
import { createEventDispatcher } from 'svelte'
// Components
import ButtonCircle from '$components/atoms/ButtonCircle.svelte'
import Select from '$components/molecules/Select.svelte'
export let item: any
const dispatch = createEventDispatcher()
const quantityLimit = 5
// When changing item quantity
const updateQuantity = ({ detail }: any) => {
dispatch('updatedQuantity', {
id: item.id,
quantity: Number(detail)
})
}
// When removing item
const removeItem = () => {
dispatch('removed', item.id)
}
</script>
<div class="cart-item shadow-small">
<div class="cart-item__left">
<img src={item.product.images[0].file.url} width={200} height={300} alt={item.product.name}>
</div>
<div class="cart-item__right">
<h3>Poster</h3>
<p>
{item.product.name}
<br> {item.price}
</p>
{#if item && item.quantity}
<Select
name="sort" id="filter_sort"
options={[...Array(item.quantity <= quantityLimit ? quantityLimit : item.quantity)].map((_, index) => {
return {
value: `${index + 1}`,
name: `${index + 1}`,
default: index === 0,
selected: index + 1 === item.quantity,
}
})}
on:change={updateQuantity}
value={String(item.quantity)}
>
<span>Quantity:</span>
</Select>
{/if}
<ButtonCircle class="remove"
size="tiny" color="gray"
on:click={removeItem}
>
<svg width="8" height="8">
<use xlink:href="#cross" />
</svg>
</ButtonCircle>
</div>
</div>

View File

@@ -0,0 +1,103 @@
<style lang="scss">
@import "../../style/molecules/newsletter-form";
</style>
<script lang="ts">
import { fly } from 'svelte/transition'
import { quartOut } from 'svelte/easing'
import { sendEvent } from '$utils/analytics'
// Components
import IconArrow from '$components/atoms/IconArrow.svelte'
import ButtonCircle from '$components/atoms/ButtonCircle.svelte'
export let past: boolean = false
let inputInFocus = false
let formStatus: string = null
let formMessageTimeout: ReturnType<typeof setTimeout> | number
const formMessages = {
PENDING: `Almost there! Please confirm your email address through the email you'll receive soon.`,
MEMBER_EXISTS_WITH_EMAIL_ADDRESS: `Uh oh! This email address is already subscribed to the newsletter.`,
INVALID_EMAIL: `Woops. This email doesn't seem to be valid.`,
}
// Toggle input focus
const toggleFocus = () => inputInFocus = !inputInFocus
/**
* Subscription form handling
*/
const formSubmission = async ({ target }) => {
const formData = new FormData(target)
const email = String(formData.get('email'))
if (email && email.match(/^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/)) {
const req = await fetch('/api/newsletter', {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: email
})
const res = await req.json()
formStatus = res.code
if (res.code === 'PENDING') {
sendEvent({ action: 'newsletterSubscribe' })
}
} else {
formStatus = 'INVALID_EMAIL'
}
}
$: if (formStatus !== 'PENDING') {
clearTimeout(formMessageTimeout)
formMessageTimeout = setTimeout(() => formStatus = null, 3000)
}
</script>
<div class="newsletter-form">
{#if formStatus !== 'PENDING'}
<form method="POST" on:submit|preventDefault={formSubmission}
out:fly={{ y: -8, easing: quartOut, duration: 600 }}
>
<div class="newsletter-form__email" class:is-focused={inputInFocus}>
<input type="email" placeholder="Your email address" name="email" id="newsletter_email" required
on:focus={toggleFocus}
on:blur={toggleFocus}
>
<ButtonCircle
type="submit"
color="pink" size="small"
clone={true}
label="Subscribe"
>
<IconArrow color="white" />
</ButtonCircle>
</div>
<div class="newsletter-form__bottom">
{#if past}
<a href="/subscribe" class="past-issues" data-sveltekit-noscroll data-sveltekit-prefetch>
<svg width="20" height="16" viewBox="0 0 20 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-label="Newsletter icon">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 2.346H2a.5.5 0 0 0-.5.5v11.102a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5V2.846a.5.5 0 0 0-.5-.5ZM2 .846a2 2 0 0 0-2 2v11.102a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V2.846a2 2 0 0 0-2-2H2Zm13.75 4.25h-2v3h2v-3Zm-2-1a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1h-2ZM3.5 6.5a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6Zm.25 3a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5Zm1.25 2a.5.5 0 0 0 0 1h6a.5.5 0 1 0 0-1H5Z" />
</svg>
<span>See past issues</span>
</a>
{/if}
<p>No spam, we promise!</p>
</div>
</form>
{/if}
{#if formStatus}
<div class="newsletter-form__message shadow-small"
class:is-error={formStatus !== 'PENDING'}
class:is-success={formStatus === 'PENDING'}
in:fly={{ y: 8, easing: quartOut, duration: 600, delay: 600 }}
out:fly={{ y: 8, easing: quartOut, duration: 600 }}
>
<p class="text-xsmall">{formMessages[formStatus]}</p>
</div>
{/if}
</div>

View File

@@ -0,0 +1,17 @@
<style lang="scss">
@import "../../style/molecules/heading";
</style>
<script lang="ts">
import SiteTitle from '$components/atoms/SiteTitle.svelte'
export let text: string
</script>
<section class="heading">
<SiteTitle variant="inline" />
<div class="text text-medium">
{@html text}
</div>
</section>

View File

@@ -0,0 +1,66 @@
<style lang="scss">
@import "../../style/molecules/house";
</style>
<script lang="ts">
import dayjs from 'dayjs'
// Components
import Image from '$components/atoms/Image.svelte'
import Icon from '$components/atoms/Icon.svelte'
export let url: string
export let photoId: string
export let photoAlt: string
export let title: string
export let index: string
export let ratio: number
export let date: string = undefined
export let city: string = undefined
export let location: string
</script>
<div class="house grid">
<div class="house__info">
<h2 class="title-image">
{title}
</h2>
<p class="info text-info">
{#if city}
<a href="https://www.openstreetmap.org/search?query={title}, {city} {location}" target="_blank" rel="noopener noreferrer">
<Icon class="icon" icon="map-pin" label="Map pin" /> {city}
</a>
<span class="sep">·</span>
{/if}
{#if date}
<time datetime={dayjs(date).format('YYYY-MM-DD')}>
{dayjs(date).format('MMMM YYYY')}
</time>
{/if}
</p>
</div>
<div class="house__photo grid" class:not-landscape={ratio < 1.475}>
<a href={url} tabindex="0">
<figure class="house__image shadow-photo">
<Image
class="photo"
id={photoId}
sizeKey="photo-list"
sizes={{
small: { width: 500 },
medium: { width: 850 },
large: { width: 1280 },
}}
ratio={1.5}
alt={photoAlt}
/>
</figure>
</a>
<span class="house__index title-index"
class:has-one-start={index.startsWith('1')}
class:has-one-end={index.endsWith('1')}
>
{index}
</span>
</div>
</div>

View File

@@ -0,0 +1,116 @@
<style lang="scss">
@import "../../style/molecules/location";
</style>
<script lang="ts">
import { getContext } from 'svelte'
import { spring } from 'svelte/motion'
import dayjs, { type Dayjs } from 'dayjs'
import { lerp } from '$utils/functions'
import { PUBLIC_PREVIEW_COUNT } from '$env/static/public'
import { seenLocations } from '$utils/stores'
// Components
import Image from '$components/atoms/Image.svelte'
import Badge from '$components/atoms/Badge.svelte'
export let location: any
export let latestPhoto: any
const { settings }: any = getContext('global')
let locationEl: HTMLElement
let photoIndex = 0
// Location date limit
let isNew = false
let dateUpdated: Dayjs
const dateNowOffset = dayjs().subtract(settings.limit_new, 'day')
$: if (latestPhoto && $seenLocations) {
dateUpdated = dayjs(latestPhoto.date_created)
// Detect if location has new content
const seenLocation = JSON.parse($seenLocations)?.hasOwnProperty(location.id)
isNew = dateUpdated.isAfter(dateNowOffset) && !seenLocation
}
/**
* Moving cursor over
*/
const offset = spring({ x: 0, y: 0 }, {
stiffness: 0.075,
damping: 0.9
})
const handleMouseMove = ({ clientX }: MouseEvent) => {
const { width, left } = locationEl.getBoundingClientRect()
const moveProgress = (clientX - left) / width // 0 to 1
// Move horizontally
offset.update(_ => ({
x: lerp(-56, 56, moveProgress),
y: 0
}))
// Change photo index from mouse position percentage
photoIndex = Math.round(lerp(0, Number(PUBLIC_PREVIEW_COUNT) - 1, moveProgress))
}
// Leaving mouseover
const handleMouseLeave = () => {
offset.update($c => ({
x: $c.x,
y: 40
}))
}
</script>
<div class="location" bind:this={locationEl}
style:--offset-x="{$offset.x}px"
style:--offset-y="{$offset.y}px"
style:--rotate="{$offset.x * 0.125}deg"
>
<a href="/{location.country.slug}/{location.slug}"
on:mousemove={handleMouseMove}
on:mouseleave={handleMouseLeave}
tabindex="0"
>
<Image
class="flag"
id={location.country.flag.id}
sizeKey="square-small"
width={32} height={32}
alt="Flag of {location.country.name}"
/>
<div class="text">
<dl>
<dt class="location__name">
{location.name}
</dt>
<dd class="location__country text-label">
{location.country.name}
</dd>
</dl>
{#if isNew}
<Badge text="New" />
{/if}
</div>
</a>
{#if location.photos.length}
<div class="location__photos">
{#each location.photos as { image }, index}
{#if image}
{@const classes = ['location__photo', index === photoIndex ? 'is-visible' : null].join(' ').trim()}
<Image
class={classes}
id={image.id}
sizeKey="photo-thumbnail"
width={340} height={226}
alt={image.title}
/>
{/if}
{/each}
</div>
{/if}
</div>

View File

@@ -0,0 +1,33 @@
<style lang="scss">
@import "../../style/molecules/issue";
</style>
<script lang="ts">
import dayjs from 'dayjs'
import Image from '$components/atoms/Image.svelte'
export let title: string
export let issue: number
export let date: string
export let link: string
export let thumbnail: { id: string }
export let size: string = undefined
</script>
<div class="issue" class:is-large={size === 'large'}>
<a href={link} target="_blank" rel="external noopener" tabindex="0">
<Image
id={thumbnail.id}
sizeKey="issue-thumbnail-small"
width={160} height={112}
alt="Issue {issue} thumbnail"
/>
<dl>
<dt>Issue #{issue}</dt>
<dd>
<p>{title}</p>
<time>{dayjs(date).format('DD/MM/YYYY')}</time>
</dd>
</dl>
</a>
</div>

View File

@@ -0,0 +1,32 @@
<style lang="scss">
@import "../../style/molecules/notification-cart";
</style>
<script lang="ts">
import { fly } from 'svelte/transition'
import { quartOut } from 'svelte/easing'
import { cartOpen } from '$utils/stores/shop'
export let title: string
export let name: string
export let image: string
const closeNotification = () => {
// Open cart
$cartOpen = true
}
</script>
<div class="notification-cart shadow-small"
on:click={closeNotification}
transition:fly={{ y: 20, duration: 700, easing: quartOut }}
>
<div class="notification-cart__left">
<img src={image} width={58} height={88} alt={title}>
</div>
<div class="notification-cart__right">
<h3>{title}</h3>
<p>{name}</p>
</div>
</div>

View File

@@ -0,0 +1,23 @@
<style lang="scss">
@import "../../style/molecules/pagination";
</style>
<script lang="ts">
export let ended: boolean = false
export let current: number
export let total: number
</script>
<div class="pagination" role="button" tabindex="0"
disabled={ended ? ended : undefined}
on:click
on:keydown
>
<div class="pagination__progress">
<span class="current">{current}</span>
<span>/</span>
<span class="total">{total}</span>
<slot />
</div>
</div>

View File

@@ -0,0 +1,70 @@
<style lang="scss">
@import "../../style/molecules/photo-card";
</style>
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import Image from '$components/atoms/Image.svelte'
export let id: string
export let alt: string
export let url: string = undefined
export let title: string = undefined
export let location: any = undefined
export let city: string = undefined
export let hovered: boolean = false
export let lazy: boolean = true
const dispatch = createEventDispatcher()
const sizes = {
small: { width: 224 },
medium: { width: 464 },
large: { width: 864 },
}
const sendHover = (hover: boolean) => dispatch('hover', hover)
</script>
<div class="photo-card"
class:is-hovered={hovered}
on:mouseenter={() => sendHover(true)}
on:focus={() => sendHover(true)}
on:mouseout={() => sendHover(false)}
on:blur={() => sendHover(false)}
>
{#if url}
<div class="photo-card__content">
<a href={url} data-sveltekit-noscroll>
<Image
{id}
sizeKey="postcard"
{sizes}
ratio={1.5}
{alt}
{lazy}
/>
{#if title && location}
<div class="photo-card__info">
<Image
id={location.country.flag.id}
sizeKey="square-small"
width={24}
height={24}
alt="Flag of {location.country.name}"
/>
<p>{title} - {city ? `${city}, ` : ''}{location.name}, {location.country.name}</p>
</div>
{/if}
</a>
</div>
{:else}
<Image
{id}
sizeKey="postcard"
{sizes}
ratio={1.5}
{alt}
{lazy}
/>
{/if}
</div>

View File

@@ -0,0 +1,48 @@
<style lang="scss">
@import "../../style/organisms/postcard";
</style>
<script lang="ts">
import Image from '$components/atoms/Image.svelte'
export let street: string
export let location: string
export let region: string = undefined
export let country: string
export let flagId: string
export let size: string = undefined
const className = 'postcard'
$: classes = [
className,
...[size].map(variant => variant && `${className}--${variant}`),
$$props.class
].join(' ').trim()
</script>
<div class={classes}>
<div class="postcard__left">
<p class="postcard__country">
<span>Houses of</span><br>
<strong class="title-country__purple">{country}</strong>
</p>
</div>
<div class="postcard__right">
<div class="postcard__stamp">
<div class="frame">
<img src="/images/icons/stamp.svg" width="32" height="42" alt="Stamp">
</div>
<Image
class="flag"
id={flagId}
sizeKey="square-small"
width={32} height={32}
alt="Flag of {country}"
/>
</div>
<ul class="postcard__address">
<li>{street}</li>
<li>{location}{region ? `, ${region}` : ''}</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,50 @@
<style lang="scss">
@import "../../style/molecules/poster";
</style>
<script lang="ts">
import { addToCart } from '$utils/functions/shop'
import { smoothScroll } from '$utils/stores'
// Components
import Button from '$components/atoms/Button.svelte'
import Image from '$components/atoms/Image.svelte'
export let product: any
export let location: { name: string, slug: string }
export let image: any
</script>
<div class="poster">
{#if image}
<a href="/shop/poster-{location.slug}" data-sveltekit-noscroll data-sveltekit-prefetch
on:click={() => $smoothScroll.scrollTo('#poster', { duration: 2 })}
>
<Image
id={image.id}
sizeKey="product"
sizes={{
small: { width: 326 },
medium: { width: 326 },
large: { width: 326 },
}}
ratio={1.5}
alt="Poster of {location.name}"
/>
</a>
{/if}
<div class="buttons">
<Button
size="xsmall"
url="/shop/poster-{location.slug}"
text="View"
on:click={() => $smoothScroll.scrollTo('#poster', { duration: 2 })}
/>
<Button
tag="button"
size="xsmall"
text="Add to cart"
color="pink"
on:click={() => addToCart(product)}
/>
</div>
</div>

View File

@@ -0,0 +1,61 @@
<style lang="scss">
@import "../../style/molecules/process-step";
</style>
<script lang="ts">
import { scaleFade } from '$animations/transitions'
// Components
import Image from '$components/atoms/Image.svelte'
import { getAssetUrlKey } from '$utils/api'
export let index: number
export let text: string
export let image: any = undefined
export let video: any = undefined
export let visible: boolean = false
let videoEl: HTMLVideoElement
const imageRatio = image ? image.width / image.height : undefined
// Toggle video playback if step is visible
$: if (videoEl) {
visible ? videoEl.play() : videoEl.pause()
}
</script>
{#if visible}
<div class="step grid" style:--index={index}
in:scaleFade|local={{ scale: [1.1, 1], opacity: [0, 1], x: [20, 0], delay: 0.2 }}
out:scaleFade|local={{ scale: [1, 0.9], opacity: [1, 0], x: [0, -20] }}
>
{#if image || video}
<div class="media">
{#if image}
<Image
class="image shadow-box-dark"
id={image.id}
sizeKey="product"
sizes={{
small: { width: 400 },
medium: { width: 600 },
}}
ratio={imageRatio}
alt={image.title}
/>
{:else if video}
<video muted loop playsinline autoplay allow="autoplay" bind:this={videoEl}>
<source type="video/mp4" src={getAssetUrlKey(video.mp4, 'step')} />
<source type="video/webm" src={getAssetUrlKey(video.webm, 'step')} />
<track kind="captions" />
</video>
{/if}
</div>
{/if}
<div class="text text-xsmall">
{@html text}
</div>
</div>
{/if}

View File

@@ -0,0 +1,51 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
interface Option {
value: string
name: string
default?: boolean
selected?: boolean
}
export let id: string
export let name: string
export let options: Option[]
export let value: string = undefined
const dispatch = createEventDispatcher()
const defaultOption = options.find(option => option.default)
let selected = value || options[0].value
$: currentOption = options.find(option => option.value === selected)
// Redefine value from parent (when reset)
$: if (value === defaultOption.value) {
selected = defaultOption.value
}
/**
* When changing select value
*/
const handleChange = ({ target: { value }}: any) => {
const option = options.find(option => option.value === value)
// Dispatch event to parent
dispatch('change', option.value)
}
</script>
<div class="select">
<slot />
<span>{currentOption.name}</span>
<select {name} {id} bind:value={selected} on:change={handleChange}>
{#each options as { value, name }}
<option {value} selected={value === selected}>
{name}
</option>
{/each}
</select>
</div>

View File

@@ -0,0 +1,45 @@
<style lang="scss">
@import "../../style/molecules/shop-locationswitcher";
</style>
<script lang="ts">
import { goto } from '$app/navigation'
import { getContext, tick } from 'svelte'
import { shopCurrentProductSlug } from '$utils/stores/shop'
import { smoothScroll } from '$utils/stores'
export let isOver: boolean = false
const { shopLocations }: any = getContext('shop')
const classes = [
'shop-locationswitcher',
isOver && 'is-over',
$$props.class
].join(' ').trim()
// Quick location change
const quickLocationChange = async ({ target: { value }}: any) => {
const pathTo = `/shop/poster-${value}`
goto(pathTo, { replaceState: true, noscroll: true, keepfocus: true })
// Scroll to anchor
await tick()
$smoothScroll.scrollTo('#poster', { duration: 2 })
}
</script>
<dl class={classes}>
<dt class="text-label">Choose a city</dt>
<dd>
<svg width="18" height="18">
<use xlink:href="#icon-map-pin" />
</svg>
<select on:change={quickLocationChange}>
{#each shopLocations as { name, slug }}
<option value={slug} selected={slug === $shopCurrentProductSlug}>{name}</option>
{/each}
</select>
</dd>
</dl>

View File

@@ -0,0 +1,69 @@
<style lang="scss">
@import "../../style/molecules/switcher";
</style>
<script lang="ts">
import { page } from '$app/stores'
import { getContext } from 'svelte'
import reveal from '$animations/reveal'
// Components
import Icon from '$components/atoms/Icon.svelte'
const { settings: { switcher_links }}: any = getContext('global')
let switcherEl: HTMLElement
let isOpen = false
/**
* Toggle switcher open state
*/
const toggleSwitcher = () => {
isOpen = !isOpen
}
/**
* Detect outside click
*/
const windowClick = ({ target }) => {
if (!switcherEl.contains(target) && isOpen) {
// Close switcher
toggleSwitcher()
}
}
</script>
<svelte:window on:click={windowClick} />
<aside class="switcher" bind:this={switcherEl}
class:is-open={isOpen}
use:reveal={{
animation: { y: [24, 0], opacity: [0, 1] },
options: {
duration: 1,
delay: 0.6,
threshold: 0,
},
}}
>
<button class="switcher__button" title="{!isOpen ? 'Open' : 'Close'} menu" tabindex="0"
on:click={toggleSwitcher}
>
<span>
{#each Array(3) as _}
<i />
{/each}
</span>
</button>
<ul class="switcher__links" data-sveltekit-noscroll data-sveltekit-prefetch>
{#each switcher_links as { text, url, icon, icon_label }}
<li class:is-active={$page.url.pathname === url}>
<a href={url} on:click={toggleSwitcher} tabindex="0">
<Icon class="icon" icon={icon} label={icon_label} />
<span>{text}</span>
</a>
</li>
{/each}
</ul>
</aside>

View File

@@ -0,0 +1,42 @@
<style lang="scss">
@import "../../style/organisms/banner";
</style>
<script lang="ts">
// Components
import Image from '$components/atoms/Image.svelte'
export let title: string
export let image: any
export let back: boolean = false
</script>
<section class="banner">
<div class="banner__top container">
{#if back}
<a href="/" class="back" data-sveltekit-noscroll>
<svg width="5" height="8" viewBox="0 0 5 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 1 1 4l3 3" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>Back to Houses Of</span>
</a>
{/if}
</div>
<div class="banner__title">
<h1 class="title-big-sans">{title}</h1>
</div>
<Image
class="banner__background"
id={image.id}
alt={image.alt}
sizeKey="hero"
sizes={{
large: { width: 1800, height: 1200 },
medium: { width: 1200, height: 800 },
small: { width: 700, height: 700 },
}}
lazy={false}
/>
</section>

View File

@@ -0,0 +1,129 @@
<style lang="scss">
@import "../../style/organisms/carousel";
</style>
<script lang="ts">
import { onMount } from 'svelte'
import { writable } from 'svelte/store'
import EmblaCarousel, { type EmblaCarouselType } from 'embla-carousel'
// Components
import Image from '$components/atoms/Image.svelte'
export let slides: any
let carouselEl: HTMLElement
let carousel: EmblaCarouselType
let currentSlide = 0
let arrowDirection: string = null
$: isFirstSlide = currentSlide === 0
$: isLastSlide = currentSlide === slides.length - 1
/** Navigate to specific slide */
const goToSlide = (index: number = 0) => carousel.scrollTo(index)
/** Move and change arrow direction when moving */
const arrowPosition = writable({ x: 0, y: 0 })
/** Move arrow and define direction on mousemove */
const handleArrowMove = (event: MouseEvent) => {
const { left, top, width } = carouselEl.getBoundingClientRect()
const offsetX = event.clientX - left
const offsetY = event.clientY - top
// Define direction
if (isFirstSlide) {
arrowDirection = 'next'
} else if (isLastSlide) {
arrowDirection = 'prev'
} else {
arrowDirection = offsetX < Math.round(width / 2) ? 'prev' : 'next'
}
// Move arrow
arrowPosition.set({
x: offsetX - 12,
y: offsetY - 56,
})
}
/** Go to prev or next slide depending on direction */
const handleArrowClick = () => {
if (!carousel.clickAllowed()) return
// Define direction
if (isFirstSlide) {
arrowDirection = 'next'
} else if (isLastSlide) {
arrowDirection = 'prev'
}
// Click only if carousel if being dragged
if (arrowDirection === 'prev') {
carousel.scrollPrev()
} else {
carousel.scrollNext()
}
}
onMount(() => {
// Init carousel
carousel = EmblaCarousel(carouselEl, {
loop: false
})
carousel.on('select', () => {
currentSlide = carousel.selectedScrollSnap()
})
// Destroy
return () => {
carousel.destroy()
}
})
</script>
<div class="carousel {$$props.class ?? ''}">
{#if slides.length}
<div class="carousel__viewport" bind:this={carouselEl}
on:mousemove={handleArrowMove}
on:click={handleArrowClick}
>
<div class="carousel__slides">
{#each slides as { id, alt }}
<Image
class="carousel__slide"
id={id}
sizeKey="product"
sizes={{
small: { width: 300 },
medium: { width: 550 },
large: { width: 800 },
}}
ratio={1.5}
alt={alt}
/>
{/each}
</div>
</div>
<ul class="carousel__dots">
{#each slides as _, index}
<li class:is-active={index === currentSlide}>
<button on:click={() => goToSlide(index)} aria-label="Go to slide #{index + 1}" />
</li>
{/each}
</ul>
<span class="carousel__arrow"
style:--x="{$arrowPosition.x}px"
style:--y="{$arrowPosition.y}px"
class:is-flipped={arrowDirection === 'prev' && !isFirstSlide || isLastSlide}
>
<svg width="29" height="32">
<use xlink:href="#arrow" />
</svg>
</span>
{/if}
</div>

View File

@@ -0,0 +1,139 @@
<style lang="scss">
@import "../../style/organisms/cart";
</style>
<script lang="ts">
import { onMount } from 'svelte'
import { fade, fly } from 'svelte/transition'
import { quartOut } from 'svelte/easing'
import { cartOpen, cartData, cartAmount, cartIsUpdating } from '$utils/stores/shop'
import { initSwell, getCart, updateCartItem, removeCartItem } from '$utils/functions/shop'
// Components
import Button from '$components/atoms/Button.svelte'
import Icon from '$components/atoms/Icon.svelte'
import CartItem from '$components/molecules/CartItem.svelte'
import ShopLocationSwitcher from '$components/molecules/ShopLocationSwitcher.svelte'
// Closing the cart
const handleCloseCart = () => {
$cartOpen = false
}
// Item quantity changed
const changedQuantity = async ({ detail: { id, quantity } }) => {
// Cart is now updating
$cartIsUpdating = true
// Update cart item
const updatedCart = await updateCartItem(id, quantity)
if (updatedCart) {
// Store new cart data
$cartData = updatedCart
// Cart is updated
$cartIsUpdating = false
}
}
// Item removed
const removedItem = async ({ detail: id }) => {
// Cart is now updating
$cartIsUpdating = true
// Remove item from cart
const updatedCart = await removeCartItem(id)
if (updatedCart) {
// Store new cart data
$cartData = updatedCart
// Cart is updated
$cartIsUpdating = false
}
}
onMount(async () => {
// Init Swell
initSwell()
// Fetch cart
const cart = await getCart()
if (cart) {
// Store cart data
$cartData = cart
}
})
</script>
{#if $cartOpen}
<div class="cart-switcher" transition:fly={{ y: -24, duration: 1000, easing: quartOut }}>
<ShopLocationSwitcher isOver={true} />
</div>
<aside class="cart shadow-box-dark"
class:is-updating={$cartIsUpdating}
transition:fly={{ x: 48, duration: 600, easing: quartOut }}
>
<header class="cart__heading">
<h2>Cart</h2>
<button class="text-label" on:click={handleCloseCart}>Close</button>
</header>
<div class="cart__content">
{#if $cartAmount > 0}
{#each $cartData.items as item}
<CartItem {item}
on:updatedQuantity={changedQuantity}
on:removed={removedItem}
/>
{/each}
{:else}
<div class="cart__empty shadow-small" transition:fade={{ duration: 600 }}>
<div class="icon">
<Icon icon="bag" label="Shopping bag icon" />
</div>
<p>Your cart is empty</p>
</div>
{/if}
{#if $cartIsUpdating}
<div class="cart__update"
in:fly={{ y: 8, duration: 600, easing: quartOut }}
out:fly={{ y: -8, duration: 600, easing: quartOut }}
>
<p>Updating…</p>
</div>
{/if}
</div>
<footer class="cart__total">
<div class="cart__total--sum">
<h3>Total</h3>
{#if $cartData}
<span>{$cartAmount} item{$cartAmount > 1 ? 's' : ''}</span>
<p>{$cartData.sub_total ? $cartData.sub_total : 0}</p>
{:else}
<span>0 item</span>
<p>0€</p>
{/if}
</div>
<div class="cart__total--checkout">
<p>Shipping will be calculated from the delivery address during the checkout process</p>
{#if $cartData && $cartAmount > 0 && $cartData.checkout_url}
<div transition:fly={{ y: 8, duration: 600, easing: quartOut }}>
<Button
url={$cartData && $cartData.checkout_url}
text="Checkout"
color="pink"
size="small"
/>
</div>
{/if}
</div>
</footer>
</aside>
<div class="cart-overlay"
transition:fade={{ duration: 600, easing: quartOut }}
on:click={handleCloseCart}
/>
{/if}

View File

@@ -0,0 +1,29 @@
<style lang="scss">
@import "../../style/organisms/collage";
</style>
<script lang="ts">
import PhotoCard from '$components/molecules/PhotoCard.svelte'
export let photos: any[] = []
let hovered: number = null
</script>
{#if photos}
<div class="collage" class:is-hovering={hovered !== null}>
{#each photos as { slug, title, image, location, city }, index}
<PhotoCard
id={image.id}
alt={title}
url="/{location.country.slug}/{location.slug}/{slug}"
title={title}
location={location}
city={city}
hovered={hovered === index}
lazy={false}
on:hover={({ detail }) => hovered = detail ? index : null}
/>
{/each}
</div>
{/if}

View File

@@ -0,0 +1,45 @@
<style lang="scss">
@import "../../style/organisms/footer";
</style>
<script lang="ts">
import { getContext } from 'svelte'
// Components
import SplitText from '$components/SplitText.svelte'
import SiteTitle from '$components/atoms/SiteTitle.svelte'
const { settings: { instagram, footer_links }}: any = getContext('global')
</script>
<footer class="footer">
<div class="container grid">
<a href="/" class="footer__title" data-sveltekit-prefetch data-sveltekit-noscroll tabindex="0">
<SiteTitle tag="div" />
</a>
<nav class="footer__links">
<ul data-sveltekit-prefetch data-sveltekit-noscroll>
{#each footer_links as { title, slug }}
<li>
<a href="/{slug}" class="link-3d" tabindex="0">
<SplitText text={title} clone={true} />
</a>
</li>
{/each}
<li class="instagram">
<a href="https://www.instagram.com/{instagram}" target="_blank" rel="noopener noreferrer external" class="link-3d">
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 1.8c2.67 0 2.99.01 4.04.06.98.04 1.5.2 1.86.34a3.27 3.27 0 0 1 1.9 1.9c.13.35.3.88.34 1.86.05 1.05.06 1.37.06 4.04s-.01 2.99-.06 4.04c-.04.98-.2 1.5-.34 1.86-.19.46-.4.8-.75 1.15a3.1 3.1 0 0 1-1.15.75c-.35.13-.88.3-1.86.34-1.05.05-1.37.06-4.04.06s-2.99-.01-4.04-.06c-.98-.04-1.5-.2-1.86-.34a3.1 3.1 0 0 1-1.15-.75 3.1 3.1 0 0 1-.75-1.15 5.6 5.6 0 0 1-.34-1.86A69.42 69.42 0 0 1 1.8 10c0-2.67.01-2.99.06-4.04.04-.98.2-1.5.34-1.86.19-.46.4-.8.75-1.15A3.1 3.1 0 0 1 4.1 2.2c.35-.13.88-.3 1.86-.34C7 1.81 7.33 1.8 10 1.8ZM10 0C7.28 0 6.94.01 5.88.06 4.8.11 4.08.28 3.45.52a4.9 4.9 0 0 0-1.77 1.16A4.9 4.9 0 0 0 .52 3.45a7.34 7.34 0 0 0-.46 2.43C.01 6.94 0 7.28 0 10s.01 3.06.06 4.12c.05 1.07.22 1.8.46 2.43.26.66.6 1.22 1.16 1.77.55.56 1.11.9 1.77 1.16a7.6 7.6 0 0 0 2.43.46c1.06.05 1.4.06 4.12.06s3.06-.01 4.12-.06a7.34 7.34 0 0 0 2.43-.46 4.9 4.9 0 0 0 1.77-1.16 4.9 4.9 0 0 0 1.16-1.77 7.6 7.6 0 0 0 .46-2.43c.05-1.06.06-1.4.06-4.12s-.01-3.06-.06-4.12a7.34 7.34 0 0 0-.46-2.43 4.9 4.9 0 0 0-1.16-1.77A4.9 4.9 0 0 0 16.55.52a7.34 7.34 0 0 0-2.43-.46C13.06.01 12.72 0 10 0Zm0 4.86a5.14 5.14 0 1 0 0 10.28 5.14 5.14 0 0 0 0-10.28Zm0 8.47a3.33 3.33 0 1 1 0-6.66 3.33 3.33 0 0 1 0 6.66Zm5.34-7.47a1.2 1.2 0 1 0 0-2.4 1.2 1.2 0 0 0 0 2.4Z" />
</svg>
<SplitText
class="instagram__text"
text="Instagram"
clone={true}
/>
</a>
</li>
</ul>
</nav>
</div>
</footer>

View File

@@ -0,0 +1,165 @@
<style lang="scss">
@import "../../style/modules/globe";
</style>
<script lang="ts">
import { getContext, onMount } from 'svelte'
import { fade, fly as flySvelte } from 'svelte/transition'
import { quartOut } from 'svelte/easing'
import { Globe, type Marker } from '$modules/globe'
import { getRandomItem, debounce } from '$utils/functions'
import reveal from '$animations/reveal'
// Components
import SplitText from '$components/SplitText.svelte'
const isDev = import.meta.env.DEV
export let type: string = undefined
export let autoRotate: boolean = true
export let enableMarkers: boolean = true
export let enableMarkersLinks: boolean = true
export let speed: number = 0.1
export let pane: boolean = isDev
export let width: number = undefined
let innerWidth: number
let globeParentEl: HTMLElement, globeEl: HTMLElement
let globe: any
let observer: IntersectionObserver
let animation: number
let hoveredMarker: { name: string, country: string } = null
const { continents, locations }: any = getContext('global')
const randomContinent: any = getRandomItem(continents)
const markers = locations.map(({ name, slug, country, coordinates: { coordinates }}): Marker => ({
name,
slug,
country: { ...country },
lat: coordinates[1],
lng: coordinates[0],
}))
onMount(() => {
const globeResolution = innerWidth > 1440 && window.devicePixelRatio > 1 ? 4 : 2
globe = new Globe({
el: globeEl,
parent: globeParentEl,
mapFile: `/images/globe-map-${globeResolution}k.png`,
mapFileDark: `/images/globe-map-dark-${globeResolution}k.png`,
dpr: Math.min(Math.round(window.devicePixelRatio), 2),
autoRotate,
speed,
sunAngle: 2,
rotationStart: {
x: randomContinent.rotation_x,
y: randomContinent.rotation_y,
},
enableMarkers,
enableMarkersLinks: enableMarkersLinks && type !== 'cropped',
markers,
pane,
})
resize()
// Render only if in viewport
observer = new IntersectionObserver(([{ isIntersecting }]) => {
if (isIntersecting) {
update()
if (isDev) {
console.log('globe: render/start')
}
} else {
stop()
if (isDev) {
console.log('globe: render/stop')
}
}
}, { threshold: 0 })
observer.observe(globeEl)
// Destroy
return () => {
destroy()
observer && observer.disconnect()
}
})
/**
* Methods
*/
// Update
const update = () => {
animation = requestAnimationFrame(update)
globe.render()
}
// Stop
const stop = () => {
cancelAnimationFrame(animation)
}
// Resize
const resize = debounce(() => {
globe.resize()
}, 100)
// Destroy
const destroy = () => {
stop()
globe.destroy()
}
</script>
<svelte:window bind:innerWidth
on:resize={resize}
/>
<div class="globe" bind:this={globeParentEl}
class:is-cropped={type === 'cropped'}
style:--width={width ? `${width}px` : null}
>
<div class="globe__canvas" bind:this={globeEl}
class:is-faded={hoveredMarker}
>
<ul class="globe__markers">
{#each markers as { name, slug, country, lat, lng }}
<li class="globe__marker" data-location={slug} data-lat={lat} data-lng={lng}>
<a href="/{country.slug}/{slug}" data-sveltekit-noscroll
on:mouseenter={() => hoveredMarker = { name, country: country.name }}
on:mouseleave={() => hoveredMarker = null}
>
<i />
<span>{name}</span>
</a>
</li>
{/each}
</ul>
</div>
{#if hoveredMarker}
<div class="globe__location"
transition:fade={{ duration: 300, easing: quartOut }}
use:reveal={{
children: '.char',
animation: { y: ['110%', 0] },
options: {
stagger: 0.04,
duration: 1,
threshold: 0,
},
}}
>
<SplitText text={hoveredMarker.name} mode="chars" class="name" />
<p class="country" in:flySvelte={{ y: 16, duration: 800, easing: quartOut, delay: 900 }}>
{hoveredMarker.country}
</p>
</div>
{/if}
</div>

View File

@@ -0,0 +1,24 @@
<style lang="scss">
.list-cta {
@include bp (sm) {
display: flex;
justify-content: center;
}
:global(li) {
display: block;
}
& > :global(*) {
margin: 20px auto 0;
@include bp (sm) {
margin: 0 16px;
}
}
}
</style>
<ul class="list-cta" data-sveltekit-noscroll>
<slot />
</ul>

View File

@@ -0,0 +1,92 @@
<style lang="scss">
@import "../../style/organisms/locations";
</style>
<script lang="ts">
import { getContext } from 'svelte'
import { flip } from 'svelte/animate'
import { quartOut } from 'svelte/easing'
import reveal from '$animations/reveal'
import { send, receive } from '$animations/crossfade'
import { throttle } from '$utils/functions'
import { sendEvent } from '$utils/analytics'
// Components
import Button from '$components/atoms/Button.svelte'
import Location from '$components/molecules/Location.svelte'
export let locations: any[]
const { continents, settings: { explore_list }}: any = getContext('global')
// Continents filtering logic
let currentContinent: string = undefined
$: filteredLocations = locations.filter(({ country: { continent }}: any) => {
if (!currentContinent) {
// Show all locations by default
return true
} else {
// Location's continent matches the clicked continent
return continent.slug === currentContinent
}
})
/**
* Filter locations from continent
*/
const filterLocation = throttle((continent: string) => {
currentContinent = continent !== currentContinent ? continent : null
}, 600)
</script>
<div class="browse">
<div class="browse__description">
<p>{explore_list}</p>
</div>
<ul class="browse__continents">
{#each continents as { name, slug }}
<li class:is-active={currentContinent === slug}>
<Button
tag="button" text={name} size="small"
slotPosition="after"
class={'is-disabled'}
on:click={() => {
filterLocation(slug)
sendEvent({ action: 'filterContinent' })
}}
>
<svg width="12" height="12">
<use xlink:href="#cross" />
</svg>
</Button>
</li>
{/each}
</ul>
<ul class="browse__locations" data-sveltekit-noscroll
use:reveal={{
children: '.location',
animation: { y: ['20%', 0], opacity: [0, 1] },
options: {
stagger: 0.105,
duration: 1,
threshold: 0.3,
},
}}
>
{#each filteredLocations as location (location)}
<li
animate:flip={{ duration: 1000, easing: quartOut }}
in:receive={{ key: location.slug }}
out:send={{ key: location.slug }}
>
<Location
location={location}
latestPhoto={location.photos[0]}
/>
</li>
{/each}
</ul>
</div>

View File

@@ -0,0 +1,24 @@
<style lang="scss">
@import "../../style/organisms/newsletter";
</style>
<script lang="ts">
import { getContext } from 'svelte'
// Components
import EmailForm from '$components/molecules/EmailForm.svelte'
export let theme: string = 'default'
const { settings: { newsletter_text, newsletter_subtitle }}: any = getContext('global')
</script>
<div class="newsletter newsletter--{theme} shadow-box-dark">
<div class="newsletter__wrapper">
<h3 class="title-medium">
<label for="newsletter_email">{newsletter_subtitle}</label>
</h3>
<p class="text-small">{newsletter_text}</p>
<EmailForm past={true} />
</div>
</div>

View File

@@ -0,0 +1,121 @@
<style lang="scss">
@import "../../style/pages/shop/posters";
</style>
<script lang="ts">
import { getContext, onMount } from 'svelte'
import EmblaCarousel, { type EmblaCarouselType } from 'embla-carousel'
// Components
import Poster from '$components/molecules/Poster.svelte'
import EmailForm from '$components/molecules/EmailForm.svelte'
import { debounce } from '$utils/functions'
export let posters: any = []
let innerWidth: number
let carouselEl: HTMLElement
let carousel: EmblaCarouselType
let currentSlide = 0
let carouselDots = []
const { shopProducts }: any = getContext('shop')
/** Navigate to specific slide */
const goToSlide = (index: number = 0) => {
carousel.scrollTo(index)
}
/** Init Carousel */
const initCarousel = () => {
if (innerWidth < 1200) {
if (!carousel) {
carousel = EmblaCarousel(carouselEl, {
slidesToScroll: innerWidth < 550 ? 1 : 2,
})
// On init
carousel.on('init', () => {
// Define amounts of dots
carouselDots = carousel.scrollSnapList()
})
// On slide change
carousel.on('select', () => {
// Define current slide
currentSlide = carousel.selectedScrollSnap()
})
// On resize
carousel.on('resize', () => {
// Redefine options
carousel.reInit({
slidesToScroll: innerWidth < 550 ? 1 : 2
})
// Define amounts of dots
carouselDots = carousel.scrollSnapList()
// Define current slide
currentSlide = carousel.selectedScrollSnap()
})
}
} else {
if (carousel) {
destroyCarousel()
}
}
}
/** Destroy carousel */
const destroyCarousel = () => {
carousel.destroy()
carousel = undefined
}
/** Destroy carousel for larger screens */
const handleResize = debounce(initCarousel, 200)
onMount(() => {
if (innerWidth < 1200) {
initCarousel()
}
// Destroy
return () => {
if (carousel) {
carousel.destroy()
}
}
})
</script>
<svelte:window
bind:innerWidth
on:resize={handleResize}
/>
<section class="shop-page__posters grid">
<h3>View all of our available posters</h3>
{#if posters}
<div class="set" bind:this={carouselEl}>
<div class="set__content">
{#each posters as { location, photos_product }}
<Poster
location={location}
image={photos_product.length && photos_product[1].directus_files_id}
product={shopProducts.find(item => item.slug.includes(location.slug))}
/>
{/each}
</div>
{#if carousel}
<ul class="set__dots">
{#each carouselDots as _, index}
<li class:is-active={index === currentSlide}>
<button on:click={() => goToSlide(index)} aria-label="Go to slide #{index + 1}" />
</li>
{/each}
</ul>
{/if}
</div>
{/if}
</section>

View File

@@ -0,0 +1,160 @@
<style lang="scss">
@import "../../style/pages/shop/intro";
</style>
<script lang="ts">
import { navigating } from '$app/stores'
import { getContext, onMount } from 'svelte'
import { stagger, timeline } from 'motion'
import { smoothScroll } from '$utils/stores'
import { cartOpen } from '$utils/stores/shop'
import { DELAY } from '$utils/contants'
import { quartOut } from '$animations/easings'
// Components
import Image from '$components/atoms/Image.svelte'
import ButtonCart from '$components/atoms/ButtonCart.svelte'
import ShopLocationSwitcher from '$components/molecules/ShopLocationSwitcher.svelte'
export let product: any = undefined
const { shop, shopLocations }: any = getContext('shop')
let navObserver: IntersectionObserver
let introEl: HTMLElement, navChooseEl: HTMLElement
let scrolledPastIntro = false
onMount(() => {
// Reveal the nav past the Intro
navObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
scrolledPastIntro = !entry.isIntersecting
})
}, {
threshold: 0,
rootMargin: '-3% 0px 0px'
})
navObserver.observe(introEl)
// Set navigation horizontal scroll depending on current link position
const navChooseActive: HTMLElement = navChooseEl.querySelector('.is-active')
const offsetLeft = navChooseActive.offsetLeft
if (offsetLeft > window.innerWidth / 2) {
navChooseEl.scrollLeft = offsetLeft
}
/**
* Animations
*/
const animation = timeline([
// Hero image
['.shop-page__background', {
scale: [1.06, 1],
opacity: [0, 1],
z: 0,
}, {
at: 0.4,
duration: 2.4,
}],
// Intro top elements
['.shop-page__intro .top > *', {
y: [-100, 0],
opacity: [0, 1],
}, {
at: 0.4,
delay: stagger(0.25),
}],
// Hero title
['.shop-page__title h1', {
y: [32, 0],
opacity: [0, 1],
}, {
at: 0.5,
}],
// Intro navbar
['.shop-page__nav .container > *, .shop-page__intro .button-cart', {
y: [100, 0],
opacity: [0, 1],
}, {
at: 0.7,
delay: stagger(0.25),
}]
], {
delay: $navigating ? DELAY.PAGE_LOADING / 1000 : 0,
defaultOptions: {
duration: 1.6,
easing: quartOut,
},
})
animation.stop()
// Run animation
requestAnimationFrame(animation.play)
// Destroy
return () => {
navObserver && navObserver.disconnect()
}
})
</script>
<section class="shop-page__intro" bind:this={introEl}>
<div class="top container">
<a href="/" class="back" data-sveltekit-noscroll>
<svg width="5" height="8" viewBox="0 0 5 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 1 1 4l3 3" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>Back to Houses Of</span>
</a>
</div>
<div class="shop-page__title">
<h1 class="title-big-sans">Shop</h1>
</div>
<nav class="shop-page__nav">
<div class="container">
<p class="text-label">Choose a city</p>
<nav>
<ul data-sveltekit-noscroll data-sveltekit-prefetch bind:this={navChooseEl}>
{#each shopLocations as { name, slug }}
<li class:is-active={product && slug === product.location.slug}>
<a href="/shop/poster-{slug}" on:click={() => $smoothScroll.scrollTo('#poster', { duration: 2 })}>
{name}
</a>
</li>
{/each}
</ul>
</nav>
</div>
</nav>
<ButtonCart />
<Image
class="shop-page__background"
id={shop.page_heroimage.id}
alt={shop.page_heroimage.alt}
sizeKey="hero"
sizes={{
large: { width: 1800, height: 1200 },
medium: { width: 1200, height: 800 },
small: { width: 700, height: 700 },
}}
lazy={false}
/>
</section>
<nav class="shop-location"
class:is-visible={scrolledPastIntro}
class:is-overlaid={$cartOpen}
>
<ShopLocationSwitcher />
<ButtonCart />
</nav>

View File

@@ -0,0 +1,93 @@
<style lang="scss">
@import "../../style/organisms/shop";
</style>
<script lang="ts">
import { getContext, onMount } from 'svelte'
// Components
import Button from '$components/atoms/Button.svelte'
import Image from '$components/atoms/Image.svelte'
const { locations, shop }: any = getContext('global')
const locationsWithPoster = locations
// Filter locations with posters only
.filter((loc: Location) => loc.has_poster)
// Sort locations alphabetically from slug (a>z)
.sort((a: Location, b: Location) => a.slug.localeCompare(b.slug))
// Return name only
.map((loc: Location) => loc.name)
export let images: any[] = shop.module_images
export let title: string = shop.module_title
export let text: string = shop.module_text
export let textBottom: string = undefined
export let buttonText: string = 'Shop'
export let url: string = '/shop'
export let enabled: boolean = true
if (textBottom !== null) {
textBottom = `Posters available for ${locationsWithPoster.join(', ').replace(/,(?!.*,)/gmi, ' and')}.`
}
interface Location {
slug: string
name: string
has_poster: boolean
}
// Image rotation
let imagesLoop: ReturnType<typeof setTimeout>
let currentImageIndex = 0
const incrementCurrentImageIndex = () => {
currentImageIndex = currentImageIndex === images.length - 1 ? 0 : currentImageIndex + 1
imagesLoop = setTimeout(() => requestAnimationFrame(incrementCurrentImageIndex), 3000)
}
onMount(() => {
if (images.length > 1) {
incrementCurrentImageIndex()
}
return () => {
// Clear rotating words timeout
if (imagesLoop) {
clearTimeout(imagesLoop)
}
}
})
</script>
<div class="shop shadow-box-dark" class:has-no-bottom={!textBottom}>
<div class="content">
<div class="shop__images">
{#if images}
<a href={enabled ? url : undefined} title="Visit our shop" data-sveltekit-noscroll data-sveltekit-prefetch>
{#each images as { directus_files_id: { id, title }}, index}
<Image
class={index === currentImageIndex ? 'is-visible' : null}
{id}
sizeKey="square"
sizes={{
small: { width: 400, height: 400 },
large: { width: 800, height: 800 },
}}
alt={title}
/>
{/each}
</a>
{/if}
</div>
<div class="shop__content">
<h3 class="title-medium">{title}</h3>
<p class="text-small">{text}</p>
{#if enabled}
<Button {url} text={buttonText} color="pinklight" />
{/if}
{#if textBottom}
<p class="detail">{textBottom}</p>
{/if}
</div>
</div>
</div>

View File

@@ -1,26 +0,0 @@
class ArrayBuffer {
constructor(gl, data, size, element, name) {
this.name = name;
this.gl = gl;
this._buffer = this.gl.createBuffer();
this.type = this.gl.FLOAT;
this._target = this.gl[ element ? 'ELEMENT_ARRAY_BUFFER' : 'ARRAY_BUFFER' ];
this.update(data, size);
}
update(data, size) {
this.data = data;
this.size = size;
this.length = this.data.length;
this.gl.bindBuffer( this._target, this._buffer);
this.gl.bufferData( this._target, this.data, this.gl.STATIC_DRAW);
}
bind() {
this.gl.bindBuffer( this._target, this._buffer );
}
}
export default ArrayBuffer;

View File

@@ -1,336 +0,0 @@
import Object3d from './Object3d';
import Container from './Container';
import * as mat4 from './glMatrix/mat4';
import * as vec3 from './glMatrix/vec3';
import * as quat from './glMatrix/quat';
const TOUCH = ('ontouchstart' in window) || (navigator.msMaxTouchPoints > 0);
const POINTER = !!window.navigator.pointerEnabled;
const MS_POINTER = !!window.navigator.msPointerEnabled;
const POINTER_DOWN = TOUCH ? 'touchstart' : (POINTER ? 'pointerdown' : (MS_POINTER ? 'MSPointerDown' : 'mousedown' ) );
const POINTER_MOVE = TOUCH ? 'touchmove' : (POINTER ? 'pointermove' : (MS_POINTER ? 'MSPointerMove' : 'mousemove' ) );
const POINTER_UP = TOUCH ? 'touchend' : (POINTER ? 'pointerup' : (MS_POINTER ? 'MSPointerUp' : 'mouseup' ) );
var objects = []
function needsUpdateLoop() {
requestAnimationFrame(needsUpdateLoop);
for (let i=0; i<objects.length; i++) {
objects[i]._canUpdate = true;
}
}
needsUpdateLoop();
class Camera extends Object3d {
constructor(options) {
super();
options = Object.assign({},{
fov: 45,
aspect: window.innerWidth / window.innerHeight,
near: 10,
far: 1000,
type: 'perspective',
left: 0,
right: 0,
top: 0,
bottom: 0,
orbitControl: false,
lookAt: null,
pointerParent: document,
firstPerson: false,
moveSpeed: 20,
distance: 20,
wheel: true,
position: [0,0,0]
}, options);
this.fov = options.fov;
this.aspect = options.aspect;
this.near = options.near;
this.far = options.far;
this.type = options.type;
this.left = options.left;
this.right = options.right;
this.top = options.top;
this.bottom = options.bottom;
this.orbitControl = options.orbitControl;
this.firstPerson = options.firstPerson;
this.wheel = options.wheel;
this.projectionMatrix = mat4.create();
this.updateProjectionMatrix();
if ( this.orbitControl || this.firstPerson ) {
if (!this.lookAt) {
this.lookAt = vec3.create();
vec3.set(this.lookAt, 0,0,0 );
}
this._pointerParent = options.pointerParent
this._initPointerEvents();
this._cameraDistance = options.position[2];
this._canUpdate = true;
objects.push(this);
}
if (this.firstPerson) {
document.addEventListener("contextmenu", this.onContextMenu.bind(this), false);
document.addEventListener("keydown", this.onKeyDown.bind(this), false);
document.addEventListener("keyup", this.onKeyUp.bind(this), false);
}
this.pitchObject = new Container();
this.yawObject = new Container();
this.yawObject.add( this.pitchObject );
this.moveSpeed = options.moveSpeed;
this.time = Date.now();
this._velocity = vec3.create();
this._moveForward = false;
this._moveBackward = false;
this._moveLeft = false;
this._moveRight = false;
this._moveUp = false;
this._camera = vec3.create()
this._oldPosition = vec3.create();
}
updateProjectionMatrix() {
if (this.type == 'perspective') {
mat4.perspective(this.projectionMatrix, this.fov * Math.PI/180.0, this.aspect, this.near, this.far);
}
else if(this.type == 'orthographic' || this.type == 'ortho'){
mat4.ortho(this.projectionMatrix, this.left, this.right, this.bottom, this.top, this.near, this.far);
}
}
_initPointerEvents() {
this.winWidth = window.innerWidth;
this.winHeight = window.innerHeight;
this._isPointerDown = false;
this.isRightClick = false;
this.pointerXMove = 0;
this.pointerYMove = 0;
this.pointerX = 0;
this.pointerY = 0;
this.pointerZ = 0;
this.lastPointerX = 0;
this.lastPointerY = 0;
this.lastPointerZ = 0;
this.theta = 0;//Math.PI/2;
this.phi = 0;
this.thetaDown = 0;
this.phiDown = 0;
this.currTheta = 0;
this.currPhi = 0;
this._minPolarAngle = Math.PI * -.5; // radians
this._maxPolarAngle = Math.PI * .5; // radians
this._onPointerDown = this._onPointerDown.bind(this);
this._onPointerMove = this._onPointerMove.bind(this);
this._onPointerUp = this._onPointerUp.bind(this);
this._onMouseWheel = this._onMouseWheel.bind(this);
this.onContextMenu = this.onContextMenu.bind(this);
this._pointerParent.addEventListener(POINTER_DOWN, this._onPointerDown, false);
document.addEventListener(POINTER_MOVE, this._onPointerMove, false);
document.addEventListener(POINTER_UP, this._onPointerUp, false);
this._pointerParent.addEventListener( "contextmenu", this.onContextMenu, false);
if (this.wheel) {
this._pointerParent.addEventListener( 'DOMMouseScroll', this._onMouseWheel, false );
this._pointerParent.addEventListener( 'mousewheel', this._onMouseWheel, false );
}
}
delete() {
this._pointerParent.removeEventListener(POINTER_DOWN, this._onPointerDown, false);
document.removeEventListener(POINTER_MOVE, this._onPointerMove, false);
document.removeEventListener(POINTER_UP, this._onPointerUp, false);
this._pointerParent.removeEventListener( "contextmenu", this.onContextMenu, false);
if (this.wheel) {
this._pointerParent.removeEventListener( 'DOMMouseScroll', this._onMouseWheel, false );
this._pointerParent.removeEventListener( 'mousewheel', this._onMouseWheel, false );
}
}
onContextMenu(e) {
event.preventDefault();
}
onKeyDown(event) {
switch ( event.keyCode ) {
// case 38: // up
case 87: // w
this._moveForward = true;
break;
// case 37: // left
case 65: // a
this._moveLeft = true; break;
// case 40: // down
case 83: // s
this._moveBackward = true;
break;
// case 39: // right
case 68: // d
this._moveRight = true;
break;
case 32: // space
this._velocity[1] += 5;
break;
}
}
onKeyUp(event) {
switch( event.keyCode ) {
case 38: // up
case 87: // w
this._moveForward = false;
break;
case 37: // left
case 65: // a
this._moveLeft = false;
break;
case 40: // down
case 83: // s
this._moveBackward = false;
break;
case 39: // right
case 68: // d
this._moveRight = false;
break;
}
}
_onMouseWheel(e) {
// e.preventDefault();
e.stopPropagation();
var wheelDeltaX, wheelDeltaY;
if ( e.wheelDelta ){
wheelDeltaX = wheelDeltaY = e.wheelDelta; //6 or 12
} else if ( e.detail ){
wheelDeltaX = wheelDeltaY = -e.detail * 40; // *3
} else if ( e.wheelDeltaX ) {
wheelDeltaY = e.wheelDeltaY/12;
wheelDeltaX = -1 * e.wheelDeltaX/12;
} else if ( e.axis !== undefined && e.axis === e.HORIZONTAL_AXIS ) {
wheelDeltaY = 0;
wheelDeltaX = -1 * wheelDeltaY;
} else {
wheelDeltaY = 0;
wheelDeltaX = 0;
}
// this._cameraDistance += wheelDeltaY * -2 * 0.01;
// this._cameraDistance = Math.max(3, Math.min(5, this._cameraDistance));
}
_onPointerDown(event) {
if (event.which == 3) {
this.isRightClick = true;
// event.preventDefault();
// event.stopPropagation();
}
this._isPointerDown = true;
this._pointerParent.classList.add('is-grabbing')
this.touchEvent = TOUCH ? (event.touches[0] || event.changedTouches[0]) : event;
this.touchEventPageX = this.touchEvent.pageX;
this.touchEventPageY = this.touchEvent.pageY;
this.touchEventPageX -= window.pageXOffset || document.documentElement.scrollLeft;
this.touchEventPageY -= window.pageYOffset || document.documentElement.scrollTop;
this.pointerXDown = this.touchEventPageX;
this.pointerYDown = this.touchEventPageY;
if (this.isRightClick) {
this.startPointerX = this.pointerXMove;
this.startPointerY = this.pointerYMove;
}
this.thetaDown = this.theta;
this.phiDown = this.phi;
}
_onPointerMove(event) {
if( !this._isPointerDown){
return;
}
// event.preventDefault();
this.touchEvent = TOUCH ? (event.touches[0] || event.changedTouches[0]) : event;
this.touchEventPageX = this.touchEvent.pageX;
this.touchEventPageY = this.touchEvent.pageY;
this.touchEventPageX -= window.pageXOffset || document.documentElement.scrollLeft;
this.touchEventPageY -= window.pageYOffset || document.documentElement.scrollTop;
if (this.isRightClick) {
this.pointerXMove = this.startPointerX + (this.touchEventPageX - this.pointerXDown);
this.pointerYMove = this.startPointerY + (this.touchEventPageY - this.pointerYDown);
}
else {
this.pointerXOrbiter = (this.pointerXDown - this.touchEventPageX);
this.pointerYOrbiter = (this.pointerYDown - this.touchEventPageY);
this.theta = this.thetaDown + ( this.pointerXOrbiter / this.winWidth * 2 * Math.PI);
this.phi = this.phiDown + ( this.pointerYOrbiter / this.winHeight * 2 * Math.PI * -1);
this.phi = Math.max( this._minPolarAngle, Math.min( this._maxPolarAngle, this.phi ) );
if( TOUCH ) {
this.phi = 0;
}
}
}
_onPointerUp() {
this._isPointerDown = false;
this.isRightClick = false;
this._pointerParent.classList.remove('is-grabbing')
}
update(force) {
if (this.orbitControl) {
if (this._canUpdate || force) {
this.currTheta += (this.theta - this.currTheta) * 0.1;
this.currPhi += (this.phi - this.currPhi) * 0.1;
this.position[0] = Math.sin(this.currTheta) * Math.cos(this.phi) * this._cameraDistance;
this.position[1] = Math.sin(this.phi) * this._cameraDistance;
this.position[2] = Math.cos(this.currTheta) * Math.cos(this.phi) * this._cameraDistance;
super.render();
}
}
else {
super.render();
}
this._canUpdate = false;
}
}
export default Camera;

View File

@@ -1,53 +0,0 @@
import Object3d from './Object3d';
class Container extends Object3d {
constructor() {
super();
this.visible = true;
this.parent = null;
this.children = [];
}
add(child) {
for (let i=0,l=this.children.length;i<l; i++) {
if (this.children[i] == child) {
break;
}
}
this.children.push(child);
child.parent = this;
}
remove(child) {
for (let i=0,l=this.children.length;i<l; i++) {
if (this.children[i] == child) {
child.parent = null;
this.children.splice(i, 1);
break;
}
}
}
destroy() {
for (let i=0,l=this.children.length;i<l; i++) {
this.children[i].destroy();
}
if (this.parent !== null) {
this.parent.removeChild( this );
}
}
render(camera, options) {
super.render();
for (let i=0,l=this.children.length;i<l; i++) {
if (this.children[i].visible) {
this.children[i].render(camera, options);
}
}
}
}
export default Container;

View File

@@ -1,30 +0,0 @@
import ArrayBuffer from './ArrayBuffer';
class GeometryBuffer {
constructor(gl, size) {
if (!gl) {
return;
}
this.gl = gl;
this.attributes = {}
this.length = size || 0;
// this.vertices = [];
// this.addAttribute( 'index', new Uint16Array( this.indices ), 1 );
// this.addAttribute( 'position', new Float32Array( this.vertices ), 3 );
// this.addAttribute( 'normal', new Float32Array( this.normals ), 3 );
// this.addAttribute( 'uv', new Float32Array( this.uvs ), 2 );
}
addAttribute(attribName, data, size, geometry) {
this.attributes[attribName] = new ArrayBuffer(this.gl, data, size, attribName === 'index', geometry);//true=use element array buffer
}
}
export default GeometryBuffer;

View File

@@ -1,54 +0,0 @@
import Program from './Program';
import vertexShader from './shaders/default-vs.glsl';
import fragmentShader from './shaders/mesh-fs.glsl';
class Material extends Program {
constructor( gl, options ){
options = Object.assign({}, {
vertexShader: vertexShader,
fragmentShader: fragmentShader,
map: null,
}, options);
options.uniforms = Object.assign({}, {
color: [1,1,1],
alpha: 1
}, options.uniforms);
options.defines = Object.assign({}, {
USE_MAP: false
}, options.defines);
super(gl, options);
if (!gl) {
return;
}
Object.defineProperty(this, 'map', {
set: (value) => {
if (value) {
this.defines.USE_MAP = true;
this.compile();
if (this.uniforms.map) {
this.uniforms.map.value = value;
}
}
else {
this.defines.USE_MAP = false;
this.compile();
}
}
});
this.map = options.map;
}
}
export default Material;

View File

@@ -1,90 +0,0 @@
import Container from './Container';
import * as mat4 from './glMatrix/mat4';
class Mesh extends Container {
constructor(options) {
super();
this.material = null;
this.geometry = null;
this.options = options || {};
this._viewMatrix = mat4.create()
this._invViewMatrix = mat4.create()
this._modelViewMatrix = mat4.create()
this._normalMatrix = mat4.create()
}
render(camera, options={}) {
super.render( camera, options );
let material = options.overrideMaterial || this.material;
if (camera && material &&
this.geometry && //TODO: check for geometry.length
this.visible) {
if (this.options.beforeRender) {
this.options.beforeRender()
}
// setTimeout(()=>{
mat4.invert(this._viewMatrix, camera.worldMatrix);
mat4.multiply(this._modelViewMatrix, this._viewMatrix, this.worldMatrix);
if (material.uniforms['uInverseViewMatrix'] !== void 0) {
mat4.copy(this._invViewMatrix, camera.worldMatrix);
mat4.invert(this._invViewMatrix, this._invViewMatrix);
material.uniforms['uInverseViewMatrix'].value = camera.worldMatrix;
}
if (material.uniforms['uCameraPosition'] !== void 0) {
material.uniforms['uCameraPosition'].value = camera.position;
}
if (material.uniforms['uVMatrix'] !== void 0) {
material.uniforms['uVMatrix'].value = this._viewMatrix;
}
if (material.uniforms['uNormalMatrix'] !== void 0) {
mat4.multiply( this._normalMatrix, this._rotationMat4, this.parent._rotationMat4);
material.uniforms['uNormalMatrix'].value = this._normalMatrix
}
if (material.uniforms['uMMatrix'] !== void 0) {
material.uniforms['uMMatrix'].value = this.worldMatrix;
// console.log('MANUALLY ASSIGN ', this.matrix, this.name)
// console.log('setMM', this.name, this.matrix[0], this.matrix[1], this.matrix[2], this.matrix[3])
}
if (material.uniforms['uMVMatrix'] !== void 0) {
material.uniforms['uMVMatrix'].value = this._modelViewMatrix;
}
if (material.uniforms['uPMatrix'] !== void 0) {
material.uniforms['uPMatrix'].value = camera.projectionMatrix;
}
for (let u in options.uniforms) {
if (material.uniforms[ u ] !== void 0) {
material.uniforms[ u ].value = options.uniforms[u];
}
}
let needsCompile = false;
for (let k in options.defines) {
if (material.defines[ k ] !== options.defines[ k ]) {
material.defines[ k ] = options.defines[ k ];
needsCompile = true;
}
}
if (needsCompile) {
material.compile();
}
material.draw( this.geometry );
// })
}
}
}
export default Mesh;

View File

@@ -1,142 +0,0 @@
import * as mat4 from './glMatrix/mat4';
import * as mat3 from './glMatrix/mat3';
import * as quat from './glMatrix/quat';
import * as vec3 from './glMatrix/vec3';
class Object3d {
constructor() {
this.position = vec3.create();
this.rotation = vec3.create();
this.scale = vec3.create();
this.lookAt = null;
//use to diff and only update matrix if needed
this.lastPosition = vec3.create();
this.lastRotation = vec3.create();
this.lastScale = vec3.create();
this.lastLookAt = vec3.create();
vec3.set(this.scale, 1, 1, 1);
this.up = vec3.create();
vec3.set(this.up, 0, 1, 0);
this.matrix = mat4.create();//modelMatrix
this.worldMatrix = mat4.create();//modelMatrix * parentMatri(x|ces)
this.quaternion = null;//quat.create();
this._quaternion = quat.create();
this.inverseWorldMatrix = mat4.create();
this._invLookatMat4 = mat4.create();
this._m3Rotation = mat3.create();
this._rotationMat4 = mat4.create();
this._lookAtMat4 = mat4.create();
this._lastUpdate = Date.now()
}
render() {
let needsUpdate = false;
// let needsUpdateOrigin = null;
if (this.position[0] !== this.lastPosition[0] ||
this.position[1] !== this.lastPosition[1] ||
this.position[2] !== this.lastPosition[2] ) {
this.lastPosition[0] = this.position[0];
this.lastPosition[1] = this.position[1];
this.lastPosition[2] = this.position[2];
// needsUpdateOrigin = 'position'
needsUpdate = true;
}
else if (
this.rotation[0] !== this.lastRotation[0] ||
this.rotation[1] !== this.lastRotation[1] ||
this.rotation[2] !== this.lastRotation[2] ) {
this.lastScale[0] = this.rotation[0];
this.lastScale[1] = this.rotation[1];
this.lastScale[2] = this.rotation[2];
// needsUpdateOrigin = 'rotation'
needsUpdate = true;
}
else if (
this.scale[0] !== this.lastScale[0] ||
this.scale[1] !== this.lastScale[1] ||
this.scale[2] !== this.lastScale[2] ) {
this.lastScale[0] = this.scale[0];
this.lastScale[1] = this.scale[1];
this.lastScale[2] = this.scale[2];
// needsUpdateOrigin = 'scale'
needsUpdate = true;
}
else if (
this.lookAt !== null &&
( this.lookAt[0] !== this.lastLookAt[0] ||
this.lookAt[1] !== this.lastLookAt[1] ||
this.lookAt[2] !== this.lastLookAt[2] ) ) {
this.lastLookAt[0] = this.lookAt[0];
this.lastLookAt[1] = this.lookAt[1];
this.lastLookAt[2] = this.lookAt[2];
// needsUpdateOrigin = 'lookAt'
needsUpdate = true;
}
this.updateMatrix();
this.updateWorldMatrix();
}
updateMatrix() {
mat4.identity(this.matrix);
mat4.identity(this._invLookatMat4)
mat3.identity(this._m3Rotation)
mat4.identity(this._rotationMat4)
mat4.identity(this._lookAtMat4)
if (this.quaternion) {
mat4.fromRotationTranslation(this.matrix, this.quaternion, this.position );
}
else {
mat4.translate(this.matrix, this.matrix, this.position);
mat4.rotateX(this._rotationMat4, this._rotationMat4, this.rotation[0]);
mat4.rotateY(this._rotationMat4, this._rotationMat4, this.rotation[1]);
mat4.rotateZ(this._rotationMat4, this._rotationMat4, this.rotation[2]);
}
if (this.lookAt !== null) {
mat4.lookAt(
this.matrix,
this.position,
this.lookAt,
this.up
);
mat4.invert(this.matrix, this.matrix);
mat4.scale(this.matrix, this.matrix, this.scale);
}
else {
mat4.scale(this.matrix, this.matrix, this.scale);
mat4.multiply(this.matrix, this.matrix, this._rotationMat4);
}
}
updateWorldMatrix() {
if (this.parent) {
mat4.multiply(this.worldMatrix, this.parent.worldMatrix, this.matrix );
}
else{
this.worldMatrix = this.matrix;
}
mat4.invert(this.inverseWorldMatrix, this.worldMatrix);
}
}
export default Object3d;

View File

@@ -1,94 +0,0 @@
import GeometryBuffer from './GeometryBuffer';
class PlaneGeometryBuffer extends GeometryBuffer {
constructor(gl, options) {
super(gl, 4.);
options = Object.assign({}, {
width: 1000,
height: 1000,
widthSegments: 1,
heightSegments: 1
}, options);
if (!gl) {
return;
}
this.width = options.width;
this.height = options.height;
this.widthSegments = options.widthSegments;
this.heightSegments = options.heightSegments;
this._build();
this.addAttribute( 'index', new Uint16Array( this.indices ), 1 );
this.addAttribute( 'position', new Float32Array( this.vertices ), 3 );
this.addAttribute( 'normal', new Float32Array( this.normals ), 3 );
this.addAttribute( 'uv', new Float32Array( this.uvs ), 2 );
this.addAttribute( 'color', new Float32Array( this.colors ), 3 );
}
update() {
this._build();
this.attributes['index'].update( new Uint16Array(this.indices), 1 );
this.attributes['position'].update( new Float32Array(this.vertices), 3 );
this.attributes['normal'].update( new Float32Array(this.normals), 3 );
this.attributes['uv'].update( new Float32Array(this.uvs), 2 );
this.attributes['color'].update( new Float32Array(this.colors), 3 );
}
_build() {
// buffers
this.indices = [];
this.vertices = [];
this.normals = [];
this.uvs = [];
this.colors = [];
var width_half = this.width * 0.5;
var height_half = this.height * 0.5;
var gridX = this.widthSegments >> 0;
var gridY = this.heightSegments >> 0;
var gridX1 = gridX + 1;
var gridY1 = gridY + 1;
var segment_width = this.width / gridX;
var segment_height = this.height / gridY;
var ix, iy;
// generate vertices, normals and uvs
for ( iy = 0; iy < gridY1; iy ++ ) {
var y = iy * segment_height - height_half;
for ( ix = 0; ix < gridX1; ix ++ ) {
var x = ix * segment_width - width_half;
this.vertices.push( x, - y, 0 );
this.normals.push( 0, 0, 1 );
this.uvs.push( ix / gridX, 1 - ( iy / gridY ) );
this.colors.push( 1, 1, 1 );
}
}
// indices
for ( iy = 0; iy < gridY; iy ++ ) {
for ( ix = 0; ix < gridX; ix ++ ) {
var a = ix + gridX1 * iy;
var b = ix + gridX1 * ( iy + 1 );
var c = ( ix + 1 ) + gridX1 * ( iy + 1 );
var d = ( ix + 1 ) + gridX1 * iy;
// faces
this.indices.push( a, b, d );
this.indices.push( b, c, d );
}
}
this.length = this.vertices.length/3;
}
}
export default PlaneGeometryBuffer;

View File

@@ -1,448 +0,0 @@
import UNIFORM_TYPE from './uniformTypes';
import defaultVertexShader from './shaders/default-vs.glsl';
import defaultFragmentShader from './shaders/default-fs.glsl';
import uuid from './utils/uuid';
const TEXTURE_2D = 35678
const TEXTURE_CUBE_MAP = 35680;
function addLineNumbers( string ) {
var lines = string.split( '\n' );
for ( var i = 0; i < lines.length; i ++ ) {
lines[ i ] = ( i + 1 ) + ': ' + lines[ i ];
}
return lines.join( '\n' );
}
function compileShader( gl, shader, code ){
gl.shaderSource( shader, code );
gl.compileShader( shader );
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('Shader cannot compile: \n' + gl.getShaderInfoLog(shader) || "" );
console.warn(addLineNumbers(code));
return false;
}
return true;
}
var _lastUsedDepthTest = null
var _lastUsedGeometry = null
class Program {
constructor(gl, options={}) {
this._uuid = uuid();
if (!gl) {
return;
}
options = Object.assign({}, {
vertexShader: defaultVertexShader,
fragmentShader: defaultFragmentShader,
defines: {},
extentions: {},
uniforms: {},
type: gl.TRIANGLES
}, options);
this.options = options;
this._vertexShaderSource = options.vertexShader;
this._fragmentShaderSource = options.fragmentShader;
this.gl = gl;
this._program = gl.createProgram();
this.vertexShader = gl.createShader( gl.VERTEX_SHADER );
this.fragmentShader = gl.createShader( gl.FRAGMENT_SHADER );
gl.attachShader(this._program, this.vertexShader);
gl.attachShader(this._program, this.fragmentShader);
this.type = options.type;
this.attributes = {};
this.defines = options.defines;
this.extentions = options.extentions;
this._textureUnit = 0;
this.depthTest = options.depthTest !== void 0 ? options.depthTest : true;
this.blend = options.blend !== void 0 ? options.blend : false;
this.blendEquation = options.blendEquation !== void 0 ? options.blendEquation : this.gl.FUNC_ADD;
this.blendSrc = options.blendSrc !== void 0 ? options.blendSrc : this.gl.SRC_ALPHA;
this.blendDst = options.blendDst !== void 0 ? options.blendDst : this.gl.ONE_MINUS_SRC_ALPHA;
this.blendSrcRGB = options.blendSrcRGB !== void 0 ? options.blendSrcRGB : this.gl.SRC_ALPHA;
this.blendDstRGB = options.blendDstRGB !== void 0 ? options.blendDstRGB : this.gl.ONE_MINUS_SRC_ALPHA;
this.blendSrcAlpha = options.blendSrcAlpha !== void 0 ? options.blendSrcAlpha : this.gl.ONE;
this.blendDstAlpha = options.blendDstAlpha !== void 0 ? options.blendDstAlpha : this.gl.ONE_MINUS_SRC_ALPHA;
this.wireframe = options.wireframe !== void 0 ? options.wireframe : false;
this.uniforms = {};
this._userDefinedUniforms = options.uniforms;
this.compile();
}
compile() {
if (!this.gl) {
return;
}
if (this.isCompiling) {
return;
}
this.isCompiling = true;
var defines = '';
for (var d in this.defines) {
if (this.defines[d]) {
defines += '#define '+d+' '+this.defines[d]+'\n';
}
}
if( !( compileShader( this.gl, this.vertexShader, defines + this._vertexShaderSource ) &&
compileShader( this.gl, this.fragmentShader, defines + this._fragmentShaderSource ) ) ) {
console.warn('compile error')
return false;
}
this.gl.linkProgram(this._program);
if (!this.gl.getProgramParameter(this._program, this.gl.LINK_STATUS)) {
console.error("Cannot link program: \n" + this.gl.getProgramInfoLog(this._program) || "");
console.warn("VERTEX_SHADER:\n"+addLineNumbers(this._vertexShaderSource)
+"\n\nFRAGMENT_SHADER:\n"+addLineNumbers(this._fragmentShaderSource));
}
this.gl.useProgram(this._program);
this._retrieveUniformsFromShader();
this.isCompiling = false;
}
_retrieveUniformsFromShader() {
//debug
let isMMatrix = false;
this._savedUniforms = {};
for (let k in this.uniforms) {
this._savedUniforms[k] = {
value: this.uniforms[k].value
}
}
this.uniforms = {};
this._textureUnit = 0;
var numUniforms = this.gl.getProgramParameter( this._program, this.gl.ACTIVE_UNIFORMS );
for (let i = 0; i < numUniforms; ++i) {
var uniform = this.gl.getActiveUniform( this._program, i );
if( uniform === null ){
this.gl.getError();
continue;
}
let name = uniform.name;
let isArray = false;//is uniform array ?(ex: 3fv, 4fv...)
// tltr; we want 'myUniform[0]' to become 'myUniform'
// if array uniform, replace the retrieved name as it includes the first index acces
if (/\[.*\]/.test(name) ) {
isArray = true;
name = name.replace(/\[.*\]/,'');
}
if (this.uniforms[ name ] !== void 0) {
this.uniforms[ name ].location = this.gl.getUniformLocation( this._program, name );
this.uniforms[ name ].type = uniform.type;
}
else {
this.uniforms[ name ] = {
isArray: isArray,
location: this.gl.getUniformLocation( this._program, name ),
type: uniform.type,
value: null,
size: uniform.size
}
//set texture unit
if (uniform.type === TEXTURE_2D || uniform.type === TEXTURE_CUBE_MAP) {
this.uniforms[ name ].unit = this._textureUnit;
this._textureUnit++;
}
}
}
let isEnd = false;
//merge user defined uniforms
for (let u in this._savedUniforms) {
if (this.uniforms[u] !== void 0){
if (this._savedUniforms[u].value !== void 0
&& this._savedUniforms[u].value !== null) {
this.uniforms[u].value = this._savedUniforms[u].value;
}
}
else {
}
}
for (let u in this._userDefinedUniforms) {
if (this.uniforms[u] !== void 0
&& this._userDefinedUniforms[u] !== void 0
&& this._userDefinedUniforms[u] !== null) {
this.uniforms[u].value = this._userDefinedUniforms[u];
}
}
var numAttributes = this.gl.getProgramParameter( this._program, this.gl.ACTIVE_ATTRIBUTES );
for (let i = 0; i < numAttributes; ++i) {
var attribute = this.gl.getActiveAttrib( this._program, i );
if( attribute === null ){
this.gl.getError();
continue;
}
this.attributes[ attribute.name ] = {
location: this.gl.getAttribLocation( this._program, attribute.name ),
type: attribute.type
}
//the attribute is only enabled when the buffer is binded
//(so it's enabled by the Program that will use the buffer)
//this way we make sure that any enabled attribute has some data not to trigger an error
//see http://www.mjbshaw.com/2013/03/webgl-fixing-invalidoperation.html
// this.gl.enableVertexAttribArray( this.attributes[attribute.name].location );
}
}
dispose() {
}
use () {
if (!this.gl) {
return;
}
this.gl.useProgram(this._program);
}
attribPointer(attributes, geometry) {
if (!this.gl) {
return;
}
for (var attr in this.attributes) {
if (attributes[attr] !== void 0) {
attributes[attr].bind();
this.gl.vertexAttribPointer( this.attributes[attr].location, attributes[attr].size, attributes[attr].type, false, 0, 0);
this.gl.enableVertexAttribArray( this.attributes[attr].location );
}
}
}
draw(geometry) {
if (!this.gl) {
return;
}
this.gl.useProgram(this._program);
//todo add a flah on attribute to check if they changed and thus needs to be binded again
//todo check the currently used program to know if it need some buffer bindings
// if (geometry !== _lastUsedGeometry) {
//TODO: check if geometry has changed
this.attribPointer(geometry.attributes, geometry);
// _lastUsedGeometry = geometry;
// }
// if (this.depthTest !== _lastUsedDepthTest) {
this.gl[ this.depthTest ? 'enable' : 'disable' ](this.gl.DEPTH_TEST);
// _lastUsedDepthTest = this.depthTest;
// }
if (this.blend) {
// this.gl.disable(this.gl.DEPTH_TEST);
// this.gl[ this.depthTest ? 'enable' : 'disable' ](this.gl.DEPTH_TEST);
if (this.depthTest) {
this.gl.depthFunc( this.gl.LESS );
}
this.gl.blendEquation(this.blendEquation);
this.gl.blendFuncSeparate(this.blendSrcRGB, this.blendDstRGB, this.blendSrcAlpha, this.blendDstAlpha);
// this.gl.blendFunc(this.blendSrc,this.blendDst);
this.gl.enable(this.gl.BLEND);
}
else {
this.gl.disable(this.gl.BLEND);
// this.gl[ this.depthTest ? 'enable' : 'disable' ](this.gl.DEPTH_TEST);
if (this.depthTest) {
this.gl.depthFunc( this.gl.LESS );
}
}
var keys = Object.keys(this.uniforms);
for (var i=0,l=keys.length; i<l; i++) {
let uniformName = keys[i]
switch (this.uniforms[ uniformName ].type) {
case this.gl.FLOAT_MAT2:
case this.gl.FLOAT_MAT3:
case this.gl.FLOAT_MAT4:
if (this.uniforms[ uniformName ].value !== null &&
this.uniforms[ uniformName ].value !== void 0) {
this.gl['uniform' + UNIFORM_TYPE[this.uniforms[ uniformName ].type]+'v'](this.uniforms[ uniformName ].location, false, this.uniforms[ uniformName ].value);
}
break;
default:
//texture2D
if (this.uniforms[ uniformName ].type === TEXTURE_2D ||
this.uniforms[ uniformName ].type === TEXTURE_CUBE_MAP ){
if (this.uniforms[ uniformName ].value) {
this.uniforms[ uniformName ].value.bind( this.uniforms[ uniformName ].unit );
this.gl['uniform' + UNIFORM_TYPE[this.uniforms[ uniformName ].type] ](this.uniforms[ uniformName ].location, this.uniforms[ uniformName ].unit);
}
else {
// console.log('no value for texture...', keys[i], this._userDefinedUniforms[keys[i]], this.uniforms[ keys[i] ].value, this.defines.USE_ALBEDO_MAP)
}
// if (keys[i] == 'tAlbedo' && this.defines.USE_ALBEDO_MAP) {
// console.log('BIND ALBEDO UNIRFORM', this.uniforms.tAlbedo.value, this.defines)
// }
// drawnUniformsTextures.push(keys[i]);
}
else {
let type = UNIFORM_TYPE[this.uniforms[ keys[i] ].type];
//add 'v' to the uniformType if the unifor is an array: ex "3f" => "3fv"
if (this.uniforms[ uniformName ].isArray) {
type += 'v';
}
if (this.uniforms[ uniformName ].value !== null) {
if (type == '2f') {
this.gl['uniform' + type ](this.uniforms[ uniformName ].location, this.uniforms[ uniformName ].value[0], this.uniforms[ uniformName ].value[1]);
// drawnUniforms2f.push(uniformName);
}
else if (type == '3f') {
this.gl['uniform' + type ](this.uniforms[ uniformName ].location, this.uniforms[ uniformName ].value[0], this.uniforms[ uniformName ].value[1], this.uniforms[ uniformName ].value[2]);
// drawnUniforms3f.push(uniformName);
}
else if (type == '4f') {
this.gl['uniform' + type ](this.uniforms[ uniformName ].location, this.uniforms[ uniformName ].value[0], this.uniforms[ uniformName ].value[1], this.uniforms[ uniformName ].value[2], this.uniforms[ uniformName ].value[3]);
// drawnUniforms4f.push(uniformName);
}
else {
// drawnUniforms1f.push(uniformName);
this.gl['uniform' + type ](this.uniforms[ uniformName ].location, this.uniforms[ uniformName ].value);
}
}
}
//break;
}
}
if ( this.type !== this.gl.POINTS &&
geometry.attributes['index'] ) {
geometry.attributes['index'].bind()
this.gl.drawElements(this.wireframe ? this.gl.LINE_STRIP : this.type, geometry.attributes['index'].length, this.gl.UNSIGNED_SHORT, 0);
}
else {
this.gl.drawArrays(this.wireframe ? this.gl.LINE_STRIP : this.type, 0, geometry.length);
}
}
}
// var drawnUniformsTextures = []
// var drawnUniforms1f = []
// var drawnUniforms2f = []
// var drawnUniforms3f = []
// var drawnUniforms4f = []
// function flushUniforms() {
// console.log('1f: ' + drawnUniforms1f.length + '\n' +
// '2f: ' + drawnUniforms2f.length + '\n' +
// '3f: ' + drawnUniforms3f.length + '\n' +
// '4f: ' + drawnUniforms4f.length + '\n' +
// 'tx: ' + drawnUniformsTextures.length + '\n' );
// drawnUniformsTextures = []
// drawnUniforms1f = []
// drawnUniforms2f = []
// drawnUniforms3f = []
// drawnUniforms4f = []
// requestAnimationFrame(flushUniforms)
// }
// flushUniforms();
export default Program;

View File

@@ -1,103 +0,0 @@
class Renderer {
constructor(options) {
this.canvas = (options && options.canvas) || document.createElement('canvas');
this.canvas.style.transformOrigin = '0 0';
this.contextAttributes = Object.assign({},{
alpha: false,
depth: true,
stencil: true,
antialias: false,
premultipliedAlpha: true,
preserveDrawingBuffer: false,
failIfMajorPerformanceCaveat: false,
}, (options || {}) );
this._pixelRatio = 1;
this.gl = this.canvas.getContext("experimental-webgl", this.contextAttributes);
this.handleContextLost = this.handleContextLost.bind(this);
this.handleContextRestored = this.handleContextRestored.bind(this);
this.canvas.addEventListener('webglcontextlost', this.handleContextLost, false);
this.canvas.addEventListener('webglcontextrestored', this.handleContextRestored, false);
}
handleContextLost(event){
event.preventDefault();
}
handleContextRestored(){
}
handleContextRestored() {
}
render(container, camera, frameBuffer, preventCameraUpdate) {
if (!this.gl) {
return;
}
if (!preventCameraUpdate) {
camera.update();
}
if (frameBuffer){
frameBuffer.bindFrame();
container.render( camera );
frameBuffer.unbind();
}
else{
this.gl.viewport(0, 0, this._width* this._pixelRatio , this._height* this._pixelRatio );
container.render( camera );
}
}
resize(width, height) {
if (!this.gl) {
return;
}
this._width = width;
this._height = height;
this.canvas.width = this._width * this._pixelRatio;
this.canvas.height = this._height * this._pixelRatio;
this.canvas.style.transform = 'scale('+(1/this._pixelRatio)+') translateZ(0)';
this.gl.viewport(0, 0, this._width* this._pixelRatio , this._height* this._pixelRatio );
}
clearColor(r, g, b, alpha) {
if (!this.gl) {
return;
}
this.gl.clearColor(r, g, b, alpha);
}
clear(color, depth, stencil) {
if (!this.gl) {
return;
}
var bits = 0;
if ( color === void 0 || color ) bits |= this.gl.COLOR_BUFFER_BIT;
if ( depth === void 0 || depth ) bits |= this.gl.DEPTH_BUFFER_BIT;
if ( stencil === void 0 || stencil ) bits |= this.gl.STENCIL_BUFFER_BIT;
this.gl.clear( bits );
}
setPixelRatio(ratio) {
this._pixelRatio = ratio;
this.resize(this._width, this._height);
}
}
export default Renderer;

View File

@@ -1,112 +0,0 @@
import GeometryBuffer from './GeometryBuffer';
import * as vec3 from './glMatrix/vec3';
class SphereGeometryBuffer extends GeometryBuffer {
constructor(gl, options) {
super(gl, 0.);
options = Object.assign({},{
radius: 50,
widthSegments: 8,
heightSegments: 6,
phiStart: 0,
phiLength: Math.PI * 2,
thetaStart: 0,
thetaLength: Math.PI
}, options);
if (!gl) {
return;
}
var radius = options.radius || 50;
var widthSegments = Math.max( 3, Math.floor( options.widthSegments ) || 8 );
var heightSegments = Math.max( 2, Math.floor( options.heightSegments ) || 6 );
var phiStart = phiStart !== undefined ? options.phiStart : 0;
var phiLength = phiLength !== undefined ? options.phiLength : Math.PI * 2;
var thetaStart = thetaStart !== undefined ? options.thetaStart : 0;
var thetaLength = thetaLength !== undefined ? options.thetaLength : Math.PI;
var thetaEnd = thetaStart + thetaLength;
var ix, iy;
var index = 0;
var grid = [];
var vertex = vec3.create();
var normal = vec3.create();
// buffers
var indices = [];
var vertices = [];
var normals = [];
var uvs = [];
var colors = [];
// generate vertices, normals and uvs
for ( iy = 0; iy <= heightSegments; iy ++ ) {
var verticesRow = [];
var v = iy / heightSegments;
for ( ix = 0; ix <= widthSegments; ix ++ ) {
var u = ix / widthSegments;
vertex[0] = - radius * Math.cos( phiStart + u * phiLength ) * Math.sin( thetaStart + v * thetaLength );
vertex[1] = radius * Math.cos( thetaStart + v * thetaLength );
vertex[2] = radius * Math.sin( phiStart + u * phiLength ) * Math.sin( thetaStart + v * thetaLength );
vertices.push( vertex[0], vertex[1], vertex[2] );
vec3.set(normal, vertex[0], vertex[1], vertex[2] );
vec3.normalize(normal, normal);
normals.push( normal[0], normal[1], normal[2] );
uvs.push( u, 1 - v );
verticesRow.push( index ++ );
colors.push( 1,1,1 )
}
grid.push( verticesRow );
}
// indices
for ( iy = 0; iy < heightSegments; iy ++ ) {
for ( ix = 0; ix < widthSegments; ix ++ ) {
var a = grid[ iy ][ ix + 1 ];
var b = grid[ iy ][ ix ];
var c = grid[ iy + 1 ][ ix ];
var d = grid[ iy + 1 ][ ix + 1 ];
if ( iy !== 0 || thetaStart > 0 ) indices.push( a, b, d );
if ( iy !== heightSegments - 1 || thetaEnd < Math.PI ) indices.push( b, c, d );
}
}
// build geometry
this.length = vertices.length/3;
this.addAttribute( 'index', new Uint16Array( indices ), 1 );
this.addAttribute( 'position', new Float32Array( vertices ), 3 );
this.addAttribute( 'normal', new Float32Array( normals ), 3 );
this.addAttribute( 'color', new Float32Array( colors ), 3 );
this.addAttribute( 'uv', new Float32Array( uvs ), 2, 'Sphere' );
}
}
export default SphereGeometryBuffer;

View File

@@ -1,245 +0,0 @@
import uuid from './utils/uuid';
import getFilter from './utils/getFilter';
import isPowerOf2 from './utils/isPowerOf2';
var TEXTURE_CACHE = {};
class Texture {
constructor(gl, options) {
if (!gl) {
return;
}
this.options = Object.assign({}, {
format: gl.RGBA,
type: gl.UNSIGNED_BYTE,
width: 1,
height: 1,
linear: true,
mipmap: false,
miplinear: false,
wrapS: gl.CLAMP_TO_EDGE,
wrapT: gl.CLAMP_TO_EDGE,
anisotropy: 0,
flipY: true,
repeat: [1,1]
}, options);
this._uid = uuid();//debug purpose
this.gl = gl;
this.width = this.options.width;
this.height = this.options.height;
this.format = this.options.format;
this.type = this.options.type;
this.flipY = this.options.flipY;
this.repeat = this.options.repeat;
this._anisotropy = this.options.anisotropy;
if (this.type == gl.FLOAT) {
var floatTextures = gl.getExtension('OES_texture_float');
var floatTextureLinearFiltering = gl.getExtension('OES_texture_float_linear');
if (!floatTextures) {
console.warn('trying to create a FrameBuffer of with gl.FLOAT type but there\s no floating point texture support. trying HALF_FLOAT');
this.type = "HALF_FLOAT"
}
}
if (this.type == "HALF_FLOAT") {
var halfFloatTexturesExt = gl.getExtension('OES_texture_half_float');
var halfFloatTextureLinearFiltering = gl.getExtension('OES_texture_half_float_linear');
if (!halfFloatTexturesExt) {
console.warn('trying to create a texture of with gl.HALF_FLOAT type but there\s no half floating point texture support; falling bck to UNSIGNED_BYTE type');
this.type = gl.UNSIGNED_BYTE;
}
else {
this.type = halfFloatTexturesExt.HALF_FLOAT_OES;
this.isHalfFloat = true;
}
}
this._texture = this.gl.createTexture();
gl.bindTexture( gl.TEXTURE_2D, this._texture );
//1x1 pixel default texture
gl.texImage2D(gl.TEXTURE_2D, 0, this.options.format, this.width, this.height, 0, this.options.format, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 255]));
/**
* add getter and setter to update texture_wrap when this.wrap changes
*/
Object.defineProperty(this, 'wrapS', {
set: (value) => {
this.gl.texParameteri( this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, value );
}
});
Object.defineProperty(this, 'wrapT', {
set: (value) => {
this.gl.texParameteri( this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, value );
}
});
//nPOT texture can't repeat
this.wrapS = this.options.wrapS;
this.wrapT = this.options.wrapT;
//nPOT texture cannot mipmap
this.setFilter( this.options.linear, this.options.mipmap, this.options.mipmapLinear );
//unbind texture
gl.bindTexture( gl.TEXTURE_2D, null);
Object.defineProperty(this, 'anisotropy', {
set: (value) => {
this._anisotropy = value
this.updateAnisotropyFilter()
},
get: () => {
return this._anisotropy
}
});
}
updateAnisotropyFilter() {
let gl = this.gl;
if (!gl) {
return;
}
var ext = (
gl.getExtension('EXT_texture_filter_anisotropic') ||
gl.getExtension('MOZ_EXT_texture_filter_anisotropic') ||
gl.getExtension('WEBKIT_EXT_texture_filter_anisotropic')
);
if (ext) {
if (this._anisotropy > 0) {
gl.bindTexture( gl.TEXTURE_2D, this._texture );
var max = gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
gl.texParameterf(gl.TEXTURE_2D, ext.TEXTURE_MAX_ANISOTROPY_EXT, Math.min(this._anisotropy, max) );
}
}
}
bindImage(img) {
if (!this.gl) {
return;
}
this.width = img.width;
this.height = img.height;
var isPOT = isPowerOf2(img.width) && isPowerOf2(img.height);
this.gl.bindTexture( this.gl.TEXTURE_2D, this._texture);
this.gl.pixelStorei( this.gl.UNPACK_FLIP_Y_WEBGL, this.flipY);
// console.log('BIND2D', img)
this.gl.texImage2D( this.gl.TEXTURE_2D, 0, this.format, this.format, this.type, img);
if (isPOT) {
//nPOT texture cannot mipmap
this.setFilter( this.options.linear, this.options.mipmap, this.options.mipmapLinear );
this.gl.generateMipmap(this.gl.TEXTURE_2D);
}
else {
this.setFilter(this.options.linear, false, false);
this.wrapS = this.gl.CLAMP_TO_EDGE;
this.wrapT = this.gl.CLAMP_TO_EDGE;
}
this.gl.bindTexture( this.gl.TEXTURE_2D, null);
}
bind(unit) {
if (!this.gl) {
return;
}
//unit is sent by the Program and defined by the unfirom order in the shaders;
if (unit !== void 0) {
this.gl.activeTexture( this.gl.TEXTURE0 + (0|unit) );
}
this.gl.bindTexture(this.gl.TEXTURE_2D, this._texture);
}
delete() {
if (this.gl) {
this.gl.deleteTexture( this._texture );
}
this._texture = null;
this.gl = null;
}
/**
* Change the filtering parameters
* @param {boolean} [smooth=false] if true, use LINEAR filtering
* @param {boolean} [mipmap=false] if true, enable mipmaping
* @param {boolean} [miplinear=false] if true, use linear Mipmapping
*/
setFilter(smooth, mipmap, miplinear) {
if (!this.gl) {
return;
}
var gl = this.gl;
var filter = getFilter( !!smooth, !!mipmap, !!miplinear);
gl.bindTexture( gl.TEXTURE_2D, this._texture );
gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, getFilter( !!smooth, false, false ) );
gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter );
this.updateAnisotropyFilter()
}
}
Texture.fromUrl = function(gl, url, options) {
var texture = new Texture(gl, options);
// if (TEXTURE_CACHE[url] !== void 0) {
// this.fromImage( gl, TEXTURE_CACHE[url] );
// return;
// }
var img = new Image();
img.onload = ()=>{
img.onload = null;
img.onerror = null;
TEXTURE_CACHE[url] = img;
options && options.loaded && options.loaded()
texture.bindImage(img);
};
img.onerror = ()=>{
img.onload = null;
img.onerror = null;
console.warn('Invalid url provided to Texture.fromUrl() : ' + url);
};
img.src = url;
return texture;
};
Texture.fromImage = function(gl, img, options) {
if (!img.width || !img.height) {
console.warn('Cannot create texture with provided image\n Please make sure the image is loaded before calling Texture.fromImage() or use Texture.fromUrl()', img);
return;
}
var texture = new Texture(gl, options);
texture.bindImage(img);
return texture;
};
export default Texture;

Some files were not shown because too many files have changed in this diff Show More