├── .gitignore
├── img
├── 1.jpg
├── 2.jpg
├── 3.jpg
├── 4.jpg
├── 5.jpg
├── 6.jpg
├── 7.jpg
├── 8.jpg
├── big1.jpg
└── big2.jpg
├── .gitattributes
├── favicon.ico
├── 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
├── big1.0ab44b73.jpg
├── big2.839b2789.jpg
├── favicon.2e5f6236.ico
├── base.0b18b771.css
├── index.html
└── js.c39dff1f.js
├── js
├── winsize.js
├── mathUtils.js
├── index.js
└── textOnPath.js
├── package.json
├── LICENSE
├── README.md
├── css
└── base.css
└── index.html
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | .cache
3 | .DS_Store
4 | package-lock.json
--------------------------------------------------------------------------------
/img/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/img/1.jpg
--------------------------------------------------------------------------------
/img/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/img/2.jpg
--------------------------------------------------------------------------------
/img/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/img/3.jpg
--------------------------------------------------------------------------------
/img/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/img/4.jpg
--------------------------------------------------------------------------------
/img/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/img/5.jpg
--------------------------------------------------------------------------------
/img/6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/img/6.jpg
--------------------------------------------------------------------------------
/img/7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/img/7.jpg
--------------------------------------------------------------------------------
/img/8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/img/8.jpg
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/favicon.ico
--------------------------------------------------------------------------------
/img/big1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/img/big1.jpg
--------------------------------------------------------------------------------
/img/big2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/img/big2.jpg
--------------------------------------------------------------------------------
/dist/1.d5d0bbe2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/dist/1.d5d0bbe2.jpg
--------------------------------------------------------------------------------
/dist/2.b0539b47.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/dist/2.b0539b47.jpg
--------------------------------------------------------------------------------
/dist/3.caf4bd5d.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/dist/3.caf4bd5d.jpg
--------------------------------------------------------------------------------
/dist/4.563dcc99.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/dist/4.563dcc99.jpg
--------------------------------------------------------------------------------
/dist/5.1c2ed3fb.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/dist/5.1c2ed3fb.jpg
--------------------------------------------------------------------------------
/dist/6.df718cc7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/dist/6.df718cc7.jpg
--------------------------------------------------------------------------------
/dist/7.fd69eac9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/dist/7.fd69eac9.jpg
--------------------------------------------------------------------------------
/dist/8.e174b0a5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/dist/8.e174b0a5.jpg
--------------------------------------------------------------------------------
/dist/big1.0ab44b73.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/dist/big1.0ab44b73.jpg
--------------------------------------------------------------------------------
/dist/big2.839b2789.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/dist/big2.839b2789.jpg
--------------------------------------------------------------------------------
/dist/favicon.2e5f6236.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/AnimateSVGTextPath/HEAD/dist/favicon.2e5f6236.ico
--------------------------------------------------------------------------------
/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;
--------------------------------------------------------------------------------
/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/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 | });
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | 
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/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Animating SVG Text on a Path | Codrops
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 | //