diff --git a/bqplot_image_gl/imagegl.py b/bqplot_image_gl/imagegl.py index 18132fb..b37d1c9 100644 --- a/bqplot_image_gl/imagegl.py +++ b/bqplot_image_gl/imagegl.py @@ -37,6 +37,7 @@ class ImageGL(bqplot.Mark): atype='bqplot.Axis', **array_serialization)\ .valid(array_squeeze, shape(2)) + transform = List([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0], allow_none=False).tag(sync=True) scales_metadata = Dict({ 'x': {'orientation': 'horizontal', 'dimension': 'x'}, 'y': {'orientation': 'vertical', 'dimension': 'y'}, diff --git a/js/lib/imagegl.js b/js/lib/imagegl.js index 5118cb3..cb737b3 100644 --- a/js/lib/imagegl.js +++ b/js/lib/imagegl.js @@ -27,6 +27,8 @@ class ImageGLModel extends bqplot.MarkModel { opacity: 1.0, x: (0.0, 1.0), y: (0.0, 1.0), + // flat 3x3 identity matrix + transform: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0], scales_metadata: { 'x': {'orientation': 'horizontal', 'dimension': 'x'}, 'y': {'orientation': 'vertical', 'dimension': 'y'}, @@ -114,6 +116,8 @@ class ImageGLView extends bqplot.Mark { // basically the corners of the image image_domain_x : { type: "2f", value: [0.0, 1.0] }, image_domain_y : { type: "2f", value: [0.0, 1.0] }, + // transform matrix + transform : { type: "mat3", value: this.model.get("transform") }, // extra opacity value opacity: {type: 'f', value: 1.0} }, @@ -200,6 +204,10 @@ class ImageGLView extends bqplot.Mark { }; this.listenTo(this.model, "change:visible", sync_visible , this); sync_visible(); + this.listenTo(this.model, "change:transform", () => { + this.update_transform(); + this.update_scene(); + }, this); this.listenTo(this.model, "change:opacity", () => { this.update_opacity(); this.update_scene(); @@ -235,6 +243,11 @@ class ImageGLView extends bqplot.Mark { this.image_material.uniforms.color_max.value = max; } + update_transform() { + var transform = this.model.get('transform'); + this.image_material.uniforms.transform.value = transform; + } + update_opacity() { this.image_material.uniforms.opacity.value = this.model.get('opacity'); } diff --git a/js/shaders/image-vertex.glsl b/js/shaders/image-vertex.glsl index 92dd3bd..17007f1 100644 --- a/js/shaders/image-vertex.glsl +++ b/js/shaders/image-vertex.glsl @@ -1,7 +1,11 @@ varying vec2 pixel_coordinate; +uniform mat3 transform; void main(void) { - vec4 view_position = modelViewMatrix * vec4(position,1.0); - pixel_coordinate = view_position.xy; + vec3 pos3d = transform * position; + vec2 pos2d = pos3d.xy;// / pos3d.z; + vec4 view_position = modelViewMatrix * vec4(pos2d.x, pos2d.y, 0., 1.0); + vec4 view_position2 = modelViewMatrix * vec4(position.x, position.y, 0., 1.0); + pixel_coordinate = view_position2.xy; gl_Position = projectionMatrix * view_position; } diff --git a/ui-tests/tests/bqplot-image-gl.test.ts-snapshots/light-image-rotate-update-ipynb-cell-0-linux.png b/ui-tests/tests/bqplot-image-gl.test.ts-snapshots/light-image-rotate-update-ipynb-cell-0-linux.png new file mode 100644 index 0000000..d14c715 Binary files /dev/null and b/ui-tests/tests/bqplot-image-gl.test.ts-snapshots/light-image-rotate-update-ipynb-cell-0-linux.png differ diff --git a/ui-tests/tests/bqplot-image-gl.test.ts-snapshots/light-image-rotate-update-ipynb-cell-1-linux.png b/ui-tests/tests/bqplot-image-gl.test.ts-snapshots/light-image-rotate-update-ipynb-cell-1-linux.png new file mode 100644 index 0000000..3644914 Binary files /dev/null and b/ui-tests/tests/bqplot-image-gl.test.ts-snapshots/light-image-rotate-update-ipynb-cell-1-linux.png differ diff --git a/ui-tests/tests/notebooks/image_rotate_update.ipynb b/ui-tests/tests/notebooks/image_rotate_update.ipynb new file mode 100644 index 0000000..708e411 --- /dev/null +++ b/ui-tests/tests/notebooks/image_rotate_update.ipynb @@ -0,0 +1,72 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "940b6b42", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from math import cos, sin, pi\n", + "from bqplot import Figure, LinearScale, Axis, ColorScale\n", + "from bqplot_image_gl import ImageGL\n", + "scale_x = LinearScale(min=0, max=1, allow_padding=False)\n", + "scale_y = LinearScale(min=0, max=1, allow_padding=False)\n", + "scales = {'x': scale_x,\n", + " 'y': scale_y}\n", + "axis_x = Axis(scale=scale_x, label='x', )\n", + "axis_y = Axis(scale=scale_y, label='y', orientation='vertical')\n", + "\n", + "figure = Figure(scales=scales, axes=[axis_x, axis_y])\n", + "\n", + "scales_image = {'x': scale_x,\n", + " 'y': scale_y,\n", + " 'image': ColorScale(min=0, max=1)}\n", + "\n", + "def rotation_matrix(theta):\n", + " return [cos(theta), -sin(theta), 0,\n", + " sin(theta), cos(theta), 0,\n", + " 0, 0, 1]\n", + "\n", + "np.random.seed(0)\n", + "M = rotation_matrix(pi/8)\n", + "image = ImageGL(image=np.random.random((10, 10)).astype('float32'), scales=scales_image, transform=M)\n", + "\n", + "figure.marks = (image,)\n", + "figure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "658efd7a", + "metadata": {}, + "outputs": [], + "source": [ + "image.transform = rotation_matrix(pi/4)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.16" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}