/** * @typedef {Object} Point * @property {number} x - X coordinate * @property {number} y - Y coordinate */ /** * @typedef {Object} PathSegment * @property {Point} startPoint - Starting point of the segment * @property {Point} endPoint - Ending point of the segment * @property {Object} properties - Segment properties * @property {number} properties.width - Line width * @property {string} properties.strokeColor - Stroke color */ /** * @typedef {Object} DecorativeElement * @property {Point} position - Element position * @property {string} type - Element type (e.g., 'circle', 'dot') * @property {Object} properties - Element properties * @property {number} properties.radius - Element radius * @property {string} properties.color - Element color */ /** * @typedef {Object} RadiatingEffect * @property {Point} origin - Starting point on the boundary * @property {Object} path - Path information * @property {Object} path.direction - Direction vector * @property {number} path.direction.dx - X component of direction * @property {number} path.direction.dy - Y component of direction * @property {number} path.length - Path length * @property {PathSegment[]} path.segments - Path segments * @property {DecorativeElement[]} [decorativeElements] - Optional decorative elements */ /** * @typedef {Object} MaskInfo * @property {Point} center - Centroid of the mask * @property {Point[]} edgePoints - Array of points along the mask's edge */ /** * @typedef {Object} OverlayEffect * @property {Object} baseShape - Base shape information * @property {Point} baseShape.center - Center point * @property {Point[]} baseShape.boundaryPoints - Boundary points * @property {Object} baseShape.bounds - Shape bounds * @property {number} baseShape.bounds.width - Width * @property {number} baseShape.bounds.height - Height * @property {RadiatingEffect[]} radiatingEffects - Array of radiating effects */ /** * @class MaskProcessor * @description Processes bitmap masks to generate decorative flame-like effects radiating from edges. */ class MaskProcessor { /** * @constructor * @param {number} width - Canvas width in pixels * @param {number} height - Canvas height in pixels * @param {number} [lineLengthRatio=1.0] - Ratio controlling the length of radiating lines * @param {number} [numLines=10] - Number of radiating lines to draw * @param {number} [lineWidthRatio=0.01] - Ratio of line width to contour circumference */ constructor(width, height, lineLengthRatio = 1.0, numLines = 20, lineWidthRatio = 0.01) { this.debug = false; this.width = width; this.height = height; this.inputCanvas = document.createElement('canvas'); this.outputCanvas = document.createElement('canvas'); this.inputCanvas.width = width; this.inputCanvas.height = height; this.outputCanvas.width = width; this.outputCanvas.height = height; this.inputCtx = this.inputCanvas.getContext('2d'); this.outputCtx = this.outputCanvas.getContext('2d'); this.maskInfo = null; this.lineLengthRatio = lineLengthRatio; this.numLines = numLines; this.lineWidthRatio = lineWidthRatio; this.strokeColor = 'black'; this.elementColor = 'black'; this.domInputCanvas = document.getElementById('inputCanvas'); this.domOutputCanvas = document.getElementById('outputCanvas'); if (!this.domInputCanvas || !this.domOutputCanvas) { console.error('Failed to find canvas elements'); } } /** * @method generateRandomMask * @description Generates a random organic mask shape using quadratic curves and enhanced bulge calculations * @returns {MaskInfo} Contains center point and edge points of the generated mask */ generateRandomMask() { this.inputCanvas.width = 400; this.inputCanvas.height = 400; this.width = 400; this.height = 400; // Reset DOM canvas dimensions this.domInputCanvas.width = 400; this.domInputCanvas.height = 400; this.domOutputCanvas.width = 400; this.domOutputCanvas.height = 400; // Reset display styles this.domInputCanvas.style.width = '400px'; this.domInputCanvas.style.height = '400px'; this.domOutputCanvas.style.width = '400px'; this.domOutputCanvas.style.height = '400px'; // Clear canvas this.inputCtx.fillStyle = 'white'; this.inputCtx.fillRect(0, 0, this.width, this.height); const centerX = this.width / 2; const centerY = this.height / 2; const baseRadius = Math.min(this.width, this.height) * 0.15; const numPoints = 6 + Math.floor(Math.random() * 3); const points = []; // Generate base points with smoother distribution for (let i = 0; i < numPoints; i++) { const angle = (i / numPoints) * Math.PI * 2; // Smoother radius variation const radiusVariation = baseRadius * (0.8 + Math.random() * 0.4); const x = centerX + Math.cos(angle) * radiusVariation; const y = centerY + Math.sin(angle) * radiusVariation; points.push({ x, y }); } // Draw the mask with enhanced curves this.inputCtx.fillStyle = 'black'; this.inputCtx.beginPath(); this.inputCtx.moveTo(points[0].x, points[0].y); const curvePoints = []; const numCurvePoints = 240; // Increased for smoother sampling // Draw and sample points along curves between vertices for (let i = 0; i < points.length; i++) { const current = points[i]; const next = points[(i + 1) % points.length]; // Enhanced bulge calculation const midX = (current.x + next.x) / 2; const midY = (current.y + next.y) / 2; const bulgeX = midX - centerX; const bulgeY = midY - centerY; const bulgeLength = Math.sqrt(bulgeX * bulgeX + bulgeY * bulgeY); const segmentLength = Math.sqrt( Math.pow(next.x - current.x, 2) + Math.pow(next.y - current.y, 2) ); // Scale bulge factor based on segment length const bulgeFactor = baseRadius * (0.2 + Math.random() * 0.2) * (segmentLength / baseRadius) * 0.5; const controlX = midX + (bulgeX / bulgeLength) * bulgeFactor; const controlY = midY + (bulgeY / bulgeLength) * bulgeFactor; // Sample points with adaptive density const pointsPerSegment = Math.ceil((numCurvePoints / points.length) * (segmentLength / (baseRadius * 2))); for (let t = 0; t < 1; t += 1/pointsPerSegment) { const t1 = 1 - t; const x = t1 * t1 * current.x + 2 * t1 * t * controlX + t * t * next.x; const y = t1 * t1 * current.y + 2 * t1 * t * controlY + t * t * next.y; curvePoints.push({ x, y }); } this.inputCtx.quadraticCurveTo(controlX, controlY, next.x, next.y); } this.inputCtx.closePath(); this.inputCtx.fill(); // Store mask info with evenly distributed points this.maskInfo = { center: { x: centerX, y: centerY }, edgePoints: this.redistributePoints(curvePoints, 100) }; return this.maskInfo; } /** * @method redistributePoints * @description Redistributes points evenly along a path defined by a series of points * @param {Point[]} points - Original array of points defining the path * @param {number} targetCount - Desired number of evenly spaced points * @returns {Point[]} Array of evenly distributed points * @private */ redistributePoints(points, targetCount) { // Calculate total perimeter length let totalLength = 0; const segments = []; for (let i = 0; i < points.length; i++) { const next = points[(i + 1) % points.length]; const length = Math.sqrt( Math.pow(next.x - points[i].x, 2) + Math.pow(next.y - points[i].y, 2) ); totalLength += length; segments.push({ start: points[i], end: next, length }); } // Redistribute points evenly const spacing = totalLength / targetCount; const newPoints = []; let currentDist = 0; let currentSegment = 0; let segmentPos = 0; for (let i = 0; i < targetCount; i++) { while (currentDist >= segments[currentSegment].length) { currentDist -= segments[currentSegment].length; currentSegment = (currentSegment + 1) % segments.length; } const seg = segments[currentSegment]; const t = currentDist / seg.length; newPoints.push({ x: seg.start.x + (seg.end.x - seg.start.x) * t, y: seg.start.y + (seg.end.y - seg.start.y) * t }); currentDist += spacing; } return newPoints; } /** * @private * @method drawRadiatingLine * @description Draws a single radiating line from a point on the mask's edge * @param {Object} point - The starting point on the mask's edge * @param {number} lineLength - The length of the line to draw */ drawRadiatingLine(point, lineLength) { const dx = point.x - this.maskInfo.center.x; const dy = point.y - this.maskInfo.center.y; const length = Math.sqrt(dx * dx + dy * dy); const normalizedDx = dx / length; const normalizedDy = dy / length; this.outputCtx.beginPath(); this.outputCtx.moveTo(point.x, point.y); this.outputCtx.lineTo( point.x + normalizedDx * lineLength, point.y + normalizedDy * lineLength ); this.outputCtx.stroke(); } /** * @private * @method calculateBaseLineWidth * @description Calculates the base line width based on the contour circumference * @returns {number} Calculated base line width */ calculateBaseLineWidth() { if (!this.maskInfo || !this.maskInfo.edgePoints) { return 10; // Default fallback value } let circumference = 0; const points = this.maskInfo.edgePoints; for (let i = 0; i < points.length; i++) { const current = points[i]; const next = points[(i + 1) % points.length]; const distance = Math.sqrt( Math.pow(next.x - current.x, 2) + Math.pow(next.y - current.y, 2) ); circumference += distance; } return Math.max(1, circumference * this.lineWidthRatio); } /** * @method generateFlameRayEffect * @description Generates data for a flame-like effect radiating from an edge point * @param {Point} point - Starting point on the mask's edge * @param {Point} nextPoint - Next point on the mask's edge * @param {number} lineLength - Length of the flame effect * @returns {RadiatingEffect} Generated flame effect data * @private */ generateFlameRayEffect(point, nextPoint, lineLength) { const amplitudeInitialMod = 0.2; const amplitudeDecayMod = 0.8; const baseLineWidth = this.calculateBaseLineWidth(); // Calculate direction vectors for both current and next points const dx1 = point.x - this.maskInfo.center.x; const dy1 = point.y - this.maskInfo.center.y; const length1 = Math.sqrt(dx1 * dx1 + dy1 * dy1); const normalizedDx1 = dx1 / length1; const normalizedDy1 = dy1 / length1; const dx2 = nextPoint.x - this.maskInfo.center.x; const dy2 = nextPoint.y - this.maskInfo.center.y; const length2 = Math.sqrt(dx2 * dx2 + dy2 * dy2); const normalizedDx2 = dx2 / length2; const normalizedDy2 = dy2 / length2; // Calculate midpoint between current and next flame const midPoint = { x: (point.x + nextPoint.x) / 2, y: (point.y + nextPoint.y) / 2 }; const numWaves = Math.max(5, lineLength / 5); const initialAmplitude = lineLength * amplitudeInitialMod; const minLineWidth = 1; const segments = []; const decorativeElements = []; // Generate path segments for the main flame rays for (let i = 0; i < numWaves; i++) { const t = i / numWaves; const nextT = (i + 1) / numWaves; // Calculate current wave points for both flames const waveX = point.x + normalizedDx1 * lineLength * t; const waveY = point.y + normalizedDy1 * lineLength * t; const nextWaveX = point.x + normalizedDx1 * lineLength * nextT; const nextWaveY = point.y + normalizedDy1 * lineLength * nextT; // Calculate wave amplitudes and offsets const currentAmplitude = initialAmplitude * (1 - t * amplitudeDecayMod); const offsetX = -normalizedDy1 * Math.sin(t * Math.PI * 2) * currentAmplitude; const offsetY = normalizedDx1 * Math.sin(t * Math.PI * 2) * currentAmplitude; const nextAmplitude = initialAmplitude * (1 - nextT * amplitudeDecayMod); const nextOffsetX = -normalizedDy1 * Math.sin(nextT * Math.PI * 2) * nextAmplitude; const nextOffsetY = normalizedDx1 * Math.sin(nextT * Math.PI * 2) * nextAmplitude; // Interpolate between the two direction vectors for virtual flame const interpolationFactor = 0.5; // Can be adjusted for different effects const virtualDx = normalizedDx1 * (1 - interpolationFactor) + normalizedDx2 * interpolationFactor; const virtualDy = normalizedDy1 * (1 - interpolationFactor) + normalizedDy2 * interpolationFactor; // Normalize the interpolated direction const virtualLength = Math.sqrt(virtualDx * virtualDx + virtualDy * virtualDy); const normalizedVirtualDx = virtualDx / virtualLength; const normalizedVirtualDy = virtualDy / virtualLength; // Calculate virtual flame point with interpolated direction and sinusoidal pattern const virtualFlameX = midPoint.x + normalizedVirtualDx * lineLength * t; const virtualFlameY = midPoint.y + normalizedVirtualDy * lineLength * t; // Add sinusoidal pattern to virtual flame position const virtualAmplitude = currentAmplitude; const virtualOffsetX = -normalizedVirtualDy * Math.sin(t * Math.PI * 2) * virtualAmplitude; const virtualOffsetY = normalizedVirtualDx * Math.sin(t * Math.PI * 2) * virtualAmplitude; segments.push({ startPoint: { x: waveX + offsetX, y: waveY + offsetY }, endPoint: { x: nextWaveX + nextOffsetX, y: nextWaveY + nextOffsetY }, properties: { width: baseLineWidth - (baseLineWidth - minLineWidth) * t, strokeColor: this.strokeColor } }); // Add decorative elements along virtual flame path with sinusoidal pattern const startStep = 2; if (i >= startStep && i % 3 === 0) { const progress = (i - startStep) / (numWaves - startStep); const minRadius = minLineWidth / 2; const maxRadius = baseLineWidth * 0.6; const currentRadius = minRadius + (maxRadius - minRadius) * progress; decorativeElements.push({ position: { x: virtualFlameX + virtualOffsetX, y: virtualFlameY + virtualOffsetY }, type: 'circle', properties: { radius: currentRadius, color: this.elementColor } }); } } return { origin: point, path: { direction: { dx: normalizedDx1, dy: normalizedDy1 }, length: lineLength, segments }, decorativeElements }; } /** * @method renderFlameRayEffect * @description Renders a flame ray effect including path segments and decorative elements * @param {RadiatingEffect} effect - The effect data to render * @private */ renderFlameRayEffect(effect) { // Render main path segments effect.path.segments.forEach(segment => { this.outputCtx.beginPath(); this.outputCtx.lineWidth = segment.properties.width; this.outputCtx.strokeStyle = segment.properties.strokeColor || this.strokeColor; this.outputCtx.moveTo(segment.startPoint.x, segment.startPoint.y); this.outputCtx.lineTo(segment.endPoint.x, segment.endPoint.y); this.outputCtx.stroke(); }); // Render decorative elements if (effect.decorativeElements) { effect.decorativeElements.forEach(element => { if (element.type === 'circle') { this.outputCtx.fillStyle = element.properties.color || this.elementColor; this.outputCtx.beginPath(); this.outputCtx.arc( element.position.x, element.position.y, element.properties.radius, 0, Math.PI * 2 ); this.outputCtx.fill(); } }); } } /** * @private * @method setupOutputCanvas * @description Prepares the output canvas for drawing */ setupOutputCanvas() { this.outputCtx.fillStyle = 'white'; this.outputCtx.fillRect(0, 0, this.width, this.height); this.outputCtx.drawImage(this.inputCanvas, 0, 0); this.outputCtx.strokeStyle = 'black'; this.outputCtx.lineCap = 'round'; // Line width will be set per segment in renderFlameRayEffect } /** * @private * @method copyToDOM * @description Copies the internal canvases to the DOM canvases */ copyToDOM() { const domInputCtx = this.domInputCanvas.getContext('2d'); const domOutputCtx = this.domOutputCanvas.getContext('2d'); domInputCtx.clearRect(0, 0, this.width, this.height); domOutputCtx.clearRect(0, 0, this.width, this.height); domInputCtx.drawImage(this.inputCanvas, 0, 0); domOutputCtx.drawImage(this.outputCanvas, 0, 0); } /** * @method processMask * @description Generates decorative effects based on the analyzed mask. */ processMask() { if (!this.maskInfo) { this.generateRandomMask(); } this.setupOutputCanvas(); // Debug visualization if (this.debug) { this.outputCtx.fillStyle = 'red'; this.maskInfo.edgePoints.forEach(point => { this.outputCtx.beginPath(); this.outputCtx.arc(point.x, point.y, 2, 0, Math.PI * 2); this.outputCtx.fill(); }); } // Select evenly distributed points around the contour const selectedPoints = this.selectEvenlyDistributedPoints( this.maskInfo.edgePoints, this.numLines ); // Process each selected point for (let i = 0; i < selectedPoints.length; i++) { const point = selectedPoints[i]; const nextPoint = selectedPoints[(i + 1) % selectedPoints.length]; const lineLength = Math.sqrt( Math.pow(point.x - this.maskInfo.center.x, 2) + Math.pow(point.y - this.maskInfo.center.y, 2) ) * this.lineLengthRatio; const effect = this.generateFlameRayEffect(point, nextPoint, lineLength); this.renderFlameRayEffect(effect); } this.copyToDOM(); } /** * @private * @method selectEvenlyDistributedPoints * @description Selects points that are evenly distributed along the contour * @param {Point[]} points - Array of points along the contour * @param {number} count - Number of points to select * @returns {Point[]} Array of selected points */ selectEvenlyDistributedPoints(points, count) { if (points.length <= count) return points; // Calculate the total contour length let totalLength = 0; const distances = []; for (let i = 0; i < points.length; i++) { const current = points[i]; const next = points[(i + 1) % points.length]; const distance = Math.sqrt( Math.pow(next.x - current.x, 2) + Math.pow(next.y - current.y, 2) ); distances.push(distance); totalLength += distance; } // Calculate the ideal spacing between points const spacing = totalLength / count; const selectedPoints = []; let currentDistance = 0; let currentIndex = 0; let accumulatedDistance = 0; // Select points based on accumulated distance for (let i = 0; i < count; i++) { const targetDistance = (i * totalLength) / count; // Find the point closest to the target distance while (accumulatedDistance < targetDistance && currentIndex < distances.length) { accumulatedDistance += distances[currentIndex]; currentIndex++; } // Get the actual point selectedPoints.push(points[currentIndex % points.length]); } return selectedPoints; } /** * @method detectMaskFromBitmap * @description Analyzes bitmap data to extract mask information * @returns {MaskInfo} Analyzed mask data including center and edge points */ detectMaskFromBitmap() { const imageData = this.inputCtx.getImageData(0, 0, this.width, this.height); const pixels = imageData.data; // Find mask boundaries and center const center = this.calculateMaskCentroid(pixels); const edges = this.detectEdgePoints(pixels, center); // Store and return mask info this.maskInfo = { center: center, edgePoints: this.redistributePoints(edges, 100) // Maintain consistent point count }; return this.maskInfo; } /** * @private * @method calculateMaskCentroid * @description Calculates the centroid (center of mass) of the mask * @param {Uint8ClampedArray} pixels - Canvas pixel data * @returns {Object} Center point {x, y} */ calculateMaskCentroid(pixels) { let sumX = 0, sumY = 0, total = 0; for (let y = 0; y < this.height; y++) { for (let x = 0; x < this.width; x++) { const idx = (y * this.width + x) * 4; // Check if pixel is black (part of mask) if (pixels[idx] === 0) { sumX += x; sumY += y; total++; } } } return { x: sumX / total, y: sumY / total }; } /** * @private * @method detectEdgePoints * @description Detects edge points of the mask using a simple edge detection algorithm * @param {Uint8ClampedArray} pixels - Canvas pixel data * @returns {Array} Array of edge points */ detectEdgePoints(pixels, center) { const edges = []; // Scan from each direction to find edges // Top to bottom for (let x = 0; x < this.width; x++) { let lastPixel = 255; // Start with white for (let y = 0; y < this.height; y++) { const idx = (y * this.width + x) * 4; const currentPixel = pixels[idx]; if (lastPixel !== currentPixel) { edges.push({ x, y }); } lastPixel = currentPixel; } } // Left to right for (let y = 0; y < this.height; y++) { let lastPixel = 255; // Start with white for (let x = 0; x < this.width; x++) { const idx = (y * this.width + x) * 4; const currentPixel = pixels[idx]; if (lastPixel !== currentPixel) { edges.push({ x, y }); } lastPixel = currentPixel; } } // Remove duplicates const uniqueEdges = []; const seen = new Set(); for (const point of edges) { const key = `${point.x},${point.y}`; if (!seen.has(key)) { seen.add(key); uniqueEdges.push(point); } } // Sort points clockwise around the center uniqueEdges.sort((a, b) => { const angleA = Math.atan2(a.y - center.y, a.x - center.x); const angleB = Math.atan2(b.y - center.y, b.x - center.x); return angleA - angleB; }); return uniqueEdges; } /** * @method updateCanvasDimensions * @description Updates the dimensions of both internal and DOM canvases * @param {number} width - New width for the canvases * @param {number} height - New height for the canvases */ updateCanvasDimensions(width, height) { this.width = width; this.height = height; // Update internal canvases this.inputCanvas.width = width; this.inputCanvas.height = height; this.outputCanvas.width = width; this.outputCanvas.height = height; // Update DOM canvases this.domInputCanvas.width = width; this.domInputCanvas.height = height; this.domOutputCanvas.width = width; this.domOutputCanvas.height = height; // Update display styles this.domInputCanvas.style.width = `${width}px`; this.domInputCanvas.style.height = `${height}px`; this.domOutputCanvas.style.width = `${width}px`; this.domOutputCanvas.style.height = `${height}px`; } /** * @method loadMask * @description Primary entry point for processing an existing mask image. * Handles the complete workflow of loading, detecting edges, and generating effects. * @param {HTMLImageElement} img - The mask image to process */ loadMask(img) { // Update canvas dimensions this.updateCanvasDimensions(img.width, img.height); // Clear the input canvas this.inputCtx.clearRect(0, 0, this.width, this.height); // Draw the image onto the input canvas this.inputCtx.drawImage(img, 0, 0, img.width, img.height); // Detect the mask from the bitmap this.detectMaskFromBitmap(); // Process the mask this.processMask(); } // /** // * @private // * @method generateOverlayData // * @description Generates complete overlay effect data structure // * @returns {OverlayEffect} Generated overlay effect data // * @private // */ // generateOverlayData() { // if (!this.maskInfo) { // this.generateRandomMask(); // } // const totalEdgePoints = this.maskInfo.edgePoints.length; // const step = totalEdgePoints >= this.numLines ? // Math.floor(totalEdgePoints / this.numLines) : 1; // const radiatingEffects = []; // // Generate data for each radiating effect // for (let i = 0; i < totalEdgePoints; i += step) { // const point = this.maskInfo.edgePoints[i]; // const dx = point.x - this.maskInfo.center.x; // const dy = point.y - this.maskInfo.center.y; // const length = Math.sqrt(dx * dx + dy * dy) * this.lineLengthRatio; // radiatingEffects.push({ // origin: point, // path: { // direction: { // dx: dx / length, // dy: dy / length // }, // length: length, // segments: [] // Would be populated based on effect type // } // }); // } // return { // baseShape: { // center: this.maskInfo.center, // boundaryPoints: this.maskInfo.edgePoints, // bounds: { // width: this.width, // height: this.height // } // }, // radiatingEffects // }; // } // /** // * @method setColors // * @description Sets the base colors for strokes and decorative elements // * @param {string} strokeColor - Color for line strokes // * @param {string} elementColor - Color for decorative elements // */ // setColors(strokeColor, elementColor) { // this.strokeColor = strokeColor; // this.elementColor = elementColor; // } } if (typeof module !== 'undefined' && module.exports) { module.exports = MaskProcessor; }