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;
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.
I'm trying to draw an Icon over everything on the screen (TOP MOST) similar to the chathead of new Facebook messenger
I have create a service to work in the background and based on a specific condition my icon should appear on the screen (exactly like when someone sends you a message on facebook the messenger service will hook the message and shows the chathead on the screen to notify you about the new message)
What I did:
I have created the service and gave it the permission to show system alert windows (since the head is actually a system alert window)
[assembly: UsesPermission(Name = Android.Manifest.Permission.SystemAlertWindow)]
I have inherited a class (StickyHeadView) from ImageView and implemented OnTouchListener listener using the following way :
class StickyHeadView : ImageView, Android.Views.View.IOnTouchListener
{
private StickyHeadService OwnerService;
public StickyHeadView(StickyHeadService ContextService, Context context)
: base(context)
{
OwnerService = ContextService;
SetOnTouchListener(this);
}
float TouchMoveX;
float TouchMoveY;
public bool OnTouch(View v, MotionEvent e)
{
var windowService = OwnerService.GetSystemService(Android.Content.Context.WindowService);
var windowManager = windowService.JavaCast<Android.Views.IWindowManager>();
switch (e.Action & e.ActionMasked)
{
case MotionEventActions.Move:
TouchMoveX = (int)e.GetX();
TouchMoveY = (int)e.GetY();
OwnerService.LOParams.X = (int)(TouchMoveX);
OwnerService.LOParams.Y = (int)(TouchMoveY);
windowManager.UpdateViewLayout(this, OwnerService.LOParams);
Log.Debug("Point : ", "X: " + Convert.ToString(OwnerService.LOParams.X) + " Y: " + Convert.ToString(OwnerService.LOParams.Y));
return true;
case MotionEventActions.Down:
return true;
case MotionEventActions.Up:
return true;
}
return false;
}
}
The service has wiindow manager to show the Icon on it...in Service "OnStart" event I initialize the Head :
private StickyHeadView MyHead;
public Android.Views.WindowManagerLayoutParams LOParams;
public override void OnStart(Android.Content.Intent intent, int startId)
{
base.OnStart(intent, startId);
var windowService = this.GetSystemService(Android.Content.Context.WindowService);
var windowManager = windowService.JavaCast<Android.Views.IWindowManager>();
MyHead = new StickyHeadView(this, this);
MyHead.SetImageResource(Resource.Drawable.Icon);
LOParams = new Android.Views.WindowManagerLayoutParams(Android.Views.WindowManagerLayoutParams.WrapContent,
Android.Views.WindowManagerLayoutParams.WrapContent,
Android.Views.WindowManagerTypes.Phone,
Android.Views.WindowManagerFlags.NotFocusable,
Android.Graphics.Format.Translucent);
LOParams.Gravity = GravityFlags.Top | GravityFlags.Left;
LOParams.X = 10;
LOParams.Y = 10;
windowManager.AddView(MyHead, LOParams);
}
as you can see I have declared a WindowManager and added the view (MyHead) to it with special parameters
My Problem :
When ever I try to move the View (My head) it doesn't move in a stable way and keeps having a quake!
I'm testing it using android 4.0.4 on real HTC Phone
I'm using monodroid
Please help...if the implementation of the touch is not right please suggest a better way...thank you.
In your code just use...
TYPE_SYSTEM_ALERT
or
TYPE_PHONE
instead of
TYPE_SYSTEM_OVERLAY
Hope this will help you.
a working example:
#Override
public void onCreate() {
super.onCreate();
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
chatHead = new ImageView(this);
chatHead.setImageResource(R.drawable.ic_launcher);
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, //TYPE_PHONE
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);
params.gravity = Gravity.TOP | Gravity.LEFT;
params.x = 0;
params.y = 100;
windowManager.addView(chatHead, params);
chatHead.setOnTouchListener(new View.OnTouchListener() {
private int initialX;
private int initialY;
private float initialTouchX;
private float initialTouchY;
#Override public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
initialX = params.x;
initialY = params.y;
initialTouchX = event.getRawX();
initialTouchY = event.getRawY();
return true;
case MotionEvent.ACTION_UP:
return true;
case MotionEvent.ACTION_MOVE:
params.x = initialX + (int) (event.getRawX() - initialTouchX);
params.y = initialY + (int) (event.getRawY() - initialTouchY);
windowManager.updateViewLayout(chatHead, params);
return true;
}
return false;
}
});
}
The e.GetX()/eGetY() you are using is relative to view position so when you move the view with UpdateViewLayout the next values will be relative to the move. It works using GetRawX()/GetRawY(), but you have to keep track of the initial Down rawX and rawY also.
Here is my JAVA that works:
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
layoutParams.x = Math.round(event.getRawX() - downX);
layoutParams.y = Math.round(event.getRawY() - downY);
windowManager.updateViewLayout(floatingView, layoutParams);
return true;
case MotionEvent.ACTION_DOWN:
downX = event.getRawX() - layoutParams.x;
downY = event.getRawY() - layoutParams.y;
return true;
case MotionEvent.ACTION_UP:
return true;
}
return false;
}
One comment, there's a big downside in using windowManager.updateViewLayout(...) this method will call onLayout on the floating view for each move, and that might be a performance issue, anyway until now I haven't found another method to move the floating view.
Try this might be help ful
first add global variable on your activity:
WindowManager wm;
LinearLayout lay;
float downX,downY;
after put in code to oncreate on your activity
Button btnstart=(Button)findViewById(R.id.button1);
Button btnstop=(Button)findViewById(R.id.button2);
btnstart.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
// TODO Auto-generated method stub
if(lay==null)
{
wm = (WindowManager) getApplicationContext().getSystemService(
Context.WINDOW_SERVICE);
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
| WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);
params.x = (int) wm.getDefaultDisplay().getWidth();
params.y = 0;
// params.height = wm.getDefaultDisplay().getHeight()/2;
params.width = LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
params.gravity = Gravity.TOP | Gravity.LEFT;
params.setTitle("Info");
lay = null;
lay = new LinearLayout(getApplicationContext());
lay.setOrientation(LinearLayout.VERTICAL);
// lay.setAlpha(0.5f);
TextView txt_no = new TextView(getApplicationContext());
txt_no.setTextSize(10.0f);
txt_no.setText("Moving view by stack user!");
txt_no.setTextColor(Color.BLACK);
// txt_no.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
// LayoutParams.WRAP_CONTENT));
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
layoutParams.setMargins(0, 0, 0, 0); // margins as you wish
txt_no.setGravity(Gravity.RIGHT);
txt_no.setBackgroundColor(Color.WHITE);
txt_no.setLayoutParams(layoutParams);
txt_no.setPadding(10, 10, 10, 10);
lay.addView(txt_no);
AlphaAnimation alpha = new AlphaAnimation(0.5F, 0.5F);
alpha.setDuration(0); // Make animation instant
alpha.setFillAfter(true); // Tell it to persist after the animation ends
// And then on your layout
wm.addView(lay, params);
txt_no.startAnimation(alpha);
downX=params.x;
downY=params.y;
Log.v("MSES>", "x="+ downX +",y="+ downY);
lay.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
params.x = Math.round(event.getRawX() - downX);
params.y = Math.round(event.getRawY() - downY);
wm.updateViewLayout(lay, params);
Log.v("MSES EVENT>", "x="+ event.getRawX() +",y="+ event.getRawY());
Log.v("MSES MOVE>", "x="+ params.x +",y="+ params.y);
return true;
case MotionEvent.ACTION_DOWN:
downX = event.getRawX() - params.x;
downY = event.getRawY() - params.y;
Log.v("MSES DOWN>", "x="+ params.x +",y="+ params.y);
return true;
case MotionEvent.ACTION_UP:
//params.x = Math.round(event.getRawX() - downX);
//params.y = Math.round(event.getRawY() - downY);
//wm.updateViewLayout(lay, params);
return true;
}
return false;
}
});
}
}
});
btnstop.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
// TODO Auto-generated method stub
if (lay != null) {
lay.removeAllViews();
wm.removeViewImmediate(lay);
lay = null;
}
}
});