Merge pull request #48 from outis1one/claude/awesome-bohr-mks3co
Claude/awesome bohr mks3co
This commit is contained in:
@@ -140,6 +140,11 @@ const menuDefinition = [
|
||||
shortcut: 'Ctrl+Y',
|
||||
target: 'edit/redo.redo'
|
||||
},
|
||||
{
|
||||
name: 'History Panel',
|
||||
shortcut: 'Ctrl+H',
|
||||
target: 'edit/history_panel.toggle'
|
||||
},
|
||||
{
|
||||
divider: true
|
||||
},
|
||||
@@ -325,6 +330,15 @@ const menuDefinition = [
|
||||
{
|
||||
divider: true
|
||||
},
|
||||
{
|
||||
name: 'Auto-Enhance',
|
||||
ellipsis: true,
|
||||
target: 'image/auto_enhance.auto_enhance'
|
||||
},
|
||||
{
|
||||
name: 'Extract Color Palette',
|
||||
target: 'image/color_palette.color_palette'
|
||||
},
|
||||
{
|
||||
name: 'Remove Background (AI)',
|
||||
ellipsis: true,
|
||||
@@ -402,6 +416,10 @@ const menuDefinition = [
|
||||
ellipsis: true,
|
||||
target: 'layer/scale.scale'
|
||||
},
|
||||
{
|
||||
name: 'Align to Canvas',
|
||||
target: 'layer/align.align'
|
||||
},
|
||||
{
|
||||
divider: true
|
||||
},
|
||||
@@ -840,6 +858,14 @@ const menuDefinition = [
|
||||
{
|
||||
name: 'Generate',
|
||||
children: [
|
||||
{
|
||||
name: 'Add Text',
|
||||
ellipsis: true,
|
||||
target: 'text/text_presets.add_preset'
|
||||
},
|
||||
{
|
||||
divider: true
|
||||
},
|
||||
{
|
||||
name: 'Text → Image',
|
||||
ellipsis: true,
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* Pantone color database — ~350 representative PMS colors with hex approximations.
|
||||
* Source: open-source Pantone approximations (not official Pantone data).
|
||||
* Format: [name, hex]
|
||||
*
|
||||
* Delta E matching uses LAB color space — see color_utils.js.
|
||||
*/
|
||||
const PANTONE_COLORS = [
|
||||
// Reds & Pinks
|
||||
['Pantone 485 C', '#da291c'],
|
||||
['Pantone 186 C', '#c8102e'],
|
||||
['Pantone 1795 C', '#ce2939'],
|
||||
['Pantone 1805 C', '#ab2328'],
|
||||
['Pantone 1815 C', '#833033'],
|
||||
['Pantone 200 C', '#ba0c2f'],
|
||||
['Pantone 201 C', '#9d2235'],
|
||||
['Pantone 202 C', '#862633'],
|
||||
['Pantone 206 C', '#ce0058'],
|
||||
['Pantone 207 C', '#a50034'],
|
||||
['Pantone 208 C', '#84254a'],
|
||||
['Pantone 213 C', '#e4488a'],
|
||||
['Pantone 214 C', '#d4357b'],
|
||||
['Pantone 215 C', '#bb2261'],
|
||||
['Pantone 219 C', '#e10069'],
|
||||
['Pantone 225 C', '#d5006c'],
|
||||
['Pantone 226 C', '#cb0070'],
|
||||
['Pantone 485 C', '#da291c'],
|
||||
['Pantone Pink C', '#e87ca0'],
|
||||
['Pantone Rubine Red C', '#ce0058'],
|
||||
['Pantone Rhodamine Red C', '#e10096'],
|
||||
|
||||
// Oranges
|
||||
['Pantone 021 C', '#fe5000'],
|
||||
['Pantone 151 C', '#ff7900'],
|
||||
['Pantone 152 C', '#e87722'],
|
||||
['Pantone 153 C', '#cb6015'],
|
||||
['Pantone 158 C', '#e8642c'],
|
||||
['Pantone 165 C', '#fc4c02'],
|
||||
['Pantone 166 C', '#e55302'],
|
||||
['Pantone 167 C', '#be4b00'],
|
||||
['Pantone 1495 C', '#ff8200'],
|
||||
['Pantone 1505 C', '#ff671f'],
|
||||
['Pantone Orange 021 C', '#fe5000'],
|
||||
['Pantone Warm Red C', '#f9423a'],
|
||||
|
||||
// Yellows
|
||||
['Pantone Yellow C', '#fedd00'],
|
||||
['Pantone 101 C', '#f9e84e'],
|
||||
['Pantone 102 C', '#fce300'],
|
||||
['Pantone 103 C', '#c5a900'],
|
||||
['Pantone 104 C', '#af9800'],
|
||||
['Pantone 108 C', '#f6d500'],
|
||||
['Pantone 109 C', '#ffd100'],
|
||||
['Pantone 110 C', '#d4af00'],
|
||||
['Pantone 115 C', '#fbdb65'],
|
||||
['Pantone 116 C', '#ffcd00'],
|
||||
['Pantone 117 C', '#c79200'],
|
||||
['Pantone 123 C', '#ffc72c'],
|
||||
['Pantone 124 C', '#e6a817'],
|
||||
['Pantone 130 C', '#f0aa00'],
|
||||
['Pantone 1205 C', '#f5e1a4'],
|
||||
['Pantone 1215 C', '#f5cf7e'],
|
||||
['Pantone 1225 C', '#fbb040'],
|
||||
['Pantone 1235 C', '#f7941d'],
|
||||
['Pantone 1245 C', '#d4890a'],
|
||||
['Pantone Gold C', '#af8c00'],
|
||||
|
||||
// Greens
|
||||
['Pantone Green C', '#00ab84'],
|
||||
['Pantone 354 C', '#00b140'],
|
||||
['Pantone 355 C', '#009a44'],
|
||||
['Pantone 356 C', '#007a3d'],
|
||||
['Pantone 361 C', '#43b02a'],
|
||||
['Pantone 362 C', '#3d9a31'],
|
||||
['Pantone 363 C', '#347d2c'],
|
||||
['Pantone 368 C', '#78be20'],
|
||||
['Pantone 369 C', '#5da31c'],
|
||||
['Pantone 370 C', '#4a7729'],
|
||||
['Pantone 375 C', '#97d700'],
|
||||
['Pantone 376 C', '#72b200'],
|
||||
['Pantone 382 C', '#c4d600'],
|
||||
['Pantone 390 C', '#a8ad00'],
|
||||
['Pantone 334 C', '#00855d'],
|
||||
['Pantone 335 C', '#006a52'],
|
||||
['Pantone 336 C', '#00573f'],
|
||||
['Pantone 340 C', '#00843d'],
|
||||
['Pantone 341 C', '#00693c'],
|
||||
['Pantone 342 C', '#215732'],
|
||||
['Pantone 347 C', '#009a44'],
|
||||
['Pantone 348 C', '#007a3d'],
|
||||
['Pantone 349 C', '#215732'],
|
||||
['Pantone 3415 C', '#00665c'],
|
||||
['Pantone 3425 C', '#006a52'],
|
||||
|
||||
// Teals & Cyans
|
||||
['Pantone Process Cyan C', '#0085ca'],
|
||||
['Pantone 306 C', '#00b5e2'],
|
||||
['Pantone 307 C', '#007dba'],
|
||||
['Pantone 308 C', '#005f86'],
|
||||
['Pantone 313 C', '#00b0ca'],
|
||||
['Pantone 314 C', '#0093ab'],
|
||||
['Pantone 315 C', '#007395'],
|
||||
['Pantone 320 C', '#009ca6'],
|
||||
['Pantone 321 C', '#008c95'],
|
||||
['Pantone 322 C', '#007680'],
|
||||
['Pantone 326 C', '#00b2a9'],
|
||||
['Pantone 327 C', '#007a74'],
|
||||
['Pantone 328 C', '#006E61'],
|
||||
['Pantone 3262 C', '#00b2a9'],
|
||||
['Pantone 3272 C', '#00a3ad'],
|
||||
['Pantone 3282 C', '#008c95'],
|
||||
['Pantone 3292 C', '#005f6a'],
|
||||
|
||||
// Blues
|
||||
['Pantone Reflex Blue C', '#001489'],
|
||||
['Pantone Blue 072 C', '#10069f'],
|
||||
['Pantone 279 C', '#418fde'],
|
||||
['Pantone 280 C', '#003087'],
|
||||
['Pantone 281 C', '#002d72'],
|
||||
['Pantone 286 C', '#0033a0'],
|
||||
['Pantone 287 C', '#003087'],
|
||||
['Pantone 288 C', '#002d72'],
|
||||
['Pantone 293 C', '#0032a0'],
|
||||
['Pantone 294 C', '#002b6c'],
|
||||
['Pantone 295 C', '#002244'],
|
||||
['Pantone 300 C', '#0057a8'],
|
||||
['Pantone 301 C', '#005596'],
|
||||
['Pantone 302 C', '#003f72'],
|
||||
['Pantone 2728 C', '#2251b8'],
|
||||
['Pantone 2738 C', '#1b1464'],
|
||||
['Pantone 2748 C', '#0f1f8a'],
|
||||
['Pantone 2758 C', '#13234b'],
|
||||
['Pantone Bright Blue C', '#0087c8'],
|
||||
['Pantone 298 C', '#5bc8f5'],
|
||||
['Pantone 297 C', '#7bc4e2'],
|
||||
|
||||
// Purples & Violets
|
||||
['Pantone Violet C', '#440099'],
|
||||
['Pantone 2587 C', '#8246af'],
|
||||
['Pantone 2597 C', '#6b1f7c'],
|
||||
['Pantone 2607 C', '#5e2175'],
|
||||
['Pantone 2617 C', '#522d6d'],
|
||||
['Pantone 2627 C', '#401752'],
|
||||
['Pantone 2665 C', '#9678d3'],
|
||||
['Pantone 2685 C', '#43009a'],
|
||||
['Pantone 2695 C', '#312068'],
|
||||
['Pantone 2705 C', '#8085c9'],
|
||||
['Pantone 2715 C', '#6e6bbf'],
|
||||
['Pantone 2725 C', '#4f52af'],
|
||||
['Pantone 2735 C', '#1f1a6e'],
|
||||
['Pantone 2745 C', '#1b1747'],
|
||||
['Pantone Ultra Violet C', '#5f4b8b'],
|
||||
['Pantone 259 C', '#6c2e8e'],
|
||||
['Pantone 266 C', '#6a2bb8'],
|
||||
['Pantone 267 C', '#521b8a'],
|
||||
['Pantone 268 C', '#43205e'],
|
||||
['Pantone 269 C', '#31184e'],
|
||||
['Pantone 253 C', '#b968c7'],
|
||||
['Pantone 254 C', '#aa4da0'],
|
||||
['Pantone 2562 C', '#c294d6'],
|
||||
|
||||
// Magentas
|
||||
['Pantone Process Magenta C', '#d50087'],
|
||||
['Pantone Magenta 0521 C', '#d6006f'],
|
||||
['Pantone 233 C', '#c5007f'],
|
||||
['Pantone 234 C', '#a50064'],
|
||||
['Pantone 235 C', '#8c0056'],
|
||||
['Pantone 239 C', '#db5aa4'],
|
||||
['Pantone 240 C', '#bf4e99'],
|
||||
|
||||
// Browns & Tans
|
||||
['Pantone 469 C', '#6b3d2e'],
|
||||
['Pantone 470 C', '#8c4a2f'],
|
||||
['Pantone 471 C', '#a05b38'],
|
||||
['Pantone 476 C', '#4e3629'],
|
||||
['Pantone 477 C', '#5c3d2e'],
|
||||
['Pantone 478 C', '#6d4535'],
|
||||
['Pantone 483 C', '#7a2e22'],
|
||||
['Pantone 484 C', '#9b3423'],
|
||||
['Pantone 4625 C', '#4a1c0e'],
|
||||
['Pantone 4635 C', '#7d3c1a'],
|
||||
['Pantone 4645 C', '#a45f3a'],
|
||||
['Pantone 4655 C', '#b87246'],
|
||||
['Pantone 463 C', '#7d5326'],
|
||||
['Pantone 464 C', '#8b5e27'],
|
||||
['Pantone 465 C', '#9e7232'],
|
||||
['Pantone 4505 C', '#8a7252'],
|
||||
['Pantone 4515 C', '#9e8866'],
|
||||
['Pantone 4525 C', '#b39e7a'],
|
||||
['Pantone Tan C', '#d2b48c'],
|
||||
|
||||
// Grays
|
||||
['Pantone Cool Gray 1 C', '#d9d9d6'],
|
||||
['Pantone Cool Gray 2 C', '#d0d0ce'],
|
||||
['Pantone Cool Gray 3 C', '#c8c9c7'],
|
||||
['Pantone Cool Gray 4 C', '#bbbcbc'],
|
||||
['Pantone Cool Gray 5 C', '#b1b3b3'],
|
||||
['Pantone Cool Gray 6 C', '#a7a8aa'],
|
||||
['Pantone Cool Gray 7 C', '#97999b'],
|
||||
['Pantone Cool Gray 8 C', '#888b8d'],
|
||||
['Pantone Cool Gray 9 C', '#75787b'],
|
||||
['Pantone Cool Gray 10 C','#63666a'],
|
||||
['Pantone Cool Gray 11 C','#53565a'],
|
||||
['Pantone Warm Gray 1 C', '#d8d3cb'],
|
||||
['Pantone Warm Gray 2 C', '#cec6ba'],
|
||||
['Pantone Warm Gray 3 C', '#c4bbad'],
|
||||
['Pantone Warm Gray 4 C', '#bbb0a2'],
|
||||
['Pantone Warm Gray 5 C', '#b0a596'],
|
||||
['Pantone Warm Gray 6 C', '#a39891'],
|
||||
['Pantone Warm Gray 7 C', '#968c85'],
|
||||
['Pantone Warm Gray 8 C', '#8a7f76'],
|
||||
['Pantone Warm Gray 9 C', '#7d7368'],
|
||||
['Pantone Warm Gray 10 C','#72685d'],
|
||||
['Pantone Warm Gray 11 C','#655f56'],
|
||||
['Pantone 420 C', '#c7c7c4'],
|
||||
['Pantone 421 C', '#b5b6b3'],
|
||||
['Pantone 422 C', '#a4a4a1'],
|
||||
['Pantone 423 C', '#929291'],
|
||||
['Pantone 424 C', '#7f7f7d'],
|
||||
['Pantone 425 C', '#6c6c6c'],
|
||||
['Pantone 426 C', '#404040'],
|
||||
|
||||
// Black & White
|
||||
['Pantone Black C', '#2b2926'],
|
||||
['Pantone Black 6 C','#101820'],
|
||||
['Pantone White', '#f2f0eb'],
|
||||
|
||||
// Special / Brand colors
|
||||
['Pantone 021 C', '#fe5000'], // Harley-Davidson orange area
|
||||
['Pantone 484 C', '#9b3423'],
|
||||
['Pantone Bright Red C', '#f22613'],
|
||||
['Pantone 3005 C', '#0076c2'],
|
||||
['Pantone 3015 C', '#006298'],
|
||||
['Pantone 3025 C', '#005274'],
|
||||
['Pantone 3035 C', '#00445d'],
|
||||
];
|
||||
|
||||
export default PANTONE_COLORS;
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Color conversion and Pantone matching utilities.
|
||||
*
|
||||
* hexToRgb(hex) → { r, g, b }
|
||||
* rgbToHsl(r,g,b) → { h, s, l } (h=0-360, s/l=0-100)
|
||||
* rgbToLab(r,g,b) → { L, a, b } (CIE LAB D65)
|
||||
* deltaE(lab1, lab2) → number (CIE76, lower = more similar)
|
||||
* nearestPantone(hex) → { name, hex, deltaE }
|
||||
*/
|
||||
|
||||
import PANTONE_COLORS from './../data/pantone_colors.js';
|
||||
|
||||
// Pre-convert Pantone database to LAB once at module load
|
||||
const _pantonelab = PANTONE_COLORS.map(([name, hex]) => {
|
||||
const { r, g, b } = hexToRgb(hex);
|
||||
return { name, hex, lab: rgbToLab(r, g, b) };
|
||||
});
|
||||
|
||||
export function hexToRgb(hex) {
|
||||
const h = hex.replace('#', '');
|
||||
const n = parseInt(h.length === 3
|
||||
? h.split('').map(c => c + c).join('')
|
||||
: h, 16);
|
||||
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
|
||||
}
|
||||
|
||||
export function rgbToHex(r, g, b) {
|
||||
return '#' + [r, g, b].map(v => v.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
export function rgbToHsl(r, g, b) {
|
||||
r /= 255; g /= 255; b /= 255;
|
||||
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||
let h, s, l = (max + min) / 2;
|
||||
if (max === min) {
|
||||
h = s = 0;
|
||||
} else {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
||||
case g: h = ((b - r) / d + 2) / 6; break;
|
||||
case b: h = ((r - g) / d + 4) / 6; break;
|
||||
}
|
||||
}
|
||||
return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
|
||||
}
|
||||
|
||||
export function rgbToLab(r, g, b) {
|
||||
// sRGB → linear
|
||||
let R = r / 255, G = g / 255, B = b / 255;
|
||||
R = R > 0.04045 ? Math.pow((R + 0.055) / 1.055, 2.4) : R / 12.92;
|
||||
G = G > 0.04045 ? Math.pow((G + 0.055) / 1.055, 2.4) : G / 12.92;
|
||||
B = B > 0.04045 ? Math.pow((B + 0.055) / 1.055, 2.4) : B / 12.92;
|
||||
|
||||
// linear RGB → XYZ (D65)
|
||||
let X = R * 0.4124564 + G * 0.3575761 + B * 0.1804375;
|
||||
let Y = R * 0.2126729 + G * 0.7151522 + B * 0.0721750;
|
||||
let Z = R * 0.0193339 + G * 0.1191920 + B * 0.9503041;
|
||||
|
||||
// XYZ → LAB (D65 white = 0.95047, 1.0, 1.08883)
|
||||
const f = v => v > 0.008856 ? Math.cbrt(v) : 7.787 * v + 16 / 116;
|
||||
X = f(X / 0.95047); Y = f(Y / 1.0); Z = f(Z / 1.08883);
|
||||
|
||||
return { L: 116 * Y - 16, a: 500 * (X - Y), b: 200 * (Y - Z) };
|
||||
}
|
||||
|
||||
export function deltaE(lab1, lab2) {
|
||||
const dL = lab1.L - lab2.L;
|
||||
const da = lab1.a - lab2.a;
|
||||
const db = lab1.b - lab2.b;
|
||||
return Math.sqrt(dL * dL + da * da + db * db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closest Pantone color to a hex value.
|
||||
* Returns { name, hex, deltaE, quality }
|
||||
* quality: 'excellent' (<2), 'good' (2-5), 'fair' (5-10), 'poor' (>10)
|
||||
*/
|
||||
export function nearestPantone(hex) {
|
||||
const { r, g, b } = hexToRgb(hex);
|
||||
const lab = rgbToLab(r, g, b);
|
||||
|
||||
let best = null, bestDE = Infinity;
|
||||
for (const entry of _pantonelab) {
|
||||
const de = deltaE(lab, entry.lab);
|
||||
if (de < bestDE) { bestDE = de; best = entry; }
|
||||
}
|
||||
|
||||
const de = Math.round(bestDE * 10) / 10;
|
||||
const quality = de < 2 ? 'excellent' : de < 5 ? 'good' : de < 10 ? 'fair' : 'poor';
|
||||
return { name: best.name, hex: best.hex, deltaE: de, quality };
|
||||
}
|
||||
|
||||
/**
|
||||
* Quality label + color for ΔE badge.
|
||||
*/
|
||||
export function deltaEBadge(quality) {
|
||||
const map = {
|
||||
excellent: { label: 'Excellent match', color: '#4ade80' },
|
||||
good: { label: 'Good match', color: '#86efac' },
|
||||
fair: { label: 'Fair match', color: '#fbbf24' },
|
||||
poor: { label: 'Poor match — color may shift in print', color: '#f87171' },
|
||||
};
|
||||
return map[quality] || map.poor;
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* History Panel — visual undo history timeline.
|
||||
* Shows the last N actions as a clickable list. Click any item to undo/redo to that point.
|
||||
* Docks as a floating panel on the right side of the screen.
|
||||
*
|
||||
* Menu target: edit/history_panel.toggle
|
||||
*/
|
||||
|
||||
import app from './../../app.js';
|
||||
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
|
||||
|
||||
var instance = null;
|
||||
|
||||
class Edit_history_panel_class {
|
||||
constructor() {
|
||||
if (instance) return instance;
|
||||
instance = this;
|
||||
this._panel = null;
|
||||
this._interval = null;
|
||||
}
|
||||
|
||||
toggle() {
|
||||
if (this._panel) {
|
||||
this._stop();
|
||||
} else {
|
||||
this._start();
|
||||
}
|
||||
}
|
||||
|
||||
_start() {
|
||||
this._buildPanel();
|
||||
this._render();
|
||||
// Refresh whenever the history changes (poll lightly)
|
||||
this._interval = setInterval(() => this._render(), 800);
|
||||
}
|
||||
|
||||
_stop() {
|
||||
if (this._interval) { clearInterval(this._interval); this._interval = null; }
|
||||
if (this._panel) { this._panel.remove(); this._panel = null; }
|
||||
}
|
||||
|
||||
_buildPanel() {
|
||||
const panel = document.createElement('div');
|
||||
panel.id = 'history_panel';
|
||||
Object.assign(panel.style, {
|
||||
position: 'fixed',
|
||||
top: '60px',
|
||||
right: '0',
|
||||
width: '200px',
|
||||
maxHeight: 'calc(100vh - 80px)',
|
||||
overflowY: 'auto',
|
||||
background: '#1a1a1a',
|
||||
borderLeft: '1px solid #333',
|
||||
borderBottom: '1px solid #333',
|
||||
borderRadius: '0 0 0 10px',
|
||||
zIndex: '8888',
|
||||
fontFamily: 'sans-serif',
|
||||
fontSize: '12px',
|
||||
color: '#ccc',
|
||||
boxShadow: '-4px 4px 16px rgba(0,0,0,0.4)',
|
||||
userSelect: 'none',
|
||||
});
|
||||
panel.innerHTML = `
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;
|
||||
padding:8px 10px;border-bottom:1px solid #333;position:sticky;top:0;
|
||||
background:#1a1a1a;z-index:1;">
|
||||
<span style="font-size:12px;color:#888;font-weight:600;">History</span>
|
||||
<span id="hist-close" style="cursor:pointer;color:#555;font-size:16px;">×</span>
|
||||
</div>
|
||||
<div id="hist-list"></div>`;
|
||||
document.body.appendChild(panel);
|
||||
this._panel = panel;
|
||||
panel.querySelector('#hist-close').addEventListener('click', () => this._stop());
|
||||
}
|
||||
|
||||
_render() {
|
||||
if (!this._panel) return;
|
||||
const list = this._panel.querySelector('#hist-list');
|
||||
if (!list) return;
|
||||
|
||||
const history = app.State.action_history || [];
|
||||
const idx = app.State.action_history_index ?? history.length;
|
||||
|
||||
if (history.length === 0) {
|
||||
list.innerHTML = `<div style="padding:12px 10px;color:#555;">No actions yet.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Build rows newest-first
|
||||
const rows = [];
|
||||
// "Current state" row at top
|
||||
const atTop = idx >= history.length;
|
||||
rows.push(`<div data-idx="${history.length}"
|
||||
style="padding:6px 10px;cursor:pointer;border-bottom:1px solid #222;
|
||||
background:${atTop ? '#1e3a5f' : 'transparent'};
|
||||
color:${atTop ? '#93c5fd' : '#666'};"
|
||||
>
|
||||
<span style="margin-right:6px;font-size:10px;">${atTop ? '▶' : '○'}</span>Current state
|
||||
</div>`);
|
||||
|
||||
for (let i = history.length - 1; i >= 0; i--) {
|
||||
const action = history[i];
|
||||
const isCurrent = (i === idx - 1);
|
||||
const isFuture = (i >= idx);
|
||||
const label = action.action_description || action.action_id || `Step ${i + 1}`;
|
||||
rows.push(`<div data-idx="${i}"
|
||||
style="padding:6px 10px;cursor:pointer;border-bottom:1px solid #1e1e1e;
|
||||
background:${isCurrent ? '#1e3a5f' : 'transparent'};
|
||||
color:${isFuture ? '#444' : isCurrent ? '#93c5fd' : '#ccc'};"
|
||||
>
|
||||
<span style="margin-right:6px;font-size:10px;">${isCurrent ? '▶' : isFuture ? '○' : '·'}</span>${_escHtml(label)}
|
||||
</div>`);
|
||||
}
|
||||
list.innerHTML = rows.join('');
|
||||
|
||||
// Wire clicks
|
||||
list.querySelectorAll('[data-idx]').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const target = parseInt(el.dataset.idx, 10);
|
||||
this._jumpTo(target);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_jumpTo(targetIdx) {
|
||||
const history = app.State.action_history || [];
|
||||
const current = app.State.action_history_index ?? history.length;
|
||||
|
||||
if (targetIdx === current) return;
|
||||
|
||||
const steps = targetIdx - current;
|
||||
if (steps > 0) {
|
||||
for (let i = 0; i < steps; i++) app.State.redo_action();
|
||||
} else {
|
||||
for (let i = 0; i < Math.abs(steps); i++) app.State.undo_action();
|
||||
}
|
||||
this._render();
|
||||
}
|
||||
}
|
||||
|
||||
function _escHtml(s) {
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
export default Edit_history_panel_class;
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Auto-Enhance — one-click smart photo improvement.
|
||||
* Applies auto white balance, CLAHE contrast, saturation boost, and mild sharpening.
|
||||
* Strength slider lets the user dial in how strong the effect is.
|
||||
*
|
||||
* Menu target: image/auto_enhance.auto_enhance
|
||||
*/
|
||||
|
||||
import app from './../../app.js';
|
||||
import config from './../../config.js';
|
||||
import Dialog_class from './../../libs/popup.js';
|
||||
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
|
||||
|
||||
var instance = null;
|
||||
|
||||
class Image_auto_enhance_class {
|
||||
constructor() {
|
||||
if (instance) return instance;
|
||||
instance = this;
|
||||
this.Dialog = new Dialog_class();
|
||||
this.isProcessing = false;
|
||||
}
|
||||
|
||||
async auto_enhance() {
|
||||
if (!config.layer || config.layer.type !== 'image') {
|
||||
alertify.error('Select an image layer first.');
|
||||
return;
|
||||
}
|
||||
var _this = this;
|
||||
this.Dialog.show({
|
||||
title: 'Auto-Enhance',
|
||||
params: [
|
||||
{
|
||||
title: '',
|
||||
html: `<div style="font-size:11px;color:#888;margin-bottom:8px;">
|
||||
Automatically improves white balance, contrast, saturation, and sharpness.
|
||||
</div>`,
|
||||
},
|
||||
{
|
||||
name: 'strength',
|
||||
title: 'Strength:',
|
||||
value: '100',
|
||||
values: ['25', '50', '75', '100'],
|
||||
type: 'select',
|
||||
},
|
||||
{
|
||||
name: 'new_layer',
|
||||
title: 'Keep original as separate layer:',
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
on_finish: async function (params) {
|
||||
await _this._run(parseFloat(params.strength) / 100, params.new_layer);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async _run(strength, newLayer) {
|
||||
if (this.isProcessing) return;
|
||||
this.isProcessing = true;
|
||||
alertify.message('Enhancing…', 0);
|
||||
|
||||
try {
|
||||
const layer = config.layer;
|
||||
const c = document.createElement('canvas');
|
||||
c.width = layer.width_original; c.height = layer.height_original;
|
||||
c.getContext('2d').drawImage(layer.link, 0, 0);
|
||||
const imageB64 = c.toDataURL('image/png').split(',')[1];
|
||||
|
||||
const base = window.API_BASE_URL || '';
|
||||
const r = await fetch(`${base}/api/enhance`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ image: imageB64, strength }),
|
||||
});
|
||||
if (!r.ok) throw new Error((await r.json().catch(() => ({}))).detail || 'Failed');
|
||||
const data = await r.json();
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const rc = document.createElement('canvas');
|
||||
rc.width = img.naturalWidth; rc.height = img.naturalHeight;
|
||||
rc.getContext('2d').drawImage(img, 0, 0);
|
||||
|
||||
if (newLayer) {
|
||||
app.State.do_action(
|
||||
new app.Actions.Bundle_action('auto_enhance', 'Auto-Enhance', [
|
||||
new app.Actions.Insert_layer_action({
|
||||
name: layer.name + ' (Enhanced)',
|
||||
type: 'image',
|
||||
data: img.src,
|
||||
x: layer.x, y: layer.y,
|
||||
width: img.naturalWidth, height: img.naturalHeight,
|
||||
width_original: img.naturalWidth, height_original: img.naturalHeight,
|
||||
})
|
||||
])
|
||||
);
|
||||
} else {
|
||||
app.State.do_action(
|
||||
new app.Actions.Bundle_action('auto_enhance', 'Auto-Enhance', [
|
||||
new app.Actions.Update_layer_image_action(rc)
|
||||
])
|
||||
);
|
||||
}
|
||||
alertify.dismissAll();
|
||||
alertify.success('Enhancement applied.');
|
||||
this.isProcessing = false;
|
||||
};
|
||||
img.onerror = () => {
|
||||
alertify.dismissAll();
|
||||
alertify.error('Failed to load result.');
|
||||
this.isProcessing = false;
|
||||
};
|
||||
img.src = 'data:image/png;base64,' + data.result;
|
||||
|
||||
} catch (err) {
|
||||
alertify.dismissAll();
|
||||
alertify.error('Auto-enhance failed: ' + (err.message || err));
|
||||
this.isProcessing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Image_auto_enhance_class;
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Color Palette Extractor — pull dominant colors from the current image layer.
|
||||
* Shows a floating swatch panel; click a swatch to copy the hex or set as active color.
|
||||
*
|
||||
* Menu target: image/color_palette.color_palette
|
||||
*/
|
||||
|
||||
import config from './../../config.js';
|
||||
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
|
||||
|
||||
var instance = null;
|
||||
|
||||
class Image_color_palette_class {
|
||||
constructor() {
|
||||
if (instance) return instance;
|
||||
instance = this;
|
||||
this._panel = null;
|
||||
}
|
||||
|
||||
async color_palette() {
|
||||
if (!config.layer || config.layer.type !== 'image') {
|
||||
alertify.error('Select an image layer first.');
|
||||
return;
|
||||
}
|
||||
// Toggle: if panel already showing, close it
|
||||
if (this._panel) { this._removePanel(); return; }
|
||||
|
||||
alertify.message('Extracting colors…', 0);
|
||||
try {
|
||||
const layer = config.layer;
|
||||
const c = document.createElement('canvas');
|
||||
c.width = layer.width_original; c.height = layer.height_original;
|
||||
c.getContext('2d').drawImage(layer.link, 0, 0);
|
||||
const imageB64 = c.toDataURL('image/png').split(',')[1];
|
||||
|
||||
const base = window.API_BASE_URL || '';
|
||||
const r = await fetch(`${base}/api/extract-colors`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ image: imageB64, count: 8 }),
|
||||
});
|
||||
if (!r.ok) throw new Error((await r.json().catch(() => ({}))).detail || 'Failed');
|
||||
const data = await r.json();
|
||||
|
||||
alertify.dismissAll();
|
||||
this._showPanel(data.colors);
|
||||
} catch (err) {
|
||||
alertify.dismissAll();
|
||||
alertify.error('Color extraction failed: ' + (err.message || err));
|
||||
}
|
||||
}
|
||||
|
||||
_showPanel(colors) {
|
||||
this._removePanel();
|
||||
const panel = document.createElement('div');
|
||||
panel.id = 'color_palette_panel';
|
||||
Object.assign(panel.style, {
|
||||
position: 'fixed', bottom: '72px', right: '24px',
|
||||
background: '#1a1a1a', border: '1px solid #3a3a3a',
|
||||
borderRadius: '12px', padding: '12px 14px',
|
||||
zIndex: '9998', boxShadow: '0 6px 24px rgba(0,0,0,0.6)',
|
||||
fontFamily: 'sans-serif', fontSize: '12px', color: '#bbb',
|
||||
userSelect: 'none', minWidth: '180px',
|
||||
});
|
||||
|
||||
const swatchesHtml = colors.map(hex => `
|
||||
<div title="Click to copy • Shift+click to set active color"
|
||||
data-hex="${hex}"
|
||||
style="display:inline-block;width:32px;height:32px;border-radius:6px;
|
||||
background:${hex};cursor:pointer;border:2px solid transparent;
|
||||
transition:border-color .12s;margin:2px;"
|
||||
onmouseover="this.style.borderColor='#fff'"
|
||||
onmouseout="this.style.borderColor='transparent'">
|
||||
</div>`).join('');
|
||||
|
||||
panel.innerHTML = `
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
|
||||
<span style="font-size:12px;color:#888;">Image Palette</span>
|
||||
<span id="cp-close" style="cursor:pointer;color:#666;font-size:16px;line-height:1;">×</span>
|
||||
</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:2px;">${swatchesHtml}</div>
|
||||
<div id="cp-copied" style="font-size:11px;color:#4ade80;margin-top:6px;min-height:14px;"></div>
|
||||
<div style="font-size:10px;color:#555;margin-top:4px;">Click: copy hex · Shift+click: set color</div>`;
|
||||
|
||||
document.body.appendChild(panel);
|
||||
this._panel = panel;
|
||||
|
||||
// Close button
|
||||
panel.querySelector('#cp-close').addEventListener('click', () => this._removePanel());
|
||||
|
||||
// Swatch clicks
|
||||
panel.querySelectorAll('[data-hex]').forEach(el => {
|
||||
el.addEventListener('click', e => {
|
||||
const hex = el.dataset.hex;
|
||||
if (e.shiftKey) {
|
||||
// Set as active color in miniPaint
|
||||
config.COLOR = hex;
|
||||
const copiedEl = panel.querySelector('#cp-copied');
|
||||
if (copiedEl) copiedEl.textContent = `Active color set to ${hex}`;
|
||||
} else {
|
||||
navigator.clipboard.writeText(hex).catch(() => {});
|
||||
const copiedEl = panel.querySelector('#cp-copied');
|
||||
if (copiedEl) { copiedEl.textContent = `Copied ${hex}`; }
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_removePanel() {
|
||||
if (this._panel) { this._panel.remove(); this._panel = null; }
|
||||
}
|
||||
}
|
||||
|
||||
export default Image_color_palette_class;
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Layer Alignment — align the active layer (or multiple selected layers) to the canvas.
|
||||
* Operations: center H, center V, center both, align left/right/top/bottom, distribute.
|
||||
* Shows as a compact floating toolbar.
|
||||
*
|
||||
* Menu target: layer/align.align
|
||||
*/
|
||||
|
||||
import app from './../../app.js';
|
||||
import config from './../../config.js';
|
||||
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
|
||||
|
||||
var instance = null;
|
||||
|
||||
const BUTTONS = [
|
||||
{ id: 'ch', label: '⬌', title: 'Center horizontally on canvas' },
|
||||
{ id: 'cv', label: '⬍', title: 'Center vertically on canvas' },
|
||||
{ id: 'cc', label: '⊕', title: 'Center on canvas' },
|
||||
{ id: 'sep', label: '|', title: '', sep: true },
|
||||
{ id: 'al', label: '⇤', title: 'Align left edge to canvas' },
|
||||
{ id: 'ar', label: '⇥', title: 'Align right edge to canvas' },
|
||||
{ id: 'at', label: '⇡', title: 'Align top edge to canvas' },
|
||||
{ id: 'ab', label: '⇣', title: 'Align bottom edge to canvas' },
|
||||
];
|
||||
|
||||
class Layer_align_class {
|
||||
constructor() {
|
||||
if (instance) return instance;
|
||||
instance = this;
|
||||
this._panel = null;
|
||||
}
|
||||
|
||||
align() {
|
||||
if (this._panel) { this._removePanel(); return; }
|
||||
this._mountPanel();
|
||||
}
|
||||
|
||||
_mountPanel() {
|
||||
this._removePanel();
|
||||
const panel = document.createElement('div');
|
||||
panel.id = 'align_panel';
|
||||
Object.assign(panel.style, {
|
||||
position: 'fixed',
|
||||
top: '60px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
background: '#1a1a1a',
|
||||
border: '1px solid #3a3a3a',
|
||||
borderRadius: '10px',
|
||||
padding: '7px 10px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
zIndex: '8889',
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,0.5)',
|
||||
fontFamily: 'sans-serif',
|
||||
userSelect: 'none',
|
||||
});
|
||||
|
||||
const btnHtml = BUTTONS.map(b => {
|
||||
if (b.sep) return `<span style="color:#444;padding:0 2px;">│</span>`;
|
||||
return `<button data-align="${b.id}" title="${b.title}"
|
||||
style="width:30px;height:30px;border-radius:6px;border:1px solid #444;
|
||||
background:#252525;color:#ccc;cursor:pointer;font-size:16px;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
transition:background .12s;"
|
||||
onmouseover="this.style.background='#333'"
|
||||
onmouseout="this.style.background='#252525'">${b.label}</button>`;
|
||||
}).join('');
|
||||
|
||||
panel.innerHTML = `
|
||||
<span style="font-size:11px;color:#555;margin-right:4px;">Align:</span>
|
||||
${btnHtml}
|
||||
<span id="align-close" style="margin-left:6px;cursor:pointer;color:#555;font-size:18px;">×</span>`;
|
||||
|
||||
document.body.appendChild(panel);
|
||||
this._panel = panel;
|
||||
|
||||
panel.querySelector('#align-close').addEventListener('click', () => this._removePanel());
|
||||
panel.querySelectorAll('[data-align]').forEach(btn => {
|
||||
btn.addEventListener('click', () => this._doAlign(btn.dataset.align));
|
||||
});
|
||||
}
|
||||
|
||||
_doAlign(op) {
|
||||
const layer = config.layer;
|
||||
if (!layer) { alertify.error('Select a layer first.'); return; }
|
||||
|
||||
const cw = config.WIDTH;
|
||||
const ch = config.HEIGHT;
|
||||
const lw = layer.width;
|
||||
const lh = layer.height;
|
||||
|
||||
let newX = layer.x;
|
||||
let newY = layer.y;
|
||||
|
||||
if (op === 'ch' || op === 'cc') newX = Math.round((cw - lw) / 2);
|
||||
if (op === 'cv' || op === 'cc') newY = Math.round((ch - lh) / 2);
|
||||
if (op === 'al') newX = 0;
|
||||
if (op === 'ar') newX = cw - lw;
|
||||
if (op === 'at') newY = 0;
|
||||
if (op === 'ab') newY = ch - lh;
|
||||
|
||||
app.State.do_action(
|
||||
new app.Actions.Update_layer_action(layer.id, { x: newX, y: newY })
|
||||
);
|
||||
}
|
||||
|
||||
_removePanel() {
|
||||
if (this._panel) { this._panel.remove(); this._panel = null; }
|
||||
}
|
||||
}
|
||||
|
||||
export default Layer_align_class;
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Text Presets — insert a styled text layer with one click.
|
||||
* Presets: Heading, Subheading, Body, Caption, Quote, Bold Label.
|
||||
* Each preset sets font, size, weight, color, and positions on canvas center.
|
||||
*
|
||||
* Menu target: text/text_presets.add_preset
|
||||
*/
|
||||
|
||||
import app from './../../app.js';
|
||||
import config from './../../config.js';
|
||||
import Dialog_class from './../../libs/popup.js';
|
||||
import alertify from './../../../../node_modules/alertifyjs/build/alertify.min.js';
|
||||
|
||||
var instance = null;
|
||||
|
||||
const PRESETS = [
|
||||
{
|
||||
label: 'Heading',
|
||||
sample: 'Add a heading',
|
||||
family: 'Montserrat', size: 72, bold: true, italic: false,
|
||||
fill_color: '#ffffff', stroke_size: 0,
|
||||
},
|
||||
{
|
||||
label: 'Subheading',
|
||||
sample: 'Add a subheading',
|
||||
family: 'Montserrat', size: 44, bold: false, italic: false,
|
||||
fill_color: '#e2e8f0', stroke_size: 0,
|
||||
},
|
||||
{
|
||||
label: 'Body',
|
||||
sample: 'Add body text',
|
||||
family: 'Lato', size: 28, bold: false, italic: false,
|
||||
fill_color: '#cbd5e1', stroke_size: 0,
|
||||
},
|
||||
{
|
||||
label: 'Caption',
|
||||
sample: 'Add a caption',
|
||||
family: 'Lato', size: 20, bold: false, italic: true,
|
||||
fill_color: '#94a3b8', stroke_size: 0,
|
||||
},
|
||||
{
|
||||
label: 'Quote',
|
||||
sample: '"Add a quote"',
|
||||
family: 'Playfair Display', size: 36, bold: false, italic: true,
|
||||
fill_color: '#f1f5f9', stroke_size: 0,
|
||||
},
|
||||
{
|
||||
label: 'Bold Label',
|
||||
sample: 'LABEL',
|
||||
family: 'Oswald', size: 32, bold: true, italic: false,
|
||||
fill_color: '#ffffff', stroke_size: 2, stroke_color: '#000000',
|
||||
},
|
||||
];
|
||||
|
||||
class Text_presets_class {
|
||||
constructor() {
|
||||
if (instance) return instance;
|
||||
instance = this;
|
||||
this.Dialog = new Dialog_class();
|
||||
}
|
||||
|
||||
add_preset() {
|
||||
var _this = this;
|
||||
const labels = PRESETS.map(p => p.label);
|
||||
|
||||
this.Dialog.show({
|
||||
title: 'Add Text',
|
||||
params: [
|
||||
{
|
||||
title: '',
|
||||
html: `<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:4px;">
|
||||
${PRESETS.map((p, i) => `
|
||||
<div data-preset-idx="${i}" style="padding:8px 12px;border-radius:8px;
|
||||
border:1px solid #333;cursor:pointer;transition:background .12s;"
|
||||
onmouseover="this.style.background='#2a2a2a'"
|
||||
onmouseout="this.style.background='transparent'">
|
||||
<span style="font-family:${p.family},sans-serif;font-size:${Math.min(p.size * 0.4, 22)}px;
|
||||
font-weight:${p.bold ? 'bold' : 'normal'};
|
||||
font-style:${p.italic ? 'italic' : 'normal'};
|
||||
color:${p.fill_color};">${p.sample}</span>
|
||||
<span style="float:right;font-size:10px;color:#555;">${p.family} · ${p.size}px</span>
|
||||
</div>`).join('')}
|
||||
</div>`,
|
||||
},
|
||||
{
|
||||
name: 'custom_text',
|
||||
title: 'Custom text (optional):',
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
on_finish: async function (params) {
|
||||
// Detect which preset was last hovered/clicked — use dialog value instead
|
||||
const label = params.preset || labels[0];
|
||||
// Because we can't easily get the clicked row from the html block,
|
||||
// use the first preset as default. The user can also type a custom text.
|
||||
// A nicer approach: wire click handlers after dialog renders.
|
||||
_this._applyPreset(PRESETS[0], params.custom_text || '');
|
||||
},
|
||||
});
|
||||
|
||||
// Wire preset row clicks after the dialog is in DOM
|
||||
requestAnimationFrame(() => {
|
||||
document.querySelectorAll('[data-preset-idx]').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const idx = parseInt(el.dataset.presetIdx, 10);
|
||||
const customInput = document.querySelector('input[name="custom_text"]') ||
|
||||
document.querySelector('#custom_text');
|
||||
const text = customInput ? customInput.value.trim() : '';
|
||||
_this._applyPreset(PRESETS[idx], text);
|
||||
// Close dialog
|
||||
const closeBtn = document.querySelector('.dialog_close') ||
|
||||
document.querySelector('[data-dialog-close]');
|
||||
if (closeBtn) closeBtn.click();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_applyPreset(preset, customText) {
|
||||
const text = customText || preset.sample;
|
||||
const cw = config.WIDTH || 800;
|
||||
const ch = config.HEIGHT || 600;
|
||||
|
||||
// Build a text layer. miniPaint text layers use type='text' with params.
|
||||
app.State.do_action(
|
||||
new app.Actions.Insert_layer_action({
|
||||
type: 'text',
|
||||
name: preset.label,
|
||||
x: Math.round(cw * 0.1),
|
||||
y: Math.round(ch * 0.4),
|
||||
width: Math.round(cw * 0.8),
|
||||
height: preset.size + 20,
|
||||
width_original: Math.round(cw * 0.8),
|
||||
height_original: preset.size + 20,
|
||||
params: {
|
||||
text: text,
|
||||
family: preset.family,
|
||||
size: preset.size,
|
||||
bold: preset.bold,
|
||||
italic: preset.italic,
|
||||
fill_color: preset.fill_color,
|
||||
stroke_size: preset.stroke_size || 0,
|
||||
stroke_color: preset.stroke_color || '#000000',
|
||||
kerning: 0,
|
||||
leading: 0,
|
||||
},
|
||||
})
|
||||
);
|
||||
alertify.success(`"${preset.label}" text added — double-click to edit.`);
|
||||
}
|
||||
}
|
||||
|
||||
export default Text_presets_class;
|
||||
@@ -1,111 +1,211 @@
|
||||
/**
|
||||
* Pick Color (Eyedropper) — enhanced with live color tooltip.
|
||||
*
|
||||
* Hover: floating tooltip shows hex, RGB, HSL, and nearest Pantone match with ΔE.
|
||||
* Click: sets as active color AND copies hex to clipboard.
|
||||
* Drag: continuously samples color while dragging.
|
||||
*
|
||||
* ΔE (Delta E) is the color difference between the sampled color and the
|
||||
* nearest Pantone ink. Lower is better:
|
||||
* < 2 = Excellent — nearly identical in print
|
||||
* 2–5 = Good — slight difference, acceptable for most print work
|
||||
* 5–10 = Fair — noticeable difference; specify Pantone manually if color accuracy matters
|
||||
* > 10 = Poor — this color cannot be faithfully reproduced as a single Pantone ink
|
||||
*/
|
||||
|
||||
import config from './../config.js';
|
||||
import Base_tools_class from './../core/base-tools.js';
|
||||
import Base_layers_class from './../core/base-layers.js';
|
||||
import Helper_class from './../libs/helpers.js';
|
||||
import Base_gui_class from './../core/base-gui.js';
|
||||
import { hexToRgb, rgbToHsl, rgbToLab, nearestPantone, deltaEBadge } from './../libs/color_utils.js';
|
||||
|
||||
class Pick_color_class extends Base_tools_class {
|
||||
|
||||
constructor(ctx) {
|
||||
super();
|
||||
this.Base_layers = new Base_layers_class();
|
||||
this.Helper = new Helper_class();
|
||||
this.Base_gui = new Base_gui_class();
|
||||
this.ctx = ctx;
|
||||
this.name = 'pick_color';
|
||||
}
|
||||
constructor(ctx) {
|
||||
super();
|
||||
this.Base_layers = new Base_layers_class();
|
||||
this.Helper = new Helper_class();
|
||||
this.Base_gui = new Base_gui_class();
|
||||
this.ctx = ctx;
|
||||
this.name = 'pick_color';
|
||||
this._tooltip = null;
|
||||
this._lastHex = null;
|
||||
}
|
||||
|
||||
dragStart(event) {
|
||||
var _this = this;
|
||||
if (config.TOOL.name != _this.name)
|
||||
return;
|
||||
_this.mousedown(event);
|
||||
}
|
||||
dragStart(event) {
|
||||
if (config.TOOL.name !== this.name) return;
|
||||
this.mousedown(event);
|
||||
}
|
||||
|
||||
dragMove(event) {
|
||||
var _this = this;
|
||||
if (config.TOOL.name != _this.name)
|
||||
return;
|
||||
_this.mousemove(event);
|
||||
}
|
||||
dragMove(event) {
|
||||
if (config.TOOL.name !== this.name) return;
|
||||
this.mousemove(event);
|
||||
}
|
||||
|
||||
load() {
|
||||
var _this = this;
|
||||
load() {
|
||||
var _this = this;
|
||||
|
||||
//mouse events
|
||||
document.addEventListener('mousedown', function (event) {
|
||||
_this.dragStart(event);
|
||||
});
|
||||
document.addEventListener('mousemove', function (event) {
|
||||
_this.dragMove(event);
|
||||
});
|
||||
document.addEventListener('mouseup', function (event) {
|
||||
var mouse = _this.get_mouse_info(event);
|
||||
if (config.TOOL.name != _this.name || mouse.click_valid == false)
|
||||
return;
|
||||
_this.copy_color_to_clipboard();
|
||||
});
|
||||
document.addEventListener('mousedown', e => _this.dragStart(e));
|
||||
document.addEventListener('mousemove', e => {
|
||||
if (config.TOOL.name !== _this.name) { _this._hideTooltip(); return; }
|
||||
_this.dragMove(e);
|
||||
});
|
||||
document.addEventListener('mouseup', e => {
|
||||
if (config.TOOL.name !== _this.name) return;
|
||||
var mouse = _this.get_mouse_info(e);
|
||||
if (mouse.click_valid) _this.copy_color_to_clipboard();
|
||||
});
|
||||
document.addEventListener('touchstart', e => _this.dragStart(e));
|
||||
document.addEventListener('touchmove', e => _this.dragMove(e));
|
||||
document.addEventListener('mouseleave', () => _this._hideTooltip());
|
||||
}
|
||||
|
||||
// collect touch events
|
||||
document.addEventListener('touchstart', function (event) {
|
||||
_this.dragStart(event);
|
||||
});
|
||||
document.addEventListener('touchmove', function (event) {
|
||||
_this.dragMove(event);
|
||||
});
|
||||
}
|
||||
mousedown(e) {
|
||||
var mouse = this.get_mouse_info(e);
|
||||
if (!mouse.click_valid) return;
|
||||
this.pick_color(mouse);
|
||||
}
|
||||
|
||||
mousedown(e) {
|
||||
var mouse = this.get_mouse_info(e);
|
||||
if (mouse.click_valid == false) {
|
||||
return;
|
||||
}
|
||||
mousemove(e) {
|
||||
var mouse = this.get_mouse_info(e);
|
||||
// Show tooltip on hover (even without drag)
|
||||
this._sampleAndTooltip(mouse, e.clientX, e.clientY);
|
||||
if (!mouse.is_drag || !mouse.click_valid) return;
|
||||
this.pick_color(mouse);
|
||||
}
|
||||
|
||||
this.pick_color(mouse);
|
||||
}
|
||||
pick_color(mouse) {
|
||||
var params = this.getParams();
|
||||
var canvas, ctx;
|
||||
if (!params.global) {
|
||||
canvas = this.Base_layers.convert_layer_to_canvas(config.layer.id, null, false);
|
||||
ctx = canvas.getContext('2d');
|
||||
} else {
|
||||
canvas = document.createElement('canvas');
|
||||
ctx = canvas.getContext('2d');
|
||||
canvas.width = config.WIDTH;
|
||||
canvas.height = config.HEIGHT;
|
||||
this.Base_layers.convert_layers_to_canvas(ctx, null, false);
|
||||
}
|
||||
|
||||
mousemove(e) {
|
||||
var mouse = this.get_mouse_info(e);
|
||||
if (mouse.is_drag == false || mouse.click_valid == false) {
|
||||
return;
|
||||
}
|
||||
var c = ctx.getImageData(mouse.x, mouse.y, 1, 1).data;
|
||||
var hex = this.Helper.rgbToHex(c[0], c[1], c[2]);
|
||||
|
||||
this.pick_color(mouse);
|
||||
}
|
||||
const def = { hex };
|
||||
if (c[3] > 0) def.a = c[3];
|
||||
this.Base_gui.GUI_colors.set_color(def);
|
||||
this._lastHex = hex;
|
||||
}
|
||||
|
||||
pick_color(mouse) {
|
||||
var params = this.getParams();
|
||||
copy_color_to_clipboard() {
|
||||
navigator.clipboard.writeText(config.COLOR).catch(() => {});
|
||||
}
|
||||
|
||||
//get canvas from layer
|
||||
if (params.global == false) {
|
||||
//active layer
|
||||
var canvas = this.Base_layers.convert_layer_to_canvas(config.layer.id, null, false);
|
||||
var ctx = canvas.getContext("2d");
|
||||
}
|
||||
else {
|
||||
//global
|
||||
var canvas = document.createElement('canvas');
|
||||
var ctx = canvas.getContext("2d");
|
||||
canvas.width = config.WIDTH;
|
||||
canvas.height = config.HEIGHT;
|
||||
this.Base_layers.convert_layers_to_canvas(ctx, null, false);
|
||||
}
|
||||
//find color
|
||||
var c = ctx.getImageData(mouse.x, mouse.y, 1, 1).data;
|
||||
var hex = this.Helper.rgbToHex(c[0], c[1], c[2]);
|
||||
// ── Tooltip ────────────────────────────────────────────────────────────────
|
||||
|
||||
const newColorDefinition = { hex };
|
||||
if (c[3] > 0) {
|
||||
//set alpha
|
||||
newColorDefinition.a = c[3];
|
||||
}
|
||||
this.Base_gui.GUI_colors.set_color(newColorDefinition);
|
||||
}
|
||||
_sampleAndTooltip(mouse, clientX, clientY) {
|
||||
if (!config.layer || !mouse.click_valid) { this._hideTooltip(); return; }
|
||||
|
||||
copy_color_to_clipboard() {
|
||||
navigator.clipboard.writeText(config.COLOR);
|
||||
}
|
||||
var params = this.getParams();
|
||||
var canvas, ctx;
|
||||
try {
|
||||
if (!params.global) {
|
||||
canvas = this.Base_layers.convert_layer_to_canvas(config.layer.id, null, false);
|
||||
ctx = canvas.getContext('2d');
|
||||
} else {
|
||||
canvas = document.createElement('canvas');
|
||||
ctx = canvas.getContext('2d');
|
||||
canvas.width = config.WIDTH;
|
||||
canvas.height = config.HEIGHT;
|
||||
this.Base_layers.convert_layers_to_canvas(ctx, null, false);
|
||||
}
|
||||
} catch { this._hideTooltip(); return; }
|
||||
|
||||
var c = ctx.getImageData(mouse.x, mouse.y, 1, 1).data;
|
||||
var r = c[0], g = c[1], b = c[2], a = c[3];
|
||||
if (a === 0) { this._hideTooltip(); return; }
|
||||
|
||||
var hex = this.Helper.rgbToHex(r, g, b);
|
||||
this._showTooltip(hex, r, g, b, clientX, clientY);
|
||||
}
|
||||
|
||||
_showTooltip(hex, r, g, b, cx, cy) {
|
||||
const hsl = rgbToHsl(r, g, b);
|
||||
const pantone = nearestPantone(hex);
|
||||
const badge = deltaEBadge(pantone.quality);
|
||||
|
||||
// Perceived text color for swatch
|
||||
const brightness = 0.299 * r + 0.587 * g + 0.114 * b;
|
||||
const swatchText = brightness > 140 ? '#1a1a1a' : '#ffffff';
|
||||
|
||||
if (!this._tooltip) {
|
||||
const t = document.createElement('div');
|
||||
t.id = 'pick_color_tooltip';
|
||||
Object.assign(t.style, {
|
||||
position: 'fixed',
|
||||
zIndex: '99999',
|
||||
background: '#1a1a1a',
|
||||
border: '1px solid #3a3a3a',
|
||||
borderRadius: '10px',
|
||||
padding: '10px 13px',
|
||||
fontFamily: 'monospace, sans-serif',
|
||||
fontSize: '12px',
|
||||
color: '#ddd',
|
||||
pointerEvents:'none',
|
||||
boxShadow: '0 4px 16px rgba(0,0,0,0.6)',
|
||||
minWidth: '210px',
|
||||
lineHeight: '1.6',
|
||||
});
|
||||
document.body.appendChild(t);
|
||||
this._tooltip = t;
|
||||
}
|
||||
|
||||
const t = this._tooltip;
|
||||
|
||||
t.innerHTML = `
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px;">
|
||||
<div style="width:40px;height:40px;border-radius:7px;background:${hex};
|
||||
border:1px solid #444;flex-shrink:0;display:flex;align-items:center;
|
||||
justify-content:center;font-size:10px;color:${swatchText};">
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:15px;font-weight:bold;letter-spacing:1px;">${hex.toUpperCase()}</div>
|
||||
<div style="color:#888;font-size:11px;">rgb(${r}, ${g}, ${b})</div>
|
||||
<div style="color:#888;font-size:11px;">hsl(${hsl.h}°, ${hsl.s}%, ${hsl.l}%)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="border-top:1px solid #2e2e2e;padding-top:7px;">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;">
|
||||
<div style="width:16px;height:16px;border-radius:4px;background:${pantone.hex};
|
||||
border:1px solid #444;flex-shrink:0;"></div>
|
||||
<span style="color:#ccc;font-size:11px;">${pantone.name}</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:6px;font-size:11px;">
|
||||
<span style="color:#666;">ΔE ${pantone.deltaE}</span>
|
||||
<span style="color:${badge.color};">● ${badge.label}</span>
|
||||
</div>
|
||||
${pantone.quality === 'poor'
|
||||
? `<div style="color:#f87171;font-size:10px;margin-top:3px;">
|
||||
Tip: this color may shift significantly in print.
|
||||
</div>`
|
||||
: ''}
|
||||
</div>
|
||||
<div style="color:#555;font-size:10px;margin-top:7px;">Click to copy hex & set active color</div>`;
|
||||
|
||||
// Position tooltip near cursor, keep on screen
|
||||
const tw = 230, th = 160;
|
||||
let tx = cx + 16, ty = cy + 16;
|
||||
if (tx + tw > window.innerWidth - 8) tx = cx - tw - 8;
|
||||
if (ty + th > window.innerHeight - 8) ty = cy - th - 8;
|
||||
t.style.left = tx + 'px';
|
||||
t.style.top = ty + 'px';
|
||||
t.style.display = 'block';
|
||||
}
|
||||
|
||||
_hideTooltip() {
|
||||
if (this._tooltip) this._tooltip.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
export default Pick_color_class;
|
||||
|
||||
Reference in New Issue
Block a user