initial work on new color picker
This commit is contained in:
parent
f010faef73
commit
747b34ec67
239
src/main.js
239
src/main.js
|
|
@ -3,7 +3,7 @@ import * as fitCurve from '/fit-curve.js';
|
|||
import { Bezier } from "/bezier.js";
|
||||
import { Quadtree } from './quadtree.js';
|
||||
import { createNewFileDialog, showNewFileDialog, closeDialog } from './newfile.js';
|
||||
import { titleCase, getMousePositionFraction, getKeyframesSurrounding, invertPixels, lerpColor, lerp, camelToWords, generateWaveform, floodFillRegion, getShapeAtPoint } from './utils.js';
|
||||
import { titleCase, getMousePositionFraction, getKeyframesSurrounding, invertPixels, lerpColor, lerp, camelToWords, generateWaveform, floodFillRegion, getShapeAtPoint, hslToRgb, drawCheckerboardBackground, hexToHsl, hsvToRgb, hexToHsv, rgbToHex, clamp } from './utils.js';
|
||||
const { writeTextFile: writeTextFile, readTextFile: readTextFile, writeFile: writeFile, readFile: readFile }= window.__TAURI__.fs;
|
||||
const {
|
||||
open: openFileDialog,
|
||||
|
|
@ -3149,39 +3149,230 @@ function toolbar() {
|
|||
let tools_break = document.createElement("div")
|
||||
tools_break.className = "horiz_break"
|
||||
tools_scroller.appendChild(tools_break)
|
||||
let fillColor = document.createElement("input")
|
||||
let strokeColor = document.createElement("input")
|
||||
let fillColor = document.createElement("div")
|
||||
let strokeColor = document.createElement("div")
|
||||
fillColor.className = "color-field"
|
||||
strokeColor.className = "color-field"
|
||||
fillColor.style.setProperty('--color', "#ff0000");
|
||||
strokeColor.style.setProperty('--color', "#000000");
|
||||
fillColor.type="color"
|
||||
fillColor.value = "#ff0000"
|
||||
strokeColor.value = "#000000"
|
||||
context.fillStyle = fillColor.value
|
||||
context.strokeStyle = strokeColor.value
|
||||
let evtListener;
|
||||
let padding = 10
|
||||
let gradwidth = 25
|
||||
let ccwidth = 300
|
||||
let mainSize = ccwidth - (3*padding + gradwidth)
|
||||
fillColor.addEventListener('click', e => {
|
||||
Coloris({
|
||||
el: ".color-field",
|
||||
selectInput: true,
|
||||
focusInput: true,
|
||||
theme: 'default',
|
||||
swatches: context.swatches,
|
||||
defaultColor: '#ff0000',
|
||||
onChange: (color) => {
|
||||
context.fillStyle = color;
|
||||
// Coloris({
|
||||
// el: ".color-field",
|
||||
// selectInput: true,
|
||||
// focusInput: true,
|
||||
// theme: 'default',
|
||||
// swatches: context.swatches,
|
||||
// defaultColor: '#ff0000',
|
||||
// onChange: (color) => {
|
||||
// context.fillStyle = color;
|
||||
// }
|
||||
// })
|
||||
let colorCvs = document.querySelector("#color-cvs")
|
||||
if (colorCvs==null) {
|
||||
console.log('creating new one')
|
||||
colorCvs = document.createElement("canvas")
|
||||
colorCvs.id = "color-cvs"
|
||||
document.body.appendChild(colorCvs)
|
||||
colorCvs.width = ccwidth
|
||||
colorCvs.height = 500
|
||||
colorCvs.style.width = "300px"
|
||||
colorCvs.style.height = "500px"
|
||||
colorCvs.style.position = "absolute"
|
||||
colorCvs.style.left = '500px'
|
||||
colorCvs.style.top = '500px'
|
||||
colorCvs.style.boxShadow = "0 2px 2px rgba(0,0,0,0.2)"
|
||||
colorCvs.style.cursor = "crosshair"
|
||||
colorCvs.currentColor = "#00ffba88"
|
||||
colorCvs.draw = function() {
|
||||
const darkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
let ctx = colorCvs.getContext('2d')
|
||||
if (darkMode) {
|
||||
ctx.fillStyle = "#333"
|
||||
} else {
|
||||
ctx.fillStyle = "#ccc" //TODO
|
||||
}
|
||||
ctx.fillRect(0,0,colorCvs.width, colorCvs.height)
|
||||
|
||||
// draw current color
|
||||
drawCheckerboardBackground(ctx, padding, padding, colorCvs.width - 2*padding, 50, 10)
|
||||
ctx.fillStyle = colorCvs.currentColor //TODO
|
||||
ctx.fillRect(padding,padding,colorCvs.width-2*padding, 50)
|
||||
|
||||
// Draw main gradient
|
||||
let mainGradient = ctx.createImageData(mainSize, mainSize)
|
||||
let data = mainGradient.data
|
||||
let {h, s, v} = hexToHsv(colorCvs.currentColor)
|
||||
for (let i=0; i<data.length; i+=4) {
|
||||
let x = ((i/4)%mainSize) / mainSize
|
||||
let y = Math.floor((i/4) / mainSize) / mainSize;
|
||||
let hue = h
|
||||
let rgb = hsvToRgb(hue, x, 1-y)
|
||||
data[i+0] = rgb.r;
|
||||
data[i+1] = rgb.g;
|
||||
data[i+2] = rgb.b;
|
||||
data[i+3] = 255;
|
||||
}
|
||||
ctx.putImageData(mainGradient, padding, 2*padding + 50)
|
||||
ctx.beginPath()
|
||||
ctx.arc(s*mainSize + padding, (1-v)*mainSize + 2*padding + 50, 3, 0, 2*Math.PI)
|
||||
ctx.strokeStyle = "white"
|
||||
ctx.stroke()
|
||||
|
||||
|
||||
let hueGradient = ctx.createImageData(mainSize, gradwidth)
|
||||
data = hueGradient.data
|
||||
for (let i=0; i<data.length; i+=4) {
|
||||
let x = ((i/4)%mainSize) / mainSize
|
||||
let y = Math.floor((i/4) / gradwidth)
|
||||
let rgb = hslToRgb(x, 1, 0.5)
|
||||
data[i+0] = rgb.r;
|
||||
data[i+1] = rgb.g;
|
||||
data[i+2] = rgb.b;
|
||||
data[i+3] = 255;
|
||||
}
|
||||
ctx.putImageData(hueGradient, padding, 3*padding + 50 + mainSize)
|
||||
drawCheckerboardBackground(ctx, colorCvs.width - (padding + gradwidth), 2*padding+50, gradwidth, mainSize, 10)
|
||||
const gradient = ctx.createLinearGradient(0, 2*padding+50, 0, 2*padding+50+mainSize); // Vertical gradient
|
||||
gradient.addColorStop(0, `${colorCvs.currentColor.slice(0,7)}ff`); // Full color at the top
|
||||
gradient.addColorStop(1, `${colorCvs.currentColor.slice(0,7)}00`)
|
||||
ctx.fillStyle = gradient
|
||||
ctx.fillRect(colorCvs.width - (padding + gradwidth), 2*padding+50, gradwidth, mainSize)
|
||||
}
|
||||
colorCvs.addEventListener('mousedown', (e) => {
|
||||
colorCvs.clickedMainGradient = false
|
||||
colorCvs.clickedHueGradient = false
|
||||
colorCvs.clickedAlphaGradient = false
|
||||
let mouse = getMousePos(colorCvs, e)
|
||||
let {h, s, v} = hexToHsv(colorCvs.currentColor)
|
||||
if (mouse.x > padding && mouse.x < padding + mainSize &&
|
||||
mouse.y > 2*padding + 50 && mouse.y < 2*padding + 50 + mainSize) {
|
||||
// we clicked in the main gradient
|
||||
let x = (mouse.x - padding) / mainSize
|
||||
let y = (mouse.y - (2*padding + 50)) / mainSize
|
||||
let rgb = hsvToRgb(h, x, 1-y)
|
||||
let alpha = colorCvs.currentColor.slice(7,9) || 'ff'
|
||||
colorCvs.currentColor = rgbToHex(rgb.r, rgb.g, rgb.b) + alpha
|
||||
colorCvs.clickedMainGradient = true
|
||||
} else if (mouse.x > padding && mouse.x < padding + mainSize &&
|
||||
mouse.y > 3*padding + 50+ mainSize && mouse.y < 3*padding + 50 + mainSize + gradwidth
|
||||
) {
|
||||
// we clicked in the hue gradient
|
||||
let x = (mouse.x - padding) / mainSize
|
||||
let rgb = hsvToRgb(x, s, v)
|
||||
let alpha = colorCvs.currentColor.slice(7,9) || 'ff'
|
||||
colorCvs.currentColor = rgbToHex(rgb.r, rgb.g, rgb.b) + alpha
|
||||
colorCvs.clickedHueGradient = true
|
||||
} else if (mouse.x > colorCvs.width - (padding + gradwidth) && mouse.x < colorCvs.width - padding &&
|
||||
mouse.y > 2 * padding + 50 && mouse.y < 2 * padding + 50 + mainSize
|
||||
) {
|
||||
// we clicked in the alpha gradient
|
||||
let y = 1 - ((mouse.y - (2 * padding + 50)) / mainSize)
|
||||
let alpha = Math.round(y*255).toString(16)
|
||||
colorCvs.currentColor = `${colorCvs.currentColor.slice(0,7)}${alpha}`
|
||||
colorCvs.clickedAlphaGradient = true
|
||||
}
|
||||
fillColor.style.setProperty('--color', colorCvs.currentColor);
|
||||
colorCvs.draw()
|
||||
})
|
||||
|
||||
window.addEventListener('mouseup', (e) => {
|
||||
let mouse = getMousePos(colorCvs, e)
|
||||
colorCvs.clickedMainGradient = false
|
||||
colorCvs.clickedHueGradient = false
|
||||
colorCvs.clickedAlphaGradient = false
|
||||
if (e.target != colorCvs) {
|
||||
colorCvs.style.display = 'none'
|
||||
window.removeEventListener('mousemove', evtListener)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
colorCvs.style.display = "block"
|
||||
}
|
||||
evtListener = window.addEventListener('mousemove', (e) => {
|
||||
let mouse = getMousePos(colorCvs, e)
|
||||
let {h, s, v} = hexToHsv(colorCvs.currentColor)
|
||||
if (colorCvs.clickedMainGradient) {
|
||||
let x = clamp((mouse.x - padding) / mainSize)
|
||||
let y = clamp((mouse.y - (2*padding + 50)) / mainSize)
|
||||
let rgb = hsvToRgb(h, x, 1-y)
|
||||
let alpha = colorCvs.currentColor.slice(7,9) || 'ff'
|
||||
colorCvs.currentColor = rgbToHex(rgb.r, rgb.g, rgb.b) + alpha
|
||||
colorCvs.draw()
|
||||
} else if (colorCvs.clickedHueGradient) {
|
||||
let x = clamp((mouse.x - padding) / mainSize)
|
||||
let rgb = hsvToRgb(x, s, v)
|
||||
let alpha = colorCvs.currentColor.slice(7,9) || 'ff'
|
||||
colorCvs.currentColor = rgbToHex(rgb.r, rgb.g, rgb.b) + alpha
|
||||
colorCvs.draw()
|
||||
} else if (colorCvs.clickedAlphaGradient) {
|
||||
let y = clamp(1 - ((mouse.y - (2 * padding + 50)) / mainSize))
|
||||
let alpha = Math.round(y * 255).toString(16).padStart(2, '0');
|
||||
colorCvs.currentColor = `${colorCvs.currentColor.slice(0,7)}${alpha}`
|
||||
colorCvs.draw()
|
||||
}
|
||||
console.log(colorCvs.currentColor)
|
||||
fillColor.style.setProperty('--color', colorCvs.currentColor);
|
||||
})
|
||||
// Get mouse coordinates relative to the viewport
|
||||
const mouseX = e.clientX + window.scrollX;
|
||||
const mouseY = e.clientY + window.scrollY;
|
||||
|
||||
const divWidth = colorCvs.offsetWidth;
|
||||
const divHeight = colorCvs.offsetHeight;
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
// Default position to the mouse cursor
|
||||
let left = mouseX;
|
||||
let top = mouseY;
|
||||
|
||||
// If the window is narrower than twice the width, center horizontally
|
||||
if (windowWidth < divWidth * 2) {
|
||||
left = (windowWidth - divWidth) / 2;
|
||||
} else {
|
||||
// If it would overflow on the right side, position it to the left of the cursor
|
||||
if (left + divWidth > windowWidth) {
|
||||
left = mouseX - divWidth;
|
||||
}
|
||||
}
|
||||
|
||||
// If the window is shorter than twice the height, center vertically
|
||||
if (windowHeight < divHeight * 2) {
|
||||
top = (windowHeight - divHeight) / 2;
|
||||
} else {
|
||||
// If it would overflow at the bottom, position it above the cursor
|
||||
if (top + divHeight > windowHeight) {
|
||||
top = mouseY - divHeight;
|
||||
}
|
||||
}
|
||||
|
||||
colorCvs.style.left = `${left}px`;
|
||||
colorCvs.style.top = `${top}px`;
|
||||
colorCvs.draw()
|
||||
e.preventDefault()
|
||||
})
|
||||
strokeColor.addEventListener('click', e => {
|
||||
Coloris({
|
||||
el: ".color-field",
|
||||
selectInput: true,
|
||||
focusInput: true,
|
||||
theme: 'default',
|
||||
swatches: context.swatches,
|
||||
defaultColor: '#000000',
|
||||
onChange: (color) => {
|
||||
context.strokeStyle = color;
|
||||
}
|
||||
})
|
||||
// Coloris({
|
||||
// el: ".color-field",
|
||||
// selectInput: true,
|
||||
// focusInput: true,
|
||||
// theme: 'default',
|
||||
// swatches: context.swatches,
|
||||
// defaultColor: '#000000',
|
||||
// onChange: (color) => {
|
||||
// context.strokeStyle = color;
|
||||
// }
|
||||
// })
|
||||
})
|
||||
// Fill and stroke colors use the same set of swatches
|
||||
fillColor.addEventListener("change", e => {
|
||||
|
|
@ -3357,7 +3548,7 @@ function splitPane(div, percent, horiz, newPane=undefined) {
|
|||
event.currentTarget.setAttribute("dragging", false)
|
||||
// event.currentTarget.style.userSelect = 'auto';
|
||||
})
|
||||
Coloris({el: ".color-field"})
|
||||
// Coloris({el: ".color-field"})
|
||||
updateAll()
|
||||
updateUI()
|
||||
updateLayers()
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ button {
|
|||
color: #0f0f0f;
|
||||
background-color: #ffffff;
|
||||
transition: border-color 0.25s;
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.2);
|
||||
box-sizing: border-box;
|
||||
min-height: var(--lineheight);
|
||||
}
|
||||
|
|
@ -278,9 +278,35 @@ button {
|
|||
|
||||
background-color: #999;
|
||||
}
|
||||
.clr-field {
|
||||
.color-field {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 10px;
|
||||
width: 100%;
|
||||
height: calc(2 * var(--lineheight));
|
||||
--color: red; /* CSS variable for the pseudo-element color */
|
||||
}
|
||||
|
||||
.color-field::before {
|
||||
content: "Color:";
|
||||
font-size: 16px;
|
||||
color: black;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.color-field::after {
|
||||
content: "";
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
border-radius: 5px;
|
||||
background:
|
||||
linear-gradient(to right, var(--color), var(--color)),
|
||||
repeating-conic-gradient(#B0B0B0 0% 25%, #E0E0E0 0% 50%)
|
||||
50% / 20px 20px,
|
||||
linear-gradient(to right, white, white);
|
||||
}
|
||||
|
||||
.clr-field button {
|
||||
width: 50% !important;
|
||||
/* height: 100% !important; */
|
||||
|
|
@ -582,6 +608,9 @@ button {
|
|||
#popupMenu li:not(:last-child) {
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
.color-field::before {
|
||||
color: #eee;
|
||||
}
|
||||
.layers {
|
||||
background-color: #222222;
|
||||
}
|
||||
|
|
|
|||
210
src/utils.js
210
src/utils.js
|
|
@ -104,11 +104,6 @@ function lerpColor(color1, color2, t) {
|
|||
return { r, g, b };
|
||||
};
|
||||
|
||||
// Convert RGB to hex color
|
||||
const rgbToHex = (r, g, b) => {
|
||||
return `#${(1 << 24 | (r << 16) | (g << 8) | b).toString(16).slice(1).toUpperCase()}`;
|
||||
};
|
||||
|
||||
// Get RGB values of both colors
|
||||
const start = hexToRgb(color1);
|
||||
const end = hexToRgb(color2);
|
||||
|
|
@ -356,11 +351,201 @@ function intToHexColor(value) {
|
|||
return '#' + value.toString(16).padStart(6, '0').toUpperCase();
|
||||
}
|
||||
|
||||
// Helper function to convert RGB to hex (for sampling)
|
||||
function rgbToHex(r, g, b) {
|
||||
return '#' + (1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1).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)
|
||||
}
|
||||
|
||||
export {
|
||||
titleCase,
|
||||
|
|
@ -372,5 +557,12 @@ export {
|
|||
camelToWords,
|
||||
generateWaveform,
|
||||
floodFillRegion,
|
||||
getShapeAtPoint
|
||||
getShapeAtPoint,
|
||||
hslToRgb,
|
||||
hsvToRgb,
|
||||
hexToHsl,
|
||||
hexToHsv,
|
||||
rgbToHex,
|
||||
drawCheckerboardBackground,
|
||||
clamp
|
||||
};
|
||||
Loading…
Reference in New Issue