From 03eb28f09ed3c1d158c5d2254246f10cb5c581e1 Mon Sep 17 00:00:00 2001 From: Gustl22 Date: Thu, 1 Feb 2018 22:47:52 +0100 Subject: [PATCH] Animator2: physical fling (#497) --- vtm-gdx/src/org/oscim/gdx/GestureHandler.java | 7 +- vtm-gdx/src/org/oscim/gdx/InputHandler.java | 2 +- vtm/src/org/oscim/layers/MapEventLayer.java | 14 +- vtm/src/org/oscim/layers/MapEventLayer2.java | 13 +- vtm/src/org/oscim/map/Animator.java | 26 +- vtm/src/org/oscim/map/Animator2.java | 254 ++++++++++++++++++ vtm/src/org/oscim/map/Map.java | 5 +- vtm/src/org/oscim/map/ViewController.java | 30 ++- vtm/src/org/oscim/utils/Parameters.java | 5 + .../org/oscim/utils/animation/DragForce.java | 87 ++++++ .../oscim/utils/{ => animation}/Easing.java | 2 +- 11 files changed, 413 insertions(+), 32 deletions(-) create mode 100644 vtm/src/org/oscim/map/Animator2.java create mode 100644 vtm/src/org/oscim/utils/animation/DragForce.java rename vtm/src/org/oscim/utils/{ => animation}/Easing.java (99%) diff --git a/vtm-gdx/src/org/oscim/gdx/GestureHandler.java b/vtm-gdx/src/org/oscim/gdx/GestureHandler.java index 41793dff..be87480a 100644 --- a/vtm-gdx/src/org/oscim/gdx/GestureHandler.java +++ b/vtm-gdx/src/org/oscim/gdx/GestureHandler.java @@ -21,7 +21,9 @@ import com.badlogic.gdx.input.GestureDetector.GestureListener; import com.badlogic.gdx.math.Vector2; import org.oscim.core.Tile; +import org.oscim.map.Animator2; import org.oscim.map.Map; +import org.oscim.utils.Parameters; public class GestureHandler implements GestureListener { private boolean mayFling = true; @@ -84,7 +86,10 @@ public class GestureHandler implements GestureListener { //log.debug("fling " + button + " " + velocityX + "/" + velocityY); if (mayFling && button == Buttons.LEFT) { int m = Tile.SIZE * 4; - mMap.animator().animateFling((int) velocityX, (int) velocityY, -m, m, -m, m); + if (Parameters.ANIMATOR2) + ((Animator2) mMap.animator()).animateFlingScroll(velocityX, velocityY, -m, m, -m, m); + else + mMap.animator().animateFling(velocityX, velocityY, -m, m, -m, m); return true; } return false; diff --git a/vtm-gdx/src/org/oscim/gdx/InputHandler.java b/vtm-gdx/src/org/oscim/gdx/InputHandler.java index 32c316fb..3daed996 100644 --- a/vtm-gdx/src/org/oscim/gdx/InputHandler.java +++ b/vtm-gdx/src/org/oscim/gdx/InputHandler.java @@ -31,7 +31,7 @@ import org.oscim.layers.tile.buildings.BuildingLayer; import org.oscim.map.Map; import org.oscim.map.ViewController; import org.oscim.theme.VtmThemes; -import org.oscim.utils.Easing; +import org.oscim.utils.animation.Easing; import java.util.List; diff --git a/vtm/src/org/oscim/layers/MapEventLayer.java b/vtm/src/org/oscim/layers/MapEventLayer.java index f2c1d40c..c2e7a93a 100644 --- a/vtm/src/org/oscim/layers/MapEventLayer.java +++ b/vtm/src/org/oscim/layers/MapEventLayer.java @@ -19,15 +19,19 @@ */ package org.oscim.layers; +import org.oscim.backend.CanvasAdapter; +import org.oscim.backend.Platform; import org.oscim.core.MapPosition; import org.oscim.core.Tile; import org.oscim.event.Event; import org.oscim.event.Gesture; import org.oscim.event.GestureListener; import org.oscim.event.MotionEvent; +import org.oscim.map.Animator2; import org.oscim.map.Map; import org.oscim.map.Map.InputListener; import org.oscim.map.ViewController; +import org.oscim.utils.Parameters; import static org.oscim.backend.CanvasAdapter.dpi; import static org.oscim.utils.FastMath.withinSquaredDist; @@ -469,8 +473,14 @@ public class MapEventLayer extends AbstractMapEventLayer implements InputListene int w = Tile.SIZE * 5; int h = Tile.SIZE * 5; - mMap.animator().animateFling(velocityX * 2, velocityY * 2, - -w, w, -h, h); + if (Parameters.ANIMATOR2) { + if (!CanvasAdapter.platform.isDesktop() && CanvasAdapter.platform != Platform.WEBGL) { + velocityX *= 2; + velocityY *= 2; + } + ((Animator2) mMap.animator()).animateFlingScroll(velocityX, velocityY, -w, w, -h, h); + } else + mMap.animator().animateFling(velocityX * 2, velocityY * 2, -w, w, -h, h); return true; } diff --git a/vtm/src/org/oscim/layers/MapEventLayer2.java b/vtm/src/org/oscim/layers/MapEventLayer2.java index 6c137e41..95e66eb4 100644 --- a/vtm/src/org/oscim/layers/MapEventLayer2.java +++ b/vtm/src/org/oscim/layers/MapEventLayer2.java @@ -21,14 +21,17 @@ package org.oscim.layers; import org.oscim.backend.CanvasAdapter; +import org.oscim.backend.Platform; import org.oscim.core.MapPosition; import org.oscim.core.Tile; import org.oscim.event.Event; import org.oscim.event.Gesture; import org.oscim.event.MotionEvent; +import org.oscim.map.Animator2; import org.oscim.map.Map; import org.oscim.map.Map.InputListener; import org.oscim.map.ViewController; +import org.oscim.utils.Parameters; import org.oscim.utils.async.Task; import static org.oscim.backend.CanvasAdapter.dpi; @@ -569,8 +572,14 @@ public class MapEventLayer2 extends AbstractMapEventLayer implements InputListen int w = Tile.SIZE * 5; int h = Tile.SIZE * 5; - mMap.animator().animateFling(velocityX * 2, velocityY * 2, - -w, w, -h, h); + if (Parameters.ANIMATOR2) { + if (!CanvasAdapter.platform.isDesktop() && CanvasAdapter.platform != Platform.WEBGL) { + velocityX *= 2; + velocityY *= 2; + } + ((Animator2) mMap.animator()).animateFlingScroll(velocityX, velocityY, -w, w, -h, h); + } else + mMap.animator().animateFling(velocityX * 2, velocityY * 2, -w, w, -h, h); return true; } diff --git a/vtm/src/org/oscim/map/Animator.java b/vtm/src/org/oscim/map/Animator.java index f11631fa..2f15896e 100644 --- a/vtm/src/org/oscim/map/Animator.java +++ b/vtm/src/org/oscim/map/Animator.java @@ -28,8 +28,8 @@ import org.oscim.core.MapPosition; import org.oscim.core.Point; import org.oscim.core.Tile; import org.oscim.renderer.MapRenderer; -import org.oscim.utils.Easing; import org.oscim.utils.ThreadUtils; +import org.oscim.utils.animation.Easing; import org.oscim.utils.async.Task; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,21 +48,21 @@ public class Animator { public final static int ANIM_TILT = 1 << 3; public final static int ANIM_FLING = 1 << 4; - private final Map mMap; + final Map mMap; - private final MapPosition mCurPos = new MapPosition(); - private final MapPosition mStartPos = new MapPosition(); - private final MapPosition mDeltaPos = new MapPosition(); + final MapPosition mCurPos = new MapPosition(); + final MapPosition mStartPos = new MapPosition(); + final MapPosition mDeltaPos = new MapPosition(); private final Point mScroll = new Point(); - private final Point mPivot = new Point(); + final Point mPivot = new Point(); private final Point mVelocity = new Point(); - private float mDuration = 500; - private long mAnimEnd = -1; - private Easing.Type mEasingType = Easing.Type.LINEAR; + float mDuration = 500; + long mAnimEnd = -1; + Easing.Type mEasingType = Easing.Type.LINEAR; - private int mState = ANIM_NONE; + int mState = ANIM_NONE; public Animator(Map map) { mMap = map; @@ -251,7 +251,7 @@ public class Animator { animStart(duration, ANIM_FLING, Easing.Type.SINE_OUT); } - private void animStart(float duration, int state, Easing.Type easingType) { + void animStart(float duration, int state, Easing.Type easingType) { if (!isActive()) mMap.events.fire(Map.ANIM_START, mMap.mMapPosition); mCurPos.copy(mStartPos); @@ -332,7 +332,7 @@ public class Animator { } } - private Task updateTask = new Task() { + Task updateTask = new Task() { @Override public int go(boolean canceled) { if (!canceled) @@ -341,7 +341,7 @@ public class Animator { } }; - private double doScale(ViewController v, float adv) { + double doScale(ViewController v, float adv) { double newScale = mStartPos.scale + mDeltaPos.scale * Math.sqrt(adv); v.scaleMap((float) (newScale / mCurPos.scale), diff --git a/vtm/src/org/oscim/map/Animator2.java b/vtm/src/org/oscim/map/Animator2.java new file mode 100644 index 00000000..a74d425e --- /dev/null +++ b/vtm/src/org/oscim/map/Animator2.java @@ -0,0 +1,254 @@ +/* + * Copyright 2013 Hannes Janetzek + * Copyright 2016 Stephan Leuschner + * Copyright 2016 devemux86 + * Copyright 2016 Izumi Kawashima + * Copyright 2017 Wolfgang Schramm + * Copyright 2018 Gustl22 + * + * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.oscim.map; + +import org.oscim.backend.CanvasAdapter; +import org.oscim.core.Point; +import org.oscim.core.Tile; +import org.oscim.renderer.MapRenderer; +import org.oscim.utils.ThreadUtils; +import org.oscim.utils.animation.DragForce; +import org.oscim.utils.animation.Easing; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.oscim.utils.FastMath.clamp; + +public class Animator2 extends Animator { + private static final Logger log = LoggerFactory.getLogger(Animator2.class); + + private final static int ANIM_KINETIC = 1 << 5; + + /** + * The minimum changes that are pleasant for users. + */ + private static final float DEFAULT_MIN_VISIBLE_CHANGE_PIXELS = 0.5f; + + private static final float FLING_FRICTION_MOVE = 0.9f; + + private final DragForce mFlingRotateForce = new DragForce(); + private final DragForce mFlingScaleForce = new DragForce(); + private final DragForce mFlingScrollForce = new DragForce(); + + private final Point mMovePoint = new Point(); + private final Point mScrollRatio = new Point(); + + private long mFrameStart = -1; + private float mScrollDet2D = 1f; + + public Animator2(Map map) { + super(map); + } + + /** + * @param velocityX the x velocity depends on screen resolution + * @param velocityY the y velocity depends on screen resolution + */ + public void animateFlingScroll(float velocityX, float velocityY, + int xmin, int xmax, int ymin, int ymax) { + ThreadUtils.assertMainThread(); + + if (velocityX * velocityX + velocityY * velocityY < 2048) + return; + + mMap.getMapPosition(mStartPos); + + float flingFactor = 2.0f; // Can be changed but should be standardized for all callers + float screenFactor = CanvasAdapter.DEFAULT_DPI / CanvasAdapter.dpi; + + velocityX *= screenFactor * flingFactor; + velocityY *= screenFactor * flingFactor; + velocityX = clamp(velocityX, xmin, xmax); + velocityY = clamp(velocityY, ymin, ymax); + + float sumVelocity = Math.abs(velocityX) + Math.abs(velocityY); + mScrollRatio.x = velocityX / sumVelocity; + mScrollRatio.y = velocityY / sumVelocity; + mScrollDet2D = (float) (mScrollRatio.x * mScrollRatio.x + mScrollRatio.y * mScrollRatio.y); + + mFlingScrollForce.setValueThreshold(DEFAULT_MIN_VISIBLE_CHANGE_PIXELS); + mFlingScrollForce.setFrictionScalar(FLING_FRICTION_MOVE); + mFlingScrollForce.setValueAndVelocity(0f, (float) Math.sqrt(velocityX * velocityX + velocityY * velocityY)); + + animFlingStart(ANIM_MOVE); + } + + private void animFlingStart(int state) { + if (!isActive()) + mMap.events.fire(Map.ANIM_START, mMap.mMapPosition); + mCurPos.copy(mStartPos); + mState |= ANIM_FLING | state; + mFrameStart = MapRenderer.frametime; // CurrentTimeMillis would cause negative delta + mMap.render(); + } + + /** + * @param velocityX the x velocity depends on screen resolution + * @param velocityY the y velocity depends on screen resolution + */ + public void kineticScroll(float velocityX, float velocityY, + int xmin, int xmax, int ymin, int ymax) { + ThreadUtils.assertMainThread(); + + if (velocityX * velocityX + velocityY * velocityY < 2048) + return; + + mMap.getMapPosition(mStartPos); + + float duration = 500; + + float screenFactor = CanvasAdapter.DEFAULT_DPI / CanvasAdapter.dpi; + velocityX = velocityX * screenFactor; + velocityY = velocityY * screenFactor; + velocityX = clamp(velocityX, xmin, xmax); + velocityY = clamp(velocityY, ymin, ymax); + if (Float.isNaN(velocityX) || Float.isNaN(velocityY)) { + log.debug("fling NaN!"); + return; + } + + double tileScale = mStartPos.scale * Tile.SIZE; + ViewController.applyRotation(-velocityX, -velocityY, mStartPos.bearing, mMovePoint); + mDeltaPos.setX(mMovePoint.x / tileScale); + mDeltaPos.setY(mMovePoint.y / tileScale); + + animStart(duration, ANIM_KINETIC | ANIM_MOVE, Easing.Type.SINE_OUT); + } + + /** + * called by MapRenderer at begin of each frame. + */ + @Override + void updateAnimation() { + if (mState == ANIM_NONE) + return; + + ViewController v = mMap.viewport(); + + /* cancel animation when position was changed since last + * update, i.e. when it was modified outside the animator. */ + if (v.getMapPosition(mCurPos)) { + log.debug("cancel anim - changed"); + cancel(); + return; + } + + final long currentFrametime = MapRenderer.frametime; + + if ((mState & ANIM_FLING) == 0) { + // Do predicted animations + long millisLeft = mAnimEnd - currentFrametime; + + float adv = clamp(1.0f - millisLeft / mDuration, 1E-6f, 1); + // Avoid redundant calculations in case of linear easing + if (mEasingType != Easing.Type.LINEAR) { + adv = Easing.ease(0, (long) (adv * Long.MAX_VALUE), Long.MAX_VALUE, mEasingType); + adv = clamp(adv, 0, 1); + } + + double scaleAdv = 1; + if ((mState & ANIM_SCALE) != 0) { + scaleAdv = doScale(v, adv); + } + + if ((mState & ANIM_KINETIC) != 0) { + adv = (float) Math.sqrt(adv); + } + + if ((mState & ANIM_MOVE) != 0) { + v.moveTo(mStartPos.x + mDeltaPos.x * (adv / scaleAdv), + mStartPos.y + mDeltaPos.y * (adv / scaleAdv)); + } + + if ((mState & ANIM_ROTATE) != 0) { + v.setRotation(mStartPos.bearing + mDeltaPos.bearing * adv); + } + + if ((mState & ANIM_TILT) != 0) { + v.setTilt(mStartPos.tilt + mDeltaPos.tilt * adv); + } + + if (millisLeft <= 0) { + //log.debug("animate END"); + cancel(); + } + } else { + // Do physical fling animation + long deltaT = currentFrametime - mFrameStart; + mFrameStart = currentFrametime; + boolean isAnimationFinished = true; + + if ((mState & ANIM_SCALE) != 0) { + float valueDelta = mFlingScaleForce.updateValueAndVelocity(deltaT) / 1000f; + float velocity = mFlingScaleForce.getVelocity(); + if (valueDelta != 0) { + valueDelta = valueDelta > 0 ? valueDelta + 1 : -1 / (valueDelta - 1); + v.scaleMap(valueDelta, (float) mPivot.x, (float) mPivot.y); + } + isAnimationFinished = (velocity == 0); + } + + if ((mState & ANIM_MOVE) != 0) { + float valueDelta = mFlingScrollForce.updateValueAndVelocity(deltaT); + float velocity = mFlingScrollForce.getVelocity(); + + float valFactor = (float) Math.sqrt((valueDelta * valueDelta) / mScrollDet2D); + float dx = (float) mScrollRatio.x * valFactor; + float dy = (float) mScrollRatio.y * valFactor; + + if (dx != 0 || dy != 0) { + v.moveMap(dx, dy); + } + + isAnimationFinished = isAnimationFinished && (velocity == 0); + } + + if ((mState & ANIM_ROTATE) != 0) { + float valueDelta = mFlingRotateForce.updateValueAndVelocity(deltaT); + float velocity = mFlingRotateForce.getVelocity(); + + v.rotateMap(valueDelta, (float) mPivot.x, (float) mPivot.y); + + isAnimationFinished = isAnimationFinished && (velocity == 0); + } + + /*if ((mState & ANIM_TILT) != 0) { + // Do some tilt fling + isAnimationFinished = isAnimationFinished && (velocity == 0); + }*/ + + if (isAnimationFinished) { + //log.debug("animate END"); + cancel(); + } + } + + /* remember current map position */ + final boolean changed = v.getMapPosition(mCurPos); + + if (changed) { + mMap.updateMap(true); + } else { + mMap.postDelayed(updateTask, 10); + } + } +} diff --git a/vtm/src/org/oscim/map/Map.java b/vtm/src/org/oscim/map/Map.java index d5279ab3..9ad3e330 100644 --- a/vtm/src/org/oscim/map/Map.java +++ b/vtm/src/org/oscim/map/Map.java @@ -132,7 +132,10 @@ public abstract class Map implements TaskQueue { ThreadUtils.init(); mViewport = new ViewController(); - mAnimator = new Animator(this); + if (Parameters.ANIMATOR2) + mAnimator = new Animator2(this); + else + mAnimator = new Animator(this); mLayers = new Layers(this); input = new EventDispatcher() { diff --git a/vtm/src/org/oscim/map/ViewController.java b/vtm/src/org/oscim/map/ViewController.java index af6d2a4a..44f352e2 100644 --- a/vtm/src/org/oscim/map/ViewController.java +++ b/vtm/src/org/oscim/map/ViewController.java @@ -86,12 +86,12 @@ public class ViewController extends Viewport { * @param mx the amount of pixels to move the map horizontally. * @param my the amount of pixels to move the map vertically. */ - public void moveMap(float mx, float my) { + public synchronized void moveMap(float mx, float my) { ThreadUtils.assertMainThread(); - Point p = applyRotation(mx, my); + applyRotation(mx, my, mPos.bearing, mMovePoint); double tileScale = mPos.scale * Tile.SIZE; - moveTo(mPos.x - p.x / tileScale, mPos.y - p.y / tileScale); + moveTo(mPos.x - mMovePoint.x / tileScale, mPos.y - mMovePoint.y / tileScale); } /* used by MapAnimator */ @@ -120,18 +120,26 @@ public class ViewController extends Viewport { mPos.y = mMinY; } - private synchronized Point applyRotation(double mx, double my) { - if (mPos.bearing == 0) { - mMovePoint.x = mx; - mMovePoint.y = my; + /** + * @param mx the amount of pixels to move the map horizontally. + * @param my the amount of pixels to move the map vertically. + * @param bearing the bearing to rotate the map. + * @param out the position where to move. + */ + public static void applyRotation(double mx, double my, float bearing, Point out) { + if (out == null) + out = new Point(); + + if (bearing == 0) { + out.x = mx; + out.y = my; } else { - double rad = Math.toRadians(mPos.bearing); + double rad = Math.toRadians(bearing); double rcos = Math.cos(rad); double rsin = Math.sin(rad); - mMovePoint.x = mx * rcos + my * rsin; - mMovePoint.y = mx * -rsin + my * rcos; + out.x = mx * rcos + my * rsin; + out.y = mx * -rsin + my * rcos; } - return mMovePoint; } /** diff --git a/vtm/src/org/oscim/utils/Parameters.java b/vtm/src/org/oscim/utils/Parameters.java index 486c64bb..b60073f4 100644 --- a/vtm/src/org/oscim/utils/Parameters.java +++ b/vtm/src/org/oscim/utils/Parameters.java @@ -16,6 +16,11 @@ package org.oscim.utils; public final class Parameters { + /** + * If true the Animator2 will be used instead of default Animator. + */ + public static boolean ANIMATOR2 = false; + /** * Allow custom tile size instead of the calculated one. */ diff --git a/vtm/src/org/oscim/utils/animation/DragForce.java b/vtm/src/org/oscim/utils/animation/DragForce.java new file mode 100644 index 00000000..93ed4fb7 --- /dev/null +++ b/vtm/src/org/oscim/utils/animation/DragForce.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.oscim.utils.animation; + +/** + * See https://developer.android.com/reference/android/support/animation/FlingAnimation.html + * Class android.support.animation.FlingAnimation + */ +public final class DragForce { + + private static final float DEFAULT_FRICTION = -4.2f; + private static final float DEFAULT_MIN_VISIBLE_CHANGE_PIXELS = 0.5f; + + // This multiplier is used to calculate the velocity threshold given a certain value + // threshold. The idea is that if it takes >= 1 frame to move the value threshold amount, + // then the velocity is a reasonable threshold. + private static final float VELOCITY_THRESHOLD_MULTIPLIER = 1000f / 16f; // 1 frame ≙ 16 ms (62.5 fps) + private float mFriction = DEFAULT_FRICTION; + private float mVelocityThreshold = DEFAULT_MIN_VISIBLE_CHANGE_PIXELS * VELOCITY_THRESHOLD_MULTIPLIER; + + // Internal state to hold a value/velocity pair. + private float mValue; + private float mVelocity; + + public void setFrictionScalar(float frictionScalar) { + mFriction = frictionScalar * DEFAULT_FRICTION; + } + + public float getFrictionScalar() { + return mFriction / DEFAULT_FRICTION; + } + + /** + * Updates the animation state (i.e. value and velocity). + * + * @param deltaT time elapsed in millisecond since last frame + * @return the value delta since last frame + */ + public float updateValueAndVelocity(long deltaT) { + float velocity = mVelocity; + mVelocity = (float) (velocity * Math.exp((deltaT / 1000f) * mFriction)); + float valueDelta = (mVelocity - velocity); + mValue += valueDelta; + if (isAtEquilibrium(mValue, mVelocity)) { + mVelocity = 0f; + } + return valueDelta; + } + + public void setValueAndVelocity(float value, float velocity) { + mValue = value; + mVelocity = velocity; + } + + public float getValue() { + return mValue; + } + + public float getVelocity() { + return mVelocity; + } + + public float getAcceleration(float position, float velocity) { + return velocity * mFriction; + } + + public boolean isAtEquilibrium(float value, float velocity) { + return Math.abs(velocity) < mVelocityThreshold; + } + + public void setValueThreshold(float threshold) { + mVelocityThreshold = threshold * VELOCITY_THRESHOLD_MULTIPLIER; + } +} diff --git a/vtm/src/org/oscim/utils/Easing.java b/vtm/src/org/oscim/utils/animation/Easing.java similarity index 99% rename from vtm/src/org/oscim/utils/Easing.java rename to vtm/src/org/oscim/utils/animation/Easing.java index b6f3d4de..c5c34ed7 100644 --- a/vtm/src/org/oscim/utils/Easing.java +++ b/vtm/src/org/oscim/utils/animation/Easing.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU Lesser General Public License along with * this program. If not, see . */ -package org.oscim.utils; +package org.oscim.utils.animation; import static org.oscim.utils.FastMath.clamp;