initial work on new color picker

This commit is contained in:
Skyler Lehmkuhl 2024-12-16 17:59:27 -05:00
parent f010faef73
commit 747b34ec67
3 changed files with 447 additions and 35 deletions

View File

@ -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()

View File

@ -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;
} }

View File

@ -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
}; };