├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── css └── base.css ├── dist ├── 1.d5d0bbe2.jpg ├── 2.b0539b47.jpg ├── 3.caf4bd5d.jpg ├── 4.563dcc99.jpg ├── 5.1c2ed3fb.jpg ├── 6.df718cc7.jpg ├── 7.fd69eac9.jpg ├── 8.e174b0a5.jpg ├── base.0b18b771.css ├── big1.0ab44b73.jpg ├── big2.839b2789.jpg ├── favicon.2e5f6236.ico ├── index.html └── js.c39dff1f.js ├── favicon.ico ├── img ├── 1.jpg ├── 2.jpg ├── 3.jpg ├── 4.jpg ├── 5.jpg ├── 6.jpg ├── 7.jpg ├── 8.jpg ├── big1.jpg └── big2.jpg ├── index.html ├── js ├── index.js ├── mathUtils.js ├── textOnPath.js └── winsize.js └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .cache 3 | .DS_Store 4 | package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2009 - 2020 [Codrops](https://tympanus.net/codrops) 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Animating SVG Text on a Path 2 | 3 | A demo where we are animating SVG text on a path on scroll using the Intersection Observer API and SVG filters. 4 | 5 | ![Animating SVG Text on a Path](https://tympanus.net/codrops/wp-content/uploads/2020/02/TextPath_featured.jpg) 6 | 7 | [Article on Codrops](https://tympanus.net/codrops/?p=47831) 8 | 9 | [Demo](http://tympanus.net/Development/AnimateSVGTextPath/) 10 | 11 | 12 | ## Installation 13 | 14 | Install dependencies: 15 | 16 | ``` 17 | npm install 18 | ``` 19 | 20 | Compile the code for development and start a local server: 21 | 22 | ``` 23 | npm start 24 | ``` 25 | 26 | Create the build: 27 | 28 | ``` 29 | npm run build 30 | ``` 31 | 32 | ## Credits 33 | 34 | - [imagesLoaded](https://imagesloaded.desandro.com/) by Dave DeSandro 35 | - Images from [Unsplash.com](https://unsplash.com/) 36 | 37 | ## Misc 38 | 39 | Follow Codrops: [Twitter](http://www.twitter.com/codrops), [Facebook](http://www.facebook.com/codrops), [GitHub](https://github.com/codrops), [Instagram](https://www.instagram.com/codropsss/) 40 | 41 | ## License 42 | [MIT](LICENSE) 43 | 44 | Made with :blue_heart: by [Codrops](http://www.codrops.com) 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /css/base.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::after, 3 | *::before { 4 | box-sizing: border-box; 5 | } 6 | 7 | :root { 8 | font-size: 17px; 9 | } 10 | 11 | body { 12 | margin: 0; 13 | --color-text: #fff; 14 | --color-bg: #0a0104; 15 | --color-link: #5c5c5c; 16 | --color-link-hover: #fff; 17 | --color-description: #504f4f; 18 | color: var(--color-text); 19 | background-color: var(--color-bg); 20 | font-family: poynter-oldstyle-display-con, serif; 21 | -webkit-font-smoothing: antialiased; 22 | -moz-osx-font-smoothing: grayscale; 23 | } 24 | 25 | main { 26 | width: 100%; 27 | overflow: hidden; 28 | position: relative; 29 | } 30 | 31 | /* Page Loader */ 32 | .js .loading::before, 33 | .js .loading::after { 34 | content: ''; 35 | position: fixed; 36 | z-index: 1000; 37 | } 38 | 39 | .js .loading::before { 40 | top: 0; 41 | left: 0; 42 | width: 100%; 43 | height: 100%; 44 | background: var(--color-bg); 45 | } 46 | 47 | .js .loading::after { 48 | top: 50%; 49 | left: 50%; 50 | width: 60px; 51 | height: 60px; 52 | margin: -30px 0 0 -30px; 53 | border-radius: 50%; 54 | opacity: 0.4; 55 | background: var(--color-link); 56 | animation: loaderAnim 0.7s linear infinite alternate forwards; 57 | 58 | } 59 | 60 | @keyframes loaderAnim { 61 | to { 62 | opacity: 1; 63 | transform: scale3d(0.5,0.5,1); 64 | } 65 | } 66 | 67 | a { 68 | text-decoration: none; 69 | color: var(--color-link); 70 | outline: none; 71 | } 72 | 73 | a:hover, 74 | a:focus { 75 | color: var(--color-link-hover); 76 | outline: none; 77 | } 78 | 79 | .hidden { 80 | position: absolute; 81 | pointer-events: none; 82 | width: 0; 83 | height: 0; 84 | overflow: hidden; 85 | } 86 | 87 | .frame { 88 | padding: 3rem 5vw; 89 | text-align: center; 90 | position: relative; 91 | z-index: 1000; 92 | text-transform: uppercase; 93 | } 94 | 95 | .frame__title { 96 | font-size: 1rem; 97 | margin: 0 0 1rem; 98 | font-weight: normal; 99 | } 100 | 101 | .frame__links { 102 | display: inline; 103 | } 104 | 105 | .frame__links a:not(:last-child) { 106 | margin-right: 1rem; 107 | } 108 | 109 | .frame__heading { 110 | margin: 1rem 0; 111 | font-size: 1rem; 112 | font-weight: 400; 113 | } 114 | 115 | .frame__counter { 116 | margin: 2rem 0; 117 | align-items: baseline; 118 | line-height: 0.8rem; 119 | text-align: center; 120 | } 121 | 122 | .frame__counter span { 123 | margin: 0 0.25rem; 124 | } 125 | 126 | .frame__counter-text:nth-child(2) { 127 | margin: 0 1.5rem 0 0; 128 | } 129 | 130 | .frame__counter-number { 131 | font-size: 200%; 132 | } 133 | 134 | .intro { 135 | pointer-events: none; 136 | display: flex; 137 | align-items: center; 138 | justify-content: center; 139 | flex-direction: column; 140 | } 141 | 142 | .intro__title { 143 | font-size: 19vw; 144 | margin: 0; 145 | font-weight: 400; 146 | line-height: 1; 147 | } 148 | 149 | .intro__hint { 150 | position: relative; 151 | text-transform: uppercase; 152 | margin: 8vh 0 0 0; 153 | } 154 | 155 | .intro__hint::after { 156 | content: ''; 157 | position: absolute; 158 | width: 1px; 159 | height: 2rem; 160 | top: calc(100% + 2rem); 161 | left: 50%; 162 | background-color: currentColor; 163 | } 164 | 165 | .grid-wrap { 166 | position: relative; 167 | } 168 | 169 | .grid { 170 | display: grid; 171 | grid-template-columns: repeat(auto-fit, minmax(200px, calc(390px + 3rem))); 172 | justify-content: center; 173 | grid-gap: 10vw; 174 | margin: 15rem auto; 175 | } 176 | 177 | .grid__item { 178 | padding: 1.5rem; 179 | } 180 | 181 | .grid__item-number { 182 | display: block; 183 | text-align: right; 184 | font-size: 3rem; 185 | line-height: 1; 186 | } 187 | 188 | .grid__item-img { 189 | margin: 1rem 0 1.75rem; 190 | max-width: 100%; 191 | display: block; 192 | } 193 | 194 | .grid__item-title { 195 | font-size: 1.25rem; 196 | text-transform: uppercase; 197 | font-weight: 400; 198 | margin: 0 0 2.75rem 0; 199 | } 200 | 201 | .grid__item-description { 202 | color: var(--color-description); 203 | font-family: news-gothic-std, sans-serif; 204 | line-height: 1.5; 205 | padding-right: 1rem; 206 | } 207 | 208 | .bigimg { 209 | display: block; 210 | width: 100%; 211 | max-width: calc(1025px - 3rem); 212 | margin: 25vh auto; 213 | } 214 | 215 | .svgtext { 216 | flex: none; 217 | position: relative; 218 | left: -10%; 219 | } 220 | 221 | .svgtext text { 222 | fill: #fff; 223 | font-size: 42px; 224 | } 225 | 226 | .svgtext--1 text { 227 | fill: #fff; 228 | } 229 | 230 | .svgtext--2 text { 231 | fill: #8569c2; 232 | } 233 | 234 | .svgtext--3 text { 235 | font-size: 32px; 236 | } 237 | 238 | .svgtext--4 { 239 | position: absolute; 240 | } 241 | 242 | .svgtext--4 text { 243 | font-size: 48px; 244 | fill: #f9e9a4; 245 | } 246 | 247 | @media screen and (min-width: 53em) { 248 | .frame--screen { 249 | position: absolute; 250 | text-align: left; 251 | z-index: 100; 252 | top: 0; 253 | left: 0; 254 | display: grid; 255 | align-content: space-between; 256 | width: 100%; 257 | max-width: none; 258 | height: 100vh; 259 | padding: 2.25rem 2.5rem; 260 | pointer-events: none; 261 | grid-template-columns: 30% 40% 30%; 262 | grid-template-rows: auto auto auto; 263 | grid-template-areas: 'heading counter links' 264 | '... ... ...' 265 | 'title title ...'; 266 | } 267 | .frame__title-wrap { 268 | grid-area: title; 269 | display: flex; 270 | } 271 | .frame__title { 272 | margin: 0 4rem 0 0; 273 | } 274 | .frame__counter { 275 | grid-area: counter; 276 | justify-self: center; 277 | } 278 | .frame__heading { 279 | margin: 0; 280 | grid-area: heading; 281 | } 282 | .frame__demos { 283 | margin: 0; 284 | grid-area: demos; 285 | justify-self: end; 286 | } 287 | .frame__links { 288 | padding: 0; 289 | justify-self: end; 290 | } 291 | .frame__links--header { 292 | grid-area: links; 293 | } 294 | .frame a { 295 | pointer-events: auto; 296 | } 297 | .frame__counter { 298 | display: flex; 299 | margin: 0; 300 | } 301 | .intro { 302 | min-height: 100vh; 303 | } 304 | .grid__item:nth-child(even) { 305 | margin-top: 35vh; 306 | text-align: right; 307 | } 308 | .grid__item:nth-child(even) .grid__item-description { 309 | padding: 0 0 0 1rem; 310 | } 311 | .grid__item-number { 312 | font-size: 4.75rem; 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /dist/1.d5d0bbe2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/984c2f6812d93c2bae0717b7098e4d801a260a55/dist/1.d5d0bbe2.jpg -------------------------------------------------------------------------------- /dist/2.b0539b47.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/984c2f6812d93c2bae0717b7098e4d801a260a55/dist/2.b0539b47.jpg -------------------------------------------------------------------------------- /dist/3.caf4bd5d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/984c2f6812d93c2bae0717b7098e4d801a260a55/dist/3.caf4bd5d.jpg -------------------------------------------------------------------------------- /dist/4.563dcc99.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/984c2f6812d93c2bae0717b7098e4d801a260a55/dist/4.563dcc99.jpg -------------------------------------------------------------------------------- /dist/5.1c2ed3fb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/984c2f6812d93c2bae0717b7098e4d801a260a55/dist/5.1c2ed3fb.jpg -------------------------------------------------------------------------------- /dist/6.df718cc7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/984c2f6812d93c2bae0717b7098e4d801a260a55/dist/6.df718cc7.jpg -------------------------------------------------------------------------------- /dist/7.fd69eac9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/984c2f6812d93c2bae0717b7098e4d801a260a55/dist/7.fd69eac9.jpg -------------------------------------------------------------------------------- /dist/8.e174b0a5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/984c2f6812d93c2bae0717b7098e4d801a260a55/dist/8.e174b0a5.jpg -------------------------------------------------------------------------------- /dist/base.0b18b771.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::after, 3 | *::before { 4 | box-sizing: border-box; 5 | } 6 | 7 | :root { 8 | font-size: 17px; 9 | } 10 | 11 | body { 12 | margin: 0; 13 | --color-text: #fff; 14 | --color-bg: #0a0104; 15 | --color-link: #5c5c5c; 16 | --color-link-hover: #fff; 17 | --color-description: #504f4f; 18 | color: var(--color-text); 19 | background-color: var(--color-bg); 20 | font-family: poynter-oldstyle-display-con, serif; 21 | -webkit-font-smoothing: antialiased; 22 | -moz-osx-font-smoothing: grayscale; 23 | } 24 | 25 | main { 26 | width: 100%; 27 | overflow: hidden; 28 | position: relative; 29 | } 30 | 31 | /* Page Loader */ 32 | .js .loading::before, 33 | .js .loading::after { 34 | content: ''; 35 | position: fixed; 36 | z-index: 1000; 37 | } 38 | 39 | .js .loading::before { 40 | top: 0; 41 | left: 0; 42 | width: 100%; 43 | height: 100%; 44 | background: var(--color-bg); 45 | } 46 | 47 | .js .loading::after { 48 | top: 50%; 49 | left: 50%; 50 | width: 60px; 51 | height: 60px; 52 | margin: -30px 0 0 -30px; 53 | border-radius: 50%; 54 | opacity: 0.4; 55 | background: var(--color-link); 56 | animation: loaderAnim 0.7s linear infinite alternate forwards; 57 | 58 | } 59 | 60 | @keyframes loaderAnim { 61 | to { 62 | opacity: 1; 63 | transform: scale3d(0.5,0.5,1); 64 | } 65 | } 66 | 67 | a { 68 | text-decoration: none; 69 | color: var(--color-link); 70 | outline: none; 71 | } 72 | 73 | a:hover, 74 | a:focus { 75 | color: var(--color-link-hover); 76 | outline: none; 77 | } 78 | 79 | .hidden { 80 | position: absolute; 81 | pointer-events: none; 82 | width: 0; 83 | height: 0; 84 | overflow: hidden; 85 | } 86 | 87 | .frame { 88 | padding: 3rem 5vw; 89 | text-align: center; 90 | position: relative; 91 | z-index: 1000; 92 | text-transform: uppercase; 93 | } 94 | 95 | .frame__title { 96 | font-size: 1rem; 97 | margin: 0 0 1rem; 98 | font-weight: normal; 99 | } 100 | 101 | .frame__links { 102 | display: inline; 103 | } 104 | 105 | .frame__links a:not(:last-child) { 106 | margin-right: 1rem; 107 | } 108 | 109 | .frame__heading { 110 | margin: 1rem 0; 111 | font-size: 1rem; 112 | font-weight: 400; 113 | } 114 | 115 | .frame__counter { 116 | margin: 2rem 0; 117 | align-items: baseline; 118 | line-height: 0.8rem; 119 | text-align: center; 120 | } 121 | 122 | .frame__counter span { 123 | margin: 0 0.25rem; 124 | } 125 | 126 | .frame__counter-text:nth-child(2) { 127 | margin: 0 1.5rem 0 0; 128 | } 129 | 130 | .frame__counter-number { 131 | font-size: 200%; 132 | } 133 | 134 | .intro { 135 | pointer-events: none; 136 | display: flex; 137 | align-items: center; 138 | justify-content: center; 139 | flex-direction: column; 140 | } 141 | 142 | .intro__title { 143 | font-size: 19vw; 144 | margin: 0; 145 | font-weight: 400; 146 | line-height: 1; 147 | } 148 | 149 | .intro__hint { 150 | position: relative; 151 | text-transform: uppercase; 152 | margin: 8vh 0 0 0; 153 | } 154 | 155 | .intro__hint::after { 156 | content: ''; 157 | position: absolute; 158 | width: 1px; 159 | height: 2rem; 160 | top: calc(100% + 2rem); 161 | left: 50%; 162 | background-color: currentColor; 163 | } 164 | 165 | .grid-wrap { 166 | position: relative; 167 | } 168 | 169 | .grid { 170 | display: grid; 171 | grid-template-columns: repeat(auto-fit, minmax(200px, calc(390px + 3rem))); 172 | justify-content: center; 173 | grid-gap: 10vw; 174 | margin: 15rem auto; 175 | } 176 | 177 | .grid__item { 178 | padding: 1.5rem; 179 | } 180 | 181 | .grid__item-number { 182 | display: block; 183 | text-align: right; 184 | font-size: 3rem; 185 | line-height: 1; 186 | } 187 | 188 | .grid__item-img { 189 | margin: 1rem 0 1.75rem; 190 | max-width: 100%; 191 | display: block; 192 | } 193 | 194 | .grid__item-title { 195 | font-size: 1.25rem; 196 | text-transform: uppercase; 197 | font-weight: 400; 198 | margin: 0 0 2.75rem 0; 199 | } 200 | 201 | .grid__item-description { 202 | color: var(--color-description); 203 | font-family: news-gothic-std, sans-serif; 204 | line-height: 1.5; 205 | padding-right: 1rem; 206 | } 207 | 208 | .bigimg { 209 | display: block; 210 | width: 100%; 211 | max-width: calc(1025px - 3rem); 212 | margin: 25vh auto; 213 | } 214 | 215 | .svgtext { 216 | flex: none; 217 | position: relative; 218 | left: -10%; 219 | } 220 | 221 | .svgtext text { 222 | fill: #fff; 223 | font-size: 42px; 224 | } 225 | 226 | .svgtext--1 text { 227 | fill: #fff; 228 | } 229 | 230 | .svgtext--2 text { 231 | fill: #8569c2; 232 | } 233 | 234 | .svgtext--3 text { 235 | font-size: 32px; 236 | } 237 | 238 | .svgtext--4 { 239 | position: absolute; 240 | } 241 | 242 | .svgtext--4 text { 243 | font-size: 48px; 244 | fill: #f9e9a4; 245 | } 246 | 247 | @media screen and (min-width: 53em) { 248 | .frame--screen { 249 | position: absolute; 250 | text-align: left; 251 | z-index: 100; 252 | top: 0; 253 | left: 0; 254 | display: grid; 255 | align-content: space-between; 256 | width: 100%; 257 | max-width: none; 258 | height: 100vh; 259 | padding: 2.25rem 2.5rem; 260 | pointer-events: none; 261 | grid-template-columns: 30% 40% 30%; 262 | grid-template-rows: auto auto auto; 263 | grid-template-areas: 'heading counter links' 264 | '... ... ...' 265 | 'title title ...'; 266 | } 267 | .frame__title-wrap { 268 | grid-area: title; 269 | display: flex; 270 | } 271 | .frame__title { 272 | margin: 0 4rem 0 0; 273 | } 274 | .frame__counter { 275 | grid-area: counter; 276 | justify-self: center; 277 | } 278 | .frame__heading { 279 | margin: 0; 280 | grid-area: heading; 281 | } 282 | .frame__demos { 283 | margin: 0; 284 | grid-area: demos; 285 | justify-self: end; 286 | } 287 | .frame__links { 288 | padding: 0; 289 | justify-self: end; 290 | } 291 | .frame__links--header { 292 | grid-area: links; 293 | } 294 | .frame a { 295 | pointer-events: auto; 296 | } 297 | .frame__counter { 298 | display: flex; 299 | margin: 0; 300 | } 301 | .intro { 302 | min-height: 100vh; 303 | } 304 | .grid__item:nth-child(even) { 305 | margin-top: 35vh; 306 | text-align: right; 307 | } 308 | .grid__item:nth-child(even) .grid__item-description { 309 | padding: 0 0 0 1rem; 310 | } 311 | .grid__item-number { 312 | font-size: 4.75rem; 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /dist/big1.0ab44b73.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/984c2f6812d93c2bae0717b7098e4d801a260a55/dist/big1.0ab44b73.jpg -------------------------------------------------------------------------------- /dist/big2.839b2789.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/984c2f6812d93c2bae0717b7098e4d801a260a55/dist/big2.839b2789.jpg -------------------------------------------------------------------------------- /dist/favicon.2e5f6236.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/984c2f6812d93c2bae0717b7098e4d801a260a55/dist/favicon.2e5f6236.ico -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Animating SVG Text on a Path | Codrops 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 23 | 24 | 25 | 26 | 58 |
59 |
60 |
61 |

Animating SVG Text on a Path

62 | 67 |
68 |

A NOVA space project

69 |
70 | 06 71 | months 72 | 04 73 | days left 74 |
75 | 80 |
81 |
82 |

Terraforming

83 |

Discover our mission

84 |
85 |
86 |
87 | 01 88 | Some image 89 |

One man, one mission

90 |

Daedalus in the meantime, hating Crete and his long exile 91 | and having been touched by the love of his birthplace, 92 | had been closed in by the sea. He says, "Although Minos obstructs 93 | the land and waves, the sky at least lies open; we will fly there. 94 | Minos may possess everything, but he does not possess the air."

95 |
96 |
97 | 02 98 | Some image 99 |

Geode planning

100 |

He spoke and sends down his mind into unknown arts 101 | and changes his nature. For he puts feathers in a row 102 | beginning with the small ones, and the shorter ones following the long ones, 103 | so that you should think it has grown on an incline; in the same way that 104 | a countryman's pipe gradually builds up with reeds of different lengths.

105 |
106 |
107 | 108 | 109 | 110 | 111 | You may think I’m small, but I have a universe inside my mind. 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | Don't forget about the stardust. Don't forget about the quartz rocks in the woods. 120 | 121 | 122 | 123 |
124 |
125 | 03 126 | Some image 127 |

Disaster management

128 |

Then he binds the middle ones with thread and the last feathers with wax 129 | and then bends what he has created by a small curvature as 130 | to mimic real birds. Together with his father, the boy Icarus 131 | was standing nearby, unaware that he was facing danger, 132 | now with a beaming face was capturing the feathers 133 | which the wandering air has moved, with his thumb now was softening the yellow wax 134 | and with his play he kept interrupting the marvelous work of his father.

135 |
136 |
137 | 04 138 | Some image 139 |

Impact theory

140 |

After the finishing touch had been placed 141 | on the work, the craftsman balanced his body 142 | on the twin wings and suspended his body in the open air; 143 | "I warn you to travel in the middle course, Icarus, so that the waves 144 | may not weigh down your wings if you go too low, 145 | and so that the sun will not scorch your wings if you go too high. 146 | Stay between both. I order you not to look at Boötes, 147 | or Helice, or the drawn sword of Orion.

148 |
149 |
150 | Some image 151 | 152 | 153 | 154 | 155 | Dwell on the beauty of life. Watch the stars. 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | The cosmos is within us. We are made of star-stuff. 164 | 165 | 166 | 167 |
168 |
169 | 05 170 | Some image 171 |

Incubation assertion

172 |

With me leading, seize the way! 173 | He hands over at the same time the rules of flying 174 | and fits the unknown wings on his shoulders. 175 | Between the work and warnings the old cheeks grew wet, 176 | and his fatherly hands trembled; He gave to his son kisses 177 | not to be repeated, and having lifted himself up on his wings 178 | he flies before and he fears for his comrade.

179 |
180 |
181 | 06 182 | Some image 183 |

Hyperdrive vessel

184 |

Just as a bird 185 | who has led forth a tender offspring from a high nest into the air, 186 | and encourages him to follow and instructs him in the destructive arts 187 | and he moves himself and looks back at the wings of his son. 188 | Someone while catching fish with a trembling rod, 189 | either a shepherd leaning on his staff or a plowman on a plow 190 | saw these men and was stunned, and they who were able to snatch the sky, 191 | he believed were gods.

192 |
193 |
194 | 195 | 196 | 197 | 198 | When it is dark enough, you can see the stars. 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | It is not the man who has too little, but the man who craves more, that is poor. 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | Live by love though the stars walk backward. 215 | 216 | 217 | 218 | Some image 219 |
220 | 221 | 222 | 223 | 224 | Yours is the light by which my spirit's born: you are my sun, my moon, and all my stars. 225 | 226 | 227 | 228 |
229 |
230 | 07 231 | Some image 232 |

Icarian Engine

233 |

And now Juno's Samos was on the left 234 | side for Delos and Paros had been left behind 235 | and on the right was Lebynthos and Kalymnos rich in honey, 236 | when the boy began to rejoice in his bold flight 237 | and deserted his leader, and attracted by a desire for the sky 238 | he took his path went higher.

239 |
240 |
241 | 08 242 | Some image 243 |

Neutrospace Accelerator

244 |

The vicinity of the sun 245 | softens the fragrant wax, the chains of the feathers; 246 | the wax melted: he shook his bare arms 247 | and lacking oarage he takes up no air, 248 | and his mouth shouting his father's name 249 | is swept up in the blue sea, which takes its name from him.

250 |
251 |
252 |
253 | 260 |
261 | 262 | 263 | 264 | 265 | -------------------------------------------------------------------------------- /dist/js.c39dff1f.js: -------------------------------------------------------------------------------- 1 | // modules are defined as an array 2 | // [ module function, map of requires ] 3 | // 4 | // map of requires is short require name -> numeric require 5 | // 6 | // anything defined in a previous bundle is accessed via the 7 | // orig method which is the require for previous bundles 8 | parcelRequire = (function (modules, cache, entry, globalName) { 9 | // Save the require from previous bundle to this closure if any 10 | var previousRequire = typeof parcelRequire === 'function' && parcelRequire; 11 | var nodeRequire = typeof require === 'function' && require; 12 | 13 | function newRequire(name, jumped) { 14 | if (!cache[name]) { 15 | if (!modules[name]) { 16 | // if we cannot find the module within our internal map or 17 | // cache jump to the current global require ie. the last bundle 18 | // that was added to the page. 19 | var currentRequire = typeof parcelRequire === 'function' && parcelRequire; 20 | if (!jumped && currentRequire) { 21 | return currentRequire(name, true); 22 | } 23 | 24 | // If there are other bundles on this page the require from the 25 | // previous one is saved to 'previousRequire'. Repeat this as 26 | // many times as there are bundles until the module is found or 27 | // we exhaust the require chain. 28 | if (previousRequire) { 29 | return previousRequire(name, true); 30 | } 31 | 32 | // Try the node require function if it exists. 33 | if (nodeRequire && typeof name === 'string') { 34 | return nodeRequire(name); 35 | } 36 | 37 | var err = new Error('Cannot find module \'' + name + '\''); 38 | err.code = 'MODULE_NOT_FOUND'; 39 | throw err; 40 | } 41 | 42 | localRequire.resolve = resolve; 43 | localRequire.cache = {}; 44 | 45 | var module = cache[name] = new newRequire.Module(name); 46 | 47 | modules[name][0].call(module.exports, localRequire, module, module.exports, this); 48 | } 49 | 50 | return cache[name].exports; 51 | 52 | function localRequire(x){ 53 | return newRequire(localRequire.resolve(x)); 54 | } 55 | 56 | function resolve(x){ 57 | return modules[name][1][x] || x; 58 | } 59 | } 60 | 61 | function Module(moduleName) { 62 | this.id = moduleName; 63 | this.bundle = newRequire; 64 | this.exports = {}; 65 | } 66 | 67 | newRequire.isParcelRequire = true; 68 | newRequire.Module = Module; 69 | newRequire.modules = modules; 70 | newRequire.cache = cache; 71 | newRequire.parent = previousRequire; 72 | newRequire.register = function (id, exports) { 73 | modules[id] = [function (require, module) { 74 | module.exports = exports; 75 | }, {}]; 76 | }; 77 | 78 | var error; 79 | for (var i = 0; i < entry.length; i++) { 80 | try { 81 | newRequire(entry[i]); 82 | } catch (e) { 83 | // Save first error but execute all entries 84 | if (!error) { 85 | error = e; 86 | } 87 | } 88 | } 89 | 90 | if (entry.length) { 91 | // Expose entry point to Node, AMD or browser globals 92 | // Based on https://github.com/ForbesLindesay/umd/blob/master/template.js 93 | var mainExports = newRequire(entry[entry.length - 1]); 94 | 95 | // CommonJS 96 | if (typeof exports === "object" && typeof module !== "undefined") { 97 | module.exports = mainExports; 98 | 99 | // RequireJS 100 | } else if (typeof define === "function" && define.amd) { 101 | define(function () { 102 | return mainExports; 103 | }); 104 | 105 | // 14 | 15 | 16 | 17 | 18 | 50 |
51 |
52 |
53 |

Animating SVG Text on a Path

54 | 59 |
60 |

A NOVA space project

61 |
62 | 06 63 | months 64 | 04 65 | days left 66 |
67 | 72 |
73 |
74 |

Terraforming

75 |

Discover our mission

76 |
77 |
78 |
79 | 01 80 | Some image 81 |

One man, one mission

82 |

Daedalus in the meantime, hating Crete and his long exile 83 | and having been touched by the love of his birthplace, 84 | had been closed in by the sea. He says, "Although Minos obstructs 85 | the land and waves, the sky at least lies open; we will fly there. 86 | Minos may possess everything, but he does not possess the air."

87 |
88 |
89 | 02 90 | Some image 91 |

Geode planning

92 |

He spoke and sends down his mind into unknown arts 93 | and changes his nature. For he puts feathers in a row 94 | beginning with the small ones, and the shorter ones following the long ones, 95 | so that you should think it has grown on an incline; in the same way that 96 | a countryman's pipe gradually builds up with reeds of different lengths.

97 |
98 |
99 | 100 | 101 | 102 | 103 | You may think I’m small, but I have a universe inside my mind. 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | Don't forget about the stardust. Don't forget about the quartz rocks in the woods. 112 | 113 | 114 | 115 |
116 |
117 | 03 118 | Some image 119 |

Disaster management

120 |

Then he binds the middle ones with thread and the last feathers with wax 121 | and then bends what he has created by a small curvature as 122 | to mimic real birds. Together with his father, the boy Icarus 123 | was standing nearby, unaware that he was facing danger, 124 | now with a beaming face was capturing the feathers 125 | which the wandering air has moved, with his thumb now was softening the yellow wax 126 | and with his play he kept interrupting the marvelous work of his father.

127 |
128 |
129 | 04 130 | Some image 131 |

Impact theory

132 |

After the finishing touch had been placed 133 | on the work, the craftsman balanced his body 134 | on the twin wings and suspended his body in the open air; 135 | "I warn you to travel in the middle course, Icarus, so that the waves 136 | may not weigh down your wings if you go too low, 137 | and so that the sun will not scorch your wings if you go too high. 138 | Stay between both. I order you not to look at Boötes, 139 | or Helice, or the drawn sword of Orion.

140 |
141 |
142 | Some image 143 | 144 | 145 | 146 | 147 | Dwell on the beauty of life. Watch the stars. 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | The cosmos is within us. We are made of star-stuff. 156 | 157 | 158 | 159 |
160 |
161 | 05 162 | Some image 163 |

Incubation assertion

164 |

With me leading, seize the way! 165 | He hands over at the same time the rules of flying 166 | and fits the unknown wings on his shoulders. 167 | Between the work and warnings the old cheeks grew wet, 168 | and his fatherly hands trembled; He gave to his son kisses 169 | not to be repeated, and having lifted himself up on his wings 170 | he flies before and he fears for his comrade.

171 |
172 |
173 | 06 174 | Some image 175 |

Hyperdrive vessel

176 |

Just as a bird 177 | who has led forth a tender offspring from a high nest into the air, 178 | and encourages him to follow and instructs him in the destructive arts 179 | and he moves himself and looks back at the wings of his son. 180 | Someone while catching fish with a trembling rod, 181 | either a shepherd leaning on his staff or a plowman on a plow 182 | saw these men and was stunned, and they who were able to snatch the sky, 183 | he believed were gods.

184 |
185 |
186 | 187 | 188 | 189 | 190 | When it is dark enough, you can see the stars. 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | It is not the man who has too little, but the man who craves more, that is poor. 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | Live by love though the stars walk backward. 207 | 208 | 209 | 210 | Some image 211 |
212 | 213 | 214 | 215 | 216 | Yours is the light by which my spirit's born: you are my sun, my moon, and all my stars. 217 | 218 | 219 | 220 |
221 |
222 | 07 223 | Some image 224 |

Icarian Engine

225 |

And now Juno's Samos was on the left 226 | side for Delos and Paros had been left behind 227 | and on the right was Lebynthos and Kalymnos rich in honey, 228 | when the boy began to rejoice in his bold flight 229 | and deserted his leader, and attracted by a desire for the sky 230 | he took his path went higher.

231 |
232 |
233 | 08 234 | Some image 235 |

Neutrospace Accelerator

236 |

The vicinity of the sun 237 | softens the fragrant wax, the chains of the feathers; 238 | the wax melted: he shook his bare arms 239 | and lacking oarage he takes up no air, 240 | and his mouth shouting his father's name 241 | is swept up in the blue sea, which takes its name from him.

242 |
243 |
244 |
245 | 252 |
253 | 254 | 255 | 256 | 257 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | import TextOnPath from "./textOnPath"; 2 | const imagesLoaded = require('imagesloaded'); 3 | 4 | // Preload images 5 | const preloadImages = () => { 6 | return new Promise((resolve, reject) => { 7 | imagesLoaded(document.querySelectorAll('.grid__item-img, .bigimg'), resolve); 8 | }); 9 | }; 10 | 11 | // Preload fonts 12 | const preloadFonts = () => { 13 | return new Promise((resolve, reject) => { 14 | WebFont.load({ 15 | typekit: { 16 | id: 'rhw1vur' 17 | }, 18 | active: resolve 19 | }); 20 | }); 21 | }; 22 | 23 | // Preload fonts and images 24 | Promise.all([preloadImages(), preloadFonts()]).then(() => { 25 | // And then initialize the TextOnScroll instances 26 | [...document.querySelectorAll('svg.svgtext')].forEach(el => new TextOnPath(el)); 27 | // Remove loader (loading class) 28 | document.body.classList.remove('loading'); 29 | }); -------------------------------------------------------------------------------- /js/mathUtils.js: -------------------------------------------------------------------------------- 1 | // Map number x from range [a, b] to [c, d] 2 | const map = (x, a, b, c, d) => (x - a) * (d - c) / (b - a) + c; 3 | 4 | // Linear interpolation 5 | const lerp = (a, b, n) => (1 - n) * a + n * b; 6 | 7 | // Clamp val within min and max 8 | const clamp = (val, min, max) => Math.max(Math.min(val, max), min); 9 | 10 | export { map, lerp, clamp }; 11 | -------------------------------------------------------------------------------- /js/textOnPath.js: -------------------------------------------------------------------------------- 1 | import { map, lerp, clamp } from './mathUtils'; 2 | import winsize from './winsize' 3 | 4 | // Check if firefox 5 | const firefoxAgent = navigator.userAgent.indexOf('Firefox') > -1; 6 | 7 | export default class TextOnPath { 8 | constructor(svgEl) { 9 | // The SVG element 10 | this.DOM = {svg: svgEl}; 11 | // The text element 12 | this.DOM.text = this.DOM.svg.querySelector('text'); 13 | // Sadly firefox does not yet play nicely with SVG filters, so take them out if any applied to the text element.. 14 | if ( firefoxAgent ) { 15 | this.DOM.text.removeAttribute('filter'); 16 | } 17 | // Get the filter to know which one to get the primitive from 18 | // The textPath element 19 | this.DOM.textPath = this.DOM.text.querySelector('textPath'); 20 | // The filter type (defined in the svg element as data-filter-type) 21 | const filterType = this.DOM.svg.dataset.filterType; 22 | // The filter element id 23 | const filterId = this.DOM.text.getAttribute('filter') && this.DOM.text.getAttribute('filter').match(/url\(["']?([^"']*)["']?\)/)[1]; 24 | // The SVG filter primitive object 25 | // This is where the logic of the svg filter is done for the update on scroll 26 | // Depending on what filter type we set up in the data-filter-type, a specific filter primitive attribute will get updated depending on the scroll speed 27 | this.filterPrimitive = filterType && filterId && new FilterPrimitive(filterType, filterId); 28 | // The path total length 29 | this.pathLength = this.DOM.svg.querySelector('path').getTotalLength(); 30 | // SVG element's size/position 31 | this.svgRect = this.DOM.svg.getBoundingClientRect(); 32 | // this is the svg element top value relative to the document 33 | // To calculate this, we need to get the top value relative to the viewport and sum the current page scroll 34 | this.positionY = this.svgRect.top + window.pageYOffset; 35 | // Recalculate on window resize 36 | window.addEventListener('resize', () => { 37 | this.svgRect = this.DOM.svg.getBoundingClientRect(); 38 | this.positionY = this.svgRect.top + window.pageYOffset; 39 | }); 40 | // In order to smooth the text animation, we will use linear interpolation to calculate the value of the startOffset 41 | // "value" is the current interpolated value and "amt" the amount to interpolate 42 | this.startOffset = { 43 | value: this.computeOffset(), 44 | amt: 0.22 45 | }; 46 | // Calculate and set initial startOffset value 47 | this.startOffset.value = this.computeOffset(); 48 | this.updateTextPathOffset(); 49 | // Interpolated scroll value. 50 | // This will be used to calculate the text blur value which will change proportionally to the scrolling speed 51 | // To calculate the speed, we use the distance from the current scroll value to the previous scroll value (or interpolated one) 52 | this.scroll = { 53 | value: window.pageYOffset, 54 | amt: 0.17 55 | }; 56 | // By using the IntersectionObserverAPI to check when the SVG element in inside the viewport, we can avoid calculating and updating the values for the elements outside the viewport 57 | this.observer = new IntersectionObserver((entries) => { 58 | entries.forEach(entry => { 59 | this.isVisible = entry.intersectionRatio > 0; 60 | if ( !this.isVisible ) { 61 | this.entered = false; 62 | // reset 63 | this.update(); 64 | } 65 | }); 66 | }); 67 | this.observer.observe(this.DOM.svg); 68 | 69 | // rAF/loop 70 | requestAnimationFrame(() => this.render()); 71 | } 72 | // Calculate the textPath element startOffset value 73 | // This will allow us to position the text, depending on the current scroll position 74 | computeOffset() { 75 | // We want the text to start appearing from the right side of the screen when it comes into the viewport. 76 | // This translates into saying that the text startOffset should have it's highest value (total path length) when the svg top value minus the page scroll equals the viewport height and it's lowest value (this case -this.pathLength/2) when it equals 0 (element is on the top part of the viewport) 77 | return map(this.positionY - window.pageYOffset, winsize.height, 0, this.pathLength, -this.pathLength/2); 78 | } 79 | // Updates the text startOffset value 80 | updateTextPathOffset() { 81 | this.DOM.textPath.setAttribute('startOffset', this.startOffset.value); 82 | } 83 | update() { 84 | // Calculate and set the interpolated startOffset value 85 | const currentOffset = this.computeOffset(); 86 | this.startOffset.value = !this.entered ? currentOffset : lerp(this.startOffset.value, currentOffset, this.startOffset.amt); 87 | this.updateTextPathOffset(); 88 | 89 | // SVG Filter related: 90 | // The current scroll value 91 | const currentScroll = window.pageYOffset; 92 | // Interpolated scroll value 93 | this.scroll.value = !this.entered ? currentScroll : lerp(this.scroll.value, currentScroll, this.scroll.amt); 94 | // Distance between the current and interpolated scroll value 95 | const distance = Math.abs(this.scroll.value - currentScroll); 96 | // Update the filter primitive attribute that changes as the scroll speed increases 97 | this.filterPrimitive && this.filterPrimitive.update(distance); 98 | 99 | if ( !this.entered ) { 100 | this.entered = true; 101 | } 102 | } 103 | render() { 104 | if ( this.isVisible ) { 105 | this.update(); 106 | } 107 | // ... 108 | requestAnimationFrame(() => this.render()); 109 | } 110 | } 111 | 112 | class FilterPrimitive { 113 | constructor(type, id) { 114 | this.type = type; 115 | this.DOM = {el: document.querySelector(`${id} > ${this.getPrimitiveType(this.type)}`)}; 116 | } 117 | getPrimitiveType(type) { 118 | const types = { 119 | 'blur': 'feGaussianBlur', 120 | 'distortion': 'feDisplacementMap' 121 | }; 122 | return types[type]; 123 | } 124 | update(distance) { 125 | const types = { 126 | // The blur stdDeviation will be 0 when the distance equals 0 and 10 when the distance equals 400 127 | 'blur': () => this.DOM.el.setAttribute('stdDeviation', clamp(map(distance, 0, 400, this.DOM.el.dataset.minDeviation || 0, this.DOM.el.dataset.maxDeviation || 10), this.DOM.el.dataset.minDeviation || 0, this.DOM.el.dataset.maxDeviation || 10)), 128 | // The displacementMap scale will be 0 when the distance equals 0 and 100 when the distance equals 200 129 | 'distortion': () => this.DOM.el.scale.baseVal = clamp(map(distance, 0, 200, this.DOM.el.dataset.minScale || 0, this.DOM.el.dataset.maxScale || 100), this.DOM.el.dataset.minScale || 0, this.DOM.el.dataset.maxScale || 100) 130 | }; 131 | return types[this.type](); 132 | } 133 | } -------------------------------------------------------------------------------- /js/winsize.js: -------------------------------------------------------------------------------- 1 | // Viewport size 2 | const getWinSize = () => { 3 | return { 4 | width: window.innerWidth, 5 | height: window.innerHeight 6 | }; 7 | }; 8 | let winsize = getWinSize(); 9 | window.addEventListener('resize', () => winsize = getWinSize()); 10 | 11 | export default winsize; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AnimateSVGTextPath", 3 | "version": "1.0.0", 4 | "description": "Demo showing how to animate SVG text on a path on scroll using the Intersection Observer API and SVG filters.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "parcel index.html --open", 8 | "clean": "rm -rf dist/*", 9 | "build:parcel": "parcel build index.html --no-minify --no-source-maps --public-url ./", 10 | "build": "npm run clean && npm run build:parcel" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/codrops/AnimateSVGTextPath.git" 15 | }, 16 | "keywords": [], 17 | "author": "Codrops", 18 | "license": "MIT", 19 | "homepage": "https://tympanus.net/codrops/2020/02/26/animating-svg-text-on-a-path/", 20 | "bugs": { 21 | "url": "https://github.com/codrops/AnimateSVGTextPath/issues" 22 | }, 23 | "dependencies": { 24 | "parcel-bundler": "^1.12.4", 25 | "imagesloaded": "^4.1.4" 26 | } 27 | } 28 | --------------------------------------------------------------------------------