aboutsummaryrefslogtreecommitdiffstats
path: root/src/graphui/classes/com/jogamp/graph/ui/AnimGroup.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/graphui/classes/com/jogamp/graph/ui/AnimGroup.java')
-rw-r--r--src/graphui/classes/com/jogamp/graph/ui/AnimGroup.java812
1 files changed, 812 insertions, 0 deletions
diff --git a/src/graphui/classes/com/jogamp/graph/ui/AnimGroup.java b/src/graphui/classes/com/jogamp/graph/ui/AnimGroup.java
new file mode 100644
index 000000000..2f3d2bf07
--- /dev/null
+++ b/src/graphui/classes/com/jogamp/graph/ui/AnimGroup.java
@@ -0,0 +1,812 @@
+/**
+ * Copyright 2010-2023 JogAmp Community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of
+ * conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list
+ * of conditions and the following disclaimer in the documentation and/or other materials
+ * provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of JogAmp Community.
+ */
+package com.jogamp.graph.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+import com.jogamp.common.os.Clock;
+import com.jogamp.graph.curve.opengl.GLRegion;
+import com.jogamp.graph.curve.opengl.RegionRenderer;
+import com.jogamp.graph.font.Font;
+import com.jogamp.graph.font.Font.Glyph;
+import com.jogamp.graph.geom.plane.AffineTransform;
+import com.jogamp.graph.ui.Group.Layout;
+import com.jogamp.graph.ui.shapes.GlyphShape;
+import com.jogamp.opengl.GL2ES2;
+import com.jogamp.opengl.GLProfile;
+import com.jogamp.opengl.fixedfunc.GLMatrixFunc;
+import com.jogamp.opengl.math.FloatUtil;
+import com.jogamp.opengl.math.Quaternion;
+import com.jogamp.opengl.math.Recti;
+import com.jogamp.opengl.math.Vec2f;
+import com.jogamp.opengl.math.Vec3f;
+import com.jogamp.opengl.math.Vec4f;
+import com.jogamp.opengl.math.geom.AABBox;
+import com.jogamp.opengl.util.PMVMatrix;
+
+/**
+ * Group of animated {@link Shape}s including other static {@link Shape}s, optionally utilizing a {@link Group.Layout}.
+ * @see Scene
+ * @see Shape
+ * @see Group.Layout
+ */
+public class AnimGroup extends Group {
+ private static final boolean DEBUG = false;
+ /** Epsilon of position, 5000 x {@link FloatUtil#EPSILON} */
+ public static final float POS_EPS = FloatUtil.EPSILON * 5000; // ~= 0.0005960
+ /** Epsilon of rotation [radian], 0.5 degrees or 0.008726646 radians */
+ public static final float ROT_EPS = FloatUtil.adegToRad(0.5f); // 1 adeg ~= 0.01745 rad
+
+ private volatile long tstart_us = 0;
+ private volatile long tlast_us = 0;
+ private volatile long tpause_us = 0;
+ private volatile boolean tickOnDraw = true;
+ private volatile boolean tickPaused = false;
+ private long frame_count = 0;
+
+ /** Animation {@link Shapes} data covering one {@link Shape} of {@link Set}. */
+ public static final class ShapeData {
+ /** Indicator whether the {@link Shapes} is animating or not. */
+ public boolean active;
+ /** {@link Shapes} scaled start position */
+ public final Vec3f startPos;
+ /** {@link Shapes} scaled target position */
+ public final Vec3f targetPos;
+ /** The {@link Shapes} */
+ public final Shape shape;
+ /** Optional user attachment per {@link Shape} to be used within {@link LerpFunc}. */
+ public Object user;
+
+ /** New instance with set {@link Shape} using its scaled {@link Shape#getPosition()} for {@link #startPos} and {@link #targetPos}. */
+ public ShapeData(final Shape s) {
+ active = true;
+ startPos = new Vec3f( s.getPosition() );
+ targetPos = startPos.copy();
+ shape = s;
+ user = null;
+ }
+ }
+
+ /** Animation-Set covering its {@link ShapeData} elements, {@link LerpFunc} and animation parameter. */
+ public static final class Set {
+ /** Pixel per millimeter */
+ public final float pixPerMM;
+ /** Pixel per shape unit */
+ public final Vec2f pixPerShapeUnit;
+ /** Reference {@link Shape} giving reference size */
+ public final Shape refShape;
+
+ /** Translation acceleration in [m]/[s*s] */
+ public final float accel;
+ /** Translation acceleration in [shapeUnit]/[s*s] */
+ public final float accel_obj;
+ /** Start translation velocity in [m]/[s] */
+ public final float start_velocity;
+ /** Start translation velocity in [shapeUnit]/[s] */
+ public final float start_velocity_obj;
+ /** Current translation velocity in [m]/[s] */
+ public float velocity;
+ /** Current translation velocity in [shapeUnit]/[s] */
+ public float velocity_obj;
+
+ /** Angular acceleration in [radians]/[s*s] */
+ public final float ang_accel;
+ /** Start angular velocity in [radians]/[s] */
+ public final float start_ang_velo;
+ /** Current angular velocity in [radians]/[s] */
+ public float ang_velo;
+
+ /** {@link LerpFunc} function */
+ public final LerpFunc lerp;
+
+ /** All {@link Shape}s wrapped within {@link ShapeData}. */
+ public final List<ShapeData> allShapes;
+
+ /** Unscaled bounds of {@link #allShapes} at their original position, size and rotation. */
+ public final AABBox sourceBounds;
+
+ private Set(final float pixPerMM, final float[/*2*/] pixPerShapeUnit, final Shape refShape,
+ final float accel, final float velocity,
+ final float ang_accel, final float ang_velo,
+ final List<ShapeData> allShapes, final AABBox sourceBounds,
+ final LerpFunc lerp) {
+ this.pixPerMM = pixPerMM;
+ this.pixPerShapeUnit = new Vec2f( pixPerShapeUnit );
+ this.refShape = refShape;
+ this.accel = accel;
+ this.start_velocity = velocity;
+ this.velocity = velocity;
+ {
+ final float accel_px = accel * 1e3f * pixPerMM; // [px]/[s*s]
+ this.accel_obj = accel_px / this.pixPerShapeUnit.x(); // [shapeUnit]/[s*s]
+
+ final float velocity_px = velocity * 1e3f * pixPerMM; // [px]/[s]
+ this.start_velocity_obj = velocity_px / this.pixPerShapeUnit.x(); // [shapeUnit]/[s]
+ this.velocity_obj = this.start_velocity_obj;
+ }
+ this.ang_accel = ang_accel;
+ this.start_ang_velo = ang_velo;
+ this.ang_velo = ang_velo;
+ this.lerp = lerp;
+ this.allShapes = allShapes;
+ this.sourceBounds = sourceBounds;
+ }
+
+ /**
+ * Adds given {@link Shape} to this {@link Set} and its {@link AnimGroup} wrapping it in {@link ShapeData}.
+ * <p>
+ * Also issues {@link ShapeSetup#setup(Set, int, ShapeData)}.
+ * </p>
+ * @return newly created {@link ShapeData}
+ */
+ public ShapeData addShape(final AnimGroup g, final Shape s, final ShapeSetup op) {
+ final ShapeData sd = new ShapeData(s);
+ final int idx = this.allShapes.size();
+ this.allShapes.add( sd );
+ this.sourceBounds.resize(sd.shape.getBounds());
+ op.setup(this, idx, sd);
+ g.addShape(sd.shape);
+ return sd;
+ }
+
+ /**
+ * Removes given {@link ShapeData} from this {@link Set} and its {@link AnimGroup}.
+ * <p>
+ * Also destroys the {@link ShapeData}, including its {@link ShapeData} and their {@link Shape}.
+ * </p>
+ */
+ public void removeShape(final AnimGroup g, final GL2ES2 gl, final RegionRenderer renderer, final ShapeData sd) {
+ g.removeShape(gl, renderer, sd.shape);
+ sd.active = false;
+ allShapes.remove(sd);
+ }
+
+ /**
+ * Removes all {@link ShapeData} from this {@link Set} and its {@link AnimGroup}.
+ * <p>
+ * Also destroys the {@link ShapeData}, including its {@link ShapeData} and their {@link Shape}.
+ * </p>
+ */
+ public void removeShapes(final AnimGroup g, final GL2ES2 gl, final RegionRenderer renderer) {
+ for(final ShapeData sd : allShapes) {
+ g.removeShape(gl, renderer, sd.shape);
+ sd.active = false;
+ }
+ allShapes.clear();
+ }
+
+ /** Removes this {@link Set} from its {@link AnimGroup} and destroys it, including its {@link ShapeData} and their {@link Shape}. */
+ private void remove(final AnimGroup g, final GL2ES2 gl, final RegionRenderer renderer) {
+ removeShapes(g, gl, renderer);
+ refShape.destroy(gl, renderer);
+ }
+
+ public void setAnimationActive(final boolean v) {
+ for(final ShapeData sd : allShapes) {
+ sd.active = v;
+ }
+ }
+ public boolean isAnimationActive() {
+ for(final ShapeData sd : allShapes) {
+ if( sd.active ) { return true; }
+ }
+ return false;
+ }
+ }
+ private final List<Set> animSets = new ArrayList<Set>();
+
+ /**
+ * Create a group of animated {@link Shape}s including other static {@link Shape}s w/ given {@link Group.Layout}.
+ * <p>
+ * Default is non-interactive, see {@link #setInteractive(boolean)}.
+ * </p>
+ * @param l optional {@link Layout}, maybe {@code null}
+ */
+ public AnimGroup(final Layout l) {
+ super(l);
+ }
+
+ /** Return the {@link Set} at given index or {@code null} if n/a. */
+ public Set getAnimSet(final int idx) {
+ if( idx < animSets.size() ) {
+ return animSets.get(idx);
+ }
+ return null;
+ }
+
+ /** Removes all {@link Set}s and destroys them, including all {@link ShapeData} and their {@link Shape}s. */
+ public final void removeAllAnimSets(final GL2ES2 gl, final RegionRenderer renderer) {
+ for(final Set as : animSets) {
+ as.remove(this, gl, renderer);
+ }
+ animSets.clear();
+ }
+
+ /** Removes the given {@link Set} and destroys it, including its {@link ShapeData} and {@link Shape}. */
+ public final void removeAnimSet(final GL2ES2 gl, final RegionRenderer renderer, final Set as) {
+ if( null != as ) {
+ as.remove(this, gl, renderer);
+ animSets.remove(as);
+ }
+ }
+
+ /** Removes the given {@link Set}s and destroys them, including their {@link ShapeData} and {@link Shape}. */
+ public final void removeAnimSets(final GL2ES2 gl, final RegionRenderer renderer, final List<Set> asList) {
+ for(final Set as : asList) {
+ if( null != as ) {
+ as.remove(this, gl, renderer);
+ animSets.remove(as);
+ }
+ }
+ }
+
+ /**
+ * {@link ShapeData} setup function for animation using its enclosing {@link Set} and other data points
+ * <p>
+ * At minimum, {@link ShapeData}'s {@link ShapeData#startPos} and {@link ShapeData#targetPos} shall be adjusted.
+ * </p>
+ */
+ public static interface ShapeSetup {
+ /**
+ * Setting up the {@link ShapeData} for animation using its enclosing {@link Set} and other data points
+ * @param as {@link Set} of the animation
+ * @param idx {@link ShapeData} index within the {@link Set#allShapes}
+ * @param sd the {@link ShapeData} matching {@code idx} containing the {@link Shape} to apply this operation
+ */
+ public void setup(final Set as, final int idx, final ShapeData sd);
+ }
+
+ /**
+ * Linear interpolation (LERP) function to evaluate the next animated frame for each {@link ShapeData} of a {@link Set}.
+ * @see AnimGroup.TargetLerp
+ */
+ public static interface LerpFunc {
+ /**
+ * Evaluate next LERP step for the given {@link ShapeData} within the animation {@link Set}.
+ * @param frame_cnt frame count for the given {@link ShapeData}
+ * @param as {@link Set} of the animation
+ * @param idx {@link ShapeData} index within the {@link Set#allShapes}
+ * @param sd the {@link ShapeData} matching {@code idx} containing the {@link Shape} to apply this operation
+ * @param at_s time delta to animation start, i.e. animation duration [s]
+ * @param dt_s time delta to last call [s]
+ * @return true if target animation shall continue, false otherwise
+ */
+ public boolean eval(long frame_cnt, Set as, final int idx, ShapeData sd, float at_s, float dt_s);
+ }
+
+ /**
+ * Add a new {@link Set} with an empty {@link ShapeData} container.
+ * <p>
+ * The given {@link PMVMatrix} has to be setup properly for this object,
+ * i.e. its {@link GLMatrixFunc#GL_PROJECTION} and {@link GLMatrixFunc#GL_MODELVIEW} for the surrounding scene
+ * only, without a shape's {@link #setTransform(PMVMatrix)}. See {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti)}.
+ * </p>
+ * @param pixPerMM monitor pixel per millimeter for accurate animation
+ * @param glp used {@link GLProfile}
+ * @param pmv well formed {@link PMVMatrix}, e.g. could have been setup via {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti)}.
+ * @param viewport the int[4] viewport
+ * @param accel translation acceleration in [m]/[s*s]
+ * @param velocity translation velocity in [m]/[s]
+ * @param ang_accel angular acceleration in [radians]/[s*s], usable for rotation etc
+ * @param ang_velo angular velocity in [radians]/[s], usable for rotation etc
+ * @param lerp {@link LerpFunc} function, see {@link AnimGroup.TargetLerp}
+ * @param refShape reference {@link Shape} giving reference size, see {@link #refShape}
+ * @param op {@link ShapeData} setup function for {@link ShapeData#startPos} and {@link ShapeData#targetPos}
+ * @return a new {@link Set} instance
+ */
+ public Set addAnimSet(final float pixPerMM,
+ final GLProfile glp, final PMVMatrix pmv, final Recti viewport,
+ final float accel, final float velocity,
+ final float ang_accel, final float ang_velo,
+ final LerpFunc lerp, final Shape refShape)
+ {
+ final Set as;
+ refShape.validate(glp);
+ pmv.glPushMatrix();
+ {
+ refShape.setTransform(pmv);
+ as = new Set(pixPerMM, refShape.getPixelPerShapeUnit(pmv, viewport, new float[2]), refShape,
+ accel, velocity, ang_accel, ang_velo,
+ new ArrayList<ShapeData>(), new AABBox(), lerp);
+ }
+ pmv.glPopMatrix();
+ animSets.add(as);
+ return as;
+ }
+
+ /**
+ * Add a new {@link Set} with {@link ShapeData} for each {@link GlyphShape}, moving towards its target position
+ * using a generic displacement via {@link ShapeSetup} to determine each {@link ShapeData}'s starting position.
+ * <p>
+ * The given {@link PMVMatrix} has to be setup properly for this object,
+ * i.e. its {@link GLMatrixFunc#GL_PROJECTION} and {@link GLMatrixFunc#GL_MODELVIEW} for the surrounding scene
+ * only, without a shape's {@link #setTransform(PMVMatrix)}. See {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti)}.
+ * </p>
+ * @param pixPerMM monitor pixel per millimeter for accurate animation
+ * @param glp used {@link GLProfile}
+ * @param pmv well formed {@link PMVMatrix}, e.g. could have been setup via {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti)}.
+ * @param viewport the int[4] viewport
+ * @param renderModes used {@link GLRegion#create(GLProfile, int, com.jogamp.opengl.util.texture.TextureSequence) region render-modes}
+ * @param font {@link Font} to be used for resulting {@link GlyphShape}s
+ * @param refChar reference character to calculate the reference {@link GlyphShape}
+ * @param text the text for resulting {@link GlyphShape}s
+ * @param fontScale font scale factor for resulting {@link GlyphShape}s
+ * @param accel translation acceleration in [m]/[s*s]
+ * @param velocity translation velocity in [m]/[s]
+ * @param ang_accel angular acceleration in [radians]/[s*s], usable for rotation etc
+ * @param ang_velo angular velocity in [radians]/[s], usable for rotation etc
+ * @param lerp {@link LerpFunc} function, see {@link AnimGroup.TargetLerp}
+ * @param op {@link ShapeData} setup function for {@link ShapeData#startPos} and {@link ShapeData#targetPos}
+ * @return newly created and added {@link Set}
+ */
+ public final Set addGlyphSet(final float pixPerMM,
+ final GLProfile glp, final PMVMatrix pmv, final Recti viewport, final int renderModes,
+ final Font font, final char refChar, final CharSequence text, final float fontScale,
+ final float accel, final float velocity, final float ang_accel, final float ang_velo,
+ final LerpFunc lerp, final ShapeSetup op)
+ {
+ final Set as;
+ {
+ final List<ShapeData> allShapes = new ArrayList<ShapeData>();
+ final AABBox sourceBounds = processString(allShapes, renderModes, font, fontScale, text);
+ final GlyphShape refShape = new GlyphShape(renderModes, font, refChar, 0, 0);
+ refShape.setScale(fontScale, fontScale, 1f);
+ refShape.validate(glp);
+ pmv.glPushMatrix();
+ {
+ refShape.setTransform(pmv);
+ as = new Set(pixPerMM, refShape.getPixelPerShapeUnit(pmv, viewport, new float[2]), refShape,
+ accel, velocity, ang_accel, ang_velo, allShapes, sourceBounds, lerp);
+ }
+ pmv.glPopMatrix();
+ }
+ animSets.add(as);
+
+ for (int idx = 0; idx < as.allShapes.size(); ++idx) {
+ final ShapeData sd = as.allShapes.get(idx);
+ op.setup(as, idx, sd);
+ super.addShape(sd.shape);
+ }
+ if( DEBUG ) {
+ System.err.println("addAnimShapes: AnimSet.sourceBounds = "+as.sourceBounds);
+ }
+ resetAnimation();
+ return as;
+ }
+ private static final AABBox processString(final List<ShapeData> res, final int renderModes,
+ final Font font, final float fontScale, final CharSequence text)
+ {
+ final Font.GlyphVisitor fgv = new Font.GlyphVisitor() {
+ @Override
+ public void visit(final char symbol, final Glyph glyph, final AffineTransform t) {
+ if( !glyph.isWhiteSpace() && null != glyph.getShape() ) {
+ final GlyphShape gs = new GlyphShape(renderModes, symbol, glyph, t.getTranslateX(), t.getTranslateY());
+ gs.setScale(fontScale, fontScale, 1f);
+ gs.moveTo(gs.getOrigPos().x()*fontScale, gs.getOrigPos().y()*fontScale, gs.getOrigPos().z());
+ res.add( new ShapeData( gs ) );
+ }
+ }
+ };
+ return font.processString(fgv, null, text, new AffineTransform(), new AffineTransform());
+ }
+
+ /**
+ * Add a new {@link Set} with {@link ShapeData} for each {@link GlyphShape}, moving towards its target position
+ * using a fixed displacement function, defining each {@link ShapeData}'s starting position.
+ * <p>
+ * The start-position is randomly chosen within given {@link AABBox} glyphBox.
+ * </p>
+ * <p>
+ * The given {@link PMVMatrix} has to be setup properly for this object,
+ * i.e. its {@link GLMatrixFunc#GL_PROJECTION} and {@link GLMatrixFunc#GL_MODELVIEW} for the surrounding scene
+ * only, without a shape's {@link #setTransform(PMVMatrix)}. See {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti)}.
+ * </p>
+ * @param pixPerMM monitor pixel per millimeter for accurate animation
+ * @param glp used {@link GLProfile}
+ * @param pmv well formed {@link PMVMatrix}, e.g. could have been setup via {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti)}.
+ * @param viewport the int[4] viewport
+ * @param renderModes used {@link GLRegion#create(GLProfile, int, com.jogamp.opengl.util.texture.TextureSequence) region render-modes}
+ * @param font {@link Font} to be used for resulting {@link GlyphShape}s
+ * @param text the text for resulting {@link GlyphShape}s
+ * @param fontScale font scale factor for resulting {@link GlyphShape}s
+ * @param fgCol foreground color for resulting {@link GlyphShape}s
+ * @param accel translation acceleration in [m]/[s*s]
+ * @param velocity translation velocity in [m]/[s]
+ * @param ang_accel angular acceleration in [radians]/[s*s], usable for rotation etc
+ * @param ang_velo angular velocity in [radians]/[s], usable for rotation etc
+ * @param animBox {@link AABBox} denoting the maximum extend of {@link ShapeData}s start-position, also used for their x-offset
+ * @param z_only Pass true for z-only distance
+ * @param random the random float generator
+ * @param lerp {@link LerpFunc} function, see {@link AnimGroup.TargetLerp}
+ * @return newly created and added {@link Set}
+ */
+ public final Set addGlyphSetRandom01(final float pixPerMM,
+ final GLProfile glp, final PMVMatrix pmv, final Recti viewport, final int renderModes,
+ final Font font, final CharSequence text, final float fontScale, final Vec4f fgCol,
+ final float accel, final float velocity, final float ang_accel, final float ang_velo,
+ final AABBox animBox, final boolean z_only, final Random random, final LerpFunc lerp)
+ {
+ return addGlyphSet(pixPerMM, glp, pmv, viewport, renderModes, font, 'X', text, fontScale,
+ accel, velocity, ang_accel, ang_velo, lerp, (final Set as, final int idx, final ShapeData sd) -> {
+ sd.shape.setColor(fgCol);
+
+ // shift targetPost to glyphBox.getMinX()
+ sd.targetPos.add(animBox.getMinX(), 0f, 0f);
+
+ final Vec3f target = sd.targetPos;
+
+ sd.startPos.set( z_only ? target.x() : animBox.getMinX() + random.nextFloat() * animBox.getWidth(),
+ z_only ? target.y() : animBox.getMinY() + random.nextFloat() * animBox.getHeight(),
+ 0f + random.nextFloat() * animBox.getHeight() * 1f);
+ sd.shape.moveTo(sd.startPos);
+ } );
+ }
+
+ /**
+ * Add a new {@link Set} with {@link ShapeData} for each {@link GlyphShape}, implementing<br/>
+ * horizontal continuous scrolling while repeating the given {@code text}.
+ * <p>
+ * The given {@link PMVMatrix} has to be setup properly for this object,
+ * i.e. its {@link GLMatrixFunc#GL_PROJECTION} and {@link GLMatrixFunc#GL_MODELVIEW} for the surrounding scene
+ * only, without a shape's {@link #setTransform(PMVMatrix)}. See {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti)}.
+ * </p>
+ * @param pixPerMM monitor pixel per millimeter for accurate animation
+ * @param glp used {@link GLProfile}
+ * @param pmv well formed {@link PMVMatrix}, e.g. could have been setup via {@link Scene.PMVMatrixSetup#set(PMVMatrix, Recti)}.
+ * @param viewport the int[4] viewport
+ * @param renderModes used {@link GLRegion#create(GLProfile, int, com.jogamp.opengl.util.texture.TextureSequence) region render-modes}
+ * @param font {@link Font} to be used for resulting {@link GlyphShape}s
+ * @param text the text for resulting {@link GlyphShape}s
+ * @param fontScale font scale factor for resulting {@link GlyphShape}s
+ * @param fgCol foreground color for resulting {@link GlyphShape}s
+ * @param velocity translation velocity in [m]/[s]
+ * @param animBox {@link AABBox} denoting the maximum extend of {@link ShapeData}s start-position, also used for their x-offset
+ * @return newly created and added {@link Set}
+ */
+ public final Set addGlyphSetHorizScroll01(final float pixPerMM,
+ final GLProfile glp, final PMVMatrix pmv, final Recti viewport, final int renderModes,
+ final Font font, final CharSequence text, final float fontScale, final Vec4f fgCol,
+ final float velocity, final AABBox animBox, final float y_offset)
+ {
+ return addGlyphSet(pixPerMM, glp, pmv, viewport,
+ renderModes, font, 'X', text, fontScale,
+ 0f /* accel */, velocity, 0f /* ang_accel */, 0f /* 1-rotation/s */,
+ new AnimGroup.ScrollLerp(animBox),
+ (final AnimGroup.Set as, final int idx, final AnimGroup.ShapeData sd) -> {
+ sd.shape.setColor(fgCol);
+
+ sd.targetPos.set(animBox.getMinX(), y_offset, 0);
+
+ sd.startPos.set( sd.startPos.x() + animBox.getMaxX(), sd.targetPos.y(), sd.targetPos.z());
+
+ sd.shape.moveTo( sd.startPos );
+ } );
+ }
+
+ /** Sets whether {@link #tick()} shall be automatic issued on {@link #draw(GL2ES2, RegionRenderer, int[])}, default is {@code true}. */
+ public final void setTickOnDraw(final boolean v) { tickOnDraw = v; }
+ public final boolean getTickOnDraw() { return tickOnDraw; }
+
+ /**
+ * Sets whether {@link #tick()} shall be paused, default is {@code false}.
+ * <p>
+ * Unpausing {@link #tick()} will also forward animation start-time about paused duration,
+ * as well as set last-tick timestamp to now. This prevents animation artifacts and resumes where left off.
+ * </p>
+ */
+ public final void setTickPaused(final boolean v) {
+ if( tickPaused == v ) {
+ return;
+ }
+ if( v ) {
+ tickPaused = true;
+ tpause_us = Clock.currentNanos() / 1000; // [us]
+ } else {
+ final long tnow_us = Clock.currentNanos() / 1000; // [us]
+ final long dtP_us = tnow_us - tpause_us;
+ tstart_us += dtP_us;
+ tlast_us += dtP_us;
+ tickPaused = false;
+ }
+ }
+ public final boolean getTickPaused() { return tickPaused; }
+
+ @Override
+ public void draw(final GL2ES2 gl, final RegionRenderer renderer, final int[] sampleCount) {
+ if( tickOnDraw && !tickPaused) {
+ tickImpl();
+ }
+ super.draw(gl, renderer, sampleCount);
+ }
+
+ public final void resetAnimation() {
+ tstart_us = Clock.currentNanos() / 1000; // [us]
+ tlast_us = tstart_us;
+ frame_count = 0;
+ }
+
+ public final void restartAnimation() {
+ super.runSynced( () -> {
+ for(final Set as : animSets) {
+ as.setAnimationActive(true);
+ } } );
+ resetAnimation();
+ }
+
+ public void stopAnimation() {
+ super.runSynced( () -> {
+ for(final Set as : animSets) {
+ as.setAnimationActive(false);
+ } } );
+ }
+
+ public final boolean isAnimationActive() {
+ for(final Set as : animSets) {
+ if( as.isAnimationActive() ) { return true; }
+ }
+ return false;
+ }
+
+ /**
+ * Issues an animation tick, usually done at {@link #draw(GL2ES2, RegionRenderer, int[])}.
+ * @see #setTickOnDraw(boolean)
+ * @see #setTickPaused(boolean)
+ */
+ public final void tick() {
+ if( !tickPaused ) {
+ super.runSynced( () -> { tickImpl(); } );
+ }
+ }
+ private final void tickImpl() {
+ final long tnow_us = Clock.currentNanos() / 1000;
+ final float at_s = (tnow_us - tstart_us) / 1e6f;
+ final float dt_s = (tnow_us - tlast_us) / 1e6f;
+ tlast_us = tnow_us;
+ for(final Set as : animSets) {
+ if( as.isAnimationActive() ) {
+ if( !FloatUtil.isZero( as.accel ) ) {
+ as.velocity += as.accel * dt_s; // [shapeUnit]/[s]
+ as.velocity_obj += as.accel_obj * dt_s; // [shapeUnit]/[s]
+ }
+ if( !FloatUtil.isZero( as.ang_accel ) ) {
+ as.ang_velo += as.ang_accel * dt_s; // [radians]/[s]
+ }
+ for (int idx = 0; idx < as.allShapes.size(); ++idx) {
+ final ShapeData sd = as.allShapes.get(idx);
+ if( !as.lerp.eval(frame_count, as, idx, sd, at_s, dt_s) ) {
+ sd.active = false;
+ }
+ }
+ }
+ }
+ ++frame_count;
+ }
+
+ /**
+ * Default target {@link LerpFunc}, approaching {@link ShapeData}'s target position inclusive angular rotation around given normalized axis.
+ * <p>
+ * Implementation uses the current shape position and time delta since last call,
+ * hence allows rugged utilization even if shapes are dragged around.
+ * </p>
+ */
+ public static class TargetLerp implements LerpFunc {
+ final Vec3f rotAxis;
+ /**
+ * New target {@link LerpFunc} instance
+ * @param rotAxis normalized axis vector for {@link Quaternion#rotateByAngleNormalAxis(float, Vec3f)}
+ */
+ public TargetLerp(final Vec3f rotAxis) {
+ this.rotAxis = rotAxis;
+ }
+ @Override
+ public boolean eval(final long frame_cnt, final Set as, final int idx, final ShapeData sd, final float at_s, final float dt_s) {
+ final float dxy = as.velocity_obj * dt_s; // [shapeUnit]
+ final float rot_step = as.ang_velo * dt_s; // [radians]
+ final float shapeScale = sd.shape.getScale().y();
+ final Vec3f pos = sd.shape.getPosition().copy();
+ final Vec3f p_t = sd.targetPos.minus(pos);
+ final float p_t_diff = p_t.length();
+ final Quaternion q = sd.shape.getRotation();
+ final Vec3f euler = q.toEuler(new Vec3f());
+ final float rotAng = euler.length();
+ final float rotAngDiff = Math.min(Math.abs(rotAng), FloatUtil.TWO_PI - Math.abs(rotAng));
+ final boolean pos_ok = p_t_diff <= AnimGroup.POS_EPS;
+ final boolean pos_near = pos_ok || p_t_diff <= sd.shape.getBounds().getSize() * shapeScale * 2f;
+ final boolean rot_ok = pos_near && ( rot_step < AnimGroup.ROT_EPS || rotAngDiff <= AnimGroup.ROT_EPS || rotAngDiff <= rot_step * 2f );
+ if ( pos_ok && rot_ok ) {
+ // arrived
+ if( DEBUG ) {
+ if( 0 == idx ) {
+ System.err.println("F: dt "+(dt_s*1000f)+" ms, p_t[OK "+pos_ok+", near "+pos_near+", diff: "+p_t_diff+", dxy "+dxy+"], rot[OK "+rot_ok+", radY "+rotAng+" ("+FloatUtil.radToADeg(rotAng)+"), diff "+rotAngDiff+" ("+FloatUtil.radToADeg(rotAngDiff)+"), ang_velo "+as.ang_velo+", step "+rot_step+" ("+FloatUtil.radToADeg(rot_step)+")]");
+ }
+ }
+ sd.shape.moveTo(sd.targetPos);
+ q.setIdentity();
+ sd.shape.setInteractive(false);
+ return false;
+ }
+ if( !pos_ok ) {
+ if( DEBUG ) {
+ if( 0 == idx ) {
+ System.err.println("P: dt "+(dt_s*1000f)+" ms, p_t[OK "+pos_ok+", near "+pos_near+", diff: "+p_t_diff+", dxy "+dxy+"], rot[OK "+rot_ok+", radY "+rotAng+" ("+FloatUtil.radToADeg(rotAng)+"), diff "+rotAngDiff+" ("+FloatUtil.radToADeg(rotAngDiff)+"), ang_velo "+as.ang_velo+", step "+rot_step+" ("+FloatUtil.radToADeg(rot_step)+")]");
+ }
+ }
+ if( p_t_diff <= dxy || p_t_diff <= AnimGroup.POS_EPS ) {
+ sd.shape.moveTo(sd.targetPos);
+ } else {
+ pos.add( p_t.normalize().scale( dxy ) );
+ sd.shape.moveTo(pos);
+ }
+ if( !rot_ok ) {
+ if( pos_near ) {
+ q.rotateByAngleNormalAxis( rot_step * 2f, rotAxis );
+ } else {
+ q.rotateByAngleNormalAxis( rot_step, rotAxis );
+ }
+ }
+ } else {
+ if( DEBUG ) {
+ if( 0 == idx ) {
+ System.err.println("p: dt "+(dt_s*1000f)+" ms, p_t[OK "+pos_ok+", near "+pos_near+", diff: "+p_t_diff+", dxy "+dxy+"], rot[OK "+rot_ok+", radY "+rotAng+" ("+FloatUtil.radToADeg(rotAng)+"), diff "+rotAngDiff+" ("+FloatUtil.radToADeg(rotAngDiff)+"), ang_velo "+as.ang_velo+", step "+rot_step+" ("+FloatUtil.radToADeg(rot_step)+")]");
+ }
+ }
+ if( rot_ok || rotAngDiff <= rot_step * 3f ) {
+ q.setIdentity();
+ } else {
+ q.rotateByAngleNormalAxis( rot_step * 3f, rotAxis );
+ }
+ }
+ return true;
+ }
+ };
+
+ /**
+ * Scrolling {@link LerpFunc}, approaching {@link ShapeData}'s target position over and over.
+ * <p>
+ * Implementation uses the current shape position and time delta since last call,
+ * hence allows rugged utilization even if shapes are dragged around.
+ * </p>
+ */
+ public static class ScrollLerp implements LerpFunc {
+ final AABBox clip;
+ /**
+ * New scroller {@link LerpFunc} instance
+ * @param clip clipping box for each shape
+ */
+ public ScrollLerp(final AABBox clip) {
+ this.clip = clip;
+ }
+ @Override
+ public boolean eval(final long frame_cnt, final Set as, final int idx, final ShapeData sd, final float at_s, final float dt_s) {
+ final float dxy = as.velocity_obj * dt_s; // [shapeUnit]
+ final Vec3f pos = sd.shape.getPosition().copy();
+ final Vec3f p_t = sd.targetPos.minus(pos);
+ final float p_t_diff = p_t.length();
+ final boolean pos_ok = p_t_diff <= dxy || p_t_diff <= AnimGroup.POS_EPS;
+ if ( pos_ok ) {
+ // arrived -> restart
+ if( 0 == idx ) {
+ as.velocity = as.start_velocity;
+ as.velocity_obj = as.start_velocity_obj;
+ as.ang_velo = as.start_ang_velo;
+ final ShapeData sd_last = as.allShapes.get(as.allShapes.size()-1);
+ final Vec3f v_thisstart_lastpos = sd_last.shape.getPosition().minus( sd.startPos );
+ final float angle_thisstart_lastpos = Vec3f.UNIT_X.angle(v_thisstart_lastpos);
+ if( angle_thisstart_lastpos >= FloatUtil.HALF_PI ) {
+ // start position of this is 'right of' current position of last: short shape-string case
+ pos.set( sd.startPos );
+ } else {
+ // start position of this is 'left of' current position of last: long shape-string case
+ pos.set( sd_last.shape.getPosition() ).add( Vec3f.UNIT_X.mul( sd_last.shape.getScaledWidth() * 2f ) );
+ }
+ // System.err.println("Scroll-0: idx "+idx+", this "+sd.shape.getPosition()+", lst "+sd_last.shape.getPosition()+", angle "+angle_thisstart_lastpos+" rad ("+FloatUtil.radToADeg(angle_thisstart_lastpos)+" deg) -> "+pos);
+ } else {
+ final ShapeData sd_pre = as.allShapes.get(idx-1);
+ final Vec3f diff_start_pre_this = sd.startPos.minus( sd_pre.startPos );
+ pos.set( sd_pre.shape.getPosition() ).add( diff_start_pre_this );
+ // System.err.println("Scroll-n: idx "+idx+", this "+sd.shape.getPosition()+", pre "+sd_pre.shape.getPosition()+" -> "+pos);
+ }
+ } else {
+ pos.add( p_t.normalize().scale( dxy ) );
+ }
+ if( clip.intersects2DRegion(pos.x(), pos.y(), sd.shape.getScaledWidth(), sd.shape.getScaledHeight()) ) {
+ sd.shape.setEnabled(true);
+ } else {
+ sd.shape.setEnabled(false);
+ }
+ sd.shape.moveTo(pos);
+ return true;
+ }
+ };
+
+ /**
+ * Sine target {@link LerpFunc}, approaching {@link ShapeData}'s target position utilizing the angular value for sine amplitude
+ * towards the given normalized direction vector.
+ * <p>
+ * The sine amplitude is flattened towards target.
+ * </p>
+ * <p>
+ * Implementation uses the current shape position and relative time duration since last call to interpolate,
+ * hence allows rugged utilization even if shapes are dragged around.
+ * </p>
+ */
+ public static class SineLerp implements LerpFunc {
+ final Vec3f sineDir;
+ final float sineScale;
+ final float shapeStep;
+
+ /**
+ * New sine {@link LerpFunc} instance
+ * @param sineDir normalized vector for sine amplitude direction
+ * @param sineScale sine scale factor to amplify effect
+ * @param shapeStep shape index {@code idx} factor for {@code dt_s}, amplifying angular distance between each shape. Golden ratio {@code 1.618f} reveals dynamic characteristics.
+ */
+ public SineLerp(final Vec3f sineDir, final float sineScale, final float shapeStep) {
+ this.sineDir = sineDir;
+ this.sineScale = sineScale;
+ this.shapeStep = shapeStep;
+ }
+ @Override
+ public boolean eval(final long frame_cnt, final Set as, final int idx, final ShapeData sd, final float at_s, final float dt_s) {
+ final float dxy = as.velocity_obj * dt_s; // [shapeUnit]
+ final float angle = as.ang_velo * ( at_s + idx * shapeStep * dt_s ); // [radians]
+ final Vec3f pos = sd.shape.getPosition().copy();
+ if( 0 == frame_cnt ) {
+ sd.user = null;
+ } else if( null != sd.user ) {
+ final Vec3f lastSineVal = (Vec3f)sd.user;
+ pos.sub(lastSineVal);
+ }
+ final Vec3f p_t = sd.targetPos.minus(pos);
+ final float p_t_diff = p_t.length();
+ final boolean pos_ok = p_t_diff <= dxy || p_t_diff <= AnimGroup.POS_EPS;
+
+ if ( pos_ok ) {
+ // arrived
+ sd.shape.moveTo(sd.targetPos);
+ sd.shape.setInteractive(false);
+ return false;
+ } else {
+ final float shapeScale = sd.shape.getScale().y();
+ final float p_t_norm;
+ {
+ final Vec3f s_t = sd.targetPos.minus(sd.startPos);
+ p_t_norm = p_t_diff / s_t.length(); // [1 -> 0] from start to target
+ }
+ final float sineAmp = FloatUtil.sin(angle)*p_t_norm*shapeScale*sineScale;
+ final Vec3f sineVec = sineDir.copy().scale( sineAmp );
+ sd.user = sineVec;
+ sd.shape.moveTo( pos.add( p_t.normalize().scale( dxy ) ).add( sineVec ) );
+ }
+ return true;
+ }
+ static final boolean methodB = true;
+ };
+}