File size: 5,533 Bytes
774fe36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/* ===== Character Reference (角色参考) Module ===== */
window.NAIReference = {
    image: null,  // {dataUrl, b64Processed} — b64Processed is after resize_and_pad

    getInfoExtract() {
        const el = $('#reference-info-extract');
        return el ? parseFloat(el.value) : 1;
    },

    getStrength() {
        const el = $('#reference-strength');
        return el ? parseFloat(el.value) : 1;
    },

    getStyleMode() {
        const el = $('#reference-style-select');
        return el ? el.value : 'character&style';
    },

    async handleUpload(file) {
        if (!file || !file.type.startsWith('image/')) return;
        const dataUrl = await NAIUtils.fileToDataUrl(file);
        const processed = await this.resizeAndPad(dataUrl);
        this.image = { dataUrl, b64Processed: processed };
        this.renderPreview();
    },

    useLastGenerated() {
        const imgEl = document.querySelector('#result-image-area img');
        if (!imgEl || !imgEl.src) return;
        const dataUrl = imgEl.src;
        // Process async
        this.resizeAndPad(dataUrl).then(processed => {
            this.image = { dataUrl, b64Processed: processed };
            this.renderPreview();
        });
    },

    clear() {
        this.image = null;
        this.renderPreview();
    },

    renderPreview() {
        const previewArea = $('#reference-preview');
        const slidersArea = $('#reference-sliders');
        const clearBtn = $('#btn-reference-clear');

        if (!this.image) {
            previewArea.innerHTML = '';
            previewArea.style.display = 'none';
            slidersArea.innerHTML = '';
            clearBtn.style.display = 'none';
            return;
        }

        clearBtn.style.display = '';
        previewArea.style.display = 'block';
        previewArea.innerHTML = `
            <div class="reference-preview-container">
                <img src="${this.image.dataUrl}" class="reference-preview-img" />
            </div>`;

        slidersArea.innerHTML = '';
        slidersArea.appendChild(NAIUtils.createDynSlider('参考度', 'reference-info-extract', 0, 1, 0.05, 1));
        slidersArea.appendChild(NAIUtils.createDynSlider('强度', 'reference-strength', 0, 1, 0.05, 1));
    },

    /**
     * Resize and pad image to nearest target size (1024x1536, 1472x1472, 1536x1024),
     * centered on black background. Returns base64 PNG string (no prefix).
     */
    resizeAndPad(dataUrl) {
        return new Promise(async (resolve) => {
            const img = await NAIUtils.loadImageFromDataUrl(dataUrl);
            const ow = img.naturalWidth;
            const oh = img.naturalHeight;
            const ratio = ow / oh;

            // Find closest target size by aspect ratio
            const targets = [[1024, 1536], [1472, 1472], [1536, 1024]];
            let best = targets[0];
            let minDiff = Infinity;
            for (const [tw, th] of targets) {
                const diff = Math.abs(ratio - tw / th);
                if (diff < minDiff) { minDiff = diff; best = [tw, th]; }
            }

            const [tw, th] = best;

            // Scale to fit
            const scale = Math.min(tw / ow, th / oh);
            const nw = Math.round(ow * scale);
            const nh = Math.round(oh * scale);

            // Draw on canvas
            const canvas = document.createElement('canvas');
            canvas.width = tw;
            canvas.height = th;
            const ctx = canvas.getContext('2d');

            // Black background
            ctx.fillStyle = '#000000';
            ctx.fillRect(0, 0, tw, th);

            // Centered image
            const xOff = Math.floor((tw - nw) / 2);
            const yOff = Math.floor((th - nh) / 2);
            ctx.drawImage(img, xOff, yOff, nw, nh);

            // Export as PNG base64
            const result = canvas.toDataURL('image/png');
            resolve(NAIUtils.dataUrlToBase64(result));
        });
    },

    collectParams() {
        if (!this.image) return null;
        return {
            image: this.image.b64Processed,
            info_extract: this.getInfoExtract(),
            strength: this.getStrength(),
            style_mode: this.getStyleMode(),
        };
    },

    init() {
        // Toggle
        $('#reference-toggle').addEventListener('click', () => {
            const c = $('#reference-content');
            const a = $('#reference-arrow');
            const open = c.classList.toggle('open');
            a.textContent = open ? '▼' : '▶';
        });

        // Upload
        const input = $('#reference-input-file');
        const drop = $('#reference-dropzone');
        drop.addEventListener('click', () => input.click());
        drop.addEventListener('dragover', e => { e.preventDefault(); drop.classList.add('dragover'); });
        drop.addEventListener('dragleave', () => drop.classList.remove('dragover'));
        drop.addEventListener('drop', e => {
            e.preventDefault(); drop.classList.remove('dragover');
            if (e.dataTransfer.files.length > 0) this.handleUpload(e.dataTransfer.files[0]);
        });
        input.addEventListener('change', () => {
            if (input.files.length > 0) this.handleUpload(input.files[0]);
            input.value = '';
        });

        // Use last generated
        $('#btn-reference-use-last').addEventListener('click', () => this.useLastGenerated());

        // Clear
        $('#btn-reference-clear').addEventListener('click', () => this.clear());
    },
};