807 lines
29 KiB
JavaScript
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;
|
|
}
|
|
|