File size: 8,639 Bytes
c6535db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
import {app} from "@/composable/comfyAPI.js";

const is_all_same_aspect_ratio = imgs => {
    if (!imgs.length || imgs.length === 1) return true

    const ratio = imgs[0].naturalWidth / imgs[0].naturalHeight

    for (let i = 1; i < imgs.length; i++) {
        const this_ratio = imgs[i].naturalWidth / imgs[i].naturalHeight
        if (ratio != this_ratio) return false
    }
    return true
}
export function calculateImageGrid(imgs, dw, dh) {
    let best = 0
    let w = imgs[0]?.naturalWidth
    let h = imgs[0]?.naturalHeight
    const numImages = imgs.length

    let cellWidth, cellHeight, cols, rows, shiftX
    // compact style
    for (let c = 1; c <= numImages; c++) {
        const r = Math.ceil(numImages / c)
        const cW = dw / c
        const cH = dh / r
        const scaleX = cW / w
        const scaleY = cH / h

        const scale = Math.min(scaleX, scaleY, 1)
        const imageW = w * scale
        const imageH = h * scale
        const area = imageW * imageH * numImages

        if (area > best) {
            best = area
            cellWidth = imageW
            cellHeight = imageH
            cols = c
            rows = r
            shiftX = c * ((cW - imageW) / 2)
        }
    }

    return { cellWidth, cellHeight, cols, rows, shiftX }
}

export const renderPreview = (
    ctx,
    node,
    shiftY
) => {
    const canvas = app.canvas
    const mouse = canvas.graph_mouse

    if (!canvas.pointer_is_down && node.pointerDown) {
        if (
            mouse[0] === node.pointerDown.pos[0] &&
            mouse[1] === node.pointerDown.pos[1]
        ) {
            node.imageIndex = node.pointerDown.index
        }
        node.pointerDown = null
    }

    const imgs = node.imgs ?? []
    let { imageIndex } = node
    const numImages = imgs.length
    if (numImages === 1 && !imageIndex) {
        // This skips the thumbnail render section below
        node.imageIndex = imageIndex = 0
    }

    const IMAGE_TEXT_SIZE_TEXT_HEIGHT = 15
    const dw = node.size[0]
    const dh = node.size[1] - shiftY - IMAGE_TEXT_SIZE_TEXT_HEIGHT

    if (imageIndex == null) {
        // No image selected; draw thumbnails of all
        let cellWidth
        let cellHeight
        let shiftX
        let cell_padding
        let cols

        const compact_mode = is_all_same_aspect_ratio(imgs)
        if (!compact_mode) {
            // use rectangle cell style and border line
            cell_padding = 2
            // Prevent infinite canvas2d scale-up
            const largestDimension = imgs.reduce(
                (acc, current) =>
                    Math.max(acc, current.naturalWidth, current.naturalHeight),
                0
            )
            const fakeImgs = []
            fakeImgs.length = imgs.length
            fakeImgs[0] = {
                naturalWidth: largestDimension,
                naturalHeight: largestDimension
            }
            ;({ cellWidth, cellHeight, cols, shiftX } = calculateImageGrid(
                fakeImgs,
                dw,
                dh
            ))
        } else {
            cell_padding = 0
            ;({ cellWidth, cellHeight, cols, shiftX } = calculateImageGrid(
                imgs,
                dw,
                dh
            ))
        }

        let anyHovered = false
        node.imageRects = []
        for (let i = 0; i < numImages; i++) {
            const img = imgs[i]
            const row = Math.floor(i / cols)
            const col = i % cols
            const x = col * cellWidth + shiftX
            const y = row * cellHeight + shiftY
            if (!anyHovered) {
                anyHovered = LiteGraph.isInsideRectangle(
                    mouse[0],
                    mouse[1],
                    x + node.pos[0],
                    y + node.pos[1],
                    cellWidth,
                    cellHeight
                )
                if (anyHovered) {
                    node.overIndex = i
                    let value = 110
                    if (canvas.pointer_is_down) {
                        if (!node.pointerDown || node.pointerDown.index !== i) {
                            node.pointerDown = { index: i, pos: [...mouse] }
                        }
                        value = 125
                    }
                    ctx.filter = `contrast(${value}%) brightness(${value}%)`
                    canvas.canvas.style.cursor = 'pointer'
                }
            }
            node.imageRects.push([x, y, cellWidth, cellHeight])

            const wratio = cellWidth / img.width
            const hratio = cellHeight / img.height
            const ratio = Math.min(wratio, hratio)

            const imgHeight = ratio * img.height
            const imgY = row * cellHeight + shiftY + (cellHeight - imgHeight) / 2
            const imgWidth = ratio * img.width
            const imgX = col * cellWidth + shiftX + (cellWidth - imgWidth) / 2

            ctx.drawImage(
                img,
                imgX + cell_padding,
                imgY + cell_padding,
                imgWidth - cell_padding * 2,
                imgHeight - cell_padding * 2
            )
            if (!compact_mode) {
                // rectangle cell and border line style
                ctx.strokeStyle = '#8F8F8F'
                ctx.lineWidth = 1
                ctx.strokeRect(
                    x + cell_padding,
                    y + cell_padding,
                    cellWidth - cell_padding * 2,
                    cellHeight - cell_padding * 2
                )
            }

            ctx.filter = 'none'
        }

        if (!anyHovered) {
            node.pointerDown = null
            node.overIndex = null
        }

        return
    }


    // Draw individual
    const img = imgs[imageIndex]
    if(!img) return
    let w = img?.naturalWidth
    let h = img?.naturalHeight

    const scaleX = dw / w
    const scaleY = dh / h
    const scale = Math.min(scaleX, scaleY, 1)

    w *= scale
    h *= scale

    const x = (dw - w) / 2
    const y = (dh - h) / 2 + shiftY
    ctx.drawImage(img, x, y, w, h)

    // Draw image size text below the image
    ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR
    ctx.textAlign = 'center'
    ctx.font = '10px sans-serif'
    const sizeText = `${Math.round(img.naturalWidth)} × ${Math.round(img.naturalHeight)}`
    const textY = y + h + 10
    ctx.fillText(sizeText, x + w / 2, textY)

    const drawButton = (
            x,
        y,
        sz,
        text
    )  => {
        const hovered = LiteGraph.isInsideRectangle(
            mouse[0],
            mouse[1],
            x + node.pos[0],
            y + node.pos[1],
            sz,
            sz
        )
        let fill = '#333'
        let textFill = '#fff'
        let isClicking = false
        if (hovered) {
            canvas.canvas.style.cursor = 'pointer'
            if (canvas.pointer_is_down) {
                fill = '#1e90ff'
                isClicking = true
            } else {
                fill = '#eee'
                textFill = '#000'
            }
        }

        ctx.fillStyle = fill
        ctx.beginPath()
        ctx.roundRect(x, y, sz, sz, [4])
        ctx.fill()
        ctx.fillStyle = textFill
        ctx.font = '12px Arial'
        ctx.textAlign = 'center'
        ctx.fillText(text, x + 15, y + 20)

        return isClicking
    }

    if (!(numImages > 1)) return

    const imageNum = (node.imageIndex ?? 0) + 1
    if (drawButton(dw - 40, dh + shiftY - 40, 30, `${imageNum}/${numImages}`)) {
        const i = imageNum >= numImages ? 0 : imageNum
        if (!node.pointerDown || node.pointerDown.index !== i) {
            node.pointerDown = { index: i, pos: [...mouse] }
        }
    }

    if (drawButton(dw - 40, shiftY + 10, 30, `x`)) {
        if (!node.pointerDown || node.pointerDown.index !== null) {
            node.pointerDown = { index: null, pos: [...mouse] }
        }
    }
}

class ImagePreviewWidget {

    constructor(name, options) {
        this.type = 'custom'
        this.name = name
        this.options = options
        this.value = ''
    }

    draw(
        ctx,
        node,
        widget_width,
        y,
        H
    ) {
        renderPreview(ctx, node, y)
    }

    computeLayoutSize(_, node) {
        return {minHeight: 220,minWidth: 1}
    }
}


export const useImagePreviewWidget = () => {
    const widgetConstructor = (
        node,
        inputSpec
    ) => {
        return node.addCustomWidget(
            new ImagePreviewWidget(inputSpec.name, {
                serialize: false
            })
        )
    }
    return widgetConstructor
}