Render animated layers, still needs image loading changes (#900)

* Render animated layers, still needs image loading changes

* convert params from strings once initially, add return types

* rename params->style

* fix gotoframe

* split out defaults/type conversion into own functions, review tweaks

* fix more null checks

* abstract out backgroundPosition and width/height calculation

* use raf for animation loop, split out into functions

* fix lint errors, specify window as global
This commit is contained in:
jberg 2019-09-06 08:39:53 -07:00 committed by Jordan Eldredge
parent 803b89d9e0
commit c4adc4f2b7
3 changed files with 242 additions and 56 deletions

View file

@ -321,6 +321,86 @@ function Layer({ node, id, image, x, y }) {
);
}
function animatedLayerOffsetAndSize(
frameNum,
frameSize,
layerSize,
imgSize,
imgOffset
) {
let size, offset;
if (frameSize !== undefined) {
size = Number(frameSize);
offset = -Number(frameSize) * frameNum;
} else if (layerSize !== undefined) {
size = Number(layerSize);
offset = -Number(layerSize) * frameNum;
} else {
if (imgSize !== undefined) {
size = Number(imgSize);
}
if (imgOffset !== undefined) {
offset = -Number(imgOffset);
}
}
return { offset, size };
}
function AnimatedLayer({
node,
id,
image,
x,
y,
w,
h,
framewidth,
frameheight,
}) {
if (image == null) {
console.warn("Got an AnimatedLayer without an image. Rendering null", id);
return null;
}
const img = node.js_imageLookup(image.toLowerCase());
const frameNum = node.getcurframe();
let style = {};
if (x !== undefined) {
style.left = Number(x);
}
if (y !== undefined) {
style.top = Number(y);
}
const {
offset: backgroundPositionX,
size: width,
} = animatedLayerOffsetAndSize(frameNum, framewidth, w, img.w, img.x);
const {
offset: backgroundPositionY,
size: height,
} = animatedLayerOffsetAndSize(frameNum, frameheight, h, img.h, img.y);
style = { ...style, width, height, backgroundPositionX, backgroundPositionY };
if (img.imgUrl !== undefined) {
style.backgroundImage = `url(${img.imgUrl}`;
}
return (
<GuiObjectEvents node={node}>
<div
data-node-type="AnimatedLayer"
data-node-id={id}
draggable={false}
style={{ position: "absolute", ...style }}
>
<XmlChildren node={node} />
</div>
</GuiObjectEvents>
);
}
function Button({
id,
image,
@ -510,6 +590,7 @@ const NODE_NAME_TO_COMPONENT = {
group: Group,
popupmenu: Popupmenu,
text: Text,
animatedlayer: AnimatedLayer,
};
function DummyComponent({ node }) {
@ -528,15 +609,21 @@ function XmlChildren({ node }) {
// Given a skin XML node, pick which component to use, and render it.
function XmlNode({ node }) {
const { name } = node;
let { name } = node;
if (name == null) {
// name is null is likely a comment
return null;
}
name = name.toLowerCase();
if (
name == null ||
name === "groupdef" ||
name === "elements" ||
name === "gammaset" ||
name === "scripts"
name === "scripts" ||
name === "script" ||
name === "skininfo"
) {
// name is null is likely a comment
// these nodes dont need to be rendered
return null;
}
useJsUpdates(node);

View file

@ -346,28 +346,14 @@ Set {
"Button.onleftclick",
"Button.onrightclick",
"Button.setactivatednocallback",
"AnimatedLayer.play",
"AnimatedLayer.pause",
"AnimatedLayer.stop",
"AnimatedLayer.setspeed",
"AnimatedLayer.gotoframe",
"AnimatedLayer.getlength",
"AnimatedLayer.onplay",
"AnimatedLayer.onpause",
"AnimatedLayer.onresume",
"AnimatedLayer.onstop",
"AnimatedLayer.onframe",
"AnimatedLayer.setstartframe",
"AnimatedLayer.setendframe",
"AnimatedLayer.setautoreplay",
"AnimatedLayer.isplaying",
"AnimatedLayer.ispaused",
"AnimatedLayer.isstopped",
"AnimatedLayer.getstartframe",
"AnimatedLayer.getendframe",
"AnimatedLayer.getdirection",
"AnimatedLayer.getautoreplay",
"AnimatedLayer.getcurframe",
"AnimatedLayer.setrealtime",
"ToggleButton.ontoggle",
"ToggleButton.getcurcfgval",

View file

@ -2,39 +2,159 @@ import Layer from "./Layer";
import { unimplementedWarning } from "../utils";
class AnimatedLayer extends Layer {
_playing: boolean;
_frameNum: number;
_animationStartTime: number;
constructor(node, parent, annotations, store) {
super(node, parent, annotations, store);
this._setAttributeDefaults(this.attributes);
this._convertAttributeTypes(this.attributes);
this._initializeStartAndEnd(this.attributes);
this._playing = this.attributes.autoplay;
this._frameNum = this.attributes.start || 0;
this._animationStartTime = 0;
this._setupAnimationLoop();
}
_setAttributeDefaults(attributes: Object): void {
if (attributes.autoplay == null) {
attributes.autoplay = "0";
}
if (attributes.autoreplay == null) {
attributes.autoreplay = "1";
}
if (attributes.speed == null) {
attributes.speed = "200";
}
}
_convertAttributeTypes(attributes: Object): void {
if (attributes.autoplay != null) {
attributes.autoplay = !!Number(attributes.autoplay);
}
if (attributes.autoreplay != null) {
attributes.autoreplay = !!Number(attributes.autoreplay);
}
if (attributes.speed != null) {
attributes.speed = Number(attributes.speed);
}
if (attributes.start != null) {
attributes.start = Number(attributes.start);
}
if (attributes.end != null) {
attributes.end = Number(attributes.end);
}
}
_initializeStartAndEnd(attributes: Object): void {
if (attributes.start != null && attributes.end != null) {
return;
}
const image = this.js_imageLookup(attributes.image);
if (!image) {
console.warn("Could not find image: ", attributes.image);
return;
}
if (attributes.start == null) {
attributes.start = 0;
}
if (attributes.end == null) {
if (attributes.frameheight != null) {
attributes.end = Math.ceil(image.h / attributes.frameheight);
} else if (attributes.framewidth != null) {
attributes.end = Math.ceil(image.w / attributes.framewidth);
} else {
// In the general case where we don't have a frameheight/framewidth and
// the start/end are not both set, we calculate the end frame by
// calculating the end in both directions and picking the longer repeat length
const width = attributes.w != null ? attributes.w : image.w;
const height = attributes.h != null ? attributes.h : image.h;
attributes.end = Math.max(
Math.ceil(image.w / width),
Math.ceil(image.h / height)
);
}
}
}
_animationLoop() {
window.requestAnimationFrame(() => {
const currentTime = window.performance.now();
if (currentTime > this._animationStartTime + this.attributes.speed) {
this._animationStartTime = currentTime;
this.js_trigger("js_framechange");
} else {
this._animationLoop();
}
});
}
_setupAnimationLoop() {
this.js_listen("js_framechange", () => {
this._frameNum += 1;
if (this._frameNum > this.getendframe()) {
this._frameNum = this.getstartframe();
if (!this.attributes.autoreplay) {
return;
}
}
this.js_trigger("js_update");
if (this._playing) {
this._animationLoop();
}
});
if (this._playing) {
this.js_trigger("js_framechange");
}
}
/**
* getclassname()
*
* Returns the class name for the object.
* @ret The class name.
*/
getclassname() {
getclassname(): string {
return "AnimatedLayer";
}
play() {
unimplementedWarning("play");
play(): void {
// TODO: do we need to trigger something for `onplay`/`onresume` events?
this._playing = true;
this.js_trigger("js_framechange");
}
pause() {
unimplementedWarning("pause");
pause(): void {
// TODO: do we need to trigger something for `onpause` events?
this._playing = false;
}
stop() {
unimplementedWarning("stop");
stop(): void {
// TODO: do we need to trigger something for `onstop` events?
this._playing = false;
this._frameNum = this.getstartframe();
}
setspeed(msperframe: number) {
unimplementedWarning("setspeed");
setspeed(msperframe: number): void {
this.attributes.speed = msperframe;
}
gotoframe(framenum: number) {
unimplementedWarning("gotoframe");
gotoframe(framenum: number): void {
this._frameNum = framenum;
this.js_trigger("js_update");
}
getlength() {
unimplementedWarning("getlength");
return 10;
getlength(): number {
return this.getendframe() - this.getstartframe();
}
onplay() {
@ -62,24 +182,21 @@ class AnimatedLayer extends Layer {
return;
}
setstartframe(framenum: number) {
unimplementedWarning("setstartframe");
return;
setstartframe(framenum: number): void {
this.attributes.start = framenum;
}
setendframe(framenum: number) {
unimplementedWarning("setendframe");
return;
setendframe(framenum: number): void {
this.attributes.end = framenum;
}
setautoreplay(onoff: boolean) {
unimplementedWarning("setautoreplay");
return;
setautoreplay(onoff: boolean): void {
// TODO: should this trigger the animation if it isn't currently runnnig?
this.attributes.autoreplay = onoff;
}
isplaying() {
unimplementedWarning("isplaying");
return;
isplaying(): boolean {
return this._playing;
}
ispaused() {
@ -92,14 +209,12 @@ class AnimatedLayer extends Layer {
return;
}
getstartframe() {
unimplementedWarning("getstartframe");
return;
getstartframe(): number {
return this.attributes.start;
}
getendframe() {
unimplementedWarning("getendframe");
return;
getendframe(): number {
return this.attributes.end;
}
getdirection() {
@ -107,14 +222,12 @@ class AnimatedLayer extends Layer {
return;
}
getautoreplay() {
unimplementedWarning("getautoreplay");
return;
getautoreplay(): boolean {
return this.attributes.autoreplay;
}
getcurframe() {
unimplementedWarning("getcurframe");
return;
getcurframe(): number {
return this._frameNum;
}
setrealtime(onoff: boolean) {