[READ-ONLY] Mirror of https://github.com/flo-bit/flo-bit.github.io. my personal website, w/ astro, svelte, tailwind, typescript, threlte flo-bit.dev/
portfolio portfolio-website svelte sveltekit tailwind threejs threlte typescript
0

Configure Feed

Select the types of activity you want to include in your feed.

first version with project pages

Florian (Aug 24, 2024, 5:19 AM +0200) 49e82d33 b7860105

+1147 -693
+16
README.md
··· 14 14 15 15 https://github.com/flo-bit/flo-bit.github.io/assets/45694132/a251ab6f-a416-4a2d-905f-8dffb2e36e0f 16 16 17 + 18 + 19 + ## todo 20 + 21 + - add projects 22 + - [x] shmup 23 + - youtube party djay 24 + - instalingua 25 + - vr portfolio 26 + - setup projects v3 27 + - improve planet stuff 28 + - add favourite stack? 29 + - add images, descriptions, links, tags to all projects 30 + - update favicon 31 + - fix android bug 32 + - improve performance
+14
package-lock.json
··· 15 15 "@tailwindcss/typography": "^0.5.14", 16 16 "@threlte/rapier": "^2.0.1", 17 17 "@threlte/xr": "^0.1.4", 18 + "@use-gesture/vanilla": "^10.3.1", 18 19 "alea": "^1.0.1", 19 20 "autoprefixer": "^10.4.16", 20 21 "clsx": "^2.1.1", ··· 1913 1914 "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", 1914 1915 "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", 1915 1916 "dev": true 1917 + }, 1918 + "node_modules/@use-gesture/core": { 1919 + "version": "10.3.1", 1920 + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", 1921 + "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==" 1922 + }, 1923 + "node_modules/@use-gesture/vanilla": { 1924 + "version": "10.3.1", 1925 + "resolved": "https://registry.npmjs.org/@use-gesture/vanilla/-/vanilla-10.3.1.tgz", 1926 + "integrity": "sha512-lT4scGLu59ovA3zmtUonukAGcA0AdOOh+iwNDS05Bsu7Lq9aZToDHhI6D8Q2qvsVraovtsLLYwPrWdG/noMAKw==", 1927 + "dependencies": { 1928 + "@use-gesture/core": "10.3.1" 1929 + } 1916 1930 }, 1917 1931 "node_modules/acorn": { 1918 1932 "version": "8.12.0",
+1
package.json
··· 46 46 "@tailwindcss/typography": "^0.5.14", 47 47 "@threlte/rapier": "^2.0.1", 48 48 "@threlte/xr": "^0.1.4", 49 + "@use-gesture/vanilla": "^10.3.1", 49 50 "alea": "^1.0.1", 50 51 "autoprefixer": "^10.4.16", 51 52 "clsx": "^2.1.1",
+1 -1
src/app.html
··· 42 42 </head> 43 43 <body 44 44 data-sveltekit-preload-data="hover" 45 - class="h-full bg-white dark:bg-black transition-colors duration-150 dark:transition-color dark:duration-150" 45 + class="h-full bg-white dark:bg-black transition-colors duration-150 dark:transition-color dark:duration-150 lowercase" 46 46 > 47 47 <div style="display: contents">%sveltekit.body%</div> 48 48 </body>
-159
src/projects.ts
··· 1 - import svelteswipecards from '$lib/images/projects-new/svelte-swiper-cards-demo.mp4'; 2 - import fluidtexteffect from '$lib/images/projects-new/text-effect-fluid-demo.mp4'; 3 - import fake3d from '$lib/images/projects-new/fake3dimage-demo.mp4'; 4 - import hyperlumen from '$lib/images/projects-new/hyperlumen-demo.mp4'; 5 - import marblellous from '$lib/images/projects-new/marblellous-demo.mp4'; 6 - import oldcode from '$lib/images/projects-new/old-code-demo.mp4'; 7 - import autostereogramrenderer from '$lib/images/projects-new/autostereogram-renderer-demo.mp4'; 8 - import mandala from '$lib/images/projects-new/mandala-demo.mp4'; 9 - import threltevrcodeeditor from '$lib/images/projects-new/threlte-vr-code-editor-demo.mp4'; 10 - 11 - import svelteswipecards1 from '$lib/images/projects-new/svelte-swiper-cards-image.png'; 12 - 13 - import hyperlumen1 from '$lib/images/projects-new/hyperlumen/1.jpg'; 14 - import hyperlumen2 from '$lib/images/projects-new/hyperlumen/2.jpg'; 15 - import hyperlumen3 from '$lib/images/projects-new/hyperlumen/3.jpg'; 16 - import hyperlumen4 from '$lib/images/projects-new/hyperlumen/4.jpg'; 17 - 18 - import shadowshmup from '$lib/images/projects-new/shmup/shadow-shmup-demo.mp4'; 19 - 20 - export type Project = { 21 - src: string; 22 - key: string; 23 - name: string; 24 - alt: string; 25 - 26 - href?: string; 27 - aspect?: string; 28 - description?: string; 29 - code?: string; 30 - images?: string[]; 31 - highlights?: string[]; 32 - details?: string; 33 - projectPageVersion?: 'v1' | 'v2' | 'v3'; 34 - tags?: string[]; 35 - 36 - projectUrl?: string; 37 - codeUrl?: string; 38 - }; 39 - 40 - export const projects: Project[] = [ 41 - { 42 - src: svelteswipecards, 43 - key: 'svelte-swiper-cards', 44 - alt: 'svelte-swiper-cards', 45 - href: 'https://flo-bit.github.io/svelte-swiper-cards/', 46 - 47 - projectUrl: 'https://flo-bit.github.io/svelte-swiper-cards/', 48 - 49 - aspect: 'aspect-[9/18]', 50 - name: 'swipable card component', 51 - description: 'a swipeable tinder like card component for svelte.', 52 - code: 'https://github.com/flo-bit/svelte-swiper-cards/', 53 - 54 - codeUrl: 'https://github.com/flo-bit/svelte-swiper-cards/', 55 - 56 - projectPageVersion: 'v2', 57 - tags: ['svelte', 'typescript', 'component'], 58 - images: [svelteswipecards1, svelteswipecards1] 59 - }, 60 - { 61 - src: fluidtexteffect, 62 - key: 'text-effect-fluid', 63 - alt: 'Text Fluid Effect', 64 - projectUrl: 'https://flo-bit.github.io/text_effect_fluid/', 65 - aspect: 'aspect-[23/9]', 66 - name: 'fluid text effect', 67 - codeUrl: 'https://github.com/flo-bit/text_effect_fluid' 68 - }, 69 - { 70 - src: shadowshmup, 71 - key: 'shadow-shmup', 72 - codeUrl: 'https://flo-bit.github.io/shadow-shmup/', 73 - alt: 'shadow shmup', 74 - aspect: 'aspect-[13/9]', 75 - name: 'shadow shmup', 76 - description: 'a 2d shoot em up game with cool visuals. made for piratejam 2024', 77 - tags: ['game', '2d', 'pixijs'] 78 - }, 79 - { 80 - src: hyperlumen, 81 - key: 'hyperlumen', 82 - alt: 'Hyperlumen', 83 - 84 - projectUrl: 'https://hyperlumen.de', 85 - 86 - aspect: 'aspect-[9/14]', 87 - name: 'hyperlumen', 88 - description: 89 - "light-up clothes that make you feel like you're in a sci-fi movie. perfect for raves.", 90 - images: [hyperlumen1, hyperlumen2, hyperlumen3, hyperlumen4], 91 - highlights: [ 92 - 'controlled via wifi with your smartphone', 93 - 'all the colors, all the animations', 94 - 'powered by a powerbank', 95 - 'handmade in germany' 96 - ], 97 - projectPageVersion: 'v1', 98 - tags: ['wled', 'wearable', 'leds', 'hardware'] 99 - }, 100 - { 101 - src: mandala, 102 - key: 'svg-mandala', 103 - alt: 'svg mandala drawer', 104 - projectUrl: 'https://flo-bit.github.io/mandala/', 105 - aspect: 'aspect-1', 106 - name: 'svg mandala drawer', 107 - description: 'draw svg mandalas in the browser.' 108 - }, 109 - { 110 - src: fake3d, 111 - key: 'image2fake3d', 112 - alt: 'Image2Fake3D', 113 - projectUrl: 'https://flo-bit.github.io/image2fake3d/', 114 - aspect: 'aspect-[9/16]', 115 - name: 'image2fake3d', 116 - description: 117 - 'turns an image into a fake 3d image that you can rotate with your mouse or gyro sensor.', 118 - codeUrl: 'https://github.com/flo-bit/image2fake3d' 119 - }, 120 - { 121 - src: marblellous, 122 - key: 'marblellous', 123 - alt: 'marblellous', 124 - projectUrl: 'https://flo-bit.github.io/ball-game/', 125 - aspect: 'aspect-[9/10]', 126 - name: 'marblellous', 127 - description: 128 - 'simple 3d browser game made with threlte. inspired by the game "marble blast gold".', 129 - codeUrl: 'https://github.com/flo-bit/ball-game' 130 - }, 131 - { 132 - src: oldcode, 133 - key: 'old-code', 134 - alt: 'old projects', 135 - projectUrl: 'https://flo-bit.github.io/old-code/', 136 - aspect: 'aspect-[16/11]', 137 - name: 'old coding projects', 138 - code: 'https://github.com/flo-bit/old-code' 139 - }, 140 - { 141 - src: threltevrcodeeditor, 142 - key: 'threlte-vr-code-editor', 143 - alt: 'threlte vr code editor', 144 - projectUrl: 'https://github.com/flo-bit/threlte-vr-code-editor', 145 - aspect: 'aspect-[16/11]', 146 - name: 'threlte vr code editor', 147 - description: 'a virtual reality code editor made with threlte.', 148 - code: 'https://github.com/flo-bit/threlte-vr-code-editor' 149 - }, 150 - { 151 - src: autostereogramrenderer, 152 - key: 'autostereogram-renderer', 153 - alt: 'autostereogram renderer', 154 - projectUrl: 'https://flo-bit.github.io/autostereogram-renderer/', 155 - aspect: 'aspect-1', 156 - name: 'autostereogram renderer', 157 - description: 'rendering a 3d scene as an autostereogram.' 158 - } 159 - ];
+208
src/lib/projects.ts
··· 1 + import svelteswipecards from '$lib/images/projects-new/svelte-swiper-cards-demo.mp4'; 2 + import fluidtexteffect from '$lib/images/projects-new/text-effect-fluid-demo.mp4'; 3 + import fake3d from '$lib/images/projects-new/fake3dimage-demo.mp4'; 4 + import hyperlumen from '$lib/images/projects-new/hyperlumen-demo.mp4'; 5 + import marblellous from '$lib/images/projects-new/marblellous-demo.mp4'; 6 + import oldcode from '$lib/images/projects-new/old-code-demo.mp4'; 7 + import autostereogramrenderer from '$lib/images/projects-new/autostereogram-renderer-demo.mp4'; 8 + import mandala from '$lib/images/projects-new/mandala-demo.mp4'; 9 + import threltevrcodeeditor from '$lib/images/projects-new/threlte-vr-code-editor-demo.mp4'; 10 + 11 + import svelteswipecards1 from '$lib/images/projects-new/svelte-swiper-cards-image.png'; 12 + 13 + // @ts-expect-error - module not found 14 + import hyperlumen1 from '$lib/images/projects-new/hyperlumen/1.jpg?w=1024&format=webp'; 15 + // @ts-expect-error - module not found 16 + import hyperlumen2 from '$lib/images/projects-new/hyperlumen/2.jpg?w=1024&format=webp'; 17 + // @ts-expect-error - module not found 18 + import hyperlumen3 from '$lib/images/projects-new/hyperlumen/3.jpg?w=1024&format=webp'; 19 + // @ts-expect-error - module not found 20 + import hyperlumen4 from '$lib/images/projects-new/hyperlumen/4.jpg?w=1024&format=webp'; 21 + 22 + // @ts-expect-error - module not found 23 + import shadowshmup1 from '$lib/images/projects-new/shmup/image1.png?w=1024&format=webp'; 24 + // @ts-expect-error - module not found 25 + import shadowshmup2 from '$lib/images/projects-new/shmup/image2.png?w=1024&format=webp'; 26 + // @ts-expect-error - module not found 27 + import shadowshmup3 from '$lib/images/projects-new/shmup/image3.png?w=1024&format=webp'; 28 + // @ts-expect-error - module not found 29 + import shadowshmup4 from '$lib/images/projects-new/shmup/image4.png?w=1024&format=webp'; 30 + 31 + import shadowshmup from '$lib/images/projects-new/shmup/shadow-shmup-demo.mp4'; 32 + 33 + export type Project = { 34 + src: string; 35 + key: string; 36 + name: string; 37 + alt: string; 38 + 39 + href?: string; 40 + aspect?: string; 41 + description?: string; 42 + code?: string; 43 + images?: string[]; 44 + highlights?: string[]; 45 + details?: string; 46 + projectPageVersion?: 'grid' | 'carousel' | 'tiered' | 'full'; 47 + tags?: string[]; 48 + 49 + projectUrl?: string; 50 + codeUrl?: string; 51 + 52 + demo?: string; 53 + }; 54 + 55 + export const projects: Project[] = [ 56 + { 57 + src: svelteswipecards, 58 + key: 'svelte-swiper-cards', 59 + alt: 'svelte-swiper-cards', 60 + href: 'https://flo-bit.github.io/svelte-swiper-cards/', 61 + 62 + projectUrl: 'https://flo-bit.github.io/svelte-swiper-cards/', 63 + 64 + aspect: 'aspect-[9/18]', 65 + name: 'swipable card component for svelte', 66 + description: 'a swipeable tinder like card component for svelte. try the interactive demo!', 67 + code: 'https://github.com/flo-bit/svelte-swiper-cards/', 68 + 69 + codeUrl: 'https://github.com/flo-bit/svelte-swiper-cards/', 70 + 71 + projectPageVersion: 'tiered', 72 + tags: ['svelte', 'typescript', 'component', 'tailwindcss'], 73 + images: [svelteswipecards1], 74 + demo: 'swiper', 75 + highlights: [ 76 + 'tailwindcss', 77 + 'reuses cards', 78 + 'customizable', 79 + 'modern (uses @use-gesture/vanilla for gestures)' 80 + ] 81 + }, 82 + { 83 + src: fluidtexteffect, 84 + key: 'text-effect-fluid', 85 + alt: 'Text Fluid Effect', 86 + projectUrl: 'https://flo-bit.github.io/text_effect_fluid/', 87 + aspect: 'aspect-[23/9]', 88 + name: 'fluid text effect', 89 + codeUrl: 'https://github.com/flo-bit/text_effect_fluid', 90 + description: 91 + 'inspired by lumalabs landing page, adopted a cool fluid effect to work with text. sometimes still a bit broken (try refreshing the page once or twice if it is).', 92 + projectPageVersion: 'full', 93 + demo: 'fluid' 94 + }, 95 + { 96 + src: shadowshmup, 97 + key: 'shadow-shmup', 98 + codeUrl: 'https://flo-bit.github.io/shadow-shmup/', 99 + alt: 'shadow shmup', 100 + aspect: 'aspect-[13/9]', 101 + name: 'shadow shmup', 102 + description: 103 + "fun and casual top-down, roguelike shoot 'em up with shadow elements and colorful neon effects. made for piratejam 2024", 104 + tags: ['game', '2d', 'pixijs', 'rapier2d', 'typescript'], 105 + images: [shadowshmup1, shadowshmup2, shadowshmup3, shadowshmup4], 106 + projectPageVersion: 'carousel', 107 + projectUrl: 'https://flo-bit.itch.io/shadow-shmup' 108 + }, 109 + { 110 + src: hyperlumen, 111 + key: 'hyperlumen', 112 + alt: 'Hyperlumen', 113 + 114 + projectUrl: 'https://hyperlumen.de', 115 + codeUrl: '#', 116 + 117 + aspect: 'aspect-[9/14]', 118 + name: 'hyperlumen', 119 + description: 120 + "light-up clothes that make you feel like you're in a sci-fi movie. perfect for raves.", 121 + images: [hyperlumen1, hyperlumen2, hyperlumen3, hyperlumen4], 122 + highlights: [ 123 + 'controlled via wifi with your smartphone', 124 + 'all the colors, all the animations', 125 + 'powered by a powerbank', 126 + 'handmade in germany' 127 + ], 128 + details: 129 + 'for this project - apart from taking part in building the prototypes - i customized the awesome wled fireware, adding some effects and simplifying the web ui. i also made a simple website showcasing our prototypes using svelte and tailwindcss.', 130 + projectPageVersion: 'grid', 131 + tags: ['wled', 'wearable', 'leds', 'hardware', 'c++', 'svelte', 'hardware', 'tailwindcss'] 132 + }, 133 + { 134 + src: mandala, 135 + key: 'svg-mandala', 136 + alt: 'svg mandala drawer', 137 + projectUrl: 'https://flo-bit.github.io/mandala/', 138 + aspect: 'aspect-1', 139 + name: 'svg mandala drawer', 140 + description: 141 + 'one of my earlier projects, draw svg mandalas in the browser. works on mobile too. made with paperjs and bulma. try the interactive demo above!', 142 + tags: ['svg', 'paperjs', 'javascript', 'bulma'], 143 + projectPageVersion: 'full', 144 + demo: 'mandala', 145 + codeUrl: 'https://github.com/flo-bit/mandala' 146 + }, 147 + { 148 + src: fake3d, 149 + key: 'image2fake3d', 150 + alt: 'Image2Fake3D', 151 + projectUrl: 'https://flo-bit.github.io/image2fake3d/', 152 + aspect: 'aspect-[9/16]', 153 + name: 'image2fake3d', 154 + description: 155 + 'turns an image into a fake 3d image that you can rotate with your mouse or gyro sensor.', 156 + codeUrl: 'https://github.com/flo-bit/image2fake3d', 157 + tags: ['svelte', 'depthmap', '3d', 'threlte'] 158 + }, 159 + { 160 + src: marblellous, 161 + key: 'marblellous', 162 + alt: 'marblellous', 163 + projectUrl: 'https://flo-bit.github.io/ball-game/', 164 + aspect: 'aspect-[9/10]', 165 + name: 'marblellous', 166 + description: 167 + 'simple 3d browser game made with threlte. inspired by the game "marble blast gold". work in progress.', 168 + codeUrl: 'https://github.com/flo-bit/ball-game', 169 + tags: ['threlte', 'svelte', '3d', 'typescript', 'game'] 170 + }, 171 + { 172 + src: oldcode, 173 + key: 'old-code', 174 + alt: 'old projects', 175 + projectUrl: 'https://flo-bit.github.io/old-code/', 176 + aspect: 'aspect-[16/11]', 177 + name: 'old coding projects', 178 + codeUrl: 'https://github.com/flo-bit/old-code', 179 + description: 'a collection of my old coding projects. mostly made with p5.js', 180 + tags: ['p5.js', 'javascript', 'game', 'art', 'flowfield'] 181 + }, 182 + { 183 + src: threltevrcodeeditor, 184 + key: 'threlte-vr-code-editor', 185 + alt: 'threlte vr code editor', 186 + projectUrl: 'https://github.com/flo-bit/threlte-vr-code-editor', 187 + aspect: 'aspect-[16/11]', 188 + name: 'threlte vr code editor', 189 + description: 'a code editor in virtual reality made with threlte.', 190 + codeUrl: 'https://github.com/flo-bit/threlte-vr-code-editor', 191 + highlights: ['works with hot module replacement'], 192 + tags: ['threlte', 'svelte', 'vr', 'typescript', 'code editor'] 193 + }, 194 + { 195 + src: autostereogramrenderer, 196 + key: 'autostereogram-renderer', 197 + alt: 'autostereogram renderer', 198 + projectUrl: 'https://flo-bit.github.io/autostereogram-renderer/', 199 + codeUrl: 'https://github.com/flo-bit/autostereogram-renderer', 200 + aspect: 'aspect-1', 201 + name: 'autostereogram renderer', 202 + description: 'rendering a 3d scene as an autostereogram. try to see the hidden animal!', 203 + demo: 'autostereogram', 204 + projectPageVersion: 'tiered', 205 + highlights: ['creates autosteregram from rendered depth map'], 206 + tags: ['three.js', 'javascript', '3d', 'autostereogram'] 207 + } 208 + ];
-1
src/routes/+page.svelte
··· 14 14 import About from '$lib/components/About.svelte'; 15 15 import Learning from '$lib/components/Learning.svelte'; 16 16 import Posts from '$lib/components/Posts.svelte'; 17 - import Tools from '$lib/components/Tools.svelte'; 18 17 19 18 let active: 'home' | 'about' | 'projects' | 'learning' | 'contact' = 'home'; 20 19
-2
src/lib/3D/PlanetModel.svelte
··· 8 8 import { T, type Props, type Events, type Slots, forwardEventHandlers } from '@threlte/core'; 9 9 import { useGltf } from '@threlte/extras'; 10 10 11 - import FakeGlowMaterial from './FakeGlowMaterial/FakeGlowMaterial.svelte'; 12 - 13 11 type $$Props = Props<THREE.Group>; 14 12 type $$Events = Events<THREE.Group>; 15 13 type $$Slots = Slots<THREE.Group> & { fallback: {}; error: { error: any } };
+1
src/lib/3D/Scene.svelte
··· 125 125 far={100} 126 126 /> 127 127 128 + <T.AmbientLight intensity={0.15} /> 128 129 <T.DirectionalLight intensity={2} position={[-pos * 10 + 5, 2 + pos * 3, 2]} /> 129 130 130 131 <T.Group
+2 -2
src/lib/components/About.svelte
··· 2 2 import Depth3D from '$lib/3D/Depth3D/Depth3D.svelte'; 3 3 4 4 // @ts-ignore 5 - import imageMe from '$lib/images/avatar.png?w=1024&format=webp'; 5 + import imageMe from '$lib/images/avatar.png?w=512&format=webp'; 6 6 // @ts-ignore 7 - import depthMe from '$lib/images/avatar-depth.png?w=1024&format=webp'; 7 + import depthMe from '$lib/images/avatar-depth.png?w=512&format=webp'; 8 8 9 9 import Resume from './Resume.svelte'; 10 10 </script>
+1 -1
src/lib/components/Hero.svelte
··· 1 - <div class="relative isolate overflow-hidden section h-screen pointer-events-none ml-52" id="home"> 1 + <div class="relative isolate overflow-hidden section h-screen pointer-events-none ml-48" id="home"> 2 2 <div class="mx-auto max-w-5xl px-6 py-40 lg:px-8"> 3 3 <h1 4 4 class="text-6xl mt-0 md:mt-36 font-bold tracking-tight text-right w-full text-zinc-900 dark:text-zinc-100 md:text-8xl"
-31
src/lib/components/Photos.svelte
··· 1 - <script lang="ts"> 2 - let rotations = ['rotate-2', '-rotate-2', 'rotate-2', 'rotate-2', '-rotate-2']; 3 - 4 - export let images: { href?: string; src: string; alt: string }[]; 5 - </script> 6 - 7 - <div class="bg-black overflow-x-scroll no-scrollbar pt-10"> 8 - <div class="flex gap-5 py-4 sm:gap-8 2xl:justify-center"> 9 - {#each images as image, index} 10 - <div 11 - class="relative aspect-[9/10] w-44 flex-none overflow-hidden rounded-xl ring-1 ring-white/10 bg-zinc-100 dark:bg-zinc-800 sm:w-72 sm:rounded-2xl {rotations[ 12 - index % rotations.length 13 - ]}" 14 - > 15 - {#if image.src.endsWith('.mp4')} 16 - <video autoplay loop muted playsinline class="absolute inset-0 h-full w-full object-cover rounded-xl" preload="none"> 17 - <source src={image.src} type="video/mp4" /> 18 - </video> 19 - {:else} 20 - <img src={image.src} alt={image.alt} class="absolute inset-0 h-full w-full object-cover rounded-xl" loading="lazy"/> 21 - {/if} 22 - {#if image.href} 23 - <a href={image.href} target="_blank"> 24 - <div class="sr-only">{image.alt}</div> 25 - <div class="absolute inset-0 rounded-xl"></div> 26 - </a> 27 - {/if} 28 - </div> 29 - {/each} 30 - </div> 31 - </div>
-37
src/lib/components/ProjectBreadcrumb.svelte
··· 1 - <script lang="ts"> 2 - export let name: string; 3 - </script> 4 - 5 - <nav aria-label="Breadcrumb"> 6 - <ol 7 - role="list" 8 - class="mx-auto flex max-w-2xl items-center space-x-2 px-4 sm:px-6 lg:max-w-5xl lg:px-8" 9 - > 10 - <li> 11 - <div class="flex items-center"> 12 - <a 13 - href="/#projects" 14 - class="mr-2 text-sm font-medium text-neutral-900 hover:text-neutral-800 dark:text-neutral-100" 15 - >projects</a 16 - > 17 - <svg 18 - width="16" 19 - height="20" 20 - viewBox="0 0 16 20" 21 - fill="currentColor" 22 - aria-hidden="true" 23 - class="h-5 w-4 text-neutral-300" 24 - > 25 - <path d="M5.697 4.34L8.98 16.532h1.327L7.025 4.341H5.697z" /> 26 - </svg> 27 - </div> 28 - </li> 29 - 30 - <li class="text-sm"> 31 - <div 32 - class="font-medium text-neutral-500 dark:text-neutral-400 " 33 - >{name}</div 34 - > 35 - </li> 36 - </ol> 37 - </nav>
-22
src/lib/components/ProjectButtons.svelte
··· 1 - <script lang="ts"> 2 - export let codeUrl: string | undefined; 3 - export let projectUrl: string | undefined; 4 - </script> 5 - 6 - {#if codeUrl} 7 - <a 8 - href={codeUrl} 9 - target="_blank" 10 - class="flex w-full items-center justify-center rounded-xl bg-white/10 px-8 py-3 text-base font-medium text-white hover:bg-white/20" 11 - >check out the code</a 12 - > 13 - {/if} 14 - 15 - {#if projectUrl} 16 - <a 17 - href={projectUrl} 18 - target="_blank" 19 - class="flex w-full items-center justify-center rounded-xl dark:text-black bg-accent-400 px-8 py-3 text-base font-medium text-white hover:bg-accent-300" 20 - >launch project</a 21 - > 22 - {/if}
-132
src/lib/components/ProjectV1.svelte
··· 1 - <script lang="ts"> 2 - import type { Project } from '../../projects'; 3 - import Badge from './badge/badge.svelte'; 4 - import ProjectBreadcrumb from './ProjectBreadcrumb.svelte'; 5 - import ProjectButtons from './ProjectButtons.svelte'; 6 - 7 - export let project: Project; 8 - </script> 9 - 10 - <div class=""> 11 - <div class="pt-6 sm:pt-24"> 12 - <ProjectBreadcrumb name={project.name} /> 13 - 14 - <!-- Image gallery --> 15 - {#if project.images} 16 - <div 17 - class="mx-auto mt-6 max-w-2xl sm:px-6 lg:grid lg:max-w-5xl lg:grid-cols-3 lg:gap-x-8 lg:px-8" 18 - > 19 - <div class="aspect-h-4 aspect-w-3 hidden overflow-hidden rounded-xl lg:block"> 20 - <img 21 - src={project.images[0]} 22 - alt="Two each of neutral, white, and black shirts laying flat." 23 - class="h-full w-full object-cover object-center" 24 - /> 25 - </div> 26 - <div class="hidden lg:grid lg:grid-cols-1 lg:gap-y-8"> 27 - <div class="aspect-h-2 aspect-w-3 overflow-hidden rounded-xl"> 28 - <img 29 - src={project.images[1]} 30 - alt="Model wearing plain black basic tee." 31 - class="h-full w-full object-cover object-center" 32 - /> 33 - </div> 34 - <div class="aspect-h-2 aspect-w-3 overflow-hidden rounded-xl"> 35 - <img 36 - src={project.images[2]} 37 - alt="Model wearing plain neutral basic tee." 38 - class="h-full w-full object-cover object-center" 39 - /> 40 - </div> 41 - </div> 42 - <div 43 - class="aspect-h-5 aspect-w-4 lg:aspect-h-4 lg:aspect-w-3 sm:overflow-hidden sm:rounded-xl" 44 - > 45 - <img 46 - src={project.images[3]} 47 - alt="Model wearing plain white basic tee." 48 - class="h-full w-full object-cover object-center" 49 - /> 50 - </div> 51 - </div> 52 - {/if} 53 - 54 - <!-- Product info --> 55 - <div 56 - class="mx-auto max-w-2xl px-4 pb-16 pt-10 sm:px-6 lg:grid lg:max-w-5xl lg:grid-cols-3 lg:grid-rows-[auto,auto,1fr] lg:gap-x-8 lg:px-8 lg:pb-24 lg:pt-16" 57 - > 58 - <div class="lg:col-span-2 lg:border-r lg:border-neutral-200 dark:border-neutral-800 lg:pr-8"> 59 - <h1 class="text-2xl font-bold tracking-tight text-neutral-900 dark:text-white sm:text-3xl"> 60 - {project.name} 61 - </h1> 62 - </div> 63 - 64 - <!-- Options --> 65 - <div class="mt-4 lg:row-span-3 lg:mt-0"> 66 - <h2 class="sr-only">project information</h2> 67 - 68 - <div class="mt-10"> 69 - {#if project.tags} 70 - <!-- tags --> 71 - <div class="mt-10"> 72 - <div class="mt-4"> 73 - <div class="flex flex-wrap gap-4"> 74 - {#each project.tags as tag} 75 - <Badge size="lg">{tag}</Badge> 76 - {/each} 77 - </div> 78 - </div> 79 - </div> 80 - {/if} 81 - 82 - <div class="mt-8 flex flex-col gap-4"> 83 - <ProjectButtons codeUrl={project.codeUrl} projectUrl={project.projectUrl} /> 84 - </div> 85 - </div> 86 - </div> 87 - 88 - <div 89 - class="py-10 lg:col-span-2 lg:col-start-1 lg:border-r lg:border-neutral-200 dark:border-neutral-800 lg:pb-16 lg:pr-8 lg:pt-6" 90 - > 91 - <!-- Description and details --> 92 - <div> 93 - <h3 class="sr-only">Description</h3> 94 - 95 - <div class="space-y-6"> 96 - <p class="text-base text-neutral-900 dark:text-neutral-200"> 97 - {project.description} 98 - </p> 99 - </div> 100 - </div> 101 - 102 - {#if project.highlights} 103 - <div class="mt-10"> 104 - <h3 class="text-sm font-medium text-neutral-900 dark:text-neutral-50">highlights</h3> 105 - 106 - <div class="mt-4"> 107 - <ul role="list" class="list-disc space-y-2 pl-4 text-sm"> 108 - {#each project.highlights as highlight} 109 - <li class="text-neutral-400 dark:text-neutral-600"> 110 - <span class="text-neutral-600 dark:text-neutral-300">{highlight}</span> 111 - </li> 112 - {/each} 113 - </ul> 114 - </div> 115 - </div> 116 - {/if} 117 - 118 - {#if project.details} 119 - <div class="mt-10"> 120 - <h2 class="text-sm font-medium text-neutral-900 dark:text-neutral-50">details</h2> 121 - 122 - <div class="mt-4 space-y-6"> 123 - <p class="text-sm text-neutral-600 dark:text-neutral-300"> 124 - {project.details} 125 - </p> 126 - </div> 127 - </div> 128 - {/if} 129 - </div> 130 - </div> 131 - </div> 132 - </div>
-132
src/lib/components/ProjectV2.svelte
··· 1 - <script lang="ts"> 2 - import type { Project } from '../../projects'; 3 - import { Badge } from './badge'; 4 - import ProjectBreadcrumb from './ProjectBreadcrumb.svelte'; 5 - import ProjectButtons from './ProjectButtons.svelte'; 6 - 7 - export let project: Project; 8 - 9 - let selectedImage = 0; 10 - </script> 11 - 12 - <div class=""> 13 - <div class="mx-auto max-w-2xl px-4 py-8 sm:px-6 sm:py-24 lg:max-w-5xl lg:px-8"> 14 - <ProjectBreadcrumb name={project.name} /> 15 - 16 - <div class="mt-6 lg:grid lg:grid-cols-2 lg:items-start lg:gap-x-8"> 17 - <!-- Image gallery --> 18 - {#if project.images} 19 - <div class="flex flex-col-reverse"> 20 - <!-- Image selector --> 21 - 22 - {#if project.images.length > 0} 23 - <div class="mx-auto mt-6 hidden w-full max-w-2xl sm:block lg:max-w-none"> 24 - <div class="grid grid-cols-4 gap-6" aria-orientation="horizontal" role="tablist"> 25 - {#each project.images as image, index} 26 - <button 27 - class="relative flex h-24 cursor-pointer items-center justify-center rounded-md bg-white/10 text-sm font-medium text-neutral-900 hover:bg-neutral-400 border {index == 28 - selectedImage 29 - ? 'border-accent-400' 30 - : 'border-accent-100/20 hover:border-accent-400'}" 31 - type="button" 32 - on:click={() => (selectedImage = index)} 33 - > 34 - <span class="absolute inset-0 overflow-hidden rounded-md"> 35 - <img src={image} alt="" class="h-full w-full object-cover object-center" /> 36 - </span> 37 - <!-- Selected: "ring-indigo-500", Not Selected: "ring-transparent" --> 38 - <span class="pointer-events-none absolute inset-0 rounded-md" aria-hidden="true" 39 - ></span> 40 - </button> 41 - 42 - <!-- More images... --> 43 - {/each} 44 - 45 - <!-- More images... --> 46 - </div> 47 - </div> 48 - {/if} 49 - 50 - <div class="aspect-h-1 aspect-w-1 w-full"> 51 - <!-- Tab panel, show/hide based on tab state. --> 52 - <div id="tabs-1-panel-1" aria-labelledby="tabs-1-tab-1" role="tabpanel" tabindex="0"> 53 - <img 54 - src={project.images[selectedImage]} 55 - alt="Angled front view with bag zipped and handles upright." 56 - class="h-full w-full object-cover object-center sm:rounded-lg" 57 - /> 58 - </div> 59 - 60 - <!-- More images... --> 61 - </div> 62 - </div> 63 - {/if} 64 - 65 - <!-- Product info --> 66 - <div class="mt-10 px-4 sm:mt-16 sm:px-0 lg:mt-0"> 67 - <h1 class="text-3xl font-bold tracking-tight text-neutral-900 dark:text-white"> 68 - {project.name} 69 - </h1> 70 - 71 - {#if project.tags} 72 - <!-- tags --> 73 - <div class="mt-10"> 74 - <div class="mt-4 mb-8"> 75 - <div class="flex flex-wrap gap-4"> 76 - {#each project.tags as tag} 77 - <Badge size="lg">{tag}</Badge> 78 - {/each} 79 - </div> 80 - </div> 81 - 82 - <div class="grid sm:grid-cols-2 gap-4"> 83 - <ProjectButtons codeUrl={project.code} projectUrl={project.projectUrl} /> 84 - </div> 85 - </div> 86 - {/if} 87 - 88 - <div class="mt-6"> 89 - <h3 class="sr-only">Description</h3> 90 - 91 - <div class="space-y-6 text-base text-neutral-700 dark:text-neutral-300"> 92 - <p> 93 - {project.description} 94 - </p> 95 - </div> 96 - </div> 97 - 98 - <section aria-labelledby="details-heading" class="mt-12"> 99 - <h2 id="details-heading" class="sr-only">Additional details</h2> 100 - 101 - {#if project.highlights} 102 - <div 103 - class="divide-y divide-neutral-200 dark:divide-neutral-800 border-t dark:border-neutral-800" 104 - > 105 - <div> 106 - <h3> 107 - <!-- Expand/collapse question button --> 108 - <div class="group relative flex w-full items-center justify-between py-6 text-left"> 109 - <!-- Open: "text-indigo-600", Closed: "text-neutral-900" --> 110 - <span class="text-sm font-medium text-neutral-900 dark:text-neutral-100" 111 - >highlights</span 112 - > 113 - </div> 114 - </h3> 115 - <div class="prose prose-sm pb-6 dark:prose-invert prose-neutral" id="disclosure-1"> 116 - <ul role="list"> 117 - {#each project.highlights as highlight} 118 - <li>{highlight}</li> 119 - {/each} 120 - </ul> 121 - </div> 122 - </div> 123 - 124 - <!-- More sections... --> 125 - </div> 126 - {/if} 127 - 128 - </section> 129 - </div> 130 - </div> 131 - </div> 132 - </div>
-47
src/lib/components/ProjectV3.svelte
··· 1 - <script lang="ts"> 2 - import Project from '$lib/vr/components/Project.svelte'; 3 - import ProjectBreadcrumb from './ProjectBreadcrumb.svelte'; 4 - import ProjectButtons from './ProjectButtons.svelte'; 5 - 6 - export let project: Project; 7 - </script> 8 - 9 - <div class=""> 10 - <div 11 - class="mx-auto max-w-2xl px-4 py-16 sm:px-6 sm:py-24 lg:grid lg:max-w-5xl lg:grid-cols-2 lg:gap-x-8 lg:px-8" 12 - > 13 - <!-- Product details --> 14 - <div class="lg:max-w-lg lg:self-end"> 15 - <ProjectBreadcrumb name={project.name} /> 16 - 17 - <div class="mt-4"> 18 - <h1 class="text-3xl font-bold tracking-tight text-white sm:text-4xl">{project.name}</h1> 19 - </div> 20 - 21 - <section aria-labelledby="information-heading" class="mt-4"> 22 - <h2 id="information-heading" class="sr-only">project information</h2> 23 - 24 - <div class="mt-4 space-y-6"> 25 - <p class="text-base text-neutral-400">{project.description}</p> 26 - </div> 27 - 28 - <div class="mt-8 grid sm:grid-cols-2 gap-4"> 29 - <ProjectButtons codeUrl={project.codeUrl} projectUrl={project.projectUrl} /> 30 - </div> 31 - </section> 32 - </div> 33 - 34 - {#if project.images} 35 - <!-- Product image --> 36 - <div class="mt-10 lg:col-start-2 lg:row-span-2 lg:mt-0 lg:self-center"> 37 - <div class="aspect-h-1 aspect-w-1 overflow-hidden rounded-lg"> 38 - <img 39 - src={project.images[0]} 40 - alt="Model wearing light green backpack with black canvas straps and front zipper pouch." 41 - class="h-full w-full object-cover object-center" 42 - /> 43 - </div> 44 - </div> 45 - {/if} 46 - </div> 47 - </div>
+2 -1
src/lib/components/Projects.svelte
··· 1 1 <script lang="ts"> 2 - import { projects } from '../../projects'; 2 + import { browser } from '$app/environment'; 3 + import { projects } from '$lib/projects'; 3 4 </script> 4 5 5 6 <div id="projects" class="z-20 py-16 md:py-32 section bg-black relative isolate">
-56
src/lib/3D/FakeGlowMaterial/FakeGlowMaterial.svelte
··· 1 - <script lang="ts"> 2 - import { T, useThrelte, forwardEventHandlers } from '@threlte/core' 3 - import { Color, AdditiveBlending, ShaderMaterial } from 'three' 4 - import type { 5 - FakeGlowMaterialEvents, 6 - FakeGlowMaterialProps, 7 - FakeGlowMaterialSlots 8 - } from './FakeGlowMaterial.svelte' 9 - 10 - import { fragmentShader } from './fragment' 11 - import { vertexShader } from './vertex' 12 - 13 - type $$Props = Required<FakeGlowMaterialProps> 14 - type $$Events = FakeGlowMaterialEvents 15 - type $$Slots = FakeGlowMaterialSlots 16 - 17 - export let falloff: $$Props['falloff'] = 0.1 18 - export let glowInternalRadius: $$Props['glowInternalRadius'] = 6.0 19 - export let glowColor: $$Props['glowColor'] = 'green' 20 - export let glowSharpness: $$Props['glowSharpness'] = 1.0 21 - 22 - let material = new ShaderMaterial({ 23 - uniforms: { 24 - falloff: { value: falloff }, 25 - glowInternalRadius: { value: glowInternalRadius }, 26 - glowColor: { value: new Color(glowColor) }, 27 - glowSharpness: { value: glowSharpness } 28 - } 29 - }) 30 - 31 - let { invalidate } = useThrelte() 32 - 33 - $: { 34 - material.uniforms.falloff.value = falloff 35 - material.uniforms.glowInternalRadius.value = glowInternalRadius 36 - material.uniforms.glowColor.value = new Color(glowColor) 37 - material.uniforms.glowSharpness.value = glowSharpness 38 - 39 - invalidate() 40 - } 41 - 42 - const component = forwardEventHandlers() 43 - </script> 44 - 45 - <T 46 - is={material} 47 - bind:this={$component} 48 - {fragmentShader} 49 - {vertexShader} 50 - transparent={true} 51 - blending={AdditiveBlending} 52 - depthTest={false} 53 - {...$$restProps} 54 - > 55 - <slot ref={material} /> 56 - </T>
-19
src/lib/3D/FakeGlowMaterial/FakeGlowMaterial.svelte.d.ts
··· 1 - import type { Events, Props, Slots } from '@threlte/core' 2 - import { SvelteComponent } from 'svelte' 3 - import type { ShaderMaterial, ColorRepresentation } from 'three' 4 - 5 - export type FakeGlowMaterialProps = Props<ShaderMaterial> & { 6 - falloff?: number 7 - glowInternalRadius?: number 8 - glowColor?: ColorRepresentation 9 - glowSharpness?: number 10 - } 11 - 12 - export type FakeGlowMaterialEvents = Events<ShaderMaterial> 13 - export type FakeGlowMaterialSlots = Slots<ShaderMaterial> 14 - 15 - export default class FakeGlowMaterial extends SvelteComponent< 16 - FakeGlowMaterialProps, 17 - FakeGlowMaterialEvents, 18 - FakeGlowMaterialSlots 19 - > {}
-29
src/lib/3D/FakeGlowMaterial/fragment.ts
··· 1 - import { ShaderChunk } from 'three'; 2 - 3 - export const fragmentShader = ` 4 - uniform vec3 glowColor; 5 - uniform float falloffAmount; 6 - uniform float glowSharpness; 7 - uniform float glowInternalRadius; 8 - 9 - varying vec3 vPosition; 10 - varying vec3 vNormal; 11 - 12 - void main() 13 - { 14 - // Normal 15 - vec3 normal = normalize(vNormal); 16 - if(!gl_FrontFacing) 17 - normal *= - 1.0; 18 - vec3 viewDirection = normalize(cameraPosition - vPosition); 19 - float fresnel = dot(viewDirection, normal); 20 - fresnel = pow(fresnel, glowInternalRadius + 0.1); 21 - float falloff = smoothstep(0., falloffAmount, fresnel); 22 - float fakeGlow = fresnel; 23 - fakeGlow += fresnel * glowSharpness; 24 - fakeGlow *= falloff; 25 - gl_FragColor = vec4(clamp(glowColor * fresnel, 0., 1.0), clamp(fakeGlow, 0., 1.0) * 0.5); 26 - 27 - ${ShaderChunk.tonemapping_fragment} 28 - ${ShaderChunk.colorspace_fragment} 29 - }`;
-10
src/lib/3D/FakeGlowMaterial/vertex.ts
··· 1 - export const vertexShader = `varying vec3 vPosition; 2 - varying vec3 vNormal; 3 - 4 - void main() { 5 - vec4 modelPosition = modelMatrix * vec4(position, 1.0); 6 - gl_Position = projectionMatrix * viewMatrix * modelPosition; 7 - vec4 modelNormal = modelMatrix * vec4(normal, 0.0); 8 - vPosition = modelPosition.xyz; 9 - vNormal = modelNormal.xyz; 10 - }`
+1 -1
src/lib/components/badge/index.ts
··· 6 6 variants: { 7 7 variant: { 8 8 default: 9 - 'rounded-md bg-accent-50 dark:bg-accent-500/10 text-accent-700 dark:text-accent-400 ring-1 fill-accent-500 dark:fill-accent-400', 9 + 'rounded-xl bg-accent-50 dark:bg-accent-500/10 text-accent-700 dark:text-accent-400 ring-1 fill-accent-500 dark:fill-accent-400', 10 10 pill: 'bg-accent-50 dark:bg-accent-500/10 text-accent-700 dark:text-accent-400 ring-1 fill-accent-500 dark:fill-accent-400 rounded-full', 11 11 dot: 'rounded-md bg-accent-50 dark:bg-accent-500/10 text-accent-700 dark:text-accent-400 ring-1 fill-accent-500 dark:fill-accent-400', 12 12 pill_dot:
+37
src/lib/components/project/ProjectBreadcrumb.svelte
··· 1 + <script lang="ts"> 2 + export let name: string; 3 + </script> 4 + 5 + <nav aria-label="Breadcrumb"> 6 + <ol 7 + role="list" 8 + class="mx-auto flex max-w-2xl items-center space-x-2 px-4 sm:px-6 lg:max-w-5xl lg:px-8" 9 + > 10 + <li> 11 + <div class="flex items-center"> 12 + <a 13 + href="/#projects" 14 + class="mr-2 text-sm font-medium text-neutral-900 hover:text-neutral-800 dark:text-neutral-100" 15 + >projects</a 16 + > 17 + <svg 18 + width="16" 19 + height="20" 20 + viewBox="0 0 16 20" 21 + fill="currentColor" 22 + aria-hidden="true" 23 + class="h-5 w-4 text-neutral-300" 24 + > 25 + <path d="M5.697 4.34L8.98 16.532h1.327L7.025 4.341H5.697z" /> 26 + </svg> 27 + </div> 28 + </li> 29 + 30 + <li class="text-sm"> 31 + <div 32 + class="font-medium text-neutral-500 dark:text-neutral-400 " 33 + >{name}</div 34 + > 35 + </li> 36 + </ol> 37 + </nav>
+22
src/lib/components/project/ProjectButtons.svelte
··· 1 + <script lang="ts"> 2 + export let codeUrl: string | undefined; 3 + export let projectUrl: string | undefined; 4 + </script> 5 + 6 + {#if codeUrl} 7 + <a 8 + href={codeUrl} 9 + target="_blank" 10 + class="flex w-full items-center justify-center rounded-xl bg-white/10 px-8 py-3 text-base font-medium text-white hover:bg-white/20" 11 + >check out the code</a 12 + > 13 + {/if} 14 + 15 + {#if projectUrl} 16 + <a 17 + href={projectUrl} 18 + target="_blank" 19 + class="flex w-full items-center justify-center rounded-xl dark:text-black bg-accent-400 px-8 py-3 text-base font-medium text-white hover:bg-accent-300" 20 + >launch project</a 21 + > 22 + {/if}
+135
src/lib/components/project/ProjectCarousel.svelte
··· 1 + <script lang="ts"> 2 + import type { Project } from '$lib/projects'; 3 + import { fade, slide } from 'svelte/transition'; 4 + import { Badge } from '../badge'; 5 + import ProjectBreadcrumb from './ProjectBreadcrumb.svelte'; 6 + import ProjectButtons from './ProjectButtons.svelte'; 7 + 8 + export let project: Project; 9 + 10 + let selectedImage = 0; 11 + </script> 12 + 13 + <div class="mx-auto max-w-2xl py-8 sm:py-24 lg:max-w-5xl"> 14 + <ProjectBreadcrumb name={project.name} /> 15 + 16 + <div class="px-4 sm:px-6 lg:px-8"> 17 + 18 + <div class="mt-6 lg:grid lg:grid-cols-2 lg:items-start lg:gap-x-8"> 19 + <!-- Image gallery --> 20 + {#if project.images} 21 + <div class="flex flex-col-reverse"> 22 + <!-- Image selector --> 23 + 24 + {#if project.images.length > 0} 25 + <div class="mx-auto mt-6 w-full max-w-2xl sm:block lg:max-w-none"> 26 + <div class="grid grid-cols-4 gap-6" aria-orientation="horizontal" role="tablist"> 27 + {#each project.images as image, index} 28 + <button 29 + class="relative flex h-24 cursor-pointer items-center justify-center rounded-xl bg-white/10 text-sm font-medium text-neutral-900 transition-all duration-200 border {index == 30 + selectedImage 31 + ? 'border-accent-400' 32 + : 'border-accent-100/20 hover:opacity-80'}" 33 + type="button" 34 + on:click={() => (selectedImage = index)} 35 + > 36 + <span class="absolute inset-0 overflow-hidden rounded-xl"> 37 + <img src={image} alt="" class="h-full w-full object-cover object-center" /> 38 + </span> 39 + <!-- Selected: "ring-indigo-500", Not Selected: "ring-transparent" --> 40 + <span class="pointer-events-none absolute inset-0 rounded-xl" aria-hidden="true" 41 + ></span> 42 + </button> 43 + 44 + <!-- More images... --> 45 + {/each} 46 + 47 + <!-- More images... --> 48 + </div> 49 + </div> 50 + {/if} 51 + 52 + <div class="aspect-h-1 aspect-w-1 w-full"> 53 + <!-- Tab panel, show/hide based on tab state. --> 54 + {#key project.images[selectedImage]} 55 + <img 56 + transition:fade 57 + src={project.images[selectedImage]} 58 + alt="Angled front view with bag zipped and handles upright." 59 + class="h-full w-full object-cover object-center sm:rounded-xl" 60 + /> 61 + 62 + {/key} 63 + <!-- More images... --> 64 + </div> 65 + </div> 66 + {/if} 67 + 68 + <!-- Product info --> 69 + <div class="mt-10 sm:mt-16 sm:px-0 lg:mt-0"> 70 + <h1 class="text-3xl font-bold tracking-tight text-neutral-900 dark:text-white"> 71 + {project.name} 72 + </h1> 73 + 74 + {#if project.tags} 75 + <!-- tags --> 76 + <div class="mt-10"> 77 + <div class="mt-4 mb-8"> 78 + <div class="flex flex-wrap gap-4"> 79 + {#each project.tags as tag} 80 + <Badge size="lg">{tag}</Badge> 81 + {/each} 82 + </div> 83 + </div> 84 + 85 + <div class="grid sm:grid-cols-2 gap-4"> 86 + <ProjectButtons codeUrl={project.codeUrl} projectUrl={project.projectUrl} /> 87 + </div> 88 + </div> 89 + {/if} 90 + 91 + <div class="mt-6"> 92 + <h3 class="sr-only">Description</h3> 93 + 94 + <div class="space-y-6 text-base text-neutral-700 dark:text-neutral-300"> 95 + <p> 96 + {project.description} 97 + </p> 98 + </div> 99 + </div> 100 + 101 + <section aria-labelledby="details-heading" class="mt-12"> 102 + <h2 id="details-heading" class="sr-only">Additional details</h2> 103 + 104 + {#if project.highlights} 105 + <div 106 + class="divide-y divide-neutral-200 dark:divide-neutral-800 border-t dark:border-neutral-800" 107 + > 108 + <div> 109 + <h3> 110 + <!-- Expand/collapse question button --> 111 + <div class="group relative flex w-full items-center justify-between py-6 text-left"> 112 + <!-- Open: "text-indigo-600", Closed: "text-neutral-900" --> 113 + <span class="text-sm font-medium text-neutral-900 dark:text-neutral-100" 114 + >highlights</span 115 + > 116 + </div> 117 + </h3> 118 + <div class="prose prose-sm pb-6 dark:prose-invert prose-neutral" id="disclosure-1"> 119 + <ul role="list"> 120 + {#each project.highlights as highlight} 121 + <li>{highlight}</li> 122 + {/each} 123 + </ul> 124 + </div> 125 + </div> 126 + 127 + <!-- More sections... --> 128 + </div> 129 + {/if} 130 + 131 + </section> 132 + </div> 133 + </div> 134 + </div> 135 + </div>
+117
src/lib/components/project/ProjectFull.svelte
··· 1 + <script lang="ts"> 2 + import type { Project } from '$lib/projects'; 3 + import Badge from '../badge/badge.svelte'; 4 + import ProjectBreadcrumb from './ProjectBreadcrumb.svelte'; 5 + import ProjectButtons from './ProjectButtons.svelte'; 6 + 7 + export let project: Project; 8 + </script> 9 + 10 + <div class=""> 11 + <div class="pt-6 sm:pt-24"> 12 + <ProjectBreadcrumb name={project.name} /> 13 + 14 + <!-- Image gallery --> 15 + {#if project.images || project.demo} 16 + <div class="mx-auto mt-6 max-w-2xl sm:px-6 lg:max-w-5xl lg:px-8 aspect-3"> 17 + {#if project.demo == 'fluid'} 18 + <iframe 19 + src="https://flo-bit.github.io/text_effect_fluid/" 20 + title="fluid text effect demo" 21 + class="w-full h-full" 22 + /> 23 + {:else if project.demo == 'mandala'} 24 + <iframe 25 + src="https://flo-bit.github.io/mandala/" 26 + title="fluid text effect demo" 27 + class="w-full h-full rounded-xl" 28 + /> 29 + {:else if project.images} 30 + <img 31 + src={project.images[0]} 32 + alt="Two each of neutral, white, and black shirts laying flat." 33 + class="h-full w-full object-cover object-center" 34 + /> 35 + {/if} 36 + </div> 37 + {/if} 38 + 39 + <!-- Product info --> 40 + <div 41 + class="mx-auto max-w-2xl px-4 pb-0 pt-10 sm:px-6 lg:grid lg:max-w-5xl lg:grid-cols-3 lg:grid-rows-[auto,auto,1fr] lg:gap-x-8 lg:px-8 lg:pb-24 lg:pt-16" 42 + > 43 + <div class="lg:col-span-2 lg:border-r lg:border-neutral-200 dark:border-neutral-800 lg:pr-8"> 44 + <h1 class="text-2xl font-bold tracking-tight text-neutral-900 dark:text-white sm:text-3xl"> 45 + {project.name} 46 + </h1> 47 + </div> 48 + 49 + <!-- Options --> 50 + <div class="mt-4 lg:row-span-3 lg:mt-0"> 51 + <h2 class="sr-only">project information</h2> 52 + 53 + <div class="mt-10"> 54 + {#if project.tags} 55 + <!-- tags --> 56 + <div class="mt-10"> 57 + <div class="mt-4"> 58 + <div class="flex flex-wrap gap-4"> 59 + {#each project.tags as tag} 60 + <Badge size="lg">{tag}</Badge> 61 + {/each} 62 + </div> 63 + </div> 64 + </div> 65 + {/if} 66 + 67 + <div class="mt-8 flex flex-col gap-4"> 68 + <ProjectButtons codeUrl={project.codeUrl} projectUrl={project.projectUrl} /> 69 + </div> 70 + </div> 71 + </div> 72 + 73 + <div 74 + class="py-10 lg:col-span-2 lg:col-start-1 lg:border-r lg:border-neutral-200 dark:border-neutral-800 lg:pb-16 lg:pr-8 lg:pt-6" 75 + > 76 + <!-- Description and details --> 77 + <div> 78 + <h3 class="sr-only">Description</h3> 79 + 80 + <div class="space-y-6"> 81 + <p class="text-base text-neutral-900 dark:text-neutral-200"> 82 + {project.description} 83 + </p> 84 + </div> 85 + </div> 86 + 87 + {#if project.highlights} 88 + <div class="mt-10"> 89 + <h3 class="text-sm font-medium text-neutral-900 dark:text-neutral-50">highlights</h3> 90 + 91 + <div class="mt-4"> 92 + <ul role="list" class="list-disc space-y-2 pl-4 text-sm"> 93 + {#each project.highlights as highlight} 94 + <li class="text-neutral-400 dark:text-neutral-600"> 95 + <span class="text-neutral-600 dark:text-neutral-300">{highlight}</span> 96 + </li> 97 + {/each} 98 + </ul> 99 + </div> 100 + </div> 101 + {/if} 102 + 103 + {#if project.details} 104 + <div class="mt-10"> 105 + <h2 class="text-sm font-medium text-neutral-900 dark:text-neutral-50">details</h2> 106 + 107 + <div class="mt-4 space-y-6"> 108 + <p class="text-sm text-neutral-600 dark:text-neutral-300 leading-6"> 109 + {project.details} 110 + </p> 111 + </div> 112 + </div> 113 + {/if} 114 + </div> 115 + </div> 116 + </div> 117 + </div>
+156
src/lib/components/project/ProjectGrid.svelte
··· 1 + <script lang="ts"> 2 + import type { Project } from '$lib/projects'; 3 + import Badge from '../badge/badge.svelte'; 4 + import ProjectBreadcrumb from './ProjectBreadcrumb.svelte'; 5 + import ProjectButtons from './ProjectButtons.svelte'; 6 + 7 + export let project: Project; 8 + </script> 9 + 10 + <div class=""> 11 + <div class="pt-6 sm:pt-24"> 12 + <ProjectBreadcrumb name={project.name} /> 13 + 14 + <!-- Image gallery --> 15 + {#if project.images} 16 + <div 17 + class="mx-auto mt-6 max-w-2xl sm:px-6 lg:grid lg:max-w-5xl lg:grid-cols-3 lg:gap-x-8 lg:px-8" 18 + > 19 + <div class="aspect-h-4 aspect-w-3 hidden overflow-hidden rounded-xl lg:block"> 20 + <img 21 + src={project.images[0]} 22 + alt="Two each of neutral, white, and black shirts laying flat." 23 + class="h-full w-full object-cover object-center" 24 + /> 25 + </div> 26 + <div class="hidden lg:grid lg:grid-cols-1 lg:gap-y-8"> 27 + <div class="aspect-h-2 aspect-w-3 overflow-hidden rounded-xl"> 28 + <img 29 + src={project.images[1]} 30 + alt="Model wearing plain black basic tee." 31 + class="h-full w-full object-cover object-center" 32 + /> 33 + </div> 34 + <div class="aspect-h-2 aspect-w-3 overflow-hidden rounded-xl"> 35 + <img 36 + src={project.images[2]} 37 + alt="Model wearing plain neutral basic tee." 38 + class="h-full w-full object-cover object-center" 39 + /> 40 + </div> 41 + </div> 42 + <div 43 + class="aspect-h-5 aspect-w-4 lg:aspect-h-4 lg:aspect-w-3 sm:overflow-hidden sm:rounded-xl" 44 + > 45 + <img 46 + src={project.images[3]} 47 + alt="Model wearing plain white basic tee." 48 + class="h-full w-full object-cover object-center" 49 + /> 50 + </div> 51 + </div> 52 + {/if} 53 + 54 + <!-- Product info --> 55 + <div 56 + class="mx-auto max-w-2xl px-4 pb-0 pt-10 sm:px-6 lg:grid lg:max-w-5xl lg:grid-cols-3 lg:grid-rows-[auto,auto,1fr] lg:gap-x-8 lg:px-8 lg:pb-24 lg:pt-16" 57 + > 58 + <div class="lg:col-span-2 lg:border-r lg:border-neutral-200 dark:border-neutral-800 lg:pr-8"> 59 + <h1 class="text-2xl font-bold tracking-tight text-neutral-900 dark:text-white sm:text-3xl"> 60 + {project.name} 61 + </h1> 62 + </div> 63 + 64 + <!-- Options --> 65 + <div class="mt-4 lg:row-span-3 lg:mt-0"> 66 + <h2 class="sr-only">project information</h2> 67 + 68 + <div class="mt-0"> 69 + {#if project.tags} 70 + <!-- tags --> 71 + <div class="mt-0 sm:mt-4"> 72 + <div class="flex flex-wrap gap-4"> 73 + {#each project.tags as tag} 74 + <Badge size="lg">{tag}</Badge> 75 + {/each} 76 + </div> 77 + </div> 78 + {/if} 79 + 80 + <div class="mt-8 flex flex-col gap-4"> 81 + <ProjectButtons codeUrl={project.codeUrl} projectUrl={project.projectUrl} /> 82 + </div> 83 + </div> 84 + </div> 85 + 86 + <div 87 + class="py-10 lg:col-span-2 lg:col-start-1 lg:border-r lg:border-neutral-200 dark:border-neutral-800 lg:pb-16 lg:pr-8 lg:pt-6" 88 + > 89 + <!-- Description and details --> 90 + <div> 91 + <h3 class="sr-only">Description</h3> 92 + 93 + <div class="space-y-6"> 94 + <p class="text-base text-neutral-900 dark:text-neutral-200"> 95 + {project.description} 96 + </p> 97 + </div> 98 + </div> 99 + 100 + {#if project.highlights} 101 + <div class="mt-10"> 102 + <h3 class="text-sm font-medium text-neutral-900 dark:text-neutral-50">highlights</h3> 103 + 104 + <div class="mt-4"> 105 + <ul role="list" class="list-disc space-y-2 pl-4 text-sm"> 106 + {#each project.highlights as highlight} 107 + <li class="text-neutral-400 dark:text-neutral-600"> 108 + <span class="text-neutral-600 dark:text-neutral-300">{highlight}</span> 109 + </li> 110 + {/each} 111 + </ul> 112 + </div> 113 + </div> 114 + {/if} 115 + 116 + {#if project.details} 117 + <div class="mt-10"> 118 + <h2 class="text-sm font-medium text-neutral-900 dark:text-neutral-50">details</h2> 119 + 120 + <div class="mt-4 space-y-6"> 121 + <p class="text-sm text-neutral-600 dark:text-neutral-300 leading-6"> 122 + {project.details} 123 + </p> 124 + </div> 125 + </div> 126 + {/if} 127 + </div> 128 + </div> 129 + 130 + {#if project.images} 131 + <div class="mx-auto mt-6 max-w-2xl sm:px-6 lg:hidden pb-24"> 132 + <div class="aspect-h-4 aspect-w-3 overflow-hidden sm:rounded-xl"> 133 + <img 134 + src={project.images[0]} 135 + alt="Two each of neutral, white, and black shirts laying flat." 136 + class="h-full w-full object-cover object-center" 137 + /> 138 + </div> 139 + <div class="aspect-h-2 aspect-w-3 overflow-hidden sm:rounded-xl mt-4"> 140 + <img 141 + src={project.images[1]} 142 + alt="Model wearing plain black basic tee." 143 + class="h-full w-full object-cover object-center" 144 + /> 145 + </div> 146 + <div class="aspect-h-2 aspect-w-3 overflow-hidden sm:rounded-xl mt-4"> 147 + <img 148 + src={project.images[2]} 149 + alt="Model wearing plain neutral basic tee." 150 + class="h-full w-full object-cover object-center" 151 + /> 152 + </div> 153 + </div> 154 + {/if} 155 + </div> 156 + </div>
+115
src/lib/components/project/ProjectTiered.svelte
··· 1 + <script lang="ts"> 2 + import type { Project } from '$lib/projects'; 3 + import Badge from '../badge/badge.svelte'; 4 + import Swiper from './demos/swiper.svelte'; 5 + import ProjectBreadcrumb from './ProjectBreadcrumb.svelte'; 6 + import ProjectButtons from './ProjectButtons.svelte'; 7 + 8 + export let project: Project; 9 + </script> 10 + 11 + <div class=""> 12 + <div class="pb-16 pt-6 sm:pt-24 sm:pb-24"> 13 + <ProjectBreadcrumb name={project.name} /> 14 + 15 + <div class="mx-auto mt-8 max-w-2xl px-4 sm:px-6 lg:max-w-5xl lg:px-8"> 16 + <div class="lg:grid lg:auto-rows-min lg:grid-cols-12 lg:gap-x-8"> 17 + <div class="lg:col-span-5 lg:col-start-8"> 18 + <div class="flex justify-between"> 19 + <h1 class="text-3xl font-bold text-neutral-900 dark:text-white">{project.name}</h1> 20 + </div> 21 + </div> 22 + 23 + <!-- Image gallery --> 24 + {#if project.images || project.demo} 25 + <div class="mt-8 lg:col-span-7 lg:col-start-1 lg:row-span-3 lg:row-start-1 lg:mt-0"> 26 + <h2 class="sr-only">Images</h2> 27 + 28 + <div 29 + class="grid grid-cols-1 overflow-hidden rounded-xl lg:grid-cols-2 {project.images && project.images 30 + .length > 1 31 + ? 'lg:grid-rows-3' 32 + : ''} lg:gap-8" 33 + > 34 + {#if project.demo == 'swiper'} 35 + <div class="rounded-xl h-[60vh] md:h-[70vh] lg:col-span-2 lg:row-span-2"> 36 + <Swiper /> 37 + </div> 38 + {:else if project.demo == 'autostereogram'} 39 + <div class="rounded-xl h-[60vh] md:h-[70vh] lg:col-span-2 lg:row-span-2"> 40 + <iframe 41 + src={project.projectUrl} 42 + title="autostereogram demo" 43 + class="w-full h-full" 44 + /> 45 + </div> 46 + {:else if project.images} 47 + <img 48 + src={project.images[0]} 49 + alt="Back of women&#039;s Basic Tee in black." 50 + class="rounded-xl lg:col-span-2 lg:row-span-2" 51 + /> 52 + {/if} 53 + {#if project.images && project.images.length > 1} 54 + <img 55 + src={project.images[1]} 56 + alt="Side profile of women&#039;s Basic Tee in black." 57 + class="hidden rounded-xl lg:block" 58 + /> 59 + {/if} 60 + {#if project.images && project.images.length > 2} 61 + <img 62 + src={project.images[2]} 63 + alt="Front of women&#039;s Basic Tee in black." 64 + class="hidden rounded-xl lg:block" 65 + /> 66 + {/if} 67 + </div> 68 + </div> 69 + {/if} 70 + 71 + <div class="mt-8 lg:col-span-5"> 72 + <div class="flex flex-col gap-4"> 73 + <ProjectButtons codeUrl={project.codeUrl} projectUrl={project.projectUrl} /> 74 + </div> 75 + 76 + <!-- Product details --> 77 + <div class="mt-10"> 78 + <h2 class="text-sm font-medium text-neutral-900 dark:text-neutral-100">Description</h2> 79 + 80 + <div class="prose prose-sm mt-4 text-neutral-500 dark:text-neutral-400"> 81 + <p>{project.description}</p> 82 + </div> 83 + </div> 84 + 85 + <div class="mt-8 border-t border-neutral-200 dark:border-neutral-800 pt-8"> 86 + <h2 class="text-sm font-medium text-neutral-900 dark:text-neutral-100">highlights</h2> 87 + 88 + {#if project.highlights} 89 + <div class="prose prose-sm mt-4 text-neutral-500 dark:text-neutral-400"> 90 + <ul role="list"> 91 + {#each project.highlights as highlight} 92 + <li>{highlight}</li> 93 + {/each} 94 + </ul> 95 + </div> 96 + {/if} 97 + </div> 98 + 99 + {#if project.tags} 100 + <!-- tags --> 101 + <div class="mt-10"> 102 + <div class="mt-4"> 103 + <div class="flex flex-wrap gap-4"> 104 + {#each project.tags as tag} 105 + <Badge size="lg">{tag}</Badge> 106 + {/each} 107 + </div> 108 + </div> 109 + </div> 110 + {/if} 111 + </div> 112 + </div> 113 + </div> 114 + </div> 115 + </div>
+47
src/lib/components/project/ProjectV3.svelte
··· 1 + <script lang="ts"> 2 + import { type Project } from '$lib/projects'; 3 + import ProjectBreadcrumb from './ProjectBreadcrumb.svelte'; 4 + import ProjectButtons from './ProjectButtons.svelte'; 5 + 6 + export let project: Project; 7 + </script> 8 + 9 + <div class=""> 10 + <div 11 + class="mx-auto max-w-2xl px-4 py-16 sm:px-6 sm:py-24 lg:grid lg:max-w-5xl lg:grid-cols-2 lg:gap-x-8 lg:px-8" 12 + > 13 + <!-- Product details --> 14 + <div class="lg:max-w-lg lg:self-end"> 15 + <ProjectBreadcrumb name={project.name} /> 16 + 17 + <div class="mt-4"> 18 + <h1 class="text-3xl font-bold tracking-tight text-white sm:text-4xl">{project.name}</h1> 19 + </div> 20 + 21 + <section aria-labelledby="information-heading" class="mt-4"> 22 + <h2 id="information-heading" class="sr-only">project information</h2> 23 + 24 + <div class="mt-4 space-y-6"> 25 + <p class="text-base text-neutral-400">{project.description}</p> 26 + </div> 27 + 28 + <div class="mt-8 grid sm:grid-cols-2 gap-4"> 29 + <ProjectButtons codeUrl={project.codeUrl} projectUrl={project.projectUrl} /> 30 + </div> 31 + </section> 32 + </div> 33 + 34 + {#if project.images} 35 + <!-- Product image --> 36 + <div class="mt-10 lg:col-start-2 lg:row-span-2 lg:mt-0 lg:self-center"> 37 + <div class="aspect-h-1 aspect-w-1 overflow-hidden rounded-lg"> 38 + <img 39 + src={project.images[0]} 40 + alt="Model wearing light green backpack with black canvas straps and front zipper pouch." 41 + class="h-full w-full object-cover object-center" 42 + /> 43 + </div> 44 + </div> 45 + {/if} 46 + </div> 47 + </div>
+13 -9
src/routes/projects/[key]/+page.svelte
··· 1 1 <script lang="ts"> 2 - import Navbar from '$lib/components/nav/Navbar.svelte'; 3 - import ProjectV1 from '$lib/components/ProjectV1.svelte'; 4 - import ProjectV2 from '$lib/components/ProjectV2.svelte'; 5 - import ProjectV3 from '$lib/components/ProjectV3.svelte'; 6 2 import type { PageData } from './$types'; 3 + 4 + import Navbar from '$lib/components/nav/Navbar.svelte'; 5 + import ProjectTiered from '$lib/components/project/ProjectTiered.svelte'; 6 + import ProjectCarousel from '$lib/components/project/ProjectCarousel.svelte'; 7 + import ProjectGrid from '$lib/components/project/ProjectGrid.svelte'; 8 + import ProjectFull from '$lib/components/project/ProjectFull.svelte'; 7 9 8 10 export let data: PageData; 9 11 </script> ··· 11 13 <Navbar active="projects" /> 12 14 13 15 {#if data.project} 14 - {#if data.project.projectPageVersion === 'v2'} 15 - <ProjectV2 project={data.project} /> 16 - {:else if data.project.projectPageVersion === 'v3'} 17 - <ProjectV3 project={data.project} /> 16 + {#if data.project.projectPageVersion === 'carousel'} 17 + <ProjectCarousel project={data.project} /> 18 + {:else if data.project.projectPageVersion === 'tiered'} 19 + <ProjectTiered project={data.project} /> 20 + {:else if data.project.projectPageVersion === 'full'} 21 + <ProjectFull project={data.project} /> 18 22 {:else} 19 - <ProjectV1 project={data.project} /> 23 + <ProjectGrid project={data.project} /> 20 24 {/if} 21 25 {/if}
+1 -1
src/routes/projects/[key]/+page.ts
··· 1 - import { projects } from '../../../projects.js'; 1 + import { projects } from '$lib/projects.js'; 2 2 3 3 export const load = ({ params }) => { 4 4 const project = projects.find((project) => project.key === params.key);
+60
src/lib/components/project/demos/swiper.svelte
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import { CardSwiper } from './CardSwiper'; 4 + 5 + //@ts-expect-error - not found 6 + import person1 from './CardSwiper/images/1.jpg?format=webp&w=512'; 7 + //@ts-expect-error - not found 8 + import person2 from './CardSwiper/images/2.jpg?format=webp&w=512'; 9 + //@ts-expect-error - not found 10 + import person3 from './CardSwiper/images/3.jpg?format=webp&w=512'; 11 + //@ts-expect-error - not found 12 + import person4 from './CardSwiper/images/4.jpg?format=webp&w=512'; 13 + //@ts-expect-error - not found 14 + import person5 from './CardSwiper/images/5.jpg?format=webp&w=512'; 15 + //@ts-expect-error - not found 16 + import person6 from './CardSwiper/images/6.jpg?format=webp&w=512'; 17 + //@ts-expect-error - not found 18 + import person7 from './CardSwiper/images/7.jpg?format=webp&w=512'; 19 + //@ts-expect-error - not found 20 + import person8 from './CardSwiper/images/8.jpg?format=webp&w=512'; 21 + //@ts-expect-error - not found 22 + import person9 from './CardSwiper/images/9.jpg?format=webp&w=512'; 23 + 24 + let people = [person1, person2, person3, person4, person5, person6, person7, person8, person9]; 25 + 26 + function getRandomPerson(): { firstName: string; lastName: string; age: number } { 27 + const firstNames = ['John', 'Jane', 'Alice', 'Bob', 'Mary', 'Tom']; 28 + const lastNames = ['Smith', 'Doe', 'Johnson', 'White', 'Brown', 'Davis']; 29 + 30 + const firstNameIndex = Math.floor(Math.random() * firstNames.length); 31 + const lastNameIndex = Math.floor(Math.random() * lastNames.length); 32 + 33 + const age = Math.floor(Math.random() * (60 - 18 + 1)) + 18; 34 + 35 + return { 36 + firstName: firstNames[firstNameIndex], 37 + lastName: lastNames[lastNameIndex], 38 + age: age 39 + }; 40 + } 41 + 42 + 43 + let data = (index: number) => { 44 + let person = getRandomPerson(); 45 + return { 46 + title: `${person.firstName} ${person.lastName}, ${person.age}`, 47 + description: 'swipe left or right', 48 + image: people[index % people.length] 49 + }; 50 + }; 51 + 52 + onMount(() => { 53 + // preload images 54 + people.forEach((person) => { 55 + new Image().src = person; 56 + }); 57 + }); 58 + </script> 59 + 60 + <CardSwiper cardData={data} />
src/lib/images/projects-new/shmup/image1.png

This is a binary file and will not be displayed.

src/lib/images/projects-new/shmup/image2.png

This is a binary file and will not be displayed.

src/lib/images/projects-new/shmup/image3.png

This is a binary file and will not be displayed.

src/lib/images/projects-new/shmup/image4.png

This is a binary file and will not be displayed.

+28
src/lib/components/project/demos/CardSwiper/Card.svelte
··· 1 + <script lang="ts"> 2 + import { fade } from "svelte/transition"; 3 + 4 + export let element: HTMLElement; 5 + 6 + export let color: string = ''; 7 + export let title: string = ''; 8 + export let description: string = ''; 9 + export let image: string | undefined = undefined; 10 + </script> 11 + 12 + <div 13 + class="w-full h-full absolute cursor-grab ease-in-out rounded-xl touch-none select-none border border-white/20 bg-black {color}" 14 + bind:this={element} 15 + > 16 + {#key image} 17 + {#if image} 18 + <img class="w-full h-full rounded-xl object-cover" src={image} alt={title} /> 19 + {/if} 20 + {/key} 21 + <div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/0 rounded-b-xl"></div> 22 + <div class="p-4 absolute bottom-0 w-full flex justify-center text-white"> 23 + <div class="flex items-center flex-col"> 24 + <h3 class="text-3xl font-semibold pb-4">{title}</h3> 25 + <p>{description}</p> 26 + </div> 27 + </div> 28 + </div>
+159
src/lib/components/project/demos/CardSwiper/CardSwiper.svelte
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import { DragGesture, type FullGestureState } from '@use-gesture/vanilla'; 4 + import type { CardData, Direction } from '.'; 5 + import Card from './Card.svelte'; 6 + 7 + import { createEventDispatcher } from 'svelte'; 8 + 9 + const dispatch = createEventDispatcher(); 10 + 11 + let container: HTMLElement; 12 + 13 + let card1: HTMLElement, card2: HTMLElement; 14 + let card1Data: CardData, card2Data: CardData; 15 + 16 + let cardIndex = 0; 17 + let topCard: HTMLElement; 18 + let currentZ = 100000; 19 + 20 + onMount(async () => { 21 + card1Data = cardData(cardIndex++); 22 + card2Data = cardData(cardIndex++); 23 + 24 + [card1, card2].forEach(function (el) { 25 + el.style.zIndex = currentZ.toString(); 26 + currentZ--; 27 + 28 + new DragGesture(el, (state) => { 29 + onDrag(el, state); 30 + }); 31 + }); 32 + 33 + topCard = card1; 34 + container.classList.remove('hidden'); 35 + }); 36 + 37 + const cardSwiped = (el: HTMLElement, velocity: [number, number], movement: [number, number]) => { 38 + // move card out of the view 39 + el.classList.add('transition-transform', 'duration-300'); 40 + 41 + let direction: Direction = movement[0] > 0 ? 'right' : 'left'; 42 + let data = el === card1 ? card1Data : card2Data; 43 + dispatch('swiped', { direction, element: el, data, index: cardIndex - 2 }); 44 + thresholdPassed = movement[0] > 0 ? 1 : -1; 45 + 46 + let moveOutWidth = document.body.clientWidth; 47 + 48 + let endX = Math.max(Math.abs(velocity[0]) * moveOutWidth, moveOutWidth); 49 + let toX = movement[0] > 0 ? endX : -endX; 50 + let endY = Math.abs(velocity[1]) * moveOutWidth; 51 + let toY = movement[1] > 0 ? endY : -endY; 52 + 53 + let rotate = movement[0] * 0.03 * (movement[1] / 80); 54 + 55 + el.style.transform = `translate(${toX}px, ${toY + movement[1]}px) rotate(${rotate}deg)`; 56 + 57 + setTimeout(() => { 58 + thresholdPassed = 0; 59 + 60 + // move card back to start position at bottom of stack and update data 61 + if (el === card1) { 62 + card1Data = {}; 63 + card1Data = cardData(cardIndex++); 64 + topCard = card2; 65 + } else { 66 + card2Data = {}; 67 + card2Data = cardData(cardIndex++); 68 + topCard = card1; 69 + } 70 + 71 + currentZ--; 72 + el.style.zIndex = currentZ.toString(); 73 + 74 + el.classList.remove('transition-transform', 'duration-300'); 75 + el.style.transform = ''; 76 + }, 350); 77 + }; 78 + 79 + const onDrag = ( 80 + el: HTMLElement, 81 + state: Omit<FullGestureState<'drag'>, 'event'> & { 82 + event: PointerEvent | MouseEvent | TouchEvent | KeyboardEvent; 83 + } 84 + ) => { 85 + let elWidth = el.offsetWidth; 86 + 87 + if (state.pressed) { 88 + let rotate = state.movement[0] * 0.03 * (state.movement[1] / 80); 89 + 90 + // fix movement on a curved path if anchor is set 91 + if (anchor) { 92 + let vec = [state.movement[0], state.movement[1] - anchor]; 93 + let len = Math.sqrt(vec[0] ** 2 + vec[1] ** 2); 94 + vec = [vec[0] / len * anchor, vec[1] / len * anchor]; 95 + 96 + state.movement[0] = vec[0]; 97 + state.movement[1] = vec[1] + anchor; 98 + } 99 + 100 + el.style.transform = `translate(${state.movement[0]}px, ${state.movement[1]}px) rotate(${rotate}deg)`; 101 + 102 + if(Math.abs(state.movement[0]) / elWidth > minSwipeDistance) { 103 + thresholdPassed = state.movement[0] > 0 ? 1 : -1; 104 + } else { 105 + thresholdPassed = 0; 106 + } 107 + return; 108 + } 109 + // if dragging is finished 110 + let keep = 111 + Math.abs(state.movement[0]) / elWidth < minSwipeDistance && 112 + Math.abs(state.velocity[0]) < minSwipeVelocity; 113 + 114 + if (keep) { 115 + thresholdPassed = 0; 116 + el.classList.add('transition-transform', 'duration-300'); 117 + el.style.transform = ''; 118 + setTimeout(() => { 119 + el.classList.remove('transition-transform', 'duration-300'); 120 + }, 300); 121 + } else { 122 + cardSwiped(el, state.velocity, state.movement); 123 + } 124 + }; 125 + 126 + export const swipe = (direction: Direction = 'right') => { 127 + if(thresholdPassed !== 0) return; 128 + 129 + let dir = direction === 'left' ? -1 : 1; 130 + cardSwiped(topCard, [dir, 0.1], [dir, 1]); 131 + }; 132 + 133 + export let cardData: (index: number) => CardData; 134 + 135 + export let minSwipeDistance: number = 0.5; 136 + export let minSwipeVelocity: number = 0.5; 137 + 138 + export let arrowKeys = true; 139 + 140 + export let thresholdPassed = 0; 141 + 142 + export let anchor: number | null = null; 143 + </script> 144 + 145 + <svelte:body on:keydown={(e) => { 146 + if(!arrowKeys) return; 147 + if (e.key === 'ArrowLeft') { 148 + swipe('left'); 149 + } else if (e.key === 'ArrowRight') { 150 + swipe('right'); 151 + } 152 + }} /> 153 + 154 + <div class="w-full h-full"> 155 + <div class="w-full h-full relative hidden z-0" bind:this={container}> 156 + <svelte:component this={Card} bind:element={card1} {...card1Data} /> 157 + <svelte:component this={Card} bind:element={card2} {...card2Data} /> 158 + </div> 159 + </div>
+10
src/lib/components/project/demos/CardSwiper/index.ts
··· 1 + export type CardData = { 2 + title?: string; 3 + color?: string; 4 + description?: string; 5 + image?: string; 6 + }; 7 + 8 + export type Direction = 'left' | 'right'; 9 + 10 + export { default as CardSwiper } from './CardSwiper.svelte';
src/lib/components/project/demos/CardSwiper/images/1.jpg

This is a binary file and will not be displayed.

src/lib/components/project/demos/CardSwiper/images/2.jpg

This is a binary file and will not be displayed.

src/lib/components/project/demos/CardSwiper/images/3.jpg

This is a binary file and will not be displayed.

src/lib/components/project/demos/CardSwiper/images/4.jpg

This is a binary file and will not be displayed.

src/lib/components/project/demos/CardSwiper/images/5.jpg

This is a binary file and will not be displayed.

src/lib/components/project/demos/CardSwiper/images/6.jpg

This is a binary file and will not be displayed.

src/lib/components/project/demos/CardSwiper/images/7.jpg

This is a binary file and will not be displayed.

src/lib/components/project/demos/CardSwiper/images/8.jpg

This is a binary file and will not be displayed.

src/lib/components/project/demos/CardSwiper/images/9.jpg

This is a binary file and will not be displayed.