diff --git a/media/tools/dark/unblender.svg b/media/tools/dark/unblender.svg
new file mode 100644
index 0000000..0059ee0
--- /dev/null
+++ b/media/tools/dark/unblender.svg
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/media/tools/light/unblender.svg b/media/tools/light/unblender.svg
new file mode 100644
index 0000000..8580cff
--- /dev/null
+++ b/media/tools/light/unblender.svg
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/constants/tools.ts b/src/constants/tools.ts
index 2ab64fe..c072cf8 100644
--- a/src/constants/tools.ts
+++ b/src/constants/tools.ts
@@ -110,6 +110,16 @@ export const TOOLS: Tool[] = [
icon: 'blend',
tags: ['color', 'mix', 'blend', 'combine', 'design', 'rgb', 'hsl', 'shade', 'tint', 'tone', 'saturation', 'hue']
},
+ {
+ id: 'blend-unmixer',
+ label: 'Blend Unmixer',
+ version: '1.0.0',
+ category: ToolCategory.Design,
+ template: ' ',
+ icon: 'unblender',
+ isNew: true,
+ tags: ['color', 'blend', 'unmix', 'transparent', 'foreground', 'background', 'alpha', 'rgba', 'design', 'ui', 'transparency', 'color-analysis']
+ },
{
id: 'uuid-generator',
label: 'UUID Generator',
diff --git a/src/tools/components/blend-unmixer/BlendUnmixer.ts b/src/tools/components/blend-unmixer/BlendUnmixer.ts
new file mode 100644
index 0000000..66ea38a
--- /dev/null
+++ b/src/tools/components/blend-unmixer/BlendUnmixer.ts
@@ -0,0 +1,542 @@
+import { html, css } from 'lit';
+import { customElement, state } from 'lit/decorators.js';
+import { BaseTool } from '../../base/BaseTool';
+
+interface ColorResult {
+ rgba: string;
+ hex: string;
+ alpha: number;
+ rgb: [number, number, number];
+}
+
+@customElement('blend-unmixer')
+export class BlendUnmixer extends BaseTool {
+ @state() private bg1 = '#323232';
+ @state() private mix1 = '#3D3559';
+ @state() private bg2 = '#1e1e1e';
+ @state() private mix2 = '#2D2549';
+ // @state() private bg1 = '#ff7b00';
+ // @state() private mix1 = '#adbd00';
+ // @state() private bg2 = '#1e1e1e';
+ // @state() private mix2 = '#3c8e0f';
+ @state() private useSecondPair = false;
+ @state() private results: ColorResult[] = [];
+ @state() private message = '';
+ @state() private showCopiedMessage = '';
+
+ private styles = css`
+ ${BaseTool.styles}
+
+ .input-group {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-bottom: 12px;
+ }
+
+ .color-input-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 10px;
+ }
+
+ .input-label {
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--vscode-foreground);
+ opacity: 0.8;
+ }
+
+ .color-field {
+ min-width: 0;
+ }
+
+ .color-field-title {
+ margin: 0 0 6px;
+ font-size: 11px;
+ opacity: 0.9;
+ user-select: none;
+ }
+
+ .color-field-picker {
+ width: 100%;
+ height: 28px;
+ }
+
+ .color-field-value {
+ margin: 6px 0 0;
+ font-size: 11px;
+ opacity: 0.7;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+ }
+
+ .results-container {
+ margin-top: 16px;
+ padding-top: 12px;
+ border-top: 1px solid var(--vscode-panel-border);
+ }
+
+ .result-item {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ margin-bottom: 8px;
+ padding: 8px;
+ border-radius: 2px;
+ background-color: var(--vscode-list-hoverBackground);
+ cursor: pointer;
+ transition: all 0.2s ease;
+ }
+
+ .result-item:hover {
+ background-color: var(--vscode-list-activeSelectionBackground);
+ }
+
+ .result-preview {
+ width: 40px;
+ height: 40px;
+ border-radius: 2px;
+ border: 1px solid var(--vscode-panel-border);
+ background-image:
+ linear-gradient(45deg, #ccc 25%, transparent 25%),
+ linear-gradient(-45deg, #ccc 25%, transparent 25%),
+ linear-gradient(45deg, transparent 75%, #ccc 75%),
+ linear-gradient(-45deg, transparent 75%, #ccc 75%);
+ background-size: 8px 8px;
+ background-position: 0 0, 0 4px, 4px -4px, -4px 0;
+ flex-shrink: 0;
+ position: relative;
+ overflow: hidden;
+ }
+
+ .result-preview::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background-color: var(--preview-color);
+ }
+
+ .result-info {
+ flex: 1;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+ font-size: 12px;
+ }
+
+ .result-rgba {
+ color: var(--vscode-foreground);
+ display: flex;
+ justify-content: space-between;
+ gap: 8px;
+ flex-wrap: wrap;
+ }
+
+ .result-hex {
+ color: var(--vscode-foreground);
+ opacity: 0.7;
+ font-size: 11px;
+ margin-top: 2px;
+ }
+
+ .message {
+ padding: 8px;
+ border-radius: 2px;
+ font-size: 12px;
+ margin-bottom: 12px;
+ }
+
+ .message.success {
+ background-color: rgba(76, 175, 80, 0.15);
+ color: #76c776;
+ }
+
+ .message.warning {
+ background-color: rgba(255, 193, 7, 0.15);
+ color: #ffb84d;
+ }
+
+ .message.error {
+ background-color: rgba(244, 67, 54, 0.15);
+ color: #f48771;
+ }
+
+ .checkbox-group {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin: 12px 0;
+ }
+
+ .checkbox-label {
+ font-size: 13px;
+ cursor: pointer;
+ user-select: none;
+ }
+
+ .button-group {
+ display: flex;
+ gap: 8px;
+ margin-top: 12px;
+ }
+
+ .btn {
+ flex: 1;
+ padding: 6px 12px;
+ border: 1px solid var(--vscode-button-border, transparent);
+ border-radius: 2px;
+ background-color: var(--vscode-button-background);
+ color: var(--vscode-button-foreground);
+ cursor: pointer;
+ font-size: 12px;
+ transition: all 0.2s ease;
+ }
+
+ .btn:hover:not(:disabled) {
+ background-color: var(--vscode-button-hoverBackground);
+ }
+
+ .btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ .btn-secondary {
+ background-color: var(--vscode-button-secondaryBackground);
+ color: var(--vscode-button-secondaryForeground);
+ }
+
+ .btn-secondary:hover:not(:disabled) {
+ background-color: var(--vscode-button-secondaryHoverBackground);
+ }
+ `;
+
+ connectedCallback() {
+ super.connectedCallback();
+ }
+
+ protected renderTool() {
+ return html`
+
+
+ `;
+ }
+
+ private renderResultItem(result: ColorResult, index: number) {
+ return html`
+ this.copyResult(result)}"
+ title="Click to copy RGBA value"
+ >
+
+
+
+ ${result.rgba}
+ ${this.showCopiedMessage === `${index}` ? html`
+ Copied!
+ ` : ''}
+
+
${result.hex}
+
+
+ `;
+ }
+
+ private renderColorInput(
+ title: string,
+ value: string,
+ key: 'bg1' | 'mix1' | 'bg2' | 'mix2'
+ ) {
+ return html`
+
+
${title}
+
this.handleColorChange(e, key)}"
+ >
+
${this.normalizeHex(value) || value.toUpperCase()}
+
+ `;
+ }
+
+ private handleColorChange(e: CustomEvent, key: 'bg1' | 'mix1' | 'bg2' | 'mix2') {
+ const value = e.detail.value as string;
+ this[key] = value;
+ }
+
+ private getMessageClass(): string {
+ if (this.message.includes('✔️') || this.message.includes('Found')) {
+ return 'success';
+ }
+ if (this.message.includes('❌') || this.message.includes('No')) {
+ return 'error';
+ }
+ return 'warning';
+ }
+
+ private calculate() {
+ try {
+ this.results = [];
+ this.message = '';
+
+ const bg1Hex = this.normalizeHex(this.bg1);
+ const mix1Hex = this.normalizeHex(this.mix1);
+
+ if (!bg1Hex || !mix1Hex) {
+ this.message = '❌ Invalid color format. Please use valid hex colors.';
+ return;
+ }
+
+ let bg2Hex = null;
+ let mix2Hex = null;
+
+ if (this.useSecondPair) {
+ bg2Hex = this.normalizeHex(this.bg2);
+ mix2Hex = this.normalizeHex(this.mix2);
+
+ if (!bg2Hex || !mix2Hex) {
+ this.message = '❌ Invalid color format in second pair. Please use valid hex colors.';
+ return;
+ }
+ }
+
+ const bg1 = this.hexToRgb(bg1Hex);
+ const mix1 = this.hexToRgb(mix1Hex);
+ const bg2 = bg2Hex ? this.hexToRgb(bg2Hex) : null;
+ const mix2 = mix2Hex ? this.hexToRgb(mix2Hex) : null;
+
+ // First try rounded alpha values
+ const possibleAlphas = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
+ let results: ColorResult[] = [];
+
+ for (const alpha of possibleAlphas) {
+ const fg1 = this.findForegroundColor(bg1, mix1, alpha);
+ if (fg1) {
+ if (bg2 && mix2) {
+ if (!this.colorsMatch(fg1, bg2, mix2, alpha)) {
+ continue;
+ }
+ }
+ results.push({
+ rgba: `rgba(${fg1[0]}, ${fg1[1]}, ${fg1[2]}, ${alpha})`,
+ hex: this.rgbaToHex(fg1[0], fg1[1], fg1[2], alpha),
+ alpha,
+ rgb: fg1
+ });
+ }
+ }
+
+ if (results.length > 0) {
+ this.message = '✔️ Found rounded transparency colors:';
+ this.results = results.sort((a, b) => a.alpha - b.alpha);
+ } else {
+ this.message = '❌ No rounded transparency values found. Trying all possible colors...';
+
+ // Try all possible RGB values
+ results = [];
+ for (let r = 0; r <= 255; r++) {
+ for (let g = 0; g <= 255; g++) {
+ for (let b = 0; b <= 255; b++) {
+ try {
+ const denom1 = r - bg1[0];
+ if (denom1 === 0) continue;
+
+ const alpha1 = (mix1[0] - bg1[0]) / denom1;
+ if (!(0 < alpha1 && alpha1 < 1)) continue;
+
+ if (!this.colorsMatch([r, g, b], bg1, mix1, alpha1)) {
+ continue;
+ }
+
+ if (bg2 && mix2) {
+ if (!this.colorsMatch([r, g, b], bg2, mix2, alpha1)) {
+ continue;
+ }
+ }
+
+ results.push({
+ rgba: `rgba(${r}, ${g}, ${b}, ${alpha1.toFixed(3)})`,
+ hex: this.rgbaToHex(r, g, b, alpha1),
+ alpha: alpha1,
+ rgb: [r, g, b]
+ });
+ } catch (e) {
+ continue;
+ }
+ }
+ }
+ }
+
+ if (results.length > 0) {
+ this.message = `✔️ Found ${results.length} valid colors (non-rounded alpha):`;
+ results.sort((a, b) => a.alpha - b.alpha);
+ this.results = results.slice(0, 10); // Limit to first 10 results
+ if (results.length > 10) {
+ this.message += ` (showing first 10 of ${results.length})`;
+ }
+ } else {
+ this.message = '🚫 No solution found. Make sure the colors are consistent.';
+ }
+ }
+ } catch (error) {
+ this.message = `❌ Error: ${(error as Error).message}`;
+ this.results = [];
+ }
+ }
+
+ private findForegroundColor(
+ bg: [number, number, number],
+ mix: [number, number, number],
+ alpha: number
+ ): [number, number, number] | null {
+ const fg: number[] = [];
+
+ for (let i = 0; i < 3; i++) {
+ const cb = bg[i];
+ const cm = mix[i];
+
+ if (alpha === 0) {
+ fg.push(cb);
+ } else {
+ const cf = (cm - (1 - alpha) * cb) / alpha;
+
+ // Check if value is within valid range and close to integer
+ if (!(0 <= cf && cf <= 255)) {
+ return null;
+ }
+
+ if (Math.abs(Math.round(cf) - cf) > 1e-6) {
+ return null;
+ }
+
+ fg.push(Math.round(cf));
+ }
+ }
+
+ return [fg[0], fg[1], fg[2]] as [number, number, number];
+ }
+
+ private colorsMatch(
+ fg: [number, number, number],
+ bg: [number, number, number],
+ mix: [number, number, number],
+ alpha: number
+ ): boolean {
+ for (let i = 0; i < 3; i++) {
+ const blended = Math.round(alpha * fg[i] + (1 - alpha) * bg[i]);
+ if (blended !== mix[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private normalizeHex(hex: string): string {
+ hex = hex.trim().toUpperCase();
+ if (hex.startsWith('#')) {
+ hex = hex.substring(1);
+ }
+
+ // Validate hex format
+ if (!/^[0-9A-F]{6}$/.test(hex)) {
+ return '';
+ }
+
+ return '#' + hex;
+ }
+
+ private hexToRgb(hex: string): [number, number, number] {
+ const cleanHex = hex.replace('#', '');
+ return [
+ parseInt(cleanHex.substring(0, 2), 16),
+ parseInt(cleanHex.substring(2, 4), 16),
+ parseInt(cleanHex.substring(4, 6), 16)
+ ];
+ }
+
+ private rgbaToHex(r: number, g: number, b: number, a: number): string {
+ const hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}${(Math.round(a * 255)).toString(16).padStart(2, '0')}`;
+ return hex.toUpperCase();
+ }
+
+ private async copyResult(result: ColorResult) {
+ try {
+ await navigator.clipboard.writeText(result.rgba);
+ const index = this.results.indexOf(result);
+ this.showCopiedMessage = `${index}`;
+ setTimeout(() => {
+ this.showCopiedMessage = '';
+ }, 2000);
+ } catch (err) {
+ console.error('Failed to copy text:', err);
+ }
+ }
+}
diff --git a/src/tools/components/blend-unmixer/LICENSE.md b/src/tools/components/blend-unmixer/LICENSE.md
new file mode 100644
index 0000000..e747f95
--- /dev/null
+++ b/src/tools/components/blend-unmixer/LICENSE.md
@@ -0,0 +1,23 @@
+# MIT License
+
+Copyright (c) 2025 GG
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+Original project: [Github - Blend-Unmixer](https://github.com/nsbgg/blend-unmixer)
diff --git a/src/tools/toolComponents.ts b/src/tools/toolComponents.ts
index 667da06..af56efe 100644
--- a/src/tools/toolComponents.ts
+++ b/src/tools/toolComponents.ts
@@ -17,6 +17,7 @@ import './components/ascii-encoder/AsciiEncoder';
import './components/base64-encoder/Base64Encoder';
import './components/color-convertor/ColorConvertor';
import './components/color-mixer/ColorMixer';
+import './components/blend-unmixer/BlendUnmixer';
import './components/color-palette/ColorPalette';
import './components/contrast-checker/ContrastChecker';
import './components/cubic-bezier/CubicBezier';