forked from OpenNeo/impress
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 {
|
||||
static observedAttributes = ["playing"];
|
||||
#internals;
|
||||
|
||||
constructor() {
|
||||
|
@ -23,6 +64,13 @@ class OutfitLayer extends HTMLElement {
|
|||
window.removeEventListener("message", this.#onMessage);
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
if (name === "playing") {
|
||||
const isPlaying = newValue != null;
|
||||
this.#forwardIsPlaying(isPlaying);
|
||||
}
|
||||
}
|
||||
|
||||
#connectToChildren() {
|
||||
const image = this.querySelector("img");
|
||||
const iframe = this.querySelector("iframe");
|
||||
|
@ -46,8 +94,17 @@ class OutfitLayer extends HTMLElement {
|
|||
return;
|
||||
}
|
||||
|
||||
if (data.type === "status" && ["loaded", "error"].includes(data.status)) {
|
||||
this.#setStatus(data.status);
|
||||
if (data.type === "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 {
|
||||
throw new Error(
|
||||
`<outfit-layer> got unexpected message: ${JSON.stringify(data)}`,
|
||||
|
@ -56,11 +113,33 @@ class OutfitLayer extends HTMLElement {
|
|||
}
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
#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);
|
||||
|
||||
// Morph turbo-frames on this page, to reuse asset nodes when we want to—very
|
||||
|
|
|
@ -237,9 +237,7 @@ function onAnimationFrame() {
|
|||
|
||||
if (msSinceLastLog >= 5000) {
|
||||
const fps = numFramesSinceLastLog / (msSinceLastLog / 1000);
|
||||
console.debug(
|
||||
`${logPrefix} FPS: ${fps.toFixed(2)} (Target: ${targetFps})`,
|
||||
);
|
||||
console.debug(`${logPrefix} FPS: ${fps.toFixed(2)} (Target: ${targetFps})`);
|
||||
lastLogTime = document.timeline.currentTime;
|
||||
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", () => {
|
||||
updateCanvasDimensions();
|
||||
|
||||
|
@ -304,7 +318,11 @@ window.addEventListener("message", ({ data }) => {
|
|||
startMovie()
|
||||
.then(() => {
|
||||
parent.postMessage(
|
||||
{ type: "status", status: "loaded" },
|
||||
{
|
||||
type: "status",
|
||||
status: "loaded",
|
||||
hasAnimations: hasAnimations(movieClip),
|
||||
},
|
||||
document.location.origin,
|
||||
);
|
||||
})
|
||||
|
|
|
@ -38,7 +38,7 @@ body.items-show
|
|||
height: 16px
|
||||
width: 16px
|
||||
|
||||
.outfit-viewer
|
||||
outfit-viewer
|
||||
display: block
|
||||
position: relative
|
||||
width: 300px
|
||||
|
@ -59,7 +59,7 @@ body.items-show
|
|||
|
||||
.loading-indicator
|
||||
position: absolute
|
||||
z-index: 999
|
||||
z-index: 1000
|
||||
bottom: 0px
|
||||
right: 4px
|
||||
padding: 8px
|
||||
|
@ -68,6 +68,17 @@ body.items-show
|
|||
opacity: 0
|
||||
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
|
||||
font-size: 85%
|
||||
color: $error-color
|
||||
|
@ -79,14 +90,14 @@ body.items-show
|
|||
// 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
|
||||
// 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
|
||||
.loading-indicator
|
||||
opacity: 1
|
||||
transition-delay: 2s
|
||||
|
||||
#item-preview:has(outfit-layer:state(error))
|
||||
.outfit-viewer
|
||||
outfit-viewer
|
||||
border: 2px solid red
|
||||
.error-indicator
|
||||
display: block
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
.outfit-viewer
|
||||
%outfit-viewer
|
||||
.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-layer{
|
||||
data: {
|
||||
|
@ -8,6 +13,6 @@
|
|||
},
|
||||
}
|
||||
- if swf_asset.canvas_movie?
|
||||
%iframe{src: swf_asset_path(swf_asset) + "?playing"}
|
||||
%iframe{src: swf_asset_path(swf_asset)}
|
||||
- else
|
||||
= image_tag swf_asset.image_url, alt: ""
|
Loading…
Reference in a new issue