├── .DS_Store ├── .gitignore ├── README.md ├── dist ├── dynamowaves.d.ts ├── dynamowaves.js └── dynamowaves.min.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── dynamowaves.d.ts └── dynamowaves.js └── www ├── LICENSE ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── dynamowaves.d.ts ├── dynamowaves.min.js ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── index.html ├── main.css ├── main.css.map ├── main.scss ├── mstile-144x144.png ├── mstile-150x150.png ├── mstile-310x150.png ├── mstile-310x310.png ├── mstile-70x70.png ├── normalize.css ├── safari-pinned-tab.svg └── site.webmanifest /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzebley/dynamowaves/eef761843dbec62216f79252bbb8168f482728f0/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dynamowaves 2 | Animateable SVG waves that dynamically generate their shape on each render! 3 | 4 | [Documentation + examples](https://dynamowaves.markzebley.com) 5 | -------------------------------------------------------------------------------- /dist/dynamowaves.d.ts: -------------------------------------------------------------------------------- 1 | // dynamoves.d.ts 2 | 3 | // Interface for wave generation options 4 | interface WaveGenerationOptions { 5 | width: number; 6 | height: number; 7 | points: number; 8 | variance: number; 9 | vertical?: boolean; 10 | } 11 | 12 | // Interface for wave point structure 13 | interface WavePoint { 14 | cpX: number; 15 | cpY: number; 16 | x: number; 17 | y: number; 18 | } 19 | 20 | // Type for the wave direction 21 | type WaveDirection = 'top' | 'bottom' | 'left' | 'right'; 22 | 23 | // Interface for intersection observer options 24 | interface WaveObserverOptions { 25 | root: Element | null; 26 | rootMargin: string; 27 | threshold: number; 28 | } 29 | 30 | declare class DynamoWave extends HTMLElement { 31 | // Properties 32 | private isAnimating: boolean; 33 | private animationFrameId: number | null; 34 | private elapsedTime: number; 35 | private startTime: number | null; 36 | private isGeneratingWave: boolean; 37 | private currentPath: string | null; 38 | private targetPath: string | null; 39 | private pendingTargetPath: string | null; 40 | private intersectionObserver: IntersectionObserver | null; 41 | private observerOptions: WaveObserverOptions | null; 42 | private points: number; 43 | private variance: number; 44 | private duration: number; 45 | private vertical: boolean; 46 | private width: number; 47 | private height: number; 48 | private svg: SVGSVGElement; 49 | private path: SVGPathElement; 50 | 51 | constructor(); 52 | 53 | // Lifecycle methods 54 | connectedCallback(): void; 55 | disconnectedCallback(): void; 56 | 57 | // Public methods 58 | play(customDuration?: number | null): void; 59 | pause(): void; 60 | generateNewWave(duration?: number): void; 61 | 62 | // Private methods 63 | private setupIntersectionObserver(observeConfig: string): void; 64 | private animateWave(duration: number, onComplete?: (() => void) | null): void; 65 | } 66 | 67 | // Global declaration for custom element 68 | declare global { 69 | interface HTMLElementTagNameMap { 70 | 'dynamo-wave': DynamoWave; 71 | } 72 | } 73 | 74 | // Component attributes interface 75 | interface DynamoWaveAttributes { 76 | 'data-wave-face'?: WaveDirection; 77 | 'data-wave-points'?: string; 78 | 'data-variance'?: string; 79 | 'data-wave-speed'?: string; 80 | 'data-wave-animate'?: string; 81 | 'data-wave-observe'?: string; 82 | } 83 | 84 | // Extend HTMLElement interface to include our attributes 85 | declare global { 86 | interface HTMLElementTagNameMap { 87 | 'dynamo-wave': DynamoWave; 88 | } 89 | 90 | namespace JSX { 91 | interface IntrinsicElements { 92 | 'dynamo-wave': Partial; 93 | } 94 | } 95 | } 96 | 97 | export { DynamoWave, type DynamoWaveAttributes, type WaveDirection, type WaveGenerationOptions, type WaveObserverOptions, type WavePoint }; 98 | -------------------------------------------------------------------------------- /dist/dynamowaves.js: -------------------------------------------------------------------------------- 1 | (function (factory) { 2 | typeof define === 'function' && define.amd ? define(factory) : 3 | factory(); 4 | })((function () { 'use strict'; 5 | 6 | class DynamoWave extends HTMLElement { 7 | /** 8 | * Constructs a new instance of the class. 9 | * 10 | * @constructor 11 | * 12 | * @property {boolean} isAnimating - Indicates whether the animation is currently running. 13 | * @property {number|null} animationFrameId - The ID of the current animation frame request. 14 | * @property {number} elapsedTime - The elapsed time since the animation started. 15 | * @property {number|null} startTime - The start time of the animation. 16 | * 17 | * @property {boolean} isGeneratingWave - Indicates whether a wave is currently being generated. 18 | * 19 | * @property {Path2D|null} currentPath - The current wave path. 20 | * @property {Path2D|null} targetPath - The target wave path. 21 | * @property {Path2D|null} pendingTargetPath - The next wave path to be generated. 22 | * 23 | * @property {IntersectionObserver|null} intersectionObserver - The Intersection Observer instance. 24 | * @property {Object|null} observerOptions - The options for the Intersection Observer. 25 | */ 26 | 27 | constructor() { 28 | super(); 29 | this.isAnimating = false; 30 | this.animationFrameId = null; 31 | this.elapsedTime = 0; 32 | this.startTime = null; 33 | 34 | this.isGeneratingWave = false; 35 | 36 | // Track current and target wave paths 37 | this.currentPath = null; 38 | this.targetPath = null; 39 | this.pendingTargetPath = null; // New property to track the next wave 40 | 41 | // Intersection Observer properties 42 | this.intersectionObserver = null; 43 | this.observerOptions = null; 44 | } 45 | 46 | /** 47 | * Called when the custom element is appended to the DOM. 48 | * Initializes the wave properties, constructs the SVG element, 49 | * and sets up animation and observation if specified. 50 | * 51 | * @method connectedCallback 52 | * @returns {void} 53 | */ 54 | connectedCallback() { 55 | const classes = this.className; 56 | const id = this.id ?? Math.random().toString(36).substring(7); 57 | const styles = this.getAttribute("style"); 58 | 59 | const waveDirection = this.getAttribute("data-wave-face") || "top"; 60 | this.points = parseInt(this.getAttribute("data-wave-points")) || 6; 61 | this.variance = parseFloat(this.getAttribute("data-variance")) || 3; 62 | this.duration = parseFloat(this.getAttribute("data-wave-speed")) || 7500; 63 | 64 | this.vertical = waveDirection === "left" || waveDirection === "right"; 65 | const flipX = waveDirection === "right"; 66 | const flipY = waveDirection === "bottom"; 67 | 68 | this.width = this.vertical ? 160 : 1440; 69 | this.height = this.vertical ? 1440 : 160; 70 | 71 | // Initialize current and target paths 72 | this.currentPath = generateWave({ 73 | width: this.width, 74 | height: this.height, 75 | points: this.points, 76 | variance: this.variance, 77 | vertical: this.vertical, 78 | }); 79 | 80 | this.targetPath = generateWave({ 81 | width: this.width, 82 | height: this.height, 83 | points: this.points, 84 | variance: this.variance, 85 | vertical: this.vertical, 86 | }); 87 | 88 | // Construct the SVG 89 | this.innerHTML = ` 90 | 101 | `; 102 | 103 | // Save SVG references 104 | this.svg = this.querySelector("svg"); 105 | this.path = this.querySelector("path"); 106 | 107 | // Bind methods 108 | this.play = this.play.bind(this); 109 | this.pause = this.pause.bind(this); 110 | this.generateNewWave = this.generateNewWave.bind(this); 111 | 112 | // Check for wave observation attribute 113 | const observeAttr = this.getAttribute("data-wave-observe"); 114 | if (observeAttr) { 115 | this.setupIntersectionObserver(observeAttr); 116 | } 117 | 118 | // Automatically start animation if enabled 119 | if (this.getAttribute("data-wave-animate") === "true") { 120 | if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) { 121 | this.play(); 122 | } 123 | } 124 | } 125 | 126 | // Public method to play the animation 127 | /** 128 | * Starts the wave animation. If a custom duration is provided, it will be used for the animation; 129 | * otherwise, the instance's default duration will be used. The animation will continue looping 130 | * until `stop` is called. 131 | * 132 | * @param {number|null} [customDuration=null] - Optional custom duration for the animation in milliseconds. 133 | */ 134 | play(customDuration = null) { 135 | if (this.isAnimating) return; 136 | this.isAnimating = true; 137 | 138 | // Use custom duration if provided, otherwise use the instance duration 139 | const animationDuration = customDuration || this.duration; 140 | 141 | const continueAnimation = () => { 142 | // If there's no pending target path, generate a new one 143 | if (!this.pendingTargetPath) { 144 | this.pendingTargetPath = generateWave({ 145 | width: this.width, 146 | height: this.height, 147 | points: this.points, 148 | variance: this.variance, 149 | vertical: this.vertical, 150 | }); 151 | } 152 | 153 | // Animate to the pending target path 154 | this.animateWave(animationDuration, () => { 155 | // Update current path to the target path 156 | this.currentPath = this.targetPath; 157 | 158 | // Set the pending path as the new target 159 | this.targetPath = this.pendingTargetPath; 160 | 161 | // Clear the pending path and generate a new one for the next iteration 162 | this.pendingTargetPath = generateWave({ 163 | width: this.width, 164 | height: this.height, 165 | points: this.points, 166 | variance: this.variance, 167 | vertical: this.vertical, 168 | }); 169 | 170 | // Continue the animation loop if still playing 171 | if (this.isAnimating) { 172 | continueAnimation(); 173 | } 174 | }); 175 | }; 176 | 177 | // Start the continuous animation 178 | continueAnimation(); 179 | } 180 | 181 | // Public method to pause the animation 182 | /** 183 | * Pauses the animation if it is currently running. 184 | * Sets the `isAnimating` flag to false, cancels the animation frame, 185 | * and saves the current elapsed time. 186 | */ 187 | pause() { 188 | if (!this.isAnimating) return; 189 | this.isAnimating = false; 190 | cancelAnimationFrame(this.animationFrameId); 191 | this.animationFrameId = null; 192 | 193 | // Save the current elapsed time 194 | this.elapsedTime += performance.now() - (this.startTime || performance.now()); 195 | this.startTime = null; 196 | } 197 | 198 | /** 199 | * Called when the element is disconnected from the document's DOM. 200 | * Cleans up the intersection observer if it exists. 201 | */ 202 | disconnectedCallback() { 203 | // Clean up intersection observer when element is removed 204 | if (this.intersectionObserver) { 205 | this.intersectionObserver.disconnect(); 206 | this.intersectionObserver = null; 207 | } 208 | } 209 | 210 | /** 211 | * Sets up an IntersectionObserver to monitor the visibility of the element. 212 | * 213 | * @param {string} observeConfig - Configuration string for observation. 214 | * Format: "mode:rootMargin". 215 | * "mode" can be "once" for one-time observation. 216 | * "rootMargin" is an optional margin around the root. 217 | * 218 | * @example 219 | * // Observe with default root margin and trigger only once 220 | * setupIntersectionObserver('once:0px'); 221 | * 222 | * @example 223 | * // Observe with custom root margin and continuous triggering 224 | * setupIntersectionObserver('continuous:10px'); 225 | */ 226 | setupIntersectionObserver(observeConfig) { 227 | // Parse observation configuration 228 | const [mode, rootMargin = '0px'] = observeConfig.split(':'); 229 | 230 | // Determine observation mode 231 | const isOneTime = mode === 'once'; 232 | 233 | // Default options if not specified 234 | this.observerOptions = { 235 | root: null, // viewport 236 | rootMargin: rootMargin, 237 | threshold: 0 // trigger as soon as element completely leaves/enters 238 | }; 239 | 240 | this.intersectionObserver = new IntersectionObserver((entries) => { 241 | entries.forEach((entry) => { 242 | // Trigger new wave when completely outside viewport 243 | if (!entry.isIntersecting) { 244 | // Generate new wave 245 | this.generateNewWave(); 246 | 247 | // If one-time mode, disconnect observer 248 | if (isOneTime) { 249 | this.intersectionObserver.disconnect(); 250 | this.intersectionObserver = null; 251 | } 252 | } 253 | }); 254 | }, this.observerOptions); 255 | 256 | // Start observing this element 257 | this.intersectionObserver.observe(this); 258 | } 259 | 260 | // Public method to morph to a new wave 261 | /** 262 | * Generates a new wave animation with the specified duration. 263 | * Prevents multiple simultaneous wave generations by setting a flag. 264 | * 265 | * @param {number} [duration=800] - The duration of the wave animation in milliseconds. Minimum value is 1. 266 | */ 267 | generateNewWave(duration = 800) { 268 | // Prevent multiple simultaneous wave generations 269 | if (this.isGeneratingWave || this.animationFrameId) { 270 | return; 271 | } 272 | 273 | if (duration < 1) duration = 1; 274 | 275 | // Set flag to prevent concurrent wave generations 276 | this.isGeneratingWave = true; 277 | 278 | // Set the pending target path to a new wave 279 | this.pendingTargetPath = generateWave({ 280 | width: this.width, 281 | height: this.height, 282 | points: this.points, 283 | variance: this.variance, 284 | vertical: this.vertical, 285 | }); 286 | 287 | // Animate from current path to new target 288 | this.animateWave(duration, () => { 289 | // Update paths 290 | this.currentPath = this.targetPath; 291 | this.targetPath = this.pendingTargetPath; 292 | this.pendingTargetPath = null; 293 | 294 | // Reset wave generation flag 295 | this.isGeneratingWave = false; 296 | this.animationFrameId = null; 297 | }); 298 | } 299 | 300 | // Core animation logic 301 | /** 302 | * Animates the wave transition from the current path to the target path over a specified duration. 303 | * 304 | * @param {number} duration - The duration of the animation in milliseconds. 305 | * @param {Function} [onComplete=null] - Optional callback function to be called upon animation completion. 306 | */ 307 | animateWave(duration, onComplete = null) { 308 | // Ensure we have valid start and target paths 309 | const startPoints = parsePath(this.currentPath); 310 | const endPoints = parsePath(this.targetPath); 311 | 312 | if (startPoints.length !== endPoints.length) { 313 | console.error("Point mismatch! Regenerating waves to ensure consistency."); 314 | 315 | // Regenerate both current and target paths to ensure consistency 316 | this.currentPath = generateWave({ 317 | width: this.width, 318 | height: this.height, 319 | points: this.points, 320 | variance: this.variance, 321 | vertical: this.vertical, 322 | }); 323 | 324 | this.targetPath = generateWave({ 325 | width: this.width, 326 | height: this.height, 327 | points: this.points, 328 | variance: this.variance, 329 | vertical: this.vertical, 330 | }); 331 | 332 | return; 333 | } 334 | 335 | const animate = (timestamp) => { 336 | if (!this.startTime) this.startTime = timestamp - this.elapsedTime; 337 | const elapsed = timestamp - this.startTime; 338 | const progress = Math.min(elapsed / duration, 1); 339 | 340 | const interpolatedPath = interpolateWave( 341 | startPoints, 342 | endPoints, 343 | progress, 344 | this.vertical, 345 | this.height, 346 | this.width 347 | ); 348 | 349 | this.path.setAttribute("d", interpolatedPath); 350 | 351 | if (progress < 1) { 352 | this.animationFrameId = requestAnimationFrame(animate); 353 | } else { 354 | // Animation completed 355 | this.elapsedTime = 0; 356 | this.startTime = null; 357 | 358 | // Call completion callback if provided 359 | if (onComplete) onComplete(); 360 | } 361 | }; 362 | 363 | this.animationFrameId = requestAnimationFrame(animate); 364 | } 365 | } 366 | 367 | // Custom element definition 368 | customElements.define("dynamo-wave", DynamoWave); 369 | 370 | /** 371 | * Generates an SVG path string representing a wave pattern. 372 | * 373 | * @param {Object} options - The options for generating the wave. 374 | * @param {number} options.width - The width of the wave. 375 | * @param {number} options.height - The height of the wave. 376 | * @param {number} options.points - The number of points in the wave. 377 | * @param {number} options.variance - The variance factor for the wave's randomness. 378 | * @param {boolean} [options.vertical=false] - Whether the wave should be vertical. 379 | * @returns {string} The SVG path string representing the wave. 380 | */ 381 | function generateWave({ width, height, points, variance, vertical = false }) { 382 | const anchors = []; 383 | const step = vertical ? height / (points - 1) : width / (points - 1); 384 | 385 | for (let i = 0; i < points; i++) { 386 | const x = vertical 387 | ? height - step * i 388 | : step * i; 389 | const y = vertical 390 | ? width - width * 0.1 - Math.random() * (variance * width * 0.25) 391 | : height - height * 0.1 - Math.random() * (variance * height * 0.25); 392 | anchors.push(vertical ? { x: y, y: x } : { x, y }); 393 | } 394 | 395 | let path = vertical 396 | ? `M ${width} ${height} L ${anchors[0].x} ${height}` 397 | : `M 0 ${height} L 0 ${anchors[0].y}`; 398 | 399 | for (let i = 0; i < anchors.length - 1; i++) { 400 | const curr = anchors[i]; 401 | const next = anchors[i + 1]; 402 | const controlX = (curr.x + next.x) / 2; 403 | const controlY = (curr.y + next.y) / 2; 404 | path += ` Q ${curr.x} ${curr.y}, ${controlX} ${controlY}`; 405 | } 406 | 407 | const last = anchors[anchors.length - 1]; 408 | path += vertical 409 | ? ` Q ${last.x} ${last.y}, 0 0 L ${width} 0 L ${width} ${height} Z` 410 | : ` Q ${last.x} ${last.y}, ${width} ${last.y} L ${width} ${height} Z`; 411 | 412 | return path; 413 | } 414 | 415 | /** 416 | * Parses a path string containing quadratic Bezier curve commands and extracts the control points and end points. 417 | * 418 | * @param {string} pathString - The path string containing 'Q' commands followed by control point and end point coordinates. 419 | * @returns {Array} An array of objects, each containing the control point (cpX, cpY) and end point (x, y) coordinates. 420 | */ 421 | function parsePath(pathString) { 422 | const points = []; 423 | const regex = /Q\s([\d.]+)\s([\d.]+),\s([\d.]+)\s([\d.]+)/g; 424 | let match; 425 | 426 | while ((match = regex.exec(pathString)) !== null) { 427 | points.push({ 428 | cpX: parseFloat(match[1]), 429 | cpY: parseFloat(match[2]), 430 | x: parseFloat(match[3]), 431 | y: parseFloat(match[4]), 432 | }); 433 | } 434 | return points; 435 | } 436 | 437 | /** 438 | * Interpolates between two sets of points to create a smooth wave transition. 439 | * 440 | * @param {Array} currentPoints - The current set of points. 441 | * @param {Array} targetPoints - The target set of points. 442 | * @param {number} progress - The progress of the interpolation (0 to 1). 443 | * @param {boolean} [vertical=false] - Whether the wave is vertical or horizontal. 444 | * @param {number} height - The height of the wave container. 445 | * @param {number} width - The width of the wave container. 446 | * @returns {string} - The SVG path data for the interpolated wave. 447 | */ 448 | function interpolateWave(currentPoints, targetPoints, progress, vertical = false, height, width) { 449 | const interpolatedPoints = currentPoints.map((current, i) => { 450 | const target = targetPoints[i]; 451 | return { 452 | cpX: current.cpX + (target.cpX - current.cpX) * progress, 453 | cpY: vertical ? current.cpY : current.cpY + (target.cpY - current.cpY) * progress, 454 | x: vertical ? current.x + (target.x - current.x) * progress : current.x, 455 | y: vertical ? current.y : current.y + (target.y - current.y) * progress, 456 | }; 457 | }); 458 | 459 | let path = vertical 460 | ? `M ${width} ${height} L ${interpolatedPoints[0].x} ${height}` 461 | : `M 0 ${height} L 0 ${interpolatedPoints[0].y}`; 462 | 463 | for (let i = 0; i < interpolatedPoints.length; i++) { 464 | const { cpX, cpY, x, y } = interpolatedPoints[i]; 465 | path += ` Q ${cpX} ${cpY}, ${x} ${y}`; 466 | } 467 | 468 | path += vertical 469 | ? ` L 0 0 L ${width} 0 L ${width} ${height} Z` 470 | : ` L ${width} ${height} Z`; 471 | 472 | return path; 473 | } 474 | 475 | })); 476 | -------------------------------------------------------------------------------- /dist/dynamowaves.min.js: -------------------------------------------------------------------------------- 1 | !function(t){"function"==typeof define&&define.amd?define(t):t()}((function(){"use strict";class t extends HTMLElement{constructor(){super(),this.isAnimating=!1,this.animationFrameId=null,this.elapsedTime=0,this.startTime=null,this.isGeneratingWave=!1,this.currentPath=null,this.targetPath=null,this.pendingTargetPath=null,this.intersectionObserver=null,this.observerOptions=null}connectedCallback(){const t=this.className,e=this.id??Math.random().toString(36).substring(7),s=this.getAttribute("style"),n=this.getAttribute("data-wave-face")||"top";this.points=parseInt(this.getAttribute("data-wave-points"))||6,this.variance=parseFloat(this.getAttribute("data-variance"))||3,this.duration=parseFloat(this.getAttribute("data-wave-speed"))||7500,this.vertical="left"===n||"right"===n;const a="right"===n,h="bottom"===n;this.width=this.vertical?160:1440,this.height=this.vertical?1440:160,this.currentPath=i({width:this.width,height:this.height,points:this.points,variance:this.variance,vertical:this.vertical}),this.targetPath=i({width:this.width,height:this.height,points:this.points,variance:this.variance,vertical:this.vertical}),this.innerHTML=`\n \n `,this.svg=this.querySelector("svg"),this.path=this.querySelector("path"),this.play=this.play.bind(this),this.pause=this.pause.bind(this),this.generateNewWave=this.generateNewWave.bind(this);const r=this.getAttribute("data-wave-observe");r&&this.setupIntersectionObserver(r),"true"===this.getAttribute("data-wave-animate")&&(window.matchMedia("(prefers-reduced-motion: reduce)").matches||this.play())}play(t=null){if(this.isAnimating)return;this.isAnimating=!0;const e=t||this.duration,s=()=>{this.pendingTargetPath||(this.pendingTargetPath=i({width:this.width,height:this.height,points:this.points,variance:this.variance,vertical:this.vertical})),this.animateWave(e,(()=>{this.currentPath=this.targetPath,this.targetPath=this.pendingTargetPath,this.pendingTargetPath=i({width:this.width,height:this.height,points:this.points,variance:this.variance,vertical:this.vertical}),this.isAnimating&&s()}))};s()}pause(){this.isAnimating&&(this.isAnimating=!1,cancelAnimationFrame(this.animationFrameId),this.animationFrameId=null,this.elapsedTime+=performance.now()-(this.startTime||performance.now()),this.startTime=null)}disconnectedCallback(){this.intersectionObserver&&(this.intersectionObserver.disconnect(),this.intersectionObserver=null)}setupIntersectionObserver(t){const[i,e="0px"]=t.split(":"),s="once"===i;this.observerOptions={root:null,rootMargin:e,threshold:0},this.intersectionObserver=new IntersectionObserver((t=>{t.forEach((t=>{t.isIntersecting||(this.generateNewWave(),s&&(this.intersectionObserver.disconnect(),this.intersectionObserver=null))}))}),this.observerOptions),this.intersectionObserver.observe(this)}generateNewWave(t=800){this.isGeneratingWave||this.animationFrameId||(t<1&&(t=1),this.isGeneratingWave=!0,this.pendingTargetPath=i({width:this.width,height:this.height,points:this.points,variance:this.variance,vertical:this.vertical}),this.animateWave(t,(()=>{this.currentPath=this.targetPath,this.targetPath=this.pendingTargetPath,this.pendingTargetPath=null,this.isGeneratingWave=!1,this.animationFrameId=null})))}animateWave(t,s=null){const n=e(this.currentPath),a=e(this.targetPath);if(n.length!==a.length)return console.error("Point mismatch! Regenerating waves to ensure consistency."),this.currentPath=i({width:this.width,height:this.height,points:this.points,variance:this.variance,vertical:this.vertical}),void(this.targetPath=i({width:this.width,height:this.height,points:this.points,variance:this.variance,vertical:this.vertical}));const h=i=>{this.startTime||(this.startTime=i-this.elapsedTime);const e=i-this.startTime,r=Math.min(e/t,1),c=function(t,i,e,s=!1,n,a){const h=t.map(((t,n)=>{const a=i[n];return{cpX:t.cpX+(a.cpX-t.cpX)*e,cpY:s?t.cpY:t.cpY+(a.cpY-t.cpY)*e,x:s?t.x+(a.x-t.x)*e:t.x,y:s?t.y:t.y+(a.y-t.y)*e}}));let r=s?`M ${a} ${n} L ${h[0].x} ${n}`:`M 0 ${n} L 0 ${h[0].y}`;for(let t=0;t=6.9.0" 31 | } 32 | }, 33 | "node_modules/@babel/helper-validator-identifier": { 34 | "version": "7.25.9", 35 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", 36 | "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", 37 | "dev": true, 38 | "license": "MIT", 39 | "optional": true, 40 | "engines": { 41 | "node": ">=6.9.0" 42 | } 43 | }, 44 | "node_modules/@jridgewell/gen-mapping": { 45 | "version": "0.3.8", 46 | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", 47 | "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", 48 | "dev": true, 49 | "license": "MIT", 50 | "dependencies": { 51 | "@jridgewell/set-array": "^1.2.1", 52 | "@jridgewell/sourcemap-codec": "^1.4.10", 53 | "@jridgewell/trace-mapping": "^0.3.24" 54 | }, 55 | "engines": { 56 | "node": ">=6.0.0" 57 | } 58 | }, 59 | "node_modules/@jridgewell/resolve-uri": { 60 | "version": "3.1.2", 61 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 62 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 63 | "dev": true, 64 | "license": "MIT", 65 | "engines": { 66 | "node": ">=6.0.0" 67 | } 68 | }, 69 | "node_modules/@jridgewell/set-array": { 70 | "version": "1.2.1", 71 | "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", 72 | "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", 73 | "dev": true, 74 | "license": "MIT", 75 | "engines": { 76 | "node": ">=6.0.0" 77 | } 78 | }, 79 | "node_modules/@jridgewell/source-map": { 80 | "version": "0.3.6", 81 | "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", 82 | "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", 83 | "dev": true, 84 | "license": "MIT", 85 | "dependencies": { 86 | "@jridgewell/gen-mapping": "^0.3.5", 87 | "@jridgewell/trace-mapping": "^0.3.25" 88 | } 89 | }, 90 | "node_modules/@jridgewell/sourcemap-codec": { 91 | "version": "1.5.0", 92 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", 93 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", 94 | "dev": true, 95 | "license": "MIT" 96 | }, 97 | "node_modules/@jridgewell/trace-mapping": { 98 | "version": "0.3.25", 99 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", 100 | "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", 101 | "dev": true, 102 | "license": "MIT", 103 | "dependencies": { 104 | "@jridgewell/resolve-uri": "^3.1.0", 105 | "@jridgewell/sourcemap-codec": "^1.4.14" 106 | } 107 | }, 108 | "node_modules/@rollup/plugin-terser": { 109 | "version": "0.4.4", 110 | "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", 111 | "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", 112 | "dev": true, 113 | "license": "MIT", 114 | "dependencies": { 115 | "serialize-javascript": "^6.0.1", 116 | "smob": "^1.0.0", 117 | "terser": "^5.17.4" 118 | }, 119 | "engines": { 120 | "node": ">=14.0.0" 121 | }, 122 | "peerDependencies": { 123 | "rollup": "^2.0.0||^3.0.0||^4.0.0" 124 | }, 125 | "peerDependenciesMeta": { 126 | "rollup": { 127 | "optional": true 128 | } 129 | } 130 | }, 131 | "node_modules/acorn": { 132 | "version": "8.14.0", 133 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", 134 | "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", 135 | "dev": true, 136 | "license": "MIT", 137 | "bin": { 138 | "acorn": "bin/acorn" 139 | }, 140 | "engines": { 141 | "node": ">=0.4.0" 142 | } 143 | }, 144 | "node_modules/buffer-from": { 145 | "version": "1.1.2", 146 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", 147 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", 148 | "dev": true, 149 | "license": "MIT" 150 | }, 151 | "node_modules/commander": { 152 | "version": "2.20.3", 153 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 154 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", 155 | "dev": true, 156 | "license": "MIT" 157 | }, 158 | "node_modules/fsevents": { 159 | "version": "2.3.3", 160 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 161 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 162 | "dev": true, 163 | "hasInstallScript": true, 164 | "optional": true, 165 | "os": [ 166 | "darwin" 167 | ], 168 | "engines": { 169 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 170 | } 171 | }, 172 | "node_modules/js-tokens": { 173 | "version": "4.0.0", 174 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 175 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 176 | "dev": true, 177 | "license": "MIT", 178 | "optional": true 179 | }, 180 | "node_modules/magic-string": { 181 | "version": "0.30.17", 182 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", 183 | "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", 184 | "dev": true, 185 | "license": "MIT", 186 | "dependencies": { 187 | "@jridgewell/sourcemap-codec": "^1.5.0" 188 | } 189 | }, 190 | "node_modules/picocolors": { 191 | "version": "1.1.1", 192 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 193 | "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 194 | "dev": true, 195 | "license": "ISC", 196 | "optional": true 197 | }, 198 | "node_modules/randombytes": { 199 | "version": "2.1.0", 200 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 201 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 202 | "dev": true, 203 | "license": "MIT", 204 | "dependencies": { 205 | "safe-buffer": "^5.1.0" 206 | } 207 | }, 208 | "node_modules/rollup": { 209 | "version": "3.29.5", 210 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", 211 | "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", 212 | "dev": true, 213 | "license": "MIT", 214 | "bin": { 215 | "rollup": "dist/bin/rollup" 216 | }, 217 | "engines": { 218 | "node": ">=14.18.0", 219 | "npm": ">=8.0.0" 220 | }, 221 | "optionalDependencies": { 222 | "fsevents": "~2.3.2" 223 | } 224 | }, 225 | "node_modules/rollup-plugin-dts": { 226 | "version": "6.1.1", 227 | "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-6.1.1.tgz", 228 | "integrity": "sha512-aSHRcJ6KG2IHIioYlvAOcEq6U99sVtqDDKVhnwt70rW6tsz3tv5OSjEiWcgzfsHdLyGXZ/3b/7b/+Za3Y6r1XA==", 229 | "dev": true, 230 | "license": "LGPL-3.0-only", 231 | "dependencies": { 232 | "magic-string": "^0.30.10" 233 | }, 234 | "engines": { 235 | "node": ">=16" 236 | }, 237 | "funding": { 238 | "url": "https://github.com/sponsors/Swatinem" 239 | }, 240 | "optionalDependencies": { 241 | "@babel/code-frame": "^7.24.2" 242 | }, 243 | "peerDependencies": { 244 | "rollup": "^3.29.4 || ^4", 245 | "typescript": "^4.5 || ^5.0" 246 | } 247 | }, 248 | "node_modules/safe-buffer": { 249 | "version": "5.2.1", 250 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 251 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 252 | "dev": true, 253 | "funding": [ 254 | { 255 | "type": "github", 256 | "url": "https://github.com/sponsors/feross" 257 | }, 258 | { 259 | "type": "patreon", 260 | "url": "https://www.patreon.com/feross" 261 | }, 262 | { 263 | "type": "consulting", 264 | "url": "https://feross.org/support" 265 | } 266 | ], 267 | "license": "MIT" 268 | }, 269 | "node_modules/serialize-javascript": { 270 | "version": "6.0.2", 271 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", 272 | "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", 273 | "dev": true, 274 | "license": "BSD-3-Clause", 275 | "dependencies": { 276 | "randombytes": "^2.1.0" 277 | } 278 | }, 279 | "node_modules/smob": { 280 | "version": "1.5.0", 281 | "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", 282 | "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", 283 | "dev": true, 284 | "license": "MIT" 285 | }, 286 | "node_modules/source-map": { 287 | "version": "0.6.1", 288 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 289 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 290 | "dev": true, 291 | "license": "BSD-3-Clause", 292 | "engines": { 293 | "node": ">=0.10.0" 294 | } 295 | }, 296 | "node_modules/source-map-support": { 297 | "version": "0.5.21", 298 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", 299 | "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", 300 | "dev": true, 301 | "license": "MIT", 302 | "dependencies": { 303 | "buffer-from": "^1.0.0", 304 | "source-map": "^0.6.0" 305 | } 306 | }, 307 | "node_modules/terser": { 308 | "version": "5.37.0", 309 | "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", 310 | "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", 311 | "dev": true, 312 | "license": "BSD-2-Clause", 313 | "dependencies": { 314 | "@jridgewell/source-map": "^0.3.3", 315 | "acorn": "^8.8.2", 316 | "commander": "^2.20.0", 317 | "source-map-support": "~0.5.20" 318 | }, 319 | "bin": { 320 | "terser": "bin/terser" 321 | }, 322 | "engines": { 323 | "node": ">=10" 324 | } 325 | }, 326 | "node_modules/typescript": { 327 | "version": "5.7.2", 328 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", 329 | "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", 330 | "dev": true, 331 | "license": "Apache-2.0", 332 | "peer": true, 333 | "bin": { 334 | "tsc": "bin/tsc", 335 | "tsserver": "bin/tsserver" 336 | }, 337 | "engines": { 338 | "node": ">=14.17" 339 | } 340 | } 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dynamowaves", 3 | "version": "2.0.1", 4 | "type": "module", 5 | "description": "Lightweight, dependency-free SVG wave templates that dynamically generate themselves on render.", 6 | "main": "dist/dynamowaves.js", 7 | "module": "dist/dynamowaves.js", 8 | "types": "dist/dynamowaves.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "build": "rollup -c", 14 | "prepublishOnly": "npm run build" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/mzebley/dynamowaves.git" 19 | }, 20 | "keywords": [ 21 | "javascript", 22 | "svg", 23 | "html", 24 | "waves", 25 | "wave", 26 | "generative", 27 | "templates-html" 28 | ], 29 | "author": "Mark Zebley", 30 | "license": "ISC", 31 | "bugs": { 32 | "url": "https://github.com/mzebley/dynamowaves/issues" 33 | }, 34 | "homepage": "https://dynamowaves.markzebley.com/", 35 | "devDependencies": { 36 | "@rollup/plugin-terser": "^1.0.0", 37 | "rollup": "^3.29.4", 38 | "rollup-plugin-dts": "^6.1.1" 39 | } 40 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import terser from '@rollup/plugin-terser'; 2 | import dts from 'rollup-plugin-dts'; 3 | 4 | export default [ 5 | // Main JavaScript bundle configuration 6 | { 7 | input: 'src/dynamowaves.js', 8 | output: [ 9 | { 10 | file: 'dist/dynamowaves.js', 11 | format: 'umd', 12 | name: 'Dynamowaves', 13 | }, 14 | { 15 | file: 'dist/dynamowaves.min.js', 16 | format: 'umd', 17 | name: 'Dynamowaves', 18 | plugins: [terser()], 19 | }, 20 | { 21 | file: 'www/dynamowaves.min.js', 22 | format: 'umd', 23 | name: 'Dynamowaves', 24 | plugins: [terser()], 25 | }, 26 | ], 27 | }, 28 | // Types bundle configuration 29 | { 30 | input: './src/dynamowaves.d.ts', 31 | output: [ 32 | { 33 | file: 'dist/dynamowaves.d.ts', 34 | format: 'es', 35 | }, 36 | ], 37 | plugins: [dts()], 38 | }, 39 | ]; -------------------------------------------------------------------------------- /src/dynamowaves.d.ts: -------------------------------------------------------------------------------- 1 | // dynamoves.d.ts 2 | 3 | // Interface for wave generation options 4 | interface WaveGenerationOptions { 5 | width: number; 6 | height: number; 7 | points: number; 8 | variance: number; 9 | vertical?: boolean; 10 | } 11 | 12 | // Interface for wave point structure 13 | interface WavePoint { 14 | cpX: number; 15 | cpY: number; 16 | x: number; 17 | y: number; 18 | } 19 | 20 | // Type for the wave direction 21 | type WaveDirection = 'top' | 'bottom' | 'left' | 'right'; 22 | 23 | // Interface for intersection observer options 24 | interface WaveObserverOptions { 25 | root: Element | null; 26 | rootMargin: string; 27 | threshold: number; 28 | } 29 | 30 | declare class DynamoWave extends HTMLElement { 31 | // Properties 32 | private isAnimating: boolean; 33 | private animationFrameId: number | null; 34 | private elapsedTime: number; 35 | private startTime: number | null; 36 | private isGeneratingWave: boolean; 37 | private currentPath: string | null; 38 | private targetPath: string | null; 39 | private pendingTargetPath: string | null; 40 | private intersectionObserver: IntersectionObserver | null; 41 | private observerOptions: WaveObserverOptions | null; 42 | private points: number; 43 | private variance: number; 44 | private duration: number; 45 | private vertical: boolean; 46 | private width: number; 47 | private height: number; 48 | private svg: SVGSVGElement; 49 | private path: SVGPathElement; 50 | 51 | constructor(); 52 | 53 | // Lifecycle methods 54 | connectedCallback(): void; 55 | disconnectedCallback(): void; 56 | 57 | // Public methods 58 | play(customDuration?: number | null): void; 59 | pause(): void; 60 | generateNewWave(duration?: number): void; 61 | 62 | // Private methods 63 | private setupIntersectionObserver(observeConfig: string): void; 64 | private animateWave(duration: number, onComplete?: (() => void) | null): void; 65 | } 66 | 67 | // Helper function declarations 68 | declare function generateWave(options: WaveGenerationOptions): string; 69 | declare function parsePath(pathString: string): WavePoint[]; 70 | declare function interpolateWave( 71 | currentPoints: WavePoint[], 72 | targetPoints: WavePoint[], 73 | progress: number, 74 | vertical?: boolean, 75 | height?: number, 76 | width?: number 77 | ): string; 78 | 79 | // Global declaration for custom element 80 | declare global { 81 | interface HTMLElementTagNameMap { 82 | 'dynamo-wave': DynamoWave; 83 | } 84 | } 85 | 86 | // Component attributes interface 87 | interface DynamoWaveAttributes { 88 | 'data-wave-face'?: WaveDirection; 89 | 'data-wave-points'?: string; 90 | 'data-variance'?: string; 91 | 'data-wave-speed'?: string; 92 | 'data-wave-animate'?: string; 93 | 'data-wave-observe'?: string; 94 | } 95 | 96 | // Extend HTMLElement interface to include our attributes 97 | declare global { 98 | interface HTMLElementTagNameMap { 99 | 'dynamo-wave': DynamoWave; 100 | } 101 | 102 | namespace JSX { 103 | interface IntrinsicElements { 104 | 'dynamo-wave': Partial; 105 | } 106 | } 107 | } 108 | 109 | export { 110 | DynamoWave, 111 | WaveGenerationOptions, 112 | WavePoint, 113 | WaveDirection, 114 | WaveObserverOptions, 115 | DynamoWaveAttributes 116 | }; -------------------------------------------------------------------------------- /src/dynamowaves.js: -------------------------------------------------------------------------------- 1 | class DynamoWave extends HTMLElement { 2 | /** 3 | * Constructs a new instance of the class. 4 | * 5 | * @constructor 6 | * 7 | * @property {boolean} isAnimating - Indicates whether the animation is currently running. 8 | * @property {number|null} animationFrameId - The ID of the current animation frame request. 9 | * @property {number} elapsedTime - The elapsed time since the animation started. 10 | * @property {number|null} startTime - The start time of the animation. 11 | * 12 | * @property {boolean} isGeneratingWave - Indicates whether a wave is currently being generated. 13 | * 14 | * @property {Path2D|null} currentPath - The current wave path. 15 | * @property {Path2D|null} targetPath - The target wave path. 16 | * @property {Path2D|null} pendingTargetPath - The next wave path to be generated. 17 | * 18 | * @property {IntersectionObserver|null} intersectionObserver - The Intersection Observer instance. 19 | * @property {Object|null} observerOptions - The options for the Intersection Observer. 20 | */ 21 | 22 | constructor() { 23 | super(); 24 | this.isAnimating = false; 25 | this.animationFrameId = null; 26 | this.elapsedTime = 0; 27 | this.startTime = null; 28 | 29 | this.isGeneratingWave = false; 30 | 31 | // Track current and target wave paths 32 | this.currentPath = null; 33 | this.targetPath = null; 34 | this.pendingTargetPath = null; // New property to track the next wave 35 | 36 | // Intersection Observer properties 37 | this.intersectionObserver = null; 38 | this.observerOptions = null; 39 | } 40 | 41 | /** 42 | * Called when the custom element is appended to the DOM. 43 | * Initializes the wave properties, constructs the SVG element, 44 | * and sets up animation and observation if specified. 45 | * 46 | * @method connectedCallback 47 | * @returns {void} 48 | */ 49 | connectedCallback() { 50 | const classes = this.className; 51 | const id = this.id ?? Math.random().toString(36).substring(7); 52 | const styles = this.getAttribute("style"); 53 | 54 | const waveDirection = this.getAttribute("data-wave-face") || "top"; 55 | this.points = parseInt(this.getAttribute("data-wave-points")) || 6; 56 | this.variance = parseFloat(this.getAttribute("data-variance")) || 3; 57 | this.duration = parseFloat(this.getAttribute("data-wave-speed")) || 7500; 58 | 59 | this.vertical = waveDirection === "left" || waveDirection === "right"; 60 | const flipX = waveDirection === "right"; 61 | const flipY = waveDirection === "bottom"; 62 | 63 | this.width = this.vertical ? 160 : 1440; 64 | this.height = this.vertical ? 1440 : 160; 65 | 66 | // Initialize current and target paths 67 | this.currentPath = generateWave({ 68 | width: this.width, 69 | height: this.height, 70 | points: this.points, 71 | variance: this.variance, 72 | vertical: this.vertical, 73 | }); 74 | 75 | this.targetPath = generateWave({ 76 | width: this.width, 77 | height: this.height, 78 | points: this.points, 79 | variance: this.variance, 80 | vertical: this.vertical, 81 | }); 82 | 83 | // Construct the SVG 84 | this.innerHTML = ` 85 | 96 | `; 97 | 98 | // Save SVG references 99 | this.svg = this.querySelector("svg"); 100 | this.path = this.querySelector("path"); 101 | 102 | // Bind methods 103 | this.play = this.play.bind(this); 104 | this.pause = this.pause.bind(this); 105 | this.generateNewWave = this.generateNewWave.bind(this); 106 | 107 | // Check for wave observation attribute 108 | const observeAttr = this.getAttribute("data-wave-observe"); 109 | if (observeAttr) { 110 | this.setupIntersectionObserver(observeAttr); 111 | } 112 | 113 | // Automatically start animation if enabled 114 | if (this.getAttribute("data-wave-animate") === "true") { 115 | if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) { 116 | this.play(); 117 | } 118 | } 119 | } 120 | 121 | // Public method to play the animation 122 | /** 123 | * Starts the wave animation. If a custom duration is provided, it will be used for the animation; 124 | * otherwise, the instance's default duration will be used. The animation will continue looping 125 | * until `stop` is called. 126 | * 127 | * @param {number|null} [customDuration=null] - Optional custom duration for the animation in milliseconds. 128 | */ 129 | play(customDuration = null) { 130 | if (this.isAnimating) return; 131 | this.isAnimating = true; 132 | 133 | // Use custom duration if provided, otherwise use the instance duration 134 | const animationDuration = customDuration || this.duration; 135 | 136 | const continueAnimation = () => { 137 | // If there's no pending target path, generate a new one 138 | if (!this.pendingTargetPath) { 139 | this.pendingTargetPath = generateWave({ 140 | width: this.width, 141 | height: this.height, 142 | points: this.points, 143 | variance: this.variance, 144 | vertical: this.vertical, 145 | }); 146 | } 147 | 148 | // Animate to the pending target path 149 | this.animateWave(animationDuration, () => { 150 | // Update current path to the target path 151 | this.currentPath = this.targetPath; 152 | 153 | // Set the pending path as the new target 154 | this.targetPath = this.pendingTargetPath; 155 | 156 | // Clear the pending path and generate a new one for the next iteration 157 | this.pendingTargetPath = generateWave({ 158 | width: this.width, 159 | height: this.height, 160 | points: this.points, 161 | variance: this.variance, 162 | vertical: this.vertical, 163 | }); 164 | 165 | // Continue the animation loop if still playing 166 | if (this.isAnimating) { 167 | continueAnimation(); 168 | } 169 | }); 170 | }; 171 | 172 | // Start the continuous animation 173 | continueAnimation(); 174 | } 175 | 176 | // Public method to pause the animation 177 | /** 178 | * Pauses the animation if it is currently running. 179 | * Sets the `isAnimating` flag to false, cancels the animation frame, 180 | * and saves the current elapsed time. 181 | */ 182 | pause() { 183 | if (!this.isAnimating) return; 184 | this.isAnimating = false; 185 | cancelAnimationFrame(this.animationFrameId); 186 | this.animationFrameId = null; 187 | 188 | // Save the current elapsed time 189 | this.elapsedTime += performance.now() - (this.startTime || performance.now()); 190 | this.startTime = null; 191 | } 192 | 193 | /** 194 | * Called when the element is disconnected from the document's DOM. 195 | * Cleans up the intersection observer if it exists. 196 | */ 197 | disconnectedCallback() { 198 | // Clean up intersection observer when element is removed 199 | if (this.intersectionObserver) { 200 | this.intersectionObserver.disconnect(); 201 | this.intersectionObserver = null; 202 | } 203 | } 204 | 205 | /** 206 | * Sets up an IntersectionObserver to monitor the visibility of the element. 207 | * 208 | * @param {string} observeConfig - Configuration string for observation. 209 | * Format: "mode:rootMargin". 210 | * "mode" can be "once" for one-time observation. 211 | * "rootMargin" is an optional margin around the root. 212 | * 213 | * @example 214 | * // Observe with default root margin and trigger only once 215 | * setupIntersectionObserver('once:0px'); 216 | * 217 | * @example 218 | * // Observe with custom root margin and continuous triggering 219 | * setupIntersectionObserver('continuous:10px'); 220 | */ 221 | setupIntersectionObserver(observeConfig) { 222 | // Parse observation configuration 223 | const [mode, rootMargin = '0px'] = observeConfig.split(':'); 224 | 225 | // Determine observation mode 226 | const isOneTime = mode === 'once'; 227 | 228 | // Default options if not specified 229 | this.observerOptions = { 230 | root: null, // viewport 231 | rootMargin: rootMargin, 232 | threshold: 0 // trigger as soon as element completely leaves/enters 233 | }; 234 | 235 | this.intersectionObserver = new IntersectionObserver((entries) => { 236 | entries.forEach((entry) => { 237 | // Trigger new wave when completely outside viewport 238 | if (!entry.isIntersecting) { 239 | // Generate new wave 240 | this.generateNewWave(); 241 | 242 | // If one-time mode, disconnect observer 243 | if (isOneTime) { 244 | this.intersectionObserver.disconnect(); 245 | this.intersectionObserver = null; 246 | } 247 | } 248 | }); 249 | }, this.observerOptions); 250 | 251 | // Start observing this element 252 | this.intersectionObserver.observe(this); 253 | } 254 | 255 | // Public method to morph to a new wave 256 | /** 257 | * Generates a new wave animation with the specified duration. 258 | * Prevents multiple simultaneous wave generations by setting a flag. 259 | * 260 | * @param {number} [duration=800] - The duration of the wave animation in milliseconds. Minimum value is 1. 261 | */ 262 | generateNewWave(duration = 800) { 263 | // Prevent multiple simultaneous wave generations 264 | if (this.isGeneratingWave || this.animationFrameId) { 265 | return; 266 | } 267 | 268 | if (duration < 1) duration = 1; 269 | 270 | // Set flag to prevent concurrent wave generations 271 | this.isGeneratingWave = true; 272 | 273 | // Set the pending target path to a new wave 274 | this.pendingTargetPath = generateWave({ 275 | width: this.width, 276 | height: this.height, 277 | points: this.points, 278 | variance: this.variance, 279 | vertical: this.vertical, 280 | }); 281 | 282 | // Animate from current path to new target 283 | this.animateWave(duration, () => { 284 | // Update paths 285 | this.currentPath = this.targetPath; 286 | this.targetPath = this.pendingTargetPath; 287 | this.pendingTargetPath = null; 288 | 289 | // Reset wave generation flag 290 | this.isGeneratingWave = false; 291 | this.animationFrameId = null; 292 | }); 293 | } 294 | 295 | // Core animation logic 296 | /** 297 | * Animates the wave transition from the current path to the target path over a specified duration. 298 | * 299 | * @param {number} duration - The duration of the animation in milliseconds. 300 | * @param {Function} [onComplete=null] - Optional callback function to be called upon animation completion. 301 | */ 302 | animateWave(duration, onComplete = null) { 303 | // Ensure we have valid start and target paths 304 | const startPoints = parsePath(this.currentPath); 305 | const endPoints = parsePath(this.targetPath); 306 | 307 | if (startPoints.length !== endPoints.length) { 308 | console.error("Point mismatch! Regenerating waves to ensure consistency."); 309 | 310 | // Regenerate both current and target paths to ensure consistency 311 | this.currentPath = generateWave({ 312 | width: this.width, 313 | height: this.height, 314 | points: this.points, 315 | variance: this.variance, 316 | vertical: this.vertical, 317 | }); 318 | 319 | this.targetPath = generateWave({ 320 | width: this.width, 321 | height: this.height, 322 | points: this.points, 323 | variance: this.variance, 324 | vertical: this.vertical, 325 | }); 326 | 327 | return; 328 | } 329 | 330 | const animate = (timestamp) => { 331 | if (!this.startTime) this.startTime = timestamp - this.elapsedTime; 332 | const elapsed = timestamp - this.startTime; 333 | const progress = Math.min(elapsed / duration, 1); 334 | 335 | const interpolatedPath = interpolateWave( 336 | startPoints, 337 | endPoints, 338 | progress, 339 | this.vertical, 340 | this.height, 341 | this.width 342 | ); 343 | 344 | this.path.setAttribute("d", interpolatedPath); 345 | 346 | if (progress < 1) { 347 | this.animationFrameId = requestAnimationFrame(animate); 348 | } else { 349 | // Animation completed 350 | this.elapsedTime = 0; 351 | this.startTime = null; 352 | 353 | // Call completion callback if provided 354 | if (onComplete) onComplete(); 355 | } 356 | }; 357 | 358 | this.animationFrameId = requestAnimationFrame(animate); 359 | } 360 | } 361 | 362 | // Custom element definition 363 | customElements.define("dynamo-wave", DynamoWave); 364 | 365 | /** 366 | * Generates an SVG path string representing a wave pattern. 367 | * 368 | * @param {Object} options - The options for generating the wave. 369 | * @param {number} options.width - The width of the wave. 370 | * @param {number} options.height - The height of the wave. 371 | * @param {number} options.points - The number of points in the wave. 372 | * @param {number} options.variance - The variance factor for the wave's randomness. 373 | * @param {boolean} [options.vertical=false] - Whether the wave should be vertical. 374 | * @returns {string} The SVG path string representing the wave. 375 | */ 376 | function generateWave({ width, height, points, variance, vertical = false }) { 377 | const anchors = []; 378 | const step = vertical ? height / (points - 1) : width / (points - 1); 379 | 380 | for (let i = 0; i < points; i++) { 381 | const x = vertical 382 | ? height - step * i 383 | : step * i; 384 | const y = vertical 385 | ? width - width * 0.1 - Math.random() * (variance * width * 0.25) 386 | : height - height * 0.1 - Math.random() * (variance * height * 0.25); 387 | anchors.push(vertical ? { x: y, y: x } : { x, y }); 388 | } 389 | 390 | let path = vertical 391 | ? `M ${width} ${height} L ${anchors[0].x} ${height}` 392 | : `M 0 ${height} L 0 ${anchors[0].y}`; 393 | 394 | for (let i = 0; i < anchors.length - 1; i++) { 395 | const curr = anchors[i]; 396 | const next = anchors[i + 1]; 397 | const controlX = (curr.x + next.x) / 2; 398 | const controlY = (curr.y + next.y) / 2; 399 | path += ` Q ${curr.x} ${curr.y}, ${controlX} ${controlY}`; 400 | } 401 | 402 | const last = anchors[anchors.length - 1]; 403 | path += vertical 404 | ? ` Q ${last.x} ${last.y}, 0 0 L ${width} 0 L ${width} ${height} Z` 405 | : ` Q ${last.x} ${last.y}, ${width} ${last.y} L ${width} ${height} Z`; 406 | 407 | return path; 408 | } 409 | 410 | /** 411 | * Parses a path string containing quadratic Bezier curve commands and extracts the control points and end points. 412 | * 413 | * @param {string} pathString - The path string containing 'Q' commands followed by control point and end point coordinates. 414 | * @returns {Array} An array of objects, each containing the control point (cpX, cpY) and end point (x, y) coordinates. 415 | */ 416 | function parsePath(pathString) { 417 | const points = []; 418 | const regex = /Q\s([\d.]+)\s([\d.]+),\s([\d.]+)\s([\d.]+)/g; 419 | let match; 420 | 421 | while ((match = regex.exec(pathString)) !== null) { 422 | points.push({ 423 | cpX: parseFloat(match[1]), 424 | cpY: parseFloat(match[2]), 425 | x: parseFloat(match[3]), 426 | y: parseFloat(match[4]), 427 | }); 428 | } 429 | return points; 430 | } 431 | 432 | /** 433 | * Interpolates between two sets of points to create a smooth wave transition. 434 | * 435 | * @param {Array} currentPoints - The current set of points. 436 | * @param {Array} targetPoints - The target set of points. 437 | * @param {number} progress - The progress of the interpolation (0 to 1). 438 | * @param {boolean} [vertical=false] - Whether the wave is vertical or horizontal. 439 | * @param {number} height - The height of the wave container. 440 | * @param {number} width - The width of the wave container. 441 | * @returns {string} - The SVG path data for the interpolated wave. 442 | */ 443 | function interpolateWave(currentPoints, targetPoints, progress, vertical = false, height, width) { 444 | const interpolatedPoints = currentPoints.map((current, i) => { 445 | const target = targetPoints[i]; 446 | return { 447 | cpX: current.cpX + (target.cpX - current.cpX) * progress, 448 | cpY: vertical ? current.cpY : current.cpY + (target.cpY - current.cpY) * progress, 449 | x: vertical ? current.x + (target.x - current.x) * progress : current.x, 450 | y: vertical ? current.y : current.y + (target.y - current.y) * progress, 451 | }; 452 | }); 453 | 454 | let path = vertical 455 | ? `M ${width} ${height} L ${interpolatedPoints[0].x} ${height}` 456 | : `M 0 ${height} L 0 ${interpolatedPoints[0].y}`; 457 | 458 | for (let i = 0; i < interpolatedPoints.length; i++) { 459 | const { cpX, cpY, x, y } = interpolatedPoints[i]; 460 | path += ` Q ${cpX} ${cpY}, ${x} ${y}`; 461 | } 462 | 463 | path += vertical 464 | ? ` L 0 0 L ${width} 0 L ${width} ${height} Z` 465 | : ` L ${width} ${height} Z`; 466 | 467 | return path; 468 | } -------------------------------------------------------------------------------- /www/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Mark Zebley 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 | -------------------------------------------------------------------------------- /www/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzebley/dynamowaves/eef761843dbec62216f79252bbb8168f482728f0/www/android-chrome-192x192.png -------------------------------------------------------------------------------- /www/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzebley/dynamowaves/eef761843dbec62216f79252bbb8168f482728f0/www/android-chrome-512x512.png -------------------------------------------------------------------------------- /www/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzebley/dynamowaves/eef761843dbec62216f79252bbb8168f482728f0/www/apple-touch-icon.png -------------------------------------------------------------------------------- /www/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #e4ecec 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /www/dynamowaves.d.ts: -------------------------------------------------------------------------------- 1 | export * from '../src/dynamowaves'; -------------------------------------------------------------------------------- /www/dynamowaves.min.js: -------------------------------------------------------------------------------- 1 | !function(t){"function"==typeof define&&define.amd?define(t):t()}((function(){"use strict";class t extends HTMLElement{constructor(){super(),this.isAnimating=!1,this.animationFrameId=null,this.elapsedTime=0,this.startTime=null,this.isGeneratingWave=!1,this.currentPath=null,this.targetPath=null,this.pendingTargetPath=null,this.intersectionObserver=null,this.observerOptions=null}connectedCallback(){const t=this.className,e=this.id??Math.random().toString(36).substring(7),s=this.getAttribute("style"),n=this.getAttribute("data-wave-face")||"top";this.points=parseInt(this.getAttribute("data-wave-points"))||6,this.variance=parseFloat(this.getAttribute("data-variance"))||3,this.duration=parseFloat(this.getAttribute("data-wave-speed"))||7500,this.vertical="left"===n||"right"===n;const a="right"===n,h="bottom"===n;this.width=this.vertical?160:1440,this.height=this.vertical?1440:160,this.currentPath=i({width:this.width,height:this.height,points:this.points,variance:this.variance,vertical:this.vertical}),this.targetPath=i({width:this.width,height:this.height,points:this.points,variance:this.variance,vertical:this.vertical}),this.innerHTML=`\n \n `,this.svg=this.querySelector("svg"),this.path=this.querySelector("path"),this.play=this.play.bind(this),this.pause=this.pause.bind(this),this.generateNewWave=this.generateNewWave.bind(this);const r=this.getAttribute("data-wave-observe");r&&this.setupIntersectionObserver(r),"true"===this.getAttribute("data-wave-animate")&&(window.matchMedia("(prefers-reduced-motion: reduce)").matches||this.play())}play(t=null){if(this.isAnimating)return;this.isAnimating=!0;const e=t||this.duration,s=()=>{this.pendingTargetPath||(this.pendingTargetPath=i({width:this.width,height:this.height,points:this.points,variance:this.variance,vertical:this.vertical})),this.animateWave(e,(()=>{this.currentPath=this.targetPath,this.targetPath=this.pendingTargetPath,this.pendingTargetPath=i({width:this.width,height:this.height,points:this.points,variance:this.variance,vertical:this.vertical}),this.isAnimating&&s()}))};s()}pause(){this.isAnimating&&(this.isAnimating=!1,cancelAnimationFrame(this.animationFrameId),this.animationFrameId=null,this.elapsedTime+=performance.now()-(this.startTime||performance.now()),this.startTime=null)}disconnectedCallback(){this.intersectionObserver&&(this.intersectionObserver.disconnect(),this.intersectionObserver=null)}setupIntersectionObserver(t){const[i,e="0px"]=t.split(":"),s="once"===i;this.observerOptions={root:null,rootMargin:e,threshold:0},this.intersectionObserver=new IntersectionObserver((t=>{t.forEach((t=>{t.isIntersecting||(this.generateNewWave(),s&&(this.intersectionObserver.disconnect(),this.intersectionObserver=null))}))}),this.observerOptions),this.intersectionObserver.observe(this)}generateNewWave(t=800){this.isGeneratingWave||this.animationFrameId||(t<1&&(t=1),this.isGeneratingWave=!0,this.pendingTargetPath=i({width:this.width,height:this.height,points:this.points,variance:this.variance,vertical:this.vertical}),this.animateWave(t,(()=>{this.currentPath=this.targetPath,this.targetPath=this.pendingTargetPath,this.pendingTargetPath=null,this.isGeneratingWave=!1,this.animationFrameId=null})))}animateWave(t,s=null){const n=e(this.currentPath),a=e(this.targetPath);if(n.length!==a.length)return console.error("Point mismatch! Regenerating waves to ensure consistency."),this.currentPath=i({width:this.width,height:this.height,points:this.points,variance:this.variance,vertical:this.vertical}),void(this.targetPath=i({width:this.width,height:this.height,points:this.points,variance:this.variance,vertical:this.vertical}));const h=i=>{this.startTime||(this.startTime=i-this.elapsedTime);const e=i-this.startTime,r=Math.min(e/t,1),c=function(t,i,e,s=!1,n,a){const h=t.map(((t,n)=>{const a=i[n];return{cpX:t.cpX+(a.cpX-t.cpX)*e,cpY:s?t.cpY:t.cpY+(a.cpY-t.cpY)*e,x:s?t.x+(a.x-t.x)*e:t.x,y:s?t.y:t.y+(a.y-t.y)*e}}));let r=s?`M ${a} ${n} L ${h[0].x} ${n}`:`M 0 ${n} L 0 ${h[0].y}`;for(let t=0;t 2 | 3 | 4 | 5 | 6 | Dynamowaves - SVG wave templates 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 30 | 31 | 112 | 113 | 114 | 161 |

Dynamowaves

162 |

163 | Buttery smooth HTML templates 164 | that randomly generate themselves on render.
5.4 KB minified—with no 166 | dependencies! 167 |

168 |
169 |
170 |
171 | 173 | 174 |
175 |
176 | 177 |
178 |
179 |
180 |

Installation

181 |

182 | To make render times functionally instant, Dynamowaves intentionally eskews importing a 183 | library 184 | such as SVG.js to build a new SVG on execution.

185 | Instead, it selects at random from a curated collection of potential 186 | <paths> 187 | and leverages HTML web components (sorry, IE) to allow it to easily grab its reference 188 | element's applied attributes and then fully replaces the reference with a slick lil' wave.

189 | You can install Dynamowaves via npm or by including the script file directly in your project. 190 |

191 |

npm Installation

192 |

193 | To install Dynamowaves via npm, run the following command: 194 |

195 |
196 |           npm install dynamowaves
197 |         
198 |

199 | After installation, you can import Dynamowaves in your JavaScript or TypeScript files: 200 |

201 |
202 |           import 'dynamowaves';
203 |         
204 |

Angular Setup

205 |

206 | To use Dynamowaves in an Angular project, follow these additional steps: 207 |

208 |
    209 |
  1. 210 | In your angular.json file, add the dynamowaves 211 | script to the scripts 212 | array: 213 |
    214 |               "scripts": [
    215 |     "node_modules/dynamowaves/dist/dynamowaves.js"
    216 |   ]
    217 |             
    218 |
  2. 219 |
  3. 220 | In your app.module.ts file, add CUSTOM_ELEMENTS_SCHEMA to the schemas 222 | array: 223 |
    224 |               import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
    225 |   @NgModule({
    226 |   // ...
    227 |   schemas: [CUSTOM_ELEMENTS_SCHEMA]
    228 |   })
    229 |   export class AppModule { }
    230 |   
    231 |
  4. 232 |
233 |

Script Installation

234 |

235 | Alternatively, you can include the Dynamowaves script file directly in your project: 236 |

237 |
238 |           <!-- Download and add to your project locally -->
239 |             <script src="path/to/dynamowaves.js"></script>
240 |   
241 |             <!-- Or, reference the CDN -->
242 |             <script src="https://cdn.jsdelivr.net/gh/mzebley/dynamowaves/dist/dynamowaves.min.js" crossorigin="anonymous"></script>
243 |           
244 |   
245 |
246 |
247 |
248 |
249 |

Usage

250 |

251 | Since Dynamowaves use HTML templating syntax, all it takes to call one is to add the custom 252 | element to your HTML!

253 |
254 |           <!-- Without any added attributes you get a top facing wave filled with the current color of its parent --> 
255 |             <dynamo-wave></dynamo-wave>
256 |           
257 |         
258 | 259 |

260 | A dynamo-wave will inherit any class, 261 | id, or style applied to its invoking element. 262 |

263 |
264 |           <!-- Example 1 -->
265 |             <dynamo-wave style="fill:slateblue"></dynamo-wave>
266 |           
267 |         
268 | 269 |
270 |           .fill-theme {
271 |     fill: var(--theme);
272 |   }
273 |         
274 |
275 |           <!-- Example 2 -->
276 |             <dynamo-wave class="fill-theme"></dynamo-wave>
277 |           
278 |         
279 | 280 |
281 |           #special_wave {
282 |     height: 3rem;
283 |     width:80%;
284 |     transform: translateX(10%);
285 |   }
286 |         
287 |
288 |           <!-- Example 2 -->
289 |             <dynamo-wave id="special_wave" class="fill-theme fill-light"></dynamo-wave>
290 |           
291 |         
292 | 293 |
294 |
295 |

Data Attributes

296 |

297 | Dynamowaves come out of the box with a variety of data attributes that can be used to 298 | customize their 299 | appearance and behavior. 300 |

301 |

Points and Variance

302 |

303 | A dynamowave will generate itself a new, randomized wave path each time it's rendered. This 304 | wave path is calculated using points, which determine the number of points that 305 | make up the wave path, and variance - the maximum amount each point can deviate 306 | from the wave's center. 307 |

308 |
309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 322 | 323 | 324 | 325 | 326 | 329 | 330 | 331 | 332 | 333 |
AttributeDefaultOptions
320 |

data-wave-points

321 |
6Any positive integer
327 |

data-wave-variance

328 |
3Any positive integer
334 |
335 | 336 |
337 |           <!-- Update the points and variance to change up your wave feel --> 
338 |             <dynamo-wave data-wave-points="100" data-wave-variance="2"></dynamo-wave>
339 |           
340 |         
341 | 342 |
343 |
344 |

Wave Direction

345 |

346 | Need more than a just a wave that faces up? Leverage the data-wave-face 347 | attribute.

348 |
349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 362 | 363 | 364 | 365 | 366 |
AttributeDefaultOptions
360 |

data-wave-face

361 |
'top''top', 'bottom', 'left', 'right'
367 |
368 |
369 |           <!-- Bottom facing wave -->
370 |             <dynamo-wave class="fill-theme" data-wave-face="bottom"></dynamo-wave>
371 |           
372 |         
373 | 374 |
375 |
376 |
377 |               <!-- Left facing wave -->
378 |                 <dynamo-wave class="fill-theme" data-wave-face="left"></dynamo-wave>
379 |   
380 |                 <!-- Right facing wave -->
381 |                 <dynamo-wave class="fill-theme" data-wave-face="right"></dynamo-wave>
382 |               
383 |             
384 |
385 |
386 | 387 | 388 |
389 |
390 |
391 |
392 |

Wave Animation

393 |

394 | Want a dynamowave that you can just sit around and stare at? You might be interested in the 395 | data-wave-speed and data-wave-animate attributes. 396 |

397 |
398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 411 | 412 | 413 | 414 | 415 | 418 | 419 | 420 | 421 | 422 |
AttributeDefaultOptions
409 |

data-wave-speed

410 |
7500Duration in milliseconds
416 |

data-wave-animate

417 |
falsetrue, false
423 |
424 |
425 |

426 | Accessibility Note: The data-wave-animate attribute will be ignored if the 427 | viewer's browser has reduced motion enabled. 428 |

429 |
430 |
431 |           <!-- Animated wave -->
432 |             <dynamo-wave class="fill-theme" data-wave-speed="5000" data-wave-animate="true"></dynamo-wave>
433 |           
434 |         
435 | 436 |
437 |
438 |

Wave Observation

439 |

440 | Looking to really lean into generative design? The data-wave-observe 441 | attribute adds an intelligent IntersectionObserver 443 | to your dynamowave, enabling dynamic wave regeneration. 444 |

445 |
446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 459 | 460 | 468 | 469 | 470 |
AttributeDefaultOptions
457 |

data-wave-observe

458 |
unset 461 |
    462 |
  • once Generates a wave when leaving viewport, then stops
  • 463 |
  • repeat Continuously regenerates waves when leaving viewport
  • 464 |
  • once:300px Adds custom root margin
  • 465 |
  • repeat:100px Combines mode with custom margin
  • 466 |
467 |
471 |
472 | 473 |
474 |

475 | Margin Configuration: The optional pixel value after a colon adjusts the viewport 476 | intersection threshold. Use positive margins to start regeneration earlier, or negative margins to delay 477 | wave regeneration until the element is further from the viewport. 478 |

479 |
480 | 481 |
482 |           <!-- One-time wave regeneration -->
483 |       <dynamo-wave class="fill-theme" data-wave-observe="once"></dynamo-wave>
484 |       
485 |       <!-- Continuous wave regeneration -->
486 |       <dynamo-wave class="fill-theme" data-wave-observe="repeat"></dynamo-wave>
487 |       
488 |       <!-- Wave regenerates with 100px expanded viewport -->
489 |       <dynamo-wave class="fill-theme" data-wave-observe="repeat:100px"></dynamo-wave>
490 |       
491 |       <!-- Wave regenerates with 50px contracted viewport -->
492 |       <dynamo-wave class="fill-theme" data-wave-observe="once:-50px"></dynamo-wave>
493 |         
494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 |
506 |
507 |

Available Functions

508 |

509 | Dynamowaves come with a few functions that can be called to manipulate the wave after it's 510 | been rendered. 511 |

512 |

.generateNewWave()

513 |

514 | Want to see a new wave? Call generateNewWave(duration) on the 515 | dynamowave you'd like to regenerate. The duration parameter is 516 | an optional integer that determines how quickly the old wave morphs into the new wave - default is 517 | 800(ms). 518 |

519 |
520 |           const wave = document.querySelector('dynamo-wave');
521 |             wave.generateNewWave(500);
522 |         
523 | 524 | 526 |
527 |
528 |

.play()

529 |

530 | Call play(duration) on any dynamowave that you'd like 531 | to animate. The duration parameter is an optional integer that determines the 532 | length of the animation loop - default is 7500(ms). 533 |

534 |

.pause()

535 |

536 | To stop the animation loop, call pause() on the dynamowave 537 | you'd like to stop. 538 |

539 |
540 |           const wave = document.querySelector('dynamo-wave');
541 | 
542 | function toggleWaveAnimation() { 
543 |   if (wave.isAnimating) {
544 |     wave.pause();
545 |   } else {
546 |     wave.play(5000);
547 |   }
548 | }
549 |         
550 | 551 | 553 | 555 |
556 |
557 | 558 |
559 |
560 |

Practical Application

561 |

562 | Dynamowaves come out of the box entirely style agnostic. The onus is on the developer to add 563 | the necessary attributes to get them to fit their intended platform, but at the same time this provides nearly 564 | endless possibilities for customization.

565 | Make use of position:sticky and create a neat little header! 566 |

567 |
<!-- Widget classes not, uh, provided out-of-the-box -->
568 |   <div class="widget">
569 |     <div class="header">
570 |       <h2>I'm the heading!</h2>
571 |       <dynamo-wave data-wave-face="bottom"></dynamo-wave>
572 |     </div>
573 |     <div class="content">...</div>
574 |   </div>
575 |
576 |
577 |

I'm the heading!

578 | 579 |
580 |
Lorem ipsum dolor sit amet, ea sea regione concludaturque. Te eam pericula prodesset 581 | constituto. In forensibus voluptatum nam. Ius ne modus laboramus, quo illud altera mandamus eu. Persius 582 | oportere molestiae vel ut. Ei vel nusquam forensibus eloquentiam. 583 |

584 | Mei in posse error incorrupte. Ex rebum vidisse sea. Per sumo quando mucius cu, no persius signiferumque 585 | eos, 586 | et has deserunt pertinacia. Ea malis everti nostrud sed. 587 |

588 | Id regione prompta denique est, mei at veri essent instructior, mei id congue instructior. Nec ne legere 589 | tritani sadipscing. Oblique propriae theophrastus id quo, vero persequeris vix cu. Qui singulis pertinacia 590 | ex, 591 | ad agam doctus graecis eos, vis et erat accumsan. 592 |

593 | Cum esse quot essent te, in delicata conceptam cum, dicam iuvaret inimicus mei ad. Est ex cetero commune 594 | eleifend. Vim in aeque constituam, timeam debitis argumentum sed ea. His epicurei evertitur et, ea sanctus 595 | saperet sed. An harum referrentur nec, te sed errem patrioque, mei et tempor blandit sapientem. Vim te 596 | aeterno 597 | sapientem. 598 |

599 | In eam putant labores accusam. Ne sed evertitur torquatos. Dolor option regione nam ei, summo constituto usu 600 | at. Postea accusata et has, his te prima porro verterem. 601 |
602 |
603 |
604 |
605 |

606 | Use an animated dynamowave to add some more pizzazz to transition effects. 607 |

608 |
609 |
610 |
Content 1
611 |
Content 2
612 |
Content 3
613 |
Content 4
614 |
615 | 625 | 626 |
627 |
628 |
629 |
630 |

631 | Slap one of these bad boys along the edge of a photo to create an always fresh, 632 | mask-image effect without having to actually create multiple clip paths or 633 | image 634 | masks! 635 |

636 |
<div class="widget horizontal">
637 |     <div class="image-wrapper">
638 |       <img src="./img/image_path.jpeg" />
639 |       <!-- When covering an image, I find it helps the browser render 
640 |       to set the far edge with a bit of a negative overlap
641 |       It keeps the image from peeking through from behind the wave -->
642 |       <dynamo-wave data-wave-face="left" style="fill:white;position:absolute;right:-1px"></dynamo-wave>
643 |     </div>
644 |     <div class="content">...</div>
645 |   </div>
646 |
647 |
648 | 650 | 651 |
652 |
653 |

Eye-catching headline.

654 |

Further information to draw interest.

655 | Explore This 657 |
658 |
659 |
660 |
661 |
662 | 674 | 675 | 676 |
677 | 713 | 714 | 715 | 752 | 753 | 754 | 755 | -------------------------------------------------------------------------------- /www/main.css: -------------------------------------------------------------------------------- 1 | @import"https://fonts.googleapis.com/css?family=Merriweather:wght@900&family=Nunito:wght@400;700&display=swap";:root{--theme: hsl(179, 19%, 44%);--theme-light: hsl(179, 19%, 81%);--theme-medium: hsl(179, 19%, 55%);--theme-dark: hsl(179, 19%, 21%);--complement: hsl(359, 19%, 51%);--complement-light: hsl(359, 14%, 97%);--complement-medium: hsl(359, 14%, 67%);--transition-timing-natural: cubic-bezier(0.280, 0.840, 0.420, 1);--shadow-color: 0deg 4% 65%;--shadow-elevation-medium: 0.2px 0.7px 0.9px hsl(var(--shadow-color) / 0.22), 0.5px 1.8px 2.2px -0.5px hsl(var(--shadow-color) / 0.23), 1px 3.5px 4.3px -1.1px hsl(var(--shadow-color) / 0.24), 1.9px 6.8px 8.3px -1.6px hsl(var(--shadow-color) / 0.26), 3.6px 12.9px 15.7px -2.2px hsl(var(--shadow-color) / 0.27)}html{background-color:var(--complement-light)}pre+svg{margin-top:-1.5rem}.file-name{padding:.125rem .25rem;border-radius:.425rem;background:#d8dad9}body{flex-direction:column;justify-content:space-between;display:flex;font-family:"Nunito",sans-serif;font-size:1.25rem}*,::before,::after{box-sizing:inherit}h1,h2,h3,h4{font-size:2rem;font-family:"Merriweather",sans-serif;line-height:140%;color:var(--theme-dark);margin:.75rem 0;font-weight:900}h2{font-size:2rem}h3{font-size:1.75rem}h4{font-size:1.5rem}ol{margin-left:0;padding-left:1.25rem}p,li{font-size:1.25rem;max-width:600px;margin:.75rem 0;line-height:150%}.note{background:#fee972;padding:.5rem 1rem;border-radius:.425rem;margin:2rem 0;border:.125rem solid #fdd90d}.box{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:1rem 1.25rem;gap:3rem}.box>div{width:100%;max-width:800px}.widget{width:280px;height:360px;overflow-y:scroll;background:#fff;box-shadow:var(--shadow-elevation-medium);border-radius:1rem;position:relative;margin:3rem auto;display:flex;flex-direction:column}.widget#widget_example_2{overflow-y:hidden;width:320px;height:260px}.widget#widget_example_2>.content{flex:1;display:flex;width:1280px;padding:0;transition:all .5s ease}.widget#widget_example_2>.content>div{width:320px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1rem;padding:1rem}.widget.horizontal{width:100%;height:240px;overflow:hidden;display:grid;grid-template-columns:1fr 2fr;margin-left:auto}.widget .image-wrapper{position:relative;background-image:url("https://images.unsplash.com/photo-1655826525700-71fa9e171d72?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2864&q=80");background-size:cover;background-position:center}.widget .header{position:sticky;top:0}.widget .header dynamo-wave{display:flex;margin-top:-2px}.widget .footer{position:sticky;bottom:0;display:flex;flex-direction:column;align-items:center}.widget .footer dynamo-wave{display:flex;margin-bottom:-2px;height:3rem;width:100%}.widget .footer button{background-color:var(--complement-light)}.widget .footer button:after{display:none}.header h2{padding:1rem 1rem 0;margin:0;font-size:1.325rem;background:var(--theme);color:#fff}.footer .content{padding:.125rem 1rem 1rem;margin:0;background:var(--theme-light);width:100%;display:flex;flex-direction:column;align-items:center}.content{padding:.125rem 1rem 2rem}.content a,.link{color:#4169e1;font-weight:bold;cursor:pointer;text-decoration:underline;display:inline-flex;align-items:center;gap:.125rem}.content a[rel=external]::after,.link[rel=external]::after{content:url("data:image/svg+xml; utf8, ");display:inline-flex;height:24px;width:24px}button{background-color:rgba(0,0,0,0);color:var(--theme-dark);height:38px;border-radius:.425rem;border:2px solid var(--theme);padding:.5rem 1.5rem;font-size:1.25rem;text-align:center;cursor:pointer;align-items:center;-webkit-appearance:none;-moz-appearance:none;appearance:none;display:inline-flex;font-family:"Nunito",sans-serif;transition:color .325s var(--transition-timing-natural);transition-delay:.175s;-webkit-user-select:none;-moz-user-select:none;user-select:none;touch-action:manipulation;position:relative;gap:.325rem}button[disabled]{opacity:.5}button::after{content:"";background-color:var(--theme-light);position:absolute;width:100%;height:100%;z-index:-1;opacity:.5;border-radius:calc(.425rem - 2px);top:0;left:0;transform:translate3d(0.325rem, 0.325rem, 0) scale3d(1.025, 1.125, 1);transform-origin:top left;transition:opacity .325s var(--transition-timing-natural),transform .325s var(--transition-timing-natural);z-index:1;mix-blend-mode:darken}button>svg{height:1.5rem;width:1.5rem}button:active::after{transform:translate3d(0, 0, 0) scale3d(1, 1, 1);opacity:.5}@media(hover: hover)and (pointer: fine){button:hover::after{transform:translate3d(0, 0, 0) scale3d(1, 1, 1);opacity:.325}}header,footer{padding-left:1.5rem;padding-right:1.5rem;padding-top:1.5rem;text-align:center;position:relative;background:var(--theme-light)}footer{display:flex;justify-content:center;flex-wrap:wrap;gap:2rem;padding-bottom:1rem;text-align:left}header>svg{margin:3rem 0 0;width:100%;max-width:600px}header a>svg{position:absolute;width:32px;height:32px;opacity:.5;right:1rem;top:1rem}code{font-size:1.25rem}strong code{background:var(--theme-light);padding:.125rem .25rem;border-radius:.425rem}.wave-wrapper{z-index:-1;width:100%;transition:all .325s ease;transform-origin:top;transform:translate3d(0, -100%, 0)}.wave-wrapper.reveal{transform:translate3d(0, 0, 0)}pre{white-space:pre-line;margin:1.25rem 0;width:calc(100% + 2.5rem);margin-left:-1.25rem}pre code.hljs{padding:2rem 1.75em;border-radius:0}.list-indicator{color:var(--theme-dark);opacity:.75}.fill-theme{fill:var(--theme)}.fill-theme.fill-light{fill:var(--theme-light)}#special_wave{height:3rem;width:80%;transform:translateX(10%)}.table-container{overflow-x:auto;-webkit-overflow-scrolling:touch;width:calc(100% + 2.5rem);margin-left:-1.25rem}table{border-collapse:collapse;font-family:monospace;margin-top:1rem}th,td{border:.125rem solid #bbb;padding:.725rem;text-align:left}th p,td p{min-width:-moz-max-content;min-width:max-content}th{background-color:#eaeaea;font-weight:bold}tr:nth-child(even){background-color:#eaeaea}@media screen and (min-width: 39.999rem){footer{justify-content:space-between}.widget.horizontal{max-width:460px}.table-container{width:100%;margin-left:0;overflow:visible}.table-container table{width:100%}pre code.hljs{border-radius:.425rem}}/*# sourceMappingURL=main.css.map */ -------------------------------------------------------------------------------- /www/main.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["main.scss"],"names":[],"mappings":"AAAQ,8GAAA,CAGJ,MACE,2BAAA,CACA,iCAAA,CACA,kCAAA,CACA,gCAAA,CACA,gCAAA,CACA,sCAAA,CACA,uCAAA,CACA,iEAAA,CACA,2BAAA,CACA,oTAAA,CAQF,KACE,wCAAA,CAGF,QACE,kBAAA,CAGF,WACE,sBAAA,CACA,qBAAA,CACA,kBAAA,CAGF,KAGE,qBAAA,CAGA,6BAAA,CAIA,YAAA,CACA,+BAAA,CACA,iBAAA,CAGF,mBAGE,kBAAA,CAGF,YACE,cAAA,CACA,qCAAA,CACA,gBAAA,CACA,uBAAA,CACA,eAAA,CACA,eAAA,CAGF,GACE,cAAA,CAGF,GACE,iBAAA,CAGF,GACE,gBAAA,CAGF,GACE,aAAA,CACA,oBAAA,CAGF,KACE,iBAAA,CACA,eAAA,CACA,eAAA,CACA,gBAAA,CAGF,MACE,kBAAA,CACA,kBAAA,CACA,qBAAA,CACA,aAAA,CACA,4BAAA,CAGF,KACE,YAAA,CACA,qBAAA,CACA,kBAAA,CACA,sBAAA,CACA,oBAAA,CACA,QAAA,CAGF,SACE,UAAA,CACA,eAAA,CAGF,QACE,WAAA,CACA,YAAA,CACA,iBAAA,CACA,eAAA,CACA,yCAAA,CACA,kBAAA,CACA,iBAAA,CACA,gBAAA,CACA,YAAA,CACA,qBAAA,CACA,yBACE,iBAAA,CACA,WAAA,CACA,YAAA,CACA,kCACE,MAAA,CACA,YAAA,CACA,YAAA,CACA,SAAA,CACA,uBAAA,CACA,sCACE,WAAA,CACA,YAAA,CACA,qBAAA,CACA,kBAAA,CACA,sBAAA,CACA,QAAA,CACA,YAAA,CAMR,mBACE,UAAA,CACA,YAAA,CACA,eAAA,CACA,YAAA,CACA,6BAAA,CACA,gBAAA,CAGF,uBACE,iBAAA,CACA,sLAAA,CACA,qBAAA,CACA,0BAAA,CAGF,gBACE,eAAA,CACA,KAAA,CACA,4BACE,YAAA,CACA,eAAA,CAIJ,gBACE,eAAA,CACA,QAAA,CACA,YAAA,CACA,qBAAA,CACA,kBAAA,CACA,4BACE,YAAA,CACA,kBAAA,CACA,WAAA,CACA,UAAA,CAEF,uBACE,wCAAA,CACA,6BACE,YAAA,CAKN,WACE,mBAAA,CACA,QAAA,CACA,kBAAA,CACA,uBAAA,CACA,UAAA,CAIF,iBACE,yBAAA,CACA,QAAA,CACA,6BAAA,CACA,UAAA,CACA,YAAA,CACA,qBAAA,CACA,kBAAA,CAGF,SACE,yBAAA,CAGF,iBACE,aAAA,CACA,gBAAA,CACA,cAAA,CACA,yBAAA,CACA,mBAAA,CACA,kBAAA,CACA,WAAA,CACA,2DACE,mZAAA,CACA,mBAAA,CACA,WAAA,CACA,UAAA,CAIJ,OACE,8BAAA,CACA,uBAAA,CACA,WAAA,CACA,qBAAA,CACA,6BAAA,CACA,oBAAA,CACA,iBAAA,CACA,iBAAA,CACA,cAAA,CACA,kBAAA,CACA,uBAAA,CAAA,oBAAA,CAAA,eAAA,CACA,mBAAA,CACA,+BAAA,CACA,uDAAA,CACA,sBAAA,CACA,wBAAA,CACA,qBAAA,CACA,gBAAA,CACA,yBAAA,CACA,iBAAA,CACA,WAAA,CACA,iBACE,UAAA,CAIJ,cACE,UAAA,CACA,mCAAA,CACA,iBAAA,CACA,UAAA,CACA,WAAA,CACA,UAAA,CACA,UAAA,CACA,iCAAA,CACA,KAAA,CACA,MAAA,CACA,qEAAA,CACA,yBAAA,CACA,0GAAA,CACA,SAAA,CACA,qBAAA,CAGF,WACE,aAAA,CACA,YAAA,CAGF,qBACE,+CAAA,CACA,UAAA,CAGF,wCACE,oBACA,+CAAA,CACA,YAAA,CAAA,CAMF,cACE,mBAAA,CACA,oBAAA,CACA,kBAAA,CACA,iBAAA,CACA,iBAAA,CACA,6BAAA,CAGF,OACE,YAAA,CACA,sBAAA,CACA,cAAA,CACA,QAAA,CACA,mBAAA,CACA,eAAA,CAKF,WACE,eAAA,CACA,UAAA,CACA,eAAA,CAGF,aACE,iBAAA,CACA,UAAA,CACA,WAAA,CACA,UAAA,CACA,UAAA,CACA,QAAA,CAGF,KACE,iBAAA,CAGF,YACE,6BAAA,CACF,sBAAA,CACA,qBAAA,CAGA,cACE,UAAA,CACA,UAAA,CACA,yBAAA,CACA,oBAAA,CACA,kCAAA,CAGF,qBACE,8BAAA,CAGF,IACE,oBAAA,CACA,gBAAA,CACA,yBAAA,CACA,oBAAA,CAGF,cACE,mBAAA,CACA,eAAA,CAGF,gBACE,uBAAA,CACA,WAAA,CAGF,YACE,iBAAA,CAGF,uBACE,uBAAA,CAGF,cACE,WAAA,CACA,SAAA,CACA,yBAAA,CAGF,iBACE,eAAA,CACA,gCAAA,CACA,yBAAA,CACA,oBAAA,CAGF,MAEE,wBAAA,CAGA,qBAAA,CACA,eAAA,CAKJ,MAEI,yBAAA,CAEA,eAAA,CAEA,eAAA,CAEA,UACE,0BAAA,CAAA,qBAAA,CAON,GAEI,wBAAA,CAEA,gBAAA,CAMJ,mBAEI,wBAAA,CAIJ,yCACE,OACE,6BAAA,CAEF,mBACE,eAAA,CAIF,iBACE,UAAA,CACA,aAAA,CACA,gBAAA,CACA,uBACE,UAAA,CAIJ,cACE,qBAAA,CAAA","file":"main.css"} -------------------------------------------------------------------------------- /www/main.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Merriweather:wght@900&family=Nunito:wght@400;700&display=swap'); 2 | 3 | /* @import url('https://fonts.googleapis.com/css2?family=Cabin+Sketch:wght@700&family=Fascinate&family=Permanent+Marker&family=Rubik+Moonrocks&display=swap&text=shuffle'); */ 4 | :root { 5 | --theme: hsl(179, 19%, 44%); 6 | --theme-light: hsl(179, 19%, 81%); 7 | --theme-medium: hsl(179, 19%, 55%); 8 | --theme-dark: hsl(179, 19%, 21%); 9 | --complement: hsl(359, 19%, 51%); 10 | --complement-light: hsl(359, 14%, 97%); 11 | --complement-medium: hsl(359, 14%, 67%); 12 | --transition-timing-natural: cubic-bezier(0.280, 0.840, 0.420, 1); 13 | --shadow-color: 0deg 4% 65%; 14 | --shadow-elevation-medium: 15 | 0.2px 0.7px 0.9px hsl(var(--shadow-color) / 0.22), 16 | 0.5px 1.8px 2.2px -0.5px hsl(var(--shadow-color) / 0.23), 17 | 1px 3.5px 4.3px -1.1px hsl(var(--shadow-color) / 0.24), 18 | 1.9px 6.8px 8.3px -1.6px hsl(var(--shadow-color) / 0.26), 19 | 3.6px 12.9px 15.7px -2.2px hsl(var(--shadow-color) / 0.27); 20 | } 21 | 22 | html { 23 | background-color: var(--complement-light); 24 | } 25 | 26 | pre + svg { 27 | margin-top:-1.5rem; 28 | } 29 | 30 | .file-name { 31 | padding: .125rem .25rem; 32 | border-radius: .425rem; 33 | background: hsl(179, 2%, 85%) 34 | } 35 | 36 | body { 37 | -webkit-flex-direction: column; 38 | -ms-flex-direction: column; 39 | flex-direction: column; 40 | -webkit-box-pack: justify; 41 | -webkit-justify-content: space-between; 42 | justify-content: space-between; 43 | display: -webkit-box; 44 | display: -webkit-flex; 45 | display: -ms-flexbox; 46 | display: flex; 47 | font-family: 'Nunito', sans-serif; 48 | font-size: 1.25rem; 49 | } 50 | 51 | *, 52 | ::before, 53 | ::after { 54 | box-sizing: inherit; 55 | } 56 | 57 | h1, h2, h3, h4 { 58 | font-size: 2rem; 59 | font-family: 'Merriweather', sans-serif; 60 | line-height: 140%; 61 | color:var(--theme-dark); 62 | margin:.75rem 0; 63 | font-weight: 900; 64 | } 65 | 66 | h2 { 67 | font-size: 2rem; 68 | } 69 | 70 | h3 { 71 | font-size: 1.75rem; 72 | } 73 | 74 | h4 { 75 | font-size: 1.5rem; 76 | } 77 | 78 | ol { 79 | margin-left: 0; 80 | padding-left:1.25rem; 81 | } 82 | 83 | p, li { 84 | font-size:1.25rem; 85 | max-width:600px; 86 | margin:.75rem 0; 87 | line-height: 150%; 88 | } 89 | 90 | .note { 91 | background:hsl(51, 98%, 72%); 92 | padding:.5rem 1rem; 93 | border-radius:.425rem; 94 | margin:2rem 0; 95 | border: .125rem solid hsl(51, 98%, 52%); 96 | } 97 | 98 | .box { 99 | display: flex; 100 | flex-direction: column; 101 | align-items: center; 102 | justify-content: center; 103 | padding:1rem 1.25rem; 104 | gap:3rem; 105 | } 106 | 107 | .box > div { 108 | width:100%; 109 | max-width:800px; 110 | } 111 | 112 | .widget { 113 | width: 280px; 114 | height: 360px; 115 | overflow-y: scroll; 116 | background:white; 117 | box-shadow: var(--shadow-elevation-medium); 118 | border-radius: 1rem; 119 | position:relative; 120 | margin:3rem auto; 121 | display:flex; 122 | flex-direction: column; 123 | &#widget_example_2{ 124 | overflow-y:hidden; 125 | width: 320px; 126 | height: 260px; 127 | > .content { 128 | flex:1; 129 | display:flex; 130 | width: calc(320px * 4); 131 | padding: 0; 132 | transition: all .5s ease; 133 | > div { 134 | width: 320px; 135 | display:flex; 136 | flex-direction: column; 137 | align-items: center; 138 | justify-content: center; 139 | gap:1rem; 140 | padding: 1rem; 141 | } 142 | } 143 | } 144 | } 145 | 146 | .widget.horizontal { 147 | width: 100%; 148 | height:240px; 149 | overflow:hidden; 150 | display:grid; 151 | grid-template-columns: 1fr 2fr; 152 | margin-left:auto; 153 | } 154 | 155 | .widget .image-wrapper { 156 | position:relative; 157 | background-image: url('https://images.unsplash.com/photo-1655826525700-71fa9e171d72?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2864&q=80'); 158 | background-size: cover; 159 | background-position: center; 160 | } 161 | 162 | .widget .header { 163 | position:sticky; 164 | top:0; 165 | dynamo-wave { 166 | display:flex; 167 | margin-top:-2px; 168 | } 169 | } 170 | 171 | .widget .footer { 172 | position:sticky; 173 | bottom:0; 174 | display:flex; 175 | flex-direction: column; 176 | align-items: center; 177 | dynamo-wave { 178 | display:flex; 179 | margin-bottom:-2px; 180 | height: 3rem; 181 | width: 100%; 182 | } 183 | button { 184 | background-color: var(--complement-light); 185 | &:after { 186 | display:none; 187 | } 188 | } 189 | } 190 | 191 | .header h2 { 192 | padding:1rem 1rem 0; 193 | margin:0; 194 | font-size: 1.325rem; 195 | background:var(--theme); 196 | color:white; 197 | } 198 | 199 | 200 | .footer .content { 201 | padding: .125rem 1rem 1rem; 202 | margin:0; 203 | background:var(--theme-light); 204 | width:100%; 205 | display:flex; 206 | flex-direction: column; 207 | align-items: center; 208 | } 209 | 210 | .content { 211 | padding: .125rem 1rem 2rem; 212 | } 213 | 214 | .content a, .link { 215 | color:RoyalBlue; 216 | font-weight:bold; 217 | cursor: pointer; 218 | text-decoration: underline; 219 | display:inline-flex; 220 | align-items: center; 221 | gap:.125rem; 222 | &[rel="external"]::after { 223 | content: url("data:image/svg+xml; utf8, "); 224 | display:inline-flex; 225 | height:24px; 226 | width:24px; 227 | } 228 | } 229 | 230 | button { 231 | background-color: rgba(0, 0, 0, 0); 232 | color: var(--theme-dark); 233 | height: 38px; 234 | border-radius: .425rem; 235 | border: 2px solid var(--theme); 236 | padding: .5rem 1.5rem; 237 | font-size: 1.25rem; 238 | text-align: center; 239 | cursor: pointer; 240 | align-items: center; 241 | appearance:none; 242 | display: inline-flex; 243 | font-family: "Nunito", sans-serif; 244 | transition: color .325s var(--transition-timing-natural); 245 | transition-delay: .175s; 246 | -webkit-user-select: none; 247 | -moz-user-select: none; 248 | user-select: none; 249 | touch-action: manipulation; 250 | position: relative; 251 | gap:.325rem; 252 | &[disabled] { 253 | opacity:.5; 254 | } 255 | } 256 | 257 | button::after { 258 | content: ""; 259 | background-color: var(--theme-light); 260 | position: absolute; 261 | width: 100%; 262 | height: 100%; 263 | z-index: -1; 264 | opacity: .5; 265 | border-radius: calc(.425rem - 2px); 266 | top: 0; 267 | left: 0; 268 | transform: translate3d(.325rem, .325rem, 0) scale3d(1.025, 1.125, 1); 269 | transform-origin: top left; 270 | transition: opacity .325s var(--transition-timing-natural), transform .325s var(--transition-timing-natural); 271 | z-index:1; 272 | mix-blend-mode: darken; 273 | } 274 | 275 | button>svg { 276 | height: 1.5rem; 277 | width: 1.5rem; 278 | } 279 | 280 | button:active::after { 281 | transform: translate3d(0, 0, 0) scale3d(1, 1, 1); 282 | opacity: .5; 283 | } 284 | 285 | @media (hover: hover) and (pointer: fine) { 286 | button:hover::after { 287 | transform: translate3d(0, 0, 0) scale3d(1, 1, 1); 288 | opacity: .325; 289 | } 290 | } 291 | 292 | 293 | 294 | header, footer { 295 | padding-left: 1.5rem; 296 | padding-right: 1.5rem; 297 | padding-top: 1.5rem; 298 | text-align: center; 299 | position: relative; 300 | background:var(--theme-light); 301 | } 302 | 303 | footer { 304 | display:flex; 305 | justify-content: center; 306 | flex-wrap: wrap; 307 | gap:2rem; 308 | padding-bottom:1rem; 309 | text-align: left; 310 | } 311 | 312 | 313 | 314 | header>svg { 315 | margin: 3rem 0 0; 316 | width: 100%; 317 | max-width: 600px; 318 | } 319 | 320 | header a>svg { 321 | position: absolute; 322 | width: 32px; 323 | height: 32px; 324 | opacity: .5; 325 | right: 1rem; 326 | top: 1rem; 327 | } 328 | 329 | code { 330 | font-size: 1.25rem; 331 | } 332 | 333 | strong code { 334 | background: var(--theme-light); 335 | padding: 0.125rem .25rem; 336 | border-radius: 0.425rem; 337 | } 338 | 339 | .wave-wrapper { 340 | z-index: -1; 341 | width: 100%; 342 | transition: all .325s ease; 343 | transform-origin: top; 344 | transform: translate3d(0, -100%, 0); 345 | } 346 | 347 | .wave-wrapper.reveal { 348 | transform: translate3d(0, 0, 0); 349 | } 350 | 351 | pre { 352 | white-space: pre-line; 353 | margin: 1.25rem 0; 354 | width:calc(100% + 2.5rem); 355 | margin-left:-1.25rem; 356 | } 357 | 358 | pre code.hljs { 359 | padding: 2rem 1.75em; 360 | border-radius: 0; 361 | } 362 | 363 | .list-indicator { 364 | color:var(--theme-dark); 365 | opacity:.75; 366 | } 367 | 368 | .fill-theme { 369 | fill: var(--theme); 370 | } 371 | 372 | .fill-theme.fill-light { 373 | fill: var(--theme-light); 374 | } 375 | 376 | #special_wave { 377 | height: 3rem; 378 | width:80%; 379 | transform: translateX(10%); 380 | } 381 | 382 | .table-container { 383 | overflow-x: auto; 384 | -webkit-overflow-scrolling: touch; 385 | width: calc(100% + 2.5rem); 386 | margin-left: -1.25rem; 387 | } 388 | 389 | table { 390 | 391 | border-collapse: collapse; 392 | // width:100%; 393 | 394 | font-family:monospace; 395 | margin-top:1rem; 396 | 397 | } 398 | 399 | 400 | th, td { 401 | 402 | border: .125rem solid #bbb; 403 | 404 | padding: .725rem; 405 | 406 | text-align: left; 407 | 408 | p { 409 | min-width: max-content; 410 | } 411 | 412 | } 413 | 414 | 415 | 416 | th { 417 | 418 | background-color: #eaeaea; 419 | 420 | font-weight: bold; 421 | 422 | } 423 | 424 | 425 | 426 | tr:nth-child(even) { 427 | 428 | background-color: #eaeaea; 429 | 430 | } 431 | 432 | @media screen and (min-width: 39.999rem) { 433 | footer { 434 | justify-content: space-between; 435 | } 436 | .widget.horizontal { 437 | max-width: 460px; 438 | 439 | 440 | } 441 | .table-container { 442 | width: 100%; 443 | margin-left:0; 444 | overflow: visible; 445 | table { 446 | width:100%; 447 | } 448 | } 449 | 450 | pre code.hljs { 451 | border-radius: .425rem; 452 | } 453 | 454 | } -------------------------------------------------------------------------------- /www/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzebley/dynamowaves/eef761843dbec62216f79252bbb8168f482728f0/www/mstile-144x144.png -------------------------------------------------------------------------------- /www/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzebley/dynamowaves/eef761843dbec62216f79252bbb8168f482728f0/www/mstile-150x150.png -------------------------------------------------------------------------------- /www/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzebley/dynamowaves/eef761843dbec62216f79252bbb8168f482728f0/www/mstile-310x150.png -------------------------------------------------------------------------------- /www/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzebley/dynamowaves/eef761843dbec62216f79252bbb8168f482728f0/www/mstile-310x310.png -------------------------------------------------------------------------------- /www/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzebley/dynamowaves/eef761843dbec62216f79252bbb8168f482728f0/www/mstile-70x70.png -------------------------------------------------------------------------------- /www/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | 7 | /* Sections 8 | ========================================================================== */ 9 | 10 | /** 11 | * Remove the margin in all browsers. 12 | */ 13 | 14 | body { 15 | margin: 0; 16 | transition: background-color .225s ease-in-out; 17 | } 18 | 19 | *, 20 | *::before, 21 | *::after { 22 | box-sizing: border-box; 23 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0); 24 | } 25 | 26 | /** 27 | * Render the `main` element consistently in IE. 28 | */ 29 | 30 | main { 31 | display: block; 32 | } 33 | 34 | /* Grouping content 35 | ========================================================================== */ 36 | 37 | /** 38 | * 1. Add the correct box sizing in Firefox. 39 | * 2. Show the overflow in Edge and IE. 40 | */ 41 | 42 | hr { 43 | box-sizing: content-box; 44 | /* 1 */ 45 | height: 0; 46 | /* 1 */ 47 | overflow: visible; 48 | /* 2 */ 49 | } 50 | 51 | /** 52 | * 1. Correct the inheritance and scaling of font size in all browsers. 53 | * 2. Correct the odd `em` font sizing in all browsers. 54 | */ 55 | 56 | pre { 57 | font-family: monospace, monospace; 58 | /* 1 */ 59 | font-size: 1em; 60 | /* 2 */ 61 | } 62 | 63 | /* Text-level semantics 64 | ========================================================================== */ 65 | 66 | /** 67 | * Remove the gray background on active links in IE 10. 68 | */ 69 | 70 | a { 71 | background-color: transparent; 72 | text-decoration: none; 73 | } 74 | 75 | /** 76 | * 1. Remove the bottom border in Chrome 57- 77 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 78 | */ 79 | 80 | abbr[title] { 81 | border-bottom: none; 82 | /* 1 */ 83 | text-decoration: underline; 84 | /* 2 */ 85 | text-decoration: underline dotted; 86 | /* 2 */ 87 | } 88 | 89 | /** 90 | * 1. Correct the inheritance and scaling of font size in all browsers. 91 | * 2. Correct the odd `em` font sizing in all browsers. 92 | */ 93 | 94 | code, 95 | kbd, 96 | samp { 97 | font-family: monospace, monospace; 98 | /* 1 */ 99 | font-size: 1em; 100 | /* 2 */ 101 | } 102 | 103 | /** 104 | * Add the correct font size in all browsers. 105 | */ 106 | 107 | small { 108 | font-size: 80%; 109 | } 110 | 111 | /** 112 | * Prevent `sub` and `sup` elements from affecting the line height in 113 | * all browsers. 114 | */ 115 | 116 | sub, 117 | sup { 118 | font-size: 75%; 119 | line-height: 0; 120 | position: relative; 121 | vertical-align: baseline; 122 | } 123 | 124 | sub { 125 | bottom: -0.25em; 126 | } 127 | 128 | sup { 129 | top: -0.5em; 130 | } 131 | 132 | /* Embedded content 133 | ========================================================================== */ 134 | 135 | /** 136 | * Remove the border on images inside links in IE 10. 137 | */ 138 | 139 | img { 140 | border-style: none; 141 | } 142 | 143 | /* Forms 144 | ========================================================================== */ 145 | 146 | /** 147 | * Show the overflow in IE. 148 | * 1. Show the overflow in Edge. 149 | */ 150 | 151 | button, 152 | input { 153 | /* 1 */ 154 | overflow: visible; 155 | } 156 | 157 | ul.reset, ol.reset { 158 | padding:0; 159 | margin:0; 160 | padding:0; 161 | } 162 | 163 | /** 164 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 165 | * 1. Remove the inheritance of text transform in Firefox. 166 | */ 167 | 168 | button, 169 | select { 170 | /* 1 */ 171 | text-transform: none; 172 | } 173 | 174 | /** 175 | * Correct the inability to style clickable types in iOS and Safari. 176 | */ 177 | 178 | button, 179 | [type="button"], 180 | [type="reset"], 181 | [type="submit"] { 182 | -webkit-appearance: button; 183 | } 184 | 185 | /** 186 | * Remove the inner border and padding in Firefox. 187 | */ 188 | 189 | button::-moz-focus-inner, 190 | [type="button"]::-moz-focus-inner, 191 | [type="reset"]::-moz-focus-inner, 192 | [type="submit"]::-moz-focus-inner { 193 | border-style: none; 194 | padding: 0; 195 | } 196 | 197 | /** 198 | * Restore the focus styles unset by the previous rule. 199 | */ 200 | 201 | button:-moz-focusring, 202 | [type="button"]:-moz-focusring, 203 | [type="reset"]:-moz-focusring, 204 | [type="submit"]:-moz-focusring { 205 | outline: 1px dotted ButtonText; 206 | } 207 | 208 | /** 209 | * Correct the padding in Firefox. 210 | */ 211 | 212 | fieldset { 213 | padding: 0.35em 0.75em 0.625em; 214 | } 215 | 216 | /** 217 | * 1. Correct the text wrapping in Edge and IE. 218 | * 2. Correct the color inheritance from `fieldset` elements in IE. 219 | * 3. Remove the padding so developers are not caught out when they zero out 220 | * `fieldset` elements in all browsers. 221 | */ 222 | 223 | legend { 224 | box-sizing: border-box; 225 | /* 1 */ 226 | color: inherit; 227 | /* 2 */ 228 | display: table; 229 | /* 1 */ 230 | max-width: 100%; 231 | /* 1 */ 232 | padding: 0; 233 | /* 3 */ 234 | white-space: normal; 235 | /* 1 */ 236 | } 237 | 238 | /** 239 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 240 | */ 241 | 242 | progress { 243 | vertical-align: baseline; 244 | } 245 | 246 | /** 247 | * Remove the default vertical scrollbar in IE 10+. 248 | */ 249 | 250 | textarea { 251 | overflow: auto; 252 | } 253 | 254 | /** 255 | * 1. Add the correct box sizing in IE 10. 256 | * 2. Remove the padding in IE 10. 257 | */ 258 | 259 | [type="checkbox"], 260 | [type="radio"] { 261 | box-sizing: border-box; 262 | /* 1 */ 263 | padding: 0; 264 | /* 2 */ 265 | } 266 | 267 | /** 268 | * Correct the cursor style of increment and decrement buttons in Chrome. 269 | */ 270 | 271 | [type="number"]::-webkit-inner-spin-button, 272 | [type="number"]::-webkit-outer-spin-button { 273 | height: auto; 274 | } 275 | 276 | /** 277 | * 1. Correct the odd appearance in Chrome and Safari. 278 | * 2. Correct the outline style in Safari. 279 | */ 280 | 281 | [type="search"] { 282 | -webkit-appearance: textfield; 283 | /* 1 */ 284 | outline-offset: -2px; 285 | /* 2 */ 286 | } 287 | 288 | /** 289 | * Remove the inner padding in Chrome and Safari on macOS. 290 | */ 291 | 292 | [type="search"]::-webkit-search-decoration { 293 | -webkit-appearance: none; 294 | } 295 | 296 | /** 297 | * 1. Correct the inability to style clickable types in iOS and Safari. 298 | * 2. Change font properties to `inherit` in Safari. 299 | */ 300 | 301 | ::-webkit-file-upload-button { 302 | -webkit-appearance: button; 303 | /* 1 */ 304 | font: inherit; 305 | /* 2 */ 306 | } 307 | 308 | /* Interactive 309 | ========================================================================== */ 310 | 311 | /* 312 | * Add the correct display in Edge, IE 10+, and Firefox. 313 | */ 314 | 315 | details { 316 | display: block; 317 | } 318 | 319 | /* 320 | * Add the correct display in all browsers. 321 | */ 322 | 323 | summary { 324 | display: list-item; 325 | } 326 | 327 | /* Misc 328 | ========================================================================== */ 329 | 330 | /** 331 | * Add the correct display in IE 10+. 332 | */ 333 | 334 | template { 335 | display: none; 336 | } 337 | 338 | /** 339 | * Add the correct display in IE 10. 340 | */ 341 | 342 | [hidden] { 343 | display: none; 344 | } 345 | 346 | /*! HTML5 Boilerplate v8.0.0 | MIT License | https://html5boilerplate.com/ */ 347 | 348 | /* main.css 2.1.0 | MIT License | https://github.com/h5bp/main.css#readme */ 349 | /* 350 | * What follows is the result of much research on cross-browser styling. 351 | * Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal, 352 | * Kroc Camen, and the H5BP dev community and team. 353 | */ 354 | 355 | 356 | /* 357 | * Remove text-shadow in selection highlight: 358 | * https://twitter.com/miketaylr/status/12228805301 359 | * 360 | * Vendor-prefixed and regular ::selection selectors cannot be combined: 361 | * https://stackoverflow.com/a/16982510/7133471 362 | * 363 | * Customize the background color to match your design. 364 | */ 365 | 366 | ::-moz-selection { 367 | background: #b3d4fc; 368 | text-shadow: none; 369 | } 370 | 371 | ::selection { 372 | background: #b3d4fc; 373 | text-shadow: none; 374 | } 375 | 376 | /* 377 | * A better looking default horizontal rule 378 | */ 379 | 380 | hr { 381 | display: block; 382 | height: 1px; 383 | border: 0; 384 | border-top: 1px solid #ccc; 385 | margin: 1em 0; 386 | padding: 0; 387 | } 388 | 389 | /* 390 | * Remove the gap between audio, canvas, iframes, 391 | * images, videos and the bottom of their containers: 392 | * https://github.com/h5bp/html5-boilerplate/issues/440 393 | */ 394 | 395 | audio, 396 | canvas, 397 | iframe, 398 | img, 399 | svg, 400 | video { 401 | vertical-align: middle; 402 | } 403 | 404 | /* 405 | * Remove default fieldset styles. 406 | */ 407 | 408 | fieldset { 409 | border: 0; 410 | margin: 0; 411 | padding: 0; 412 | } 413 | 414 | /* 415 | * Allow only vertical resizing of textareas. 416 | */ 417 | 418 | textarea { 419 | resize: vertical; 420 | } -------------------------------------------------------------------------------- /www/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 18 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /www/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | --------------------------------------------------------------------------------