Lightningbeam/src/utils.js

810 lines
26 KiB
JavaScript

function titleCase(str) {
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
function getMousePositionFraction(event, element) {
const rect = element.getBoundingClientRect(); // Get the element's position and size
if (element.classList.contains('horizontal-grid')) {
// If the element has the "horizontal-grid" class, calculate the horizontal position (X)
const xPos = event.clientX - rect.left; // Mouse X position relative to the element
const fraction = xPos / rect.width; // Fraction of the width
return Math.min(Math.max(fraction, 0), 1); // Ensure the fraction is between 0 and 1
} else if (element.classList.contains('vertical-grid')) {
// If the element has the "vertical-grid" class, calculate the vertical position (Y)
const yPos = event.clientY - rect.top; // Mouse Y position relative to the element
const fraction = yPos / rect.height; // Fraction of the height
return Math.min(Math.max(fraction, 0), 1); // Ensure the fraction is between 0 and 1
}
return 0; // If neither class is present, return 0 (or handle as needed)
}
function getKeyframesSurrounding(frames, index) {
let lastKeyframeBefore = undefined;
let firstKeyframeAfter = undefined;
// Find the last keyframe before the given index
for (let i = index - 1; i >= 0; i--) {
if (frames[i]?.frameType === "keyframe") {
lastKeyframeBefore = i;
break;
}
}
// Find the first keyframe after the given index
for (let i = index + 1; i < frames.length; i++) {
if (frames[i]?.frameType === "keyframe") {
firstKeyframeAfter = i;
break;
}
}
return { lastKeyframeBefore, firstKeyframeAfter };
}
function invertPixels(ctx, width, height) {
// Create an off-screen canvas for the pattern
const patternCanvas = document.createElement('canvas');
const patternContext = patternCanvas.getContext('2d');
// Define the size of the repeating pattern (2x2 pixels)
const patternSize = 2;
patternCanvas.width = patternSize;
patternCanvas.height = patternSize;
// Create the alternating pattern (regular and inverted pixels)
function createInvertedPattern() {
const patternData = patternContext.createImageData(patternSize, patternSize);
const data = patternData.data;
// Fill the pattern with alternating colors (inverted every other pixel)
for (let i = 0; i < patternSize; i++) {
for (let j = 0; j < patternSize; j++) {
const index = (i * patternSize + j) * 4;
// Determine if we should invert the color
if ((i + j) % 2 === 0 || j%2===0) {
data[index] = 0; // Red
data[index + 1] = 0; // Green
data[index + 2] = 0; // Blue
data[index + 3] = 255; // Alpha
} else {
data[index] = 255; // Red (inverted)
data[index + 1] = 255; // Green (inverted)
data[index + 2] = 255; // Blue (inverted)
data[index + 3] = 255; // Alpha
}
}
}
// Set the pattern on the off-screen canvas
patternContext.putImageData(patternData, 0, 0);
return patternCanvas;
}
// Create the pattern using the function
const pattern = ctx.createPattern(createInvertedPattern(), 'repeat');
// Draw a rectangle with the pattern
ctx.globalCompositeOperation = "difference"
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, width, height);
ctx.globalCompositeOperation = "source-over"
}
function lerp(a, b, t) {
return a + (b - a) * t;
}
function lerpColor(color1, color2, t) {
// Convert hex color to RGB
const hexToRgb = (hex) => {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return { r, g, b };
};
// Get RGB values of both colors
const start = hexToRgb(color1);
const end = hexToRgb(color2);
// Calculate the interpolated RGB values
const r = Math.round(start.r + (end.r - start.r) * t);
const g = Math.round(start.g + (end.g - start.g) * t);
const b = Math.round(start.b + (end.b - start.b) * t);
// Convert the interpolated RGB back to hex
return rgbToHex(r, g, b);
}
function camelToWords(camelCaseString) {
// Insert a space before each uppercase letter and make it lowercase
const words = camelCaseString.replace(/([A-Z])/g, ' $1').toLowerCase();
// Capitalize the first letter of each word
return words.replace(/\b\w/g, char => char.toUpperCase());
}
function generateWaveform(img, buffer, imgHeight, frameWidth, framesPerSecond) {
// Total duration of the audio in seconds
const duration = buffer.duration;
const canvasWidth = Math.ceil(frameWidth * framesPerSecond * duration);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = canvasWidth;
canvas.height = imgHeight;
// Get the audio buffer's data (mono or stereo channels)
const channels = buffer.numberOfChannels;
const leftChannelData = buffer.getChannelData(0); // Left channel
const rightChannelData = channels > 1 ? buffer.getChannelData(1) : null; // Right channel, if stereo
const width = canvas.width;
const step = Math.ceil(leftChannelData.length / width); // Step size for drawing
const halfHeight = canvas.height / 2;
ctx.fillStyle = '#000';
function drawChannel(channelData) {
const samples = [];
// Draw the waveform by taking the maximum value of samples in each window
for (let i = 0; i < width; i++) {
let maxSample = -Infinity;
// Calculate the maximum value within the window
for (let j = i * step; j < (i + 1) * step && j < channelData.length; j++) {
maxSample = Math.max(maxSample, Math.abs(channelData[j])); // Find the maximum absolute sample
}
// Normalize and scale the max sample to fit within the canvas height
const y = maxSample * halfHeight;
samples.push([i, y]);
}
// Fill the waveform
if (samples.length > 0) {
ctx.beginPath();
ctx.moveTo(samples[0][0], samples[0][1]);
for (let i = 0; i < samples.length; i++) {
ctx.lineTo(samples[i][0], samples[i][1]);
}
for (let i = samples.length - 1; i >= 0; i--) {
ctx.lineTo(samples[i][0], -samples[i][1]);
}
ctx.fill();
}
}
if (channels>1) {
ctx.save();
ctx.translate(0, halfHeight*0.5);
drawChannel(leftChannelData);
ctx.restore();
ctx.save();
ctx.translate(0, halfHeight*1.5);
drawChannel(rightChannelData);
ctx.restore();
} else {
ctx.save();
ctx.translate(0, halfHeight);
drawChannel(leftChannelData);
ctx.restore();
}
const dataUrl = canvas.toDataURL("image/png");
img.src = dataUrl;
}
function floodFillRegion(startPoint, epsilon, fileWidth, fileHeight, context, debugPoints, debugPaintbucket) {
// Helper function to check if the point is at the boundary of the region
function isBoundaryPoint(point) {
return point.x <= 0 || point.x >= fileWidth || point.y <= 0 || point.y >= fileHeight;
}
let halfEpsilon = epsilon/2
// Helper function to check if a point is near any curve in the shape
function isNearCurve(point, shape) {
// Generate bounding box around the point for quadtree query
const bbox = {
x: { min: point.x - halfEpsilon, max: point.x + halfEpsilon },
y: { min: point.y - halfEpsilon, max: point.y + halfEpsilon }
};
// Get the list of curve indices that are near the point
const nearbyCurveIndices = shape.quadtree.query(bbox);
// const nearbyCurveIndices = shape.curves.keys()
// Check if any of the curves are close enough to the point
for (const idx of nearbyCurveIndices) {
const curve = shape.curves[idx];
const projection = curve.project(point);
if (projection.d < epsilon) {
return projection;
}
}
return false;
}
const shapes = context.activeObject.currentFrame.shapes;
const visited = new Set();
const stack = [startPoint];
const regionPoints = [];
// Begin the flood fill process
while (stack.length > 0) {
const currentPoint = stack.pop();
// If we reach the boundary of the region, throw an exception
if (isBoundaryPoint(currentPoint)) {
throw new Error("Flood fill reached the boundary of the area.");
}
// If the current point is already visited, skip it
const pointKey = `${currentPoint.x},${currentPoint.y}`;
if (visited.has(pointKey)) {
continue;
}
visited.add(pointKey);
if (debugPaintbucket) {
debugPoints.push(currentPoint)
}
let isNearAnyCurve = false;
for (const shape of shapes) {
let projection = isNearCurve(currentPoint, shape)
if (projection) {
isNearAnyCurve = true;
regionPoints.push(projection)
break;
}
}
// Skip the points that are near curves, to prevent jumping past them
if (!isNearAnyCurve) {
const neighbors = [
{ x: currentPoint.x - epsilon, y: currentPoint.y },
{ x: currentPoint.x + epsilon, y: currentPoint.y },
{ x: currentPoint.x, y: currentPoint.y - epsilon },
{ x: currentPoint.x, y: currentPoint.y + epsilon }
];
// Add unvisited neighbors to the stack
for (const neighbor of neighbors) {
const neighborKey = `${neighbor.x},${neighbor.y}`;
if (!visited.has(neighborKey)) {
stack.push(neighbor);
}
}
}
}
// Return the region points in connected order
return sortPointsByProximity(regionPoints)
}
function sortPointsByProximity(points) {
if (points.length <= 1) return points;
// Start with the first point as the initial sorted point
const sortedPoints = [points[0]];
points.splice(0, 1); // Remove the first point from the original list
// Iterate through the remaining points and find the nearest neighbor
while (points.length > 0) {
const lastPoint = sortedPoints[sortedPoints.length - 1];
// Find the closest point to the last point
let closestIndex = -1;
let closestDistance = Infinity;
for (let i = 0; i < points.length; i++) {
const currentPoint = points[i];
const distance = Math.sqrt(Math.pow(currentPoint.x - lastPoint.x, 2) + Math.pow(currentPoint.y - lastPoint.y, 2));
if (distance < closestDistance) {
closestDistance = distance;
closestIndex = i;
}
}
// Add the closest point to the sorted points
sortedPoints.push(points[closestIndex]);
points.splice(closestIndex, 1); // Remove the closest point from the original list
}
return sortedPoints;
}
function getShapeAtPoint(point, shapes) {
// Create a 1x1 off-screen canvas and translate so it is in the first pixel
const offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = 1;
offscreenCanvas.height = 1;
const ctx = offscreenCanvas.getContext('2d');
ctx.translate(-point.x, -point.y);
const colorToShapeMap = {};
// Generate a unique color for each shape (start from #000001 and increment)
let colorIndex = 1;
// Draw all shapes to the off-screen canvas with their unique colors
shapes.forEach(shape => {
// Generate a unique color for this shape
const debugColor = intToHexColor(colorIndex);
colorToShapeMap[debugColor] = shape;
const context = {
ctx: ctx,
debugColor: debugColor
};
shape.draw(context);
colorIndex++;
});
const pixel = ctx.getImageData(0, 0, 1, 1).data;
const sampledColor = rgbToHex(pixel[0], pixel[1], pixel[2]);
return colorToShapeMap[sampledColor] || null;
}
// Helper function to convert a number (0-16777215) to a hex color code
function intToHexColor(value) {
// Ensure the value is between 0 and 16777215 (0xFFFFFF)
value = value & 0xFFFFFF;
return '#' + value.toString(16).padStart(6, '0').toUpperCase();
}
function hslToRgb(h, s, l) {
// Ensure that the input values are within the expected range [0, 1]
h = h % 1; // Hue wraps around at 1
s = Math.min(Math.max(s, 0), 1); // Saturation should be between 0 and 1
l = Math.min(Math.max(l, 0), 1); // Lightness should be between 0 and 1
// Handle case where saturation is 0 (the color is gray)
if (s === 0) {
const gray = Math.round(l * 255); // All RGB values are equal to the lightness value
return { r: gray, g: gray, b: gray };
}
// Calculate temporary values
const temp2 = (l < 0.5) ? (l * (1 + s)) : (l + s - l * s);
const temp1 = 2 * l - temp2;
// Pre-calculate hues at the different points to avoid repeating calculations
const r = hueToRgb(temp1, temp2, h + 1 / 3);
const g = hueToRgb(temp1, temp2, h);
const b = hueToRgb(temp1, temp2, h - 1 / 3);
// Return RGB values in 0-255 range, rounding the result
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
function hueToRgb(t1, t2, t3) {
// Normalize hue to be between 0 and 1
if (t3 < 0) t3 += 1;
if (t3 > 1) t3 -= 1;
// Efficient calculation of RGB component
if (6 * t3 < 1) return t1 + (t2 - t1) * 6 * t3;
if (2 * t3 < 1) return t2;
if (3 * t3 < 2) return t1 + (t2 - t1) * (2 / 3 - t3) * 6;
return t1;
}
function hsvToRgb(h, s, v) {
let r, g, b;
if (s === 0) {
// If saturation is 0, the color is a shade of gray
r = g = b = v; // All channels are equal
} else {
// Calculate the hue sector (6 sectors, for each of the primary and secondary colors)
const i = Math.floor(h * 6); // The integer part of the hue value
const f = h * 6 - i; // The fractional part of the hue
const p = v * (1 - s); // The value at the lower boundary
const q = v * (1 - f * s); // Intermediate value
const t = v * (1 - (1 - f) * s); // Another intermediate value
// Use the hue sector index (i) to determine which RGB component will be maximum
switch (i % 6) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
}
}
// Return RGB values between 0 and 255 (scaled from 0-1 to 0-255)
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
let cachedPattern = null; // Cache the pattern
function drawCheckerboardBackground(ctx, x, y, width, height, squareSize) {
// If the pattern is not cached, create and cache it
if (!cachedPattern) {
// Define two shades of gray for the checkerboard
const color1 = '#E0E0E0'; // Light gray
const color2 = '#B0B0B0'; // Dark gray
// Create a 2x2 checkerboard pattern with four squares
const patternCanvas = document.createElement('canvas');
const patternCtx = patternCanvas.getContext('2d');
// Set the pattern canvas size to 2x2 squares (width and height)
patternCanvas.width = 2 * squareSize;
patternCanvas.height = 2 * squareSize;
// Fill the four squares to create the checkerboard pattern
patternCtx.fillStyle = color1; // Light gray for the first square
patternCtx.fillRect(0, 0, squareSize, squareSize); // Top-left square
patternCtx.fillStyle = color2; // Dark gray for the second square
patternCtx.fillRect(squareSize, 0, squareSize, squareSize); // Top-right square
patternCtx.fillStyle = color2; // Dark gray for the third square
patternCtx.fillRect(0, squareSize, squareSize, squareSize); // Bottom-left square
patternCtx.fillStyle = color1; // Light gray for the fourth square
patternCtx.fillRect(squareSize, squareSize, squareSize, squareSize); // Bottom-right square
// Cache the repeating pattern
cachedPattern = ctx.createPattern(patternCanvas, 'repeat');
}
// Set the cached pattern as the fill style for the rectangle
ctx.fillStyle = cachedPattern;
// Draw the rectangle with the repeating checkerboard pattern
ctx.fillRect(x, y, width, height);
}
function hexToHsl(hex) {
// Step 1: Convert hex to RGB
let r = parseInt(hex.substring(1, 3), 16) / 255;
let g = parseInt(hex.substring(3, 5), 16) / 255;
let b = parseInt(hex.substring(5, 7), 16) / 255;
// Step 2: Find the maximum and minimum values of r, g, and b
let max = Math.max(r, g, b);
let min = Math.min(r, g, b);
// Step 3: Calculate Lightness (L)
let l = (max + min) / 2;
// Step 4: Calculate Saturation (S)
let s = 0;
if (max !== min) {
s = (l > 0.5) ? (max - min) / (2 - max - min) : (max - min) / (max + min);
}
// Step 5: Calculate Hue (H)
let h = 0;
if (max === r) {
h = (g - b) / (max - min);
} else if (max === g) {
h = (b - r) / (max - min) + 2;
} else {
h = (r - g) / (max - min) + 4;
}
h = (h / 6) % 1; // Normalize hue to be between 0 and 1
// Return HSL values with H, S, and L scaled to [0.0, 1.0]
return { h: h, s: s, l: l };
}
function hexToHsv(hex) {
// Step 1: Convert hex to RGB
let r = parseInt(hex.substring(1, 3), 16) / 255;
let g = parseInt(hex.substring(3, 5), 16) / 255;
let b = parseInt(hex.substring(5, 7), 16) / 255;
// Step 2: Calculate Min and Max RGB values
let min = Math.min(r, g, b);
let max = Math.max(r, g, b);
let delta = max - min;
// Step 3: Calculate Hue
let h = 0;
if (delta !== 0) {
if (max === r) {
h = (g - b) / delta; // Red is max
} else if (max === g) {
h = (b - r) / delta + 2; // Green is max
} else {
h = (r - g) / delta + 4; // Blue is max
}
h = (h / 6 + 1) % 1; // Normalize to [0, 1]
}
// Step 4: Calculate Saturation
let s = 0;
if (max !== 0) {
s = delta / max;
}
// Step 5: Calculate Value
let v = max;
// Return HSV values, with H, S, and V between 0.0 and 1.0
return { h: h, s: s, v: v };
}
const rgbToHex = (r, g, b) => {
return `#${(1 << 24 | (r << 16) | (g << 8) | b).toString(16).slice(1).toUpperCase()}`;
};
function clamp(n) {
// Clamps a value between 0 and 1
return Math.min(Math.max(n,0),1)
}
function drawBorderedRect(ctx, x, y, width, height, top, bottom, left, right) {
ctx.fillRect(x, y, width, height)
if (top) {
ctx.strokeStyle = top
ctx.beginPath()
ctx.moveTo(x, y)
ctx.lineTo(x+width, y)
ctx.stroke()
}
if (bottom) {
ctx.strokeStyle = bottom
ctx.beginPath()
ctx.moveTo(x, y+height)
ctx.lineTo(x+width, y+height)
ctx.stroke()
}
if (left) {
ctx.strokeStyle = left
ctx.beginPath()
ctx.moveTo(x, y)
ctx.lineTo(x, y+height)
ctx.stroke()
}
if (right) {
ctx.strokeStyle = right
ctx.beginPath()
ctx.moveTo(x+width, y)
ctx.lineTo(x+width, y+height)
ctx.stroke()
}
}
function drawCenteredText(ctx, text, x, y, height) {
ctx.font = `${height}px Arial`; // TODO: allow configuring font somewhere
// Calculate the width of the text
const textWidth = ctx.measureText(text).width;
// Calculate the position to center the text
const centerX = x - textWidth / 2;
const centerY = y + height / 4; // Adjust for vertical centering
// Draw the text centered at (x, y) with the specified font size
ctx.fillText(text, centerX, centerY);
}
function drawHorizontallyCenteredText(ctx, text, x, y, height) {
ctx.font = `${height}px Arial`; // TODO: allow configuring font somewhere
const centerY = y + height / 4; // Adjust for vertical centering
ctx.fillText(text, x, centerY);
}
function drawRegularPolygon(ctx, x, y, radius, sides, color, rotate = 0) {
ctx.beginPath();
// First point, adding rotation to the angle
ctx.moveTo(x + radius * Math.cos(0 + rotate), y + radius * Math.sin(0 + rotate));
// Draw the rest of the sides, adding the rotation to each angle
for (let i = 1; i <= sides; i++) {
let angle = (i * 2 * Math.PI) / sides + rotate; // Add rotation to the angle
ctx.lineTo(x + radius * Math.cos(angle), y + radius * Math.sin(angle));
}
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
}
function deepMerge(target, source) {
// If either target or source is not an object, return source (base case)
if (typeof target !== 'object' || target === null) {
return source;
}
// If target is an object, recursively merge
if (typeof source === 'object' && source !== null) {
for (let key in source) {
// If the key exists in both objects, and both are objects, recursively merge
if (target.hasOwnProperty(key) && typeof target[key] === 'object' && typeof source[key] === 'object') {
target[key] = deepMerge(target[key], source[key]);
} else {
// Otherwise, just assign the source value to target
target[key] = source[key];
}
}
}
return target;
}
function getPointNearBox(boundingBox, point, threshold = 5, checkCenters = true) {
const { x: { min: xMin, max: xMax }, y: { min: yMin, max: yMax } } = boundingBox;
const { x, y } = point;
// List of points to check (corners and centers of sides) with their names
const pointsToCheck = [
{ name: 'nw', x: xMin, y: yMin }, // top-left corner
{ name: 'ne', x: xMax, y: yMin }, // top-right corner
{ name: 'sw', x: xMin, y: yMax }, // bottom-left corner
{ name: 'se', x: xMax, y: yMax }, // bottom-right corner
];
// Optionally add the center points if checkCenters is true
if (checkCenters) {
pointsToCheck.push(
{ name: 'n', x: (xMin + xMax) / 2, y: yMin }, // center of top side
{ name: 's', x: (xMin + xMax) / 2, y: yMax }, // center of bottom side
{ name: 'w', x: xMin, y: (yMin + yMax) / 2 }, // center of left side
{ name: 'e', x: xMax, y: (yMin + yMax) / 2 } // center of right side
);
}
// Check if the point is within the threshold distance of any of the points
for (let i = 0; i < pointsToCheck.length; i++) {
const pt = pointsToCheck[i];
const manhattanDistance = Math.abs(pt.x - x) + Math.abs(pt.y - y);
if (manhattanDistance <= threshold) {
return pt.name; // Return the name of the point that is close to the input point
}
}
return null; // Point is not within the threshold distance of any relevant point
}
function arraysAreEqual(arr1, arr2) {
if (arr1.length != arr2.length) return false;
if (arr1.every((value, index) => value === arr2[index])) {
return true;
} else {
return false;
}
}
function getFileExtension(filename) {
const dotIndex = filename.lastIndexOf('.'); // Find the last period in the filename
if (dotIndex === -1) return ''; // No extension found (no dot in filename)
return filename.substring(dotIndex + 1); // Extract the extension
}
function createModal(contentFunction, arg) {
// Create the modal overlay
const modalOverlay = document.createElement('div');
modalOverlay.style.position = 'fixed';
modalOverlay.style.top = 0;
modalOverlay.style.left = 0;
modalOverlay.style.width = '100%';
modalOverlay.style.height = '100%';
modalOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
modalOverlay.style.zIndex = 1000;
modalOverlay.style.display = 'flex';
modalOverlay.style.alignItems = 'center';
modalOverlay.style.justifyContent = 'center';
// Create the modal container
const modalContainer = document.createElement('div');
modalContainer.style.backgroundColor = 'white';
modalContainer.style.padding = '20px';
modalContainer.style.borderRadius = '8px';
modalContainer.style.maxWidth = '80%';
modalContainer.style.maxHeight = '80%';
modalContainer.style.overflowY = 'auto';
const modalContent = contentFunction(arg);
modalContainer.appendChild(modalContent);
// Create Ok and Cancel buttons
const buttonContainer = document.createElement('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.justifyContent = 'space-between';
buttonContainer.style.marginTop = '20px';
const okButton = document.createElement('button');
okButton.innerText = 'Ok';
okButton.style.padding = '10px 20px';
okButton.style.fontSize = '16px';
okButton.style.cursor = 'pointer';
okButton.style.backgroundColor = '#4CAF50';
okButton.style.color = 'white';
okButton.style.border = 'none';
okButton.style.borderRadius = '4px';
const cancelButton = document.createElement('button');
cancelButton.innerText = 'Cancel';
cancelButton.style.padding = '10px 20px';
cancelButton.style.fontSize = '16px';
cancelButton.style.cursor = 'pointer';
cancelButton.style.backgroundColor = '#f44336';
cancelButton.style.color = 'white';
cancelButton.style.border = 'none';
cancelButton.style.borderRadius = '4px';
// Add button events
okButton.addEventListener('click', () => {
modalOverlay.remove(); // Close modal on Ok
console.log(modalContent.active)
// You can add additional action here if needed
});
cancelButton.addEventListener('click', () => {
modalOverlay.remove(); // Close modal on Cancel
});
// Append buttons to the container
buttonContainer.appendChild(okButton);
buttonContainer.appendChild(cancelButton);
// Add button container to the modal
modalContainer.appendChild(buttonContainer);
// Add the modal container to the overlay
modalOverlay.appendChild(modalContainer);
// Append the modal overlay to the body
document.body.appendChild(modalOverlay);
}
function deeploop(obj, callback) {
// Loop through all the entries in the object
for (const [key, value] of Object.entries(obj)) {
// Call the callback with the key and value
callback(key, value);
// If the value is an object, recursively call deeploop on it
if (typeof value === 'object' && value !== null) {
deeploop(value, callback);
}
}
}
export {
titleCase,
getMousePositionFraction,
getKeyframesSurrounding,
invertPixels,
lerp,
lerpColor,
camelToWords,
generateWaveform,
floodFillRegion,
getShapeAtPoint,
hslToRgb,
hsvToRgb,
hexToHsl,
hexToHsv,
rgbToHex,
drawCheckerboardBackground,
clamp,
drawBorderedRect,
drawCenteredText,
drawHorizontallyCenteredText,
drawRegularPolygon,
deepMerge,
getPointNearBox,
arraysAreEqual,
getFileExtension,
createModal,
deeploop
};