810 lines
26 KiB
JavaScript
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
|
|
}; |