├── .editorconfig ├── .gitignore ├── 3d-carousel.css ├── 3d-carousel.js ├── LICENSE ├── README.md ├── calendar.css ├── calendar.js ├── debog.js ├── debog.svg ├── draw-interactive-connection.js ├── favicon.svg ├── fortress.js ├── fortress.svg ├── index.html ├── kitt-like-lights.css ├── kitt-like-lights.js ├── marching-ants.js ├── media ├── back.svg ├── front.svg ├── left.svg └── right.svg ├── padlock.js ├── padlock.svg ├── search.js ├── search.svg ├── starfield.js ├── sun.svg ├── svg-morph.js ├── uidrafter-logo.js └── uidrafter-logo.svg /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = tab 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /3d-carousel.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --accent: #ff4000; 3 | --accentDark: darkblue; 4 | --accentHover: #7e2708; 5 | --shadow: 0 2px 2px -1px rgba(0, 0, 0, .19), 0 1px 4px 0 rgba(0, 0, 0, .17), 0 3px 1px -2px rgba(0, 0, 0, .25); 6 | } 7 | 8 | html { 9 | overflow-x: hidden; /* Firefox fix for the cube when rotating */ 10 | } 11 | 12 | .CubeCarousel { 13 | position: relative; 14 | width: 80%; 15 | margin: 0 auto; 16 | } 17 | 18 | .CubeScene img { 19 | display: block; 20 | width: 100%; 21 | height: auto; 22 | transform: scale(0.99999999); 23 | box-shadow: var(--shadow); 24 | } 25 | 26 | .CubeControls { 27 | margin-top: 20px; 28 | text-align: center; 29 | } 30 | .cubeRadioButton { 31 | display: inline-block; 32 | position: relative; 33 | padding: 20px 6px 8px; 34 | background: none; 35 | border: 0; 36 | color: #ccc; 37 | font-size: 15px; 38 | cursor: pointer; 39 | transition-property: box-shadow; 40 | } 41 | .cubeRadioButton input { 42 | width: 0; 43 | height: 0; 44 | position: absolute; 45 | } 46 | .cubeRadioButton:not(.cubeSelected):hover { 47 | background: var(--accentHover); 48 | } 49 | .cubeRadioButton:not(:last-of-type) { 50 | margin-right: 4px; 51 | } 52 | .cubeRadioButton .title { 53 | display: block; 54 | padding: 2px 3px; 55 | line-height: 1.2; 56 | } 57 | .cubeRadioButton:active:not(.cubeSelected) { 58 | transition: none; 59 | } 60 | .cubeRadioButton.cubeSelected { 61 | cursor: default; 62 | pointer-events: none; 63 | } 64 | .cubeRadioButton .icon, 65 | .cubeRadioButton.cubeSelected .icon { 66 | position: relative; 67 | top: -8px; 68 | left: calc(50% - 10px); 69 | display: block; 70 | width: 20px; 71 | height: 20px; 72 | border: 2px solid var(--accent); 73 | border-radius: 15px; 74 | } 75 | .cubeRadioButton.cubeSelected .icon { 76 | background: var(--accent); 77 | } 78 | .cubeRadioButton input:focus + .icon { 79 | animation: _pulseScale 500ms infinite alternate-reverse ease-in-out; 80 | } 81 | @keyframes _pulseScale { 82 | 0% { 83 | transform: scale(0.92); 84 | } 85 | 100% { 86 | transform: scale(1.15); 87 | } 88 | } 89 | .Cube { 90 | position: relative; 91 | width: 100%; 92 | height: 100%; 93 | transform-style: preserve-3d; 94 | } 95 | .cf { 96 | position: absolute; 97 | top: 0; 98 | display: none; 99 | } 100 | -------------------------------------------------------------------------------- /3d-carousel.js: -------------------------------------------------------------------------------- 1 | // CSS: ./3d-carousel.css 2 | 3 | const FaceIdAttr = 'data-for' 4 | 5 | const FaceAttr = 'data-cubeface' 6 | const FrontFace = 'cfFront' 7 | const RightFace = 'cfRight' 8 | const BackFace = 'cfBack' 9 | const LeftFace = 'cfLeft' 10 | 11 | const cRadioBtn = 'cubeRadioButton' 12 | const cSelected = 'cubeSelected' 13 | 14 | const cCubeScene = 'CubeScene' 15 | const cCube = 'Cube' 16 | 17 | const FaceElement = 'img' 18 | const RotateDuration_ms = 740 19 | 20 | /** 21 | * This code renders a cube on top of the face-element (e.g. video) during the 22 | * rotation transition. i.e. it's a cube on top of an opacity=0 face-element for the 23 | * duration of the animation. It's like this, because the `.vpRoot` 24 | * container is fluid, if it was discrete it would have been much easier. 25 | */ 26 | InitVideoGroups(document.body) 27 | function InitVideoGroups(Section) { 28 | for (const button of Section.getElementsByClassName(cRadioBtn)) 29 | button.addEventListener('change', function () { 30 | if (this.className.indexOf(cSelected) === -1) { // isNotSelected 31 | const oldBtn = Section.getElementsByClassName(cSelected)[0] 32 | const oldFace = document.getElementById(oldBtn.getAttribute(FaceIdAttr)).querySelector(FaceElement) 33 | const faceWidth = parseFloat(getComputedStyle(oldFace).width) 34 | 35 | RotateCube(Section, faceWidth, oldBtn.getAttribute(FaceAttr), this.getAttribute(FaceAttr)) 36 | oldBtn.className = cRadioBtn 37 | this.className = cRadioBtn + ' ' + cSelected 38 | } 39 | }) 40 | } 41 | 42 | let timer0 43 | function RotateCube(Section, faceWidth, fromFace, toFace) { 44 | clearTimeout(timer0) // For fast clicking. It handles changing a video during a rotation. 45 | const CUBE_T = 'transform ' + RotateDuration_ms + 'ms ease-in-out' 46 | const scene = Section.getElementsByClassName(cCubeScene)[0] 47 | scene.style.perspective = faceWidth * 2 + 'px' 48 | 49 | updateFace(FrontFace, 0) 50 | updateFace(RightFace, 90) 51 | updateFace(BackFace, 180) 52 | updateFace(LeftFace, -90) 53 | 54 | function updateFace(className, angle) { 55 | const wrap = Section.getElementsByClassName(className)[0] 56 | if (wrap) { 57 | wrap.style.transform = 'rotateY(' + angle + 'deg) translateZ(' + faceWidth / 2 + 'px)' 58 | wrap.style.display = 'block' 59 | wrap.style.backfaceVisibility = 'hidden' 60 | // Hiding the backfaceVisibility is also a workaround for a glitch when resizing. 61 | // For instance, if we did transform: rotateY(-90deg) translateX(100%) 62 | // rotateY(90deg); see https://stackoverflow.com/a/20226596 this would 63 | // not be needed to fix the side faces going visible through the sides 64 | } 65 | } 66 | 67 | 68 | // This is needed because ATM, we need the width to set initial face 69 | // cube in 3D space. Therefore, we are going through this overhead 70 | // for now. See the right way in the backfaceVisibility comment above 71 | const cube = Section.getElementsByClassName(cCube)[0] 72 | cube.style.transform = cubeRotations(fromFace) 73 | timer0 = setTimeout(function () { 74 | cube.style.transition = CUBE_T 75 | cube.style.transform = cubeRotations(toFace) 76 | allowFocusOnVisibleVideoFace() 77 | }, 100) // I don't know why this has to be longer than one raf 78 | 79 | 80 | function allowFocusOnVisibleVideoFace() { // only for videos 81 | const faces = Section.getElementsByTagName('video') 82 | for (const face of faces) 83 | face.nextElementSibling.setAttribute('tabindex', '-1') 84 | 85 | const currVideoPlayButton = Section.querySelector('.' + toFace + ' video + button') 86 | if (currVideoPlayButton) 87 | currVideoPlayButton.setAttribute('tabindex', '0') 88 | } 89 | 90 | function cubeRotations(face) { 91 | switch (face) { 92 | case FrontFace: 93 | return 'translateZ(' + faceWidth / -2 + 'px) rotateY(0deg)' 94 | case RightFace: 95 | return 'translateZ(' + faceWidth / -2 + 'px) rotateY(-90deg)' 96 | case BackFace: 97 | return 'translateZ(' + faceWidth / -2 + 'px) rotateY(-180deg)' 98 | case LeftFace: 99 | return 'translateZ(' + faceWidth / -2 + 'px) rotateY(90deg)' 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Eric Fortis 2 | 3 | web-animations 4 | 5 | Permission to use, copy, modify, and/or distribute this software for 6 | any purpose with or without fee is hereby granted, provided that the 7 | above copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 11 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, 12 | OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA 13 | OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 14 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web Animation Examples 2 | 3 | Learn web frontend animation from the ground up. All the 4 | examples are in plain JavaScript, CSS, HTML, and SVG. 5 | 6 | ## [Live Demo](https://ericfortis.github.io/web-animations/) 7 | -------------------------------------------------------------------------------- /calendar.css: -------------------------------------------------------------------------------- 1 | #CalendarWrap { 2 | line-height: 0; 3 | height: calc((13px + 2px) * 7); 4 | text-align: left; 5 | } 6 | 7 | #CalendarWrap .Day { 8 | display: inline-block; 9 | width: calc(140px / 7); 10 | height: 13px; 11 | margin: 2px 2px 0 0; 12 | animation: _kfFadeIn 120ms ease-in-out; 13 | animation-fill-mode: forwards; 14 | opacity: 0; 15 | } 16 | 17 | #CalendarWrap .Spacer { 18 | visibility: hidden; 19 | } 20 | #CalendarWrap .On { 21 | background: #ff4000; 22 | } 23 | #CalendarWrap .Off { 24 | background: #555; 25 | } 26 | 27 | @keyframes _kfFadeIn { 28 | 0% { 29 | opacity: 0; 30 | } 31 | 100% { 32 | opacity: 1; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /calendar.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('load', AnimateCalendar) 2 | 3 | function AnimateCalendar() { 4 | const Calendar = document.getElementById('CalendarWrap') 5 | Calendar.removeEventListener('click', AnimateCalendar) 6 | 7 | Calendar.innerHTML = '' 8 | let nOff = 0 9 | const msSpeed = 30 10 | const nDays = (7 * 7 - 3) 11 | const frag = document.createDocumentFragment() 12 | for (let i = 0; i < nDays; i++) { 13 | const day = document.createElement('div') 14 | if (i < 4) 15 | day.className = 'Day Spacer' 16 | else if (i < 25) { 17 | day.className = 'Day Off' 18 | day.style.animationDelay = nOff++ * msSpeed + 'ms' 19 | } 20 | else { 21 | day.className = 'Day On' 22 | day.style.animationDelay = (nDays - i) * msSpeed + 'ms' 23 | } 24 | frag.appendChild(day) 25 | } 26 | Calendar.appendChild(frag) 27 | 28 | // Throttle Click 29 | Promise.all(Calendar.getAnimations({ subtree: true }).map(a => a.finished)).then(() => 30 | Calendar.addEventListener('click', AnimateCalendar)) 31 | } 32 | -------------------------------------------------------------------------------- /debog.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('load', AnimateDebog) 2 | 3 | function AnimateDebog() { 4 | const Debog = byId('Debog') 5 | Debog.removeEventListener('click', AnimateDebog) 6 | 7 | const WingL = byId('WingL') 8 | const WingR = byId('WingR') 9 | const Mouth = byId('Mouth') 10 | const Nose = byId('Nose') 11 | const Legs = byId('Legs') 12 | 13 | const Letters = Array.from(document.querySelectorAll('.Letters path')) 14 | const all = [WingL, WingR, Mouth, Nose, Legs].concat(Letters) 15 | 16 | all.forEach(elem => elem.style.opacity = 0) 17 | 18 | animateRotation(WingL, -360, 360 * 4, 2000) 19 | animateRotation(WingR, 0, 360 * 6, 2200) 20 | animateStrokePainting(Mouth, 2300) 21 | animateStrokePainting(Nose, 2300) 22 | 23 | setTimeout(function () { 24 | animatePositionY(Legs, -50, 0, 180) 25 | }, 1900) 26 | 27 | Letters.forEach((letter, i) => { 28 | setTimeout(() => { 29 | animatePositionY(letter, -80, 0, 120) 30 | }, 100 * i + 1600) 31 | }) 32 | 33 | setTimeout(function ThrottleClick() { 34 | Debog.addEventListener('click', AnimateDebog) 35 | }, 2200) 36 | 37 | function animateRotation(elem, startAngle, endAngle, duration) { 38 | const start = Date.now() 39 | elem.style.opacity = 1 40 | requestAnimationFrame(function anim() { 41 | const normTime = (Date.now() - start) / duration 42 | const angle = easeOutQuad(normTime) * (endAngle - startAngle) + startAngle 43 | elem.style.transform = `rotate(${angle}deg)` 44 | if (normTime < 1) 45 | requestAnimationFrame(anim) 46 | }) 47 | } 48 | 49 | function animateStrokePainting(elem, duration) { 50 | const length = elem.getTotalLength() 51 | const start = Date.now() 52 | elem.style.opacity = 1 53 | elem.style.strokeDasharray = length 54 | elem.style.strokeDashoffset = length 55 | requestAnimationFrame(function anim() { 56 | const normTime = (Date.now() - start) / duration 57 | elem.style.strokeDashoffset = (length * easeOutQuad(normTime)) + length 58 | if (normTime < 1) 59 | requestAnimationFrame(anim) 60 | }) 61 | } 62 | 63 | function animatePositionY(elem, startY, endY, duration) { 64 | const start = Date.now() 65 | requestAnimationFrame(function anim() { 66 | const normTime = (Date.now() - start) / duration 67 | elem.style.opacity = normTime 68 | const y = easeOutQuad(normTime) * (endY - startY) + startY 69 | elem.style.transform = `translate(0, ${y}px)` 70 | if (normTime < 1) 71 | requestAnimationFrame(anim) 72 | }) 73 | } 74 | 75 | function byId(id) { 76 | return document.getElementById(id) 77 | } 78 | 79 | function easeOutQuad(x) { // https://easings.net/#easeOutQuad 80 | return x * (2 - x) 81 | } 82 | } 83 | 84 | 85 | -------------------------------------------------------------------------------- /debog.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 34 | 37 | 40 | 44 | 48 | 51 | 52 | 53 | 56 | 59 | 62 | 65 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /draw-interactive-connection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A Bezier curve between two Points for drawing smooth connections. 3 | * Although we use for SVG, it works for canvas too. 4 | * Path "M:p1 C:p1Anchor p2Anchor p2" 5 | */ 6 | function describeCurve(x1, y1, x2, y2) { 7 | const xAnchorDelta = ((Math.abs(y2 - y1) + Math.abs(x2 - x1)) >> 2) | 0 8 | return `M${x1},${y1} C${x1 + xAnchorDelta},${y1} ${x2 - xAnchorDelta},${y2} ${x2},${y2}` 9 | } 10 | 11 | 12 | window.addEventListener('load', function () { 13 | const board = document.getElementById('InteractiveConnectionTarget') 14 | const conn = document.getElementById('InteractiveConnection') 15 | 16 | let xStart = 0 17 | let yStart = 0 18 | 19 | function onMove({ offsetX, offsetY }) { 20 | if (offsetX > xStart) 21 | conn.setAttribute('d', describeCurve(xStart, yStart, offsetX, offsetY)) 22 | else 23 | conn.setAttribute('d', describeCurve(offsetX, offsetY, xStart, yStart)) 24 | } 25 | 26 | board.addEventListener('pointerdown', function (event) { 27 | event.preventDefault() 28 | board.setPointerCapture(event.pointerId) 29 | xStart = event.offsetX 30 | yStart = event.offsetY 31 | board.addEventListener('pointermove', onMove) 32 | }) 33 | 34 | board.onpointerup = function () { 35 | board.removeEventListener('pointermove', onMove) 36 | } 37 | 38 | board.onpointercancel = function (event) { 39 | board.releasePointerCapture(event.pointerId) 40 | board.removeEventListener('pointermove', onMove) 41 | } 42 | }) 43 | -------------------------------------------------------------------------------- /favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /fortress.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('load', function () { 2 | const Fortress = document.getElementById('Fortress') 3 | const bottomDoor = Fortress.querySelector('.BottomDoor') 4 | const middleWindows = Fortress.querySelector('.MiddleWindows') 5 | const topLeftWindow = Fortress.querySelector('.TopLeftWindow') 6 | const topRightWindow = Fortress.querySelector('.TopRightWindow') 7 | 8 | const duration = 120 9 | const offDelay = 400 10 | 11 | animate() 12 | function animate() { 13 | Fortress.removeEventListener('click', animate) 14 | 15 | fadeIn(bottomDoor, 0) 16 | fadeIn(middleWindows, 1 * duration) 17 | fadeIn(topLeftWindow, 2 * duration) 18 | fadeIn(topRightWindow, 3 * duration) 19 | fadeOut(middleWindows, 4 * duration + offDelay) 20 | fadeOut(topLeftWindow, 5 * duration + offDelay) 21 | fadeOut(topRightWindow, 6 * duration + offDelay) 22 | fadeOut(bottomDoor, 7 * duration + offDelay) 23 | 24 | setTimeout(() => { 25 | Fortress.addEventListener('click', animate) 26 | }, 7 * duration + offDelay) 27 | } 28 | 29 | 30 | function fadeIn(element, delay) { 31 | setTimeout(function () { 32 | animateOpacity(element, 0, 1) 33 | }, delay) 34 | } 35 | 36 | function fadeOut(element, delay) { 37 | setTimeout(function () { 38 | animateOpacity(element, 1, 0) 39 | }, delay) 40 | } 41 | 42 | function animateOpacity(element, startOpacity, endOpacity) { 43 | const start = Date.now() 44 | requestAnimationFrame(function anim() { 45 | const normTime = (Date.now() - start) / duration 46 | const opacity = easeOutQuad(normTime) * (endOpacity - startOpacity) + startOpacity 47 | element.setAttribute('opacity', opacity) 48 | if (normTime < 1) 49 | requestAnimationFrame(anim) 50 | }) 51 | } 52 | 53 | function easeOutQuad(x) { // https://easings.net/#easeOutQuad 54 | return x * (2 - x) 55 | } 56 | }) 57 | -------------------------------------------------------------------------------- /fortress.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Web Frontend Animations (by Eric Fortis) 6 | 7 | 8 | 9 | 122 | 123 | 124 | 125 | 126 | 127 |

Web Animations

128 |

129 | Open source plain JS, CSS, HTML, SVG animations for learning web frontend development. 130 | Source code at Github. 131 |

132 | 133 |

Click the animations to replay them.

134 | 135 | 136 |
137 |
138 |

3D Spin (frame-by-frame)

139 | 140 |
141 | 142 | 143 |
144 | 145 | 146 | 147 | 148 |
149 | 150 | 151 |
152 |

SVG Morph

153 | 154 | 155 |

Play to Pause (polygon)
156 | Car to Bus (path)

157 |
158 | 159 | 160 |
161 |

KITT-like lights

162 |
163 |
164 | 165 | 166 |
167 |

Marching Ants

168 | 169 | 170 | 171 | 172 | 173 |
174 | 175 | 176 |
177 |

SVG animateTransform

178 | Sun Animation by Vincent Diaz 179 |

Artwork by Vincent Diaz

180 |
181 | 182 | 183 |
184 |

Calendar (weeks faster)

185 |
186 |
187 | 188 | 189 |
190 |

Fortress

191 | 192 |
193 | 194 | 195 |
196 |

Searching

197 | 198 |
199 | 200 | 201 |
202 |

Padlock

203 | 204 |
205 | 206 | 207 |
208 |

Starfield

209 |
210 |
211 | 212 |
213 |

Draw Interactive Connection

214 | 215 | 216 | 217 |
218 | 219 | 220 |
221 | 222 |
223 |
224 |
225 |
226 | Left 227 |
228 |
229 | Front 230 |
231 |
232 | Right` 233 |
234 |
235 | Back` 236 |
237 |
238 |
239 |
240 | 241 |
242 | 247 | 248 | 253 | 254 | 259 | 260 | 265 |
266 |
267 |
268 | 269 | 270 |
271 |

Source Code

272 |

273 | Github 274 |

275 | 276 | 277 |

See also

278 |

279 | Web Tricks 280 |

281 |

282 | Web Frontend Projects 283 |

284 | 285 | 286 | 295 |
296 | 297 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | -------------------------------------------------------------------------------- /kitt-like-lights.css: -------------------------------------------------------------------------------- 1 | .Kitt .light { 2 | display: inline-block; 3 | width: 32px; 4 | height: 10px; 5 | border: 1px solid #333; 6 | margin-right: 2px; 7 | transition: all 120ms ease-in-out; 8 | } 9 | 10 | .Kitt .light:last-of-type { 11 | margin-right: 0; 12 | } 13 | 14 | .Kitt .light.on { 15 | border-color: #ff4000; 16 | background: #ffa080; 17 | box-shadow: 0 0 32px #ff4000, 0 0 10px #ff4000, 0 0 24px #ff4000 inset; 18 | } 19 | -------------------------------------------------------------------------------- /kitt-like-lights.js: -------------------------------------------------------------------------------- 1 | // Knight Rider's KITT-like lights 2 | // https://www.youtube.com/watch?v=oNyXYPhnUIs 3 | 4 | window.addEventListener('load', function () { 5 | const cKitt = 'Kitt' 6 | const cLight = 'light' 7 | const cOn = 'on' // This CSS class makes a light shine. 8 | const nLights = 6 9 | const msSpeed = 166 10 | 11 | const Lights = [] 12 | for (let i = 0; i < nLights; i++) { 13 | const light = document.createElement('div') 14 | light.className = cLight 15 | Lights.push(light) 16 | } 17 | document.querySelector('.' + cKitt).append(...Lights) 18 | 19 | const states = makeKittStates(nLights) 20 | let nState = 0 21 | setInterval(() => { 22 | for (let i = 0; i < Lights.length; i++) 23 | Lights[i].classList.toggle(cOn, states[nState][i]) 24 | nState = (nState + 1) % states.length 25 | }, msSpeed) 26 | }) 27 | 28 | 29 | ;(function test() { 30 | const actual = makeKittStates(3) 31 | const expected = [ 32 | [0, 0, 1], 33 | [0, 1, 0], 34 | [1, 0, 0], 35 | [0, 1, 0], 36 | [0, 0, 1], 37 | [0, 1, 1], 38 | [1, 1, 1], 39 | [1, 1, 0], 40 | [1, 0, 0], 41 | [1, 1, 0], 42 | [1, 1, 1], 43 | [0, 1, 1] 44 | ] 45 | 46 | for (let i = 0; i < expected.length; i++) { 47 | if (expected[i].length !== actual[i].length) 48 | throw `FAILED: The arrays at ${i} have different number of lights` 49 | 50 | for (let j = 0; j < expected[i].length; j++) 51 | if (expected[i][j] !== actual[i][j]) 52 | throw `FAILED: The array at ${i} has a light at index: ${j} that doesn't match` 53 | } 54 | }()) 55 | 56 | 57 | // Each light state is represented by a bit of an integer. 58 | function makeKittStates(nLights) { 59 | const binaryMaxValue = 2 ** nLights - 1 // e.g. 6 -> 0b111111 60 | const leftmostBit = 1 << (nLights - 1) // e.g. 6 -> 0b100000 61 | 62 | let bitmap = 1 63 | const states = [bitmap] 64 | 65 | while (bitmap < leftmostBit) { 66 | bitmap <<= 1 67 | states.push(bitmap) 68 | } 69 | 70 | while (bitmap > 1) { 71 | bitmap >>= 1 72 | states.push(bitmap) 73 | } 74 | 75 | let rev = 1 76 | while (bitmap < binaryMaxValue) { 77 | rev <<= 1 78 | bitmap += rev 79 | states.push(bitmap) 80 | } 81 | 82 | rev = 1 83 | while (bitmap > leftmostBit) { 84 | bitmap -= rev 85 | rev <<= 1 86 | states.push(bitmap) 87 | } 88 | 89 | rev = leftmostBit 90 | while (bitmap < binaryMaxValue) { 91 | rev >>= 1 92 | bitmap += rev 93 | states.push(bitmap) 94 | } 95 | 96 | rev = leftmostBit 97 | while (bitmap > 2) { 98 | bitmap -= rev 99 | rev >>= 1 100 | states.push(bitmap) 101 | } 102 | 103 | return states.map(lights => lights // Numbers to BitArrays 104 | .toString(2) // to binary string 105 | .padStart(nLights, '0') 106 | .split('') 107 | .map(Number)) 108 | } 109 | -------------------------------------------------------------------------------- /marching-ants.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | document.querySelectorAll('.marchingAnts').forEach(animateLine) 3 | 4 | function animateLine({ style }) { 5 | const SPEED = 0.2 6 | let offset = 0 7 | style.strokeDasharray = '6 2' 8 | requestAnimationFrame(function anim() { 9 | offset -= SPEED // The `offset` "extra" variable is for FF and Safari 10 | style.strokeDashoffset = offset 11 | requestAnimationFrame(anim) 12 | }) 13 | } 14 | }()) 15 | -------------------------------------------------------------------------------- /media/back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Back 6 | 7 | 8 | -------------------------------------------------------------------------------- /media/front.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Front 6 | 7 | 8 | -------------------------------------------------------------------------------- /media/left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Left 6 | 7 | 8 | -------------------------------------------------------------------------------- /media/right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Right 6 | 7 | 8 | -------------------------------------------------------------------------------- /padlock.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('load', function () { 2 | 3 | AnimateLock() 4 | function AnimateLock() { 5 | const Lock = document.getElementById('Padlock') 6 | Lock.removeEventListener('click', AnimateLock) 7 | 8 | const LockShape = Lock.getElementById('PadlockShape') 9 | 10 | let delay = 0 11 | let lastRotation = 0 12 | let duration = 400 13 | let rotation = 11 14 | 15 | rotateLock(lastRotation, rotation, duration, delay) 16 | 17 | lastRotation = rotation 18 | rotation = -12 19 | delay += duration 20 | duration = 300 21 | rotateLock(lastRotation, rotation, duration, delay) 22 | 23 | lastRotation = rotation 24 | rotation = 8 25 | delay += duration 26 | duration = 300 27 | rotateLock(lastRotation, rotation, duration, delay) 28 | 29 | lastRotation = rotation 30 | rotation = -6 31 | delay += duration 32 | duration = 300 33 | rotateLock(lastRotation, rotation, duration, delay) 34 | 35 | lastRotation = rotation 36 | rotation = 4 37 | delay += duration 38 | duration = 300 39 | rotateLock(lastRotation, rotation, duration, delay) 40 | 41 | lastRotation = rotation 42 | rotation = -2 43 | delay += duration 44 | duration = 200 45 | rotateLock(lastRotation, rotation, duration, delay) 46 | 47 | lastRotation = rotation 48 | rotation = 0 49 | delay += duration 50 | duration = 200 51 | rotateLock(lastRotation, rotation, duration, delay) 52 | 53 | setTimeout(function throttle() { 54 | Lock.addEventListener('click', AnimateLock) 55 | }, duration + delay) 56 | 57 | function rotateLock(startRotation, endRotation, durationMs, delayMs) { 58 | setTimeout(() => animateRotation({ 59 | element: LockShape, 60 | centerX: 75, 61 | centerY: 27, 62 | startRotation, 63 | endRotation, 64 | durationMs 65 | }), delayMs) 66 | } 67 | } 68 | 69 | function animateRotation(o) { 70 | animate() 71 | function animate() { 72 | const start = Date.now() 73 | requestAnimationFrame(function anim() { 74 | const time = Math.min(1, (Date.now() - start) / o.durationMs) 75 | const angle = easeOutQuad(time) * (o.endRotation - o.startRotation) + o.startRotation 76 | o.element.setAttribute('transform', `rotate( ${angle}, ${o.centerX}, ${o.centerY})`) 77 | if (time < 1) 78 | requestAnimationFrame(anim) 79 | }) 80 | } 81 | } 82 | 83 | function easeOutQuad(x) { // https://easings.net/#easeOutQuad 84 | return x * (2 - x) 85 | } 86 | }) 87 | -------------------------------------------------------------------------------- /padlock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /search.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('load', function () { 2 | const Search = document.getElementById('Search') 3 | const glass = Search.querySelector('.MagnifierGlass') 4 | 5 | const duration = 280 6 | 7 | AnimateSearch() 8 | function AnimateSearch() { 9 | Search.removeEventListener('click', AnimateSearch) 10 | let delay = 0 11 | animateGlassPosition(0, 0, 60, 0, delay) 12 | 13 | delay += duration 14 | animateGlassPosition(60, 0, 0, 25, delay) 15 | 16 | delay += duration 17 | animateGlassPosition(0, 25, 60, 25, delay) 18 | 19 | delay += duration 20 | animateGlassPosition(60, 25, 0, 50, delay) 21 | 22 | delay += duration 23 | animateGlassPosition(0, 50, 42, 50, delay) 24 | 25 | delay += duration + 800 26 | animateGlassPosition(42, 50, 0, 0, delay) 27 | 28 | setTimeout(function throttle() { 29 | Search.addEventListener('click', AnimateSearch) 30 | }, delay + duration) 31 | } 32 | 33 | function animateGlassPosition(x0, y0, x1, y1, delayMs) { 34 | setTimeout(() => animateXY(x0, y0, x1, y1), delayMs) 35 | } 36 | 37 | function animateXY(x0, y0, x1, y1) { 38 | const elem = glass 39 | animate() 40 | function animate() { 41 | const start = Date.now() 42 | requestAnimationFrame(function anim() { 43 | const time = Math.min(1, (Date.now() - start) / duration) 44 | const positionX = easeOutQuad(time) * (x1 - x0) + x0 45 | const positionY = easeOutQuad(time) * (y1 - y0) + y0 46 | elem.setAttribute('transform', `translate(${positionX}, ${positionY})`) 47 | if (time < 1) 48 | requestAnimationFrame(anim) 49 | }) 50 | } 51 | } 52 | 53 | function easeOutQuad(x) { // https://easings.net/#easeOutQuad 54 | return x * (2 - x) 55 | } 56 | }) 57 | -------------------------------------------------------------------------------- /search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /starfield.js: -------------------------------------------------------------------------------- 1 | // Based on Coding Math 2 | // @bit101 3 | // https://www.youtube.com/watch?v=OuzWDQ6zFXo 4 | 5 | Starfield(document.getElementById('Starfield')) 6 | 7 | function Starfield(target) { 8 | if (!window.requestAnimationFrame) 9 | return 10 | 11 | const colors = ['#aaa', '#eee', '#fffbce'] 12 | const maxSize = 1.5 13 | 14 | const canvas = document.createElement('canvas') 15 | const context = canvas.getContext('2d') 16 | canvas.style.background = getComputedStyle(target).background 17 | target.insertAdjacentElement('beforebegin', canvas) 18 | target.style.background = 'none' 19 | 20 | let stars = [] 21 | let raf, maxX, maxY 22 | 23 | init() 24 | window.addEventListener('resize', init) 25 | 26 | function init() { 27 | cancelAnimationFrame(raf) 28 | const nStars = window.innerWidth * 0.2 | 0 29 | maxX = target.offsetWidth 30 | maxY = target.offsetHeight 31 | canvas.width = maxX 32 | canvas.height = maxY 33 | stars = [] 34 | 35 | for (let i = 0; i < nStars; i++) 36 | stars.push({ 37 | x: Math.random() * maxX, 38 | y: Math.random() * maxY, 39 | vx: Math.random() * 0.2, // velocity 40 | vy: Math.random() * -0.2, 41 | size: Math.random() * maxSize, 42 | dsize: Math.random() * 0.03, // delta size 43 | color: colors[i % colors.length] 44 | }) 45 | render() 46 | } 47 | 48 | function render() { 49 | context.clearRect(0, 0, maxX, maxY) 50 | context.globalAlpha = 0.8 51 | 52 | for (const star of stars) { 53 | star.x += star.vx 54 | star.y += star.vy 55 | star.size += star.dsize 56 | 57 | if (star.x > maxX) 58 | star.x = 0 59 | else if (star.x < 0) 60 | star.x = maxX 61 | 62 | if (star.y > maxY) 63 | star.y = 0 64 | else if (star.y < 0) 65 | star.y = maxY 66 | 67 | if (star.size > maxSize || star.size < 0) { 68 | star.dsize *= -1 69 | star.size = Math.abs(star.size) 70 | } 71 | 72 | context.beginPath() 73 | context.arc(star.x, star.y, star.size, 0, Math.PI * 2) 74 | context.fillStyle = star.color 75 | context.fill() 76 | } 77 | 78 | raf = requestAnimationFrame(render) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /sun.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 11 | 13 | 16 | 19 | 20 | 23 | 31 | 32 | 33 | 36 | 44 | 45 | 46 | 48 | 49 | 52 | 53 | 55 | 57 | 60 | 62 | 64 | 67 | 69 | 71 | 74 | 76 | 78 | 81 | 83 | 85 | 88 | 90 | 92 | 95 | 97 | 99 | 102 | 104 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /svg-morph.js: -------------------------------------------------------------------------------- 1 | // Based on: 2 | // https://css-tricks.com/svg-shape-morphing-works/ 3 | 4 | // For converting paths to polygons: 5 | // https://betravis.github.io/shape-tools/path-to-polygon/ 6 | 7 | (function () { 8 | // For having the same number of points, the BUS icon was drawn starting 9 | // with the CAR icon and moving points (no adding, no removing) 10 | svgMorph({ 11 | parent: document.getElementById('SvgMorphCarToBus'), 12 | viewBox: '0 0 24 24', 13 | shapeTagName: 'path', 14 | startShapes: ['M 18.92,6.01 C 18.72,5.42 18.16,5 17.5,5 H 6.5 C 5.84,5 5.29,5.42 5.08,6.01 L 3,12 v 8 c 0,0.55 0.45,1 1,1 h 1 c 0.55,0 1,-0.45 1,-1 v -1 h 12 v 1 c 0,0.55 0.45,1 1,1 h 1 c 0.55,0 1,-0.45 1,-1 V 12 Z M 7.5,16 C 6.67,16 6,15.33 6,14.5 6,13.67 6.67,13 7.5,13 8.33,13 9,13.67 9,14.5 9,15.33 8.33,16 7.5,16 Z m 9,0 C 15.67,16 15,15.33 15,14.5 15,13.67 15.67,13 16.5,13 17.33,13 18,13.67 18,14.5 18,15.33 17.33,16 16.5,16 Z M 5.81,10 6.85,7 h 10.29 l 1.04,3 z'], // car 15 | endShapes: ['m 20.9,4.8 c -0.2,-0.6 -0.75,-1 -1.42,-1 h -15 c -0.7,0 -1.21,0.42 -1.42,1.01 L 3,12 v 8 c 0,0.55 0.45,1 1,1 h 1 c 0.55,0 1,-0.45 1,-1 v -1 h 12 v 1 c 0,0.55 0.45,1 1,1 h 1 c 0.55,0 1,-0.45 1,-1 V 12 Z M 5.9,17.6 c -0.83,0 -1.5,-0.67 -1.5,-1.5 0,-0.83 0.67,-1.5 1.5,-1.5 0.83,0 1.5,0.67 1.5,1.5 0,0.83 -0.67,1.5 -1.5,1.5 z m 12.4,0.07 c -0.83,0 -1.5,-0.67 -1.5,-1.5 0,-0.83 0.67,-1.5 1.5,-1.5 0.83,0 1.5,0.67 1.5,1.5 0,0.83 -0.67,1.5 -1.5,1.5 z M 4.82,11.78 4.85,5.78 h 14.29 l 0.11,6 z'], // bus 16 | fill: '#ff4000', 17 | dur: '240ms' 18 | }) 19 | 20 | // These polygons also have the same number points 21 | svgMorph({ 22 | parent: document.getElementById('SvgMorphPlayToPause'), 23 | viewBox: '0 0 24 24', 24 | shapeTagName: 'polygon', 25 | startShapes: ['6.5 19, 17.5 12, 17.5 12, 6.5 5'], // Play icon 26 | endShapes: ['6 19, 10 19, 10 5, 6 5', '14 5, 14 19, 18 19, 18 5'], // Pause icon 27 | fill: '#ff4000', 28 | dur: '240ms' 29 | }) 30 | 31 | function svgMorph({ parent, viewBox, shapeTagName, startShapes, endShapes, fill, dur }) { 32 | // Ensure both states have the same number of shapes by appending the last one 33 | while (endShapes.length > startShapes.length) startShapes.push(startShapes.at(-1)) 34 | while (endShapes.length < startShapes.length) endShapes.push(endShapes.at(-1)) 35 | 36 | const attributeName = { path: 'd', polygon: 'points' }[shapeTagName] 37 | 38 | const startAnimations = [] 39 | const endAnimations = [] 40 | const svg = createSvgElement('svg', { viewBox }) 41 | 42 | for (let i = 0; i < startShapes.length; i++) { 43 | const startPoints = startShapes[i] 44 | const endPoints = endShapes[i] 45 | 46 | const poly = createSvgElement(shapeTagName, { 47 | fill, 48 | [attributeName]: startPoints 49 | }) 50 | const toEnd = createSvgElement('animate', { 51 | to: endPoints, 52 | dur, 53 | fill: 'freeze', 54 | begin: 'indefinite', 55 | attributeName 56 | }) 57 | const toStart = createSvgElement('animate', { 58 | to: startPoints, 59 | dur, 60 | fill: 'freeze', 61 | begin: 'indefinite', 62 | attributeName 63 | }) 64 | 65 | endAnimations.push(toEnd) 66 | startAnimations.push(toStart) 67 | poly.append(toEnd, toStart) 68 | svg.append(poly) 69 | } 70 | 71 | let animationEnded = false 72 | parent.append(svg) 73 | parent.addEventListener('click', function () { 74 | for (const anim of animationEnded ? startAnimations : endAnimations) 75 | anim.beginElement() 76 | animationEnded = !animationEnded 77 | }) 78 | } 79 | 80 | function createSvgElement(tagName, props) { 81 | const elem = document.createElementNS('http://www.w3.org/2000/svg', tagName) 82 | for (const [key, value] of Object.entries(props)) 83 | elem.setAttribute(key, value) 84 | return elem 85 | } 86 | }()) 87 | -------------------------------------------------------------------------------- /uidrafter-logo.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('load', function () { 2 | const Logo = byId('UIDrafterLogo') 3 | 4 | const logoFrames = [ // SVG groups, each with the logo drawn in a different angle. 5 | byId('L00'), 6 | byId('L15'), 7 | byId('L30'), 8 | byId('L40') 9 | ] 10 | const frameSequence = [1, 2, 3, 3, 2, 1, 0] 11 | const fps = 30 12 | const frameDuration = 1000 / fps 13 | 14 | spinLogo() 15 | function spinLogo() { 16 | Logo.removeEventListener('click', spinLogo) 17 | 18 | setTimeout(function () { 19 | logoFrames[0].style.filter = '' 20 | }, frameSequence.length * frameDuration) 21 | 22 | for (let i = 0; i < frameSequence.length; i++) 23 | setTimeout(render.bind(null, frameSequence[i]), i * frameDuration) 24 | 25 | setTimeout(function throttle() { 26 | Logo.addEventListener('click', spinLogo) 27 | }, frameSequence.length * frameDuration) 28 | 29 | function render(frame) { 30 | for (let i = 0; i < logoFrames.length; i++) 31 | logoFrames[i].style.opacity = 0 32 | logoFrames[frame].style.opacity = 1 33 | logoFrames[frame].style.filter = 'url(#hBlur)' 34 | } 35 | } 36 | 37 | function byId(qs) { 38 | return document.getElementById(qs) 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /uidrafter-logo.svg: -------------------------------------------------------------------------------- 1 | 59 | --------------------------------------------------------------------------------