1 | /* |
2 | nanogui/imageview.cpp -- Widget used to display images. |
3 | |
4 | The image view widget was contributed by Stefan Ivanov. |
5 | |
6 | NanoGUI was developed by Wenzel Jakob <wenzel.jakob@epfl.ch>. |
7 | The widget drawing code is based on the NanoVG demo application |
8 | by Mikko Mononen. |
9 | |
10 | All rights reserved. Use of this source code is governed by a |
11 | BSD-style license that can be found in the LICENSE.txt file. |
12 | */ |
13 | |
14 | #include <nanogui/imageview.h> |
15 | #include <nanogui/window.h> |
16 | #include <nanogui/screen.h> |
17 | #include <nanogui/theme.h> |
18 | #include <cmath> |
19 | |
20 | NAMESPACE_BEGIN(nanogui) |
21 | |
22 | namespace { |
23 | std::vector<std::string> tokenize(const std::string &string, |
24 | const std::string &delim = "\n" , |
25 | bool includeEmpty = false) { |
26 | std::string::size_type lastPos = 0, pos = string.find_first_of(delim, lastPos); |
27 | std::vector<std::string> tokens; |
28 | |
29 | while (lastPos != std::string::npos) { |
30 | std::string substr = string.substr(lastPos, pos - lastPos); |
31 | if (!substr.empty() || includeEmpty) |
32 | tokens.push_back(std::move(substr)); |
33 | lastPos = pos; |
34 | if (lastPos != std::string::npos) { |
35 | lastPos += 1; |
36 | pos = string.find_first_of(delim, lastPos); |
37 | } |
38 | } |
39 | |
40 | return tokens; |
41 | } |
42 | |
43 | constexpr char const *const defaultImageViewVertexShader = |
44 | R"(#version 330 |
45 | uniform vec2 scaleFactor; |
46 | uniform vec2 position; |
47 | in vec2 vertex; |
48 | out vec2 uv; |
49 | void main() { |
50 | uv = vertex; |
51 | vec2 scaledVertex = (vertex * scaleFactor) + position; |
52 | gl_Position = vec4(2.0*scaledVertex.x - 1.0, |
53 | 1.0 - 2.0*scaledVertex.y, |
54 | 0.0, 1.0); |
55 | |
56 | })" ; |
57 | |
58 | constexpr char const *const defaultImageViewFragmentShader = |
59 | R"(#version 330 |
60 | uniform sampler2D image; |
61 | out vec4 color; |
62 | in vec2 uv; |
63 | void main() { |
64 | color = texture(image, uv); |
65 | })" ; |
66 | |
67 | } |
68 | |
69 | ImageView::ImageView(Widget* parent, GLuint imageID) |
70 | : Widget(parent), mImageID(imageID), mScale(1.0f), mOffset(Vector2f::Zero()), |
71 | mFixedScale(false), mFixedOffset(false), mPixelInfoCallback(nullptr) { |
72 | updateImageParameters(); |
73 | mShader.init("ImageViewShader" , defaultImageViewVertexShader, |
74 | defaultImageViewFragmentShader); |
75 | |
76 | MatrixXu indices(3, 2); |
77 | indices.col(0) << 0, 1, 2; |
78 | indices.col(1) << 2, 3, 1; |
79 | |
80 | MatrixXf vertices(2, 4); |
81 | vertices.col(0) << 0, 0; |
82 | vertices.col(1) << 1, 0; |
83 | vertices.col(2) << 0, 1; |
84 | vertices.col(3) << 1, 1; |
85 | |
86 | mShader.bind(); |
87 | mShader.uploadIndices(indices); |
88 | mShader.uploadAttrib("vertex" , vertices); |
89 | } |
90 | |
91 | ImageView::~ImageView() { |
92 | mShader.free(); |
93 | } |
94 | |
95 | void ImageView::bindImage(GLuint imageId) { |
96 | mImageID = imageId; |
97 | updateImageParameters(); |
98 | fit(); |
99 | } |
100 | |
101 | Vector2f ImageView::imageCoordinateAt(const Vector2f& position) const { |
102 | auto imagePosition = position - mOffset; |
103 | return imagePosition / mScale; |
104 | } |
105 | |
106 | Vector2f ImageView::clampedImageCoordinateAt(const Vector2f& position) const { |
107 | auto imageCoordinate = imageCoordinateAt(position); |
108 | return imageCoordinate.cwiseMax(Vector2f::Zero()).cwiseMin(imageSizeF()); |
109 | } |
110 | |
111 | Vector2f ImageView::positionForCoordinate(const Vector2f& imageCoordinate) const { |
112 | return mScale*imageCoordinate + mOffset; |
113 | } |
114 | |
115 | void ImageView::setImageCoordinateAt(const Vector2f& position, const Vector2f& imageCoordinate) { |
116 | // Calculate where the new offset must be in order to satisfy the image position equation. |
117 | // Round the floating point values to balance out the floating point to integer conversions. |
118 | mOffset = position - (imageCoordinate * mScale); |
119 | |
120 | // Clamp offset so that the image remains near the screen. |
121 | mOffset = mOffset.cwiseMin(sizeF()).cwiseMax(-scaledImageSizeF()); |
122 | } |
123 | |
124 | void ImageView::center() { |
125 | mOffset = (sizeF() - scaledImageSizeF()) / 2; |
126 | } |
127 | |
128 | void ImageView::fit() { |
129 | // Calculate the appropriate scaling factor. |
130 | mScale = (sizeF().cwiseQuotient(imageSizeF())).minCoeff(); |
131 | center(); |
132 | } |
133 | |
134 | void ImageView::setScaleCentered(float scale) { |
135 | auto centerPosition = sizeF() / 2; |
136 | auto p = imageCoordinateAt(centerPosition); |
137 | mScale = scale; |
138 | setImageCoordinateAt(centerPosition, p); |
139 | } |
140 | |
141 | void ImageView::moveOffset(const Vector2f& delta) { |
142 | // Apply the delta to the offset. |
143 | mOffset += delta; |
144 | |
145 | // Prevent the image from going out of bounds. |
146 | auto scaledSize = scaledImageSizeF(); |
147 | if (mOffset.x() + scaledSize.x() < 0) |
148 | mOffset.x() = -scaledSize.x(); |
149 | if (mOffset.x() > sizeF().x()) |
150 | mOffset.x() = sizeF().x(); |
151 | if (mOffset.y() + scaledSize.y() < 0) |
152 | mOffset.y() = -scaledSize.y(); |
153 | if (mOffset.y() > sizeF().y()) |
154 | mOffset.y() = sizeF().y(); |
155 | } |
156 | |
157 | void ImageView::zoom(int amount, const Vector2f& focusPosition) { |
158 | auto focusedCoordinate = imageCoordinateAt(focusPosition); |
159 | float scaleFactor = std::pow(mZoomSensitivity, amount); |
160 | mScale = std::max(0.01f, scaleFactor * mScale); |
161 | setImageCoordinateAt(focusPosition, focusedCoordinate); |
162 | } |
163 | |
164 | bool ImageView::mouseDragEvent(const Vector2i& p, const Vector2i& rel, int button, int /*modifiers*/) { |
165 | if ((button & (1 << GLFW_MOUSE_BUTTON_LEFT)) != 0 && !mFixedOffset) { |
166 | setImageCoordinateAt((p + rel).cast<float>(), imageCoordinateAt(p.cast<float>())); |
167 | return true; |
168 | } |
169 | return false; |
170 | } |
171 | |
172 | bool ImageView::gridVisible() const { |
173 | return (mGridThreshold != -1) && (mScale > mGridThreshold); |
174 | } |
175 | |
176 | bool ImageView::pixelInfoVisible() const { |
177 | return mPixelInfoCallback && (mPixelInfoThreshold != -1) && (mScale > mPixelInfoThreshold); |
178 | } |
179 | |
180 | bool ImageView::helpersVisible() const { |
181 | return gridVisible() || pixelInfoVisible(); |
182 | } |
183 | |
184 | bool ImageView::scrollEvent(const Vector2i& p, const Vector2f& rel) { |
185 | if (mFixedScale) |
186 | return false; |
187 | float v = rel.y(); |
188 | if (std::abs(v) < 1) |
189 | v = std::copysign(1.f, v); |
190 | zoom(v, (p - position()).cast<float>()); |
191 | return true; |
192 | } |
193 | |
194 | bool ImageView::keyboardEvent(int key, int /*scancode*/, int action, int modifiers) { |
195 | if (action) { |
196 | switch (key) { |
197 | case GLFW_KEY_LEFT: |
198 | if (!mFixedOffset) { |
199 | if (GLFW_MOD_CONTROL & modifiers) |
200 | moveOffset(Vector2f(30, 0)); |
201 | else |
202 | moveOffset(Vector2f(10, 0)); |
203 | return true; |
204 | } |
205 | break; |
206 | case GLFW_KEY_RIGHT: |
207 | if (!mFixedOffset) { |
208 | if (GLFW_MOD_CONTROL & modifiers) |
209 | moveOffset(Vector2f(-30, 0)); |
210 | else |
211 | moveOffset(Vector2f(-10, 0)); |
212 | return true; |
213 | } |
214 | break; |
215 | case GLFW_KEY_DOWN: |
216 | if (!mFixedOffset) { |
217 | if (GLFW_MOD_CONTROL & modifiers) |
218 | moveOffset(Vector2f(0, -30)); |
219 | else |
220 | moveOffset(Vector2f(0, -10)); |
221 | return true; |
222 | } |
223 | break; |
224 | case GLFW_KEY_UP: |
225 | if (!mFixedOffset) { |
226 | if (GLFW_MOD_CONTROL & modifiers) |
227 | moveOffset(Vector2f(0, 30)); |
228 | else |
229 | moveOffset(Vector2f(0, 10)); |
230 | return true; |
231 | } |
232 | break; |
233 | } |
234 | } |
235 | return false; |
236 | } |
237 | |
238 | bool ImageView::keyboardCharacterEvent(unsigned int codepoint) { |
239 | switch (codepoint) { |
240 | case '-': |
241 | if (!mFixedScale) { |
242 | zoom(-1, sizeF() / 2); |
243 | return true; |
244 | } |
245 | break; |
246 | case '+': |
247 | if (!mFixedScale) { |
248 | zoom(1, sizeF() / 2); |
249 | return true; |
250 | } |
251 | break; |
252 | case 'c': |
253 | if (!mFixedOffset) { |
254 | center(); |
255 | return true; |
256 | } |
257 | break; |
258 | case 'f': |
259 | if (!mFixedOffset && !mFixedScale) { |
260 | fit(); |
261 | return true; |
262 | } |
263 | break; |
264 | case '1': case '2': case '3': case '4': case '5': |
265 | case '6': case '7': case '8': case '9': |
266 | if (!mFixedScale) { |
267 | setScaleCentered(1 << (codepoint - '1')); |
268 | return true; |
269 | } |
270 | break; |
271 | default: |
272 | return false; |
273 | } |
274 | return false; |
275 | } |
276 | |
277 | Vector2i ImageView::preferredSize(NVGcontext* /*ctx*/) const { |
278 | return mImageSize; |
279 | } |
280 | |
281 | void ImageView::performLayout(NVGcontext* ctx) { |
282 | Widget::performLayout(ctx); |
283 | center(); |
284 | } |
285 | |
286 | void ImageView::draw(NVGcontext* ctx) { |
287 | Widget::draw(ctx); |
288 | nvgEndFrame(ctx); // Flush the NanoVG draw stack, not necessary to call nvgBeginFrame afterwards. |
289 | |
290 | drawImageBorder(ctx); |
291 | |
292 | // Calculate several variables that need to be send to OpenGL in order for the image to be |
293 | // properly displayed inside the widget. |
294 | const Screen* screen = dynamic_cast<const Screen*>(this->window()->parent()); |
295 | assert(screen); |
296 | Vector2f screenSize = screen->size().cast<float>(); |
297 | Vector2f scaleFactor = mScale * imageSizeF().cwiseQuotient(screenSize); |
298 | Vector2f positionInScreen = absolutePosition().cast<float>(); |
299 | Vector2f positionAfterOffset = positionInScreen + mOffset; |
300 | Vector2f imagePosition = positionAfterOffset.cwiseQuotient(screenSize); |
301 | glEnable(GL_SCISSOR_TEST); |
302 | float r = screen->pixelRatio(); |
303 | glScissor(positionInScreen.x() * r, |
304 | (screenSize.y() - positionInScreen.y() - size().y()) * r, |
305 | size().x() * r, size().y() * r); |
306 | mShader.bind(); |
307 | glActiveTexture(GL_TEXTURE0); |
308 | glBindTexture(GL_TEXTURE_2D, mImageID); |
309 | mShader.setUniform("image" , 0); |
310 | mShader.setUniform("scaleFactor" , scaleFactor); |
311 | mShader.setUniform("position" , imagePosition); |
312 | mShader.drawIndexed(GL_TRIANGLES, 0, 2); |
313 | glDisable(GL_SCISSOR_TEST); |
314 | |
315 | if (helpersVisible()) |
316 | drawHelpers(ctx); |
317 | |
318 | drawWidgetBorder(ctx); |
319 | } |
320 | |
321 | void ImageView::updateImageParameters() { |
322 | // Query the width of the OpenGL texture. |
323 | glBindTexture(GL_TEXTURE_2D, mImageID); |
324 | GLint w, h; |
325 | glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w); |
326 | glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &h); |
327 | mImageSize = Vector2i(w, h); |
328 | } |
329 | |
330 | void ImageView::drawWidgetBorder(NVGcontext* ctx) const { |
331 | nvgBeginPath(ctx); |
332 | nvgStrokeWidth(ctx, 1); |
333 | nvgRoundedRect(ctx, mPos.x() + 0.5f, mPos.y() + 0.5f, mSize.x() - 1, |
334 | mSize.y() - 1, 0); |
335 | nvgStrokeColor(ctx, mTheme->mWindowPopup); |
336 | nvgStroke(ctx); |
337 | |
338 | nvgBeginPath(ctx); |
339 | nvgRoundedRect(ctx, mPos.x() + 0.5f, mPos.y() + 0.5f, mSize.x() - 1, |
340 | mSize.y() - 1, mTheme->mButtonCornerRadius); |
341 | nvgStrokeColor(ctx, mTheme->mBorderDark); |
342 | nvgStroke(ctx); |
343 | } |
344 | |
345 | void ImageView::drawImageBorder(NVGcontext* ctx) const { |
346 | nvgSave(ctx); |
347 | nvgBeginPath(ctx); |
348 | nvgScissor(ctx, mPos.x(), mPos.y(), mSize.x(), mSize.y()); |
349 | nvgStrokeWidth(ctx, 1.0f); |
350 | Vector2i borderPosition = mPos + mOffset.cast<int>(); |
351 | Vector2i borderSize = scaledImageSizeF().cast<int>(); |
352 | nvgRect(ctx, borderPosition.x() - 0.5f, borderPosition.y() - 0.5f, |
353 | borderSize.x() + 1, borderSize.y() + 1); |
354 | nvgStrokeColor(ctx, Color(1.0f, 1.0f, 1.0f, 1.0f)); |
355 | nvgStroke(ctx); |
356 | nvgResetScissor(ctx); |
357 | nvgRestore(ctx); |
358 | } |
359 | |
360 | void ImageView::drawHelpers(NVGcontext* ctx) const { |
361 | // We need to apply mPos after the transformation to account for the position of the widget |
362 | // relative to the parent. |
363 | Vector2f upperLeftCorner = positionForCoordinate(Vector2f::Zero()) + positionF(); |
364 | Vector2f lowerRightCorner = positionForCoordinate(imageSizeF()) + positionF(); |
365 | if (gridVisible()) |
366 | drawPixelGrid(ctx, upperLeftCorner, lowerRightCorner, mScale); |
367 | if (pixelInfoVisible()) |
368 | drawPixelInfo(ctx, mScale); |
369 | } |
370 | |
371 | void ImageView::drawPixelGrid(NVGcontext* ctx, const Vector2f& upperLeftCorner, |
372 | const Vector2f& lowerRightCorner, float stride) { |
373 | nvgBeginPath(ctx); |
374 | |
375 | // Draw the vertical grid lines |
376 | float currentX = upperLeftCorner.x(); |
377 | while (currentX <= lowerRightCorner.x()) { |
378 | nvgMoveTo(ctx, std::round(currentX), std::round(upperLeftCorner.y())); |
379 | nvgLineTo(ctx, std::round(currentX), std::round(lowerRightCorner.y())); |
380 | currentX += stride; |
381 | } |
382 | |
383 | // Draw the horizontal grid lines |
384 | float currentY = upperLeftCorner.y(); |
385 | while (currentY <= lowerRightCorner.y()) { |
386 | nvgMoveTo(ctx, std::round(upperLeftCorner.x()), std::round(currentY)); |
387 | nvgLineTo(ctx, std::round(lowerRightCorner.x()), std::round(currentY)); |
388 | currentY += stride; |
389 | } |
390 | |
391 | nvgStrokeWidth(ctx, 1.0f); |
392 | nvgStrokeColor(ctx, Color(1.0f, 1.0f, 1.0f, 0.2f)); |
393 | nvgStroke(ctx); |
394 | } |
395 | |
396 | void ImageView::drawPixelInfo(NVGcontext* ctx, float stride) const { |
397 | // Extract the image coordinates at the two corners of the widget. |
398 | Vector2i topLeft = clampedImageCoordinateAt(Vector2f::Zero()) |
399 | .unaryExpr([](float x) { return std::floor(x); }) |
400 | .cast<int>(); |
401 | |
402 | Vector2i bottomRight = clampedImageCoordinateAt(sizeF()) |
403 | .unaryExpr([](float x) { return std::ceil(x); }) |
404 | .cast<int>(); |
405 | |
406 | // Extract the positions for where to draw the text. |
407 | Vector2f currentCellPosition = |
408 | (positionF() + positionForCoordinate(topLeft.cast<float>())); |
409 | |
410 | float xInitialPosition = currentCellPosition.x(); |
411 | int xInitialIndex = topLeft.x(); |
412 | |
413 | // Properly scale the pixel information for the given stride. |
414 | auto fontSize = stride * mFontScaleFactor; |
415 | static constexpr float maxFontSize = 30.0f; |
416 | fontSize = fontSize > maxFontSize ? maxFontSize : fontSize; |
417 | nvgBeginPath(ctx); |
418 | nvgFontSize(ctx, fontSize); |
419 | nvgTextAlign(ctx, NVG_ALIGN_CENTER | NVG_ALIGN_TOP); |
420 | nvgFontFace(ctx, "sans" ); |
421 | while (topLeft.y() != bottomRight.y()) { |
422 | while (topLeft.x() != bottomRight.x()) { |
423 | writePixelInfo(ctx, currentCellPosition, topLeft, stride, fontSize); |
424 | currentCellPosition.x() += stride; |
425 | ++topLeft.x(); |
426 | } |
427 | currentCellPosition.x() = xInitialPosition; |
428 | currentCellPosition.y() += stride; |
429 | ++topLeft.y(); |
430 | topLeft.x() = xInitialIndex; |
431 | } |
432 | } |
433 | |
434 | void ImageView::writePixelInfo(NVGcontext* ctx, const Vector2f& cellPosition, |
435 | const Vector2i& pixel, float stride, float fontSize) const { |
436 | auto pixelData = mPixelInfoCallback(pixel); |
437 | auto pixelDataRows = tokenize(pixelData.first); |
438 | |
439 | // If no data is provided for this pixel then simply return. |
440 | if (pixelDataRows.empty()) |
441 | return; |
442 | |
443 | nvgFillColor(ctx, pixelData.second); |
444 | float yOffset = (stride - fontSize * pixelDataRows.size()) / 2; |
445 | for (size_t i = 0; i != pixelDataRows.size(); ++i) { |
446 | nvgText(ctx, cellPosition.x() + stride / 2, cellPosition.y() + yOffset, |
447 | pixelDataRows[i].data(), nullptr); |
448 | yOffset += fontSize; |
449 | } |
450 | } |
451 | |
452 | NAMESPACE_END(nanogui) |
453 | |