├── .npmignore ├── website ├── utils │ ├── utils.ts │ ├── createState.ts │ ├── apiInject.ts │ ├── text.ts │ └── namhai.ts ├── styles │ ├── sass │ │ ├── _use.scss │ │ ├── _vh-property.scss │ │ ├── _colors.scss │ │ ├── _functions.scss │ │ ├── _breakpoints.module.scss │ │ ├── _mixins.scss │ │ ├── _easing.scss │ │ ├── _variables.module.scss │ │ └── _breakpoints.scss │ ├── app │ │ ├── index.scss │ │ ├── easing.scss │ │ ├── fonts.scss │ │ ├── grid.scss │ │ ├── reset.scss │ │ └── anim.scss │ ├── _shared.scss │ └── core.scss ├── .npmrc ├── bun.lockb ├── public │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── fonts │ │ ├── Amarante-Regular.ttf │ │ ├── EDDaffodil-Regular.ttf │ │ ├── HelveticaNeue-Bold.otf │ │ └── HelveticaNeue-Regular.otf │ ├── browserconfig.xml │ └── site.webmanifest ├── .gitignore ├── tsconfig.json ├── server │ └── api │ │ ├── getSlugs.ts │ │ └── mocks.json ├── pages │ ├── [[catchError]].vue │ ├── baz.vue │ ├── foo.vue │ ├── index.vue │ ├── work │ │ └── [slug].vue │ └── error.vue ├── composables │ ├── useCursor.ts │ ├── useStore.ts │ ├── useEventListener.ts │ ├── useFlowProvider.ts │ ├── useScreen.ts │ ├── useCleanedScope.ts │ ├── useDrag.ts │ └── pluginComposables.ts ├── components │ ├── Baz │ │ ├── Display.vue │ │ └── Button.vue │ ├── Foo │ │ └── Unique.vue │ └── global │ │ ├── Overlay.vue │ │ └── Menu.vue ├── app.vue ├── plugins │ ├── 02.stopMotion.client.ts │ ├── 01.namhai.client.ts │ ├── 03.lenis.client.ts │ ├── 10.directives.cursor.ts │ └── core │ │ ├── eases.ts │ │ ├── resize.ts │ │ ├── frame.ts │ │ └── stopMotion.ts ├── package.json ├── README.md ├── lib │ └── waterflow │ │ ├── utils │ │ └── apiInject.ts │ │ ├── composables │ │ ├── onFlow.ts │ │ └── usePageFlow.ts │ │ ├── FlowProvider.ts │ │ └── WaterflowRouter.vue ├── layouts │ └── default.vue ├── nuxt.config.ts ├── pages.transition │ └── defaultFlow.ts └── assets │ └── lorem.ts ├── bun.lockb ├── public └── waterflow.png ├── tsconfig.node.json ├── .gitignore ├── vite.config.ts ├── index.ts ├── package.json ├── tsconfig.json ├── LICENSE └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | website -------------------------------------------------------------------------------- /website/utils/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | export const nuxtFetch = ()=> $fetch -------------------------------------------------------------------------------- /website/styles/sass/_use.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | @use "sass:map"; -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nam-Hai/Waterflow/HEAD/bun.lockb -------------------------------------------------------------------------------- /website/.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /website/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nam-Hai/Waterflow/HEAD/website/bun.lockb -------------------------------------------------------------------------------- /public/waterflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nam-Hai/Waterflow/HEAD/public/waterflow.png -------------------------------------------------------------------------------- /website/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nam-Hai/Waterflow/HEAD/website/public/favicon.ico -------------------------------------------------------------------------------- /website/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nam-Hai/Waterflow/HEAD/website/public/favicon-16x16.png -------------------------------------------------------------------------------- /website/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nam-Hai/Waterflow/HEAD/website/public/favicon-32x32.png -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .nuxt 4 | .nitro 5 | .cache 6 | .output 7 | .env 8 | dist 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /website/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nam-Hai/Waterflow/HEAD/website/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /website/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nam-Hai/Waterflow/HEAD/website/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /website/public/fonts/Amarante-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nam-Hai/Waterflow/HEAD/website/public/fonts/Amarante-Regular.ttf -------------------------------------------------------------------------------- /website/public/fonts/EDDaffodil-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nam-Hai/Waterflow/HEAD/website/public/fonts/EDDaffodil-Regular.ttf -------------------------------------------------------------------------------- /website/public/fonts/HelveticaNeue-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nam-Hai/Waterflow/HEAD/website/public/fonts/HelveticaNeue-Bold.otf -------------------------------------------------------------------------------- /website/public/fonts/HelveticaNeue-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nam-Hai/Waterflow/HEAD/website/public/fonts/HelveticaNeue-Regular.otf -------------------------------------------------------------------------------- /website/utils/createState.ts: -------------------------------------------------------------------------------- 1 | export function createState(getter: () => State) { 2 | const state = getter() 3 | return () => state 4 | } 5 | -------------------------------------------------------------------------------- /website/styles/app/index.scss: -------------------------------------------------------------------------------- 1 | @forward "easing.scss"; 2 | @forward "anim.scss"; 3 | @forward "fonts.scss"; 4 | @forward "grid.scss"; 5 | @forward "./reset.scss"; 6 | -------------------------------------------------------------------------------- /website/styles/sass/_vh-property.scss: -------------------------------------------------------------------------------- 1 | @mixin vh-property($property, $value) { 2 | #{$property}: #{$value}vh; 3 | #{$property}: calc(var(--vh) * #{$value}); 4 | } -------------------------------------------------------------------------------- /website/server/api/getSlugs.ts: -------------------------------------------------------------------------------- 1 | import mocks from "./mocks.json" 2 | export default defineEventHandler(async (event) => { 3 | const response = mocks 4 | return response 5 | }) -------------------------------------------------------------------------------- /website/styles/sass/_colors.scss: -------------------------------------------------------------------------------- 1 | // Palette 2 | $white: white; 3 | $black: black; 4 | $dark-grey: black; 5 | 6 | $primary: #FF5C00; 7 | $text-dark: #1A1A1A; 8 | $text-mid: #5F5F5F; 9 | -------------------------------------------------------------------------------- /website/pages/[[catchError]].vue: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /website/composables/useCursor.ts: -------------------------------------------------------------------------------- 1 | export const useCursor = createState(() => { 2 | type State = "default" | "active" 3 | const state: Ref = ref("default") 4 | 5 | return { 6 | state 7 | } 8 | }) -------------------------------------------------------------------------------- /website/components/Baz/Display.vue: -------------------------------------------------------------------------------- 1 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /website/server/api/mocks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Neomi" 4 | }, 5 | { 6 | "name": "Lea" 7 | }, 8 | { 9 | "name": "Leo" 10 | }, 11 | { 12 | "name": "Nam" 13 | } 14 | ] -------------------------------------------------------------------------------- /website/components/Foo/Unique.vue: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /website/components/Baz/Button.vue: -------------------------------------------------------------------------------- 1 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /website/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #b91d47 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /website/styles/_shared.scss: -------------------------------------------------------------------------------- 1 | @forward "./sass/use.scss"; 2 | @forward "./sass/vh-property.scss"; 3 | @forward "./sass/variables.module.scss"; 4 | @forward "./sass/functions.scss"; 5 | @forward "./sass/mixins.scss"; 6 | @forward "./sass/breakpoints.scss"; 7 | @forward "./sass/easing.scss"; 8 | @forward "./sass/colors.scss"; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? -------------------------------------------------------------------------------- /website/pages/baz.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /website/composables/useStore.ts: -------------------------------------------------------------------------------- 1 | export const useStore = createState(() => { 2 | 3 | const isMobile = ref(false); 4 | 5 | const pageLoaded = ref(false); 6 | 7 | const preventScroll = ref(false); 8 | 9 | const fromPreloader = ref(true) 10 | 11 | const manifestLoaded = ref(false); 12 | 13 | const preloaderComplete = ref(false); 14 | return { isMobile, pageLoaded, preventScroll, fromPreloader, manifestLoaded, preloaderComplete } 15 | }) -------------------------------------------------------------------------------- /website/app.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /website/composables/useEventListener.ts: -------------------------------------------------------------------------------- 1 | export function useEventListener(target: Ref | Document, event: K, callback: (e: Event) => void) { 2 | let t = shallowRef(); 3 | if (target == document) t.value = target; 4 | else t = target as Ref 5 | 6 | onMounted(() => { 7 | t.value.addEventListener(event, callback) 8 | }) 9 | onBeforeUnmount(() => { 10 | t.value.removeEventListener(event, callback) 11 | }) 12 | } -------------------------------------------------------------------------------- /website/pages/foo.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | 20 | -------------------------------------------------------------------------------- /website/pages/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /website/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Waterflow", 3 | "short_name": "Waterflow", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /website/styles/app/easing.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --ease-in-quad: cubic-bezier(0.55, 0.085, 0.68, 0.53); 3 | --ease-out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94); 4 | 5 | --ease-in-cubic: cubic-bezier(0.550, 0.055, 0.675, 0.190); 6 | --ease-out-cubic: cubic-bezier(0.215, 0.61, 0.355, 1); 7 | 8 | --ease-in-quart: cubic-bezier(0.895, 0.030, 0.685, 0.220); 9 | --ease-out-quart: cubic-bezier(0.165, 0.840, 0.440, 1.000); 10 | 11 | 12 | --ease-in-out-sine: cubic-bezier(0.445, 0.050, 0.550, 0.950); 13 | --easeInOutCustom: cubic-bezier(0.8, 0, 0.2, 1); 14 | } -------------------------------------------------------------------------------- /website/plugins/02.stopMotion.client.ts: -------------------------------------------------------------------------------- 1 | import type { FrameFactory } from "./core/frame"; 2 | import { MotionFactory, MotionManager } from "./core/stopMotion" 3 | 4 | export default defineNuxtPlugin({ 5 | dependsOn: ['frame'], 6 | setup: nuxtApp => { 7 | const { $frameFactory } = nuxtApp 8 | const MM = new MotionManager($frameFactory as FrameFactory) 9 | const motionFactory = new MotionFactory(MM) 10 | 11 | return { 12 | 13 | provide: { 14 | motionFactory 15 | } 16 | } 17 | 18 | } 19 | }) -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import { resolve } from "node:path"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [vue()], 8 | build: { 9 | lib: { 10 | entry: resolve(__dirname, "index.ts"), 11 | name: "@nam-hai/water-flow", 12 | fileName: "@nam-hai/water-flow", 13 | }, 14 | rollupOptions: { 15 | external: ["vue"], 16 | output: { 17 | globals: { 18 | vue: "Vue", 19 | }, 20 | }, 21 | }, 22 | }, 23 | }) 24 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import WaterflowRouter from "./website/lib/waterflow/WaterflowRouter.vue" 2 | import { usePageFlow } from "./website/lib/waterflow/composables/usePageFlow" 3 | import { provideFlowProvider, useFlowProvider } from "./website/lib/waterflow/FlowProvider" 4 | import type { FlowFunction } from "./website/lib/waterflow/composables/usePageFlow" 5 | import { onFlow, onLeave } from "./website/lib/waterflow/composables/onFlow" 6 | 7 | export { 8 | WaterflowRouter, 9 | useFlowProvider, 10 | provideFlowProvider, 11 | usePageFlow, 12 | FlowFunction, 13 | onFlow, 14 | onLeave 15 | } 16 | -------------------------------------------------------------------------------- /website/plugins/01.namhai.client.ts: -------------------------------------------------------------------------------- 1 | import { FrameFactory, FrameManager, TabManager } from './core/frame' 2 | import { ResizeFactory, ResizeManager } from './core/resize' 3 | 4 | export default defineNuxtPlugin({ 5 | name: "frame", 6 | setup: nuxtApp => { 7 | const tab = new TabManager() 8 | const FM = new FrameManager(tab) 9 | const frameFactory = new FrameFactory(FM) 10 | 11 | const RM = new ResizeManager(frameFactory.Timer) 12 | const resizeFactory = new ResizeFactory(RM) 13 | 14 | return { 15 | provide: { 16 | frameFactory, 17 | resizeFactory 18 | } 19 | } 20 | 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /website/plugins/03.lenis.client.ts: -------------------------------------------------------------------------------- 1 | import Lenis from "lenis"; 2 | import type { FrameEvent, FrameFactory } from "./core/frame"; 3 | 4 | export default defineNuxtPlugin((nuxtApp) => { 5 | const lenis = new Lenis(); 6 | const { $frameFactory } = nuxtApp 7 | 8 | const callback = (e: FrameEvent) => { 9 | lenis.raf(e.elapsed); 10 | }; 11 | const frame = ($frameFactory as FrameFactory).Frame({ callback }) 12 | frame.run() 13 | 14 | // lenis.on("scroll", ScrollTrigger.update); 15 | // ticker.add(raf); 16 | 17 | return { 18 | provide: { 19 | lenis, 20 | }, 21 | }; 22 | }); 23 | 24 | -------------------------------------------------------------------------------- /website/styles/sass/_functions.scss: -------------------------------------------------------------------------------- 1 | @use "sass:map"; 2 | @use "sass:list"; 3 | 4 | @function index-to-key($map, $index) { 5 | @return list.nth(map.keys($map), $index); 6 | } 7 | 8 | 9 | @function pxToRem($px, $factor) { 10 | @return $px * $factor * 0.1rem; 11 | } 12 | 13 | 14 | @function ipxTop($val) { 15 | @return calc(env(safe-area-inset-top) + #{$val}); 16 | } 17 | 18 | @function ipxBottom($val) { 19 | @return calc(env(safe-area-inset-bottom) + #{$val}); 20 | } 21 | 22 | @function ipxLeft($val) { 23 | @return calc(env(safe-area-inset-left) + #{$val}); 24 | } 25 | 26 | @function ipxRight($val) { 27 | @return calc(env(safe-area-inset-right) + #{$val}); 28 | }; -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "waterflow-website", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "start": "nuxt start", 10 | "preview": "nuxt preview", 11 | "postinstall": "nuxt prepare" 12 | }, 13 | "devDependencies": { 14 | "@nuxt/devtools": "1.6.4", 15 | "@types/bun": "^1.1.14", 16 | "@types/node": "^22.10.2", 17 | "eslint": "^9.17.0", 18 | "nuxt": "^3.14.1592", 19 | "prettier": "3.4.2", 20 | "sass": "^1.83.0" 21 | }, 22 | "dependencies": { 23 | "lenis": "^1.1.18", 24 | "vue-demi": "^0.14.10" 25 | } 26 | } -------------------------------------------------------------------------------- /website/styles/sass/_breakpoints.module.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | @use "sass:map"; 3 | @use "sass:list"; 4 | 5 | @use "./breakpoints" as *; 6 | 7 | // EXPORTS 8 | // **************************** 9 | :export { 10 | site_scale: $site-scale; 11 | scale_mode: $scale-mode; 12 | 13 | breakpoints: map.keys($breakpoints); 14 | 15 | @each $key, $val in $breakpoints { 16 | breakpoint_#{$key}_width: map.get(($val), width); 17 | breakpoint_#{$key}_design_width: map.get(($val), design-width); 18 | breakpoint_#{$key}_design_height: map.get(($val), design-height); 19 | breakpoint_#{$key}_scale_min: map.get(($val), scale-min); 20 | breakpoint_#{$key}_scale_max: map.get(($val), scale-max); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /website/components/global/Overlay.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt 3 Minimal Starter 2 | 3 | Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 4 | 5 | ## Setup 6 | 7 | Make sure to install the dependencies: 8 | 9 | ```bash 10 | # yarn 11 | yarn install 12 | 13 | # npm 14 | npm install 15 | 16 | # pnpm 17 | pnpm install 18 | ``` 19 | 20 | ## Development Server 21 | 22 | Start the development server on `http://localhost:3000` 23 | 24 | ```bash 25 | npm run dev 26 | ``` 27 | 28 | ## Production 29 | 30 | Build the application for production: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | Locally preview production build: 37 | 38 | ```bash 39 | npm run preview 40 | ``` 41 | 42 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 43 | -------------------------------------------------------------------------------- /website/composables/useFlowProvider.ts: -------------------------------------------------------------------------------- 1 | import { useFlowProvider as flow } from "~/lib/waterflow/FlowProvider" 2 | export const useFlowProvider = flow 3 | 4 | export const flowCreateError = (args: { 5 | statusCode: number; 6 | statusMessage: string; 7 | }) => { 8 | // navigateTo("error", { 9 | // query: args, 10 | // replace: false 11 | // }) 12 | navigateTo({ 13 | name: "error", 14 | query: args, 15 | replace: true 16 | }) 17 | throw createError(args) 18 | } 19 | 20 | export const useQuery = () => { 21 | const route = useFlowProvider().currentRoute.value 22 | return { 23 | query: route.query, 24 | params: route.params 25 | } 26 | } 27 | 28 | export const flowRoute = () => { 29 | return useFlowProvider().currentRoute 30 | } -------------------------------------------------------------------------------- /website/composables/useScreen.ts: -------------------------------------------------------------------------------- 1 | import type { Breakpoints } from "~/plugins/core/resize" 2 | 3 | export const [provideScreen, useScreen] = createContext(() => { 4 | const vh = ref(0) 5 | const vw = ref(0) 6 | const dpr = ref(1) 7 | const scale = ref(1) 8 | const breakpoint: Ref = ref("") 9 | 10 | onMounted(() => { 11 | useResize((e) => { 12 | vh.value = e.vh 13 | vw.value = e.vw 14 | dpr.value = window.devicePixelRatio 15 | scale.value = e.scale 16 | breakpoint.value = e.breakpoint 17 | }) 18 | }) 19 | 20 | return { 21 | vh: shallowReadonly(vh), 22 | vw: shallowReadonly(vw), 23 | dpr: shallowReadonly(dpr), 24 | breakpoint: shallowReadonly(breakpoint), 25 | scale: shallowReadonly(scale) 26 | } 27 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nam-hai/water-flow", 3 | "version": "1.0.1", 4 | "type": "module", 5 | "main": "index.ts", 6 | "files": [ 7 | "/website/lib/waterflow" 8 | ], 9 | "module": "index.ts", 10 | "author": "Nam Hai", 11 | "license": "ISC", 12 | "description": "", 13 | "scripts": { 14 | "test": "", 15 | "preview": "vite preview", 16 | "build": "vue-tsc && vite build" 17 | }, 18 | "devDependencies": { 19 | "@vitejs/plugin-vue": "^5.2.1", 20 | "np": "^10.1.0", 21 | "vue": "^3.5.13", 22 | "vue-router": "^4.5.0" 23 | }, 24 | "peerDependencies": { 25 | "vue": "^3.0.0", 26 | "vue-router": "^4.1.6" 27 | }, 28 | "dependencies": {}, 29 | "keywords": [ 30 | "transition", 31 | "router" 32 | ], 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/Nam-Hai/Waterflow.git" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /website/lib/waterflow/utils/apiInject.ts: -------------------------------------------------------------------------------- 1 | export function createContext>( 2 | defaultValue: (args: Args) => Values, 3 | ) { 4 | 5 | const key = Symbol() 6 | const provideContext = (value: Args): any => { 7 | const defaultVal = defaultValue(value); 8 | const constructedVal = Object.assign(defaultVal, value) 9 | provide(key, constructedVal); 10 | return constructedVal 11 | }; 12 | 13 | const useContext = (value?: Values): Values => { 14 | return inject(key, value) as Values 15 | } 16 | // const useContext = (args?: Parameters>) => { 17 | // if (args?.[2] !== undefined) { 18 | // return inject(key, args?.[1] || undefined, args[2]) 19 | // } 20 | // return inject(key, args?.[1] || undefined) 21 | // } 22 | 23 | return [provideContext, useContext, key] as const; 24 | } -------------------------------------------------------------------------------- /website/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | 24 | 30 | 31 | 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable", "ESNext"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": [ 24 | "website/lib/waterflow/**/*.ts", 25 | "website/lib/waterflow/**/*.d.ts", 26 | "website/lib/waterflow/**/*.tsx", 27 | "website/lib/waterflow/**/*.vue", 28 | "index.ts" 29 | ], 30 | "references": [{ "path": "./tsconfig.node.json" }] 31 | } 32 | -------------------------------------------------------------------------------- /website/styles/app/fonts.scss: -------------------------------------------------------------------------------- 1 | // ************************************************************************************* 2 | // 3 | // Helvetica Neue 4 | // 5 | // ************************************************************************************* 6 | 7 | // @font-face { 8 | // font-family: "HelveticaNeue"; 9 | // src: url("/fonts/HelveticaNeue-Regular.otf") format("opentype"); 10 | // font-weight: 400; 11 | // font-display: swap; 12 | // } 13 | 14 | // @font-face { 15 | // font-family: "HelveticaNeue"; 16 | // src: url("/fonts/HelveticaNeue-Bold.otf") format("opentype"); 17 | // font-weight: 700; 18 | // font-display: swap; 19 | // } 20 | 21 | // @font-face { 22 | // font-family: "Amarante"; 23 | // src: url("/fonts/Amarante-Regular.ttf") format("truetype"); 24 | // font-weight: 400; 25 | // font-display: swap; 26 | // } 27 | 28 | // @font-face { 29 | // font-family: "Daffodil"; 30 | // src: url("/fonts/EDDaffodil-Regular.ttf") format("truetype"); 31 | // font-weight: 500; 32 | // font-display: swap; 33 | // } 34 | -------------------------------------------------------------------------------- /website/utils/apiInject.ts: -------------------------------------------------------------------------------- 1 | import { inject, provide } from 'vue'; 2 | 3 | /** 4 | * Helper function around provide/inject to create a typed pair with a curried "key" and default values 5 | */ 6 | export function createContext>( 7 | defaultValue: (args: Args) => Values, 8 | ) { 9 | 10 | const key = Symbol() 11 | const provideContext = (value: Args): any => { 12 | const defaultVal = defaultValue(value); 13 | const constructedVal = Object.assign(defaultVal, value) 14 | provide(key, constructedVal); 15 | return constructedVal 16 | }; 17 | 18 | const useContext = (value?: Values): Values => { 19 | return inject(key, value) as Values 20 | } 21 | // const useContext = (args?: Parameters>) => { 22 | // if (args?.[2] !== undefined) { 23 | // return inject(key, args?.[1] || undefined, args[2]) 24 | // } 25 | // return inject(key, args?.[1] || undefined) 26 | // } 27 | 28 | return [provideContext, useContext, key] as const; 29 | } -------------------------------------------------------------------------------- /website/composables/useCleanedScope.ts: -------------------------------------------------------------------------------- 1 | import { onWatcherCleanup, getCurrentWatcher } from "vue" 2 | 3 | export const useCleanScope = (callback: (() => void), options?: { detached: boolean }) => { 4 | const currentScope = getCurrentScope(), currentWatcher = getCurrentWatcher(), currentInstance = getCurrentInstance(), isVue = !!currentInstance 5 | const detached = options?.detached ?? false 6 | if (!detached && !currentScope && !currentWatcher && !currentInstance) throw "useCleanScope is outside a scope or watcher" 7 | 8 | const scope = effectScope(detached); 9 | 10 | if (!!currentWatcher) { 11 | onWatcherCleanup(() => { 12 | scope.stop() 13 | }) 14 | } 15 | 16 | if (!!currentInstance && !currentInstance?.isMounted && !currentWatcher) { 17 | onMounted(() => { 18 | scope.run(() => { 19 | callback() 20 | }) 21 | }) 22 | } else { 23 | scope.run(() => { 24 | callback(); 25 | }); 26 | } 27 | return scope 28 | }; 29 | -------------------------------------------------------------------------------- /website/plugins/10.directives.cursor.ts: -------------------------------------------------------------------------------- 1 | import type { DirectiveBinding, EffectScope } from "vue"; 2 | 3 | export default defineNuxtPlugin((nuxtApp) => { 4 | const cleanUpMap = new WeakMap(); 5 | 6 | nuxtApp.vueApp.directive("cursor", { 7 | mounted(el: HTMLElement, binding: DirectiveBinding<{ speed?: number }>) { 8 | const speed = binding.value?.speed ?? 1 9 | 10 | const { state } = useCursor() 11 | 12 | const scope = useCleanScope( 13 | () => { 14 | el.addEventListener("mousemove", () => { 15 | state.value = "active" 16 | }) 17 | el.addEventListener("mouseleave", () => { 18 | state.value = "default" 19 | }) 20 | }, 21 | { detached: true }, 22 | ); 23 | 24 | cleanUpMap.set(el, scope); 25 | }, 26 | 27 | beforeUnmount(el) { 28 | cleanUpMap.get(el)?.stop(); 29 | }, 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Nam HaÏ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /website/pages/work/[slug].vue: -------------------------------------------------------------------------------- 1 | 8 | 33 | 34 | -------------------------------------------------------------------------------- /website/pages/error.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | 26 | 51 | -------------------------------------------------------------------------------- /website/styles/app/grid.scss: -------------------------------------------------------------------------------- 1 | @use "../sass/mixins.scss" as *; 2 | @use "../sass/variables.module.scss" as *; 3 | @use "../sass/breakpoints.scss" as *; 4 | 5 | :root{ 6 | 7 | // note all values need px unit for calc() to work. 8 | // ie: 0 should be 0px 9 | 10 | --grid-columns: 12; 11 | --grid-column-gap: 2.4rem; 12 | --grid-side-margin: 0; 13 | --scrollbar-width: 0px; 14 | 15 | --grid-max-width: #{$desktopGridWidth * $desktopMaxScale * 1px}; 16 | --grid-width-input: calc(100vw - var(--scrollbar-width) - var(--grid-side-margin) * 2); 17 | 18 | --grid-width: min( var(--grid-width-input) , var(--grid-max-width) ); 19 | --grid-column-width: calc( (var(--grid-width) - (var(--grid-columns) - 1) * var(--grid-column-gap)) / var(--grid-columns) ); 20 | 21 | &.windows{ 22 | 23 | --scrollbar-width: #{$scrollbarWidth}; 24 | @include breakpoint(mobile); 25 | //--scrollbar-width: 0px // this breaks the layout - leave for windows 26 | } 27 | 28 | 29 | @include breakpoint(mobile){ 30 | --scrollbar-width: 0px; 31 | --grid-columns: 4; 32 | --grid-column-gap: 1.6rem; 33 | --grid-side-margin: 0; 34 | } 35 | 36 | } 37 | 38 | .the-container { 39 | @include mainContainer(); 40 | } 41 | .the-grid { 42 | @include mainGrid(); 43 | } -------------------------------------------------------------------------------- /website/styles/app/reset.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | // line-height: 0 !important; 5 | } 6 | 7 | body { 8 | white-space: pre-line; 9 | } 10 | 11 | #__nuxt { 12 | display: contents; 13 | } 14 | 15 | html, 16 | body, 17 | #__nuxt { 18 | width: 100%; 19 | height: 100%; 20 | } 21 | 22 | html { 23 | height: auto; 24 | } 25 | 26 | body { 27 | display: grid; 28 | } 29 | 30 | *, 31 | *:before, 32 | *:after { 33 | box-sizing: border-box; 34 | -webkit-tap-highlight-color: transparent; 35 | } 36 | 37 | a { 38 | text-decoration: none; 39 | color: inherit; 40 | } 41 | 42 | ul, 43 | ol { 44 | margin: 0; 45 | padding: 0; 46 | list-style: none; 47 | } 48 | 49 | h1, 50 | h2, 51 | h3, 52 | h4, 53 | p { 54 | margin: 0; 55 | } 56 | 57 | button, 58 | input { 59 | border: none; 60 | outline: none; 61 | appearance: auto; 62 | font-size: inherit; 63 | font-weight: inherit; 64 | font-family: inherit; 65 | text-transform: inherit; 66 | color: inherit; 67 | padding: 0; 68 | } 69 | 70 | button { 71 | cursor: pointer; 72 | -webkit-tap-highlight-color: transparent; 73 | background: none; 74 | } 75 | 76 | img, 77 | video, 78 | svg { 79 | user-select: none; 80 | pointer-events: none; 81 | -webkit-user-drag: none; 82 | -webkit-touch-callout: none; 83 | } 84 | -------------------------------------------------------------------------------- /website/components/global/Menu.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 27 | 28 | 46 | -------------------------------------------------------------------------------- /website/lib/waterflow/composables/onFlow.ts: -------------------------------------------------------------------------------- 1 | import { EffectScope, onMounted, watch } from "vue" 2 | import { useFlowProvider } from "../FlowProvider" 3 | import type { RouteLocationNormalized } from 'vue-router'; 4 | 5 | export function onFlow(callback?: (from: RouteLocationNormalized, to: RouteLocationNormalized) => void) { 6 | const { flowIsHijackedPromise, routeFrom, routeTo } = useFlowProvider() 7 | const flow = ref(false) 8 | 9 | let asyncScope: EffectScope | undefined 10 | flowIsHijackedPromise.value && flowIsHijackedPromise.value.then(() => { 11 | asyncScope = effectScope(true) 12 | asyncScope.run(() => { 13 | flow.value = true 14 | callback && callback(routeFrom.value, routeTo.value) 15 | }) 16 | }) 17 | 18 | onScopeDispose(() => { 19 | asyncScope && asyncScope.stop() 20 | }) 21 | 22 | onMounted(() => { 23 | if (!!flowIsHijackedPromise.value) return 24 | flow.value = true 25 | callback && callback(routeFrom.value, routeTo.value) 26 | }) 27 | 28 | return flow 29 | } 30 | 31 | /** experimental */ 32 | export function onLeave(callback: (from: RouteLocationNormalized, to: RouteLocationNormalized) => void) { 33 | const { flowIsHijackedPromise, routeFrom, routeTo } = useFlowProvider() 34 | watch(flowIsHijackedPromise, flow => { 35 | if (!!flow) { 36 | callback(routeFrom.value, routeTo.value) 37 | } 38 | }) 39 | } -------------------------------------------------------------------------------- /website/styles/sass/_mixins.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | @use "sass:map"; 3 | @use "./variables.module.scss" as *; 4 | @use "vh-property.scss" as *; 5 | 6 | @mixin full { 7 | position: absolute; 8 | top: 0; 9 | left: 0; 10 | width: 100%; 11 | height: 100%; 12 | } 13 | @mixin center-img { 14 | position: absolute; 15 | top: 0px; 16 | bottom: 0px; 17 | left: 0px; 18 | right: 0px; 19 | max-width: 100%; 20 | max-height: 100%; 21 | margin: auto; 22 | } 23 | 24 | @mixin user-select($val) { 25 | -webkit-user-select: $val; 26 | -moz-user-select: $val; 27 | -ms-user-select: $val; 28 | user-select: $val; 29 | } 30 | @mixin user-drag($val) { 31 | -webkit-user-drag: $val; 32 | -moz-user-drag: $val; 33 | -ms-user-drag: $val; 34 | user-drag: $val; 35 | } 36 | 37 | @mixin mainContainer() { 38 | $desktopWidth: map.get(map.get(($breakpoints), desktop), design-width); 39 | $desktopMaxScale: map.get(map.get(($breakpoints), desktop), scale-max); 40 | width: var(--grid-width, 100%); 41 | max-width: #{$desktopWidth * $desktopMaxScale * 1px}; // css var doesn't work here for some reason 42 | margin-left: auto; 43 | margin-right: auto; 44 | } 45 | 46 | @mixin mainGrid() { 47 | @include mainContainer; 48 | display: grid; 49 | grid-template-columns: repeat(var(--grid-columns), 1fr); 50 | grid-column-gap: var(--grid-column-gap); 51 | } 52 | 53 | @mixin hide-cursor() { 54 | cursor: none; 55 | 56 | > * { 57 | cursor: none; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /website/styles/sass/_easing.scss: -------------------------------------------------------------------------------- 1 | $easeInSine: cubic-bezier(0.470, 0.000, 0.745, 0.715); 2 | $easeOutSine: cubic-bezier(0.390, 0.575, 0.565, 1.000); 3 | $easeInOutSine: cubic-bezier(0.445, 0.050, 0.550, 0.950); 4 | 5 | $easeInQuad: cubic-bezier(0.550, 0.085, 0.680, 0.530); 6 | $easeOutQuad: cubic-bezier(0.250, 0.460, 0.450, 0.940); 7 | $easeInOutQuad: cubic-bezier(0.455, 0.030, 0.515, 0.955); 8 | 9 | $easeInCubic: cubic-bezier(0.550, 0.055, 0.675, 0.190); 10 | $easeOutCubic: cubic-bezier(0.215, 0.610, 0.355, 1.000); 11 | $easeInOutCubic: cubic-bezier(0.645, 0.045, 0.355, 1.000); 12 | 13 | $easeInQuart: cubic-bezier(0.895, 0.030, 0.685, 0.220); 14 | $easeOutQuart: cubic-bezier(0.165, 0.840, 0.440, 1.000); 15 | $easeInOutQuart: cubic-bezier(0.770, 0.000, 0.175, 1.000); 16 | 17 | $easeInQuint: cubic-bezier(0.755, 0.050, 0.855, 0.060); 18 | $easeOutQuint: cubic-bezier(0.230, 1.000, 0.320, 1.000); 19 | $easeInOutQuint: cubic-bezier(0.860, 0.000, 0.070, 1.000); 20 | 21 | $easeInExpo: cubic-bezier(0.950, 0.050, 0.795, 0.035); 22 | $easeOutExpo: cubic-bezier(0.190, 1.000, 0.220, 1.000); 23 | $easeInOutExpo: cubic-bezier(1.000, 0.000, 0.000, 1.000); 24 | 25 | $easeInCirc: cubic-bezier(0.600, 0.040, 0.980, 0.335); 26 | $easeOutCirc: cubic-bezier(0.075, 0.820, 0.165, 1.000); 27 | $easeInOutCirc: cubic-bezier(0.785, 0.135, 0.150, 0.860); 28 | 29 | $easeInBack: cubic-bezier(0.600, -0.280, 0.735, 0.045); 30 | $easeOutBack: cubic-bezier(0.175, 0.885, 0.320, 1.275); 31 | $easeInOutBack: cubic-bezier(0.680, -0.550, 0.265, 1.550); -------------------------------------------------------------------------------- /website/styles/core.scss: -------------------------------------------------------------------------------- 1 | @forward "./app/index.scss"; 2 | 3 | :root { 4 | --vh100max: 100vh; 5 | --100vh: 100vh; 6 | --vh100min: 100vh; 7 | --vh: 1vh; 8 | 9 | --jsvh100min: 100vh; 10 | 11 | --roughMin: calc(100vh - (100vw * 0.3)); 12 | --safe100min: max(var(--roughMin), var(--jsvh100min)); 13 | 14 | @include breakpoint(mobile) { 15 | --vh100min: var(--safe100min); 16 | } 17 | } 18 | 19 | html { 20 | -webkit-font-smoothing: antialiased; 21 | -moz-osx-font-smoothing: grayscale; 22 | -webkit-text-size-adjust: none; 23 | font-size: 10px; 24 | 25 | font-weight: 500; 26 | background-color: rgb(29, 29, 29); 27 | 28 | &.remlock { 29 | font-size: 10px !important; 30 | } 31 | @include rem-scale(); 32 | overflow: hidden; 33 | } 34 | 35 | .mobile-only { 36 | @include breakpoint(desktop) { 37 | display: none !important; 38 | } 39 | } 40 | 41 | .desktop-only { 42 | @include breakpoint(mobile) { 43 | display: none !important; 44 | } 45 | } 46 | 47 | main { 48 | position: relative; 49 | z-index: 5; 50 | } 51 | 52 | section, 53 | footer, 54 | main { 55 | // overflow: hidden; 56 | position: relative; 57 | } 58 | 59 | main { 60 | width: 100%; 61 | min-height: var(--100vh); 62 | position: relative; 63 | } 64 | 65 | .flowIsHijacked { 66 | pointer-events: none; 67 | 68 | * { 69 | pointer-events: none; 70 | } 71 | } 72 | 73 | main { 74 | font-size: 2rem; 75 | 76 | padding-top: 10rem; 77 | background-color: white; 78 | width: 100%; 79 | display: flex; 80 | align-items: center; 81 | flex-direction: column; 82 | row-gap: 1rem; 83 | 84 | p { 85 | width: 70rem; 86 | border-radius: 0.2rem; 87 | padding: 2rem; 88 | background-color: beige; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /website/styles/app/anim.scss: -------------------------------------------------------------------------------- 1 | @keyframes spin-clockwise { 2 | to { 3 | transform: rotate(360deg); 4 | } 5 | } 6 | 7 | @keyframes spin-counterclockwise { 8 | to { 9 | transform: rotate(-360deg); 10 | } 11 | } 12 | 13 | 14 | @keyframes dashoffset { 15 | to { 16 | stroke-dashoffset: 0; 17 | } 18 | } 19 | 20 | @keyframes flash { 21 | 0% { 22 | opacity: 0.2; 23 | } 24 | 20% { 25 | opacity: 1; 26 | } 27 | 40% { 28 | opacity: 0.2; 29 | } 30 | 60% { 31 | opacity: 1; 32 | } 33 | 80% { 34 | opacity: 0.2; 35 | } 36 | 100% { 37 | opacity: 1; 38 | } 39 | } 40 | 41 | 42 | 43 | 44 | 45 | @keyframes pulse { 46 | 0% { 47 | opacity: 1; 48 | transform: none; 49 | } 50 | 100% { 51 | opacity: 0; 52 | transform: scale(1.5); 53 | } 54 | } 55 | 56 | 57 | 58 | 59 | 60 | @keyframes blink { 61 | from { 62 | opacity: 0; 63 | } 64 | to { 65 | opacity: 1; 66 | } 67 | } 68 | 69 | 70 | 71 | 72 | 73 | @keyframes bounce { 74 | 0%, 75 | 100% { 76 | transform: translateY(0); 77 | } 78 | 50% { 79 | transform: translateY(-30%); 80 | } 81 | } 82 | 83 | 84 | 85 | 86 | 87 | @keyframes slideright { 88 | 0% { 89 | transform: translateX(-100%); 90 | } 91 | 100% { 92 | transform: translateX(200%); 93 | } 94 | } 95 | 96 | 97 | 98 | @keyframes redraw { 99 | 0% { 100 | transform: translateX(-100%); 101 | } 102 | 100% { 103 | transform: translateX(0%); 104 | } 105 | } 106 | 107 | 108 | 109 | .fade-enter-active { 110 | transition: all 0.2s ease 0s; 111 | } 112 | .fade-enter-from { 113 | opacity: 0; 114 | } 115 | .fade-leave-active { 116 | transition: all 0.2s ease 0s; 117 | } 118 | .fade-leave-to { 119 | opacity: 0; 120 | } -------------------------------------------------------------------------------- /website/styles/sass/_variables.module.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | @use "sass:map"; 3 | 4 | // **************************** 5 | // BREAKPOINTS 6 | // **************************** 7 | $site-scale: 1; 8 | $scale-mode: fit; // width, height, fit 9 | // $scale-mode: fit; // width, height, fit 10 | 11 | // height: 932px; 12 | $design-mobile-width: 430; 13 | $design-mobile-height: 932; 14 | $design-desktop-width: 1440; 15 | $design-desktop-height: 900; 16 | 17 | $mobile-scale-min: math.div(320, $design-mobile-width); 18 | $mobile-scale-max: math.div(550, $design-mobile-width); 19 | $desktop-scale-min: math.div(768, $design-desktop-width); 20 | 21 | $breakpoints: ( 22 | mobile: ( 23 | width: 320, 24 | design-width: $design-mobile-width, 25 | design-height: $design-mobile-height, 26 | scale-min: 0.8, 27 | scale-max: $mobile-scale-max, 28 | ), 29 | desktop: ( 30 | width: 768, 31 | design-width: $design-desktop-width, 32 | design-height: $design-desktop-height, 33 | scale-min: 0.8, 34 | scale-max: 3, 35 | ), 36 | ); 37 | 38 | // **************************** 39 | // GRID 40 | // **************************** 41 | $desktopGridWidth: 1480; 42 | $desktopWidth: map.get(map.get(($breakpoints), desktop), design-width); 43 | $desktopMaxScale: map.get(map.get(($breakpoints), desktop), scale-max); 44 | $scrollbarWidth: 14px; 45 | 46 | // **************************** 47 | // Z-INDEXES 48 | // **************************** 49 | $z-popup: 100; 50 | $z-menu: 200; 51 | $z-tutorial: 300; 52 | $z-preloader: 400; 53 | $z-cursor: 500; 54 | $z-rotation-message: 600; 55 | 56 | // **************************** 57 | // SIZE 58 | // **************************** 59 | $p1: 0.8rem; 60 | $p2: 1.6rem; 61 | $p3: 2.4rem; 62 | $p4: 3.2rem; 63 | $p5: 4rem; 64 | $p6: 4.8rem; 65 | $p7: 5.6rem; 66 | $p8: 6.4rem; 67 | $p9: 7.2rem; 68 | $p10: 8rem; 69 | 70 | // **************************** 71 | // LAYOUT 72 | // **************************** 73 | 74 | -------------------------------------------------------------------------------- /website/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | import mocks from "./server/api/mocks.json" 3 | 4 | 5 | export default defineNuxtConfig({ 6 | compatibilityDate: "2024-11-23", 7 | ssr: true, 8 | pages: true, 9 | devtools: { enabled: true }, 10 | devServer: { 11 | host: "0.0.0.0", 12 | }, 13 | css: ["@/styles/core.scss"], 14 | app: { 15 | layoutTransition: false, 16 | head: { 17 | link: [ 18 | { 19 | rel: 'icon', 20 | type: 'image/png', 21 | sizes: '32x32', 22 | href: 'favicon-32x32.png' 23 | }, 24 | { 25 | rel: 'icon', 26 | type: 'image/png', 27 | sizes: '16x16x', 28 | href: 'favicon-16x16.png' 29 | }, 30 | { 31 | rel: 'manifest', 32 | href: 'site.webmanifest' 33 | } 34 | ], 35 | meta: [ 36 | { 37 | name: "description", 38 | content: "Waterflow" 39 | }, 40 | { 41 | charset: 'utf-8' 42 | } 43 | ], 44 | title: 'Waterflow' 45 | } 46 | }, 47 | vite: { 48 | css: { 49 | preprocessorOptions: { 50 | scss: { 51 | additionalData: '@use "~/styles/_shared.scss" as *;', 52 | api: "modern-compiler", 53 | quietDeps: true, 54 | silenceDeprecations: ["mixed-decls"], 55 | }, 56 | }, 57 | }, 58 | }, 59 | vue: { 60 | runtimeCompiler: true, 61 | }, 62 | 63 | routeRules: { 64 | "/": { prerender: true }, 65 | "/foo": { prerender: true }, 66 | "/baz": { prerender: true }, 67 | // "/work/**": { prerender: true }, 68 | '/work/**': { isr: true }, 69 | "/api/**": { isr: true }, 70 | }, 71 | }) 72 | -------------------------------------------------------------------------------- /website/lib/waterflow/FlowProvider.ts: -------------------------------------------------------------------------------- 1 | import type { ShallowRef } from "vue"; 2 | import { createContext } from "./utils/apiInject" 3 | import type { RouteLocationNormalized } from '#vue-router'; 4 | 5 | 6 | 7 | type CrossFadeMode = "TOP" | "BOTTOM" 8 | export const [provideFlowProvider, useFlowProvider, flowKey] = createContext(() => { 9 | const route = useRoute() 10 | const currentRoute = shallowRef(route) 11 | const routeTo = shallowRef(route) 12 | const routeFrom = shallowRef(route) 13 | 14 | const crossfadeMode: ShallowRef = shallowRef("TOP") 15 | 16 | const flowIsHijackedPromise: ShallowRef | undefined> = shallowRef(undefined) 17 | const flowIsHijacked = computed(() => { 18 | return !!flowIsHijackedPromise.value 19 | }) 20 | let flowHijackResolver: (() => void) | undefined 21 | 22 | function releaseHijackFlow() { 23 | if (!flowHijackResolver) return 24 | flowHijackResolver() 25 | flowIsHijackedPromise.value = undefined 26 | flowHijackResolver = undefined 27 | } 28 | 29 | function hijackFlow() { 30 | flowIsHijackedPromise.value = new Promise((resolve) => { 31 | flowHijackResolver = resolve; 32 | }); 33 | 34 | return flowIsHijackedPromise.value 35 | } 36 | 37 | const flowInPromise: Ref | undefined> = shallowRef() 38 | function startFlowIn(): undefined | (() => void) { 39 | let resolver: ((() => void) | undefined) = undefined 40 | 41 | flowInPromise.value = new Promise((resolve) => { 42 | resolver = resolve; 43 | }); 44 | return resolver 45 | } 46 | 47 | watch(currentRoute, (newVal, oldVal) => { 48 | routeTo.value = newVal 49 | routeFrom.value = oldVal 50 | }) 51 | 52 | return { 53 | currentRoute: currentRoute, 54 | routeTo: shallowReadonly(routeTo), 55 | routeFrom: shallowReadonly(routeFrom), 56 | 57 | crossfadeMode, 58 | 59 | flowIsHijackedPromise: shallowReadonly(flowIsHijackedPromise), 60 | flowIsHijacked: shallowReadonly(flowIsHijacked), 61 | hijackFlow, 62 | releaseHijackFlow, 63 | 64 | flowInPromise: shallowReadonly(flowInPromise), 65 | startFlowIn, 66 | } 67 | }); 68 | -------------------------------------------------------------------------------- /website/lib/waterflow/composables/usePageFlow.ts: -------------------------------------------------------------------------------- 1 | import { EffectScope, onUnmounted } from "vue"; 2 | import { useRouter } from "vue-router"; 3 | import { useFlowProvider } from "../FlowProvider"; 4 | import type { RouteLocationNormalized } from "#vue-router"; 5 | import { onLeave } from "./onFlow"; 6 | 7 | export type FlowFunction = (props: T, resolve: () => void) => void 8 | 9 | type FlowKey = `default` | `${string} => ${string}`; 10 | 11 | type PageFlowOptions = { 12 | props: T, 13 | flowOut?: FlowFunction, 14 | flowOutMap?: Map>, 15 | flowIn?: FlowFunction, 16 | flowInMap?: Map>, 17 | } 18 | 19 | export function usePageFlow({ 20 | props, 21 | flowOutMap, 22 | flowInMap, 23 | }: PageFlowOptions) { 24 | 25 | const { flowIsHijackedPromise, flowInPromise, startFlowIn, routeFrom, routeTo } = useFlowProvider() 26 | 27 | const scopeIn = effectScope() 28 | const resolver = startFlowIn() 29 | onMounted(() => { 30 | if (!flowIsHijackedPromise.value) return resolver && resolver() 31 | scopeIn.run(async () => { 32 | await createFlow(routeFrom.value, routeTo.value, flowInMap, props) 33 | resolver && resolver() 34 | }) 35 | }) 36 | 37 | onLeave(async (from, to) => { 38 | scopeIn.stop() 39 | 40 | const scope = effectScope(true) 41 | await new Promise((res, rej) => { 42 | scope.run(async () => { 43 | await createFlow(from, to, flowOutMap, props) 44 | res() 45 | }) 46 | }) 47 | 48 | scope.stop() 49 | 50 | }) 51 | } 52 | 53 | function createFlow(from: RouteLocationNormalized, to: RouteLocationNormalized, flowMap: Map> | undefined, props: T): Promise { 54 | const fromName = from.name?.toString(), toName = to.name?.toString() 55 | const key: string = fromName + ' => ' + toName 56 | const keyDefaultIn = fromName + " => any" 57 | const keyDefaultOut = "any => " + toName 58 | 59 | const FlowFunction = flowMap?.get(key) || flowMap?.get(keyDefaultIn) || flowMap?.get(keyDefaultOut) || flowMap?.get('default') || undefined 60 | return new Promise(cb => { 61 | if (!FlowFunction) cb() 62 | else FlowFunction(props, cb) 63 | }) 64 | } -------------------------------------------------------------------------------- /website/composables/useDrag.ts: -------------------------------------------------------------------------------- 1 | export const useDrag = ({ wrapper, limit }: { wrapper: Ref, limit?: { min: number, max: number } }) => { 2 | limit = limit ? limit : { min: -Infinity, max: Infinity } 3 | const on = ref(false) 4 | 5 | const start = { 6 | x: 0, 7 | y: 0 8 | } 9 | const end = { 10 | x: 0, 11 | y: 0 12 | } 13 | const distance = reactive({ 14 | x: 0, 15 | y: 0 16 | }) 17 | const distanceOnStart = { 18 | x: 0, 19 | y: 0 20 | } 21 | 22 | onMounted(() => { 23 | wrapper.value.addEventListener('mousedown', onMouseDown) 24 | wrapper.value.addEventListener('touchstart', onTouchStart) 25 | window.addEventListener('mousemove', onMouseMove) 26 | window.addEventListener('touchmove', onTouchMove) 27 | 28 | window.addEventListener('mouseup', onMouseUp) 29 | window.addEventListener('touchend', onTouchEnd) 30 | }) 31 | 32 | onBeforeUnmount(() => { 33 | wrapper.value.removeEventListener('mousedown', onMouseDown) 34 | wrapper.value.removeEventListener('touchstart', onTouchStart) 35 | 36 | window.removeEventListener('mousemove', onMouseMove) 37 | window.removeEventListener('touchmove', onTouchMove) 38 | 39 | window.removeEventListener('mouseup', onMouseUp) 40 | window.removeEventListener('touchend', onTouchEnd) 41 | }) 42 | 43 | function onTouchStart(event: TouchEvent) { 44 | dragStart({ 45 | x: event.touches[0].clientX, 46 | y: event.touches[0].clientY 47 | }) 48 | } 49 | function onMouseDown(event: MouseEvent) { 50 | dragStart({ 51 | x: event.clientX, 52 | y: event.clientY 53 | }) 54 | } 55 | 56 | function onMouseMove(event: MouseEvent) { 57 | dragMove({ 58 | x: event.clientX, 59 | y: event.clientY 60 | }) 61 | } 62 | function onTouchMove(event: TouchEvent) { 63 | dragMove({ 64 | x: event.touches[0].clientX, 65 | y: event.touches[0].clientY 66 | }) 67 | } 68 | function onMouseUp() { 69 | dragEnd() 70 | } 71 | function onTouchEnd() { 72 | dragEnd() 73 | } 74 | 75 | 76 | function dragStart({ x, y }: { x: number, y: number }) { 77 | on.value = true 78 | start.x = x 79 | start.y = y 80 | 81 | distanceOnStart.x = distance.x 82 | distanceOnStart.y = distance.y 83 | } 84 | function dragMove({ x, y }: { x: number, y: number }) { 85 | end.x = x 86 | end.y = y 87 | if (!on.value) return 88 | distance.x = end.x - start.x + distanceOnStart.x 89 | distance.y = end.y - start.y + distanceOnStart.y 90 | 91 | distance.x = N.Clamp(distance.x, limit!.min, limit!.max) 92 | distance.y = N.Clamp(distance.y, limit!.min, limit!.max) 93 | } 94 | 95 | function dragEnd() { 96 | on.value = false 97 | } 98 | return { distance, on } 99 | } -------------------------------------------------------------------------------- /website/utils/text.ts: -------------------------------------------------------------------------------- 1 | export function split(element: HTMLElement, expression = ' ', append = true ) { 2 | const words = splitText(element.innerText.trim(), expression) 3 | 4 | let innerHTML = '' 5 | 6 | for (const line of words) { 7 | if (line.indexOf('
') > -1) { 8 | const lines = line.split('
') 9 | 10 | for (const [index, line] of lines.entries()) { 11 | innerHTML += (index > 0) ? '
' + parseLine(line) : parseLine(line) 12 | } 13 | } else { 14 | innerHTML += parseLine(line) 15 | } 16 | } 17 | 18 | element.innerHTML = innerHTML 19 | 20 | const spans = element.querySelectorAll('span') 21 | 22 | 23 | if (append) { 24 | for (const span of spans) { 25 | const isSingleLetter = span.textContent?.length === 1, 26 | isNotEmpty = span.innerHTML.trim() !== '', 27 | isNotAndCharacter = span.textContent !== '&', 28 | isNotDashCharacter = span.textContent !== '-' 29 | 30 | if (isSingleLetter && isNotEmpty && isNotAndCharacter && isNotDashCharacter) { 31 | span.innerHTML = `${span.textContent} ` 32 | } 33 | } 34 | } 35 | return spans 36 | } 37 | function splitText(text: string, expression: string) { 38 | const splits = text.split('
') 39 | 40 | let words: string[] = [] 41 | 42 | for (const [index, item] of splits.entries()) { 43 | if (index > 0) { 44 | words.push('
') 45 | } 46 | 47 | words = words.concat(item.split(expression)) 48 | 49 | let isLink = false 50 | let link = '' 51 | 52 | const innerHTML = [] 53 | 54 | for (const word of words) { 55 | if (!isLink && (word.includes('') || word.includes('/strong>'))) { 66 | innerHTML.push(link) 67 | 68 | link = '' 69 | } 70 | 71 | if (!isLink && link === '') { 72 | innerHTML.push(word) 73 | } 74 | 75 | if (isLink && (word.includes('/a>') || word.includes('/strong>'))) { 76 | isLink = false 77 | } 78 | } 79 | 80 | words = innerHTML 81 | } 82 | 83 | return words 84 | } 85 | function parseLine(line: string) { 86 | line = line.trim() 87 | 88 | return (line === '' || line === ' ') ? line : line == '
' ? '
' : `${line}` + ((line.length > 1) ? ' ' : '') 89 | } 90 | 91 | export function calculate(spans: HTMLElement[] | NodeListOf) { 92 | const lines = [] 93 | let words = [] 94 | 95 | let position = spans[0].offsetTop 96 | 97 | for (const [index, span] of spans.entries()) { 98 | 99 | if (span.offsetTop === position) { 100 | words.push(span) 101 | } 102 | 103 | if (span.offsetTop !== position) { 104 | lines.push(words) 105 | 106 | words = [] 107 | words.push(span) 108 | 109 | position = span.offsetTop 110 | } 111 | 112 | if (index + 1 === spans.length) { 113 | lines.push(words) 114 | } 115 | } 116 | 117 | return lines 118 | } 119 | 120 | -------------------------------------------------------------------------------- /website/plugins/core/eases.ts: -------------------------------------------------------------------------------- 1 | export type EaseFunctionName = 'linear' | 'i1' | 'o1' | 'i2' | 'o2' | 'i3' | 'o3' | 'i4' | 'o4' | 'i5' | 'o5' | 'i6' | 'o6' | 'io1' | 'io2' | 'io3' | 'io4' | 'io5' | 'io6' 2 | export enum EaseEnum { 3 | linear = 'linear', 4 | i1 = "i1", 5 | i2 = "i2", 6 | i3 = "i3", 7 | i4 = "i4", 8 | i5 = "i5", 9 | i6 = "i6", 10 | o1 = "o1", 11 | o2 = "o2", 12 | o3 = "o3", 13 | o4 = "o4", 14 | o5 = "o5", 15 | o6 = "o6", 16 | io1 = "io1", 17 | io2 = "io2", 18 | io3 = "io3", 19 | io4 = "io4", 20 | io5 = "io5", 21 | io6 = "io6", 22 | } 23 | type EaseFunctionMap = { 24 | [K in EaseFunctionName]: (t: number) => number; 25 | } 26 | const Ease: EaseFunctionMap = { 27 | linear: t => t, 28 | i1: t => 1 - Math.cos(t * (.5 * Math.PI)), 29 | o1: t => Math.sin(t * (.5 * Math.PI)), 30 | io1: t => -.5 * (Math.cos(Math.PI * t) - 1), 31 | i2: t => t * t, 32 | o2: t => t * (2 - t), 33 | io2: t => t < .5 ? 2 * t * t : (4 - 2 * t) * t - 1, 34 | i3: t => t * t * t, 35 | o3: t => --t * t * t + 1, 36 | io3: t => t < .5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1, 37 | i4: t => t * t * t * t, 38 | o4: t => 1 - --t * t * t * t, 39 | io4: t => t < .5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t, 40 | i5: t => t * t * t * t * t, 41 | o5: t => 1 + --t * t * t * t * t, 42 | io5: t => t < .5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t, 43 | i6: t => 0 === t ? 0 : 2 ** (10 * (t - 1)), 44 | o6: t => 1 === t ? 1 : 1 - 2 ** (-10 * t), 45 | io6: t => 0 === t ? 0 : 1 === t ? 1 : (t /= .5) < 1 ? .5 * 2 ** (10 * (t - 1)) : .5 * (2 - 2 ** (-10 * --t)), 46 | } 47 | const r0 = (t: number, r: number) => 1 - 3 * r + 3 * t 48 | const r1 = (t: number, r: number) => 3 * r - 6 * t 49 | const r2 = (t: number, r: number, s: number) => ((r0(r, s) * t + r1(r, s)) * t + 3 * r) * t 50 | const r3 = (t: number, r: number, s: number) => 3 * r0(r, s) * t * t + 2 * r1(r, s) * t + 3 * r 51 | const r4 = (t: number, r: number, s: number, e: number, i: number) => { 52 | let a, n, o = 0; 53 | for (; n = r + .5 * (s - r), 0 < (a = r2(n, e, i) - t) ? s = n : r = n, 1e-7 < Math.abs(a) && ++o < 10;); 54 | return n 55 | } 56 | const r5 = (r: number, s: number, e: number, i: number) => { 57 | for (let t = 0; t < 4; ++t) { 58 | var a = r3(s, e, i); 59 | if (0 === a) return s; 60 | s -= (r2(s, e, i) - r) / a 61 | } 62 | return s 63 | } 64 | 65 | export type Ease4Arg = [number, number, number, number] 66 | const Ease4 = (t: [number, number, number, number]) => { 67 | const a = t[0], 68 | r = t[1], 69 | n = t[2], 70 | s = t[3]; 71 | let o = new Float32Array(11); 72 | if (a !== r || n !== s) 73 | for (let t = 0; t < 11; ++t) o[t] = r2(.1 * t, a, n); 74 | return (t: number) => a === r && n === s ? t : 0 === t ? 0 : 1 === t ? 1 : r2(function (t) { 75 | let r = 0; 76 | for (var s = 1; 10 !== s && o[s] <= t; ++s) r += .1; 77 | --s; 78 | var e = (t - o[s]) / (o[s + 1] - o[s]), 79 | e = r + .1 * e, 80 | i = r3(e, a, n); 81 | return .001 <= i ? r5(t, e, a, n) : 0 === i ? e : r4(t, i, i + .1, a, n) 82 | }(t), r, s) 83 | } 84 | 85 | 86 | export { Ease, Ease4 } -------------------------------------------------------------------------------- /website/lib/waterflow/WaterflowRouter.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 83 | 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Waterflow 3 |

4 | 5 | [![npm version](https://img.shields.io/npm/v/@nam-hai/water-flow/latest?color=green&label=%40nam-hai%2Fwater-flow&logo=npm)](https://www.npmjs.com/package/@nam-hai/water-flow) 6 | 7 | # Introduction 8 | 9 | Waterflow is a Nuxt3 library that enables seamless page transitions. 10 | 11 | # QUICK START 12 | 13 | ```bash 14 | $ npm i @nam-hai/water-flow 15 | ``` 16 | 17 | # Setup 18 | 19 | Pass the WaterflowRouter a callback to reset scroll after the page transitions 20 | 21 | ```vue 22 | // app.vue 23 | 30 | 31 | 34 | ``` 35 | 36 | # Example 37 | 38 | add `usePageFlow` to your page to enabled page-transtion : 39 | 40 | ```ts 41 | usePageFlow({ 42 | props: { 43 | main, // shallowRef, 44 | }, 45 | flowOutMap: new Map([ 46 | ["default", useDefaultFlowOut()], 47 | ["any => baz", useDefaultFlowOut("y", -1)], 48 | ["any => work-slug", useDefaultFlowOut("x")], 49 | ["work-slug => work-slug", useDefaultFlowOut("x")], 50 | ]), 51 | flowInMap: new Map([ 52 | ["default", useDefaultFlowIn()], 53 | ["any => baz", useDefaultFlowIn("y", -1)], 54 | ["any => work-slug", useDefaultFlowIn("x")], 55 | ["work-slug => work-slug", useDefaultFlowIn("x")], 56 | ]), 57 | }); 58 | ``` 59 | 60 | [See more](./website/pages.transition/defaultFlow.ts) 61 | 62 | # usePageFlow props 63 | 64 | | Name | Type | Default | Description | 65 | | ---------- | -------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------- | 66 | | props | T | | Pass props for later use | 67 | | flowOutMap | Map<[FlowKey](#type-flowfunction), [FlowFunction](#type-flowfunction)\> | undefined | Specify a Map of animations for the current page ([see more](#flowoutmap-and-flowincrossfademap)) | 68 | | flowInMap | Map<[FlowKey](#type-flowfunction), [FlowFunction](#type-flowfunction)\> | undefined | Specify a Map of animations for the next page ([see more](#flowoutmap-and-flowincrossfademap)) | 69 | 70 | # Type `FlowFunction` and `FlowKey` 71 | 72 | ```ts 73 | type FlowKey = `default` | `${string} => ${string}`; 74 | 75 | /** 76 | * call `resolve` when you are done 77 | */ 78 | type FlowFunction = (props: T, resolve: () => void) => void; 79 | ``` 80 | 81 | # flowOutMap and flowInMap 82 | 83 | Match a flowFunction to a string key following the patern : `routeNameFrom => routeNameTo`. `routeName` in the key can also take the value `any`. The key `default` also serve as a fallback if no match was found. 84 | 85 | # onFlow and onLeave 86 | 87 | `onFlow` is equivalent to onMounted, but is triggered after the page-transition ended. EffectScope are working in its callback (as far as I tested) 88 | `onLeave` is equivalent to onBeforeUnmounted, but is triggered when the page-transition start 89 | 90 | # useFlowProvider 91 | 92 | Change `crossfadeMode` to place the buffer-page on top or under the current-page. 93 | Use `const { currentRoute } = useFlowProvider()` instead of `useRoute()`. 94 | -------------------------------------------------------------------------------- /website/composables/pluginComposables.ts: -------------------------------------------------------------------------------- 1 | import type { EffectScope } from "vue" 2 | import { FramePriority, type FrameEvent } from "~/plugins/core/frame" 3 | import type { ResizeEvent } from "~/plugins/core/resize" 4 | import type { StopMotionOption } from "~/plugins/core/stopMotion" 5 | 6 | export function getFilm() { 7 | const { $motionFactory } = useNuxtApp() 8 | return $motionFactory.Film() 9 | } 10 | 11 | 12 | export function getMotion(arg: StopMotionOption) { 13 | const { $motionFactory } = useNuxtApp() 14 | return $motionFactory.Motion(arg) 15 | } 16 | 17 | 18 | export function getTimer(callback: () => void, throttle: number) { 19 | const { $frameFactory } = useNuxtApp() 20 | return $frameFactory.Timer({ callback, throttle: throttle }) 21 | } 22 | 23 | export function getDelay(callback: () => void, delay: number) { 24 | const { $frameFactory } = useNuxtApp() 25 | 26 | return $frameFactory.Delay({ callback, delay }) 27 | } 28 | 29 | export function getFrame(callback: (arg: FrameEvent) => void, priority: FramePriority = FramePriority.MAIN) { 30 | const { $frameFactory } = useNuxtApp() 31 | return $frameFactory.Frame({ callback, priority }) 32 | } 33 | 34 | export function useTimer(callback: () => void, throttle: number) { 35 | const timer = getTimer(callback, throttle) 36 | useCleanScope(() => { 37 | onScopeDispose(() => { 38 | timer.stop() 39 | }) 40 | }) 41 | 42 | return timer 43 | } 44 | export function getResize(callback: (e: ResizeEvent) => void) { 45 | const { $resizeFactory } = useNuxtApp() 46 | return $resizeFactory.Resize(callback) 47 | } 48 | 49 | export function useLenis() { 50 | const { $lenis } = useNuxtApp() 51 | return $lenis 52 | } 53 | 54 | export function useResize(callback: (e: ResizeEvent) => void) { 55 | const resize = getResize(callback) 56 | useCleanScope(() => { 57 | resize.on() 58 | 59 | onScopeDispose(() => { 60 | resize.off() 61 | }) 62 | }) 63 | return resize 64 | } 65 | 66 | 67 | /** 68 | * The delay created and its callback are scoped 69 | * If scope is destroyed before the delay, the callback is never called. 70 | * If scope is destroyed after the delay, the callback's scoped is cleanedup 71 | * 72 | * @example 73 | * ```js 74 | * onMounted(() => { 75 | * useDelay(() => { 76 | * useFrame(()=>{ 77 | * // insert code 78 | * }) 79 | * }, 1000) 80 | * }) 81 | * ``` 82 | * 83 | */ 84 | export function useDelay(callback: () => void, delay: number, detached = false) { 85 | let delayedScope: EffectScope | undefined; 86 | const d = getDelay(() => { 87 | delayedScope = useCleanScope(() => { 88 | return callback() 89 | }, { detached: true }) 90 | }, delay) 91 | 92 | useCleanScope(() => { 93 | d.run() 94 | onScopeDispose(() => { 95 | delayedScope?.stop() 96 | d.stop() 97 | }) 98 | }, { detached: true }) 99 | 100 | return d 101 | } 102 | 103 | 104 | export const useFrame = (cb: (e: FrameEvent) => void, priority: FramePriority = FramePriority.MAIN) => { 105 | const raf = getFrame(cb, priority) 106 | useCleanScope(() => { 107 | raf.run() 108 | 109 | onScopeDispose(() => { 110 | raf.kill() 111 | }) 112 | }) 113 | return raf 114 | } 115 | 116 | 117 | export function useMotion(arg: StopMotionOption) { 118 | const motion = getMotion(arg) 119 | useCleanScope(() => { 120 | onScopeDispose(() => { 121 | motion.pause() 122 | }) 123 | }) 124 | return motion 125 | } 126 | 127 | export function useFilm() { 128 | const film = getFilm() 129 | useCleanScope(() => { 130 | onScopeDispose(() => { 131 | film.pause() 132 | }) 133 | }) 134 | return film 135 | } 136 | -------------------------------------------------------------------------------- /website/pages.transition/defaultFlow.ts: -------------------------------------------------------------------------------- 1 | import { useLayout } from "../layouts/default.vue" 2 | import { usePageFlow } from "../lib/waterflow/composables/usePageFlow" 3 | 4 | const borderRadius = 3 5 | export const useDefaultFlowIn = (axis: "x" | "y" = "y", dir: 1 | -1 = 1) => { 6 | const { vh, vw, scale } = useScreen() 7 | 8 | return (props: { main: Ref }, resolve: () => void) => { 9 | if (!props.main.value) { 10 | resolve() 11 | return 12 | } 13 | const tl = useFilm() 14 | const bounds = props.main.value.getBoundingClientRect() 15 | const scaleXFrom = (bounds.width - 30 * scale.value) / bounds.width 16 | props.main.value.style.transformOrigin = `center ${vh.value / 2}px` 17 | props.main.value.style.borderRadius = `${borderRadius}px` 18 | 19 | tl.from({ 20 | el: props.main.value, 21 | p: { 22 | s: [scaleXFrom, scaleXFrom], 23 | [axis]: [dir * (axis === "x" ? vw.value : vh.value), 0, "px"] 24 | }, 25 | d: 750, 26 | e: "io2", 27 | }) 28 | tl.from({ 29 | el: props.main.value, 30 | p: { 31 | s: [scaleXFrom, 1], 32 | }, 33 | update(e) { 34 | if (!props.main.value) return 35 | props.main.value.style.borderRadius = `${N.Lerp(borderRadius, 0, e.easeProgress)}px` 36 | }, 37 | d: 750, 38 | delay: 750, 39 | e: "io2", 40 | }) 41 | tl.play().then(() => { 42 | resolve() 43 | }) 44 | } 45 | } 46 | 47 | export const useDefaultFlowOut = (axis: "x" | "y" = "y", dir: 1 | -1 = 1) => { 48 | const { overlay } = useLayout() 49 | const { vh, vw, scale } = useScreen() 50 | const lenis = useLenis() 51 | return (props: { main: Ref }, resolve: () => void) => { 52 | if (!props.main.value) { 53 | resolve() 54 | return 55 | } 56 | const tl = useFilm() 57 | 58 | const scroll = lenis.animatedScroll 59 | const bounds = props.main.value.getBoundingClientRect() 60 | const padding = 100 61 | const scaleXTo = (bounds.width - padding * scale.value) / bounds.width 62 | props.main.value.style.transformOrigin = `center ${vh.value / 2}px` 63 | 64 | tl.from({ 65 | el: overlay.value as HTMLElement, 66 | p: { 67 | o: [0, 0.8], 68 | }, 69 | d: 1500, 70 | }) 71 | tl.from({ 72 | el: props.main.value, 73 | p: { 74 | scaleX: [1, scaleXTo], 75 | scaleY: [1, scaleXTo], 76 | }, 77 | update(e) { 78 | if (!props.main.value) return 79 | props.main.value.style.borderRadius = `${N.Lerp(0, borderRadius, e.easeProgress)}px` 80 | }, 81 | d: 500, 82 | e: "o2" 83 | }) 84 | 85 | tl.play().then(() => { 86 | overlay.value && (overlay.value.style.opacity = "0") 87 | resolve() 88 | }) 89 | } 90 | } 91 | 92 | export const useDefaultFlow = (main: Ref) => { 93 | usePageFlow({ 94 | props: { 95 | main 96 | }, 97 | flowOutMap: new Map([ 98 | ["default", useDefaultFlowOut()], 99 | ["any => baz", useDefaultFlowOut("y", -1)], 100 | ["any => work-slug", useDefaultFlowOut("x")], 101 | ["work-slug => work-slug", useDefaultFlowOut("x")] 102 | ]), 103 | flowInMap: new Map([ 104 | ["default", useDefaultFlowIn()], 105 | ["any => baz", useDefaultFlowIn("y", -1)], 106 | ["any => work-slug", useDefaultFlowIn("x")], 107 | ["work-slug => work-slug", useDefaultFlowIn("x", -1)] 108 | ]) 109 | }) 110 | } -------------------------------------------------------------------------------- /website/utils/namhai.ts: -------------------------------------------------------------------------------- 1 | import { Ease, Ease4 } from "~/plugins/core/eases"; 2 | 3 | const Lerp = (xi: number, xf: number, t: number) => { 4 | return (1 - t) * xi + t * xf 5 | } 6 | 7 | const iLerp = (x: number, xi: number, xf: number) => { 8 | return (x - xi) / (xf - xi) 9 | } 10 | 11 | const Clamp = (x: number, min: number, max: number) => { 12 | return Math.max(Math.min(x, max), min) 13 | } 14 | 15 | const map = (x: number, start1: number, end1: number, start2: number, end2: number) => { 16 | return Lerp(start2, end2, iLerp(x, start1, end1)) 17 | } 18 | 19 | const get = (selector: string, context?: ParentNode) => { 20 | const c = context || document; 21 | return c.querySelector(selector) 22 | } 23 | const getAll = (selector: string, context?: ParentNode) => { 24 | const c = context || document 25 | return c.querySelectorAll(selector) 26 | } 27 | 28 | 29 | const Round = (x: number, decimal?: number) => { 30 | decimal = decimal === undefined ? 100 : 10 ** decimal; 31 | return Math.round(x * decimal) / decimal 32 | } 33 | const random = Math.random 34 | const Rand = { 35 | /** Rand.range avec par default step = 1% de la range */ 36 | range: (min: number, max: number, step = (max - min) / 1000) => { 37 | return Round(Math.random() * (max - min) + min, step) 38 | }, 39 | } 40 | const Arr = { 41 | /** Create an Array of n element */ 42 | create: (ArrayLength: number) => { 43 | return [...Array(ArrayLength).keys()] 44 | }, 45 | randomElement: (array: Array) => { 46 | return array[Rand.range(0, array.length, 0)] 47 | }, 48 | /** shuffle an Array */ 49 | shuffle: (array: Array) => { 50 | for (let i = array.length - 1; i > 0; i--) { 51 | const j = ~~(random() * (i + 1)); 52 | [array[i], array[j]] = [array[j], array[i]] 53 | } 54 | } 55 | } 56 | 57 | const T = (el: HTMLElement, x: number, y: number, unit: string = "%") => { 58 | el.style.transform = "translate3d(" + x + unit + "," + y + unit + ",0)" 59 | } 60 | const Bind = (context: any, methodArray: string[]) => { 61 | for (const methodString of methodArray) { 62 | context[methodString] = context[methodString].bind(context) 63 | } 64 | } 65 | 66 | const DOM = { 67 | ga: (context: Element, attribute: string) => context.getAttribute(attribute), 68 | sa: (context: Element, attribute: string, value: string) => context.setAttribute(attribute, value), 69 | T: T 70 | } 71 | const PD = (event: Event) => { 72 | event.cancelable && event.preventDefault() 73 | } 74 | const ZL = (t: number) => 9 < t ? '' + t : '0' + t 75 | 76 | const Class = { 77 | add: (el: Element, name: string) => { 78 | el.classList.add(name) 79 | }, 80 | remove: (el: Element, name: string) => { 81 | el.classList.remove(name) 82 | }, 83 | toggle: (el: Element, name: string, force?: boolean) => { 84 | el.classList.toggle(name, force) 85 | } 86 | } 87 | export class OrderedMap extends Map { 88 | orderedKeys: K[]; 89 | constructor() { 90 | super() 91 | 92 | this.orderedKeys = [] 93 | } 94 | 95 | override set(key: K, value: V) { 96 | if (!this.has(key)) { 97 | this.orderedKeys.push(key) 98 | this.orderedKeys.sort((a, b) => { return a - b }) 99 | } 100 | super.set(key, value) 101 | 102 | return this 103 | } 104 | } 105 | 106 | function binarySearch(arr: { id: number }[], n: number): { index: number, miss: boolean } { 107 | let left = 0 108 | let right = arr.length - 1 109 | 110 | while (left <= right) { 111 | const mid = Math.floor((right - left) / 2) + left 112 | const m = arr[mid].id 113 | 114 | if (n === m) { 115 | return { 116 | index: mid, 117 | miss: false 118 | } 119 | } else if (n < m) { 120 | right = mid - 1; 121 | } else { 122 | left = mid + 1; 123 | } 124 | } 125 | 126 | return { 127 | index: left, 128 | miss: true 129 | } 130 | } 131 | 132 | 133 | const mod = (n: number, m: number) => (n % m + m) % m; 134 | 135 | function lazy(getter: () => T): { value: T } { 136 | return { 137 | get value() { 138 | const value = getter() 139 | Object.assign(this, "value", value) 140 | return value 141 | } 142 | } 143 | } 144 | 145 | export const N = { 146 | Lerp, 147 | iLerp, 148 | Clamp, 149 | map, 150 | get, 151 | getAll, 152 | Round, 153 | binarySearch, 154 | random, 155 | Rand, 156 | Arr, 157 | lazy, 158 | Bind, 159 | DOM, 160 | PD, 161 | ZL, 162 | mod, 163 | Ease, 164 | Ease4, 165 | Class 166 | } -------------------------------------------------------------------------------- /website/styles/sass/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | @use "sass:math"; 2 | @use "sass:map"; 3 | @use "sass:list"; 4 | 5 | @use "variables.module.scss" as *; 6 | @use "functions.scss" as *; 7 | 8 | // ---------------------------------- 9 | // ADJUST ACTUAL BREAKPOINTS IN: 10 | // src/styles/config/variables.sass 11 | // ---------------------------------- 12 | 13 | // ---------------------------------- 14 | // set rem scaling breakpoints on HTML 15 | // ---------------------------------- 16 | 17 | @mixin rem-scale() { 18 | @each $breakpoint-key, $breakpoint in $breakpoints { 19 | $width: map.get(($breakpoint), width); 20 | $design-width: map.get(($breakpoint), design-width); 21 | $design-height: map.get(($breakpoint), design-height); 22 | $scale-min: map.get($breakpoint, scale-min); 23 | $scale-max: map.get($breakpoint, scale-max); 24 | $breakpoint-min: max(1px, $scale-min * $site-scale * 10px); 25 | $breakpoint-val-w: math.div(10, ($design-width * $site-scale)) * 100vw; 26 | $breakpoint-val-h: math.div(10, ($design-height * $site-scale)) * 100vh; 27 | $breakpoint-max: 10 * $scale-max * $site-scale * 1px; 28 | 29 | //clamp(MIN, VAL, MAX) 30 | // base breakpoint 31 | @include breakpoint($breakpoint-key) { 32 | min-height: 0vw; // safari fix; 33 | 34 | @if $scale-mode ==fit { 35 | font-size: clamp( 36 | #{$breakpoint-min}, 37 | min(#{$breakpoint-val-w}, #{$breakpoint-val-h}), 38 | #{$breakpoint-max} 39 | ); 40 | } @else if $scale-mode ==width { 41 | font-size: clamp( 42 | #{$breakpoint-min}, 43 | #{$breakpoint-val-w}, 44 | #{$breakpoint-max} 45 | ); 46 | } @else if $scale-mode ==height { 47 | font-size: clamp( 48 | #{$breakpoint-min}, 49 | #{$breakpoint-val-h}, 50 | #{$breakpoint-max} 51 | ); 52 | } 53 | } 54 | } 55 | } 56 | 57 | @mixin query-orientation($query, $orientation) { 58 | @if $orientation { 59 | @media #{$query} and (orientation: $orientation) { 60 | @content; 61 | } 62 | } @else { 63 | @media only screen and #{$query} { 64 | @content; 65 | } 66 | } 67 | } 68 | 69 | // ---------------------------------- 70 | // target CSS to a specific breakpoint 71 | // 72 | // example: 73 | // @include breakpoint(mobile){ 74 | // color: orage; 75 | // } 76 | // ---------------------------------- 77 | @mixin breakpoint($points, $orientation: false) { 78 | @each $point in $points { 79 | @each $breakpoint-key, $breakpoint in $breakpoints { 80 | $index: list.index(($breakpoints), ($breakpoint-key $breakpoint)); 81 | $last: $index ==list.length($breakpoints); 82 | $first: $index ==1; 83 | $width: map.get(($breakpoint), width); 84 | $scale-min: map.get($breakpoint, scale-min); 85 | $scale-max: map.get($breakpoint, scale-max); 86 | $scaled-min: $width * $scale-min; 87 | $scaled-max: $width * $scale-max; 88 | 89 | // base breakpoint 90 | @if $point ==$breakpoint-key { 91 | @if $last { 92 | @include query-orientation( 93 | "(min-width: #{$width * 1px})", 94 | $orientation 95 | ) { 96 | @content; 97 | } 98 | } @else { 99 | $next: map.get( 100 | ($breakpoints), 101 | index-to-key($breakpoints, $index + 1) 102 | ); 103 | $next-width: map.get(($next), width); 104 | 105 | $next-width2: $next-width - 1; 106 | 107 | @if $first { 108 | @include query-orientation( 109 | "(max-width: #{$next-width2 * 1px})", 110 | $orientation 111 | ) { 112 | @content; 113 | } 114 | } @else { 115 | @include query-orientation( 116 | "(min-width: #{$width * 1px}) and (max-width: #{$next-width2 * 1px})", 117 | $orientation 118 | ) { 119 | @content; 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | @mixin breakpoint-portrait() { 129 | @include breakpoint(mobile) { 130 | @content; 131 | } 132 | 133 | @include breakpoint(tablet, "portrait") { 134 | @content; 135 | } 136 | } 137 | 138 | @mixin breakpoint-width-min($width) { 139 | @media only screen and (min-width: $width) { 140 | @content; 141 | } 142 | } 143 | 144 | @mixin breakpoint-width-max($width) { 145 | @media only screen and (max-width: $width) { 146 | @content; 147 | } 148 | } 149 | 150 | @mixin breakpoint-debug() { 151 | @include breakpoint(desktop) { 152 | border: 1px solid cyan; 153 | } 154 | 155 | @include breakpoint(tablet) { 156 | border: 1px solid blue; 157 | } 158 | 159 | @include breakpoint(mobile) { 160 | border: 1px solid red; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /website/plugins/core/resize.ts: -------------------------------------------------------------------------------- 1 | import SassVars from '@/styles/sass/_breakpoints.module.scss' 2 | import { N } from "../../utils/namhai" 3 | import { FrameFactory, Timer } from './frame' 4 | 5 | 6 | type BreakpointType = { 7 | name: string, 8 | width: number 9 | } 10 | 11 | type DeviceTypes = { 12 | designSize: { 13 | width: number, 14 | height: number 15 | }, 16 | remScale: { 17 | min: number, 18 | max: number 19 | } 20 | } 21 | 22 | export type Breakpoints = "desktop" | "mobile" 23 | type ResizeEvent = { 24 | vh: number, 25 | vw: number, 26 | scale: number, 27 | breakpoint: Breakpoints 28 | } 29 | 30 | export class ResizeManager { 31 | timer: Timer 32 | stack: { 33 | id: number, 34 | cb: (e: ResizeEvent) => void 35 | }[] 36 | vw!: number 37 | vh!: number 38 | scale!: number 39 | breakpoint!: "desktop" | "mobile" 40 | breakpoints: Record = {} 41 | deviceTypes: Record = {} 42 | resizeId = 0 43 | mode: 'fit' | 'width' | 'height' 44 | 45 | constructor(Timer: FrameFactory["Timer"]) { 46 | N.Bind(this, ["resizePayload"]) 47 | this.stack = [] 48 | 49 | this.timer = Timer({ callback: this.resizePayload, throttle: 200 }) 50 | 51 | this.mode = SassVars.scale_mode as 'fit' | 'width' | 'height' 52 | this.init() 53 | this.update() 54 | window.addEventListener('resize', () => this.timer.tick()) 55 | } 56 | 57 | init() { 58 | SassVars.breakpoints.split(',').forEach((b: string) => { 59 | const point = b.trim() 60 | 61 | this.breakpoints[point] = { 62 | name: point, 63 | width: parseInt(SassVars[`breakpoint_${point}_width`]), 64 | } 65 | 66 | this.deviceTypes[point] = { 67 | designSize: { 68 | width: parseInt(SassVars[`breakpoint_${point}_design_width`]), 69 | height: parseInt(SassVars[`breakpoint_${point}_design_height`]), 70 | }, 71 | remScale: { 72 | min: parseFloat(SassVars[`breakpoint_${point}_scale_min`]), 73 | max: parseFloat(SassVars[`breakpoint_${point}_scale_max`]), 74 | }, 75 | } 76 | }) 77 | } 78 | 79 | add(t: { 80 | id: number, 81 | cb: (e: ResizeEvent) => void 82 | }) { 83 | const arg = { 84 | vw: this.vw, 85 | vh: this.vh, 86 | scale: this.scale, 87 | breakpoint: this.breakpoint 88 | } 89 | t.cb(arg) 90 | this.stack.push(t) 91 | } 92 | remove(id: number) { 93 | 94 | const { index, miss } = N.binarySearch(this.stack, id) 95 | 96 | if (miss) { 97 | console.warn("ResizeManager remove jammed : id not in stack") 98 | return 99 | } 100 | this.stack.splice(index, 1) 101 | } 102 | get callbackArg() { 103 | return { 104 | vw: this.vw, 105 | vh: this.vh, 106 | scale: this.scale, 107 | breakpoint: this.breakpoint 108 | } 109 | } 110 | 111 | private resizePayload() { 112 | this.update() 113 | 114 | const arg = { 115 | vw: this.vw, 116 | vh: this.vh, 117 | scale: this.scale, 118 | breakpoint: this.breakpoint 119 | } 120 | for (const el of this.stack) { 121 | el.cb(arg) 122 | } 123 | } 124 | 125 | private update() { 126 | this.updateSize() 127 | this.updateBreakpoint() 128 | this.updateScale() 129 | } 130 | 131 | private updateBreakpoint() { 132 | const bps = this.breakpoints 133 | let device: Breakpoints = 'mobile' 134 | 135 | for (const k of ['mobile', 'desktop'] as const) { 136 | if (this.vw >= bps[k].width) { 137 | device = k 138 | } 139 | } 140 | this.breakpoint = device 141 | } 142 | 143 | private updateSize() { 144 | this.vw = innerWidth 145 | this.vh = innerHeight 146 | 147 | document.documentElement.style.setProperty('--vh', `${this.vh * 0.01}px`) 148 | document.documentElement.style.setProperty('--100vh', `${this.vh}px`) 149 | } 150 | 151 | private updateScale() { 152 | const d = this.deviceTypes[this.breakpoint] 153 | const scaleX = this.vw / d.designSize.width 154 | const scaleY = this.vh / d.designSize.height 155 | 156 | // if (this.remlock) { 157 | // this.scale = 1 158 | // return 159 | // } 160 | 161 | // NOTE ACTUAL REMSCALE IS CALCULATED IN CSS 162 | // src/styles/config/variables.sass 163 | // src/styles/helpers/breakpoints.sass // =remscale() 164 | 165 | if (this.mode === 'fit') { 166 | this.scale = N.Clamp(Math.min(scaleX, scaleY), d.remScale.min, d.remScale.max) 167 | } 168 | else if (this.mode === 'width') { 169 | this.scale = N.Clamp(scaleX, d.remScale.min, d.remScale.max) 170 | } 171 | else if (this.mode === 'height') { 172 | this.scale = N.Clamp(scaleY, d.remScale.min, d.remScale.max) 173 | } 174 | } 175 | } 176 | 177 | export class ResizeFactory { 178 | private resizeManager: ResizeManager 179 | constructor(resizeManager: ResizeManager) { 180 | this.resizeManager = resizeManager 181 | } 182 | 183 | Resize(cb: (e: { 184 | vh: number, 185 | vw: number, 186 | scale: number, 187 | breakpoint: Breakpoints 188 | }) => void) { 189 | return new Resize(cb, this.resizeManager) 190 | } 191 | } 192 | 193 | export class Resize { 194 | cb: (e: ResizeEvent) => void 195 | id: number 196 | private resizeManager: ResizeManager 197 | constructor(cb: (e: { 198 | vh: number, 199 | vw: number, 200 | scale: number, 201 | breakpoint: Breakpoints 202 | }) => void, RM: ResizeManager) { 203 | this.resizeManager = RM 204 | this.cb = cb 205 | this.id = this.resizeManager.resizeId 206 | this.resizeManager.resizeId++ 207 | } 208 | on() { 209 | this.resizeManager.add({ 210 | id: this.id, 211 | cb: this.cb 212 | }) 213 | } 214 | off() { 215 | this.resizeManager.remove(this.id) 216 | } 217 | 218 | trigger() { 219 | this.cb(this.resizeManager.callbackArg) 220 | } 221 | } 222 | 223 | export { Resize as ROR } 224 | export type { ResizeEvent } 225 | -------------------------------------------------------------------------------- /website/plugins/core/frame.ts: -------------------------------------------------------------------------------- 1 | import { N } from "~/utils/namhai" 2 | 3 | enum FramePriority { 4 | FIRST = 0, 5 | 6 | DELAY = 50, 7 | MOTION = 100, 8 | MAIN = 500, 9 | 10 | LAST = 10000 11 | } 12 | 13 | type FrameEvent = { 14 | elapsed: number, 15 | delta: number 16 | } 17 | 18 | type FrameItem = { 19 | id: number, 20 | cb: (e: FrameEvent) => void, 21 | startTime?: number 22 | } 23 | 24 | class TabManager { 25 | array: { 26 | stop: () => void, 27 | resume: (delta: number) => void 28 | }[] 29 | pause: number 30 | constructor() { 31 | this.array = [] 32 | this.pause = 0 33 | 34 | N.Bind(this, ["update"]) 35 | document.addEventListener("visibilitychange", this.update) 36 | } 37 | add(arg: { stop: () => void, resume: (delta: number) => void }) { 38 | this.array.push(arg) 39 | } 40 | 41 | // calcule le temps entre le moment ou pas visible a visible, puis actionne, tOff() ou tOn(r) 42 | update(e: Event) { 43 | const t = e.timeStamp; 44 | let dT = 0 45 | 46 | if (document.hidden) { 47 | this.pause = t 48 | } else { 49 | dT = t - this.pause 50 | } 51 | for (let index = this.array.length - 1; 0 <= index; --index) { 52 | if (document.hidden) { 53 | this.array[index].stop() 54 | } else { 55 | this.array[index].resume(dT) 56 | } 57 | } 58 | } 59 | } 60 | 61 | class OrderedArray extends Array<{ id: number, value: T }> { 62 | constructor() { 63 | super() 64 | } 65 | 66 | indexOfId(id: number) { 67 | return N.binarySearch(this, id) 68 | } 69 | 70 | override push(el: { id: number, value: T }) { 71 | const { index } = N.binarySearch(this, el.id) 72 | 73 | this.splice(index, 0, el) 74 | return index 75 | } 76 | } 77 | 78 | class FrameManager { 79 | now: number = 0; 80 | on: boolean; 81 | frameId = 0 82 | 83 | private stacks: OrderedArray> 84 | 85 | constructor(tab: TabManager) { 86 | N.Bind(this, ['update', 'stop', 'resume']) 87 | 88 | this.stacks = new OrderedArray() 89 | this.stacks.push({ id: FramePriority.FIRST, value: [] }) 90 | this.stacks.push({ id: FramePriority.MAIN, value: [] }) 91 | this.stacks.push({ id: FramePriority.LAST, value: [] }) 92 | 93 | this.on = true 94 | tab.add({ stop: this.stop, resume: this.resume }) 95 | this.raf() 96 | } 97 | 98 | resume(delta = 0) { 99 | this.on = true 100 | for (const stack of this.stacks) { 101 | for (const frameItem of stack.value) { 102 | frameItem.startTime = (frameItem.startTime || 0) + delta 103 | } 104 | } 105 | this.now += delta 106 | } 107 | stop() { 108 | this.on = false 109 | } 110 | 111 | add(frameItem: FrameItem, priority: number) { 112 | const { index, miss } = this.stacks.indexOfId(priority) 113 | let stack: FrameItem[] 114 | 115 | if (miss) { 116 | stack = [] 117 | this.stacks.splice(index, 0, { id: priority, value: stack }) 118 | } else { 119 | stack = this.stacks[index].value 120 | } 121 | 122 | stack.push(frameItem) 123 | 124 | if (priority === FramePriority.MAIN && stack.length > 10000) console.warn("Main raf stack congested", stack.length) 125 | } 126 | 127 | remove(id: number, priority: number) { 128 | const { index: priorityIndex, miss: priorityMiss } = this.stacks.indexOfId(priority) 129 | if (priorityMiss) { 130 | console.error("Raf remove jammed : priority stack doesn't exist") 131 | return 132 | } 133 | const stack = this.stacks[priorityIndex].value 134 | 135 | const { index, miss } = N.binarySearch(stack, id) 136 | 137 | if (miss) { 138 | console.warn("Raf remove jammed : id not in stack") 139 | return 140 | } 141 | stack.splice(index, 1) 142 | } 143 | 144 | update(t: number) { 145 | const delta = t - (this.now || t - 16) 146 | this.now = t 147 | 148 | if (Math.floor(1 / delta * 1000) < 20) { 149 | console.warn("Huge frame drop") 150 | } 151 | 152 | if (this.on) { 153 | for (const stack of this.stacks) { 154 | for (const frameItem of stack.value) { 155 | if (!frameItem.startTime) { 156 | frameItem.startTime = t 157 | } 158 | 159 | const elapsed = t - frameItem.startTime 160 | frameItem.cb({ elapsed, delta }) 161 | } 162 | } 163 | } 164 | 165 | this.raf() 166 | } 167 | 168 | private raf() { 169 | requestAnimationFrame(this.update) 170 | } 171 | 172 | 173 | } 174 | 175 | class FrameFactory { 176 | private FrameManager: FrameManager 177 | constructor(FrameManager: FrameManager) { 178 | this.FrameManager = FrameManager 179 | N.Bind(this, ["Frame", "Delay", "Timer"]) 180 | } 181 | Frame(options: Omit[0], "FrameManager">) { 182 | return new Frame({ ...options, FrameManager: this.FrameManager }) 183 | } 184 | Delay(options: Omit[0], "FrameManager">) { 185 | return new Delay({ ...options, FrameManager: this.FrameManager }) 186 | } 187 | Timer(options: Omit[0], "FrameManager">) { 188 | return new Timer({ ...options, FrameManager: this.FrameManager }) 189 | } 190 | } 191 | 192 | class Frame { 193 | readonly callback: (e: FrameEvent) => void; 194 | readonly priority: number; 195 | private killed: boolean; 196 | on: boolean 197 | id?: number 198 | private FrameManager!: FrameManager 199 | 200 | constructor(options: { callback: (e: FrameEvent) => void, priority?: number, FrameManager: FrameManager }) { 201 | N.Bind(this, ["stop", "run", "kill"]) 202 | this.FrameManager = options.FrameManager 203 | this.callback = options.callback 204 | this.priority = options.priority ?? FramePriority.MAIN 205 | 206 | this.on = false 207 | this.killed = false 208 | } 209 | 210 | run(startTime?: number) { 211 | if (this.on || this.killed) return this 212 | this.on = true 213 | this.FrameManager.frameId++ 214 | this.id = this.FrameManager.frameId 215 | const frameItem: FrameItem = { 216 | id: this.id, 217 | cb: this.callback, 218 | startTime: startTime ? this.FrameManager.now - startTime : undefined 219 | } 220 | 221 | this.FrameManager.add(frameItem, this.priority) 222 | return this 223 | } 224 | 225 | stop() { 226 | if (!this.on) return this 227 | this.on = false 228 | 229 | this.id && this.FrameManager.remove(this.id, this.priority) 230 | return this 231 | } 232 | 233 | kill() { 234 | this.stop() 235 | this.killed = true 236 | return this 237 | } 238 | } 239 | 240 | class Delay { 241 | readonly callback: (lateStart?: number) => void; 242 | readonly delay: number; 243 | private frame: Frame; 244 | constructor(options: { delay: number, callback: (lateStart?: number) => void, FrameManager: FrameManager }) { 245 | const { callback, delay, FrameManager } = options 246 | 247 | N.Bind(this, ["update", "stop", "run"]) 248 | this.callback = options.callback 249 | this.delay = options.delay 250 | 251 | this.frame = new Frame({ callback: this.update, priority: FramePriority.DELAY, FrameManager }) 252 | 253 | } 254 | 255 | run() { 256 | if (this.delay === 0) this.callback() 257 | else this.frame.run() 258 | 259 | return this 260 | } 261 | 262 | stop() { 263 | this.frame.stop() 264 | return this 265 | } 266 | 267 | update(e: FrameEvent) { 268 | const t = N.Clamp(e.elapsed, 0, this.delay) 269 | 270 | if (t >= this.delay) { 271 | this.stop() 272 | const lateStart = e.elapsed - this.delay 273 | this.callback(lateStart) 274 | } 275 | 276 | return this 277 | } 278 | } 279 | 280 | class Timer { 281 | private ticker: Delay 282 | constructor(options: { callback: () => void, throttle: number, FrameManager: FrameManager }) { 283 | const { callback, throttle: delay = 200, FrameManager } = options 284 | this.ticker = new Delay({ delay, callback, FrameManager }) 285 | } 286 | 287 | tick() { 288 | this.ticker.stop() 289 | this.ticker.run() 290 | return this 291 | } 292 | 293 | stop() { 294 | this.ticker.stop() 295 | return this 296 | } 297 | } 298 | 299 | export { Frame, Delay, Timer, FramePriority, TabManager, FrameManager, FrameFactory } 300 | export type { FrameItem, FrameEvent } -------------------------------------------------------------------------------- /website/plugins/core/stopMotion.ts: -------------------------------------------------------------------------------- 1 | 2 | import { EaseEnum, type Ease4Arg, type EaseFunctionName, Ease, Ease4 } from "~/plugins/core/eases"; 3 | import { Frame, FramePriority, type FrameEvent, Delay, FrameFactory } from "./frame"; 4 | 5 | const STYLE_MAP = [ 6 | "o", 7 | "x", 8 | "y", 9 | "s", 10 | "scaleX", 11 | "scaleY", 12 | "r" 13 | ] as const; 14 | type StyleMapKey = typeof STYLE_MAP[number]; 15 | 16 | const styleMapObject: Record = STYLE_MAP.reduce((acc, key, index) => { 17 | acc[key] = index; 18 | return acc; 19 | }, {} as Record); 20 | 21 | type MotionEvent = { progress: number, easeProgress: number } 22 | interface StopMotionOptionPrimitive { 23 | e?: EaseFunctionName | Ease4Arg 24 | d?: number, 25 | delay?: number, 26 | cb?: () => void, 27 | reverse?: boolean 28 | update?: (e: MotionEvent) => void, 29 | } 30 | interface StopMotionOptionPrimitiveI extends StopMotionOptionPrimitive { 31 | svg?: never, 32 | el?: never, 33 | p?: never 34 | } 35 | 36 | type FromTo = [number, number] 37 | type DOMPropName = StyleMapKey 38 | interface StopMotionOptionBasicDOMAnimation extends StopMotionOptionPrimitive { 39 | el: HTMLElement | HTMLElement[], 40 | p: { 41 | x?: [number, number, string] | [number, number] 42 | y?: [number, number, string] | [number, number] 43 | o?: FromTo 44 | s?: FromTo 45 | scaleX?: FromTo 46 | scaleY?: FromTo 47 | r?: FromTo | [number, number, string] 48 | }, 49 | override?: boolean, 50 | svg?: never, 51 | } 52 | interface StopMotionUpdatePropsOption extends StopMotionOptionPrimitive { 53 | p?: { 54 | [K in DOMPropName]?: { 55 | start?: number, 56 | end?: number 57 | } 58 | }, 59 | override?: boolean 60 | } 61 | 62 | interface StopMotionOptionSvg extends StopMotionOptionPrimitive { 63 | el: NodeList | HTMLElement, 64 | svg: { 65 | end: string, 66 | start?: string, 67 | type?: string 68 | }, 69 | p?: never, 70 | } 71 | 72 | export type StopMotionOption = StopMotionOptionSvg | StopMotionOptionBasicDOMAnimation | StopMotionOptionPrimitiveI 73 | 74 | let MotionId = 0 75 | type MotionItem = { 76 | ticker: Ticker 77 | startTime?: number 78 | } 79 | export class MotionManager { 80 | 81 | motions: MotionItem[] 82 | frame: Frame; 83 | frameFactory: FrameFactory; 84 | motionWeakMapAnimation: WeakMap 85 | constructor(frameFactory: FrameFactory) { 86 | 87 | this.frameFactory = frameFactory; 88 | 89 | N.Bind(this, ["raf"]) 90 | this.motions = [] 91 | 92 | this.frame = this.frameFactory.Frame({ callback: this.raf, priority: FramePriority.MOTION }) 93 | this.frame.run() 94 | 95 | this.motionWeakMapAnimation = new WeakMap() 96 | } 97 | 98 | add(ticker: Ticker) { 99 | this.motions.push({ ticker }) 100 | } 101 | 102 | remove(id: number, canMiss: boolean = false) { 103 | const { index, miss } = N.binarySearch(this.motions.map(el => { return { id: el.ticker.id } }), id) 104 | 105 | if (!canMiss && miss) { 106 | console.warn("Motion remove jammed : id not in stack") 107 | return 108 | } 109 | 110 | this.motions.splice(index, 1) 111 | } 112 | 113 | raf(e: FrameEvent) { 114 | for (let i = this.motions.length - 1; i >= 0; i--) { 115 | const item = this.motions[i] 116 | const ticker = item.ticker 117 | 118 | if (!item.startTime) item.startTime = item.startTime || e.elapsed 119 | const t = N.Clamp(e.elapsed - item.startTime, 0, ticker.d) 120 | if (ticker.d == 0) { 121 | ticker.prog = 1 122 | ticker.progE = 1 123 | } else { 124 | ticker.prog = N.Clamp(t / ticker.d, 0, 1) 125 | } 126 | ticker.progE = ticker.calc!(ticker.prog) 127 | ticker.update({ progress: ticker.prog, easeProgress: ticker.progE }) 128 | 129 | if (ticker.prog === 1) { 130 | ticker.stop() 131 | ticker.cb && ticker.cb() 132 | } 133 | } 134 | } 135 | } 136 | 137 | export class MotionFactory { 138 | motionManager: MotionManager; 139 | constructor(motionManager: MotionManager) { 140 | this.motionManager = motionManager 141 | N.Bind(this, ["Motion", "Film"]) 142 | } 143 | 144 | Motion(options: StopMotionOption) { 145 | return new Motion(options, this.motionManager) 146 | } 147 | Film() { 148 | return new Film(this.motionManager) 149 | } 150 | } 151 | 152 | export class Motion { 153 | ticker: Ticker; 154 | delay!: Delay; 155 | promise: Promise; 156 | promiseRelease!: (value: void | PromiseLike) => void; 157 | id?: number; 158 | motionManager: MotionManager; 159 | 160 | constructor(option: StopMotionOption, motionManager: MotionManager) { 161 | this.promise = new Promise(res => { 162 | this.promiseRelease = res 163 | }) 164 | const callback = option.cb 165 | const cb = () => { 166 | callback?.() 167 | this.promiseRelease() 168 | } 169 | Object.assign(option, { cb }) 170 | this.motionManager = motionManager 171 | this.ticker = TickerFactory.createTicker(option, this.motionManager) 172 | 173 | } 174 | 175 | pause() { 176 | this.ticker.stop() 177 | this.delay && this.delay.stop() 178 | 179 | return this 180 | } 181 | play(prop?: StopMotionUpdatePropsOption) { 182 | this.pause() 183 | this.ticker.wait() 184 | this.ticker.updateProps(prop) 185 | 186 | const delay = this.ticker.delay || 0 187 | 188 | this.delay = this.motionManager.frameFactory.Delay({ 189 | delay, 190 | callback: () => { 191 | this.ticker.run() 192 | } 193 | }) 194 | 195 | this.delay.run() 196 | 197 | this.promise = new Promise(res => { 198 | this.promiseRelease = res 199 | }) 200 | 201 | return this.promise 202 | } 203 | } 204 | 205 | class TickerFactory { 206 | static createTicker(props: StopMotionOption, motionManager: MotionManager) { 207 | let ticker: Ticker 208 | if (props.svg && false) { 209 | props.svg 210 | // TODO 211 | } else if (props.p) { 212 | const elements = (props.el instanceof window.NodeList || Array.isArray(props.el)) ? props.el : [props.el]; 213 | ticker = new TickerDOMAnimation({ ...props, el: elements }, motionManager) 214 | } else { 215 | ticker = new Ticker(props, motionManager) 216 | } 217 | 218 | return ticker 219 | } 220 | } 221 | 222 | interface TickerI { 223 | update: (e: MotionEvent) => void 224 | updateProps: (props?: StopMotionUpdatePropsOption) => void 225 | } 226 | 227 | enum TickerOn { 228 | stop = 0, 229 | waiting = 1, 230 | play = 2 231 | } 232 | class Ticker implements TickerI { 233 | reverse: boolean; 234 | cb: (() => void) | undefined; 235 | ease: EaseFunctionName | Ease4Arg; 236 | delay: number; 237 | d: number; 238 | calc: (t: number) => number; 239 | prog: number = 0 240 | progE: number = 0 241 | updateFunc?: (e: MotionEvent) => void; 242 | id: number 243 | motionManager: MotionManager; 244 | on: TickerOn; 245 | constructor(props: StopMotionOptionPrimitive, motionManager: MotionManager) { 246 | this.motionManager = motionManager 247 | this.d = props.d || 0 248 | this.delay = props.delay || 0 249 | this.cb = props.cb 250 | this.reverse = props.reverse || false 251 | this.ease = props.e || EaseEnum.linear 252 | this.calc = typeof this.ease === "string" ? Ease[this.ease] : Ease4(this.ease) 253 | this.updateFunc = props.update 254 | this.on = TickerOn.stop 255 | 256 | MotionId++ 257 | this.id = MotionId 258 | 259 | if (this.reverse === true) { 260 | const ease = this.calc 261 | this.calc = (t: number) => ease(1 - t) 262 | } 263 | } 264 | run() { 265 | if (this.on !== TickerOn.play) this.motionManager.add(this) 266 | this.on = TickerOn.play 267 | } 268 | stop() { 269 | if (this.on === TickerOn.play) { 270 | this.motionManager.remove(this.id) 271 | } 272 | this.on = TickerOn.stop 273 | } 274 | wait() { 275 | this.on = TickerOn.waiting 276 | } 277 | 278 | update(e: MotionEvent) { 279 | if (this.updateFunc !== undefined) { 280 | this.updateFunc(e) 281 | } 282 | } 283 | 284 | updateProps(props?: StopMotionUpdatePropsOption) { 285 | if (!props) return 286 | MotionId++ 287 | this.id = MotionId 288 | this.d = props.d || this.d 289 | this.delay = props.delay || this.delay 290 | this.ease = props.e || this.ease 291 | this.cb = props.cb || this.cb 292 | this.reverse = props.reverse || this.reverse 293 | this.updateFunc = props.update 294 | 295 | this.calc = typeof this.ease === "string" ? Ease[this.ease] : Ease4(this.ease) 296 | 297 | if (this.reverse === true) { 298 | const ease = this.calc 299 | this.calc = (t: number) => ease(1 - t) 300 | } 301 | } 302 | } 303 | 304 | type DOMProp = { 305 | curr: number, 306 | start: number, 307 | end: number, 308 | unit: string 309 | } 310 | 311 | class TickerDOMAnimation extends Ticker implements TickerI { 312 | override: boolean | undefined; 313 | el: [HTMLElement, DOMProp[]][]; 314 | propToIndex: Record 315 | constructor(props: StopMotionOptionBasicDOMAnimation, motionManager: MotionManager) { 316 | super(props, motionManager) 317 | 318 | const el = ((props.el instanceof window.NodeList || Array.isArray(props.el)) ? props.el : [props.el]) as HTMLElement[]; 319 | 320 | this.override = props.override || false 321 | 322 | 323 | this.propToIndex = Object.entries(props.p).reduce((acc, [key, prop], index) => { 324 | acc[key as keyof typeof props.p] = index; 325 | return acc; 326 | }, {} as Record); 327 | 328 | this.el = el.map((el) => { 329 | const p = Object.entries(props.p).map(([key, prop]) => { 330 | const [from, to, unit = "%"] = prop || [] 331 | 332 | return { 333 | curr: this.reverse ? to : from, 334 | start: from, 335 | end: to, 336 | unit 337 | } as DOMProp 338 | }) 339 | 340 | return [el, p] 341 | }) 342 | } 343 | 344 | override update(e: MotionEvent) { 345 | super.update(e) 346 | for (const [key, [el, props]] of this.el.entries()) { 347 | for (const prop of props) { 348 | prop.curr = N.Lerp(prop.start, prop.end, e.easeProgress) 349 | } 350 | } 351 | 352 | 353 | const has = (...args: Parameters) => Object.hasOwn(args[0], args[1]) 354 | // styleMapObject["x"] 355 | const x = has(this.propToIndex, "x"), y = has(this.propToIndex, "y") 356 | const translate = x || y 357 | const opacity = has(this.propToIndex, "o") 358 | const scale = has(this.propToIndex, "s") 359 | const scaleX = has(this.propToIndex, "scaleX") 360 | const scaleY = has(this.propToIndex, "scaleY") 361 | const rotate = has(this.propToIndex, "r") 362 | 363 | if (!this.el) return 364 | const elements = this.el 365 | for (const [element, prop] of elements) { 366 | if (translate || scale || scaleX || scaleY || rotate) { 367 | let transformString = "" 368 | if (translate) { 369 | const valueX = x ? prop[this.propToIndex["x"]] : undefined 370 | const valueY = y ? prop[this.propToIndex["y"]] : undefined 371 | transformString += `translate3d(${valueX?.curr ?? 0}${valueX?.unit || "%"}, ${valueY?.curr ?? 0}${valueY?.unit || "%"}, 0) ` 372 | } 373 | 374 | if (scale) { 375 | const value = prop[this.propToIndex["s"]] 376 | transformString += `scale(${value.curr})` 377 | } else if (scaleX || scaleY) { 378 | const valueX = scaleX ? prop[this.propToIndex["scaleX"]] : undefined 379 | const valueY = scaleY ? prop[this.propToIndex["scaleY"]] : undefined 380 | transformString += `scale(${valueX?.curr ?? 1}, ${valueY?.curr ?? 1}) ` 381 | } 382 | if (rotate) { 383 | const value = prop[this.propToIndex["r"]] 384 | transformString += `rotate(${value.curr}${value.unit})` 385 | } 386 | element.style.transform = transformString 387 | } 388 | 389 | if (opacity) { 390 | const value = prop[this.propToIndex["o"]] 391 | element.style.opacity = `${value.curr}` 392 | } 393 | } 394 | } 395 | 396 | 397 | override updateProps(arg?: StopMotionUpdatePropsOption) { 398 | for (const [el, props] of this.el) { 399 | const id = this.motionManager.motionWeakMapAnimation.get(el) 400 | !!id && this.motionManager.remove(+id) 401 | } 402 | 403 | super.updateProps(arg) 404 | 405 | if (!arg) return 406 | this.override = arg.override || this.override 407 | const argProp = arg.p || {} 408 | 409 | for (const [key, p] of Object.entries(argProp)) { 410 | const k = key as unknown as StyleMapKey 411 | const index = this.propToIndex[k] 412 | 413 | for (const [el, props] of this.el) { 414 | 415 | const prop = props[index] 416 | prop.end = this.reverse ? props[index].start : prop.end 417 | 418 | // // if (!this.override) { 419 | // // const value = currProps[key] 420 | // // if (value) prop.curr = value 421 | // // } 422 | prop.start = prop.curr 423 | 424 | if (!!p.end) { 425 | const end = p.end! 426 | prop.end = end 427 | } 428 | if (p.start) { 429 | const start = p.start! 430 | prop.start = start 431 | } 432 | } 433 | } 434 | } 435 | 436 | } 437 | const Svg = { 438 | getLength: (e: Element) => { 439 | if ("circle" === e.tagName) return 2 * Math.PI * (+N.DOM.ga(e, "r")!) 440 | if ('line' === e.tagName) { 441 | let a, b, c, d; 442 | a = +N.DOM.ga(e, 'x1')! 443 | b = +N.DOM.ga(e, "x2")! 444 | c = +N.DOM.ga(e, 'y1')! 445 | d = +N.DOM.ga(e, 'y2')! 446 | 447 | return Math.sqrt((a -= b) * a + (c -= d) * c) 448 | } 449 | if ("polyline" !== e.tagName) return (e as SVGGeometryElement).getTotalLength() 450 | let poly = e as SVGPolylineElement, n = poly.points.numberOfItems 451 | let length = 0, previousItem = poly.points.getItem(0); 452 | for (let index = 1; index < n; index++) { 453 | let item = poly.points.getItem(index) 454 | length += Math.sqrt((item.x - previousItem.x) ** 2 + (item.y - previousItem.y) ** 2) 455 | previousItem = item 456 | } 457 | return length 458 | }, 459 | split: (pointsString: string) => { 460 | const s: Array = [], 461 | r = pointsString.split(" "); 462 | for (const partial of r) { 463 | let part = partial.split(',') 464 | for (const p of part) { 465 | if (isNaN(+p)) { 466 | s.push(p) 467 | } else { 468 | s.push(+p) 469 | } 470 | } 471 | } 472 | return s 473 | } 474 | } 475 | 476 | export class Film { 477 | 478 | stopMotions: Motion[] 479 | on: boolean 480 | motionManager: MotionManager; 481 | 482 | constructor(motionManager: MotionManager) { 483 | this.motionManager = motionManager 484 | this.stopMotions = [] 485 | this.on = false 486 | } 487 | 488 | from(props: StopMotionOption) { 489 | const stopMotion = new Motion(props, this.motionManager) 490 | this.stopMotions.push(stopMotion) 491 | return this 492 | } 493 | 494 | getPromise() { 495 | return Promise.all(this.stopMotions.map(motion => motion.promise)) 496 | } 497 | 498 | play(props?: StopMotionUpdatePropsOption) { 499 | const promises = this.stopMotions.map(motion => motion.play(props)) 500 | 501 | return Promise.all(promises) 502 | } 503 | 504 | pause() { 505 | for (let i = this.stopMotions.length - 1; i >= 0; i--) { 506 | const stopMotion = this.stopMotions[i] 507 | stopMotion.pause() 508 | } 509 | } 510 | 511 | reset() { 512 | for (let i = this.stopMotions.length - 1; i >= 0; i--) { 513 | const motion = this.stopMotions[i] 514 | motion.pause() 515 | } 516 | 517 | this.stopMotions = [] 518 | } 519 | } -------------------------------------------------------------------------------- /website/assets/lorem.ts: -------------------------------------------------------------------------------- 1 | export const lorem = ` 2 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus iaculis erat quis tellus aliquet, nec 3 | tempor orci fringilla. Duis nec semper augue. Sed iaculis laoreet nunc et fringilla. Sed commodo nisi 4 | sapien, non luctus lacus ornare rhoncus. Suspendisse potenti. Donec aliquet dapibus metus. Vestibulum et 5 | lacinia mi, ac egestas diam. Vivamus viverra viverra ultricies. In vel arcu quis eros facilisis consectetur 6 | non id erat. Phasellus dignissim lobortis tellus sed tincidunt. Phasellus mollis enim ac nulla tristique 7 | fringilla. Aliquam egestas turpis quis ex consequat, sed posuere odio volutpat. Nullam id mi lectus. Quisque 8 | interdum ipsum ac urna condimentum, eget sagittis justo maximus. Fusce a neque non quam elementum volutpat 9 | sed quis turpis. 10 | 11 | Sed eu rutrum nibh. Etiam finibus aliquam diam non mattis. Morbi ut dignissim orci. Nam sed suscipit nibh. 12 | Suspendisse at nunc sit amet urna scelerisque tincidunt a et ex. Aliquam consectetur, felis et iaculis 13 | luctus, nunc massa facilisis dolor, ac sollicitudin erat nisi a nunc. Donec ut tellus in enim molestie 14 | vehicula eget sed justo. Mauris lacinia risus ac odio facilisis, eget tempus dui commodo. Vivamus pharetra 15 | mauris metus, id vestibulum lorem vulputate sit amet. 16 | 17 | Morbi et felis a enim dapibus tincidunt non a elit. Proin tristique augue ac sapien maximus, quis sagittis 18 | augue auctor. Donec nec massa ut massa ullamcorper consectetur. Cras odio libero, aliquet ac augue et, 19 | efficitur vehicula turpis. Morbi pulvinar vel tortor lobortis mattis. Pellentesque ullamcorper enim quis 20 | elit tempus, at venenatis erat faucibus. Nulla vitae nisl semper, accumsan ligula pharetra, euismod metus. 21 | Vestibulum vel odio venenatis, molestie turpis ornare, malesuada sapien. Pellentesque rhoncus leo ut rutrum 22 | scelerisque. Aenean id consectetur turpis. Nunc cursus ex a rutrum consectetur. Lorem ipsum dolor sit amet, 23 | consectetur adipiscing elit. Ut orci nulla, posuere in metus at, dictum pharetra quam. Fusce ante justo, 24 | tristique sed urna at, bibendum cursus justo. 25 | 26 | Aenean a mauris posuere nulla vulputate tincidunt. Morbi sodales eu elit sit amet egestas. Mauris dictum 27 | erat sit amet mi tristique, at porta tortor mollis. Nam vel consectetur ex. Lorem ipsum dolor sit amet, 28 | consectetur adipiscing elit. Suspendisse ex nisl, ornare sed placerat euismod, posuere nec augue. Quisque 29 | volutpat nisl vel convallis volutpat. 30 | 31 | Maecenas quis rhoncus ipsum, eu tincidunt nunc. Class aptent taciti sociosqu ad litora torquent per conubia 32 | nostra, per inceptos himenaeos. Proin in sagittis purus. Maecenas et nunc urna. Integer vitae ante turpis. 33 | Sed et nibh magna. Nam bibendum ex massa, non molestie metus mollis a. 34 | 35 | Vestibulum a ante ac massa vestibulum finibus. Duis pellentesque mi ut elit suscipit, ut dignissim metus 36 | varius. Aenean ac mollis ante, in fermentum magna. Integer dapibus orci mi, sed tristique sem consectetur a. 37 | Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vestibulum magna 38 | mauris, sollicitudin et pharetra et, feugiat sit amet diam. Duis eu blandit massa. Fusce luctus blandit mi, 39 | sed scelerisque dolor luctus vel. Nullam a dapibus augue. Nam accumsan congue scelerisque. Pellentesque in 40 | hendrerit lorem, sed interdum nisi. Suspendisse et ipsum tempor, placerat diam ac, tincidunt urna. Aenean 41 | vitae condimentum eros, sed finibus eros. Proin mollis elementum tincidunt. 42 | 43 | Nullam sit amet metus sit amet nibh porttitor iaculis. Suspendisse ornare libero non imperdiet suscipit. 44 | Donec lectus diam, ultricies at ultrices venenatis, lacinia a tellus. Nullam libero leo, molestie quis metus 45 | ac, egestas finibus lorem. Aliquam erat volutpat. Proin ante nibh, ultricies vitae condimentum eget, pretium 46 | et ex. Aliquam eget ante in lorem fringilla suscipit vitae eu ex. Pellentesque ultricies augue et tortor 47 | aliquet tristique. 48 | 49 | Phasellus sed lorem volutpat, ullamcorper dolor eu, iaculis quam. Duis ullamcorper tellus felis, sed 50 | pulvinar tellus sodales a. Sed augue nibh, vestibulum at euismod vel, aliquam nec libero. Cras sit amet 51 | sodales erat. Proin tristique ex ac facilisis efficitur. Maecenas eleifend quis purus facilisis viverra. 52 | Vivamus purus neque, tempus non mi sit amet, varius imperdiet elit. Nam ut tortor facilisis nisi egestas 53 | aliquet. Vivamus euismod ultricies mi, id scelerisque dui faucibus ornare. In vel lectus eget justo volutpat 54 | pulvinar non quis elit. Suspendisse potenti. Sed nunc justo, rutrum id ultrices sit amet, viverra ut mi. 55 | Fusce mattis elit mollis est consectetur, a dictum arcu dignissim. Proin quis eros accumsan, tempor tortor 56 | et, cursus arcu. 57 | 58 | Morbi vel imperdiet dolor. Morbi in dui eget odio ullamcorper tristique. Vivamus eget libero id enim 59 | facilisis venenatis non sed ante. Duis ac commodo enim, at tempus leo. Cras sagittis rutrum eros ut 60 | consequat. Fusce at elit est. Aenean a dui a odio auctor porta. Pellentesque habitant morbi tristique 61 | senectus et netus et malesuada fames ac turpis egestas. Integer rhoncus quis sem at ullamcorper. Sed 62 | lobortis, elit ac condimentum feugiat, velit orci condimentum turpis, in maximus arcu orci eget nulla. 63 | Mauris nisi nisl, fringilla vel lacinia id, vulputate non justo. In at velit ex. Sed ut orci malesuada, 64 | accumsan mauris eget, cursus purus. Cras sollicitudin consectetur neque, dapibus aliquam elit faucibus quis. 65 | In convallis vel felis ac tempus. Proin faucibus est lectus, non imperdiet justo porttitor in. 66 | 67 | Vivamus sapien ipsum, porttitor ut mauris ut, posuere rhoncus ligula. Proin eu purus ut leo egestas congue. 68 | Morbi lobortis, ante non aliquam feugiat, leo ex maximus libero, ac scelerisque libero neque eu lorem. 69 | Praesent aliquam metus in suscipit consequat. Phasellus auctor turpis sit amet felis venenatis, vitae 70 | pellentesque urna ultricies. Proin quis tempor purus, congue mattis sapien. Nulla a ligula vitae risus 71 | facilisis faucibus. Vestibulum ligula libero, sagittis vitae feugiat a, fermentum eu neque. Sed at sapien 72 | quis libero mollis viverra a suscipit tellus. Nullam tellus eros, malesuada non leo eu, pharetra blandit 73 | elit. Pellentesque vel lectus sed magna accumsan porta ut ac sapien. Nunc dignissim massa vel iaculis 74 | efficitur. Phasellus aliquam vulputate quam sit amet dignissim. 75 | 76 | Nullam suscipit mauris elit, at auctor ligula accumsan in. Nunc vitae augue mauris. Proin et maximus mauris. 77 | Morbi nec viverra dolor. Curabitur congue molestie dolor. Praesent iaculis, turpis at rutrum tristique, nunc 78 | mauris accumsan urna, in tristique mauris ante at enim. Aenean sed massa non sapien tincidunt porttitor. Sed 79 | malesuada tortor diam, a molestie erat viverra a. Ut tempor fermentum tortor, volutpat varius odio rutrum 80 | eu. Vivamus non tellus id turpis blandit tempor in sit amet nisl. Donec porta semper arcu. Nunc convallis a 81 | odio eget vulputate. Aliquam mauris felis, pretium volutpat tincidunt eu, tristique eu diam. Vivamus 82 | imperdiet pellentesque varius. Nunc imperdiet, tellus a porttitor pretium, massa velit dictum diam, in 83 | convallis dui purus sit amet lectus. Nunc rutrum nunc at dapibus venenatis. 84 | 85 | Vivamus vitae ultrices nibh, sed semper felis. Cras quis commodo nibh, et efficitur libero. Donec in odio 86 | vitae dolor lacinia pulvinar quis nec massa. In vel turpis varius, suscipit nibh et, laoreet quam. Etiam a 87 | enim non augue condimentum vulputate. Cras venenatis augue nec facilisis dictum. Ut a libero nibh. 88 | Vestibulum nec nibh eros. Etiam ut tortor in nunc feugiat porttitor. Cras blandit rhoncus dapibus. Fusce vel 89 | cursus massa. Ut congue eget libero in auctor. 90 | 91 | Etiam nec efficitur odio, vitae porttitor nibh. Nam in nibh mauris. Proin nunc ipsum, blandit id imperdiet 92 | quis, convallis vitae odio. Suspendisse tortor eros, consequat id rutrum auctor, facilisis sit amet nulla. 93 | Maecenas lorem orci, bibendum vitae nulla nec, scelerisque congue mi. Ut feugiat sem ex, vitae pretium neque 94 | faucibus ac. Vivamus laoreet porttitor felis, at ultrices velit mollis semper. Pellentesque euismod 95 | condimentum ligula, eu blandit nunc volutpat id. Curabitur sodales neque semper, rhoncus mi nec, suscipit 96 | dolor. Aliquam tortor nisi, aliquam ac odio ut, pulvinar ornare sem. Nulla convallis est arcu, nec cursus 97 | mauris venenatis at. 98 | 99 | Nulla varius condimentum dapibus. Vestibulum lobortis turpis turpis, non mollis ante tincidunt eget. Class 100 | aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Etiam ac ipsum semper, 101 | gravida magna vitae, pulvinar felis. Vestibulum ullamcorper dapibus rhoncus. Vestibulum consequat velit eget 102 | feugiat mollis. In viverra dui consectetur dui aliquet ullamcorper et ut nisi. Curabitur dapibus rutrum 103 | mollis. Maecenas ultricies dictum tempus. Donec quis mi dignissim, finibus risus sed, finibus nisl. Duis et 104 | erat sollicitudin, hendrerit dui in, congue orci. Donec eu nibh a diam mollis finibus. Donec tincidunt, eros 105 | quis egestas dictum, ex metus cursus dui, at hendrerit ante erat non elit. 106 | 107 | Praesent elementum placerat porta. Mauris vitae erat eget arcu ornare porttitor. Quisque erat nisi, commodo 108 | id consequat gravida, ullamcorper id urna. Vestibulum placerat nisl vel felis commodo mattis. Fusce id leo 109 | congue, porttitor mi nec, pulvinar turpis. Nam semper metus non purus laoreet, ut ullamcorper mi tincidunt. 110 | Vestibulum ac augue tincidunt lorem feugiat scelerisque. Aliquam dui risus, venenatis vitae dictum nec, 111 | venenatis eget arcu. Cras molestie dui vitae turpis semper, auctor semper odio viverra. Nam nunc enim, 112 | semper eget sem sed, euismod dictum nunc. 113 | 114 | Suspendisse eu auctor est. Vestibulum eget molestie orci. Sed in rutrum eros. Aenean gravida tellus eu quam 115 | blandit vehicula. Integer tincidunt imperdiet tellus, at sollicitudin magna fringilla eu. Nam lobortis justo 116 | velit, sit amet consequat turpis condimentum at. In in sapien blandit, fringilla tellus eu, tincidunt 117 | ligula. Nunc volutpat condimentum enim sed imperdiet. Suspendisse bibendum mi mauris, in placerat magna 118 | venenatis at. 119 | 120 | Cras quis arcu id quam mattis consequat in at massa. In purus eros, viverra sit amet sem nec, aliquam 121 | fermentum ipsum. Proin mollis a ligula at efficitur. Maecenas rhoncus, ipsum vitae maximus consequat, quam 122 | lectus bibendum felis, sed aliquet ante nisl vitae sem. Nulla sit amet lacinia turpis. Sed id sodales augue, 123 | quis scelerisque dolor. Aenean diam turpis, ullamcorper dapibus ligula a, efficitur scelerisque mauris. 124 | Fusce sit amet tortor quis massa suscipit porta. Aliquam erat volutpat. Nunc iaculis lorem nec libero 125 | bibendum dapibus. Cras euismod, mi vel iaculis pharetra, eros quam facilisis lorem, sit amet porttitor mi 126 | tortor et velit. Phasellus ac condimentum odio, sed varius justo. Ut ullamcorper, lectus at posuere 127 | interdum, nibh libero aliquam urna, a ultrices quam est ac elit. Aliquam in elit diam. 128 | 129 | Donec egestas, est vel vehicula volutpat, augue neque maximus purus, ac auctor nulla dui non lectus. 130 | Pellentesque dignissim quam viverra pellentesque pharetra. Nullam vel ligula quis felis congue faucibus. 131 | Suspendisse scelerisque augue eu metus ornare vehicula. Nullam sit amet ex faucibus est efficitur convallis. 132 | Praesent eleifend, lacus quis mattis ullamcorper, lectus turpis laoreet turpis, sed posuere ligula leo quis 133 | quam. Nullam diam mauris, molestie eu sem in, rhoncus lacinia purus. Nunc elementum cursus bibendum. Duis 134 | cursus, mi sit amet gravida imperdiet, mauris purus imperdiet neque, suscipit lobortis risus dui vitae 135 | turpis. Cras lectus ligula, molestie eu eleifend vitae, luctus nec felis. 136 | 137 | Nulla sit amet justo lorem. Ut lobortis ac odio a eleifend. Nunc pretium, justo id pulvinar maximus, urna 138 | justo vehicula metus, ac ultricies purus arcu vel nibh. Quisque sagittis posuere ornare. Mauris laoreet quam 139 | odio, in placerat leo hendrerit sed. Cras nec quam ut sapien fermentum laoreet. Vestibulum vel odio augue. 140 | Aliquam vulputate massa lobortis rhoncus dapibus. Pellentesque luctus pellentesque sapien et bibendum. 141 | Suspendisse eu pulvinar mauris. 142 | 143 | Maecenas volutpat lorem nec tellus tempus dictum. Suspendisse vitae varius arcu, efficitur scelerisque 144 | libero. Suspendisse rutrum ultricies nulla, eu vulputate tellus finibus auctor. Etiam laoreet lectus sit 145 | amet libero finibus aliquet. Donec vel nibh facilisis, tincidunt enim in, imperdiet ex. Nullam laoreet 146 | maximus ante, sed viverra justo maximus at. Nulla fermentum tincidunt pulvinar. Vivamus ac nibh in felis 147 | commodo fringilla sit amet ut justo. Nunc elementum facilisis urna sit amet lacinia. Aliquam aliquam eros ut 148 | nunc congue, quis iaculis ipsum fermentum. Aenean sed rutrum mauris. Curabitur lacus justo, ullamcorper quis 149 | tellus vitae, lobortis euismod nisi. Donec sem quam, elementum nec scelerisque eget, dignissim sed lacus. 150 | Interdum et malesuada fames ac ante ipsum primis in faucibus. 151 | 152 | Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. In eu leo ac 153 | quam lacinia eleifend. Praesent aliquet odio sit amet ligula blandit venenatis in fermentum leo. 154 | Pellentesque ut tristique neque, egestas ullamcorper nulla. Praesent et maximus dui. Phasellus rhoncus 155 | tellus in mollis pharetra. Ut justo dui, dignissim eu commodo non, luctus nec dolor. Phasellus in magna 156 | quam. Curabitur efficitur tempus tortor. Aliquam vitae nisl sollicitudin, vehicula eros vel, fermentum 157 | metus. Nunc eu leo nec sem interdum vehicula faucibus quis nisl. Curabitur lacinia urna condimentum, 158 | interdum enim a, pellentesque risus. Aenean laoreet ut mauris sit amet viverra. Curabitur dictum leo quis 159 | neque congue, eu rhoncus libero molestie. Nam volutpat, neque sed ultricies pellentesque, ipsum turpis 160 | aliquam ipsum, et pellentesque purus ipsum in lacus. Aliquam rutrum posuere purus nec maximus. 161 | 162 | Maecenas eget viverra justo, sit amet elementum orci. Ut lacinia eget leo at consequat. Suspendisse 163 | scelerisque fermentum maximus. Nullam rhoncus posuere enim, eget ornare velit congue id. Morbi aliquet, 164 | ipsum in volutpat lacinia, sapien neque suscipit est, sit amet porttitor ipsum sem vitae lectus. Aliquam 165 | urna lorem, imperdiet at justo accumsan, sodales fringilla urna. Suspendisse consequat risus vitae felis 166 | tempus dictum. Sed sagittis tortor pretium ipsum ullamcorper, eget tincidunt felis maximus. Nullam viverra 167 | hendrerit ante fringilla venenatis. Donec mattis venenatis velit, vitae mollis dui varius vel. Nullam rutrum 168 | enim in dui facilisis dignissim. Quisque finibus lectus non libero sodales egestas. Nullam venenatis rhoncus 169 | dapibus. Aliquam nec laoreet lorem. Sed quis maximus felis. Nunc et nibh elit. 170 | 171 | Nullam porta auctor neque. Fusce laoreet urna quis lectus ultrices porta. Nunc ultrices dictum auctor. 172 | Nullam cursus ut tellus nec euismod. In sit amet urna congue elit ultrices interdum ac a ex. Nullam nec 173 | metus nec lorem malesuada fermentum sit amet tincidunt neque. Mauris vestibulum massa orci, a semper odio 174 | volutpat tincidunt. Vivamus sapien sapien, hendrerit eu magna a, finibus facilisis tortor. Nullam eu orci 175 | faucibus, egestas eros non, malesuada enim. Donec hendrerit a orci quis maximus. Suspendisse sagittis non 176 | orci vitae dictum. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. 177 | Suspendisse iaculis convallis felis id rutrum. 178 | 179 | Nunc sit amet tincidunt mauris. In ultrices quis nunc vel porta. Nulla bibendum dolor vitae ipsum consequat 180 | iaculis. Morbi porttitor, libero et maximus ornare, tortor nisi lobortis dolor, ac sagittis massa tellus at 181 | velit. Nulla odio tortor, rhoncus vitae rutrum et, vehicula non lorem. Ut eget ipsum hendrerit, semper enim 182 | at, tristique ex. Aenean nisl libero, faucibus in odio sed, aliquam pellentesque neque. Cras non risus urna. 183 | Cras sodales euismod vestibulum. Integer nec odio at ante consectetur aliquam. In scelerisque, tellus in 184 | tempus gravida, mi lorem pharetra mauris, at euismod ligula ligula sit amet lacus. Aenean molestie malesuada 185 | ex. 186 | 187 | Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Proin lacinia 188 | malesuada est. Cras vulputate elit a scelerisque facilisis. In eget posuere ante, non iaculis urna. 189 | Pellentesque enim justo, feugiat sed magna euismod, porta porta dui. Nam sit amet pretium magna. Maecenas 190 | efficitur ultricies cursus. Vivamus justo ipsum, mattis eget quam sed, accumsan hendrerit ipsum. Maecenas 191 | pretium non dolor non tristique. Vivamus quis tempor eros, sit amet blandit lorem. Proin leo risus, 192 | condimentum ac nisi quis, aliquet pretium dolor. Pellentesque id volutpat mauris. In hac habitasse platea 193 | dictumst. Proin eu diam mollis dui varius placerat.` --------------------------------------------------------------------------------