I have a text element and a skybox in my scene. When the scene initialises, I want the text to animate its position once.
<!-- scene elements -->
<a-scene coogee2006-scene embedded style="width:100%;height:400px;">
<a-assets>
<img
id="coogee2006"
src="/assets/vr/sydney-coogee-3-peter-gawthrop.jpg"
preload="auto">
<audio
id="beachsound"
src="/assets/vr/beach1.wav"
preload="auto">
</a-assets>
<a-sky src="#coogee2006"
rotation="0 -90 0">
</a-sky>
<!-- text animates in on startup (see searise_vr.js) -->
<a-text
id="coogee2006-text"
value="Coogee, Sydney\n2006"
position="5 12.5 -50"
rotation="0 -15 0"
scale="20 20 20"
visible="true"
text="anchor:align;alphaTest:0.2;width:5;
value:COOGEE, SYDNEY\n2006;
zOffset:0;color:#000;font:exo2bold"
sound="src:#beachsound;autoplay:true;loop:true;volume:20;">
<a-animation
attribute="position"
dur="3000"
begin="coogeetour"
to="12.5 12.5 -50"
easing="ease-in"
fill="both"
repeat="0">
</a-animation>
</a-text>
</a-scene>
If I set a static delay with begin=5000, it works fine, but if I try to set it on an event, like begin="coogeetour", the animation doesn't occur. I've tried firing the event two ways:
First, by registering a component for the scene, in a script tag above the a-scene tag, and using document.querySelector() identify the text element:
<script>
AFRAME.registerComponent('coogee2006-scene', {
// emit text events when the scene is initialised
init: function () {
console.log('Running coogee2006-scene.init()');
document.querySelector("#coogee2006-text").emit('coogeetour');
}
});
</script>
Second, by registering a component for the text element and using this.el, as in the A-Frame Writing a Component section, and putting this in an external file that is linked:
AFRAME.registerComponent('coogee2006-text', {
// emit text events when the scene is initialised
init: function () {
console.log('Initialising text element');
this.el.emit('coogeetour');
}
});
In either case, the console.log works, so the component is initialising, but the animation isn't happening. I can't find coogeetour in the elements' event listeners when debugging, but I don't know if that's because emit() isn't working properly or because it oughtn't show up in the debugging.
EDIT: here's my console log on loading:
Navigated to http://127.0.0.1:4000/private/JoQyfM/
index.js:73A-Frame Version: 0.5.0 (Date 10-02-2017, Commit #110055d)
index.js:74three Version: ^0.83.0
index.js:75WebVR Polyfill Version: dmarcos/webvr-polyfill#a02a8089b
browser.js:117 core:a-assets:warn Asset loading timed out in +0ms 3000 ms
three.js:19590 THREE.WebGLRenderer 83
(index):81 Running coogee2006-scene.init()
browser.js:117 components:sound:warn All the sounds are playing. If you need to play more sounds simultaneously consider increasing the size of pool with the `poolSize` attribute. +118ms
three.js:17507 THREE.WebGLRenderer: image is not power of two (5980x2990). Resized to 8192x4096 <img crossorigin="anonymous" src="/assets/vr/sydney-coogee-3-peter-gawthrop.jpg">
I am not sure exactly why, but it looks like the component methods are swallowing the emit() event. I tried putting that same call in the update() function but it still did not work.
What did work was to put the emit call in a timeout:
setTimeout(function() { document.querySelector("#coogee2006-text").emit('coogeetour'); }, 0);
Codepen: http://codepen.io/shanabus/pen/zZYBaa
Okay, it looks like begin only works with numerical values; delay works with both numerical values and event names. To be fair, this is described in the attribute table for animations, but the block below it on the begin attribute shows an example of it being used with an event name. Maybe a depreciated attribute?
EDIT: okay, maybe this isn't the answer. I'm not entirely sure why delay and begin both exist—is it so there can be a delay following an event trigger, or is delay just depreciated?
Related
I am looking to add a piece of text to the camera on an A-frame scene which will act as a link to transfer to a non-VR page.
<a-entity camera look-controls position="0 0 0">
<a-cursor visible="false">
</a-cursor>
<a-entity text="value: Galleries; color: black; width: 2;" position="-0.25 0.7 -1" onClickLink>
</a-entity>
</a-entity>
The onClickLink function is registered and uses window.location.href = "https://www.google.com"; to change page as suggested in the docs.
I have created a gallery, and want to give users a simple UI link at the top to take them back to a list of other galleries - however - in the example above the onClickLink function just gets applied to the whole scene rather than only the piece of text. I guess it has maybe been applied to its parent - the camera - so any click counts as firing the event? Or do I need to add something to the event to determine what object was clicked? I'm not sure how raycasting would work on something in the top left corner of the camera?
I do not want to use a 3D object in the scene, and I similarly don't want to use the Link portal entity that will be placed in the scene (I did try going down this route similar to the above, and it again just applied the link to the entire scene rather than on clicking the link itself).
Is there a way to achieve what I'm after?
You can use standard html elements and a-frame element to gather and use Css (position: fixed; z-index: 999) to bring it forward to the top but to create an external html link I share part of my code hope you find the idea, you can use jquery(or pure javascript) to add click event to your a-frame objest, but first add below code to <a-camera tag:
raycaster="far: 10000; objects: .clickable"
then add "clickable" class name to any A-frame object you want to be clicked by user
<a-image visible=true id="web" class="clickable external" src="#imgweb" alpha-test="0.5" position="-0.14 -0.9 0" height="0.18" width="0.18"></a-image>
and then like any html dom object you can access to aframe objects by javascript:
window.onload = function() {
$(".external").each(function(index) {
$(this).on("click", function() {
console.log(this.id + ' Pushed');
switch (this.id.toLowerCase()) {
case 'web':
window.open("http://www.farsix.com/");
break;
case 'instagram':
window.open("https://www.instagram.com/");
break;
default:
console.log("Unknown Link!");
};
});
});
};
How in a-frame to cyclically play the next sound after the first start?
Animation does not work.
<a-sky src="#room" sound="src:#soundstart; autoplay:true; loop:false" rotation="0 300 0" animation="property:sound.src; to:#soundfon; autoplay:true; delay:5000; loop:true"></a-sky>
This seems like a job for an custom a-frame component:
Play the first sound
Wait until it's finished
Play the second sound.
Lets say you have a setup like this:
<a-entity manager></a-entity>
<a-box id="first" sound="src: url(sound1.mp3);"></a-box>
<a-box id="second" sound="src: url(sound2.mp3);"></a-box>
You can easily manage the sounds with a component as such:
AFRAME.registerComponent('manager', {
init: function() {
// grab the boxes
let first = document.querySelector("#first")
let second = document.querySelector("#second")
// play the first sound
first.components.sound.playSound();
// wait for it to end
first.addEventListener("sound-ended", function() {
// play the second one
second.components.sound.playSound();
})
}
}
check it out in this glitch
I am using the updated super-hands component in a-frame. I have replicated the basic example from the docs (see below).
<head>
<title>Most Basic Super-Hands Example</title>
<script src="https://aframe.io/releases/0.8.2/aframe.min.js"></script>
<script src="https://cdn.rawgit.com/donmccurdy/aframe-extras/v4.1.2/dist/aframe-extras.min.js"></script>
<script src="https://unpkg.com/super-hands#3.0.0/dist/super-hands.min.js"></script>
</head>
<body>
<a-scene>
<a-assets></a-assets>
<a-entity>
<a-camera></a-camera>
<a-entity sphere-collider="objects: a-box" super-hands hand-controls="left"></a-entity>
<a-entity sphere-collider="objects: a-box" super-hands hand-controls="right"></a-entity>
</a-entity>
<!-- hover & drag-drop won't have any obvious effect without some additional event handlers or components. See the examples page for more -->
<a-box hoverable grabbable stretchable draggable dropppable color="blue" position="0 0 -1"></a-box>
</a-scene>
</body>
Which works well out of the box when it comes to grabbing objects and being able to affect their position, but not their rotation. I wondered if it was possible, (without using physics and adding a static body to the controller) to affect the rotation of the targeted objects?
I have been able to get the rotation working by adding the relevant mixin and phase-shift component from this example but given it seems possible to grab objects affecting their position I wondered if I could modify the basic example to change the rotation instead of the position (ideally I would be able to choose whether to affect position, rotation or both). I guess I'm imagining perhaps creating another component like rotatable or maybe a build on the grabbable component like grabbable="property: rotation" that I could add to the target objects.
I would like to know if this is possible in general as I would like to understand the component better so as to have more control. To give some context for this particular question however, I have a situation where the super-hands controllers are children of a dynamic-body and once a static-body is added, it causes problems with the behaviour of the parent dynamic-body.
Any advice appreciated as ever, if you need any more info, please let me know.
If you want the rotation without physics, you'll need to implement your own version of grabbable that does the matrix math. It would go something like this:
tick: (function () {
const grabeeMatrix = new window.THREE.Matrix4()
const ignoreScale = new window.THREE.Vector3()
return function () {
if (this.grabber) {
grabeeMatrix.multiplyMatrices(
this.grabber.object3D.matrixWorld,
this.grabOffsetMatrix
)
grabeeMatrix.multiplyMatrices(this.parentOffsetMatrix, grabeeMatrix)
// using decomp over direct Object3D.matrix manipulation
// keeps in sync with other A-Frame components
grabeeMatrix.decompose(
this.el.object3D.position,
this.el.object3D.quaternion,
// let stretchable manage scale
ignoreScale
)
}
}
})(),
resetGrabber: function () {
if (!this.grabber) {
return false
}
this.grabber.object3D.updateMatrixWorld()
this.el.object3D.parent.updateMatrixWorld()
// save difference between grabber world matrix and grabee world matrix
this.grabOffsetMatrix
.getInverse(this.grabber.object3D.matrixWorld)
.multiply(this.el.object3D.matrixWorld)
// save difference between grabee world and local matrices
this.parentOffsetMatrix.getInverse(this.el.object3D.parent.matrixWorld)
return true
},
I am using the event-set-component to cause my obj model under to increase scale when the cursor hovers over it.
This is working correctly.
But how would I make it spin as well as increase size?
I found the following code on AFrame docs but I do not know how to implement it so it triggers when the mouse is over the entity.
<a-animation attribute="material.opacity" begin="fade" to="0"></a-animation>
As you have asked for a different method in your comment I suggest to use a multi use component like the one I have written:
AFRAME.registerComponent('event-animate', {
schema: {
target: {type: 'selector'},
aevent: {default: 'animation1'},
triggeraction: {default: 'click'}
},
init: function () {
var data = this.data;
this.el.addEventListener(data.triggeraction, function () {
data.target.emit(data.aevent);
});
}
});
So in HTML it would look something like this:
<a-entity id="object1"
event-animate="target:object1;
triggeraction:mouseenter;
aevent:eventstart">
<a-animation attribute="scale"
dur="5000"
begin="eventstart"
from="1"
to ="5"
direction="alternate">
</a-animation>
<a-animation attribute="rotation"
dur="5000"
begin="eventstart"
from="0 0 0"
to="0 360 0"
direction="alternate">
</a-animation>
</a-entity>
The direction="alternate" should bring it back to its original position.
The quoted animation will work, if You set the begin event properly:
<a-animation attribute="rotation"
dur="2000"
begin="mouseenter"
to="0 360 0"
repeat="1"><a-animation>
On mouseenter, the animation triggers, and rotates the entity once.
To gain more control over what You do, You would need to get deep into making components.
1. The Easiest way i can think of, is using both the animation component, and Your own. You would need to set up a component listening for the mouseenter/mousexit, and trigger the animation:
AFRAME.registerComponent('mouseenterhandler', {
init: function () {
let el = this.el; //Your element reference
el.addEventListener('mouseenter, function () {
// get the rotation, by getAttribute, or by accessing the
//object3D.rotation
let rotation = el.getAttribute('rotation');
//lets rotate it to the same position
rotation.y += 360;
//set the animation component accordingly:
el.children[0].setAttribute('to',rotation);
//emit the 'begin' event:
el.emit('startrotating');
});
}
});
Quick Improvement if necessary: disable the listener, when the animation is triggered. Made with a boolean switched on the mouseenter event, and the animationend event.
2. You can choose not to use the animation component, and check on tick() if the cursor is over. If so, rotate the element by the actualRotation.y+0.1 ( or any other desired rotation ).
As noted before, You can access the rotation by getAttribute() or el.object3D.rotation.
As for the scale, You if You need to rotate + rescale the object on the mouseenter event, just add another animation, and use it like i did with the rotation.I'm not sure how it's usually done, in my experience animations are good, when there are not that many interactions, because they sometimes do unexpected things, which You have to predict/find out, and prevent.
On the other hand, making any animation manually ( changing properties on tick ) may seem laggy if the rotation delta is too big. You need to play with it, and find out which suits You best.
I have a "variable assignment" component that looks like the following (the blue diamond with the yellow sphere attached):
http://i.imgur.com/nJotPgW.gif (unfortunately I don't have enough reputation to inline the image)
Here the yellow sphere that things can snap into is created in the initialization of the variable assignment component like so
AFRAME.registerComponent('variable-assignment', {
schema: {
grabbable: {default: true}
},
init: function () {
this.el.innerHTML = `
<a-sphere
snap-site="controller:#right-hand"
radius=".1"
color="yellow"
material="transparent:true; opacity:.5;"
position=".22 0 0">
</a-sphere>
`;
this.label = 'x';
this.el.setAttribute('geometry', {
primitive: 'octahedron',
radius: .1,
color: 'blue'
});
...
The snap-site component has code that detects a collision with the red sphere and then makes it a child element. So the DOM looks something like this before the collision.
<a-sphere color=red></a-sphere>
<a-entity variable-assignment>
<a-sphere snap-site>
<a-sphere>
</a-entity>
and after
<a-entity variable-assignment>
<a-sphere snap-site>
<a-sphere color=red></a-sphere>
<a-sphere>
</a-entity>
The problem is when I want to move the entity with variable-assignment inside another DOM element using appendChild the initialization function for the variable-assignment called again the innerHTML is reset. So for example if we have
<a-entity container></a-entity>
<a-entity variable-assignment>
<a-sphere snap-site>
<a-sphere color=red></a-sphere>
<a-sphere>
</a-entity>
And we want to move variable-assignment into container using something like containerElement.appendChild(variableAssignmentEntity) the inner red sphere gets removed
<a-entity container>
<a-entity variable-assignment>
<a-sphere snap-site>
<a-sphere>
</a-entity>
</a-entity>
As a work around/hack I was thinking about using a flag of some sort to see if initialize had already been called before for the element/entity the component was a property of and then not run the initialization code, something like
init: function () {
if (this.el.getAttribute('initialized')) {
return;
}
this.el.setAttribute('initialized', true);
...
but this seems like a bad way to do it and also looking into the A-Frame source it seems appendChild causes all components to be removed then added again so it doesn't actually work either or at least causes other things to break.
Is there a good way to do this or is there a different way to define the variable-assignment component so the yellow sphere snap-site component isn't a child set in the initialization?
You're correct that appendChild causes components to be re-initialized, and might not work as expected. Here are a couple alternatives:
After removing the element, call el = el.cloneNode() to make a clean copy and append that, instead.
Don't actually make the sphere a child of your variable-assignment component, but instead just have the 'parent' component update the position/rotation of the child in the tick() function.
Use the underlying three.js API to pass around the THREE.Object3D, without actually changing the parent of any elements.
An example of (3):
var mesh = el.getObject3D('mesh)';
el.removeObject3D('mesh');
otherEl.setObject3D('mesh', mesh);