Files
codeprimate-ComfyUI-MaskCon…/web/js/maskProcessor.js
T
2024-12-15 20:23:15 -08:00

807 lines
29 KiB
JavaScript

/**
* @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<Object>} 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;
}