diff --git a/cypress/e2e/DoenetML/tagSpecific/image.cy.js b/cypress/e2e/DoenetML/tagSpecific/image.cy.js
index 42e3626831..9b4d04079f 100644
--- a/cypress/e2e/DoenetML/tagSpecific/image.cy.js
+++ b/cypress/e2e/DoenetML/tagSpecific/image.cy.js
@@ -484,6 +484,60 @@ describe('Image Tag Tests', function () {
});
+ it('rotate image in graph', () => {
+ cy.window().then(async (win) => {
+ win.postMessage({
+ doenetML: `
+ a
+
+
+
+
+
+
Rotate 1: $image1.rotate
+ Change rotate 1
+ Change rotate 1a
+
+
+
+ `}, "*");
+ });
+
+ cy.get('#\\/_text1').should('have.text', 'a') //wait for page to load
+
+ // Is there a way to test the rotation of the image in the graph?
+
+ cy.get("#\\/pRotate1").should('contain.text', 'Rotate 1: 0.785')
+
+
+ cy.window().then(async (win) => {
+ let stateVariables = await win.returnAllStateVariables1();
+ expect(stateVariables["/image1"].stateValues.rotate).eq(Math.PI / 4)
+ });
+
+ cy.log("change rotate")
+
+ cy.get('#\\/rotate1 textarea').type("{end}{shift+home}{backspace}3pi/4{enter}", { force: true })
+
+ cy.get("#\\/pRotate1").should('contain.text', 'Rotate 1: 2.356')
+
+ cy.window().then(async (win) => {
+ let stateVariables = await win.returnAllStateVariables1();
+ expect(stateVariables["/image1"].stateValues.rotate).eq(3 * Math.PI / 4)
+ });
+
+ cy.get('#\\/rotate1a textarea').type("{end}{shift+home}{backspace}-pi{enter}", { force: true })
+
+ cy.get("#\\/pRotate1").should('contain.text', 'Rotate 1: -3.14159')
+
+ cy.window().then(async (win) => {
+ let stateVariables = await win.returnAllStateVariables1();
+ expect(stateVariables["/image1"].stateValues.rotate).eq(-Math.PI)
+ });
+
+
+ });
+
})
diff --git a/src/Core/components/Image.js b/src/Core/components/Image.js
index 394d400897..4fa5020f46 100644
--- a/src/Core/components/Image.js
+++ b/src/Core/components/Image.js
@@ -107,6 +107,14 @@ export default class Image extends BlockComponent {
validValues: ["upperright", "upperleft", "lowerright", "lowerleft", "top", "bottom", "left", "right", "center"]
}
+ attributes.rotate = {
+ createComponentOfType: "number",
+ createStateVariable: "rotate",
+ defaultValue: 0,
+ public: true,
+ forRenderer: true,
+ }
+
attributes.styleNumber.defaultValue = 0;
return attributes;
diff --git a/src/Viewer/renderers/image.jsx b/src/Viewer/renderers/image.jsx
index feb09d28dc..cea4c6f6ff 100644
--- a/src/Viewer/renderers/image.jsx
+++ b/src/Viewer/renderers/image.jsx
@@ -29,6 +29,9 @@ export default React.memo(function Image(props) {
let currentOffset = useRef(null);
+ let rotationTransform = useRef(null);
+ let lastRotate = useRef(SVs.rotate);
+
const urlOrSource = (SVs.cid ? url : SVs.source) || "";
let onChangeVisibility = isVisible => {
@@ -70,7 +73,7 @@ export default React.memo(function Image(props) {
visible: !SVs.hidden,
fixed,
layer: 10 * SVs.layer + 0,
- highlight: !fixed
+ highlight: !fixed,
};
@@ -137,6 +140,33 @@ export default React.memo(function Image(props) {
let newImageJXG = board.create('image', [urlOrSource, offset, [width, height]], jsxImageAttributes);
+ // tranformation code copied from jsxgraph documentation:
+ // https://jsxgraph.uni-bayreuth.de/wiki/index.php?title=Images#The_JavaScript_code_5
+ var tOff = board.create('transform', [
+ function () {
+ return -newImageJXG.X() - newImageJXG.W() * 0.5;
+ }, function () {
+ return -newImageJXG.Y() - newImageJXG.H() * 0.5;
+ }
+ ], { type: 'translate' });
+ var tOffInverse = board.create('transform', [
+ function () {
+ return newImageJXG.X() + newImageJXG.W() * 0.5;
+ }, function () {
+ return newImageJXG.Y() + newImageJXG.H() * 0.5;
+ }
+ ], { type: 'translate' });
+ var tRot = board.create('transform', [
+ SVs.rotate
+ ], { type: 'rotate' });
+
+
+ tOff.bindTo(newImageJXG); // Shift image to origin
+ tRot.bindTo(newImageJXG); // Rotate
+ tOffInverse.bindTo(newImageJXG); // Shift image back
+
+ rotationTransform.current = tRot;
+ lastRotate.current = SVs.rotate;
newImageJXG.on('down', function (e) {
pointerAtDown.current = [e.x, e.y];
@@ -214,6 +244,9 @@ export default React.memo(function Image(props) {
previousPositionFromAnchor.current = SVs.positionFromAnchor;
currentSize.current = [width, height];
+ // need fullUpdate to get initial rotation in case image was from a blob
+ imageJXG.current.fullUpdate();
+
}
if (board) {
@@ -289,6 +322,12 @@ export default React.memo(function Image(props) {
currentSize.current = [width, height];
}
+ if (SVs.rotate != lastRotate.current) {
+ rotationTransform.current.setMatrix(board, "rotate", [SVs.rotate]);
+ lastRotate.current = SVs.rotate;
+ }
+
+
if (SVs.positionFromAnchor !== previousPositionFromAnchor.current || sizeChanged) {
let offset;
if (SVs.positionFromAnchor === "center") {