Add a bunch of killer animations

This commit is contained in:
Greg Fodor 2018-11-11 07:03:09 +00:00
Родитель ba473ce5d9
Коммит 09d9095219
8 изменённых файлов: 694 добавлений и 1 удалений

5
package-lock.json сгенерированный
Просмотреть файл

@ -659,6 +659,11 @@
"resolved": "https://registry.npmjs.org/an-array/-/an-array-1.0.0.tgz",
"integrity": "sha1-wSWlu4JXd4419LT2qpx9D6nkJmU="
},
"animejs": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/animejs/-/animejs-2.2.0.tgz",
"integrity": "sha1-Ne79/FNbgZScnLBvCz5gwC5v3IA="
},
"ansi-colors": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.0.5.tgz",

Просмотреть файл

@ -35,6 +35,7 @@
"aframe-rounded": "^1.0.3",
"aframe-slice9-component": "^1.0.0",
"aframe-teleport-controls": "github:mozillareality/aframe-teleport-controls#hubs/master",
"animejs": "^2.2.0",
"classnames": "^2.2.5",
"copy-to-clipboard": "^3.0.8",
"deepmerge": "^2.1.1",

643
src/components/animation.js Normal file
Просмотреть файл

@ -0,0 +1,643 @@
// Taken from A-Frame 0.9.0 master, TODO remove
const anime = require("animejs");
const components = AFRAME.components;
const registerComponent = AFRAME.registerComponent;
const utils = AFRAME.utils;
const colorHelperFrom = new THREE.Color();
const colorHelperTo = new THREE.Color();
const getComponentProperty = utils.entity.getComponentProperty;
const setComponentProperty = utils.entity.setComponentProperty;
const splitCache = {};
const TYPE_COLOR = "color";
const PROP_POSITION = "position";
const PROP_ROTATION = "rotation";
const PROP_SCALE = "scale";
const STRING_COMPONENTS = "components";
const STRING_OBJECT3D = "object3D";
/**
* Given property name, check schema to see what type we are animating.
* We just care whether the property is a vector.
*/
function getPropertyType(el, property) {
const split = property.split(".");
const componentName = split[0];
const propertyName = split[1];
const component = el.components[componentName] || components[componentName];
// Primitives.
if (!component) {
return null;
}
// Dynamic schema. We only care about vectors anyways.
if (propertyName && !component.schema[propertyName]) {
return null;
}
// Multi-prop.
if (propertyName) {
return component.schema[propertyName].type;
}
// Single-prop.
return component.schema.type;
}
/**
* Convert object to radians.
*/
function toRadians(obj) {
obj.x = THREE.Math.degToRad(obj.x);
obj.y = THREE.Math.degToRad(obj.y);
obj.z = THREE.Math.degToRad(obj.z);
}
function addEventListeners(el, eventNames, handler) {
let i;
for (i = 0; i < eventNames.length; i++) {
el.addEventListener(eventNames[i], handler);
}
}
function removeEventListeners(el, eventNames, handler) {
let i;
for (i = 0; i < eventNames.length; i++) {
el.removeEventListener(eventNames[i], handler);
}
}
function splitDot(path) {
if (path in splitCache) {
return splitCache[path];
}
splitCache[path] = path.split(".");
return splitCache[path];
}
function getRawProperty(el, path) {
let i;
let value;
const split = splitDot(path);
value = el;
for (i = 0; i < split.length; i++) {
value = value[split[i]];
}
return value;
}
function setRawProperty(el, path, value, type) {
let i;
if (path.startsWith("object3D.rotation")) {
value = THREE.Math.degToRad(value);
}
// Walk.
const split = splitDot(path);
let targetValue = el;
for (i = 0; i < split.length - 1; i++) {
targetValue = targetValue[split[i]];
}
const propertyName = split[split.length - 1];
// Raw color.
if (type === TYPE_COLOR) {
if ("r" in targetValue[propertyName]) {
targetValue[propertyName].r = value.r;
targetValue[propertyName].g = value.g;
targetValue[propertyName].b = value.b;
} else {
targetValue[propertyName].x = value.r;
targetValue[propertyName].y = value.g;
targetValue[propertyName].z = value.b;
}
return;
}
targetValue[propertyName] = value;
}
function isRawProperty(data) {
return data.isRawProperty || data.property.startsWith(STRING_COMPONENTS) || data.property.startsWith(STRING_OBJECT3D);
}
/**
* Animation component for A-Frame using anime.js.
*
* The component manually controls the tick by setting `autoplay: false` on anime.js and
* manually * calling `animation.tick()` in the tick handler. To pause or resume, we toggle a
* boolean * flag * `isAnimationPlaying`.
*
* anime.js animation config for tweenining Javascript objects and values works as:
*
* config = {
* targets: {foo: 0.0, bar: '#000'},
* foo: 1.0,
* bar: '#FFF'
* }
*
* The above will tween each property in `targets`. The `to` values are set in the root of
* the config.
*
* @member {object} animation - anime.js instance.
* @member {boolean} animationIsPlaying - Control if animation is playing.
*/
module.exports.Component = registerComponent("animation", {
schema: {
autoplay: { default: true },
delay: { default: 0 },
dir: { default: "" },
dur: { default: 1000 },
easing: { default: "easeInQuad" },
elasticity: { default: 400 },
enabled: { default: true },
from: { default: "" },
loop: {
default: 0,
parse: function(value) {
// Boolean or integer.
if (value === true || value === "true") {
return true;
}
if (value === false || value === "false") {
return false;
}
return parseInt(value, 10);
}
},
property: { default: "" },
startEvents: { type: "array" },
pauseEvents: { type: "array" },
resumeEvents: { type: "array" },
round: { default: false },
to: { default: "" },
type: { default: "" },
isRawProperty: { default: false }
},
multiple: true,
init: function() {
const self = this;
this.eventDetail = { name: this.attrName };
this.time = 0;
this.animation = null;
this.animationIsPlaying = false;
this.onStartEvent = this.onStartEvent.bind(this);
this.beginAnimation = this.beginAnimation.bind(this);
this.pauseAnimation = this.pauseAnimation.bind(this);
this.resumeAnimation = this.resumeAnimation.bind(this);
this.fromColor = {};
this.toColor = {};
this.targets = {};
this.targetsArray = [];
this.updateConfigForDefault = this.updateConfigForDefault.bind(this);
this.updateConfigForRawColor = this.updateConfigForRawColor.bind(this);
this.config = {
complete: function() {
self.animationIsPlaying = false;
self.el.emit("animationcomplete", self.eventDetail, false);
if (self.id) {
self.el.emit("animationcomplete__" + self.id, self.eventDetail, false);
}
}
};
},
update: function(oldData) {
const config = this.config;
const data = this.data;
this.animationIsPlaying = false;
if (oldData.enabled && !this.data.enabled) {
return;
}
if (!data.property) {
return;
}
// Base config.
config.autoplay = false;
config.direction = data.dir;
config.duration = data.dur;
config.easing = data.easing;
config.elasticity = data.elasticity;
config.loop = data.loop;
config.round = data.round;
// Start new animation.
this.createAndStartAnimation();
},
tick: function(t, dt) {
if (!this.animationIsPlaying) {
return;
}
this.time += dt;
this.animation.tick(this.time);
},
remove: function() {
this.pauseAnimation();
this.removeEventListeners();
},
pause: function() {
this.paused = true;
this.pausedWasPlaying = true;
this.pauseAnimation();
this.removeEventListeners();
},
/**
* `play` handler only for resuming scene.
*/
play: function() {
if (!this.paused) {
return;
}
this.paused = false;
this.addEventListeners();
if (this.pausedWasPlaying) {
this.resumeAnimation();
this.pausedWasPlaying = false;
}
},
/**
* Start animation from scratch.
*/
createAndStartAnimation: function() {
const data = this.data;
this.updateConfig();
this.animationIsPlaying = false;
this.animation = anime(this.config);
this.removeEventListeners();
this.addEventListeners();
// Wait for start events for animation.
if (!data.autoplay || (data.startEvents && data.startEvents.length)) {
return;
}
// Delay animation.
if (data.delay) {
setTimeout(this.beginAnimation, data.delay);
return;
}
// Play animation.
this.beginAnimation();
},
/**
* This is before animation start (including from startEvents).
* Set to initial state (config.from, time = 0, seekTime = 0).
*/
beginAnimation: function() {
this.updateConfig();
this.time = 0;
this.animationIsPlaying = true;
this.stopRelatedAnimations();
this.el.emit("animationbegin", this.eventDetail);
},
pauseAnimation: function() {
this.animationIsPlaying = false;
},
resumeAnimation: function() {
this.animationIsPlaying = true;
},
/**
* startEvents callback.
*/
onStartEvent: function() {
if (!this.data.enabled) {
return;
}
this.updateConfig();
if (this.animation) {
this.animation.pause();
}
this.animation = anime(this.config);
// Include the delay before each start event.
if (this.data.delay) {
setTimeout(this.beginAnimation, this.data.delay);
return;
}
this.beginAnimation();
},
/**
* rawProperty: true and type: color;
*/
updateConfigForRawColor: function() {
const config = this.config;
const data = this.data;
const el = this.el;
let from;
let key;
let to;
if (this.waitComponentInitRawProperty(this.updateConfigForRawColor)) {
return;
}
from = data.from === "" ? getRawProperty(el, data.property) : data.from;
to = data.to;
// Use r/g/b vector for color type.
this.setColorConfig(from, to);
from = this.fromColor;
to = this.toColor;
this.targetsArray.length = 0;
this.targetsArray.push(from);
config.targets = this.targetsArray;
for (key in to) {
config[key] = to[key];
}
config.update = (function() {
const lastValue = {};
return function(anim) {
const value = anim.animatables[0].target;
// For animation timeline.
if (value.r === lastValue.r && value.g === lastValue.g && value.b === lastValue.b) {
return;
}
setRawProperty(el, data.property, value, data.type);
};
})();
},
/**
* Stuff property into generic `property` key.
*/
updateConfigForDefault: function() {
const config = this.config;
const data = this.data;
const el = this.el;
let from;
let to;
if (this.waitComponentInitRawProperty(this.updateConfigForDefault)) {
return;
}
if (data.from === "") {
// Infer from.
from = isRawProperty(data) ? getRawProperty(el, data.property) : getComponentProperty(el, data.property);
} else {
// Explicit from.
from = data.from;
}
to = data.to;
const isNumber = !isNaN(from || to);
if (isNumber) {
from = parseFloat(from);
to = parseFloat(to);
} else {
from = from ? from.toString() : from;
to = to ? to.toString() : to;
}
// Convert booleans to integer to allow boolean flipping.
const isBoolean = data.to === "true" || data.to === "false" || data.to === true || data.to === false;
if (isBoolean) {
from = data.from === "true" || data.from === true ? 1 : 0;
to = data.to === "true" || data.to === true ? 1 : 0;
}
this.targets.aframeProperty = from;
config.targets = this.targets;
config.aframeProperty = to;
config.update = (function() {
let lastValue;
return function(anim) {
let value;
value = anim.animatables[0].target.aframeProperty;
// Need to do a last value check for animation timeline since all the tweening
// begins simultaenously even if the value has not changed. Also better for perf
// anyways.
if (value === lastValue) {
return;
}
lastValue = value;
if (isBoolean) {
value = value >= 1;
}
if (isRawProperty(data)) {
setRawProperty(el, data.property, value, data.type);
} else {
setComponentProperty(el, data.property, value);
}
};
})();
},
/**
* Extend x/y/z/w onto the config.
* Update vector by modifying object3D.
*/
updateConfigForVector: function() {
const config = this.config;
const data = this.data;
const el = this.el;
let key;
// Parse coordinates.
const from =
data.from !== ""
? utils.coordinates.parse(data.from) // If data.from defined, use that.
: getComponentProperty(el, data.property); // If data.from not defined, get on the fly.
const to = utils.coordinates.parse(data.to);
if (data.property === PROP_ROTATION) {
toRadians(from);
toRadians(to);
}
// Set to and from.
this.targetsArray.length = 0;
this.targetsArray.push(from);
config.targets = this.targetsArray;
for (key in to) {
config[key] = to[key];
}
// If animating object3D transformation, run more optimized updater.
if (data.property === PROP_POSITION || data.property === PROP_ROTATION || data.property === PROP_SCALE) {
config.update = (function() {
const lastValue = {};
return function(anim) {
const value = anim.animatables[0].target;
if (data.property === PROP_SCALE) {
value.x = Math.max(0.0001, value.x);
value.y = Math.max(0.0001, value.y);
value.z = Math.max(0.0001, value.z);
}
// For animation timeline.
if (value.x === lastValue.x && value.y === lastValue.y && value.z === lastValue.z) {
return;
}
lastValue.x = value.x;
lastValue.y = value.y;
lastValue.z = value.z;
el.object3D[data.property].set(value.x, value.y, value.z);
};
})();
return;
}
// Animating some vector.
config.update = (function() {
const lastValue = {};
return function(anim) {
const value = anim.animations[0].target;
// Animate rotation through radians.
// For animation timeline.
if (value.x === lastValue.x && value.y === lastValue.y && value.z === lastValue.z) {
return;
}
lastValue.x = value.x;
lastValue.y = value.y;
lastValue.z = value.z;
setComponentProperty(el, data.property, value);
};
})();
},
/**
* Update the config before each run.
*/
updateConfig: function() {
// Route config type.
const propType = getPropertyType(this.el, this.data.property);
if (isRawProperty(this.data) && this.data.type === TYPE_COLOR) {
this.updateConfigForRawColor();
} else if (propType === "vec2" || propType === "vec3" || propType === "vec4") {
this.updateConfigForVector();
} else {
this.updateConfigForDefault();
}
},
/**
* Wait for component to initialize.
*/
waitComponentInitRawProperty: function(cb) {
const data = this.data;
const el = this.el;
const self = this;
if (data.from !== "") {
return false;
}
if (!data.property.startsWith(STRING_COMPONENTS)) {
return false;
}
const componentName = splitDot(data.property)[1];
if (el.components[componentName]) {
return false;
}
el.addEventListener("componentinitialized", function wait(evt) {
if (evt.detail.name !== componentName) {
return;
}
cb();
// Since the config was created async, create the animation now since we missed it
// earlier.
self.animation = anime(self.config);
el.removeEventListener("componentinitialized", wait);
});
return true;
},
/**
* Make sure two animations on the same property don't fight each other.
* e.g., animation__mouseenter="property: material.opacity"
* animation__mouseleave="property: material.opacity"
*/
stopRelatedAnimations: function() {
let component;
let componentName;
for (componentName in this.el.components) {
component = this.el.components[componentName];
if (componentName === this.attrName) {
continue;
}
if (component.name !== "animation") {
continue;
}
if (!component.animationIsPlaying) {
continue;
}
if (component.data.property !== this.data.property) {
continue;
}
component.animationIsPlaying = false;
}
},
addEventListeners: function() {
const data = this.data;
const el = this.el;
addEventListeners(el, data.startEvents, this.onStartEvent);
addEventListeners(el, data.pauseEvents, this.pauseAnimation);
addEventListeners(el, data.resumeEvents, this.resumeAnimation);
},
removeEventListeners: function() {
const data = this.data;
const el = this.el;
removeEventListeners(el, data.startEvents, this.onStartEvent);
removeEventListeners(el, data.pauseEvents, this.pauseAnimation);
removeEventListeners(el, data.resumeEvents, this.resumeAnimation);
},
setColorConfig: function(from, to) {
colorHelperFrom.set(from);
colorHelperTo.set(to);
from = this.fromColor;
to = this.toColor;
from.r = colorHelperFrom.r;
from.g = colorHelperFrom.g;
from.b = colorHelperFrom.b;
to.r = colorHelperTo.r;
to.g = colorHelperTo.g;
to.b = colorHelperTo.b;
}
});

Просмотреть файл

@ -42,6 +42,27 @@ AFRAME.registerComponent("pinnable", {
_fireEvents() {
if (this.data.pinned) {
this.el.emit("pinned", { el: this.el });
this.el.removeAttribute("animation__pin-start");
this.el.removeAttribute("animation__pin-end");
const currentScale = this.el.object3D.scale;
this.el.setAttribute("animation__pin-start", {
property: "scale",
dur: 100,
from: { x: currentScale.x, y: currentScale.y, z: currentScale.z },
to: { x: currentScale.x * 1.1, y: currentScale.y * 1.1, z: currentScale.z * 1.1 },
easing: "easeInOutElastic"
});
this.el.setAttribute("animation__pin-end", {
property: "scale",
delay: 150,
dur: 100,
from: { x: currentScale.x * 1.1, y: currentScale.y * 1.1, z: currentScale.z * 1.1 },
to: { x: currentScale.x, y: currentScale.y, z: currentScale.z },
easing: "easeInOutElastic"
});
} else {
this.el.emit("unpinned", { el: this.el });
}

Просмотреть файл

@ -3,7 +3,18 @@ AFRAME.registerComponent("remove-networked-object-button", {
this.onClick = () => {
if (!NAF.utils.isMine(this.targetEl) && !NAF.utils.takeOwnership(this.targetEl)) return;
this.targetEl.parentNode.removeChild(this.targetEl);
this.targetEl.setAttribute("animation__remove", {
property: "scale",
dur: 200,
to: { x: 0.01, y: 0.01, z: 0.01 },
easing: "easeInQuad"
});
this.el.parentNode.setAttribute("visible", false);
this.targetEl.addEventListener("animationcomplete", () => {
this.targetEl.parentNode.removeChild(this.targetEl);
});
};
NAF.utils.getNetworkedEntity(this.el).then(networkedEl => {

Просмотреть файл

@ -167,6 +167,7 @@
class="interactable"
super-networked-interactable="counter: #media-counter;"
body="type: dynamic; shape: none; mass: 1;"
animation__spawn="property: scale; dur: 200; from: 0.5 0.5 0.5; to: 1 1 1; easing: easeInQuad"
grabbable
stretchable="useWorldPosition: true; usePhysics: never"
hoverable
@ -239,6 +240,7 @@
hoverable
stretchable
camera-tool
animation__spawn="property: scale; dur: 200; from: 0.5 0.5 0.5; to: 1 1 1; easing: easeInQuad"
body="type: dynamic; shape: none; mass: 1;"
shape="shape: box; halfExtents: 0.22 0.145 0.1; offset: 0 0.02 0"
sticky-object="autoLockOnRelease: true; autoLockOnLoad: true; autoLockSpeedLimit: 0;"

Просмотреть файл

@ -67,6 +67,7 @@ import "./components/scene-sound";
import "./components/emit-state-change";
import "./components/action-to-event";
import "./components/stop-event-propagation";
import "./components/animation";
import ReactDOM from "react-dom";
import React from "react";

Просмотреть файл

@ -115,6 +115,15 @@ export const addMedia = (src, template, contentOrigin, resolve = false, resize =
["model-loaded", "video-loaded", "image-loaded"].forEach(eventName => {
entity.addEventListener(eventName, () => {
clearTimeout(fireLoadingTimeout);
entity.setAttribute("animation__spawn-start", {
property: "scale",
dur: 300,
from: { x: 0.5, y: 0.5, z: 0.5 },
to: { x: 1.0, y: 1.0, z: 1.0 },
easing: "easeOutElastic"
});
scene.emit("media-loaded", { src: src });
});
});