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 { Bezier } from "/bezier.js";
|
||||||
import { Quadtree } from './quadtree.js';
|
import { Quadtree } from './quadtree.js';
|
||||||
import { createNewFileDialog, showNewFileDialog, closeDialog } from './newfile.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 { writeTextFile: writeTextFile, readTextFile: readTextFile, writeFile: writeFile, readFile: readFile }= window.__TAURI__.fs;
|
||||||
const {
|
const {
|
||||||
open: openFileDialog,
|
open: openFileDialog,
|
||||||
|
|
@ -3149,39 +3149,230 @@ function toolbar() {
|
||||||
let tools_break = document.createElement("div")
|
let tools_break = document.createElement("div")
|
||||||
tools_break.className = "horiz_break"
|
tools_break.className = "horiz_break"
|
||||||
tools_scroller.appendChild(tools_break)
|
tools_scroller.appendChild(tools_break)
|
||||||
let fillColor = document.createElement("input")
|
let fillColor = document.createElement("div")
|
||||||
let strokeColor = document.createElement("input")
|
let strokeColor = document.createElement("div")
|
||||||
fillColor.className = "color-field"
|
fillColor.className = "color-field"
|
||||||
strokeColor.className = "color-field"
|
strokeColor.className = "color-field"
|
||||||
|
fillColor.style.setProperty('--color', "#ff0000");
|
||||||
|
strokeColor.style.setProperty('--color', "#000000");
|
||||||
|
fillColor.type="color"
|
||||||
fillColor.value = "#ff0000"
|
fillColor.value = "#ff0000"
|
||||||
strokeColor.value = "#000000"
|
strokeColor.value = "#000000"
|
||||||
context.fillStyle = fillColor.value
|
context.fillStyle = fillColor.value
|
||||||
context.strokeStyle = strokeColor.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 => {
|
fillColor.addEventListener('click', e => {
|
||||||
Coloris({
|
// Coloris({
|
||||||
el: ".color-field",
|
// el: ".color-field",
|
||||||
selectInput: true,
|
// selectInput: true,
|
||||||
focusInput: true,
|
// focusInput: true,
|
||||||
theme: 'default',
|
// theme: 'default',
|
||||||
swatches: context.swatches,
|
// swatches: context.swatches,
|
||||||
defaultColor: '#ff0000',
|
// defaultColor: '#ff0000',
|
||||||
onChange: (color) => {
|
// onChange: (color) => {
|
||||||
context.fillStyle = 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 => {
|
strokeColor.addEventListener('click', e => {
|
||||||
Coloris({
|
// Coloris({
|
||||||
el: ".color-field",
|
// el: ".color-field",
|
||||||
selectInput: true,
|
// selectInput: true,
|
||||||
focusInput: true,
|
// focusInput: true,
|
||||||
theme: 'default',
|
// theme: 'default',
|
||||||
swatches: context.swatches,
|
// swatches: context.swatches,
|
||||||
defaultColor: '#000000',
|
// defaultColor: '#000000',
|
||||||
onChange: (color) => {
|
// onChange: (color) => {
|
||||||
context.strokeStyle = color;
|
// context.strokeStyle = color;
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
})
|
})
|
||||||
// Fill and stroke colors use the same set of swatches
|
// Fill and stroke colors use the same set of swatches
|
||||||
fillColor.addEventListener("change", e => {
|
fillColor.addEventListener("change", e => {
|
||||||
|
|
@ -3357,7 +3548,7 @@ function splitPane(div, percent, horiz, newPane=undefined) {
|
||||||
event.currentTarget.setAttribute("dragging", false)
|
event.currentTarget.setAttribute("dragging", false)
|
||||||
// event.currentTarget.style.userSelect = 'auto';
|
// event.currentTarget.style.userSelect = 'auto';
|
||||||
})
|
})
|
||||||
Coloris({el: ".color-field"})
|
// Coloris({el: ".color-field"})
|
||||||
updateAll()
|
updateAll()
|
||||||
updateUI()
|
updateUI()
|
||||||
updateLayers()
|
updateLayers()
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ button {
|
||||||
color: #0f0f0f;
|
color: #0f0f0f;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
transition: border-color 0.25s;
|
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;
|
box-sizing: border-box;
|
||||||
min-height: var(--lineheight);
|
min-height: var(--lineheight);
|
||||||
}
|
}
|
||||||
|
|
@ -278,9 +278,35 @@ button {
|
||||||
|
|
||||||
background-color: #999;
|
background-color: #999;
|
||||||
}
|
}
|
||||||
.clr-field {
|
.color-field {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 10px;
|
||||||
width: 100%;
|
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 {
|
.clr-field button {
|
||||||
width: 50% !important;
|
width: 50% !important;
|
||||||
/* height: 100% !important; */
|
/* height: 100% !important; */
|
||||||
|
|
@ -582,6 +608,9 @@ button {
|
||||||
#popupMenu li:not(:last-child) {
|
#popupMenu li:not(:last-child) {
|
||||||
border-bottom: 1px solid #444;
|
border-bottom: 1px solid #444;
|
||||||
}
|
}
|
||||||
|
.color-field::before {
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
.layers {
|
.layers {
|
||||||
background-color: #222222;
|
background-color: #222222;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
210
src/utils.js
210
src/utils.js
|
|
@ -104,11 +104,6 @@ function lerpColor(color1, color2, t) {
|
||||||
return { r, g, b };
|
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
|
// Get RGB values of both colors
|
||||||
const start = hexToRgb(color1);
|
const start = hexToRgb(color1);
|
||||||
const end = hexToRgb(color2);
|
const end = hexToRgb(color2);
|
||||||
|
|
@ -356,11 +351,201 @@ function intToHexColor(value) {
|
||||||
return '#' + value.toString(16).padStart(6, '0').toUpperCase();
|
return '#' + value.toString(16).padStart(6, '0').toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to convert RGB to hex (for sampling)
|
function hslToRgb(h, s, l) {
|
||||||
function rgbToHex(r, g, b) {
|
// Ensure that the input values are within the expected range [0, 1]
|
||||||
return '#' + (1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1).toUpperCase();
|
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 {
|
export {
|
||||||
titleCase,
|
titleCase,
|
||||||
|
|
@ -372,5 +557,12 @@ export {
|
||||||
camelToWords,
|
camelToWords,
|
||||||
generateWaveform,
|
generateWaveform,
|
||||||
floodFillRegion,
|
floodFillRegion,
|
||||||
getShapeAtPoint
|
getShapeAtPoint,
|
||||||
|
hslToRgb,
|
||||||
|
hsvToRgb,
|
||||||
|
hexToHsl,
|
||||||
|
hexToHsv,
|
||||||
|
rgbToHex,
|
||||||
|
drawCheckerboardBackground,
|
||||||
|
clamp
|
||||||
};
|
};
|
||||||
Loading…
Reference in New Issue