5 years experience - HTML, CSS, JavaScript. Passion for creativity in the digital space. Problem solver. Hiker, guitar player, culinary enthusiast. Constantly seeking new challenges, growth opportunities.Bringing imaginative ideas to life. Skilled in modern web development frameworks such as React and Angular. Strong understanding of UI/UX design principles and ability to create visually appealing and usable websites.
5 years experience - HTML, CSS, JavaScript. Passion for creativity in the digital space. Problem solver. Hiker, guitar player, culinary enthusiast. Constantly seeking new challenges, growth opportunities.Bringing imaginative ideas to life. Skilled in modern web development frameworks such as React and Angular. Strong understanding of UI/UX design principles and ability to create visually appealing and usable websites.
99 |
Projects
100 |
Eco Explorer, SkyBridge, SparkSail
101 |
Awards
102 |
Best User Experience Design, Webby Awards 2021
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
--------------------------------------------------------------------------------
/src/js/index.js:
--------------------------------------------------------------------------------
1 | import { preloadFonts } from './utils';
2 | import { TypeShuffle } from './typeShuffle';
3 |
4 | preloadFonts('biu0hfr').then(() => {
5 | document.body.classList.remove('loading');
6 |
7 | const textElement = document.querySelector('.content');
8 |
9 | const ts = new TypeShuffle(textElement);
10 | ts.trigger('fx1');
11 |
12 | [...document.querySelectorAll('.effects > button')].forEach(button => {
13 | button.addEventListener('click', () => {
14 | ts.trigger(`fx${button.dataset.fx}`);
15 | });
16 | });
17 |
18 | });
--------------------------------------------------------------------------------
/src/js/typeShuffle.js:
--------------------------------------------------------------------------------
1 | import 'splitting/dist/splitting.css';
2 | import 'splitting/dist/splitting-cells.css';
3 | import Splitting from 'splitting';
4 | import { randomNumber } from './utils';
5 |
6 | /**
7 | * Class representing one line
8 | */
9 | class Line {
10 | // line position
11 | position = -1;
12 | // cells/chars
13 | cells = [];
14 |
15 | /**
16 | * Constructor.
17 | * @param {Element} DOM_el - the char element ()
18 | */
19 | constructor(linePosition) {
20 | this.position = linePosition;
21 | }
22 | }
23 |
24 | /**
25 | * Class representing one cell/char
26 | */
27 | class Cell {
28 | // DOM elements
29 | DOM = {
30 | // the char element ()
31 | el: null,
32 | };
33 | // cell position
34 | position = -1;
35 | // previous cell position
36 | previousCellPosition = -1;
37 | // original innerHTML
38 | original;
39 | // current state/innerHTML
40 | state;
41 | color;
42 | originalColor;
43 | // cached values
44 | cache;
45 |
46 | /**
47 | * Constructor.
48 | * @param {Element} DOM_el - the char element ()
49 | */
50 | constructor(DOM_el, {
51 | position,
52 | previousCellPosition
53 | } = {}) {
54 | this.DOM.el = DOM_el;
55 | this.original = this.DOM.el.innerHTML;
56 | this.state = this.original;
57 | this.color = this.originalColor = getComputedStyle(document.documentElement).getPropertyValue('--color-text');
58 | this.position = position;
59 | this.previousCellPosition = previousCellPosition;
60 | }
61 | /**
62 | * @param {string} value
63 | */
64 | set(value) {
65 | this.state = value;
66 | this.DOM.el.innerHTML = this.state;
67 | }
68 | }
69 |
70 | /**
71 | * Class representing the TypeShuffle object
72 | */
73 | export class TypeShuffle {
74 | // DOM elements
75 | DOM = {
76 | // the main text element
77 | el: null,
78 | };
79 | // array of Line objs
80 | lines = [];
81 | // array of letters and symbols
82 | lettersAndSymbols = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '!', '@', '#', '$', '&', '*', '(', ')', '-', '_', '+', '=', '/', '[', ']', '{', '}', ';', ':', '<', '>', ',', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
83 | // effects and respective methods
84 | effects = {
85 | 'fx1': () => this.fx1(),
86 | 'fx2': () => this.fx2(),
87 | 'fx3': () => this.fx3(),
88 | 'fx4': () => this.fx4(),
89 | 'fx5': () => this.fx5(),
90 | 'fx6': () => this.fx6(),
91 | };
92 | totalChars = 0;
93 |
94 | /**
95 | * Constructor.
96 | * @param {Element} DOM_el - main text element
97 | */
98 | constructor(DOM_el) {
99 | this.DOM.el = DOM_el;
100 | // Apply Splitting (two times to have lines, words and chars)
101 | const results = Splitting({
102 | target: this.DOM.el,
103 | by: 'lines'
104 | })
105 | results.forEach(s => Splitting({ target: s.words }));
106 |
107 | // for every line
108 | for (const [linePosition, lineArr] of results[0].lines.entries()) {
109 | // create a new Line
110 | const line = new Line(linePosition);
111 | let cells = [];
112 | let charCount = 0;
113 | // for every word of each line
114 | for (const word of lineArr) {
115 | // for every character of each line
116 | for (const char of [...word.querySelectorAll('.char')]) {
117 | cells.push(
118 | new Cell(char, {
119 | position: charCount,
120 | previousCellPosition: charCount === 0 ? -1 : charCount-1
121 | })
122 | );
123 | ++charCount;
124 | }
125 | }
126 | line.cells = cells;
127 | this.lines.push(line);
128 | this.totalChars += charCount;
129 | }
130 |
131 | // TODO
132 | // window.addEventListener('resize', () => this.resize());
133 | }
134 | /**
135 | * clear all the cells chars
136 | */
137 | clearCells() {
138 | for (const line of this.lines) {
139 | for (const cell of line.cells) {
140 | cell.set(' ');
141 | }
142 | }
143 | }
144 | /**
145 | *
146 | * @returns {string} a random char from this.lettersAndSymbols
147 | */
148 | getRandomChar() {
149 | return this.lettersAndSymbols[Math.floor(Math.random() * this.lettersAndSymbols.length)];
150 | }
151 | /**
152 | * Effect 1 - clear cells and animate each line cells (delays per line and per cell)
153 | */
154 | fx1() {
155 | // max iterations for each cell to change the current value
156 | const MAX_CELL_ITERATIONS = 45;
157 |
158 | let finished = 0;
159 |
160 | // clear all cells values
161 | this.clearCells();
162 |
163 | // cell's loop animation
164 | // each cell will change its value MAX_CELL_ITERATIONS times
165 | const loop = (line, cell, iteration = 0) => {
166 | // cache the previous value
167 | cell.cache = cell.state;
168 |
169 | // set back the original cell value if at the last iteration
170 | if ( iteration === MAX_CELL_ITERATIONS-1 ) {
171 | cell.set(cell.original);
172 | ++finished;
173 | if ( finished === this.totalChars ) {
174 | this.isAnimating = false;
175 | }
176 | }
177 | // if the cell is the first one in its line then generate a random char
178 | else if ( cell.position === 0 ) {
179 | // show specific characters for the first 9 iterations (looks cooler)
180 | cell.set(iteration < 9 ?
181 | ['*', '-', '\u0027', '\u0022'][Math.floor(Math.random() * 4)] :
182 | this.getRandomChar());
183 | }
184 | // get the cached value of the previous cell.
185 | // This will result in the illusion that the chars are sliding from left to right
186 | else {
187 | cell.set(line.cells[cell.previousCellPosition].cache);
188 | }
189 |
190 | // doesn't count if it's an empty space
191 | if ( cell.cache != ' ' ) {
192 | ++iteration;
193 | }
194 |
195 | // repeat...
196 | if ( iteration < MAX_CELL_ITERATIONS ) {
197 | setTimeout(() => loop(line, cell, iteration), 15);
198 | }
199 | };
200 |
201 | // set delays for each cell animation
202 | for (const line of this.lines) {
203 | for (const cell of line.cells) {
204 | setTimeout(() => loop(line, cell), (line.position+1)*200);
205 | }
206 | }
207 | }
208 | fx2() {
209 | const MAX_CELL_ITERATIONS = 20;
210 | let finished = 0;
211 | const loop = (line, cell, iteration = 0) => {
212 | if ( iteration === MAX_CELL_ITERATIONS-1 ) {
213 | cell.set(cell.original);
214 | cell.DOM.el.style.opacity = 0;
215 | setTimeout(() => {
216 | cell.DOM.el.style.opacity = 1;
217 | }, 300);
218 |
219 | ++finished;
220 | if ( finished === this.totalChars ) {
221 | this.isAnimating = false;
222 | }
223 | }
224 | else {
225 | cell.set(this.getRandomChar());
226 | }
227 |
228 | ++iteration;
229 | if ( iteration < MAX_CELL_ITERATIONS ) {
230 | setTimeout(() => loop(line, cell, iteration), 40);
231 | }
232 | };
233 |
234 | for (const line of this.lines) {
235 | for (const cell of line.cells) {
236 | setTimeout(() => loop(line, cell), (cell.position+1)*30);
237 | }
238 | }
239 | }
240 | fx3() {
241 | const MAX_CELL_ITERATIONS = 10;
242 | let finished = 0;
243 | this.clearCells();
244 |
245 | const loop = (line, cell, iteration = 0) => {
246 | if ( iteration === MAX_CELL_ITERATIONS-1 ) {
247 | cell.set(cell.original);
248 | ++finished;
249 | if ( finished === this.totalChars ) {
250 | this.isAnimating = false;
251 | }
252 | }
253 | else {
254 | cell.set(this.getRandomChar());
255 | }
256 |
257 | ++iteration;
258 | if ( iteration < MAX_CELL_ITERATIONS ) {
259 | setTimeout(() => loop(line, cell, iteration), 80);
260 | }
261 | };
262 |
263 | for (const line of this.lines) {
264 | for (const cell of line.cells) {
265 | setTimeout(() => loop(line, cell), randomNumber(0,2000));
266 | }
267 | }
268 | }
269 | fx4() {
270 | const MAX_CELL_ITERATIONS = 30;
271 | let finished = 0;
272 | this.clearCells();
273 |
274 | const loop = (line, cell, iteration = 0) => {
275 | cell.cache = cell.state;
276 |
277 | if ( iteration === MAX_CELL_ITERATIONS-1 ) {
278 | cell.set(cell.original);
279 |
280 | ++finished;
281 | if ( finished === this.totalChars ) {
282 | this.isAnimating = false;
283 | }
284 | }
285 | else if ( cell.position === 0 ) {
286 | cell.set(['*',':'][Math.floor(Math.random() * 2)]);
287 | }
288 | else {
289 | cell.set(line.cells[cell.previousCellPosition].cache);
290 | }
291 |
292 | if ( cell.cache != ' ' ) {
293 | ++iteration;
294 | }
295 |
296 | if ( iteration < MAX_CELL_ITERATIONS ) {
297 | setTimeout(() => loop(line, cell, iteration), 15);
298 | }
299 | };
300 |
301 | for (const line of this.lines) {
302 | for (const cell of line.cells) {
303 | setTimeout(() => loop(line, cell), Math.abs(this.lines.length/2-line.position)*400);
304 | }
305 | }
306 | }
307 | fx5() {
308 | // max iterations for each cell to change the current value
309 | const MAX_CELL_ITERATIONS = 30;
310 | let finished = 0;
311 | this.clearCells();
312 |
313 | const loop = (line, cell, iteration = 0) => {
314 | cell.cache = {'state': cell.state, 'color': cell.color};
315 |
316 | if ( iteration === MAX_CELL_ITERATIONS-1 ) {
317 | cell.color = cell.originalColor;
318 | cell.DOM.el.style.color = cell.color;
319 | cell.set(cell.original);
320 |
321 | ++finished;
322 | if ( finished === this.totalChars ) {
323 | this.isAnimating = false;
324 | }
325 | }
326 | else if ( cell.position === 0 ) {
327 | cell.color = ['#3e775d', '#61dca3', '#61b3dc'][Math.floor(Math.random() * 3)]
328 | cell.DOM.el.style.color = cell.color
329 | cell.set(iteration < 9 ?
330 | ['*', '-', '\u0027', '\u0022'][Math.floor(Math.random() * 4)] :
331 | this.getRandomChar());
332 | }
333 | else {
334 | cell.set(line.cells[cell.previousCellPosition].cache.state);
335 |
336 | cell.color = line.cells[cell.previousCellPosition].cache.color
337 | cell.DOM.el.style.color = cell.color
338 | }
339 |
340 | if ( cell.cache.state != ' ' ) {
341 | ++iteration;
342 | }
343 |
344 | if ( iteration < MAX_CELL_ITERATIONS ) {
345 | setTimeout(() => loop(line, cell, iteration), 10);
346 | }
347 | };
348 |
349 | for (const line of this.lines) {
350 | for (const cell of line.cells) {
351 | setTimeout(() => loop(line, cell), (line.position+1)*200);
352 | }
353 | }
354 | }
355 | fx6() {
356 | // max iterations for each cell to change the current value
357 | const MAX_CELL_ITERATIONS = 15;
358 | let finished = 0;
359 | const loop = (line, cell, iteration = 0) => {
360 | cell.cache = {'state': cell.state, 'color': cell.color};
361 |
362 | if ( iteration === MAX_CELL_ITERATIONS-1 ) {
363 | cell.set(cell.original);
364 |
365 | cell.color = cell.originalColor;
366 | cell.DOM.el.style.color = cell.color;
367 |
368 | ++finished;
369 | if ( finished === this.totalChars ) {
370 | this.isAnimating = false;
371 | }
372 | }
373 | else {
374 | cell.set(this.getRandomChar());
375 |
376 | cell.color = ['#2b4539', '#61dca3', '#61b3dc'][Math.floor(Math.random() * 3)]
377 | cell.DOM.el.style.color = cell.color
378 | }
379 |
380 | ++iteration;
381 | if ( iteration < MAX_CELL_ITERATIONS ) {
382 | setTimeout(() => loop(line, cell, iteration), randomNumber(30,110));
383 | }
384 | };
385 |
386 | for (const line of this.lines) {
387 | for (const cell of line.cells) {
388 | setTimeout(() => loop(line, cell), (line.position+1)*80);
389 | }
390 | }
391 | }
392 | /**
393 | * call the right effect method (defined in this.effects)
394 | * @param {string} effect - effect type
395 | */
396 | trigger(effect = 'fx1') {
397 | if ( !(effect in this.effects) || this.isAnimating ) return;
398 | this.isAnimating = true;
399 | this.effects[effect]();
400 | }
401 | }
--------------------------------------------------------------------------------
/src/js/utils.js:
--------------------------------------------------------------------------------
1 | // Preload images
2 | const preloadFonts = id => {
3 | return new Promise((resolve) => {
4 | WebFont.load({
5 | typekit: {
6 | id: id
7 | },
8 | active: resolve
9 | });
10 | });
11 | };
12 |
13 | const randomNumber = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
14 |
15 | export {
16 | preloadFonts,
17 | randomNumber
18 | }
--------------------------------------------------------------------------------