From 2377175a1995ae4cf707bce90005c14028db75ed Mon Sep 17 00:00:00 2001
From: jrassa <john@rassaonline.net>
Date: Sun, 16 Jul 2017 23:10:29 -0400
Subject: [PATCH] added origin and rotation support to guicomponent, exposed as
 theme options for several components

---
 THEMES.md                                     | 22 +++++
 .../views/gamelist/DetailedGameListView.cpp   |  4 +-
 .../src/views/gamelist/VideoGameListView.cpp  |  8 +-
 es-core/src/GuiComponent.cpp                  | 97 ++++++++++++++++---
 es-core/src/GuiComponent.h                    | 30 +++++-
 es-core/src/ThemeData.cpp                     | 12 +++
 es-core/src/ThemeData.h                       |  2 +-
 es-core/src/components/ImageComponent.cpp     | 39 +++-----
 es-core/src/components/ImageComponent.h       |  8 --
 es-core/src/components/VideoComponent.cpp     | 20 ++--
 es-core/src/components/VideoComponent.h       |  9 +-
 es-core/src/components/VideoVlcComponent.cpp  |  8 +-
 12 files changed, 179 insertions(+), 80 deletions(-)

diff --git a/THEMES.md b/THEMES.md
index fec2e61aa..f1b7fa2a6 100644
--- a/THEMES.md
+++ b/THEMES.md
@@ -517,6 +517,10 @@ Can be created as an extra.
 	- The image will be resized as large as possible so that it fits within this size and maintains its aspect ratio.  Use this instead of `size` when you don't know what kind of image you're using so it doesn't get grossly oversized on one axis (e.g. with a game's image metadata).
 * `origin` - type: NORMALIZED_PAIR.
 	- Where on the image `pos` refers to.  For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the image exactly in the middle of the screen.  If the "POSITION" and "SIZE" attributes are themable, "ORIGIN" is implied.
+* `rotation` - type: FLOAT.
+	- angle in degrees that the image should be rotated.  Positive values will rotate clockwise, negative values will rotate counterclockwise.
+* `rotationOrigin` - type: NORMALIZED_PAIR.
+	- Point around which the image will be rotated. Defaults to `0.5 0.5`.
 * `path` - type: PATH.
 	- Path to the image file.  Most common extensions are supported (including .jpg, .png, and unanimated .gif).
 * `tile` - type: BOOLEAN.
@@ -535,6 +539,10 @@ Can be created as an extra.
 	- The video will be resized as large as possible so that it fits within this size and maintains its aspect ratio.  Use this instead of `size` when you don't know what kind of video you're using so it doesn't get grossly oversized on one axis (e.g. with a game's video metadata).
 * `origin` - type: NORMALIZED_PAIR.
 	- Where on the image `pos` refers to.  For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the image exactly in the middle of the screen.  If the "POSITION" and "SIZE" attributes are themable, "ORIGIN" is implied.
+* `rotation` - type: FLOAT.
+	- angle in degrees that the text should be rotated.  Positive values will rotate clockwise, negative values will rotate counterclockwise.
+* `rotationOrigin` - type: NORMALIZED_PAIR.
+	- Point around which the text will be rotated. Defaults to `0.5 0.5`.
 * `delay` - type: FLOAT.  Default is false.
 	- Delay in seconds before video will start playing.
 * `default` - type: PATH.
@@ -556,6 +564,12 @@ Can be created as an extra.
 	- `0 0` - automatically size so text fits on one line (expanding horizontally).
 	- `w 0` - automatically wrap text so it doesn't go beyond `w` (expanding vertically).
 	- `w h` - works like a "text box."  If `h` is non-zero and `h` <= `fontSize` (implying it should be a single line of text), text that goes beyond `w` will be truncated with an elipses (...).
+* `origin` - type: NORMALIZED_PAIR.
+	- Where on the component `pos` refers to.  For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the component exactly in the middle of the screen.  If the "POSITION" and "SIZE" attributes are themable, "ORIGIN" is implied.
+* `rotation` - type: FLOAT.
+	- angle in degrees that the text should be rotated.  Positive values will rotate clockwise, negative values will rotate counterclockwise.
+* `rotationOrigin` - type: NORMALIZED_PAIR.
+	- Point around which the text will be rotated. Defaults to `0.5 0.5`.
 * `text` - type: STRING.
 * `color` - type: COLOR.
 * `backgroundColor` - type: COLOR;
@@ -574,6 +588,8 @@ Can be created as an extra.
 
 * `pos` - type: NORMALIZED_PAIR.
 * `size` - type: NORMALIZED_PAIR.
+* `origin` - type: NORMALIZED_PAIR.
+	- Where on the component `pos` refers to.  For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the component exactly in the middle of the screen.  If the "POSITION" and "SIZE" attributes are themable, "ORIGIN" is implied.
 * `selectorColor` - type: COLOR.
 	- Color of the "selector bar."
 * `selectorImagePath` - type: PATH.
@@ -618,6 +634,12 @@ EmulationStation borrows the concept of "nine patches" from Android (or "9-Slice
 * `pos` - type: NORMALIZED_PAIR.
 * `size` - type: NORMALIZED_PAIR.
 	- Only one value is actually used. The other value should be zero.  (e.g. specify width OR height, but not both.  This is done to maintain the aspect ratio.)
+* `origin` - type: NORMALIZED_PAIR.
+	- Where on the component `pos` refers to.  For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the component exactly in the middle of the screen.  If the "POSITION" and "SIZE" attributes are themable, "ORIGIN" is implied.
+* `rotation` - type: FLOAT.
+	- angle in degrees that the rating should be rotated.  Positive values will rotate clockwise, negative values will rotate counterclockwise.
+* `rotationOrigin` - type: NORMALIZED_PAIR.
+	- Point around which the rating will be rotated. Defaults to `0.5 0.5`.
 * `filledPath` - type: PATH.
 	- Path to the "filled star" image.  Image must be square (width equals height).
 * `unfilledPath` - type: PATH.
diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp
index b5e42758c..9d5d95002 100644
--- a/es-app/src/views/gamelist/DetailedGameListView.cpp
+++ b/es-app/src/views/gamelist/DetailedGameListView.cpp
@@ -78,7 +78,7 @@ void DetailedGameListView::onThemeChanged(const std::shared_ptr<ThemeData>& them
 	BasicGameListView::onThemeChanged(theme);
 
 	using namespace ThemeFlags;
-	mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX);
+	mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION);
 
 	initMDLabels();
 	std::vector<TextComponent*> labels = getMDLabels();
@@ -109,7 +109,7 @@ void DetailedGameListView::onThemeChanged(const std::shared_ptr<ThemeData>& them
 
 	mDescContainer.applyTheme(theme, getName(), "md_description", POSITION | ThemeFlags::SIZE | Z_INDEX);
 	mDescription.setSize(mDescContainer.getSize().x(), 0);
-	mDescription.applyTheme(theme, getName(), "md_description", ALL ^ (POSITION | ThemeFlags::SIZE | TEXT));
+	mDescription.applyTheme(theme, getName(), "md_description", ALL ^ (POSITION | ThemeFlags::SIZE | ThemeFlags::ORIGIN | TEXT | ROTATION));
 
 	sortChildren();
 }
diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp
index ec139b0fb..0813098db 100644
--- a/es-app/src/views/gamelist/VideoGameListView.cpp
+++ b/es-app/src/views/gamelist/VideoGameListView.cpp
@@ -114,9 +114,9 @@ void VideoGameListView::onThemeChanged(const std::shared_ptr<ThemeData>& theme)
 	BasicGameListView::onThemeChanged(theme);
 
 	using namespace ThemeFlags;
-	mMarquee.applyTheme(theme, getName(), "md_marquee", POSITION | ThemeFlags::SIZE | Z_INDEX);
-	mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX);
-	mVideo->applyTheme(theme, getName(), "md_video", POSITION | ThemeFlags::SIZE | ThemeFlags::DELAY | Z_INDEX);
+	mMarquee.applyTheme(theme, getName(), "md_marquee", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION);
+	mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION);
+	mVideo->applyTheme(theme, getName(), "md_video", POSITION | ThemeFlags::SIZE | ThemeFlags::DELAY | Z_INDEX | ROTATION);
 
 	initMDLabels();
 	std::vector<TextComponent*> labels = getMDLabels();
@@ -147,7 +147,7 @@ void VideoGameListView::onThemeChanged(const std::shared_ptr<ThemeData>& theme)
 
 	mDescContainer.applyTheme(theme, getName(), "md_description", POSITION | ThemeFlags::SIZE | Z_INDEX);
 	mDescription.setSize(mDescContainer.getSize().x(), 0);
-	mDescription.applyTheme(theme, getName(), "md_description", ALL ^ (POSITION | ThemeFlags::SIZE | TEXT));
+	mDescription.applyTheme(theme, getName(), "md_description", ALL ^ (POSITION | ThemeFlags::SIZE | ThemeFlags::ORIGIN | TEXT | ROTATION));
 
 	sortChildren();
 }
diff --git a/es-core/src/GuiComponent.cpp b/es-core/src/GuiComponent.cpp
index c419aedab..07feb0afc 100644
--- a/es-core/src/GuiComponent.cpp
+++ b/es-core/src/GuiComponent.cpp
@@ -6,8 +6,8 @@
 #include "ThemeData.h"
 
 GuiComponent::GuiComponent(Window* window) : mWindow(window), mParent(NULL), mOpacity(255),
-	mPosition(Eigen::Vector3f::Zero()), mSize(Eigen::Vector2f::Zero()), mTransform(Eigen::Affine3f::Identity()),
-	mIsProcessing(false)
+	mPosition(Eigen::Vector3f::Zero()), mOrigin(Eigen::Vector2f::Zero()), mRotationOrigin(0.5, 0.5),
+	mSize(Eigen::Vector2f::Zero()), mTransform(Eigen::Affine3f::Identity()), mIsProcessing(false)
 {
 	for(unsigned char i = 0; i < MAX_ANIMATIONS; i++)
 		mAnimationMap[i] = NULL;
@@ -76,35 +76,65 @@ Eigen::Vector3f GuiComponent::getPosition() const
 	return mPosition;
 }
 
-void GuiComponent::setPosition(const Eigen::Vector3f& offset)
-{
-	mPosition = offset;
-	onPositionChanged();
-}
-
 void GuiComponent::setPosition(float x, float y, float z)
 {
 	mPosition << x, y, z;
 	onPositionChanged();
 }
 
+Eigen::Vector2f GuiComponent::getOrigin() const
+{
+	return mOrigin;
+}
+
+void GuiComponent::setOrigin(float x, float y)
+{
+	mOrigin << x, y;
+	onOriginChanged();
+}
+
+Eigen::Vector2f GuiComponent::getRotationOrigin() const
+{
+	return mRotationOrigin;
+}
+
+void GuiComponent::setRotationOrigin(float x, float y)
+{
+	mRotationOrigin << x, y;;
+}
+
 Eigen::Vector2f GuiComponent::getSize() const
 {
 	return mSize;
 }
 
-void GuiComponent::setSize(const Eigen::Vector2f& size)
-{
-    mSize = size;
-    onSizeChanged();
-}
-
 void GuiComponent::setSize(float w, float h)
 {
 	mSize << w, h;
     onSizeChanged();
 }
 
+float GuiComponent::getRotation() const
+{
+	return mRotation;
+}
+
+void GuiComponent::setRotation(float rotation)
+{
+	mRotation = rotation;
+}
+
+float GuiComponent::getScale() const
+{
+	return mScale;
+}
+
+void GuiComponent::setScale(float scale)
+{
+	mScale = scale;
+	onSizeChanged();
+}
+
 float GuiComponent::getZIndex() const
 {
 	return mZIndex;
@@ -125,6 +155,12 @@ void GuiComponent::setDefaultZIndex(float z)
 	mDefaultZIndex = z;
 }
 
+Eigen::Vector2f GuiComponent::getCenter() const
+{
+	return Eigen::Vector2f(mPosition.x() - (getSize().x() * mOrigin.x()) + getSize().x() / 2,
+						   mPosition.y() - (getSize().y() * mOrigin.y()) + getSize().y() / 2);
+}
+
 //Children stuff.
 void GuiComponent::addChild(GuiComponent* cmp)
 {
@@ -208,6 +244,28 @@ const Eigen::Affine3f& GuiComponent::getTransform()
 {
 	mTransform.setIdentity();
 	mTransform.translate(mPosition);
+	if (mScale != 1.0)
+	{
+		mTransform *= Eigen::Scaling(mScale);
+	}
+	if (mRotation != 0.0)
+	{
+		// Calculate offset as difference between origin and rotation origin
+		float xOff = (mOrigin.x() - mRotationOrigin.x()) * mSize.x();
+		float yOff = (mOrigin.y() - mRotationOrigin.y()) * mSize.y();
+
+		// transform to offset point
+		if (xOff != 0.0 || yOff != 0.0)
+			mTransform.translate(Eigen::Vector3f(xOff * -1, yOff * -1, 0.0f));
+
+		// apply rotation transorm
+		mTransform *= Eigen::AngleAxisf(mRotation, Eigen::Vector3f::UnitZ());
+
+		// Tranform back to original point
+		if (xOff != 0.0 || yOff != 0.0)
+			mTransform.translate(Eigen::Vector3f(xOff, yOff, 0.0f));
+	}
+	mTransform.translate(Eigen::Vector3f(mOrigin.x() * mSize.x() * -1, mOrigin.y() * mSize.y() * -1, 0.0f));
 	return mTransform;
 }
 
@@ -348,6 +406,17 @@ void GuiComponent::applyTheme(const std::shared_ptr<ThemeData>& theme, const std
 	if(properties & ThemeFlags::SIZE && elem->has("size"))
 		setSize(elem->get<Eigen::Vector2f>("size").cwiseProduct(scale));
 
+	// position + size also implies origin
+	if((properties & ORIGIN || (properties & POSITION && properties & ThemeFlags::SIZE)) && elem->has("origin"))
+		setOrigin(elem->get<Eigen::Vector2f>("origin"));
+
+	if(properties & ThemeFlags::ROTATION) {
+		if(elem->has("rotation"))
+			setRotationDegrees(elem->get<float>("rotation"));
+		if(elem->has("rotationOrigin"))
+			setRotationOrigin(elem->get<Eigen::Vector2f>("rotationOrigin"));
+	}
+
 	if(properties & ThemeFlags::Z_INDEX && elem->has("zIndex"))
 		setZIndex(elem->get<float>("zIndex"));
 	else
diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h
index 6172cd528..5f07f0ef4 100644
--- a/es-core/src/GuiComponent.h
+++ b/es-core/src/GuiComponent.h
@@ -37,21 +37,42 @@ public:
 	virtual void render(const Eigen::Affine3f& parentTrans);
 
 	Eigen::Vector3f getPosition() const;
-	void setPosition(const Eigen::Vector3f& offset);
+	inline void setPosition(const Eigen::Vector3f& offset) { setPosition(offset.x(), offset.y(), offset.z()); }
 	void setPosition(float x, float y, float z = 0.0f);
 	virtual void onPositionChanged() {};
 
+	//Sets the origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center)
+	Eigen::Vector2f getOrigin() const;
+	void setOrigin(float originX, float originY);
+	inline void setOrigin(Eigen::Vector2f origin) { setOrigin(origin.x(), origin.y()); }
+	virtual void onOriginChanged() {};
+
+	//Sets the rotation origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center)
+	Eigen::Vector2f getRotationOrigin() const;
+	void setRotationOrigin(float originX, float originY);
+	inline void setRotationOrigin(Eigen::Vector2f origin) { setRotationOrigin(origin.x(), origin.y()); }
+
 	Eigen::Vector2f getSize() const;
-    void setSize(const Eigen::Vector2f& size);
+    inline void setSize(const Eigen::Vector2f& size) { setSize(size.x(), size.y()); }
     void setSize(float w, float h);
     virtual void onSizeChanged() {};
 
+	float getRotation() const;
+	void setRotation(float rotation);
+	inline void setRotationDegrees(float rotation) { setRotation(rotation * M_PI / 180); }
+
+	float getScale() const;
+	void setScale(float scale);
+
     float getZIndex() const;
     void setZIndex(float zIndex);
 
     float getDefaultZIndex() const;
     void setDefaultZIndex(float zIndex);
 
+	// Returns the center point of the image (takes origin into account).
+	Eigen::Vector2f getCenter() const;
+
 	void setParent(GuiComponent* parent);
 	GuiComponent* getParent() const;
 
@@ -119,8 +140,13 @@ protected:
 	std::vector<GuiComponent*> mChildren;
 
 	Eigen::Vector3f mPosition;
+	Eigen::Vector2f mOrigin;
+	Eigen::Vector2f mRotationOrigin;
 	Eigen::Vector2f mSize;
 
+	float mRotation = 0.0;
+	float mScale = 1.0;
+
 	float mDefaultZIndex = 0;
 	float mZIndex = 0;
 
diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp
index 43b76deb6..ed5b240e3 100644
--- a/es-core/src/ThemeData.cpp
+++ b/es-core/src/ThemeData.cpp
@@ -33,6 +33,8 @@ std::map< std::string, ElementMapType > ThemeData::sElementMap = boost::assign::
 		("size", NORMALIZED_PAIR)
 		("maxSize", NORMALIZED_PAIR)
 		("origin", NORMALIZED_PAIR)
+	 	("rotation", FLOAT)
+		("rotationOrigin", NORMALIZED_PAIR)
 		("path", PATH)
 		("tile", BOOLEAN)
 		("color", COLOR)
@@ -40,6 +42,9 @@ std::map< std::string, ElementMapType > ThemeData::sElementMap = boost::assign::
 	("text", makeMap(boost::assign::map_list_of
 		("pos", NORMALIZED_PAIR)
 		("size", NORMALIZED_PAIR)
+		("origin", NORMALIZED_PAIR)
+		("rotation", FLOAT)
+		("rotationOrigin", NORMALIZED_PAIR)
 		("text", STRING)
 		("backgroundColor", COLOR)
 		("fontPath", PATH)
@@ -53,6 +58,7 @@ std::map< std::string, ElementMapType > ThemeData::sElementMap = boost::assign::
 	("textlist", makeMap(boost::assign::map_list_of
 		("pos", NORMALIZED_PAIR)
 		("size", NORMALIZED_PAIR)
+		("origin", NORMALIZED_PAIR)
 		("selectorHeight", FLOAT)
 		("selectorOffsetY", FLOAT)
 		("selectorColor", COLOR)
@@ -72,6 +78,7 @@ std::map< std::string, ElementMapType > ThemeData::sElementMap = boost::assign::
 	("container", makeMap(boost::assign::map_list_of
 		("pos", NORMALIZED_PAIR)
 		("size", NORMALIZED_PAIR)
+	 	("origin", NORMALIZED_PAIR)
 		("zIndex", FLOAT)))
 	("ninepatch", makeMap(boost::assign::map_list_of
 		("pos", NORMALIZED_PAIR)
@@ -89,6 +96,9 @@ std::map< std::string, ElementMapType > ThemeData::sElementMap = boost::assign::
 	("rating", makeMap(boost::assign::map_list_of
 		("pos", NORMALIZED_PAIR)
 		("size", NORMALIZED_PAIR)
+		("origin", NORMALIZED_PAIR)
+		("rotation", FLOAT)
+		("rotationOrigin", NORMALIZED_PAIR)
 		("color", COLOR)
 		("filledPath", PATH)
 		("unfilledPath", PATH)
@@ -106,6 +116,8 @@ std::map< std::string, ElementMapType > ThemeData::sElementMap = boost::assign::
 		("size", NORMALIZED_PAIR)
 		("maxSize", NORMALIZED_PAIR)
 		("origin", NORMALIZED_PAIR)
+		("rotation", FLOAT)
+		("rotationOrigin", NORMALIZED_PAIR)
 		("default", PATH)
 		("delay", FLOAT)
 		("zIndex", FLOAT)
diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h
index a4e483b62..ba58bcf73 100644
--- a/es-core/src/ThemeData.h
+++ b/es-core/src/ThemeData.h
@@ -40,7 +40,7 @@ namespace ThemeFlags
 		LINE_SPACING = 2048,
 		DELAY = 4096,
 		Z_INDEX = 8192,
-
+		ROTATION = 16384,
 		ALL = 0xFFFFFFFF
 	};
 }
diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp
index 8e28b7cf4..7ab8f9c90 100644
--- a/es-core/src/components/ImageComponent.cpp
+++ b/es-core/src/components/ImageComponent.cpp
@@ -12,17 +12,11 @@ Eigen::Vector2i ImageComponent::getTextureSize() const
 	if(mTexture)
 		return mTexture->getSize();
 	else
-		return Eigen::Vector2i(0, 0);
-}
-
-Eigen::Vector2f ImageComponent::getCenter() const
-{
-	return Eigen::Vector2f(mPosition.x() - (getSize().x() * mOrigin.x()) + getSize().x() / 2, 
-		mPosition.y() - (getSize().y() * mOrigin.y()) + getSize().y() / 2);
+		return Eigen::Vector2i::Zero();
 }
 
 ImageComponent::ImageComponent(Window* window, bool forceLoad, bool dynamic) : GuiComponent(window),
-	mTargetIsMax(false), mFlipX(false), mFlipY(false), mOrigin(0.0, 0.0), mTargetSize(0, 0), mColorShift(0xFFFFFFFF),
+	mTargetIsMax(false), mFlipX(false), mFlipY(false), mTargetSize(0, 0), mColorShift(0xFFFFFFFF),
 	mForceLoad(forceLoad), mDynamic(dynamic), mFadeOpacity(0.0f), mFading(false)
 {
 	updateColors();
@@ -125,12 +119,6 @@ void ImageComponent::setImage(const std::shared_ptr<TextureResource>& texture)
 	resize();
 }
 
-void ImageComponent::setOrigin(float originX, float originY)
-{
-	mOrigin << originX, originY;
-	updateVertices();
-}
-
 void ImageComponent::setResize(float width, float height)
 {
 	mTargetSize << width, height;
@@ -180,16 +168,8 @@ void ImageComponent::updateVertices()
 
 	// we go through this mess to make sure everything is properly rounded
 	// if we just round vertices at the end, edge cases occur near sizes of 0.5
-	Eigen::Vector2f topLeft(-mSize.x() * mOrigin.x(), -mSize.y() * mOrigin.y());
-	Eigen::Vector2f bottomRight(mSize.x() * (1 -mOrigin.x()), mSize.y() * (1 - mOrigin.y()));
-
-	const float width = round(bottomRight.x() - topLeft.x());
-	const float height = round(bottomRight.y() - topLeft.y());
-
-	topLeft[0] = floor(topLeft[0]);
-	topLeft[1] = floor(topLeft[1]);
-	bottomRight[0] = topLeft[0] + width;
-	bottomRight[1] = topLeft[1] + height;
+	Eigen::Vector2f topLeft(0.0, 0.0);
+	Eigen::Vector2f bottomRight(round(mSize.x()), round(mSize.y()));
 
 	mVertices[0].pos << topLeft.x(), topLeft.y();
 	mVertices[1].pos << topLeft.x(), bottomRight.y();
@@ -236,9 +216,9 @@ void ImageComponent::updateColors()
 
 void ImageComponent::render(const Eigen::Affine3f& parentTrans)
 {
-	Eigen::Affine3f trans = roundMatrix(parentTrans * getTransform());
+	Eigen::Affine3f trans = parentTrans * getTransform();
 	Renderer::setMatrix(trans);
-	
+
 	if(mTexture && mOpacity > 0)
 	{
 		if(mTexture->isInitialized())
@@ -363,6 +343,13 @@ void ImageComponent::applyTheme(const std::shared_ptr<ThemeData>& theme, const s
 	if(properties & COLOR && elem->has("color"))
 		setColorShift(elem->get<unsigned int>("color"));
 
+	if(properties & ThemeFlags::ROTATION) {
+		if(elem->has("rotation"))
+			setRotationDegrees(elem->get<float>("rotation"));
+		if(elem->has("rotationOrigin"))
+			setRotationOrigin(elem->get<Eigen::Vector2f>("rotationOrigin"));
+	}
+
 	if(properties & ThemeFlags::Z_INDEX && elem->has("zIndex"))
 		setZIndex(elem->get<float>("zIndex"));
 	else
diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h
index 3ff1ffbb2..2ed005fc2 100644
--- a/es-core/src/components/ImageComponent.h
+++ b/es-core/src/components/ImageComponent.h
@@ -25,10 +25,6 @@ public:
 	void onSizeChanged() override;
 	void setOpacity(unsigned char opacity) override;
 
-	//Sets the origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center)
-	void setOrigin(float originX, float originY);
-	inline void setOrigin(Eigen::Vector2f origin) { setOrigin(origin.x(), origin.y()); }
-
 	// Resize the image to fit this size. If one axis is zero, scale that axis to maintain aspect ratio.
 	// If both are non-zero, potentially break the aspect ratio.  If both are zero, no resizing.
 	// Can be set before or after an image is loaded.
@@ -51,9 +47,6 @@ public:
 	// Returns the size of the current texture, or (0, 0) if none is loaded.  May be different than drawn size (use getSize() for that).
 	Eigen::Vector2i getTextureSize() const;
 
-	// Returns the center point of the image (takes origin into account).
-	Eigen::Vector2f getCenter() const;
-
 	bool hasImage();
 
 	void render(const Eigen::Affine3f& parentTrans) override;
@@ -63,7 +56,6 @@ public:
 	virtual std::vector<HelpPrompt> getHelpPrompts() override;
 private:
 	Eigen::Vector2f mTargetSize;
-	Eigen::Vector2f mOrigin;
 
 	bool mFlipX, mFlipY, mTargetIsMax;
 
diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp
index 623bf5d03..4e931e190 100644
--- a/es-core/src/components/VideoComponent.cpp
+++ b/es-core/src/components/VideoComponent.cpp
@@ -60,7 +60,6 @@ VideoComponent::VideoComponent(Window* window) :
 	mDisable(false),
 	mScreensaverMode(false),
 	mTargetIsMax(false),
-	mOrigin(0, 0),
 	mTargetSize(0, 0)
 {
 	// Setup the default configuration
@@ -84,18 +83,10 @@ VideoComponent::~VideoComponent()
 	remove(getTitlePath().c_str());
 }
 
-void VideoComponent::setOrigin(float originX, float originY)
+void VideoComponent::onOriginChanged()
 {
-	mOrigin << originX, originY;
-
 	// Update the embeded static image
-	mStaticImage.setOrigin(originX, originY);
-}
-
-Eigen::Vector2f VideoComponent::getCenter() const
-{
-	return Eigen::Vector2f(mPosition.x() - (getSize().x() * mOrigin.x()) + getSize().x() / 2,
-		mPosition.y() - (getSize().y() * mOrigin.y()) + getSize().y() / 2);
+	mStaticImage.setOrigin(mOrigin);
 }
 
 void VideoComponent::onSizeChanged()
@@ -221,6 +212,13 @@ void VideoComponent::applyTheme(const std::shared_ptr<ThemeData>& theme, const s
 	if (elem->has("showSnapshotDelay"))
 		mConfig.showSnapshotDelay = elem->get<bool>("showSnapshotDelay");
 
+	if(properties & ThemeFlags::ROTATION) {
+		if(elem->has("rotation"))
+			setRotationDegrees(elem->get<float>("rotation"));
+		if(elem->has("rotationOrigin"))
+			setRotationOrigin(elem->get<Eigen::Vector2f>("rotationOrigin"));
+	}
+
 	if(properties & ThemeFlags::Z_INDEX && elem->has("zIndex"))
 		setZIndex(elem->get<float>("zIndex"));
 	else
diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h
index a729b7063..f5c1f01e1 100644
--- a/es-core/src/components/VideoComponent.h
+++ b/es-core/src/components/VideoComponent.h
@@ -48,10 +48,7 @@ public:
 	virtual void onScreenSaverDeactivate() override;
 	virtual void topWindow(bool isTop) override;
 
-	//Sets the origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center)
-	void setOrigin(float originX, float originY);
-	inline void setOrigin(Eigen::Vector2f origin) { setOrigin(origin.x(), origin.y()); }
-
+	void onOriginChanged() override;
 	void onSizeChanged() override;
 	void setOpacity(unsigned char opacity) override;
 
@@ -62,9 +59,6 @@ public:
 
 	virtual std::vector<HelpPrompt> getHelpPrompts() override;
 
-	// Returns the center point of the video (takes origin into account).
-	Eigen::Vector2f getCenter() const;
-
 	virtual void update(int deltaTime);
 
 	// Resize the video to fit this size. If one axis is zero, scale that axis to maintain aspect ratio.
@@ -100,7 +94,6 @@ private:
 protected:
 	unsigned						mVideoWidth;
 	unsigned						mVideoHeight;
-	Eigen::Vector2f 				mOrigin;
 	Eigen::Vector2f					mTargetSize;
 	std::shared_ptr<TextureResource> mTexture;
 	float							mFadeIn;
diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp
index d042374cc..2b2bf1b1a 100644
--- a/es-core/src/components/VideoVlcComponent.cpp
+++ b/es-core/src/components/VideoVlcComponent.cpp
@@ -141,10 +141,10 @@ void VideoVlcComponent::render(const Eigen::Affine3f& parentTrans)
 		float x2;
 		float y2;
 
-		x = -(float)mSize.x() * mOrigin.x();
-		y = -(float)mSize.y() * mOrigin.y();
-		x2 = x+mSize.x();
-		y2 = y+mSize.y();
+		x = 0.0;
+		y = 0.0;
+		x2 = mSize.x();
+		y2 = mSize.y();
 
 		// Define a structure to contain the data for each vertex
 		struct Vertex