initial
This commit is contained in:
+177
@@ -0,0 +1,177 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mask Contour Processor v0.5</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
@keyframes errorFlash {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
|
||||
50% { box-shadow: 0 0 40px 10px rgba(239, 68, 68, 0.7); }
|
||||
}
|
||||
@keyframes successFlash {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
|
||||
50% { box-shadow: 0 0 20px 0 rgba(34, 197, 94, 0.5); }
|
||||
}
|
||||
.error-flash {
|
||||
animation: errorFlash 0.8s ease-out;
|
||||
}
|
||||
.success-flash {
|
||||
animation: successFlash 0.8s ease-out;
|
||||
}
|
||||
|
||||
#dropZone {
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
#dropZone:hover,
|
||||
#dropZone.drag-active {
|
||||
opacity: 1;
|
||||
border-color: #3b82f6; /* Tailwind's blue-500 */
|
||||
box-shadow: inset 0 0 0 2px rgba(59, 130, 246, 0.1); /* Subtle inner glow */
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.download-btn:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
@keyframes explode {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.exploding {
|
||||
animation: explode 0.5s ease-out forwards;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="p-5 font-sans bg-gray-100">
|
||||
<main class="container max-w-[850px] mx-auto">
|
||||
<!-- Controls Section -->
|
||||
<section class="flex items-center gap-4 mb-4">
|
||||
<button
|
||||
onclick="regenerateMask()"
|
||||
class="px-5 py-2.5 bg-blue-600 text-white rounded hover:bg-blue-700 text-base cursor-pointer transition-colors"
|
||||
>
|
||||
Generate New Mask
|
||||
</button>
|
||||
|
||||
<!-- Line Length Control -->
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="lineLength" class="text-sm text-gray-700">Line Length:</label>
|
||||
<input
|
||||
type="range"
|
||||
id="lineLength"
|
||||
min="0"
|
||||
max="3"
|
||||
step="0.01"
|
||||
value="1"
|
||||
class="w-32 cursor-pointer"
|
||||
oninput="updateLineLength(this.value)"
|
||||
>
|
||||
<span id="lineLengthValue" class="text-sm text-gray-700 font-mono">1</span>
|
||||
</div>
|
||||
|
||||
<!-- Line Spacing Control -->
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="numLines" class="text-sm text-gray-700">Line Count:</label>
|
||||
<input
|
||||
type="range"
|
||||
id="numLines"
|
||||
min="1"
|
||||
max="40"
|
||||
value="10"
|
||||
class="w-32 cursor-pointer"
|
||||
oninput="updateNumLines(this.value)"
|
||||
>
|
||||
<span id="numLinesValue" class="text-sm text-gray-700 font-mono">10</span>
|
||||
</div>
|
||||
|
||||
<!-- Line Width Control -->
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="lineWidth" class="text-sm text-gray-700">Line Width:</label>
|
||||
<input
|
||||
type="range"
|
||||
id="lineWidth"
|
||||
min="0"
|
||||
max="100"
|
||||
value="1"
|
||||
step="0.1"
|
||||
class="w-32 cursor-pointer"
|
||||
oninput="updateLineWidth(this.value)"
|
||||
>
|
||||
<span id="lineWidthValue" class="text-sm text-gray-700 font-mono">0.010</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Canvas Section -->
|
||||
<section class="flex gap-5">
|
||||
<div class="relative group">
|
||||
<canvas
|
||||
id="inputCanvas"
|
||||
width="400"
|
||||
height="400"
|
||||
class="bg-white border border-gray-300 rounded shadow-sm"
|
||||
aria-label="Input mask canvas"
|
||||
></canvas>
|
||||
<div
|
||||
id="dropZone"
|
||||
class="absolute inset-0 rounded flex items-center justify-center"
|
||||
>
|
||||
<div class="text-center">
|
||||
<svg class="w-8 h-8 mx-auto mb-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||
</svg>
|
||||
<p class="text-sm text-gray-600">Drop image here</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative group">
|
||||
<canvas
|
||||
id="outputCanvas"
|
||||
width="400"
|
||||
height="400"
|
||||
class="bg-white border border-gray-300 rounded shadow-sm"
|
||||
aria-label="Output mask canvas with contour lines"
|
||||
></canvas>
|
||||
<button
|
||||
onclick="downloadCanvas('output')"
|
||||
class="download-btn opacity-0 group-hover:opacity-100"
|
||||
aria-label="Download output canvas"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="js/maskProcessor.js"></script>
|
||||
<script src="js/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
// Initialize processor
|
||||
const processor = new MaskProcessor(400, 400, 1.0, 20, 0.01);
|
||||
|
||||
// Control handlers
|
||||
function updateControl(controlId, value, processorProperty, formatValue = v => v) {
|
||||
document.getElementById(`${controlId}Value`).textContent = formatValue(value);
|
||||
processor[processorProperty] = value;
|
||||
processor.processMask();
|
||||
}
|
||||
|
||||
function updateLineLength(value) {
|
||||
updateControl('lineLength', value, 'lineLengthRatio', v => (v/1.0).toFixed(2));
|
||||
}
|
||||
|
||||
function updateNumLines(value) {
|
||||
updateControl('numLines', value, 'numLines', v => parseInt(v));
|
||||
}
|
||||
|
||||
function updateLineWidth(value) {
|
||||
updateControl('lineWidth', (value/1000).toFixed(3), 'lineWidthRatio', v => (v*1000));
|
||||
}
|
||||
|
||||
function regenerateMask() {
|
||||
console.log('regenerateMask called');
|
||||
try {
|
||||
processor.generateRandomMask();
|
||||
processor.processMask();
|
||||
} catch (error) {
|
||||
console.error('Error in regenerateMask:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial mask generation
|
||||
console.log('Running initial mask generation');
|
||||
regenerateMask();
|
||||
|
||||
// Drag and drop handlers
|
||||
const dropZone = document.getElementById('dropZone');
|
||||
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('drag-active');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', () => {
|
||||
dropZone.classList.remove('drag-active');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', async (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('drag-active');
|
||||
|
||||
// Remove any existing animation classes
|
||||
dropZone.classList.remove('error-flash');
|
||||
dropZone.classList.remove('success-flash');
|
||||
|
||||
// Force a reflow to reset the animation
|
||||
void dropZone.offsetWidth;
|
||||
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (!file || !file.type.startsWith('image/')) {
|
||||
dropZone.classList.add('error-flash');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const img = new Image();
|
||||
img.src = URL.createObjectURL(file);
|
||||
await img.decode();
|
||||
|
||||
// Use the new loadMask method
|
||||
processor.loadMask(img);
|
||||
|
||||
dropZone.classList.add('success-flash');
|
||||
} catch (error) {
|
||||
console.error('Error loading image:', error);
|
||||
dropZone.classList.add('error-flash');
|
||||
}
|
||||
});
|
||||
|
||||
// Show drop zone on page load
|
||||
dropZone.style.display = 'flex';
|
||||
|
||||
// Add download functionality
|
||||
function downloadCanvas(type) {
|
||||
const button = event.currentTarget;
|
||||
const canvas = type === 'input' ? processor.domInputCanvas : processor.domOutputCanvas;
|
||||
|
||||
// Create and trigger download
|
||||
const link = document.createElement('a');
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
link.download = `mask-${type}-${timestamp}.png`;
|
||||
link.href = canvas.toDataURL('image/png');
|
||||
|
||||
// Add explosion animation
|
||||
button.classList.add('exploding');
|
||||
|
||||
// Remove animation class after it completes
|
||||
button.addEventListener('animationend', () => {
|
||||
button.classList.remove('exploding');
|
||||
}, { once: true });
|
||||
|
||||
link.click();
|
||||
}
|
||||
@@ -0,0 +1,806 @@
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user