I am trying to move a pool ball in Java2ME. It is easy when the velocity is stable. I change the x and y coordinate of the ball according to x and y velocity. They are all integers. No problem. However a normal pool ball has to go first fast then slowdown and stop. Because the x and y coordinates of the ball are integers, I can't decrease the x and y velocity by percentage. I mean if the velocity is 9 and I want to decrease it by 10% I cant do it "9 * 0.1" because it has to be an integer. I know the coordinates can't be double. What can I do?
the code:
public void move() {
//... Move the ball at the given velocity.
m_x += m_velocityX; // m_x: x coordinate of the ball
m_y += m_velocityY; // m_y: y coordinate of the ball
//... ball hits the borders and change the way
if (m_x < 0) { // If at or beyond left side
m_x = 0; // Place against edge and
m_velocityX = -m_velocityX; // reverse direction.
} else if (m_x > m_rightBound) { // If at or beyond right side
m_x = m_rightBound; // Place against right edge.
m_velocityX = -m_velocityX; // Reverse direction.
}
if (m_y < 0) { // if we're at top
m_y = 0;
m_velocityY = -m_velocityY;
} else if (m_y > m_bottomBound) { // if we're at bottom
m_y = m_bottomBound;
m_velocityY = -m_velocityY;
}
}
If the velocity must be integral, just update the values to the floor of the floating-point calculation. So to reduce velocity by 10%:
m_velocityX = floor(m_velocityX * 0.9);
You might want to do something fancier someday, but this seems simple and workable.
You should store the ball speed as velocity and angle.
Also, every variable mentioned should be float or double.
Then everything will be easier, and much more accurate.
The algorithm will then be:
float x,y,velocity,angle
int ix,iy;
...
{
if(y<0) angle=-angle;
... etc, etc.
velocity*=0.95;
x+=velocity*cos(angle);
y+=velocity*sin(angle);
// And you get your precious integers ...
ix=floor(x);
iy=floor(y);
}
if the velocity is 9 and I want to decrease it by 10% I cant do it "9 * 0.1" because it has to be an integer
scale up the velocity and coordinates (multiply eg by 256 or shift left by eg 8)
calculate decrease for scaled up velocity "9 * 256 / 10"
calculate new (scaled up) position and velocity
scale down
About like below...
Ball move(Ball ball, Border border, Tracer tracer) {
tracer.trace("scale stuff up to handle acceleration = velocity / 8");
Scale scale = new Scale(256);
Int2D position = scale.up(ball.position);
Velocity velocity = new Velocity(scale.up(ball.velocity));
tracer.trace("calculate acceleration as scaled up velocity / 8";
Int2D acceleration = new Scale(8).down(velocity);
tracer.trace("Move the ball at the given velocity");
position = position.plus(velocity);
tracer.trace("slow down velocity");
velocity = velocity.slowDown(acceleration);
tracer.trace("scale back down to original");
ball = new Ball(scale.down(position), new Velocity(scale.down(velocity)));
tracer.trace("adjust if ball hits the borders and change the way");
return border.reflectIfNeeded(ball);
}
interface Tracer { void trace(String msg); }
class Scale {
final int scale; // better be power of 2
Scale(int scale) { this.scale = scale; }
Int2D up(Int2D src) { return new Int2D(src.x * scale, src.y * scale); }
Int2D down(Int2D src) { return new Int2D(src.x / scale, src.y / scale); }
} // Scale
class Border {
final Int2D topLeft, bottomRight;
Border(Int2D topLeft, Int2D bottomRight) {
this.topLeft = topLeft;
this.bottomRight = bottomRight;
}
Ball reflectIfNeeded(Ball ball) {
if (within(ball)) { return ball; }
throw new UnsupportedOperationException("not yet implemented");
}
private boolean within(Ball ball) {
return within(ball.position.x, topLeft.x, bottomRight.x)
&& within(ball.position.y, topLeft.y, bottomRight.y);
}
private boolean within(int value, int min, int max) {
return value > min && value < max;
}
} // Border
class Ball {
final Int2D position;
final Velocity velocity;
Ball(Int2D position, Velocity velocity) {
this.position = position;
this.velocity = velocity;
}
} // Ball
class Velocity extends Int2D {
Velocity(Int2D src) { super(src.x, src.y); }
Velocity slowDown(Int2D acceleration) {
return new Velocity(this.minus(acceleration));
}
Velocity reflectX() { return new Velocity(new Int2D(-x, y)); }
Velocity reflectY() { return new Velocity(new Int2D(x, -y)); }
} // Velocity
class Int2D {
final int x, y;
Int2D(int x, int y) { this.x = x; this.y = y; }
Int2D plus(Int2D other) { return new Int2D(x + other.x, y + other.y); }
Int2D minus(Int2D other) { return new Int2D(x - other.x, y - other.y); }
} // Int2D
Related
I am working on xamarin.forms app. for that i need circular progressbar. I almost got it working in xamarin.ios using CustomRenderer but its not actually what i wanted.
My circular progressbar in xamarin.ios renderer works but starts from the right edge of the containg rectangle. but I want it to start from the top edge in clockwise direction.
I tried to change start angle to -90 or 270 but still it starts from the right side and sometimes it doesn't work also. I am not sure in which condition its not working but I think when I use Math.PI constant in code, Draw never calls. I have referred this link.
class NativeProgressBarRenderer : ViewRenderer
{
private bool _sizeChanged = false;
//private CG _paint;
private CGRect _ringDrawArea;
private nfloat _radius;
const float FULL_CIRCLE = 2 * (float)Math.PI;
// int _radius = 10;
float _lineWidth = 10;
nfloat _percentComplete = 0.0f;
UIColor _backColor = UIColor.LightGray; //UIColor.FromRGB(46, 60, 76);
UIColor _frontColor = UIColor.Green; //UIColor.FromRGB(234, 105, 92);
protected override void OnElementPropertyChanged(object sender,
System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == ProgressBar.ProgressProperty.PropertyName ||
e.PropertyName == PlayerProgressBar.RingThicknessProperty.PropertyName ||
e.PropertyName == PlayerProgressBar.RingBaseColorProperty.PropertyName ||
e.PropertyName == PlayerProgressBar.RingProgressColorProperty.PropertyName)
{
SetNeedsLayout();
SetNeedsDisplay();
}
if (e.PropertyName == VisualElement.WidthProperty.PropertyName ||
e.PropertyName == VisualElement.HeightProperty.PropertyName)
{
_sizeChanged = true;
SetNeedsLayout();
SetNeedsDisplay();
}
}
public override void Draw(CoreGraphics.CGRect rect)
{
base.Draw(rect);
using (CGContext g = UIGraphics.GetCurrentContext())
{
var progressRing = (PlayerProgressBar)Element;
// Get Metrics
var mainDisplayInfo = DeviceDisplay.MainDisplayInfo;
// Screen density
var density = mainDisplayInfo.Density;
_lineWidth = (float)Math.Ceiling(progressRing.RingThickness * density);
var diameter = Math.Min(this.Bounds.Width, this.Bounds.Height);
_radius = (int)(diameter / 2) - _lineWidth;
_backColor = progressRing.RingBaseColor.ToUIColor();
_frontColor = progressRing.RingProgressColor.ToUIColor();
_percentComplete = (float)progressRing.Progress;
var x = Bounds.GetMidX();
var y = Bounds.GetMidY();
//DrawGraph(g, Bounds.Left, Bounds.Top); // Tried to change x,y
DrawGraph(g, Bounds.GetMidX(), this.Bounds.GetMidY());
};
}
public void DrawGraph(CGContext g, nfloat x, nfloat y)
{
//g.ScaleCTM(1, -1);
//g.TranslateCTM(0, -Bounds.Height);
//g.RotateCTM(270);
g.SetLineWidth(_lineWidth);
// Draw background circle
CGPath path = new CGPath();
_backColor.SetStroke();
path.AddArc(x, y, _radius, 270, _percentComplete * FULL_CIRCLE, true);
g.AddPath(path);
g.DrawPath(CGPathDrawingMode.Stroke);
// Draw overlay circle
var pathStatus = new CGPath();
_frontColor.SetStroke();
//CGAffineTransform cGAffineTransform = new CGAffineTransform();
//cGAffineTransform.Rotate(-90); // This also doesn't work
// Same Arc params except direction so colors don't overlap
pathStatus.AddArc(x, y, _radius, 0, _percentComplete * FULL_CIRCLE, false);
g.AddPath(pathStatus);
g.DrawPath(CGPathDrawingMode.Stroke);
//cGAffineTransform.Invert();
}
I expect to run it from top of the circle. Kindly help!
Thank you
This is because the start angle is not beginning at 0 o'clock. Here is its default coordinate system:
So each angle should minus 0.5π. Modify your code like:
// Draw overlay circle
var pathStatus = new CGPath();
_frontColor.SetStroke();
pathStatus.AddArc(x, y, _radius, -0.25f * FULL_CIRCLE, (_percentComplete-0.25f) * FULL_CIRCLE, false);
g.AddPath(pathStatus);
g.DrawPath(CGPathDrawingMode.Stroke);
I made a test game in unity that makes it so when I click on a button, it spawns a cylinder created from a factory class. I'm trying to make it so when I create the cylinder, its height shrinks over the next 20 seconds. Some methods I found are difficult to translate into what I'm doing. If you could lead me to the right direction, I'd very much appreciate it.
Here's my code for the cylinder class
public class Cylinder : Shape
{
public Cylinder()
{
GameObject cylinder = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
cylinder.transform.position = new Vector3(3, 0, 0);
cylinder.transform.localScale = new Vector3(1.0f, Random.Range(1, 2)-1*Time.deltaTime, 1.0f);
cylinder.GetComponent<MeshRenderer>().material.color = Random.ColorHSV();
Destroy(cylinder, 30.0f);
}
}
This can be done with the Time.deltaTime and Vector3.Lerp in a coroutine function. Similar to Rotate GameObject over time and Move GameObject over time questions. Modified it a little bit to do just this.
bool isScaling = false;
IEnumerator scaleOverTime(Transform objectToScale, Vector3 toScale, float duration)
{
//Make sure there is only one instance of this function running
if (isScaling)
{
yield break; ///exit if this is still running
}
isScaling = true;
float counter = 0;
//Get the current scale of the object to be moved
Vector3 startScaleSize = objectToScale.localScale;
while (counter < duration)
{
counter += Time.deltaTime;
objectToScale.localScale = Vector3.Lerp(startScaleSize, toScale, counter / duration);
yield return null;
}
isScaling = false;
}
USAGE:
Will scale GameObject within 20 seconds:
StartCoroutine(scaleOverTime(cylinder.transform, new Vector3(0, 0, 90), 20f));
Check out Lerp. A general example of how to use it would be something like this:
float t = 0;
Update()
{
t += Time.deltaTime;
cylinder.localScale = new Vector3(1, Mathf.Lerp(2f, 1f, t/3f), 1); // shrink from 2 to 1 over 3 seconds;
}
You will create a new monobehaviour script and add it to your primitive. Then you wil use "Update" method of monobehaviour (or use coroutine) for change object over time.
Monobehaviour must be look like this:
public class ShrinkBehaviour : MonoBehaviour
{
bool isNeedToShrink;
Config currentConfig;
float startTime;
float totalDistance;
public void StartShrink(Config config)
{
startTime = Time.time;
currentConfig = config;
totalDistance = Vector3.Distance(currentConfig.startSize, currentConfig.destinationSize);
isNeedToShrink = true;
transform.localScale = config.startSize;
}
private void Update()
{
if (isNeedToShrink)
{
var nextSize = GetNextSize(currentConfig);
if (Vector3.Distance(nextSize, currentConfig.destinationSize) <= 0.05f)
{
isNeedToShrink = false;
return;
}
transform.localScale = nextSize;
}
}
Vector3 GetNextSize(Config config)
{
float timeCovered = (Time.time - startTime) / config.duration;
var result = Vector3.Lerp(config.startSize, config.destinationSize, timeCovered);
return result;
}
public struct Config
{
public float duration;
public Vector3 startSize;
public Vector3 destinationSize;
}
}
For using this, you must do next:
public Cylinder()
{
GameObject cylinder = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
var shrink = cylinder.AddComponent<ShrinkBehaviour>();
shrink.StartShrink(new ShrinkBehaviour.Config() { startSize = Vector3.one * 10, destinationSize = Vector3.one * 1, duration = 20f });
cylinder.transform.position = new Vector3(3, 0, 0);
cylinder.GetComponent<MeshRenderer>().material.color = Random.ColorHSV();
Destroy(cylinder, 30.0f);
}
You must remember, monobehaviour-script must be in separate file, and must have name similar to monobehaviour-class name. For example, ShrinkBehaviour.cs;
Related post: Stride of the BitmapData is different than the original
dimension.
I have taken the source code from here and modified it.
The code is generating a variety of exceptions in different occasions.
.
Error in BitmapLocker.cs
At the following line in Lock(),
// Copy data from IntegerPointer to _imageData
Marshal.Copy(IntegerPointer, _imageData, 0, _imageData.Length);
The following exception is being generated:
An unhandled exception of type 'System.AccessViolationException'
occurred in mscorlib.dll
Additional information: Attempted to read or write protected memory.
This is often an indication that other memory is corrupt.
For the following driver code,
double[,] mask = new double[,]
{
{ .11, .11, .11, },
{ .11, .11, .11, },
{ .11, .11, .11, },
};
Bitmap bitmap = ImageDataConverter.ToBitmap(mask);
BitmapLocker locker = new BitmapLocker(bitmap);
locker.Lock();
for (int i = 0; i < bitmap.Width; i++)
{
for (int j = 0; j < bitmap.Height; j++)
{
Color c = locker.GetPixel(i, j);
locker.SetPixel(i, j, c);
}
}
locker.Unlock();
At the following line in GetPixel(),
if (i > dataLength)
{
throw new IndexOutOfRangeException();
}
An unhandled exception of type 'System.IndexOutOfRangeException'
occurred in Simple.ImageProcessing.Framework.dll
Additional information: Index was outside the bounds of the array.
.
At the following line in SetPixel(),
if (ColorDepth == 8)
{
_imageData[i] = color.B;
}
An unhandled exception of type 'System.Exception' occurred in
Simple.ImageProcessing.Framework.dll
Additional information: (0, 0), 262144, Index was outside the bounds
of the array., i=262144
.
Error in Driver program
At the line,
Color c = bmp.GetPixel(i, j);
An unhandled exception of type 'System.InvalidOperationException'
occurred in System.Drawing.dll
Additional information: Bitmap region is already locked.
Source Code:
public class BitmapLocker : IDisposable
{
//private properties
Bitmap _bitmap = null;
bool _isLocked = false;
BitmapData _bitmapData = null;
private byte[] _imageData = null;
//public properties
public IntPtr IntegerPointer { get; private set; }
public int Width { get { return _bitmap.Width; } }
public int Height { get { return _bitmap.Height; } }
public int Stride { get { return _bitmapData.Stride; } }
public int ColorDepth { get { return Bitmap.GetPixelFormatSize(_bitmap.PixelFormat); } }
public int Channels { get { return ColorDepth / 8; } }
public int PaddingOffset { get { return _bitmapData.Stride - (_bitmap.Width * Channels); } }
public PixelFormat ImagePixelFormat { get { return _bitmap.PixelFormat; } }
public bool IsGrayscale { get { return Grayscale.IsGrayscale(_bitmap); } }
//Constructor
public BitmapLocker(Bitmap source)
{
IntegerPointer = IntPtr.Zero;
this._bitmap = source;
}
/// Lock bitmap
public void Lock()
{
if (_isLocked == false)
{
try
{
// Lock bitmap (so that no movement of data by .NET framework) and return bitmap data
_bitmapData = _bitmap.LockBits(
new Rectangle(0, 0, _bitmap.Width, _bitmap.Height),
ImageLockMode.ReadWrite,
_bitmap.PixelFormat);
// Create byte array to copy pixel values
int noOfBitsNeededForStorage = _bitmapData.Stride * _bitmapData.Height;
int noOfBytesNeededForStorage = noOfBitsNeededForStorage / 8;
_imageData = new byte[noOfBytesNeededForStorage * ColorDepth];//# of bytes needed for storage
IntegerPointer = _bitmapData.Scan0;
// Copy data from IntegerPointer to _imageData
Marshal.Copy(IntegerPointer, _imageData, 0, _imageData.Length);
_isLocked = true;
}
catch (Exception)
{
throw;
}
}
else
{
throw new Exception("Bitmap is already locked.");
}
}
/// Unlock bitmap
public void Unlock()
{
if (_isLocked == true)
{
try
{
// Copy data from _imageData to IntegerPointer
Marshal.Copy(_imageData, 0, IntegerPointer, _imageData.Length);
// Unlock bitmap data
_bitmap.UnlockBits(_bitmapData);
_isLocked = false;
}
catch (Exception)
{
throw;
}
}
else
{
throw new Exception("Bitmap is not locked.");
}
}
public Color GetPixel(int x, int y)
{
Color clr = Color.Empty;
// Get color components count
int channels = ColorDepth / 8;
// Get start index of the specified pixel
int i = (Height - y - 1) * Stride + x * channels;
int dataLength = _imageData.Length - channels;
if (i > dataLength)
{
throw new IndexOutOfRangeException();
}
if (ColorDepth == 32) // For 32 bpp get Red, Green, Blue and Alpha
{
byte b = _imageData[i];
byte g = _imageData[i + 1];
byte r = _imageData[i + 2];
byte a = _imageData[i + 3]; // a
clr = Color.FromArgb(a, r, g, b);
}
if (ColorDepth == 24) // For 24 bpp get Red, Green and Blue
{
byte b = _imageData[i];
byte g = _imageData[i + 1];
byte r = _imageData[i + 2];
clr = Color.FromArgb(r, g, b);
}
if (ColorDepth == 8)
// For 8 bpp get color value (Red, Green and Blue values are the same)
{
byte c = _imageData[i];
clr = Color.FromArgb(c, c, c);
}
return clr;
}
public void SetPixel(int x, int y, Color color)
{
// Get color components count
int cCount = ColorDepth / 8;
// Get start index of the specified pixel
int i = ((Height - y -1) * Stride + x * cCount);
try
{
if (ColorDepth == 32) // For 32 bpp set Red, Green, Blue and Alpha
{
_imageData[i] = color.B;
_imageData[i + 1] = color.G;
_imageData[i + 2] = color.R;
_imageData[i + 3] = color.A;
}
if (ColorDepth == 24) // For 24 bpp set Red, Green and Blue
{
_imageData[i] = color.B;
_imageData[i + 1] = color.G;
_imageData[i + 2] = color.R;
}
if (ColorDepth == 8)
// For 8 bpp set color value (Red, Green and Blue values are the same)
{
_imageData[i] = color.B;
}
}
catch(Exception ex)
{
throw new Exception("("+x+", "+y+"), "+_imageData.Length+", "+ ex.Message+", i=" + i);
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// free managed resources
_bitmap = null;
_bitmapData = null;_imageData = null;IntegerPointer = IntPtr.Zero;
}
// free native resources if there are any.
//private properties
//public properties
}
}
.
ImageDataConverter.cs
public static Bitmap ToBitmap(double[,] input)
{
int width = input.GetLength(0);
int height = input.GetLength(1);
Bitmap output = Grayscale.CreateGrayscaleImage(width, height);
BitmapData data = output.LockBits(new Rectangle(0, 0, width, height),
ImageLockMode.WriteOnly,
output.PixelFormat);
int pixelSize = System.Drawing.Image.GetPixelFormatSize(PixelFormat.Format8bppIndexed) / 8;
int offset = data.Stride - width * pixelSize;
double Min = 0.0;
double Max = 255.0;
unsafe
{
byte* address = (byte*)data.Scan0.ToPointer();
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
double v = 255 * (input[x, y] - Min) / (Max - Min);
byte value = unchecked((byte)v);
for (int c = 0; c < pixelSize; c++, address++)
{
*address = value;
}
}
address += offset;
}
}
output.UnlockBits(data);
return output;
}
Here is the picture I used for the test,
int noOfBitsNeededForStorage = _bitmapData.Stride * _bitmapData.Height;
That is the most essential bug in the code. Stride * Height are the number of bytes needed for storage. So it doesn't make the _imageData array large enough and IndexOutOfRangeException is the expected outcome.
int pixelSize = System.Drawing.Image.GetPixelFormatSize(PixelFormat.Format8bppIndexed) / 8;
Lots of possible mishaps from this statement. It hard-codes the pixel format to 8bpp but that is not the actual pixel format that the LockBits() call used. Which was output.PixelFormat. Notably fatal on the sample image, although it is not clear how it is used in the code, 8bpp is a very awkward pixel format since it requires a palette. The PNG codec will create a 32bpp image in memory, even though the original file uses 8bpp. You must use output.PixelFormat here to get a match with the locked data and adjust the pixel writing code accordingly. Not clear why it is being used at all, the SetPixel() method provided by the library code should already be good enough.
int dataLength = _imageData.Length - channels;
Unclear what that statement tries to do, subtracting the number of channels is not a sensible operation. It will generate a spurious IndexOutOfRangeException. There is no obvious reason to help, the CLR already provides array index checking on the _imageData array. So just delete that code.
Additional information: Bitmap region is already locked.
Exception handling in the code is not confident, a possible reason for this exception. In general it must be noted that the underlying bitmap is completely inaccessible, other than through _imageData, after the Lock() method was called and Unlock() wasn't called yet. Best way to do this is with try/finally with the Unlock() call in the finally block so you can always be sure that the bitmap doesn't remain locked by accident.
byte c = _imageData[i];
This is not correct, except in the corner case of an 8bpp image that has a palette that was explicitly created to handle grayscale images. The default palette for an 8bpp image does not qualify that requirement nor is it something you can blindly rely on when loading images from a file. Indexed pixel formats where a dreadful hack that was necessary in the early 1990s because video adapters where not yet powerful enough. It no longer makes any sense at all today. Note that SetPixel() also doesn't handle a 16-bit pixel formats. And that the PNG codec will never create an 8bpp memory image and cannot encode an 8bpp file. Best advice is to eliminate 8bpp support completely to arrive at more reliable code.
In fact, the point of directly accessing pixel data is to make image manipulation fast. There is only one pixel format that consistently produces fast code, it is Format32bppArgb. The pixels can now be accessed with an int* instead of a byte*, moving pixels ~4 times faster. And no special tweaks are necessary to deal with stride or special-case the code for methods like SetPixel(). So pass that format into LockBits(), the codec will do the work necessary if the actual image format is not 32bpp as well.
I should note that Format32bppPArgb is the fast pixel format for displaying images on the screen since it is compatible with the pixel format used by all modern video adapters. But that isn't the point of this code and dealing with the pre-multiplied alpha is awkward.
I would like to implement pan on my plot without zooming, by setting a origin value smaller than may lower boundaries on domain axis.
My code so far :
this.panZoom = PanZoom.attach(plot);
this.panZoom.setPan(PanZoom.Pan.HORIZONTAL);
this.panZoom.setZoom(null);
this.panZoom.setDelegate(this);
this.plot.setUserDomainOrigin(0);
this.plot.setDomainBoundaries(5, 20);
this.plot.setDomainStep(StepMode.INCREMENT_BY_VAL, 2);
By doing this, my plot starts well at 5 but won't move when I scroll..
I'm migrating my project to androidPlot 1.2.2 and it was working with 0.9.7
Thanks!
This appears to be a limitation in Androidplot 1.2.2 (Will be fixed in the next release - tracked by this bug report.)
For now you can add this implementation of PanZoom in your project:
package com.androidplot.xy;
import android.graphics.RectF;
import android.graphics.PointF;
import android.util.*;
import android.view.*;
import com.androidplot.*;
import java.util.*;
/**
* Enables basic pan/zoom touch behavior for an {#link XYPlot}.
* By default boundaries set on the associated plot will define the scroll/zoom extents as well as
* initial state of the plot's visible area. If you wish to specify a scrollable / zoomable area
* that is greater than or less than the plot's boundaries, use
* {#link #setDomainBoundaries(Number, Number)} and
* {#link #setRangeBoundaries(Number, Number)}
* TODO: zoom using dynamic center point
* TODO: stretch both mode
*/
public class PanZoom implements View.OnTouchListener {
protected static final float MIN_DIST_2_FING = 5f;
protected static final int FIRST_FINGER = 0;
protected static final int SECOND_FINGER = 1;
private XYPlot plot;
private Pan pan;
private Zoom zoom;
private boolean isEnabled = true;
private DragState dragState = DragState.NONE;
RectRegion limits = new RectRegion();
RectRegion previousLimits = new RectRegion();
private PointF firstFingerPos;
// rectangle created by the space between two fingers
private RectF fingersRect;
private View.OnTouchListener delegate;
// Definition of the touch states
protected enum DragState {
NONE,
ONE_FINGER,
TWO_FINGERS
}
public enum Pan {
NONE,
HORIZONTAL,
VERTICAL,
BOTH
}
public enum Zoom {
/**
* Comletely disable panning
*/
NONE,
/**
* Zoom on the horizontal axis only
*/
STRETCH_HORIZONTAL,
/**
* Zoom on the vertical axis only
*/
STRETCH_VERTICAL,
/**
* Zoom on the vertical axis by the vertical distance between each finger, while zooming
* on the horizontal axis by the horizantal distance between each finger.
*/
STRETCH_BOTH,
/**
* Zoom each axis by the same amount, specifically the total distance between each finger.
*/
SCALE
}
protected PanZoom(XYPlot plot, Pan pan, Zoom zoom) {
this.plot = plot;
this.pan = pan;
this.zoom = zoom;
}
/**
* Convenience method for enabling pan/zoom behavior on an instance of {#link XYPlot}, using
* a default behavior of {#link Pan#BOTH} and {#link Zoom#SCALE}.
* Use {#link PanZoom#attach(XYPlot, Pan, Zoom)} for finer grain control of this behavior.
* #param plot
* #return
*/
public static PanZoom attach(XYPlot plot) {
return attach(plot, Pan.BOTH, Zoom.SCALE);
}
public static PanZoom attach(XYPlot plot, Pan pan, Zoom zoom) {
PanZoom pz = new PanZoom(plot, pan, zoom);
plot.setOnTouchListener(pz);
return pz;
}
public boolean isEnabled() {
return isEnabled;
}
public void setEnabled(boolean enabled) {
isEnabled = enabled;
}
/**
* Set the boundaries by which domain pan/zoom calculations will abide; differs from an {#link XYPlot}'s boundaries
* in that those boundaries define the plot's starting state.
* #param lowerBoundary
* #param upperBoundary
*/
public void setDomainBoundaries(final Number lowerBoundary, final Number upperBoundary) {
limits.setMinX(lowerBoundary);
limits.setMaxX(upperBoundary);
}
/**
* Sets the range boundaries by which pan/zoom calculations will abide.
* #param lowerBoundary
* #param upperBoundary
*/
public void setRangeBoundaries(final Number lowerBoundary, final Number upperBoundary) {
limits.setMinY(lowerBoundary);
limits.setMaxY(upperBoundary);
}
#Override
public boolean onTouch(final View view, final MotionEvent event) {
boolean isConsumed = false;
if (delegate != null) {
isConsumed = delegate.onTouch(view, event);
}
if (isEnabled() && !isConsumed) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: // start gesture
firstFingerPos = new PointF(event.getX(), event.getY());
Log.d("PanZoom", "ONE_FINGER set");
dragState = DragState.ONE_FINGER;
break;
case MotionEvent.ACTION_POINTER_DOWN: // second finger
{
fingersRect = fingerDistance(event);
Log.d("PanZoom", "ACTION_POINTER_DOWN - distance: " + fingersRect.width());
// the distance check is done to avoid false alarms
if (fingersRect.width() > MIN_DIST_2_FING || fingersRect.width() < -MIN_DIST_2_FING) {
Log.d("PanZoom", "TWO_FINGERS set");
dragState = DragState.TWO_FINGERS;
}
break;
}
case MotionEvent.ACTION_POINTER_UP: // end zoom
dragState = DragState.NONE;
break;
case MotionEvent.ACTION_MOVE:
if (dragState == DragState.ONE_FINGER) {
Log.d("PanZoom", "ACTION_MOVE - one finger");
pan(event);
} else if (dragState == DragState.TWO_FINGERS) {
Log.d("PanZoom", "ACTION_MOVE - two fingers");
zoom(event);
}
break;
}
}
// we're forced to consume the event here as not consuming it will prevent future calls:
return true;
}
/**
* Calculates the distance between two finger motion events.
* #param firstFingerX
* #param firstFingerY
* #param secondFingerX
* #param secondFingerY
* #return
*/
protected RectF fingerDistance(float firstFingerX, float firstFingerY, float secondFingerX, float secondFingerY) {
final float left = firstFingerX > secondFingerX ? secondFingerX : firstFingerX;
final float right = firstFingerX > secondFingerX ? firstFingerX : secondFingerX;
final float top = firstFingerY > secondFingerY ? secondFingerY : firstFingerY;
final float bottom = firstFingerY > secondFingerY ? firstFingerY : secondFingerY;
return new RectF(left, top, right, bottom);
}
/**
* Calculates the distance between two finger motion events.
* #param evt
* #return
*/
protected RectF fingerDistance(final MotionEvent evt) {
return fingerDistance(
evt.getX(FIRST_FINGER),
evt.getY(FIRST_FINGER),
evt.getX(SECOND_FINGER),
evt.getY(SECOND_FINGER));
}
protected Number getMinXLimit() {
if (limits.getMinX() == null) {
limits.setMinX(plot.getBounds().getMinX().floatValue());
previousLimits.setMinX(limits.getMinX());
}
return limits.getMinX();
}
protected Number getMaxXLimit() {
if (limits.getMaxX() == null) {
limits.setMaxX(plot.getBounds().getMaxX().floatValue());
previousLimits.setMaxX(limits.getMaxX());
}
return limits.getMaxX();
}
protected Number getMinYLimit() {
if (limits.getMinY() == null) {
limits.setMinY(plot.getBounds().getMinY().floatValue());
previousLimits.setMinY(limits.getMinY());
}
return limits.getMinY();
}
protected Number getMaxYLimit() {
if (limits.getMaxY() == null) {
limits.setMaxY(plot.getBounds().getMaxY().floatValue());
previousLimits.setMaxY(limits.getMaxY());
}
return limits.getMaxY();
}
protected Number getLastMinX() {
if (previousLimits.getMinX() == null) {
previousLimits.setMinX(plot.getBounds().getMinX().floatValue());
}
return previousLimits.getMinX();
}
protected Number getLastMaxX() {
if (previousLimits.getMaxX() == null) {
previousLimits.setMaxX(plot.getBounds().getMaxX().floatValue());
}
return previousLimits.getMaxX();
}
protected Number getLastMinY() {
if (previousLimits.getMinY() == null) {
previousLimits.setMinY(plot.getBounds().getMinY().floatValue());
}
return previousLimits.getMinY();
}
private Number getLastMaxY() {
if (previousLimits.getMaxY() == null) {
previousLimits.setMaxY(plot.getBounds().getMaxY().floatValue());
}
return previousLimits.getMaxY();
}
protected void pan(final MotionEvent motionEvent) {
if (pan == Pan.NONE) {
return;
}
final PointF oldFirstFinger = firstFingerPos; //save old position of finger
firstFingerPos = new PointF(motionEvent.getX(), motionEvent.getY()); //update finger position
Region newBounds = new Region();
if (EnumSet.of(Pan.HORIZONTAL, Pan.BOTH).contains(pan)) {
calculatePan(oldFirstFinger, newBounds, true);
plot.setDomainBoundaries(newBounds.getMin(), newBounds.getMax(), BoundaryMode.FIXED);
previousLimits.setMinX(newBounds.getMin());
previousLimits.setMaxX(newBounds.getMax());
}
if (EnumSet.of(Pan.VERTICAL, Pan.BOTH).contains(pan)) {
calculatePan(oldFirstFinger, newBounds, false);
plot.setRangeBoundaries(newBounds.getMin(), newBounds.getMax(), BoundaryMode.FIXED);
previousLimits.setMinY(newBounds.getMin());
previousLimits.setMaxY(newBounds.getMax());
}
plot.redraw();
}
protected void calculatePan(final PointF oldFirstFinger, Region bounds, final boolean horizontal) {
final float offset;
// multiply the absolute finger movement for a factor.
// the factor is dependent on the calculated min and max
if (horizontal) {
bounds.setMin(getLastMinX());
bounds.setMax(getLastMaxX());
offset = (oldFirstFinger.x - firstFingerPos.x) *
((bounds.getMax().floatValue() - bounds.getMin().floatValue()) / plot.getWidth());
} else {
bounds.setMin(getLastMinY());
bounds.setMax(getLastMaxY());
offset = -(oldFirstFinger.y - firstFingerPos.y) *
((bounds.getMax().floatValue() - bounds.getMin().floatValue()) / plot.getHeight());
}
// move the calculated offset
bounds.setMin(bounds.getMin().floatValue() + offset);
bounds.setMax(bounds.getMax().floatValue() + offset);
//get the distance between max and min
final float diff = bounds.length().floatValue();
//check if we reached the limit of panning
if (horizontal) {
if (bounds.getMin().floatValue() < getMinXLimit().floatValue()) {
bounds.setMin(getMinXLimit());
bounds.setMax(bounds.getMin().floatValue() + diff);
}
if (bounds.getMax().floatValue() > getMaxXLimit().floatValue()) {
bounds.setMax(getMaxXLimit());
bounds.setMin(bounds.getMax().floatValue() - diff);
}
} else {
if (bounds.getMin().floatValue() < getMinYLimit().floatValue()) {
bounds.setMin(getMinYLimit());
bounds.setMax(bounds.getMin().floatValue() + diff);
}
if (bounds.getMax().floatValue() > getMaxYLimit().floatValue()) {
bounds.setMax(getMaxYLimit());
bounds.setMin(bounds.getMax().floatValue() - diff);
}
}
}
protected boolean isValidScale(float scale) {
if (Float.isInfinite(scale) || Float.isNaN(scale) || scale > -0.001 && scale < 0.001) {
return false;
}
return true;
}
protected void zoom(final MotionEvent motionEvent) {
if (zoom == Zoom.NONE) {
return;
}
final RectF oldFingersRect = fingersRect;
final RectF newFingersRect = fingerDistance(motionEvent);
fingersRect = newFingersRect;
RectF newRect = new RectF();
float scaleX = 1;
float scaleY = 1;
switch (zoom) {
case STRETCH_HORIZONTAL:
scaleX = oldFingersRect.width() / fingersRect.width();
if (!isValidScale(scaleX)) {
return;
}
break;
case STRETCH_VERTICAL:
scaleY = oldFingersRect.height() / fingersRect.height();
if (!isValidScale(scaleY)) {
return;
}
break;
case STRETCH_BOTH:
scaleX = oldFingersRect.width() / fingersRect.width();
scaleY = oldFingersRect.height() / fingersRect.height();
if (!isValidScale(scaleX) || !isValidScale(scaleY)) {
return;
}
break;
case SCALE:
float sc1 = (float) Math.hypot(oldFingersRect.height(), oldFingersRect.width());
float sc2 = (float) Math.hypot(fingersRect.height(), fingersRect.width());
float sc = sc1 / sc2;
scaleX = sc;
scaleY = sc;
if (!isValidScale(scaleX) || !isValidScale(scaleY)) {
return;
}
break;
}
if (EnumSet.of(
Zoom.STRETCH_HORIZONTAL,
Zoom.STRETCH_BOTH,
Zoom.SCALE).contains(zoom)) {
calculateZoom(newRect, scaleX, true);
plot.setDomainBoundaries(newRect.left, newRect.right, BoundaryMode.FIXED);
previousLimits.setMinX(newRect.left);
previousLimits.setMaxX(newRect.right);
}
if (EnumSet.of(
Zoom.STRETCH_VERTICAL,
Zoom.STRETCH_BOTH,
Zoom.SCALE).contains(zoom)) {
calculateZoom(newRect, scaleY, false);
plot.setRangeBoundaries(newRect.top, newRect.bottom, BoundaryMode.FIXED);
previousLimits.setMinY(newRect.top);
previousLimits.setMaxY(newRect.bottom);
}
plot.redraw();
}
protected void calculateZoom(RectF newRect, float scale, boolean isHorizontal) {
final float calcMax;
final float span;
if (isHorizontal) {
calcMax = getLastMaxX().floatValue();
span = calcMax - getLastMinX().floatValue();
} else {
calcMax = getLastMaxY().floatValue();
span = calcMax - getLastMinY().floatValue();
}
final float midPoint = calcMax - (span / 2.0f);
final float offset = span * scale / 2.0f;
if (isHorizontal) {
newRect.left = midPoint - offset;
newRect.right = midPoint + offset;
if (newRect.left < getMinXLimit().floatValue()) {
newRect.left = getMinXLimit().floatValue();
}
if (newRect.right > getMaxXLimit().floatValue()) {
newRect.right = getMaxXLimit().floatValue();
}
} else {
newRect.top = midPoint - offset;
newRect.bottom = midPoint + offset;
if (newRect.top < getMinYLimit().floatValue()) {
newRect.top = getMinYLimit().floatValue();
}
if (newRect.bottom > getMaxYLimit().floatValue()) {
newRect.bottom = getMaxYLimit().floatValue();
}
}
}
public Pan getPan() {
return pan;
}
public void setPan(Pan pan) {
this.pan = pan;
}
public Zoom getZoom() {
return zoom;
}
public void setZoom(Zoom zoom) {
this.zoom = zoom;
}
public View.OnTouchListener getDelegate() {
return delegate;
}
/**
* Set a delegate to receive onTouch calls before this class does. If the delegate wishes
* to consume the event, it should return true, otherwise it should return false. Returning
* false will not prevent future onTouch events from filtering through the delegate as it normally
* would when attaching directly to an instance of {#link View}.
* #param delegate
*/
public void setDelegate(View.OnTouchListener delegate) {
this.delegate = delegate;
}
public void reset() {
this.previousLimits = new RectRegion();
this.firstFingerPos = null;
this.fingersRect = null;
}
}
Don't forget to change the class name to avoid a collision with the existing PanZoom implementation.
If you want to constrain the starting window bounds to a sub-section of your data, set those boundaries on the XYPlot instance as normal. Then, configure the PanZoom instance with the absolute boundaries for panning and zooming, typically the min/max values of the series attached to your plot.
Also, if you need to change the visible window after your initial setup in onCreate (result of a user pressing a reset button, etc.) you'll also need to invoke PanZoom.reset() to wipe out it's internal state. This step may not be necessary in the official release implementation.
How do you create a video wall like in the JavaFX 2.0 demo here:
https://www.youtube.com/watch?v=UXSmJYFrulY#t=411
For a start it doesn't have to be videos, it can be images as well. All I'd like to have is to place the nodes like they are in the video, i. e. in a curved shape like the insides of a cylinder or a sphere.
Or is the source of that demo available somewhere?
Thank you very much.
I researched and found a very awesome site with the relevant information:
http://paulbourke.net/geometry/transformationprojection/
The relevant part was the Coordinate System Transformation, in particular the equations for converting between cartesian and spherical coordinates.
double x = r * Math.sin(angle1) * Math.cos(angle2);
double y = r * Math.sin(angle1) * Math.sin(angle2);
double z = r * Math.cos(angle1);
In my example below y isn't used from the formula, since the image rows are stacked.
Note: By using these formulas in 2 nested for-loops from -Math.PI to Math.PI you can lay out the nodes around a sphere. The difficult part regarding the full sphere was to rotate the nodes towards the center, that one I couldn't figure out.
Since I wasn't familiar with Java3D I also checked out the Building a 3D Sample Application.
In the end I got a video wall, the code is reduced to this:
public class VideoWall extends Application {
Random rand = new Random();
Group root = new Group();
PerspectiveCamera camera;
private static final double CAMERA_INITIAL_DISTANCE = -850;
private static final double CAMERA_NEAR_CLIP = 0.1;
private static final double CAMERA_FAR_CLIP = 10000.0;
Image[] images = new Image[] {
new Image("http://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Siberischer_tiger_de_edit02.jpg/320px-Siberischer_tiger_de_edit02.jpg"),
new Image("http://upload.wikimedia.org/wikipedia/commons/thumb/e/e7/White_Lion.jpg/320px-White_Lion.jpg"),
new Image("http://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Lion_female.jpg/319px-Lion_female.jpg")
};
public VideoWall(){
}
public static void main(String[] args) {
launch(args);
}
/**
* Create ImageView with random Image.
* #return
*/
private ImageView createImageView() {
Image image = images[ rand.nextInt(images.length)];
ImageView c = new ImageView( image);
c.setFitWidth(140);
c.setFitWidth(100);
c.setPreserveRatio(true);
return c;
}
#Override
public void start(Stage primaryStage) {
// build camera
camera = new PerspectiveCamera(true);
camera.setNearClip(CAMERA_NEAR_CLIP);
camera.setFarClip(CAMERA_FAR_CLIP);
camera.setTranslateZ(CAMERA_INITIAL_DISTANCE);
// we display any node (imageview, webview, etc)
Node node;
// create a single webview; we only add it once because we don't want to flood youtube
WebView webView = new WebView();
webView.getEngine().load(
"http://www.youtube.com/embed/utUPth77L_o?autoplay=1"
);
webView.setPrefSize(100, 70);
// wall. the degrees depend on the distance, image size, translate start points, etc. so these values were just as they fit
double ringBeginDeg = -30;
double ringEndDeg = 38;
double r = 1300;
double yOffset = 80; // offset per image row
double yOffsetInitial = 120; // initial y offset from "floor"
int count=0;
for( double angle1=Math.toRadians(ringBeginDeg); angle1 <Math.toRadians(ringEndDeg); angle1+=0.08)
{
double angle2 = Math.PI;
for( int i=-3; i <= 3; i++)
{
double x = r * Math.sin(angle1) * Math.cos(angle2);
// double y = r * Math.sin(angle1) * Math.sin(angle2);
double z = r * Math.cos(angle1);
// add 1 webview, the rest imageviews
if( count == 16) {
node = webView;
} else {
node = createImageView();
}
node.setTranslateX(x);
node.setTranslateY(yOffset * i - yOffsetInitial);
node.setTranslateZ(z);
// rotate towards viewer position
Rotate rx = new Rotate();
rx.setAxis(Rotate.Y_AXIS);
rx.setAngle(Math.toDegrees( -angle1));
node.getTransforms().addAll(rx);
root.getChildren().add( node);
count++;
}
}
Scene scene = new Scene(root, 1600, 900, Color.BLACK);
primaryStage.setScene( scene);
primaryStage.show();
scene.setCamera(camera);
}
}
You can add whatever node you prefer. I added a youtube webview for testing. It plays, but the video doesn't get loaded, so all you see is static noise (the grey tile in the screenshot). So in theory you could make the nodes all webview with youtube videos, but that would mean flooding youtube. Better use some offline videos.
Here's a screenshot:
I also toyed around with the full 3d example and creating a ring. That's how it looked like (with always the same image) from an outer view:
Having the camera in the center you can nicely scroll the ring.
If someone wants to toy around, here's a quick & dirty gist with the navigable ring. Use left/right/middle mouse buttons for navigation.
And if you'd like to toy around with a full sphere, you may use this:
// full sphere
for (double angle1 = -Math.PI; angle1 <= Math.PI; angle1 += 0.15) {
for (double angle2 = -Math.PI; angle2 <= Math.PI; angle2 += 0.15) {
double x = r * Math.sin(angle1) * Math.cos(angle2);
double y = r * Math.sin(angle1) * Math.sin(angle2);
double z = r * Math.cos(angle1);
c = createImageView();
c.setTranslateX(x);
c.setTranslateY(y);
c.setTranslateZ(z);
Rotate rx = new Rotate();
rx.setAxis(Rotate.Y_AXIS);
rx.setAngle(Math.toDegrees(-angle1));
c.getTransforms().addAll(rx);
world.getChildren().add(c);
}
}
Which looks like this:
But as mentioned, I haven't figured out yet how to rotate all tiles so that they look into the center. And they'd need to be equally distributed. But that's just for fun and off-topic.
Since it's part of the video in my question, it was only a matter of keeping a list of parallel transitions to create the "build-up" animation of the tiles. The bottom row has a reflection now.
The extended code:
public class VideoWall extends Application {
Random rand = new Random();
Group root = new Group();
PerspectiveCamera camera;
private static final double CAMERA_INITIAL_DISTANCE = -850;
private static final double CAMERA_NEAR_CLIP = 0.1;
private static final double CAMERA_FAR_CLIP = 10000.0;
Image[] images = new Image[] {
new Image("http://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Siberischer_tiger_de_edit02.jpg/320px-Siberischer_tiger_de_edit02.jpg"),
new Image("http://upload.wikimedia.org/wikipedia/commons/thumb/e/e7/White_Lion.jpg/320px-White_Lion.jpg"),
new Image("http://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Lion_female.jpg/319px-Lion_female.jpg")
};
List<ParallelTransition> transitionList = new ArrayList<>();
public VideoWall(){
}
public static void main(String[] args) {
launch(args);
}
/**
* Create ImageView with random Image.
* #return
*/
private ImageView createImageView() {
Image image = images[ rand.nextInt(images.length)];
ImageView c = new ImageView( image);
c.setFitWidth(140);
c.setFitWidth(100);
c.setPreserveRatio(true);
return c;
}
#Override
public void start(Stage primaryStage) {
// build camera
camera = new PerspectiveCamera(true);
camera.setNearClip(CAMERA_NEAR_CLIP);
camera.setFarClip(CAMERA_FAR_CLIP);
camera.setTranslateZ(CAMERA_INITIAL_DISTANCE);
// we display any node (imageview, webview, etc)
Node node;
// wall. the degrees depend on the distance, image size, translate start points, etc. so these values were just as they fit
double ringBeginDeg = -30;
double ringEndDeg = 38;
double r = 1300;
double yOffset = 80; // offset per image row
double yOffsetInitial = 120; // initial y offset from "floor"
int min = -3;
int max = 3;
for( double angle1=Math.toRadians(ringBeginDeg); angle1 <Math.toRadians(ringEndDeg); angle1+=0.08)
{
double angle2 = Math.PI;
for( int i=min; i <= max; i++)
{
double x = r * Math.sin(angle1) * Math.cos(angle2);
// double y = r * Math.sin(angle1) * Math.sin(angle2);
double z = r * Math.cos(angle1);
node = createImageView();
node.setTranslateX(x);
node.setTranslateY(yOffset * i - yOffsetInitial);
node.setTranslateZ(z);
// rotate towards viewer position
Rotate rx = new Rotate();
rx.setAxis(Rotate.Y_AXIS);
rx.setAngle(Math.toDegrees( -angle1));
node.getTransforms().addAll(rx);
// reflection on bottom row
if( i==max) {
Reflection refl = new Reflection();
refl.setFraction(0.8f);
node.setEffect(refl);
}
// build the wall using a transition
node.setVisible(false);
transitionList.add( createTransition( node));
root.getChildren().add( node);
}
}
Scene scene = new Scene(root, 1600, 900, Color.BLACK);
primaryStage.setScene( scene);
primaryStage.show();
scene.setCamera(camera);
AnimationTimer timer = createAnimation();
timer.start();
}
private AnimationTimer createAnimation() {
Collections.sort(transitionList, new Comparator<ParallelTransition>() {
#Override
public int compare(ParallelTransition arg0, ParallelTransition arg1) {
// bottom right to top left
Point2D ref = new Point2D(1000,1000);
Point2D pt0 = new Point2D( arg0.getNode().getTranslateX(), arg0.getNode().getTranslateY());
Point2D pt1 = new Point2D( arg1.getNode().getTranslateX(), arg1.getNode().getTranslateY());
return Double.compare(ref.distance(pt0), ref.distance(pt1));
// bottom row first
// return -Double.compare( arg0.getNode().getTranslateY(), arg1.getNode().getTranslateY());
}
});
AnimationTimer timer = new AnimationTimer() {
long last = 0;
#Override
public void handle(long now) {
//if( (now - last) > 1_000_000_000)
if( (now - last) > 40_000_000)
{
if( transitionList.size() > 0) {
ParallelTransition t = transitionList.remove(0);
t.getNode().setVisible(true);
t.play();
}
last = now;
}
if( transitionList.size() == 0) {
stop();
}
}
};
return timer;
}
private ParallelTransition createTransition( final Node node) {
Path path = new Path();
path.getElements().add(new MoveToAbs( node, node.getTranslateX() - 1000, node.getTranslateY() - 900));
path.getElements().add(new LineToAbs( node, node.getTranslateX(), node.getTranslateY()));
Duration duration = Duration.millis(1500);
PathTransition pt = new PathTransition( duration, path, node);
RotateTransition rt = new RotateTransition( duration, node);
rt.setByAngle(720);
rt.setAutoReverse(true);
ParallelTransition parallelTransition = new ParallelTransition();
parallelTransition.setNode(node);
parallelTransition.getChildren().addAll(pt, rt);
return parallelTransition;
}
public static class MoveToAbs extends MoveTo {
public MoveToAbs( Node node, double x, double y) {
super( x - node.getLayoutX() + node.getLayoutBounds().getWidth() / 2, y - node.getLayoutY() + node.getLayoutBounds().getHeight() / 2);
}
}
public static class LineToAbs extends LineTo {
public LineToAbs( Node node, double x, double y) {
super( x - node.getLayoutX() + node.getLayoutBounds().getWidth() / 2, y - node.getLayoutY() + node.getLayoutBounds().getHeight() / 2);
}
}
}
Well to me it seems to be a matter of creating a Grid, or "Mesh" out of ImageViews.
Then you would Map all the Viewport's to the Image('s) you want to display.
Here for example is a Skybox for 3D implementation using this approach.
Note that this is just a simple Cube.
The image is setup similar to this, though I made it into 6 separate Images.
If you wanted to use video, I recommend you use VLCJ,
They have samples for JavaFX setup Here
For this you would apply the same principals to the WritableImage(s)
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
I added a couple things for you to play with ...
/**
* A self initializing First Person Shooter camera
*
* #author Jason Pollastrini aka jdub1581
*/
public class SimpleFPSCamera extends Parent {
public SimpleFPSCamera() {
initialize();
}
private void update() {
updateControls();
}
private void updateControls() {
if (fwd && !back) {
moveForward();
}
if (strafeL) {
strafeLeft();
}
if (strafeR) {
strafeRight();
}
if (back && !fwd) {
moveBack();
}
if (up && !down) {
moveUp();
}
if (down && !up) {
moveDown();
}
}
/*==========================================================================
Initialization
*/
private final Group root = new Group();
private final Affine affine = new Affine();
private final Translate t = new Translate(0, 0, 0);
private final Rotate rotateX = new Rotate(0, Rotate.X_AXIS),
rotateY = new Rotate(0, Rotate.Y_AXIS),
rotateZ = new Rotate(0, Rotate.Z_AXIS);
private boolean fwd, strafeL, strafeR, back, up, down, shift;
private double mouseSpeed = 1.0, mouseModifier = 0.1;
private double moveSpeed = 10.0;
private double mousePosX;
private double mousePosY;
private double mouseOldX;
private double mouseOldY;
private double mouseDeltaX;
private double mouseDeltaY;
private void initialize() {
getChildren().add(root);
getTransforms().addAll(affine);
initializeCamera();
startUpdateThread();
}
public void loadControlsForSubScene(SubScene scene) {
sceneProperty().addListener(l -> {
if (getScene() != null) {
getScene().addEventHandler(KeyEvent.ANY, ke -> {
if (ke.getEventType() == KeyEvent.KEY_PRESSED) {
switch (ke.getCode()) {
case Q:
up = true;
break;
case E:
down = true;
break;
case W:
fwd = true;
break;
case S:
back = true;
break;
case A:
strafeL = true;
break;
case D:
strafeR = true;
break;
case SHIFT:
shift = true;
moveSpeed = 20;
break;
}
} else if (ke.getEventType() == KeyEvent.KEY_RELEASED) {
switch (ke.getCode()) {
case Q:
up = false;
break;
case E:
down = false;
break;
case W:
fwd = false;
break;
case S:
back = false;
break;
case A:
strafeL = false;
break;
case D:
strafeR = false;
break;
case SHIFT:
moveSpeed = 10;
shift = false;
break;
}
}
ke.consume();
});
}
});
scene.addEventHandler(MouseEvent.ANY, me -> {
if (me.getEventType().equals(MouseEvent.MOUSE_PRESSED)) {
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseOldX = me.getSceneX();
mouseOldY = me.getSceneY();
} else if (me.getEventType().equals(MouseEvent.MOUSE_DRAGGED)) {
mouseOldX = mousePosX;
mouseOldY = mousePosY;
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseDeltaX = (mousePosX - mouseOldX);
mouseDeltaY = (mousePosY - mouseOldY);
mouseSpeed = 1.0;
mouseModifier = 0.1;
if (me.isPrimaryButtonDown()) {
if (me.isControlDown()) {
mouseSpeed = 0.1;
}
if (me.isShiftDown()) {
mouseSpeed = 1.0;
}
t.setX(getPosition().getX());
t.setY(getPosition().getY());
t.setZ(getPosition().getZ());
affine.setToIdentity();
rotateY.setAngle(
Utils.clamp(-360, ((rotateY.getAngle() + mouseDeltaX * (mouseSpeed * mouseModifier)) % 360 + 540) % 360 - 180, 360)
); // horizontal
rotateX.setAngle(
Utils.clamp(-45, ((rotateX.getAngle() - mouseDeltaY * (mouseSpeed * mouseModifier)) % 360 + 540) % 360 - 180, 35)
); // vertical
affine.prepend(t.createConcatenation(rotateY.createConcatenation(rotateX)));
} else if (me.isSecondaryButtonDown()) {
/*
init zoom?
*/
} else if (me.isMiddleButtonDown()) {
/*
init panning?
*/
}
}
});
scene.addEventHandler(ScrollEvent.ANY, se -> {
if (se.getEventType().equals(ScrollEvent.SCROLL_STARTED)) {
} else if (se.getEventType().equals(ScrollEvent.SCROLL)) {
} else if (se.getEventType().equals(ScrollEvent.SCROLL_FINISHED)) {
}
});
}
public void loadControlsForScene(Scene scene) {
scene.addEventHandler(KeyEvent.ANY, ke -> {
if (ke.getEventType() == KeyEvent.KEY_PRESSED) {
switch (ke.getCode()) {
case Q:
up = true;
break;
case E:
down = true;
break;
case W:
fwd = true;
break;
case S:
back = true;
break;
case A:
strafeL = true;
break;
case D:
strafeR = true;
break;
case SHIFT:
shift = true;
moveSpeed = 20;
break;
}
} else if (ke.getEventType() == KeyEvent.KEY_RELEASED) {
switch (ke.getCode()) {
case Q:
up = false;
break;
case E:
down = false;
break;
case W:
fwd = false;
break;
case S:
back = false;
break;
case A:
strafeL = false;
break;
case D:
strafeR = false;
break;
case SHIFT:
moveSpeed = 10;
shift = false;
break;
}
}
ke.consume();
});
scene.addEventHandler(MouseEvent.ANY, me -> {
if (me.getEventType().equals(MouseEvent.MOUSE_PRESSED)) {
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseOldX = me.getSceneX();
mouseOldY = me.getSceneY();
} else if (me.getEventType().equals(MouseEvent.MOUSE_DRAGGED)) {
mouseOldX = mousePosX;
mouseOldY = mousePosY;
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseDeltaX = (mousePosX - mouseOldX);
mouseDeltaY = (mousePosY - mouseOldY);
mouseSpeed = 1.0;
mouseModifier = 0.1;
if (me.isPrimaryButtonDown()) {
if (me.isControlDown()) {
mouseSpeed = 0.1;
}
if (me.isShiftDown()) {
mouseSpeed = 1.0;
}
t.setX(getPosition().getX());
t.setY(getPosition().getY());
t.setZ(getPosition().getZ());
affine.setToIdentity();
rotateY.setAngle(
Utils.clamp(-360, ((rotateY.getAngle() + mouseDeltaX * (mouseSpeed * mouseModifier)) % 360 + 540) % 360 - 180, 360)
); // horizontal
rotateX.setAngle(
Utils.clamp(-45, ((rotateX.getAngle() - mouseDeltaY * (mouseSpeed * mouseModifier)) % 360 + 540) % 360 - 180, 35)
); // vertical
affine.prepend(t.createConcatenation(rotateY.createConcatenation(rotateX)));
} else if (me.isSecondaryButtonDown()) {
/*
init zoom?
*/
} else if (me.isMiddleButtonDown()) {
/*
init panning?
*/
}
}
});
scene.addEventHandler(ScrollEvent.ANY, se -> {
if (se.getEventType().equals(ScrollEvent.SCROLL_STARTED)) {
} else if (se.getEventType().equals(ScrollEvent.SCROLL)) {
} else if (se.getEventType().equals(ScrollEvent.SCROLL_FINISHED)) {
}
});
}
private void initializeCamera() {
getCamera().setNearClip(0.1);
getCamera().setFarClip(100000);
getCamera().setFieldOfView(42);
getCamera().setVerticalFieldOfView(true);
//getCamera().getTransforms().add(new Rotate(180, Rotate.Z_AXIS));
root.getChildren().add(getCamera());
}
private void startUpdateThread() {
new AnimationTimer() {
#Override
public void handle(long now) {
update();
}
}.start();
}
/*==========================================================================
Movement
*/
private void moveForward() {
affine.setTx(getPosition().getX() + moveSpeed * getN().getX());
affine.setTy(getPosition().getY() + moveSpeed * getN().getY());
affine.setTz(getPosition().getZ() + moveSpeed * getN().getZ());
}
private void strafeLeft() {
affine.setTx(getPosition().getX() + moveSpeed * -getU().getX());
affine.setTy(getPosition().getY() + moveSpeed * -getU().getY());
affine.setTz(getPosition().getZ() + moveSpeed * -getU().getZ());
}
private void strafeRight() {
affine.setTx(getPosition().getX() + moveSpeed * getU().getX());
affine.setTy(getPosition().getY() + moveSpeed * getU().getY());
affine.setTz(getPosition().getZ() + moveSpeed * getU().getZ());
}
private void moveBack() {
affine.setTx(getPosition().getX() + moveSpeed * -getN().getX());
affine.setTy(getPosition().getY() + moveSpeed * -getN().getY());
affine.setTz(getPosition().getZ() + moveSpeed * -getN().getZ());
}
private void moveUp() {
affine.setTx(getPosition().getX() + moveSpeed * -getV().getX());
affine.setTy(getPosition().getY() + moveSpeed * -getV().getY());
affine.setTz(getPosition().getZ() + moveSpeed * -getV().getZ());
}
private void moveDown() {
affine.setTx(getPosition().getX() + moveSpeed * getV().getX());
affine.setTy(getPosition().getY() + moveSpeed * getV().getY());
affine.setTz(getPosition().getZ() + moveSpeed * getV().getZ());
}
/*==========================================================================
Properties
*/
private final ReadOnlyObjectWrapper<PerspectiveCamera> camera = new ReadOnlyObjectWrapper<>(this, "camera", new PerspectiveCamera(true));
public final PerspectiveCamera getCamera() {
return camera.get();
}
public ReadOnlyObjectProperty cameraProperty() {
return camera.getReadOnlyProperty();
}
/*==========================================================================
Callbacks
| R | Up| F | | P|
U |mxx|mxy|mxz| |tx|
V |myx|myy|myz| |ty|
N |mzx|mzy|mzz| |tz|
*/
//Forward / look direction
private final Callback<Transform, Point3D> F = (a) -> {
return new Point3D(a.getMzx(), a.getMzy(), a.getMzz());
};
private final Callback<Transform, Point3D> N = (a) -> {
return new Point3D(a.getMxz(), a.getMyz(), a.getMzz());
};
// up direction
private final Callback<Transform, Point3D> UP = (a) -> {
return new Point3D(a.getMyx(), a.getMyy(), a.getMyz());
};
private final Callback<Transform, Point3D> V = (a) -> {
return new Point3D(a.getMxy(), a.getMyy(), a.getMzy());
};
// right direction
private final Callback<Transform, Point3D> R = (a) -> {
return new Point3D(a.getMxx(), a.getMxy(), a.getMxz());
};
private final Callback<Transform, Point3D> U = (a) -> {
return new Point3D(a.getMxx(), a.getMyx(), a.getMzx());
};
//position
private final Callback<Transform, Point3D> P = (a) -> {
return new Point3D(a.getTx(), a.getTy(), a.getTz());
};
private Point3D getF() {
return F.call(getLocalToSceneTransform());
}
public Point3D getLookDirection() {
return getF();
}
private Point3D getN() {
return N.call(getLocalToSceneTransform());
}
public Point3D getLookNormal() {
return getN();
}
private Point3D getR() {
return R.call(getLocalToSceneTransform());
}
private Point3D getU() {
return U.call(getLocalToSceneTransform());
}
private Point3D getUp() {
return UP.call(getLocalToSceneTransform());
}
private Point3D getV() {
return V.call(getLocalToSceneTransform());
}
public final Point3D getPosition() {
return P.call(getLocalToSceneTransform());
}
}
Your Modified code:
public class VideoWall extends Application {
Random rand = new Random();
Group root = new Group();
PerspectiveCamera camera;
SimpleFPSCamera fpsCam;
private static final double CAMERA_INITIAL_DISTANCE = -10000;
private static final double CAMERA_NEAR_CLIP = 0.1;
private static final double CAMERA_FAR_CLIP = 100000.0;
Image[] images = new Image[]{
new Image("http://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Siberischer_tiger_de_edit02.jpg/320px-Siberischer_tiger_de_edit02.jpg"),
new Image("http://upload.wikimedia.org/wikipedia/commons/thumb/e/e7/White_Lion.jpg/320px-White_Lion.jpg"),
new Image("http://upload.wikimedia.org/wikipedia/commons/thumb/4/47/Lion_female.jpg/319px-Lion_female.jpg")
};
List<ParallelTransition> transitionList = new ArrayList<>();
List<ImageView> imageList = new ArrayList<>();
public VideoWall() {
}
public static void main(String[] args) {
launch(args);
}
/**
* Create ImageView with random Image.
*
* #return
*/
private ImageView createImageView() {
Image image = images[rand.nextInt(images.length)];
ImageView c = new ImageView(image);
c.setFitWidth(140);
c.setFitWidth(100);
c.setPreserveRatio(true);
return c;
}
private BillboardImage createBillboardImage() {
Image image = images[rand.nextInt(images.length)];
BillboardImage c = new BillboardImage(image);
c.setFitWidth(140);
c.setFitWidth(100);
c.setPreserveRatio(true);
return c;
}
#Override
public void start(Stage primaryStage) {
// build camera
//camera = new PerspectiveCamera(true);
//camera.setNearClip(CAMERA_NEAR_CLIP);
//camera.setFarClip(CAMERA_FAR_CLIP);
//camera.setTranslateZ(CAMERA_INITIAL_DISTANCE);
fpsCam = new SimpleFPSCamera();
// we display any node (imageview, webview, etc)
Node node;
// wall. the degrees depend on the distance, image size, translate start points, etc. so these values were just as they fit
double ringBeginDeg = -30;
double ringEndDeg = 38;
double r = 1300;
double yOffset = 80; // offset per image row
double yOffsetInitial = 120; // initial y offset from "floor"
int min = -3;
int max = 3;
/*
for (double angle1 = Math.toRadians(ringBeginDeg); angle1 < Math.toRadians(ringEndDeg); angle1 += 0.08) {
double angle2 = Math.PI;
for (int i = min; i <= max; i++) {
double x = r * Math.sin(angle1) * Math.cos(angle2);
// double y = r * Math.sin(angle1) * Math.sin(angle2);
double z = r * Math.cos(angle1);
node = createImageView();
node.setTranslateX(x);
node.setTranslateY(yOffset * i - yOffsetInitial);
node.setTranslateZ(z);
// rotate towards viewer position
Rotate rx = new Rotate();
rx.setAxis(Rotate.Y_AXIS);
rx.setAngle(Math.toDegrees(-angle1));
node.getTransforms().addAll(rx);
// reflection on bottom row
if (i == max) {
Reflection refl = new Reflection();
refl.setFraction(0.8f);
node.setEffect(refl);
}
// build the wall using a transition
node.setVisible(false);
transitionList.add(createTransition(node));
root.getChildren().add(node);
}
}*/
// full sphere
for (double angle1 = -Math.PI; angle1 <= Math.PI; angle1 += 0.48) {
for (double angle2 = -Math.PI; angle2 <= Math.PI; angle2 += 0.48) {
double x = r * Math.sin(angle1) * Math.cos(angle2);
double y = r * Math.sin(angle1) * Math.sin(angle2);
double z = r * Math.cos(angle1);
BillboardImage c = createBillboardImage();
c.setTranslateX(x);
c.setTranslateY(y);
c.setTranslateZ(z);
imageList.add(c);
}
}
root.getChildren().add(fpsCam);
root.getChildren().addAll(imageList);
Scene scene = new Scene(root, 1600, 900, true, SceneAntialiasing.BALANCED);
scene.setFill(Color.BLACK);
scene.setCamera(fpsCam.getCamera());
fpsCam.loadControlsForScene(scene);
primaryStage.setScene(scene);
primaryStage.show();
AnimationTimer timer = createBillboardTimer();
timer.start();
}
private AnimationTimer createBillboardTimer() {
return new AnimationTimer() {
#Override
public void handle(long now) {
if(!imageList.isEmpty()){
imageList.stream().forEach(bbi ->{
((BillboardImage)bbi).updateMatrix(bbi, fpsCam);
});
}
}
};
}
private AnimationTimer createAnimation() {
Collections.sort(transitionList, new Comparator<ParallelTransition>() {
#Override
public int compare(ParallelTransition arg0, ParallelTransition arg1) {
// bottom right to top left
Point2D ref = new Point2D(1000, 1000);
Point2D pt0 = new Point2D(arg0.getNode().getTranslateX(), arg0.getNode().getTranslateY());
Point2D pt1 = new Point2D(arg1.getNode().getTranslateX(), arg1.getNode().getTranslateY());
return Double.compare(ref.distance(pt0), ref.distance(pt1));
// bottom row first
// return -Double.compare( arg0.getNode().getTranslateY(), arg1.getNode().getTranslateY());
}
});
AnimationTimer timer = new AnimationTimer() {
long last = 0;
#Override
public void handle(long now) {
//if( (now - last) > 1_000_000_000)
if ((now - last) > 40_000_000) {
if (transitionList.size() > 0) {
ParallelTransition t = transitionList.remove(0);
t.getNode().setVisible(true);
t.play();
}
last = now;
}
if (transitionList.size() == 0) {
stop();
}
}
};
return timer;
}
private ParallelTransition createTransition(final Node node) {
Path path = new Path();
path.getElements().add(new MoveToAbs(node, node.getTranslateX() - 1000, node.getTranslateY() - 900));
path.getElements().add(new LineToAbs(node, node.getTranslateX(), node.getTranslateY()));
Duration duration = Duration.millis(1500);
PathTransition pt = new PathTransition(duration, path, node);
RotateTransition rt = new RotateTransition(duration, node);
rt.setByAngle(720);
rt.setAutoReverse(true);
ParallelTransition parallelTransition = new ParallelTransition();
parallelTransition.setNode(node);
parallelTransition.getChildren().addAll(pt, rt);
return parallelTransition;
}
public static class MoveToAbs extends MoveTo {
public MoveToAbs(Node node, double x, double y) {
super(x - node.getLayoutX() + node.getLayoutBounds().getWidth() / 2, y - node.getLayoutY() + node.getLayoutBounds().getHeight() / 2);
}
}
public static class LineToAbs extends LineTo {
public LineToAbs(Node node, double x, double y) {
super(x - node.getLayoutX() + node.getLayoutBounds().getWidth() / 2, y - node.getLayoutY() + node.getLayoutBounds().getHeight() / 2);
}
}
/*
*/
public enum BillboardMode {
SPHERICAL,
CYLINDRICAL;
}
private class BillboardImage extends ImageView{
// Add transform to Node that needs to look at target..
public Affine affine = new Affine();
public BillboardImage() {
this.getTransforms().add(affine);
}
public BillboardImage(String url) {
super(url);
this.getTransforms().add(affine);
}
public BillboardImage(Image image) {
super(image);
this.getTransforms().add(affine);
}
// set up to look at camera, can change to any other Node
protected void updateMatrix(Node billBoardNode, Node other) {
Transform self = billBoardNode.getLocalToSceneTransform(),
oth = other.getLocalToSceneTransform();
Bounds b;
double cX, cY, cZ;
if (!(billBoardNode instanceof Shape3D)) {
b = billBoardNode.getBoundsInLocal();
cX = b.getWidth() / 2;
cY = b.getHeight() / 2;
cZ = b.getDepth() / 2;
} else {
cX = self.getTx();
cY = self.getTy();
cZ = self.getTz();
}
Point3D otherPos = Point3D.ZERO.add(oth.getTx(), oth.getTy(), oth.getTz());
Point3D selfPos = new Point3D(cX, cY, cZ);
Point3D up = Point3D.ZERO.add(0, -1, 0),
forward = new Point3D(
(selfPos.getX()) - otherPos.getX(),
(selfPos.getY()) - otherPos.getY(),
(selfPos.getZ()) - otherPos.getZ()
).normalize(),
right = up.crossProduct(forward).normalize();
up = forward.crossProduct(right).normalize();
switch (getBillboardMode()) {
case SPHERICAL:
affine.setMxx(right.getX()); affine.setMxy(up.getX());affine.setMzx(forward.getX());
affine.setMyx(right.getY());affine.setMyy(up.getY()); affine.setMzy(forward.getY());
affine.setMzx(right.getZ());affine.setMzy(up.getZ());affine.setMzz(forward.getZ());
affine.setTx(cX * (1 - affine.getMxx()) - cY * affine.getMxy() - cZ * affine.getMxz());
affine.setTy(cY * (1 - affine.getMyy()) - cX * affine.getMyx() - cZ * affine.getMyz());
affine.setTz(cZ * (1 - affine.getMzz()) - cX * affine.getMzx() - cY * affine.getMzy());
break;
case CYLINDRICAL:
affine.setMxx(right.getX());affine.setMxy(0);affine.setMzx(forward.getX());
affine.setMyx(0);affine.setMyy(1);affine.setMzy(0);
affine.setMzx(right.getZ()); affine.setMzy(0);affine.setMzz(forward.getZ());
affine.setTx(cX * (1 - affine.getMxx()) - cY * affine.getMxy() - cZ * affine.getMxz());
affine.setTy(cY * (1 - affine.getMyy()) - cX * affine.getMyx() - cZ * affine.getMyz());
affine.setTz(cZ * (1 - affine.getMzz()) - cX * affine.getMzx() - cY * affine.getMzy());
break;
}
}
public BillboardMode getBillboardMode() {
return BillboardMode.SPHERICAL;
}
}
}
At least now the Images are billboarded to the Camera..
Feel free to play with my FPSCamera as well ..
Controls are like any standard 1st person shooter
w = forward, s = back,
a = left-strafe, d = right-strafe,
q & e are up and down.