Add hacky play/pause toggle to new item page preview
This doesn't do a good job maintaining state across morphs, but hey it's Working At All in terms of wiring, and that's good!! Also need to style up the toggle as a cute button instead of a visible checkbox and the words "Play/pause"!
This commit is contained in:
parent
7688caebe1
commit
1aba4f405e
4 changed files with 126 additions and 13 deletions
|
@ -1,4 +1,45 @@
|
||||||
|
class OutfitViewer extends HTMLElement {
|
||||||
|
#internals;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.#internals = this.attachInternals();
|
||||||
|
this.#setIsPlaying(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
setTimeout(() => this.#connectToChildren(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#connectToChildren() {
|
||||||
|
const playPauseToggle = document.querySelector(".play-pause-toggle");
|
||||||
|
|
||||||
|
// Read our initial playing state from the toggle, and subscribe to changes.
|
||||||
|
this.#setIsPlaying(playPauseToggle.checked);
|
||||||
|
playPauseToggle.addEventListener("change", () => {
|
||||||
|
this.#setIsPlaying(playPauseToggle.checked);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#setIsPlaying(isPlaying) {
|
||||||
|
// TODO: Listen for changes to the child list, and add `playing` when new
|
||||||
|
// nodes arrive, if playing.
|
||||||
|
if (isPlaying) {
|
||||||
|
this.#internals.states.add("playing");
|
||||||
|
for (const child of this.children) {
|
||||||
|
child.setAttribute("playing", "");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.#internals.states.delete("playing");
|
||||||
|
for (const child of this.children) {
|
||||||
|
child.removeAttribute("playing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class OutfitLayer extends HTMLElement {
|
class OutfitLayer extends HTMLElement {
|
||||||
|
static observedAttributes = ["playing"];
|
||||||
#internals;
|
#internals;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -23,6 +64,13 @@ class OutfitLayer extends HTMLElement {
|
||||||
window.removeEventListener("message", this.#onMessage);
|
window.removeEventListener("message", this.#onMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(name, oldValue, newValue) {
|
||||||
|
if (name === "playing") {
|
||||||
|
const isPlaying = newValue != null;
|
||||||
|
this.#forwardIsPlaying(isPlaying);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#connectToChildren() {
|
#connectToChildren() {
|
||||||
const image = this.querySelector("img");
|
const image = this.querySelector("img");
|
||||||
const iframe = this.querySelector("iframe");
|
const iframe = this.querySelector("iframe");
|
||||||
|
@ -46,8 +94,17 @@ class OutfitLayer extends HTMLElement {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.type === "status" && ["loaded", "error"].includes(data.status)) {
|
if (data.type === "status") {
|
||||||
this.#setStatus(data.status);
|
if (data.status === "loaded") {
|
||||||
|
this.#setStatus("loaded");
|
||||||
|
this.#setHasAnimations(data.hasAnimations);
|
||||||
|
} else if (data.status === "error") {
|
||||||
|
this.#setStatus("error");
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`<outfit-layer> got unexpected status: ${JSON.stringify(data.status)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`<outfit-layer> got unexpected message: ${JSON.stringify(data)}`,
|
`<outfit-layer> got unexpected message: ${JSON.stringify(data)}`,
|
||||||
|
@ -56,11 +113,33 @@ class OutfitLayer extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
#setStatus(newStatus) {
|
#setStatus(newStatus) {
|
||||||
this.#internals.states.clear();
|
this.#internals.states.delete("loading");
|
||||||
|
this.#internals.states.delete("loaded");
|
||||||
|
this.#internals.states.delete("error");
|
||||||
this.#internals.states.add(newStatus);
|
this.#internals.states.add(newStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#setHasAnimations(hasAnimations) {
|
||||||
|
if (hasAnimations) {
|
||||||
|
this.#internals.states.add("has-animations");
|
||||||
|
} else {
|
||||||
|
this.#internals.states.delete("has-animations");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#forwardIsPlaying(isPlaying) {
|
||||||
|
if (this.iframe == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.iframe.contentWindow.postMessage(
|
||||||
|
{ type: isPlaying ? "play" : "pause" },
|
||||||
|
"*", // The frame is sandboxed (origin == null), so send to Any origin.
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define("outfit-viewer", OutfitViewer);
|
||||||
customElements.define("outfit-layer", OutfitLayer);
|
customElements.define("outfit-layer", OutfitLayer);
|
||||||
|
|
||||||
// Morph turbo-frames on this page, to reuse asset nodes when we want to—very
|
// Morph turbo-frames on this page, to reuse asset nodes when we want to—very
|
||||||
|
|
|
@ -237,9 +237,7 @@ function onAnimationFrame() {
|
||||||
|
|
||||||
if (msSinceLastLog >= 5000) {
|
if (msSinceLastLog >= 5000) {
|
||||||
const fps = numFramesSinceLastLog / (msSinceLastLog / 1000);
|
const fps = numFramesSinceLastLog / (msSinceLastLog / 1000);
|
||||||
console.debug(
|
console.debug(`${logPrefix} FPS: ${fps.toFixed(2)} (Target: ${targetFps})`);
|
||||||
`${logPrefix} FPS: ${fps.toFixed(2)} (Target: ${targetFps})`,
|
|
||||||
);
|
|
||||||
lastLogTime = document.timeline.currentTime;
|
lastLogTime = document.timeline.currentTime;
|
||||||
numFramesSinceLastLog = 0;
|
numFramesSinceLastLog = 0;
|
||||||
}
|
}
|
||||||
|
@ -276,6 +274,22 @@ function getInitialPlayingStatus() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively scans the given MovieClip (or child createjs node), to see if
|
||||||
|
* there are any animated areas.
|
||||||
|
*/
|
||||||
|
function hasAnimations(createjsNode) {
|
||||||
|
return (
|
||||||
|
// Some nodes have simple animation frames.
|
||||||
|
createjsNode.totalFrames > 1 ||
|
||||||
|
// Tweens are a form of animation that can happen separately from frames.
|
||||||
|
// They expect timer ticks to happen, and they change the scene accordingly.
|
||||||
|
createjsNode?.timeline?.tweens?.length >= 1 ||
|
||||||
|
// And some nodes have _children_ that are animated.
|
||||||
|
(createjsNode.children || []).some(hasAnimations)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener("resize", () => {
|
window.addEventListener("resize", () => {
|
||||||
updateCanvasDimensions();
|
updateCanvasDimensions();
|
||||||
|
|
||||||
|
@ -304,7 +318,11 @@ window.addEventListener("message", ({ data }) => {
|
||||||
startMovie()
|
startMovie()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
parent.postMessage(
|
parent.postMessage(
|
||||||
{ type: "status", status: "loaded" },
|
{
|
||||||
|
type: "status",
|
||||||
|
status: "loaded",
|
||||||
|
hasAnimations: hasAnimations(movieClip),
|
||||||
|
},
|
||||||
document.location.origin,
|
document.location.origin,
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
|
@ -38,7 +38,7 @@ body.items-show
|
||||||
height: 16px
|
height: 16px
|
||||||
width: 16px
|
width: 16px
|
||||||
|
|
||||||
.outfit-viewer
|
outfit-viewer
|
||||||
display: block
|
display: block
|
||||||
position: relative
|
position: relative
|
||||||
width: 300px
|
width: 300px
|
||||||
|
@ -59,7 +59,7 @@ body.items-show
|
||||||
|
|
||||||
.loading-indicator
|
.loading-indicator
|
||||||
position: absolute
|
position: absolute
|
||||||
z-index: 999
|
z-index: 1000
|
||||||
bottom: 0px
|
bottom: 0px
|
||||||
right: 4px
|
right: 4px
|
||||||
padding: 8px
|
padding: 8px
|
||||||
|
@ -68,6 +68,17 @@ body.items-show
|
||||||
opacity: 0
|
opacity: 0
|
||||||
transition: opacity .5s
|
transition: opacity .5s
|
||||||
|
|
||||||
|
.play-pause-button
|
||||||
|
position: absolute
|
||||||
|
z-index: 1001
|
||||||
|
left: 8px
|
||||||
|
bottom: 8px
|
||||||
|
display: none
|
||||||
|
|
||||||
|
&:has(outfit-layer:state(has-animations))
|
||||||
|
.play-pause-button
|
||||||
|
display: block
|
||||||
|
|
||||||
.error-indicator
|
.error-indicator
|
||||||
font-size: 85%
|
font-size: 85%
|
||||||
color: $error-color
|
color: $error-color
|
||||||
|
@ -79,14 +90,14 @@ body.items-show
|
||||||
// apply the delay here, because fading *out* on load should be instant.)
|
// apply the delay here, because fading *out* on load should be instant.)
|
||||||
// We are loading when the <turbo-frame> is busy, or when at least one layer
|
// We are loading when the <turbo-frame> is busy, or when at least one layer
|
||||||
// is loading.
|
// is loading.
|
||||||
#item-preview[busy] .outfit-viewer, .outfit-viewer:has(outfit-layer:state(loading))
|
#item-preview[busy] outfit-viewer, outfit-viewer:has(outfit-layer:state(loading))
|
||||||
cursor: wait
|
cursor: wait
|
||||||
.loading-indicator
|
.loading-indicator
|
||||||
opacity: 1
|
opacity: 1
|
||||||
transition-delay: 2s
|
transition-delay: 2s
|
||||||
|
|
||||||
#item-preview:has(outfit-layer:state(error))
|
#item-preview:has(outfit-layer:state(error))
|
||||||
.outfit-viewer
|
outfit-viewer
|
||||||
border: 2px solid red
|
border: 2px solid red
|
||||||
.error-indicator
|
.error-indicator
|
||||||
display: block
|
display: block
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
.outfit-viewer
|
%outfit-viewer
|
||||||
.loading-indicator= render partial: "hanger_spinner"
|
.loading-indicator= render partial: "hanger_spinner"
|
||||||
|
|
||||||
|
%label.play-pause-button
|
||||||
|
%input.play-pause-toggle{type: "checkbox"}
|
||||||
|
Play/pause
|
||||||
|
|
||||||
- outfit.visible_layers.each do |swf_asset|
|
- outfit.visible_layers.each do |swf_asset|
|
||||||
%outfit-layer{
|
%outfit-layer{
|
||||||
data: {
|
data: {
|
||||||
|
@ -8,6 +13,6 @@
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
- if swf_asset.canvas_movie?
|
- if swf_asset.canvas_movie?
|
||||||
%iframe{src: swf_asset_path(swf_asset) + "?playing"}
|
%iframe{src: swf_asset_path(swf_asset)}
|
||||||
- else
|
- else
|
||||||
= image_tag swf_asset.image_url, alt: ""
|
= image_tag swf_asset.image_url, alt: ""
|
Loading…
Reference in a new issue