/* * Catroid: An on-device visual programming system for Android devices * Copyright (C) 2010-2016 The Catrobat Team * (<http://developer.catrobat.org/credits>) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * An additional term exception under section 7 of the GNU Affero * General Public License, version 3, is available at * http://developer.catrobat.org/license_additional_term * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.catrobat.catroid.content; import android.graphics.PointF; import com.badlogic.gdx.graphics.Pixmap; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.graphics.glutils.ShaderProgram; import com.badlogic.gdx.math.Polygon; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.scenes.scene2d.Action; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.InputListener; import com.badlogic.gdx.scenes.scene2d.Touchable; import com.badlogic.gdx.scenes.scene2d.actions.ParallelAction; import com.badlogic.gdx.scenes.scene2d.ui.Image; import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable; import com.badlogic.gdx.utils.Array; import org.catrobat.catroid.common.Constants; import org.catrobat.catroid.common.DroneVideoLookData; import org.catrobat.catroid.common.LookData; import org.catrobat.catroid.utils.TouchUtil; import java.util.ArrayList; import java.util.Iterator; public class Look extends Image { private static final float DEGREE_UI_OFFSET = 90.0f; private static final float COLOR_SCALE = 200.0f; private static ArrayList<Action> actionsToRestart = new ArrayList<Action>(); private boolean lookVisible = true; protected boolean imageChanged = false; protected boolean brightnessChanged = false; protected boolean colorChanged = false; protected LookData lookData; protected Sprite sprite; protected float alpha = 1f; protected float brightness = 1f; protected float hue = 0f; protected Pixmap pixmap; private ParallelAction whenParallelAction; private boolean allActionsAreFinished = false; private BrightnessContrastHueShader shader; public static final int ROTATION_STYLE_ALL_AROUND = 1; public static final int ROTATION_STYLE_LEFT_RIGHT_ONLY = 0; public static final int ROTATION_STYLE_NONE = 2; private int rotationMode = ROTATION_STYLE_ALL_AROUND; private float rotation = 90f; private float realRotation = rotation; public Look(final Sprite sprite) { this.sprite = sprite; setBounds(0f, 0f, 0f, 0f); setOrigin(0f, 0f); setScale(1f, 1f); setRotation(0f); setTouchable(Touchable.enabled); addListeners(); rotation = getDirectionInUserInterfaceDimensionUnit(); } protected void addListeners() { this.addListener(new InputListener() { @Override public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) { if (doTouchDown(x, y, pointer)) { return true; } setTouchable(Touchable.disabled); Actor target = getParent().hit(event.getStageX(), event.getStageY(), true); if (target != null) { target.fire(event); } setTouchable(Touchable.enabled); return false; } }); this.addListener(new BroadcastListener() { @Override public void handleBroadcastEvent(BroadcastEvent event, String broadcastMessage) { doHandleBroadcastEvent(broadcastMessage); } @Override public void handleBroadcastFromWaiterEvent(BroadcastEvent event, String broadcastMessage) { doHandleBroadcastFromWaiterEvent(event, broadcastMessage); } }); } public boolean isLookVisible() { return lookVisible; } public void setLookVisible(boolean lookVisible) { this.lookVisible = lookVisible; } public static boolean actionsToRestartContains(Action action) { return Look.actionsToRestart.contains(action); } public static void actionsToRestartAdd(Action action) { Look.actionsToRestart.add(action); } public Look copyLookForSprite(final Sprite cloneSprite) { Look cloneLook = cloneSprite.look; cloneLook.alpha = this.alpha; cloneLook.brightness = this.brightness; cloneLook.setLookVisible(isLookVisible()); cloneLook.whenParallelAction = null; cloneLook.allActionsAreFinished = this.allActionsAreFinished; cloneLook.setPositionInUserInterfaceDimensionUnit(this.getXInUserInterfaceDimensionUnit(), this.getYInUserInterfaceDimensionUnit()); cloneLook.setTransparencyInUserInterfaceDimensionUnit(this.getTransparencyInUserInterfaceDimensionUnit()); cloneLook.setColorInUserInterfaceDimensionUnit(this.getColorInUserInterfaceDimensionUnit()); int rotationMode = this.getRotationMode(); cloneLook.setRotationMode(rotationMode); cloneLook.setDirectionInUserInterfaceDimensionUnit(this.getDirectionInUserInterfaceDimensionUnit()); cloneLook.setBrightnessInUserInterfaceDimensionUnit(this.getBrightnessInUserInterfaceDimensionUnit()); return cloneLook; } public boolean doTouchDown(float x, float y, int pointer) { if (!isLookVisible()) { return false; } if (isFlipped()) { x = (getWidth() - 1) - x; } // We use Y-down, libgdx Y-up. This is the fix for accurate y-axis detection y = (getHeight() - 1) - y; if (x >= 0 && x < getWidth() && y >= 0 && y < getHeight() && ((pixmap != null && ((pixmap.getPixel((int) x, (int) y) & 0x000000FF) > 10)))) { if (whenParallelAction == null) { sprite.createWhenScriptActionSequence("Tapped"); } else { whenParallelAction.restart(); } return true; } return false; } public void createBrightnessContrastHueShader() { shader = new BrightnessContrastHueShader(); shader.setBrightness(brightness); shader.setHue(hue); } @Override public void draw(Batch batch, float parentAlpha) { checkImageChanged(); batch.setShader(shader); if (alpha == 0.0f) { super.setVisible(false); } else { super.setVisible(true); } if (lookData instanceof DroneVideoLookData && lookData != null) { lookData.draw(batch, alpha); } if (isLookVisible() && this.getDrawable() != null) { super.draw(batch, this.alpha); } batch.setShader(null); } @Override public void act(float delta) { Array<Action> actions = getActions(); allActionsAreFinished = false; int finishedCount = 0; for (Iterator<Action> iterator = Look.actionsToRestart.iterator(); iterator.hasNext(); ) { Action actionToRestart = iterator.next(); actionToRestart.restart(); iterator.remove(); } int n = actions.size; for (int i = 0; i < n; i++) { Action action = actions.get(i); if (action.act(delta)) { finishedCount++; } } if (finishedCount == actions.size) { allActionsAreFinished = true; } } @Override public void addAction(Action action) { super.addAction(action); allActionsAreFinished = false; } protected void checkImageChanged() { if (imageChanged) { if (lookData == null) { setBounds(getX() + getWidth() / 2f, getY() + getHeight() / 2f, 0f, 0f); setDrawable(null); imageChanged = false; return; } pixmap = lookData.getPixmap(); float newX = getX() - (pixmap.getWidth() - getWidth()) / 2f; float newY = getY() - (pixmap.getHeight() - getHeight()) / 2f; setSize(pixmap.getWidth(), pixmap.getHeight()); setPosition(newX, newY); setOrigin(getWidth() / 2f, getHeight() / 2f); if (brightnessChanged) { shader.setBrightness(brightness); brightnessChanged = false; } if (colorChanged) { shader.setHue(hue); colorChanged = false; } TextureRegion region = lookData.getTextureRegion(); TextureRegionDrawable drawable = new TextureRegionDrawable(region); setDrawable(drawable); flipLookDataIfNeeded(getRotationMode()); imageChanged = false; } } public void refreshTextures() { this.imageChanged = true; } public LookData getLookData() { return lookData; } public void setLookData(LookData lookData) { this.lookData = lookData; imageChanged = true; boolean isBackgroundLook = getZIndex() == Constants.Z_INDEX_BACKGROUND; if (isBackgroundLook) { BackgroundWaitHandler.fireBackgroundChangedEvent(lookData); } } public boolean getAllActionsAreFinished() { return allActionsAreFinished; } public String getImagePath() { String path; if (this.lookData == null) { path = ""; } else { path = this.lookData.getAbsolutePath(); } return path; } public void setWhenParallelAction(ParallelAction action) { whenParallelAction = action; } public float getXInUserInterfaceDimensionUnit() { return getX() + getWidth() / 2f; } public void setXInUserInterfaceDimensionUnit(float x) { setX(x - getWidth() / 2f); } public float getYInUserInterfaceDimensionUnit() { return getY() + getHeight() / 2f; } public void setYInUserInterfaceDimensionUnit(float y) { setY(y - getHeight() / 2f); } public float getDistanceToTouchPositionInUserInterfaceDimensions() { int touchIndex = TouchUtil.getLastTouchIndex(); return (float) Math.sqrt(Math.pow((TouchUtil.getX(touchIndex) - getXInUserInterfaceDimensionUnit()), 2) + Math.pow((TouchUtil.getY(touchIndex) - getYInUserInterfaceDimensionUnit()), 2)); } public float getAngularVelocityInUserInterfaceDimensionUnit() { // only available in physicsLook return 0; } public float getXVelocityInUserInterfaceDimensionUnit() { // only available in physicsLook return 0; } public float getYVelocityInUserInterfaceDimensionUnit() { // only available in physicsLook return 0; } public void setPositionInUserInterfaceDimensionUnit(float x, float y) { setXInUserInterfaceDimensionUnit(x); setYInUserInterfaceDimensionUnit(y); } public void changeXInUserInterfaceDimensionUnit(float changeX) { setX(getX() + changeX); } public void changeYInUserInterfaceDimensionUnit(float changeY) { setY(getY() + changeY); } public float getWidthInUserInterfaceDimensionUnit() { return getWidth() * getSizeInUserInterfaceDimensionUnit() / 100f; } public float getHeightInUserInterfaceDimensionUnit() { return getHeight() * getSizeInUserInterfaceDimensionUnit() / 100f; } public float getDirectionInUserInterfaceDimensionUnit() { return realRotation; } public void setRotationMode(int mode) { rotationMode = mode; flipLookDataIfNeeded(mode); } private void flipLookDataIfNeeded(int mode) { boolean orientedLeft = sprite.look.getDirectionInUserInterfaceDimensionUnit() < 0; boolean differentModeButFlipped = mode != Look.ROTATION_STYLE_LEFT_RIGHT_ONLY && sprite.look.isFlipped(); boolean facingLeftButNotFlipped = mode == Look.ROTATION_STYLE_LEFT_RIGHT_ONLY && orientedLeft; if (differentModeButFlipped || facingLeftButNotFlipped) { getLookData().getTextureRegion().flip(true, false); } } public int getRotationMode() { return rotationMode; } private PointF rotatePointAroundPoint(PointF center, PointF point, float rotation) { float sin = (float) Math.sin(rotation); float cos = (float) Math.cos(rotation); point.x -= center.x; point.y -= center.y; float xNew = point.x * cos - point.y * sin; float yNew = point.x * sin + point.y * cos; point.x = xNew + center.x; point.y = yNew + center.y; return point; } public Rectangle getHitbox() { float x = getXInUserInterfaceDimensionUnit() - getWidthInUserInterfaceDimensionUnit() / 2; float y = getYInUserInterfaceDimensionUnit() - getHeightInUserInterfaceDimensionUnit() / 2; float width = getWidthInUserInterfaceDimensionUnit(); float height = getHeightInUserInterfaceDimensionUnit(); float[] vertices; if (getRotation() == 0) { vertices = new float[] { x, y, x, y + height, x + width, y + height, x + width, y }; } else { PointF center = new PointF(x + width / 2f, y + height / 2f); PointF upperLeft = rotatePointAroundPoint(center, new PointF(x, y), getRotation()); PointF upperRight = rotatePointAroundPoint(center, new PointF(x, y + height), getRotation()); PointF lowerRight = rotatePointAroundPoint(center, new PointF(x + width, y + height), getRotation()); PointF lowerLeft = rotatePointAroundPoint(center, new PointF(x + width, y), getRotation()); vertices = new float[] { upperLeft.x, upperLeft.y, upperRight.x, upperRight.y, lowerRight.x, lowerRight.y, lowerLeft.x, lowerLeft.y }; } Polygon p = new Polygon(vertices); return p.getBoundingRectangle(); } public void setDirectionInUserInterfaceDimensionUnit(float degrees) { rotation = (-degrees + DEGREE_UI_OFFSET) % 360; realRotation = convertStageAngleToCatroidAngle(rotation); switch (rotationMode) { case ROTATION_STYLE_LEFT_RIGHT_ONLY: setRotation(0f); boolean orientedRight = realRotation >= 0; boolean orientedLeft = realRotation < 0; boolean needsFlipping = (isFlipped() && orientedRight) || (!isFlipped() && orientedLeft); if (needsFlipping && lookData != null) { lookData.getTextureRegion().flip(true, false); } break; case ROTATION_STYLE_ALL_AROUND: setRotation(rotation); break; case ROTATION_STYLE_NONE: setRotation(0f); break; } } public float getRealRotation() { return realRotation; } public boolean isFlipped() { return (lookData != null && lookData.getTextureRegion().isFlipX()); } public void changeDirectionInUserInterfaceDimensionUnit(float changeDegrees) { setDirectionInUserInterfaceDimensionUnit( (getDirectionInUserInterfaceDimensionUnit() + changeDegrees) % 360); } public float getSizeInUserInterfaceDimensionUnit() { return getScaleX() * 100f; } public void setSizeInUserInterfaceDimensionUnit(float percent) { if (percent < 0) { percent = 0; } setScale(percent / 100f, percent / 100f); } public void changeSizeInUserInterfaceDimensionUnit(float changePercent) { setSizeInUserInterfaceDimensionUnit(getSizeInUserInterfaceDimensionUnit() + changePercent); } public float getTransparencyInUserInterfaceDimensionUnit() { return (1f - alpha) * 100f; } public void setTransparencyInUserInterfaceDimensionUnit(float percent) { if (percent < 100.0f) { if (percent < 0f) { percent = 0f; } setVisible(true); } else { percent = 100f; setVisible(false); } alpha = (100f - percent) / 100f; } public void changeTransparencyInUserInterfaceDimensionUnit(float changePercent) { setTransparencyInUserInterfaceDimensionUnit(getTransparencyInUserInterfaceDimensionUnit() + changePercent); } public float getBrightnessInUserInterfaceDimensionUnit() { return brightness * 100f; } public void setBrightnessInUserInterfaceDimensionUnit(float percent) { if (percent < 0f) { percent = 0f; } else if (percent > 200f) { percent = 200f; } brightness = percent / 100f; brightnessChanged = true; imageChanged = true; } public void changeBrightnessInUserInterfaceDimensionUnit(float changePercent) { setBrightnessInUserInterfaceDimensionUnit(getBrightnessInUserInterfaceDimensionUnit() + changePercent); } public float getColorInUserInterfaceDimensionUnit() { return hue * COLOR_SCALE; } public void setColorInUserInterfaceDimensionUnit(float val) { val = val % COLOR_SCALE; if (val < 0) { val = COLOR_SCALE + val; } hue = ((float) val) / COLOR_SCALE; colorChanged = true; imageChanged = true; } public void changeColorInUserInterfaceDimensionUnit(float val) { setColorInUserInterfaceDimensionUnit(getColorInUserInterfaceDimensionUnit() + val); } private boolean isAngleInCatroidInterval(float catroidAngle) { return (catroidAngle > -180 && catroidAngle <= 180); } private float breakDownCatroidAngle(float catroidAngle) { catroidAngle = catroidAngle % 360; if (catroidAngle >= 0 && !isAngleInCatroidInterval(catroidAngle)) { return catroidAngle - 360; } else if (catroidAngle < 0 && !isAngleInCatroidInterval(catroidAngle)) { return catroidAngle + 360; } return catroidAngle; } protected float convertCatroidAngleToStageAngle(float catroidAngle) { catroidAngle = breakDownCatroidAngle(catroidAngle); return -catroidAngle + DEGREE_UI_OFFSET; } protected float convertStageAngleToCatroidAngle(float stageAngle) { float catroidAngle = -stageAngle + DEGREE_UI_OFFSET; return breakDownCatroidAngle(catroidAngle); } protected void doHandleBroadcastEvent(String broadcastMessage) { BroadcastHandler.doHandleBroadcastEvent(this, broadcastMessage); } protected void doHandleBroadcastFromWaiterEvent(BroadcastEvent event, String broadcastMessage) { BroadcastHandler.doHandleBroadcastFromWaiterEvent(this, event, broadcastMessage); } private class BrightnessContrastHueShader extends ShaderProgram { private static final String VERTEX_SHADER = "attribute vec4 " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" + "attribute vec4 " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" + "attribute vec2 " + ShaderProgram.TEXCOORD_ATTRIBUTE + "0;\n" + "uniform mat4 u_projTrans;\n" + "varying vec4 v_color;\n" + "varying vec2 v_texCoords;\n" + "\n" + "void main()\n" + "{\n" + " v_color = " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" + " v_texCoords = " + ShaderProgram.TEXCOORD_ATTRIBUTE + "0;\n" + " gl_Position = u_projTrans * " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" + "}\n"; private static final String FRAGMENT_SHADER = "#ifdef GL_ES\n" + " #define LOWP lowp\n" + " precision mediump float;\n" + "#else\n" + " #define LOWP\n" + "#endif\n" + "varying LOWP vec4 v_color;\n" + "varying vec2 v_texCoords;\n" + "uniform sampler2D u_texture;\n" + "uniform float brightness;\n" + "uniform float contrast;\n" + "uniform float hue;\n" + "vec3 rgb2hsv(vec3 c)\n" + "{\n" + " vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);\n" + " vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));\n" + " vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));\n" + " float d = q.x - min(q.w, q.y);\n" + " float e = 1.0e-10;\n" + " return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);\n" + "}\n" + "vec3 hsv2rgb(vec3 c)\n" + "{\n" + " vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);\n" + " vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);\n" + " return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);\n" + "}\n" + "void main()\n" + "{\n" + " vec4 color = v_color * texture2D(u_texture, v_texCoords);\n" + " color.rgb /= color.a;\n" + " color.rgb = ((color.rgb - 0.5) * max(contrast, 0.0)) + 0.5;\n" + " color.rgb += brightness;\n" + " color.rgb *= color.a;\n" + " vec3 hsv = rgb2hsv(color.rgb);\n" + " hsv.x += hue;\n" + " vec3 rgb = hsv2rgb(hsv);\n" + " gl_FragColor = vec4(rgb.r, rgb.g, rgb.b, color.a);\n" + " }"; private static final String BRIGHTNESS_STRING_IN_SHADER = "brightness"; private static final String CONTRAST_STRING_IN_SHADER = "contrast"; private static final String HUE_STRING_IN_SHADER = "hue"; public BrightnessContrastHueShader() { super(VERTEX_SHADER, FRAGMENT_SHADER); ShaderProgram.pedantic = false; if (isCompiled()) { begin(); setUniformf(BRIGHTNESS_STRING_IN_SHADER, 0.0f); setUniformf(CONTRAST_STRING_IN_SHADER, 1.0f); setUniformf(HUE_STRING_IN_SHADER, 0.0f); end(); } } public void setBrightness(float brightness) { begin(); setUniformf(BRIGHTNESS_STRING_IN_SHADER, brightness - 1f); end(); } public void setHue(float hue) { begin(); setUniformf(HUE_STRING_IN_SHADER, hue); end(); } } public Polygon[] getCurrentCollisionPolygon() { Polygon[] originalPolygons; if (getLookData() == null) { originalPolygons = new Polygon[0]; } else { if (getLookData().getCollisionInformation().collisionPolygons == null) { getLookData().getCollisionInformation().loadOrCreateCollisionPolygon(); } originalPolygons = getLookData().getCollisionInformation().collisionPolygons; } Polygon[] transformedPolygons = new Polygon[originalPolygons.length]; for (int p = 0; p < transformedPolygons.length; p++) { Polygon poly = new Polygon(originalPolygons[p].getTransformedVertices()); poly.translate(getX(), getY()); poly.setRotation(getRotation()); poly.setScale(getScaleX(), getScaleY()); poly.setOrigin(getOriginX(), getOriginY()); transformedPolygons[p] = poly; } return transformedPolygons; } }