From aeb6a8073958b595bbb1e9553a882c30ecbaad7a Mon Sep 17 00:00:00 2001 From: Mark Zebley Date: Sun, 22 Dec 2024 09:17:31 -0600 Subject: [PATCH] roolup updates; better typing; minor misc refinement and polish --- dist/dynamowaves.d.ts | 98 +- dist/dynamowaves.js | 104 +- dist/dynamowaves.min.js | 2 +- package-lock.json | 5717 ++------------------------------------- package.json | 22 +- rollup.config.js | 71 +- src/dynamowaves.d.ts | 118 +- src/dynamowaves.js | 105 +- www/dynamowaves.min.js | 2 +- 9 files changed, 663 insertions(+), 5576 deletions(-) diff --git a/dist/dynamowaves.d.ts b/dist/dynamowaves.d.ts index 61d3d33..0271990 100644 --- a/dist/dynamowaves.d.ts +++ b/dist/dynamowaves.d.ts @@ -1 +1,97 @@ -export * from '../src/dynamowaves'; \ No newline at end of file +// dynamoves.d.ts + +// Interface for wave generation options +interface WaveGenerationOptions { + width: number; + height: number; + points: number; + variance: number; + vertical?: boolean; +} + +// Interface for wave point structure +interface WavePoint { + cpX: number; + cpY: number; + x: number; + y: number; +} + +// Type for the wave direction +type WaveDirection = 'top' | 'bottom' | 'left' | 'right'; + +// Interface for intersection observer options +interface WaveObserverOptions { + root: Element | null; + rootMargin: string; + threshold: number; +} + +declare class DynamoWave extends HTMLElement { + // Properties + private isAnimating: boolean; + private animationFrameId: number | null; + private elapsedTime: number; + private startTime: number | null; + private isGeneratingWave: boolean; + private currentPath: string | null; + private targetPath: string | null; + private pendingTargetPath: string | null; + private intersectionObserver: IntersectionObserver | null; + private observerOptions: WaveObserverOptions | null; + private points: number; + private variance: number; + private duration: number; + private vertical: boolean; + private width: number; + private height: number; + private svg: SVGSVGElement; + private path: SVGPathElement; + + constructor(); + + // Lifecycle methods + connectedCallback(): void; + disconnectedCallback(): void; + + // Public methods + play(customDuration?: number | null): void; + pause(): void; + generateNewWave(duration?: number): void; + + // Private methods + private setupIntersectionObserver(observeConfig: string): void; + private animateWave(duration: number, onComplete?: (() => void) | null): void; +} + +// Global declaration for custom element +declare global { + interface HTMLElementTagNameMap { + 'dynamo-wave': DynamoWave; + } +} + +// Component attributes interface +interface DynamoWaveAttributes { + 'data-wave-face'?: WaveDirection; + 'data-wave-points'?: string; + 'data-variance'?: string; + 'data-wave-speed'?: string; + 'data-wave-animate'?: string; + 'data-wave-observe'?: string; +} + +// Extend HTMLElement interface to include our attributes +declare global { + interface HTMLElementTagNameMap { + 'dynamo-wave': DynamoWave; + } + + namespace JSX { + interface IntrinsicElements { + 'dynamo-wave': Partial; + } + } +} + +export { DynamoWave, type DynamoWaveAttributes, type WaveDirection, type WaveGenerationOptions, type WaveObserverOptions, type WavePoint }; diff --git a/dist/dynamowaves.js b/dist/dynamowaves.js index 1f93531..229e913 100644 --- a/dist/dynamowaves.js +++ b/dist/dynamowaves.js @@ -4,6 +4,26 @@ })((function () { 'use strict'; class DynamoWave extends HTMLElement { + /** + * Constructs a new instance of the class. + * + * @constructor + * + * @property {boolean} isAnimating - Indicates whether the animation is currently running. + * @property {number|null} animationFrameId - The ID of the current animation frame request. + * @property {number} elapsedTime - The elapsed time since the animation started. + * @property {number|null} startTime - The start time of the animation. + * + * @property {boolean} isGeneratingWave - Indicates whether a wave is currently being generated. + * + * @property {Path2D|null} currentPath - The current wave path. + * @property {Path2D|null} targetPath - The target wave path. + * @property {Path2D|null} pendingTargetPath - The next wave path to be generated. + * + * @property {IntersectionObserver|null} intersectionObserver - The Intersection Observer instance. + * @property {Object|null} observerOptions - The options for the Intersection Observer. + */ + constructor() { super(); this.isAnimating = false; @@ -23,6 +43,14 @@ this.observerOptions = null; } + /** + * Called when the custom element is appended to the DOM. + * Initializes the wave properties, constructs the SVG element, + * and sets up animation and observation if specified. + * + * @method connectedCallback + * @returns {void} + */ connectedCallback() { const classes = this.className; const id = this.id ?? Math.random().toString(36).substring(7); @@ -66,7 +94,7 @@ style="${flipX ? "transform:scaleX(-1);" : ""}${flipY ? "transform:scaleY(-1);" : ""}${styles || ""}" id="${id}" aria-hidden="true" - role="img" + role="presentation" > @@ -96,6 +124,13 @@ } // Public method to play the animation + /** + * Starts the wave animation. If a custom duration is provided, it will be used for the animation; + * otherwise, the instance's default duration will be used. The animation will continue looping + * until `stop` is called. + * + * @param {number|null} [customDuration=null] - Optional custom duration for the animation in milliseconds. + */ play(customDuration = null) { if (this.isAnimating) return; this.isAnimating = true; @@ -144,6 +179,11 @@ } // Public method to pause the animation + /** + * Pauses the animation if it is currently running. + * Sets the `isAnimating` flag to false, cancels the animation frame, + * and saves the current elapsed time. + */ pause() { if (!this.isAnimating) return; this.isAnimating = false; @@ -155,6 +195,10 @@ this.startTime = null; } + /** + * Called when the element is disconnected from the document's DOM. + * Cleans up the intersection observer if it exists. + */ disconnectedCallback() { // Clean up intersection observer when element is removed if (this.intersectionObserver) { @@ -163,6 +207,22 @@ } } + /** + * Sets up an IntersectionObserver to monitor the visibility of the element. + * + * @param {string} observeConfig - Configuration string for observation. + * Format: "mode:rootMargin". + * "mode" can be "once" for one-time observation. + * "rootMargin" is an optional margin around the root. + * + * @example + * // Observe with default root margin and trigger only once + * setupIntersectionObserver('once:0px'); + * + * @example + * // Observe with custom root margin and continuous triggering + * setupIntersectionObserver('continuous:10px'); + */ setupIntersectionObserver(observeConfig) { // Parse observation configuration const [mode, rootMargin = '0px'] = observeConfig.split(':'); @@ -198,6 +258,12 @@ } // Public method to morph to a new wave + /** + * Generates a new wave animation with the specified duration. + * Prevents multiple simultaneous wave generations by setting a flag. + * + * @param {number} [duration=800] - The duration of the wave animation in milliseconds. Minimum value is 1. + */ generateNewWave(duration = 800) { // Prevent multiple simultaneous wave generations if (this.isGeneratingWave || this.animationFrameId) { @@ -232,6 +298,12 @@ } // Core animation logic + /** + * Animates the wave transition from the current path to the target path over a specified duration. + * + * @param {number} duration - The duration of the animation in milliseconds. + * @param {Function} [onComplete=null] - Optional callback function to be called upon animation completion. + */ animateWave(duration, onComplete = null) { // Ensure we have valid start and target paths const startPoints = parsePath(this.currentPath); @@ -294,7 +366,18 @@ // Custom element definition customElements.define("dynamo-wave", DynamoWave); - // Existing helper functions remain the same (generateWave, parsePath, interpolateWave) + + /** + * Generates an SVG path string representing a wave pattern. + * + * @param {Object} options - The options for generating the wave. + * @param {number} options.width - The width of the wave. + * @param {number} options.height - The height of the wave. + * @param {number} options.points - The number of points in the wave. + * @param {number} options.variance - The variance factor for the wave's randomness. + * @param {boolean} [options.vertical=false] - Whether the wave should be vertical. + * @returns {string} The SVG path string representing the wave. + */ function generateWave({ width, height, points, variance, vertical = false }) { const anchors = []; const step = vertical ? height / (points - 1) : width / (points - 1); @@ -329,6 +412,12 @@ return path; } + /** + * Parses a path string containing quadratic Bezier curve commands and extracts the control points and end points. + * + * @param {string} pathString - The path string containing 'Q' commands followed by control point and end point coordinates. + * @returns {Array} An array of objects, each containing the control point (cpX, cpY) and end point (x, y) coordinates. + */ function parsePath(pathString) { const points = []; const regex = /Q\s([\d.]+)\s([\d.]+),\s([\d.]+)\s([\d.]+)/g; @@ -345,6 +434,17 @@ return points; } + /** + * Interpolates between two sets of points to create a smooth wave transition. + * + * @param {Array} currentPoints - The current set of points. + * @param {Array} targetPoints - The target set of points. + * @param {number} progress - The progress of the interpolation (0 to 1). + * @param {boolean} [vertical=false] - Whether the wave is vertical or horizontal. + * @param {number} height - The height of the wave container. + * @param {number} width - The width of the wave container. + * @returns {string} - The SVG path data for the interpolated wave. + */ function interpolateWave(currentPoints, targetPoints, progress, vertical = false, height, width) { const interpolatedPoints = currentPoints.map((current, i) => { const target = targetPoints[i]; diff --git a/dist/dynamowaves.min.js b/dist/dynamowaves.min.js index 3eb68e9..a2c7f87 100644 --- a/dist/dynamowaves.min.js +++ b/dist/dynamowaves.min.js @@ -1 +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