Skip to content

Commit

Permalink
m-attr-lerp
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcusLongmuir committed May 21, 2024
1 parent 3b89fd7 commit 9266e5f
Show file tree
Hide file tree
Showing 27 changed files with 619 additions and 173 deletions.
76 changes: 76 additions & 0 deletions e2e-tests/src/attr-lerp.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<m-plane color="yellow" width="15" height="15" rx="-90"></m-plane>
<m-light type="point" intensity="900" x="10" y="10" z="10"></m-light>

<m-label width="5" y="1" rx="-45" z="3" content="This test tests snapshots of the state at intervals of document time. The document time is overridden in e2e tests"></m-label>

<m-group id="timeline" y="0.25">
<m-cube width="15" height="0.25" depth="0.1" color="black"></m-cube>
<m-cube width="1" x="-5.6" y="0.25" height="0.25" depth="0.1" z="0.1" color="green"></m-cube>
<m-cube width="1" height="0.25" depth="0.1" z="0.1" color="red">
<m-attr-anim attr="x" start="-7" end="7" loop="true" duration="10000"></m-attr-anim>
</m-cube>
</m-group>

<m-group x="0" y="2">
<m-label content="independent attributes" width="5" y="0.75" alignment="center" height="0.5"></m-label>
<!-- Two independent attributes -->
<m-cube width="5" x="-2.5" height="1" z="-0.1" depth="0.1" color="red"></m-cube>
<m-cube width="5" x="2.5" height="1" z="-0.1" depth="0.1" color="green"></m-cube>
<m-cube x="-4.25" depth="0.1" height="0.5" color="white" id="two-independent-attributes" onclick="this.setAttribute('x','4.25'); this.setAttribute('height','0.1');">
<m-attr-lerp attr="x" duration="4000"></m-attr-lerp>
<m-attr-lerp attr="height" duration="2000"></m-attr-lerp>
</m-cube>
</m-group>

<m-group x="0" y="3.75">
<m-label content="combined attributes - shadowed" width="5" y="0.75" alignment="center" height="0.5"></m-label>
<!-- Combined attributes with an unused later sibling that is overridden -->
<m-cube width="5" x="-2.5" height="1" z="-0.1" depth="0.1" color="red"></m-cube>
<m-cube width="5" x="2.5" height="1" z="-0.1" depth="0.1" color="green"></m-cube>
<m-cube x="-4.25" depth="0.1" height="0.5" color="white" id="combined-attributes-shadowed" onclick="this.setAttribute('x','4.25'); this.setAttribute('height','0.1');">
<m-attr-lerp attr="x,height" duration="4000"></m-attr-lerp>
<m-attr-lerp attr="x" duration="2000"></m-attr-lerp>
</m-cube>
</m-group>

<m-group x="0" y="5.5">
<m-label content="combined attributes - overridden" width="5" y="0.75" alignment="center" height="0.5"></m-label>
<!-- Combined attributes, but with an earlier sibling that overrides the duration -->
<m-cube width="5" x="-2.5" height="1" z="-0.1" depth="0.1" color="red"></m-cube>
<m-cube width="5" x="2.5" height="1" z="-0.1" depth="0.1" color="green"></m-cube>
<m-cube x="-4.25" depth="0.1" height="0.5" color="white" id="combined-attributes-overridden" onclick="this.setAttribute('x','4.25'); this.setAttribute('height','0.1');">
<m-attr-lerp attr="x" duration="2000"></m-attr-lerp>
<m-attr-lerp attr="x,height" duration="4000"></m-attr-lerp>
</m-cube>
</m-group>

<m-group x="0" y="7.25">
<m-label content="all attributes" width="5" y="0.75" alignment="center" height="0.5"></m-label>
<!-- Explicitly specifying "all" attributes -->
<m-cube width="5" x="-2.5" height="1" z="-0.1" depth="0.1" color="red"></m-cube>
<m-cube width="5" x="2.5" height="1" z="-0.1" depth="0.1" color="green"></m-cube>
<m-cube x="-4.25" depth="0.1" height="0.5" color="white" id="all-attributes" onclick="this.setAttribute('x','4.25'); this.setAttribute('height','0.1');">
<m-attr-lerp attr="all" duration="2000"></m-attr-lerp>
</m-cube>
</m-group>

<m-group x="0" y="9">
<m-label content="all attributes (default)" width="5" y="0.75" alignment="center" height="0.5"></m-label>
<!-- Default behaviour of not specifying "attr" is to have all attributes lerped -->
<m-cube width="5" x="-2.5" height="1" z="-0.1" depth="0.1" color="red"></m-cube>
<m-cube width="5" x="2.5" height="1" z="-0.1" depth="0.1" color="green"></m-cube>
<m-cube x="-4.25" depth="0.1" height="0.5" color="white" id="all-attributes-default" onclick="this.setAttribute('x','4.25'); this.setAttribute('height','0.1');">
<m-attr-lerp duration="2000"></m-attr-lerp>
</m-cube>
</m-group>


<m-group x="0" y="10.75">
<m-label content="no attributes" width="5" y="0.75" alignment="center" height="0.5"></m-label>
<!-- Explicitly setting "attr" to empty (no lerping) -->
<m-cube width="5" x="-2.5" height="1" z="-0.1" depth="0.1" color="red"></m-cube>
<m-cube width="5" x="2.5" height="1" z="-0.1" depth="0.1" color="green"></m-cube>
<m-cube x="-4.25" depth="0.1" height="0.5" color="white" id="no-attributes" onclick="this.setAttribute('x','4.25'); this.setAttribute('height','0.1');">
<m-attr-lerp attr="" duration="2000"></m-attr-lerp>
</m-cube>
</m-group>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
83 changes: 83 additions & 0 deletions e2e-tests/test/attr-lerp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { clickElement, setDocumentTime, takeAndCompareScreenshot } from "./testing-utils";

type ExpectedStates = { [key: string]: { x: number; height: number } };

describe("m-attr-lerp", () => {
test("lerping is applied according to attributes", async () => {
const page = await __BROWSER_GLOBAL__.newPage();

await page.setViewport({ width: 1024, height: 1024 });

await page.goto("http://localhost:7079/attr-lerp.html/reset");

await page.waitForSelector("m-attr-lerp[attr='x']");

async function testExpectedStates(expectedStates: ExpectedStates) {
for (const [id, expected] of Object.entries(expectedStates)) {
const actualX = await page.evaluate((id) => {
return (document.querySelector(`#${id}`) as any).getContainer().position.x;
}, id);

const actualHeight = await page.evaluate((id) => {
return (document.querySelector(`#${id}`) as any).getCube().scale.y;
}, id);

expect(`${id}: x: ${actualX} height: ${actualHeight}`).toEqual(
`${id}: x: ${expected.x} height: ${expected.height}`,
);
}
}

// Set the document time to 0 to use as a reference point for the start of the lerping
await setDocumentTime(page, 0);

await testExpectedStates({
"two-independent-attributes": { x: -4.25, height: 0.5 },
"combined-attributes-shadowed": { x: -4.25, height: 0.5 },
"combined-attributes-overridden": { x: -4.25, height: 0.5 },
"all-attributes": { x: -4.25, height: 0.5 },
"all-attributes-default": { x: -4.25, height: 0.5 },
"no-attributes": { x: -4.25, height: 0.5 },
});

await takeAndCompareScreenshot(page);

// Click all of the cubes to trigger the lerping
await clickElement(page, "#two-independent-attributes");
await clickElement(page, "#combined-attributes-shadowed");
await clickElement(page, "#combined-attributes-overridden");
await clickElement(page, "#all-attributes");
await clickElement(page, "#all-attributes-default");
await clickElement(page, "#no-attributes");

// Set the document time to 1000 to check the lerping
await setDocumentTime(page, 1000);

await testExpectedStates({
"two-independent-attributes": { x: -2.125, height: 0.3 },
"combined-attributes-shadowed": { x: -2.125, height: 0.4 },
"combined-attributes-overridden": { x: 0, height: 0.4 },
"all-attributes": { x: 0, height: 0.3 },
"all-attributes-default": { x: 0, height: 0.3 },
"no-attributes": { x: 4.25, height: 0.1 },
});

await takeAndCompareScreenshot(page);

// Set the document time to 1000 to check the lerping
await setDocumentTime(page, 5000);

await testExpectedStates({
"two-independent-attributes": { x: 4.25, height: 0.1 },
"combined-attributes-shadowed": { x: 4.25, height: 0.1 },
"combined-attributes-overridden": { x: 4.25, height: 0.1 },
"all-attributes": { x: 4.25, height: 0.1 },
"all-attributes-default": { x: 4.25, height: 0.1 },
"no-attributes": { x: 4.25, height: 0.1 },
});

await takeAndCompareScreenshot(page);

await page.close();
}, 60000);
});
7 changes: 7 additions & 0 deletions packages/mml-web/src/MMLDocumentTimeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ export class MMLDocumentTimeManager {
return (document.timeline.currentTime as number)! - this.relativeDocumentStartTime;
}

public getWindowTime(): number {
if (this.overridenDocumentTime !== null) {
return this.overridenDocumentTime;
}
return document.timeline.currentTime as number;
}

public addDocumentTimeListenerCallback(cb: (time: number) => void) {
this.documentTimeListeners.add(cb);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/mml-web/src/elements/AttributeAnimation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export class AttributeAnimation extends MElement {
instance.props.pauseTime = parseFloatAttribute(newValue, defaultPauseTime);
},
duration: (instance, newValue) => {
instance.props.animDuration = parseFloatAttribute(newValue, defaultAnimDuration);
instance.props.animDuration = Math.max(0, parseFloatAttribute(newValue, defaultAnimDuration));
},
});

Expand Down
135 changes: 135 additions & 0 deletions packages/mml-web/src/elements/AttributeLerp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import * as THREE from "three";

import { MElement } from "./MElement";
import { AttributeHandler, parseFloatAttribute } from "../utils/attribute-handling";
import { easingsByName } from "../utils/easings";
import { OrientedBoundingBox } from "../utils/OrientedBoundingBox";

const defaultAttribute: string = "all";
const defaultEasing = "";
const defaultLerpDuration = 1000;

export class AttributeLerp extends MElement {
static tagName = "m-attr-lerp";

private props = {
attr: defaultAttribute,
easing: defaultEasing,
lerpDuration: defaultLerpDuration,
};

private registeredParentAttachment: MElement | null = null;

private static attributeHandler = new AttributeHandler<AttributeLerp>({
attr: (instance, newValue) => {
if (instance.registeredParentAttachment) {
instance.registeredParentAttachment.removeSideEffectChild(instance);
}
instance.props.attr = newValue !== null ? newValue : defaultAttribute;
if (instance.registeredParentAttachment) {
instance.registeredParentAttachment.addSideEffectChild(instance);
}
},
easing: (instance, newValue) => {
instance.props.easing = newValue || defaultEasing;
},
duration: (instance, newValue) => {
instance.props.lerpDuration = Math.max(0, parseFloatAttribute(newValue, defaultLerpDuration));
},
});

static get observedAttributes(): Array<string> {
return [...AttributeLerp.attributeHandler.getAttributes()];
}
constructor() {
super();
}

protected enable() {
// no-op
}

protected disable() {
// no-op
}

protected getContentBounds(): OrientedBoundingBox | null {
return null;
}

public getAnimatedAttributeName(): string | null {
return this.props.attr;
}

public parentTransformed(): void {
// no-op
}

public isClickable(): boolean {
return false;
}

attributeChangedCallback(name: string, oldValue: string, newValue: string) {
super.attributeChangedCallback(name, oldValue, newValue);
AttributeLerp.attributeHandler.handle(this, name, newValue);
}

connectedCallback(): void {
super.connectedCallback();
if (this.parentElement && this.parentElement instanceof MElement) {
this.registeredParentAttachment = this.parentElement;
this.registeredParentAttachment.addSideEffectChild(this);
}
}

disconnectedCallback() {
if (this.registeredParentAttachment) {
this.registeredParentAttachment.removeSideEffectChild(this);
}
this.registeredParentAttachment = null;
super.disconnectedCallback();
}

public getColorValueForTime(
windowTime: number,
elementValueSetTime: number,
elementValue: THREE.Color,
previousValue: THREE.Color,
) {
const ratio = this.getLerpRatio(windowTime, elementValueSetTime);
if (ratio >= 1) {
return elementValue;
}
return new THREE.Color(previousValue).lerpHSL(elementValue, ratio);
}

public getFloatValueForTime(
windowTime: number,
elementValueSetTime: number,
elementValue: number,
previousValue: number,
) {
const from = previousValue;
const to = elementValue;
const ratio = this.getLerpRatio(windowTime, elementValueSetTime);
if (ratio >= 1) {
return to;
}
return from + (to - from) * ratio;
}

private getLerpRatio(windowTime: number, elementValueSetTime: number) {
const duration = this.props.lerpDuration;
const timePassed = (windowTime || 0) - elementValueSetTime;
const ratioOfTimePassed = Math.min(timePassed / duration, 1);
const easing = this.props.easing;
let ratio;
const easingFunction = easingsByName[easing];
if (easingFunction) {
ratio = easingFunction(ratioOfTimePassed, 0, 1, 1);
} else {
ratio = ratioOfTimePassed;
}
return ratio;
}
}
16 changes: 3 additions & 13 deletions packages/mml-web/src/elements/Audio.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as THREE from "three";
import { PositionalAudioHelper } from "three/addons/helpers/PositionalAudioHelper.js";

import { AnimationType, AttributeAnimation } from "./AttributeAnimation";
import { AnimationType } from "./AttributeAnimation";
import { MElement } from "./MElement";
import { TransformableElement } from "./TransformableElement";
import { AnimatedAttributeHelper } from "../utils/AnimatedAttributeHelper";
Expand Down Expand Up @@ -184,22 +184,12 @@ export class Audio extends TransformableElement {
}

public addSideEffectChild(child: MElement): void {
if (child instanceof AttributeAnimation) {
const attr = child.getAnimatedAttributeName();
if (attr) {
this.audioAnimatedAttributeHelper.addAnimation(child, attr);
}
}
this.audioAnimatedAttributeHelper.addSideEffectChild(child);
super.addSideEffectChild(child);
}

public removeSideEffectChild(child: MElement): void {
if (child instanceof AttributeAnimation) {
const attr = child.getAnimatedAttributeName();
if (attr) {
this.audioAnimatedAttributeHelper.removeAnimation(child, attr);
}
}
this.audioAnimatedAttributeHelper.removeSideEffectChild(child);
super.removeSideEffectChild(child);
}

Expand Down
Loading

0 comments on commit 9266e5f

Please sign in to comment.