@spark-apps/piclet 1.0.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -21
- package/{Readme.md → README.md} +31 -6
- package/dist/cli.js +3027 -1191
- package/dist/cli.js.map +1 -1
- package/dist/gui/border.html +229 -0
- package/dist/gui/css/theme.css +88 -88
- package/dist/gui/extract-frames.html +156 -0
- package/dist/gui/filter.html +180 -0
- package/dist/gui/iconpack.html +113 -113
- package/dist/gui/js/piclet.js +10 -1
- package/dist/gui/loading.hta +8 -17
- package/dist/gui/piclet.html +1529 -998
- package/dist/gui/recolor.html +243 -0
- package/dist/gui/remove-bg.html +178 -178
- package/dist/gui/storepack.html +179 -179
- package/dist/gui/transform.html +202 -0
- package/package.json +2 -2
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-piclet data-width="400" data-height="480">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>Recolor</title>
|
|
7
|
+
<link rel="stylesheet" href="/css/theme.css">
|
|
8
|
+
<script src="/js/piclet.js"></script>
|
|
9
|
+
<style>
|
|
10
|
+
.preview-area{flex:1;display:flex;align-items:center;justify-content:center;background:var(--bg2);border-radius:6px;min-height:140px;overflow:hidden;position:relative}
|
|
11
|
+
.preview-area::before{content:'';position:absolute;inset:0;background:repeating-conic-gradient(#1a1a1d 0% 25%, #222 0% 50%) 50%/16px 16px;z-index:0}
|
|
12
|
+
.preview-area img{max-width:100%;max-height:100%;object-fit:contain;position:relative;z-index:1}
|
|
13
|
+
.preview-area .placeholder{position:relative;z-index:1;font-size:11px;color:var(--txt3)}
|
|
14
|
+
.preview-area .mini-sp{width:16px;height:16px;border:2px solid var(--bg3);border-top-color:var(--acc);border-radius:50%;animation:s .5s linear infinite;position:relative;z-index:1}
|
|
15
|
+
.preview-info{font-size:10px;color:var(--txt3);text-align:center;margin-top:4px;height:14px}
|
|
16
|
+
.controls{display:flex;flex-direction:column;gap:10px}
|
|
17
|
+
.color-section{display:flex;align-items:center;gap:10px}
|
|
18
|
+
.color-section label{font-size:11px;color:var(--txt3);width:50px;flex-shrink:0}
|
|
19
|
+
.color-input-wrap{flex:1;display:flex;align-items:center;gap:8px}
|
|
20
|
+
.color-preview{width:28px;height:28px;border-radius:4px;border:1px solid var(--brd);cursor:pointer;flex-shrink:0;position:relative}
|
|
21
|
+
.color-preview input[type="color"]{position:absolute;opacity:0;inset:0;width:100%;height:100%;cursor:pointer}
|
|
22
|
+
.color-input-wrap input[type="text"]{flex:1;padding:6px 10px;background:var(--bg2);border:1px solid var(--brd);border-radius:5px;color:var(--txt);font:inherit;font-size:12px}
|
|
23
|
+
.color-input-wrap input[type="text"]:focus{outline:none;border-color:var(--acc)}
|
|
24
|
+
.arrow{font-size:16px;color:var(--txt3)}
|
|
25
|
+
.fuzz-row{display:flex;align-items:center;gap:10px}
|
|
26
|
+
.fuzz-row label{font-size:11px;color:var(--txt3);width:50px;flex-shrink:0}
|
|
27
|
+
.fuzz-row input[type="number"]{width:50px;padding:6px;background:var(--bg2);border:1px solid var(--brd);border-radius:5px;color:var(--txt);font:inherit;font-size:12px;text-align:center}
|
|
28
|
+
.fuzz-row input[type="number"]:focus{outline:none;border-color:var(--acc)}
|
|
29
|
+
.fuzz-row input[type="range"]{flex:1;height:4px;background:var(--bg3);border-radius:3px;-webkit-appearance:none}
|
|
30
|
+
.fuzz-row input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;background:var(--acc);border-radius:50%;cursor:pointer}
|
|
31
|
+
.detected-hint{font-size:10px;color:var(--txt3);margin-top:-6px;padding-left:60px}
|
|
32
|
+
</style>
|
|
33
|
+
</head>
|
|
34
|
+
<body>
|
|
35
|
+
<div class="app">
|
|
36
|
+
<div class="hd"><b>PicLet</b><span>Recolor</span></div>
|
|
37
|
+
<div class="meta">
|
|
38
|
+
<div>File<b id="fn">-</b></div>
|
|
39
|
+
<div>Size<b id="sz">-</b></div>
|
|
40
|
+
</div>
|
|
41
|
+
<!-- Form -->
|
|
42
|
+
<div id="F" class="form">
|
|
43
|
+
<div class="preview-area" id="pA">
|
|
44
|
+
<span class="placeholder">Adjust to preview</span>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="preview-info" id="pI"></div>
|
|
47
|
+
<div class="controls">
|
|
48
|
+
<div class="color-section">
|
|
49
|
+
<label>From</label>
|
|
50
|
+
<div class="color-input-wrap">
|
|
51
|
+
<div class="color-preview" id="fP" style="background:#ffffff">
|
|
52
|
+
<input type="color" id="fC" value="#ffffff">
|
|
53
|
+
</div>
|
|
54
|
+
<input type="text" id="fT" value="#ffffff" placeholder="#ffffff">
|
|
55
|
+
</div>
|
|
56
|
+
<span class="arrow">→</span>
|
|
57
|
+
<div class="color-input-wrap">
|
|
58
|
+
<div class="color-preview" id="tP" style="background:#000000">
|
|
59
|
+
<input type="color" id="tC" value="#000000">
|
|
60
|
+
</div>
|
|
61
|
+
<input type="text" id="tT" value="#000000" placeholder="#000000">
|
|
62
|
+
</div>
|
|
63
|
+
<label style="text-align:right">To</label>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="detected-hint" id="dH"></div>
|
|
66
|
+
<div class="fuzz-row" data-tip="Color matching tolerance - higher matches more similar colors">
|
|
67
|
+
<label>Tolerance</label>
|
|
68
|
+
<input type="number" id="fN" min="0" max="100" value="10">
|
|
69
|
+
<input type="range" id="fR" min="0" max="100" value="10">
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="btns">
|
|
73
|
+
<button class="btn btn-g" onclick="PicLet.close()">Cancel</button>
|
|
74
|
+
<button class="btn btn-p" onclick="apply()">Apply</button>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
<!-- Loading state -->
|
|
78
|
+
<div class="ld" id="L"><div class="sp"></div><span id="lT">Recoloring...</span></div>
|
|
79
|
+
<!-- Log -->
|
|
80
|
+
<div class="log" id="G"></div>
|
|
81
|
+
<!-- Done state -->
|
|
82
|
+
<div class="dn" id="D">
|
|
83
|
+
<h4 id="dT"></h4>
|
|
84
|
+
<p id="dM"></p>
|
|
85
|
+
<div class="btns" style="width:100%;margin-top:8px">
|
|
86
|
+
<button class="btn btn-p" onclick="PicLet.close()">Done</button>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
<script>
|
|
91
|
+
const { $, log, fetchJson, postJson } = PicLet;
|
|
92
|
+
const pA = $('pA'), pI = $('pI');
|
|
93
|
+
const fC = $('fC'), fT = $('fT'), fP = $('fP');
|
|
94
|
+
const tC = $('tC'), tT = $('tT'), tP = $('tP');
|
|
95
|
+
const fN = $('fN'), fR = $('fR');
|
|
96
|
+
|
|
97
|
+
let previewTimeout = null;
|
|
98
|
+
let isSliding = false;
|
|
99
|
+
let lastPreviewOpts = null;
|
|
100
|
+
|
|
101
|
+
// From color
|
|
102
|
+
fC.oninput = () => {
|
|
103
|
+
fT.value = fC.value;
|
|
104
|
+
fP.style.background = fC.value;
|
|
105
|
+
schedulePreview();
|
|
106
|
+
};
|
|
107
|
+
fT.oninput = () => {
|
|
108
|
+
if (/^#[0-9a-fA-F]{6}$/.test(fT.value)) {
|
|
109
|
+
fC.value = fT.value;
|
|
110
|
+
fP.style.background = fT.value;
|
|
111
|
+
schedulePreview();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
fT.onblur = schedulePreview;
|
|
115
|
+
|
|
116
|
+
// To color
|
|
117
|
+
tC.oninput = () => {
|
|
118
|
+
tT.value = tC.value;
|
|
119
|
+
tP.style.background = tC.value;
|
|
120
|
+
schedulePreview();
|
|
121
|
+
};
|
|
122
|
+
tT.oninput = () => {
|
|
123
|
+
if (/^#[0-9a-fA-F]{6}$/.test(tT.value)) {
|
|
124
|
+
tC.value = tT.value;
|
|
125
|
+
tP.style.background = tT.value;
|
|
126
|
+
schedulePreview();
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
tT.onblur = schedulePreview;
|
|
130
|
+
|
|
131
|
+
// Fuzz
|
|
132
|
+
fN.oninput = () => { fR.value = fN.value; schedulePreview(); };
|
|
133
|
+
fR.oninput = () => { fN.value = fR.value; };
|
|
134
|
+
fR.addEventListener('mousedown', () => { isSliding = true; });
|
|
135
|
+
fR.addEventListener('mouseup', () => { isSliding = false; schedulePreview(); });
|
|
136
|
+
fR.addEventListener('mouseleave', () => { if (isSliding) { isSliding = false; schedulePreview(); } });
|
|
137
|
+
|
|
138
|
+
// Get current options
|
|
139
|
+
function getOptions() {
|
|
140
|
+
return {
|
|
141
|
+
fromColor: fT.value || '#ffffff',
|
|
142
|
+
toColor: tT.value || '#000000',
|
|
143
|
+
fuzz: +fN.value || 10
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check if options changed
|
|
148
|
+
function optionsChanged() {
|
|
149
|
+
const current = JSON.stringify(getOptions());
|
|
150
|
+
if (current === lastPreviewOpts) return false;
|
|
151
|
+
lastPreviewOpts = current;
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Schedule preview
|
|
156
|
+
function schedulePreview() {
|
|
157
|
+
if (isSliding) return;
|
|
158
|
+
if (previewTimeout) clearTimeout(previewTimeout);
|
|
159
|
+
previewTimeout = setTimeout(() => {
|
|
160
|
+
if (!isSliding && optionsChanged()) {
|
|
161
|
+
generatePreview();
|
|
162
|
+
}
|
|
163
|
+
}, 300);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Generate preview
|
|
167
|
+
async function generatePreview() {
|
|
168
|
+
pA.innerHTML = '<div class="mini-sp"></div>';
|
|
169
|
+
pI.textContent = '';
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const result = await postJson('/api/preview', getOptions());
|
|
173
|
+
|
|
174
|
+
if (result.success && result.imageData) {
|
|
175
|
+
const img = document.createElement('img');
|
|
176
|
+
img.src = result.imageData;
|
|
177
|
+
img.alt = 'Preview';
|
|
178
|
+
pA.innerHTML = '';
|
|
179
|
+
pA.appendChild(img);
|
|
180
|
+
pI.textContent = result.width && result.height ? `${result.width}×${result.height}` : '';
|
|
181
|
+
} else {
|
|
182
|
+
pA.innerHTML = '<span class="placeholder">' + (result.error || 'Preview failed') + '</span>';
|
|
183
|
+
pI.textContent = '';
|
|
184
|
+
}
|
|
185
|
+
} catch (e) {
|
|
186
|
+
pA.innerHTML = '<span class="placeholder">Preview error</span>';
|
|
187
|
+
pI.textContent = '';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Load initial data
|
|
192
|
+
fetchJson('/api/info').then(d => {
|
|
193
|
+
$('fn').textContent = d.fileName;
|
|
194
|
+
$('sz').textContent = d.width + '×' + d.height;
|
|
195
|
+
|
|
196
|
+
if (d.borderColor) {
|
|
197
|
+
$('dH').textContent = 'Detected corner: ' + d.borderColor;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (d.defaults) {
|
|
201
|
+
const from = d.defaults.fromColor || '#ffffff';
|
|
202
|
+
fC.value = from.startsWith('#') ? from : '#ffffff';
|
|
203
|
+
fT.value = from;
|
|
204
|
+
fP.style.background = from;
|
|
205
|
+
|
|
206
|
+
const to = d.defaults.toColor || '#000000';
|
|
207
|
+
tC.value = to.startsWith('#') ? to : '#000000';
|
|
208
|
+
tT.value = to;
|
|
209
|
+
tP.style.background = to;
|
|
210
|
+
|
|
211
|
+
fN.value = fR.value = d.defaults.fuzz || 10;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
setTimeout(generatePreview, 100);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Apply
|
|
218
|
+
async function apply() {
|
|
219
|
+
$('F').classList.add('hide');
|
|
220
|
+
$('L').classList.add('on');
|
|
221
|
+
$('G').classList.add('on');
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const result = await postJson('/api/process', getOptions());
|
|
225
|
+
if (result.logs) {
|
|
226
|
+
result.logs.forEach(l => log('G', l.type[0], l.message));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
$('L').classList.remove('on');
|
|
230
|
+
$('D').classList.add('on', result.success ? 'ok' : 'err');
|
|
231
|
+
$('dT').textContent = result.success ? 'Done' : 'Failed';
|
|
232
|
+
$('dM').textContent = result.success ? result.output : result.error;
|
|
233
|
+
} catch (e) {
|
|
234
|
+
log('G', 'e', e.message);
|
|
235
|
+
$('L').classList.remove('on');
|
|
236
|
+
$('D').classList.add('on', 'err');
|
|
237
|
+
$('dT').textContent = 'Error';
|
|
238
|
+
$('dM').textContent = e.message;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
</script>
|
|
242
|
+
</body>
|
|
243
|
+
</html>
|
package/dist/gui/remove-bg.html
CHANGED
|
@@ -1,178 +1,178 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en" data-piclet data-width="400" data-height="420">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
-
<title>Remove BG</title>
|
|
7
|
-
<link rel="stylesheet" href="/css/theme.css">
|
|
8
|
-
<script src="/js/piclet.js"></script>
|
|
9
|
-
<style>
|
|
10
|
-
.preview-area{flex:1;display:flex;align-items:center;justify-content:center;background:var(--bg2);border-radius:6px;min-height:140px;overflow:hidden;position:relative}
|
|
11
|
-
.preview-area::before{content:'';position:absolute;inset:0;background:repeating-conic-gradient(#1a1a1d 0% 25%, #222 0% 50%) 50%/16px 16px;z-index:0}
|
|
12
|
-
.preview-area img{max-width:100%;max-height:100%;object-fit:contain;position:relative;z-index:1}
|
|
13
|
-
.preview-area .placeholder{position:relative;z-index:1;font-size:11px;color:var(--txt3)}
|
|
14
|
-
.preview-area .mini-sp{width:16px;height:16px;border:2px solid var(--bg3);border-top-color:var(--acc);border-radius:50%;animation:s .5s linear infinite;position:relative;z-index:1}
|
|
15
|
-
.preview-info{font-size:10px;color:var(--txt3);text-align:center;margin-top:4px;height:14px}
|
|
16
|
-
</style>
|
|
17
|
-
</head>
|
|
18
|
-
<body>
|
|
19
|
-
<div class="app">
|
|
20
|
-
<div class="hd"><b>PicLet</b><span>Remove Background</span></div>
|
|
21
|
-
<div class="meta">
|
|
22
|
-
<div>File<b id="fn">-</b></div>
|
|
23
|
-
<div>Size<b id="sz">-</b></div>
|
|
24
|
-
</div>
|
|
25
|
-
<!-- Form with inline preview -->
|
|
26
|
-
<div id="F" class="form">
|
|
27
|
-
<div class="preview-area" id="pA">
|
|
28
|
-
<span class="placeholder">Adjust fuzz to preview</span>
|
|
29
|
-
</div>
|
|
30
|
-
<div class="preview-info" id="pI"></div>
|
|
31
|
-
<div class="row" data-tip="Color matching sensitivity (higher = more colors removed)">
|
|
32
|
-
<label>Tolerance</label>
|
|
33
|
-
<input type="number" id="fN" min="0" max="100" value="10">
|
|
34
|
-
<input type="range" id="fR" min="0" max="100" value="10">
|
|
35
|
-
</div>
|
|
36
|
-
<div class="opts">
|
|
37
|
-
<label class="opt" data-tip="Remove empty edges after background removal"><input type="checkbox" id="cT" checked><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Auto-trim</label>
|
|
38
|
-
<label class="opt" data-tip="Only remove background from edges, keep inner areas"><input type="checkbox" id="cP"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Edges only</label>
|
|
39
|
-
<label class="opt" data-tip="Add transparent padding to make output square"><input type="checkbox" id="cS"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Make square</label>
|
|
40
|
-
</div>
|
|
41
|
-
<div class="btns">
|
|
42
|
-
<button class="btn btn-g" onclick="PicLet.close()">Cancel</button>
|
|
43
|
-
<button class="btn btn-p" id="applyBtn" onclick="apply()">Apply</button>
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
<!-- Loading state -->
|
|
47
|
-
<div class="ld" id="L"><div class="sp"></div><span id="lT">Processing...</span></div>
|
|
48
|
-
<!-- Log -->
|
|
49
|
-
<div class="log" id="G"></div>
|
|
50
|
-
<!-- Done state -->
|
|
51
|
-
<div class="dn" id="D">
|
|
52
|
-
<h4 id="dT"></h4>
|
|
53
|
-
<p id="dM"></p>
|
|
54
|
-
<div class="btns" style="width:100%;margin-top:8px">
|
|
55
|
-
<button class="btn btn-p" onclick="PicLet.close()">Done</button>
|
|
56
|
-
</div>
|
|
57
|
-
</div>
|
|
58
|
-
</div>
|
|
59
|
-
<script>
|
|
60
|
-
const { $, log, fetchJson, postJson } = PicLet;
|
|
61
|
-
const fN = $('fN'), fR = $('fR'), pA = $('pA'), pI = $('pI');
|
|
62
|
-
|
|
63
|
-
let previewTimeout = null;
|
|
64
|
-
let isSliding = false;
|
|
65
|
-
let lastPreviewOpts = null;
|
|
66
|
-
|
|
67
|
-
// Sync slider and input
|
|
68
|
-
fN.oninput = () => { fR.value = fN.value; schedulePreview(); };
|
|
69
|
-
fR.oninput = () => { fN.value = fR.value; };
|
|
70
|
-
|
|
71
|
-
// Track mouse state on slider
|
|
72
|
-
fR.addEventListener('mousedown', () => { isSliding = true; });
|
|
73
|
-
fR.addEventListener('mouseup', () => { isSliding = false; schedulePreview(); });
|
|
74
|
-
fR.addEventListener('mouseleave', () => { if (isSliding) { isSliding = false; schedulePreview(); } });
|
|
75
|
-
|
|
76
|
-
// Checkbox changes trigger preview
|
|
77
|
-
$('cT').onchange = schedulePreview;
|
|
78
|
-
$('cP').onchange = schedulePreview;
|
|
79
|
-
$('cS').onchange = schedulePreview;
|
|
80
|
-
|
|
81
|
-
// Get current options
|
|
82
|
-
function getOptions() {
|
|
83
|
-
return {
|
|
84
|
-
fuzz: +fN.value || 10,
|
|
85
|
-
trim: $('cT').checked,
|
|
86
|
-
preserveInner: $('cP').checked,
|
|
87
|
-
makeSquare: $('cS').checked
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Check if options changed
|
|
92
|
-
function optionsChanged() {
|
|
93
|
-
const current = JSON.stringify(getOptions());
|
|
94
|
-
if (current === lastPreviewOpts) return false;
|
|
95
|
-
lastPreviewOpts = current;
|
|
96
|
-
return true;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Schedule preview (debounced, only when not sliding)
|
|
100
|
-
function schedulePreview() {
|
|
101
|
-
if (isSliding) return;
|
|
102
|
-
if (previewTimeout) clearTimeout(previewTimeout);
|
|
103
|
-
previewTimeout = setTimeout(() => {
|
|
104
|
-
if (!isSliding && optionsChanged()) {
|
|
105
|
-
generatePreview();
|
|
106
|
-
}
|
|
107
|
-
}, 300);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Generate preview
|
|
111
|
-
async function generatePreview() {
|
|
112
|
-
// Show loading spinner
|
|
113
|
-
pA.innerHTML = '<div class="mini-sp"></div>';
|
|
114
|
-
pI.textContent = '';
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
const result = await postJson('/api/preview', getOptions());
|
|
118
|
-
|
|
119
|
-
if (result.success && result.imageData) {
|
|
120
|
-
const img = document.createElement('img');
|
|
121
|
-
img.src = result.imageData;
|
|
122
|
-
img.alt = 'Preview';
|
|
123
|
-
pA.innerHTML = '';
|
|
124
|
-
pA.appendChild(img);
|
|
125
|
-
pI.textContent = result.width && result.height ? `${result.width}×${result.height}` : '';
|
|
126
|
-
} else {
|
|
127
|
-
pA.innerHTML = '<span class="placeholder">' + (result.error || 'Preview failed') + '</span>';
|
|
128
|
-
pI.textContent = '';
|
|
129
|
-
}
|
|
130
|
-
} catch (e) {
|
|
131
|
-
pA.innerHTML = '<span class="placeholder">Preview error</span>';
|
|
132
|
-
pI.textContent = '';
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Load initial data
|
|
137
|
-
fetchJson('/api/info').then(d => {
|
|
138
|
-
$('fn').textContent = d.fileName;
|
|
139
|
-
$('sz').textContent = d.width + '×' + d.height;
|
|
140
|
-
if (d.defaults) {
|
|
141
|
-
fN.value = fR.value = d.defaults.fuzz;
|
|
142
|
-
$('cT').checked = d.defaults.trim;
|
|
143
|
-
$('cP').checked = d.defaults.preserveInner;
|
|
144
|
-
$('cS').checked = d.defaults.makeSquare;
|
|
145
|
-
}
|
|
146
|
-
// Generate initial preview
|
|
147
|
-
setTimeout(generatePreview, 100);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// Apply (process for real)
|
|
151
|
-
async function apply() {
|
|
152
|
-
$('F').classList.add('hide');
|
|
153
|
-
$('L').classList.add('on');
|
|
154
|
-
$('lT').textContent = 'Processing...';
|
|
155
|
-
$('G').classList.add('on');
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
const result = await postJson('/api/process', getOptions());
|
|
159
|
-
|
|
160
|
-
if (result.logs) {
|
|
161
|
-
result.logs.forEach(l => log('G', l.type[0], l.message));
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
$('L').classList.remove('on');
|
|
165
|
-
$('D').classList.add('on', result.success ? 'ok' : 'err');
|
|
166
|
-
$('dT').textContent = result.success ? 'Done' : 'Failed';
|
|
167
|
-
$('dM').textContent = result.success ? result.output : result.error;
|
|
168
|
-
} catch (e) {
|
|
169
|
-
log('G', 'e', e.message);
|
|
170
|
-
$('L').classList.remove('on');
|
|
171
|
-
$('D').classList.add('on', 'err');
|
|
172
|
-
$('dT').textContent = 'Error';
|
|
173
|
-
$('dM').textContent = e.message;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
</script>
|
|
177
|
-
</body>
|
|
178
|
-
</html>
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-piclet data-width="400" data-height="420">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>Remove BG</title>
|
|
7
|
+
<link rel="stylesheet" href="/css/theme.css">
|
|
8
|
+
<script src="/js/piclet.js"></script>
|
|
9
|
+
<style>
|
|
10
|
+
.preview-area{flex:1;display:flex;align-items:center;justify-content:center;background:var(--bg2);border-radius:6px;min-height:140px;overflow:hidden;position:relative}
|
|
11
|
+
.preview-area::before{content:'';position:absolute;inset:0;background:repeating-conic-gradient(#1a1a1d 0% 25%, #222 0% 50%) 50%/16px 16px;z-index:0}
|
|
12
|
+
.preview-area img{max-width:100%;max-height:100%;object-fit:contain;position:relative;z-index:1}
|
|
13
|
+
.preview-area .placeholder{position:relative;z-index:1;font-size:11px;color:var(--txt3)}
|
|
14
|
+
.preview-area .mini-sp{width:16px;height:16px;border:2px solid var(--bg3);border-top-color:var(--acc);border-radius:50%;animation:s .5s linear infinite;position:relative;z-index:1}
|
|
15
|
+
.preview-info{font-size:10px;color:var(--txt3);text-align:center;margin-top:4px;height:14px}
|
|
16
|
+
</style>
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
<div class="app">
|
|
20
|
+
<div class="hd"><b>PicLet</b><span>Remove Background</span></div>
|
|
21
|
+
<div class="meta">
|
|
22
|
+
<div>File<b id="fn">-</b></div>
|
|
23
|
+
<div>Size<b id="sz">-</b></div>
|
|
24
|
+
</div>
|
|
25
|
+
<!-- Form with inline preview -->
|
|
26
|
+
<div id="F" class="form">
|
|
27
|
+
<div class="preview-area" id="pA">
|
|
28
|
+
<span class="placeholder">Adjust fuzz to preview</span>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="preview-info" id="pI"></div>
|
|
31
|
+
<div class="row" data-tip="Color matching sensitivity (higher = more colors removed)">
|
|
32
|
+
<label>Tolerance</label>
|
|
33
|
+
<input type="number" id="fN" min="0" max="100" value="10">
|
|
34
|
+
<input type="range" id="fR" min="0" max="100" value="10">
|
|
35
|
+
</div>
|
|
36
|
+
<div class="opts">
|
|
37
|
+
<label class="opt" data-tip="Remove empty edges after background removal"><input type="checkbox" id="cT" checked><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Auto-trim</label>
|
|
38
|
+
<label class="opt" data-tip="Only remove background from edges, keep inner areas"><input type="checkbox" id="cP"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Edges only</label>
|
|
39
|
+
<label class="opt" data-tip="Add transparent padding to make output square"><input type="checkbox" id="cS"><span class="box"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,3"/></svg></span>Make square</label>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="btns">
|
|
42
|
+
<button class="btn btn-g" onclick="PicLet.close()">Cancel</button>
|
|
43
|
+
<button class="btn btn-p" id="applyBtn" onclick="apply()">Apply</button>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<!-- Loading state -->
|
|
47
|
+
<div class="ld" id="L"><div class="sp"></div><span id="lT">Processing...</span></div>
|
|
48
|
+
<!-- Log -->
|
|
49
|
+
<div class="log" id="G"></div>
|
|
50
|
+
<!-- Done state -->
|
|
51
|
+
<div class="dn" id="D">
|
|
52
|
+
<h4 id="dT"></h4>
|
|
53
|
+
<p id="dM"></p>
|
|
54
|
+
<div class="btns" style="width:100%;margin-top:8px">
|
|
55
|
+
<button class="btn btn-p" onclick="PicLet.close()">Done</button>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<script>
|
|
60
|
+
const { $, log, fetchJson, postJson } = PicLet;
|
|
61
|
+
const fN = $('fN'), fR = $('fR'), pA = $('pA'), pI = $('pI');
|
|
62
|
+
|
|
63
|
+
let previewTimeout = null;
|
|
64
|
+
let isSliding = false;
|
|
65
|
+
let lastPreviewOpts = null;
|
|
66
|
+
|
|
67
|
+
// Sync slider and input
|
|
68
|
+
fN.oninput = () => { fR.value = fN.value; schedulePreview(); };
|
|
69
|
+
fR.oninput = () => { fN.value = fR.value; };
|
|
70
|
+
|
|
71
|
+
// Track mouse state on slider
|
|
72
|
+
fR.addEventListener('mousedown', () => { isSliding = true; });
|
|
73
|
+
fR.addEventListener('mouseup', () => { isSliding = false; schedulePreview(); });
|
|
74
|
+
fR.addEventListener('mouseleave', () => { if (isSliding) { isSliding = false; schedulePreview(); } });
|
|
75
|
+
|
|
76
|
+
// Checkbox changes trigger preview
|
|
77
|
+
$('cT').onchange = schedulePreview;
|
|
78
|
+
$('cP').onchange = schedulePreview;
|
|
79
|
+
$('cS').onchange = schedulePreview;
|
|
80
|
+
|
|
81
|
+
// Get current options
|
|
82
|
+
function getOptions() {
|
|
83
|
+
return {
|
|
84
|
+
fuzz: +fN.value || 10,
|
|
85
|
+
trim: $('cT').checked,
|
|
86
|
+
preserveInner: $('cP').checked,
|
|
87
|
+
makeSquare: $('cS').checked
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check if options changed
|
|
92
|
+
function optionsChanged() {
|
|
93
|
+
const current = JSON.stringify(getOptions());
|
|
94
|
+
if (current === lastPreviewOpts) return false;
|
|
95
|
+
lastPreviewOpts = current;
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Schedule preview (debounced, only when not sliding)
|
|
100
|
+
function schedulePreview() {
|
|
101
|
+
if (isSliding) return;
|
|
102
|
+
if (previewTimeout) clearTimeout(previewTimeout);
|
|
103
|
+
previewTimeout = setTimeout(() => {
|
|
104
|
+
if (!isSliding && optionsChanged()) {
|
|
105
|
+
generatePreview();
|
|
106
|
+
}
|
|
107
|
+
}, 300);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Generate preview
|
|
111
|
+
async function generatePreview() {
|
|
112
|
+
// Show loading spinner
|
|
113
|
+
pA.innerHTML = '<div class="mini-sp"></div>';
|
|
114
|
+
pI.textContent = '';
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const result = await postJson('/api/preview', getOptions());
|
|
118
|
+
|
|
119
|
+
if (result.success && result.imageData) {
|
|
120
|
+
const img = document.createElement('img');
|
|
121
|
+
img.src = result.imageData;
|
|
122
|
+
img.alt = 'Preview';
|
|
123
|
+
pA.innerHTML = '';
|
|
124
|
+
pA.appendChild(img);
|
|
125
|
+
pI.textContent = result.width && result.height ? `${result.width}×${result.height}` : '';
|
|
126
|
+
} else {
|
|
127
|
+
pA.innerHTML = '<span class="placeholder">' + (result.error || 'Preview failed') + '</span>';
|
|
128
|
+
pI.textContent = '';
|
|
129
|
+
}
|
|
130
|
+
} catch (e) {
|
|
131
|
+
pA.innerHTML = '<span class="placeholder">Preview error</span>';
|
|
132
|
+
pI.textContent = '';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Load initial data
|
|
137
|
+
fetchJson('/api/info').then(d => {
|
|
138
|
+
$('fn').textContent = d.fileName;
|
|
139
|
+
$('sz').textContent = d.width + '×' + d.height;
|
|
140
|
+
if (d.defaults) {
|
|
141
|
+
fN.value = fR.value = d.defaults.fuzz;
|
|
142
|
+
$('cT').checked = d.defaults.trim;
|
|
143
|
+
$('cP').checked = d.defaults.preserveInner;
|
|
144
|
+
$('cS').checked = d.defaults.makeSquare;
|
|
145
|
+
}
|
|
146
|
+
// Generate initial preview
|
|
147
|
+
setTimeout(generatePreview, 100);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Apply (process for real)
|
|
151
|
+
async function apply() {
|
|
152
|
+
$('F').classList.add('hide');
|
|
153
|
+
$('L').classList.add('on');
|
|
154
|
+
$('lT').textContent = 'Processing...';
|
|
155
|
+
$('G').classList.add('on');
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const result = await postJson('/api/process', getOptions());
|
|
159
|
+
|
|
160
|
+
if (result.logs) {
|
|
161
|
+
result.logs.forEach(l => log('G', l.type[0], l.message));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
$('L').classList.remove('on');
|
|
165
|
+
$('D').classList.add('on', result.success ? 'ok' : 'err');
|
|
166
|
+
$('dT').textContent = result.success ? 'Done' : 'Failed';
|
|
167
|
+
$('dM').textContent = result.success ? result.output : result.error;
|
|
168
|
+
} catch (e) {
|
|
169
|
+
log('G', 'e', e.message);
|
|
170
|
+
$('L').classList.remove('on');
|
|
171
|
+
$('D').classList.add('on', 'err');
|
|
172
|
+
$('dT').textContent = 'Error';
|
|
173
|
+
$('dM').textContent = e.message;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
</script>
|
|
177
|
+
</body>
|
|
178
|
+
</html>
|