├── .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 |
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 |
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 |
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 |
SVG Animation Primitives
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 |
173 |
174 |
175 |
176 |
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 |
217 |
218 |
219 |
220 |
221 |
Fluid 3D Cube Carousel
222 |
223 |
224 |
225 |
226 |

227 |
228 |
229 |

230 |
231 |
232 |

233 |
234 |
235 |

236 |
237 |
238 |
239 |
240 |
241 |
242 |
247 |
248 |
253 |
254 |
259 |
260 |
265 |
266 |
267 |
268 |
269 |
270 |
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 |
8 |
--------------------------------------------------------------------------------
/media/front.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/media/left.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/media/right.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------